From a27bd2694abc52a97c8b45fd30ea2fce3de2693f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 2 Mar 2022 17:31:53 +0000 Subject: [PATCH 1/5] GUACAMOLE-462: Allow pending display frames to be cancelled. --- .../src/main/webapp/modules/Display.js | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/guacamole-common-js/src/main/webapp/modules/Display.js b/guacamole-common-js/src/main/webapp/modules/Display.js index 341480777..8baa6b7d7 100644 --- a/guacamole-common-js/src/main/webapp/modules/Display.js +++ b/guacamole-common-js/src/main/webapp/modules/Display.js @@ -201,6 +201,22 @@ Guacamole.Display = function() { */ function Frame(callback, tasks) { + /** + * Cancels rendering of this frame and all associated tasks. The + * callback provided at construction time, if any, is not invoked. + */ + this.cancel = function cancel() { + + callback = null; + + tasks.forEach(function cancelTask(task) { + task.cancel(); + }); + + tasks = []; + + }; + /** * Returns whether this frame is ready to be rendered. This function * returns true if and only if ALL underlying tasks are unblocked. @@ -271,6 +287,16 @@ Guacamole.Display = function() { */ this.blocked = blocked; + /** + * Cancels this task such that it will not run. The task handler + * provided at construction time, if any, is not invoked. Calling + * execute() after calling this function has no effect. + */ + this.cancel = function cancel() { + task.blocked = false; + taskHandler = null; + }; + /** * Unblocks this Task, allowing it to run. */ @@ -417,6 +443,27 @@ Guacamole.Display = function() { }; + /** + * Cancels rendering of all pending frames and associated rendering + * operations. The callbacks provided to outstanding past calls to flush(), + * if any, are not invoked. + */ + this.cancel = function cancel() { + + frames.forEach(function cancelFrame(frame) { + frame.cancel(); + }); + + frames = []; + + tasks.forEach(function cancelTask(task) { + task.cancel(); + }); + + tasks = []; + + }; + /** * Sets the hotspot and image of the mouse cursor displayed within the * Guacamole display. From d4899f102fc4a5bd60e98de090d2bb61b3aac337 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 2 Mar 2022 17:32:58 +0000 Subject: [PATCH 2/5] GUACAMOLE-462: Clear out pending display operations and fully reset when importing state. --- guacamole-common-js/src/main/webapp/modules/Client.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js index 866714162..83d89abaa 100644 --- a/guacamole-common-js/src/main/webapp/modules/Client.js +++ b/guacamole-common-js/src/main/webapp/modules/Client.js @@ -228,11 +228,14 @@ Guacamole.Client = function(tunnel) { currentState = state.currentState; currentTimestamp = state.currentTimestamp; + // Cancel any pending display operations/frames + display.cancel(); + // Dispose of all layers for (key in layers) { index = parseInt(key); if (index > 0) - display.dispose(layers[key]); + layers[key].dispose(); } layers = {}; From 27c7ab782ffe95dd7d7d23d0705d22ea90969b2a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 2 Mar 2022 17:34:35 +0000 Subject: [PATCH 3/5] GUACAMOLE-462: Do not continue an outstanding state import if its corresponding seek has been cancelled. --- .../src/main/webapp/modules/SessionRecording.js | 8 +++++++- 1 file changed, 7 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 8512c58dd..9836355d4 100644 --- a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js +++ b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js @@ -607,8 +607,14 @@ Guacamole.SessionRecording = function SessionRecording(source) { // current state if (frame.clientState) { frame.clientState.text().then(function textReady(text) { + + // Cancel seek if aborted + if (thisSeek.aborted) + return; + playbackClient.importState(JSON.parse(text)); currentFrame = index; + }); break; } @@ -629,7 +635,7 @@ Guacamole.SessionRecording = function SessionRecording(source) { return; // If frames remain, replay the next frame - if (!thisSeek.aborted && currentFrame < index) + if (currentFrame < index) replayFrame(currentFrame + 1, continueReplay); // Otherwise, the seek operation is completed From c4b8a13968f0464e0a6506ede4e93600003c985a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Wed, 2 Mar 2022 20:01:17 +0000 Subject: [PATCH 4/5] GUACAMOLE-462: State of recording after resetting to a keyframe is the index of that keyframe, not necessarily the requested seek index. Further instructions may need to be replayed after seeking to the keyframe in order to reach the desired frame index. --- guacamole-common-js/src/main/webapp/modules/SessionRecording.js | 2 +- 1 file changed, 1 insertion(+), 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 9836355d4..4f0ff282b 100644 --- a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js +++ b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js @@ -613,7 +613,7 @@ Guacamole.SessionRecording = function SessionRecording(source) { return; playbackClient.importState(JSON.parse(text)); - currentFrame = index; + currentFrame = startIndex; }); break; From c3aad01be8da2d5edebd1c60c845f889c2b8afd9 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Tue, 22 Feb 2022 21:11:44 -0800 Subject: [PATCH 5/5] GUACAMOLE-462: Continue playback only after keyframe import. If this is not done, asynchronous decoding of the keyframe via text() may complete AFTER replay continues, effectively ignoring the keyframe, leaving currentFrame untouched, and unnecessarily replaying instructions. --- .../main/webapp/modules/SessionRecording.js | 81 +++++++++---------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js index 4f0ff282b..984d8772b 100644 --- a/guacamole-common-js/src/main/webapp/modules/SessionRecording.js +++ b/guacamole-common-js/src/main/webapp/modules/SessionRecording.js @@ -577,11 +577,12 @@ Guacamole.SessionRecording = function SessionRecording(source) { * @param {function} callback * The callback to invoke once the seek operation has completed. * - * @param {number} [delay=0] - * The number of milliseconds that the seek operation should be - * scheduled to take. + * @param {number} [nextRealTimestamp] + * The timestamp of the point in time that the given frame should be + * displayed, as would be returned by new Date().getTime(). If omitted, + * the frame will be displayed as soon as possible. */ - var seekToFrame = function seekToFrame(index, callback, delay) { + var seekToFrame = function seekToFrame(index, callback, nextRealTimestamp) { // Abort any in-progress seek abortSeek(); @@ -591,35 +592,7 @@ Guacamole.SessionRecording = function SessionRecording(source) { aborted : false }; - var startIndex; - - // Back up until startIndex represents current state - 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) { - frame.clientState.text().then(function textReady(text) { - - // Cancel seek if aborted - if (thisSeek.aborted) - return; - - playbackClient.importState(JSON.parse(text)); - currentFrame = startIndex; - - }); - break; - } - - } + var startIndex = index; // Replay any applicable incremental frames var continueReplay = function continueReplay() { @@ -646,11 +619,39 @@ Guacamole.SessionRecording = function SessionRecording(source) { // Continue replay after requested delay has elapsed, or // immediately if no delay was requested - if (delay) - window.setTimeout(continueReplay, delay); - else - continueReplay(); + var continueAfterRequiredDelay = function continueAfterRequiredDelay() { + var delay = nextRealTimestamp ? Math.max(nextRealTimestamp - new Date().getTime(), 0) : 0; + if (delay) + window.setTimeout(continueReplay, delay); + else + continueReplay(); + }; + // Back up until startIndex represents current state + for (; 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) { + frame.clientState.text().then(function textReady(text) { + playbackClient.importState(JSON.parse(text)); + currentFrame = startIndex; + continueAfterRequiredDelay(); + }); + return; + } + + } + + continueAfterRequiredDelay(); + }; /** @@ -685,14 +686,10 @@ Guacamole.SessionRecording = function SessionRecording(source) { // 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 seekToFrame(currentFrame + 1, function frameDelayElapsed() { continuePlayback(); - }, delay); + }, nextRealTimestamp); }