GUACAMOLE-250: Automatically store keyframes while recordings are being played.

This commit is contained in:
Michael Jumper
2017-04-15 16:04:12 -07:00
parent 1fcb5f2d19
commit 9d5e1111a6

View File

@@ -41,6 +41,25 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
*/ */
var recording = this; 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. * All frames parsed from the provided tunnel.
* *
@@ -58,6 +77,24 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
*/ */
var instructions = []; 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 * Tunnel which feeds arbitrary instructions to the client used by this
* Guacamole.SessionRecording for playback of the session recording. * Guacamole.SessionRecording for playback of the session recording.
@@ -123,7 +160,9 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
tunnel.oninstruction = function handleInstruction(opcode, args) { tunnel.oninstruction = function handleInstruction(opcode, args) {
// Store opcode and arguments for received instruction // 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 // Once a sync is received, store all instructions since the last
// frame as a new frame // frame as a new frame
@@ -136,6 +175,16 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
var frame = new Guacamole.SessionRecording._Frame(timestamp, instructions); var frame = new Guacamole.SessionRecording._Frame(timestamp, instructions);
frames.push(frame); 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 // Clear set of instructions in preparation for next frame
instructions = []; instructions = [];
@@ -189,6 +238,13 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
playbackTunnel.receiveInstruction(instruction.opcode, instruction.args); 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) { 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 * The timestamp of this frame, as dictated by the "sync" instruction which
* terminates the frame. * terminates the frame.
@@ -493,6 +560,7 @@ Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
* be null. * be null.
* *
* @type {Object} * @type {Object}
* @default null
*/ */
this.clientState = null; this.clientState = null;
@@ -512,6 +580,14 @@ Guacamole.SessionRecording._Frame = function _Frame(timestamp, instructions) {
*/ */
Guacamole.SessionRecording._Frame.Instruction = function Instruction(opcode, args) { 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. * The opcode of this Guacamole instruction.
* *
@@ -526,6 +602,28 @@ Guacamole.SessionRecording._Frame.Instruction = function Instruction(opcode, arg
*/ */
this.args = args; 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;
};
}; };
/** /**