rx-player
Version:
Canal+ HTML5 Video Player
1,125 lines (1,052 loc) • 39.8 kB
text/typescript
/**
* Copyright 2015 CANAL+ Group
*
* 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.
*/
import config from "../../../../../../config";
import { NetworkError } from "../../../../../../errors";
import log from "../../../../../../log";
import type {
IRepresentationIndex,
ISegment,
IRepresentation,
} from "../../../../../../manifest";
import type { IPlayerError } from "../../../../../../public_types";
import assert from "../../../../../../utils/assert";
import isNullOrUndefined from "../../../../../../utils/is_null_or_undefined";
import getMonotonicTimeStamp from "../../../../../../utils/monotonic_timestamp";
import type { ITNode } from "../../../../../../utils/xml-parser";
import type { IEMSG } from "../../../../../containers/isobmff";
import clearTimelineFromPosition from "../../../../utils/clear_timeline_from_position";
import type { IIndexSegment } from "../../../../utils/index_helpers";
import {
checkDiscontinuity,
fromIndexTime,
getIndexSegmentEnd,
toIndexTime,
} from "../../../../utils/index_helpers";
import updateSegmentTimeline from "../../../../utils/update_segment_timeline";
import type { ISegmentTimelineElement } from "../../../node_parser_types";
import type ManifestBoundsCalculator from "../../manifest_bounds_calculator";
import getInitSegment from "../get_init_segment";
import getSegmentsFromTimeline from "../get_segments_from_timeline";
import { constructRepresentationUrl } from "../tokens";
import { getSegmentTimeRoundingError } from "../utils";
import constructTimelineFromElements from "./construct_timeline_from_elements";
import constructTimelineFromPreviousTimeline from "./construct_timeline_from_previous_timeline";
/**
* Index property defined for a SegmentTimeline RepresentationIndex
* This object contains every property needed to generate an ISegment for a
* given media time.
*/
export interface ITimelineIndex {
/** If `false`, the last segment anounced might be still incomplete. */
availabilityTimeComplete: boolean;
/** Minimum availabilityTimeOffset concerning the segments of this Representation. */
availabilityTimeOffset: number;
/** Byte range for a possible index of segments in the server. */
indexRange?: [number, number] | undefined;
/**
* Temporal offset, in the current timescale (see timescale), to add to the
* presentation time (time a segment has at decoding time) to obtain the
* corresponding media time (original time of the media segment in the index
* and on the media file).
* For example, to look for a segment beginning at a second `T` on a
* HTMLMediaElement, we actually will look for a segment in the index
* beginning at:
* ```
* T * timescale + indexTimeOffset
* ```
*/
indexTimeOffset: number;
/** Information on the initialization segment. */
initialization?:
| {
/**
* URL path, to add to the wanted CDN, to access the initialization segment.
* `null` if no URL exists.
*/
url: string | null;
/** possible byte range to request it. */
range?: [number, number] | undefined;
}
| undefined;
/**
* Template for the URL suffix (to concatenate to the wanted CDN), to access any
* media segment.
* Can contain tokens to replace to convert it to real URLs.
*
* `null` if no URL exists.
*/
segmentUrlTemplate: string | null;
/** Number from which the first segments in this index starts with. */
startNumber?: number | undefined;
/** Number associated to the last segment in this index. */
endNumber?: number | undefined;
/**
* Every segments defined in this index.
* `null` at the beginning as this property is parsed lazily (only when first
* needed) for performances reasons.
*
* /!\ Please note that this structure should follow the exact same structure
* than a SegmentTimeline element in the corresponding MPD.
* This means:
* - It should have the same amount of elements in its array than there was
* `<S>` elements in the SegmentTimeline.
* - Each of those same elements should have the same start time, the same
* duration and the same repeat counter than what could be deduced from
* the SegmentTimeline.
* This is needed to be able to run parsing optimization when refreshing the
* MPD. Not doing so could lead to the RxPlayer not being able to play the
* stream anymore.
*/
timeline: IIndexSegment[] | null;
/**
* Timescale to convert a time given here into seconds.
* This is done by this simple operation:
* ``timeInSeconds = timeInIndex * timescale``
*/
timescale: number;
}
/**
* `index` Argument for a SegmentTimeline RepresentationIndex.
* Most of the properties here are already defined in ITimelineIndex.
*/
export interface ITimelineIndexIndexArgument {
indexRange?: [number, number] | undefined;
initialization?:
| { media?: string | undefined; range?: [number, number] | undefined }
| undefined;
media?: string | undefined;
startNumber?: number | undefined;
endNumber?: number | undefined;
timescale?: number | undefined;
/**
* Offset present in the index to convert from the mediaTime (time declared in
* the media segments and in this index) to the presentationTime (time wanted
* when decoding the segment). Basically by doing something along the line
* of:
* ```
* presentationTimeInSeconds =
* mediaTimeInSeconds -
* presentationTimeOffsetInSeconds +
* periodStartInSeconds
* ```
* The time given here is in the current
* timescale (see timescale)
*/
presentationTimeOffset?: number | undefined;
timelineParser?: (() => ITNode[] | HTMLCollection) | undefined;
timeline?: ISegmentTimelineElement[] | undefined;
}
/** Aditional context needed by a SegmentTimeline RepresentationIndex. */
export interface ITimelineIndexContextArgument {
/**
* If `false`, declared segments in the MPD might still be not completely generated.
* If `true`, they are completely generated.
*
* If `undefined`, the corresponding property was not set in the MPD and it is
* thus assumed that they are all generated.
* It might however be semantically different than `true` in the RxPlayer as it
* means that the packager didn't include that information in the MPD.
*/
availabilityTimeComplete: boolean | undefined;
/**
* availability time offset of the concerned Adaptation.
*
* If `undefined`, the corresponding property was not set in the MPD and it is
* thus assumed to be equal to `0`.
* It might however be semantically different than `0` in the RxPlayer as it
* means that the packager didn't include that information in the MPD.
*/
availabilityTimeOffset: number | undefined;
/** Allows to obtain the minimum and maximum positions of a content. */
manifestBoundsCalculator: ManifestBoundsCalculator;
/** Start of the period linked to this RepresentationIndex, in seconds. */
periodStart: number;
/** End of the period linked to this RepresentationIndex, in seconds. */
periodEnd: number | undefined;
/** Whether the corresponding Manifest can be updated and changed. */
isDynamic: boolean;
/**
* Time at which the XML file containing this index was received.
*/
receivedTime?: number | undefined;
/** ID of the Representation concerned. */
representationId?: string | undefined;
/** Bitrate of the Representation concerned. */
representationBitrate?: number | undefined;
/**
* The parser should take this previous version of the
* `TimelineRepresentationIndex` - which was from the same Representation
* parsed at an earlier time - as a base to speed-up the parsing process.
* /!\ If unexpected differences exist between both, there is a risk of
* de-synchronization with what is actually on the server,
* Use with moderation.
*/
unsafelyBaseOnPreviousRepresentation: IRepresentation | null;
/** Function that tells if an EMSG is whitelisted by the manifest */
isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean;
/**
* Set to `true` if the linked Period is the chronologically last one in the
* Manifest.
*/
isLastPeriod: boolean;
}
export interface ILastSegmentInformation {
/** End of the timeline on `time`, timescaled. */
lastPosition?: number | undefined;
/** Defines the time at which `lastPosition` was last calculated. */
time: number;
}
/**
* `IRepresentationIndex` implementation for a DASH `SegmentTimeline` segment
* indexing scheme.
* @class TimelineRepresentationIndex
*/
export default class TimelineRepresentationIndex implements IRepresentationIndex {
/** Underlying structure to retrieve segment information. */
protected _index: ITimelineIndex;
/**
* Time of the last Manifest update.
* The unit is the monotonically-raising timestamp used by the RxPlayer.
*/
private _lastUpdate: number;
/** Absolute start of the period, timescaled and converted to index time. */
private _scaledPeriodStart: number;
/** Absolute end of the period, timescaled and converted to index time. */
private _scaledPeriodEnd: number | undefined;
/** Whether this RepresentationIndex can change over time. */
private _isDynamic: boolean;
/** Retrieve the maximum and minimum position of the whole content. */
private _manifestBoundsCalculator: ManifestBoundsCalculator;
/**
* Lazily get the S elements from this timeline.
* `null` once this call has been done once, to free memory.
*/
private _parseTimeline: (() => ITNode[] | HTMLCollection) | null;
/**
* This variable represents the same `TimelineRepresentationIndex` at the
* previous Manifest update.
* Note that it is not always set.
* This can be used as a base to speed-up the creation of the underlying
* index structure as it can be really heavy for long Manifests.
* To avoid taking too much memory, this variable is reset to `null` once used.
*/
private _unsafelyBaseOnPreviousIndex: TimelineRepresentationIndex | null;
/* Function that tells if an EMSG is whitelisted by the manifest */
private _isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean;
/** `true` if the linked Period is the chronologically last one in the Manifest. */
private _isLastPeriod: boolean;
/**
* @param {Object} index
* @param {Object} context
*/
constructor(
index: ITimelineIndexIndexArgument,
context: ITimelineIndexContextArgument,
) {
if (!TimelineRepresentationIndex.isTimelineIndexArgument(index)) {
throw new Error(
"The given index is not compatible with a " + "TimelineRepresentationIndex.",
);
}
const {
availabilityTimeComplete,
availabilityTimeOffset,
manifestBoundsCalculator,
isDynamic,
isLastPeriod,
representationId,
representationBitrate,
periodStart,
periodEnd,
isEMSGWhitelisted,
} = context;
const timescale = index.timescale ?? 1;
const presentationTimeOffset = index.presentationTimeOffset ?? 0;
const scaledStart = periodStart * timescale;
const indexTimeOffset = presentationTimeOffset - scaledStart;
this._manifestBoundsCalculator = manifestBoundsCalculator;
this._isEMSGWhitelisted = isEMSGWhitelisted;
this._isLastPeriod = isLastPeriod;
this._lastUpdate = context.receivedTime ?? getMonotonicTimeStamp();
this._unsafelyBaseOnPreviousIndex = null;
if (
context.unsafelyBaseOnPreviousRepresentation !== null &&
context.unsafelyBaseOnPreviousRepresentation.index instanceof
TimelineRepresentationIndex
) {
// avoid too much nested references, to keep memory down
context.unsafelyBaseOnPreviousRepresentation.index._unsafelyBaseOnPreviousIndex =
null;
this._unsafelyBaseOnPreviousIndex =
context.unsafelyBaseOnPreviousRepresentation.index;
}
this._isDynamic = isDynamic;
this._parseTimeline = index.timelineParser ?? null;
const initializationUrl =
index.initialization?.media === undefined
? null
: constructRepresentationUrl(
index.initialization.media,
representationId,
representationBitrate,
);
const segmentUrlTemplate =
index.media === undefined
? null
: constructRepresentationUrl(
index.media,
representationId,
representationBitrate,
);
let actualAvailabilityTimeOffset;
// Technically, it seems (although it is not clear) that an MPD may contain
// future segments and it's the job of a player to not request segments later
// than the time at which they should be available.
// In practice, we don't do that for various reasons: precision issues,
// various DASH spec interpretations by packagers and players...
//
// So as a compromise, if nothing in the MPD indicates that future segments
// may be announced (see code below), we will act as if ALL segments in this
// TimelineRepresentationIndex are requestable
if (availabilityTimeOffset === undefined && availabilityTimeComplete === undefined) {
actualAvailabilityTimeOffset = Infinity; // Meaning: we can request
// everything in the index
} else {
actualAvailabilityTimeOffset = availabilityTimeOffset ?? 0;
}
this._index = {
availabilityTimeComplete: availabilityTimeComplete ?? true,
availabilityTimeOffset: actualAvailabilityTimeOffset,
indexRange: index.indexRange,
indexTimeOffset,
initialization: isNullOrUndefined(index.initialization)
? undefined
: {
url: initializationUrl,
range: index.initialization.range,
},
segmentUrlTemplate,
startNumber: index.startNumber,
endNumber: index.endNumber,
timeline:
index.timeline === undefined
? null
: updateTimelineFromEndNumber(
index.timeline,
index.startNumber,
index.endNumber,
),
timescale,
};
this._scaledPeriodStart = toIndexTime(periodStart, this._index);
this._scaledPeriodEnd =
periodEnd === undefined ? undefined : toIndexTime(periodEnd, this._index);
}
/**
* Construct init Segment.
* @returns {Object}
*/
getInitSegment(): ISegment {
return getInitSegment(this._index, this._isEMSGWhitelisted);
}
/**
* Asks for segments to download for a given time range.
* @param {Number} from - Beginning of the time wanted, in seconds
* @param {Number} duration - duration wanted, in seconds
* @returns {Array.<Object>}
*/
getSegments(from: number, duration: number): ISegment[] {
this._refreshTimeline(); // clear timeline if needed
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
return getSegmentsFromTimeline(
this._index as typeof this._index & { timeline: IIndexSegment[] },
from,
duration,
this._manifestBoundsCalculator,
this._scaledPeriodEnd,
this._isEMSGWhitelisted,
);
}
/**
* Returns true if the index should be refreshed.
* @returns {Boolean}
*/
shouldRefresh(): false {
// DASH Manifest based on a SegmentTimeline should have minimumUpdatePeriod
// attribute which should be sufficient to know when to refresh it.
return false;
}
/**
* Returns the starting time, in seconds, of the earliest segment currently
* available.
* Returns null if nothing is in the index
* @returns {Number|null}
*/
getFirstAvailablePosition(): number | null {
this._refreshTimeline();
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
const timeline = this._index.timeline;
return timeline.length === 0
? null
: fromIndexTime(Math.max(this._scaledPeriodStart, timeline[0].start), this._index);
}
/**
* Returns the ending time, in seconds, of the last segment currently
* available.
* Returns null if nothing is in the index
* @returns {Number|null}
*/
getLastAvailablePosition(): number | null {
this._refreshTimeline();
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
const lastReqSegInfo = getLastRequestableSegmentInfo(
// Needed typecast for TypeScript
this._index as typeof this._index & { timeline: IIndexSegment[] },
this._manifestBoundsCalculator,
this._scaledPeriodEnd,
);
if (lastReqSegInfo === null) {
return null;
}
const lastScaledPosition = Math.min(
lastReqSegInfo.end,
this._scaledPeriodEnd ?? Infinity,
);
return fromIndexTime(lastScaledPosition, this._index);
}
/**
* Returns the absolute end in seconds this RepresentationIndex can reach once
* all segments are available.
* @returns {number|null|undefined}
*/
getEnd(): number | undefined | null {
if (this._isDynamic && !this._isLastPeriod) {
return undefined;
}
this._refreshTimeline();
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
if (this._index.timeline.length <= 0) {
return null;
}
const lastSegment = this._index.timeline[this._index.timeline.length - 1];
const lastTime = Math.min(
getIndexSegmentEnd(lastSegment, null, this._scaledPeriodEnd),
this._scaledPeriodEnd ?? Infinity,
);
return fromIndexTime(lastTime, this._index);
}
/**
* Returns:
* - `true` if in the given time interval, at least one new segment is
* expected to be available in the future.
* - `false` either if all segments in that time interval are already
* available for download or if none will ever be available for it.
* - `undefined` when it is not possible to tell.
* @param {number} start
* @param {number} end
* @returns {boolean|undefined}
*/
awaitSegmentBetween(start: number, end: number): boolean | undefined {
assert(start <= end);
if (!this._isDynamic) {
return false; // No segment will be newly available in the future
}
this._refreshTimeline();
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
const { timescale, timeline } = this._index;
const segmentTimeRounding = getSegmentTimeRoundingError(timescale);
const scaledWantedEnd = toIndexTime(end, this._index);
const lastReqSegInfo = getLastRequestableSegmentInfo(
// Needed typecast for TypeScript
this._index as typeof this._index & { timeline: IIndexSegment[] },
this._manifestBoundsCalculator,
this._scaledPeriodEnd,
);
if (lastReqSegInfo !== null) {
const lastReqSegmentEnd = Math.min(
lastReqSegInfo.end,
this._scaledPeriodEnd ?? Infinity,
);
const roundedReqSegmentEnd = lastReqSegmentEnd + segmentTimeRounding;
if (
roundedReqSegmentEnd >=
Math.min(scaledWantedEnd, this._scaledPeriodEnd ?? Infinity)
) {
return false; // everything up to that point is already requestable
}
}
const scaledWantedStart = toIndexTime(start, this._index);
if (
timeline.length > 0 &&
lastReqSegInfo !== null &&
!lastReqSegInfo.isLastOfTimeline
) {
// There are some future segments already anounced in the MPD
const lastSegment = timeline[timeline.length - 1];
const lastSegmentEnd = getIndexSegmentEnd(lastSegment, null, this._scaledPeriodEnd);
const roundedLastSegEnd = lastSegmentEnd + segmentTimeRounding;
if (scaledWantedStart < roundedLastSegEnd + segmentTimeRounding) {
return true; // The MPD's timeline already contains one such element,
// It is just not requestable yet
}
}
if (!this._isLastPeriod) {
// Let's consider - perhaps wrongly, that Periods which aren't the last
// one have all of their segments announced.
return false;
}
if (this._scaledPeriodEnd === undefined) {
return scaledWantedEnd + segmentTimeRounding > this._scaledPeriodStart
? undefined // There may be future segments at this point
: false; // Before the current Period
}
// `true` if within the boundaries of this Period. `false` otherwise.
return (
scaledWantedStart - segmentTimeRounding < this._scaledPeriodEnd &&
scaledWantedEnd + segmentTimeRounding > this._scaledPeriodStart
);
}
/**
* Returns true if a Segment returned by this index is still considered
* available.
* Returns false if it is not available anymore.
* Returns undefined if we cannot know whether it is still available or not.
* @param {Object} segment
* @returns {Boolean|undefined}
*/
isSegmentStillAvailable(segment: ISegment): boolean | undefined {
if (segment.isInit) {
return true;
}
this._refreshTimeline();
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
return isSegmentStillAvailable(
segment,
// Needed typecast for TypeScript
this._index as typeof this._index & {
timeline: IIndexSegment[];
},
this._manifestBoundsCalculator,
this._scaledPeriodEnd,
);
}
/**
* Checks if the time given is in a discontinuity. That is:
* - We're on the upper bound of the current range (end of the range - time
* is inferior to the timescale)
* - The next range starts after the end of the current range.
* @param {Number} time
* @returns {Number|null}
*/
checkDiscontinuity(time: number): number | null {
this._refreshTimeline();
let timeline = this._index.timeline;
if (timeline === null) {
timeline = this._getTimeline();
this._index.timeline = timeline;
}
return checkDiscontinuity(
{
timeline,
timescale: this._index.timescale,
indexTimeOffset: this._index.indexTimeOffset,
},
time,
this._scaledPeriodEnd,
);
}
/**
* @param {Error} error
* @returns {Boolean}
*/
canBeOutOfSyncError(error: IPlayerError): boolean {
if (!this._isDynamic) {
return false;
}
return error instanceof NetworkError && error.isHttpError(404);
}
/**
* Replace this RepresentationIndex with one from a new version of the
* Manifest.
* @param {Object} newIndex
*/
_replace(newIndex: TimelineRepresentationIndex): void {
this._parseTimeline = newIndex._parseTimeline;
this._index = newIndex._index;
this._isDynamic = newIndex._isDynamic;
this._scaledPeriodStart = newIndex._scaledPeriodStart;
this._scaledPeriodEnd = newIndex._scaledPeriodEnd;
this._lastUpdate = newIndex._lastUpdate;
this._manifestBoundsCalculator = newIndex._manifestBoundsCalculator;
this._isLastPeriod = newIndex._isLastPeriod;
}
/**
* Update this RepresentationIndex with a shorter version of it coming from a
* new version of the MPD.
* @param {Object} newIndex
*/
_update(newIndex: TimelineRepresentationIndex): void {
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
if (newIndex._index.timeline === null) {
newIndex._index.timeline = newIndex._getTimeline();
}
const hasReplaced = updateSegmentTimeline(
this._index.timeline,
newIndex._index.timeline,
);
if (hasReplaced) {
this._index.startNumber = newIndex._index.startNumber;
}
this._index.availabilityTimeOffset = newIndex._index.availabilityTimeOffset;
this._index.availabilityTimeComplete = newIndex._index.availabilityTimeComplete;
this._index.endNumber = newIndex._index.endNumber;
this._isDynamic = newIndex._isDynamic;
this._scaledPeriodStart = newIndex._scaledPeriodStart;
this._scaledPeriodEnd = newIndex._scaledPeriodEnd;
this._lastUpdate = newIndex._lastUpdate;
this._isLastPeriod = newIndex._isLastPeriod;
}
/**
* Returns `false` if this RepresentationIndex currently contains its last
* segment.
* Returns `true` if it's still pending.
* @returns {Boolean}
*/
isStillAwaitingFutureSegments(): boolean {
if (!this._isDynamic) {
return false;
}
this._refreshTimeline();
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
const { timeline } = this._index;
if (timeline.length === 0) {
// No segment announced in this Period
if (this._scaledPeriodEnd !== undefined) {
const liveEdge = this._manifestBoundsCalculator.getEstimatedLiveEdge();
if (
liveEdge !== undefined &&
toIndexTime(liveEdge, this._index) > this._scaledPeriodEnd
) {
// This Period is over, we're not awaiting anything
return false;
}
}
// Let's just consider that we're awaiting only for when this is the last Period.
return this._isLastPeriod;
}
const segmentTimeRounding = getSegmentTimeRoundingError(this._index.timescale);
const lastReqSegInfo = getLastRequestableSegmentInfo(
// Needed typecast for TypeScript
this._index as typeof this._index & { timeline: IIndexSegment[] },
this._manifestBoundsCalculator,
this._scaledPeriodEnd,
);
if (lastReqSegInfo !== null && !lastReqSegInfo.isLastOfTimeline) {
// There might be non-yet requestable segments in the manifest
const lastReqSegmentEnd = Math.min(
lastReqSegInfo.end,
this._scaledPeriodEnd ?? Infinity,
);
if (
this._scaledPeriodEnd !== undefined &&
lastReqSegmentEnd + segmentTimeRounding >= this._scaledPeriodEnd
) {
// The last requestable segment ends after the end of the Period anyway
return false;
}
return true; // There are not-yet requestable segments
}
if (!this._isLastPeriod) {
// This index is not linked to the current last Period in the MPD, in
// which case it is inferred that all segments have been announced.
//
// Note that this condition might break very very rare use cases where old
// Periods are still being generated, yet it should fix more cases than it
// breaks.
return false;
}
if (this._scaledPeriodEnd === undefined) {
// This is the last Period of a dynamic content whose end is unknown.
// Just return true.
return true;
}
const lastSegment = timeline[timeline.length - 1];
const lastSegmentEnd = getIndexSegmentEnd(lastSegment, null, this._scaledPeriodEnd);
// We're awaiting future segments only if the current end is before the end
// of the Period
return lastSegmentEnd + segmentTimeRounding < this._scaledPeriodEnd;
}
/**
* @returns {Boolean}
*/
isInitialized(): true {
return true;
}
initialize(): void {
log.error("A `TimelineRepresentationIndex` does not need to be initialized");
}
addPredictedSegments(): void {
log.warn("Cannot add predicted segments to a `TimelineRepresentationIndex`");
}
/**
* Returns the `duration` of each segment in the context of its Manifest (i.e.
* as the Manifest anounces them, actual segment duration may be different due
* to approximations), in seconds.
*
* NOTE: we could here do a median or a mean but I chose to be lazy (and
* more performant) by returning the duration of the first element instead.
* As `isPrecize` is `false`, the rest of the code should be notified that
* this is only an approximation.
* @returns {number}
*/
getTargetSegmentDuration(): { duration: number; isPrecize: boolean } | undefined {
this._refreshTimeline();
const { timeline, timescale } = this._index;
if (timeline === null) {
return undefined;
}
const firstElementInTimeline = timeline[0];
if (firstElementInTimeline === undefined) {
return undefined;
}
return {
duration: firstElementInTimeline.duration / timescale,
isPrecize: false,
};
}
/**
* Returns `true` if the given object can be used as an "index" argument to
* create a new `TimelineRepresentationIndex`.
* @param {Object} index
* @returns {boolean}
*/
static isTimelineIndexArgument(index: ITimelineIndexIndexArgument): boolean {
return typeof index.timelineParser === "function" || Array.isArray(index.timeline);
}
/**
* Clean-up timeline to remove segment information which should not be
* available due to timeshifting.
*/
private _refreshTimeline(): void {
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
if (!this._isDynamic) {
return;
}
const firstPosition = this._manifestBoundsCalculator.getEstimatedMinimumSegmentTime(
(this._index.timeline[0]?.duration ?? 0) / this._index.timescale,
);
if (isNullOrUndefined(firstPosition)) {
return; // we don't know yet
}
const scaledFirstPosition = toIndexTime(firstPosition, this._index);
const nbEltsRemoved = clearTimelineFromPosition(
this._index.timeline,
scaledFirstPosition,
);
if (this._index.startNumber !== undefined) {
this._index.startNumber += nbEltsRemoved;
} else if (this._index.endNumber !== undefined) {
this._index.startNumber = nbEltsRemoved + 1;
}
}
/**
* Allows to generate the "timeline" for this RepresentationIndex.
* Call this function when the timeline is unknown.
* This function was added to only perform that task lazily, i.e. only when
* first needed.
* After calling it, every now unneeded variable will be freed from memory.
* This means that calling _getTimeline more than once will just return an
* empty array.
*
* /!\ Please note that this structure should follow the exact same structure
* than a SegmentTimeline element in the corresponding MPD.
* This means:
* - It should have the same amount of elements in its array than there was
* `<S>` elements in the SegmentTimeline.
* - Each of those same elements should have the same start time, the same
* duration and the same repeat counter than what could be deduced from
* the SegmentTimeline.
* This is needed to be able to run parsing optimization when refreshing the
* MPD. Not doing so could lead to the RxPlayer not being able to play the
* stream anymore.
* @returns {Array.<Object>}
*/
private _getTimeline(): IIndexSegment[] {
if (this._parseTimeline === null) {
if (this._index.timeline !== null) {
return this._index.timeline;
}
log.error("DASH: Timeline already lazily parsed.");
return [];
}
const newElements = this._parseTimeline();
this._parseTimeline = null; // Free memory
const { MIN_DASH_S_ELEMENTS_TO_PARSE_UNSAFELY } = config.getCurrent();
if (
this._unsafelyBaseOnPreviousIndex === null ||
newElements.length < MIN_DASH_S_ELEMENTS_TO_PARSE_UNSAFELY
) {
// Just completely parse the current timeline
return updateTimelineFromEndNumber(
constructTimelineFromElements(newElements),
this._index.startNumber,
this._index.endNumber,
);
}
// Construct previously parsed timeline if not already done
let prevTimeline: IIndexSegment[];
if (this._unsafelyBaseOnPreviousIndex._index.timeline === null) {
prevTimeline = this._unsafelyBaseOnPreviousIndex._getTimeline();
this._unsafelyBaseOnPreviousIndex._index.timeline = prevTimeline;
} else {
prevTimeline = this._unsafelyBaseOnPreviousIndex._index.timeline;
}
this._unsafelyBaseOnPreviousIndex = null; // Free memory
return updateTimelineFromEndNumber(
constructTimelineFromPreviousTimeline(newElements, prevTimeline),
this._index.startNumber,
this._index.endNumber,
);
}
}
/**
* Take the original SegmentTimeline's parsed timeline and, if an `endNumber` is
* specified, filter segments which possess a number superior to that number.
*
* This should only be useful in only rare and broken MPDs, but we aim to
* respect the specification even in those cases.
*
* @param {Array.<Object>} timeline
* @param {number|undefined} startNumber
* @param {Array.<Object>} endNumber
* @returns {number|undefined}
*/
function updateTimelineFromEndNumber(
timeline: IIndexSegment[],
startNumber: number | undefined,
endNumber: number | undefined,
): IIndexSegment[] {
if (endNumber === undefined) {
return timeline;
}
let currNumber = startNumber ?? 1;
for (let idx = 0; idx < timeline.length; idx++) {
const seg = timeline[idx];
currNumber += seg.repeatCount + 1;
if (currNumber > endNumber) {
if (currNumber === endNumber + 1) {
return timeline.slice(0, idx + 1);
} else {
const newTimeline = timeline.slice(0, idx);
const lastElt = { ...seg };
const beginningNumber = currNumber - seg.repeatCount - 1;
lastElt.repeatCount = Math.max(0, endNumber - beginningNumber);
newTimeline.push(lastElt);
return newTimeline;
}
}
}
return timeline;
}
/**
* Returns true if a Segment returned by the corresponding index is still
* considered available.
* Returns false if it is not available anymore.
* Returns undefined if we cannot know whether it is still available or not.
* /!\ We do not check the mediaURLs of the segment.
* @param {Object} segment
* @param {Object} index
* @param {Object} manifestBoundsCalculator
* @param {number|undefined} scaledPeriodEnd
* @returns {Boolean|undefined}
*/
export function isSegmentStillAvailable(
segment: ISegment,
index: {
availabilityTimeOffset: number;
timeline: IIndexSegment[];
indexTimeOffset: number;
timescale: number;
},
manifestBoundsCalculator: ManifestBoundsCalculator,
scaledPeriodEnd: number | undefined,
): boolean | undefined {
const lastReqSegInfo = getLastRequestableSegmentInfo(
index,
manifestBoundsCalculator,
scaledPeriodEnd,
);
if (lastReqSegInfo === null) {
return false;
}
for (let i = 0; i < index.timeline.length; i++) {
if (lastReqSegInfo.timelineIdx < i) {
return false;
}
const tSegment = index.timeline[i];
const tSegmentTime = (tSegment.start - index.indexTimeOffset) / index.timescale;
if (tSegmentTime > segment.time) {
return false; // We went over it without finding it
} else if (tSegmentTime === segment.time) {
if (tSegment.range === undefined) {
return segment.range === undefined;
}
return (
!isNullOrUndefined(segment.range) &&
tSegment.range[0] === segment.range[0] &&
tSegment.range[1] === segment.range[1]
);
} else {
// tSegment.start < segment.time
if (tSegment.repeatCount >= 0 && tSegment.duration !== undefined) {
const timeDiff = tSegmentTime - tSegment.start;
const repeat = timeDiff / tSegment.duration - 1;
return repeat % 1 === 0 && repeat <= lastReqSegInfo.newRepeatCount;
}
}
}
return false;
}
/**
* Returns from the given RepresentationIndex information on the last segment
* that may be requested currently.
*
* Returns `null` if there's no such segment.
* @param {Object} index
* @param {Object} manifestBoundsCalculator
* @param {number|undefined} scaledPeriodEnd
* @returns {number|null}
*/
export function getLastRequestableSegmentInfo(
index: {
availabilityTimeOffset: number;
timeline: IIndexSegment[];
timescale: number;
},
manifestBoundsCalculator: ManifestBoundsCalculator,
scaledPeriodEnd: number | undefined,
): ILastRequestableSegmentInfo | null {
if (index.timeline.length <= 0) {
return null;
}
if (index.availabilityTimeOffset === Infinity) {
// availabilityTimeOffset to Infinity == Everything is requestable in the timeline.
const lastIndex = index.timeline.length - 1;
const lastElem = index.timeline[lastIndex];
return {
isLastOfTimeline: true,
timelineIdx: lastIndex,
newRepeatCount: lastElem.repeatCount,
end: getIndexSegmentEnd(lastElem, null, scaledPeriodEnd),
};
}
const adjustedMaxSeconds = manifestBoundsCalculator.getEstimatedMaximumPosition(
index.availabilityTimeOffset,
);
if (adjustedMaxSeconds === undefined) {
const lastIndex = index.timeline.length - 1;
const lastElem = index.timeline[lastIndex];
return {
isLastOfTimeline: true,
timelineIdx: lastIndex,
newRepeatCount: lastElem.repeatCount,
end: getIndexSegmentEnd(lastElem, null, scaledPeriodEnd),
};
}
for (let i = index.timeline.length - 1; i >= index.timeline.length; i--) {
const element = index.timeline[i];
const endOfFirstOccurence = element.start + element.duration;
if (fromIndexTime(endOfFirstOccurence, index) <= adjustedMaxSeconds) {
const endTime = getIndexSegmentEnd(element, index.timeline[i + 1], scaledPeriodEnd);
if (fromIndexTime(endTime, index) <= adjustedMaxSeconds) {
return {
isLastOfTimeline: i === index.timeline.length - 1,
timelineIdx: i,
newRepeatCount: element.repeatCount,
end: endOfFirstOccurence,
};
} else {
// We have to find the right repeatCount
const maxIndexTime = toIndexTime(adjustedMaxSeconds, index);
const diffToSegStart = maxIndexTime - element.start;
const nbOfSegs = Math.floor(diffToSegStart / element.duration);
assert(nbOfSegs >= 1);
return {
isLastOfTimeline: false,
timelineIdx: i,
newRepeatCount: nbOfSegs - 1,
end: element.start + nbOfSegs * element.duration,
};
}
}
}
return null;
}
/**
* Information on the last requestable segment deduced from a timeline array of
* segment information.
*/
export interface ILastRequestableSegmentInfo {
/**
* If `true`, we know that the last requestable segment is equal to the last
* segment that can be deduced from the corresponding given timeline.
* Written another way, there seem to be no segment announced in the timeline
* that are not yet requestable.
*
* If `false`, we know that the last requestable segment is not the last
* segment that can be deduced from the corresponding timeline.
* Written another way, there are supplementary segments in the timeline which
* are not yet requestable.
*
* Note that if the last requestable segment has its information from the last
* element from the timeline but it's not the last segment that would be
* deduced from the `repeatCount` property, then this value is set to `false`.
*/
isLastOfTimeline: boolean;
/**
* End time at which the last requestable segment ends, in the corresponding
* index timescale (__NOT__ in seconds).
*/
end: number;
/**
* The index in `timeline` of the last requestable segment.
* Note that its `repeatCount` may be updated and put as `newRepeatCount`.
*/
timelineIdx: number;
/**
* The new `repeatCount` value for that last segment. May be equal or
* different from the timeline element found at `timelineIdx`.
*/
newRepeatCount: number;
}