rx-player
Version:
Canal+ HTML5 Video Player
694 lines (634 loc) • 24.3 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 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);
}
}