Source: lib/media/playhead.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.Playhead');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.media.GapJumpingController');
  21. goog.require('shaka.media.TimeRangesUtils');
  22. goog.require('shaka.media.VideoWrapper');
  23. goog.require('shaka.util.IDestroyable');
  24. goog.require('shaka.util.StreamUtils');
  25. goog.require('shaka.util.Timer');
  26. /**
  27. * Creates a Playhead, which manages the video's current time.
  28. *
  29. * The Playhead provides mechanisms for setting the presentation's start time,
  30. * restricting seeking to valid time ranges, and stopping playback for startup
  31. * and re-buffering.
  32. *
  33. * @param {!HTMLMediaElement} video
  34. * @param {shakaExtern.Manifest} manifest
  35. * @param {shakaExtern.StreamingConfiguration} config
  36. * @param {?number} startTime The playhead's initial position in seconds. If
  37. * null, defaults to the start of the presentation for VOD and the live-edge
  38. * for live.
  39. * @param {function()} onSeek Called when the user agent seeks to a time within
  40. * the presentation timeline.
  41. * @param {function(!Event)} onEvent Called when an event is raised to be sent
  42. * to the application.
  43. *
  44. * @constructor
  45. * @struct
  46. * @implements {shaka.util.IDestroyable}
  47. */
  48. shaka.media.Playhead = function(
  49. video, manifest, config, startTime, onSeek, onEvent) {
  50. /** @private {HTMLMediaElement} */
  51. this.video_ = video;
  52. /** @private {?shakaExtern.Manifest} */
  53. this.manifest_ = manifest;
  54. /** @private {?shakaExtern.StreamingConfiguration} */
  55. this.config_ = config;
  56. /** @private {?function()} */
  57. this.onSeek_ = onSeek;
  58. /** @private {?shaka.util.Timer} */
  59. this.checkWindowTimer_ = null;
  60. /** @private {?number} */
  61. this.lastCorrectiveSeek_;
  62. /** @private {shaka.media.GapJumpingController} */
  63. this.gapController_ =
  64. new shaka.media.GapJumpingController(video, manifest, config, onEvent);
  65. /** @private {shaka.media.VideoWrapper} */
  66. this.videoWrapper_ = new shaka.media.VideoWrapper(
  67. video, this.onSeeking_.bind(this), this.getStartTime_(startTime));
  68. let poll = this.onPollWindow_ .bind(this);
  69. this.checkWindowTimer_ = new shaka.util.Timer(poll);
  70. this.checkWindowTimer_.scheduleRepeated(0.25);
  71. };
  72. /**
  73. * This is the minimum size (in seconds) that the seek range can be. If it is
  74. * smaller than this, change it to be this big so we don't repeatedly seek to
  75. * keep within a zero-width window.
  76. * This has been increased to 3s long, to account for the weaker hardware on
  77. * Chromecasts.
  78. * @private {number}
  79. * @const
  80. */
  81. shaka.media.Playhead.MIN_SEEK_RANGE_ = 3.0;
  82. /** @override */
  83. shaka.media.Playhead.prototype.destroy = function() {
  84. let p = Promise.all([
  85. this.videoWrapper_.destroy(),
  86. this.gapController_.destroy(),
  87. ]);
  88. this.videoWrapper_ = null;
  89. this.gapController_ = null;
  90. if (this.checkWindowTimer_ != null) {
  91. this.checkWindowTimer_.cancel();
  92. this.checkWindowTimer_ = null;
  93. }
  94. this.video_ = null;
  95. this.manifest_ = null;
  96. this.config_ = null;
  97. this.onSeek_ = null;
  98. return p;
  99. };
  100. /**
  101. * Adjust the start time. Used by Player to implement the
  102. * streaming.startAtSegmentBoundary configuration.
  103. *
  104. * @param {number} startTime
  105. */
  106. shaka.media.Playhead.prototype.setStartTime = function(startTime) {
  107. this.videoWrapper_.setTime(startTime);
  108. };
  109. /**
  110. * Gets the playhead's current (logical) position.
  111. *
  112. * @return {number}
  113. */
  114. shaka.media.Playhead.prototype.getTime = function() {
  115. let time = this.videoWrapper_.getTime();
  116. if (this.video_.readyState > 0) {
  117. // Although we restrict the video's currentTime elsewhere, clamp it here to
  118. // ensure timing issues don't cause us to return a time outside the segment
  119. // availability window. E.g., the user agent seeks and calls this function
  120. // before we receive the 'seeking' event.
  121. //
  122. // We don't buffer when the livestream video is paused and the playhead time
  123. // is out of the seek range; thus, we do not clamp the current time when the
  124. // video is paused.
  125. // https://github.com/google/shaka-player/issues/1121
  126. if (!this.video_.paused) {
  127. time = this.clampTime_(time);
  128. }
  129. }
  130. return time;
  131. };
  132. /**
  133. * Gets the playhead's initial position in seconds.
  134. *
  135. * @param {?number} startTime
  136. * @return {number}
  137. * @private
  138. */
  139. shaka.media.Playhead.prototype.getStartTime_ = function(startTime) {
  140. let timeline = this.manifest_.presentationTimeline;
  141. if (startTime == null) {
  142. if (timeline.getDuration() < Infinity) {
  143. // If the presentation is VOD, or if the presentation is live but has
  144. // finished broadcasting, then start from the beginning.
  145. startTime = timeline.getSeekRangeStart();
  146. } else {
  147. // Otherwise, start near the live-edge.
  148. startTime = timeline.getSeekRangeEnd();
  149. }
  150. } else if (startTime < 0) {
  151. // For live streams, if the startTime is negative, start from a certain
  152. // offset time from the live edge. If the offset from the live edge is not
  153. // available, start from the current available segment start point instead,
  154. // handled by clampTime_().
  155. startTime = timeline.getSeekRangeEnd() + startTime;
  156. }
  157. return this.clampSeekToDuration_(this.clampTime_(startTime));
  158. };
  159. /**
  160. * Stops the playhead for buffering, or resumes the playhead after buffering.
  161. *
  162. * @param {boolean} buffering True to stop the playhead; false to allow it to
  163. * continue.
  164. */
  165. shaka.media.Playhead.prototype.setBuffering = function(buffering) {
  166. this.videoWrapper_.setBuffering(buffering);
  167. };
  168. /**
  169. * Gets the current effective playback rate. This may be negative even if the
  170. * browser does not directly support rewinding.
  171. * @return {number}
  172. */
  173. shaka.media.Playhead.prototype.getPlaybackRate = function() {
  174. return this.videoWrapper_.getPlaybackRate();
  175. };
  176. /**
  177. * Sets the playback rate.
  178. * @param {number} rate
  179. */
  180. shaka.media.Playhead.prototype.setPlaybackRate = function(rate) {
  181. this.videoWrapper_.setPlaybackRate(rate);
  182. };
  183. /**
  184. * Called when a segment is appended by StreamingEngine, but not when a clear is
  185. * pending. This means StreamingEngine will continue buffering forward from
  186. * what is buffered, so that we know about any gaps before the start.
  187. */
  188. shaka.media.Playhead.prototype.onSegmentAppended = function() {
  189. this.gapController_.onSegmentAppended();
  190. };
  191. /**
  192. * Called on a recurring timer to keep the playhead from falling outside the
  193. * availability window.
  194. *
  195. * @private
  196. */
  197. shaka.media.Playhead.prototype.onPollWindow_ = function() {
  198. // Don't catch up to the seek range when we are paused or empty.
  199. // The definition of "seeking" says that we are seeking until the buffered
  200. // data intersects with the playhead. If we fall outside of the seek range,
  201. // it doesn't matter if we are in a "seeking" state. We can and should go
  202. // ahead and catch up while seeking.
  203. if (this.video_.readyState == 0 || this.video_.paused) {
  204. return;
  205. }
  206. let currentTime = this.video_.currentTime;
  207. let timeline = this.manifest_.presentationTimeline;
  208. let seekStart = timeline.getSeekRangeStart();
  209. let seekEnd = timeline.getSeekRangeEnd();
  210. const minRange = shaka.media.Playhead.MIN_SEEK_RANGE_;
  211. if (seekEnd - seekStart < minRange) {
  212. seekStart = seekEnd - minRange;
  213. }
  214. if (currentTime < seekStart) {
  215. // The seek range has moved past the playhead. Move ahead to catch up.
  216. let targetTime = this.reposition_(currentTime);
  217. shaka.log.info('Jumping forward ' + (targetTime - currentTime) +
  218. ' seconds to catch up with the seek range.');
  219. this.video_.currentTime = targetTime;
  220. }
  221. };
  222. /**
  223. * Handles when a seek happens on the video.
  224. *
  225. * @private
  226. */
  227. shaka.media.Playhead.prototype.onSeeking_ = function() {
  228. this.gapController_.onSeeking();
  229. let currentTime = this.videoWrapper_.getTime();
  230. let targetTime = this.reposition_(currentTime);
  231. const gapLimit = shaka.media.GapJumpingController.BROWSER_GAP_TOLERANCE;
  232. if (Math.abs(targetTime - currentTime) > gapLimit) {
  233. // You can only seek like this every so often. This is to prevent an
  234. // infinite loop on systems where changing currentTime takes a significant
  235. // amount of time (e.g. Chromecast).
  236. let time = new Date().getTime() / 1000;
  237. if (!this.lastCorrectiveSeek_ || this.lastCorrectiveSeek_ < time - 1) {
  238. this.lastCorrectiveSeek_ = time;
  239. this.videoWrapper_.setTime(targetTime);
  240. return;
  241. }
  242. }
  243. shaka.log.v1('Seek to ' + currentTime);
  244. this.onSeek_();
  245. };
  246. /**
  247. * Clamp seek times and playback start times so that we never seek to the
  248. * presentation duration. Seeking to or starting at duration does not work
  249. * consistently across browsers.
  250. *
  251. * TODO: Clean up and simplify Playhead. There are too many layers of, methods
  252. * for, and conditions on timestamp adjustment.
  253. *
  254. * @see https://github.com/google/shaka-player/issues/979
  255. * @param {number} time
  256. * @return {number} The adjusted seek time.
  257. * @private
  258. */
  259. shaka.media.Playhead.prototype.clampSeekToDuration_ = function(time) {
  260. let timeline = this.manifest_.presentationTimeline;
  261. let duration = timeline.getDuration();
  262. if (time >= duration) {
  263. goog.asserts.assert(this.config_.durationBackoff >= 0,
  264. 'Duration backoff must be non-negative!');
  265. return duration - this.config_.durationBackoff;
  266. }
  267. return time;
  268. };
  269. /**
  270. * Computes a new playhead position that's within the presentation timeline.
  271. *
  272. * @param {number} currentTime
  273. * @return {number} The time to reposition the playhead to.
  274. * @private
  275. */
  276. shaka.media.Playhead.prototype.reposition_ = function(currentTime) {
  277. goog.asserts.assert(this.manifest_ && this.config_, 'Must not be destroyed');
  278. /** @type {function(number)} */
  279. let isBuffered =
  280. shaka.media.TimeRangesUtils.isBuffered.bind(null, this.video_.buffered);
  281. let rebufferingGoal = shaka.util.StreamUtils.getRebufferingGoal(
  282. this.manifest_, this.config_, 1 /* scaleFactor */);
  283. let timeline = this.manifest_.presentationTimeline;
  284. let start = timeline.getSeekRangeStart();
  285. let end = timeline.getSeekRangeEnd();
  286. let duration = timeline.getDuration();
  287. const minRange = shaka.media.Playhead.MIN_SEEK_RANGE_;
  288. if (end - start < minRange) {
  289. start = end - minRange;
  290. }
  291. // With live content, the beginning of the availability window is moving
  292. // forward. This means we cannot seek to it since we will "fall" outside the
  293. // window while we buffer. So we define a "safe" region that is far enough
  294. // away. For VOD, |safe == start|.
  295. let safe = timeline.getSafeSeekRangeStart(rebufferingGoal);
  296. // These are the times to seek to rather than the exact destinations. When
  297. // we seek, we will get another event (after a slight delay) and these steps
  298. // will run again. So if we seeked directly to |start|, |start| would move
  299. // on the next call and we would loop forever.
  300. //
  301. // Offset by 5 seconds since Chromecast takes a few seconds to start playing
  302. // after a seek, even when buffered.
  303. let seekStart = timeline.getSafeSeekRangeStart(5);
  304. let seekSafe = timeline.getSafeSeekRangeStart(rebufferingGoal + 5);
  305. if (currentTime >= duration) {
  306. shaka.log.v1('Playhead past duration.');
  307. return this.clampSeekToDuration_(currentTime);
  308. }
  309. if (currentTime > end) {
  310. shaka.log.v1('Playhead past end.');
  311. return end;
  312. }
  313. if (currentTime < start) {
  314. if (isBuffered(seekStart)) {
  315. shaka.log.v1('Playhead before start & start is buffered');
  316. return seekStart;
  317. } else {
  318. shaka.log.v1('Playhead before start & start is unbuffered');
  319. return seekSafe;
  320. }
  321. }
  322. if (currentTime >= safe || isBuffered(currentTime)) {
  323. shaka.log.v1('Playhead in safe region or in buffered region.');
  324. return currentTime;
  325. } else {
  326. shaka.log.v1('Playhead outside safe region & in unbuffered region.');
  327. return seekSafe;
  328. }
  329. };
  330. /**
  331. * Clamps the given time to the seek range.
  332. *
  333. * @param {number} time The time in seconds.
  334. * @return {number} The clamped time in seconds.
  335. * @private
  336. */
  337. shaka.media.Playhead.prototype.clampTime_ = function(time) {
  338. let start = this.manifest_.presentationTimeline.getSeekRangeStart();
  339. if (time < start) return start;
  340. let end = this.manifest_.presentationTimeline.getSeekRangeEnd();
  341. if (time > end) return end;
  342. return time;
  343. };