UNPKG

rx-player

Version:
426 lines (425 loc) 20.7 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 __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); var log_1 = require("../../../log"); var assert_1 = require("../../../utils/assert"); var event_emitter_1 = require("../../../utils/event_emitter"); var noop_1 = require("../../../utils/noop"); var object_assign_1 = require("../../../utils/object_assign"); var reference_1 = require("../../../utils/reference"); var task_canceller_1 = require("../../../utils/task_canceller"); /** * Class scheduling segment downloads as a FIFO queue. */ var SegmentQueue = /** @class */ (function (_super) { __extends(SegmentQueue, _super); /** * Create a new `SegmentQueue`. * * @param {Object} segmentFetcher - Interface to facilitate the download of * segments. * @param {Object} isMediaSegmentQueueInterrupted - Reference to a boolean indicating * if the media segment queue is interrupted. */ function SegmentQueue(segmentFetcher, isMediaSegmentQueueInterrupted) { var _this = _super.call(this) || this; _this._segmentFetcher = segmentFetcher; _this._currentContentInfo = null; _this.isMediaSegmentQueueInterrupted = isMediaSegmentQueueInterrupted; return _this; } /** * Returns the initialization segment currently being requested. * Returns `null` if no initialization segment request is pending. * @returns {Object | null} */ SegmentQueue.prototype.getRequestedInitSegment = function () { var _a, _b, _c; return (_c = (_b = (_a = this._currentContentInfo) === null || _a === void 0 ? void 0 : _a.initSegmentRequest) === null || _b === void 0 ? void 0 : _b.segment) !== null && _c !== void 0 ? _c : null; }; /** * Returns the media segment currently being requested. * Returns `null` if no media segment request is pending. * @returns {Object | null} */ SegmentQueue.prototype.getRequestedMediaSegment = function () { var _a, _b, _c; return (_c = (_b = (_a = this._currentContentInfo) === null || _a === void 0 ? void 0 : _a.mediaSegmentRequest) === null || _b === void 0 ? void 0 : _b.segment) !== null && _c !== void 0 ? _c : null; }; /** * Return an object allowing to schedule segment requests linked to the given * content. * The `SegmentQueue` will emit events as it loads and parses initialization * and media segments. * * Calling this method resets all previous queues that were previously started * on the same instance. * * @param {Object} content - The context of the Representation you want to * load segments for. * @param {boolean} hasInitSegment - Declare that an initialization segment * will need to be downloaded. * * A `SegmentQueue` ALWAYS wait for the initialization segment to be * loaded and parsed before parsing a media segment. * * In cases where no initialization segment exist, this would lead to the * `SegmentQueue` waiting indefinitely for it. * * By setting that value to `false`, you anounce to the `SegmentQueue` * that it should not wait for an initialization segment before parsing a * media segment. * @returns {Object} - `SharedReference` on which the queue of segment for * that content can be communicated and updated. See type for more * information. */ SegmentQueue.prototype.resetForContent = function (content, hasInitSegment) { var _this = this; var _a; (_a = this._currentContentInfo) === null || _a === void 0 ? void 0 : _a.currentCanceller.cancel(); var downloadQueue = new reference_1.default({ initSegment: null, segmentQueue: [], }); var currentCanceller = new task_canceller_1.default(); currentCanceller.signal.register(function () { downloadQueue.finish(); }); var currentContentInfo = { content: content, downloadQueue: downloadQueue, initSegmentInfoRef: hasInitSegment ? new reference_1.default(undefined) : new reference_1.default(null), currentCanceller: currentCanceller, initSegmentRequest: null, mediaSegmentRequest: null, mediaSegmentAwaitingInitMetadata: null, }; this._currentContentInfo = currentContentInfo; this.isMediaSegmentQueueInterrupted.onUpdate(function (val) { if (!val) { log_1.default.debug("SQ: Media segment can be loaded again, restarting queue.", content.adaptation.type); _this._restartMediaSegmentDownloadingQueue(currentContentInfo); } }, { clearSignal: currentCanceller.signal }); // Listen for asked media segments downloadQueue.onUpdate(function (queue) { var segmentQueue = queue.segmentQueue; if (segmentQueue.length > 0 && segmentQueue[0].segment.id === currentContentInfo.mediaSegmentAwaitingInitMetadata) { // The most needed segment is still the same one, and there's no need to // update its priority as the request already ended, just quit. return; } var currentSegmentRequest = currentContentInfo.mediaSegmentRequest; if (segmentQueue.length === 0) { if (currentSegmentRequest === null) { // There's nothing to load but there's already no request pending. return; } log_1.default.debug("SQ: no more media segment to request. Cancelling queue.", content.adaptation.type); _this._restartMediaSegmentDownloadingQueue(currentContentInfo); return; } else if (currentSegmentRequest === null) { // There's no request although there are needed segments: start requests log_1.default.debug("SQ: Media segments now need to be requested. Starting queue.", content.adaptation.type, segmentQueue.length); _this._restartMediaSegmentDownloadingQueue(currentContentInfo); return; } else { var nextItem = segmentQueue[0]; if (currentSegmentRequest.segment.id !== nextItem.segment.id) { // The most important request if for another segment, request it log_1.default.debug("SQ: Next media segment changed, cancelling previous", content.adaptation.type); _this._restartMediaSegmentDownloadingQueue(currentContentInfo); return; } if (currentSegmentRequest.priority !== nextItem.priority) { // The priority of the most important request has changed, update it log_1.default.debug("SQ: Priority of next media segment changed, updating", content.adaptation.type, currentSegmentRequest.priority, nextItem.priority); _this._segmentFetcher.updatePriority(currentSegmentRequest.request, nextItem.priority); } return; } }, { emitCurrentValue: true, clearSignal: currentCanceller.signal }); // Listen for asked init segment downloadQueue.onUpdate(function (next) { var _a; var initSegmentRequest = currentContentInfo.initSegmentRequest; if (next.initSegment !== null && initSegmentRequest !== null) { if (next.initSegment.priority !== initSegmentRequest.priority) { _this._segmentFetcher.updatePriority(initSegmentRequest.request, next.initSegment.priority); } return; } else if (((_a = next.initSegment) === null || _a === void 0 ? void 0 : _a.segment.id) === (initSegmentRequest === null || initSegmentRequest === void 0 ? void 0 : initSegmentRequest.segment.id)) { return; } if (next.initSegment === null) { log_1.default.debug("SQ: no more init segment to request. Cancelling queue.", content.adaptation.type); } _this._restartInitSegmentDownloadingQueue(currentContentInfo, next.initSegment); }, { emitCurrentValue: true, clearSignal: currentCanceller.signal }); return downloadQueue; }; /** * Stop the currently-active `SegmentQueue`. * * Do nothing if no queue is active. */ SegmentQueue.prototype.stop = function () { var _a; (_a = this._currentContentInfo) === null || _a === void 0 ? void 0 : _a.currentCanceller.cancel(); this._currentContentInfo = null; }; /** * Internal logic performing media segment requests. */ SegmentQueue.prototype._restartMediaSegmentDownloadingQueue = function (contentInfo) { var _this = this; if (contentInfo.mediaSegmentRequest !== null) { contentInfo.mediaSegmentRequest.canceller.cancel(); } var downloadQueue = contentInfo.downloadQueue, content = contentInfo.content, initSegmentInfoRef = contentInfo.initSegmentInfoRef, currentCanceller = contentInfo.currentCanceller; var recursivelyRequestSegments = function () { var _a; if (_this.isMediaSegmentQueueInterrupted.getValue()) { log_1.default.debug("SQ: Segment fetching postponed because it cannot stream now."); return; } var segmentQueue = downloadQueue.getValue().segmentQueue; var startingSegment = segmentQueue[0]; if (currentCanceller !== null && currentCanceller.isUsed()) { contentInfo.mediaSegmentRequest = null; return; } if (startingSegment === undefined) { contentInfo.mediaSegmentRequest = null; _this.trigger("emptyQueue", null); return; } var canceller = new task_canceller_1.default(); var unlinkCanceller = currentCanceller === null ? noop_1.default : canceller.linkToSignal(currentCanceller.signal); var segment = startingSegment.segment, priority = startingSegment.priority; var context = (0, object_assign_1.default)({ segment: segment, nextSegment: (_a = segmentQueue[1]) === null || _a === void 0 ? void 0 : _a.segment }, content); /** * If `true` , the current task has either errored, finished, or was * cancelled. */ var isComplete = false; /** * If true, we're currently waiting for the initialization segment to be * parsed before parsing a received chunk. */ var isWaitingOnInitSegment = false; canceller.signal.register(function () { contentInfo.mediaSegmentRequest = null; if (isComplete) { return; } if (contentInfo.mediaSegmentAwaitingInitMetadata === segment.id) { contentInfo.mediaSegmentAwaitingInitMetadata = null; } isComplete = true; isWaitingOnInitSegment = false; }); var emitChunk = function (parsed) { (0, assert_1.default)(parsed.segmentType === "media", "Should have loaded a media segment."); _this.trigger("parsedMediaSegment", (0, object_assign_1.default)({}, parsed, { segment: segment })); }; var continueToNextSegment = function () { var lastQueue = downloadQueue.getValue().segmentQueue; if (lastQueue.length === 0) { isComplete = true; _this.trigger("emptyQueue", null); return; } else if (lastQueue[0].segment.id === segment.id) { lastQueue.shift(); } isComplete = true; recursivelyRequestSegments(); }; /** Scheduled actual segment request. */ var request = _this._segmentFetcher.createRequest(context, priority, { /** * Callback called when the request has to be retried. * @param {Error} error */ onRetry: function (error) { _this.trigger("requestRetry", { segment: segment, error: error }); }, /** * Callback called when the request has to be interrupted and * restarted later. */ beforeInterrupted: function () { log_1.default.info("SQ: segment request interrupted temporarly.", segment.id, segment.time); }, /** * Callback called when a decodable chunk of the segment is available. * @param {Function} parse - Function allowing to parse the segment. */ onChunk: function (parse) { var initTimescale = initSegmentInfoRef.getValue(); if (initTimescale !== undefined) { emitChunk(parse(initTimescale !== null && initTimescale !== void 0 ? initTimescale : undefined)); } else { isWaitingOnInitSegment = true; // We could also technically call `waitUntilDefined` in both cases, // but I found it globally clearer to segregate the two cases, // especially to always have a meaningful `isWaitingOnInitSegment` // boolean which is a very important variable. initSegmentInfoRef.waitUntilDefined(function (actualTimescale) { emitChunk(parse(actualTimescale !== null && actualTimescale !== void 0 ? actualTimescale : undefined)); }, { clearSignal: canceller.signal }); } }, /** Callback called after all chunks have been sent. */ onAllChunksReceived: function () { if (!isWaitingOnInitSegment) { _this.trigger("fullyLoadedSegment", segment); } else { contentInfo.mediaSegmentAwaitingInitMetadata = segment.id; initSegmentInfoRef.waitUntilDefined(function () { contentInfo.mediaSegmentAwaitingInitMetadata = null; isWaitingOnInitSegment = false; _this.trigger("fullyLoadedSegment", segment); }, { clearSignal: canceller.signal }); } }, /** * Callback called right after the request ended but before the next * requests are scheduled. It is used to schedule the next segment. */ beforeEnded: function () { unlinkCanceller(); contentInfo.mediaSegmentRequest = null; if (isWaitingOnInitSegment) { initSegmentInfoRef.waitUntilDefined(continueToNextSegment, { clearSignal: canceller.signal, }); } else { continueToNextSegment(); } }, }, canceller.signal); request.catch(function (error) { unlinkCanceller(); if (!isComplete) { isComplete = true; _this.stop(); _this.trigger("error", error); } }); contentInfo.mediaSegmentRequest = { segment: segment, priority: priority, request: request, canceller: canceller }; }; recursivelyRequestSegments(); }; /** * Internal logic performing initialization segment requests. * @param {Object} contentInfo * @param {Object} queuedInitSegment */ SegmentQueue.prototype._restartInitSegmentDownloadingQueue = function (contentInfo, queuedInitSegment) { var _this = this; var content = contentInfo.content, initSegmentInfoRef = contentInfo.initSegmentInfoRef; if (contentInfo.initSegmentRequest !== null) { contentInfo.initSegmentRequest.canceller.cancel(); } if (queuedInitSegment === null) { return; } var canceller = new task_canceller_1.default(); var unlinkCanceller = contentInfo.currentCanceller === null ? noop_1.default : canceller.linkToSignal(contentInfo.currentCanceller.signal); var segment = queuedInitSegment.segment, priority = queuedInitSegment.priority; var context = (0, object_assign_1.default)({ segment: segment, nextSegment: undefined }, content); /** * If `true` , the current task has either errored, finished, or was * cancelled. */ var isComplete = false; var request = this._segmentFetcher.createRequest(context, priority, { onRetry: function (err) { _this.trigger("requestRetry", { segment: segment, error: err }); }, beforeInterrupted: function () { log_1.default.info("SQ: init segment request interrupted temporarly.", segment.id); }, beforeEnded: function () { unlinkCanceller(); contentInfo.initSegmentRequest = null; isComplete = true; }, onChunk: function (parse) { var _a; var parsed = parse(undefined); (0, assert_1.default)(parsed.segmentType === "init", "Should have loaded an init segment."); _this.trigger("parsedInitSegment", (0, object_assign_1.default)({}, parsed, { segment: segment })); if (parsed.segmentType === "init") { initSegmentInfoRef.setValue((_a = parsed.initTimescale) !== null && _a !== void 0 ? _a : null); } }, onAllChunksReceived: function () { _this.trigger("fullyLoadedSegment", segment); }, }, canceller.signal); request.catch(function (error) { unlinkCanceller(); if (!isComplete) { isComplete = true; _this.stop(); _this.trigger("error", error); } }); canceller.signal.register(function () { contentInfo.initSegmentRequest = null; if (isComplete) { return; } isComplete = true; }); contentInfo.initSegmentRequest = { segment: segment, priority: priority, request: request, canceller: canceller }; }; return SegmentQueue; }(event_emitter_1.default)); exports.default = SegmentQueue;