mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-346: Merge Avoid blocking main thread when seeking.
This commit is contained in:
@@ -28,10 +28,19 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- Guacamole recording player -->
|
<!-- Guacamole recording player -->
|
||||||
<div class="player">
|
<div id="player">
|
||||||
|
|
||||||
<!-- Player display -->
|
<!-- 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 -->
|
<!-- Player controls -->
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
@@ -17,11 +17,46 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.player {
|
#player {
|
||||||
width: 640px;
|
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%;
|
width: 100%;
|
||||||
|
|
||||||
@@ -52,11 +87,11 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls > * {
|
#player .controls > * {
|
||||||
margin: 0.25em;
|
margin: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls #position-slider {
|
#player .controls #position-slider {
|
||||||
-ms-flex: 1 1 auto;
|
-ms-flex: 1 1 auto;
|
||||||
-moz-box-flex: 1;
|
-moz-box-flex: 1;
|
||||||
-webkit-box-flex: 1;
|
-webkit-box-flex: 1;
|
||||||
@@ -64,16 +99,16 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls #play-pause {
|
#player .controls #play-pause {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
min-width: 5em;
|
min-width: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls #position,
|
#player .controls #position,
|
||||||
.player .controls #duration {
|
#player .controls #duration {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls #duration {
|
#player .controls #duration {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,13 @@
|
|||||||
*/
|
*/
|
||||||
var RECORDING_URL = 'recording.guac';
|
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.
|
* The element which will contain the recording display.
|
||||||
*
|
*
|
||||||
@@ -41,6 +48,13 @@
|
|||||||
*/
|
*/
|
||||||
var playPause = document.getElementById('play-pause');
|
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
|
* Text status display indicating the current playback position within the
|
||||||
* recording.
|
* recording.
|
||||||
@@ -163,6 +177,13 @@
|
|||||||
recording.pause();
|
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
|
// Fit display within containing div
|
||||||
recordingDisplay.onresize = function displayResized(width, height) {
|
recordingDisplay.onresize = function displayResized(width, height) {
|
||||||
|
|
||||||
@@ -189,7 +210,18 @@
|
|||||||
|
|
||||||
// Seek within recording if slider is moved
|
// Seek within recording if slider is moved
|
||||||
positionSlider.onchange = function sliderPositionChanged() {
|
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';
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
@@ -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.
|
||||||
*
|
*
|
||||||
@@ -141,14 +152,14 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
var startRealTimestamp = null;
|
var startRealTimestamp = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of the timeout which will play the next frame, if playback is in
|
* The ID of the timeout which will continue the in-progress seek
|
||||||
* progress. If playback is not in progress, the ID stored here (if any)
|
* operation. If no seek operation is in progress, the ID stored here (if
|
||||||
* will not be valid.
|
* any) will not be valid.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @type {Number}
|
* @type {Number}
|
||||||
*/
|
*/
|
||||||
var playbackTimeout = null;
|
var seekTimeout = null;
|
||||||
|
|
||||||
// Start playback client connected
|
// Start playback client connected
|
||||||
playbackClient.connect();
|
playbackClient.connect();
|
||||||
@@ -294,50 +305,96 @@ 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. 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
|
* @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.
|
||||||
|
*
|
||||||
|
* @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
|
// Replay frames asynchronously
|
||||||
for (startIndex = index; startIndex >= 0; startIndex--) {
|
seekTimeout = window.setTimeout(function continueSeek() {
|
||||||
|
|
||||||
var frame = frames[startIndex];
|
var startIndex;
|
||||||
|
|
||||||
// If we've reached the current frame, startIndex represents
|
// Back up until startIndex represents current state
|
||||||
// current state by definition
|
for (startIndex = index; startIndex >= 0; startIndex--) {
|
||||||
if (startIndex === currentFrame)
|
|
||||||
break;
|
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
|
var startTime = new Date().getTime();
|
||||||
startIndex++;
|
|
||||||
|
|
||||||
// Replay any applicable incremental frames
|
// Replay any applicable incremental frames
|
||||||
for (; startIndex <= index; startIndex++)
|
for (; startIndex <= index; startIndex++) {
|
||||||
replayFrame(startIndex);
|
|
||||||
|
|
||||||
// Current frame is now at requested index
|
// Stop seeking if the operation is taking too long
|
||||||
currentFrame = index;
|
var currentTime = new Date().getTime();
|
||||||
|
if (currentTime - startTime >= MAXIMUM_SEEK_TIME)
|
||||||
|
break;
|
||||||
|
|
||||||
// Notify of changes in position
|
replayFrame(startIndex);
|
||||||
if (recording.onseek)
|
}
|
||||||
recording.onseek(recording.getPosition());
|
|
||||||
|
|
||||||
|
// 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() {
|
var continuePlayback = function continuePlayback() {
|
||||||
|
|
||||||
// Advance to next frame
|
|
||||||
seekToFrame(currentFrame + 1);
|
|
||||||
|
|
||||||
// If frames remain after advancing, schedule next frame
|
// If frames remain after advancing, schedule next frame
|
||||||
if (currentFrame + 1 < frames.length) {
|
if (currentFrame + 1 < frames.length) {
|
||||||
|
|
||||||
@@ -367,7 +421,7 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
var delay = Math.max(nextRealTimestamp - new Date().getTime(), 0);
|
var delay = Math.max(nextRealTimestamp - new Date().getTime(), 0);
|
||||||
|
|
||||||
// Advance to next frame after enough time has elapsed
|
// Advance to next frame after enough time has elapsed
|
||||||
playbackTimeout = window.setTimeout(function frameDelayElapsed() {
|
seekToFrame(currentFrame + 1, function frameDelayElapsed() {
|
||||||
continuePlayback();
|
continuePlayback();
|
||||||
}, delay);
|
}, delay);
|
||||||
|
|
||||||
@@ -502,7 +556,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 +587,17 @@ 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. The seek operation will 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 +608,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 / playback
|
||||||
|
abortSeek();
|
||||||
|
|
||||||
// Stop playback only if playback is in progress
|
// Stop playback only if playback is in progress
|
||||||
if (recording.isPlaying()) {
|
if (recording.isPlaying()) {
|
||||||
|
|
||||||
@@ -570,8 +641,7 @@ Guacamole.SessionRecording = function SessionRecording(tunnel) {
|
|||||||
if (recording.onpause)
|
if (recording.onpause)
|
||||||
recording.onpause();
|
recording.onpause();
|
||||||
|
|
||||||
// Stop playback
|
// Playback is stopped
|
||||||
window.clearTimeout(playbackTimeout);
|
|
||||||
startVideoTimestamp = null;
|
startVideoTimestamp = null;
|
||||||
startRealTimestamp = null;
|
startRealTimestamp = null;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user