rx-player
Version:
Canal+ HTML5 Video Player
429 lines (428 loc) • 20.9 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;
};
var __values = (this && this.__values) || function(o) {
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
if (m) return m.call(o);
if (o && typeof o.length === "number") return {
next: function () {
if (o && i >= o.length) o = void 0;
return { value: o && o[i++], done: !o };
}
};
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
Object.defineProperty(exports, "__esModule", { value: true });
var browser_compatibility_types_1 = require("../../../compat/browser_compatibility_types");
var features_1 = require("../../../features");
var log_1 = require("../../../log");
var manifest_1 = require("../../../manifest");
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 assert_1 = require("../../../utils/assert");
var id_generator_1 = require("../../../utils/id_generator");
var is_null_or_undefined_1 = require("../../../utils/is_null_or_undefined");
var object_assign_1 = require("../../../utils/object_assign");
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 FreezeResolver_1 = require("../common/FreezeResolver");
var globals_1 = require("./globals");
var send_message_1 = require("./send_message");
var track_choice_setter_1 = require("./track_choice_setter");
var worker_text_displayer_interface_1 = require("./worker_text_displayer_interface");
/** 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.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.
*/
function ContentPreparer(_a) {
var hasMseInWorker = _a.hasMseInWorker, hasVideo = _a.hasVideo;
this._currentContent = null;
this._currentMediaSourceCanceller = new task_canceller_1.default();
this._hasVideo = hasVideo;
this._hasMseInWorker = hasMseInWorker;
var contentCanceller = new task_canceller_1.default();
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>}
*/
ContentPreparer.prototype.initializeNewContent = function (context) {
var _this = this;
return new Promise(function (res, rej) {
var _a, _b;
_this.disposeCurrentContent();
var contentCanceller = _this._contentCanceller;
var currentMediaSourceCanceller = new task_canceller_1.default();
_this._currentMediaSourceCanceller = currentMediaSourceCanceller;
currentMediaSourceCanceller.linkToSignal(contentCanceller.signal);
var contentId = context.contentId, url = context.url, hasText = context.hasText, transportOptions = context.transportOptions, enableRepresentationAvoidance = context.enableRepresentationAvoidance;
var manifest = null;
// TODO better way
(0, assert_1.default)(features_1.default.transports.dash !== undefined, "Multithread RxPlayer should have access to the DASH feature");
var representationFilter = typeof transportOptions.representationFilter === "string"
? (0, manifest_1.createRepresentationFilterFromFnString)(transportOptions.representationFilter)
: undefined;
var dashPipelines = features_1.default.transports.dash(__assign(__assign({}, transportOptions), { representationFilter: representationFilter }));
var cmcdDataBuilder = context.cmcd === undefined ? null : new cmcd_1.default(context.cmcd);
var manifestFetcher = new fetchers_1.ManifestFetcher(url === undefined ? undefined : [url], dashPipelines, __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: {
limitResolution: { video: globals_1.limitVideoResolution },
throttleBitrate: { video: globals_1.throttleVideoBitrate },
},
});
var unbindRejectOnCancellation = currentMediaSourceCanceller.signal.register(function (error) {
rej(error);
});
var cdnPrioritizer = new cdn_prioritizer_1.default(contentCanceller.signal);
var segmentQueueCreator = new fetchers_1.SegmentQueueCreator(dashPipelines, cdnPrioritizer, cmcdDataBuilder, context.segmentRetryOptions);
var fetchThumbnailData = (0, thumbnail_fetcher_1.default)(dashPipelines.thumbnails, cdnPrioritizer);
var trackChoiceSetter = new track_choice_setter_1.default();
var _c = __read(createMediaSourceInterfaceAndSegmentSinksStore(contentId, {
hasMseInWorker: _this._hasMseInWorker,
hasVideo: _this._hasVideo,
hasText: hasText,
}, currentMediaSourceCanceller.signal), 3), mediaSource = _c[0], segmentSinksStore = _c[1], workerTextSender = _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,
workerTextSender: workerTextSender,
trackChoiceSetter: trackChoiceSetter,
};
mediaSource.addEventListener("mediaSourceOpen", function () {
checkIfReadyAndValidate();
}, currentMediaSourceCanceller.signal);
contentCanceller.signal.register(function () {
manifestFetcher.dispose();
});
manifestFetcher.addEventListener("warning", function (err) {
(0, send_message_1.default)({
type: "warning" /* WorkerMessageType.Warning */,
contentId: contentId,
value: (0, send_message_1.formatErrorForSender)(err),
});
}, contentCanceller.signal);
manifestFetcher.addEventListener("manifestReady", function (man) {
if (manifest !== null) {
log_1.default.warn("WP: Multiple `manifestReady` events, ignoring");
return;
}
manifest = man;
if (_this._currentContent !== null) {
_this._currentContent.manifest = manifest;
}
checkIfReadyAndValidate();
}, currentMediaSourceCanceller.signal);
manifestFetcher.addEventListener("error", function (err) {
(0, send_message_1.default)({
type: "error" /* WorkerMessageType.Error */,
contentId: contentId,
value: (0, send_message_1.formatErrorForSender)(err),
});
rej(err);
}, contentCanceller.signal);
manifestFetcher.start();
function checkIfReadyAndValidate() {
if (manifest === null ||
mediaSource.readyState === "closed" ||
currentMediaSourceCanceller.isUsed()) {
return;
}
updateCodecSupportInWorkerMode(manifest);
var sentManifest = manifest.getMetadataSnapshot();
manifest.addEventListener("manifestUpdate", function (updates) {
if (manifest === null) {
// TODO log warn?
return;
}
// Remove `periods` key to reduce cost of an unnecessary manifest
// clone.
var snapshot = (0, object_assign_1.default)(manifest.getMetadataSnapshot(), {
periods: [],
});
(0, send_message_1.default)({
type: "manifest-update" /* WorkerMessageType.ManifestUpdate */,
contentId: contentId,
value: { manifest: snapshot, updates: 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}
*/
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);
};
/**
* 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}
*/
ContentPreparer.prototype.reloadMediaSource = function (reloadInfo) {
var _this = this;
this._currentMediaSourceCanceller.cancel();
if (this._currentContent === null) {
return Promise.reject(new Error("CP: No content anymore"));
}
this._currentContent.trackChoiceSetter.reset();
this._currentMediaSourceCanceller = new task_canceller_1.default();
(0, send_message_1.default)({
type: "reloading-media-source" /* WorkerMessageType.ReloadingMediaSource */,
contentId: this._currentContent.contentId,
value: reloadInfo,
}, []);
var _a = __read(createMediaSourceInterfaceAndSegmentSinksStore(this._currentContent.contentId, {
hasMseInWorker: this._hasMseInWorker,
hasVideo: this._hasVideo,
hasText: this._currentContent.workerTextSender !== null,
}, this._currentMediaSourceCanceller.signal), 3), mediaSourceInterface = _a[0], segmentSinksStore = _a[1], workerTextSender = _a[2];
this._currentContent.mediaSource = mediaSourceInterface;
this._currentContent.segmentSinksStore = segmentSinksStore;
this._currentContent.freezeResolver = new FreezeResolver_1.default(segmentSinksStore);
this._currentContent.workerTextSender = workerTextSender;
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`.
*/
ContentPreparer.prototype.disposeCurrentContent = function () {
this._contentCanceller.cancel();
this._contentCanceller = new task_canceller_1.default();
};
return ContentPreparer;
}());
exports.default = ContentPreparer;
/**
* @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) {
var mediaSourceInterface;
if (capabilities.hasMseInWorker) {
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);
});
}
(0, send_message_1.default)({
type: "attach-media-source" /* WorkerMessageType.AttachMediaSource */,
contentId: contentId,
value: sentMediaSourceLink,
mediaSourceId: mediaSourceInterface.id,
}, [handle.value]);
}
else {
mediaSourceInterface = new worker_media_source_interface_1.default(generateMediaSourceId(), contentId, send_message_1.default);
}
var textSender = capabilities.hasText
? new worker_text_displayer_interface_1.default(contentId, send_message_1.default)
: null;
var hasVideo = capabilities.hasVideo;
var segmentSinksStore = new segment_sinks_1.default(mediaSourceInterface, hasVideo, textSender);
cancelSignal.register(function () {
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 e_1, _a, e_2, _b, e_3, _c;
var _d, _e;
if ((0, is_null_or_undefined_1.default)(browser_compatibility_types_1.MediaSource_)) {
return;
}
var codecsMap = new Map();
try {
for (var _f = __values(manifestToUpdate.periods), _g = _f.next(); !_g.done; _g = _f.next()) {
var period = _g.value;
var checkedAdaptations = __spreadArray(__spreadArray([], __read(((_d = period.adaptations.video) !== null && _d !== void 0 ? _d : [])), false), __read(((_e = period.adaptations.audio) !== null && _e !== void 0 ? _e : [])), false);
try {
for (var checkedAdaptations_1 = (e_2 = void 0, __values(checkedAdaptations)), checkedAdaptations_1_1 = checkedAdaptations_1.next(); !checkedAdaptations_1_1.done; checkedAdaptations_1_1 = checkedAdaptations_1.next()) {
var adaptation = checkedAdaptations_1_1.value;
try {
for (var _h = (e_3 = void 0, __values(adaptation.representations)), _j = _h.next(); !_j.done; _j = _h.next()) {
var representation = _j.value;
var codec = "".concat(representation.mimeType, ";codecs=\"").concat(representation.codecs[0], "\"");
if (codecsMap.has(codec)) {
representation.isCodecSupportedInWebWorker = codecsMap.get(codec);
}
else {
var supported = browser_compatibility_types_1.MediaSource_.isTypeSupported(codec);
representation.isCodecSupportedInWebWorker = supported;
codecsMap.set(codec, supported);
}
}
}
catch (e_3_1) { e_3 = { error: e_3_1 }; }
finally {
try {
if (_j && !_j.done && (_c = _h.return)) _c.call(_h);
}
finally { if (e_3) throw e_3.error; }
}
}
}
catch (e_2_1) { e_2 = { error: e_2_1 }; }
finally {
try {
if (checkedAdaptations_1_1 && !checkedAdaptations_1_1.done && (_b = checkedAdaptations_1.return)) _b.call(checkedAdaptations_1);
}
finally { if (e_2) throw e_2.error; }
}
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (_g && !_g.done && (_a = _f.return)) _a.call(_f);
}
finally { if (e_1) throw e_1.error; }
}
}