diff --git a/src/compat/remove_cue.ts b/src/compat/remove_cue.ts index e9277f68d2..ffb8ebf73a 100644 --- a/src/compat/remove_cue.ts +++ b/src/compat/remove_cue.ts @@ -15,6 +15,7 @@ */ import log from "../log"; +import type { ICompatTextTrack, ICompatVTTCue } from "./browser_compatibility_types"; import { isFirefox } from "./browser_detection"; /** @@ -23,7 +24,10 @@ import { isFirefox } from "./browser_detection"; * @param {TextTrackCue} cue * @returns {boolean} */ -function isActiveCue(track: TextTrack, cue: TextTrackCue): boolean { +function isActiveCue( + track: TextTrack | ICompatTextTrack, + cue: TextTrackCue | ICompatVTTCue, +): boolean { const { activeCues } = track; if (activeCues === null) { return false; @@ -41,7 +45,10 @@ function isActiveCue(track: TextTrack, cue: TextTrackCue): boolean { * @param {TextTrack} track * @param {TextTrackCue} cue */ -export default function removeCue(track: TextTrack, cue: TextTrackCue): void { +export default function removeCue( + track: TextTrack | ICompatTextTrack, + cue: TextTrackCue | ICompatVTTCue, +): void { // On Firefox, cue doesn't dissapear when it is removed from track. Track // should be hidden, and shown again after removing cue, in order to // definitely clean the cue. @@ -49,7 +56,7 @@ export default function removeCue(track: TextTrack, cue: TextTrackCue): void { const trackMode = track.mode; track.mode = "hidden"; try { - track.removeCue(cue); + track.removeCue(cue as TextTrackCue); } catch (_err) { log.warn("Compat: Could not remove cue from text track."); } @@ -57,7 +64,7 @@ export default function removeCue(track: TextTrack, cue: TextTrackCue): void { return; } try { - track.removeCue(cue); + track.removeCue(cue as TextTrackCue); } catch (_err) { log.warn("Compat: Could not remove cue from text track."); } diff --git a/src/core/segment_sinks/implementations/audio_video/audio_video_segment_buffer.ts b/src/core/segment_sinks/implementations/audio_video/audio_video_segment_sink.ts similarity index 100% rename from src/core/segment_sinks/implementations/audio_video/audio_video_segment_buffer.ts rename to src/core/segment_sinks/implementations/audio_video/audio_video_segment_sink.ts diff --git a/src/core/segment_sinks/implementations/audio_video/index.ts b/src/core/segment_sinks/implementations/audio_video/index.ts index 60c526c811..04592630b4 100644 --- a/src/core/segment_sinks/implementations/audio_video/index.ts +++ b/src/core/segment_sinks/implementations/audio_video/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -import AudioVideoSegmentSink from "./audio_video_segment_buffer"; +import AudioVideoSegmentSink from "./audio_video_segment_sink"; export default AudioVideoSegmentSink; diff --git a/src/core/segment_sinks/implementations/text/index.ts b/src/core/segment_sinks/implementations/text/index.ts index fd2de8c034..b1a10f2ab9 100644 --- a/src/core/segment_sinks/implementations/text/index.ts +++ b/src/core/segment_sinks/implementations/text/index.ts @@ -1,5 +1,5 @@ -import type { ITextDisplayerInterface } from "./text_segment_buffer"; -import TextSegmentSink from "./text_segment_buffer"; +import type { ITextDisplayerInterface } from "./text_segment_sink"; +import TextSegmentSink from "./text_segment_sink"; export default TextSegmentSink; export type { ITextDisplayerInterface }; diff --git a/src/core/segment_sinks/implementations/text/text_segment_buffer.ts b/src/core/segment_sinks/implementations/text/text_segment_sink.ts similarity index 100% rename from src/core/segment_sinks/implementations/text/text_segment_buffer.ts rename to src/core/segment_sinks/implementations/text/text_segment_sink.ts diff --git a/src/core/segment_sinks/index.ts b/src/core/segment_sinks/index.ts index 8c5797ceb0..3d3418d0be 100644 --- a/src/core/segment_sinks/index.ts +++ b/src/core/segment_sinks/index.ts @@ -38,7 +38,7 @@ import { getFirstSegmentAfterPeriod, getLastSegmentBeforePeriod, } from "./inventory"; -import SegmentSinksStore from "./segment_buffers_store"; +import SegmentSinksStore from "./segment_sinks_store"; export default SegmentSinksStore; export type { diff --git a/src/core/segment_sinks/segment_buffers_store.ts b/src/core/segment_sinks/segment_sinks_store.ts similarity index 98% rename from src/core/segment_sinks/segment_buffers_store.ts rename to src/core/segment_sinks/segment_sinks_store.ts index f469f323dc..11e63b92dc 100644 --- a/src/core/segment_sinks/segment_buffers_store.ts +++ b/src/core/segment_sinks/segment_sinks_store.ts @@ -103,7 +103,11 @@ export default class SegmentSinksStore { * disabled. This means that the corresponding type (e.g. audio, video etc.) * won't be needed when playing the current content. */ - private _initializedSegmentSinks: Partial>; + private _initializedSegmentSinks: { + audio?: AudioVideoSegmentSink | undefined | null; + video?: AudioVideoSegmentSink | undefined | null; + text?: TextSegmentSink | null; + }; /** * Callbacks called after a SourceBuffer is either created or disabled. @@ -308,7 +312,7 @@ export default class SegmentSinksStore { return memorizedSegmentSink; } - let segmentSink: SegmentSink; + let segmentSink: TextSegmentSink; if (bufferType === "text") { log.info("SB: Creating a new text SegmentSink"); if (this._textInterface === null) { diff --git a/src/main_thread/api/debug/buffer_graph.ts b/src/main_thread/api/debug/buffer_graph.ts index 0e7d098402..8967dd0957 100644 --- a/src/main_thread/api/debug/buffer_graph.ts +++ b/src/main_thread/api/debug/buffer_graph.ts @@ -1,4 +1,4 @@ -import type { IBufferedChunkSnapshot } from "../../../core/segment_sinks/segment_buffers_store"; +import type { IBufferedChunkSnapshot } from "../../../core/segment_sinks/segment_sinks_store"; import type { IRepresentationMetadata } from "../../../manifest"; const BUFFER_WIDTH_IN_SECONDS = 30 * 60; diff --git a/src/main_thread/api/debug/modules/segment_buffer_content.ts b/src/main_thread/api/debug/modules/segment_buffer_content.ts index b9984f0442..977f57471c 100644 --- a/src/main_thread/api/debug/modules/segment_buffer_content.ts +++ b/src/main_thread/api/debug/modules/segment_buffer_content.ts @@ -1,4 +1,4 @@ -import type { ISegmentSinkMetrics } from "../../../../core/segment_sinks/segment_buffers_store"; +import type { ISegmentSinkMetrics } from "../../../../core/segment_sinks/segment_sinks_store"; import type { IBufferType } from "../../../../core/types"; import type { IAdaptationMetadata, diff --git a/src/main_thread/api/public_api.ts b/src/main_thread/api/public_api.ts index 3568c73c25..e6ebd08610 100644 --- a/src/main_thread/api/public_api.ts +++ b/src/main_thread/api/public_api.ts @@ -33,7 +33,7 @@ import hasMseInWorker from "../../compat/has_mse_in_worker"; import hasWorkerApi from "../../compat/has_worker_api"; import isDebugModeEnabled from "../../compat/is_debug_mode_enabled"; import config from "../../config"; -import type { ISegmentSinkMetrics } from "../../core/segment_sinks/segment_buffers_store"; +import type { ISegmentSinkMetrics } from "../../core/segment_sinks/segment_sinks_store"; import type { IAdaptationChoice, IInbandEvent, diff --git a/src/main_thread/init/media_source_content_initializer.ts b/src/main_thread/init/media_source_content_initializer.ts index e2c004c065..562a71c53e 100644 --- a/src/main_thread/init/media_source_content_initializer.ts +++ b/src/main_thread/init/media_source_content_initializer.ts @@ -92,7 +92,7 @@ import listenToMediaError from "./utils/throw_on_media_error"; */ export default class MediaSourceContentInitializer extends ContentInitializer { /** Constructor settings associated to this `MediaSourceContentInitializer`. */ - private _settings: IInitializeArguments; + private _initSettings: IInitializeArguments; /** * `TaskCanceller` allowing to abort everything that the * `MediaSourceContentInitializer` is doing. @@ -145,7 +145,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer { */ constructor(settings: IInitializeArguments) { super(); - this._settings = settings; + this._initSettings = settings; this._initCanceller = new TaskCanceller(); this._manifest = null; this._decryptionCapabilities = { status: "uninitialized", value: null }; @@ -204,7 +204,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer { this._initCanceller.signal, ); - this._initializeMediaSourceAndDecryption(mediaElement) + this._setupInitialMediaSourceAndDecryption(mediaElement) .then((initResult) => this._onInitialMediaSourceReady( mediaElement, @@ -230,10 +230,18 @@ export default class MediaSourceContentInitializer extends ContentInitializer { this._manifestFetcher.updateContentUrls(urls, refreshNow); } + /** + * Stop content and free all resources linked to this + * `MediaSourceContentInitializer`. + */ public dispose(): void { this._initCanceller.cancel(); } + /** + * Callback called when an error interrupting playback arised. + * @param {*} err + */ private _onFatalError(err: unknown) { if (this._initCanceller.isUsed()) { return; @@ -242,14 +250,20 @@ export default class MediaSourceContentInitializer extends ContentInitializer { this.trigger("error", err); } - private _initializeMediaSourceAndDecryption(mediaElement: IMediaElement): Promise<{ + /** + * Initialize decryption mechanisms if needed and begin creating and relying + * on the initial `MediaSourceInterface` for this content. + * @param {HTMLMediaElement|null} mediaElement + * @returns {Promise.} + */ + private _setupInitialMediaSourceAndDecryption(mediaElement: IMediaElement): Promise<{ mediaSource: MainMediaSourceInterface; drmSystemId: string | undefined; unlinkMediaSource: TaskCanceller; }> { const initCanceller = this._initCanceller; return createCancellablePromise(initCanceller.signal, (resolve) => { - const { keySystems } = this._settings; + const { keySystems } = this._initSettings; /** Initialize decryption capabilities. */ const { statusRef: drmInitRef, contentDecryptor } = initializeContentDecryption( @@ -383,7 +397,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer { startAt, textTrackOptions, transport, - } = this._settings; + } = this._initSettings; const initCanceller = this._initCanceller; assert(this._manifest !== null); let manifest: IManifest; @@ -442,80 +456,76 @@ export default class MediaSourceContentInitializer extends ContentInitializer { return; } - const bufferOnMediaSource = this._startBufferingOnMediaSource.bind(this); - const triggerEvent = this.trigger.bind(this); - const onFatalError = this._onFatalError.bind(this); - // handle initial load and reloads - recursivelyLoadOnMediaSource( - initialMediaSource, - initialTime, - autoPlay, - initialMediaSourceCanceller, - ); - - /** - * Load the content defined by the Manifest in the mediaSource given at the - * given position and playing status. - * This function recursively re-call itself when a MediaSource reload is - * wanted. - * @param {MediaSource} mediaSource - * @param {number} startingPos - * @param {Object} currentCanceller - * @param {boolean} shouldPlay - */ - function recursivelyLoadOnMediaSource( - mediaSource: MainMediaSourceInterface, - startingPos: number, - shouldPlay: boolean, - currentCanceller: TaskCanceller, - ): void { - const opts = { + this._setupContentWithNewMediaSource( + { mediaElement, playbackObserver, - mediaSource, - initialTime: startingPos, - autoPlay: shouldPlay, + mediaSource: initialMediaSource, + initialTime, + autoPlay, manifest, representationEstimator, segmentQueueCreator, speed, bufferOptions: subBufferOptions, - }; - bufferOnMediaSource(opts, onReloadMediaSource, currentCanceller.signal); - - function onReloadMediaSource(reloadOrder: { - position: number; - autoPlay: boolean; - }): void { - currentCanceller.cancel(); - if (initCanceller.isUsed()) { - return; - } - triggerEvent("reloadingMediaSource", reloadOrder); - if (initCanceller.isUsed()) { - return; - } + }, + initialMediaSourceCanceller, + ); + } - const newCanceller = new TaskCanceller(); - newCanceller.linkToSignal(initCanceller.signal); - createMediaSource(mediaElement, newCanceller.signal) - .then((newMediaSource) => { - recursivelyLoadOnMediaSource( - newMediaSource, - reloadOrder.position, - reloadOrder.autoPlay, - newCanceller, - ); - }) - .catch((err) => { - if (newCanceller.isUsed()) { - return; - } - onFatalError(err); - }); + /** + * Load the content defined by the Manifest in the mediaSource given at the + * given position and playing status. + * This function recursively re-call itself when a MediaSource reload is + * wanted. + * @param {Object} args + * @param {Object} currentCanceller + */ + private _setupContentWithNewMediaSource( + args: IBufferingMediaSettings, + currentCanceller: TaskCanceller, + ): void { + const initCanceller = this._initCanceller; + const onReloadMediaSource: IReloadMediaSourceCallback = (reloadOrder: { + position: number; + autoPlay: boolean; + }): void => { + currentCanceller.cancel(); + if (initCanceller.isUsed()) { + return; } - } + this.trigger("reloadingMediaSource", reloadOrder); + if (initCanceller.isUsed()) { + return; + } + + const newCanceller = new TaskCanceller(); + newCanceller.linkToSignal(initCanceller.signal); + createMediaSource(args.mediaElement, newCanceller.signal) + .then((newMediaSource) => { + this._setupContentWithNewMediaSource( + { + ...args, + mediaSource: newMediaSource, + initialTime: reloadOrder.position, + autoPlay: reloadOrder.autoPlay, + }, + newCanceller, + ); + }) + .catch((err) => { + if (newCanceller.isUsed()) { + return; + } + this._onFatalError(err); + }); + }; + this._startLoadingContentOnMediaSource( + args, + onReloadMediaSource, + currentCanceller.signal, + ); } /** @@ -524,9 +534,9 @@ export default class MediaSourceContentInitializer extends ContentInitializer { * @param {function} onReloadOrder * @param {Object} cancelSignal */ - private _startBufferingOnMediaSource( + private _startLoadingContentOnMediaSource( args: IBufferingMediaSettings, - onReloadOrder: (reloadOrder: { position: number; autoPlay: boolean }) => void, + onReloadOrder: IReloadMediaSourceCallback, cancelSignal: CancellationSignal, ): void { const { @@ -553,18 +563,10 @@ export default class MediaSourceContentInitializer extends ContentInitializer { } let textDisplayerInterface: ITextDisplayerInterface | null = null; - let textDisplayer: ITextDisplayer | null = null; - if ( - this._settings.textTrackOptions.textTrackMode === "html" && - features.htmlTextDisplayer !== null - ) { - textDisplayer = new features.htmlTextDisplayer( - mediaElement, - this._settings.textTrackOptions.textTrackElement, - ); - } else if (features.nativeTextDisplayer !== null) { - textDisplayer = new features.nativeTextDisplayer(mediaElement); - } + const textDisplayer = createTextDisplayer( + mediaElement, + this._initSettings.textTrackOptions, + ); if (textDisplayer !== null) { const sender = new MainThreadTextDisplayerInterface(textDisplayer); textDisplayerInterface = sender; @@ -1123,6 +1125,21 @@ export default class MediaSourceContentInitializer extends ContentInitializer { } } +function createTextDisplayer( + mediaElement: IMediaElement, + textTrackOptions: ITextDisplayerOptions, +): ITextDisplayer | null { + if (textTrackOptions.textTrackMode === "html" && features.htmlTextDisplayer !== null) { + return new features.htmlTextDisplayer( + mediaElement, + textTrackOptions.textTrackElement, + ); + } else if (features.nativeTextDisplayer !== null) { + return new features.nativeTextDisplayer(mediaElement); + } + return null; +} + /** Arguments to give to the `InitializeOnMediaSource` function. */ export interface IInitializeArguments { /** Options concerning the ABR logic. */ @@ -1324,3 +1341,19 @@ function blackListProtectionDataOnManifest( return rep.decipherable; }); } + +/** + * Function to call when you want to "reload" the MediaSource: basically + * restarting playback on a new MediaSource for the same content (it may + * be for varied reasons, such as ensuring data buffers are empty, or + * restarting after some kind of fatal error). + * @param {Object} reloadOrder + * @param {number} reloadOrder.position - Position in seconds at which we + * should restart from when playback restarts. + * @param {boolean} reloadOrder.autoPlay - If `true` we will directly play + * once enough data is re-loaded. + */ +type IReloadMediaSourceCallback = (reloadOrder: { + position: number; + autoPlay: boolean; +}) => void; diff --git a/src/main_thread/init/multi_thread_content_initializer.ts b/src/main_thread/init/multi_thread_content_initializer.ts index db2c2806a9..0223668d2d 100644 --- a/src/main_thread/init/multi_thread_content_initializer.ts +++ b/src/main_thread/init/multi_thread_content_initializer.ts @@ -1,7 +1,7 @@ import type { IMediaElement } from "../../compat/browser_compatibility_types"; import mayMediaElementFailOnUndecipherableData from "../../compat/may_media_element_fail_on_undecipherable_data"; import shouldReloadMediaSourceOnDecipherabilityUpdate from "../../compat/should_reload_media_source_on_decipherability_update"; -import type { ISegmentSinkMetrics } from "../../core/segment_sinks/segment_buffers_store"; +import type { ISegmentSinkMetrics } from "../../core/segment_sinks/segment_sinks_store"; import type { IAdaptiveRepresentationSelectorArguments, IAdaptationChoice, diff --git a/src/main_thread/init/types.ts b/src/main_thread/init/types.ts index c0911b5b84..46e5a25dbc 100644 --- a/src/main_thread/init/types.ts +++ b/src/main_thread/init/types.ts @@ -15,7 +15,7 @@ */ import type { IMediaElement } from "../../compat/browser_compatibility_types"; -import type { ISegmentSinkMetrics } from "../../core/segment_sinks/segment_buffers_store"; +import type { ISegmentSinkMetrics } from "../../core/segment_sinks/segment_sinks_store"; import type { IBufferType, IAdaptationChoice, IInbandEvent } from "../../core/types"; import type { IPeriodsUpdateResult, diff --git a/src/main_thread/text_displayer/native/native_text_displayer.ts b/src/main_thread/text_displayer/native/native_text_displayer.ts index 678acd2b56..b768f62cdf 100644 --- a/src/main_thread/text_displayer/native/native_text_displayer.ts +++ b/src/main_thread/text_displayer/native/native_text_displayer.ts @@ -158,24 +158,7 @@ export default class NativeTextDisplayer implements ITextDisplayer { public reset(): void { log.debug("NTD: Aborting NativeTextDisplayer"); this._removeData(0, Infinity); - const { _trackElement, _videoElement } = this; - - if (_trackElement !== undefined && _videoElement.hasChildNodes()) { - try { - _videoElement.removeChild(_trackElement); - } catch (_e) { - log.warn("NTD: Can't remove track element from the video"); - } - } - - // Ugly trick to work-around browser bugs by refreshing its mode - const oldMode = this._track.mode; - this._track.mode = "disabled"; - this._track.mode = oldMode; - - if (this._trackElement !== undefined) { - this._trackElement.innerHTML = ""; - } + this._clearTrackElement(); } public stop(): void { @@ -213,6 +196,27 @@ export default class NativeTextDisplayer implements ITextDisplayer { } this._buffered.remove(start, end); } + + private _clearTrackElement(): void { + const { _trackElement, _videoElement } = this; + + if (_trackElement !== undefined && _videoElement.hasChildNodes()) { + try { + _videoElement.removeChild(_trackElement); + } catch (_e) { + log.warn("NTD: Can't remove track element from the video"); + } + } + + // Ugly trick to work-around browser bugs by refreshing its mode + const oldMode = this._track.mode; + this._track.mode = "disabled"; + this._track.mode = oldMode; + + if (this._trackElement !== undefined) { + this._trackElement.innerHTML = ""; + } + } } /** Data of chunks that should be pushed to the NativeTextDisplayer. */ diff --git a/src/mse/main_media_source_interface.ts b/src/mse/main_media_source_interface.ts index bf4773e417..6df253db20 100644 --- a/src/mse/main_media_source_interface.ts +++ b/src/mse/main_media_source_interface.ts @@ -212,52 +212,8 @@ export class MainSourceBufferInterface implements ISourceBufferInterface { this._operationQueue = []; this._currentOperations = []; - const onError = (evt: Event) => { - let error: Error; - if ((evt as unknown as Error) instanceof Error) { - error = evt as unknown as Error; - } else if ((evt as unknown as { error: Error }).error instanceof Error) { - error = (evt as unknown as { error: Error }).error; - } else { - error = new Error("Unknown SourceBuffer Error"); - } - const currentOps = this._currentOperations; - this._currentOperations = []; - if (currentOps.length === 0) { - log.error("SBI: error for an unknown operation", error); - } else { - const rejected = new SourceBufferError( - error.name, - error.message, - error.name === "QuotaExceededError", - ); - for (const op of currentOps) { - op.reject(rejected); - } - } - }; - const onUpdateEnd = () => { - const currentOps = this._currentOperations; - this._currentOperations = []; - try { - for (const op of currentOps) { - op.resolve(convertToRanges(this._sourceBuffer.buffered)); - } - } catch (err) { - for (const op of currentOps) { - if (err instanceof Error && err.name === "InvalidStateError") { - // Most likely the SourceBuffer just has been removed from the - // `MediaSource`. - // Just return an empty buffered range. - op.resolve([]); - } else { - op.reject(err); - } - } - } - this._performNextOperation(); - }; - + const onError = this._onError.bind(this); + const onUpdateEnd = this._onUpdateEnd.bind(this); sourceBuffer.addEventListener("updateend", onUpdateEnd); sourceBuffer.addEventListener("error", onError); this._canceller.signal.register(() => { @@ -325,6 +281,53 @@ export class MainSourceBufferInterface implements ISourceBufferInterface { this._emptyCurrentQueue(); } + private _onError(evt: Event) { + let error: Error; + if ((evt as unknown as Error) instanceof Error) { + error = evt as unknown as Error; + } else if ((evt as unknown as { error: Error }).error instanceof Error) { + error = (evt as unknown as { error: Error }).error; + } else { + error = new Error("Unknown SourceBuffer Error"); + } + const currentOps = this._currentOperations; + this._currentOperations = []; + if (currentOps.length === 0) { + log.error("SBI: error for an unknown operation", error); + } else { + const rejected = new SourceBufferError( + error.name, + error.message, + error.name === "QuotaExceededError", + ); + for (const op of currentOps) { + op.reject(rejected); + } + } + } + + private _onUpdateEnd() { + const currentOps = this._currentOperations; + this._currentOperations = []; + try { + for (const op of currentOps) { + op.resolve(convertToRanges(this._sourceBuffer.buffered)); + } + } catch (err) { + for (const op of currentOps) { + if (err instanceof Error && err.name === "InvalidStateError") { + // Most likely the SourceBuffer just has been removed from the + // `MediaSource`. + // Just return an empty buffered range. + op.resolve([]); + } else { + op.reject(err); + } + } + } + this._performNextOperation(); + } + private _emptyCurrentQueue(): void { const error = new CancellationError(); if (this._currentOperations.length > 0) { diff --git a/src/multithread_types.ts b/src/multithread_types.ts index d8bd6b19b6..bdd44b0771 100644 --- a/src/multithread_types.ts +++ b/src/multithread_types.ts @@ -4,7 +4,7 @@ * multithread situation. */ -import type { ISegmentSinkMetrics } from "./core/segment_sinks/segment_buffers_store"; +import type { ISegmentSinkMetrics } from "./core/segment_sinks/segment_sinks_store"; import type { IResolutionInfo, IManifestFetcherSettings, diff --git a/src/utils/sync_or_async.ts b/src/utils/sync_or_async.ts index 856570e454..c0e0ab123d 100644 --- a/src/utils/sync_or_async.ts +++ b/src/utils/sync_or_async.ts @@ -78,12 +78,14 @@ const SyncOrAsync = { * @returns {Object} */ createAsync(val: Promise): ISyncOrAsyncValue { - let ret = null; - val.then((resolved) => { + let ret: T | null = null; + val.then((resolved: T) => { ret = resolved; }, noop); return { - syncValue: ret, + get syncValue(): T | null { + return ret; + }, getValueAsAsync() { return val; },