Source: lib/media/video_wrapper.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.VideoWrapper');
  7. goog.provide('shaka.media.VideoWrapper.PlayheadMover');
  8. goog.require('goog.asserts');
  9. goog.require('shaka.log');
  10. goog.require('shaka.util.EventManager');
  11. goog.require('shaka.util.IReleasable');
  12. goog.require('shaka.util.MediaReadyState');
  13. goog.require('shaka.util.Timer');
  14. /**
  15. * Creates a new VideoWrapper that manages setting current time and playback
  16. * rate. This handles seeks before content is loaded and ensuring the video
  17. * time is set properly. This doesn't handle repositioning within the
  18. * presentation window.
  19. *
  20. * @implements {shaka.util.IReleasable}
  21. */
  22. shaka.media.VideoWrapper = class {
  23. /**
  24. * @param {!HTMLMediaElement} video
  25. * @param {function()} onSeek Called when the video seeks.
  26. * @param {function(number)} onStarted Called when the video has started.
  27. * @param {function():number} getStartTime Called to get the time to start at.
  28. */
  29. constructor(video, onSeek, onStarted, getStartTime) {
  30. /** @private {HTMLMediaElement} */
  31. this.video_ = video;
  32. /** @private {function()} */
  33. this.onSeek_ = onSeek;
  34. /** @private {function(number)} */
  35. this.onStarted_ = onStarted;
  36. /** @private {?number} */
  37. this.startTime_ = null;
  38. /** @private {function():number} */
  39. this.getStartTime_ = () => {
  40. if (this.startTime_ == null) {
  41. this.startTime_ = getStartTime();
  42. }
  43. return this.startTime_;
  44. };
  45. /** @private {boolean} */
  46. this.started_ = false;
  47. /** @private {shaka.util.EventManager} */
  48. this.eventManager_ = new shaka.util.EventManager();
  49. /** @private {shaka.media.VideoWrapper.PlayheadMover} */
  50. this.mover_ = new shaka.media.VideoWrapper.PlayheadMover(
  51. /* mediaElement= */ video,
  52. /* maxAttempts= */ 10);
  53. // Before we can set the start time, we must check if the video element is
  54. // ready. If the video element is not ready, we cannot set the time. To work
  55. // around this, we will wait for the "loadedmetadata" event which tells us
  56. // that the media element is now ready.
  57. shaka.util.MediaReadyState.waitForReadyState(this.video_,
  58. HTMLMediaElement.HAVE_METADATA,
  59. this.eventManager_,
  60. () => {
  61. this.setStartTime_(this.getStartTime_());
  62. });
  63. }
  64. /** @override */
  65. release() {
  66. if (this.eventManager_) {
  67. this.eventManager_.release();
  68. this.eventManager_ = null;
  69. }
  70. if (this.mover_ != null) {
  71. this.mover_.release();
  72. this.mover_ = null;
  73. }
  74. this.onSeek_ = () => {};
  75. this.video_ = null;
  76. }
  77. /**
  78. * Gets the video's current (logical) position.
  79. *
  80. * @return {number}
  81. */
  82. getTime() {
  83. return this.started_ ? this.video_.currentTime : this.getStartTime_();
  84. }
  85. /**
  86. * Sets the current time of the video.
  87. *
  88. * @param {number} time
  89. */
  90. setTime(time) {
  91. if (this.video_.readyState > 0) {
  92. this.mover_.moveTo(time);
  93. } else {
  94. shaka.util.MediaReadyState.waitForReadyState(this.video_,
  95. HTMLMediaElement.HAVE_METADATA,
  96. this.eventManager_,
  97. () => {
  98. this.setStartTime_(this.getStartTime_());
  99. });
  100. }
  101. }
  102. /**
  103. * Set the start time for the content. The given start time will be ignored if
  104. * the content does not start at 0.
  105. *
  106. * @param {number} startTime
  107. * @private
  108. */
  109. setStartTime_(startTime) {
  110. // If we start close enough to our intended start time, then we won't do
  111. // anything special.
  112. if (Math.abs(this.video_.currentTime - startTime) < 0.001) {
  113. this.startListeningToSeeks_();
  114. return;
  115. }
  116. // We will need to delay adding our normal seeking listener until we have
  117. // seen the first seek event. We will force the first seek event later in
  118. // this method.
  119. this.eventManager_.listenOnce(this.video_, 'seeking', () => {
  120. this.startListeningToSeeks_();
  121. });
  122. // If the currentTime != 0, it indicates that the user has seeked after
  123. // calling |Player.load|, meaning that |currentTime| is more meaningful than
  124. // |startTime|.
  125. //
  126. // Seeking to the current time is a work around for Issue 1298 and 4888.
  127. // If we don't do this, the video may get stuck and not play.
  128. //
  129. // TODO: Need further investigation why it happens. Before and after
  130. // setting the current time, video.readyState is 1, video.paused is true,
  131. // and video.buffered's TimeRanges length is 0.
  132. // See: https://github.com/shaka-project/shaka-player/issues/1298
  133. this.mover_.moveTo(
  134. (!this.video_.currentTime || this.video_.currentTime == 0) ?
  135. startTime :
  136. this.video_.currentTime);
  137. }
  138. /**
  139. * Add the listener for seek-events. This will call the externally-provided
  140. * |onSeek| callback whenever the media element seeks.
  141. *
  142. * @private
  143. */
  144. startListeningToSeeks_() {
  145. goog.asserts.assert(
  146. this.video_.readyState > 0,
  147. 'The media element should be ready before we listen for seeking.');
  148. // Now that any startup seeking is complete, we can trust the video element
  149. // for currentTime.
  150. this.started_ = true;
  151. this.eventManager_.listen(this.video_, 'seeking', () => this.onSeek_());
  152. this.onStarted_(this.video_.currentTime);
  153. }
  154. };
  155. /**
  156. * A class used to move the playhead away from its current time. Sometimes,
  157. * legacy Edge ignores re-seeks. After changing the current time, check every
  158. * 100ms, retrying if the change was not accepted.
  159. *
  160. * Delay stats over 100 runs of a re-seeking integration test:
  161. * Edge - 0ms - 2%
  162. * Edge - 100ms - 40%
  163. * Edge - 200ms - 32%
  164. * Edge - 300ms - 24%
  165. * Edge - 400ms - 2%
  166. * Chrome - 0ms - 100%
  167. *
  168. * Unfortunately, legacy Edge is not receiving updates anymore, but it still
  169. * must be supported as it is used for web browsers on XBox.
  170. *
  171. * @implements {shaka.util.IReleasable}
  172. * @final
  173. */
  174. shaka.media.VideoWrapper.PlayheadMover = class {
  175. /**
  176. * @param {!HTMLMediaElement} mediaElement
  177. * The media element that the mover can manipulate.
  178. *
  179. * @param {number} maxAttempts
  180. * To prevent us from infinitely trying to change the current time, the
  181. * mover accepts a max attempts value. At most, the mover will check if the
  182. * video moved |maxAttempts| times. If this is zero of negative, no
  183. * attempts will be made.
  184. */
  185. constructor(mediaElement, maxAttempts) {
  186. /** @private {HTMLMediaElement} */
  187. this.mediaElement_ = mediaElement;
  188. /** @private {number} */
  189. this.maxAttempts_ = maxAttempts;
  190. /** @private {number} */
  191. this.remainingAttempts_ = 0;
  192. /** @private {number} */
  193. this.originTime_ = 0;
  194. /** @private {number} */
  195. this.targetTime_ = 0;
  196. /** @private {shaka.util.Timer} */
  197. this.timer_ = new shaka.util.Timer(() => this.onTick_());
  198. }
  199. /** @override */
  200. release() {
  201. if (this.timer_) {
  202. this.timer_.stop();
  203. this.timer_ = null;
  204. }
  205. this.mediaElement_ = null;
  206. }
  207. /**
  208. * Try forcing the media element to move to |timeInSeconds|. If a previous
  209. * call to |moveTo| is still in progress, this will override it.
  210. *
  211. * @param {number} timeInSeconds
  212. */
  213. moveTo(timeInSeconds) {
  214. this.originTime_ = this.mediaElement_.currentTime;
  215. this.targetTime_ = timeInSeconds;
  216. this.remainingAttempts_ = this.maxAttempts_;
  217. // Set the time and then start the timer. The timer will check if the set
  218. // was successful, and retry if not.
  219. this.mediaElement_.currentTime = timeInSeconds;
  220. this.timer_.tickEvery(/* seconds= */ 0.1);
  221. }
  222. /**
  223. * @private
  224. */
  225. onTick_() {
  226. // Sigh... We ran out of retries...
  227. if (this.remainingAttempts_ <= 0) {
  228. shaka.log.warning([
  229. 'Failed to move playhead from', this.originTime_,
  230. 'to', this.targetTime_,
  231. ].join(' '));
  232. this.timer_.stop();
  233. return;
  234. }
  235. // Yay! We were successful.
  236. if (this.mediaElement_.currentTime != this.originTime_ ||
  237. this.mediaElement_.currentTime === this.targetTime_) {
  238. this.timer_.stop();
  239. return;
  240. }
  241. // Sigh... Try again...
  242. this.mediaElement_.currentTime = this.targetTime_;
  243. this.remainingAttempts_--;
  244. }
  245. };