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