UNPKG

rx-player

Version:
347 lines (346 loc) 16.2 kB
import { MediaSource_ } from "../../../compat/browser_compatibility_types"; import features from "../../../features"; import log from "../../../log"; import { createRepresentationFilterFromFnString } from "../../../manifest"; import MainMediaSourceInterface from "../../../mse/main_media_source_interface"; import WorkerMediaSourceInterface from "../../../mse/worker_media_source_interface"; import assert from "../../../utils/assert"; import idGenerator from "../../../utils/id_generator"; import isNullOrUndefined from "../../../utils/is_null_or_undefined"; import objectAssign from "../../../utils/object_assign"; 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 FreezeResolver from "../common/FreezeResolver"; import { limitVideoResolution, throttleVideoBitrate } from "./globals"; import sendMessage, { formatErrorForSender } from "./send_message"; import TrackChoiceSetter from "./track_choice_setter"; import WorkerTextDisplayerInterface from "./worker_text_displayer_interface"; /** 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.hasMseInWorker - If `true`, the current * environment has access to MediaSource API in a WebWorker context (so, * here). * If `false`, we have to go through the main thread to rely on all MSE API. * @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({ hasMseInWorker, hasVideo, }) { this._currentContent = null; this._currentMediaSourceCanceller = new TaskCanceller(); this._hasVideo = hasVideo; this._hasMseInWorker = hasMseInWorker; const contentCanceller = new TaskCanceller(); 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. * @returns {Promise.<Object>} */ initializeNewContent(context) { return new Promise((res, rej) => { var _a, _b; this.disposeCurrentContent(); const contentCanceller = this._contentCanceller; const currentMediaSourceCanceller = new TaskCanceller(); this._currentMediaSourceCanceller = currentMediaSourceCanceller; currentMediaSourceCanceller.linkToSignal(contentCanceller.signal); const { contentId, url, hasText, transportOptions, enableRepresentationAvoidance } = context; let manifest = null; // TODO better way assert(features.transports.dash !== undefined, "Multithread RxPlayer should have access to the DASH feature"); const representationFilter = typeof transportOptions.representationFilter === "string" ? createRepresentationFilterFromFnString(transportOptions.representationFilter) : undefined; const dashPipelines = features.transports.dash(Object.assign(Object.assign({}, transportOptions), { representationFilter })); const cmcdDataBuilder = context.cmcd === undefined ? null : new CmcdDataBuilder(context.cmcd); const manifestFetcher = new ManifestFetcher(url === undefined ? undefined : [url], dashPipelines, 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: { limitResolution: { video: limitVideoResolution }, throttleBitrate: { video: throttleVideoBitrate }, }, }); const unbindRejectOnCancellation = currentMediaSourceCanceller.signal.register((error) => { rej(error); }); const cdnPrioritizer = new CdnPrioritizer(contentCanceller.signal); const segmentQueueCreator = new SegmentQueueCreator(dashPipelines, cdnPrioritizer, cmcdDataBuilder, context.segmentRetryOptions); const fetchThumbnailData = createThumbnailFetcher(dashPipelines.thumbnails, cdnPrioritizer); const trackChoiceSetter = new TrackChoiceSetter(); const [mediaSource, segmentSinksStore, workerTextSender] = createMediaSourceInterfaceAndSegmentSinksStore(contentId, { hasMseInWorker: this._hasMseInWorker, 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, workerTextSender, trackChoiceSetter, }; mediaSource.addEventListener("mediaSourceOpen", function () { checkIfReadyAndValidate(); }, currentMediaSourceCanceller.signal); contentCanceller.signal.register(() => { manifestFetcher.dispose(); }); manifestFetcher.addEventListener("warning", (err) => { sendMessage({ type: "warning" /* WorkerMessageType.Warning */, contentId, value: formatErrorForSender(err), }); }, contentCanceller.signal); manifestFetcher.addEventListener("manifestReady", (man) => { if (manifest !== null) { log.warn("WP: Multiple `manifestReady` events, ignoring"); return; } manifest = man; if (this._currentContent !== null) { this._currentContent.manifest = manifest; } checkIfReadyAndValidate(); }, currentMediaSourceCanceller.signal); manifestFetcher.addEventListener("error", (err) => { sendMessage({ type: "error" /* WorkerMessageType.Error */, contentId, value: formatErrorForSender(err), }); rej(err); }, contentCanceller.signal); manifestFetcher.start(); function checkIfReadyAndValidate() { if (manifest === null || mediaSource.readyState === "closed" || currentMediaSourceCanceller.isUsed()) { return; } updateCodecSupportInWorkerMode(manifest); const sentManifest = manifest.getMetadataSnapshot(); manifest.addEventListener("manifestUpdate", (updates) => { if (manifest === null) { // TODO log warn? return; } // Remove `periods` key to reduce cost of an unnecessary manifest // clone. const snapshot = objectAssign(manifest.getMetadataSnapshot(), { periods: [], }); sendMessage({ type: "manifest-update" /* WorkerMessageType.ManifestUpdate */, contentId, value: { manifest: snapshot, updates }, }); }, contentCanceller.signal); unbindRejectOnCancellation(); res(sentManifest); } }); } /** * 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); } /** * If there is a prepared content right now, performs the destructive * "reloading" strategy: dispose of its `MediaSource` (and of its * `SourceBuffer`) and recreate one. * * The returned Promise resolves when it restarts being ready. * @param {Object} reloadInfo * @returns {Promise} */ reloadMediaSource(reloadInfo) { this._currentMediaSourceCanceller.cancel(); if (this._currentContent === null) { return Promise.reject(new Error("CP: No content anymore")); } this._currentContent.trackChoiceSetter.reset(); this._currentMediaSourceCanceller = new TaskCanceller(); sendMessage({ type: "reloading-media-source" /* WorkerMessageType.ReloadingMediaSource */, contentId: this._currentContent.contentId, value: reloadInfo, }, []); const [mediaSourceInterface, segmentSinksStore, workerTextSender] = createMediaSourceInterfaceAndSegmentSinksStore(this._currentContent.contentId, { hasMseInWorker: this._hasMseInWorker, hasVideo: this._hasVideo, hasText: this._currentContent.workerTextSender !== null, }, this._currentMediaSourceCanceller.signal); this._currentContent.mediaSource = mediaSourceInterface; this._currentContent.segmentSinksStore = segmentSinksStore; this._currentContent.freezeResolver = new FreezeResolver(segmentSinksStore); this._currentContent.workerTextSender = workerTextSender; 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`. */ disposeCurrentContent() { this._contentCanceller.cancel(); this._contentCanceller = new TaskCanceller(); } } /** * @param {string} contentId * @param {Object} capabilities * @param {boolean} capabilities.hasMseInWorker * @param {boolean} capabilities.hasVideo * @param {boolean} capabilities.hasText * @param {Object} cancelSignal * @returns {Array.<Object>} */ function createMediaSourceInterfaceAndSegmentSinksStore(contentId, capabilities, cancelSignal) { let mediaSourceInterface; if (capabilities.hasMseInWorker) { 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" /* WorkerMessageType.AttachMediaSource */, contentId, value: sentMediaSourceLink, mediaSourceId: mediaSourceInterface.id, }, [handle.value]); } else { mediaSourceInterface = new WorkerMediaSourceInterface(generateMediaSourceId(), contentId, sendMessage); } const textSender = capabilities.hasText ? new WorkerTextDisplayerInterface(contentId, sendMessage) : null; const { hasVideo } = capabilities; const segmentSinksStore = new SegmentSinksStore(mediaSourceInterface, hasVideo, textSender); cancelSignal.register(() => { segmentSinksStore.disposeAll(); textSender === null || textSender === void 0 ? void 0 : textSender.stop(); mediaSourceInterface.dispose(); }); return [mediaSourceInterface, segmentSinksStore, textSender]; } /** * Set Representation.isCodecSupportedInWebWorker to true or false * If the codec is supported in the current context. * If MSE in worker is not available, the attribute is not set. */ function updateCodecSupportInWorkerMode(manifestToUpdate) { var _a, _b; if (isNullOrUndefined(MediaSource_)) { return; } const codecsMap = new Map(); for (const period of manifestToUpdate.periods) { const checkedAdaptations = [ ...((_a = period.adaptations.video) !== null && _a !== void 0 ? _a : []), ...((_b = period.adaptations.audio) !== null && _b !== void 0 ? _b : []), ]; for (const adaptation of checkedAdaptations) { for (const representation of adaptation.representations) { const codec = `${representation.mimeType};codecs="${representation.codecs[0]}"`; if (codecsMap.has(codec)) { representation.isCodecSupportedInWebWorker = codecsMap.get(codec); } else { const supported = MediaSource_.isTypeSupported(codec); representation.isCodecSupportedInWebWorker = supported; codecsMap.set(codec, supported); } } } } }