rx-player
Version:
Canal+ HTML5 Video Player
381 lines (354 loc) • 11.9 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 log from "../../../../../log";
import type { IRepresentationIndex, ISegment } from "../../../../../manifest";
import isNullOrUndefined from "../../../../../utils/is_null_or_undefined";
import type { IEMSG } from "../../../../containers/isobmff";
import { getTimescaledRange } from "../../../utils/index_helpers";
import getInitSegment from "./get_init_segment";
import { constructRepresentationUrl } from "./tokens";
/**
* Index property defined for a SegmentList RepresentationIndex
* This object contains every property needed to generate an ISegment for a
* given media time.
*/
export interface IListIndex {
/**
* Duration of each element in the list, in the timescale given (see
* timescale and list properties.)
*/
duration: 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;
/** Information on the list of segments for this index. */
list: Array<{
/**
* URL path, to add to the wanted CDN, to access this media segment.
* `null` if no URL exists.
*/
url: string | null;
/** Possible byte-range of the segment. */
mediaRange?: [number, number] | undefined;
}>;
/**
* 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 SegmentList RepresentationIndex.
* Most of the properties here are already defined in IListIndex.
*/
export interface IListIndexIndexArgument {
duration?: number | undefined;
indexRange?: [number, number] | undefined;
initialization?: {
media?: string | undefined;
range?: [number, number] | undefined;
};
list: Array<{
media?: string | undefined;
mediaRange?: [number, 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;
timescale?: number | undefined;
}
/** Aditional context needed by a SegmentList RepresentationIndex. */
export interface IListIndexContextArgument {
/** 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;
/** 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;
}
export default class ListRepresentationIndex implements IRepresentationIndex {
/** Underlying structure to retrieve segment information. */
private _index: IListIndex;
/** Start of the period concerned by this RepresentationIndex, in seconds. */
protected _periodStart: number;
/** End of the period concerned by this RepresentationIndex, in seconds. */
protected _periodEnd: number | undefined;
/* Function that tells if an EMSG is whitelisted by the manifest */
private _isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean;
/**
* @param {Object} index
* @param {Object} context
*/
constructor(index: IListIndexIndexArgument, context: IListIndexContextArgument) {
if (index.duration === undefined) {
throw new Error("Invalid SegmentList: no duration");
}
const {
periodStart,
periodEnd,
representationId,
representationBitrate,
isEMSGWhitelisted,
} = context;
this._isEMSGWhitelisted = isEMSGWhitelisted;
this._periodStart = periodStart;
this._periodEnd = periodEnd;
const presentationTimeOffset = index.presentationTimeOffset ?? 0;
const timescale = index.timescale ?? 1;
const indexTimeOffset = presentationTimeOffset - periodStart * timescale;
const initializationUrl =
index.initialization?.media === undefined
? null
: constructRepresentationUrl(
index.initialization.media,
representationId,
representationBitrate,
);
const list = index.list.map((lItem) => ({
url:
lItem.media === undefined
? null
: constructRepresentationUrl(
lItem.media,
representationId,
representationBitrate,
),
mediaRange: lItem.mediaRange,
}));
this._index = {
list,
timescale,
duration: index.duration,
indexTimeOffset,
indexRange: index.indexRange,
initialization: isNullOrUndefined(index.initialization)
? undefined
: { url: initializationUrl, range: index.initialization.range },
};
}
/**
* Construct init Segment.
* @returns {Object}
*/
getInitSegment(): ISegment {
const initSegment = getInitSegment(this._index);
if (initSegment.privateInfos === undefined) {
initSegment.privateInfos = {};
}
initSegment.privateInfos.isEMSGWhitelisted = this._isEMSGWhitelisted;
return initSegment;
}
/**
* @param {Number} fromTime
* @param {Number} dur
* @returns {Array.<Object>}
*/
getSegments(fromTime: number, dur: number): ISegment[] {
const index = this._index;
const { duration, list, timescale } = index;
const durationInSeconds = duration / timescale;
const fromTimeInPeriod = fromTime - this._periodStart;
const [up, to] = getTimescaledRange(fromTimeInPeriod, dur, timescale);
const length = Math.min(list.length - 1, Math.floor(to / duration));
const segments: ISegment[] = [];
let i = Math.floor(up / duration);
while (i <= length) {
const range = list[i].mediaRange;
const url = list[i].url;
const time = i * durationInSeconds + this._periodStart;
const segment = {
id: String(i),
time,
isInit: false,
range,
duration: durationInSeconds,
timescale: 1 as const,
end: time + durationInSeconds,
url,
timestampOffset: -(index.indexTimeOffset / timescale),
complete: true,
privateInfos: { isEMSGWhitelisted: this._isEMSGWhitelisted },
};
segments.push(segment);
i++;
}
return segments;
}
/**
* Returns whether the Manifest should be refreshed based on the
* `ListRepresentationIndex`'s state and the time range the player is
* currently considering.
* @param {Number} _fromTime
* @param {Number} _toTime
* @returns {Boolean}
*/
shouldRefresh(_fromTime: number, _toTime: number): boolean {
// DASH Manifests are usually refreshed through other means, i.e. thanks to
// the `minimumUpdatePeriod` attribute.
// Moreover, SegmentList are usually only found in static MPDs.
return false;
}
/**
* Returns first position in this index, in seconds.
* @returns {Number}
*/
getFirstAvailablePosition(): number {
return this._periodStart;
}
/**
* Returns last position in this index, in seconds.
* @returns {Number}
*/
getLastAvailablePosition(): number {
const index = this._index;
const { duration, list } = index;
return Math.min(
(list.length * duration) / index.timescale + this._periodStart,
this._periodEnd ?? Infinity,
);
}
/**
* Returns the absolute end in seconds this RepresentationIndex can reach once
* all segments are available.
* @returns {number|null|undefined}
*/
getEnd(): number | null {
return this.getLastAvailablePosition();
}
/**
* 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 `ListRepresentationIndex` because all segments should
* be directly available.
* @returns {boolean}
*/
awaitSegmentBetween(): false {
return false;
}
/**
* Returns true if a Segment returned by this index is still considered
* available.
* @returns {Boolean}
*/
isSegmentStillAvailable(): true {
return true;
}
/**
* We do not check for discontinuity in SegmentList-based indexes.
* @returns {null}
*/
checkDiscontinuity(): null {
return null;
}
/**
* SegmentList should not be updated.
* @returns {Boolean}
*/
canBeOutOfSyncError(): false {
return false;
}
/**
* @returns {Boolean}
*/
isStillAwaitingFutureSegments(): false {
return false;
}
/**
* @returns {Boolean}
*/
isInitialized(): true {
return true;
}
initialize(): void {
log.error("dash", "A `ListRepresentationIndex` does not need to be initialized");
}
addPredictedSegments(): void {
log.warn("dash", "Cannot add predicted segments to a `ListRepresentationIndex`");
}
/**
* 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 {
const { duration, timescale } = this._index;
return {
duration: duration / timescale,
isPrecize: true,
};
}
/**
* @param {Object} newIndex
*/
_replace(newIndex: ListRepresentationIndex): void {
this._index = newIndex._index;
}
_update(): void {
log.error("dash", "A `ListRepresentationIndex` cannot be updated");
}
}