199 lines
5.1 KiB
TypeScript
199 lines
5.1 KiB
TypeScript
import { Log } from "debug-level";
|
|
import { setTimeout } from "node:timers/promises";
|
|
import { Writable } from "node:stream";
|
|
import type { Packet } from "node-av";
|
|
|
|
export class BaseMediaStream extends Writable {
|
|
private _pts?: number;
|
|
private _syncTolerance = 20;
|
|
private _loggerSend: Log;
|
|
private _loggerSync: Log;
|
|
private _loggerSleep: Log;
|
|
|
|
private _noSleep: boolean;
|
|
private _startTime?: number;
|
|
private _startPts?: number;
|
|
private _sync = true;
|
|
private _syncStream?: BaseMediaStream;
|
|
|
|
constructor(type: string, noSleep = false) {
|
|
super({ objectMode: true, highWaterMark: 0 });
|
|
this._loggerSend = new Log(`stream:${type}:send`);
|
|
this._loggerSync = new Log(`stream:${type}:sync`);
|
|
this._loggerSleep = new Log(`stream:${type}:sleep`);
|
|
this._noSleep = noSleep;
|
|
}
|
|
|
|
get sync(): boolean {
|
|
return this._sync;
|
|
}
|
|
set sync(val: boolean) {
|
|
this._sync = val;
|
|
if (val) this._loggerSync.debug("Sync enabled");
|
|
else this._loggerSync.debug("Sync disabled");
|
|
}
|
|
get syncStream() {
|
|
return this._syncStream;
|
|
}
|
|
set syncStream(stream: BaseMediaStream | undefined) {
|
|
if (stream !== undefined && this === stream.syncStream)
|
|
throw new Error("Cannot sync 2 streams with eachother");
|
|
this._syncStream = stream;
|
|
}
|
|
get noSleep(): boolean {
|
|
return this._noSleep;
|
|
}
|
|
set noSleep(val: boolean) {
|
|
this._noSleep = val;
|
|
if (!val) this.resetTimingCompensation();
|
|
}
|
|
get pts(): number | undefined {
|
|
return this._pts;
|
|
}
|
|
get syncTolerance() {
|
|
return this._syncTolerance;
|
|
}
|
|
set syncTolerance(n: number) {
|
|
if (n < 0) return;
|
|
this._syncTolerance = n;
|
|
}
|
|
protected async _sendFrame(
|
|
_frame: Buffer,
|
|
_frametime: number,
|
|
): Promise<void> {
|
|
throw new Error("Not implemented");
|
|
}
|
|
private ptsDelta() {
|
|
if (this.pts !== undefined && this.syncStream?.pts !== undefined)
|
|
return this.pts - this.syncStream.pts;
|
|
return undefined;
|
|
}
|
|
private isAhead() {
|
|
const delta = this.ptsDelta();
|
|
return (
|
|
this.syncStream?.writableEnded === false &&
|
|
delta !== undefined &&
|
|
delta > this.syncTolerance
|
|
);
|
|
}
|
|
private isBehind() {
|
|
const delta = this.ptsDelta();
|
|
return (
|
|
this.syncStream?.writableEnded === false &&
|
|
delta !== undefined &&
|
|
delta < -this.syncTolerance
|
|
);
|
|
}
|
|
private resetTimingCompensation() {
|
|
this._startTime = this._startPts = undefined;
|
|
}
|
|
async _write(
|
|
frame: Packet,
|
|
_: BufferEncoding,
|
|
callback: (error?: Error | null) => void,
|
|
) {
|
|
const { data, pts, duration, timeBase } = frame;
|
|
if (!data) {
|
|
frame.free();
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
const frametime = (Number(duration) / timeBase.den) * timeBase.num * 1000;
|
|
|
|
const start_sendFrame = performance.now();
|
|
await this._sendFrame(Buffer.from(data), frametime);
|
|
const end_sendFrame = performance.now();
|
|
|
|
this._pts = (Number(pts) / timeBase.den) * timeBase.num * 1000;
|
|
this.emit("pts", this._pts);
|
|
|
|
const sendTime = end_sendFrame - start_sendFrame;
|
|
const ratio = sendTime / frametime;
|
|
this._loggerSend.debug(
|
|
{
|
|
stats: {
|
|
pts: this._pts,
|
|
frame_size: data.length,
|
|
duration: sendTime,
|
|
frametime,
|
|
},
|
|
},
|
|
`Frame sent in ${sendTime.toFixed(2)}ms (${(ratio * 100).toFixed(2)}% frametime)`,
|
|
);
|
|
if (ratio > 1) {
|
|
this._loggerSend.warn(
|
|
{
|
|
frame_size: data.length,
|
|
duration: sendTime,
|
|
frametime,
|
|
},
|
|
`Frame takes too long to send (${(ratio * 100).toFixed(2)}% frametime)`,
|
|
);
|
|
}
|
|
|
|
this._startTime ??= start_sendFrame;
|
|
this._startPts ??= this._pts;
|
|
const sleep = Math.max(
|
|
0,
|
|
this._pts -
|
|
this._startPts +
|
|
frametime -
|
|
(end_sendFrame - this._startTime),
|
|
);
|
|
if (this._noSleep || sleep === 0) {
|
|
callback(null);
|
|
} else if (this.sync && this.isBehind()) {
|
|
this._loggerSync.debug(
|
|
{
|
|
stats: {
|
|
pts: this.pts,
|
|
pts_other: this.syncStream?.pts,
|
|
},
|
|
},
|
|
"Stream is behind. Not sleeping for this frame",
|
|
);
|
|
this.resetTimingCompensation();
|
|
callback(null);
|
|
} else if (this.sync && this.isAhead()) {
|
|
do {
|
|
this._loggerSync.debug(
|
|
{
|
|
stats: {
|
|
pts: this.pts,
|
|
pts_other: this.syncStream?.pts,
|
|
frametime,
|
|
},
|
|
},
|
|
`Stream is ahead. Waiting for ${frametime}ms`,
|
|
);
|
|
await setTimeout(frametime);
|
|
} while (this.sync && this.isAhead());
|
|
this.resetTimingCompensation();
|
|
callback(null);
|
|
} else {
|
|
this._loggerSleep.debug(
|
|
{
|
|
stats: {
|
|
pts: this._pts,
|
|
startPts: this._startPts,
|
|
time: end_sendFrame,
|
|
startTime: this._startTime,
|
|
frametime,
|
|
},
|
|
},
|
|
`Sleeping for ${sleep}ms`,
|
|
);
|
|
setTimeout(sleep).then(() => callback(null));
|
|
}
|
|
frame.free();
|
|
}
|
|
_destroy(
|
|
error: Error | null,
|
|
callback: (error?: Error | null) => void,
|
|
): void {
|
|
super._destroy(error, callback);
|
|
this.syncStream = undefined;
|
|
}
|
|
}
|