mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-16 16:16:40 +03:00
3da809019b
Change-Id: I6861541b27153ba034364a5972a9b086de581cef
508 lines
15 KiB
JavaScript
508 lines
15 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2016 Google Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
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 {number} */
|
|
this.userSeekStart_ = 0;
|
|
|
|
/** @private {boolean} */
|
|
this.autoCorrectDrift_ = autoCorrectDrift;
|
|
}
|
|
|
|
|
|
/**
|
|
* @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.
|
|
*/
|
|
getMaxSegmentDuration() {
|
|
return this.maxSegmentDuration_;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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
|
|
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;
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 segments so it can size and position
|
|
* the segment availability window, and account for missing segment
|
|
* information. This function should be called once for each Stream (no more,
|
|
* no less).
|
|
*
|
|
* @param {!Array.<!shaka.media.SegmentReference>} references
|
|
* @param {number} periodStart
|
|
* @export
|
|
*/
|
|
notifySegments(
|
|
references, periodStart) {
|
|
if (references.length == 0) {
|
|
return;
|
|
}
|
|
|
|
// TODO: Make SegmentReferences use timestamps in the presentation timeline,
|
|
// not the period timeline.
|
|
const firstReferenceStartTime = references[0].startTime + periodStart;
|
|
const lastReferenceEndTime =
|
|
references[references.length - 1].endTime + periodStart;
|
|
|
|
this.notifyMinSegmentStartTime(firstReferenceStartTime);
|
|
|
|
this.maxSegmentDuration_ = references.reduce(
|
|
(max, r) => { return Math.max(max, r.endTime - r.startTime); },
|
|
this.maxSegmentDuration_);
|
|
|
|
this.maxSegmentEndTime_ =
|
|
Math.max(this.maxSegmentEndTime_, lastReferenceEndTime);
|
|
|
|
if (this.presentationStartTime_ != null && this.autoCorrectDrift_) {
|
|
// Since we have explicit segment end times, calculate a presentation
|
|
// start based on them. This start time accounts for drift.
|
|
// Date.now() is in milliseconds, from which we compute "now" in seconds.
|
|
const now = (Date.now() + this.clockOffset_) / 1000.0;
|
|
this.presentationStartTime_ =
|
|
now - this.maxSegmentEndTime_ - this.maxSegmentDuration_;
|
|
}
|
|
|
|
shaka.log.v1('notifySegments:',
|
|
'maxSegmentDuration=' + this.maxSegmentDuration_);
|
|
}
|
|
|
|
|
|
/**
|
|
* 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 {
|
|
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');
|
|
|
|
if (this.segmentAvailabilityDuration_ == Infinity) {
|
|
return this.userSeekStart_;
|
|
}
|
|
|
|
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. Always returns the
|
|
* presentation's duration for video-on-demand.
|
|
* @export
|
|
*/
|
|
getSegmentAvailabilityEnd() {
|
|
if (!this.isLive() && !this.isInProgress()) {
|
|
return this.duration_;
|
|
}
|
|
|
|
return Math.min(this.getLiveEdge_(), 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.
|
|
* @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_);
|
|
if (this.segmentAvailabilityDuration_ == Infinity) {
|
|
return earliestSegmentTime;
|
|
}
|
|
|
|
// 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/google/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) {
|
|
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_);
|
|
}
|
|
|
|
/**
|
|
* 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!');
|
|
}
|
|
}
|
|
}
|
|
};
|