UNPKG

rx-player

Version:
572 lines (517 loc) 18.7 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 { NetworkError } from "../../../errors"; import log from "../../../log"; import type { IRepresentationIndex, ISegment } from "../../../manifest"; import type { IPlayerError } from "../../../public_types"; import assert from "../../../utils/assert"; import getMonotonicTimeStamp from "../../../utils/monotonic_timestamp"; import { checkDiscontinuity, getIndexSegmentEnd } from "../utils/index_helpers"; import type { IIndexSegment } from "./shared_smooth_segment_timeline"; import type SharedSmoothSegmentTimeline from "./shared_smooth_segment_timeline"; import { replaceSegmentSmoothTokens } from "./utils/tokens"; /** * @param {Number} start * @param {Number} up * @param {Number} duration * @returns {Number} */ function getSegmentNumber(start: number, up: number, duration: number): number { const diff = up - start; return diff > 0 ? Math.floor(diff / duration) : 0; } /** * Convert second-based start time and duration to the timescale of the * manifest's index. * @param {Object} index * @param {Number} start * @param {Number} duration * @returns {Object} - Object with two properties: * - up {Number}: timescaled timestamp of the beginning time * - to {Number}: timescaled timestamp of the end time (start time + duration) */ function normalizeRange( timescale: number | undefined, start: number, duration: number, ): { up: number; to: number } { const ts = timescale === undefined || timescale === 0 ? 1 : timescale; return { up: start * ts, to: (start + duration) * ts }; } /** * Calculate the number of times a segment repeat based on the next segment. * @param {Object} segment * @param {Object} nextSegment * @returns {Number} */ function calculateRepeat(segment: IIndexSegment, nextSegment?: IIndexSegment): number { let repeatCount = segment.repeatCount; // A negative value of the @r attribute of the S element indicates // that the duration indicated in @d attribute repeats until the // start of the next S element, the end of the Period or until the // next MPD update. // TODO Also for SMOOTH???? if (segment.duration !== undefined && repeatCount < 0) { const repeatEnd = nextSegment !== undefined ? nextSegment.start : Infinity; repeatCount = Math.ceil((repeatEnd - segment.start) / segment.duration) - 1; } return repeatCount; } /** * Supplementary options taken by a SmoothRepresentationIndex bringing the * context the segments are in. */ export interface ISmoothRepresentationIndexContextInformation { /** * If `true` the corresponding Smooth Manifest was announced as a live * content. * `false` otherwise. */ isLive: boolean; /** * Generic tokenized (e.g. with placeholders for time information) URL for * every segments anounced here. */ media: string; /** * Contains information allowing to generate the corresponding initialization * segment. */ segmentPrivateInfos: ISmoothInitSegmentPrivateInfos; sharedSmoothTimeline: SharedSmoothSegmentTimeline; } /** Information allowing to generate a Smooth initialization segment. */ interface ISmoothInitSegmentPrivateInfos { bitsPerSample?: number | undefined; channels?: number | undefined; codecPrivateData?: string | undefined; packetSize?: number | undefined; samplingRate?: number | undefined; protection?: { keyId: Uint8Array } | undefined; height?: number | undefined; width?: number | undefined; } /** * RepresentationIndex implementation for Smooth Manifests. * * Allows to interact with the index to create new Segments. * * @class SmoothRepresentationIndex */ export default class SmoothRepresentationIndex implements IRepresentationIndex { /** * Information needed to generate an initialization segment. * Taken from the Manifest. */ private _initSegmentInfos: { codecPrivateData?: string | undefined; bitsPerSample?: number | undefined; channels?: number | undefined; packetSize?: number | undefined; samplingRate?: number | undefined; timescale: number; height?: number | undefined; width?: number | undefined; protection?: { keyId: Uint8Array } | undefined; }; /** * Value only calculated for live contents. * * Calculates the difference, in timescale, between the monotonically-raising * timestamp used by the RxPlayer and the time of the last segment known to * have been generated on the server-side. * Useful to know if a segment present in the timeline has actually been * generated on the server-side */ private _scaledLiveGap: number | undefined; /** * Defines the end of the latest available segment when this index was known to * be valid, in the index's timescale. */ private _initialScaledLastPosition: number | undefined; /** * If `true` the corresponding Smooth Manifest was announced as a live * content. * `false` otherwise. */ private _isLive: boolean; /** * Contains information on the list of segments available in this * SmoothRepresentationIndex. */ private _sharedSmoothTimeline: SharedSmoothSegmentTimeline; /** * Generic tokenized (e.g. with placeholders for time information) URL for * every segments anounced here. */ private _media: string; /** * Creates a new `SmoothRepresentationIndex`. * @param {Object} index * @param {Object} options */ constructor(options: ISmoothRepresentationIndexContextInformation) { const { isLive, segmentPrivateInfos, media, sharedSmoothTimeline } = options; this._sharedSmoothTimeline = sharedSmoothTimeline; this._initSegmentInfos = { bitsPerSample: segmentPrivateInfos.bitsPerSample, channels: segmentPrivateInfos.channels, codecPrivateData: segmentPrivateInfos.codecPrivateData, packetSize: segmentPrivateInfos.packetSize, samplingRate: segmentPrivateInfos.samplingRate, timescale: sharedSmoothTimeline.timescale, height: segmentPrivateInfos.height, width: segmentPrivateInfos.width, protection: segmentPrivateInfos.protection, }; this._isLive = isLive; this._media = media; if (sharedSmoothTimeline.timeline.length !== 0 && isLive) { const { timeline, validityTime } = sharedSmoothTimeline; const lastItem = timeline[timeline.length - 1]; const scaledEnd = getIndexSegmentEnd(lastItem, null); const scaledTimelineValidityTime = (validityTime / 1000) * sharedSmoothTimeline.timescale; this._scaledLiveGap = scaledTimelineValidityTime - scaledEnd; } } /** * Construct init Segment compatible with a Smooth Manifest. * @returns {Object} */ getInitSegment(): ISegment { return { id: "init", isInit: true, privateInfos: { smoothInitSegment: this._initSegmentInfos }, url: null, time: 0, end: 0, duration: 0, timescale: 1, complete: true, }; } /** * Generate a list of Segments for a particular period of time. * * @param {Number} from * @param {Number} dur * @returns {Array.<Object>} */ getSegments(from: number, dur: number): ISegment[] { this._refreshTimeline(); const { timescale, timeline } = this._sharedSmoothTimeline; const { up, to } = normalizeRange(timescale, from, dur); const media = this._media; let currentNumber: number | undefined; const segments: ISegment[] = []; const timelineLength = timeline.length; const maxPosition = this._scaledLiveGap === undefined ? undefined : (getMonotonicTimeStamp() / 1000) * timescale - this._scaledLiveGap; for (let i = 0; i < timelineLength; i++) { const segmentRange = timeline[i]; const { duration, start } = segmentRange; const repeat = calculateRepeat(segmentRange, timeline[i + 1]); let segmentNumberInCurrentRange = getSegmentNumber(start, up, duration); let segmentTime = start + segmentNumberInCurrentRange * duration; const timeToAddToCheckMaxPosition = duration; while ( segmentTime < to && segmentNumberInCurrentRange <= repeat && (maxPosition === undefined || segmentTime + timeToAddToCheckMaxPosition <= maxPosition) ) { const time = segmentTime; const number = currentNumber !== undefined ? currentNumber + segmentNumberInCurrentRange : undefined; const segment = { id: String(segmentTime), isInit: false, time: time / timescale, end: (time + duration) / timescale, duration: duration / timescale, timescale: 1 as const, number, url: replaceSegmentSmoothTokens(media, time), complete: true, privateInfos: { smoothMediaSegment: { time, duration } }, }; segments.push(segment); // update segment number and segment time for the next segment segmentNumberInCurrentRange++; segmentTime = start + segmentNumberInCurrentRange * duration; } if (segmentTime >= to) { // we reached ``to``, we're done return segments; } if (currentNumber !== undefined) { currentNumber += repeat + 1; } } return segments; } /** * Returns true if, based on the arguments, the index should be refreshed. * (If we should re-fetch the manifest) * @param {Number} up * @param {Number} to * @returns {Boolean} */ shouldRefresh(up: number, to: number): boolean { this._refreshTimeline(); if (!this._isLive) { return false; } const { timeline, timescale } = this._sharedSmoothTimeline; const lastSegmentInCurrentTimeline = timeline[timeline.length - 1]; if (lastSegmentInCurrentTimeline === undefined) { return false; } const repeat = lastSegmentInCurrentTimeline.repeatCount; const endOfLastSegmentInCurrentTimeline = lastSegmentInCurrentTimeline.start + (repeat + 1) * lastSegmentInCurrentTimeline.duration; if (to * timescale < endOfLastSegmentInCurrentTimeline) { return false; } if (up * timescale >= endOfLastSegmentInCurrentTimeline) { return true; } // ---- const startOfLastSegmentInCurrentTimeline = lastSegmentInCurrentTimeline.start + repeat * lastSegmentInCurrentTimeline.duration; return up * timescale > startOfLastSegmentInCurrentTimeline; } /** * Returns first position available in the index. * @returns {Number|null} */ getFirstAvailablePosition(): number | null { this._refreshTimeline(); const { timeline, timescale } = this._sharedSmoothTimeline; if (timeline.length === 0) { return null; } return timeline[0].start / timescale; } /** * Returns last position available in the index. * @returns {Number} */ getLastAvailablePosition(): number | undefined { this._refreshTimeline(); const { timeline, timescale } = this._sharedSmoothTimeline; if (this._scaledLiveGap === undefined) { const lastTimelineElement = timeline[timeline.length - 1]; return getIndexSegmentEnd(lastTimelineElement, null) / timescale; } for (let i = timeline.length - 1; i >= 0; i--) { const timelineElt = timeline[i]; const timescaledNow = (getMonotonicTimeStamp() / 1000) * timescale; const { start, duration, repeatCount } = timelineElt; for (let j = repeatCount; j >= 0; j--) { const end = start + duration * (j + 1); const positionToReach = end; if (positionToReach <= timescaledNow - this._scaledLiveGap) { return end / timescale; } } } return undefined; } /** * 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._isLive) { return this.getLastAvailablePosition(); } return undefined; } /** * 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.isStillAwaitingFutureSegments()) { return false; } const lastAvailablePosition = this.getLastAvailablePosition(); if (lastAvailablePosition !== undefined && end < lastAvailablePosition) { return false; } return end > (this.getFirstAvailablePosition() ?? 0) ? undefined : false; } /** * Checks if `timeSec` is in a discontinuity. * That is, if there's no segment available for the `timeSec` position. * @param {number} timeSec - The time to check if it's in a discontinuity, in * seconds. * @returns {number | null} - If `null`, no discontinuity is encountered at * `time`. If this is a number instead, there is one and that number is the * position for which a segment is available in seconds. */ checkDiscontinuity(timeSec: number): number | null { this._refreshTimeline(); return checkDiscontinuity(this._sharedSmoothTimeline, timeSec, undefined); } /** * 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(); const { timeline, timescale } = this._sharedSmoothTimeline; for (let i = 0; i < timeline.length; i++) { const tSegment = timeline[i]; const tSegmentTime = tSegment.start / timescale; if (tSegmentTime > segment.time) { return false; // We went over it without finding it } else if (tSegmentTime === segment.time) { return true; } 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 <= tSegment.repeatCount; } } } return false; } /** * @param {Error} error * @returns {Boolean} */ canBeOutOfSyncError(error: IPlayerError): boolean { if (!this._isLive) { return false; } return ( error instanceof NetworkError && (error.isHttpError(404) || error.isHttpError(412)) ); } /** * 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._sharedSmoothTimeline; const firstElementInTimeline = timeline[0]; if (firstElementInTimeline === undefined) { return undefined; } return { duration: firstElementInTimeline.duration / timescale, isPrecize: false, }; } /** * Replace this RepresentationIndex by a newly downloaded one. * Check if the old index had more information about new segments and re-add * them if that's the case. * @param {Object} newIndex */ _replace(newIndex: SmoothRepresentationIndex): void { this._initialScaledLastPosition = newIndex._initialScaledLastPosition; this._scaledLiveGap = newIndex._scaledLiveGap; this._sharedSmoothTimeline.replace(newIndex._sharedSmoothTimeline); } /** * Update the current index with a new, partial, version. * This method might be use to only add information about new segments. * @param {Object} newIndex */ _update(newIndex: SmoothRepresentationIndex): void { this._scaledLiveGap = newIndex._scaledLiveGap; this._sharedSmoothTimeline.update(newIndex._sharedSmoothTimeline); } /** * Returns `false` if the last segments in this index have already been * generated. * Returns `true` if the index is still waiting on future segments to be * generated. * * For Smooth, it should only depend on whether the content is a live content * or not. * TODO What about Smooth live content that finishes at some point? * @returns {boolean} */ isStillAwaitingFutureSegments(): boolean { return this._isLive; } /** * @returns {Boolean} */ isInitialized(): true { return true; } initialize(): void { log.error("smooth", "A `SmoothRepresentationIndex` does not need to be initialized"); } /** * Add segments to a `SharedSmoothSegmentTimeline` that were predicted to come * after `currentSegment`. * @param {Array.<Object>} nextSegments - The segment information parsed. * @param {Object} currentSegment - Information on the segment which contained * that new segment information. */ addPredictedSegments( nextSegments: Array<{ duration: number; time: number; timescale: number }>, currentSegment: ISegment, ): void { this._sharedSmoothTimeline.addPredictedSegments(nextSegments, currentSegment); } /** * Clean-up timeline to remove segment information which should not be * available due to the timeshift window */ private _refreshTimeline(): void { this._sharedSmoothTimeline.refresh(); } }