You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@guacamole.apache.org by jm...@apache.org on 2017/04/21 05:28:45 UTC
[1/5] incubator-guacamole-client git commit: GUACAMOLE-250: Implement
Guacamole.SessionRecording having basic play/pause support.
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 <mj...@apache.org>
Authored: Wed Apr 12 23:59:42 2017 -0700
Committer: Michael Jumper <mj...@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);
+ };
+
+};
[3/5] incubator-guacamole-client git commit: GUACAMOLE-250: Fix
backward seeking - must retreat potentially all the way to frame 0 in search
of an absolute state.
Posted by jm...@apache.org.
GUACAMOLE-250: Fix backward seeking - must retreat potentially all the way to frame 0 in search of an absolute state.
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/1fcb5f2d
Tree: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/tree/1fcb5f2d
Diff: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/diff/1fcb5f2d
Branch: refs/heads/master
Commit: 1fcb5f2d197e47478621b298dbab661708c958a6
Parents: 052bb9b
Author: Michael Jumper <mj...@apache.org>
Authored: Sat Apr 15 16:29:12 2017 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Sat Apr 15 17:12:29 2017 -0700
----------------------------------------------------------------------
.../src/main/webapp/modules/SessionRecording.js | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/1fcb5f2d/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
index 34f8aca..5922f17 100644
--- a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js
+++ b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js
@@ -205,10 +205,15 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
var startIndex;
// Back up until startIndex represents current state
- for (startIndex = index; startIndex > currentFrame; startIndex--) {
+ for (startIndex = index; startIndex >= 0; startIndex--) {
var frame = frames[startIndex];
+ // If we've reached the current frame, startIndex represents
+ // current state by definition
+ if (startIndex === currentFrame)
+ break;
+
// If frame has associated absolute state, make that frame the
// current state
if (frame.clientState) {
[4/5] incubator-guacamole-client git commit: GUACAMOLE-250: Add
support for seeking within session recordings.
Posted by jm...@apache.org.
GUACAMOLE-250: Add support for seeking within session recordings.
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/4f9469d9
Tree: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/tree/4f9469d9
Diff: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/diff/4f9469d9
Branch: refs/heads/master
Commit: 4f9469d901a5f845c5d22827c28369f31c84a8eb
Parents: 9d5e111
Author: Michael Jumper <mj...@apache.org>
Authored: Sat Apr 15 17:11:32 2017 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Sat Apr 15 17:12:29 2017 -0700
----------------------------------------------------------------------
.../src/main/webapp/modules/SessionRecording.js | 73 ++++++++++++++++++++
1 file changed, 73 insertions(+)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/4f9469d9/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
index aa9e335..139597b 100644
--- a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js
+++ b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js
@@ -220,6 +220,51 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
};
/**
+ * 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.
*
@@ -483,6 +528,34 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
};
/**
+ * 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.
+ *
+ * @param {Number} position
+ * The position within the recording to seek to, in milliseconds.
+ */
+ this.seek = function seek(position) {
+
+ // Do not seek if no frames exist
+ if (frames.length === 0)
+ return;
+
+ // Pause playback, preserving playback state
+ var originallyPlaying = recording.isPlaying();
+ recording.pause();
+
+ // Perform seek
+ seekToFrame(findFrame(0, frames.length - 1, position));
+
+ // Restore playback state
+ if (originallyPlaying)
+ recording.play();
+
+ };
+
+ /**
* 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
[5/5] incubator-guacamole-client git commit: GUACAMOLE-250: Merge new
SessionRecording for recording playback.
Posted by jm...@apache.org.
GUACAMOLE-250: Merge new SessionRecording for recording playback.
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/79c66b00
Tree: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/tree/79c66b00
Diff: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/diff/79c66b00
Branch: refs/heads/master
Commit: 79c66b00f16cc4cc2e4b79f475d5aaed9289a54f
Parents: c7756c4 4f9469d
Author: James Muehlner <ja...@guac-dev.org>
Authored: Thu Apr 20 22:26:55 2017 -0700
Committer: James Muehlner <ja...@guac-dev.org>
Committed: Thu Apr 20 22:26:55 2017 -0700
----------------------------------------------------------------------
.../src/main/webapp/modules/SessionRecording.js | 749 +++++++++++++++++++
1 file changed, 749 insertions(+)
----------------------------------------------------------------------
[2/5] incubator-guacamole-client git commit: GUACAMOLE-250:
Automatically store keyframes while recordings are being played.
Posted by jm...@apache.org.
GUACAMOLE-250: Automatically store keyframes while recordings are being played.
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/9d5e1111
Tree: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/tree/9d5e1111
Diff: http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/diff/9d5e1111
Branch: refs/heads/master
Commit: 9d5e1111a674ba0df66e6839488b1c5a261230f0
Parents: 1fcb5f2
Author: Michael Jumper <mj...@apache.org>
Authored: Sat Apr 15 16:04:12 2017 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Sat Apr 15 17:12:29 2017 -0700
----------------------------------------------------------------------
.../src/main/webapp/modules/SessionRecording.js | 100 ++++++++++++++++++-
1 file changed, 99 insertions(+), 1 deletion(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-guacamole-client/blob/9d5e1111/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
index 5922f17..aa9e335 100644
--- a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js
+++ b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js
@@ -42,6 +42,25 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
var recording = this;
/**
+ * The minimum number of characters which must have been read between
+ * keyframes.
+ *
+ * @private
+ * @constant
+ * @type {Number}
+ */
+ var KEYFRAME_CHAR_INTERVAL = 16384;
+
+ /**
+ * The minimum number of milliseconds which must elapse between keyframes.
+ *
+ * @private
+ * @constant
+ * @type {Number}
+ */
+ var KEYFRAME_TIME_INTERVAL = 5000;
+
+ /**
* All frames parsed from the provided tunnel.
*
* @private
@@ -59,6 +78,24 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
var instructions = [];
/**
+ * The approximate number of characters which have been read from the
+ * provided tunnel since the last frame was flagged for use as a keyframe.
+ *
+ * @private
+ * @type {Number}
+ */
+ var charactersSinceLastKeyframe = 0;
+
+ /**
+ * The timestamp of the last frame which was flagged for use as a keyframe.
+ * If no timestamp has yet been flagged, this will be 0.
+ *
+ * @private
+ * @type {Number}
+ */
+ var lastKeyframeTimestamp = 0;
+
+ /**
* Tunnel which feeds arbitrary instructions to the client used by this
* Guacamole.SessionRecording for playback of the session recording.
*
@@ -123,7 +160,9 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
tunnel.oninstruction = function handleInstruction(opcode, args) {
// Store opcode and arguments for received instruction
- instructions.push(new Guacamole.SessionRecording._Frame.Instruction(opcode, args.slice()));
+ var instruction = new Guacamole.SessionRecording._Frame.Instruction(opcode, args.slice());
+ instructions.push(instruction);
+ charactersSinceLastKeyframe += instruction.getSize();
// Once a sync is received, store all instructions since the last
// frame as a new frame
@@ -136,6 +175,16 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
var frame = new Guacamole.SessionRecording._Frame(timestamp, instructions);
frames.push(frame);
+ // This frame should eventually become a keyframe if enough data
+ // has been processed and enough recording time has elapsed, or if
+ // this is the absolute first frame
+ if (frames.length === 1 || (charactersSinceLastKeyframe >= KEYFRAME_CHAR_INTERVAL
+ && timestamp - lastKeyframeTimestamp >= KEYFRAME_TIME_INTERVAL)) {
+ frame.keyframe = true;
+ lastKeyframeTimestamp = timestamp;
+ charactersSinceLastKeyframe = 0;
+ }
+
// Clear set of instructions in preparation for next frame
instructions = [];
@@ -189,6 +238,13 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
playbackTunnel.receiveInstruction(instruction.opcode, instruction.args);
}
+ // Store client state if frame is flagged as a keyframe
+ if (frame.keyframe && !frame.clientState) {
+ playbackClient.exportState(function storeClientState(state) {
+ frame.clientState = state;
+ });
+ }
+
};
/**
@@ -472,6 +528,17 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
/**
+ * Whether this frame should be used as a keyframe if possible. This value
+ * is purely advisory. The stored clientState must eventually be manually
+ * set for the frame to be used as a keyframe. By default, frames are not
+ * keyframes.
+ *
+ * @type {Boolean}
+ * @default false
+ */
+ this.keyframe = false;
+
+ /**
* The timestamp of this frame, as dictated by the "sync" instruction which
* terminates the frame.
*
@@ -493,6 +560,7 @@ Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
* be null.
*
* @type {Object}
+ * @default null
*/
this.clientState = null;
@@ -513,6 +581,14 @@ Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
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}
@@ -526,6 +602,28 @@ Guacamole.SessionRecording._Frame.Instruction = function Instruction(opcode, arg
*/
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;
+
+ };
+
};
/**