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