UNPKG

rx-player

Version:
782 lines (724 loc) 27.6 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 { MediaError } from "../../errors"; import log from "../../log"; import { getCodecsWithUnknownSupport } from "../../main_thread/init/utils/update_manifest_codec_support"; import type { IParsedManifest } from "../../parsers/manifest"; import type { ITrackType, IRepresentationFilter } from "../../public_types"; import arrayFind from "../../utils/array_find"; import EventEmitter from "../../utils/event_emitter"; import idGenerator from "../../utils/id_generator"; import warnOnce from "../../utils/warn_once"; import type { IAdaptationMetadata, IManifestMetadata, IPeriodMetadata, IRepresentationMetadata, } from "../types"; import { ManifestMetadataFormat } from "../types"; import { getLivePosition, getMaximumSafePosition, getMinimumSafePosition, getPeriodForTime, getPeriodAfter, toTaggedTrack, } from "../utils"; import type Adaptation from "./adaptation"; import CodecSupportCache from "./codec_support_cache"; import type { ICodecSupportInfo } from "./codec_support_cache"; import type { IManifestAdaptations } from "./period"; import Period from "./period"; import type Representation from "./representation"; import { MANIFEST_UPDATE_TYPE } from "./types"; import type { IPeriodsUpdateResult } from "./update_periods"; import { replacePeriods, updatePeriods } from "./update_periods"; const generateNewManifestId = idGenerator(); /** Options given to the `Manifest` constructor. */ interface IManifestParsingOptions { /** External callback peforming an automatic filtering of wanted Representations. */ representationFilter?: IRepresentationFilter | undefined; /** Optional URL that points to a shorter version of the Manifest used * for updates only. When using this URL for refresh, the manifest will be * updated with the partial update type. If this URL is undefined, then the * manifest will be updated fully when it needs to be refreshed, and it will * fetched through the original URL. */ manifestUpdateUrl?: string | undefined; } /** Representation affected by a `decipherabilityUpdate` event. */ export interface IUpdatedRepresentationInfo { manifest: IManifestMetadata; period: IPeriodMetadata; adaptation: IAdaptationMetadata; representation: IRepresentationMetadata; } /** Events emitted by a `Manifest` instance */ export interface IManifestEvents { /** The Manifest has been updated */ manifestUpdate: IPeriodsUpdateResult; /** Some Representation's decipherability status has been updated */ decipherabilityUpdate: IUpdatedRepresentationInfo[]; /** Some Representation's support status has been updated */ supportUpdate: null; /** * Some `Representation`'s avoidance status has been updated, meaning that we * might have to avoid playing them due to playback issues. */ representationAvoidanceUpdate: IUpdatedRepresentationInfo[]; } /** * Normalized Manifest structure. * * Details the current content being played: * - the duration of the content * - the available tracks * - the available qualities * - the segments defined in those qualities * - ... * while staying agnostic of the transport protocol used (Smooth, DASH etc.). * * The Manifest and its contained information can evolve over time (like when * updating a dynamic manifest or when right management forbid some tracks from * being played). * To perform actions on those changes, any module using this Manifest can * listen to its sent events and react accordingly. * * @class Manifest */ export default class Manifest extends EventEmitter<IManifestEvents> implements IManifestMetadata { public manifestFormat: ManifestMetadataFormat.Class; /** * ID uniquely identifying this Manifest. * No two Manifests should have this ID. * This ID is automatically calculated each time a `Manifest` instance is * created. */ public readonly id: string; /** * Type of transport used by this Manifest (e.g. `"dash"` or `"smooth"`). * * TODO This should never be needed as this structure is transport-agnostic. * But it is specified in the Manifest API. Deprecate? */ public transport: string; /** * List every Period in that Manifest chronologically (from start to end). * A Period contains information about the content available for a specific * period of time. */ public readonly periods: Period[]; /** * When that promise resolves, the whole Manifest needs to be requested again * so it can be refreshed. */ public expired: Promise<void> | null; /** * Deprecated. Equivalent to `manifest.periods[0].adaptations`. * @deprecated */ public adaptations: IManifestAdaptations; /** * If true, the Manifest can evolve over time: * New segments can become available in the future, properties of the manifest * can change... */ public isDynamic: boolean; /** * If true, this Manifest describes a live content. * A live content is a specific kind of content where you want to play very * close to the maximum position (here called the "live edge"). * E.g., a TV channel is a live content. */ public isLive: boolean; /** * If `true`, no more periods will be added after the current last manifest's * Period. * `false` if we know that more Period is coming or if we don't know. */ public isLastPeriodKnown: boolean; /* * Every URI linking to that Manifest. * They can be used for refreshing the Manifest. * Listed from the most important to the least important. */ public uris: string[]; /** Optional URL that points to a shorter version of the Manifest used * for updates only. */ public updateUrl: string | undefined; /** * Suggested delay from the "live edge" (i.e. the position corresponding to * the current broadcast for a live content) the content is suggested to start * from. * This only applies to live contents. */ public suggestedPresentationDelay: number | undefined; /** * Amount of time, in seconds, this Manifest is valid from the time when it * has been fetched. * If no lifetime is set, this Manifest does not become invalid after an * amount of time. */ public lifetime: number | undefined; /** * Minimum time, in seconds, at which a segment defined in the Manifest * can begin. * This is also used as an offset for live content to apply to a segment's * time. */ public availabilityStartTime: number | undefined; /** * It specifies the wall-clock time when the manifest was generated and published * at the origin server. It is present in order to identify different versions * of manifest instances. */ public publishTime: number | undefined; /* * Difference between the server's clock in milliseconds and the * monotonically-raising timestamp used by the RxPlayer. * This property allows to calculate the server time at any moment. * `undefined` if we did not obtain the server's time */ public clockOffset: number | undefined; /** * Data allowing to calculate the minimum and maximum seekable positions at * any given time. */ public timeBounds: { /** * This is the theoretical minimum playable position on the content * regardless of the current Adaptation chosen, as estimated at parsing * time. * `undefined` if unknown. * * More technically, the `minimumSafePosition` is the maximum between all * the minimum positions reachable in any of the audio and video Adaptation. * * Together with `timeshiftDepth` and the `maximumTimeData` object, this * value allows to compute at any time the minimum seekable time: * * - if `timeshiftDepth` is not set, the minimum seekable time is a * constant that corresponds to this value. * * - if `timeshiftDepth` is set, `minimumSafePosition` will act as the * absolute minimum seekable time we can never seek below, even when * `timeshiftDepth` indicates a possible lower position. * This becomes useful for example when playing live contents which - * despite having a large window depth - just begun and as such only * have a few segment available for now. * Here, `minimumSafePosition` would be the start time of the initial * segment, and `timeshiftDepth` would be the whole depth that will * become available once enough segments have been generated. */ minimumSafePosition?: number | undefined; /** * Some dynamic contents have the concept of a "window depth" (or "buffer * depth") which allows to set a minimum position for all reachable * segments, in function of the maximum reachable position. * * This is justified by the fact that a server might want to remove older * segments when new ones become available, to free storage size. * * If this value is set to a number, it is the amount of time in seconds * that needs to be substracted from the current maximum seekable position, * to obtain the minimum seekable position. * As such, this value evolves at the same rate than the maximum position * does (if it does at all). * * If set to `null`, this content has no concept of a "window depth". */ timeshiftDepth: number | null; /** Data allowing to calculate the maximum playable position at any given time. */ maximumTimeData: { /** * Current position representing live content. * Only makes sense for un-ended live contents. * * `undefined` if unknown or if it doesn't make sense in the current context. */ livePosition: number | undefined; /** * Whether the maximum positions should evolve linearly over time. * * If set to `true`, the maximum seekable position continuously increase at * the same rate than the time since `time` does. */ isLinear: boolean; /** * This is the theoretical maximum playable position on the content, * regardless of the current Adaptation chosen, as estimated at parsing * time. * * More technically, the `maximumSafePosition` is the minimum between all * attributes indicating the duration of the content in the Manifest. * * That is the minimum between: * - The Manifest original attributes relative to its duration * - The minimum between all known maximum audio positions * - The minimum between all known maximum video positions * * This can for example be understood as the safe maximum playable * position through all possible tacks. */ maximumSafePosition: number; /** * `Monotically-increasing timestamp used by the RxPlayer at the time both * `maximumSafePosition` and `livePosition` were calculated. * This can be used to retrieve a new maximum position from them when they * linearly evolves over time (see `isLinear` property). */ time: number; }; }; /** * Caches the information if a codec is supported or not in the context of the * current content. */ private _cachedCodecSupport: CodecSupportCache; /** * Construct a Manifest instance from a parsed Manifest object (as returned by * Manifest parsers) and options. * * @param {Object} parsedManifest * @param {Object} options */ constructor(parsedManifest: IParsedManifest, options: IManifestParsingOptions) { super(); const { representationFilter, manifestUpdateUrl } = options; this.manifestFormat = ManifestMetadataFormat.Class; this.id = generateNewManifestId(); this.expired = parsedManifest.expired ?? null; this.transport = parsedManifest.transportType; this.clockOffset = parsedManifest.clockOffset; this._cachedCodecSupport = new CodecSupportCache([]); this.periods = parsedManifest.periods .map((parsedPeriod) => { const period = new Period( parsedPeriod, this._cachedCodecSupport, representationFilter, ); return period; }) .sort((a, b) => a.start - b.start); /** * @deprecated It is here to ensure compatibility with the way the * v3.x.x manages adaptations at the Manifest level */ this.adaptations = this.periods[0] === undefined ? {} : this.periods[0].adaptations; this.timeBounds = parsedManifest.timeBounds; this.isDynamic = parsedManifest.isDynamic; this.isLive = parsedManifest.isLive; this.isLastPeriodKnown = parsedManifest.isLastPeriodKnown; this.uris = parsedManifest.uris === undefined ? [] : parsedManifest.uris; this.updateUrl = manifestUpdateUrl; this.lifetime = parsedManifest.lifetime; this.clockOffset = parsedManifest.clockOffset; this.suggestedPresentationDelay = parsedManifest.suggestedPresentationDelay; this.availabilityStartTime = parsedManifest.availabilityStartTime; this.publishTime = parsedManifest.publishTime; } /** * 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 `updateCodecSupport` manually once the codecs supported are known * by the current environnement allows to work-around this issue. * * @param {Array<Object>} [updatedCodecSupportInfo] * @returns {Error|null} - Refreshing codec support might reveal that some * `Adaptation` don't have any of their `Representation`s supported. * In that case, an error object will be created and returned, so you can * e.g. later emit it as a warning through the RxPlayer API. */ public updateCodecSupport( updatedCodecSupportInfo: ICodecSupportInfo[] = [], ): MediaError | null { if (updatedCodecSupportInfo.length === 0) { return null; } this._cachedCodecSupport.addCodecs(updatedCodecSupportInfo); const unsupportedAdaptations: Adaptation[] = []; for (const period of this.periods) { period.refreshCodecSupport(unsupportedAdaptations, this._cachedCodecSupport); } this.trigger("supportUpdate", null); if (unsupportedAdaptations.length > 0) { return new MediaError( "MANIFEST_INCOMPATIBLE_CODECS_ERROR", "An Adaptation contains only incompatible codecs.", { tracks: unsupportedAdaptations.map(toTaggedTrack) }, ); } return null; } /** * Returns the Period corresponding to the given `id`. * Returns `undefined` if there is none. * @param {string} id * @returns {Object|undefined} */ public getPeriod(id: string): Period | undefined { return arrayFind(this.periods, (period) => { return id === period.id; }); } /** * Returns the Period encountered at the given time. * Returns `undefined` if there is no Period exactly at the given time. * @param {number} time * @returns {Object|undefined} */ public getPeriodForTime(time: number): Period | undefined { return getPeriodForTime(this, time); } /** * Returns the first Period starting strictly after the given time. * Returns `undefined` if there is no Period starting after that time. * @param {number} time * @returns {Object|undefined} */ public getNextPeriod(time: number): Period | undefined { return arrayFind(this.periods, (period) => { return period.start > time; }); } /** * Returns the Period coming chronologically just after another given Period. * Returns `undefined` if not found. * @param {Object} period * @returns {Object|null} */ public getPeriodAfter(period: Period): Period | null { return getPeriodAfter(this, period); } /** * Returns the most important URL from which the Manifest can be refreshed. * `undefined` if no URL is found. * @returns {Array.<string>} */ public getUrls(): string[] { return this.uris; } /** * Update the current Manifest properties by giving a new updated version. * This instance will be updated with the new information coming from it. * @param {Object} newManifest */ public replace(newManifest: Manifest): void { this._performUpdate(newManifest, MANIFEST_UPDATE_TYPE.Full); } /** * Update the current Manifest properties by giving a new but shorter version * of it. * This instance will add the new information coming from it and will * automatically clean old Periods that shouldn't be available anymore. * * /!\ Throws if the given Manifest cannot be used or is not sufficient to * update the Manifest. * @param {Object} newManifest */ public update(newManifest: Manifest): void { this._performUpdate(newManifest, MANIFEST_UPDATE_TYPE.Partial); } /** * Returns the theoretical minimum playable position on the content * regardless of the current Adaptation chosen, as estimated at parsing * time. * @returns {number} */ public getMinimumSafePosition(): number { return getMinimumSafePosition(this); } /** * Get the position of the live edge - that is, the position of what is * currently being broadcasted, in seconds. * @returns {number|undefined} */ public getLivePosition(): number | undefined { return getLivePosition(this); } /** * Returns the theoretical maximum playable position on the content * regardless of the current Adaptation chosen, as estimated at parsing * time. */ public getMaximumSafePosition(): number { return getMaximumSafePosition(this); } public updateCodecSupportList(cachedCodecSupport: CodecSupportCache) { this._cachedCodecSupport = cachedCodecSupport; } /** * Look in the Manifest for Representations linked to the given key ID, * and mark them as being impossible to decrypt. * Then trigger a "decipherabilityUpdate" event to notify everyone of the * changes performed. * @param {Function} isDecipherableCb */ public updateRepresentationsDeciperability( isDecipherableCb: (content: { manifest: Manifest; period: Period; adaptation: Adaptation; representation: Representation; }) => boolean | undefined, ): void { const updates = updateDeciperability(this, isDecipherableCb); if (updates.length > 0) { this.trigger("decipherabilityUpdate", updates); } } /** * Indicate that some `Representation` needs to be avoided due to playback * issues. * @param {Array.<Object>} items */ public addRepresentationsToAvoid( items: Array<{ period: Period; adaptation: Adaptation; representation: Representation; }>, ) { const updates = []; for (const item of items) { const period = this.getPeriod(item.period.id); if (period === undefined) { continue; } const adaptation = period.getAdaptation(item.adaptation.id); if (adaptation === undefined) { continue; } const representation = adaptation.getRepresentation(item.representation.id); if (representation === undefined) { continue; } representation.shouldBeAvoided = true; updates.push({ manifest: this, period, adaptation, representation, }); } if (updates.length > 0) { this.trigger("representationAvoidanceUpdate", updates); } } /** * @deprecated only returns adaptations for the first period * @returns {Array.<Object>} */ public getAdaptations(): Adaptation[] { warnOnce( "manifest.getAdaptations() is deprecated." + " Please use manifest.period[].getAdaptations() instead", ); const firstPeriod = this.periods[0]; if (firstPeriod === undefined) { return []; } const adaptationsByType = firstPeriod.adaptations; const adaptationsList: Adaptation[] = []; for (const adaptationType in adaptationsByType) { if (Object.prototype.hasOwnProperty.call(adaptationsByType, adaptationType)) { const adaptations = adaptationsByType[ adaptationType as ITrackType ] as Adaptation[]; adaptationsList.push(...adaptations); } } return adaptationsList; } /** * @deprecated only returns adaptations for the first period * @returns {Array.<Object>} */ public getAdaptationsForType(adaptationType: ITrackType): Adaptation[] { warnOnce( "manifest.getAdaptationsForType(type) is deprecated." + " Please use manifest.period[].getAdaptationsForType(type) instead", ); const firstPeriod = this.periods[0]; if (firstPeriod === undefined) { return []; } const adaptationsForType = firstPeriod.adaptations[adaptationType]; return adaptationsForType === undefined ? [] : adaptationsForType; } /** * @deprecated only returns adaptations for the first period * @returns {Array.<Object>} */ public getAdaptation(wantedId: number | string): Adaptation | undefined { warnOnce( "manifest.getAdaptation(id) is deprecated." + " Please use manifest.period[].getAdaptation(id) instead", ); return arrayFind(this.getAdaptations(), ({ id }) => wantedId === id); } /** * Format the current `Manifest`'s properties into a * `IManifestMetadata` 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 `Manifest` 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 `Manifest` * instance, you will have to do it yourself. * * @returns {Object} */ public getMetadataSnapshot(): IManifestMetadata { const periods: IPeriodMetadata[] = []; for (const period of this.periods) { periods.push(period.getMetadataSnapshot()); } return { manifestFormat: ManifestMetadataFormat.MetadataObject, id: this.id, periods, isDynamic: this.isDynamic, isLive: this.isLive, isLastPeriodKnown: this.isLastPeriodKnown, suggestedPresentationDelay: this.suggestedPresentationDelay, clockOffset: this.clockOffset, uris: this.uris, availabilityStartTime: this.availabilityStartTime, timeBounds: this.timeBounds, }; } /** * Returns a list of all codecs that the support is not known yet. * If a representation with (`isSupported`) is undefined, we consider the * codec support as unknown. * * This function iterates through all periods, adaptations, and representations, * and collects unknown codecs. * * @returns {Array} The list of codecs with unknown support status. */ public getCodecsWithUnknownSupport(): Array<{ mimeType: string; codec: string }> { return getCodecsWithUnknownSupport(this); } /** * @param {Object} newManifest * @param {number} updateType */ private _performUpdate(newManifest: Manifest, updateType: MANIFEST_UPDATE_TYPE): void { this.availabilityStartTime = newManifest.availabilityStartTime; this.expired = newManifest.expired; this.isDynamic = newManifest.isDynamic; this.isLive = newManifest.isLive; this.isLastPeriodKnown = newManifest.isLastPeriodKnown; this.lifetime = newManifest.lifetime; this.clockOffset = newManifest.clockOffset; this.suggestedPresentationDelay = newManifest.suggestedPresentationDelay; this.transport = newManifest.transport; this.publishTime = newManifest.publishTime; let updatedPeriodsResult; if (updateType === MANIFEST_UPDATE_TYPE.Full) { this.timeBounds = newManifest.timeBounds; this.uris = newManifest.uris; updatedPeriodsResult = replacePeriods(this.periods, newManifest.periods); } else { this.timeBounds.maximumTimeData = newManifest.timeBounds.maximumTimeData; this.updateUrl = newManifest.uris[0]; updatedPeriodsResult = updatePeriods(this.periods, newManifest.periods); // Partial updates do not remove old Periods. // This can become a memory problem when playing a content long enough. // Let's clean manually Periods behind the minimum possible position. const min = this.getMinimumSafePosition(); while (this.periods.length > 0) { const period = this.periods[0]; if (period.end === undefined || period.end > min) { break; } this.periods.shift(); } } this.updateCodecSupport(); // Re-set this.adaptations for retro-compatibility in v3.x.x this.adaptations = this.periods[0] === undefined ? {} : this.periods[0].adaptations; // Let's trigger events at the end, as those can trigger side-effects. // We do not want the current Manifest object to be incomplete when those // happen. this.trigger("manifestUpdate", updatedPeriodsResult); } } /** * Update `decipherable` property of every `Representation` found in the * Manifest based on the result of a `isDecipherable` callback: * - When that callback returns `true`, update `decipherable` to `true` * - When that callback returns `false`, update `decipherable` to `false` * - When that callback returns `undefined`, update `decipherable` to * `undefined` * @param {Manifest} manifest * @param {Function} isDecipherable * @returns {Array.<Object>} */ function updateDeciperability( manifest: Manifest, isDecipherable: (content: { manifest: Manifest; period: Period; adaptation: Adaptation; representation: Representation; }) => boolean | undefined, ): IUpdatedRepresentationInfo[] { const updates: IUpdatedRepresentationInfo[] = []; for (const period of manifest.periods) { for (const adaptation of period.getAdaptations()) { let hasOnlyUndecipherableRepresentations = true; for (const representation of adaptation.representations) { const content = { manifest, period, adaptation, representation }; const result = isDecipherable(content); if (result !== false) { hasOnlyUndecipherableRepresentations = false; } if (result !== representation.decipherable) { updates.push(content); representation.decipherable = result; if (result === true) { adaptation.supportStatus.isDecipherable = true; } else if ( result === undefined && adaptation.supportStatus.isDecipherable === false ) { adaptation.supportStatus.isDecipherable = undefined; } log.debug( "manifest", `Decipherability changed for "${representation.id}"`, `(${representation.bitrate})`, String(representation.decipherable), ); } } if (hasOnlyUndecipherableRepresentations) { adaptation.supportStatus.isDecipherable = false; } } } return updates; } export type { IManifestParsingOptions };