VideoSynchronizer.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import defaultValue from './defaultValue.js';
  2. import defined from './defined.js';
  3. import defineProperties from './defineProperties.js';
  4. import destroyObject from './destroyObject.js';
  5. import Iso8601 from './Iso8601.js';
  6. import JulianDate from './JulianDate.js';
  7. /**
  8. * Synchronizes a video element with a simulation clock.
  9. *
  10. * @alias VideoSynchronizer
  11. * @constructor
  12. *
  13. * @param {Object} [options] Object with the following properties:
  14. * @param {Clock} [options.clock] The clock instance used to drive the video.
  15. * @param {HTMLVideoElement} [options.element] The video element to be synchronized.
  16. * @param {JulianDate} [options.epoch=Iso8601.MINIMUM_VALUE] The simulation time that marks the start of the video.
  17. * @param {Number} [options.tolerance=1.0] The maximum amount of time, in seconds, that the clock and video can diverge.
  18. *
  19. * @demo {@link https://sandcastle.cesium.com/index.html?src=Video.html|Video Material Demo}
  20. */
  21. function VideoSynchronizer(options) {
  22. options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  23. this._clock = undefined;
  24. this._element = undefined;
  25. this._clockSubscription = undefined;
  26. this._seekFunction = undefined;
  27. this._lastPlaybackRate = undefined;
  28. this.clock = options.clock;
  29. this.element = options.element;
  30. /**
  31. * Gets or sets the simulation time that marks the start of the video.
  32. * @type {JulianDate}
  33. * @default Iso8601.MINIMUM_VALUE
  34. */
  35. this.epoch = defaultValue(options.epoch, Iso8601.MINIMUM_VALUE);
  36. /**
  37. * Gets or sets the amount of time in seconds the video's currentTime
  38. * and the clock's currentTime can diverge before a video seek is performed.
  39. * Lower values make the synchronization more accurate but video
  40. * performance might suffer. Higher values provide better performance
  41. * but at the cost of accuracy.
  42. * @type {Number}
  43. * @default 1.0
  44. */
  45. this.tolerance = defaultValue(options.tolerance, 1.0);
  46. this._seeking = false;
  47. this._seekFunction = undefined;
  48. this._firstTickAfterSeek = false;
  49. }
  50. defineProperties(VideoSynchronizer.prototype, {
  51. /**
  52. * Gets or sets the clock used to drive the video element.
  53. *
  54. * @memberof VideoSynchronizer.prototype
  55. * @type {Clock}
  56. */
  57. clock : {
  58. get : function() {
  59. return this._clock;
  60. },
  61. set : function(value) {
  62. var oldValue = this._clock;
  63. if (oldValue === value) {
  64. return;
  65. }
  66. if (defined(oldValue)) {
  67. this._clockSubscription();
  68. this._clockSubscription = undefined;
  69. }
  70. if (defined(value)) {
  71. this._clockSubscription = value.onTick.addEventListener(VideoSynchronizer.prototype._onTick, this);
  72. }
  73. this._clock = value;
  74. }
  75. },
  76. /**
  77. * Gets or sets the video element to synchronize.
  78. *
  79. * @memberof VideoSynchronizer.prototype
  80. * @type {HTMLVideoElement}
  81. */
  82. element : {
  83. get : function() {
  84. return this._element;
  85. },
  86. set : function(value) {
  87. var oldValue = this._element;
  88. if (oldValue === value) {
  89. return;
  90. }
  91. if (defined(oldValue)) {
  92. oldValue.removeEventListener('seeked', this._seekFunction, false);
  93. }
  94. if (defined(value)) {
  95. this._seeking = false;
  96. this._seekFunction = createSeekFunction(this);
  97. value.addEventListener('seeked', this._seekFunction, false);
  98. }
  99. this._element = value;
  100. this._seeking = false;
  101. this._firstTickAfterSeek = false;
  102. }
  103. }
  104. });
  105. /**
  106. * Destroys and resources used by the object. Once an object is destroyed, it should not be used.
  107. *
  108. * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
  109. */
  110. VideoSynchronizer.prototype.destroy = function() {
  111. this.element = undefined;
  112. this.clock = undefined;
  113. return destroyObject(this);
  114. };
  115. /**
  116. * Returns true if this object was destroyed; otherwise, false.
  117. *
  118. * @returns {Boolean} True if this object was destroyed; otherwise, false.
  119. */
  120. VideoSynchronizer.prototype.isDestroyed = function() {
  121. return false;
  122. };
  123. VideoSynchronizer.prototype._trySetPlaybackRate = function(clock) {
  124. if (this._lastPlaybackRate === clock.multiplier) {
  125. return;
  126. }
  127. var element = this._element;
  128. try {
  129. element.playbackRate = clock.multiplier;
  130. } catch(error) {
  131. // Seek manually for unsupported playbackRates.
  132. element.playbackRate = 0.0;
  133. }
  134. this._lastPlaybackRate = clock.multiplier;
  135. };
  136. VideoSynchronizer.prototype._onTick = function(clock) {
  137. var element = this._element;
  138. if (!defined(element) || element.readyState < 2) {
  139. return;
  140. }
  141. var paused = element.paused;
  142. var shouldAnimate = clock.shouldAnimate;
  143. if (shouldAnimate === paused) {
  144. if (shouldAnimate) {
  145. element.play();
  146. } else {
  147. element.pause();
  148. }
  149. }
  150. //We need to avoid constant seeking or the video will
  151. //never contain a complete frame for us to render.
  152. //So don't do anything if we're seeing or on the first
  153. //tick after a seek (the latter of which allows the frame
  154. //to actually be rendered.
  155. if (this._seeking || this._firstTickAfterSeek) {
  156. this._firstTickAfterSeek = false;
  157. return;
  158. }
  159. this._trySetPlaybackRate(clock);
  160. var clockTime = clock.currentTime;
  161. var epoch = defaultValue(this.epoch, Iso8601.MINIMUM_VALUE);
  162. var videoTime = JulianDate.secondsDifference(clockTime, epoch);
  163. var duration = element.duration;
  164. var desiredTime;
  165. var currentTime = element.currentTime;
  166. if (element.loop) {
  167. videoTime = videoTime % duration;
  168. if (videoTime < 0.0) {
  169. videoTime = duration - videoTime;
  170. }
  171. desiredTime = videoTime;
  172. } else if (videoTime > duration) {
  173. desiredTime = duration;
  174. } else if (videoTime < 0.0) {
  175. desiredTime = 0.0;
  176. } else {
  177. desiredTime = videoTime;
  178. }
  179. //If the playing video's time and the scene's clock time
  180. //ever drift too far apart, we want to set the video to match
  181. var tolerance = shouldAnimate ? defaultValue(this.tolerance, 1.0) : 0.001;
  182. if (Math.abs(desiredTime - currentTime) > tolerance) {
  183. this._seeking = true;
  184. element.currentTime = desiredTime;
  185. }
  186. };
  187. function createSeekFunction(that) {
  188. return function() {
  189. that._seeking = false;
  190. that._firstTickAfterSeek = true;
  191. };
  192. }
  193. export default VideoSynchronizer;