Source: lib/media/presentation_timeline.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.PresentationTimeline');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.log');
  9. goog.require('shaka.media.SegmentReference');
  10. /**
  11. * PresentationTimeline.
  12. * @export
  13. */
  14. shaka.media.PresentationTimeline = class {
  15. /**
  16. * @param {?number} presentationStartTime The wall-clock time, in seconds,
  17. * when the presentation started or will start. Only required for live.
  18. * @param {number} presentationDelay The delay to give the presentation, in
  19. * seconds. Only required for live.
  20. * @param {boolean=} autoCorrectDrift Whether to account for drift when
  21. * determining the availability window.
  22. *
  23. * @see {shaka.extern.Manifest}
  24. * @see {@tutorial architecture}
  25. */
  26. constructor(presentationStartTime, presentationDelay,
  27. autoCorrectDrift = true) {
  28. /** @private {?number} */
  29. this.presentationStartTime_ = presentationStartTime;
  30. /** @private {number} */
  31. this.presentationDelay_ = presentationDelay;
  32. /** @private {number} */
  33. this.duration_ = Infinity;
  34. /** @private {number} */
  35. this.segmentAvailabilityDuration_ = Infinity;
  36. /**
  37. * The maximum segment duration (in seconds). Can be based on explicitly-
  38. * known segments or on signalling in the manifest.
  39. *
  40. * @private {number}
  41. */
  42. this.maxSegmentDuration_ = 1;
  43. /**
  44. * The minimum segment start time (in seconds, in the presentation timeline)
  45. * for segments we explicitly know about.
  46. *
  47. * This is null if we have no explicit descriptions of segments, such as in
  48. * DASH when using SegmentTemplate w/ duration.
  49. *
  50. * @private {?number}
  51. */
  52. this.minSegmentStartTime_ = null;
  53. /**
  54. * The maximum segment end time (in seconds, in the presentation timeline)
  55. * for segments we explicitly know about.
  56. *
  57. * This is null if we have no explicit descriptions of segments, such as in
  58. * DASH when using SegmentTemplate w/ duration. When this is non-null, the
  59. * presentation start time is calculated from the segment end times.
  60. *
  61. * @private {?number}
  62. */
  63. this.maxSegmentEndTime_ = null;
  64. /** @private {number} */
  65. this.clockOffset_ = 0;
  66. /** @private {boolean} */
  67. this.static_ = true;
  68. /** @private {boolean} */
  69. this.isLive2VodTransition_ = false;
  70. /** @private {number} */
  71. this.userSeekStart_ = 0;
  72. /** @private {boolean} */
  73. this.autoCorrectDrift_ = autoCorrectDrift;
  74. /**
  75. * For low latency Dash, availabilityTimeOffset indicates a segment is
  76. * available for download earlier than its availability start time.
  77. * This field is the minimum availabilityTimeOffset value among the
  78. * segments. We reduce the distance from live edge by this value.
  79. *
  80. * @private {number}
  81. */
  82. this.availabilityTimeOffset_ = 0;
  83. /** @private {boolean} */
  84. this.startTimeLocked_ = false;
  85. /** @private {?number} */
  86. this.initialProgramDateTime_ = presentationStartTime;
  87. }
  88. /**
  89. * @return {number} The presentation's duration in seconds.
  90. * Infinity indicates that the presentation continues indefinitely.
  91. * @export
  92. */
  93. getDuration() {
  94. return this.duration_;
  95. }
  96. /**
  97. * @return {number} The presentation's max segment duration in seconds.
  98. * @export
  99. */
  100. getMaxSegmentDuration() {
  101. return this.maxSegmentDuration_;
  102. }
  103. /**
  104. * Sets the presentation's start time.
  105. *
  106. * @param {number} presentationStartTime The wall-clock time, in seconds,
  107. * when the presentation started or will start. Only required for live.
  108. * @export
  109. */
  110. setPresentationStartTime(presentationStartTime) {
  111. goog.asserts.assert(presentationStartTime >= 0,
  112. 'presentationStartTime must be >= 0');
  113. this.presentationStartTime_ = presentationStartTime;
  114. }
  115. /**
  116. * Sets the presentation's duration.
  117. *
  118. * @param {number} duration The presentation's duration in seconds.
  119. * Infinity indicates that the presentation continues indefinitely.
  120. * @export
  121. */
  122. setDuration(duration) {
  123. goog.asserts.assert(duration > 0, 'duration must be > 0');
  124. this.duration_ = duration;
  125. }
  126. /**
  127. * @return {?number} The presentation's start time in seconds.
  128. * @export
  129. */
  130. getPresentationStartTime() {
  131. return this.presentationStartTime_;
  132. }
  133. /**
  134. * Sets the clock offset, which is the difference between the client's clock
  135. * and the server's clock, in milliseconds (i.e., serverTime = Date.now() +
  136. * clockOffset).
  137. *
  138. * @param {number} offset The clock offset, in ms.
  139. * @export
  140. */
  141. setClockOffset(offset) {
  142. this.clockOffset_ = offset;
  143. }
  144. /**
  145. * Sets the presentation's static flag.
  146. *
  147. * @param {boolean} isStatic If true, the presentation is static, meaning all
  148. * segments are available at once.
  149. * @export
  150. */
  151. setStatic(isStatic) {
  152. // NOTE: the argument name is not "static" because that's a keyword in ES6
  153. if (isStatic && !this.static_) {
  154. this.isLive2VodTransition_ = true;
  155. }
  156. this.static_ = isStatic;
  157. }
  158. /**
  159. * Sets the presentation's segment availability duration. The segment
  160. * availability duration should only be set for live.
  161. *
  162. * @param {number} segmentAvailabilityDuration The presentation's new segment
  163. * availability duration in seconds.
  164. * @export
  165. */
  166. setSegmentAvailabilityDuration(segmentAvailabilityDuration) {
  167. goog.asserts.assert(segmentAvailabilityDuration >= 0,
  168. 'segmentAvailabilityDuration must be >= 0');
  169. this.segmentAvailabilityDuration_ = segmentAvailabilityDuration;
  170. }
  171. /**
  172. * Gets the presentation's segment availability duration.
  173. *
  174. * @return {number}
  175. * @export
  176. */
  177. getSegmentAvailabilityDuration() {
  178. return this.segmentAvailabilityDuration_;
  179. }
  180. /**
  181. * Sets the presentation delay in seconds.
  182. *
  183. * @param {number} delay
  184. * @export
  185. */
  186. setDelay(delay) {
  187. // NOTE: This is no longer used internally, but is exported.
  188. // So we cannot remove it without deprecating it and waiting one release
  189. // cycle, or else we risk breaking custom manifest parsers.
  190. goog.asserts.assert(delay >= 0, 'delay must be >= 0');
  191. this.presentationDelay_ = delay;
  192. }
  193. /**
  194. * Gets the presentation delay in seconds.
  195. * @return {number}
  196. * @export
  197. */
  198. getDelay() {
  199. return this.presentationDelay_;
  200. }
  201. /**
  202. * Gives PresentationTimeline a Stream's timeline so it can size and position
  203. * the segment availability window, and account for missing segment
  204. * information.
  205. *
  206. * @param {!Array.<shaka.media.PresentationTimeline.TimeRange>} timeline
  207. * @param {number} startOffset
  208. * @export
  209. */
  210. notifyTimeRange(timeline, startOffset) {
  211. if (timeline.length == 0) {
  212. return;
  213. }
  214. const firstStartTime = timeline[0].start + startOffset;
  215. const lastEndTime = timeline[timeline.length - 1].end + startOffset;
  216. this.notifyMinSegmentStartTime(firstStartTime);
  217. this.maxSegmentDuration_ = timeline.reduce(
  218. (max, r) => { return Math.max(max, r.end - r.start); },
  219. this.maxSegmentDuration_);
  220. this.maxSegmentEndTime_ =
  221. Math.max(this.maxSegmentEndTime_, lastEndTime);
  222. if (this.presentationStartTime_ != null && this.autoCorrectDrift_ &&
  223. !this.startTimeLocked_) {
  224. // Since we have explicit segment end times, calculate a presentation
  225. // start based on them. This start time accounts for drift.
  226. // Date.now() is in milliseconds, from which we compute "now" in seconds.
  227. const now = (Date.now() + this.clockOffset_) / 1000.0;
  228. this.presentationStartTime_ =
  229. now - this.maxSegmentEndTime_ - this.maxSegmentDuration_;
  230. }
  231. shaka.log.v1('notifySegments:',
  232. 'maxSegmentDuration=' + this.maxSegmentDuration_);
  233. }
  234. /**
  235. * Gives PresentationTimeline an array of segments so it can size and position
  236. * the segment availability window, and account for missing segment
  237. * information. These segments do not necessarily need to all be from the
  238. * same stream.
  239. *
  240. * @param {!Array.<!shaka.media.SegmentReference>} references
  241. * @export
  242. */
  243. notifySegments(references) {
  244. if (references.length == 0) {
  245. return;
  246. }
  247. let firstReferenceStartTime = references[0].startTime;
  248. let lastReferenceEndTime = references[0].endTime;
  249. // Date.now() is in milliseconds, from which we compute "now" in seconds.
  250. const now = (Date.now() + this.clockOffset_) / 1000.0;
  251. for (const reference of references) {
  252. // Exclude segments that are in the "future".
  253. if (now < reference.startTime) {
  254. continue;
  255. }
  256. firstReferenceStartTime = Math.min(
  257. firstReferenceStartTime, reference.startTime);
  258. lastReferenceEndTime = Math.max(lastReferenceEndTime, reference.endTime);
  259. this.maxSegmentDuration_ = Math.max(
  260. this.maxSegmentDuration_, reference.endTime - reference.startTime);
  261. }
  262. this.notifyMinSegmentStartTime(firstReferenceStartTime);
  263. this.maxSegmentEndTime_ =
  264. Math.max(this.maxSegmentEndTime_, lastReferenceEndTime);
  265. if (this.presentationStartTime_ != null && this.autoCorrectDrift_ &&
  266. !this.startTimeLocked_) {
  267. // Since we have explicit segment end times, calculate a presentation
  268. // start based on them. This start time accounts for drift.
  269. this.presentationStartTime_ =
  270. now - this.maxSegmentEndTime_ - this.maxSegmentDuration_;
  271. }
  272. shaka.log.v1('notifySegments:',
  273. 'maxSegmentDuration=' + this.maxSegmentDuration_);
  274. }
  275. /**
  276. * Lock the presentation timeline's start time. After this is called, no
  277. * further adjustments to presentationStartTime_ will be permitted.
  278. *
  279. * This should be called after all Periods have been parsed, and all calls to
  280. * notifySegments() from the initial manifest parse have been made.
  281. *
  282. * Without this, we can get assertion failures in SegmentIndex for certain
  283. * DAI content. If DAI adds ad segments to the manifest faster than
  284. * real-time, adjustments to presentationStartTime_ can cause availability
  285. * windows to jump around on updates.
  286. *
  287. * @export
  288. */
  289. lockStartTime() {
  290. this.startTimeLocked_ = true;
  291. }
  292. /**
  293. * Returns if the presentation timeline's start time is locked.
  294. *
  295. * @return {boolean}
  296. * @export
  297. */
  298. isStartTimeLocked() {
  299. return this.startTimeLocked_;
  300. }
  301. /**
  302. * Sets the initial program date time.
  303. *
  304. * @param {number} initialProgramDateTime
  305. * @export
  306. */
  307. setInitialProgramDateTime(initialProgramDateTime) {
  308. this.initialProgramDateTime_ = initialProgramDateTime;
  309. }
  310. /**
  311. * @return {?number} The initial program date time in seconds.
  312. * @export
  313. */
  314. getInitialProgramDateTime() {
  315. return this.initialProgramDateTime_;
  316. }
  317. /**
  318. * Gives PresentationTimeline a Stream's minimum segment start time.
  319. *
  320. * @param {number} startTime
  321. * @export
  322. */
  323. notifyMinSegmentStartTime(startTime) {
  324. if (this.minSegmentStartTime_ == null) {
  325. // No data yet, and Math.min(null, startTime) is always 0. So just store
  326. // startTime.
  327. this.minSegmentStartTime_ = startTime;
  328. } else if (!this.isLive2VodTransition_) {
  329. this.minSegmentStartTime_ =
  330. Math.min(this.minSegmentStartTime_, startTime);
  331. }
  332. }
  333. /**
  334. * Gives PresentationTimeline a Stream's maximum segment duration so it can
  335. * size and position the segment availability window. This function should be
  336. * called once for each Stream (no more, no less), but does not have to be
  337. * called if notifySegments() is called instead for a particular stream.
  338. *
  339. * @param {number} maxSegmentDuration The maximum segment duration for a
  340. * particular stream.
  341. * @export
  342. */
  343. notifyMaxSegmentDuration(maxSegmentDuration) {
  344. this.maxSegmentDuration_ = Math.max(
  345. this.maxSegmentDuration_, maxSegmentDuration);
  346. shaka.log.v1('notifyNewSegmentDuration:',
  347. 'maxSegmentDuration=' + this.maxSegmentDuration_);
  348. }
  349. /**
  350. * Offsets the segment times by the given amount.
  351. *
  352. * @param {number} offset The number of seconds to offset by. A positive
  353. * number adjusts the segment times forward.
  354. * @export
  355. */
  356. offset(offset) {
  357. if (this.minSegmentStartTime_ != null) {
  358. this.minSegmentStartTime_ += offset;
  359. }
  360. if (this.maxSegmentEndTime_ != null) {
  361. this.maxSegmentEndTime_ += offset;
  362. }
  363. }
  364. /**
  365. * @return {boolean} True if the presentation is live; otherwise, return
  366. * false.
  367. * @export
  368. */
  369. isLive() {
  370. return this.duration_ == Infinity &&
  371. !this.static_;
  372. }
  373. /**
  374. * @return {boolean} True if the presentation is in progress (meaning not
  375. * live, but also not completely available); otherwise, return false.
  376. * @export
  377. */
  378. isInProgress() {
  379. return this.duration_ != Infinity &&
  380. !this.static_;
  381. }
  382. /**
  383. * Gets the presentation's current segment availability start time. Segments
  384. * ending at or before this time should be assumed to be unavailable.
  385. *
  386. * @return {number} The current segment availability start time, in seconds,
  387. * relative to the start of the presentation.
  388. * @export
  389. */
  390. getSegmentAvailabilityStart() {
  391. goog.asserts.assert(this.segmentAvailabilityDuration_ >= 0,
  392. 'The availability duration should be positive');
  393. const end = this.getSegmentAvailabilityEnd();
  394. const start = end - this.segmentAvailabilityDuration_;
  395. return Math.max(this.userSeekStart_, start);
  396. }
  397. /**
  398. * Sets the start time of the user-defined seek range. This is only used for
  399. * VOD content.
  400. *
  401. * @param {number} time
  402. * @export
  403. */
  404. setUserSeekStart(time) {
  405. this.userSeekStart_ = time;
  406. }
  407. /**
  408. * Gets the presentation's current segment availability end time. Segments
  409. * starting after this time should be assumed to be unavailable.
  410. *
  411. * @return {number} The current segment availability end time, in seconds,
  412. * relative to the start of the presentation. For VOD, the availability
  413. * end time is the content's duration. If the Player's playRangeEnd
  414. * configuration is used, this can override the duration.
  415. * @export
  416. */
  417. getSegmentAvailabilityEnd() {
  418. if (!this.isLive() && !this.isInProgress()) {
  419. // It's a static manifest (can also be a dynamic->static conversion)
  420. if (this.maxSegmentEndTime_) {
  421. // If we know segment times, use the min of that and duration.
  422. // Note that the playRangeEnd configuration changes this.duration_.
  423. // See https://github.com/shaka-project/shaka-player/issues/4026
  424. return Math.min(this.maxSegmentEndTime_, this.duration_);
  425. } else {
  426. // If we don't have segment times, use duration.
  427. return this.duration_;
  428. }
  429. }
  430. // Can be either live or "in-progress recording" (live with known duration)
  431. return Math.min(this.getLiveEdge_() + this.availabilityTimeOffset_,
  432. this.duration_);
  433. }
  434. /**
  435. * Gets the seek range start time, offset by the given amount. This is used
  436. * to ensure that we don't "fall" back out of the seek window while we are
  437. * buffering.
  438. *
  439. * @param {number} offset The offset to add to the start time for live
  440. * streams.
  441. * @return {number} The current seek start time, in seconds, relative to the
  442. * start of the presentation.
  443. * @export
  444. */
  445. getSafeSeekRangeStart(offset) {
  446. // The earliest known segment time, ignoring segment availability duration.
  447. const earliestSegmentTime =
  448. Math.max(this.minSegmentStartTime_, this.userSeekStart_);
  449. // For VOD, the offset and end time are ignored, and we just return the
  450. // earliest segment time. All segments are "safe" in VOD. However, we
  451. // should round up to the nearest millisecond to avoid issues like
  452. // https://github.com/shaka-project/shaka-player/issues/2831, in which we
  453. // tried to seek repeatedly to catch up to the seek range, and never
  454. // actually "arrived" within it. The video's currentTime is not as
  455. // accurate as the JS number representing the earliest segment time for
  456. // some content.
  457. if (this.segmentAvailabilityDuration_ == Infinity) {
  458. return Math.ceil(earliestSegmentTime * 1e3) / 1e3;
  459. }
  460. // AKA the live edge for live streams.
  461. const availabilityEnd = this.getSegmentAvailabilityEnd();
  462. // The ideal availability start, not considering known segments.
  463. const availabilityStart =
  464. availabilityEnd - this.segmentAvailabilityDuration_;
  465. // Add the offset to the availability start to ensure that we don't fall
  466. // outside the availability window while we buffer; we don't need to add the
  467. // offset to earliestSegmentTime since that won't change over time.
  468. // Also see: https://github.com/shaka-project/shaka-player/issues/692
  469. const desiredStart =
  470. Math.min(availabilityStart + offset, this.getSeekRangeEnd());
  471. return Math.max(earliestSegmentTime, desiredStart);
  472. }
  473. /**
  474. * Gets the seek range start time.
  475. *
  476. * @return {number}
  477. * @export
  478. */
  479. getSeekRangeStart() {
  480. return this.getSafeSeekRangeStart(/* offset= */ 0);
  481. }
  482. /**
  483. * Gets the seek range end.
  484. *
  485. * @return {number}
  486. * @export
  487. */
  488. getSeekRangeEnd() {
  489. const useDelay = this.isLive() || this.isInProgress();
  490. const delay = useDelay ? this.presentationDelay_ : 0;
  491. return Math.max(0, this.getSegmentAvailabilityEnd() - delay);
  492. }
  493. /**
  494. * True if the presentation start time is being used to calculate the live
  495. * edge.
  496. * Using the presentation start time means that the stream may be subject to
  497. * encoder drift. At runtime, we will avoid using the presentation start time
  498. * whenever possible.
  499. *
  500. * @return {boolean}
  501. * @export
  502. */
  503. usingPresentationStartTime() {
  504. // If it's VOD, IPR, or an HLS "event", we are not using the presentation
  505. // start time.
  506. if (this.presentationStartTime_ == null) {
  507. return false;
  508. }
  509. // If we have explicit segment times, we're not using the presentation
  510. // start time.
  511. if (this.maxSegmentEndTime_ != null && this.autoCorrectDrift_) {
  512. return false;
  513. }
  514. return true;
  515. }
  516. /**
  517. * @return {number} The current presentation time in seconds.
  518. * @private
  519. */
  520. getLiveEdge_() {
  521. goog.asserts.assert(this.presentationStartTime_ != null,
  522. 'Cannot compute timeline live edge without start time');
  523. // Date.now() is in milliseconds, from which we compute "now" in seconds.
  524. const now = (Date.now() + this.clockOffset_) / 1000.0;
  525. return Math.max(
  526. 0, now - this.maxSegmentDuration_ - this.presentationStartTime_);
  527. }
  528. /**
  529. * Sets the presentation's segment availability time offset. This should be
  530. * only set for Low Latency Dash.
  531. * The segments are available earlier for download than the availability start
  532. * time, so we can move closer to the live edge.
  533. *
  534. * @param {number} offset
  535. * @export
  536. */
  537. setAvailabilityTimeOffset(offset) {
  538. this.availabilityTimeOffset_ = offset;
  539. }
  540. /**
  541. * Debug only: assert that the timeline parameters make sense for the type
  542. * of presentation (VOD, IPR, live).
  543. */
  544. assertIsValid() {
  545. if (goog.DEBUG) {
  546. if (this.isLive()) {
  547. // Implied by isLive(): infinite and dynamic.
  548. // Live streams should have a start time.
  549. goog.asserts.assert(this.presentationStartTime_ != null,
  550. 'Detected as live stream, but does not match our model of live!');
  551. } else if (this.isInProgress()) {
  552. // Implied by isInProgress(): finite and dynamic.
  553. // IPR streams should have a start time, and segments should not expire.
  554. goog.asserts.assert(this.presentationStartTime_ != null &&
  555. this.segmentAvailabilityDuration_ == Infinity,
  556. 'Detected as IPR stream, but does not match our model of IPR!');
  557. } else { // VOD
  558. // VOD segments should not expire and the presentation should be finite
  559. // and static.
  560. goog.asserts.assert(this.segmentAvailabilityDuration_ == Infinity &&
  561. this.duration_ != Infinity &&
  562. this.static_,
  563. 'Detected as VOD stream, but does not match our model of VOD!');
  564. }
  565. }
  566. }
  567. };
  568. /**
  569. * @typedef {{
  570. * start: number,
  571. * unscaledStart: number,
  572. * end: number,
  573. * partialSegments: number,
  574. * segmentPosition: number
  575. * }}
  576. *
  577. * @description
  578. * Defines a time range of a media segment. Times are in seconds.
  579. *
  580. * @property {number} start
  581. * The start time of the range.
  582. * @property {number} unscaledStart
  583. * The start time of the range in representation timescale units.
  584. * @property {number} end
  585. * The end time (exclusive) of the range.
  586. * @property {number} partialSegments
  587. * The number of partial segments
  588. * @property {number} segmentPosition
  589. * The segment position of the timeline entry as it appears in the manifest
  590. *
  591. * @export
  592. */
  593. shaka.media.PresentationTimeline.TimeRange;