rx-player
Version:
Canal+ HTML5 Video Player
258 lines (228 loc) • 8.27 kB
text/typescript
import addTextTrack from "../../../compat/add_text_track";
import type {
ICompatTextTrack,
IMediaElement,
} from "../../../compat/browser_compatibility_types";
import removeCue from "../../../compat/remove_cue";
import log from "../../../log";
import type { ITextTrackSegmentData } from "../../../transports";
import type { IRange } from "../../../utils/ranges";
import { convertToRanges } from "../../../utils/ranges";
import ManualTimeRanges from "../manual_time_ranges";
import type { ITextDisplayer, ITextDisplayerData } from "../types";
import parseTextTrackToCues from "./native_parsers";
/**
* Implementation of an `ITextDisplayer` for "native" text tracks.
* "Native" text tracks rely on a `<track>` HTMLElement and its associated
* expected behavior to display subtitles synchronized to the video.
* @class NativeTextDisplayer
*/
export default class NativeTextDisplayer implements ITextDisplayer {
private readonly _videoElement: IMediaElement;
private readonly _track: ICompatTextTrack;
private readonly _trackElement: HTMLTrackElement | undefined;
private _buffered: ManualTimeRanges;
/**
* @param {HTMLMediaElement} videoElement
*/
constructor(videoElement: IMediaElement) {
log.debug("NTD: Creating NativeTextDisplayer");
const { track, trackElement } = addTextTrack(videoElement);
this._buffered = new ManualTimeRanges();
this._videoElement = videoElement;
this._track = track;
this._trackElement = trackElement;
}
/**
* Push text segment to the NativeTextDisplayer.
* @param {Object} infos
* @returns {Object}
*/
public pushTextData(infos: ITextDisplayerData): IRange[] {
log.debug("NTD: Appending new native text tracks");
if (infos.chunk === null) {
return convertToRanges(this._buffered);
}
const { timestampOffset, appendWindow, chunk } = infos;
const { start: startTime, end: endTime, data: dataString, type, language } = chunk;
const appendWindowStart = appendWindow[0] ?? 0;
const appendWindowEnd = appendWindow[1] ?? Infinity;
const cues = parseTextTrackToCues(type, dataString, timestampOffset, language);
if (appendWindowStart !== 0 && appendWindowEnd !== Infinity) {
// Removing before window start
let i = 0;
while (i < cues.length && cues[i].endTime <= appendWindowStart) {
i++;
}
cues.splice(0, i);
i = 0;
while (i < cues.length && cues[i].startTime < appendWindowStart) {
cues[i].startTime = appendWindowStart;
i++;
}
// Removing after window end
i = cues.length - 1;
while (i >= 0 && cues[i].startTime >= appendWindowEnd) {
i--;
}
cues.splice(i, cues.length);
i = cues.length - 1;
while (i >= 0 && cues[i].endTime > appendWindowEnd) {
cues[i].endTime = appendWindowEnd;
i--;
}
}
let start: number;
if (startTime !== undefined) {
start = Math.max(appendWindowStart, startTime);
} else {
if (cues.length <= 0) {
log.warn("NTD: Current text tracks have no cues nor start time. Aborting");
return convertToRanges(this._buffered);
}
log.warn("NTD: No start time given. Guessing from cues.");
start = cues[0].startTime;
}
let end: number;
if (endTime !== undefined) {
end = Math.min(appendWindowEnd, endTime);
} else {
if (cues.length <= 0) {
log.warn("NTD: Current text tracks have no cues nor end time. Aborting");
return convertToRanges(this._buffered);
}
log.warn("NTD: No end time given. Guessing from cues.");
end = cues[cues.length - 1].endTime;
}
if (end <= start) {
log.warn(
"NTD: Invalid text track appended: ",
"the start time is inferior or equal to the end time.",
);
return convertToRanges(this._buffered);
}
if (cues.length > 0) {
const firstCue = cues[0];
// NOTE(compat): cleanup all current cues if the newly added
// ones are in the past. this is supposed to fix an issue on
// IE/Edge.
// TODO Move to compat
const currentCues = this._track.cues;
if (currentCues !== null && currentCues.length > 0) {
if (firstCue.startTime < currentCues[currentCues.length - 1].startTime) {
this._removeData(firstCue.startTime, +Infinity);
}
}
for (const cue of cues) {
this._track.addCue(cue);
}
}
this._buffered.insert(start, end);
return convertToRanges(this._buffered);
}
/**
* Remove buffered data.
* @param {number} start - start position, in seconds
* @param {number} end - end position, in seconds
* @returns {Object}
*/
public removeBuffer(start: number, end: number): IRange[] {
this._removeData(start, end);
return convertToRanges(this._buffered);
}
/**
* Returns the currently buffered data, in a TimeRanges object.
* @returns {Array.<Object>}
*/
public getBufferedRanges(): IRange[] {
return convertToRanges(this._buffered);
}
public reset(): void {
log.debug("NTD: Aborting NativeTextDisplayer");
this._removeData(0, Infinity);
this._clearTrackElement();
}
public stop(): 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");
}
}
this._track.mode = "disabled";
if (this._trackElement !== undefined) {
this._trackElement.innerHTML = "";
}
}
private _removeData(start: number, end: number): void {
log.debug("NTD: Removing native text track data", start, end);
const track = this._track;
const cues = track.cues;
if (cues !== null) {
for (let i = cues.length - 1; i >= 0; i--) {
const cue = cues[i];
const { startTime, endTime } = cue;
if (startTime >= start && startTime <= end && endTime <= end) {
removeCue(track, cue);
}
}
}
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. */
export interface INativeTextTracksBufferSegmentData {
/** The text track data, in the format indicated in `type`. */
data: string;
/** The format of `data` (examples: "ttml", "srt" or "vtt") */
type: string;
/**
* Language in which the text track is, as a language code.
* This is mostly needed for "sami" subtitles, to know which cues can / should
* be parsed.
*/
language?: string | undefined;
/** start time from which the segment apply, in seconds. */
start?: number | undefined;
/** end time until which the segment apply, in seconds. */
end?: number | undefined;
}
/*
* The following ugly code is here to provide a compile-time check that an
* `INativeTextTracksBufferSegmentData` (type of data pushed to a
* `NativeTextDisplayer`) can be derived from a `ITextTrackSegmentData`
* (text track data parsed from a segment).
*
* It doesn't correspond at all to real code that will be called. This is just
* a hack to tell TypeScript to perform that check.
*/
if ((__ENVIRONMENT__.CURRENT_ENV as number) === (__ENVIRONMENT__.DEV as number)) {
// @ts-expect-error: uncalled function just for type checking
function _checkType(input: ITextTrackSegmentData): void {
function checkEqual(_arg: INativeTextTracksBufferSegmentData): void {
/* nothing */
}
checkEqual(input);
}
}