rx-player
Version:
Canal+ HTML5 Video Player
344 lines (320 loc) • 11.7 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 { MediaError } from "../../errors";
import type {
ICdnMetadata,
IManifestStreamEvent,
IParsedAdaptations,
IParsedPeriod,
} from "../../parsers/manifest";
import type { ITrackType, IRepresentationFilter } from "../../public_types";
import arrayFind from "../../utils/array_find";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import type { IAdaptationMetadata, IPeriodMetadata } from "../types";
import { getAdaptations, getSupportedAdaptations, periodContainsTime } from "../utils";
import Adaptation from "./adaptation";
import type CodecSupportCache from "./codec_support_cache";
import type { IRepresentationIndex } from "./representation_index";
/** Structure listing every `Adaptation` in a Period. */
export type IManifestAdaptations = Partial<Record<ITrackType, Adaptation[]>>;
/**
* Class representing the tracks and qualities available from a given time
* period in the the Manifest.
* @class Period
*/
export default class Period implements IPeriodMetadata {
/** ID uniquely identifying the Period in the Manifest. */
public readonly id: string;
/** Every 'Adaptation' in that Period, per type of Adaptation. */
public adaptations: IManifestAdaptations;
/** Absolute start time of the Period, in seconds. */
public start: number;
/**
* Duration of this Period, in seconds.
* `undefined` for still-running Periods.
*/
public duration: number | undefined;
/**
* Absolute end time of the Period, in seconds.
* `undefined` for still-running Periods.
*/
public end: number | undefined;
/** Array containing every stream event happening on the period */
public streamEvents: IManifestStreamEvent[];
/**
* If set to an object, this Period has thumbnail tracks.
*/
public thumbnailTracks: IThumbnailTrack[];
/**
* @constructor
* @param {Object} args
* @param {function|undefined} [representationFilter]
*/
constructor(
args: IParsedPeriod,
cachedCodecSupport: CodecSupportCache,
representationFilter?: IRepresentationFilter | undefined,
) {
this.id = args.id;
this.adaptations = createAdaptationsObject(
args.adaptations,
cachedCodecSupport,
representationFilter,
);
if (isArrayEmpty(this.adaptations.video) && isArrayEmpty(this.adaptations.audio)) {
throw new MediaError(
"MANIFEST_PARSE_ERROR",
"The manifest has no video nor audio tracks.",
);
}
this.thumbnailTracks = args.thumbnailTracks.map((thumbnailTrack) => ({
id: thumbnailTrack.id,
mimeType: thumbnailTrack.mimeType,
index: thumbnailTrack.index,
cdnMetadata: thumbnailTrack.cdnMetadata,
height: thumbnailTrack.height,
width: thumbnailTrack.width,
horizontalTiles: thumbnailTrack.horizontalTiles,
verticalTiles: thumbnailTrack.verticalTiles,
start: thumbnailTrack.start,
end: thumbnailTrack.end,
tileDuration: thumbnailTrack.tileDuration,
}));
this.duration = args.duration;
this.start = args.start;
if (!isNullOrUndefined(this.duration) && !isNullOrUndefined(this.start)) {
this.end = this.start + this.duration;
}
this.streamEvents = args.streamEvents === undefined ? [] : args.streamEvents;
}
/**
* Some environments (e.g. in a WebWorker) may not have the capability to know
* if a mimetype+codec combination is supported on the current platform.
*
* Calling `refreshCodecSupport` manually once the codecs supported are known
* by the current environnement allows to work-around this issue.
*
* @param {Array.<Object>} unsupportedAdaptations - Array on which
* `Adaptation`s objects which are now known to have no supported
* `Representation` will be pushed.
* This array might be useful for minor error reporting.
* @param {Array.<Object>} cachedCodecSupport
*/
refreshCodecSupport(
unsupportedAdaptations: Adaptation[],
cachedCodecSupport: CodecSupportCache,
) {
(Object.keys(this.adaptations) as ITrackType[]).forEach((ttype) => {
const adaptationsForType = this.adaptations[ttype];
if (adaptationsForType === undefined) {
return;
}
for (const adaptation of adaptationsForType) {
if (!adaptation.supportStatus.hasCodecWithUndefinedSupport) {
// Go to next adaptation as an optimisation measure.
// NOTE this only is true if we never change a codec from supported
// to unsuported and its opposite.
continue;
}
const wasSupported = adaptation.supportStatus.hasSupportedCodec;
adaptation.refreshCodecSupport(cachedCodecSupport);
if (
wasSupported !== false &&
adaptation.supportStatus.hasSupportedCodec === false
) {
unsupportedAdaptations.push(adaptation);
}
}
}, {});
}
/**
* Returns every `Adaptations` (or `tracks`) linked to that Period, in an
* Array.
* @returns {Array.<Object>}
*/
getAdaptations(): Adaptation[] {
return getAdaptations(this);
}
/**
* Returns every `Adaptations` (or `tracks`) linked to that Period for a
* given type.
* @param {string} adaptationType
* @returns {Array.<Object>}
*/
getAdaptationsForType(adaptationType: ITrackType): Adaptation[] {
const adaptationsForType = this.adaptations[adaptationType];
return adaptationsForType ?? [];
}
/**
* Returns the Adaptation linked to the given ID.
* @param {number|string} wantedId
* @returns {Object|undefined}
*/
getAdaptation(wantedId: string): Adaptation | undefined {
return arrayFind(this.getAdaptations(), ({ id }) => wantedId === id);
}
/**
* Returns Adaptations that contain Representations in supported codecs.
* @param {string|undefined} type - If set filter on a specific Adaptation's
* type. Will return for all types if `undefined`.
* @returns {Array.<Adaptation>}
*/
getSupportedAdaptations(type?: ITrackType | undefined): Adaptation[] {
return getSupportedAdaptations(this, type);
}
/**
* Returns true if the give time is in the time boundaries of this `Period`.
* @param {number} time
* @param {object|null} nextPeriod - Period coming chronologically just
* after in the same Manifest. `null` if this instance is the last `Period`.
* @returns {boolean}
*/
containsTime(time: number, nextPeriod: Period | null): boolean {
return periodContainsTime(this, time, nextPeriod);
}
/**
* Format the current `Period`'s properties into a
* `IPeriodMetadata` format which can better be communicated through
* another thread.
*
* Please bear in mind however that the returned object will not be updated
* when the current `Period` instance is updated, it is only a
* snapshot at the current time.
*
* If you want to keep that data up-to-date with the current `Period`
* instance, you will have to do it yourself.
*
* @returns {Object}
*/
public getMetadataSnapshot(): IPeriodMetadata {
const adaptations: Partial<Record<ITrackType, IAdaptationMetadata[]>> = {};
const baseAdaptations = this.getAdaptations();
for (const adaptation of baseAdaptations) {
let currentAdaps: IAdaptationMetadata[] | undefined = adaptations[adaptation.type];
if (currentAdaps === undefined) {
currentAdaps = [];
adaptations[adaptation.type] = currentAdaps;
}
currentAdaps.push(adaptation.getMetadataSnapshot());
}
return {
start: this.start,
end: this.end,
id: this.id,
streamEvents: this.streamEvents,
adaptations,
thumbnailTracks: this.thumbnailTracks.map((thumbnailTrack) => ({
id: thumbnailTrack.id,
mimeType: thumbnailTrack.mimeType,
height: thumbnailTrack.height,
width: thumbnailTrack.width,
horizontalTiles: thumbnailTrack.horizontalTiles,
verticalTiles: thumbnailTrack.verticalTiles,
start: thumbnailTrack.start,
end: thumbnailTrack.end,
tileDuration: thumbnailTrack.tileDuration,
})),
};
}
}
/**
* Metadata on an image thumbnail track associated to a Period.
*/
export interface IThumbnailTrack {
/** Identifier for that thumbnail track. */
id: string;
/** interface allowing to obtain information on the actual thumbnails. */
index: IRepresentationIndex;
/** Mime-type for loaded thumbnails, allowing to know their format. */
mimeType: string;
/** CDN(s) on which the thumbnails may be loaded. */
cdnMetadata: ICdnMetadata[] | null;
/**
* A loaded thumbnail's height in pixels. Note that there can be multiple actual
* thumbnails per loaded thumbnail resource (see `horizontalTiles` and
* `verticalTiles` properties.
*/
height: number;
/**
* A loaded thumbnail's width in pixels. Note that there can be multiple actual
* thumbnails per loaded thumbnail resource (see `horizontalTiles` and
* `verticalTiles` properties.
*/
width: number;
/**
* Thumbnail tracks are usually grouped together. This is the number of
* images contained horizontally in a whole loaded thumbnail resource.
*/
horizontalTiles: number;
/**
* Thumbnail tracks are usually grouped together. This is the number of
* images contained vertically in a whole loaded thumbnail resource.
*/
verticalTiles: number;
/**
* Starting `position` the first thumbnail of this thumbnail track applies to,
* if known.
*/
start: number | undefined;
/**
* Ending `position` the last thumbnail of this thumbnail track applies to,
* if known.
*/
end: number | undefined;
/**
* Thumbnail tracks are usually grouped together into separate tiles.
* This is the amount of time in seconds each tile spans.
*/
tileDuration: number | undefined;
}
function isArrayEmpty(array: unknown[] | undefined) {
if (!Array.isArray(array)) {
return true;
} else {
return array.length === 0;
}
}
/**
* Creates an object representing adaptations grouped by track type,
* from the given parsed adaptations.
* @param {Object} adaptations
* @param {Object} cachedCodecSupport
* @param {Object|undefined}representationFilter
* @returns {Object}
*/
function createAdaptationsObject(
adaptations: IParsedAdaptations,
cachedCodecSupport: CodecSupportCache,
representationFilter: IRepresentationFilter | undefined,
): Partial<Record<ITrackType, Adaptation[]>> {
const manifestAdaptations: IManifestAdaptations = {};
for (const [type, adaptationsForType] of Object.entries(adaptations)) {
if (isNullOrUndefined(adaptationsForType)) {
continue;
}
manifestAdaptations[type as ITrackType] = adaptationsForType
.map((adaptation): Adaptation => {
const newAdaptation = new Adaptation(adaptation, cachedCodecSupport, {
representationFilter,
});
return newAdaptation;
})
.filter(
(adaptation): adaptation is Adaptation => adaptation.representations.length > 0,
);
}
return manifestAdaptations;
}