perf(golive/spike): binding addTrack + TS port of @dank074 media stack
Phase 1 spike: replace @dank074/discord-video-stream + node-datachannel +
node-av (1.3GB) with minimal libdatachannel N-API binding + native RTP
packetizers (H264 FU-A, RTCP SR/NACK, pacer) + pure-TS GoLive stack.
Binding v0.4: addTrack (m=audio/video SDP), TrackWrap w/ setPacketizer +
sendFrame (raw RTP to transport) + addTimestamp — verified by two-peer
handshake emitting SDP with audio(opus 120)+video(H264 101) and 8-frame
RTP roundtrip.
TS layer (src/goLive/, 21 files): CodecPayloadType, VoiceOpCodes,
GatewayOpCodes, utils, BaseMediaConnection (voice WS + DAVE + heartbeat),
VoiceConnection, StreamConnection, Streamer, WebRtcWrapper (SDP mungling,
DAVE encrypt, packetizer chain), BaseMediaStream (pacing/sync), VideoStream,
AudioStream, Demuxer (ffmpeg-spawn NUT/AnnexB, no node-av 114M binary),
Encoders, prepareStream/playStream.
Integration: screenShareController.ts now imports from ../../goLive/index.js —
prepareStream(prepared, ...) + playStream(prepared, streamer, {...}).
Tests: tests/goLive-port.test.ts (8/8 pass). tsc --noEmit clean. biome clean.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* AnnexB bitstream reader/writer (RBSP + emulation prevention) — ported
|
||||
* from @dank074/discord-video-stream AnnexBBitstreamReaderWriter.js.
|
||||
*/
|
||||
|
||||
export class AnnexBBitstreamReader {
|
||||
private _buffer: Uint8Array;
|
||||
private _byteOffset = 0;
|
||||
private _bitOffset = 0;
|
||||
|
||||
constructor(buffer: Uint8Array) {
|
||||
this._buffer = buffer;
|
||||
}
|
||||
|
||||
readBits(count: number): number {
|
||||
if (count === 0) return 0;
|
||||
let result = 0;
|
||||
while (count > 0) {
|
||||
if (this._byteOffset >= this._buffer.length) {
|
||||
throw new Error("Bad byte offset");
|
||||
}
|
||||
if (
|
||||
this._bitOffset === 0 &&
|
||||
this._byteOffset >= 2 &&
|
||||
this._buffer[this._byteOffset - 2] === 0 &&
|
||||
this._buffer[this._byteOffset - 1] === 0 &&
|
||||
this._buffer[this._byteOffset] === 3
|
||||
) {
|
||||
// Skip over emulation prevention
|
||||
this._byteOffset++;
|
||||
}
|
||||
if (this._bitOffset === 0 && count >= 8) {
|
||||
result = (result << 8) | this._buffer[this._byteOffset++];
|
||||
count -= 8;
|
||||
} else {
|
||||
const numBitsToRead = Math.min(count, 8 - this._bitOffset);
|
||||
const mask = (1 << numBitsToRead) - 1;
|
||||
const newBits =
|
||||
(this._buffer[this._byteOffset] >>
|
||||
(8 - this._bitOffset - numBitsToRead)) &
|
||||
mask;
|
||||
result = (result << numBitsToRead) | newBits;
|
||||
count -= numBitsToRead;
|
||||
this._bitOffset += numBitsToRead;
|
||||
if (this._bitOffset === 8) {
|
||||
this._bitOffset = 0;
|
||||
this._byteOffset++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
readUnsigned(bits: number): number {
|
||||
return this.readBits(bits);
|
||||
}
|
||||
|
||||
readSigned(bits: number): number {
|
||||
const unsigned = this.readUnsigned(bits);
|
||||
if (unsigned & (1 << (bits - 1))) return unsigned - (1 << bits);
|
||||
return unsigned;
|
||||
}
|
||||
|
||||
readUnsignedExpGolomb(): number {
|
||||
let leading0 = 0;
|
||||
while (this.readBits(1) === 0) leading0++;
|
||||
return (1 << leading0) + this.readBits(leading0) - 1;
|
||||
}
|
||||
|
||||
readSignedExpGolomb(): number {
|
||||
const unsigned = this.readUnsignedExpGolomb();
|
||||
if (unsigned % 2 === 0) return unsigned / -2;
|
||||
return (unsigned + 1) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
export class AnnexBBitstreamWriter {
|
||||
private _arr: number[] = [];
|
||||
private _pendingByte = 0;
|
||||
private _bitOffset = 0;
|
||||
|
||||
toBuffer(): Buffer {
|
||||
return Buffer.from(this._arr);
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
// Emulation prevention: insert 0x03 before 00 00
|
||||
if (
|
||||
this._pendingByte <= 3 &&
|
||||
this._arr[this._arr.length - 1] === 0 &&
|
||||
this._arr[this._arr.length - 2] === 0
|
||||
) {
|
||||
this._arr.push(3);
|
||||
}
|
||||
this._arr.push(this._pendingByte);
|
||||
this._pendingByte = 0;
|
||||
this._bitOffset = 0;
|
||||
}
|
||||
|
||||
writeBits(bits: number, count: number): void {
|
||||
while (count > 0) {
|
||||
if (this._bitOffset === 0) {
|
||||
if (count >= 8) {
|
||||
this._pendingByte = (bits >> (count - 8)) & 0xff;
|
||||
count -= 8;
|
||||
this.flush();
|
||||
} else {
|
||||
const mask = (1 << count) - 1;
|
||||
this._pendingByte |= (bits & mask) << (8 - count);
|
||||
this._bitOffset = count;
|
||||
count = 0;
|
||||
}
|
||||
} else {
|
||||
const numBitsToWrite = Math.min(8 - this._bitOffset, count);
|
||||
const bitsToWrite =
|
||||
(bits >> (count - numBitsToWrite)) & ((1 << numBitsToWrite) - 1);
|
||||
this._pendingByte |=
|
||||
bitsToWrite << (8 - this._bitOffset - numBitsToWrite);
|
||||
count -= numBitsToWrite;
|
||||
this._bitOffset += numBitsToWrite;
|
||||
if (this._bitOffset === 8) {
|
||||
this._bitOffset = 0;
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeUnsigned(num: number, count: number): void {
|
||||
if (num < 0) throw new Error("Expected a non-negative number");
|
||||
this.writeBits(num, count);
|
||||
}
|
||||
|
||||
writeSigned(num: number, count: number): void {
|
||||
if (count <= 0) return;
|
||||
if (count > 32) throw new Error("writeSigned supports up to 32 bits");
|
||||
const mask =
|
||||
count === 32 ? 0xffffffff >>> 0 : (((1 << count) >>> 0) - 1) >>> 0;
|
||||
const unsigned = (num & mask) >>> 0;
|
||||
this.writeBits(unsigned, count);
|
||||
}
|
||||
|
||||
writeUnsignedExpGolomb(num: number): void {
|
||||
if (num < 0) throw new Error("Expected a non-negative number");
|
||||
num++;
|
||||
const bitCount = 32 - Math.clz32(num >>> 0);
|
||||
this.writeBits(0, bitCount - 1);
|
||||
this.writeBits(num, bitCount);
|
||||
}
|
||||
|
||||
writeSignedExpGolomb(num: number): void {
|
||||
if (num < 0) this.writeUnsignedExpGolomb(-2 * num);
|
||||
else this.writeUnsignedExpGolomb(2 * num - 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user