UNPKG

rx-player

Version:
509 lines (477 loc) 17.9 kB
/** * 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. */ /** * 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. */ import config from "../../../config"; import log from "../../../log"; import type { ISegment } from "../../../manifest"; import objectAssign from "../../../utils/object_assign"; import type { CancellationSignal } from "../../../utils/task_canceller"; import TaskCanceller, { CancellationError } from "../../../utils/task_canceller"; import type { IParsedInitSegmentPayload, IParsedSegmentPayload, } from "../../fetchers/segment/segment_queue"; import type { IQueuedSegment, IRepresentationStreamArguments, IRepresentationStreamCallbacks, } from "./types"; import getBufferStatus from "./utils/get_buffer_status"; import getSegmentPriority from "./utils/get_segment_priority"; import pushInitSegment from "./utils/push_init_segment"; import pushMediaSegment from "./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. */ export default function RepresentationStream<TSegmentDataType>( { content, options, playbackObserver, segmentSink, segmentQueue, terminate, }: IRepresentationStreamArguments<TSegmentDataType>, callbacks: IRepresentationStreamCallbacks, parentCancelSignal: CancellationSignal, ): void { log.debug("Stream", "Creating RepresentationStream", { periodStart: content.period.start, bufferType: content.adaptation.type, adaptationId: content.adaptation.id, representationBitrate: content.representation.bitrate, mimeType: content.representation.getMimeTypeString(), }); const { period, adaptation, representation } = content; const { bufferGoal, maxBufferSize, drmSystemId, fastSwitchThreshold } = options; const bufferType = adaptation.type; /** `TaskCanceller` stopping operations performed by the `RepresentationStream` */ const canceller = new TaskCanceller(); canceller.linkToSignal(parentCancelSignal); /** Saved initialization segment state for this representation. */ const initSegmentState: IInitSegmentState = { segment: representation.index.getInitSegment(), uniqueId: null, isLoaded: false, }; canceller.signal.register(() => { // 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. */ const 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. */ let 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) { const 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((e) => e.keyIds !== undefined) ) { hasSentEncryptionData = true; callbacks.encryptionDataEncountered( encryptionData.map((d) => objectAssign({ content }, d)), ); if (canceller.isUsed()) { return; // previous callback has stopped everything by side-effect } } } segmentQueue.addEventListener("error", (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", (payload) => { callbacks.warning(payload.error); if (canceller.signal.isCancelled()) { return; // If the previous callback led to loading operations being stopped, skip } const retriedSegment = payload.segment; const { index } = representation; if (index.isSegmentStillAvailable(retriedSegment) === false) { checkStatus(); } else if (index.canBeOutOfSyncError(payload.error, retriedSegment)) { callbacks.manifestMightBeOufOfSync(); } }, canceller.signal, ); segmentQueue.addEventListener( "fullyLoadedSegment", (segment) => { segmentSink .signalSegmentComplete(objectAssign({ segment }, content)) .catch(onFatalBufferError); }, canceller.signal, ); /** Emit the last scheduled downloading queue for segments. */ const segmentsToLoadRef = segmentQueue.resetForContent(content, hasInitSegment); canceller.signal.register(() => { 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(): void { if (canceller.isUsed()) { return; // Stop all buffer status checking if load operations are stopped } const observation = playbackObserver.getReference().getValue(); const initialWantedTime = observation.position.getWanted(); const status = getBufferStatus( content, initialWantedTime, playbackObserver, fastSwitchThreshold.getValue(), bufferGoal.getValue(), maxBufferSize.getValue(), segmentSink, ); const { neededSegments } = status; let neededInitSegment: IQueuedSegment | null = null; // Add initialization segment if required if (!representation.index.isInitialized()) { if (initSegmentState.segment === null) { log.warn("Stream", "Uninitialized index without an initialization segment", { bufferType, representationBitrate: content.representation.bitrate, }); } else if (initSegmentState.isLoaded) { log.warn( "Stream", "Uninitialized index with an already loaded " + "initialization segment", { bufferType, representationBitrate: content.representation.bitrate, }, ); } else { const wantedStart = observation.position.getWanted(); neededInitSegment = { segment: initSegmentState.segment, priority: getSegmentPriority(period.start, wantedStart), }; } } else if ( neededSegments.length > 0 && !initSegmentState.isLoaded && initSegmentState.segment !== null ) { const initSegmentPriority = neededSegments[0].priority; neededInitSegment = { segment: initSegmentState.segment, priority: initSegmentPriority, }; } const terminateVal = terminate.getValue(); if (terminateVal === null) { segmentsToLoadRef.setValue({ initSegment: neededInitSegment, segmentQueue: neededSegments, }); } else if (terminateVal.urgent) { log.debug("Stream", "Urgent switch, terminate now.", { 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. const mostNeededSegment = neededSegments[0]; const initSegmentRequest = segmentQueue.getRequestedInitSegment(); const currentSegmentRequest = segmentQueue.getRequestedMediaSegment(); const nextQueue = currentSegmentRequest === null || mostNeededSegment === undefined || currentSegmentRequest.id !== mostNeededSegment.segment.id ? [] : [mostNeededSegment]; const nextInit = initSegmentRequest === null ? null : neededInitSegment; segmentsToLoadRef.setValue({ initSegment: nextInit, segmentQueue: nextQueue, }); if (nextQueue.length === 0 && nextInit === null) { log.debug("Stream", "No request left, terminate", { bufferType, representationBitrate: content.representation.bitrate, }); segmentsToLoadRef.finish(); canceller.cancel(); callbacks.terminating(); return; } } callbacks.streamStatusUpdate({ period, position: observation.position.getWanted(), 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 } const { UPTO_CURRENT_POSITION_CLEANUP } = config.getCurrent(); if (status.isBufferFull) { const 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: | IParsedInitSegmentPayload<TSegmentDataType> | IParsedSegmentPayload<TSegmentDataType>, ): void { // Supplementary encryption information might have been parsed. for (const protInfo of evt.protectionData) { // 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, ); } // 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) { const allEncryptionData = representation.getAllEncryptionData(); if (allEncryptionData.length > 0) { callbacks.encryptionDataEncountered( allEncryptionData.map((p) => objectAssign({ 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) { const initSegmentUniqueId = representation.uniqueId; initSegmentState.uniqueId = initSegmentUniqueId; segmentSink.declareInitSegment(initSegmentUniqueId, evt.initializationData); pushInitSegment( { playbackObserver, bufferGoal, content, initSegmentUniqueId, segment: evt.segment, segmentData: evt.initializationData, segmentSink, }, canceller.signal, ) .then((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 { const { inbandEvents, predictedSegments, needsManifestRefresh } = evt; 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 } } const initSegmentUniqueId = initSegmentState.uniqueId; pushMediaSegment( { playbackObserver, bufferGoal, content, initSegmentUniqueId, parsedSegment: evt, segment: evt.segment, segmentSink, }, canceller.signal, ) .then((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: unknown): void { if (canceller.isUsed() && err instanceof 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.warn( "Stream", "Received fatal buffer error", { bufferType, representationBitrate: content.representation.bitrate, }, err instanceof Error ? err : null, ); canceller.cancel(); callbacks.error(err); } } /** * Information about the initialization segment linked to the Representation * which the RepresentationStream try to download segments for. */ interface IInitSegmentState { /** * Segment Object describing that initialization segment. * `null` if there's no initialization segment for that Representation. */ segment: ISegment | null; /** * Unique identifier used to identify the initialization segment data, used by * the `SegmentSink`. * `null` either when it doesn't exist or when it has not been declared yet. */ uniqueId: string | null; /** `true` if the initialization segment has been loaded and parsed. */ isLoaded: boolean; }