UNPKG

rx-player

Version:
564 lines (563 loc) 30.5 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; 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."); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = AdaptationStream; var config_1 = require("../../../config"); var errors_1 = require("../../../errors"); var log_1 = require("../../../log"); var array_includes_1 = require("../../../utils/array_includes"); var assert_1 = require("../../../utils/assert"); var cancellable_sleep_1 = require("../../../utils/cancellable_sleep"); var noop_1 = require("../../../utils/noop"); var object_assign_1 = require("../../../utils/object_assign"); var queue_microtask_1 = require("../../../utils/queue_microtask"); var reference_1 = require("../../../utils/reference"); var task_canceller_1 = require("../../../utils/task_canceller"); var representation_1 = require("../representation"); var get_representations_switch_strategy_1 = require("./get_representations_switch_strategy"); /** * Create new `AdaptationStream` whose task will be to download the media data * for a given Adaptation (i.e. "track"). * * It will rely on the IRepresentationEstimator to choose at any time the * best Representation for this Adaptation and then run the logic to download * and push the corresponding segments in the SegmentSink. * * @param {Object} args - Various arguments allowing the `AdaptationStream` to * determine which Representation to choose and which segments to load from it. * You can check the corresponding type for more information. * @param {Object} callbacks - The `AdaptationStream` relies on a system of * callbacks that it will call on various events. * * Depending on the event, the caller may be supposed to perform actions to * react upon some of them. * * This approach is taken instead of a more classical EventEmitter pattern to: * - Allow callbacks to be called synchronously after the * `AdaptationStream` is called. * - Simplify bubbling events up, by just passing through callbacks * - Force the caller to explicitely handle or not the different events. * * Callbacks may start being called immediately after the `AdaptationStream` * call and may be called until either the `parentCancelSignal` argument is * triggered, or until the `error` callback is called, whichever comes first. * @param {Object} parentCancelSignal - `CancellationSignal` allowing, when * triggered, to immediately stop all operations the `AdaptationStream` is * doing. */ function AdaptationStream(_a, callbacks, parentCancelSignal) { var playbackObserver = _a.playbackObserver, content = _a.content, options = _a.options, representationEstimator = _a.representationEstimator, segmentSink = _a.segmentSink, segmentQueueCreator = _a.segmentQueueCreator, wantedBufferAhead = _a.wantedBufferAhead, maxVideoBufferSize = _a.maxVideoBufferSize; var manifest = content.manifest, period = content.period, adaptation = content.adaptation; /** Allows to cancel everything the `AdaptationStream` is doing. */ var adapStreamCanceller = new task_canceller_1.default(); adapStreamCanceller.linkToSignal(parentCancelSignal); /** * The buffer goal ratio base itself on the value given by `wantedBufferAhead` * to determine a more dynamic buffer goal for a given Representation. * * It can help in cases such as : the current browser has issues with * buffering and tells us that we should try to bufferize less data : * https://developers.google.com/web/updates/2017/10/quotaexceedederror */ var bufferGoalRatioMap = new Map(); /** * Emit the currently chosen `Representation`. * `null` if no Representation is chosen for now. */ var currentRepresentation = new reference_1.default(null, adapStreamCanceller.signal); /** Stores the last emitted bitrate. */ var previouslyEmittedBitrate; var initialRepIds = content.representations.getValue().representationIds; var initialRepresentations = getRepresentationList(content.adaptation.representations, initialRepIds); /** Emit the list of Representation for the adaptive logic. */ var representationsList = new reference_1.default(initialRepresentations, adapStreamCanceller.signal); // Start-up Adaptive logic var _b = representationEstimator({ manifest: manifest, period: period, adaptation: adaptation }, currentRepresentation, representationsList, playbackObserver, adapStreamCanceller.signal), estimateRef = _b.estimates, abrCallbacks = _b.callbacks; var isMediaSegmentQueueInterrupted = new reference_1.default(false); /** Update the `canLoad` ref on observation update */ playbackObserver.listen(function (observation) { var _a; var observationCanStream = (_a = observation.canStream) !== null && _a !== void 0 ? _a : true; if (isMediaSegmentQueueInterrupted.getValue() === observationCanStream) { log_1.default.debug("Stream", "isMediaSegmentQueueInterrupted updated to", !observationCanStream); isMediaSegmentQueueInterrupted.setValue(!observationCanStream); } }, { clearSignal: adapStreamCanceller.signal }); /** Allows a `RepresentationStream` to easily fetch media segments. */ var segmentQueue = segmentQueueCreator.createSegmentQueue(adaptation.type, /* eslint-disable @typescript-eslint/unbound-method */ { onRequestBegin: abrCallbacks.requestBegin, onRequestEnd: abrCallbacks.requestEnd, onProgress: abrCallbacks.requestProgress, onMetrics: abrCallbacks.metrics, }, isMediaSegmentQueueInterrupted); /* eslint-enable @typescript-eslint/unbound-method */ /** Used to determine when "fast-switching" is possible. */ var fastSwitchThreshold = new reference_1.default(0); estimateRef.onUpdate(function (_a) { var bitrate = _a.bitrate, knownStableBitrate = _a.knownStableBitrate; if (options.enableFastSwitching) { fastSwitchThreshold.setValueIfChanged(knownStableBitrate); } if (bitrate === undefined || bitrate === previouslyEmittedBitrate) { return; } previouslyEmittedBitrate = bitrate; log_1.default.debug("Stream", "new ".concat(adaptation.type, " bitrate estimate received from ABR"), { bitrate: bitrate, }); callbacks.bitrateEstimateChange({ type: adaptation.type, bitrate: bitrate }); }, { emitCurrentValue: true, clearSignal: adapStreamCanceller.signal }); /** * When triggered, cancel all `RepresentationStream`s currently created. * Set to `undefined` initially. */ var cancelCurrentStreams; // Each time the list of wanted Representations changes, we restart the logic content.representations.onUpdate(function (val) { if (cancelCurrentStreams !== undefined) { cancelCurrentStreams.cancel(); } var newRepIds = content.representations.getValue().representationIds; // NOTE: We expect that the rest of the RxPlayer code is already handling // cases where the list of playable `Representation` changes: // decipherability updates, "`Representation` avoidance" etc. var newRepresentations = getRepresentationList(content.adaptation.representations, newRepIds); representationsList.setValueIfChanged(newRepresentations); cancelCurrentStreams = new task_canceller_1.default(); cancelCurrentStreams.linkToSignal(adapStreamCanceller.signal); onRepresentationsChoiceChange(val, cancelCurrentStreams.signal).catch(function (err) { if ((cancelCurrentStreams === null || cancelCurrentStreams === void 0 ? void 0 : cancelCurrentStreams.isUsed()) === true && task_canceller_1.default.isCancellationError(err)) { return; } adapStreamCanceller.cancel(); callbacks.error(err); }); }, { clearSignal: adapStreamCanceller.signal, emitCurrentValue: true }); return; /** * Function called each time the list of wanted Representations is updated. * * Returns a Promise to profit from async/await syntax. The Promise resolution * does not indicate anything. The Promise may reject however, either on some * error or on some cancellation. * @param {Object} choice - The last Representations choice that has been * made. * @param {Object} fnCancelSignal - `CancellationSignal` allowing to cancel * everything this function is doing and free all related resources. */ function onRepresentationsChoiceChange(choice, fnCancelSignal) { return __awaiter(this, void 0, void 0, function () { var switchStrat, _a, _b, _c, range, e_1_1; var e_1, _d; return __generator(this, function (_e) { switch (_e.label) { case 0: switchStrat = (0, get_representations_switch_strategy_1.default)(period, adaptation, choice, segmentSink, playbackObserver); _a = switchStrat.type; switch (_a) { case "continue": return [3 /*break*/, 1]; case "needs-reload": return [3 /*break*/, 2]; case "flush-buffer": return [3 /*break*/, 3]; case "clean-buffer": return [3 /*break*/, 3]; } return [3 /*break*/, 11]; case 1: return [3 /*break*/, 12]; // nothing to do case 2: // Just ask to reload // We begin by scheduling a micro-task to reduce the possibility of race // conditions where the inner logic would be called synchronously before // the next observation (which may reflect very different playback conditions) // is actually received. return [2 /*return*/, (0, queue_microtask_1.default)(function () { playbackObserver.listen(function () { if (fnCancelSignal.isCancelled()) { return; } var DELTA_POSITION_AFTER_RELOAD = config_1.default.getCurrent().DELTA_POSITION_AFTER_RELOAD; var timeOffset = DELTA_POSITION_AFTER_RELOAD.bitrateSwitch; return callbacks.waitingMediaSourceReload({ bufferType: adaptation.type, period: period, timeOffset: timeOffset, stayInPeriod: true, }); }, { includeLastObservation: true, clearSignal: fnCancelSignal }); })]; case 3: _e.trys.push([3, 8, 9, 10]); _b = __values(switchStrat.value), _c = _b.next(); _e.label = 4; case 4: if (!!_c.done) return [3 /*break*/, 7]; range = _c.value; return [4 /*yield*/, segmentSink.removeBuffer(range.start, range.end)]; case 5: _e.sent(); if (fnCancelSignal.isCancelled()) { return [2 /*return*/]; } _e.label = 6; case 6: _c = _b.next(); return [3 /*break*/, 4]; case 7: return [3 /*break*/, 10]; case 8: e_1_1 = _e.sent(); e_1 = { error: e_1_1 }; return [3 /*break*/, 10]; case 9: try { if (_c && !_c.done && (_d = _b.return)) _d.call(_b); } finally { if (e_1) throw e_1.error; } return [7 /*endfinally*/]; case 10: if (switchStrat.type === "flush-buffer") { callbacks.needsBufferFlush(); if (fnCancelSignal.isCancelled()) { return [2 /*return*/]; } } return [3 /*break*/, 12]; case 11: (0, assert_1.assertUnreachable)(switchStrat); _e.label = 12; case 12: recursivelyCreateRepresentationStreams(fnCancelSignal); return [2 /*return*/]; } }); }); } /** * Create `RepresentationStream`s starting with the Representation of the last * estimate performed. * Each time a new estimate is made, this function will create a new * `RepresentationStream` corresponding to that new estimate. * @param {Object} fnCancelSignal - `CancellationSignal` which will abort * anything this function is doing and free allocated resources. */ function recursivelyCreateRepresentationStreams(fnCancelSignal) { /** * `TaskCanceller` triggered when the current `RepresentationStream` is * terminating and as such the next one might be immediately created * recursively. */ var repStreamTerminatingCanceller = new task_canceller_1.default(); repStreamTerminatingCanceller.linkToSignal(fnCancelSignal); var representation = estimateRef.getValue().representation; if (representation === null) { return; } /** * Stores the last estimate emitted, starting with `null`. * This allows to easily rely on that value in inner Observables which might also * need the last already-considered value. */ var terminateCurrentStream = new reference_1.default(null, repStreamTerminatingCanceller.signal); /** Allows to stop listening to estimateRef on the following line. */ estimateRef.onUpdate(function (estimate) { if (estimate.representation === null || estimate.representation.id === representation.id) { return; } if (estimate.urgent) { log_1.default.info("Stream", "urgent Representation switch", { bufferType: adaptation.type, estimateBitrate: estimate.bitrate, prevRepresentationBitrate: representation.bitrate, newRepresentationBitrate: estimate.representation.bitrate, }); return terminateCurrentStream.setValue({ urgent: true }); } else { log_1.default.info("Stream", "slow Representation switch", { bufferType: adaptation.type, estimateBitrate: estimate.bitrate, prevRepresentationBitrate: representation.bitrate, newRepresentationBitrate: estimate.representation.bitrate, }); return terminateCurrentStream.setValue({ urgent: false }); } }, { clearSignal: repStreamTerminatingCanceller.signal, emitCurrentValue: true, }); var repInfo = { type: adaptation.type, adaptation: adaptation, period: period, representation: representation, }; currentRepresentation.setValue(representation); if (fnCancelSignal.isCancelled()) { return; // previous callback has stopped everything by side-effect } callbacks.representationChange(repInfo); if (fnCancelSignal.isCancelled()) { return; // previous callback has stopped everything by side-effect } var representationStreamCallbacks = { streamStatusUpdate: callbacks.streamStatusUpdate, encryptionDataEncountered: callbacks.encryptionDataEncountered, manifestMightBeOufOfSync: callbacks.manifestMightBeOufOfSync, needsManifestRefresh: callbacks.needsManifestRefresh, inbandEvent: callbacks.inbandEvent, warning: callbacks.warning, error: function (err) { adapStreamCanceller.cancel(); callbacks.error(err); }, addedSegment: function (segmentInfo) { abrCallbacks.addedSegment(segmentInfo); }, terminating: function () { if (repStreamTerminatingCanceller.isUsed()) { return; // Already handled } repStreamTerminatingCanceller.cancel(); return recursivelyCreateRepresentationStreams(fnCancelSignal); }, }; createRepresentationStream(representation, terminateCurrentStream, representationStreamCallbacks, fnCancelSignal); } /** * Create and returns a new `RepresentationStream`, linked to the * given Representation. * @param {Object} representation - The Representation the * `RepresentationStream` has to be created for. * @param {Object} terminateCurrentStream - Gives termination orders, * indicating that the `RepresentationStream` should stop what it's doing. * @param {Object} representationStreamCallbacks - Callbacks to call on * various `RepresentationStream` events. * @param {Object} fnCancelSignal - `CancellationSignal` which will abort * anything this function is doing and free allocated resources. */ function createRepresentationStream(representation, terminateCurrentStream, representationStreamCallbacks, fnCancelSignal) { /** Set to `true` if we've encountered an error with this `RepresentationStream` */ var hasEncounteredError = false; var bufferGoalCanceller = new task_canceller_1.default(); bufferGoalCanceller.linkToSignal(fnCancelSignal); /** Actually built buffer size, in seconds. */ var bufferGoal = (0, reference_1.createMappedReference)(wantedBufferAhead, function (prev) { return getBufferGoal(representation, prev); }, bufferGoalCanceller.signal); var maxBufferSize = adaptation.type === "video" ? maxVideoBufferSize : new reference_1.default(Infinity); log_1.default.info("Stream", "changing representation", { bufferType: adaptation.type, representationId: representation.id, representationBitrate: representation.bitrate, }); var updatedCallbacks = (0, object_assign_1.default)({}, representationStreamCallbacks, { error: function (err) { var _a; if (hasEncounteredError) { // A RepresentationStream might trigger multiple Errors (for example // multiple segments it tried to push at once led to errors). // In that case, we'll only consider the first Error. // // That could mean that we're hiding legitimate issues but handling // multiple of those errors at once is too hard a task for now. log_1.default.warn("Stream", "Ignoring RepresentationStream error", err); return; } hasEncounteredError = true; var formattedError = (0, errors_1.formatError)(err, { defaultCode: "NONE", defaultReason: "Unknown `RepresentationStream` error", }); if (formattedError.code !== "BUFFER_FULL_ERROR") { representationStreamCallbacks.error(err); } else { log_1.default.warn("Stream", "received BUFFER_FULL_ERROR", { bufferType: adaptation.type, representationBitrate: representation.bitrate, }); var wba = wantedBufferAhead.getValue(); var lastBufferGoalRatio = (_a = bufferGoalRatioMap.get(representation.id)) !== null && _a !== void 0 ? _a : 1; // 70%, 49%, 34.3%, 24%, 16.81%, 11.76%, 8.24% and 5.76% var newBufferGoalRatio = lastBufferGoalRatio * 0.7; bufferGoalRatioMap.set(representation.id, newBufferGoalRatio); if (newBufferGoalRatio <= 0.05 || getBufferGoal(representation, wba) <= 2) { representationStreamCallbacks.error(formattedError); return; } // We wait 4 seconds to let the situation evolve by itself before // retrying loading segments with a lower buffer goal (0, cancellable_sleep_1.default)(4000, fnCancelSignal) .then(function () { return createRepresentationStream(representation, terminateCurrentStream, representationStreamCallbacks, fnCancelSignal); }) .catch(noop_1.default); } }, terminating: function () { bufferGoalCanceller.cancel(); representationStreamCallbacks.terminating(); }, }); (0, representation_1.default)({ playbackObserver: playbackObserver, content: { representation: representation, adaptation: adaptation, period: period, manifest: manifest }, segmentSink: segmentSink, segmentQueue: segmentQueue, terminate: terminateCurrentStream, options: { bufferGoal: bufferGoal, maxBufferSize: maxBufferSize, drmSystemId: options.drmSystemId, fastSwitchThreshold: fastSwitchThreshold, }, }, updatedCallbacks, fnCancelSignal); // reload if the Representation disappears from the Manifest manifest.addEventListener("manifestUpdate", function (updates) { var e_2, _a, e_3, _b, e_4, _c; try { for (var _d = __values(updates.updatedPeriods), _e = _d.next(); !_e.done; _e = _d.next()) { var element = _e.value; if (element.period.id === period.id) { try { for (var _f = (e_3 = void 0, __values(element.result.updatedAdaptations)), _g = _f.next(); !_g.done; _g = _f.next()) { var updated = _g.value; if (updated.adaptation === adaptation.id) { try { for (var _h = (e_4 = void 0, __values(updated.removedRepresentations)), _j = _h.next(); !_j.done; _j = _h.next()) { var rep = _j.value; if (rep === representation.id) { if (fnCancelSignal.isCancelled()) { return; } return callbacks.waitingMediaSourceReload({ bufferType: adaptation.type, period: period, timeOffset: 0, stayInPeriod: true, }); } } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (_j && !_j.done && (_c = _h.return)) _c.call(_h); } finally { if (e_4) throw e_4.error; } } } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_g && !_g.done && (_b = _f.return)) _b.call(_f); } finally { if (e_3) throw e_3.error; } } } else if (element.period.start > period.start) { break; } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_e && !_e.done && (_a = _d.return)) _a.call(_d); } finally { if (e_2) throw e_2.error; } } }, fnCancelSignal); } /** * Returns how much media data should be pre-buffered for this * `Representation`, according to the `wantedBufferAhead` setting and previous * issues encountered with that `Representation`. * @param {Object} representation - The `Representation` you want to buffer. * @param {number} wba - The value of `wantedBufferAhead` set by the user. * @returns {number} */ function getBufferGoal(representation, wba) { var oldBufferGoalRatio = bufferGoalRatioMap.get(representation.id); var bufferGoalRatio = oldBufferGoalRatio !== undefined ? oldBufferGoalRatio : 1; if (oldBufferGoalRatio === undefined) { bufferGoalRatioMap.set(representation.id, bufferGoalRatio); } if (bufferGoalRatio < 1 && wba === Infinity) { // When `wba` is equal to `Infinity`, dividing it will still make it equal // to `Infinity`. To make the `bufferGoalRatio` still have an effect, we // just starts from a `wba` set to the high value of 5 minutes. return 5 * 60 * 1000 * bufferGoalRatio; } return wba * bufferGoalRatio; } } /** * Construct the list of the `Representation` to play, based on what's supported * and what the API seem to authorize. * @param {Array.<Object>} availableRepresentations - All available * Representation in the current `Adaptation`, including unsupported ones. * @param {Array.<string>} authorizedRepIds - The subset of `Representation` * that the API authorize us to play. * @returns {Array.<Object>} */ function getRepresentationList(availableRepresentations, authorizedRepIds) { var filteredRepresentations = availableRepresentations.filter(function (r) { return (0, array_includes_1.default)(authorizedRepIds, r.id) && !r.shouldBeAvoided && r.isPlayable() !== false; }); if (filteredRepresentations.length > 0) { return filteredRepresentations; } // Retry without "`Representation` avoidance" return availableRepresentations.filter(function (r) { return (0, array_includes_1.default)(authorizedRepIds, r.id) && r.isPlayable() !== false; }); }