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