Source: lib/media/gap_jumping_controller.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.media.GapJumpingController');
  18. goog.require('shaka.log');
  19. goog.require('shaka.media.TimeRangesUtils');
  20. goog.require('shaka.util.EventManager');
  21. goog.require('shaka.util.FakeEvent');
  22. goog.require('shaka.util.IDestroyable');
  23. goog.require('shaka.util.Timer');
  24. /**
  25. * Creates a new GapJumpingController that handles jumping gaps that appear
  26. * within the content. This will only jump gaps between two buffered ranges,
  27. * so we should not have to worry about the availability window.
  28. *
  29. * @param {!HTMLMediaElement} video
  30. * @param {shakaExtern.Manifest} manifest
  31. * @param {shakaExtern.StreamingConfiguration} config
  32. * @param {function(!Event)} onEvent Called when an event is raised to be sent
  33. * to the application.
  34. *
  35. * @constructor
  36. * @struct
  37. * @implements {shaka.util.IDestroyable}
  38. */
  39. shaka.media.GapJumpingController = function(video, manifest, config, onEvent) {
  40. /** @private {HTMLMediaElement} */
  41. this.video_ = video;
  42. /** @private {?shakaExtern.Manifest} */
  43. this.manifest_ = manifest;
  44. /** @private {?shakaExtern.StreamingConfiguration} */
  45. this.config_ = config;
  46. /** @private {?function(!Event)} */
  47. this.onEvent_ = onEvent;
  48. /** @private {shaka.util.EventManager} */
  49. this.eventManager_ = new shaka.util.EventManager();
  50. /** @private {?shaka.util.Timer} */
  51. this.gapJumpTimer_ = null;
  52. /** @private {boolean} */
  53. this.seekingEventReceived_ = false;
  54. /** @private {number} */
  55. this.prevReadyState_ = video.readyState;
  56. /** @private {boolean} */
  57. this.didFireLargeGap_ = false;
  58. /**
  59. * The wall-clock time (in milliseconds) that the stall occurred. This is
  60. * used to ensure we don't flush the pipeline too often.
  61. * @private {number}
  62. */
  63. this.stallWallTime_ = -1;
  64. /**
  65. * The playhead time where we think a stall occurred. When the ready state
  66. * says we don't have enough data and the playhead stops too long, we assume
  67. * we have stalled.
  68. * @private {number}
  69. */
  70. this.stallPlayheadTime_ = -1;
  71. /**
  72. * True if we have already flushed the pipeline at stallPlayheadTime_.
  73. * Allows us to avoid flushing multiple times for the same stall.
  74. * @private {boolean}
  75. */
  76. this.stallCorrected_ = false;
  77. /** @private {boolean} */
  78. this.hadSegmentAppended_ = false;
  79. let pollGap = this.onPollGapJump_.bind(this);
  80. this.eventManager_.listen(video, 'waiting', pollGap);
  81. // We can't trust readyState or 'waiting' events on all platforms. So poll
  82. // the current time and if we are in a gap, jump it.
  83. // See: https://goo.gl/sbSHp9 and https://goo.gl/cuAcYd
  84. this.gapJumpTimer_ = new shaka.util.Timer(pollGap);
  85. this.gapJumpTimer_.scheduleRepeated(0.25);
  86. };
  87. /**
  88. * The limit, in seconds, for the gap size that we will assume the browser will
  89. * handle for us.
  90. * @const
  91. */
  92. shaka.media.GapJumpingController.BROWSER_GAP_TOLERANCE = 0.001;
  93. /** @override */
  94. shaka.media.GapJumpingController.prototype.destroy = function() {
  95. let p = this.eventManager_.destroy();
  96. this.eventManager_ = null;
  97. this.video_ = null;
  98. this.manifest_ = null;
  99. this.onEvent_ = null;
  100. if (this.gapJumpTimer_ != null) {
  101. this.gapJumpTimer_.cancel();
  102. this.gapJumpTimer_ = null;
  103. }
  104. return p;
  105. };
  106. /**
  107. * Called when a segment is appended by StreamingEngine, but not when a clear is
  108. * pending. This means StreamingEngine will continue buffering forward from
  109. * what is buffered. So we know about any gaps before the start.
  110. */
  111. shaka.media.GapJumpingController.prototype.onSegmentAppended = function() {
  112. this.hadSegmentAppended_ = true;
  113. this.onPollGapJump_();
  114. };
  115. /** Called when a seek has started. */
  116. shaka.media.GapJumpingController.prototype.onSeeking = function() {
  117. this.seekingEventReceived_ = true;
  118. this.hadSegmentAppended_ = false;
  119. this.didFireLargeGap_ = false;
  120. };
  121. /**
  122. * Called on a recurring timer to check for gaps in the media. This is also
  123. * called in a 'waiting' event.
  124. *
  125. * @private
  126. */
  127. shaka.media.GapJumpingController.prototype.onPollGapJump_ = function() {
  128. // Don't gap jump before the video is ready to play.
  129. if (this.video_.readyState == 0) return;
  130. // Do not gap jump if seeking has begun, but the seeking event has not
  131. // yet fired for this particular seek.
  132. if (this.video_.seeking) {
  133. if (!this.seekingEventReceived_) {
  134. return;
  135. }
  136. } else {
  137. this.seekingEventReceived_ = false;
  138. }
  139. // Don't gap jump while paused, so that you don't constantly jump ahead while
  140. // paused on a livestream.
  141. if (this.video_.paused) return;
  142. // When the ready state changes, we have moved on, so we should fire the large
  143. // gap event if we see one.
  144. if (this.video_.readyState != this.prevReadyState_) {
  145. this.didFireLargeGap_ = false;
  146. this.prevReadyState_ = this.video_.readyState;
  147. }
  148. const smallGapLimit = this.config_.smallGapLimit;
  149. let currentTime = this.video_.currentTime;
  150. let buffered = this.video_.buffered;
  151. let gapIndex = shaka.media.TimeRangesUtils.getGapIndex(buffered, currentTime);
  152. // The current time is unbuffered or is too far from a gap.
  153. if (gapIndex == null) {
  154. this.handleStall_();
  155. return;
  156. }
  157. // If we are before the first buffered range, this could be an unbuffered
  158. // seek. So wait until a segment is appended so we are sure it is a gap.
  159. if (gapIndex == 0 && !this.hadSegmentAppended_) {
  160. return;
  161. }
  162. // StreamingEngine can buffer past the seek end, but still don't allow seeking
  163. // past it.
  164. let jumpTo = buffered.start(gapIndex);
  165. let seekEnd = this.manifest_.presentationTimeline.getSeekRangeEnd();
  166. if (jumpTo >= seekEnd) {
  167. return;
  168. }
  169. let jumpSize = jumpTo - currentTime;
  170. let isGapSmall = jumpSize <= smallGapLimit;
  171. let jumpLargeGap = false;
  172. // If we jump to exactly the gap start, we may detect a small gap due to
  173. // rounding errors or browser bugs. We can ignore these extremely small gaps
  174. // since the browser should play through them for us.
  175. if (jumpSize < shaka.media.GapJumpingController.BROWSER_GAP_TOLERANCE) {
  176. return;
  177. }
  178. if (!isGapSmall && !this.didFireLargeGap_) {
  179. this.didFireLargeGap_ = true;
  180. // Event firing is synchronous.
  181. let event = new shaka.util.FakeEvent(
  182. 'largegap', {'currentTime': currentTime, 'gapSize': jumpSize});
  183. event.cancelable = true;
  184. this.onEvent_(event);
  185. if (this.config_.jumpLargeGaps && !event.defaultPrevented) {
  186. jumpLargeGap = true;
  187. } else {
  188. shaka.log.info('Ignoring large gap at', currentTime, 'size', jumpSize);
  189. }
  190. }
  191. if (isGapSmall || jumpLargeGap) {
  192. if (gapIndex == 0) {
  193. shaka.log.info(
  194. 'Jumping forward', jumpSize,
  195. 'seconds because of gap before start time of', jumpTo);
  196. } else {
  197. shaka.log.info(
  198. 'Jumping forward', jumpSize, 'seconds because of gap starting at',
  199. buffered.end(gapIndex - 1), 'and ending at', jumpTo);
  200. }
  201. this.video_.currentTime = jumpTo;
  202. }
  203. };
  204. /**
  205. * This determines if we are stalled inside a buffered range and corrects it if
  206. * possible.
  207. * @private
  208. */
  209. shaka.media.GapJumpingController.prototype.handleStall_ = function() {
  210. let currentTime = this.video_.currentTime;
  211. let buffered = this.video_.buffered;
  212. if (!this.video_.paused && this.video_.playbackRate > 0) {
  213. // Some platforms/browsers can get stuck in the middle of a buffered range
  214. // (e.g. when seeking in a background tab). Flush the media pipeline to
  215. // help. Flush once we have stopped for more than 1 second inside a buffered
  216. // range.
  217. if (this.stallPlayheadTime_ != currentTime) {
  218. this.stallPlayheadTime_ = currentTime;
  219. this.stallWallTime_ = Date.now();
  220. this.stallCorrected_ = false;
  221. } else if (!this.stallCorrected_ &&
  222. this.stallWallTime_ < Date.now() - 1000) {
  223. for (let i = 0; i < buffered.length; i++) {
  224. // Ignore the end of the buffered range since it may not play any more
  225. // on all platforms.
  226. if (currentTime >= buffered.start(i) &&
  227. currentTime < buffered.end(i) - 0.5) {
  228. shaka.log.debug(
  229. 'Flushing media pipeline due to stall inside buffered range');
  230. this.video_.currentTime += 0.1;
  231. this.stallPlayheadTime_ = this.video_.currentTime;
  232. this.stallCorrected_ = true;
  233. break;
  234. }
  235. }
  236. }
  237. }
  238. };