guacamole-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jmuehl...@apache.org
Subject [1/5] incubator-guacamole-client git commit: GUACAMOLE-250: Implement Guacamole.SessionRecording having basic play/pause support.
Date Fri, 21 Apr 2017 05:28:45 GMT
Repository: incubator-guacamole-client
Updated Branches:
  refs/heads/master c7756c4fd -> 79c66b00f


GUACAMOLE-250: Implement Guacamole.SessionRecording having basic play/pause support.


Project: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/commit/052bb9b3
Tree: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/tree/052bb9b3
Diff: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/diff/052bb9b3

Branch: refs/heads/master
Commit: 052bb9b36b4db84fdcb0eeb6f81ce34b8e1cb64b
Parents: e8f4cd5
Author: Michael Jumper <mjumper@apache.org>
Authored: Wed Apr 12 23:59:42 2017 -0700
Committer: Michael Jumper <mjumper@apache.org>
Committed: Sat Apr 15 17:12:23 2017 -0700

----------------------------------------------------------------------
 .../src/main/webapp/modules/SessionRecording.js | 573 +++++++++++++++++++
 1 file changed, 573 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/052bb9b3/guacamole-common-js/src/main/webapp/modules/SessionRecording.js
----------------------------------------------------------------------
diff --git a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js
new file mode 100644
index 0000000..34f8aca
--- /dev/null
+++ b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js
@@ -0,0 +1,573 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+var Guacamole = Guacamole || {};
+
+/**
+ * A recording of a Guacamole session. Given a {@link Guacamole.Tunnel}, the
+ * Guacamole.SessionRecording automatically handles incoming Guacamole
+ * instructions, storing them for playback. Playback of the recording may be
+ * controlled through function calls to the Guacamole.SessionRecording, even
+ * while the recording has not yet finished being created or downloaded.
+ *
+ * @constructor
+ * @param {Guacamole.Tunnel} tunnel
+ *     The Guacamole.Tunnel from which the instructions of the recording should
+ *     be read.
+ */
+Guacamole.SessionRecording = function SessionRecording(tunnel) {
+
+    /**
+     * Reference to this Guacamole.SessionRecording.
+     *
+     * @private
+     * @type {Guacamole.SessionRecording}
+     */
+    var recording = this;
+
+    /**
+     * All frames parsed from the provided tunnel.
+     *
+     * @private
+     * @type {Guacamole.SessionRecording._Frame[]}
+     */
+    var frames = [];
+
+    /**
+     * All instructions which have been read since the last frame was added to
+     * the frames array.
+     *
+     * @private
+     * @type {Guacamole.SessionRecording._Frame.Instruction[]}
+     */
+    var instructions = [];
+
+    /**
+     * Tunnel which feeds arbitrary instructions to the client used by this
+     * Guacamole.SessionRecording for playback of the session recording.
+     *
+     * @private
+     * @type {Guacamole.SessionRecording._PlaybackTunnel}
+     */
+    var playbackTunnel = new Guacamole.SessionRecording._PlaybackTunnel();
+
+    /**
+     * Guacamole.Client instance used for visible playback of the session
+     * recording.
+     *
+     * @private
+     * @type {Guacamole.Client}
+     */
+    var playbackClient = new Guacamole.Client(playbackTunnel);
+
+    /**
+     * The current frame rendered within the playback client. If no frame is
+     * yet rendered, this will be -1.
+     *
+     * @private
+     * @type {Number}
+     */
+    var currentFrame = -1;
+
+    /**
+     * The timestamp of the frame when playback began, in milliseconds. If
+     * playback is not in progress, this will be null.
+     *
+     * @private
+     * @type {Number}
+     */
+    var startVideoTimestamp = null;
+
+    /**
+     * The real-world timestamp when playback began, in milliseconds. If
+     * playback is not in progress, this will be null.
+     *
+     * @private
+     * @type {Number}
+     */
+    var startRealTimestamp = null;
+
+    /**
+     * The ID of the timeout which will play the next frame, if playback is in
+     * progress. If playback is not in progress, the ID stored here (if any)
+     * will not be valid.
+     *
+     * @private
+     * @type {Number}
+     */
+    var playbackTimeout = null;
+
+    // Start playback client connected
+    playbackClient.connect();
+
+    // Hide cursor unless mouse position is received
+    playbackClient.getDisplay().showCursor(false);
+
+    // Read instructions from provided tunnel, extracting each frame
+    tunnel.oninstruction = function handleInstruction(opcode, args) {
+
+        // Store opcode and arguments for received instruction
+        instructions.push(new Guacamole.SessionRecording._Frame.Instruction(opcode, args.slice()));
+
+        // Once a sync is received, store all instructions since the last
+        // frame as a new frame
+        if (opcode === 'sync') {
+
+            // Parse frame timestamp from sync instruction
+            var timestamp = parseInt(args[0]);
+
+            // Add a new frame containing the instructions read since last frame
+            var frame = new Guacamole.SessionRecording._Frame(timestamp, instructions);
+            frames.push(frame);
+
+            // Clear set of instructions in preparation for next frame
+            instructions = [];
+
+            // Notify that additional content is available
+            if (recording.onprogress)
+                recording.onprogress(recording.getDuration());
+
+        }
+
+    };
+
+    /**
+     * Converts the given absolute timestamp to a timestamp which is relative
+     * to the first frame in the recording.
+     *
+     * @private
+     * @param {Number} timestamp
+     *     The timestamp to convert to a relative timestamp.
+     *
+     * @returns {Number}
+     *     The difference in milliseconds between the given timestamp and the
+     *     first frame of the recording, or zero if no frames yet exist.
+     */
+    var toRelativeTimestamp = function toRelativeTimestamp(timestamp) {
+
+        // If no frames yet exist, all timestamps are zero
+        if (frames.length === 0)
+            return 0;
+
+        // Calculate timestamp relative to first frame
+        return timestamp - frames[0].timestamp;
+
+    };
+
+    /**
+     * Replays the instructions associated with the given frame, sending those
+     * instructions to the playback client.
+     *
+     * @private
+     * @param {Number} index
+     *     The index of the frame within the frames array which should be
+     *     replayed.
+     */
+    var replayFrame = function replayFrame(index) {
+
+        var frame = frames[index];
+
+        // Replay all instructions within the retrieved frame
+        for (var i = 0; i < frame.instructions.length; i++) {
+            var instruction = frame.instructions[i];
+            playbackTunnel.receiveInstruction(instruction.opcode, instruction.args);
+        }
+
+    };
+
+    /**
+     * Moves the playback position to the given frame, resetting the state of
+     * the playback client and replaying frames as necessary.
+     *
+     * @private
+     * @param {Number} index
+     *     The index of the frame which should become the new playback
+     *     position.
+     */
+    var seekToFrame = function seekToFrame(index) {
+
+        var startIndex;
+
+        // Back up until startIndex represents current state
+        for (startIndex = index; startIndex > currentFrame; startIndex--) {
+
+            var frame = frames[startIndex];
+
+            // If frame has associated absolute state, make that frame the
+            // current state
+            if (frame.clientState) {
+                playbackClient.importState(frame.clientState);
+                break;
+            }
+
+        }
+
+        // Advance to frame index after current state
+        startIndex++;
+
+        // Replay any applicable incremental frames
+        for (; startIndex <= index; startIndex++)
+            replayFrame(startIndex);
+
+        // Current frame is now at requested index
+        currentFrame = index;
+
+        // Notify of changes in position
+        if (recording.onseek)
+            recording.onseek(recording.getPosition());
+
+    };
+
+    /**
+     * 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() {
+
+        // Advance to next frame
+        seekToFrame(currentFrame + 1);
+
+        // 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;
+
+            // Calculate the relative delay between the current time and
+            // the next frame start
+            var delay = Math.max(nextRealTimestamp - new Date().getTime(), 0);
+
+            // Advance to next frame after enough time has elapsed
+            playbackTimeout = window.setTimeout(function frameDelayElapsed() {
+                continuePlayback();
+            }, delay);
+
+        }
+
+        // Otherwise stop playback
+        else
+            recording.pause();
+
+    };
+
+    /**
+     * Fired when new frames have become available while the recording is
+     * being downloaded.
+     *
+     * @event
+     * @param {Number} duration
+     *     The new duration of the recording, in milliseconds.
+     */
+    this.onprogress = null;
+
+    /**
+     * Fired whenever playback of the recording has started.
+     *
+     * @event
+     */
+    this.onplay = null;
+
+    /**
+     * Fired whenever playback of the recording has been paused. This may
+     * happen when playback is explicitly paused with a call to pause(), or
+     * when playback is implicitly paused due to reaching the end of the
+     * recording.
+     *
+     * @event
+     */
+    this.onpause = null;
+
+    /**
+     * Fired whenever the playback position within the recording changes.
+     *
+     * @event
+     * @param {Number} position
+     *     The new position within the recording, in milliseconds.
+     */
+    this.onseek = null;
+
+    /**
+     * Connects the underlying tunnel, beginning download of the Guacamole
+     * session. Playback of the Guacamole session cannot occur until at least
+     * one frame worth of instructions has been downloaded.
+     *
+     * @param {String} data
+     *     The data to send to the tunnel when connecting.
+     */
+    this.connect = function connect(data) {
+        tunnel.connect(data);
+    };
+
+    /**
+     * Disconnects the underlying tunnel, stopping further download of the
+     * Guacamole session.
+     */
+    this.disconnect = function disconnect() {
+        tunnel.disconnect();
+    };
+
+    /**
+     * Returns the underlying display of the Guacamole.Client used by this
+     * Guacamole.SessionRecording for playback. The display contains an Element
+     * which can be added to the DOM, causing the display (and thus playback of
+     * the recording) to become visible.
+     *
+     * @return {Guacamole.Display}
+     *     The underlying display of the Guacamole.Client used by this
+     *     Guacamole.SessionRecording for playback.
+     */
+    this.getDisplay = function getDisplay() {
+        return playbackClient.getDisplay();
+    };
+
+    /**
+     * Returns whether playback is currently in progress.
+     *
+     * @returns {Boolean}
+     *     true if playback is currently in progress, false otherwise.
+     */
+    this.isPlaying = function isPlaying() {
+        return !!startVideoTimestamp;
+    };
+
+    /**
+     * Returns the current playback position within the recording, in
+     * milliseconds, where zero is the start of the recording.
+     *
+     * @returns {Number}
+     *     The current playback position within the recording, in milliseconds.
+     */
+    this.getPosition = function getPosition() {
+
+        // Position is simply zero if playback has not started at all
+        if (currentFrame === -1)
+            return 0;
+
+        // Return current position as a millisecond timestamp relative to the
+        // start of the recording
+        return toRelativeTimestamp(frames[currentFrame].timestamp);
+
+    };
+
+    /**
+     * Returns the duration of this recording, in milliseconds. If the
+     * recording is still being downloaded, this value will gradually increase.
+     *
+     * @returns {Number}
+     *     The duration of this recording, in milliseconds.
+     */
+    this.getDuration = function getDuration() {
+
+        // If no frames yet exist, duration is zero
+        if (frames.length === 0)
+            return 0;
+
+        // Recording duration is simply the timestamp of the last frame
+        return toRelativeTimestamp(frames[frames.length - 1].timestamp);
+
+    };
+
+    /**
+     * Begins continuous playback of the recording downloaded thus far.
+     * Playback of the recording will continue until pause() is invoked or
+     * until no further frames exist. Playback is initially paused when a
+     * Guacamole.SessionRecording is created, and must be explicitly started
+     * through a call to this function. If playback is already in progress,
+     * this function has no effect.
+     */
+    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();
+
+        }
+
+    };
+
+    /**
+     * Pauses playback of the recording, if playback is currently in progress.
+     * If playback is not in progress, this function has no effect. Playback is
+     * initially paused when a Guacamole.SessionRecording is created, and must
+     * be explicitly started through a call to play().
+     */
+    this.pause = function pause() {
+
+        // Stop playback only if playback is in progress
+        if (recording.isPlaying()) {
+
+            // Notify that playback is stopping
+            if (recording.onpause)
+                recording.onpause();
+
+            // Stop playback
+            window.clearTimeout(playbackTimeout);
+            startVideoTimestamp = null;
+            startRealTimestamp = null;
+
+        }
+
+    };
+
+};
+
+/**
+ * A single frame of Guacamole session data. Each frame is made up of the set
+ * of instructions used to generate that frame, and the timestamp as dictated
+ * by the "sync" instruction terminating the frame. Optionally, a frame may
+ * also be associated with a snapshot of Guacamole client state, such that the
+ * frame can be rendered without replaying all previous frames.
+ *
+ * @private
+ * @constructor
+ * @param {Number} timestamp
+ *     The timestamp of this frame, as dictated by the "sync" instruction which
+ *     terminates the frame.
+ *
+ * @param {Guacamole.SessionRecording._Frame.Instruction[]} instructions
+ *     All instructions which are necessary to generate this frame relative to
+ *     the previous frame in the Guacamole session.
+ */
+Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
+
+    /**
+     * The timestamp of this frame, as dictated by the "sync" instruction which
+     * terminates the frame.
+     *
+     * @type {Number}
+     */
+    this.timestamp = timestamp;
+
+    /**
+     * All instructions which are necessary to generate this frame relative to
+     * the previous frame in the Guacamole session.
+     *
+     * @type {Guacamole.SessionRecording._Frame.Instruction[]}
+     */
+    this.instructions = instructions;
+
+    /**
+     * A snapshot of client state after this frame was rendered, as returned by
+     * a call to exportState(). If no such snapshot has been taken, this will
+     * be null.
+     *
+     * @type {Object}
+     */
+    this.clientState = null;
+
+};
+
+/**
+ * A Guacamole protocol instruction. Each Guacamole protocol instruction is
+ * made up of an opcode and set of arguments.
+ *
+ * @private
+ * @constructor
+ * @param {String} opcode
+ *     The opcode of this Guacamole instruction.
+ *
+ * @param {String[]} args
+ *     All arguments associated with this Guacamole instruction.
+ */
+Guacamole.SessionRecording._Frame.Instruction = function Instruction(opcode, args) {
+
+    /**
+     * The opcode of this Guacamole instruction.
+     *
+     * @type {String}
+     */
+    this.opcode = opcode;
+
+    /**
+     * All arguments associated with this Guacamole instruction.
+     *
+     * @type {String[]}
+     */
+    this.args = args;
+
+};
+
+/**
+ * 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);
+    };
+
+};


Mime
View raw message