Source: lib/media/gap_jumping_controller.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.GapJumpingController');
  7. goog.require('shaka.log');
  8. goog.require('shaka.media.PresentationTimeline');
  9. goog.require('shaka.media.StallDetector');
  10. goog.require('shaka.media.TimeRangesUtils');
  11. goog.require('shaka.util.EventManager');
  12. goog.require('shaka.util.FakeEvent');
  13. goog.require('shaka.util.IReleasable');
  14. goog.require('shaka.util.Platform');
  15. goog.require('shaka.util.Timer');
  16. /**
  17. * GapJumpingController handles jumping gaps that appear within the content.
  18. * This will only jump gaps between two buffered ranges, so we should not have
  19. * to worry about the availability window.
  20. *
  21. * @implements {shaka.util.IReleasable}
  22. */
  23. shaka.media.GapJumpingController = class {
  24. /**
  25. * @param {!HTMLMediaElement} video
  26. * @param {!shaka.media.PresentationTimeline} timeline
  27. * @param {shaka.extern.StreamingConfiguration} config
  28. * @param {shaka.media.StallDetector} stallDetector
  29. * The stall detector is used to keep the playhead moving while in a
  30. * playable region. The gap jumping controller takes ownership over the
  31. * stall detector.
  32. * If no stall detection logic is desired, |null| may be provided.
  33. * @param {function(!Event)} onEvent
  34. * Called when an event is raised to be sent to the application.
  35. */
  36. constructor(video, timeline, config, stallDetector, onEvent) {
  37. /** @private {?function(!Event)} */
  38. this.onEvent_ = onEvent;
  39. /** @private {HTMLMediaElement} */
  40. this.video_ = video;
  41. /** @private {?shaka.media.PresentationTimeline} */
  42. this.timeline_ = timeline;
  43. /** @private {?shaka.extern.StreamingConfiguration} */
  44. this.config_ = config;
  45. /** @private {shaka.util.EventManager} */
  46. this.eventManager_ = new shaka.util.EventManager();
  47. /** @private {boolean} */
  48. this.seekingEventReceived_ = false;
  49. /** @private {number} */
  50. this.prevReadyState_ = video.readyState;
  51. /** @private {number} */
  52. this.startTime_ = 0;
  53. /** @private {number} */
  54. this.gapsJumped_ = 0;
  55. /**
  56. * The stall detector tries to keep the playhead moving forward. It is
  57. * managed by the gap-jumping controller to avoid conflicts. On some
  58. * platforms, the stall detector is not wanted, so it may be null.
  59. *
  60. * @private {shaka.media.StallDetector}
  61. */
  62. this.stallDetector_ = stallDetector;
  63. /** @private {boolean} */
  64. this.hadSegmentAppended_ = false;
  65. this.eventManager_.listen(video, 'waiting', () => this.onPollGapJump_());
  66. /**
  67. * We can't trust |readyState| or 'waiting' events on all platforms. To make
  68. * up for this, we poll the current time. If we think we are in a gap, jump
  69. * out of it.
  70. *
  71. * See: https://bit.ly/2McuXxm and https://bit.ly/2K5xmJO
  72. *
  73. * @private {?shaka.util.Timer}
  74. */
  75. this.gapJumpTimer_ = new shaka.util.Timer(() => {
  76. this.onPollGapJump_();
  77. }).tickEvery(this.config_.gapJumpTimerTime);
  78. }
  79. /** @override */
  80. release() {
  81. if (this.eventManager_) {
  82. this.eventManager_.release();
  83. this.eventManager_ = null;
  84. }
  85. if (this.gapJumpTimer_ != null) {
  86. this.gapJumpTimer_.stop();
  87. this.gapJumpTimer_ = null;
  88. }
  89. if (this.stallDetector_) {
  90. this.stallDetector_.release();
  91. this.stallDetector_ = null;
  92. }
  93. this.onEvent_ = null;
  94. this.timeline_ = null;
  95. this.video_ = null;
  96. }
  97. /**
  98. * Called when a segment is appended by StreamingEngine, but not when a clear
  99. * is pending. This means StreamingEngine will continue buffering forward from
  100. * what is buffered. So we know about any gaps before the start.
  101. */
  102. onSegmentAppended() {
  103. this.hadSegmentAppended_ = true;
  104. this.onPollGapJump_();
  105. }
  106. /**
  107. * Called when playback has started and the video element is
  108. * listening for seeks.
  109. *
  110. * @param {number} startTime
  111. */
  112. onStarted(startTime) {
  113. if (this.video_.seeking && !this.seekingEventReceived_) {
  114. this.seekingEventReceived_ = true;
  115. this.startTime_ = startTime;
  116. }
  117. }
  118. /** Called when a seek has started. */
  119. onSeeking() {
  120. this.seekingEventReceived_ = true;
  121. this.hadSegmentAppended_ = false;
  122. }
  123. /**
  124. * Returns the total number of playback gaps jumped.
  125. * @return {number}
  126. */
  127. getGapsJumped() {
  128. return this.gapsJumped_;
  129. }
  130. /**
  131. * Called on a recurring timer to check for gaps in the media. This is also
  132. * called in a 'waiting' event.
  133. *
  134. * @private
  135. */
  136. onPollGapJump_() {
  137. // Don't gap jump before the video is ready to play.
  138. if (this.video_.readyState == 0) {
  139. return;
  140. }
  141. // Do not gap jump if seeking has begun, but the seeking event has not
  142. // yet fired for this particular seek.
  143. if (this.video_.seeking) {
  144. if (!this.seekingEventReceived_) {
  145. return;
  146. }
  147. } else {
  148. this.seekingEventReceived_ = false;
  149. }
  150. // Don't gap jump while paused, so that you don't constantly jump ahead
  151. // while paused on a livestream. We make an exception for time 0, since we
  152. // may be _required_ to seek on startup before play can begin, but only if
  153. // autoplay is enabled.
  154. if (this.video_.paused && (this.video_.currentTime != this.startTime_ ||
  155. (!this.video_.autoplay && this.video_.currentTime == this.startTime_))) {
  156. return;
  157. }
  158. // When the ready state changes, we have moved on, so we should fire the
  159. // large gap event if we see one.
  160. if (this.video_.readyState != this.prevReadyState_) {
  161. this.prevReadyState_ = this.video_.readyState;
  162. }
  163. if (this.stallDetector_ && this.stallDetector_.poll()) {
  164. // Some action was taken by StallDetector, so don't do anything yet.
  165. return;
  166. }
  167. const currentTime = this.video_.currentTime;
  168. const buffered = this.video_.buffered;
  169. const gapDetectionThreshold = this.config_.gapDetectionThreshold;
  170. const gapIndex = shaka.media.TimeRangesUtils.getGapIndex(
  171. buffered, currentTime, gapDetectionThreshold);
  172. // The current time is unbuffered or is too far from a gap.
  173. if (gapIndex == null) {
  174. return;
  175. }
  176. // If we are before the first buffered range, this could be an unbuffered
  177. // seek. So wait until a segment is appended so we are sure it is a gap.
  178. if (gapIndex == 0 && !this.hadSegmentAppended_) {
  179. return;
  180. }
  181. // StreamingEngine can buffer past the seek end, but still don't allow
  182. // seeking past it.
  183. let jumpTo = buffered.start(gapIndex);
  184. // Workaround for Xbox with Legacy Edge. On this platform video element
  185. // often rounds value we want to set as currentTime and we are not able
  186. // to jump over the gap.
  187. if (shaka.util.Platform.isLegacyEdge() ||
  188. shaka.util.Platform.isXboxOne()) {
  189. jumpTo = Math.ceil((jumpTo + 0.01) * 100) / 100;
  190. }
  191. const seekEnd = this.timeline_.getSeekRangeEnd();
  192. if (jumpTo >= seekEnd) {
  193. return;
  194. }
  195. const jumpSize = jumpTo - currentTime;
  196. // If we jump to exactly the gap start, we may detect a small gap due to
  197. // rounding errors or browser bugs. We can ignore these extremely small
  198. // gaps since the browser should play through them for us.
  199. if (jumpSize < shaka.media.GapJumpingController.BROWSER_GAP_TOLERANCE) {
  200. return;
  201. }
  202. if (gapIndex == 0) {
  203. shaka.log.info(
  204. 'Jumping forward', jumpSize,
  205. 'seconds because of gap before start time of', jumpTo);
  206. } else {
  207. shaka.log.info(
  208. 'Jumping forward', jumpSize, 'seconds because of gap starting at',
  209. buffered.end(gapIndex - 1), 'and ending at', jumpTo);
  210. }
  211. this.video_.currentTime = jumpTo;
  212. // This accounts for the possibility that we jump a gap at the start
  213. // position but we jump _into_ another gap. By setting the start
  214. // position to the new jumpTo we ensure that the check above will
  215. // pass even though the video is still paused.
  216. if (currentTime == this.startTime_) {
  217. this.startTime_ = jumpTo;
  218. }
  219. this.gapsJumped_++;
  220. this.onEvent_(
  221. new shaka.util.FakeEvent(shaka.util.FakeEvent.EventName.GapJumped));
  222. }
  223. };
  224. /**
  225. * The limit, in seconds, for the gap size that we will assume the browser will
  226. * handle for us.
  227. * @const
  228. */
  229. shaka.media.GapJumpingController.BROWSER_GAP_TOLERANCE = 0.001;