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.
95 lines
3.3 KiB
TypeScript
95 lines
3.3 KiB
TypeScript
/**
|
|
* goLive port smoke tests — verify the TS layer (no native binding needed
|
|
* for these; native is covered by the C++/node test-packetizer.js).
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
import { H264Helpers } from "../src/goLive/AnnexBHelper.js";
|
|
import { AVCodecID } from "../src/goLive/Demuxer.js";
|
|
import {
|
|
BaseMediaStream,
|
|
CodecPayloadType,
|
|
Encoders,
|
|
normalizeVideoCodec,
|
|
} from "../src/goLive/index.js";
|
|
import { rewriteSPSVUI } from "../src/goLive/SPSVUIRewriter.js";
|
|
|
|
describe("goLive port: codec + encoders", () => {
|
|
it("normalizeVideoCodec maps aliases to canonical names", () => {
|
|
expect(normalizeVideoCodec("H.264")).toBe("H264");
|
|
expect(normalizeVideoCodec("AVC")).toBe("H264");
|
|
expect(normalizeVideoCodec("h265")).toBe("H265");
|
|
expect(normalizeVideoCodec("vp8")).toBe("VP8");
|
|
expect(normalizeVideoCodec("av1")).toBe("AV1");
|
|
});
|
|
|
|
it("software encoder exposes x264 libx264 superfast film", () => {
|
|
const enc = Encoders.software()();
|
|
expect(enc.H264.name).toBe("libx264");
|
|
expect(enc.H264.options).toContain("-preset superfast");
|
|
expect(enc.H264.options).toContain("-tune film");
|
|
});
|
|
|
|
it("CodecPayloadType has opus + H264 entries", () => {
|
|
expect(CodecPayloadType.opus).toBeDefined();
|
|
expect(CodecPayloadType.H264).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("goLive port: annexb + sps rewriter", () => {
|
|
it("H264Helpers detects NAL unit types", () => {
|
|
const nal = Buffer.from([0x67, 0x42, 0x00, 0x1e]); // SPS
|
|
expect(H264Helpers.getUnitType(nal)).toBe(7); // SPS type
|
|
expect(H264Helpers.getUnitType(Buffer.from([0x65, 0x88]))).toBe(5); // IDR
|
|
});
|
|
|
|
it("rewriteSPSVUI returns a buffer for valid SPS", () => {
|
|
const sps = Buffer.from([
|
|
0x67, 0x42, 0x00, 0x1e, 0x96, 0x54, 0x05, 0x01, 0xec, 0x80,
|
|
]);
|
|
expect(() => rewriteSPSVUI(sps)).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("goLive port: streams", () => {
|
|
it("BaseMediaStream accepts plain frame objects", () => {
|
|
// BaseMediaStream is abstract — use a concrete subclass that no-ops the
|
|
// packetizer hook.
|
|
class TestStream extends BaseMediaStream {
|
|
async _sendFrame(_frame: Buffer, _frametime: number): Promise<void> {
|
|
/* no-op */
|
|
}
|
|
}
|
|
const stream = new TestStream("video");
|
|
const frame = {
|
|
data: Buffer.from([1, 2, 3]),
|
|
pts: 0,
|
|
duration: 40,
|
|
timeBase: { num: 1, den: 48000 },
|
|
flags: 0,
|
|
streamIndex: 0,
|
|
free: () => {},
|
|
};
|
|
expect(() => stream.write(frame)).not.toThrow();
|
|
stream.end();
|
|
});
|
|
});
|
|
|
|
describe("goLive port: demuxer codec ids", () => {
|
|
it("maps H264/HEVC/opus AVCodecID values", () => {
|
|
expect(AVCodecID.AV_CODEC_ID_H264).toBe(27);
|
|
expect(AVCodecID.AV_CODEC_ID_HEVC).toBe(173);
|
|
expect(AVCodecID.AV_CODEC_ID_OPUS).toBe(86019);
|
|
});
|
|
});
|
|
|
|
describe("goLive port: prepareStream option merge", () => {
|
|
it("merges default options into the descriptor (no ffmpeg spawn)", () => {
|
|
// Import the merge logic directly via the module; prepareStream spawns
|
|
// ffmpeg so we verify the descriptors it would build by checking the
|
|
// encoder + option functions that prepareStream uses.
|
|
const enc = Encoders.software()();
|
|
expect(enc.H264.options).toContain("-forced-idr 1");
|
|
expect(normalizeVideoCodec("H264")).toBe("H264");
|
|
});
|
|
});
|