Source: lib/media/presentation_timeline.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.media.PresentationTimeline');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.log');
  20. goog.require('shaka.media.SegmentReference');
  21. /**
  22. * Creates a PresentationTimeline.
  23. *
  24. * @param {?number} presentationStartTime The wall-clock time, in seconds,
  25. * when the presentation started or will start. Only required for live.
  26. * @param {number} presentationDelay The delay to give the presentation, in
  27. * seconds. Only required for live.
  28. * @param {boolean=} autoCorrectDrift Whether to account for drift when
  29. * determining the availability window.
  30. *
  31. * @see {shaka.extern.Manifest}
  32. * @see {@tutorial architecture}
  33. *
  34. * @constructor
  35. * @struct
  36. * @export
  37. */
  38. shaka.media.PresentationTimeline = function(
  39. presentationStartTime, presentationDelay, autoCorrectDrift = true) {
  40. /** @private {?number} */
  41. this.presentationStartTime_ = presentationStartTime;
  42. /** @private {number} */
  43. this.presentationDelay_ = presentationDelay;
  44. /** @private {number} */
  45. this.duration_ = Infinity;
  46. /** @private {number} */
  47. this.segmentAvailabilityDuration_ = Infinity;
  48. /**
  49. * The maximum segment duration (in seconds). Can be based on explicitly-
  50. * known segments or on signalling in the manifest.
  51. *
  52. * @private {number}
  53. */
  54. this.maxSegmentDuration_ = 1;
  55. /**
  56. * The minimum segment start time (in seconds, in the presentation timeline)
  57. * for segments we explicitly know about.
  58. *
  59. * This is null if we have no explicit descriptions of segments, such as in
  60. * DASH when using SegmentTemplate w/ duration.
  61. *
  62. * @private {?number}
  63. */
  64. this.minSegmentStartTime_ = null;
  65. /**
  66. * The maximum segment end time (in seconds, in the presentation timeline) for
  67. * segments we explicitly know about.
  68. *
  69. * This is null if we have no explicit descriptions of segments, such as in
  70. * DASH when using SegmentTemplate w/ duration. When this is non-null, the
  71. * presentation start time is calculated from the segment end times.
  72. *
  73. * @private {?number}
  74. */
  75. this.maxSegmentEndTime_ = null;
  76. /** @private {number} */
  77. this.clockOffset_ = 0;
  78. /** @private {boolean} */
  79. this.static_ = true;
  80. /** @private {number} */
  81. this.userSeekStart_ = 0;
  82. /** @private {boolean} */
  83. this.autoCorrectDrift_ = autoCorrectDrift;
  84. };
  85. /**
  86. * @return {number} The presentation's duration in seconds.
  87. * Infinity indicates that the presentation continues indefinitely.
  88. * @export
  89. */
  90. shaka.media.PresentationTimeline.prototype.getDuration = function() {
  91. return this.duration_;
  92. };
  93. /**
  94. * @return {number} The presentation's max segment duration in seconds.
  95. */
  96. shaka.media.PresentationTimeline.prototype.getMaxSegmentDuration = function() {
  97. return this.maxSegmentDuration_;
  98. };
  99. /**
  100. * Sets the presentation's duration.
  101. *
  102. * @param {number} duration The presentation's duration in seconds.
  103. * Infinity indicates that the presentation continues indefinitely.
  104. * @export
  105. */
  106. shaka.media.PresentationTimeline.prototype.setDuration = function(duration) {
  107. goog.asserts.assert(duration > 0, 'duration must be > 0');
  108. this.duration_ = duration;
  109. };
  110. /**
  111. * @return {?number} The presentation's start time in seconds.
  112. * @export
  113. */
  114. shaka.media.PresentationTimeline.prototype.getPresentationStartTime =
  115. function() {
  116. return this.presentationStartTime_;
  117. };
  118. /**
  119. * Sets the clock offset, which is the difference between the client's clock
  120. * and the server's clock, in milliseconds (i.e., serverTime = Date.now() +
  121. * clockOffset).
  122. *
  123. * @param {number} offset The clock offset, in ms.
  124. * @export
  125. */
  126. shaka.media.PresentationTimeline.prototype.setClockOffset = function(offset) {
  127. this.clockOffset_ = offset;
  128. };
  129. /**
  130. * Sets the presentation's static flag.
  131. *
  132. * @param {boolean} isStatic If true, the presentation is static, meaning all
  133. * segments are available at once.
  134. * @export
  135. */
  136. shaka.media.PresentationTimeline.prototype.setStatic = function(isStatic) {
  137. // NOTE: the argument name is not "static" because that's a keyword in ES6
  138. this.static_ = isStatic;
  139. };
  140. /**
  141. * Sets the presentation's segment availability duration. The segment
  142. * availability duration should only be set for live.
  143. *
  144. * @param {number} segmentAvailabilityDuration The presentation's new segment
  145. * availability duration in seconds.
  146. * @export
  147. */
  148. shaka.media.PresentationTimeline.prototype.setSegmentAvailabilityDuration =
  149. function(segmentAvailabilityDuration) {
  150. goog.asserts.assert(segmentAvailabilityDuration >= 0,
  151. 'segmentAvailabilityDuration must be >= 0');
  152. this.segmentAvailabilityDuration_ = segmentAvailabilityDuration;
  153. };
  154. /**
  155. * Sets the presentation delay in seconds.
  156. *
  157. * @param {number} delay
  158. * @export
  159. */
  160. shaka.media.PresentationTimeline.prototype.setDelay = function(delay) {
  161. // NOTE: This is no longer used internally, but is exported.
  162. // So we cannot remove it without deprecating it and waiting one release
  163. // cycle, or else we risk breaking custom manifest parsers.
  164. goog.asserts.assert(delay >= 0, 'delay must be >= 0');
  165. this.presentationDelay_ = delay;
  166. };
  167. /**
  168. * Gets the presentation delay in seconds.
  169. * @return {number}
  170. * @export
  171. */
  172. shaka.media.PresentationTimeline.prototype.getDelay = function() {
  173. return this.presentationDelay_;
  174. };
  175. /**
  176. * Gives PresentationTimeline a Stream's segments so it can size and position
  177. * the segment availability window, and account for missing segment
  178. * information. This function should be called once for each Stream (no more,
  179. * no less).
  180. *
  181. * @param {!Array.<!shaka.media.SegmentReference>} references
  182. * @param {number} periodStart
  183. * @export
  184. */
  185. shaka.media.PresentationTimeline.prototype.notifySegments = function(
  186. references, periodStart) {
  187. if (references.length == 0) {
  188. return;
  189. }
  190. // TODO: Make SegmentReferences use timestamps in the presentation timeline,
  191. // not the period timeline.
  192. const firstReferenceStartTime = references[0].startTime + periodStart;
  193. const lastReferenceEndTime =
  194. references[references.length - 1].endTime + periodStart;
  195. this.notifyMinSegmentStartTime(firstReferenceStartTime);
  196. this.maxSegmentDuration_ = references.reduce(
  197. function(max, r) { return Math.max(max, r.endTime - r.startTime); },
  198. this.maxSegmentDuration_);
  199. this.maxSegmentEndTime_ =
  200. Math.max(this.maxSegmentEndTime_, lastReferenceEndTime);
  201. if (this.presentationStartTime_ != null && this.autoCorrectDrift_) {
  202. // Since we have explicit segment end times, calculate a presentation start
  203. // based on them. This start time accounts for drift.
  204. // Date.now() is in milliseconds, from which we compute "now" in seconds.
  205. let now = (Date.now() + this.clockOffset_) / 1000.0;
  206. this.presentationStartTime_ =
  207. now - this.maxSegmentEndTime_ - this.maxSegmentDuration_;
  208. }
  209. shaka.log.v1('notifySegments:',
  210. 'maxSegmentDuration=' + this.maxSegmentDuration_);
  211. };
  212. /**
  213. * Gives PresentationTimeline a Stream's minimum segment start time.
  214. *
  215. * @param {number} startTime
  216. * @export
  217. */
  218. shaka.media.PresentationTimeline.prototype.notifyMinSegmentStartTime = function(
  219. startTime) {
  220. if (this.minSegmentStartTime_ == null) {
  221. // No data yet, and Math.min(null, startTime) is always 0. So just store
  222. // startTime.
  223. this.minSegmentStartTime_ = startTime;
  224. } else {
  225. this.minSegmentStartTime_ =
  226. Math.min(this.minSegmentStartTime_, startTime);
  227. }
  228. };
  229. /**
  230. * Gives PresentationTimeline a Stream's maximum segment duration so it can
  231. * size and position the segment availability window. This function should be
  232. * called once for each Stream (no more, no less), but does not have to be
  233. * called if notifySegments() is called instead for a particular stream.
  234. *
  235. * @param {number} maxSegmentDuration The maximum segment duration for a
  236. * particular stream.
  237. * @export
  238. */
  239. shaka.media.PresentationTimeline.prototype.notifyMaxSegmentDuration = function(
  240. maxSegmentDuration) {
  241. this.maxSegmentDuration_ = Math.max(
  242. this.maxSegmentDuration_, maxSegmentDuration);
  243. shaka.log.v1('notifyNewSegmentDuration:',
  244. 'maxSegmentDuration=' + this.maxSegmentDuration_);
  245. };
  246. /**
  247. * Offsets the segment times by the given amount.
  248. *
  249. * @param {number} offset The number of seconds to offset by. A positive number
  250. * adjusts the segment times forward.
  251. * @export
  252. */
  253. shaka.media.PresentationTimeline.prototype.offset = function(offset) {
  254. if (this.minSegmentStartTime_ != null) {
  255. this.minSegmentStartTime_ += offset;
  256. }
  257. if (this.maxSegmentEndTime_ != null) {
  258. this.maxSegmentEndTime_ += offset;
  259. }
  260. };
  261. /**
  262. * @return {boolean} True if the presentation is live; otherwise, return
  263. * false.
  264. * @export
  265. */
  266. shaka.media.PresentationTimeline.prototype.isLive = function() {
  267. return this.duration_ == Infinity &&
  268. !this.static_;
  269. };
  270. /**
  271. * @return {boolean} True if the presentation is in progress (meaning not live,
  272. * but also not completely available); otherwise, return false.
  273. * @export
  274. */
  275. shaka.media.PresentationTimeline.prototype.isInProgress = function() {
  276. return this.duration_ != Infinity &&
  277. !this.static_;
  278. };
  279. /**
  280. * Gets the presentation's current segment availability start time. Segments
  281. * ending at or before this time should be assumed to be unavailable.
  282. *
  283. * @return {number} The current segment availability start time, in seconds,
  284. * relative to the start of the presentation.
  285. * @export
  286. */
  287. shaka.media.PresentationTimeline.prototype.getSegmentAvailabilityStart =
  288. function() {
  289. goog.asserts.assert(this.segmentAvailabilityDuration_ >= 0,
  290. 'The availability duration should be positive');
  291. if (this.segmentAvailabilityDuration_ == Infinity) {
  292. return this.userSeekStart_;
  293. }
  294. let end = this.getSegmentAvailabilityEnd();
  295. let start = end - this.segmentAvailabilityDuration_;
  296. return Math.max(this.userSeekStart_, start);
  297. };
  298. /**
  299. * Sets the start time of the user-defined seek range. This is only used for
  300. * VOD content.
  301. *
  302. * @param {number} time
  303. * @export
  304. */
  305. shaka.media.PresentationTimeline.prototype.setUserSeekStart =
  306. function(time) {
  307. this.userSeekStart_ = time;
  308. };
  309. /**
  310. * Gets the presentation's current segment availability end time. Segments
  311. * starting after this time should be assumed to be unavailable.
  312. *
  313. * @return {number} The current segment availability end time, in seconds,
  314. * relative to the start of the presentation. Always returns the
  315. * presentation's duration for video-on-demand.
  316. * @export
  317. */
  318. shaka.media.PresentationTimeline.prototype.getSegmentAvailabilityEnd =
  319. function() {
  320. if (!this.isLive() && !this.isInProgress()) {
  321. return this.duration_;
  322. }
  323. return Math.min(this.getLiveEdge_(), this.duration_);
  324. };
  325. /**
  326. * Gets the seek range start time, offset by the given amount. This is used to
  327. * ensure that we don't "fall" back out of the seek window while we are
  328. * buffering.
  329. *
  330. * @param {number} offset The offset to add to the start time.
  331. * @return {number} The current seek start time, in seconds, relative to the
  332. * start of the presentation.
  333. * @export
  334. */
  335. shaka.media.PresentationTimeline.prototype.getSafeSeekRangeStart = function(
  336. offset) {
  337. // The earliest known segment time, ignoring segment availability duration.
  338. const earliestSegmentTime =
  339. Math.max(this.minSegmentStartTime_, this.userSeekStart_);
  340. if (this.segmentAvailabilityDuration_ == Infinity) {
  341. return earliestSegmentTime;
  342. }
  343. // AKA the live edge for live streams.
  344. const availabilityEnd = this.getSegmentAvailabilityEnd();
  345. // The ideal availability start, not considering known segments.
  346. const availabilityStart = availabilityEnd - this.segmentAvailabilityDuration_;
  347. // Add the offset to the availability start to ensure that we don't fall
  348. // outside the availability window while we buffer; we don't need to add the
  349. // offset to earliestSegmentTime since that won't change over time.
  350. // Also see: https://github.com/google/shaka-player/issues/692
  351. const desiredStart =
  352. Math.min(availabilityStart + offset, this.getSeekRangeEnd());
  353. return Math.max(earliestSegmentTime, desiredStart);
  354. };
  355. /**
  356. * Gets the seek range start time.
  357. *
  358. * @return {number}
  359. * @export
  360. */
  361. shaka.media.PresentationTimeline.prototype.getSeekRangeStart = function() {
  362. return this.getSafeSeekRangeStart(/* offset */ 0);
  363. };
  364. /**
  365. * Gets the seek range end.
  366. *
  367. * @return {number}
  368. * @export
  369. */
  370. shaka.media.PresentationTimeline.prototype.getSeekRangeEnd = function() {
  371. let useDelay = this.isLive() || this.isInProgress();
  372. let delay = useDelay ? this.presentationDelay_ : 0;
  373. return Math.max(0, this.getSegmentAvailabilityEnd() - delay);
  374. };
  375. /**
  376. * True if the presentation start time is being used to calculate the live edge.
  377. * Using the presentation start time means that the stream may be subject to
  378. * encoder drift. At runtime, we will avoid using the presentation start time
  379. * whenever possible.
  380. *
  381. * @return {boolean}
  382. * @export
  383. */
  384. shaka.media.PresentationTimeline.prototype.usingPresentationStartTime =
  385. function() {
  386. // If it's VOD, IPR, or an HLS "event", we are not using the presentation
  387. // start time.
  388. if (this.presentationStartTime_ == null) {
  389. return false;
  390. }
  391. // If we have explicit segment times, we're not using the presentation
  392. // start time.
  393. if (this.maxSegmentEndTime_ != null) {
  394. return false;
  395. }
  396. return true;
  397. };
  398. /**
  399. * @return {number} The current presentation time in seconds.
  400. * @private
  401. */
  402. shaka.media.PresentationTimeline.prototype.getLiveEdge_ = function() {
  403. goog.asserts.assert(this.presentationStartTime_ != null,
  404. 'Cannot compute timeline live edge without start time');
  405. // Date.now() is in milliseconds, from which we compute "now" in seconds.
  406. let now = (Date.now() + this.clockOffset_) / 1000.0;
  407. return Math.max(
  408. 0, now - this.maxSegmentDuration_ - this.presentationStartTime_);
  409. };
  410. if (goog.DEBUG) {
  411. /**
  412. * Debug only: assert that the timeline parameters make sense for the type of
  413. * presentation (VOD, IPR, live).
  414. */
  415. shaka.media.PresentationTimeline.prototype.assertIsValid = function() {
  416. if (this.isLive()) {
  417. // Implied by isLive(): infinite and dynamic.
  418. // Live streams should have a start time.
  419. goog.asserts.assert(this.presentationStartTime_ != null,
  420. 'Detected as live stream, but does not match our model of live!');
  421. } else if (this.isInProgress()) {
  422. // Implied by isInProgress(): finite and dynamic.
  423. // IPR streams should have a start time, and segments should not expire.
  424. goog.asserts.assert(this.presentationStartTime_ != null &&
  425. this.segmentAvailabilityDuration_ == Infinity,
  426. 'Detected as IPR stream, but does not match our model of IPR!');
  427. } else { // VOD
  428. // VOD segments should not expire and the presentation should be finite
  429. // and static.
  430. goog.asserts.assert(this.segmentAvailabilityDuration_ == Infinity &&
  431. this.duration_ != Infinity &&
  432. this.static_,
  433. 'Detected as VOD stream, but does not match our model of VOD!');
  434. }
  435. };
  436. }