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
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;
|