mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 05:07:41 +00:00
GUACAMOLE-1803: Update session recording playback at least once per second and allow arbitrary seeking.
This commit is contained in:
@@ -32,8 +32,13 @@ var Guacamole = Guacamole || {};
|
|||||||
* @param {!Blob|Guacamole.Tunnel} source
|
* @param {!Blob|Guacamole.Tunnel} source
|
||||||
* The Blob from which the instructions of the recording should
|
* The Blob from which the instructions of the recording should
|
||||||
* be read.
|
* be read.
|
||||||
|
* @param {number} [refreshInterval=1000]
|
||||||
|
* The minimum number of milliseconds between updates to the recording
|
||||||
|
* position through the provided onseek() callback. If non-positive, this
|
||||||
|
* parameter will be ignored, and the recording position will only be
|
||||||
|
* updated when seek requests are made, or when new frames are rendered.
|
||||||
*/
|
*/
|
||||||
Guacamole.SessionRecording = function SessionRecording(source) {
|
Guacamole.SessionRecording = function SessionRecording(source, refreshInterval) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to this Guacamole.SessionRecording.
|
* Reference to this Guacamole.SessionRecording.
|
||||||
@@ -139,22 +144,20 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
var currentFrame = -1;
|
var currentFrame = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The timestamp of the frame when playback began, in milliseconds. If
|
* True if the player is currently playing, or false otherwise.
|
||||||
* playback is not in progress, this will be null.
|
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @type {number}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
var startVideoTimestamp = null;
|
var currentlyPlaying = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The real-world timestamp when playback began, in milliseconds. If
|
* The current position within the recording, in milliseconds.
|
||||||
* playback is not in progress, this will be null.
|
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @type {number}
|
* @type {!number}
|
||||||
*/
|
*/
|
||||||
var startRealTimestamp = null;
|
var currentPosition = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object containing a single "aborted" property which is set to
|
* An object containing a single "aborted" property which is set to
|
||||||
@@ -211,6 +214,25 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
*/
|
*/
|
||||||
var seekCallback = null;
|
var seekCallback = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any current timeout associated with scheduling frame replay, or updating
|
||||||
|
* the current position, or null if no frame position increment is currently
|
||||||
|
* scheduled.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
var updateTimeout = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The browser timestamp of the last time that currentPosition was updated
|
||||||
|
* while playing, or null if the recording is not currently playing.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
var lastUpdateTimestamp = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses all Guacamole instructions within the given blob, invoking
|
* Parses all Guacamole instructions within the given blob, invoking
|
||||||
* the provided instruction callback for each such instruction. Once
|
* the provided instruction callback for each such instruction. Once
|
||||||
@@ -482,8 +504,9 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches through the given region of frames for the frame having a
|
* Searches through the given region of frames for the closest frame
|
||||||
* relative timestamp closest to the timestamp given.
|
* having a relative timestamp less than or equal to the to the given
|
||||||
|
* relative timestamp.
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @param {!number} minIndex
|
* @param {!number} minIndex
|
||||||
@@ -504,10 +527,23 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
*/
|
*/
|
||||||
var findFrame = function findFrame(minIndex, maxIndex, timestamp) {
|
var findFrame = function findFrame(minIndex, maxIndex, timestamp) {
|
||||||
|
|
||||||
// Do not search if the region contains only one element
|
// The region has only one frame - determine if it is before or after
|
||||||
if (minIndex === maxIndex)
|
// the requested timestamp
|
||||||
|
if (minIndex === maxIndex) {
|
||||||
|
|
||||||
|
// Skip checking if this is the very first frame - no frame could
|
||||||
|
// possibly be earlier
|
||||||
|
if (minIndex === 0)
|
||||||
return minIndex;
|
return minIndex;
|
||||||
|
|
||||||
|
// If the closest frame occured after the requested timestamp,
|
||||||
|
// return the previous frame, which will be the closest with a
|
||||||
|
// timestamp before the requested timestamp
|
||||||
|
if (toRelativeTimestamp(frames[minIndex].timestamp) > timestamp)
|
||||||
|
return minIndex - 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Split search region into two halves
|
// Split search region into two halves
|
||||||
var midIndex = Math.floor((minIndex + maxIndex) / 2);
|
var midIndex = Math.floor((minIndex + maxIndex) / 2);
|
||||||
var midTimestamp = toRelativeTimestamp(frames[midIndex].timestamp);
|
var midTimestamp = toRelativeTimestamp(frames[midIndex].timestamp);
|
||||||
@@ -599,10 +635,11 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
// Replay any applicable incremental frames
|
// Replay any applicable incremental frames
|
||||||
var continueReplay = function continueReplay() {
|
var continueReplay = function continueReplay() {
|
||||||
|
|
||||||
// Notify of changes in position
|
// Set the current position and notify changes
|
||||||
if (recording.onseek && currentFrame > startIndex) {
|
if (recording.onseek && currentFrame > startIndex) {
|
||||||
recording.onseek(toRelativeTimestamp(frames[currentFrame].timestamp),
|
currentPosition = toRelativeTimestamp(frames[currentFrame].timestamp);
|
||||||
currentFrame - startIndex, index - startIndex);
|
recording.onseek(currentPosition, currentFrame - startIndex,
|
||||||
|
index - startIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel seek if aborted
|
// Cancel seek if aborted
|
||||||
@@ -623,8 +660,18 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
// immediately if no delay was requested
|
// immediately if no delay was requested
|
||||||
var continueAfterRequiredDelay = function continueAfterRequiredDelay() {
|
var continueAfterRequiredDelay = function continueAfterRequiredDelay() {
|
||||||
var delay = nextRealTimestamp ? Math.max(nextRealTimestamp - new Date().getTime(), 0) : 0;
|
var delay = nextRealTimestamp ? Math.max(nextRealTimestamp - new Date().getTime(), 0) : 0;
|
||||||
if (delay)
|
if (delay) {
|
||||||
window.setTimeout(continueReplay, delay);
|
|
||||||
|
// Clear any already-scheduled update before scheduling again
|
||||||
|
// to avoid multiple updates in flight at the same time
|
||||||
|
updateTimeout && clearTimeout(updateTimeout);
|
||||||
|
|
||||||
|
// Schedule with the appropriate delay
|
||||||
|
updateTimeout = window.setTimeout(function timeoutComplete() {
|
||||||
|
updateTimeout = null;
|
||||||
|
continueReplay();
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
continueReplay();
|
continueReplay();
|
||||||
};
|
};
|
||||||
@@ -678,20 +725,62 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
*/
|
*/
|
||||||
var continuePlayback = function continuePlayback() {
|
var continuePlayback = function continuePlayback() {
|
||||||
|
|
||||||
|
// Do not continue playback if the recording is paused
|
||||||
|
if (!recording.isPlaying())
|
||||||
|
return;
|
||||||
|
|
||||||
// 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) {
|
||||||
|
|
||||||
// Pull the upcoming frame
|
// Pull the upcoming frame
|
||||||
var next = frames[currentFrame + 1];
|
var next = frames[currentFrame + 1];
|
||||||
|
|
||||||
// Calculate the real timestamp corresponding to when the next
|
// The position at which the next frame should be rendered
|
||||||
// frame begins
|
var nextFramePosition = toRelativeTimestamp(next.timestamp);
|
||||||
var nextRealTimestamp = next.timestamp - startVideoTimestamp + startRealTimestamp;
|
|
||||||
|
// The position at which the refresh interval would induce an
|
||||||
|
// update to the current recording position, rounded to the nearest
|
||||||
|
// whole multiple of refreshInterval to ensure consistent timing
|
||||||
|
// for refresh intervals even with inconsistent frame timing
|
||||||
|
var nextRefreshPosition = refreshInterval > 0 ? refreshInterval * (
|
||||||
|
Math.floor((currentPosition + refreshInterval) / refreshInterval)
|
||||||
|
) : nextFramePosition;
|
||||||
|
|
||||||
|
// If the next frame will occur before the next refresh interval,
|
||||||
|
// advance to the frame after the appropriate delay
|
||||||
|
if (nextFramePosition <= nextRefreshPosition)
|
||||||
|
|
||||||
// Advance to next frame after enough time has elapsed
|
|
||||||
seekToFrame(currentFrame + 1, function frameDelayElapsed() {
|
seekToFrame(currentFrame + 1, function frameDelayElapsed() {
|
||||||
|
|
||||||
|
// Record when the timestamp was updated and continue on
|
||||||
|
lastUpdateTimestamp = Date.now();
|
||||||
continuePlayback();
|
continuePlayback();
|
||||||
}, nextRealTimestamp);
|
|
||||||
|
}, Date.now() + (nextFramePosition - currentPosition));
|
||||||
|
|
||||||
|
// The position needs to be incremented before the next frame
|
||||||
|
else {
|
||||||
|
|
||||||
|
// Clear any existing update timeout
|
||||||
|
updateTimeout && window.clearTimeout(updateTimeout);
|
||||||
|
|
||||||
|
updateTimeout = window.setTimeout(function incrementPosition() {
|
||||||
|
|
||||||
|
updateTimeout = null;
|
||||||
|
|
||||||
|
// Update the position
|
||||||
|
currentPosition = nextRefreshPosition;
|
||||||
|
|
||||||
|
// Notifiy the new position using the onseek handler
|
||||||
|
if (recording.onseek)
|
||||||
|
recording.onseek(currentPosition);
|
||||||
|
|
||||||
|
// Record when the timestamp was updated and continue on
|
||||||
|
lastUpdateTimestamp = Date.now();
|
||||||
|
continuePlayback();
|
||||||
|
|
||||||
|
}, nextRefreshPosition - currentPosition);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -839,7 +928,7 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
* true if playback is currently in progress, false otherwise.
|
* true if playback is currently in progress, false otherwise.
|
||||||
*/
|
*/
|
||||||
this.isPlaying = function isPlaying() {
|
this.isPlaying = function isPlaying() {
|
||||||
return !!startVideoTimestamp;
|
return !!currentlyPlaying;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -899,13 +988,9 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
if (recording.onplay)
|
if (recording.onplay)
|
||||||
recording.onplay();
|
recording.onplay();
|
||||||
|
|
||||||
// Store timestamp of playback start for relative scheduling of
|
|
||||||
// future frames
|
|
||||||
var next = frames[currentFrame + 1];
|
|
||||||
startVideoTimestamp = next.timestamp;
|
|
||||||
startRealTimestamp = new Date().getTime();
|
|
||||||
|
|
||||||
// Begin playback of video
|
// Begin playback of video
|
||||||
|
currentlyPlaying = true;
|
||||||
|
lastUpdateTimestamp = Date.now();
|
||||||
continuePlayback();
|
continuePlayback();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -957,8 +1042,23 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Perform seek
|
// Find the index of the closest frame at or before the requested position
|
||||||
seekToFrame(findFrame(0, frames.length - 1, position), seekCallback);
|
var closestFrame = findFrame(0, frames.length - 1, position);
|
||||||
|
|
||||||
|
// Seek to the closest frame before or at the requested position
|
||||||
|
seekToFrame(closestFrame, function seekComplete() {
|
||||||
|
|
||||||
|
// Update the current position to the requested position
|
||||||
|
// and invoke the the onseek callback. Note that this is the
|
||||||
|
// position provided to this function, NOT the position of the
|
||||||
|
// frame that was just seeked
|
||||||
|
currentPosition = position;
|
||||||
|
if (recording.onseek)
|
||||||
|
recording.onseek(position);
|
||||||
|
|
||||||
|
seekCallback();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -988,6 +1088,14 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
// Abort any in-progress seek / playback
|
// Abort any in-progress seek / playback
|
||||||
abortSeek();
|
abortSeek();
|
||||||
|
|
||||||
|
// Cancel any currently-scheduled updates
|
||||||
|
updateTimeout && clearTimeout(updateTimeout);
|
||||||
|
|
||||||
|
// Increment the current position by the amount of time passed since the
|
||||||
|
// the last time it was updated
|
||||||
|
currentPosition += Date.now() - lastUpdateTimestamp;
|
||||||
|
lastUpdateTimestamp = null;
|
||||||
|
|
||||||
// Stop playback only if playback is in progress
|
// Stop playback only if playback is in progress
|
||||||
if (recording.isPlaying()) {
|
if (recording.isPlaying()) {
|
||||||
|
|
||||||
@@ -996,8 +1104,7 @@ Guacamole.SessionRecording = function SessionRecording(source) {
|
|||||||
recording.onpause();
|
recording.onpause();
|
||||||
|
|
||||||
// Playback is stopped
|
// Playback is stopped
|
||||||
startVideoTimestamp = null;
|
currentlyPlaying = false;
|
||||||
startRealTimestamp = null;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -96,6 +96,14 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
|
|||||||
config.controller = ['$scope', '$element', '$injector',
|
config.controller = ['$scope', '$element', '$injector',
|
||||||
function guacPlayerController($scope) {
|
function guacPlayerController($scope) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum number of milliseconds that should occur between updates
|
||||||
|
* to the progress indicator.
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
const PROGRESS_REFRESH_INTERVAL = 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Guacamole.SessionRecording instance to be used to playback the
|
* Guacamole.SessionRecording instance to be used to playback the
|
||||||
* session recording given via $scope.src. If the recording has not
|
* session recording given via $scope.src. If the recording has not
|
||||||
@@ -303,7 +311,8 @@ angular.module('player').directive('guacPlayer', ['$injector', function guacPlay
|
|||||||
// Otherwise, begin loading the provided recording
|
// Otherwise, begin loading the provided recording
|
||||||
else {
|
else {
|
||||||
|
|
||||||
$scope.recording = new Guacamole.SessionRecording(src);
|
$scope.recording = new Guacamole.SessionRecording(
|
||||||
|
src, PROGRESS_REFRESH_INTERVAL);
|
||||||
|
|
||||||
// Begin downloading the recording
|
// Begin downloading the recording
|
||||||
$scope.recording.connect();
|
$scope.recording.connect();
|
||||||
|
Reference in New Issue
Block a user