From 1d6e8d2216adbebc1c6377bf774a23a6762c877f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 8 Jun 2017 11:20:10 -0700 Subject: [PATCH] GUACAMOLE-346: Avoid blocking the main thread when seeking within a session recording. --- .../main/webapp/modules/SessionRecording.js | 110 +++++++++++++++--- 1 file changed, 96 insertions(+), 14 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js index 139597b1d..74eeb0bdd 100644 --- a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js +++ b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js @@ -60,6 +60,17 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { */ 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. * @@ -150,6 +161,16 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { */ 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 playbackClient.connect(); @@ -294,17 +315,27 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { /** * 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 * @param {Number} index * The index of the frame which should become the new playback * 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; + // Abort any in-progress seek + abortSeek(); + // Back up until startIndex represents current state for (startIndex = index; startIndex >= 0; startIndex--) { @@ -327,17 +358,50 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { // Advance to frame index after current state startIndex++; + var startTime = new Date().getTime(); + // 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); + } // Current frame is now at requested index - currentFrame = index; + currentFrame = startIndex - 1; // Notify of changes in position if (recording.onseek) 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 * 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 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() { @@ -531,12 +597,18 @@ 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. + * 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 * 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 if (frames.length === 0) @@ -547,22 +619,32 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) { recording.pause(); // Perform seek - seekToFrame(findFrame(0, frames.length - 1, position)); + seekToFrame(findFrame(0, frames.length - 1, position), function restorePlaybackState() { - // Restore playback state - if (originallyPlaying) - recording.play(); + // Restore playback state + if (originallyPlaying) + recording.play(); + + // Notify that seek has completed + if (callback) + callback(); + + }); }; /** * 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(). + * If playback is not in progress, this function has no effect. If a seek + * operation is in progress, the seek is aborted. Playback is initially + * paused when a Guacamole.SessionRecording is created, and must be + * explicitly started through a call to play(). */ this.pause = function pause() { + // Abort any in-progress seek + abortSeek(); + // Stop playback only if playback is in progress if (recording.isPlaying()) {