| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| var Guacamole = Guacamole || {}; |
| |
| /** |
| * A recording of a Guacamole session. Given a {@link Guacamole.Tunnel} 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 {!Blob|Guacamole.Tunnel} source |
| * The Blob from which the instructions of the recording should |
| * be read. |
| */ |
| Guacamole.SessionRecording = function SessionRecording(source) { |
| |
| /** |
| * Reference to this Guacamole.SessionRecording. |
| * |
| * @private |
| * @type {!Guacamole.SessionRecording} |
| */ |
| 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} |
| */ |
| var KEYFRAME_CHAR_INTERVAL = 16384; |
| |
| /** |
| * The minimum number of milliseconds which must elapse between keyframes. |
| * |
| * @private |
| * @constant |
| * @type {Number} |
| */ |
| var KEYFRAME_TIME_INTERVAL = 5000; |
| |
| /** |
| * All frames parsed from the provided blob. |
| * |
| * @private |
| * @type {!Guacamole.SessionRecording._Frame[]} |
| */ |
| var frames = []; |
| |
| /** |
| * The timestamp of the last frame which was flagged for use as a keyframe. |
| * If no timestamp has yet been flagged, this will be 0. |
| * |
| * @private |
| * @type {!number} |
| */ |
| var lastKeyframe = 0; |
| |
| /** |
| * Tunnel which feeds arbitrary instructions to the client used by this |
| * Guacamole.SessionRecording for playback of the session recording. |
| * |
| * @private |
| * @type {!Guacamole.SessionRecording._PlaybackTunnel} |
| */ |
| var playbackTunnel = new Guacamole.SessionRecording._PlaybackTunnel(); |
| |
| /** |
| * Guacamole.Client instance used for visible playback of the session |
| * recording. |
| * |
| * @private |
| * @type {!Guacamole.Client} |
| */ |
| var playbackClient = new Guacamole.Client(playbackTunnel); |
| |
| /** |
| * The current frame rendered within the playback client. If no frame is |
| * yet rendered, this will be -1. |
| * |
| * @private |
| * @type {!number} |
| */ |
| var currentFrame = -1; |
| |
| /** |
| * The timestamp of the frame when playback began, in milliseconds. If |
| * playback is not in progress, this will be null. |
| * |
| * @private |
| * @type {number} |
| */ |
| var startVideoTimestamp = null; |
| |
| /** |
| * The real-world timestamp when playback began, in milliseconds. If |
| * playback is not in progress, this will be null. |
| * |
| * @private |
| * @type {number} |
| */ |
| var startRealTimestamp = null; |
| |
| /** |
| * 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 {object} |
| */ |
| 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(); |
| |
| // Hide cursor unless mouse position is received |
| playbackClient.getDisplay().showCursor(false); |
| |
| /** |
| * 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) { |
| |
| // 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 |
| if (opcode === 'sync') { |
| |
| // Parse frame timestamp from sync instruction |
| var timestamp = parseInt(args[0]); |
| |
| // Add a new frame containing the instructions read since last frame |
| var frame = new Guacamole.SessionRecording._Frame(timestamp, 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 || (frameEnd - frames[lastKeyframe].start >= KEYFRAME_CHAR_INTERVAL |
| && timestamp - frames[lastKeyframe].timestamp >= KEYFRAME_TIME_INTERVAL)) { |
| frame.keyframe = true; |
| lastKeyframe = frames.length - 1; |
| } |
| |
| // Notify that additional content is available |
| if (recording.onprogress) |
| 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 any remaining instructions |
| if (instructionBuffer.length) { |
| 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. |
| * |
| * @private |
| * @param {!number} timestamp |
| * The timestamp to convert to a relative timestamp. |
| * |
| * @returns {!number} |
| * The difference in milliseconds between the given timestamp and the |
| * first frame of the recording, or zero if no frames yet exist. |
| */ |
| var toRelativeTimestamp = function toRelativeTimestamp(timestamp) { |
| |
| // If no frames yet exist, all timestamps are zero |
| if (frames.length === 0) |
| return 0; |
| |
| // Calculate timestamp relative to first frame |
| return timestamp - frames[0].timestamp; |
| |
| }; |
| |
| /** |
| * Searches through the given region of frames for the frame having a |
| * relative timestamp closest to the timestamp given. |
| * |
| * @private |
| * @param {!number} minIndex |
| * The index of the first frame in the region (the frame having the |
| * smallest timestamp). |
| * |
| * @param {!number} maxIndex |
| * The index of the last frame in the region (the frame having the |
| * largest timestamp). |
| * |
| * @param {!number} timestamp |
| * The relative timestamp to search for, where zero denotes the first |
| * frame in the recording. |
| * |
| * @returns {!number} |
| * The index of the frame having a relative timestamp closest to the |
| * given value. |
| */ |
| var findFrame = function findFrame(minIndex, maxIndex, timestamp) { |
| |
| // Do not search if the region contains only one element |
| if (minIndex === maxIndex) |
| return minIndex; |
| |
| // Split search region into two halves |
| var midIndex = Math.floor((minIndex + maxIndex) / 2); |
| var midTimestamp = toRelativeTimestamp(frames[midIndex].timestamp); |
| |
| // If timestamp is within lesser half, search again within that half |
| if (timestamp < midTimestamp && midIndex > minIndex) |
| return findFrame(minIndex, midIndex - 1, timestamp); |
| |
| // If timestamp is within greater half, search again within that half |
| if (timestamp > midTimestamp && midIndex < maxIndex) |
| return findFrame(midIndex + 1, maxIndex, timestamp); |
| |
| // Otherwise, we lucked out and found a frame with exactly the |
| // desired timestamp |
| return midIndex; |
| |
| }; |
| |
| /** |
| * Replays the instructions associated with the given frame, sending those |
| * instructions to the playback client. |
| * |
| * @private |
| * @param {!number} index |
| * The index of the frame within the frames array which should be |
| * replayed. |
| * |
| * @param {function} callback |
| * The callback to invoke once replay of the frame has completed. |
| */ |
| var replayFrame = function replayFrame(index, callback) { |
| |
| var frame = frames[index]; |
| |
| // Replay all instructions within the retrieved frame |
| 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 = new Blob([JSON.stringify(state)]); |
| }); |
| } |
| |
| // Update state to correctly represent the current frame |
| currentFrame = index; |
| |
| if (callback) |
| callback(); |
| |
| }); |
| |
| }; |
| |
| /** |
| * Moves the playback position to the given frame, resetting the state of |
| * the playback client and replaying frames as necessary. The seek |
| * operation will proceed asynchronously. If a seek operation is already in |
| * progress, that seek is first aborted. The progress of the seek operation |
| * can be observed through the onseek handler and the provided callback. |
| * |
| * @private |
| * @param {!number} index |
| * The index of the frame which should become the new playback |
| * position. |
| * |
| * @param {function} callback |
| * The callback to invoke once the seek operation has completed. |
| * |
| * @param {number} [nextRealTimestamp] |
| * The timestamp of the point in time that the given frame should be |
| * displayed, as would be returned by new Date().getTime(). If omitted, |
| * the frame will be displayed as soon as possible. |
| */ |
| var seekToFrame = function seekToFrame(index, callback, nextRealTimestamp) { |
| |
| // Abort any in-progress seek |
| abortSeek(); |
| |
| // Note that a new seek operation is in progress |
| var thisSeek = activeSeek = { |
| aborted : false |
| }; |
| |
| var startIndex = index; |
| |
| // Replay any applicable incremental frames |
| var continueReplay = function continueReplay() { |
| |
| // Notify of changes in position |
| if (recording.onseek && currentFrame > startIndex) { |
| recording.onseek(toRelativeTimestamp(frames[currentFrame].timestamp), |
| currentFrame - startIndex, index - startIndex); |
| } |
| |
| // Cancel seek if aborted |
| if (thisSeek.aborted) |
| return; |
| |
| // If frames remain, replay the next frame |
| if (currentFrame < index) |
| replayFrame(currentFrame + 1, continueReplay); |
| |
| // Otherwise, the seek operation is completed |
| else |
| callback(); |
| |
| }; |
| |
| // Continue replay after requested delay has elapsed, or |
| // immediately if no delay was requested |
| var continueAfterRequiredDelay = function continueAfterRequiredDelay() { |
| var delay = nextRealTimestamp ? Math.max(nextRealTimestamp - new Date().getTime(), 0) : 0; |
| if (delay) |
| window.setTimeout(continueReplay, delay); |
| else |
| continueReplay(); |
| }; |
| |
| // Back up until startIndex represents current state |
| for (; startIndex >= 0; startIndex--) { |
| |
| var frame = frames[startIndex]; |
| |
| // If we've reached the current frame, startIndex represents |
| // current state by definition |
| if (startIndex === currentFrame) |
| break; |
| |
| // If frame has associated absolute state, make that frame the |
| // current state |
| if (frame.clientState) { |
| frame.clientState.text().then(function textReady(text) { |
| playbackClient.importState(JSON.parse(text)); |
| currentFrame = startIndex; |
| continueAfterRequiredDelay(); |
| }); |
| return; |
| } |
| |
| } |
| |
| continueAfterRequiredDelay(); |
| |
| }; |
| |
| /** |
| * Aborts the seek operation currently in progress, if any. If no seek |
| * operation is in progress, this function has no effect. |
| * |
| * @private |
| */ |
| var abortSeek = function abortSeek() { |
| if (activeSeek) { |
| activeSeek.aborted = true; |
| activeSeek = null; |
| } |
| }; |
| |
| /** |
| * Advances playback to the next frame in the frames array and schedules |
| * playback of the frame following that frame based on their associated |
| * timestamps. If no frames exist after the next frame, playback is paused. |
| * |
| * @private |
| */ |
| var continuePlayback = function continuePlayback() { |
| |
| // If frames remain after advancing, schedule next frame |
| if (currentFrame + 1 < frames.length) { |
| |
| // Pull the upcoming frame |
| var next = frames[currentFrame + 1]; |
| |
| // Calculate the real timestamp corresponding to when the next |
| // frame begins |
| var nextRealTimestamp = next.timestamp - startVideoTimestamp + startRealTimestamp; |
| |
| // Advance to next frame after enough time has elapsed |
| seekToFrame(currentFrame + 1, function frameDelayElapsed() { |
| continuePlayback(); |
| }, nextRealTimestamp); |
| |
| } |
| |
| // Otherwise stop playback |
| else |
| recording.pause(); |
| |
| }; |
| |
| /** |
| * 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. |
| * |
| * @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; |
| |
| /** |
| * Fired whenever playback of the recording has started. |
| * |
| * @event |
| */ |
| this.onplay = null; |
| |
| /** |
| * Fired whenever playback of the recording has been paused. This may |
| * happen when playback is explicitly paused with a call to pause(), or |
| * when playback is implicitly paused due to reaching the end of the |
| * recording. |
| * |
| * @event |
| */ |
| this.onpause = null; |
| |
| /** |
| * Fired whenever the playback position within the recording changes. |
| * |
| * @event |
| * @param {!number} position |
| * The new position within the recording, in milliseconds. |
| * |
| * @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. 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) { |
| if (tunnel) |
| tunnel.connect(data); |
| }; |
| |
| /** |
| * Disconnects the underlying tunnel, stopping further download of the |
| * Guacamole session. If the underlying recording source is a Blob, this |
| * function has no effect. |
| */ |
| this.disconnect = function 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(); |
| |
| } |
| }; |
| |
| /** |
| * Returns the underlying display of the Guacamole.Client used by this |
| * Guacamole.SessionRecording for playback. The display contains an Element |
| * which can be added to the DOM, causing the display (and thus playback of |
| * the recording) to become visible. |
| * |
| * @return {!Guacamole.Display} |
| * The underlying display of the Guacamole.Client used by this |
| * Guacamole.SessionRecording for playback. |
| */ |
| this.getDisplay = function getDisplay() { |
| return playbackClient.getDisplay(); |
| }; |
| |
| /** |
| * Returns whether playback is currently in progress. |
| * |
| * @returns {!boolean} |
| * true if playback is currently in progress, false otherwise. |
| */ |
| this.isPlaying = function isPlaying() { |
| return !!startVideoTimestamp; |
| }; |
| |
| /** |
| * Returns the current playback position within the recording, in |
| * milliseconds, where zero is the start of the recording. |
| * |
| * @returns {!number} |
| * The current playback position within the recording, in milliseconds. |
| */ |
| this.getPosition = function getPosition() { |
| |
| // Position is simply zero if playback has not started at all |
| if (currentFrame === -1) |
| return 0; |
| |
| // Return current position as a millisecond timestamp relative to the |
| // start of the recording |
| return toRelativeTimestamp(frames[currentFrame].timestamp); |
| |
| }; |
| |
| /** |
| * Returns the duration of this recording, in milliseconds. If the |
| * recording is still being downloaded, this value will gradually increase. |
| * |
| * @returns {!number} |
| * The duration of this recording, in milliseconds. |
| */ |
| this.getDuration = function getDuration() { |
| |
| // If no frames yet exist, duration is zero |
| if (frames.length === 0) |
| return 0; |
| |
| // Recording duration is simply the timestamp of the last frame |
| return toRelativeTimestamp(frames[frames.length - 1].timestamp); |
| |
| }; |
| |
| /** |
| * Begins continuous playback of the recording downloaded thus far. |
| * Playback of the recording will continue until pause() is invoked or |
| * until no further frames exist. Playback is initially paused when a |
| * Guacamole.SessionRecording is created, and must be explicitly started |
| * through a call to this function. If playback is already in progress, |
| * this function has no effect. If a seek operation is in progress, |
| * playback resumes at the current position, and the seek is aborted as if |
| * completed. |
| */ |
| this.play = function play() { |
| |
| // If playback is not already in progress and frames remain, |
| // begin playback |
| if (!recording.isPlaying() && currentFrame + 1 < frames.length) { |
| |
| // Notify that playback is starting |
| if (recording.onplay) |
| recording.onplay(); |
| |
| // Store timestamp of playback start for relative scheduling of |
| // future frames |
| var next = frames[currentFrame + 1]; |
| startVideoTimestamp = next.timestamp; |
| startRealTimestamp = new Date().getTime(); |
| |
| // Begin playback of video |
| continuePlayback(); |
| |
| } |
| |
| }; |
| |
| /** |
| * Seeks to the given position within the recording. If the recording is |
| * currently being played back, playback will continue after the seek is |
| * performed. If the recording is currently paused, playback will be |
| * paused after the seek is performed. If a seek operation is already in |
| * progress, that seek is first aborted. The seek operation will proceed |
| * asynchronously. |
| * |
| * @param {!number} position |
| * The position within the recording to seek to, in milliseconds. |
| * |
| * @param {function} [callback] |
| * The callback to invoke once the seek operation has completed. |
| */ |
| this.seek = function seek(position, callback) { |
| |
| // Do not seek if no frames exist |
| if (frames.length === 0) |
| return; |
| |
| // Abort active seek operation, if any |
| recording.cancel(); |
| |
| // Pause playback, preserving playback state |
| var originallyPlaying = recording.isPlaying(); |
| recording.pause(); |
| |
| // Restore playback when seek is completed or cancelled |
| seekCallback = function restorePlaybackState() { |
| |
| // Seek is no longer in progress |
| seekCallback = null; |
| |
| // Restore playback state |
| 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(); |
| } |
| }; |
| |
| /** |
| * Pauses playback of the recording, if playback is currently in progress. |
| * If playback is not in progress, this function has no effect. If a seek |
| * operation is in progress, the seek is aborted. Playback is initially |
| * paused when a Guacamole.SessionRecording is created, and must be |
| * explicitly started through a call to play(). |
| */ |
| this.pause = function pause() { |
| |
| // Abort any in-progress seek / playback |
| abortSeek(); |
| |
| // Stop playback only if playback is in progress |
| if (recording.isPlaying()) { |
| |
| // Notify that playback is stopping |
| if (recording.onpause) |
| recording.onpause(); |
| |
| // Playback is stopped |
| startVideoTimestamp = null; |
| startRealTimestamp = null; |
| |
| } |
| |
| }; |
| |
| }; |
| |
| /** |
| * A single frame of Guacamole session data. Each frame is made up of the set |
| * of instructions used to generate that frame, and the timestamp as dictated |
| * by the "sync" instruction terminating the frame. Optionally, a frame may |
| * also be associated with a snapshot of Guacamole client state, such that the |
| * frame can be rendered without replaying all previous frames. |
| * |
| * @private |
| * @constructor |
| * @param {!number} timestamp |
| * The timestamp of this frame, as dictated by the "sync" instruction which |
| * terminates the frame. |
| * |
| * @param {!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, start, end) { |
| |
| /** |
| * Whether this frame should be used as a keyframe if possible. This value |
| * is purely advisory. The stored clientState must eventually be manually |
| * set for the frame to be used as a keyframe. By default, frames are not |
| * keyframes. |
| * |
| * @type {!boolean} |
| * @default false |
| */ |
| this.keyframe = false; |
| |
| /** |
| * The timestamp of this frame, as dictated by the "sync" instruction which |
| * terminates the frame. |
| * |
| * @type {!number} |
| */ |
| this.timestamp = timestamp; |
| |
| /** |
| * The byte offset within the blob of the first character of the first |
| * instruction of this frame. |
| * |
| * @type {!number} |
| */ |
| 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 call to exportState(), serialized as JSON, and stored within a Blob. |
| * Use of Blobs here is required to ensure the browser can make use of |
| * larger disk-backed storage if the size of the recording is large. If no |
| * such snapshot has been taken, this will be null. |
| * |
| * @type {Blob} |
| * @default null |
| */ |
| this.clientState = null; |
| |
| }; |
| |
| /** |
| * A read-only Guacamole.Tunnel implementation which streams instructions |
| * received through explicit calls to its receiveInstruction() function. |
| * |
| * @private |
| * @constructor |
| * @augments {Guacamole.Tunnel} |
| */ |
| Guacamole.SessionRecording._PlaybackTunnel = function _PlaybackTunnel() { |
| |
| /** |
| * Reference to this Guacamole.SessionRecording._PlaybackTunnel. |
| * |
| * @private |
| * @type {!Guacamole.SessionRecording._PlaybackTunnel} |
| */ |
| var tunnel = this; |
| |
| this.connect = function connect(data) { |
| // Do nothing |
| }; |
| |
| this.sendMessage = function sendMessage(elements) { |
| // Do nothing |
| }; |
| |
| this.disconnect = function disconnect() { |
| // Do nothing |
| }; |
| |
| /** |
| * Invokes this tunnel's oninstruction handler, notifying users of this |
| * tunnel (such as a Guacamole.Client instance) that an instruction has |
| * been received. If the oninstruction handler has not been set, this |
| * function has no effect. |
| * |
| * @param {!string} opcode |
| * The opcode of the Guacamole instruction. |
| * |
| * @param {!string[]} args |
| * All arguments associated with this Guacamole instruction. |
| */ |
| this.receiveInstruction = function receiveInstruction(opcode, args) { |
| if (tunnel.oninstruction) |
| tunnel.oninstruction(opcode, args); |
| }; |
| |
| }; |