UNPKG

shaka-player

Version:
743 lines (633 loc) 22.3 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shaka.media.PresentationTimeline'); goog.require('goog.asserts'); goog.require('shaka.log'); goog.require('shaka.media.SegmentReference'); /** * PresentationTimeline. * @export */ shaka.media.PresentationTimeline = class { /** * @param {?number} presentationStartTime The wall-clock time, in seconds, * when the presentation started or will start. Only required for live. * @param {number} presentationDelay The delay to give the presentation, in * seconds. Only required for live. * @param {boolean=} autoCorrectDrift Whether to account for drift when * determining the availability window. * * @see {shaka.extern.Manifest} * @see {@tutorial architecture} */ constructor(presentationStartTime, presentationDelay, autoCorrectDrift = true) { /** @private {?number} */ this.presentationStartTime_ = presentationStartTime; /** @private {number} */ this.presentationDelay_ = presentationDelay; /** @private {number} */ this.duration_ = Infinity; /** @private {number} */ this.segmentAvailabilityDuration_ = Infinity; /** * The maximum segment duration (in seconds). Can be based on explicitly- * known segments or on signalling in the manifest. * * @private {number} */ this.maxSegmentDuration_ = 1; /** * The minimum segment start time (in seconds, in the presentation timeline) * for segments we explicitly know about. * * This is null if we have no explicit descriptions of segments, such as in * DASH when using SegmentTemplate w/ duration. * * @private {?number} */ this.minSegmentStartTime_ = null; /** * The maximum segment end time (in seconds, in the presentation timeline) * for segments we explicitly know about. * * This is null if we have no explicit descriptions of segments, such as in * DASH when using SegmentTemplate w/ duration. When this is non-null, the * presentation start time is calculated from the segment end times. * * @private {?number} */ this.maxSegmentEndTime_ = null; /** @private {number} */ this.clockOffset_ = 0; /** @private {boolean} */ this.static_ = true; /** @private {boolean} */ this.isLive2VodTransition_ = false; /** @private {number} */ this.userSeekStart_ = 0; /** @private {boolean} */ this.autoCorrectDrift_ = autoCorrectDrift; /** * For low latency Dash, availabilityTimeOffset indicates a segment is * available for download earlier than its availability start time. * This field is the minimum availabilityTimeOffset value among the * segments. We reduce the distance from live edge by this value. * * @private {number} */ this.availabilityTimeOffset_ = 0; /** @private {boolean} */ this.startTimeLocked_ = false; /** @private {?number} */ this.initialProgramDateTime_ = presentationStartTime; } /** * @return {number} The presentation's duration in seconds. * Infinity indicates that the presentation continues indefinitely. * @export */ getDuration() { return this.duration_; } /** * @return {number} The presentation's max segment duration in seconds. * @export */ getMaxSegmentDuration() { return this.maxSegmentDuration_; } /** * Sets the presentation's start time. * * @param {number} presentationStartTime The wall-clock time, in seconds, * when the presentation started or will start. Only required for live. * @export */ setPresentationStartTime(presentationStartTime) { goog.asserts.assert(presentationStartTime >= 0, 'presentationStartTime must be >= 0'); this.presentationStartTime_ = presentationStartTime; } /** * Sets the presentation's duration. * * @param {number} duration The presentation's duration in seconds. * Infinity indicates that the presentation continues indefinitely. * @export */ setDuration(duration) { goog.asserts.assert(duration > 0, 'duration must be > 0'); this.duration_ = duration; } /** * @return {?number} The presentation's start time in seconds. * @export */ getPresentationStartTime() { return this.presentationStartTime_; } /** * Sets the clock offset, which is the difference between the client's clock * and the server's clock, in milliseconds (i.e., serverTime = Date.now() + * clockOffset). * * @param {number} offset The clock offset, in ms. * @export */ setClockOffset(offset) { this.clockOffset_ = offset; } /** * Sets the presentation's static flag. * * @param {boolean} isStatic If true, the presentation is static, meaning all * segments are available at once. * @export */ setStatic(isStatic) { // NOTE: the argument name is not "static" because that's a keyword in ES6 if (isStatic && !this.static_) { this.isLive2VodTransition_ = true; } this.static_ = isStatic; } /** * Sets the presentation's segment availability duration. The segment * availability duration should only be set for live. * * @param {number} segmentAvailabilityDuration The presentation's new segment * availability duration in seconds. * @export */ setSegmentAvailabilityDuration(segmentAvailabilityDuration) { goog.asserts.assert(segmentAvailabilityDuration >= 0, 'segmentAvailabilityDuration must be >= 0'); this.segmentAvailabilityDuration_ = segmentAvailabilityDuration; } /** * Gets the presentation's segment availability duration. * * @return {number} * @export */ getSegmentAvailabilityDuration() { return this.segmentAvailabilityDuration_; } /** * Sets the presentation delay in seconds. * * @param {number} delay * @export */ setDelay(delay) { // NOTE: This is no longer used internally, but is exported. // So we cannot remove it without deprecating it and waiting one release // cycle, or else we risk breaking custom manifest parsers. goog.asserts.assert(delay >= 0, 'delay must be >= 0'); this.presentationDelay_ = delay; } /** * Gets the presentation delay in seconds. * @return {number} * @export */ getDelay() { return this.presentationDelay_; } /** * Gives PresentationTimeline a Stream's timeline so it can size and position * the segment availability window, and account for missing segment * information. * * @param {!Array<shaka.media.PresentationTimeline.TimeRange>} timeline * @param {number} startOffset * @export */ notifyTimeRange(timeline, startOffset) { if (timeline.length == 0) { return; } // Date.now() is in milliseconds, from which we compute "now" in seconds. const now = (Date.now() + this.clockOffset_) / 1000.0; // Exclude time ranges that are in the "future". const timelineForStart = timeline.filter((timeRange) => timeRange.start + startOffset < now); if (timelineForStart.length == 0) { return; } const firstStartTime = timelineForStart[0].start + startOffset; const lastEndTime = timelineForStart[timelineForStart.length - 1].end + startOffset; this.notifyMinSegmentStartTime(firstStartTime); this.maxSegmentDuration_ = timelineForStart.reduce( (max, r) => { return Math.max(max, r.end - r.start); }, this.maxSegmentDuration_); this.maxSegmentEndTime_ = Math.max(this.maxSegmentEndTime_, lastEndTime); if (this.presentationStartTime_ != null && this.autoCorrectDrift_ && !this.startTimeLocked_) { // Since we have explicit segment end times, calculate a presentation // start based on them. This start time accounts for drift. this.presentationStartTime_ = now - this.maxSegmentEndTime_ - this.maxSegmentDuration_; } shaka.log.v1('notifySegments:', 'maxSegmentDuration=' + this.maxSegmentDuration_); } /** * Gives PresentationTimeline an array of segments so it can size and position * the segment availability window, and account for missing segment * information. These segments do not necessarily need to all be from the * same stream. * * @param {!Array<!shaka.media.SegmentReference>} references * @export */ notifySegments(references) { if (references.length == 0) { return; } let firstReferenceStartTime = references[0].startTime; let lastReferenceEndTime = references[0].endTime; // Date.now() is in milliseconds, from which we compute "now" in seconds. const now = (Date.now() + this.clockOffset_) / 1000.0; for (const reference of references) { // Exclude segments that are in the "future". if (now < reference.startTime) { continue; } firstReferenceStartTime = Math.min( firstReferenceStartTime, reference.startTime); lastReferenceEndTime = Math.max(lastReferenceEndTime, reference.endTime); this.maxSegmentDuration_ = Math.max( this.maxSegmentDuration_, reference.endTime - reference.startTime); } this.notifyMinSegmentStartTime(firstReferenceStartTime); this.maxSegmentEndTime_ = Math.max(this.maxSegmentEndTime_, lastReferenceEndTime); if (this.presentationStartTime_ != null && this.autoCorrectDrift_ && !this.startTimeLocked_) { // Since we have explicit segment end times, calculate a presentation // start based on them. This start time accounts for drift. this.presentationStartTime_ = now - this.maxSegmentEndTime_ - this.maxSegmentDuration_; } shaka.log.v1('notifySegments:', 'maxSegmentDuration=' + this.maxSegmentDuration_); } /** * Gives PresentationTimeline an startTime and endTime of the period. * This should be only set for Dash. * * @param {number} startTime * @param {number} endTime * @export */ notifyPeriodDuration(startTime, endTime) { this.notifyMinSegmentStartTime(startTime); if (endTime != Infinity && !this.isLive()) { this.maxSegmentEndTime_ = Math.max(this.maxSegmentEndTime_, endTime); } } /** * Gets the end time of the last available segment. * * @return {?number} * @export */ getMaxSegmentEndTime() { return this.maxSegmentEndTime_; } /** * Lock the presentation timeline's start time. After this is called, no * further adjustments to presentationStartTime_ will be permitted. * * This should be called after all Periods have been parsed, and all calls to * notifySegments() from the initial manifest parse have been made. * * Without this, we can get assertion failures in SegmentIndex for certain * DAI content. If DAI adds ad segments to the manifest faster than * real-time, adjustments to presentationStartTime_ can cause availability * windows to jump around on updates. * * @export */ lockStartTime() { this.startTimeLocked_ = true; } /** * Returns if the presentation timeline's start time is locked. * * @return {boolean} * @export */ isStartTimeLocked() { return this.startTimeLocked_; } /** * Sets the initial program date time. * * @param {number} initialProgramDateTime * @export */ setInitialProgramDateTime(initialProgramDateTime) { this.initialProgramDateTime_ = initialProgramDateTime; } /** * @return {?number} The initial program date time in seconds. * @export */ getInitialProgramDateTime() { return this.initialProgramDateTime_; } /** * Gives PresentationTimeline a Stream's minimum segment start time. * * @param {number} startTime * @export */ notifyMinSegmentStartTime(startTime) { if (this.minSegmentStartTime_ == null) { // No data yet, and Math.min(null, startTime) is always 0. So just store // startTime. this.minSegmentStartTime_ = startTime; } else if (!this.isLive2VodTransition_) { this.minSegmentStartTime_ = Math.min(this.minSegmentStartTime_, startTime); } } /** * Gives PresentationTimeline a Stream's maximum segment duration so it can * size and position the segment availability window. This function should be * called once for each Stream (no more, no less), but does not have to be * called if notifySegments() is called instead for a particular stream. * * @param {number} maxSegmentDuration The maximum segment duration for a * particular stream. * @export */ notifyMaxSegmentDuration(maxSegmentDuration) { this.maxSegmentDuration_ = Math.max( this.maxSegmentDuration_, maxSegmentDuration); shaka.log.v1('notifyNewSegmentDuration:', 'maxSegmentDuration=' + this.maxSegmentDuration_); } /** * Offsets the segment times by the given amount. * * @param {number} offset The number of seconds to offset by. A positive * number adjusts the segment times forward. * @export */ offset(offset) { if (this.minSegmentStartTime_ != null) { this.minSegmentStartTime_ += offset; } if (this.maxSegmentEndTime_ != null) { this.maxSegmentEndTime_ += offset; } } /** * @return {boolean} True if the presentation is live; otherwise, return * false. * @export */ isLive() { return this.duration_ == Infinity && !this.static_; } /** * @return {boolean} True if the presentation is in progress (meaning not * live, but also not completely available); otherwise, return false. * @export */ isInProgress() { return this.duration_ != Infinity && !this.static_; } /** * Gets the presentation's current segment availability start time. Segments * ending at or before this time should be assumed to be unavailable. * * @return {number} The current segment availability start time, in seconds, * relative to the start of the presentation. * @export */ getSegmentAvailabilityStart() { goog.asserts.assert(this.segmentAvailabilityDuration_ >= 0, 'The availability duration should be positive'); const end = this.getSegmentAvailabilityEnd(); const start = end - this.segmentAvailabilityDuration_; return Math.max(this.userSeekStart_, start); } /** * Sets the start time of the user-defined seek range. This is only used for * VOD content. * * @param {number} time * @export */ setUserSeekStart(time) { this.userSeekStart_ = time; } /** * Gets the presentation's current segment availability end time. Segments * starting after this time should be assumed to be unavailable. * * @return {number} The current segment availability end time, in seconds, * relative to the start of the presentation. For VOD, the availability * end time is the content's duration. If the Player's playRangeEnd * configuration is used, this can override the duration. * @export */ getSegmentAvailabilityEnd() { if (!this.isLive() && !this.isInProgress()) { // It's a static manifest (can also be a dynamic->static conversion) if (this.maxSegmentEndTime_) { // If we know segment times, use the min of that and duration. // Note that the playRangeEnd configuration changes this.duration_. // See https://github.com/shaka-project/shaka-player/issues/4026 return Math.min(this.maxSegmentEndTime_, this.duration_); } else { // If we don't have segment times, use duration. return this.duration_; } } // Can be either live or "in-progress recording" (live with known duration) return Math.min(this.getLiveEdge_() + this.availabilityTimeOffset_, this.duration_); } /** * Gets the seek range start time, offset by the given amount. This is used * to ensure that we don't "fall" back out of the seek window while we are * buffering. * * @param {number} offset The offset to add to the start time for live * streams. * @return {number} The current seek start time, in seconds, relative to the * start of the presentation. * @export */ getSafeSeekRangeStart(offset) { // The earliest known segment time, ignoring segment availability duration. const earliestSegmentTime = Math.max(this.minSegmentStartTime_, this.userSeekStart_); // For VOD, the offset and end time are ignored, and we just return the // earliest segment time. All segments are "safe" in VOD. However, we // should round up to the nearest millisecond to avoid issues like // https://github.com/shaka-project/shaka-player/issues/2831, in which we // tried to seek repeatedly to catch up to the seek range, and never // actually "arrived" within it. The video's currentTime is not as // accurate as the JS number representing the earliest segment time for // some content. if (this.segmentAvailabilityDuration_ == Infinity) { return Math.ceil(earliestSegmentTime * 1e3) / 1e3; } // AKA the live edge for live streams. const availabilityEnd = this.getSegmentAvailabilityEnd(); // The ideal availability start, not considering known segments. const availabilityStart = availabilityEnd - this.segmentAvailabilityDuration_; // Add the offset to the availability start to ensure that we don't fall // outside the availability window while we buffer; we don't need to add the // offset to earliestSegmentTime since that won't change over time. // Also see: https://github.com/shaka-project/shaka-player/issues/692 const desiredStart = Math.min(availabilityStart + offset, this.getSeekRangeEnd()); return Math.max(earliestSegmentTime, desiredStart); } /** * Gets the seek range start time. * * @return {number} * @export */ getSeekRangeStart() { return this.getSafeSeekRangeStart(/* offset= */ 0); } /** * Gets the seek range end. * * @return {number} * @export */ getSeekRangeEnd() { const useDelay = this.isLive() || this.isInProgress(); const delay = useDelay ? this.presentationDelay_ : 0; return Math.max(0, this.getSegmentAvailabilityEnd() - delay); } /** * True if the presentation start time is being used to calculate the live * edge. * Using the presentation start time means that the stream may be subject to * encoder drift. At runtime, we will avoid using the presentation start time * whenever possible. * * @return {boolean} * @export */ usingPresentationStartTime() { // If it's VOD, IPR, or an HLS "event", we are not using the presentation // start time. if (this.presentationStartTime_ == null) { return false; } // If we have explicit segment times, we're not using the presentation // start time. if (this.maxSegmentEndTime_ != null && this.autoCorrectDrift_) { return false; } return true; } /** * @return {number} The current presentation time in seconds. * @private */ getLiveEdge_() { goog.asserts.assert(this.presentationStartTime_ != null, 'Cannot compute timeline live edge without start time'); // Date.now() is in milliseconds, from which we compute "now" in seconds. const now = (Date.now() + this.clockOffset_) / 1000.0; return Math.max( 0, now - this.maxSegmentDuration_ - this.presentationStartTime_); } /** * Sets the presentation's segment availability time offset. This should be * only set for Low Latency Dash. * The segments are available earlier for download than the availability start * time, so we can move closer to the live edge. * * @param {number} offset * @export */ setAvailabilityTimeOffset(offset) { this.availabilityTimeOffset_ = offset; } /** * Gets the presentation's segment availability time offset. This should be * only configured for Low Latency Dash. * * @return {number} availabilityTimeOffset parameter * @export */ getAvailabilityTimeOffset() { return this.availabilityTimeOffset_; } /** * Debug only: assert that the timeline parameters make sense for the type * of presentation (VOD, IPR, live). */ assertIsValid() { if (goog.DEBUG) { if (this.isLive()) { // Implied by isLive(): infinite and dynamic. // Live streams should have a start time. goog.asserts.assert(this.presentationStartTime_ != null, 'Detected as live stream, but does not match our model of live!'); } else if (this.isInProgress()) { // Implied by isInProgress(): finite and dynamic. // IPR streams should have a start time, and segments should not expire. goog.asserts.assert(this.presentationStartTime_ != null && this.segmentAvailabilityDuration_ == Infinity, 'Detected as IPR stream, but does not match our model of IPR!'); } else { // VOD // VOD segments should not expire and the presentation should be finite // and static. goog.asserts.assert(this.segmentAvailabilityDuration_ == Infinity && this.duration_ != Infinity && this.static_, 'Detected as VOD stream, but does not match our model of VOD!'); } } } }; /** * @typedef {{ * start: number, * unscaledStart: number, * end: number, * partialSegments: number, * segmentPosition: number * }} * * @description * Defines a time range of a media segment. Times are in seconds. * * @property {number} start * The start time of the range. * @property {number} unscaledStart * The start time of the range in representation timescale units. * @property {number} end * The end time (exclusive) of the range. * @property {number} partialSegments * The number of partial segments * @property {number} segmentPosition * The segment position of the timeline entry as it appears in the manifest * * @export */ shaka.media.PresentationTimeline.TimeRange;