UNPKG

rx-player

Version:
722 lines (672 loc) 25.1 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. */ import log from "../../../log"; import type { IManifest, IAdaptation, ISegment, IPeriod, IRepresentation, } from "../../../manifest"; import type { IPlayerError } from "../../../public_types"; import type { ISegmentParserParsedInitChunk, ISegmentParserParsedMediaChunk, } from "../../../transports"; import assert from "../../../utils/assert"; import EventEmitter from "../../../utils/event_emitter"; import noop from "../../../utils/noop"; import objectAssign from "../../../utils/object_assign"; import SharedReference from "../../../utils/reference"; import TaskCanceller from "../../../utils/task_canceller"; import type { IPrioritizedSegmentFetcher } from "./prioritized_segment_fetcher"; /** Information about a Segment waiting to be loaded by the SegmentQueue. */ export interface IQueuedSegment { /** Priority of the segment request (lower number = higher priority). */ priority: number; /** Segment wanted. */ segment: ISegment; } /** * Class scheduling segment downloads as a FIFO queue. */ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>> { /** Interface used to load segments. */ private _segmentFetcher: IPrioritizedSegmentFetcher<T>; /** * Metadata on the content for which segments are currently loaded. * `null` if no queue is active. */ private _currentContentInfo: ISegmentQueueContentInfo | null; /** * Indicates whether the segment queue is inturrupted or if * it is authorized to download segment data. * Updating this value to `true` should stop the * loading of new media data. Updating this value to `false` should * restart the downloading queue. * Note: this does not affect the downloading of init segment as * they are always downloaded, regardless of this property value. * * This option can be used to temporarly reduce the usage of the * network. */ private isMediaSegmentQueueInterrupted: SharedReference<boolean>; /** * 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. */ constructor( segmentFetcher: IPrioritizedSegmentFetcher<T>, isMediaSegmentQueueInterrupted: SharedReference<boolean>, ) { super(); this._segmentFetcher = segmentFetcher; this._currentContentInfo = null; this.isMediaSegmentQueueInterrupted = isMediaSegmentQueueInterrupted; } /** * Returns the initialization segment currently being requested. * Returns `null` if no initialization segment request is pending. * @returns {Object | null} */ public getRequestedInitSegment(): ISegment | null { return this._currentContentInfo?.initSegmentRequest?.segment ?? null; } /** * Returns the media segment currently being requested. * Returns `null` if no media segment request is pending. * @returns {Object | null} */ public getRequestedMediaSegment(): ISegment | null { return this._currentContentInfo?.mediaSegmentRequest?.segment ?? 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. */ public resetForContent( content: ISegmentQueueContext, hasInitSegment: boolean, ): SharedReference<ISegmentQueueItem> { this._currentContentInfo?.currentCanceller.cancel(); const downloadQueue = new SharedReference<ISegmentQueueItem>({ initSegment: null, segmentQueue: [], }); const currentCanceller = new TaskCanceller(); currentCanceller.signal.register(() => { downloadQueue.finish(); }); const currentContentInfo: ISegmentQueueContentInfo = { content, downloadQueue, initSegmentInfoRef: hasInitSegment ? new SharedReference<number | undefined | null>(undefined) : new SharedReference<number | undefined | null>(null), currentCanceller, initSegmentRequest: null, mediaSegmentRequest: null, mediaSegmentAwaitingInitMetadata: null, }; this._currentContentInfo = currentContentInfo; this.isMediaSegmentQueueInterrupted.onUpdate( (val) => { if (!val) { log.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( (queue) => { const { segmentQueue } = queue; 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; } const currentSegmentRequest = currentContentInfo.mediaSegmentRequest; if (segmentQueue.length === 0) { if (currentSegmentRequest === null) { // There's nothing to load but there's already no request pending. return; } log.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.debug( "SQ: Media segments now need to be requested. Starting queue.", content.adaptation.type, segmentQueue.length, ); this._restartMediaSegmentDownloadingQueue(currentContentInfo); return; } else { const nextItem = segmentQueue[0]; if (currentSegmentRequest.segment.id !== nextItem.segment.id) { // The most important request if for another segment, request it log.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.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( (next) => { const 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 (next.initSegment?.segment.id === initSegmentRequest?.segment.id) { return; } if (next.initSegment === null) { log.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. */ public stop() { this._currentContentInfo?.currentCanceller.cancel(); this._currentContentInfo = null; } /** * Internal logic performing media segment requests. */ private _restartMediaSegmentDownloadingQueue( contentInfo: ISegmentQueueContentInfo, ): void { if (contentInfo.mediaSegmentRequest !== null) { contentInfo.mediaSegmentRequest.canceller.cancel(); } const { downloadQueue, content, initSegmentInfoRef, currentCanceller } = contentInfo; const recursivelyRequestSegments = (): void => { if (this.isMediaSegmentQueueInterrupted.getValue()) { log.debug("SQ: Segment fetching postponed because it cannot stream now."); return; } const { segmentQueue } = downloadQueue.getValue(); const startingSegment = segmentQueue[0]; if (currentCanceller !== null && currentCanceller.isUsed()) { contentInfo.mediaSegmentRequest = null; return; } if (startingSegment === undefined) { contentInfo.mediaSegmentRequest = null; this.trigger("emptyQueue", null); return; } const canceller = new TaskCanceller(); const unlinkCanceller = currentCanceller === null ? noop : canceller.linkToSignal(currentCanceller.signal); const { segment, priority } = startingSegment; const context = objectAssign( { segment, nextSegment: segmentQueue[1]?.segment }, content, ); /** * If `true` , the current task has either errored, finished, or was * cancelled. */ let isComplete = false; /** * If true, we're currently waiting for the initialization segment to be * parsed before parsing a received chunk. */ let isWaitingOnInitSegment = false; canceller.signal.register(() => { contentInfo.mediaSegmentRequest = null; if (isComplete) { return; } if (contentInfo.mediaSegmentAwaitingInitMetadata === segment.id) { contentInfo.mediaSegmentAwaitingInitMetadata = null; } isComplete = true; isWaitingOnInitSegment = false; }); const emitChunk = ( parsed: ISegmentParserParsedInitChunk<T> | ISegmentParserParsedMediaChunk<T>, ): void => { assert(parsed.segmentType === "media", "Should have loaded a media segment."); this.trigger("parsedMediaSegment", objectAssign({}, parsed, { segment })); }; const continueToNextSegment = (): void => { const 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. */ const request = this._segmentFetcher.createRequest( context, priority, { /** * Callback called when the request has to be retried. * @param {Error} error */ onRetry: (error: IPlayerError): void => { this.trigger("requestRetry", { segment, error }); }, /** * Callback called when the request has to be interrupted and * restarted later. */ beforeInterrupted() { log.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: ( parse: ( initTimescale: number | undefined, ) => ISegmentParserParsedInitChunk<T> | ISegmentParserParsedMediaChunk<T>, ): void => { const initTimescale = initSegmentInfoRef.getValue(); if (initTimescale !== undefined) { emitChunk(parse(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( (actualTimescale) => { emitChunk(parse(actualTimescale ?? undefined)); }, { clearSignal: canceller.signal }, ); } }, /** Callback called after all chunks have been sent. */ onAllChunksReceived: (): void => { if (!isWaitingOnInitSegment) { this.trigger("fullyLoadedSegment", segment); } else { contentInfo.mediaSegmentAwaitingInitMetadata = segment.id; initSegmentInfoRef.waitUntilDefined( () => { 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: (): void => { unlinkCanceller(); contentInfo.mediaSegmentRequest = null; if (isWaitingOnInitSegment) { initSegmentInfoRef.waitUntilDefined(continueToNextSegment, { clearSignal: canceller.signal, }); } else { continueToNextSegment(); } }, }, canceller.signal, ); request.catch((error: unknown) => { unlinkCanceller(); if (!isComplete) { isComplete = true; this.stop(); this.trigger("error", error); } }); contentInfo.mediaSegmentRequest = { segment, priority, request, canceller }; }; recursivelyRequestSegments(); } /** * Internal logic performing initialization segment requests. * @param {Object} contentInfo * @param {Object} queuedInitSegment */ private _restartInitSegmentDownloadingQueue( contentInfo: ISegmentQueueContentInfo, queuedInitSegment: IQueuedSegment | null, ): void { const { content, initSegmentInfoRef } = contentInfo; if (contentInfo.initSegmentRequest !== null) { contentInfo.initSegmentRequest.canceller.cancel(); } if (queuedInitSegment === null) { return; } const canceller = new TaskCanceller(); const unlinkCanceller = contentInfo.currentCanceller === null ? noop : canceller.linkToSignal(contentInfo.currentCanceller.signal); const { segment, priority } = queuedInitSegment; const context = objectAssign({ segment, nextSegment: undefined }, content); /** * If `true` , the current task has either errored, finished, or was * cancelled. */ let isComplete = false; const request = this._segmentFetcher.createRequest( context, priority, { onRetry: (err: IPlayerError): void => { this.trigger("requestRetry", { segment, error: err }); }, beforeInterrupted: () => { log.info("SQ: init segment request interrupted temporarly.", segment.id); }, beforeEnded: () => { unlinkCanceller(); contentInfo.initSegmentRequest = null; isComplete = true; }, onChunk: ( parse: ( x: undefined, ) => ISegmentParserParsedInitChunk<T> | ISegmentParserParsedMediaChunk<T>, ): void => { const parsed = parse(undefined); assert(parsed.segmentType === "init", "Should have loaded an init segment."); this.trigger("parsedInitSegment", objectAssign({}, parsed, { segment })); if (parsed.segmentType === "init") { initSegmentInfoRef.setValue(parsed.initTimescale ?? null); } }, onAllChunksReceived: (): void => { this.trigger("fullyLoadedSegment", segment); }, }, canceller.signal, ); request.catch((error: unknown) => { unlinkCanceller(); if (!isComplete) { isComplete = true; this.stop(); this.trigger("error", error); } }); canceller.signal.register(() => { contentInfo.initSegmentRequest = null; if (isComplete) { return; } isComplete = true; }); contentInfo.initSegmentRequest = { segment, priority, request, canceller }; } } /** * Events sent by the `SegmentQueue`. * * The key is the event's name and the value the format of the corresponding * event's payload. */ export interface ISegmentQueueEvent<T> { /** * Notify that the initialization segment has been fully loaded and parsed. * * You can now push that segment to its corresponding buffer and use its parsed * metadata. * * Only sent if an initialization segment exists (when the `SegmentQueue`'s * `hasInitSegment` constructor option has been set to `true`). * In that case, an `IParsedInitSegmentEvent` will always be sent before any * `IParsedSegmentEvent` event is sent. */ parsedInitSegment: IParsedInitSegmentPayload<T>; /** * Notify that a media chunk (decodable sub-part of a media segment) has been * loaded and parsed. * * If an initialization segment exists (when the `SegmentQueue`'s * `hasInitSegment` constructor option has been set to `true`), an * `IParsedSegmentEvent` will always be sent AFTER the `IParsedInitSegmentEvent` * event. * * It can now be pushed to its corresponding buffer. Note that there might be * multiple `IParsedSegmentEvent` for a single segment, if that segment is * divided into multiple decodable chunks. * You will know that all `IParsedSegmentEvent` have been loaded for a given * segment once you received the corresponding event. */ parsedMediaSegment: IParsedSegmentPayload<T>; /** Notify that a media or initialization segment has been fully-loaded. */ fullyLoadedSegment: ISegment; /** * Notify that a media or initialization segment request is retried. * This happened most likely because of an HTTP error. */ requestRetry: IRequestRetryPayload; /** * Notify that the media segment queue is now empty. * This can be used to re-check if any segment are now needed. */ emptyQueue: null; /** * Notify that a fatal error happened (such as request failures), which has * completely stopped the downloading queue. * * You may still restart the queue after receiving this event. */ error: unknown; } /** Payload for a `parsedInitSegment` event. */ export type IParsedInitSegmentPayload<T> = ISegmentParserParsedInitChunk<T> & { segment: ISegment; }; /** Payload for a `parsedMediaSegment` event. */ export type IParsedSegmentPayload<T> = ISegmentParserParsedMediaChunk<T> & { segment: ISegment; }; /** Payload for a `requestRetry` event. */ export interface IRequestRetryPayload { segment: ISegment; error: IPlayerError; } /** * Structure of the object that has to be emitted through the `SegmentQueue` * shared reference, to signal which segments are currently needed. */ export interface ISegmentQueueItem { /** * A potential initialization segment that needs to be loaded and parsed. * It will generally be requested in parralel of the first media segments. * * Can be set to `null` if you don't need to load the initialization segment * for now. * * If the `SegmentQueue`'s `hasInitSegment` constructor option has been * set to `true`, no media segment will be parsed before the initialization * segment has been loaded and parsed. */ initSegment: IQueuedSegment | null; /** * The queue of media segments currently needed for download. * * Those will be loaded from the first element in that queue to the last * element in it. * * Note that any media segments in the segment queue will only be parsed once * either of these is true: * - An initialization segment has been loaded and parsed by this * `SegmentQueue` instance. * - The `SegmentQueue`'s `hasInitSegment` constructor option has been * set to `false`. */ segmentQueue: IQueuedSegment[]; } /** Object describing a pending Segment request. */ interface ISegmentRequestObject { /** The segment the request is for. */ segment: ISegment; /** The request itself. Can be used to update its priority. */ request: Promise<void>; /** Last set priority of the segment request (lower number = higher priority). */ priority: number; /** Allows to cancel that segment from being requested. */ canceller: TaskCanceller; } /** Context for segments downloaded through the SegmentQueue. */ export interface ISegmentQueueContext { /** Adaptation linked to the segments you want to load. */ adaptation: IAdaptation; /** Manifest linked to the segments you want to load. */ manifest: IManifest; /** Period linked to the segments you want to load. */ period: IPeriod; /** Representation linked to the segments you want to load. */ representation: IRepresentation; } interface ISegmentQueueContentInfo { /** Context of the Representation that will be loaded through this SegmentQueue. */ content: ISegmentQueueContext; /** * Current queue of segments scheduled for download. * * Segments whose request are still pending are still in that queue. Segments * are only removed from it once their request has succeeded. */ downloadQueue: SharedReference<ISegmentQueueItem>; /** * Allows to stop listening to queue updates and stop performing requests. * Set to `null` if the SegmentQueue is not started right now. */ currentCanceller: TaskCanceller; /** * Pending request for the initialization segment. * `null` if no request is pending for it. */ initSegmentRequest: ISegmentRequestObject | null; /** * Pending request for a media (i.e. non-initialization) segment. * `null` if no request is pending for it. */ mediaSegmentRequest: ISegmentRequestObject | null; /** * Emit the timescale anounced in the initialization segment once parsed. * Emit `undefined` when this is not yet known. * Emit `null` when no initialization segment or timescale exists. */ initSegmentInfoRef: SharedReference<number | undefined | null>; /** * Some media segment might have been loaded and are only awaiting for the * initialization segment to be parsed before being parsed themselves. * This string will contain the `id` property of that segment if one exist or * `null` if no segment is awaiting an init segment. */ mediaSegmentAwaitingInitMetadata: string | null; }