UNPKG

rx-player

Version:
591 lines (590 loc) 26.5 kB
"use strict"; /** * Copyright 2015 CANAL+ Group * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); 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 }); var errors_1 = require("../../errors"); var array_includes_1 = require("../../utils/array_includes"); var event_emitter_1 = require("../../utils/event_emitter"); var is_null_or_undefined_1 = require("../../utils/is_null_or_undefined"); var queue_microtask_1 = require("../../utils/queue_microtask"); var sorted_list_1 = require("../../utils/sorted_list"); var task_canceller_1 = require("../../utils/task_canceller"); /** * Observes what's being played and take care of media events relating to time * boundaries: * - Emits a `endingPositionChange` when the known maximum playable position * of the current content is known and every time it changes. * - Emits `endOfStream` API once segments have been pushed until the end and * `resumeStream` if downloads starts back. * - Emits a `periodChange` event when the currently-playing Period seemed to * have changed. * - emit "warning" events when what is being played is outside of the * Manifest range. * @class ContentTimeBoundariesObserver */ // `HTMLMediaElement.currentTime` can be off by a very small amount from the // Manifest-derived boundaries. We keep an epsilon so those rounding // differences do not trigger spurious boundary warnings. var EPSILON = 1e-3; var ContentTimeBoundariesObserver = /** @class */ (function (_super) { __extends(ContentTimeBoundariesObserver, _super); /** * @param {Object} manifest * @param {Object} playbackObserver */ function ContentTimeBoundariesObserver(manifest, playbackObserver, bufferTypes) { var _this = _super.call(this) || this; _this._canceller = new task_canceller_1.default("Boundaries Observation"); _this._manifest = manifest; _this._activeStreams = new Map(); _this._allBufferTypes = bufferTypes; _this._lastCurrentPeriodId = null; /** * Allows to calculate the minimum and maximum playable position on the * whole content. */ var maximumPositionCalculator = new MaximumPositionCalculator(manifest); // Indicate directly that no Adaptation of a particular type will be set if (!(0, array_includes_1.default)(_this._allBufferTypes, "video")) { maximumPositionCalculator.updateLastVideoAdaptation(null); } if (!(0, array_includes_1.default)(_this._allBufferTypes, "audio")) { maximumPositionCalculator.updateLastAudioAdaptation(null); } _this._maximumPositionCalculator = maximumPositionCalculator; var cancelSignal = _this._canceller.signal; // As the following code may send events synchronously, which would not be // catchable as a caller could not have called `addEventListener` yet, // we schedule it in a micro-task (0, queue_microtask_1.default)(function () { playbackObserver.listen(function (_a) { var position = _a.position; var wantedPosition = position.getWanted(); var minimumPosition = manifest.getMinimumSafePosition(); var maximumPosition = maximumPositionCalculator.getMaximumAvailablePosition(); // Only report "before manifest" when the position is meaningfully // behind the minimum, not when `<video>.currentTime` and the // Manifest disagree by a tiny floating-point delta. if (wantedPosition < minimumPosition - EPSILON) { var warning = new errors_1.MediaError("MEDIA_TIME_BEFORE_MANIFEST", "The current position is behind the " + "earliest time announced in the Manifest.", { timeInfo: { position: wantedPosition, minPosition: minimumPosition, maxPosition: maximumPosition, }, }); _this.trigger("warning", warning); } else if (wantedPosition > maximumPosition + EPSILON) { var warning = new errors_1.MediaError("MEDIA_TIME_AFTER_MANIFEST", "The current position is after the latest " + "time announced in the Manifest.", { timeInfo: { position: wantedPosition, minPosition: minimumPosition, maxPosition: maximumPosition, }, }); _this.trigger("warning", warning); } }, { includeLastObservation: false, clearSignal: cancelSignal }); }); manifest.addEventListener("manifestUpdate", function () { _this.trigger("endingPositionChange", _this._getManifestEndTime()); if (cancelSignal.isCancelled()) { return; } _this._checkEndOfStream(); }, cancelSignal); return _this; } /** * Returns an estimate of the current last position which may be played in * the content at the moment. * @returns {Object} */ ContentTimeBoundariesObserver.prototype.getCurrentEndingTime = function () { return this._getManifestEndTime(); }; /** * Method to call any time an Adaptation has been selected. * * That Adaptation switch will be considered as active until the * `onPeriodCleared` method has been called for the same `bufferType` and * `Period`, or until `dispose` is called. * @param {string} bufferType - The type of buffer concerned by the Adaptation * switch * @param {Object} period - The Period concerned by the Adaptation switch * @param {Object|null} adaptation - The Adaptation selected. `null` if the * absence of `Adaptation` has been explicitely selected for this Period and * buffer type (e.g. no video). */ ContentTimeBoundariesObserver.prototype.onAdaptationChange = function (bufferType, period, adaptation) { if (this._manifest.isLastPeriodKnown) { var lastPeriod = this._manifest.periods[this._manifest.periods.length - 1]; if (period.id === (lastPeriod === null || lastPeriod === void 0 ? void 0 : lastPeriod.id)) { if (bufferType === "audio" || bufferType === "video") { if (bufferType === "audio") { this._maximumPositionCalculator.updateLastAudioAdaptation(adaptation); } else { this._maximumPositionCalculator.updateLastVideoAdaptation(adaptation); } var endingPosition = this._maximumPositionCalculator.getEndingPosition(); var newEndingPosition = endingPosition !== undefined ? { isEnd: true, endingPosition: endingPosition } : { isEnd: false, endingPosition: this._maximumPositionCalculator.getMaximumAvailablePosition(), }; this.trigger("endingPositionChange", newEndingPosition); } } } if (this._canceller.isUsed()) { return; } if (adaptation === null) { this._addActivelyLoadedPeriod(period, bufferType); } }; /** * Method to call any time a Representation has been selected. * * That Representation switch will be considered as active until the * `onPeriodCleared` method has been called for the same `bufferType` and * `Period`, or until `dispose` is called. * @param {string} bufferType - The type of buffer concerned by the * Representation switch * @param {Object} period - The Period concerned by the Representation switch */ ContentTimeBoundariesObserver.prototype.onRepresentationChange = function (bufferType, period) { this._addActivelyLoadedPeriod(period, bufferType); }; /** * Method to call any time a Period and type combination is not considered * anymore. * * Calling this method allows to signal that a previous Adaptation and/or * Representation change respectively indicated by an `onAdaptationChange` and * an `onRepresentationChange` call, are not active anymore. * @param {string} bufferType - The type of buffer concerned * @param {Object} period - The Period concerned */ ContentTimeBoundariesObserver.prototype.onPeriodCleared = function (bufferType, period) { this._removeActivelyLoadedPeriod(period, bufferType); }; /** * Method to call when the last chronological segment for a given buffer type * is known to have been loaded and is either pushed or in the process of * being pushed to the corresponding MSE `SourceBuffer` or equivalent. * * This method can even be called multiple times in a row as long as the * aforementioned condition is true, if it simplify your code's management. * @param {string} bufferType */ ContentTimeBoundariesObserver.prototype.onLastSegmentFinishedLoading = function (bufferType) { var streamInfo = this._lazilyCreateActiveStreamInfo(bufferType); if (!streamInfo.hasFinishedLoadingLastPeriod) { streamInfo.hasFinishedLoadingLastPeriod = true; this._checkEndOfStream(); } }; /** * Method to call to "cancel" a previous call to * `onLastSegmentFinishedLoading`. * * That is, calling this method indicates that the last chronological segment * of a given buffer type is now either not loaded or it is not known. * * This method can even be called multiple times in a row as long as the * aforementioned condition is true, if it simplify your code's management. * @param {string} bufferType */ ContentTimeBoundariesObserver.prototype.onLastSegmentLoadingResume = function (bufferType) { var streamInfo = this._lazilyCreateActiveStreamInfo(bufferType); if (streamInfo.hasFinishedLoadingLastPeriod) { streamInfo.hasFinishedLoadingLastPeriod = false; this._checkEndOfStream(); } }; /** * Free all resources used by the `ContentTimeBoundariesObserver` and cancels * all recurring processes it performs. * @param {string | undefined} reason - Human-inspectable reason behind the * dispose. Used for debugging matters, especially for debug log * inspection. */ ContentTimeBoundariesObserver.prototype.dispose = function (reason) { this.removeEventListener(); this._canceller.cancel(reason !== null && reason !== void 0 ? reason : "ContentTimeBoundariesObserver dispose"); }; ContentTimeBoundariesObserver.prototype._addActivelyLoadedPeriod = function (period, bufferType) { var streamInfo = this._lazilyCreateActiveStreamInfo(bufferType); if (!streamInfo.activePeriods.has(period)) { streamInfo.activePeriods.add(period); this._checkCurrentPeriod(); } }; ContentTimeBoundariesObserver.prototype._removeActivelyLoadedPeriod = function (period, bufferType) { var streamInfo = this._activeStreams.get(bufferType); if (streamInfo === undefined) { return; } if (streamInfo.activePeriods.has(period)) { streamInfo.activePeriods.removeElement(period); this._checkCurrentPeriod(); } }; ContentTimeBoundariesObserver.prototype._checkCurrentPeriod = function () { var e_1, _a; if (this._allBufferTypes.length === 0) { return; } var streamInfo = this._activeStreams.get(this._allBufferTypes[0]); if (streamInfo === undefined) { return; } var _loop_1 = function (period) { var e_2, _d; var wasFoundInAllTypes = true; try { for (var _e = (e_2 = void 0, __values(this_1._allBufferTypes)), _f = _e.next(); !_f.done; _f = _e.next()) { var bufferType = _f.value; var streamInfo2 = this_1._activeStreams.get(bufferType); if (streamInfo2 === undefined) { return { value: void 0 }; } var activePeriods = streamInfo2.activePeriods.toArray(); var hasPeriod = activePeriods.some(function (p) { return p.id === period.id; }); if (!hasPeriod) { wasFoundInAllTypes = false; break; } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_f && !_f.done && (_d = _e.return)) _d.call(_e); } finally { if (e_2) throw e_2.error; } } if (wasFoundInAllTypes) { if (this_1._lastCurrentPeriodId !== period.id) { this_1._lastCurrentPeriodId = period.id; this_1.trigger("periodChange", period); } return { value: void 0 }; } }; var this_1 = this; try { for (var _b = __values(streamInfo.activePeriods.toArray()), _c = _b.next(); !_c.done; _c = _b.next()) { var period = _c.value; var state_1 = _loop_1(period); if (typeof state_1 === "object") return state_1.value; } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } }; ContentTimeBoundariesObserver.prototype._getManifestEndTime = function () { var endingPosition = this._maximumPositionCalculator.getEndingPosition(); return endingPosition !== undefined ? { isEnd: true, endingPosition: endingPosition } : { isEnd: false, endingPosition: this._maximumPositionCalculator.getMaximumAvailablePosition(), }; }; ContentTimeBoundariesObserver.prototype._lazilyCreateActiveStreamInfo = function (bufferType) { var streamInfo = this._activeStreams.get(bufferType); if (streamInfo === undefined) { streamInfo = { activePeriods: new sorted_list_1.default(function (a, b) { return a.start - b.start; }), hasFinishedLoadingLastPeriod: false, }; this._activeStreams.set(bufferType, streamInfo); } return streamInfo; }; ContentTimeBoundariesObserver.prototype._checkEndOfStream = function () { var _this = this; if (!this._manifest.isLastPeriodKnown) { return; } var everyBufferTypeLoaded = this._allBufferTypes.every(function (bt) { var streamInfo = _this._activeStreams.get(bt); return streamInfo !== undefined && streamInfo.hasFinishedLoadingLastPeriod; }); if (everyBufferTypeLoaded) { this.trigger("endOfStream", null); } else { this.trigger("resumeStream", null); } }; return ContentTimeBoundariesObserver; }(event_emitter_1.default)); exports.default = ContentTimeBoundariesObserver; /** * Calculate the last position from the last chosen audio and video Adaptations * for the last Period (or a default one, if no Adaptations has been chosen). * @class MaximumPositionCalculator */ var MaximumPositionCalculator = /** @class */ (function () { /** * @param {Object} manifest */ function MaximumPositionCalculator(manifest) { this._manifest = manifest; this._lastAudioAdaptation = undefined; this._lastVideoAdaptation = undefined; } /** * Update the last known audio Adaptation for the last Period. * If no Adaptation has been set, it should be set to `null`. * * Allows to calculate the maximum position more precizely in * `getMaximumAvailablePosition` and `getEndingPosition`. * @param {Object|null} adaptation */ MaximumPositionCalculator.prototype.updateLastAudioAdaptation = function (adaptation) { this._lastAudioAdaptation = adaptation; }; /** * Update the last known video Adaptation for the last Period. * If no Adaptation has been set, it should be set to `null`. * * Allows to calculate the maximum position more precizely in * `getMaximumAvailablePosition` and `getEndingPosition`. * @param {Object|null} adaptation */ MaximumPositionCalculator.prototype.updateLastVideoAdaptation = function (adaptation) { this._lastVideoAdaptation = adaptation; }; /** * Returns an estimate of the maximum position currently reachable (i.e. * segments are available) under the current circumstances. * @returns {number} */ MaximumPositionCalculator.prototype.getMaximumAvailablePosition = function () { if (this._manifest.isDynamic) { return this._manifest.getMaximumSafePosition(); } if (this._lastVideoAdaptation === undefined || this._lastAudioAdaptation === undefined) { return this._manifest.getMaximumSafePosition(); } else if (this._lastAudioAdaptation === null) { if (this._lastVideoAdaptation === null) { return this._manifest.getMaximumSafePosition(); } else { var lastVideoPosition = getLastAvailablePositionFromAdaptation(this._lastVideoAdaptation); if (typeof lastVideoPosition !== "number") { return this._manifest.getMaximumSafePosition(); } return lastVideoPosition; } } else if (this._lastVideoAdaptation === null) { var lastAudioPosition = getLastAvailablePositionFromAdaptation(this._lastAudioAdaptation); if (typeof lastAudioPosition !== "number") { return this._manifest.getMaximumSafePosition(); } return lastAudioPosition; } else { var lastAudioPosition = getLastAvailablePositionFromAdaptation(this._lastAudioAdaptation); var lastVideoPosition = getLastAvailablePositionFromAdaptation(this._lastVideoAdaptation); if (typeof lastAudioPosition !== "number" || typeof lastVideoPosition !== "number") { return this._manifest.getMaximumSafePosition(); } else { return Math.min(lastAudioPosition, lastVideoPosition); } } }; /** * Returns an estimate of the actual ending position once * the full content is available. * Returns `undefined` if that could not be determined, for various reasons. * @returns {number|undefined} */ MaximumPositionCalculator.prototype.getEndingPosition = function () { var _a, _b; if (!this._manifest.isDynamic) { return this.getMaximumAvailablePosition(); } if (this._lastVideoAdaptation === undefined || this._lastAudioAdaptation === undefined) { return undefined; } else if (this._lastAudioAdaptation === null) { if (this._lastVideoAdaptation === null) { return undefined; } else { return (_a = getEndingPositionFromAdaptation(this._lastVideoAdaptation)) !== null && _a !== void 0 ? _a : undefined; } } else if (this._lastVideoAdaptation === null) { return (_b = getEndingPositionFromAdaptation(this._lastAudioAdaptation)) !== null && _b !== void 0 ? _b : undefined; } else { var lastAudioPosition = getEndingPositionFromAdaptation(this._lastAudioAdaptation); var lastVideoPosition = getEndingPositionFromAdaptation(this._lastVideoAdaptation); if (typeof lastAudioPosition !== "number" || typeof lastVideoPosition !== "number") { return undefined; } else { return Math.min(lastAudioPosition, lastVideoPosition); } } }; return MaximumPositionCalculator; }()); /** * Returns last currently available position from the Adaptation given. * `undefined` if a time could not be found. * `null` if the Adaptation has no segments (it could be that it didn't started or * that it already finished for example). * * We consider the earliest last available position from every Representation * in the given Adaptation. * @param {Object} adaptation * @returns {Number|undefined|null} */ function getLastAvailablePositionFromAdaptation(adaptation) { var e_3, _a; var representations = adaptation.representations; var min = null; /** * Some Manifest parsers use the exact same `IRepresentationIndex` reference * for each Representation of a given Adaptation, because in the actual source * Manifest file, indexing data is often defined at Adaptation-level. * This variable allows to optimize the logic here when this is the case. */ var lastIndex; try { for (var representations_1 = __values(representations), representations_1_1 = representations_1.next(); !representations_1_1.done; representations_1_1 = representations_1.next()) { var representation = representations_1_1.value; if (representation.index !== lastIndex) { lastIndex = representation.index; var lastPosition = representation.index.getLastAvailablePosition(); if (lastPosition === undefined) { // we cannot tell return undefined; } if (lastPosition !== null) { min = (0, is_null_or_undefined_1.default)(min) ? lastPosition : Math.min(min, lastPosition); } } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (representations_1_1 && !representations_1_1.done && (_a = representations_1.return)) _a.call(representations_1); } finally { if (e_3) throw e_3.error; } } return min; } /** * Returns ending time from the Adaptation given, once all its segments are * available. * `undefined` if a time could not be found. * `null` if the Adaptation has no segments (it could be that it already * finished for example). * * We consider the earliest ending time from every Representation in the given * Adaptation. * @param {Object} adaptation * @returns {Number|undefined|null} */ function getEndingPositionFromAdaptation(adaptation) { var e_4, _a; var representations = adaptation.representations; var min = null; /** * Some Manifest parsers use the exact same `IRepresentationIndex` reference * for each Representation of a given Adaptation, because in the actual source * Manifest file, indexing data is often defined at Adaptation-level. * This variable allows to optimize the logic here when this is the case. */ var lastIndex; try { for (var representations_2 = __values(representations), representations_2_1 = representations_2.next(); !representations_2_1.done; representations_2_1 = representations_2.next()) { var representation = representations_2_1.value; if (representation.index !== lastIndex) { lastIndex = representation.index; var lastPosition = representation.index.getEnd(); if (lastPosition === undefined) { // we cannot tell return undefined; } if (lastPosition !== null) { min = (0, is_null_or_undefined_1.default)(min) ? lastPosition : Math.min(min, lastPosition); } } } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (representations_2_1 && !representations_2_1.done && (_a = representations_2.return)) _a.call(representations_2); } finally { if (e_4) throw e_4.error; } } return min; }