mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 00:53:21 +00:00 
			
		
		
		
	GUACAMOLE-896: Update session recording player to support efficient reading from Blobs.
This commit is contained in:
		| @@ -20,18 +20,20 @@ | ||||
| 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. | ||||
|  * A recording of a Guacamole session. Given a {@link Guacamole.Tunnel} or Blob, | ||||
|  * the Guacamole.SessionRecording automatically parses Guacamole instructions | ||||
|  * within the recording source as it plays back the recording. 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. Parsing of the contents of the recording will | ||||
|  * begin immediately and automatically after this constructor is invoked. | ||||
|  * | ||||
|  * @constructor | ||||
|  * @param {!Guacamole.Tunnel} tunnel | ||||
|  *     The Guacamole.Tunnel from which the instructions of the recording should | ||||
|  * @param {!Blob|Guacamole.Tunnel} source | ||||
|  *     The Blob from which the instructions of the recording should | ||||
|  *     be read. | ||||
|  */ | ||||
| Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
| Guacamole.SessionRecording = function SessionRecording(source) { | ||||
|  | ||||
|     /** | ||||
|      * Reference to this Guacamole.SessionRecording. | ||||
| @@ -41,13 +43,45 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
|      */ | ||||
|     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 | ||||
|      * keyframes. | ||||
|      * | ||||
|      * @private | ||||
|      * @constant | ||||
|      * @type {!number} | ||||
|      * @type {Number} | ||||
|      */ | ||||
|     var KEYFRAME_CHAR_INTERVAL = 16384; | ||||
|  | ||||
| @@ -56,47 +90,18 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
|      * | ||||
|      * @private | ||||
|      * @constant | ||||
|      * @type {!number} | ||||
|      * @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. | ||||
|      * All frames parsed from the provided blob. | ||||
|      * | ||||
|      * @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. | ||||
| @@ -104,7 +109,7 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
|      * @private | ||||
|      * @type {!number} | ||||
|      */ | ||||
|     var lastKeyframeTimestamp = 0; | ||||
|     var lastKeyframe = 0; | ||||
|  | ||||
|     /** | ||||
|      * Tunnel which feeds arbitrary instructions to the client used by this | ||||
| @@ -152,14 +157,180 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
|     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. | ||||
|      * An object containing a single "aborted" property which is set to | ||||
|      * true if the in-progress seek operation should be aborted. If no seek | ||||
|      * operation is in progress, this will be null. | ||||
|      * | ||||
|      * @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 | ||||
|     playbackClient.connect(); | ||||
| @@ -167,13 +338,24 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
|     // 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) { | ||||
|     /** | ||||
|      * 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 | ||||
|         var instruction = new Guacamole.SessionRecording._Frame.Instruction(opcode, args.slice()); | ||||
|         instructions.push(instruction); | ||||
|         charactersSinceLastKeyframe += instruction.getSize(); | ||||
|         // Advance end of frame by overall length of parsed instruction | ||||
|         frameEnd += getElementSize(opcode); | ||||
|         for (var i = 0; i < args.length; i++) | ||||
|             frameEnd += getElementSize(args[i]); | ||||
|  | ||||
|         // Once a sync is received, store all instructions since the last | ||||
|         // frame as a new frame | ||||
| @@ -183,30 +365,97 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
|             var timestamp = parseInt(args[0]); | ||||
|  | ||||
|             // 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); | ||||
|             frameStart = frameEnd; | ||||
|  | ||||
|             // 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 | ||||
|                     && timestamp - lastKeyframeTimestamp >= KEYFRAME_TIME_INTERVAL)) { | ||||
|             if (frames.length === 1 || (frameEnd - frames[lastKeyframe].start >= KEYFRAME_CHAR_INTERVAL | ||||
|                     && timestamp - frames[lastKeyframe].timestamp >= KEYFRAME_TIME_INTERVAL)) { | ||||
|                 frame.keyframe = true; | ||||
|                 lastKeyframeTimestamp = timestamp; | ||||
|                 charactersSinceLastKeyframe = 0; | ||||
|                 lastKeyframe = frames.length - 1; | ||||
|             } | ||||
|  | ||||
|             // Clear set of instructions in preparation for next frame | ||||
|             instructions = []; | ||||
|  | ||||
|             // Notify that additional content is available | ||||
|             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 | ||||
|      * to the first frame in the recording. | ||||
| @@ -283,23 +532,33 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
|      * @param {!number} index | ||||
|      *     The index of the frame within the frames array which should be | ||||
|      *     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]; | ||||
|  | ||||
|         // Replay all instructions within the retrieved frame | ||||
|         for (var i = 0; i < frame.instructions.length; i++) { | ||||
|             var instruction = frame.instructions[i]; | ||||
|             playbackTunnel.receiveInstruction(instruction.opcode, instruction.args); | ||||
|         } | ||||
|         parseBlob(recordingBlob.slice(frame.start, frame.end), function handleInstruction(opcode, args) { | ||||
|             playbackTunnel.receiveInstruction(opcode, args); | ||||
|         }, function replayCompleted() { | ||||
|  | ||||
|         // Store client state if frame is flagged as a keyframe | ||||
|         if (frame.keyframe && !frame.clientState) { | ||||
|             playbackClient.exportState(function storeClientState(state) { | ||||
|                 frame.clientState = state; | ||||
|             }); | ||||
|         } | ||||
|             // Store client state if frame is flagged as a keyframe | ||||
|             if (frame.keyframe && !frame.clientState) { | ||||
|                 playbackClient.exportState(function storeClientState(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 | ||||
|      *     position. | ||||
|      * | ||||
|      * @param {!function} callback | ||||
|      * @param {function} callback | ||||
|      *     The callback to invoke once the seek operation has completed. | ||||
|      * | ||||
|      * @param {number} [delay=0] | ||||
| @@ -327,63 +586,62 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
|         // Abort any in-progress seek | ||||
|         abortSeek(); | ||||
|  | ||||
|         // Replay frames asynchronously | ||||
|         seekTimeout = window.setTimeout(function continueSeek() { | ||||
|         // Note that a new seek operation is in progress | ||||
|         var thisSeek = activeSeek = { | ||||
|             aborted : false | ||||
|         }; | ||||
|  | ||||
|             var startIndex; | ||||
|         var startIndex; | ||||
|  | ||||
|             // Back up until startIndex represents current state | ||||
|             for (startIndex = index; startIndex >= 0; startIndex--) { | ||||
|         // Back up until startIndex represents current state | ||||
|         for (startIndex = index; startIndex >= 0; startIndex--) { | ||||
|  | ||||
|                 var frame = frames[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; | ||||
|                 } | ||||
|             // 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); | ||||
|                 currentFrame = index; | ||||
|                 break; | ||||
|             } | ||||
|  | ||||
|             // Advance to frame index after current state | ||||
|             startIndex++; | ||||
|         } | ||||
|  | ||||
|             var startTime = new Date().getTime(); | ||||
|  | ||||
|             // 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; | ||||
|         // Replay any applicable incremental frames | ||||
|         var continueReplay = function continueReplay() { | ||||
|  | ||||
|             // Notify of changes in position | ||||
|             if (recording.onseek) | ||||
|                 recording.onseek(recording.getPosition()); | ||||
|             if (recording.onseek && currentFrame > startIndex) { | ||||
|                 recording.onseek(toRelativeTimestamp(frames[currentFrame].timestamp), | ||||
|                     currentFrame - startIndex, index - startIndex); | ||||
|             } | ||||
|  | ||||
|             // If the seek operation has not yet completed, schedule continuation | ||||
|             if (currentFrame !== index) | ||||
|                 seekToFrame(index, callback, | ||||
|                     Math.max(delay - (new Date().getTime() - startTime), 0)); | ||||
|             // Cancel seek if aborted | ||||
|             if (thisSeek.aborted) | ||||
|                 return; | ||||
|  | ||||
|             // 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 | ||||
|                 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 | ||||
|      */ | ||||
|     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 | ||||
|      * being downloaded. | ||||
| @@ -440,6 +727,9 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
|      * @event | ||||
|      * @param {!number} duration | ||||
|      *     The new duration of the recording, in milliseconds. | ||||
|      * | ||||
|      * @param {!number} parsedSize | ||||
|      *     The number of bytes that have been loaded/parsed. | ||||
|      */ | ||||
|     this.onprogress = null; | ||||
|  | ||||
| @@ -466,27 +756,59 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { | ||||
|      * @event | ||||
|      * @param {!number} position | ||||
|      *     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; | ||||
|  | ||||
|     /** | ||||
|      * 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. | ||||
|      * one frame worth of instructions has been downloaded. If the underlying | ||||
|      * recording source is a Blob, this function has no effect. | ||||
|      * | ||||
|      * @param {string} [data] | ||||
|      *     The data to send to the tunnel when connecting. | ||||
|      */ | ||||
|     this.connect = function connect(data) { | ||||
|         tunnel.connect(data); | ||||
|         if (tunnel) | ||||
|             tunnel.connect(data); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * 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() { | ||||
|         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) | ||||
|             return; | ||||
|  | ||||
|         // Abort active seek operation, if any | ||||
|         recording.cancel(); | ||||
|  | ||||
|         // Pause playback, preserving playback state | ||||
|         var originallyPlaying = recording.isPlaying(); | ||||
|         recording.pause(); | ||||
|  | ||||
|         // Perform seek | ||||
|         seekToFrame(findFrame(0, frames.length - 1, position), function restorePlaybackState() { | ||||
|         // Restore playback when seek is completed or cancelled | ||||
|         seekCallback = function restorePlaybackState() { | ||||
|  | ||||
|             // Seek is no longer in progress | ||||
|             seekCallback = null; | ||||
|  | ||||
|             // Restore playback state | ||||
|             if (originallyPlaying) | ||||
|             if (originallyPlaying) { | ||||
|                 recording.play(); | ||||
|                 originallyPlaying = null; | ||||
|             } | ||||
|  | ||||
|             // Notify that seek has completed | ||||
|             if (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 | ||||
|  *     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. | ||||
|  * @param {!number} start | ||||
|  *     The byte offset within the blob of the first character of the first | ||||
|  *     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 | ||||
| @@ -690,12 +1041,20 @@ Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) { | ||||
|     this.timestamp = timestamp; | ||||
|  | ||||
|     /** | ||||
|      * All instructions which are necessary to generate this frame relative to | ||||
|      * the previous frame in the Guacamole session. | ||||
|      * The byte offset within the blob of the first character of the first | ||||
|      * 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 | ||||
| @@ -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 | ||||
|  * received through explicit calls to its receiveInstruction() function. | ||||
| @@ -816,4 +1115,4 @@ Guacamole.SessionRecording._PlaybackTunnel = function _PlaybackTunnel() { | ||||
|             tunnel.oninstruction(opcode, args); | ||||
|     }; | ||||
|  | ||||
| }; | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user