UNPKG

rx-player

Version:
1,125 lines (1,052 loc) 39.8 kB
/** * 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; }