refactor: vendor content as regular files (remove git submodules) so CI builds don't need GitHub auth
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user