/*global define*/
define([
'./defaultValue',
'./defined',
'./defineProperties',
'./destroyObject',
'./DeveloperError',
'./Event',
'./Iso8601',
'./JulianDate'
], function(
defaultValue,
defined,
defineProperties,
destroyObject,
DeveloperError,
Event,
Iso8601,
JulianDate) {
'use strict';
/**
* 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 http://cesiumjs.org/Cesium/Apps/Sandcastle/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.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;
}
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._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;
}
element.playbackRate = clock.multiplier;
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;
};
}
return VideoSynchronizer;
});