@illgrenoble/guacamole-common-js
Version:
Guacamole common js as an NPM module
1,573 lines (1,286 loc) • 385 kB
JavaScript
;(function(){
var Guacamole = Guacamole || {};
/**
* A reader which automatically handles the given input stream, returning
* strictly received packets as array buffers. Note that this object will
* overwrite any installed event handlers on the given Guacamole.InputStream.
*
* @constructor
* @param {Guacamole.InputStream} stream The stream that data will be read
* from.
*/
Guacamole.ArrayBufferReader = function(stream) {
/**
* Reference to this Guacamole.InputStream.
* @private
*/
var guac_reader = this;
// Receive blobs as array buffers
stream.onblob = function(data) {
// Convert to ArrayBuffer
var binary = window.atob(data);
var arrayBuffer = new ArrayBuffer(binary.length);
var bufferView = new Uint8Array(arrayBuffer);
for (var i=0; i<binary.length; i++)
bufferView[i] = binary.charCodeAt(i);
// Call handler, if present
if (guac_reader.ondata)
guac_reader.ondata(arrayBuffer);
};
// Simply call onend when end received
stream.onend = function() {
if (guac_reader.onend)
guac_reader.onend();
};
/**
* Fired once for every blob of data received.
*
* @event
* @param {ArrayBuffer} buffer The data packet received.
*/
this.ondata = null;
/**
* Fired once this stream is finished and no further data will be written.
* @event
*/
this.onend = null;
};
var Guacamole = Guacamole || {};
/**
* A writer which automatically writes to the given output stream with arbitrary
* binary data, supplied as ArrayBuffers.
*
* @constructor
* @param {Guacamole.OutputStream} stream The stream that data will be written
* to.
*/
Guacamole.ArrayBufferWriter = function(stream) {
/**
* Reference to this Guacamole.StringWriter.
* @private
*/
var guac_writer = this;
// Simply call onack for acknowledgements
stream.onack = function(status) {
if (guac_writer.onack)
guac_writer.onack(status);
};
/**
* Encodes the given data as base64, sending it as a blob. The data must
* be small enough to fit into a single blob instruction.
*
* @private
* @param {Uint8Array} bytes The data to send.
*/
function __send_blob(bytes) {
var binary = "";
// Produce binary string from bytes in buffer
for (var i=0; i<bytes.byteLength; i++)
binary += String.fromCharCode(bytes[i]);
// Send as base64
stream.sendBlob(window.btoa(binary));
}
/**
* The maximum length of any blob sent by this Guacamole.ArrayBufferWriter,
* in bytes. Data sent via
* [sendData()]{@link Guacamole.ArrayBufferWriter#sendData} which exceeds
* this length will be split into multiple blobs. As the Guacamole protocol
* limits the maximum size of any instruction or instruction element to
* 8192 bytes, and the contents of blobs will be base64-encoded, this value
* should only be increased with extreme caution.
*
* @type {Number}
* @default {@link Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH}
*/
this.blobLength = Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH;
/**
* Sends the given data.
*
* @param {ArrayBuffer|TypedArray} data The data to send.
*/
this.sendData = function(data) {
var bytes = new Uint8Array(data);
// If small enough to fit into single instruction, send as-is
if (bytes.length <= guac_writer.blobLength)
__send_blob(bytes);
// Otherwise, send as multiple instructions
else {
for (var offset=0; offset<bytes.length; offset += guac_writer.blobLength)
__send_blob(bytes.subarray(offset, offset + guac_writer.blobLength));
}
};
/**
* Signals that no further text will be sent, effectively closing the
* stream.
*/
this.sendEnd = function() {
stream.sendEnd();
};
/**
* Fired for received data, if acknowledged by the server.
* @event
* @param {Guacamole.Status} status The status of the operation.
*/
this.onack = null;
};
/**
* The default maximum blob length for new Guacamole.ArrayBufferWriter
* instances.
*
* @constant
* @type {Number}
*/
Guacamole.ArrayBufferWriter.DEFAULT_BLOB_LENGTH = 6048;
var Guacamole = Guacamole || {};
/**
* Maintains a singleton instance of the Web Audio API AudioContext class,
* instantiating the AudioContext only in response to the first call to
* getAudioContext(), and only if no existing AudioContext instance has been
* provided via the singleton property. Subsequent calls to getAudioContext()
* will return the same instance.
*
* @namespace
*/
Guacamole.AudioContextFactory = {
/**
* A singleton instance of a Web Audio API AudioContext object, or null if
* no instance has yes been created. This property may be manually set if
* you wish to supply your own AudioContext instance, but care must be
* taken to do so as early as possible. Assignments to this property will
* not retroactively affect the value returned by previous calls to
* getAudioContext().
*
* @type {AudioContext}
*/
'singleton' : null,
/**
* Returns a singleton instance of a Web Audio API AudioContext object.
*
* @return {AudioContext}
* A singleton instance of a Web Audio API AudioContext object, or null
* if the Web Audio API is not supported.
*/
'getAudioContext' : function getAudioContext() {
// Fallback to Webkit-specific AudioContext implementation
var AudioContext = window.AudioContext || window.webkitAudioContext;
// Get new AudioContext instance if Web Audio API is supported
if (AudioContext) {
try {
// Create new instance if none yet exists
if (!Guacamole.AudioContextFactory.singleton)
Guacamole.AudioContextFactory.singleton = new AudioContext();
// Return singleton instance
return Guacamole.AudioContextFactory.singleton;
}
catch (e) {
// Do not use Web Audio API if not allowed by browser
}
}
// Web Audio API not supported
return null;
}
};
var Guacamole = Guacamole || {};
/**
* Abstract audio player which accepts, queues and plays back arbitrary audio
* data. It is up to implementations of this class to provide some means of
* handling a provided Guacamole.InputStream. Data received along the provided
* stream is to be played back immediately.
*
* @constructor
*/
Guacamole.AudioPlayer = function AudioPlayer() {
/**
* Notifies this Guacamole.AudioPlayer that all audio up to the current
* point in time has been given via the underlying stream, and that any
* difference in time between queued audio data and the current time can be
* considered latency.
*/
this.sync = function sync() {
// Default implementation - do nothing
};
};
/**
* Determines whether the given mimetype is supported by any built-in
* implementation of Guacamole.AudioPlayer, and thus will be properly handled
* by Guacamole.AudioPlayer.getInstance().
*
* @param {String} mimetype
* The mimetype to check.
*
* @returns {Boolean}
* true if the given mimetype is supported by any built-in
* Guacamole.AudioPlayer, false otherwise.
*/
Guacamole.AudioPlayer.isSupportedType = function isSupportedType(mimetype) {
return Guacamole.RawAudioPlayer.isSupportedType(mimetype);
};
/**
* Returns a list of all mimetypes supported by any built-in
* Guacamole.AudioPlayer, in rough order of priority. Beware that only the core
* mimetypes themselves will be listed. Any mimetype parameters, even required
* ones, will not be included in the list. For example, "audio/L8" is a
* supported raw audio mimetype that is supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {String[]}
* A list of all mimetypes supported by any built-in Guacamole.AudioPlayer,
* excluding any parameters.
*/
Guacamole.AudioPlayer.getSupportedTypes = function getSupportedTypes() {
return Guacamole.RawAudioPlayer.getSupportedTypes();
};
/**
* Returns an instance of Guacamole.AudioPlayer providing support for the given
* audio format. If support for the given audio format is not available, null
* is returned.
*
* @param {Guacamole.InputStream} stream
* The Guacamole.InputStream to read audio data from.
*
* @param {String} mimetype
* The mimetype of the audio data in the provided stream.
*
* @return {Guacamole.AudioPlayer}
* A Guacamole.AudioPlayer instance supporting the given mimetype and
* reading from the given stream, or null if support for the given mimetype
* is absent.
*/
Guacamole.AudioPlayer.getInstance = function getInstance(stream, mimetype) {
// Use raw audio player if possible
if (Guacamole.RawAudioPlayer.isSupportedType(mimetype))
return new Guacamole.RawAudioPlayer(stream, mimetype);
// No support for given mimetype
return null;
};
/**
* Implementation of Guacamole.AudioPlayer providing support for raw PCM format
* audio. This player relies only on the Web Audio API and does not require any
* browser-level support for its audio formats.
*
* @constructor
* @augments Guacamole.AudioPlayer
* @param {Guacamole.InputStream} stream
* The Guacamole.InputStream to read audio data from.
*
* @param {String} mimetype
* The mimetype of the audio data in the provided stream, which must be a
* "audio/L8" or "audio/L16" mimetype with necessary parameters, such as:
* "audio/L16;rate=44100,channels=2".
*/
Guacamole.RawAudioPlayer = function RawAudioPlayer(stream, mimetype) {
/**
* The format of audio this player will decode.
*
* @private
* @type {Guacamole.RawAudioFormat}
*/
var format = Guacamole.RawAudioFormat.parse(mimetype);
/**
* An instance of a Web Audio API AudioContext object, or null if the
* Web Audio API is not supported.
*
* @private
* @type {AudioContext}
*/
var context = Guacamole.AudioContextFactory.getAudioContext();
/**
* The earliest possible time that the next packet could play without
* overlapping an already-playing packet, in seconds. Note that while this
* value is in seconds, it is not an integer value and has microsecond
* resolution.
*
* @private
* @type {Number}
*/
var nextPacketTime = context.currentTime;
/**
* Guacamole.ArrayBufferReader wrapped around the audio input stream
* provided with this Guacamole.RawAudioPlayer was created.
*
* @private
* @type {Guacamole.ArrayBufferReader}
*/
var reader = new Guacamole.ArrayBufferReader(stream);
/**
* The minimum size of an audio packet split by splitAudioPacket(), in
* seconds. Audio packets smaller than this will not be split, nor will the
* split result of a larger packet ever be smaller in size than this
* minimum.
*
* @private
* @constant
* @type {Number}
*/
var MIN_SPLIT_SIZE = 0.02;
/**
* The maximum amount of latency to allow between the buffered data stream
* and the playback position, in seconds. Initially, this is set to
* roughly one third of a second.
*
* @private
* @type {Number}
*/
var maxLatency = 0.3;
/**
* The type of typed array that will be used to represent each audio packet
* internally. This will be either Int8Array or Int16Array, depending on
* whether the raw audio format is 8-bit or 16-bit.
*
* @private
* @constructor
*/
var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;
/**
* The maximum absolute value of any sample within a raw audio packet
* received by this audio player. This depends only on the size of each
* sample, and will be 128 for 8-bit audio and 32768 for 16-bit audio.
*
* @private
* @type {Number}
*/
var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;
/**
* The queue of all pending audio packets, as an array of sample arrays.
* Audio packets which are pending playback will be added to this queue for
* further manipulation prior to scheduling via the Web Audio API. Once an
* audio packet leaves this queue and is scheduled via the Web Audio API,
* no further modifications can be made to that packet.
*
* @private
* @type {SampleArray[]}
*/
var packetQueue = [];
/**
* Given an array of audio packets, returns a single audio packet
* containing the concatenation of those packets.
*
* @private
* @param {SampleArray[]} packets
* The array of audio packets to concatenate.
*
* @returns {SampleArray}
* A single audio packet containing the concatenation of all given
* audio packets. If no packets are provided, this will be undefined.
*/
var joinAudioPackets = function joinAudioPackets(packets) {
// Do not bother joining if one or fewer packets are in the queue
if (packets.length <= 1)
return packets[0];
// Determine total sample length of the entire queue
var totalLength = 0;
packets.forEach(function addPacketLengths(packet) {
totalLength += packet.length;
});
// Append each packet within queue
var offset = 0;
var joined = new SampleArray(totalLength);
packets.forEach(function appendPacket(packet) {
joined.set(packet, offset);
offset += packet.length;
});
return joined;
};
/**
* Given a single packet of audio data, splits off an arbitrary length of
* audio data from the beginning of that packet, returning the split result
* as an array of two packets. The split location is determined through an
* algorithm intended to minimize the liklihood of audible clicking between
* packets. If no such split location is possible, an array containing only
* the originally-provided audio packet is returned.
*
* @private
* @param {SampleArray} data
* The audio packet to split.
*
* @returns {SampleArray[]}
* An array of audio packets containing the result of splitting the
* provided audio packet. If splitting is possible, this array will
* contain two packets. If splitting is not possible, this array will
* contain only the originally-provided packet.
*/
var splitAudioPacket = function splitAudioPacket(data) {
var minValue = Number.MAX_VALUE;
var optimalSplitLength = data.length;
// Calculate number of whole samples in the provided audio packet AND
// in the minimum possible split packet
var samples = Math.floor(data.length / format.channels);
var minSplitSamples = Math.floor(format.rate * MIN_SPLIT_SIZE);
// Calculate the beginning of the "end" of the audio packet
var start = Math.max(
format.channels * minSplitSamples,
format.channels * (samples - minSplitSamples)
);
// For all samples at the end of the given packet, find a point where
// the perceptible volume across all channels is lowest (and thus is
// the optimal point to split)
for (var offset = start; offset < data.length; offset += format.channels) {
// Calculate the sum of all values across all channels (the result
// will be proportional to the average volume of a sample)
var totalValue = 0;
for (var channel = 0; channel < format.channels; channel++) {
totalValue += Math.abs(data[offset + channel]);
}
// If this is the smallest average value thus far, set the split
// length such that the first packet ends with the current sample
if (totalValue <= minValue) {
optimalSplitLength = offset + format.channels;
minValue = totalValue;
}
}
// If packet is not split, return the supplied packet untouched
if (optimalSplitLength === data.length)
return [data];
// Otherwise, split the packet into two new packets according to the
// calculated optimal split length
return [
new SampleArray(data.buffer.slice(0, optimalSplitLength * format.bytesPerSample)),
new SampleArray(data.buffer.slice(optimalSplitLength * format.bytesPerSample))
];
};
/**
* Pushes the given packet of audio data onto the playback queue. Unlike
* other private functions within Guacamole.RawAudioPlayer, the type of the
* ArrayBuffer packet of audio data here need not be specific to the type
* of audio (as with SampleArray). The ArrayBuffer type provided by a
* Guacamole.ArrayBufferReader, for example, is sufficient. Any necessary
* conversions will be performed automatically internally.
*
* @private
* @param {ArrayBuffer} data
* A raw packet of audio data that should be pushed onto the audio
* playback queue.
*/
var pushAudioPacket = function pushAudioPacket(data) {
packetQueue.push(new SampleArray(data));
};
/**
* Shifts off and returns a packet of audio data from the beginning of the
* playback queue. The length of this audio packet is determined
* dynamically according to the click-reduction algorithm implemented by
* splitAudioPacket().
*
* @private
* @returns {SampleArray}
* A packet of audio data pulled from the beginning of the playback
* queue.
*/
var shiftAudioPacket = function shiftAudioPacket() {
// Flatten data in packet queue
var data = joinAudioPackets(packetQueue);
if (!data)
return null;
// Pull an appropriate amount of data from the front of the queue
packetQueue = splitAudioPacket(data);
data = packetQueue.shift();
return data;
};
/**
* Converts the given audio packet into an AudioBuffer, ready for playback
* by the Web Audio API. Unlike the raw audio packets received by this
* audio player, AudioBuffers require floating point samples and are split
* into isolated planes of channel-specific data.
*
* @private
* @param {SampleArray} data
* The raw audio packet that should be converted into a Web Audio API
* AudioBuffer.
*
* @returns {AudioBuffer}
* A new Web Audio API AudioBuffer containing the provided audio data,
* converted to the format used by the Web Audio API.
*/
var toAudioBuffer = function toAudioBuffer(data) {
// Calculate total number of samples
var samples = data.length / format.channels;
// Determine exactly when packet CAN play
var packetTime = context.currentTime;
if (nextPacketTime < packetTime)
nextPacketTime = packetTime;
// Get audio buffer for specified format
var audioBuffer = context.createBuffer(format.channels, samples, format.rate);
// Convert each channel
for (var channel = 0; channel < format.channels; channel++) {
var audioData = audioBuffer.getChannelData(channel);
// Fill audio buffer with data for channel
var offset = channel;
for (var i = 0; i < samples; i++) {
audioData[i] = data[offset] / maxSampleValue;
offset += format.channels;
}
}
return audioBuffer;
};
// Defer playback of received audio packets slightly
reader.ondata = function playReceivedAudio(data) {
// Push received samples onto queue
pushAudioPacket(new SampleArray(data));
// Shift off an arbitrary packet of audio data from the queue (this may
// be different in size from the packet just pushed)
var packet = shiftAudioPacket();
if (!packet)
return;
// Determine exactly when packet CAN play
var packetTime = context.currentTime;
if (nextPacketTime < packetTime)
nextPacketTime = packetTime;
// Set up buffer source
var source = context.createBufferSource();
source.connect(context.destination);
// Use noteOn() instead of start() if necessary
if (!source.start)
source.start = source.noteOn;
// Schedule packet
source.buffer = toAudioBuffer(packet);
source.start(nextPacketTime);
// Update timeline by duration of scheduled packet
nextPacketTime += packet.length / format.channels / format.rate;
};
/** @override */
this.sync = function sync() {
// Calculate elapsed time since last sync
var now = context.currentTime;
// Reschedule future playback time such that playback latency is
// bounded within a reasonable latency threshold
nextPacketTime = Math.min(nextPacketTime, now + maxLatency);
};
};
Guacamole.RawAudioPlayer.prototype = new Guacamole.AudioPlayer();
/**
* Determines whether the given mimetype is supported by
* Guacamole.RawAudioPlayer.
*
* @param {String} mimetype
* The mimetype to check.
*
* @returns {Boolean}
* true if the given mimetype is supported by Guacamole.RawAudioPlayer,
* false otherwise.
*/
Guacamole.RawAudioPlayer.isSupportedType = function isSupportedType(mimetype) {
// No supported types if no Web Audio API
if (!Guacamole.AudioContextFactory.getAudioContext())
return false;
return Guacamole.RawAudioFormat.parse(mimetype) !== null;
};
/**
* Returns a list of all mimetypes supported by Guacamole.RawAudioPlayer. Only
* the core mimetypes themselves will be listed. Any mimetype parameters, even
* required ones, will not be included in the list. For example, "audio/L8" is
* a raw audio mimetype that may be supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {String[]}
* A list of all mimetypes supported by Guacamole.RawAudioPlayer, excluding
* any parameters. If the necessary JavaScript APIs for playing raw audio
* are absent, this list will be empty.
*/
Guacamole.RawAudioPlayer.getSupportedTypes = function getSupportedTypes() {
// No supported types if no Web Audio API
if (!Guacamole.AudioContextFactory.getAudioContext())
return [];
// We support 8-bit and 16-bit raw PCM
return [
'audio/L8',
'audio/L16'
];
};
var Guacamole = Guacamole || {};
/**
* Abstract audio recorder which streams arbitrary audio data to an underlying
* Guacamole.OutputStream. It is up to implementations of this class to provide
* some means of handling this Guacamole.OutputStream. Data produced by the
* recorder is to be sent along the provided stream immediately.
*
* @constructor
*/
Guacamole.AudioRecorder = function AudioRecorder() {
/**
* 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;
};
/**
* Determines whether the given mimetype is supported by any built-in
* implementation of Guacamole.AudioRecorder, and thus will be properly handled
* by Guacamole.AudioRecorder.getInstance().
*
* @param {String} mimetype
* The mimetype to check.
*
* @returns {Boolean}
* true if the given mimetype is supported by any built-in
* Guacamole.AudioRecorder, false otherwise.
*/
Guacamole.AudioRecorder.isSupportedType = function isSupportedType(mimetype) {
return Guacamole.RawAudioRecorder.isSupportedType(mimetype);
};
/**
* Returns a list of all mimetypes supported by any built-in
* Guacamole.AudioRecorder, in rough order of priority. Beware that only the
* core mimetypes themselves will be listed. Any mimetype parameters, even
* required ones, will not be included in the list. For example, "audio/L8" is
* a supported raw audio mimetype that is supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {String[]}
* A list of all mimetypes supported by any built-in
* Guacamole.AudioRecorder, excluding any parameters.
*/
Guacamole.AudioRecorder.getSupportedTypes = function getSupportedTypes() {
return Guacamole.RawAudioRecorder.getSupportedTypes();
};
/**
* Returns an instance of Guacamole.AudioRecorder providing support for the
* given audio format. If support for the given audio format is not available,
* null is returned.
*
* @param {Guacamole.OutputStream} stream
* The Guacamole.OutputStream to send audio data through.
*
* @param {String} mimetype
* The mimetype of the audio data to be sent along the provided stream.
*
* @return {Guacamole.AudioRecorder}
* A Guacamole.AudioRecorder instance supporting the given mimetype and
* writing to the given stream, or null if support for the given mimetype
* is absent.
*/
Guacamole.AudioRecorder.getInstance = function getInstance(stream, mimetype) {
// Use raw audio recorder if possible
if (Guacamole.RawAudioRecorder.isSupportedType(mimetype))
return new Guacamole.RawAudioRecorder(stream, mimetype);
// No support for given mimetype
return null;
};
/**
* Implementation of Guacamole.AudioRecorder providing support for raw PCM
* format audio. This recorder relies only on the Web Audio API and does not
* require any browser-level support for its audio formats.
*
* @constructor
* @augments Guacamole.AudioRecorder
* @param {Guacamole.OutputStream} stream
* The Guacamole.OutputStream to write audio data to.
*
* @param {String} mimetype
* The mimetype of the audio data to send along the provided stream, which
* must be a "audio/L8" or "audio/L16" mimetype with necessary parameters,
* such as: "audio/L16;rate=44100,channels=2".
*/
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
* two between 256 and 16384 inclusive, as required by
* AudioContext.createScriptProcessor().
*
* @private
* @constant
* @type {Number}
*/
var BUFFER_SIZE = 2048;
/**
* The window size to use when applying Lanczos interpolation, commonly
* denoted by the variable "a".
* See: https://en.wikipedia.org/wiki/Lanczos_resampling
*
* @private
* @contant
* @type Number
*/
var LANCZOS_WINDOW_SIZE = 3;
/**
* The format of audio this recorder will encode.
*
* @private
* @type {Guacamole.RawAudioFormat}
*/
var format = Guacamole.RawAudioFormat.parse(mimetype);
/**
* An instance of a Web Audio API AudioContext object, or null if the
* Web Audio API is not supported.
*
* @private
* @type {AudioContext}
*/
var context = Guacamole.AudioContextFactory.getAudioContext();
// Some browsers do not implement navigator.mediaDevices - this
// shims in this functionality to ensure code compatibility.
if (!navigator.mediaDevices)
navigator.mediaDevices = {};
// Browsers that either do not implement navigator.mediaDevices
// at all or do not implement it completely need the getUserMedia
// method defined. This shims in this function by detecting
// one of the supported legacy methods.
if (!navigator.mediaDevices.getUserMedia)
navigator.mediaDevices.getUserMedia = (navigator.getUserMedia
|| navigator.webkitGetUserMedia
|| navigator.mozGetUserMedia
|| navigator.msGetUserMedia).bind(navigator);
/**
* Guacamole.ArrayBufferWriter wrapped around the audio output stream
* provided when this Guacamole.RawAudioRecorder was created.
*
* @private
* @type {Guacamole.ArrayBufferWriter}
*/
var writer = new Guacamole.ArrayBufferWriter(stream);
/**
* The type of typed array that will be used to represent each audio packet
* internally. This will be either Int8Array or Int16Array, depending on
* whether the raw audio format is 8-bit or 16-bit.
*
* @private
* @constructor
*/
var SampleArray = (format.bytesPerSample === 1) ? window.Int8Array : window.Int16Array;
/**
* The maximum absolute value of any sample within a raw audio packet sent
* by this audio recorder. This depends only on the size of each sample,
* and will be 128 for 8-bit audio and 32768 for 16-bit audio.
*
* @private
* @type {Number}
*/
var maxSampleValue = (format.bytesPerSample === 1) ? 128 : 32768;
/**
* The total number of audio samples read from the local audio input device
* over the life of this audio recorder.
*
* @private
* @type {Number}
*/
var readSamples = 0;
/**
* The total number of audio samples written to the underlying Guacamole
* connection over the life of this audio recorder.
*
* @private
* @type {Number}
*/
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.
*
* @private
* @type {MediaStreamAudioSourceNode}
*/
var source = null;
/**
* The script processing node which receives audio input from the media
* stream source node as individual audio buffers.
*
* @private
* @type {ScriptProcessorNode}
*/
var processor = null;
/**
* The normalized sinc function. The normalized sinc function is defined as
* 1 for x=0 and sin(PI * x) / (PI * x) for all other values of x.
*
* See: https://en.wikipedia.org/wiki/Sinc_function
*
* @private
* @param {Number} x
* The point at which the normalized sinc function should be computed.
*
* @returns {Number}
* The value of the normalized sinc function at x.
*/
var sinc = function sinc(x) {
// The value of sinc(0) is defined as 1
if (x === 0)
return 1;
// Otherwise, normlized sinc(x) is sin(PI * x) / (PI * x)
var piX = Math.PI * x;
return Math.sin(piX) / piX;
};
/**
* Calculates the value of the Lanczos kernal at point x for a given window
* size. See: https://en.wikipedia.org/wiki/Lanczos_resampling
*
* @private
* @param {Number} x
* The point at which the value of the Lanczos kernel should be
* computed.
*
* @param {Number} a
* The window size to use for the Lanczos kernel.
*
* @returns {Number}
* The value of the Lanczos kernel at the given point for the given
* window size.
*/
var lanczos = function lanczos(x, a) {
// Lanczos is sinc(x) * sinc(x / a) for -a < x < a ...
if (-a < x && x < a)
return sinc(x) * sinc(x / a);
// ... and 0 otherwise
return 0;
};
/**
* Determines the value of the waveform represented by the audio data at
* the given location. If the value cannot be determined exactly as it does
* not correspond to an exact sample within the audio data, the value will
* be derived through interpolating nearby samples.
*
* @private
* @param {Float32Array} audioData
* An array of audio data, as returned by AudioBuffer.getChannelData().
*
* @param {Number} t
* The relative location within the waveform from which the value
* should be retrieved, represented as a floating point number between
* 0 and 1 inclusive, where 0 represents the earliest point in time and
* 1 represents the latest.
*
* @returns {Number}
* The value of the waveform at the given location.
*/
var interpolateSample = function getValueAt(audioData, t) {
// Convert [0, 1] range to [0, audioData.length - 1]
var index = (audioData.length - 1) * t;
// Determine the start and end points for the summation used by the
// Lanczos interpolation algorithm (see: https://en.wikipedia.org/wiki/Lanczos_resampling)
var start = Math.floor(index) - LANCZOS_WINDOW_SIZE + 1;
var end = Math.floor(index) + LANCZOS_WINDOW_SIZE;
// Calculate the value of the Lanczos interpolation function for the
// required range
var sum = 0;
for (var i = start; i <= end; i++) {
sum += (audioData[i] || 0) * lanczos(index - i, LANCZOS_WINDOW_SIZE);
}
return sum;
};
/**
* Converts the given AudioBuffer into an audio packet, ready for streaming
* along the underlying output stream. Unlike the raw audio packets used by
* this audio recorder, AudioBuffers require floating point samples and are
* split into isolated planes of channel-specific data.
*
* @private
* @param {AudioBuffer} audioBuffer
* The Web Audio API AudioBuffer that should be converted to a raw
* audio packet.
*
* @returns {SampleArray}
* A new raw audio packet containing the audio data from the provided
* AudioBuffer.
*/
var toSampleArray = function toSampleArray(audioBuffer) {
// Track overall amount of data read
var inSamples = audioBuffer.length;
readSamples += inSamples;
// Calculate the total number of samples that should be written as of
// the audio data just received and adjust the size of the output
// packet accordingly
var expectedWrittenSamples = Math.round(readSamples * format.rate / audioBuffer.sampleRate);
var outSamples = expectedWrittenSamples - writtenSamples;
// Update number of samples written
writtenSamples += outSamples;
// Get array for raw PCM storage
var data = new SampleArray(outSamples * format.channels);
// Convert each channel
for (var channel = 0; channel < format.channels; channel++) {
var audioData = audioBuffer.getChannelData(channel);
// Fill array with data from audio buffer channel
var offset = channel;
for (var i = 0; i < outSamples; i++) {
data[offset] = interpolateSample(audioData, i / (outSamples - 1)) * maxSampleValue;
offset += format.channels;
}
}
return data;
};
/**
* 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
navigator.mediaDevices.getUserMedia({ 'audio' : true }, function streamReceived(stream) {
// Create processing node which receives appropriately-sized audio buffers
processor = context.createScriptProcessor(BUFFER_SIZE, format.channels, format.channels);
processor.connect(context.destination);
// Send blobs when audio buffers are received
processor.onaudioprocess = function processAudio(e) {
writer.sendData(toSampleArray(e.inputBuffer).buffer);
};
// Connect processing node to user's audio input source
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();
/**
* Determines whether the given mimetype is supported by
* Guacamole.RawAudioRecorder.
*
* @param {String} mimetype
* The mimetype to check.
*
* @returns {Boolean}
* true if the given mimetype is supported by Guacamole.RawAudioRecorder,
* false otherwise.
*/
Guacamole.RawAudioRecorder.isSupportedType = function isSupportedType(mimetype) {
// No supported types if no Web Audio API
if (!Guacamole.AudioContextFactory.getAudioContext())
return false;
return Guacamole.RawAudioFormat.parse(mimetype) !== null;
};
/**
* Returns a list of all mimetypes supported by Guacamole.RawAudioRecorder. Only
* the core mimetypes themselves will be listed. Any mimetype parameters, even
* required ones, will not be included in the list. For example, "audio/L8" is
* a raw audio mimetype that may be supported, but it is invalid without
* additional parameters. Something like "audio/L8;rate=44100" would be valid,
* however (see https://tools.ietf.org/html/rfc4856).
*
* @returns {String[]}
* A list of all mimetypes supported by Guacamole.RawAudioRecorder,
* excluding any parameters. If the necessary JavaScript APIs for recording
* raw audio are absent, this list will be empty.
*/
Guacamole.RawAudioRecorder.getSupportedTypes = function getSupportedTypes() {
// No supported types if no Web Audio API
if (!Guacamole.AudioContextFactory.getAudioContext())
return [];
// We support 8-bit and 16-bit raw PCM
return [
'audio/L8',
'audio/L16'
];
};
var Guacamole = Guacamole || {};
/**
* A reader which automatically handles the given input stream, assembling all
* received blobs into a single blob by appending them to each other in order.
* Note that this object will overwrite any installed event handlers on the
* given Guacamole.InputStream.
*
* @constructor
* @param {Guacamole.InputStream} stream The stream that data will be read
* from.
* @param {String} mimetype The mimetype of the blob being built.
*/
Guacamole.BlobReader = function(stream, mimetype) {
/**
* Reference to this Guacamole.InputStream.
* @private
*/
var guac_reader = this;
/**
* The length of this Guacamole.InputStream in bytes.
* @private
*/
var length = 0;
// Get blob builder
var blob_builder;
if (window.BlobBuilder) blob_builder = new BlobBuilder();
else if (window.WebKitBlobBuilder) blob_builder = new WebKitBlobBuilder();
else if (window.MozBlobBuilder) blob_builder = new MozBlobBuilder();
else
blob_builder = new (function() {
var blobs = [];
/** @ignore */
this.append = function(data) {
blobs.push(new Blob([data], {"type": mimetype}));
};
/** @ignore */
this.getBlob = function() {
return new Blob(blobs, {"type": mimetype});
};
})();
// Append received blobs
stream.onblob = function(data) {
// Convert to ArrayBuffer
var binary = window.atob(data);
var arrayBuffer = new ArrayBuffer(binary.length);
var bufferView = new Uint8Array(arrayBuffer);
for (var i=0; i<binary.length; i++)
bufferView[i] = binary.charCodeAt(i);
blob_builder.append(arrayBuffer);
length += arrayBuffer.byteLength;
// Call handler, if present
if (guac_reader.onprogress)
guac_reader.onprogress(arrayBuffer.byteLength);
// Send success response
stream.sendAck("OK", 0x0000);
};
// Simply call onend when end received
stream.onend = function() {
if (guac_reader.onend)
guac_reader.onend();
};
/**
* Returns the current length of this Guacamole.InputStream, in bytes.
* @return {Number} The current length of this Guacamole.InputStream.
*/
this.getLength = function() {
return length;
};
/**
* Returns the contents of this Guacamole.BlobReader as a Blob.
* @return {Blob} The contents of this Guacamole.BlobReader.
*/
this.getBlob = function() {
return blob_builder.getBlob();
};
/**
* Fired once for every blob of data received.
*
* @event
* @param {Number} length The number of bytes received.
*/
this.onprogress = null;
/**
* Fired once this stream is finished and no further data will be written.
* @event
*/
this.onend = null;
};
var Guacamole = Guacamole || {};
/**
* A writer which automatically writes to the given output stream with the
* contents of provided Blob objects.
*
* @constructor
* @param {Guacamole.OutputStream} stream
* The stream that data will be written to.
*/
Guacamole.BlobWriter = function BlobWriter(stream) {
/**
* Reference to this Guacamole.BlobWriter.
*
* @private
* @type {Guacamole.BlobWriter}
*/
var guacWriter = this;
/**
* Wrapped Guacamole.ArrayBufferWriter which will be used to send any
* provided file data.
*
* @private
* @type {Guacamole.ArrayBufferWriter}
*/
var arrayBufferWriter = new Guacamole.ArrayBufferWriter(stream);
// Initially, simply call onack for acknowledgements
arrayBufferWriter.onack = function(status) {
if (guacWriter.onack)
guacWriter.onack(status);
};
/**
* Browser-independent implementation of Blob.slice() which uses an end
* offset to determine the span of the resulting slice, rather than a
* length.
*
* @private
* @param {Blob} blob
* The Blob to slice.
*
* @param {Number} start
* The starting offset of the slice, in bytes, inclusive.
*
* @param {Number} end
* The ending offset of the slice, in bytes, exclusive.
*
* @returns {Blob}
* A Blob containing the data within the given Blob starting at
* <code>start</code> and ending at <code>end - 1</code>.
*/
var slice = function slice(blob, start, end) {
// Use prefixed implementations if necessary
var sliceImplementation = (
blob.slice
|| blob.webkitSlice
|| blob.mozSlice
).bind(blob);
var length = end - start;
// The old Blob.slice() was length-based (not end-based). Try the
// length version first, if the two calls are not equivalent.
if (length !== end) {
// If the result of the slice() call matches the expected length,
// trust that result. It must be correct.
var sliceResult = sliceImplementation(start, length);
if (sliceResult.size === length)
return sliceResult;
}
// Otherwise, use the most-recent standard: end-based slice()
return sliceImplementation(start, end);
};
/**
* Sends the contents of the given blob over the underlying stream.
*
* @param {Blob} blob
* The blob to send.
*/
this.sendBlob = function sendBlob(blob) {
var offset = 0;
var reader = new FileReader();
/**
* Reads the next chunk of the blob provided to
* [sendBlob()]{@link Guacamole.BlobWriter#sendBlob}. The chunk itself
* is read asynchronously, and will not be available until
* reader.onload fires.
*
* @private
*/
var readNextChunk = function readNextChunk() {
// If no further chunks remain, inform of completion and stop
if (offset >= blob.size) {
// Fire completion event for completed blob
if (guacWriter.oncomplete)
guacWriter.oncomplete(blob);
// No further chunks to read
return;
}
// Obtain reference to next chunk as a new blob
var chunk = slice(blob, offset, offset + arrayBufferWriter.blobLength);
offset += arrayBufferWriter.blobLength;
// Attempt to read the blob contents represented by the blob into
// a new array buffer
reader.readAsArrayBuffer(chunk);
};
// Send each chunk over the stream, continue reading the next chunk
reader.onload = function chunkLoadComplete() {
// Send the successfully-read chunk
arrayBufferWriter.sendData(reader.result);
// Continue sending more chunks after the latest chunk is
// acknowledged
arrayBufferWriter.onack = function sendMoreChunks(status) {
if (guacWriter.onack)
guacWriter.onack(status);
// Abort transfer if an error occurs
if (status.isError())
return;
// Inform of blob upload progress via progress events
if (guacWriter.onprogress)
guacWriter.onprogress(blob, offset - arrayBufferWriter.blobLength);
// Queue the next chunk for reading
readNextChunk();
};
};
// If an error prevents further reading, inform of error and stop
reader.onerror = function chunkLoadFailed() {
// Fire error event, including the context of the error
if (guacWriter.onerror)
guacWriter.onerror(blob, offset, reader.error);
};
// Begin reading the first chunk
readNextChunk();
};
/**
* Signals that no further text will be sent, effectively closing the
* stream.
*/
this.sendEnd = function sendEnd() {
arrayBufferWriter.sendEnd();
};
/**
* Fired for received data, if acknowledged by the server.
*
* @event
* @param {Guacamole.Status} status
* The status of the operation.
*/
this.onack = null;
/**
* Fired when an error occurs reading a blob passed to
* [sendBlob()]{@link Gua