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