GUACAMOLE-346: Merge Avoid blocking main thread when seeking.

This commit is contained in:
Nick Couchman
2017-07-19 14:35:44 -04:00
4 changed files with 203 additions and 57 deletions

View File

@@ -28,10 +28,19 @@
<body>
<!-- Guacamole recording player -->
<div class="player">
<div id="player">
<!-- Player display -->
<div id="display"></div>
<div id="display">
<div class="notification-container">
<div class="seek-notification">
<p>
Seek in progress...
<button id="cancel-seek">Cancel</button>
</p>
</div>
</div>
</div>
<!-- Player controls -->
<div class="controls">

View File

@@ -17,11 +17,46 @@
* under the License.
*/
.player {
#player {
width: 640px;
}
.player .controls {
#display {
position: relative;
}
#player .notification-container {
position: absolute;
z-index: 1;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
#player .seek-notification {
color: white;
background: rgba(0, 0, 0, 0.75);
display: none; /* Initially hidden */
width: 100%;
height: 100%;
}
#player.seeking .seek-notification {
display: table;
}
#player .seek-notification p {
display: table-cell;
text-align: center;
vertical-align: middle;
font-family: sans-serif;
}
#player .controls {
width: 100%;
@@ -52,11 +87,11 @@
}
.player .controls > * {
#player .controls > * {
margin: 0.25em;
}
.player .controls #position-slider {
#player .controls #position-slider {
-ms-flex: 1 1 auto;
-moz-box-flex: 1;
-webkit-box-flex: 1;
@@ -64,16 +99,16 @@
flex: 1 1 auto;
}
.player .controls #play-pause {
#player .controls #play-pause {
margin-left: 0;
min-width: 5em;
}
.player .controls #position,
.player .controls #duration {
#player .controls #position,
#player .controls #duration {
font-family: monospace;
}
.player .controls #duration {
#player .controls #duration {
margin-right: 0;
}

View File

@@ -27,6 +27,13 @@
*/
var RECORDING_URL = 'recording.guac';
/**
* The element representing the session recording player.
*
* @type Element
*/
var player = document.getElementById('player');
/**
* The element which will contain the recording display.
*
@@ -41,6 +48,13 @@
*/
var playPause = document.getElementById('play-pause');
/**
* Button for cancelling in-progress seek operations.
*
* @type Element
*/
var cancelSeek = document.getElementById('cancel-seek');
/**
* Text status display indicating the current playback position within the
* recording.
@@ -163,6 +177,13 @@
recording.pause();
};
// Resume playback when cancel button is clicked
cancelSeek.onclick = function cancelSeekOperation(e) {
recording.play();
player.className = '';
e.stopPropagation();
};
// Fit display within containing div
recordingDisplay.onresize = function displayResized(width, height) {
@@ -189,7 +210,18 @@
// Seek within recording if slider is moved
positionSlider.onchange = function sliderPositionChanged() {
recording.seek(positionSlider.value);
// Request seek
recording.seek(positionSlider.value, function seekComplete() {
// Seek has completed
player.className = '';
});
// Seek is in progress
player.className = 'seeking';
};
})();

View File

@@ -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.
*
@@ -141,14 +152,14 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
var startRealTimestamp = null;
/**
* The ID of the timeout which will play the next frame, if playback is in
* progress. If playback is not in progress, the ID stored here (if any)
* will not be valid.
* 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 playbackTimeout = null;
var seekTimeout = null;
// Start playback client connected
playbackClient.connect();
@@ -294,50 +305,96 @@ 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. The seek
* operation will 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.
*
* @param {Number} [delay=0]
* The number of milliseconds that the seek operation should be
* scheduled to take.
*/
var seekToFrame = function seekToFrame(index) {
var seekToFrame = function seekToFrame(index, callback, delay) {
var startIndex;
// Abort any in-progress seek
abortSeek();
// Back up until startIndex represents current state
for (startIndex = index; startIndex >= 0; startIndex--) {
// Replay frames asynchronously
seekTimeout = window.setTimeout(function continueSeek() {
var frame = frames[startIndex];
var startIndex;
// If we've reached the current frame, startIndex represents
// current state by definition
if (startIndex === currentFrame)
break;
// 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) {
playbackClient.importState(frame.clientState);
break;
}
// If frame has associated absolute state, make that frame the
// current state
if (frame.clientState) {
playbackClient.importState(frame.clientState);
break;
}
}
// Advance to frame index after current state
startIndex++;
// Advance to frame index after current state
startIndex++;
var startTime = new Date().getTime();
// Replay any applicable incremental frames
for (; startIndex <= index; startIndex++)
replayFrame(startIndex);
// Replay any applicable incremental frames
for (; startIndex <= index; startIndex++) {
// Current frame is now at requested index
currentFrame = index;
// Stop seeking if the operation is taking too long
var currentTime = new Date().getTime();
if (currentTime - startTime >= MAXIMUM_SEEK_TIME)
break;
// Notify of changes in position
if (recording.onseek)
recording.onseek(recording.getPosition());
replayFrame(startIndex);
}
// Current frame is now at requested 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)
seekToFrame(index, callback,
Math.max(delay - (new Date().getTime() - startTime), 0));
// Notify that the requested seek has completed
else
callback();
}, delay || 0);
};
/**
* 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);
};
/**
@@ -349,9 +406,6 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
*/
var continuePlayback = function continuePlayback() {
// Advance to next frame
seekToFrame(currentFrame + 1);
// If frames remain after advancing, schedule next frame
if (currentFrame + 1 < frames.length) {
@@ -367,7 +421,7 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
var delay = Math.max(nextRealTimestamp - new Date().getTime(), 0);
// Advance to next frame after enough time has elapsed
playbackTimeout = window.setTimeout(function frameDelayElapsed() {
seekToFrame(currentFrame + 1, function frameDelayElapsed() {
continuePlayback();
}, delay);
@@ -502,7 +556,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 +587,17 @@ 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. The seek operation will 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 +608,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 / playback
abortSeek();
// Stop playback only if playback is in progress
if (recording.isPlaying()) {
@@ -570,8 +641,7 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
if (recording.onpause)
recording.onpause();
// Stop playback
window.clearTimeout(playbackTimeout);
// Playback is stopped
startVideoTimestamp = null;
startRealTimestamp = null;