UNPKG

rx-player

Version:
301 lines (300 loc) 14.3 kB
import features from "../../features"; import log from "../../log"; import MainMediaSourceInterface from "../../mse/main_media_source_interface"; import WorkerMediaSourceInterface from "../../mse/worker_media_source_interface"; import idGenerator from "../../utils/id_generator"; import TaskCanceller from "../../utils/task_canceller"; import createAdaptiveRepresentationSelector from "../adaptive"; import CmcdDataBuilder from "../cmcd"; import { ManifestFetcher, SegmentQueueCreator } from "../fetchers"; import CdnPrioritizer from "../fetchers/cdn_prioritizer"; import createThumbnailFetcher from "../fetchers/thumbnails/thumbnail_fetcher"; import SegmentSinksStore from "../segment_sinks"; import CoreTextDisplayerInterface from "./core_text_displayer_interface"; import FreezeResolver from "./FreezeResolver"; import TrackChoiceSetter from "./track_choice_setter"; 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 { /** * @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 }) { 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>} */ initializeNewContent(sendMessage, context, /** Allows to filter which Representations can be choosen. */ throttlers, corePlugins) { return new Promise((res, rej) => { var _a, _b; 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 = 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(Object.assign(Object.assign({}, transportOptions), extractExternalPlugins(transportOptions, corePlugins))); const cmcdDataBuilder = context.cmcd === undefined ? null : new CmcdDataBuilder(context.cmcd); const manifestFetcher = new ManifestFetcher(url === undefined ? undefined : [url], transportPipelines, Object.assign({ cmcdDataBuilder }, context.manifestRetryOptions)); const representationEstimator = createAdaptiveRepresentationSelector({ initialBitrates: { audio: (_a = context.initialAudioBitrate) !== null && _a !== void 0 ? _a : 0, video: (_b = context.initialVideoBitrate) !== null && _b !== void 0 ? _b : 0, }, lowLatencyMode: transportOptions.lowLatencyMode, throttlers, }); const unbindRejectOnCancellation = currentMediaSourceCanceller.signal.register((error) => { 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) => { sendMessage({ type: "warning" /* CoreMessageType.Warning */, contentId, value: formatErrorForSender(err), }); }, contentCanceller.signal); manifestFetcher.addEventListener("manifestReady", (man) => { 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) => { 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: "manifest-update" /* 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} */ getCurrentContent() { 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. */ scheduleManifestRefresh(settings) { var _a; (_a = this._currentContent) === null || _a === void 0 ? void 0 : _a.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} */ reloadMediaSource(sendMessage) { var _a; this._currentMediaSourceCanceller.cancel("ContentPreparer MediaSource reload"); if (this._currentContent === null) { return Promise.reject(new Error("CP: No content anymore")); } this._currentContent.trackChoiceSetter.reset(); (_a = this._currentContent.coreTextSender) === null || _a === void 0 ? void 0 : _a.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. */ disposeCurrentContent(reason) { this._contentCanceller.cancel(reason); this._contentCanceller = new TaskCanceller("ContentPreparer"); } } /** * @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, contentId, capabilities, cancelSignal) { let mediaSourceInterface; if (capabilities.useMseInWorker) { const mainMediaSource = new MainMediaSourceInterface(generateMediaSourceId()); mediaSourceInterface = mainMediaSource; let sentMediaSourceLink; const handle = mainMediaSource.handle; if (handle.type === "handle") { sentMediaSourceLink = { type: "handle", value: handle.value }; } else { const url = URL.createObjectURL(handle.value); sentMediaSourceLink = { type: "url", value: url }; cancelSignal.register(() => { URL.revokeObjectURL(url); }); } sendMessage({ type: "attach-media-source" /* CoreMessageType.AttachMediaSource */, contentId, value: sentMediaSourceLink, mediaSourceId: mediaSourceInterface.id, }, // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion [handle.value]); } 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 === null || textSender === void 0 ? void 0 : textSender.stop(err.reason); mediaSourceInterface.dispose(err.reason); }); return [mediaSourceInterface, segmentSinksStore, textSender]; }