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