You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

223 lines
6.1 KiB
JavaScript

import defaultValue from "./defaultValue.js";
import defined from "./defined.js";
import destroyObject from "./destroyObject.js";
import Iso8601 from "./Iso8601.js";
import JulianDate from "./JulianDate.js";
/**
* Synchronizes a video element with a simulation clock.
*
* @alias VideoSynchronizer
* @constructor
*
* @param {Object} [options] Object with the following properties:
* @param {Clock} [options.clock] The clock instance used to drive the video.
* @param {HTMLVideoElement} [options.element] The video element to be synchronized.
* @param {JulianDate} [options.epoch=Iso8601.MINIMUM_VALUE] The simulation time that marks the start of the video.
* @param {Number} [options.tolerance=1.0] The maximum amount of time, in seconds, that the clock and video can diverge.
*
* @demo {@link https://sandcastle.cesium.com/index.html?src=Video.html|Video Material Demo}
*/
function VideoSynchronizer(options) {
options = defaultValue(options, defaultValue.EMPTY_OBJECT);
this._clock = undefined;
this._element = undefined;
this._clockSubscription = undefined;
this._seekFunction = undefined;
this._lastPlaybackRate = undefined;
this.clock = options.clock;
this.element = options.element;
/**
* Gets or sets the simulation time that marks the start of the video.
* @type {JulianDate}
* @default Iso8601.MINIMUM_VALUE
*/
this.epoch = defaultValue(options.epoch, Iso8601.MINIMUM_VALUE);
/**
* Gets or sets the amount of time in seconds the video's currentTime
* and the clock's currentTime can diverge before a video seek is performed.
* Lower values make the synchronization more accurate but video
* performance might suffer. Higher values provide better performance
* but at the cost of accuracy.
* @type {Number}
* @default 1.0
*/
this.tolerance = defaultValue(options.tolerance, 1.0);
this._seeking = false;
this._seekFunction = undefined;
this._firstTickAfterSeek = false;
}
Object.defineProperties(VideoSynchronizer.prototype, {
/**
* Gets or sets the clock used to drive the video element.
*
* @memberof VideoSynchronizer.prototype
* @type {Clock}
*/
clock: {
get: function () {
return this._clock;
},
set: function (value) {
var oldValue = this._clock;
if (oldValue === value) {
return;
}
if (defined(oldValue)) {
this._clockSubscription();
this._clockSubscription = undefined;
}
if (defined(value)) {
this._clockSubscription = value.onTick.addEventListener(
VideoSynchronizer.prototype._onTick,
this
);
}
this._clock = value;
},
},
/**
* Gets or sets the video element to synchronize.
*
* @memberof VideoSynchronizer.prototype
* @type {HTMLVideoElement}
*/
element: {
get: function () {
return this._element;
},
set: function (value) {
var oldValue = this._element;
if (oldValue === value) {
return;
}
if (defined(oldValue)) {
oldValue.removeEventListener("seeked", this._seekFunction, false);
}
if (defined(value)) {
this._seeking = false;
this._seekFunction = createSeekFunction(this);
value.addEventListener("seeked", this._seekFunction, false);
}
this._element = value;
this._seeking = false;
this._firstTickAfterSeek = false;
},
},
});
/**
* Destroys and resources used by the object. Once an object is destroyed, it should not be used.
*
* @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
*/
VideoSynchronizer.prototype.destroy = function () {
this.element = undefined;
this.clock = undefined;
return destroyObject(this);
};
/**
* Returns true if this object was destroyed; otherwise, false.
*
* @returns {Boolean} True if this object was destroyed; otherwise, false.
*/
VideoSynchronizer.prototype.isDestroyed = function () {
return false;
};
VideoSynchronizer.prototype._trySetPlaybackRate = function (clock) {
if (this._lastPlaybackRate === clock.multiplier) {
return;
}
var element = this._element;
try {
element.playbackRate = clock.multiplier;
} catch (error) {
// Seek manually for unsupported playbackRates.
element.playbackRate = 0.0;
}
this._lastPlaybackRate = clock.multiplier;
};
VideoSynchronizer.prototype._onTick = function (clock) {
var element = this._element;
if (!defined(element) || element.readyState < 2) {
return;
}
var paused = element.paused;
var shouldAnimate = clock.shouldAnimate;
if (shouldAnimate === paused) {
if (shouldAnimate) {
element.play();
} else {
element.pause();
}
}
//We need to avoid constant seeking or the video will
//never contain a complete frame for us to render.
//So don't do anything if we're seeing or on the first
//tick after a seek (the latter of which allows the frame
//to actually be rendered.
if (this._seeking || this._firstTickAfterSeek) {
this._firstTickAfterSeek = false;
return;
}
this._trySetPlaybackRate(clock);
var clockTime = clock.currentTime;
var epoch = defaultValue(this.epoch, Iso8601.MINIMUM_VALUE);
var videoTime = JulianDate.secondsDifference(clockTime, epoch);
var duration = element.duration;
var desiredTime;
var currentTime = element.currentTime;
if (element.loop) {
videoTime = videoTime % duration;
if (videoTime < 0.0) {
videoTime = duration - videoTime;
}
desiredTime = videoTime;
} else if (videoTime > duration) {
desiredTime = duration;
} else if (videoTime < 0.0) {
desiredTime = 0.0;
} else {
desiredTime = videoTime;
}
//If the playing video's time and the scene's clock time
//ever drift too far apart, we want to set the video to match
var tolerance = shouldAnimate ? defaultValue(this.tolerance, 1.0) : 0.001;
if (Math.abs(desiredTime - currentTime) > tolerance) {
this._seeking = true;
element.currentTime = desiredTime;
}
};
function createSeekFunction(that) {
return function () {
that._seeking = false;
that._firstTickAfterSeek = true;
};
}
export default VideoSynchronizer;