UNPKG

rx-player

Version:
531 lines (503 loc) 18.2 kB
import features from "../../features"; import log from "../../log"; import type { IContentInitializationData } from "../../main_thread/types"; import type { IManifest } from "../../manifest"; import type { IMediaSourceInterface } from "../../mse"; import MainMediaSourceInterface from "../../mse/main_media_source_interface"; import WorkerMediaSourceInterface from "../../mse/worker_media_source_interface"; import type { IPlayerError } from "../../public_types"; import idGenerator from "../../utils/id_generator"; import type { CancellationError, CancellationSignal } from "../../utils/task_canceller"; import TaskCanceller from "../../utils/task_canceller"; import type { IRepresentationEstimator } from "../adaptive"; import createAdaptiveRepresentationSelector from "../adaptive"; import type { IRepresentationEstimatorThrottlers } from "../adaptive/adaptive_representation_selector"; import CmcdDataBuilder from "../cmcd"; import type { IManifestRefreshSettings } from "../fetchers"; import { ManifestFetcher, SegmentQueueCreator } from "../fetchers"; import CdnPrioritizer from "../fetchers/cdn_prioritizer"; import createThumbnailFetcher from "../fetchers/thumbnails/thumbnail_fetcher"; import type { IThumbnailFetcher } from "../fetchers/thumbnails/thumbnail_fetcher"; import SegmentSinksStore from "../segment_sinks"; import type { IAttachMediaSourceCoreMessagePayload, ICoreMessage } from "../types"; import { CoreMessageType } from "../types"; import CoreTextDisplayerInterface from "./core_text_displayer_interface"; import FreezeResolver from "./FreezeResolver"; import TrackChoiceSetter from "./track_choice_setter"; import type { ICorePlugins } from "./utils"; import { extractExternalPlugins, formatErrorForSender, updateCodecSupportInWorkerMode, } from "./utils"; /** Function allowing to associate a unique identifier to all created `MediaSource` */ const generateMediaSourceId = idGenerator(); /** * Class facilitating the workflows behind loading a new content for the * RxPlayer Core: * * - Handle Manifest fetching and Manifest updates. * * - Handle the `MediaSource`'s creation and indirectly of its `SourceBuffer`s * as well as handling "MediaSource reloading". * * - initialize various modules (`segmentQueueCreator`, CmcdDataBuilder`, * `RepresentationEstimator`) linked to the initialized content. * * You can start loading a content through the `initializeNewContent` method. * * When a content is linked to the `ContentPreparer` you can inspect the * different initialized modules by calling its `getCurrentContent` method. * * @class ContentPreparer */ export default class ContentPreparer { /** * Information on the content linked to that `ContentPreparer` through its * `initializeNewContent` method. * `null` if no content is initialized. */ private _currentContent: IPreparedContentData | null; /** * TaskCanceller which is triggered when the currently-initialized content is * not needed anymore, because we stopped it since or switched to a new content. */ private _contentCanceller: TaskCanceller; /** * TaskCanceller which is triggered when the currently-created MediaSource is * not needed anymore, either because the content has changed or because we * had to reload. */ private _currentMediaSourceCanceller: TaskCanceller; /** @see constructor */ private _hasVideo: boolean; /** * @param {Object} capabilities * @param {boolean} capabilities.hasVideo - If `true`, we're playing on an * element which has video capabilities. * If `false`, we're only able to play audio, optionally with subtitles. * * Typically this boolean is `true` for `<video>` HTMLElement and `false` for * `<audio>` HTMLElement. */ constructor({ hasVideo }: { hasVideo: boolean }) { this._currentContent = null; this._currentMediaSourceCanceller = new TaskCanceller("ContentPreparer MediaSource"); this._hasVideo = hasVideo; const contentCanceller = new TaskCanceller("ContentPreparer"); this._contentCanceller = contentCanceller; } /** * Start fetching the wanted content's Manifest and initializing the various * modules stored by the `ContentPreparer` linked to that content. * * The returned Promise resolves with the parsed Manifest when those modules * are all ready and you can thus begin to load the content. * * Reject if it failed to do so. * @param {Object} context - Information on the content that should be * initialized. * @param {Object} corePlugins - Callbacks that may have been registered by * the application if it loaded the core independently as a worker. * @returns {Promise.<Object>} */ public initializeNewContent( sendMessage: (msg: ICoreMessage, transferables?: Transferable[]) => void, context: IContentInitializationData, /** Allows to filter which Representations can be choosen. */ throttlers: IRepresentationEstimatorThrottlers, corePlugins: ICorePlugins, ): Promise<IManifest> { return new Promise((res, rej) => { this.disposeCurrentContent("new init"); const contentCanceller = this._contentCanceller; const currentMediaSourceCanceller = new TaskCanceller( "ContentPreparer MediaSource", ); this._currentMediaSourceCanceller = currentMediaSourceCanceller; currentMediaSourceCanceller.linkToSignal(contentCanceller.signal); const { contentId, url, hasText, transportOptions, useMseInWorker, enableRepresentationAvoidance, transport, } = context; let manifest: IManifest | null = null; const transportFn = features.transports[transport]; if (typeof transportFn !== "function") { rej( new Error( `transport "${transport}" not supported. ` + "Did you add the corresponding feature?", ), ); return; } const transportPipelines = transportFn({ ...transportOptions, ...extractExternalPlugins(transportOptions, corePlugins), }); const cmcdDataBuilder = context.cmcd === undefined ? null : new CmcdDataBuilder(context.cmcd); const manifestFetcher = new ManifestFetcher( url === undefined ? undefined : [url], transportPipelines, { cmcdDataBuilder, ...context.manifestRetryOptions, }, ); const representationEstimator = createAdaptiveRepresentationSelector({ initialBitrates: { audio: context.initialAudioBitrate ?? 0, video: context.initialVideoBitrate ?? 0, }, lowLatencyMode: transportOptions.lowLatencyMode, throttlers, }); const unbindRejectOnCancellation = currentMediaSourceCanceller.signal.register( (error: CancellationError) => { rej(error); }, ); const cdnPrioritizer = new CdnPrioritizer(contentCanceller.signal); const segmentQueueCreator = new SegmentQueueCreator( transportPipelines, cdnPrioritizer, cmcdDataBuilder, context.segmentRetryOptions, ); const fetchThumbnailData = createThumbnailFetcher( transportPipelines.thumbnails, cdnPrioritizer, ); const trackChoiceSetter = new TrackChoiceSetter(); const [mediaSource, segmentSinksStore, coreTextSender] = createMediaSourceInterfaceAndSegmentSinksStore( sendMessage, contentId, { useMseInWorker, hasVideo: this._hasVideo, hasText, }, currentMediaSourceCanceller.signal, ); const freezeResolver = new FreezeResolver(segmentSinksStore); this._currentContent = { cmcdDataBuilder, contentId, enableRepresentationAvoidance, freezeResolver, mediaSource, manifest: null, manifestFetcher, representationEstimator, segmentSinksStore, segmentQueueCreator, fetchThumbnailData, coreTextSender, trackChoiceSetter, useMseInWorker, }; mediaSource.addEventListener( "mediaSourceOpen", function () { checkIfReadyAndValidate(); }, currentMediaSourceCanceller.signal, ); contentCanceller.signal.register((err) => { manifestFetcher.dispose(err.reason); }); manifestFetcher.addEventListener( "warning", (err: IPlayerError) => { sendMessage({ type: CoreMessageType.Warning, contentId, value: formatErrorForSender(err), }); }, contentCanceller.signal, ); manifestFetcher.addEventListener( "manifestReady", (man: IManifest) => { if (manifest !== null) { log.warn("Core", "Multiple `manifestReady` events, ignoring"); return; } manifest = man; if (this._currentContent !== null) { this._currentContent.manifest = manifest; } checkIfReadyAndValidate(); }, currentMediaSourceCanceller.signal, ); manifestFetcher.addEventListener( "error", (err: unknown) => { rej(err); }, contentCanceller.signal, ); manifestFetcher.start(); function checkIfReadyAndValidate() { if ( manifest === null || mediaSource.readyState === "closed" || currentMediaSourceCanceller.isUsed() ) { return; } updateCodecSupportInWorkerMode(manifest); manifest.addEventListener( "manifestUpdate", (updates) => { if (manifest === null) { // TODO log warn? return; } sendMessage({ type: CoreMessageType.ManifestUpdate, contentId, value: { manifest, updates }, }); }, contentCanceller.signal, ); unbindRejectOnCancellation(); res(manifest); } }); } /** * Get information on the current content prepared through the * `initializeNewContent` method, or `null` if no content is currently * prepared. * @returns {Object|null} */ public getCurrentContent(): IPreparedContentData | null { return this._currentContent; } /** * Schedule an update for the Manifest file, * * Do nothing if no content is currently prepared. * @param {Object} settings - Various settings to configure the ways and * moment at which the Manifest will be refreshed. */ public scheduleManifestRefresh(settings: IManifestRefreshSettings): void { this._currentContent?.manifestFetcher.scheduleManualRefresh(settings); } /** * Change the MediaSource attached for the current content. * It is assumed that main thread is already notified that such a reload is * happening. * * The returned Promise resolves when it restarts being ready. * @param {Function} sendMessage * @returns {Promise} */ public reloadMediaSource( sendMessage: (msg: ICoreMessage, transferables?: Transferable[]) => void, ): Promise<void> { this._currentMediaSourceCanceller.cancel("ContentPreparer MediaSource reload"); if (this._currentContent === null) { return Promise.reject(new Error("CP: No content anymore")); } this._currentContent.trackChoiceSetter.reset(); this._currentContent.coreTextSender?.stop("ContentPreparer MediaSource reload"); this._currentMediaSourceCanceller = new TaskCanceller("ContentPreparer MediaSource"); this._currentMediaSourceCanceller.linkToSignal(this._contentCanceller.signal); const [mediaSourceInterface, segmentSinksStore, coreTextSender] = createMediaSourceInterfaceAndSegmentSinksStore( sendMessage, this._currentContent.contentId, { useMseInWorker: this._currentContent.useMseInWorker, hasVideo: this._hasVideo, hasText: this._currentContent.coreTextSender !== null, }, this._currentMediaSourceCanceller.signal, ); this._currentContent.mediaSource = mediaSourceInterface; this._currentContent.segmentSinksStore = segmentSinksStore; this._currentContent.freezeResolver = new FreezeResolver(segmentSinksStore); this._currentContent.coreTextSender = coreTextSender; return new Promise((res, rej) => { mediaSourceInterface.addEventListener( "mediaSourceOpen", function () { res(); }, this._currentMediaSourceCanceller.signal, ); mediaSourceInterface.addEventListener( "mediaSourceClose", function () { rej(new Error("MediaSource ReadyState changed to close during init.")); }, this._currentMediaSourceCanceller.signal, ); this._currentMediaSourceCanceller.signal.register((error) => { rej(error); }); }); } /** * Dispose all resources linked to the currently preopared content if one and * stop linking it to this `ContentPreparer`. * @param {string | undefined} reason - Human-inspectable reason behind the * dispose. Used for debugging matters, especially for debug log * inspection. */ public disposeCurrentContent(reason: string | undefined) { this._contentCanceller.cancel(reason); this._contentCanceller = new TaskCanceller("ContentPreparer"); } } /** * Modules and Metadata associated to the current "prepared" content. */ export interface IPreparedContentData { /** * Identifier uniquely identifying a specific content. * * Protects against all kind of race conditions or asynchronous issues. */ contentId: string; /** * Perform data collection and retrieval for the "Common Media Client Data" * scheme, which is a specification allowing to communicate about playback * conditions with a CDN. */ cmcdDataBuilder: CmcdDataBuilder | null; /** * If `true`, the RxPlayer can enable its "Representation avoidance" * mechanism, where it avoid loading Representation that it suspect * have issues being decoded on the current device. */ enableRepresentationAvoidance: boolean; /** * Interface to the MediaSource implementation, allowing to buffer audio * and video media segments. */ mediaSource: IMediaSourceInterface; /** Class abstracting Manifest fetching and refreshing. */ manifestFetcher: ManifestFetcher; /** * Manifest instance. * * `null` when not fetched yet. */ manifest: IManifest | null; /** * Specific module detecting freezing issues and trying to work-around * them. */ freezeResolver: FreezeResolver; /** * Perform the adaptive logic, allowing to choose the best Representation for * the different types of media to load. */ representationEstimator: IRepresentationEstimator; /** * Allows to create a "SegmentSink" (powerful abstraction over media * buffering API) for each type of media. */ segmentSinksStore: SegmentSinksStore; /** Allows to send timed text media data so it can be rendered. */ coreTextSender: CoreTextDisplayerInterface | null; /** * Allows to create `SegmentQueue` which simplifies complex media segment * fetching. */ segmentQueueCreator: SegmentQueueCreator; /** Allows to load image thumbnails. */ fetchThumbnailData: IThumbnailFetcher; /** * Allows to store and update the wanted tracks and Representation inside that * track. */ trackChoiceSetter: TrackChoiceSetter; /** * If `true`, MSE API should be used in the core part of the RxPlayer (in the * WebWorker). * If `false`, they should be relied on on main thread. */ useMseInWorker: boolean; } /** * @param {Function} sendMessage * @param {string} contentId * @param {Object} capabilities * @param {boolean} capabilities.useMseInWorker * @param {boolean} capabilities.hasVideo * @param {boolean} capabilities.hasText * @param {Object} cancelSignal * @returns {Array.<Object>} */ function createMediaSourceInterfaceAndSegmentSinksStore( sendMessage: (msg: ICoreMessage, transferables?: Transferable[]) => void, contentId: string, capabilities: { useMseInWorker: boolean; hasVideo: boolean; hasText: boolean; }, cancelSignal: CancellationSignal, ): [IMediaSourceInterface, SegmentSinksStore, CoreTextDisplayerInterface | null] { let mediaSourceInterface: IMediaSourceInterface; if (capabilities.useMseInWorker) { const mainMediaSource = new MainMediaSourceInterface(generateMediaSourceId()); mediaSourceInterface = mainMediaSource; let sentMediaSourceLink: IAttachMediaSourceCoreMessagePayload; const handle = mainMediaSource.handle; if (handle.type === "handle") { sentMediaSourceLink = { type: "handle" as const, value: handle.value }; } else { const url = URL.createObjectURL(handle.value); sentMediaSourceLink = { type: "url" as const, value: url }; cancelSignal.register(() => { URL.revokeObjectURL(url); }); } sendMessage( { type: CoreMessageType.AttachMediaSource, contentId, value: sentMediaSourceLink, mediaSourceId: mediaSourceInterface.id, }, // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion [handle.value as unknown as Transferable], ); } else { mediaSourceInterface = new WorkerMediaSourceInterface( generateMediaSourceId(), contentId, sendMessage, ); } const textSender = capabilities.hasText ? new CoreTextDisplayerInterface(contentId, sendMessage) : null; const { hasVideo } = capabilities; const segmentSinksStore = new SegmentSinksStore( mediaSourceInterface, hasVideo, textSender, ); cancelSignal.register((err) => { segmentSinksStore.disposeAll(err.reason); textSender?.stop(err.reason); mediaSourceInterface.dispose(err.reason); }); return [mediaSourceInterface, segmentSinksStore, textSender]; }