rx-player
Version:
Canal+ HTML5 Video Player
239 lines (222 loc) • 7.56 kB
text/typescript
import log from "../../../../log";
import type { ITextDisplayer } from "../../../../main_thread/types";
import type { ITextTrackSegmentData } from "../../../../transports";
import getMonotonicTimeStamp from "../../../../utils/monotonic_timestamp";
import type { IRange } from "../../../../utils/ranges";
import type { ICompleteSegmentInfo, IPushChunkInfos, ISBOperation } from "../types";
import { SegmentSink, SegmentSinkOperation } from "../types";
/**
* SegmentSink implementation to add text data, most likely subtitles.
* @class TextSegmentSink
*/
export default class TextSegmentSink extends SegmentSink {
readonly bufferType: "text";
private _sender: ITextDisplayerInterface;
private _pendingOperations: Array<{
operation: ISBOperation<unknown>;
promise: Promise<unknown>;
}>;
/**
* @param {Object} textDisplayerSender
*/
constructor(textDisplayerSender: ITextDisplayerInterface) {
log.debug("HTSB: Creating TextSegmentSink");
super();
this.bufferType = "text";
this._sender = textDisplayerSender;
this._pendingOperations = [];
this._sender.reset();
}
/**
* @param {string} uniqueId
*/
public declareInitSegment(uniqueId: string): void {
log.warn("HTSB: Declaring initialization segment for Text SegmentSink", uniqueId);
}
/**
* @param {string} uniqueId
*/
public freeInitSegment(uniqueId: string): void {
log.warn("HTSB: Freeing initialization segment for Text SegmentSink", uniqueId);
}
/**
* Push text segment to the TextSegmentSink.
* @param {Object} infos
* @returns {Promise}
*/
public async pushChunk(infos: IPushChunkInfos<unknown>): Promise<IRange[]> {
const { data } = infos;
assertChunkIsTextTrackSegmentData(data.chunk);
// Needed for TypeScript :(
const promise = this._sender.pushTextData({
...data,
chunk: data.chunk,
});
this._addToOperationQueue(promise, {
type: SegmentSinkOperation.Push,
value: infos,
});
const ranges = await promise;
if (infos.inventoryInfos !== null) {
this._segmentInventory.insertChunk(
infos.inventoryInfos,
true,
getMonotonicTimeStamp(),
);
}
this._segmentInventory.synchronizeBuffered(ranges);
return ranges;
}
/**
* Remove buffered data.
* @param {number} start - start position, in seconds
* @param {number} end - end position, in seconds
* @returns {Promise}
*/
public async removeBuffer(start: number, end: number): Promise<IRange[]> {
const promise = this._sender.remove(start, end);
this._addToOperationQueue(promise, {
type: SegmentSinkOperation.Remove,
value: { start, end },
});
const ranges = await promise;
this._segmentInventory.synchronizeBuffered(ranges);
return ranges;
}
/**
* @param {Object} infos
* @returns {Promise}
*/
public async signalSegmentComplete(infos: ICompleteSegmentInfo): Promise<void> {
if (this._pendingOperations.length > 0) {
// Only validate after preceding operation
const { promise } = this._pendingOperations[this._pendingOperations.length - 1];
this._addToOperationQueue(promise, {
type: SegmentSinkOperation.SignalSegmentComplete,
value: infos,
});
try {
await promise;
} catch (_) {
// We don't really care of what happens of the preceding operation here
}
}
this._segmentInventory.completeSegment(infos);
}
/**
* @returns {Array.<Object>}
*/
public getPendingOperations(): Array<ISBOperation<unknown>> {
return this._pendingOperations.map((p) => p.operation);
}
public dispose(): void {
log.debug("HTSB: Disposing TextSegmentSink");
this._sender.reset();
}
private _addToOperationQueue(
promise: Promise<unknown>,
operation: ISBOperation<unknown>,
): void {
const queueObject = { operation, promise };
this._pendingOperations.push(queueObject);
const endOperation = () => {
const indexOf = this._pendingOperations.indexOf(queueObject);
if (indexOf >= 0) {
this._pendingOperations.splice(indexOf, 1);
}
};
promise.then(endOperation, endOperation); // `finally` not supported everywhere
}
}
/** Data of chunks that should be pushed to the HTMLTextSegmentSink. */
export interface ITextTracksBufferSegmentData {
/** 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;
}
/**
* Throw if the given input is not in the expected format.
* Allows to enforce runtime type-checking as compile-time type-checking here is
* difficult to enforce.
* @param {Object} chunk
*/
function assertChunkIsTextTrackSegmentData(
chunk: unknown,
): asserts chunk is ITextTracksBufferSegmentData {
if (
(__ENVIRONMENT__.CURRENT_ENV as number) === (__ENVIRONMENT__.PRODUCTION as number)
) {
return;
}
if (
typeof chunk !== "object" ||
chunk === null ||
typeof (chunk as ITextTracksBufferSegmentData).data !== "string" ||
typeof (chunk as ITextTracksBufferSegmentData).type !== "string" ||
((chunk as ITextTracksBufferSegmentData).language !== undefined &&
typeof (chunk as ITextTracksBufferSegmentData).language !== "string") ||
((chunk as ITextTracksBufferSegmentData).start !== undefined &&
typeof (chunk as ITextTracksBufferSegmentData).start !== "number") ||
((chunk as ITextTracksBufferSegmentData).end !== undefined &&
typeof (chunk as ITextTracksBufferSegmentData).end !== "number")
) {
throw new Error("Invalid format given to a TextSegmentSink");
}
}
/**
* Abstraction over an `ITextDisplayer`, making parts of its initial API
* returning a result asynchronously, to allow a common interface for when
* the `ITextDisplayerInterface` runs in a main thread or in a WebWorker
* (considering that an `ITextDisplayer` always run in main thread).
*/
export interface ITextDisplayerInterface {
/**
* @see ITextDisplayer
*/
pushTextData(
...args: Parameters<ITextDisplayer["pushTextData"]>
): Promise<ReturnType<ITextDisplayer["pushTextData"]>>;
/**
* @see ITextDisplayer
*/
remove(
...args: Parameters<ITextDisplayer["removeBuffer"]>
): Promise<ReturnType<ITextDisplayer["removeBuffer"]>>;
/**
* @see ITextDisplayer
*/
reset(): void;
/**
* @see ITextDisplayer
*/
stop(): void;
}
/*
* The following ugly code is here to provide a compile-time check that an
* `ITextTracksBufferSegmentData` (type of data pushed to a
* `TextSegmentSink`) 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: unused function for type checking
function _checkType(input: ITextTrackSegmentData): void {
function checkEqual(_arg: ITextTracksBufferSegmentData): void {
/* nothing */
}
checkEqual(input);
}
}