rx-player
Version:
Canal+ HTML5 Video Player
301 lines (300 loc) • 14.3 kB
JavaScript
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];
}