From 47d9f6321d0cd5044ab39eb075f15c61e5427733 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Mon, 13 Jun 2022 15:53:41 +0200 Subject: [PATCH] Remove RxJS from the HTMLTextSegmentBuffer --- src/compat/event_listeners.ts | 87 ++++++++++++- .../text/html/html_text_segment_buffer.ts | 123 +++++++++--------- 2 files changed, 148 insertions(+), 62 deletions(-) diff --git a/src/compat/event_listeners.ts b/src/compat/event_listeners.ts index c50f1e8144f..6b87a0f5860 100644 --- a/src/compat/event_listeners.ts +++ b/src/compat/event_listeners.ts @@ -107,6 +107,69 @@ export type IEventTargetLike = HTMLElement | IEventEmitterLike | IEventEmitter; +/** + * Returns a function allowing to add event listeners for particular event(s) + * optionally automatically adding browser prefixes if needed. + * @param {Array.} eventNames - The event(s) to listen to. If multiple + * events are set, the event listener will be triggered when any of them emits. + * @returns {Function} - Returns function allowing to easily add a callback to + * be triggered when that event is emitted on a given event target. + */ +function createCompatibleEventListener( + eventNames : string[] +) : + ( + element : IEventTargetLike, + listener : (event? : unknown) => void, + cancelSignal: CancellationSignal + ) => void +{ + let mem : string|undefined; + const prefixedEvents = eventPrefixed(eventNames); + + return ( + element : IEventTargetLike, + listener: (event? : unknown) => void, + cancelSignal: CancellationSignal + ) => { + if (cancelSignal.isCancelled) { + return; + } + + // if the element is a HTMLElement we can detect + // the supported event, and memoize it in `mem` + if (element instanceof HTMLElement) { + if (typeof mem === "undefined") { + mem = findSupportedEvent(element, prefixedEvents); + } + + if (isNonEmptyString(mem)) { + element.addEventListener(mem, listener); + cancelSignal.register(() => { + if (mem !== undefined) { + element.removeEventListener(mem, listener); + } + }); + } else { + if (__ENVIRONMENT__.CURRENT_ENV === __ENVIRONMENT__.DEV as number) { + log.warn(`compat: element ${element.tagName}` + + " does not support any of these events: " + + prefixedEvents.join(", ")); + } + return ; + } + } + + prefixedEvents.forEach(eventName => { + (element as IEventEmitterLike).addEventListener(eventName, listener); + cancelSignal.register(() => { + (element as IEventEmitterLike).removeEventListener(eventName, listener); + }); + }); + }; + +} + /** * @param {Array.} eventNames * @param {Array.|undefined} prefixes @@ -245,7 +308,8 @@ export interface IPictureInPictureEvent { /** * Emit when video enters and leaves Picture-In-Picture mode. - * @param {HTMLMediaElement} mediaElement + * @param {HTMLMediaElement} elt + * @param {Object} stopListening * @returns {Observable} */ function getPictureOnPictureStateRef( @@ -508,6 +572,24 @@ const onKeyError$ = compatibleListener(["keyerror", "error"]); */ const onKeyStatusesChange$ = compatibleListener(["keystatuseschange"]); +/** + * @param {HTMLMediaElement} mediaElement + * @returns {Observable} + */ +const onSeeking = createCompatibleEventListener(["seeking"]); + +/** + * @param {HTMLMediaElement} mediaElement + * @returns {Observable} + */ +const onSeeked = createCompatibleEventListener(["seeked"]); + +/** + * @param {HTMLMediaElement} mediaElement + * @returns {Observable} + */ +const onEnded = createCompatibleEventListener(["ended"]); + /** * Utilitary function allowing to add an event listener and remove it * automatically once the given `CancellationSignal` emits. @@ -537,6 +619,7 @@ export { getVideoVisibilityRef, getVideoWidthRef, onEncrypted$, + onEnded, onEnded$, onFullscreenChange$, onKeyAdded$, @@ -546,7 +629,9 @@ export { onLoadedMetadata$, onPlayPause$, onRemoveSourceBuffers$, + onSeeked, onSeeked$, + onSeeking, onSeeking$, onSourceClose$, onSourceEnded$, diff --git a/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts b/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts index 5568b9cd4ac..dfc28dd7913 100644 --- a/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts +++ b/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts @@ -14,17 +14,6 @@ * limitations under the License. */ -import { - concat as observableConcat, - interval as observableInterval, - map, - merge as observableMerge, - Observable, - of as observableOf, - startWith, - switchMap, - takeUntil, -} from "rxjs"; import { events, onHeightWidthChange, @@ -32,7 +21,9 @@ import { import config from "../../../../../config"; import log from "../../../../../log"; import { ITextTrackSegmentData } from "../../../../../transports"; -import TaskCanceller from "../../../../../utils/task_canceller"; +import TaskCanceller, { + CancellationSignal, +} from "../../../../../utils/task_canceller"; import { IEndOfSegmentInfos, IPushChunkInfos, @@ -43,31 +34,7 @@ import parseTextTrackToElements from "./parsers"; import TextTrackCuesStore from "./text_track_cues_store"; import updateProportionalElements from "./update_proportional_elements"; -const { onEnded$, - onSeeked$, - onSeeking$ } = events; - - -/** - * Generate the interval at which TextTrack HTML Cues should be refreshed. - * @param {HTMLMediaElement} videoElement - * @returns {Observable} - */ -function generateRefreshInterval(videoElement : HTMLMediaElement) : Observable { - const seeking$ = onSeeking$(videoElement); - const seeked$ = onSeeked$(videoElement); - const ended$ = onEnded$(videoElement); - const { MAXIMUM_HTML_TEXT_TRACK_UPDATE_INTERVAL } = config.getCurrent(); - const manualRefresh$ = observableMerge(seeked$, ended$); - const autoRefresh$ = observableInterval(MAXIMUM_HTML_TEXT_TRACK_UPDATE_INTERVAL) - .pipe(startWith(null)); - - return manualRefresh$.pipe( - startWith(null), - switchMap(() => observableConcat(autoRefresh$.pipe(map(() => true), - takeUntil(seeking$)), - observableOf(false)))); -} +const { onEnded, onSeeked, onSeeking } = events; /** * @param {Element} element @@ -167,28 +134,7 @@ export default class HTMLTextSegmentBuffer extends SegmentBuffer { this._buffer = new TextTrackCuesStore(); this._currentCues = []; - // update text tracks - const refreshSub = generateRefreshInterval(this._videoElement) - .subscribe((shouldDisplay) => { - if (!shouldDisplay) { - this._disableCurrentCues(); - return; - } - const { MAXIMUM_HTML_TEXT_TRACK_UPDATE_INTERVAL } = config.getCurrent(); - // to spread the time error, we divide the regular chosen interval. - const time = Math.max(this._videoElement.currentTime + - (MAXIMUM_HTML_TEXT_TRACK_UPDATE_INTERVAL / 1000) / 2, - 0); - const cues = this._buffer.get(time); - if (cues.length === 0) { - this._disableCurrentCues(); - } else { - this._displayCues(cues); - } - }); - this._canceller.signal.register(() => { - refreshSub.unsubscribe(); - }); + this.autoRefreshSubtitles(this._canceller.signal); } /** @@ -253,7 +199,7 @@ export default class HTMLTextSegmentBuffer extends SegmentBuffer { * * /!\ This method won't add any data to the linked inventory. * Please use the `pushChunk` method for most use-cases. - * @param {Object} data + * @param {Object} infos * @returns {boolean} */ public pushChunkSync(infos : IPushChunkInfos) : void { @@ -375,7 +321,7 @@ export default class HTMLTextSegmentBuffer extends SegmentBuffer { /** * Display a new Cue. If one was already present, it will be replaced. - * @param {HTMLElement} element + * @param {HTMLElement} elements */ private _displayCues(elements : HTMLElement[]) : void { const nothingChanged = this._currentCues.length === elements.length && @@ -422,6 +368,61 @@ export default class HTMLTextSegmentBuffer extends SegmentBuffer { emitCurrentValue: true }); } } + + /** + * Auto-refresh the display of subtitles according to the media element's + * position and events. + * @param {Object} cancellationSignal + */ + private autoRefreshSubtitles( + cancellationSignal : CancellationSignal + ) : void { + let autoRefreshCanceller : TaskCanceller | null = null; + const { MAXIMUM_HTML_TEXT_TRACK_UPDATE_INTERVAL } = config.getCurrent(); + + const startAutoRefresh = () => { + stopAutoRefresh(); + autoRefreshCanceller = new TaskCanceller({ cancelOn: cancellationSignal }); + const intervalId = setInterval(() => this.refreshSubtitles(), + MAXIMUM_HTML_TEXT_TRACK_UPDATE_INTERVAL); + autoRefreshCanceller.signal.register(() => { + clearInterval(intervalId); + }); + this.refreshSubtitles(); + }; + + onSeeking(this._videoElement, () => { + stopAutoRefresh(); + this._disableCurrentCues(); + }, cancellationSignal); + onSeeked(this._videoElement, startAutoRefresh, cancellationSignal); + onEnded(this._videoElement, startAutoRefresh, cancellationSignal); + + function stopAutoRefresh() { + if (autoRefreshCanceller !== null) { + autoRefreshCanceller.cancel(); + autoRefreshCanceller = null; + } + } + } + + /** + * Refresh current subtitles according to the current media element's + * position. + */ + private refreshSubtitles() : void { + const { MAXIMUM_HTML_TEXT_TRACK_UPDATE_INTERVAL } = config.getCurrent(); + // to spread the time error, we divide the regular chosen interval. + const time = Math.max(this._videoElement.currentTime + + (MAXIMUM_HTML_TEXT_TRACK_UPDATE_INTERVAL / 1000) / 2, + 0); + const cues = this._buffer.get(time); + if (cues.length === 0) { + this._disableCurrentCues(); + } else { + this._displayCues(cues); + } + } } /** Data of chunks that should be pushed to the NativeTextSegmentBuffer. */