Skip to content

Commit

Permalink
Remove RxJS from the HTMLTextSegmentBuffer
Browse files Browse the repository at this point in the history
  • Loading branch information
peaBerberian committed Sep 1, 2022
1 parent 06ac38f commit c581e7a
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 62 deletions.
87 changes: 86 additions & 1 deletion src/compat/event_listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,69 @@ export type IEventTargetLike = HTMLElement |
IEventEmitterLike |
IEventEmitter<unknown>;

/**
* Returns a function allowing to add event listeners for particular event(s)
* optionally automatically adding browser prefixes if needed.
* @param {Array.<string>} 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.<string>} eventNames
* @param {Array.<string>|undefined} prefixes
Expand Down Expand Up @@ -247,7 +310,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(
Expand Down Expand Up @@ -505,6 +569,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.
Expand Down Expand Up @@ -534,6 +616,7 @@ export {
getVideoVisibilityRef,
getVideoWidthRef,
onEncrypted$,
onEnded,
onEnded$,
onFullscreenChange$,
onKeyAdded$,
Expand All @@ -542,7 +625,9 @@ export {
onKeyStatusesChange$,
onLoadedMetadata$,
onRemoveSourceBuffers$,
onSeeked,
onSeeked$,
onSeeking,
onSeeking$,
onSourceClose$,
onSourceEnded$,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,16 @@
* 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,
} from "../../../../../compat";
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,
Expand All @@ -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<boolean> {
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
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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<unknown>) : void {
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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. */
Expand Down

0 comments on commit c581e7a

Please sign in to comment.