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