From 9d5e1111a674ba0df66e6839488b1c5a261230f0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 15 Apr 2017 16:04:12 -0700 Subject: [PATCH] GUACAMOLE-250: Automatically store keyframes while recordings are being played. --- .../main/webapp/modules/SessionRecording.js | 100 +++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js index 5922f1726..aa9e33505 100644 --- a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js +++ b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js @@ -41,6 +41,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. * @@ -58,6 +77,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; + }); + } + }; /** @@ -471,6 +527,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; @@ -512,6 +580,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. * @@ -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; + + }; + }; /**