VideoSynchronizer.js 6.1 KB

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