UNPKG

rx-player

Version:
422 lines (421 loc) 19.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 __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 = RepresentationStream; /** * This file allows to create RepresentationStreams. * * A RepresentationStream downloads and push segment for a single * Representation (e.g. a single video stream of a given quality). * It chooses which segments should be downloaded according to the current * position and what is currently buffered. */ var config_1 = require("../../../config"); var log_1 = require("../../../log"); var object_assign_1 = require("../../../utils/object_assign"); var task_canceller_1 = require("../../../utils/task_canceller"); var get_buffer_status_1 = require("./utils/get_buffer_status"); var get_segment_priority_1 = require("./utils/get_segment_priority"); var push_init_segment_1 = require("./utils/push_init_segment"); var push_media_segment_1 = require("./utils/push_media_segment"); /** * Perform the logic to load the right segments for the given Representation and * push them to the given `SegmentSink`. * * In essence, this is the entry point of the core streaming logic of the * RxPlayer, the one actually responsible for finding which are the current * right segments to load, loading them, and pushing them so they can be decoded. * * Multiple RepresentationStream can run on the same SegmentSink. * This allows for example smooth transitions between multiple periods. * * @param {Object} args - Various arguments allowing to know which segments to * load, loading them and pushing them. * You can check the corresponding type for more information. * @param {Object} callbacks - The `RepresentationStream` 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 * `RepresentationStream` 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 `RepresentationStream` * call and may be called until either the `parentCancelSignal` argument is * triggered, until the `terminating` callback has been triggered AND all loaded * segments have been pushed, or until the `error` callback is called, whichever * comes first. * @param {Object} parentCancelSignal - `CancellationSignal` allowing, when * triggered, to immediately stop all operations the `RepresentationStream` is * doing. */ function RepresentationStream(_a, callbacks, parentCancelSignal) { var content = _a.content, options = _a.options, playbackObserver = _a.playbackObserver, segmentSink = _a.segmentSink, segmentQueue = _a.segmentQueue, terminate = _a.terminate; log_1.default.debug("Stream", "Creating RepresentationStream", { periodStart: content.period.start, bufferType: content.adaptation.type, adaptationId: content.adaptation.id, representationBitrate: content.representation.bitrate, mimeType: content.representation.getMimeTypeString(), }); var period = content.period, adaptation = content.adaptation, representation = content.representation; var bufferGoal = options.bufferGoal, maxBufferSize = options.maxBufferSize, drmSystemId = options.drmSystemId, fastSwitchThreshold = options.fastSwitchThreshold; var bufferType = adaptation.type; /** `TaskCanceller` stopping operations performed by the `RepresentationStream` */ var canceller = new task_canceller_1.default(); canceller.linkToSignal(parentCancelSignal); /** Saved initialization segment state for this representation. */ var initSegmentState = { segment: representation.index.getInitSegment(), uniqueId: null, isLoaded: false, }; canceller.signal.register(function () { // Free initialization segment if one has been declared if (initSegmentState.uniqueId !== null) { segmentSink.freeInitSegment(initSegmentState.uniqueId); } }); /** If `true`, the current Representation has a linked initialization segment. */ var hasInitSegment = initSegmentState.segment !== null; if (!hasInitSegment) { initSegmentState.isLoaded = true; } /** * `true` if the event notifying about encryption data has already been * constructed. * Allows to avoid sending multiple times protection events. */ var hasSentEncryptionData = false; // If the DRM system id is already known, and if we already have encryption data // for it, we may not need to wait until the initialization segment is loaded to // signal required protection data, thus performing License negotiations sooner if (drmSystemId !== undefined) { var encryptionData = representation.getEncryptionData(drmSystemId); // If some key ids are not known yet, it may be safer to wait for this initialization // segment to be loaded first if (encryptionData.length > 0 && encryptionData.every(function (e) { return e.keyIds !== undefined; })) { hasSentEncryptionData = true; callbacks.encryptionDataEncountered(encryptionData.map(function (d) { return (0, object_assign_1.default)({ content: content }, d); })); if (canceller.isUsed()) { return; // previous callback has stopped everything by side-effect } } } segmentQueue.addEventListener("error", function (err) { if (canceller.signal.isCancelled()) { return; // ignore post requests-cancellation loading-related errors, } canceller.cancel(); // Stop every operations callbacks.error(err); }); segmentQueue.addEventListener("parsedInitSegment", onParsedChunk, canceller.signal); segmentQueue.addEventListener("parsedMediaSegment", onParsedChunk, canceller.signal); segmentQueue.addEventListener("emptyQueue", checkStatus, canceller.signal); segmentQueue.addEventListener("requestRetry", function (payload) { callbacks.warning(payload.error); if (canceller.signal.isCancelled()) { return; // If the previous callback led to loading operations being stopped, skip } var retriedSegment = payload.segment; var index = representation.index; if (index.isSegmentStillAvailable(retriedSegment) === false) { checkStatus(); } else if (index.canBeOutOfSyncError(payload.error, retriedSegment)) { callbacks.manifestMightBeOufOfSync(); } }, canceller.signal); segmentQueue.addEventListener("fullyLoadedSegment", function (segment) { segmentSink .signalSegmentComplete((0, object_assign_1.default)({ segment: segment }, content)) .catch(onFatalBufferError); }, canceller.signal); /** Emit the last scheduled downloading queue for segments. */ var segmentsToLoadRef = segmentQueue.resetForContent(content, hasInitSegment); canceller.signal.register(function () { segmentQueue.stop(); }); playbackObserver.listen(checkStatus, { includeLastObservation: false, clearSignal: canceller.signal, }); content.manifest.addEventListener("manifestUpdate", checkStatus, canceller.signal); bufferGoal.onUpdate(checkStatus, { emitCurrentValue: false, clearSignal: canceller.signal, }); maxBufferSize.onUpdate(checkStatus, { emitCurrentValue: false, clearSignal: canceller.signal, }); terminate.onUpdate(checkStatus, { emitCurrentValue: false, clearSignal: canceller.signal, }); checkStatus(); return; /** * Produce a buffer status update synchronously on call, update the list * of current segments to update and check various buffer and manifest related * issues at the current time, calling the right callbacks if necessary. */ function checkStatus() { if (canceller.isUsed()) { return; // Stop all buffer status checking if load operations are stopped } var observation = playbackObserver.getReference().getValue(); var initialWantedTime = observation.position.getWanted(); var status = (0, get_buffer_status_1.default)(content, initialWantedTime, playbackObserver, fastSwitchThreshold.getValue(), bufferGoal.getValue(), maxBufferSize.getValue(), segmentSink); var neededSegments = status.neededSegments; var neededInitSegment = null; // Add initialization segment if required if (!representation.index.isInitialized()) { if (initSegmentState.segment === null) { log_1.default.warn("Stream", "Uninitialized index without an initialization segment", { bufferType: bufferType, representationBitrate: content.representation.bitrate, }); } else if (initSegmentState.isLoaded) { log_1.default.warn("Stream", "Uninitialized index with an already loaded " + "initialization segment", { bufferType: bufferType, representationBitrate: content.representation.bitrate, }); } else { var wantedStart = observation.position.getWanted(); neededInitSegment = { segment: initSegmentState.segment, priority: (0, get_segment_priority_1.default)(period.start, wantedStart), }; } } else if (neededSegments.length > 0 && !initSegmentState.isLoaded && initSegmentState.segment !== null) { var initSegmentPriority = neededSegments[0].priority; neededInitSegment = { segment: initSegmentState.segment, priority: initSegmentPriority, }; } var terminateVal = terminate.getValue(); if (terminateVal === null) { segmentsToLoadRef.setValue({ initSegment: neededInitSegment, segmentQueue: neededSegments, }); } else if (terminateVal.urgent) { log_1.default.debug("Stream", "Urgent switch, terminate now.", { bufferType: bufferType, representationBitrate: content.representation.bitrate, }); segmentsToLoadRef.setValue({ initSegment: null, segmentQueue: [] }); segmentsToLoadRef.finish(); canceller.cancel(); callbacks.terminating(); return; } else { // Non-urgent termination wanted: // End the download of the current media segment if pending and // terminate once either that request is finished or another segment // is wanted instead, whichever comes first. var mostNeededSegment = neededSegments[0]; var initSegmentRequest = segmentQueue.getRequestedInitSegment(); var currentSegmentRequest = segmentQueue.getRequestedMediaSegment(); var nextQueue = currentSegmentRequest === null || mostNeededSegment === undefined || currentSegmentRequest.id !== mostNeededSegment.segment.id ? [] : [mostNeededSegment]; var nextInit = initSegmentRequest === null ? null : neededInitSegment; segmentsToLoadRef.setValue({ initSegment: nextInit, segmentQueue: nextQueue, }); if (nextQueue.length === 0 && nextInit === null) { log_1.default.debug("Stream", "No request left, terminate", { bufferType: bufferType, representationBitrate: content.representation.bitrate, }); segmentsToLoadRef.finish(); canceller.cancel(); callbacks.terminating(); return; } } callbacks.streamStatusUpdate({ period: period, position: observation.position.getWanted(), bufferType: bufferType, imminentDiscontinuity: status.imminentDiscontinuity, isEmptyStream: false, hasFinishedLoading: status.hasFinishedLoading, neededSegments: status.neededSegments, }); if (canceller.signal.isCancelled()) { return; // previous callback has stopped loading operations by side-effect } var UPTO_CURRENT_POSITION_CLEANUP = config_1.default.getCurrent().UPTO_CURRENT_POSITION_CLEANUP; if (status.isBufferFull) { var gcedPosition = Math.max(0, initialWantedTime - UPTO_CURRENT_POSITION_CLEANUP); if (gcedPosition > 0) { segmentSink.removeBuffer(0, gcedPosition).catch(onFatalBufferError); } } if (status.shouldRefreshManifest) { callbacks.needsManifestRefresh(); } } /** * Process a chunk that has just been parsed by pushing it to the * SegmentSink and emitting the right events. * @param {Object} evt */ function onParsedChunk(evt) { var e_1, _a; try { // Supplementary encryption information might have been parsed. for (var _b = __values(evt.protectionData), _c = _b.next(); !_c.done; _c = _b.next()) { var protInfo = _c.value; // TODO better handle use cases like key rotation by not always grouping // every protection data together? To check. representation.addProtectionData(protInfo.initDataType, protInfo.keyId, protInfo.initData); } } 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; } } // Now that the initialization segment has been parsed - which may have // included encryption information - take care of the encryption event // if not already done. if (!hasSentEncryptionData) { var allEncryptionData = representation.getAllEncryptionData(); if (allEncryptionData.length > 0) { callbacks.encryptionDataEncountered(allEncryptionData.map(function (p) { return (0, object_assign_1.default)({ content: content }, p); })); hasSentEncryptionData = true; } } if (evt.segmentType === "init") { if (!representation.index.isInitialized() && evt.segmentList !== undefined) { representation.index.initialize(evt.segmentList); } initSegmentState.isLoaded = true; if (evt.initializationData !== null) { var initSegmentUniqueId = representation.uniqueId; initSegmentState.uniqueId = initSegmentUniqueId; segmentSink.declareInitSegment(initSegmentUniqueId, evt.initializationData); (0, push_init_segment_1.default)({ playbackObserver: playbackObserver, bufferGoal: bufferGoal, content: content, initSegmentUniqueId: initSegmentUniqueId, segment: evt.segment, segmentData: evt.initializationData, segmentSink: segmentSink, }, canceller.signal) .then(function (result) { if (result !== null) { callbacks.addedSegment(result); } }) .catch(onFatalBufferError); } // Sometimes the segment list is only known once the initialization segment // is parsed. Thus we immediately re-check if there's new segments to load. checkStatus(); return; } else { var inbandEvents = evt.inbandEvents, predictedSegments = evt.predictedSegments, needsManifestRefresh = evt.needsManifestRefresh; if (predictedSegments !== undefined) { representation.index.addPredictedSegments(predictedSegments, evt.segment); } if (needsManifestRefresh === true) { callbacks.needsManifestRefresh(); if (canceller.isUsed()) { return; // previous callback has stopped everything by side-effect } } if (inbandEvents !== undefined && inbandEvents.length > 0) { callbacks.inbandEvent(inbandEvents); if (canceller.isUsed()) { return; // previous callback has stopped everything by side-effect } } var initSegmentUniqueId = initSegmentState.uniqueId; (0, push_media_segment_1.default)({ playbackObserver: playbackObserver, bufferGoal: bufferGoal, content: content, initSegmentUniqueId: initSegmentUniqueId, parsedSegment: evt, segment: evt.segment, segmentSink: segmentSink, }, canceller.signal) .then(function (result) { if (result !== null) { callbacks.addedSegment(result); } }) .catch(onFatalBufferError); } } /** * Handle Buffer-related fatal errors by cancelling everything the * `RepresentationStream` is doing and calling the error callback with the * corresponding error. * @param {*} err */ function onFatalBufferError(err) { if (canceller.isUsed() && err instanceof task_canceller_1.CancellationError) { // The error is linked to cancellation AND we explicitely cancelled buffer // operations. // We can thus ignore it, it is very unlikely to lead to true buffer issues. return; } log_1.default.warn("Stream", "Received fatal buffer error", { bufferType: bufferType, representationBitrate: content.representation.bitrate, }, err instanceof Error ? err : null); canceller.cancel(); callbacks.error(err); } }