rx-player
Version:
Canal+ HTML5 Video Player
347 lines (346 loc) • 16.2 kB
JavaScript
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);
}
}
}
}
}