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