GUACAMOLE-346: Avoid blocking the main thread when seeking within a session recording.

This commit is contained in:
Michael Jumper
2017-06-08 11:20:10 -07:00
parent 33e76c4d72
commit 1d6e8d2216

View File

@@ -60,6 +60,17 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
*/ */
var KEYFRAME_TIME_INTERVAL = 5000; var KEYFRAME_TIME_INTERVAL = 5000;
/**
* The maximum amount of time to spend in any particular seek operation
* before returning control to the main thread, in milliseconds. Seek
* operations exceeding this amount of time will proceed asynchronously.
*
* @private
* @constant
* @type {Number}
*/
var MAXIMUM_SEEK_TIME = 5;
/** /**
* All frames parsed from the provided tunnel. * All frames parsed from the provided tunnel.
* *
@@ -150,6 +161,16 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
*/ */
var playbackTimeout = null; var playbackTimeout = null;
/**
* The ID of the timeout which will continue the in-progress seek
* operation. If no seek operation is in progress, the ID stored here (if
* any) will not be valid.
*
* @private
* @type {Number}
*/
var seekTimeout = null;
// Start playback client connected // Start playback client connected
playbackClient.connect(); playbackClient.connect();
@@ -294,17 +315,27 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
/** /**
* Moves the playback position to the given frame, resetting the state of * Moves the playback position to the given frame, resetting the state of
* the playback client and replaying frames as necessary. * the playback client and replaying frames as necessary. If the seek
* cannot be completed quickly, the seek operation may 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 * @private
* @param {Number} index * @param {Number} index
* The index of the frame which should become the new playback * The index of the frame which should become the new playback
* position. * position.
*
* @param {function} [callback]
* The callback to invoke once the seek operation has completed.
*/ */
var seekToFrame = function seekToFrame(index) { var seekToFrame = function seekToFrame(index, callback) {
var startIndex; var startIndex;
// Abort any in-progress seek
abortSeek();
// Back up until startIndex represents current state // Back up until startIndex represents current state
for (startIndex = index; startIndex >= 0; startIndex--) { for (startIndex = index; startIndex >= 0; startIndex--) {
@@ -327,17 +358,50 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
// Advance to frame index after current state // Advance to frame index after current state
startIndex++; startIndex++;
var startTime = new Date().getTime();
// Replay any applicable incremental frames // Replay any applicable incremental frames
for (; startIndex <= index; startIndex++) for (; startIndex <= index; startIndex++) {
// Stop seeking if the operation is taking too long
var currentTime = new Date().getTime();
if (currentTime - startTime >= MAXIMUM_SEEK_TIME)
break;
replayFrame(startIndex); replayFrame(startIndex);
}
// Current frame is now at requested index // Current frame is now at requested index
currentFrame = index; currentFrame = startIndex - 1;
// Notify of changes in position // Notify of changes in position
if (recording.onseek) if (recording.onseek)
recording.onseek(recording.getPosition()); recording.onseek(recording.getPosition());
// If the seek operation has not yet completed, schedule continuation
if (currentFrame !== index)
seekTimeout = window.setTimeout(function continueSeek() {
seekToFrame(index, callback);
}, 0);
else {
// Notify that the requested seek has completed
if (callback)
callback();
}
};
/**
* 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() {
window.clearTimeout(seekTimeout);
}; };
/** /**
@@ -502,7 +566,9 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
* until no further frames exist. Playback is initially paused when a * until no further frames exist. Playback is initially paused when a
* Guacamole.SessionRecording is created, and must be explicitly started * Guacamole.SessionRecording is created, and must be explicitly started
* through a call to this function. If playback is already in progress, * through a call to this function. If playback is already in progress,
* this function has no effect. * 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() { this.play = function play() {
@@ -531,12 +597,18 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
* Seeks to the given position within the recording. If the recording is * Seeks to the given position within the recording. If the recording is
* currently being played back, playback will continue after the seek is * currently being played back, playback will continue after the seek is
* performed. If the recording is currently paused, playback will be * performed. If the recording is currently paused, playback will be
* paused after the seek is performed. * paused after the seek is performed. If a seek operation is already in
* progress, that seek is first aborted. Depending on how much processing
* the seek operation requires, the seek operation may proceed
* asynchronously.
* *
* @param {Number} position * @param {Number} position
* The position within the recording to seek to, in milliseconds. * 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) { this.seek = function seek(position, callback) {
// Do not seek if no frames exist // Do not seek if no frames exist
if (frames.length === 0) if (frames.length === 0)
@@ -547,22 +619,32 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
recording.pause(); recording.pause();
// Perform seek // Perform seek
seekToFrame(findFrame(0, frames.length - 1, position)); seekToFrame(findFrame(0, frames.length - 1, position), function restorePlaybackState() {
// Restore playback state // Restore playback state
if (originallyPlaying) if (originallyPlaying)
recording.play(); recording.play();
// Notify that seek has completed
if (callback)
callback();
});
}; };
/** /**
* Pauses playback of the recording, if playback is currently in progress. * Pauses playback of the recording, if playback is currently in progress.
* If playback is not in progress, this function has no effect. Playback is * If playback is not in progress, this function has no effect. If a seek
* initially paused when a Guacamole.SessionRecording is created, and must * operation is in progress, the seek is aborted. Playback is initially
* be explicitly started through a call to play(). * paused when a Guacamole.SessionRecording is created, and must be
* explicitly started through a call to play().
*/ */
this.pause = function pause() { this.pause = function pause() {
// Abort any in-progress seek
abortSeek();
// Stop playback only if playback is in progress // Stop playback only if playback is in progress
if (recording.isPlaying()) { if (recording.isPlaying()) {