UNPKG

rx-player

Version:
694 lines (634 loc) 24.3 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 log from "../../../../../log"; import type { IRepresentationIndex, ISegment } from "../../../../../manifest"; import assert from "../../../../../utils/assert"; import isNullOrUndefined from "../../../../../utils/is_null_or_undefined"; import type { IEMSG } from "../../../../containers/isobmff"; import type ManifestBoundsCalculator from "../manifest_bounds_calculator"; import getInitSegment from "./get_init_segment"; import { createDashUrlDetokenizer, constructRepresentationUrl } from "./tokens"; import { getSegmentTimeRoundingError } from "./utils"; /** * Index property defined for a SegmentTemplate RepresentationIndex * This object contains every property needed to generate an ISegment for a * given media time. */ export interface ITemplateIndex { /** * Duration of each segment, in the timescale given (see timescale property). * timescale and list properties.) */ duration: number; /** * Timescale to convert a time given here into seconds. * This is done by this simple operation: * ``timeInSeconds = timeInIndex * timescale`` */ timescale: number; /** Byte range for a possible index of segments in the server. */ indexRange?: [number, number] | undefined; /** Information on the initialization segment. */ initialization?: | { /** * URL path, to add to the wanted CDN, to access the initialization segment. */ url: string | null; /** possible byte range to request it. */ range?: [number, number] | undefined; } | undefined; /** * URL base to access any segment. * Can contain token to replace to convert it to real URLs. * `null` if no URL exists. */ url: string | null; /** * 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; /** * 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; /** 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; } /** * `index` Argument for a SegmentTemplate RepresentationIndex. * Most of the properties here are already defined in ITemplateIndex. */ export interface ITemplateIndexIndexArgument { duration?: number | undefined; indexRange?: [number, number] | undefined; initialization?: | { media?: string | undefined; range?: [number, number] | undefined } | undefined; media?: string | undefined; presentationTimeOffset?: number | undefined; startNumber?: number | undefined; endNumber?: number | undefined; timescale?: number | undefined; } /** Aditional context needed by a SegmentTemplate RepresentationIndex. */ export interface ITemplateIndexContextArgument { /** * 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 concerned by this RepresentationIndex, in seconds. */ periodStart: number; /** End of the period concerned by this RepresentationIndex, in seconds. */ periodEnd: number | undefined; /** Whether the corresponding Manifest can be updated and changed. */ isDynamic: boolean; /** ID of the Representation concerned. */ representationId?: string | undefined; /** Bitrate of the Representation concerned. */ representationBitrate?: number | undefined; /* Function that tells if an EMSG is whitelisted by the manifest */ isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean; } /** * IRepresentationIndex implementation for DASH' SegmentTemplate without a * SegmentTimeline. * @class TemplateRepresentationIndex */ export default class TemplateRepresentationIndex implements IRepresentationIndex { /** Underlying structure to retrieve segment information. */ private _index: ITemplateIndex; /** Retrieve the maximum and minimum position of the whole content. */ private _manifestBoundsCalculator: ManifestBoundsCalculator; /** Absolute start of the Period, in seconds. */ private _periodStart: number; /** Difference between the end time of the Period and its start time, in timescale. */ private _scaledRelativePeriodEnd: number | undefined; /** Minimum availabilityTimeOffset concerning the segments of this Representation. */ private _availabilityTimeOffset: number | undefined; /** Whether the corresponding Manifest can be updated and changed. */ private _isDynamic: boolean; /* Function that tells if an EMSG is whitelisted by the manifest */ private _isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean; /** * @param {Object} index * @param {Object} context */ constructor( index: ITemplateIndexIndexArgument, context: ITemplateIndexContextArgument, ) { const { availabilityTimeOffset, manifestBoundsCalculator, isDynamic, periodEnd, periodStart, representationId, representationBitrate, isEMSGWhitelisted, } = context; const timescale = index.timescale ?? 1; this._availabilityTimeOffset = availabilityTimeOffset; this._manifestBoundsCalculator = manifestBoundsCalculator; const presentationTimeOffset = index.presentationTimeOffset ?? 0; const scaledStart = periodStart * timescale; const indexTimeOffset = presentationTimeOffset - scaledStart; if (index.duration === undefined) { throw new Error("Invalid SegmentTemplate: no duration"); } const initializationUrl = index.initialization?.media === undefined ? null : constructRepresentationUrl( index.initialization.media, representationId, representationBitrate, ); const segmentUrlTemplate = index.media === undefined ? null : constructRepresentationUrl( index.media, representationId, representationBitrate, ); this._index = { duration: index.duration, timescale, indexRange: index.indexRange, indexTimeOffset, initialization: isNullOrUndefined(index.initialization) ? undefined : { url: initializationUrl, range: index.initialization.range }, url: segmentUrlTemplate, presentationTimeOffset, startNumber: index.startNumber, endNumber: index.endNumber, }; this._isDynamic = isDynamic; this._periodStart = periodStart; this._scaledRelativePeriodEnd = periodEnd === undefined ? undefined : (periodEnd - periodStart) * timescale; this._isEMSGWhitelisted = isEMSGWhitelisted; } /** * Construct init Segment. * @returns {Object} */ getInitSegment(): ISegment { return getInitSegment(this._index, this._isEMSGWhitelisted); } /** * @param {Number} fromTime * @param {Number} dur * @returns {Array.<Object>} */ getSegments(fromTime: number, dur: number): ISegment[] { const index = this._index; const { duration, startNumber, endNumber, timescale, url } = index; const scaledStart = this._periodStart * timescale; const scaledEnd = this._scaledRelativePeriodEnd; // Convert the asked position to the right timescales, and consider them // relatively to the Period's start. const upFromPeriodStart = fromTime * timescale - scaledStart; const toFromPeriodStart = (fromTime + dur) * timescale - scaledStart; const firstSegmentStart = this._getFirstSegmentStart(); const lastSegmentStart = this._getLastSegmentStart(); if (isNullOrUndefined(firstSegmentStart) || isNullOrUndefined(lastSegmentStart)) { return []; } const startPosition = Math.max(firstSegmentStart, upFromPeriodStart); const lastWantedStartPosition = Math.min(lastSegmentStart, toFromPeriodStart); if (lastWantedStartPosition + duration <= startPosition) { return []; } const segments: ISegment[] = []; // number corresponding to the Period's start const numberOffset = startNumber ?? 1; // calcul initial time from Period start, where the first segment would have // the `0` number let numberIndexedToZero = Math.floor(startPosition / duration); for ( let timeFromPeriodStart = numberIndexedToZero * duration; timeFromPeriodStart <= lastWantedStartPosition; timeFromPeriodStart += duration ) { // To obtain the real number, adds the real number from the Period's start const realNumber = numberIndexedToZero + numberOffset; if (endNumber !== undefined && realNumber > endNumber) { return segments; } const realDuration = !isNullOrUndefined(scaledEnd) && timeFromPeriodStart + duration > scaledEnd ? scaledEnd - timeFromPeriodStart : duration; const realTime = timeFromPeriodStart + scaledStart; const manifestTime = timeFromPeriodStart + this._index.presentationTimeOffset; const detokenizedURL = url === null ? null : createDashUrlDetokenizer(manifestTime, realNumber)(url); const args = { id: String(realNumber), number: realNumber, time: realTime / timescale, end: (realTime + realDuration) / timescale, duration: realDuration / timescale, timescale: 1 as const, isInit: false, scaledDuration: realDuration / timescale, url: detokenizedURL, timestampOffset: -(index.indexTimeOffset / timescale), complete: true, privateInfos: { isEMSGWhitelisted: this._isEMSGWhitelisted, }, }; segments.push(args); numberIndexedToZero++; } return segments; } /** * Returns first possible position in the index, in seconds. * @returns {number|null|undefined} */ getFirstAvailablePosition(): number | null | undefined { const firstSegmentStart = this._getFirstSegmentStart(); if (isNullOrUndefined(firstSegmentStart)) { return firstSegmentStart; // return undefined or null } return firstSegmentStart / this._index.timescale + this._periodStart; } /** * Returns last possible position in the index, in seconds. * @returns {number|null} */ getLastAvailablePosition(): number | null | undefined { const lastSegmentStart = this._getLastSegmentStart(); if (isNullOrUndefined(lastSegmentStart)) { // In that case (null or undefined), getLastAvailablePosition should reflect // the result of getLastSegmentStart, as the meaning is the same for // the two functions. So, we return the result of the latter. return lastSegmentStart; } const scaledRelativeIndexEnd = this._estimateRelativeScaledEnd(); const lastSegmentEnd = Math.min( lastSegmentStart + this._index.duration, scaledRelativeIndexEnd ?? Infinity, ); return lastSegmentEnd / this._index.timescale + this._periodStart; } /** * Returns the absolute end in seconds this RepresentationIndex can reach once * all segments are available. * @returns {number|null|undefined} */ getEnd(): number | null | undefined { if (!this._isDynamic) { return this.getLastAvailablePosition(); } const scaledRelativeIndexEnd = this._estimateRelativeScaledEnd(); if (scaledRelativeIndexEnd === undefined) { return undefined; } const { timescale } = this._index; const absoluteScaledIndexEnd = scaledRelativeIndexEnd + this._periodStart * timescale; return absoluteScaledIndexEnd / timescale; } /** * 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. * * Always `false` in a `BaseRepresentationIndex` because all segments should * be directly available. * @returns {boolean} */ awaitSegmentBetween(start: number, end: number): boolean | undefined { assert(start <= end); if (!this._isDynamic) { return false; } const { timescale } = this._index; const segmentTimeRounding = getSegmentTimeRoundingError(timescale); const scaledPeriodStart = this._periodStart * timescale; const scaledRelativeStart = start * timescale - scaledPeriodStart; const scaledRelativeEnd = end * timescale - scaledPeriodStart; const lastSegmentStart = this._getLastSegmentStart(); if (isNullOrUndefined(lastSegmentStart)) { const relativeScaledIndexEnd = this._estimateRelativeScaledEnd(); if (relativeScaledIndexEnd === undefined) { return scaledRelativeEnd + segmentTimeRounding >= 0; } return ( scaledRelativeEnd + segmentTimeRounding >= 0 && scaledRelativeStart < relativeScaledIndexEnd - segmentTimeRounding ); } const lastSegmentEnd = lastSegmentStart + this._index.duration; const relativeScaledIndexEnd = this._estimateRelativeScaledEnd(); if (relativeScaledIndexEnd === undefined) { return scaledRelativeEnd > lastSegmentEnd - segmentTimeRounding; } return ( scaledRelativeEnd > lastSegmentEnd - segmentTimeRounding && scaledRelativeStart < relativeScaledIndexEnd - segmentTimeRounding ); } /** * Returns true if, based on the arguments, the index should be refreshed. * We never have to refresh a SegmentTemplate-based manifest. * @returns {Boolean} */ shouldRefresh(): false { return false; } /** * We cannot check for discontinuity in SegmentTemplate-based indexes. * @returns {null} */ checkDiscontinuity(): null { return null; } /** * Returns `true` if the given segment should still be available as of now * (not removed since and still request-able). * Returns `false` if that's not the case. * Returns `undefined` if we do not know whether that's the case or not. * @param {Object} segment * @returns {boolean|undefined} */ isSegmentStillAvailable(segment: ISegment): boolean | undefined { if (segment.isInit) { return true; } const segmentsForTime = this.getSegments(segment.time, 0.1); if (segmentsForTime.length === 0) { return false; } return ( segmentsForTime[0].time === segment.time && segmentsForTime[0].end === segment.end && segmentsForTime[0].number === segment.number ); } /** * SegmentTemplate without a SegmentTimeline should not be updated. * @returns {Boolean} */ canBeOutOfSyncError(): false { return false; } /** * Returns `false` if the last segments in this index have already been * generated so that we can freely go to the next period. * Returns `true` if the index is still waiting on future segments to be * generated. * @returns {Boolean} */ isStillAwaitingFutureSegments(): boolean { if (!this._isDynamic) { return false; } const scaledRelativeIndexEnd = this._estimateRelativeScaledEnd(); if (scaledRelativeIndexEnd === undefined) { return true; } const { timescale } = this._index; const lastSegmentStart = this._getLastSegmentStart(); // As last segment start is null if live time is before // current period, consider the index not to be finished. if (isNullOrUndefined(lastSegmentStart)) { return true; } const lastSegmentEnd = lastSegmentStart + this._index.duration; const segmentTimeRounding = getSegmentTimeRoundingError(timescale); return lastSegmentEnd + segmentTimeRounding < scaledRelativeIndexEnd; } /** * @returns {Boolean} */ isInitialized(): true { return true; } initialize(): void { log.error("A `TemplateRepresentationIndex` does not need to be initialized"); } addPredictedSegments(): void { log.warn("Cannot add predicted segments to a `TemplateRepresentationIndex`"); } /** * 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. * @returns {number} */ getTargetSegmentDuration(): { duration: number; isPrecize: boolean; } { return { duration: this._index.duration / this._index.timescale, isPrecize: true, }; } /** * @param {Object} newIndex */ _replace(newIndex: TemplateRepresentationIndex): void { this._index = newIndex._index; this._isDynamic = newIndex._isDynamic; this._periodStart = newIndex._periodStart; this._scaledRelativePeriodEnd = newIndex._scaledRelativePeriodEnd; this._manifestBoundsCalculator = newIndex._manifestBoundsCalculator; } /** * @param {Object} newIndex */ _update(newIndex: TemplateRepresentationIndex): void { // As segments are not declared individually, as long as this Representation // is present, we have every information we need this._replace(newIndex); } /** * Returns the timescaled start of the first segment that should be available, * relatively to the start of the Period. * @returns {number | null | undefined} */ private _getFirstSegmentStart(): number | null | undefined { if (!this._isDynamic) { return 0; // it is the start of the Period } // 1 - check that this index is already available if ( this._scaledRelativePeriodEnd === 0 || this._scaledRelativePeriodEnd === undefined ) { // /!\ The scaled max position augments continuously and might not // reflect exactly the real server-side value. As segments are // generated discretely. const maximumSegmentTime = this._manifestBoundsCalculator.getEstimatedMaximumPosition( this._availabilityTimeOffset ?? 0, ); if (maximumSegmentTime !== undefined && maximumSegmentTime < this._periodStart) { // Maximum position is before this period. // No segment is yet available here return null; } } const { duration, timescale } = this._index; const firstPosition = this._manifestBoundsCalculator.getEstimatedMinimumSegmentTime( duration / timescale, ); if (firstPosition === undefined) { return undefined; } const segmentTime = firstPosition > this._periodStart ? (firstPosition - this._periodStart) * timescale : 0; const numberIndexedToZero = Math.floor(segmentTime / duration); return numberIndexedToZero * duration; } /** * Returns the timescaled start of the last segment that should be available, * relatively to the start of the Period. * Returns null if live time is before current period. * @returns {number|null|undefined} */ private _getLastSegmentStart(): number | null | undefined { const { duration, timescale, endNumber, startNumber = 1 } = this._index; if (this._isDynamic) { const liveEdge = this._manifestBoundsCalculator.getEstimatedLiveEdge(); if ( liveEdge !== undefined && this._scaledRelativePeriodEnd !== undefined && this._scaledRelativePeriodEnd < liveEdge - this._periodStart * this._index.timescale ) { let numberOfSegments = Math.ceil(this._scaledRelativePeriodEnd / duration); if (endNumber !== undefined && endNumber - startNumber + 1 < numberOfSegments) { numberOfSegments = endNumber - startNumber + 1; } return (numberOfSegments - 1) * duration; } const lastPosition = this._manifestBoundsCalculator.getEstimatedMaximumPosition( this._availabilityTimeOffset ?? 0, ); if (lastPosition === undefined) { return undefined; } // /!\ The scaled last position augments continuously and might not // reflect exactly the real server-side value. As segments are // generated discretely. const scaledLastPosition = (lastPosition - this._periodStart) * timescale; // Maximum position is before this period. // No segment is yet available here if (scaledLastPosition < 0) { return null; } let numberOfSegmentsAvailable = Math.floor(scaledLastPosition / duration); if ( endNumber !== undefined && endNumber - startNumber + 1 < numberOfSegmentsAvailable ) { numberOfSegmentsAvailable = endNumber - startNumber + 1; } return numberOfSegmentsAvailable <= 0 ? null : (numberOfSegmentsAvailable - 1) * duration; } else { const maximumTime = this._scaledRelativePeriodEnd ?? 0; let numberOfSegments = Math.ceil(maximumTime / duration); if (endNumber !== undefined && endNumber - startNumber + 1 < numberOfSegments) { numberOfSegments = endNumber - startNumber + 1; } const regularLastSegmentStart = (numberOfSegments - 1) * duration; // In some SegmentTemplate, we could think that there is one more // segment that there actually is due to a very little difference between // the period's duration and a multiple of a segment's duration. // Check that we're within a good margin const minimumDuration = config.getCurrent().MINIMUM_SEGMENT_SIZE * timescale; if ( endNumber !== undefined || maximumTime - regularLastSegmentStart > minimumDuration || numberOfSegments < 2 ) { return regularLastSegmentStart; } return (numberOfSegments - 2) * duration; } } /** * Returns an estimate of the last available position in this * `RepresentationIndex` based on attributes such as the Period's end and * the `endNumber` attribute. * If the estimate cannot be made (e.g. this Period's segments are still being * generated and its end is yet unknown), returns `undefined`. * @returns {number|undefined} */ private _estimateRelativeScaledEnd(): number | undefined { if (this._index.endNumber !== undefined) { const numberOfSegments = this._index.endNumber - (this._index.startNumber ?? 1) + 1; return Math.max( Math.min( numberOfSegments * this._index.duration, this._scaledRelativePeriodEnd ?? Infinity, ), 0, ); } if (this._scaledRelativePeriodEnd === undefined) { return undefined; } return Math.max(this._scaledRelativePeriodEnd, 0); } }