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. const firstStartTime = timeline[0].start + startOffset;
  210. const lastEndTime = timeline[timeline.length - 1].end + startOffset;
  211. this.notifyMinSegmentStartTime(firstStartTime);
  212. this.maxSegmentDuration_ = timeline.reduce(
  213. (max, r) => { return Math.max(max, r.end - r.start); },
  214. this.maxSegmentDuration_);
  215. this.maxSegmentEndTime_ =
  216. Math.max(this.maxSegmentEndTime_, lastEndTime);
  217. if (this.presentationStartTime_ != null && this.autoCorrectDrift_ &&
  218. !this.startTimeLocked_) {
  219. // Since we have explicit segment end times, calculate a presentation
  220. // start based on them. This start time accounts for drift.
  221. // Date.now() is in milliseconds, from which we compute "now" in seconds.
  222. const now = (Date.now() + this.clockOffset_) / 1000.0;
  223. this.presentationStartTime_ =
  224. now - this.maxSegmentEndTime_ - this.maxSegmentDuration_;
  225. }
  226. shaka.log.v1('notifySegments:',
  227. 'maxSegmentDuration=' + this.maxSegmentDuration_);
  228. }
  229. /**
  230. * Gives PresentationTimeline an array of segments so it can size and position
  231. * the segment availability window, and account for missing segment
  232. * information. These segments do not necessarily need to all be from the
  233. * same stream.
  234. *
  235. * @param {!Array.<!shaka.media.SegmentReference>} references
  236. * @export
  237. */
  238. notifySegments(references) {
  239. if (references.length == 0) {
  240. return;
  241. }
  242. let firstReferenceStartTime = references[0].startTime;
  243. let lastReferenceEndTime = references[0].endTime;
  244. // Date.now() is in milliseconds, from which we compute "now" in seconds.
  245. const now = (Date.now() + this.clockOffset_) / 1000.0;
  246. for (const reference of references) {
  247. // Exclude segments that are in the "future".
  248. if (now < reference.startTime) {
  249. continue;
  250. }
  251. firstReferenceStartTime = Math.min(
  252. firstReferenceStartTime, reference.startTime);
  253. lastReferenceEndTime = Math.max(lastReferenceEndTime, reference.endTime);
  254. this.maxSegmentDuration_ = Math.max(
  255. this.maxSegmentDuration_, reference.endTime - reference.startTime);
  256. }
  257. this.notifyMinSegmentStartTime(firstReferenceStartTime);
  258. this.maxSegmentEndTime_ =
  259. Math.max(this.maxSegmentEndTime_, lastReferenceEndTime);
  260. if (this.presentationStartTime_ != null && this.autoCorrectDrift_ &&
  261. !this.startTimeLocked_) {
  262. // Since we have explicit segment end times, calculate a presentation
  263. // start based on them. This start time accounts for drift.
  264. this.presentationStartTime_ =
  265. now - this.maxSegmentEndTime_ - this.maxSegmentDuration_;
  266. }
  267. shaka.log.v1('notifySegments:',
  268. 'maxSegmentDuration=' + this.maxSegmentDuration_);
  269. }
  270. /**
  271. * Lock the presentation timeline's start time. After this is called, no
  272. * further adjustments to presentationStartTime_ will be permitted.
  273. *
  274. * This should be called after all Periods have been parsed, and all calls to
  275. * notifySegments() from the initial manifest parse have been made.
  276. *
  277. * Without this, we can get assertion failures in SegmentIndex for certain
  278. * DAI content. If DAI adds ad segments to the manifest faster than
  279. * real-time, adjustments to presentationStartTime_ can cause availability
  280. * windows to jump around on updates.
  281. *
  282. * @export
  283. */
  284. lockStartTime() {
  285. this.startTimeLocked_ = true;
  286. }
  287. /**
  288. * Returns if the presentation timeline's start time is locked.
  289. *
  290. * @return {boolean}
  291. * @export
  292. */
  293. isStartTimeLocked() {
  294. return this.startTimeLocked_;
  295. }
  296. /**
  297. * Sets the initial program date time.
  298. *
  299. * @param {number} initialProgramDateTime
  300. * @export
  301. */
  302. setInitialProgramDateTime(initialProgramDateTime) {
  303. this.initialProgramDateTime_ = initialProgramDateTime;
  304. }
  305. /**
  306. * @return {?number} The initial program date time in seconds.
  307. * @export
  308. */
  309. getInitialProgramDateTime() {
  310. return this.initialProgramDateTime_;
  311. }
  312. /**
  313. * Gives PresentationTimeline a Stream's minimum segment start time.
  314. *
  315. * @param {number} startTime
  316. * @export
  317. */
  318. notifyMinSegmentStartTime(startTime) {
  319. if (this.minSegmentStartTime_ == null) {
  320. // No data yet, and Math.min(null, startTime) is always 0. So just store
  321. // startTime.
  322. this.minSegmentStartTime_ = startTime;
  323. } else {
  324. this.minSegmentStartTime_ =
  325. Math.min(this.minSegmentStartTime_, startTime);
  326. }
  327. }
  328. /**
  329. * Gives PresentationTimeline a Stream's maximum segment duration so it can
  330. * size and position the segment availability window. This function should be
  331. * called once for each Stream (no more, no less), but does not have to be
  332. * called if notifySegments() is called instead for a particular stream.
  333. *
  334. * @param {number} maxSegmentDuration The maximum segment duration for a
  335. * particular stream.
  336. * @export
  337. */
  338. notifyMaxSegmentDuration(maxSegmentDuration) {
  339. this.maxSegmentDuration_ = Math.max(
  340. this.maxSegmentDuration_, maxSegmentDuration);
  341. shaka.log.v1('notifyNewSegmentDuration:',
  342. 'maxSegmentDuration=' + this.maxSegmentDuration_);
  343. }
  344. /**
  345. * Offsets the segment times by the given amount.
  346. *
  347. * @param {number} offset The number of seconds to offset by. A positive
  348. * number adjusts the segment times forward.
  349. * @export
  350. */
  351. offset(offset) {
  352. if (this.minSegmentStartTime_ != null) {
  353. this.minSegmentStartTime_ += offset;
  354. }
  355. if (this.maxSegmentEndTime_ != null) {
  356. this.maxSegmentEndTime_ += offset;
  357. }
  358. }
  359. /**
  360. * @return {boolean} True if the presentation is live; otherwise, return
  361. * false.
  362. * @export
  363. */
  364. isLive() {
  365. return this.duration_ == Infinity &&
  366. !this.static_;
  367. }
  368. /**
  369. * @return {boolean} True if the presentation is in progress (meaning not
  370. * live, but also not completely available); otherwise, return false.
  371. * @export
  372. */
  373. isInProgress() {
  374. return this.duration_ != Infinity &&
  375. !this.static_;
  376. }
  377. /**
  378. * Gets the presentation's current segment availability start time. Segments
  379. * ending at or before this time should be assumed to be unavailable.
  380. *
  381. * @return {number} The current segment availability start time, in seconds,
  382. * relative to the start of the presentation.
  383. * @export
  384. */
  385. getSegmentAvailabilityStart() {
  386. goog.asserts.assert(this.segmentAvailabilityDuration_ >= 0,
  387. 'The availability duration should be positive');
  388. const end = this.getSegmentAvailabilityEnd();
  389. const start = end - this.segmentAvailabilityDuration_;
  390. return Math.max(this.userSeekStart_, start);
  391. }
  392. /**
  393. * Sets the start time of the user-defined seek range. This is only used for
  394. * VOD content.
  395. *
  396. * @param {number} time
  397. * @export
  398. */
  399. setUserSeekStart(time) {
  400. this.userSeekStart_ = time;
  401. }
  402. /**
  403. * Gets the presentation's current segment availability end time. Segments
  404. * starting after this time should be assumed to be unavailable.
  405. *
  406. * @return {number} The current segment availability end time, in seconds,
  407. * relative to the start of the presentation. For VOD, the availability
  408. * end time is the content's duration. If the Player's playRangeEnd
  409. * configuration is used, this can override the duration.
  410. * @export
  411. */
  412. getSegmentAvailabilityEnd() {
  413. if (!this.isLive() && !this.isInProgress()) {
  414. // It's a static manifest (can also be a dynamic->static conversion)
  415. if (this.maxSegmentEndTime_) {
  416. // If we know segment times, use the min of that and duration.
  417. // Note that the playRangeEnd configuration changes this.duration_.
  418. // See https://github.com/shaka-project/shaka-player/issues/4026
  419. return Math.min(this.maxSegmentEndTime_, this.duration_);
  420. } else {
  421. // If we don't have segment times, use duration.
  422. return this.duration_;
  423. }
  424. }
  425. // Can be either live or "in-progress recording" (live with known duration)
  426. return Math.min(this.getLiveEdge_() + this.availabilityTimeOffset_,
  427. this.duration_);
  428. }
  429. /**
  430. * Gets the seek range start time, offset by the given amount. This is used
  431. * to ensure that we don't "fall" back out of the seek window while we are
  432. * buffering.
  433. *
  434. * @param {number} offset The offset to add to the start time for live
  435. * streams.
  436. * @return {number} The current seek start time, in seconds, relative to the
  437. * start of the presentation.
  438. * @export
  439. */
  440. getSafeSeekRangeStart(offset) {
  441. // The earliest known segment time, ignoring segment availability duration.
  442. const earliestSegmentTime =
  443. Math.max(this.minSegmentStartTime_, this.userSeekStart_);
  444. // For VOD, the offset and end time are ignored, and we just return the
  445. // earliest segment time. All segments are "safe" in VOD. However, we
  446. // should round up to the nearest millisecond to avoid issues like
  447. // https://github.com/shaka-project/shaka-player/issues/2831, in which we
  448. // tried to seek repeatedly to catch up to the seek range, and never
  449. // actually "arrived" within it. The video's currentTime is not as
  450. // accurate as the JS number representing the earliest segment time for
  451. // some content.
  452. if (this.segmentAvailabilityDuration_ == Infinity) {
  453. return Math.ceil(earliestSegmentTime * 1e3) / 1e3;
  454. }
  455. // AKA the live edge for live streams.
  456. const availabilityEnd = this.getSegmentAvailabilityEnd();
  457. // The ideal availability start, not considering known segments.
  458. const availabilityStart =
  459. availabilityEnd - this.segmentAvailabilityDuration_;
  460. // Add the offset to the availability start to ensure that we don't fall
  461. // outside the availability window while we buffer; we don't need to add the
  462. // offset to earliestSegmentTime since that won't change over time.
  463. // Also see: https://github.com/shaka-project/shaka-player/issues/692
  464. const desiredStart =
  465. Math.min(availabilityStart + offset, this.getSeekRangeEnd());
  466. return Math.max(earliestSegmentTime, desiredStart);
  467. }
  468. /**
  469. * Gets the seek range start time.
  470. *
  471. * @return {number}
  472. * @export
  473. */
  474. getSeekRangeStart() {
  475. return this.getSafeSeekRangeStart(/* offset= */ 0);
  476. }
  477. /**
  478. * Gets the seek range end.
  479. *
  480. * @return {number}
  481. * @export
  482. */
  483. getSeekRangeEnd() {
  484. const useDelay = this.isLive() || this.isInProgress();
  485. const delay = useDelay ? this.presentationDelay_ : 0;
  486. return Math.max(0, this.getSegmentAvailabilityEnd() - delay);
  487. }
  488. /**
  489. * True if the presentation start time is being used to calculate the live
  490. * edge.
  491. * Using the presentation start time means that the stream may be subject to
  492. * encoder drift. At runtime, we will avoid using the presentation start time
  493. * whenever possible.
  494. *
  495. * @return {boolean}
  496. * @export
  497. */
  498. usingPresentationStartTime() {
  499. // If it's VOD, IPR, or an HLS "event", we are not using the presentation
  500. // start time.
  501. if (this.presentationStartTime_ == null) {
  502. return false;
  503. }
  504. // If we have explicit segment times, we're not using the presentation
  505. // start time.
  506. if (this.maxSegmentEndTime_ != null && this.autoCorrectDrift_) {
  507. return false;
  508. }
  509. return true;
  510. }
  511. /**
  512. * @return {number} The current presentation time in seconds.
  513. * @private
  514. */
  515. getLiveEdge_() {
  516. goog.asserts.assert(this.presentationStartTime_ != null,
  517. 'Cannot compute timeline live edge without start time');
  518. // Date.now() is in milliseconds, from which we compute "now" in seconds.
  519. const now = (Date.now() + this.clockOffset_) / 1000.0;
  520. return Math.max(
  521. 0, now - this.maxSegmentDuration_ - this.presentationStartTime_);
  522. }
  523. /**
  524. * Sets the presentation's segment availability time offset. This should be
  525. * only set for Low Latency Dash.
  526. * The segments are available earlier for download than the availability start
  527. * time, so we can move closer to the live edge.
  528. *
  529. * @param {number} offset
  530. * @export
  531. */
  532. setAvailabilityTimeOffset(offset) {
  533. this.availabilityTimeOffset_ = offset;
  534. }
  535. /**
  536. * Debug only: assert that the timeline parameters make sense for the type
  537. * of presentation (VOD, IPR, live).
  538. */
  539. assertIsValid() {
  540. if (goog.DEBUG) {
  541. if (this.isLive()) {
  542. // Implied by isLive(): infinite and dynamic.
  543. // Live streams should have a start time.
  544. goog.asserts.assert(this.presentationStartTime_ != null,
  545. 'Detected as live stream, but does not match our model of live!');
  546. } else if (this.isInProgress()) {
  547. // Implied by isInProgress(): finite and dynamic.
  548. // IPR streams should have a start time, and segments should not expire.
  549. goog.asserts.assert(this.presentationStartTime_ != null &&
  550. this.segmentAvailabilityDuration_ == Infinity,
  551. 'Detected as IPR stream, but does not match our model of IPR!');
  552. } else { // VOD
  553. // VOD segments should not expire and the presentation should be finite
  554. // and static.
  555. goog.asserts.assert(this.segmentAvailabilityDuration_ == Infinity &&
  556. this.duration_ != Infinity &&
  557. this.static_,
  558. 'Detected as VOD stream, but does not match our model of VOD!');
  559. }
  560. }
  561. }
  562. };
  563. /**
  564. * @typedef {{
  565. * start: number,
  566. * unscaledStart: number,
  567. * end: number,
  568. * partialSegments: number,
  569. * segmentPosition: number
  570. * }}
  571. *
  572. * @description
  573. * Defines a time range of a media segment. Times are in seconds.
  574. *
  575. * @property {number} start
  576. * The start time of the range.
  577. * @property {number} unscaledStart
  578. * The start time of the range in representation timescale units.
  579. * @property {number} end
  580. * The end time (exclusive) of the range.
  581. * @property {number} partialSegments
  582. * The number of partial segments
  583. * @property {number} segmentPosition
  584. * The segment position of the timeline entry as it appears in the manifest
  585. *
  586. * @export
  587. */
  588. shaka.media.PresentationTimeline.TimeRange;