mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
GUACAMOLE-896: Merge allow playback of session recordings that cannot fit in memory.
This commit is contained in:
@@ -211,6 +211,9 @@
|
|||||||
// Seek within recording if slider is moved
|
// Seek within recording if slider is moved
|
||||||
positionSlider.onchange = function sliderPositionChanged() {
|
positionSlider.onchange = function sliderPositionChanged() {
|
||||||
|
|
||||||
|
// Seek is in progress
|
||||||
|
player.className = 'seeking';
|
||||||
|
|
||||||
// Request seek
|
// Request seek
|
||||||
recording.seek(positionSlider.value, function seekComplete() {
|
recording.seek(positionSlider.value, function seekComplete() {
|
||||||
|
|
||||||
@@ -219,9 +222,6 @@
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Seek is in progress
|
|
||||||
player.className = 'seeking';
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
@@ -20,18 +20,20 @@
|
|||||||
var Guacamole = Guacamole || {};
|
var Guacamole = Guacamole || {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A recording of a Guacamole session. Given a {@link Guacamole.Tunnel}, the
|
* A recording of a Guacamole session. Given a {@link Guacamole.Tunnel} or Blob,
|
||||||
* Guacamole.SessionRecording automatically handles incoming Guacamole
|
* the Guacamole.SessionRecording automatically parses Guacamole instructions
|
||||||
* instructions, storing them for playback. Playback of the recording may be
|
* within the recording source as it plays back the recording. Playback of the
|
||||||
* controlled through function calls to the Guacamole.SessionRecording, even
|
* recording may be controlled through function calls to the
|
||||||
* while the recording has not yet finished being created or downloaded.
|
* Guacamole.SessionRecording, even while the recording has not yet finished
|
||||||
|
* being created or downloaded. Parsing of the contents of the recording will
|
||||||
|
* begin immediately and automatically after this constructor is invoked.
|
||||||
*
|
*
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {!Guacamole.Tunnel} tunnel
|
* @param {!Blob|Guacamole.Tunnel} source
|
||||||
* The Guacamole.Tunnel from which the instructions of the recording should
|
* The Blob from which the instructions of the recording should
|
||||||
* be read.
|
* be read.
|
||||||
*/
|
*/
|
||||||
Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
Guacamole.SessionRecording = function SessionRecording(source) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to this Guacamole.SessionRecording.
|
* Reference to this Guacamole.SessionRecording.
|
||||||
@@ -41,13 +43,45 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
*/
|
*/
|
||||||
var recording = this;
|
var recording = this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Blob from which the instructions of the recording should be read.
|
||||||
|
* Note that this value is initialized far below.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {!Blob}
|
||||||
|
*/
|
||||||
|
var recordingBlob;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tunnel from which the recording should be read, if the recording is
|
||||||
|
* being read from a tunnel. If the recording was supplied as a Blob, this
|
||||||
|
* will be null.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {Guacamole.Tunnel}
|
||||||
|
*/
|
||||||
|
var tunnel = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of bytes that this Guacamole.SessionRecording should attempt
|
||||||
|
* to read from the given blob in each read operation. Larger blocks will
|
||||||
|
* generally read the blob more quickly, but may result in excessive
|
||||||
|
* time being spent within the parser, making the page unresponsive
|
||||||
|
* while the recording is loading.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @constant
|
||||||
|
* @type {Number}
|
||||||
|
*/
|
||||||
|
var BLOCK_SIZE = 262144;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The minimum number of characters which must have been read between
|
* The minimum number of characters which must have been read between
|
||||||
* keyframes.
|
* keyframes.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @constant
|
* @constant
|
||||||
* @type {!number}
|
* @type {Number}
|
||||||
*/
|
*/
|
||||||
var KEYFRAME_CHAR_INTERVAL = 16384;
|
var KEYFRAME_CHAR_INTERVAL = 16384;
|
||||||
|
|
||||||
@@ -56,47 +90,18 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @constant
|
* @constant
|
||||||
* @type {!number}
|
* @type {Number}
|
||||||
*/
|
*/
|
||||||
var KEYFRAME_TIME_INTERVAL = 5000;
|
var KEYFRAME_TIME_INTERVAL = 5000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum amount of time to spend in any particular seek operation
|
* All frames parsed from the provided blob.
|
||||||
* 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
|
* @private
|
||||||
* @type {!Guacamole.SessionRecording._Frame[]}
|
* @type {!Guacamole.SessionRecording._Frame[]}
|
||||||
*/
|
*/
|
||||||
var frames = [];
|
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.
|
* 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.
|
* If no timestamp has yet been flagged, this will be 0.
|
||||||
@@ -104,7 +109,7 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
* @private
|
* @private
|
||||||
* @type {!number}
|
* @type {!number}
|
||||||
*/
|
*/
|
||||||
var lastKeyframeTimestamp = 0;
|
var lastKeyframe = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tunnel which feeds arbitrary instructions to the client used by this
|
* Tunnel which feeds arbitrary instructions to the client used by this
|
||||||
@@ -152,14 +157,180 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
var startRealTimestamp = null;
|
var startRealTimestamp = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the timeout which will continue the in-progress seek
|
* An object containing a single "aborted" property which is set to
|
||||||
* operation. If no seek operation is in progress, the ID stored here (if
|
* true if the in-progress seek operation should be aborted. If no seek
|
||||||
* any) will not be valid.
|
* operation is in progress, this will be null.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @type {number}
|
* @type {object}
|
||||||
*/
|
*/
|
||||||
var seekTimeout = null;
|
var activeSeek = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The byte offset within the recording blob of the first character of
|
||||||
|
* the first instruction of the current frame. Here, "current frame"
|
||||||
|
* refers to the frame currently being parsed when the provided
|
||||||
|
* recording is initially loading. If the recording is not being
|
||||||
|
* loaded, this value has no meaning.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
var frameStart = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The byte offset within the recording blob of the character which
|
||||||
|
* follows the last character of the most recently parsed instruction
|
||||||
|
* of the current frame. Here, "current frame" refers to the frame
|
||||||
|
* currently being parsed when the provided recording is initially
|
||||||
|
* loading. If the recording is not being loaded, this value has no
|
||||||
|
* meaning.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
var frameEnd = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the initial loading process has been aborted. If the loading
|
||||||
|
* process has been aborted, no further blocks of data should be read
|
||||||
|
* from the recording.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {!boolean}
|
||||||
|
*/
|
||||||
|
var aborted = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function to invoke when the seek operation initiated by a call
|
||||||
|
* to seek() is cancelled or successfully completed. If no seek
|
||||||
|
* operation is in progress, this will be null.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {function}
|
||||||
|
*/
|
||||||
|
var seekCallback = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses all Guacamole instructions within the given blob, invoking
|
||||||
|
* the provided instruction callback for each such instruction. Once
|
||||||
|
* the end of the blob has been reached (no instructions remain to be
|
||||||
|
* parsed), the provided completion callback is invoked. If a parse
|
||||||
|
* error prevents reading instructions from the blob, the onerror
|
||||||
|
* callback of the Guacamole.SessionRecording is invoked, and no further
|
||||||
|
* data is handled within the blob.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {!Blob} blob
|
||||||
|
* The blob to parse Guacamole instructions from.
|
||||||
|
*
|
||||||
|
* @param {function} [instructionCallback]
|
||||||
|
* The callback to invoke for each Guacamole instruction read from
|
||||||
|
* the given blob. This function must accept the same arguments
|
||||||
|
* as the oninstruction handler of Guacamole.Parser.
|
||||||
|
*
|
||||||
|
* @param {function} [completionCallback]
|
||||||
|
* The callback to invoke once all instructions have been read from
|
||||||
|
* the given blob.
|
||||||
|
*/
|
||||||
|
var parseBlob = function parseBlob(blob, instructionCallback, completionCallback) {
|
||||||
|
|
||||||
|
// Do not read any further blocks if loading has been aborted
|
||||||
|
if (aborted && blob === recordingBlob)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Prepare a parser to handle all instruction data within the blob,
|
||||||
|
// automatically invoking the provided instruction callback for all
|
||||||
|
// parsed instructions
|
||||||
|
var parser = new Guacamole.Parser();
|
||||||
|
parser.oninstruction = instructionCallback;
|
||||||
|
|
||||||
|
var offset = 0;
|
||||||
|
var reader = new FileReader();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the block of data at offset bytes within the blob. If no
|
||||||
|
* such block exists, then the completion callback provided to
|
||||||
|
* parseBlob() is invoked as all data has been read.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
var readNextBlock = function readNextBlock() {
|
||||||
|
|
||||||
|
// Do not read any further blocks if loading has been aborted
|
||||||
|
if (aborted && blob === recordingBlob)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Parse all instructions within the block, invoking the
|
||||||
|
// onerror handler if a parse error occurs
|
||||||
|
if (reader.readyState === 2 /* DONE */) {
|
||||||
|
try {
|
||||||
|
parser.receive(reader.result);
|
||||||
|
}
|
||||||
|
catch (parseError) {
|
||||||
|
if (recording.onerror) {
|
||||||
|
recording.onerror(parseError.message);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no data remains, the read operation is complete and no
|
||||||
|
// further blocks need to be read
|
||||||
|
if (offset >= blob.size) {
|
||||||
|
if (completionCallback)
|
||||||
|
completionCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, read the next block
|
||||||
|
else {
|
||||||
|
var block = blob.slice(offset, offset + BLOCK_SIZE);
|
||||||
|
offset += block.size;
|
||||||
|
reader.readAsText(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read blocks until the end of the given blob is reached
|
||||||
|
reader.onload = readNextBlock;
|
||||||
|
readNextBlock();
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the size of the given Guacamole instruction element, in
|
||||||
|
* Unicode characters. The size returned includes the characters which
|
||||||
|
* make up the length, the "." separator between the length and the
|
||||||
|
* element itself, and the "," or ";" terminator which follows the
|
||||||
|
* element.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {!string} value
|
||||||
|
* The value of the element which has already been parsed (lacks
|
||||||
|
* the initial length, "." separator, and "," or ";" terminator).
|
||||||
|
*
|
||||||
|
* @returns {!number}
|
||||||
|
* The number of Unicode characters which would make up the given
|
||||||
|
* element within a Guacamole instruction.
|
||||||
|
*/
|
||||||
|
var getElementSize = function getElementSize(value) {
|
||||||
|
|
||||||
|
var valueLength = value.length;
|
||||||
|
|
||||||
|
// Calculate base size, assuming at least one digit, the "."
|
||||||
|
// separator, and the "," or ";" terminator
|
||||||
|
var protocolSize = valueLength + 3;
|
||||||
|
|
||||||
|
// Add one character for each additional digit that would occur
|
||||||
|
// in the element length prefix
|
||||||
|
while (valueLength >= 10) {
|
||||||
|
protocolSize++;
|
||||||
|
valueLength = Math.floor(valueLength / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
return protocolSize;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
// Start playback client connected
|
// Start playback client connected
|
||||||
playbackClient.connect();
|
playbackClient.connect();
|
||||||
@@ -167,13 +338,24 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
// Hide cursor unless mouse position is received
|
// Hide cursor unless mouse position is received
|
||||||
playbackClient.getDisplay().showCursor(false);
|
playbackClient.getDisplay().showCursor(false);
|
||||||
|
|
||||||
// Read instructions from provided tunnel, extracting each frame
|
/**
|
||||||
tunnel.oninstruction = function handleInstruction(opcode, args) {
|
* Handles a newly-received instruction, whether from the main Blob or a
|
||||||
|
* tunnel, adding new frames and keyframes as necessary. Load progress is
|
||||||
|
* reported via onprogress automatically.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {!string} opcode
|
||||||
|
* The opcode of the instruction to handle.
|
||||||
|
*
|
||||||
|
* @param {!string[]} args
|
||||||
|
* The arguments of the received instruction, if any.
|
||||||
|
*/
|
||||||
|
var loadInstruction = function loadInstruction(opcode, args) {
|
||||||
|
|
||||||
// Store opcode and arguments for received instruction
|
// Advance end of frame by overall length of parsed instruction
|
||||||
var instruction = new Guacamole.SessionRecording._Frame.Instruction(opcode, args.slice());
|
frameEnd += getElementSize(opcode);
|
||||||
instructions.push(instruction);
|
for (var i = 0; i < args.length; i++)
|
||||||
charactersSinceLastKeyframe += instruction.getSize();
|
frameEnd += getElementSize(args[i]);
|
||||||
|
|
||||||
// Once a sync is received, store all instructions since the last
|
// Once a sync is received, store all instructions since the last
|
||||||
// frame as a new frame
|
// frame as a new frame
|
||||||
@@ -183,30 +365,97 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
var timestamp = parseInt(args[0]);
|
var timestamp = parseInt(args[0]);
|
||||||
|
|
||||||
// Add a new frame containing the instructions read since last frame
|
// Add a new frame containing the instructions read since last frame
|
||||||
var frame = new Guacamole.SessionRecording._Frame(timestamp, instructions);
|
var frame = new Guacamole.SessionRecording._Frame(timestamp, frameStart, frameEnd);
|
||||||
frames.push(frame);
|
frames.push(frame);
|
||||||
|
frameStart = frameEnd;
|
||||||
|
|
||||||
// This frame should eventually become a keyframe if enough data
|
// This frame should eventually become a keyframe if enough data
|
||||||
// has been processed and enough recording time has elapsed, or if
|
// has been processed and enough recording time has elapsed, or if
|
||||||
// this is the absolute first frame
|
// this is the absolute first frame
|
||||||
if (frames.length === 1 || (charactersSinceLastKeyframe >= KEYFRAME_CHAR_INTERVAL
|
if (frames.length === 1 || (frameEnd - frames[lastKeyframe].start >= KEYFRAME_CHAR_INTERVAL
|
||||||
&& timestamp - lastKeyframeTimestamp >= KEYFRAME_TIME_INTERVAL)) {
|
&& timestamp - frames[lastKeyframe].timestamp >= KEYFRAME_TIME_INTERVAL)) {
|
||||||
frame.keyframe = true;
|
frame.keyframe = true;
|
||||||
lastKeyframeTimestamp = timestamp;
|
lastKeyframe = frames.length - 1;
|
||||||
charactersSinceLastKeyframe = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear set of instructions in preparation for next frame
|
|
||||||
instructions = [];
|
|
||||||
|
|
||||||
// Notify that additional content is available
|
// Notify that additional content is available
|
||||||
if (recording.onprogress)
|
if (recording.onprogress)
|
||||||
recording.onprogress(recording.getDuration());
|
recording.onprogress(recording.getDuration(), frameEnd);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that the session recording has been fully loaded. If the onload
|
||||||
|
* handler has not been defined, this function has no effect.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
var notifyLoaded = function notifyLoaded() {
|
||||||
|
if (recording.onload)
|
||||||
|
recording.onload();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read instructions from provided blob, extracting each frame
|
||||||
|
if (source instanceof Blob)
|
||||||
|
parseBlob(recordingBlob, loadInstruction, notifyLoaded);
|
||||||
|
|
||||||
|
// If tunnel provided instead of Blob, extract frames, etc. as instructions
|
||||||
|
// are received, buffering things into a Blob for future seeks
|
||||||
|
else {
|
||||||
|
|
||||||
|
tunnel = source;
|
||||||
|
recordingBlob = new Blob();
|
||||||
|
|
||||||
|
var errorEncountered = false;
|
||||||
|
var instructionBuffer = '';
|
||||||
|
|
||||||
|
// Read instructions from provided tunnel, extracting each frame
|
||||||
|
tunnel.oninstruction = function handleInstruction(opcode, args) {
|
||||||
|
|
||||||
|
// Reconstitute received instruction
|
||||||
|
instructionBuffer += opcode.length + '.' + opcode;
|
||||||
|
args.forEach(function appendArg(arg) {
|
||||||
|
instructionBuffer += ',' + arg.length + '.' + arg;
|
||||||
|
});
|
||||||
|
instructionBuffer += ';';
|
||||||
|
|
||||||
|
// Append to Blob (creating a new Blob in the process)
|
||||||
|
if (instructionBuffer.length >= BLOCK_SIZE) {
|
||||||
|
recordingBlob = new Blob([recordingBlob, instructionBuffer]);
|
||||||
|
instructionBuffer = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load parsed instruction into recording
|
||||||
|
loadInstruction(opcode, args);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Report any errors encountered
|
||||||
|
tunnel.onerror = function tunnelError(status) {
|
||||||
|
errorEncountered = true;
|
||||||
|
if (recording.onerror)
|
||||||
|
recording.onerror(status.message);
|
||||||
|
};
|
||||||
|
|
||||||
|
tunnel.onstatechange = function tunnelStateChanged(state) {
|
||||||
|
if (state === Guacamole.Tunnel.State.CLOSED) {
|
||||||
|
|
||||||
|
// Append to Blob (creating a new Blob in the process)
|
||||||
|
if (instructionBuffer.length >= BLOCK_SIZE) {
|
||||||
|
recordingBlob = new Blob([recordingBlob, instructionBuffer]);
|
||||||
|
instructionBuffer = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consider recording loaded if tunnel has closed without errors
|
||||||
|
if (!errorEncountered)
|
||||||
|
notifyLoaded();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given absolute timestamp to a timestamp which is relative
|
* Converts the given absolute timestamp to a timestamp which is relative
|
||||||
* to the first frame in the recording.
|
* to the first frame in the recording.
|
||||||
@@ -283,23 +532,33 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
* @param {!number} index
|
* @param {!number} index
|
||||||
* The index of the frame within the frames array which should be
|
* The index of the frame within the frames array which should be
|
||||||
* replayed.
|
* replayed.
|
||||||
|
*
|
||||||
|
* @param {function} callback
|
||||||
|
* The callback to invoke once replay of the frame has completed.
|
||||||
*/
|
*/
|
||||||
var replayFrame = function replayFrame(index) {
|
var replayFrame = function replayFrame(index, callback) {
|
||||||
|
|
||||||
var frame = frames[index];
|
var frame = frames[index];
|
||||||
|
|
||||||
// Replay all instructions within the retrieved frame
|
// Replay all instructions within the retrieved frame
|
||||||
for (var i = 0; i < frame.instructions.length; i++) {
|
parseBlob(recordingBlob.slice(frame.start, frame.end), function handleInstruction(opcode, args) {
|
||||||
var instruction = frame.instructions[i];
|
playbackTunnel.receiveInstruction(opcode, args);
|
||||||
playbackTunnel.receiveInstruction(instruction.opcode, instruction.args);
|
}, function replayCompleted() {
|
||||||
}
|
|
||||||
|
|
||||||
// Store client state if frame is flagged as a keyframe
|
// Store client state if frame is flagged as a keyframe
|
||||||
if (frame.keyframe && !frame.clientState) {
|
if (frame.keyframe && !frame.clientState) {
|
||||||
playbackClient.exportState(function storeClientState(state) {
|
playbackClient.exportState(function storeClientState(state) {
|
||||||
frame.clientState = state;
|
frame.clientState = state;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update state to correctly represent the current frame
|
||||||
|
currentFrame = index;
|
||||||
|
|
||||||
|
if (callback)
|
||||||
|
callback();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -315,7 +574,7 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
* The index of the frame which should become the new playback
|
* The index of the frame which should become the new playback
|
||||||
* position.
|
* position.
|
||||||
*
|
*
|
||||||
* @param {!function} callback
|
* @param {function} callback
|
||||||
* The callback to invoke once the seek operation has completed.
|
* The callback to invoke once the seek operation has completed.
|
||||||
*
|
*
|
||||||
* @param {number} [delay=0]
|
* @param {number} [delay=0]
|
||||||
@@ -327,63 +586,62 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
// Abort any in-progress seek
|
// Abort any in-progress seek
|
||||||
abortSeek();
|
abortSeek();
|
||||||
|
|
||||||
// Replay frames asynchronously
|
// Note that a new seek operation is in progress
|
||||||
seekTimeout = window.setTimeout(function continueSeek() {
|
var thisSeek = activeSeek = {
|
||||||
|
aborted : false
|
||||||
|
};
|
||||||
|
|
||||||
var startIndex;
|
var startIndex;
|
||||||
|
|
||||||
// Back up until startIndex represents current state
|
// Back up until startIndex represents current state
|
||||||
for (startIndex = index; startIndex >= 0; startIndex--) {
|
for (startIndex = index; startIndex >= 0; startIndex--) {
|
||||||
|
|
||||||
var frame = frames[startIndex];
|
var frame = frames[startIndex];
|
||||||
|
|
||||||
// If we've reached the current frame, startIndex represents
|
// If we've reached the current frame, startIndex represents
|
||||||
// current state by definition
|
// current state by definition
|
||||||
if (startIndex === currentFrame)
|
if (startIndex === currentFrame)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// If frame has associated absolute state, make that frame the
|
|
||||||
// current state
|
|
||||||
if (frame.clientState) {
|
|
||||||
playbackClient.importState(frame.clientState);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// If frame has associated absolute state, make that frame the
|
||||||
|
// current state
|
||||||
|
if (frame.clientState) {
|
||||||
|
playbackClient.importState(frame.clientState);
|
||||||
|
currentFrame = index;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance to frame index after current state
|
}
|
||||||
startIndex++;
|
|
||||||
|
|
||||||
var startTime = new Date().getTime();
|
// Replay any applicable incremental frames
|
||||||
|
var continueReplay = function continueReplay() {
|
||||||
// Replay any applicable incremental frames
|
|
||||||
for (; startIndex <= 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
|
// Notify of changes in position
|
||||||
if (recording.onseek)
|
if (recording.onseek && currentFrame > startIndex) {
|
||||||
recording.onseek(recording.getPosition());
|
recording.onseek(toRelativeTimestamp(frames[currentFrame].timestamp),
|
||||||
|
currentFrame - startIndex, index - startIndex);
|
||||||
|
}
|
||||||
|
|
||||||
// If the seek operation has not yet completed, schedule continuation
|
// Cancel seek if aborted
|
||||||
if (currentFrame !== index)
|
if (thisSeek.aborted)
|
||||||
seekToFrame(index, callback,
|
return;
|
||||||
Math.max(delay - (new Date().getTime() - startTime), 0));
|
|
||||||
|
|
||||||
// Notify that the requested seek has completed
|
// If frames remain, replay the next frame
|
||||||
|
if (!thisSeek.aborted && currentFrame < index)
|
||||||
|
replayFrame(currentFrame + 1, continueReplay);
|
||||||
|
|
||||||
|
// Otherwise, the seek operation is completed
|
||||||
else
|
else
|
||||||
callback();
|
callback();
|
||||||
|
|
||||||
}, delay || 0);
|
};
|
||||||
|
|
||||||
|
// Continue replay after requested delay has elapsed, or
|
||||||
|
// immediately if no delay was requested
|
||||||
|
if (delay)
|
||||||
|
window.setTimeout(continueReplay, delay);
|
||||||
|
else
|
||||||
|
continueReplay();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -394,7 +652,10 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
var abortSeek = function abortSeek() {
|
var abortSeek = function abortSeek() {
|
||||||
window.clearTimeout(seekTimeout);
|
if (activeSeek) {
|
||||||
|
activeSeek.aborted = true;
|
||||||
|
activeSeek = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -433,6 +694,32 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when loading of this recording has completed and all frames
|
||||||
|
* are available.
|
||||||
|
*
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
this.onload = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when an error occurs which prevents the recording from being
|
||||||
|
* played back.
|
||||||
|
*
|
||||||
|
* @event
|
||||||
|
* @param {!string} message
|
||||||
|
* A human-readable message describing the error that occurred.
|
||||||
|
*/
|
||||||
|
this.onerror = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fired when further loading of this recording has been explicitly
|
||||||
|
* aborted through a call to abort().
|
||||||
|
*
|
||||||
|
* @event
|
||||||
|
*/
|
||||||
|
this.onabort = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired when new frames have become available while the recording is
|
* Fired when new frames have become available while the recording is
|
||||||
* being downloaded.
|
* being downloaded.
|
||||||
@@ -440,6 +727,9 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
* @event
|
* @event
|
||||||
* @param {!number} duration
|
* @param {!number} duration
|
||||||
* The new duration of the recording, in milliseconds.
|
* The new duration of the recording, in milliseconds.
|
||||||
|
*
|
||||||
|
* @param {!number} parsedSize
|
||||||
|
* The number of bytes that have been loaded/parsed.
|
||||||
*/
|
*/
|
||||||
this.onprogress = null;
|
this.onprogress = null;
|
||||||
|
|
||||||
@@ -466,27 +756,59 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
* @event
|
* @event
|
||||||
* @param {!number} position
|
* @param {!number} position
|
||||||
* The new position within the recording, in milliseconds.
|
* The new position within the recording, in milliseconds.
|
||||||
|
*
|
||||||
|
* @param {!number} current
|
||||||
|
* The number of frames that have been seeked through. If not
|
||||||
|
* seeking through multiple frames due to a call to seek(), this
|
||||||
|
* will be 1.
|
||||||
|
*
|
||||||
|
* @param {!number} total
|
||||||
|
* The number of frames that are being seeked through in the
|
||||||
|
* current seek operation. If not seeking through multiple frames
|
||||||
|
* due to a call to seek(), this will be 1.
|
||||||
*/
|
*/
|
||||||
this.onseek = null;
|
this.onseek = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects the underlying tunnel, beginning download of the Guacamole
|
* Connects the underlying tunnel, beginning download of the Guacamole
|
||||||
* session. Playback of the Guacamole session cannot occur until at least
|
* session. Playback of the Guacamole session cannot occur until at least
|
||||||
* one frame worth of instructions has been downloaded.
|
* one frame worth of instructions has been downloaded. If the underlying
|
||||||
|
* recording source is a Blob, this function has no effect.
|
||||||
*
|
*
|
||||||
* @param {string} [data]
|
* @param {string} [data]
|
||||||
* The data to send to the tunnel when connecting.
|
* The data to send to the tunnel when connecting.
|
||||||
*/
|
*/
|
||||||
this.connect = function connect(data) {
|
this.connect = function connect(data) {
|
||||||
tunnel.connect(data);
|
if (tunnel)
|
||||||
|
tunnel.connect(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnects the underlying tunnel, stopping further download of the
|
* Disconnects the underlying tunnel, stopping further download of the
|
||||||
* Guacamole session.
|
* Guacamole session. If the underlying recording source is a Blob, this
|
||||||
|
* function has no effect.
|
||||||
*/
|
*/
|
||||||
this.disconnect = function disconnect() {
|
this.disconnect = function disconnect() {
|
||||||
tunnel.disconnect();
|
if (tunnel)
|
||||||
|
tunnel.disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aborts the loading process, stopping further processing of the
|
||||||
|
* provided data. If the underlying recording source is a Guacamole tunnel,
|
||||||
|
* it will be disconnected.
|
||||||
|
*/
|
||||||
|
this.abort = function abort() {
|
||||||
|
if (!aborted) {
|
||||||
|
|
||||||
|
aborted = true;
|
||||||
|
if (recording.onabort)
|
||||||
|
recording.onabort();
|
||||||
|
|
||||||
|
if (tunnel)
|
||||||
|
tunnel.disconnect();
|
||||||
|
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -603,23 +925,48 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
if (frames.length === 0)
|
if (frames.length === 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Abort active seek operation, if any
|
||||||
|
recording.cancel();
|
||||||
|
|
||||||
// Pause playback, preserving playback state
|
// Pause playback, preserving playback state
|
||||||
var originallyPlaying = recording.isPlaying();
|
var originallyPlaying = recording.isPlaying();
|
||||||
recording.pause();
|
recording.pause();
|
||||||
|
|
||||||
// Perform seek
|
// Restore playback when seek is completed or cancelled
|
||||||
seekToFrame(findFrame(0, frames.length - 1, position), function restorePlaybackState() {
|
seekCallback = function restorePlaybackState() {
|
||||||
|
|
||||||
|
// Seek is no longer in progress
|
||||||
|
seekCallback = null;
|
||||||
|
|
||||||
// Restore playback state
|
// Restore playback state
|
||||||
if (originallyPlaying)
|
if (originallyPlaying) {
|
||||||
recording.play();
|
recording.play();
|
||||||
|
originallyPlaying = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Notify that seek has completed
|
// Notify that seek has completed
|
||||||
if (callback)
|
if (callback)
|
||||||
callback();
|
callback();
|
||||||
|
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Perform seek
|
||||||
|
seekToFrame(findFrame(0, frames.length - 1, position), seekCallback);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the current seek operation, setting the current frame of the
|
||||||
|
* recording to wherever the seek operation was able to reach prior to
|
||||||
|
* being cancelled. If a callback was provided to seek(), that callback
|
||||||
|
* is invoked. If a seek operation is not currently underway, this
|
||||||
|
* function has no effect.
|
||||||
|
*/
|
||||||
|
this.cancel = function cancel() {
|
||||||
|
if (seekCallback) {
|
||||||
|
abortSeek();
|
||||||
|
seekCallback();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -664,11 +1011,15 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
* The timestamp of this frame, as dictated by the "sync" instruction which
|
* The timestamp of this frame, as dictated by the "sync" instruction which
|
||||||
* terminates the frame.
|
* terminates the frame.
|
||||||
*
|
*
|
||||||
* @param {!Guacamole.SessionRecording._Frame.Instruction[]} instructions
|
* @param {!number} start
|
||||||
* All instructions which are necessary to generate this frame relative to
|
* The byte offset within the blob of the first character of the first
|
||||||
* the previous frame in the Guacamole session.
|
* instruction of this frame.
|
||||||
|
*
|
||||||
|
* @param {!number} end
|
||||||
|
* The byte offset within the blob of character which follows the last
|
||||||
|
* character of the last instruction of this frame.
|
||||||
*/
|
*/
|
||||||
Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
|
Guacamole.SessionRecording._Frame = function _Frame(timestamp, start, end) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this frame should be used as a keyframe if possible. This value
|
* Whether this frame should be used as a keyframe if possible. This value
|
||||||
@@ -690,12 +1041,20 @@ Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
|
|||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All instructions which are necessary to generate this frame relative to
|
* The byte offset within the blob of the first character of the first
|
||||||
* the previous frame in the Guacamole session.
|
* instruction of this frame.
|
||||||
*
|
*
|
||||||
* @type {!Guacamole.SessionRecording._Frame.Instruction[]}
|
* @type {!number}
|
||||||
*/
|
*/
|
||||||
this.instructions = instructions;
|
this.start = start;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The byte offset within the blob of character which follows the last
|
||||||
|
* character of the last instruction of this frame.
|
||||||
|
*
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
this.end = end;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A snapshot of client state after this frame was rendered, as returned by
|
* A snapshot of client state after this frame was rendered, as returned by
|
||||||
@@ -709,66 +1068,6 @@ Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 < instruction.args.length; i++)
|
|
||||||
size += instruction.args[i].length;
|
|
||||||
|
|
||||||
return size;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A read-only Guacamole.Tunnel implementation which streams instructions
|
* A read-only Guacamole.Tunnel implementation which streams instructions
|
||||||
* received through explicit calls to its receiveInstruction() function.
|
* received through explicit calls to its receiveInstruction() function.
|
||||||
|
@@ -38,6 +38,13 @@ Guacamole.StringReader = function(stream) {
|
|||||||
*/
|
*/
|
||||||
var guac_reader = this;
|
var guac_reader = this;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser for received UTF-8 data.
|
||||||
|
*
|
||||||
|
* @type {!Guacamole.UTF8Parser}
|
||||||
|
*/
|
||||||
|
var utf8Parser = new Guacamole.UTF8Parser();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapped Guacamole.ArrayBufferReader.
|
* Wrapped Guacamole.ArrayBufferReader.
|
||||||
*
|
*
|
||||||
@@ -46,103 +53,11 @@ Guacamole.StringReader = function(stream) {
|
|||||||
*/
|
*/
|
||||||
var array_reader = new Guacamole.ArrayBufferReader(stream);
|
var array_reader = new Guacamole.ArrayBufferReader(stream);
|
||||||
|
|
||||||
/**
|
|
||||||
* The number of bytes remaining for the current codepoint.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @type {!number}
|
|
||||||
*/
|
|
||||||
var bytes_remaining = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current codepoint value, as calculated from bytes read so far.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @type {!number}
|
|
||||||
*/
|
|
||||||
var codepoint = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the given UTF-8 data into a Unicode string. The data may end in
|
|
||||||
* the middle of a multibyte character.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
* @param {!ArrayBuffer} buffer
|
|
||||||
* Arbitrary UTF-8 data.
|
|
||||||
*
|
|
||||||
* @return {!string}
|
|
||||||
* A decoded Unicode string.
|
|
||||||
*/
|
|
||||||
function __decode_utf8(buffer) {
|
|
||||||
|
|
||||||
var text = "";
|
|
||||||
|
|
||||||
var bytes = new Uint8Array(buffer);
|
|
||||||
for (var i=0; i<bytes.length; i++) {
|
|
||||||
|
|
||||||
// Get current byte
|
|
||||||
var value = bytes[i];
|
|
||||||
|
|
||||||
// Start new codepoint if nothing yet read
|
|
||||||
if (bytes_remaining === 0) {
|
|
||||||
|
|
||||||
// 1 byte (0xxxxxxx)
|
|
||||||
if ((value | 0x7F) === 0x7F)
|
|
||||||
text += String.fromCharCode(value);
|
|
||||||
|
|
||||||
// 2 byte (110xxxxx)
|
|
||||||
else if ((value | 0x1F) === 0xDF) {
|
|
||||||
codepoint = value & 0x1F;
|
|
||||||
bytes_remaining = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3 byte (1110xxxx)
|
|
||||||
else if ((value | 0x0F )=== 0xEF) {
|
|
||||||
codepoint = value & 0x0F;
|
|
||||||
bytes_remaining = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4 byte (11110xxx)
|
|
||||||
else if ((value | 0x07) === 0xF7) {
|
|
||||||
codepoint = value & 0x07;
|
|
||||||
bytes_remaining = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid byte
|
|
||||||
else
|
|
||||||
text += "\uFFFD";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue existing codepoint (10xxxxxx)
|
|
||||||
else if ((value | 0x3F) === 0xBF) {
|
|
||||||
|
|
||||||
codepoint = (codepoint << 6) | (value & 0x3F);
|
|
||||||
bytes_remaining--;
|
|
||||||
|
|
||||||
// Write codepoint if finished
|
|
||||||
if (bytes_remaining === 0)
|
|
||||||
text += String.fromCharCode(codepoint);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invalid byte
|
|
||||||
else {
|
|
||||||
bytes_remaining = 0;
|
|
||||||
text += "\uFFFD";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive blobs as strings
|
// Receive blobs as strings
|
||||||
array_reader.ondata = function(buffer) {
|
array_reader.ondata = function(buffer) {
|
||||||
|
|
||||||
// Decode UTF-8
|
// Decode UTF-8
|
||||||
var text = __decode_utf8(buffer);
|
var text = utf8Parser.decode(buffer);
|
||||||
|
|
||||||
// Call handler, if present
|
// Call handler, if present
|
||||||
if (guac_reader.ontext)
|
if (guac_reader.ontext)
|
||||||
|
@@ -1346,13 +1346,14 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
|
|||||||
var tunnel = this;
|
var tunnel = this;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current, in-progress HTTP request. If no request is currently in
|
* AbortController instance which allows the current, in-progress HTTP
|
||||||
* progress, this will be null.
|
* request to be aborted. If no request is currently in progress, this will
|
||||||
|
* be null.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @type {XMLHttpRequest}
|
* @type {AbortController}
|
||||||
*/
|
*/
|
||||||
var xhr = null;
|
var abortController = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional headers to be sent in tunnel requests. This dictionary can be
|
* Additional headers to be sent in tunnel requests. This dictionary can be
|
||||||
@@ -1364,23 +1365,6 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
|
|||||||
*/
|
*/
|
||||||
var extraHeaders = extraTunnelHeaders || {};
|
var extraHeaders = extraTunnelHeaders || {};
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds the configured additional headers to the given request.
|
|
||||||
*
|
|
||||||
* @param {!XMLHttpRequest} request
|
|
||||||
* The request where the configured extra headers will be added.
|
|
||||||
*
|
|
||||||
* @param {!object} headers
|
|
||||||
* The headers to be added to the request.
|
|
||||||
*
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
function addExtraHeaders(request, headers) {
|
|
||||||
for (var name in headers) {
|
|
||||||
request.setRequestHeader(name, headers[name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sendMessage = function sendMessage(elements) {
|
this.sendMessage = function sendMessage(elements) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
};
|
};
|
||||||
@@ -1393,18 +1377,10 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
|
|||||||
// Connection is now starting
|
// Connection is now starting
|
||||||
tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
|
tunnel.setState(Guacamole.Tunnel.State.CONNECTING);
|
||||||
|
|
||||||
// Start a new connection
|
// Create Guacamole protocol and UTF-8 parsers specifically for this
|
||||||
xhr = new XMLHttpRequest();
|
// connection
|
||||||
xhr.open('GET', url);
|
|
||||||
xhr.withCredentials = !!crossDomain;
|
|
||||||
addExtraHeaders(xhr, extraHeaders);
|
|
||||||
xhr.responseType = 'text';
|
|
||||||
xhr.send(null);
|
|
||||||
|
|
||||||
var offset = 0;
|
|
||||||
|
|
||||||
// Create Guacamole protocol parser specifically for this connection
|
|
||||||
var parser = new Guacamole.Parser();
|
var parser = new Guacamole.Parser();
|
||||||
|
var utf8Parser = new Guacamole.UTF8Parser();
|
||||||
|
|
||||||
// Invoke tunnel's oninstruction handler for each parsed instruction
|
// Invoke tunnel's oninstruction handler for each parsed instruction
|
||||||
parser.oninstruction = function instructionReceived(opcode, args) {
|
parser.oninstruction = function instructionReceived(opcode, args) {
|
||||||
@@ -1412,51 +1388,62 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
|
|||||||
tunnel.oninstruction(opcode, args);
|
tunnel.oninstruction(opcode, args);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Continuously parse received data
|
// Allow new request to be aborted
|
||||||
xhr.onreadystatechange = function readyStateChanged() {
|
abortController = new AbortController();
|
||||||
|
|
||||||
// Parse while data is being received
|
// Stream using the Fetch API
|
||||||
if (xhr.readyState === 3 || xhr.readyState === 4) {
|
fetch(url, {
|
||||||
|
headers : extraHeaders,
|
||||||
|
credentials : crossDomain ? 'include' : 'same-origin',
|
||||||
|
signal : abortController.signal
|
||||||
|
})
|
||||||
|
.then(function gotResponse(response) {
|
||||||
|
|
||||||
// Connection is open
|
// Reset state and close upon error
|
||||||
tunnel.setState(Guacamole.Tunnel.State.OPEN);
|
if (!response.ok) {
|
||||||
|
|
||||||
var buffer = xhr.responseText;
|
if (tunnel.onerror)
|
||||||
var length = buffer.length;
|
tunnel.onerror(new Guacamole.Status(
|
||||||
|
Guacamole.Status.Code.fromHTTPCode(response.status), response.statusText));
|
||||||
|
|
||||||
// Parse only the portion of data which is newly received
|
tunnel.disconnect();
|
||||||
if (offset < length) {
|
return;
|
||||||
parser.receive(buffer.substring(offset));
|
|
||||||
offset = length;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up and close when done
|
// Connection is open
|
||||||
if (xhr.readyState === 4)
|
tunnel.setState(Guacamole.Tunnel.State.OPEN);
|
||||||
tunnel.disconnect();
|
|
||||||
|
|
||||||
};
|
var reader = response.body.getReader();
|
||||||
|
var processReceivedText = function processReceivedText(result) {
|
||||||
|
|
||||||
// Reset state and close upon error
|
// Clean up and close when done
|
||||||
xhr.onerror = function httpError() {
|
if (result.done) {
|
||||||
|
tunnel.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Fail if file could not be downloaded via HTTP
|
// Parse only the portion of data which is newly received
|
||||||
if (tunnel.onerror)
|
parser.receive(utf8Parser.decode(result.value));
|
||||||
tunnel.onerror(new Guacamole.Status(
|
|
||||||
Guacamole.Status.Code.fromHTTPCode(xhr.status), xhr.statusText));
|
|
||||||
|
|
||||||
tunnel.disconnect();
|
// Continue parsing when next chunk is received
|
||||||
};
|
reader.read().then(processReceivedText);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
// Schedule parse of first chunk
|
||||||
|
reader.read().then(processReceivedText);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.disconnect = function disconnect() {
|
this.disconnect = function disconnect() {
|
||||||
|
|
||||||
// Abort and dispose of XHR if a request is in progress
|
// Abort any in-progress request
|
||||||
if (xhr) {
|
if (abortController) {
|
||||||
xhr.abort();
|
abortController.abort();
|
||||||
xhr = null;
|
abortController = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection is now closed
|
// Connection is now closed
|
||||||
|
126
guacamole-common-js/src/main/webapp/modules/UTF8Parser.js
Normal file
126
guacamole-common-js/src/main/webapp/modules/UTF8Parser.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* 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 || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser that decodes UTF-8 text from a series of provided ArrayBuffers.
|
||||||
|
* Multi-byte characters that continue from one buffer to the next are handled
|
||||||
|
* correctly.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
Guacamole.UTF8Parser = function UTF8Parser() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of bytes remaining for the current codepoint.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
var bytesRemaining = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current codepoint value, as calculated from bytes read so far.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {!number}
|
||||||
|
*/
|
||||||
|
var codepoint = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the given UTF-8 data into a Unicode string, returning a string
|
||||||
|
* containing all complete UTF-8 characters within the provided data. The
|
||||||
|
* data may end in the middle of a multi-byte character, in which case the
|
||||||
|
* complete character will be returned from a later call to decode() after
|
||||||
|
* enough bytes have been provided.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {!ArrayBuffer} buffer
|
||||||
|
* Arbitrary UTF-8 data.
|
||||||
|
*
|
||||||
|
* @return {!string}
|
||||||
|
* The decoded Unicode string.
|
||||||
|
*/
|
||||||
|
this.decode = function decode(buffer) {
|
||||||
|
|
||||||
|
var text = '';
|
||||||
|
|
||||||
|
var bytes = new Uint8Array(buffer);
|
||||||
|
for (var i=0; i<bytes.length; i++) {
|
||||||
|
|
||||||
|
// Get current byte
|
||||||
|
var value = bytes[i];
|
||||||
|
|
||||||
|
// Start new codepoint if nothing yet read
|
||||||
|
if (bytesRemaining === 0) {
|
||||||
|
|
||||||
|
// 1 byte (0xxxxxxx)
|
||||||
|
if ((value | 0x7F) === 0x7F)
|
||||||
|
text += String.fromCharCode(value);
|
||||||
|
|
||||||
|
// 2 byte (110xxxxx)
|
||||||
|
else if ((value | 0x1F) === 0xDF) {
|
||||||
|
codepoint = value & 0x1F;
|
||||||
|
bytesRemaining = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3 byte (1110xxxx)
|
||||||
|
else if ((value | 0x0F )=== 0xEF) {
|
||||||
|
codepoint = value & 0x0F;
|
||||||
|
bytesRemaining = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4 byte (11110xxx)
|
||||||
|
else if ((value | 0x07) === 0xF7) {
|
||||||
|
codepoint = value & 0x07;
|
||||||
|
bytesRemaining = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid byte
|
||||||
|
else
|
||||||
|
text += '\uFFFD';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue existing codepoint (10xxxxxx)
|
||||||
|
else if ((value | 0x3F) === 0xBF) {
|
||||||
|
|
||||||
|
codepoint = (codepoint << 6) | (value & 0x3F);
|
||||||
|
bytesRemaining--;
|
||||||
|
|
||||||
|
// Write codepoint if finished
|
||||||
|
if (bytesRemaining === 0)
|
||||||
|
text += String.fromCharCode(codepoint);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid byte
|
||||||
|
else {
|
||||||
|
bytesRemaining = 0;
|
||||||
|
text += '\uFFFD';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
Reference in New Issue
Block a user