blob: 93810a153196556fdee8f69ce264f4eb19c54af1 [file] [log] [blame]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: SessionRecording.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: SessionRecording.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
var Guacamole = Guacamole || {};
/**
* A recording of a Guacamole session. Given a {@link Guacamole.Tunnel}, the
* Guacamole.SessionRecording automatically handles incoming Guacamole
* instructions, storing them for playback. Playback of the recording may be
* controlled through function calls to the Guacamole.SessionRecording, even
* while the recording has not yet finished being created or downloaded.
*
* @constructor
* @param {Guacamole.Tunnel} tunnel
* The Guacamole.Tunnel from which the instructions of the recording should
* be read.
*/
Guacamole.SessionRecording = function SessionRecording(tunnel) {
/**
* Reference to this Guacamole.SessionRecording.
*
* @private
* @type {Guacamole.SessionRecording}
*/
var recording = this;
/**
* The minimum number of characters which must have been read between
* keyframes.
*
* @private
* @constant
* @type {Number}
*/
var KEYFRAME_CHAR_INTERVAL = 16384;
/**
* The minimum number of milliseconds which must elapse between keyframes.
*
* @private
* @constant
* @type {Number}
*/
var KEYFRAME_TIME_INTERVAL = 5000;
/**
* The maximum amount of time to spend in any particular seek operation
* before returning control to the main thread, in milliseconds. Seek
* operations exceeding this amount of time will proceed asynchronously.
*
* @private
* @constant
* @type {Number}
*/
var MAXIMUM_SEEK_TIME = 5;
/**
* All frames parsed from the provided tunnel.
*
* @private
* @type {Guacamole.SessionRecording._Frame[]}
*/
var frames = [];
/**
* All instructions which have been read since the last frame was added to
* the frames array.
*
* @private
* @type {Guacamole.SessionRecording._Frame.Instruction[]}
*/
var instructions = [];
/**
* The approximate number of characters which have been read from the
* provided tunnel since the last frame was flagged for use as a keyframe.
*
* @private
* @type {Number}
*/
var charactersSinceLastKeyframe = 0;
/**
* The timestamp of the last frame which was flagged for use as a keyframe.
* If no timestamp has yet been flagged, this will be 0.
*
* @private
* @type {Number}
*/
var lastKeyframeTimestamp = 0;
/**
* Tunnel which feeds arbitrary instructions to the client used by this
* Guacamole.SessionRecording for playback of the session recording.
*
* @private
* @type {Guacamole.SessionRecording._PlaybackTunnel}
*/
var playbackTunnel = new Guacamole.SessionRecording._PlaybackTunnel();
/**
* Guacamole.Client instance used for visible playback of the session
* recording.
*
* @private
* @type {Guacamole.Client}
*/
var playbackClient = new Guacamole.Client(playbackTunnel);
/**
* The current frame rendered within the playback client. If no frame is
* yet rendered, this will be -1.
*
* @private
* @type {Number}
*/
var currentFrame = -1;
/**
* The timestamp of the frame when playback began, in milliseconds. If
* playback is not in progress, this will be null.
*
* @private
* @type {Number}
*/
var startVideoTimestamp = null;
/**
* The real-world timestamp when playback began, in milliseconds. If
* playback is not in progress, this will be null.
*
* @private
* @type {Number}
*/
var startRealTimestamp = null;
/**
* The ID of the timeout which will continue the in-progress seek
* operation. If no seek operation is in progress, the ID stored here (if
* any) will not be valid.
*
* @private
* @type {Number}
*/
var seekTimeout = null;
// Start playback client connected
playbackClient.connect();
// Hide cursor unless mouse position is received
playbackClient.getDisplay().showCursor(false);
// Read instructions from provided tunnel, extracting each frame
tunnel.oninstruction = function handleInstruction(opcode, args) {
// Store opcode and arguments for received instruction
var instruction = new Guacamole.SessionRecording._Frame.Instruction(opcode, args.slice());
instructions.push(instruction);
charactersSinceLastKeyframe += instruction.getSize();
// Once a sync is received, store all instructions since the last
// frame as a new frame
if (opcode === 'sync') {
// Parse frame timestamp from sync instruction
var timestamp = parseInt(args[0]);
// Add a new frame containing the instructions read since last frame
var frame = new Guacamole.SessionRecording._Frame(timestamp, instructions);
frames.push(frame);
// This frame should eventually become a keyframe if enough data
// has been processed and enough recording time has elapsed, or if
// this is the absolute first frame
if (frames.length === 1 || (charactersSinceLastKeyframe >= KEYFRAME_CHAR_INTERVAL
&amp;&amp; timestamp - lastKeyframeTimestamp >= KEYFRAME_TIME_INTERVAL)) {
frame.keyframe = true;
lastKeyframeTimestamp = timestamp;
charactersSinceLastKeyframe = 0;
}
// Clear set of instructions in preparation for next frame
instructions = [];
// Notify that additional content is available
if (recording.onprogress)
recording.onprogress(recording.getDuration());
}
};
/**
* Converts the given absolute timestamp to a timestamp which is relative
* to the first frame in the recording.
*
* @private
* @param {Number} timestamp
* The timestamp to convert to a relative timestamp.
*
* @returns {Number}
* The difference in milliseconds between the given timestamp and the
* first frame of the recording, or zero if no frames yet exist.
*/
var toRelativeTimestamp = function toRelativeTimestamp(timestamp) {
// If no frames yet exist, all timestamps are zero
if (frames.length === 0)
return 0;
// Calculate timestamp relative to first frame
return timestamp - frames[0].timestamp;
};
/**
* Searches through the given region of frames for the frame having a
* relative timestamp closest to the timestamp given.
*
* @private
* @param {Number} minIndex
* The index of the first frame in the region (the frame having the
* smallest timestamp).
*
* @param {Number} maxIndex
* The index of the last frame in the region (the frame having the
* largest timestamp).
*
* @param {Number} timestamp
* The relative timestamp to search for, where zero denotes the first
* frame in the recording.
*
* @returns {Number}
* The index of the frame having a relative timestamp closest to the
* given value.
*/
var findFrame = function findFrame(minIndex, maxIndex, timestamp) {
// Do not search if the region contains only one element
if (minIndex === maxIndex)
return minIndex;
// Split search region into two halves
var midIndex = Math.floor((minIndex + maxIndex) / 2);
var midTimestamp = toRelativeTimestamp(frames[midIndex].timestamp);
// If timestamp is within lesser half, search again within that half
if (timestamp &lt; midTimestamp &amp;&amp; midIndex > minIndex)
return findFrame(minIndex, midIndex - 1, timestamp);
// If timestamp is within greater half, search again within that half
if (timestamp > midTimestamp &amp;&amp; midIndex &lt; maxIndex)
return findFrame(midIndex + 1, maxIndex, timestamp);
// Otherwise, we lucked out and found a frame with exactly the
// desired timestamp
return midIndex;
};
/**
* Replays the instructions associated with the given frame, sending those
* instructions to the playback client.
*
* @private
* @param {Number} index
* The index of the frame within the frames array which should be
* replayed.
*/
var replayFrame = function replayFrame(index) {
var frame = frames[index];
// Replay all instructions within the retrieved frame
for (var i = 0; i &lt; frame.instructions.length; i++) {
var instruction = frame.instructions[i];
playbackTunnel.receiveInstruction(instruction.opcode, instruction.args);
}
// Store client state if frame is flagged as a keyframe
if (frame.keyframe &amp;&amp; !frame.clientState) {
playbackClient.exportState(function storeClientState(state) {
frame.clientState = state;
});
}
};
/**
* Moves the playback position to the given frame, resetting the state of
* the playback client and replaying frames as necessary. The seek
* operation will proceed asynchronously. If a seek operation is already in
* progress, that seek is first aborted. The progress of the seek operation
* can be observed through the onseek handler and the provided callback.
*
* @private
* @param {Number} index
* The index of the frame which should become the new playback
* position.
*
* @param {function} callback
* The callback to invoke once the seek operation has completed.
*
* @param {Number} [delay=0]
* The number of milliseconds that the seek operation should be
* scheduled to take.
*/
var seekToFrame = function seekToFrame(index, callback, delay) {
// Abort any in-progress seek
abortSeek();
// Replay frames asynchronously
seekTimeout = window.setTimeout(function continueSeek() {
var startIndex;
// Back up until startIndex represents current state
for (startIndex = index; startIndex >= 0; startIndex--) {
var frame = frames[startIndex];
// If we've reached the current frame, startIndex represents
// current state by definition
if (startIndex === currentFrame)
break;
// If frame has associated absolute state, make that frame the
// current state
if (frame.clientState) {
playbackClient.importState(frame.clientState);
break;
}
}
// Advance to frame index after current state
startIndex++;
var startTime = new Date().getTime();
// Replay any applicable incremental frames
for (; startIndex &lt;= index; startIndex++) {
// Stop seeking if the operation is taking too long
var currentTime = new Date().getTime();
if (currentTime - startTime >= MAXIMUM_SEEK_TIME)
break;
replayFrame(startIndex);
}
// Current frame is now at requested index
currentFrame = startIndex - 1;
// Notify of changes in position
if (recording.onseek)
recording.onseek(recording.getPosition());
// If the seek operation has not yet completed, schedule continuation
if (currentFrame !== index)
seekToFrame(index, callback,
Math.max(delay - (new Date().getTime() - startTime), 0));
// Notify that the requested seek has completed
else
callback();
}, delay || 0);
};
/**
* Aborts the seek operation currently in progress, if any. If no seek
* operation is in progress, this function has no effect.
*
* @private
*/
var abortSeek = function abortSeek() {
window.clearTimeout(seekTimeout);
};
/**
* Advances playback to the next frame in the frames array and schedules
* playback of the frame following that frame based on their associated
* timestamps. If no frames exist after the next frame, playback is paused.
*
* @private
*/
var continuePlayback = function continuePlayback() {
// If frames remain after advancing, schedule next frame
if (currentFrame + 1 &lt; frames.length) {
// Pull the upcoming frame
var next = frames[currentFrame + 1];
// Calculate the real timestamp corresponding to when the next
// frame begins
var nextRealTimestamp = next.timestamp - startVideoTimestamp + startRealTimestamp;
// Calculate the relative delay between the current time and
// the next frame start
var delay = Math.max(nextRealTimestamp - new Date().getTime(), 0);
// Advance to next frame after enough time has elapsed
seekToFrame(currentFrame + 1, function frameDelayElapsed() {
continuePlayback();
}, delay);
}
// Otherwise stop playback
else
recording.pause();
};
/**
* Fired when new frames have become available while the recording is
* being downloaded.
*
* @event
* @param {Number} duration
* The new duration of the recording, in milliseconds.
*/
this.onprogress = null;
/**
* Fired whenever playback of the recording has started.
*
* @event
*/
this.onplay = null;
/**
* Fired whenever playback of the recording has been paused. This may
* happen when playback is explicitly paused with a call to pause(), or
* when playback is implicitly paused due to reaching the end of the
* recording.
*
* @event
*/
this.onpause = null;
/**
* Fired whenever the playback position within the recording changes.
*
* @event
* @param {Number} position
* The new position within the recording, in milliseconds.
*/
this.onseek = null;
/**
* Connects the underlying tunnel, beginning download of the Guacamole
* session. Playback of the Guacamole session cannot occur until at least
* one frame worth of instructions has been downloaded.
*
* @param {String} data
* The data to send to the tunnel when connecting.
*/
this.connect = function connect(data) {
tunnel.connect(data);
};
/**
* Disconnects the underlying tunnel, stopping further download of the
* Guacamole session.
*/
this.disconnect = function disconnect() {
tunnel.disconnect();
};
/**
* Returns the underlying display of the Guacamole.Client used by this
* Guacamole.SessionRecording for playback. The display contains an Element
* which can be added to the DOM, causing the display (and thus playback of
* the recording) to become visible.
*
* @return {Guacamole.Display}
* The underlying display of the Guacamole.Client used by this
* Guacamole.SessionRecording for playback.
*/
this.getDisplay = function getDisplay() {
return playbackClient.getDisplay();
};
/**
* Returns whether playback is currently in progress.
*
* @returns {Boolean}
* true if playback is currently in progress, false otherwise.
*/
this.isPlaying = function isPlaying() {
return !!startVideoTimestamp;
};
/**
* Returns the current playback position within the recording, in
* milliseconds, where zero is the start of the recording.
*
* @returns {Number}
* The current playback position within the recording, in milliseconds.
*/
this.getPosition = function getPosition() {
// Position is simply zero if playback has not started at all
if (currentFrame === -1)
return 0;
// Return current position as a millisecond timestamp relative to the
// start of the recording
return toRelativeTimestamp(frames[currentFrame].timestamp);
};
/**
* Returns the duration of this recording, in milliseconds. If the
* recording is still being downloaded, this value will gradually increase.
*
* @returns {Number}
* The duration of this recording, in milliseconds.
*/
this.getDuration = function getDuration() {
// If no frames yet exist, duration is zero
if (frames.length === 0)
return 0;
// Recording duration is simply the timestamp of the last frame
return toRelativeTimestamp(frames[frames.length - 1].timestamp);
};
/**
* Begins continuous playback of the recording downloaded thus far.
* Playback of the recording will continue until pause() is invoked or
* until no further frames exist. Playback is initially paused when a
* Guacamole.SessionRecording is created, and must be explicitly started
* through a call to this function. If playback is already in progress,
* this function has no effect. If a seek operation is in progress,
* playback resumes at the current position, and the seek is aborted as if
* completed.
*/
this.play = function play() {
// If playback is not already in progress and frames remain,
// begin playback
if (!recording.isPlaying() &amp;&amp; currentFrame + 1 &lt; frames.length) {
// Notify that playback is starting
if (recording.onplay)
recording.onplay();
// Store timestamp of playback start for relative scheduling of
// future frames
var next = frames[currentFrame + 1];
startVideoTimestamp = next.timestamp;
startRealTimestamp = new Date().getTime();
// Begin playback of video
continuePlayback();
}
};
/**
* Seeks to the given position within the recording. If the recording is
* currently being played back, playback will continue after the seek is
* performed. If the recording is currently paused, playback will be
* paused after the seek is performed. If a seek operation is already in
* progress, that seek is first aborted. The seek operation will proceed
* asynchronously.
*
* @param {Number} position
* The position within the recording to seek to, in milliseconds.
*
* @param {function} [callback]
* The callback to invoke once the seek operation has completed.
*/
this.seek = function seek(position, callback) {
// Do not seek if no frames exist
if (frames.length === 0)
return;
// Pause playback, preserving playback state
var originallyPlaying = recording.isPlaying();
recording.pause();
// Perform seek
seekToFrame(findFrame(0, frames.length - 1, position), function restorePlaybackState() {
// Restore playback state
if (originallyPlaying)
recording.play();
// Notify that seek has completed
if (callback)
callback();
});
};
/**
* Pauses playback of the recording, if playback is currently in progress.
* If playback is not in progress, this function has no effect. If a seek
* operation is in progress, the seek is aborted. Playback is initially
* paused when a Guacamole.SessionRecording is created, and must be
* explicitly started through a call to play().
*/
this.pause = function pause() {
// Abort any in-progress seek / playback
abortSeek();
// Stop playback only if playback is in progress
if (recording.isPlaying()) {
// Notify that playback is stopping
if (recording.onpause)
recording.onpause();
// Playback is stopped
startVideoTimestamp = null;
startRealTimestamp = null;
}
};
};
/**
* A single frame of Guacamole session data. Each frame is made up of the set
* of instructions used to generate that frame, and the timestamp as dictated
* by the "sync" instruction terminating the frame. Optionally, a frame may
* also be associated with a snapshot of Guacamole client state, such that the
* frame can be rendered without replaying all previous frames.
*
* @private
* @constructor
* @param {Number} timestamp
* The timestamp of this frame, as dictated by the "sync" instruction which
* terminates the frame.
*
* @param {Guacamole.SessionRecording._Frame.Instruction[]} instructions
* All instructions which are necessary to generate this frame relative to
* the previous frame in the Guacamole session.
*/
Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
/**
* Whether this frame should be used as a keyframe if possible. This value
* is purely advisory. The stored clientState must eventually be manually
* set for the frame to be used as a keyframe. By default, frames are not
* keyframes.
*
* @type {Boolean}
* @default false
*/
this.keyframe = false;
/**
* The timestamp of this frame, as dictated by the "sync" instruction which
* terminates the frame.
*
* @type {Number}
*/
this.timestamp = timestamp;
/**
* All instructions which are necessary to generate this frame relative to
* the previous frame in the Guacamole session.
*
* @type {Guacamole.SessionRecording._Frame.Instruction[]}
*/
this.instructions = instructions;
/**
* A snapshot of client state after this frame was rendered, as returned by
* a call to exportState(). If no such snapshot has been taken, this will
* be null.
*
* @type {Object}
* @default null
*/
this.clientState = null;
};
/**
* A Guacamole protocol instruction. Each Guacamole protocol instruction is
* made up of an opcode and set of arguments.
*
* @private
* @constructor
* @param {String} opcode
* The opcode of this Guacamole instruction.
*
* @param {String[]} args
* All arguments associated with this Guacamole instruction.
*/
Guacamole.SessionRecording._Frame.Instruction = function Instruction(opcode, args) {
/**
* Reference to this Guacamole.SessionRecording._Frame.Instruction.
*
* @private
* @type {Guacamole.SessionRecording._Frame.Instruction}
*/
var instruction = this;
/**
* The opcode of this Guacamole instruction.
*
* @type {String}
*/
this.opcode = opcode;
/**
* All arguments associated with this Guacamole instruction.
*
* @type {String[]}
*/
this.args = args;
/**
* Returns the approximate number of characters which make up this
* instruction. This value is only approximate as it excludes the length
* prefixes and various delimiters used by the Guacamole protocol; only
* the content of the opcode and each argument is taken into account.
*
* @returns {Number}
* The approximate size of this instruction, in characters.
*/
this.getSize = function getSize() {
// Init with length of opcode
var size = instruction.opcode.length;
// Add length of all arguments
for (var i = 0; i &lt; instruction.args.length; i++)
size += instruction.args[i].length;
return size;
};
};
/**
* A read-only Guacamole.Tunnel implementation which streams instructions
* received through explicit calls to its receiveInstruction() function.
*
* @private
* @constructor
* @augments {Guacamole.Tunnel}
*/
Guacamole.SessionRecording._PlaybackTunnel = function _PlaybackTunnel() {
/**
* Reference to this Guacamole.SessionRecording._PlaybackTunnel.
*
* @private
* @type {Guacamole.SessionRecording._PlaybackTunnel}
*/
var tunnel = this;
this.connect = function connect(data) {
// Do nothing
};
this.sendMessage = function sendMessage(elements) {
// Do nothing
};
this.disconnect = function disconnect() {
// Do nothing
};
/**
* Invokes this tunnel's oninstruction handler, notifying users of this
* tunnel (such as a Guacamole.Client instance) that an instruction has
* been received. If the oninstruction handler has not been set, this
* function has no effect.
*
* @param {String} opcode
* The opcode of the Guacamole instruction.
*
* @param {String[]} args
* All arguments associated with this Guacamole instruction.
*/
this.receiveInstruction = function receiveInstruction(opcode, args) {
if (tunnel.oninstruction)
tunnel.oninstruction(opcode, args);
};
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="Guacamole.ArrayBufferReader.html">ArrayBufferReader</a></li><li><a href="Guacamole.ArrayBufferWriter.html">ArrayBufferWriter</a></li><li><a href="Guacamole.AudioPlayer.html">AudioPlayer</a></li><li><a href="Guacamole.AudioRecorder.html">AudioRecorder</a></li><li><a href="Guacamole.BlobReader.html">BlobReader</a></li><li><a href="Guacamole.BlobWriter.html">BlobWriter</a></li><li><a href="Guacamole.ChainedTunnel.html">ChainedTunnel</a></li><li><a href="Guacamole.Client.html">Client</a></li><li><a href="Guacamole.DataURIReader.html">DataURIReader</a></li><li><a href="Guacamole.Display.html">Display</a></li><li><a href="Guacamole.Display.VisibleLayer.html">VisibleLayer</a></li><li><a href="Guacamole.HTTPTunnel.html">HTTPTunnel</a></li><li><a href="Guacamole.InputSink.html">InputSink</a></li><li><a href="Guacamole.InputStream.html">InputStream</a></li><li><a href="Guacamole.IntegerPool.html">IntegerPool</a></li><li><a href="Guacamole.JSONReader.html">JSONReader</a></li><li><a href="Guacamole.Keyboard.html">Keyboard</a></li><li><a href="Guacamole.Keyboard.ModifierState.html">ModifierState</a></li><li><a href="Guacamole.Layer.html">Layer</a></li><li><a href="Guacamole.Layer.Pixel.html">Pixel</a></li><li><a href="Guacamole.Mouse.html">Mouse</a></li><li><a href="Guacamole.Mouse.State.html">State</a></li><li><a href="Guacamole.Mouse.Touchpad.html">Touchpad</a></li><li><a href="Guacamole.Mouse.Touchscreen.html">Touchscreen</a></li><li><a href="Guacamole.Object.html">Object</a></li><li><a href="Guacamole.OnScreenKeyboard.html">OnScreenKeyboard</a></li><li><a href="Guacamole.OnScreenKeyboard.Key.html">Key</a></li><li><a href="Guacamole.OnScreenKeyboard.Layout.html">Layout</a></li><li><a href="Guacamole.OutputStream.html">OutputStream</a></li><li><a href="Guacamole.Parser.html">Parser</a></li><li><a href="Guacamole.RawAudioFormat.html">RawAudioFormat</a></li><li><a href="Guacamole.RawAudioPlayer.html">RawAudioPlayer</a></li><li><a href="Guacamole.RawAudioRecorder.html">RawAudioRecorder</a></li><li><a href="Guacamole.SessionRecording.html">SessionRecording</a></li><li><a href="Guacamole.StaticHTTPTunnel.html">StaticHTTPTunnel</a></li><li><a href="Guacamole.Status.html">Status</a></li><li><a href="Guacamole.StringReader.html">StringReader</a></li><li><a href="Guacamole.StringWriter.html">StringWriter</a></li><li><a href="Guacamole.Tunnel.html">Tunnel</a></li><li><a href="Guacamole.VideoPlayer.html">VideoPlayer</a></li><li><a href="Guacamole.WebSocketTunnel.html">WebSocketTunnel</a></li></ul><h3>Events</h3><ul><li><a href="Guacamole.ArrayBufferReader.html#event:ondata">ondata</a></li><li><a href="Guacamole.ArrayBufferReader.html#event:onend">onend</a></li><li><a href="Guacamole.ArrayBufferWriter.html#event:onack">onack</a></li><li><a href="Guacamole.AudioRecorder.html#event:onclose">onclose</a></li><li><a href="Guacamole.AudioRecorder.html#event:onerror">onerror</a></li><li><a href="Guacamole.BlobReader.html#event:onend">onend</a></li><li><a href="Guacamole.BlobReader.html#event:onprogress">onprogress</a></li><li><a href="Guacamole.BlobWriter.html#event:onack">onack</a></li><li><a href="Guacamole.BlobWriter.html#event:oncomplete">oncomplete</a></li><li><a href="Guacamole.BlobWriter.html#event:onerror">onerror</a></li><li><a href="Guacamole.BlobWriter.html#event:onprogress">onprogress</a></li><li><a href="Guacamole.ChainedTunnel.html#event:onerror">onerror</a></li><li><a href="Guacamole.ChainedTunnel.html#event:oninstruction">oninstruction</a></li><li><a href="Guacamole.ChainedTunnel.html#event:onstatechange">onstatechange</a></li><li><a href="Guacamole.Client.html#event:onargv">onargv</a></li><li><a href="Guacamole.Client.html#event:onaudio">onaudio</a></li><li><a href="Guacamole.Client.html#event:onclipboard">onclipboard</a></li><li><a href="Guacamole.Client.html#event:onerror">onerror</a></li><li><a href="Guacamole.Client.html#event:onfile">onfile</a></li><li><a href="Guacamole.Client.html#event:onfilesystem">onfilesystem</a></li><li><a href="Guacamole.Client.html#event:onname">onname</a></li><li><a href="Guacamole.Client.html#event:onpipe">onpipe</a></li><li><a href="Guacamole.Client.html#event:onstatechange">onstatechange</a></li><li><a href="Guacamole.Client.html#event:onsync">onsync</a></li><li><a href="Guacamole.Client.html#event:onvideo">onvideo</a></li><li><a href="Guacamole.DataURIReader.html#event:onend">onend</a></li><li><a href="Guacamole.Display.html#event:oncursor">oncursor</a></li><li><a href="Guacamole.Display.html#event:onresize">onresize</a></li><li><a href="Guacamole.HTTPTunnel.html#event:onerror">onerror</a></li><li><a href="Guacamole.HTTPTunnel.html#event:oninstruction">oninstruction</a></li><li><a href="Guacamole.HTTPTunnel.html#event:onstatechange">onstatechange</a></li><li><a href="Guacamole.InputStream.html#event:onblob">onblob</a></li><li><a href="Guacamole.InputStream.html#event:onend">onend</a></li><li><a href="Guacamole.JSONReader.html#event:onend">onend</a></li><li><a href="Guacamole.JSONReader.html#event:onprogress">onprogress</a></li><li><a href="Guacamole.Keyboard.html#event:onkeydown">onkeydown</a></li><li><a href="Guacamole.Keyboard.html#event:onkeyup">onkeyup</a></li><li><a href="Guacamole.Mouse.Touchpad.html#event:onmousedown">onmousedown</a></li><li><a href="Guacamole.Mouse.Touchpad.html#event:onmousemove">onmousemove</a></li><li><a href="Guacamole.Mouse.Touchpad.html#event:onmouseup">onmouseup</a></li><li><a href="Guacamole.Mouse.Touchscreen.html#event:onmousedown">onmousedown</a></li><li><a href="Guacamole.Mouse.Touchscreen.html#event:onmousemove">onmousemove</a></li><li><a href="Guacamole.Mouse.Touchscreen.html#event:onmouseup">onmouseup</a></li><li><a href="Guacamole.Mouse.html#event:onmousedown">onmousedown</a></li><li><a href="Guacamole.Mouse.html#event:onmousemove">onmousemove</a></li><li><a href="Guacamole.Mouse.html#event:onmouseout">onmouseout</a></li><li><a href="Guacamole.Mouse.html#event:onmouseup">onmouseup</a></li><li><a href="Guacamole.Object.html#event:onbody">onbody</a></li><li><a href="Guacamole.Object.html#event:onundefine">onundefine</a></li><li><a href="Guacamole.OnScreenKeyboard.html#event:onkeydown">onkeydown</a></li><li><a href="Guacamole.OnScreenKeyboard.html#event:onkeyup">onkeyup</a></li><li><a href="Guacamole.OutputStream.html#event:onack">onack</a></li><li><a href="Guacamole.Parser.html#event:oninstruction">oninstruction</a></li><li><a href="Guacamole.RawAudioRecorder.html#event:onclose">onclose</a></li><li><a href="Guacamole.RawAudioRecorder.html#event:onerror">onerror</a></li><li><a href="Guacamole.SessionRecording._PlaybackTunnel.html#event:onerror">onerror</a></li><li><a href="Guacamole.SessionRecording._PlaybackTunnel.html#event:oninstruction">oninstruction</a></li><li><a href="Guacamole.SessionRecording._PlaybackTunnel.html#event:onstatechange">onstatechange</a></li><li><a href="Guacamole.SessionRecording.html#event:onpause">onpause</a></li><li><a href="Guacamole.SessionRecording.html#event:onplay">onplay</a></li><li><a href="Guacamole.SessionRecording.html#event:onprogress">onprogress</a></li><li><a href="Guacamole.SessionRecording.html#event:onseek">onseek</a></li><li><a href="Guacamole.StaticHTTPTunnel.html#event:onerror">onerror</a></li><li><a href="Guacamole.StaticHTTPTunnel.html#event:oninstruction">oninstruction</a></li><li><a href="Guacamole.StaticHTTPTunnel.html#event:onstatechange">onstatechange</a></li><li><a href="Guacamole.StringReader.html#event:onend">onend</a></li><li><a href="Guacamole.StringReader.html#event:ontext">ontext</a></li><li><a href="Guacamole.StringWriter.html#event:onack">onack</a></li><li><a href="Guacamole.Tunnel.html#event:onerror">onerror</a></li><li><a href="Guacamole.Tunnel.html#event:oninstruction">oninstruction</a></li><li><a href="Guacamole.Tunnel.html#event:onstatechange">onstatechange</a></li><li><a href="Guacamole.WebSocketTunnel.html#event:onerror">onerror</a></li><li><a href="Guacamole.WebSocketTunnel.html#event:oninstruction">oninstruction</a></li><li><a href="Guacamole.WebSocketTunnel.html#event:onstatechange">onstatechange</a></li></ul><h3>Namespaces</h3><ul><li><a href="Guacamole.html">Guacamole</a></li><li><a href="Guacamole.AudioContextFactory.html">AudioContextFactory</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.5.5</a> on Sun Jan 26 2020 10:17:42 GMT-0800 (PST)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>