From 4b88066f26a42f3b57036b4a3945d64269689fcf Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 1 May 2016 22:29:29 -0700 Subject: [PATCH 1/6] GUACAMOLE-25: Only capture as long as stream is open. --- .../src/main/webapp/modules/AudioRecorder.js | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js b/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js index a75dbe102..43394e53f 100644 --- a/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js +++ b/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js @@ -212,6 +212,14 @@ Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) { */ var writtenSamples = 0; + /** + * The audio stream provided by the browse, if allowed. If no stream has + * yet been received, this will be null. + * + * @type MediaStream + */ + var mediaStream = null; + /** * The source node providing access to the local audio input device. * @@ -386,9 +394,17 @@ Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) { if (processor) processor.disconnect(); + // Stop capture + if (mediaStream) { + var tracks = mediaStream.getTracks(); + for (var i = 0; i < tracks.length; i++) + tracks[i].stop(); + } + // Remove references to now-unneeded components processor = null; source = null; + mediaStream = null; writer.sendEnd(); return; @@ -396,7 +412,7 @@ Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) { } // Attempt to retrieve an audio input stream from the browser - getUserMedia({ 'audio' : true }, function streamReceived(mediaStream) { + getUserMedia({ 'audio' : true }, function streamReceived(stream) { // Create processing node which receives appropriately-sized audio buffers processor = context.createScriptProcessor(BUFFER_SIZE, format.channels, format.channels); @@ -408,9 +424,12 @@ Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) { }; // Connect processing node to user's audio input source - source = context.createMediaStreamSource(mediaStream); + source = context.createMediaStreamSource(stream); source.connect(processor); + // Save stream for later cleanup + mediaStream = stream; + }, function streamDenied() { // Simply end stream if audio access is not allowed From df3347f63110c14ce7394352cc144911f862a694 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 1 May 2016 23:51:18 -0700 Subject: [PATCH 2/6] GUACAMOLE-25: Automatically invalidate output stream due to "ack" ONLY if it's the same stream that received the "ack". It is possible for the output stream to be freed and replaced with a different stream within onack. --- guacamole-common-js/src/main/webapp/modules/Client.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js index 8903f2193..060f46026 100644 --- a/guacamole-common-js/src/main/webapp/modules/Client.js +++ b/guacamole-common-js/src/main/webapp/modules/Client.js @@ -648,8 +648,9 @@ Guacamole.Client = function(tunnel) { if (stream.onack) stream.onack(new Guacamole.Status(code, reason)); - // If code is an error, invalidate stream - if (code >= 0x0100) { + // If code is an error, invalidate stream if not already + // invalidated by onack handler + if (code >= 0x0100 && output_streams[stream_index] === stream) { stream_indices.free(stream_index); delete output_streams[stream_index]; } From d0533d097feb19927feb5a546716f5fbea75deba Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 1 May 2016 23:51:50 -0700 Subject: [PATCH 3/6] GUACAMOLE-25: Implement new "RESOURCE_CLOSED" status code. --- guacamole-common-js/src/main/webapp/modules/Status.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/guacamole-common-js/src/main/webapp/modules/Status.js b/guacamole-common-js/src/main/webapp/modules/Status.js index e105f600f..457dc3c6a 100644 --- a/guacamole-common-js/src/main/webapp/modules/Status.js +++ b/guacamole-common-js/src/main/webapp/modules/Status.js @@ -132,6 +132,14 @@ Guacamole.Status.Code = { */ "RESOURCE_CONFLICT": 0x0205, + /** + * The operation could not be performed as the requested resource is now + * closed. + * + * @type {Number} + */ + "RESOURCE_CLOSED": 0x0206, + /** * The operation could not be performed because bad parameters were given. * From c32a779825732a27f130e26bc73aa5f60566608c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 2 May 2016 00:12:23 -0700 Subject: [PATCH 4/6] GUACAMOLE-25: Add onclose/onerror handlers to Guacamole.AudioRecorder. --- .../src/main/webapp/modules/AudioRecorder.js | 137 ++++++++++++++---- 1 file changed, 106 insertions(+), 31 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js b/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js index 43394e53f..306acbd8c 100644 --- a/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js +++ b/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js @@ -29,7 +29,26 @@ var Guacamole = Guacamole || {}; */ Guacamole.AudioRecorder = function AudioRecorder() { - // AudioRecorder currently provides no functions + /** + * Callback which is invoked when the audio recording process has stopped + * and the underlying Guacamole stream has been closed normally. Audio will + * only resume recording if a new Guacamole.AudioRecorder is started. This + * Guacamole.AudioRecorder instance MAY NOT be reused. + * + * @event + */ + this.onclose = null; + + /** + * Callback which is invoked when the audio recording process cannot + * continue due to an error, if it has started at all. The underlying + * Guacamole stream is automatically closed. Future attempts to record + * audio should not be made, and this Guacamole.AudioRecorder instance + * MAY NOT be reused. + * + * @event + */ + this.onerror = null; }; @@ -114,6 +133,14 @@ Guacamole.AudioRecorder.getInstance = function getInstance(stream, mimetype) { */ Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) { + /** + * Reference to this RawAudioRecorder. + * + * @private + * @type {Guacamole.RawAudioRecorder} + */ + var recorder = this; + /** * The size of audio buffer to request from the Web Audio API when * recording or processing audio, in sample-frames. This must be a power of @@ -380,36 +407,16 @@ Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) { }; - // Once audio stream is successfully open, request and begin reading audio - writer.onack = function audioStreamAcknowledged(status) { - - // Abort stream if rejected - if (status.code !== Guacamole.Status.Code.SUCCESS) { - - // Disconnect media source node from script processor - if (source) - source.disconnect(); - - // Disconnect associated script processor node - if (processor) - processor.disconnect(); - - // Stop capture - if (mediaStream) { - var tracks = mediaStream.getTracks(); - for (var i = 0; i < tracks.length; i++) - tracks[i].stop(); - } - - // Remove references to now-unneeded components - processor = null; - source = null; - mediaStream = null; - - writer.sendEnd(); - return; - - } + /** + * Requests access to the user's microphone and begins capturing audio. All + * received audio data is resampled as necessary and forwarded to the + * Guacamole stream underlying this Guacamole.RawAudioRecorder. This + * function must be invoked ONLY ONCE per instance of + * Guacamole.RawAudioRecorder. + * + * @private + */ + var beginAudioCapture = function beginAudioCapture() { // Attempt to retrieve an audio input stream from the browser getUserMedia({ 'audio' : true }, function streamReceived(stream) { @@ -435,10 +442,78 @@ Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) { // Simply end stream if audio access is not allowed writer.sendEnd(); + // Notify of closure + if (recorder.onerror) + recorder.onerror(); + }); }; + /** + * Stops capturing audio, if the capture has started, freeing all associated + * resources. If the capture has not started, this function simply ends the + * underlying Guacamole stream. + * + * @private + */ + var stopAudioCapture = function stopAudioCapture() { + + // Disconnect media source node from script processor + if (source) + source.disconnect(); + + // Disconnect associated script processor node + if (processor) + processor.disconnect(); + + // Stop capture + if (mediaStream) { + var tracks = mediaStream.getTracks(); + for (var i = 0; i < tracks.length; i++) + tracks[i].stop(); + } + + // Remove references to now-unneeded components + processor = null; + source = null; + mediaStream = null; + + // End stream + writer.sendEnd(); + + }; + + // Once audio stream is successfully open, request and begin reading audio + writer.onack = function audioStreamAcknowledged(status) { + + // Begin capture if successful response and not yet started + if (status.code === Guacamole.Status.Code.SUCCESS && !mediaStream) + beginAudioCapture(); + + // Otherwise stop capture and cease handling any further acks + else { + + // Stop capturing audio + stopAudioCapture(); + writer.onack = null; + + // Notify if stream has closed normally + if (status.code === Guacamole.Status.Code.RESOURCE_CLOSED) { + if (recorder.onclose) + recorder.onclose(); + } + + // Otherwise notify of closure due to error + else { + if (recorder.onerror) + recorder.onerror(); + } + + } + + }; + }; Guacamole.RawAudioRecorder.prototype = new Guacamole.AudioRecorder(); From 421294490b2e44367fca1c6b97533035eb78a950 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 2 May 2016 00:39:03 -0700 Subject: [PATCH 5/6] GUACAMOLE-25: Automatically reestablish audio stream when it closes normally. --- .../webapp/app/client/types/ManagedClient.js | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index 64418e0b8..9a3ffb1c4 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -262,6 +262,36 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }; + /** + * Requests the creation of a new audio stream, recorded from the user's + * local audio input device. If audio input is supported by the connection, + * an audio stream will be created which will remain open until the remote + * desktop requests that it be closed. If the audio stream is successfully + * created but is later closed, a new audio stream will automatically be + * established to take its place. The mimetype used for all audio streams + * produced by this function is defined by + * ManagedClient.AUDIO_INPUT_MIMETYPE. + * + * @param {Guacamole.Client} client + * The Guacamole.Client for which the audio stream is being requested. + */ + var requestAudioStream = function requestAudioStream(client) { + + // Create new audio stream, associating it with an AudioRecorder + var stream = client.createAudioStream(ManagedClient.AUDIO_INPUT_MIMETYPE); + var recorder = Guacamole.AudioRecorder.getInstance(stream, ManagedClient.AUDIO_INPUT_MIMETYPE); + + // If creation of the AudioRecorder failed, simply end the stream + if (!recorder) + stream.sendEnd(); + + // Otherwise, ensure that another audio stream is created after this + // audio stream is closed + else + recorder.onclose = requestAudioStream.bind(this, client); + + }; + /** * Creates a new ManagedClient, connecting it to the specified connection * or group. @@ -363,9 +393,7 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', ManagedClientState.ConnectionState.CONNECTED); // Begin streaming audio input if possible - var stream = client.createAudioStream(ManagedClient.AUDIO_INPUT_MIMETYPE); - if (!Guacamole.AudioRecorder.getInstance(stream, ManagedClient.AUDIO_INPUT_MIMETYPE)) - stream.sendEnd(); + requestAudioStream(client); break; From 4b552ce419d354b40323f99f5511669b789a83e6 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Mon, 23 May 2016 21:15:55 -0700 Subject: [PATCH 6/6] GUACAMOLE-25: It's a browser, not a browse. --- guacamole-common-js/src/main/webapp/modules/AudioRecorder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js b/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js index 306acbd8c..954b79f5c 100644 --- a/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js +++ b/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js @@ -240,7 +240,7 @@ Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) { var writtenSamples = 0; /** - * The audio stream provided by the browse, if allowed. If no stream has + * The audio stream provided by the browser, if allowed. If no stream has * yet been received, this will be null. * * @type MediaStream