rx-player
Version:
Canal+ HTML5 Video Player
335 lines (334 loc) • 16.4 kB
JavaScript
;
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
Object.defineProperty(exports, "__esModule", { value: true });
var features_1 = require("../../features");
var log_1 = require("../../log");
var main_media_source_interface_1 = require("../../mse/main_media_source_interface");
var worker_media_source_interface_1 = require("../../mse/worker_media_source_interface");
var id_generator_1 = require("../../utils/id_generator");
var task_canceller_1 = require("../../utils/task_canceller");
var adaptive_1 = require("../adaptive");
var cmcd_1 = require("../cmcd");
var fetchers_1 = require("../fetchers");
var cdn_prioritizer_1 = require("../fetchers/cdn_prioritizer");
var thumbnail_fetcher_1 = require("../fetchers/thumbnails/thumbnail_fetcher");
var segment_sinks_1 = require("../segment_sinks");
var core_text_displayer_interface_1 = require("./core_text_displayer_interface");
var FreezeResolver_1 = require("./FreezeResolver");
var track_choice_setter_1 = require("./track_choice_setter");
var utils_1 = require("./utils");
/** Function allowing to associate a unique identifier to all created `MediaSource` */
var generateMediaSourceId = (0, id_generator_1.default)();
/**
* 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
*/
var ContentPreparer = /** @class */ (function () {
/**
* @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.
*/
function ContentPreparer(_a) {
var hasVideo = _a.hasVideo;
this._currentContent = null;
this._currentMediaSourceCanceller = new task_canceller_1.default("ContentPreparer MediaSource");
this._hasVideo = hasVideo;
var contentCanceller = new task_canceller_1.default("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>}
*/
ContentPreparer.prototype.initializeNewContent = function (sendMessage, context,
/** Allows to filter which Representations can be choosen. */
throttlers, corePlugins) {
var _this = this;
return new Promise(function (res, rej) {
var _a, _b;
_this.disposeCurrentContent("new init");
var contentCanceller = _this._contentCanceller;
var currentMediaSourceCanceller = new task_canceller_1.default("ContentPreparer MediaSource");
_this._currentMediaSourceCanceller = currentMediaSourceCanceller;
currentMediaSourceCanceller.linkToSignal(contentCanceller.signal);
var contentId = context.contentId, url = context.url, hasText = context.hasText, transportOptions = context.transportOptions, useMseInWorker = context.useMseInWorker, enableRepresentationAvoidance = context.enableRepresentationAvoidance, transport = context.transport;
var manifest = null;
var transportFn = features_1.default.transports[transport];
if (typeof transportFn !== "function") {
rej(new Error("transport \"".concat(transport, "\" not supported. ") +
"Did you add the corresponding feature?"));
return;
}
var transportPipelines = transportFn(__assign(__assign({}, transportOptions), (0, utils_1.extractExternalPlugins)(transportOptions, corePlugins)));
var cmcdDataBuilder = context.cmcd === undefined ? null : new cmcd_1.default(context.cmcd);
var manifestFetcher = new fetchers_1.ManifestFetcher(url === undefined ? undefined : [url], transportPipelines, __assign({ cmcdDataBuilder: cmcdDataBuilder }, context.manifestRetryOptions));
var representationEstimator = (0, adaptive_1.default)({
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: throttlers,
});
var unbindRejectOnCancellation = currentMediaSourceCanceller.signal.register(function (error) {
rej(error);
});
var cdnPrioritizer = new cdn_prioritizer_1.default(contentCanceller.signal);
var segmentQueueCreator = new fetchers_1.SegmentQueueCreator(transportPipelines, cdnPrioritizer, cmcdDataBuilder, context.segmentRetryOptions);
var fetchThumbnailData = (0, thumbnail_fetcher_1.default)(transportPipelines.thumbnails, cdnPrioritizer);
var trackChoiceSetter = new track_choice_setter_1.default();
var _c = __read(createMediaSourceInterfaceAndSegmentSinksStore(sendMessage, contentId, {
useMseInWorker: useMseInWorker,
hasVideo: _this._hasVideo,
hasText: hasText,
}, currentMediaSourceCanceller.signal), 3), mediaSource = _c[0], segmentSinksStore = _c[1], coreTextSender = _c[2];
var freezeResolver = new FreezeResolver_1.default(segmentSinksStore);
_this._currentContent = {
cmcdDataBuilder: cmcdDataBuilder,
contentId: contentId,
enableRepresentationAvoidance: enableRepresentationAvoidance,
freezeResolver: freezeResolver,
mediaSource: mediaSource,
manifest: null,
manifestFetcher: manifestFetcher,
representationEstimator: representationEstimator,
segmentSinksStore: segmentSinksStore,
segmentQueueCreator: segmentQueueCreator,
fetchThumbnailData: fetchThumbnailData,
coreTextSender: coreTextSender,
trackChoiceSetter: trackChoiceSetter,
useMseInWorker: useMseInWorker,
};
mediaSource.addEventListener("mediaSourceOpen", function () {
checkIfReadyAndValidate();
}, currentMediaSourceCanceller.signal);
contentCanceller.signal.register(function (err) {
manifestFetcher.dispose(err.reason);
});
manifestFetcher.addEventListener("warning", function (err) {
sendMessage({
type: "warning" /* CoreMessageType.Warning */,
contentId: contentId,
value: (0, utils_1.formatErrorForSender)(err),
});
}, contentCanceller.signal);
manifestFetcher.addEventListener("manifestReady", function (man) {
if (manifest !== null) {
log_1.default.warn("Core", "Multiple `manifestReady` events, ignoring");
return;
}
manifest = man;
if (_this._currentContent !== null) {
_this._currentContent.manifest = manifest;
}
checkIfReadyAndValidate();
}, currentMediaSourceCanceller.signal);
manifestFetcher.addEventListener("error", function (err) {
rej(err);
}, contentCanceller.signal);
manifestFetcher.start();
function checkIfReadyAndValidate() {
if (manifest === null ||
mediaSource.readyState === "closed" ||
currentMediaSourceCanceller.isUsed()) {
return;
}
(0, utils_1.updateCodecSupportInWorkerMode)(manifest);
manifest.addEventListener("manifestUpdate", function (updates) {
if (manifest === null) {
// TODO log warn?
return;
}
sendMessage({
type: "manifest-update" /* CoreMessageType.ManifestUpdate */,
contentId: contentId,
value: { manifest: manifest, updates: 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}
*/
ContentPreparer.prototype.getCurrentContent = function () {
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.
*/
ContentPreparer.prototype.scheduleManifestRefresh = function (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}
*/
ContentPreparer.prototype.reloadMediaSource = function (sendMessage) {
var _this = this;
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 task_canceller_1.default("ContentPreparer MediaSource");
this._currentMediaSourceCanceller.linkToSignal(this._contentCanceller.signal);
var _b = __read(createMediaSourceInterfaceAndSegmentSinksStore(sendMessage, this._currentContent.contentId, {
useMseInWorker: this._currentContent.useMseInWorker,
hasVideo: this._hasVideo,
hasText: this._currentContent.coreTextSender !== null,
}, this._currentMediaSourceCanceller.signal), 3), mediaSourceInterface = _b[0], segmentSinksStore = _b[1], coreTextSender = _b[2];
this._currentContent.mediaSource = mediaSourceInterface;
this._currentContent.segmentSinksStore = segmentSinksStore;
this._currentContent.freezeResolver = new FreezeResolver_1.default(segmentSinksStore);
this._currentContent.coreTextSender = coreTextSender;
return new Promise(function (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(function (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.
*/
ContentPreparer.prototype.disposeCurrentContent = function (reason) {
this._contentCanceller.cancel(reason);
this._contentCanceller = new task_canceller_1.default("ContentPreparer");
};
return ContentPreparer;
}());
exports.default = 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) {
var mediaSourceInterface;
if (capabilities.useMseInWorker) {
var mainMediaSource = new main_media_source_interface_1.default(generateMediaSourceId());
mediaSourceInterface = mainMediaSource;
var sentMediaSourceLink = void 0;
var handle = mainMediaSource.handle;
if (handle.type === "handle") {
sentMediaSourceLink = { type: "handle", value: handle.value };
}
else {
var url_1 = URL.createObjectURL(handle.value);
sentMediaSourceLink = { type: "url", value: url_1 };
cancelSignal.register(function () {
URL.revokeObjectURL(url_1);
});
}
sendMessage({
type: "attach-media-source" /* CoreMessageType.AttachMediaSource */,
contentId: contentId,
value: sentMediaSourceLink,
mediaSourceId: mediaSourceInterface.id,
},
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
[handle.value]);
}
else {
mediaSourceInterface = new worker_media_source_interface_1.default(generateMediaSourceId(), contentId, sendMessage);
}
var textSender = capabilities.hasText
? new core_text_displayer_interface_1.default(contentId, sendMessage)
: null;
var hasVideo = capabilities.hasVideo;
var segmentSinksStore = new segment_sinks_1.default(mediaSourceInterface, hasVideo, textSender);
cancelSignal.register(function (err) {
segmentSinksStore.disposeAll(err.reason);
textSender === null || textSender === void 0 ? void 0 : textSender.stop(err.reason);
mediaSourceInterface.dispose(err.reason);
});
return [mediaSourceInterface, segmentSinksStore, textSender];
}