diff --git a/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js b/guacamole-common-js/src/main/webapp/modules/AudioRecorder.js index a75dbe102..954b79f5c 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 @@ -212,6 +239,14 @@ Guacamole.RawAudioRecorder = function RawAudioRecorder(stream, mimetype) { */ var writtenSamples = 0; + /** + * The audio stream provided by the browser, 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. * @@ -372,31 +407,19 @@ 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(); - - // Remove references to now-unneeded components - processor = null; - source = 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(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,18 +431,89 @@ 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 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(); 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]; } 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. * 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;