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.
113 lines
2.5 KiB
TypeScript
113 lines
2.5 KiB
TypeScript
/**
|
|
* H264/H265 NAL helpers — ported from @dank074/discord-video-stream
|
|
* AnnexBHelper.js. Only the H264 parts are used by GoLive (H264 encoder),
|
|
* H265 constants kept for completeness of the port.
|
|
*/
|
|
|
|
export enum H264NalUnitTypes {
|
|
Unspecified = 0,
|
|
CodedSliceNonIDR = 1,
|
|
CodedSlicePartitionA = 2,
|
|
CodedSlicePartitionB = 3,
|
|
CodedSlicePartitionC = 4,
|
|
CodedSliceIdr = 5,
|
|
SEI = 6,
|
|
SPS = 7,
|
|
PPS = 8,
|
|
AccessUnitDelimiter = 9,
|
|
EndOfSequence = 10,
|
|
EndOfStream = 11,
|
|
FillerData = 12,
|
|
SEIExtenstion = 13,
|
|
PrefixNalUnit = 14,
|
|
SubsetSPS = 15,
|
|
}
|
|
|
|
export enum H265NalUnitTypes {
|
|
TRAIL_N = 0,
|
|
TRAIL_R = 1,
|
|
TSA_N = 2,
|
|
TSA_R = 3,
|
|
STSA_N = 4,
|
|
STSA_R = 5,
|
|
RADL_N = 6,
|
|
RADL_R = 7,
|
|
RASL_N = 8,
|
|
RASL_R = 9,
|
|
RSV_VCL_N10 = 10,
|
|
RSV_VCL_R11 = 11,
|
|
RSV_VCL_N12 = 12,
|
|
RSV_VCL_R13 = 13,
|
|
RSV_VCL_N14 = 14,
|
|
RSV_VCL_R15 = 15,
|
|
BLA_W_LP = 16,
|
|
BLA_W_RADL = 17,
|
|
BLA_N_LP = 18,
|
|
IDR_W_RADL = 19,
|
|
IDR_N_LP = 20,
|
|
CRA_NUT = 21,
|
|
RSV_IRAP_VCL22 = 22,
|
|
RSV_IRAP_VCL23 = 23,
|
|
RSV_VCL24 = 24,
|
|
RSV_VCL25 = 25,
|
|
RSV_VCL26 = 26,
|
|
RSV_VCL27 = 27,
|
|
RSV_VCL28 = 28,
|
|
RSV_VCL29 = 29,
|
|
RSV_VCL30 = 30,
|
|
RSV_VCL31 = 31,
|
|
VPS_NUT = 32,
|
|
SPS_NUT = 33,
|
|
PPS_NUT = 34,
|
|
AUD_NUT = 35,
|
|
EOS_NUT = 36,
|
|
EOB_NUT = 37,
|
|
FD_NUT = 38,
|
|
PREFIX_SEI_NUT = 39,
|
|
SUFFIX_SEI_NUT = 40,
|
|
}
|
|
|
|
export const H264Helpers = {
|
|
getUnitType(frame: Uint8Array): number {
|
|
return frame[0] & 0x1f;
|
|
},
|
|
splitHeader(frame: Uint8Array): [Uint8Array, Uint8Array] {
|
|
return [frame.subarray(0, 1), frame.subarray(1)];
|
|
},
|
|
isAUD(unitType: number): boolean {
|
|
return unitType === H264NalUnitTypes.AccessUnitDelimiter;
|
|
},
|
|
};
|
|
|
|
export const H265Helpers = {
|
|
getUnitType(frame: Uint8Array): number {
|
|
return (frame[0] >> 1) & 0x3f;
|
|
},
|
|
splitHeader(frame: Uint8Array): [Uint8Array, Uint8Array] {
|
|
return [frame.subarray(0, 2), frame.subarray(2)];
|
|
},
|
|
isAUD(unitType: number): boolean {
|
|
return unitType === H265NalUnitTypes.AUD_NUT;
|
|
},
|
|
};
|
|
|
|
export const startCode3 = Buffer.from([0, 0, 1]);
|
|
|
|
/** Split an AnnexB bitstream into NAL units (start codes stripped). */
|
|
export function splitNalu(buf: Buffer): Buffer[] {
|
|
let temp: Buffer | null = buf;
|
|
const nalus: Buffer[] = [];
|
|
while (temp?.byteLength) {
|
|
let pos: number = temp.indexOf(startCode3);
|
|
let length = 3;
|
|
if (pos > 0 && temp[pos - 1] === 0) {
|
|
pos--;
|
|
length++;
|
|
}
|
|
const nalu = pos === -1 ? temp : temp.subarray(0, pos);
|
|
temp = pos === -1 ? null : temp.subarray(pos + length);
|
|
if (nalu.byteLength) nalus.push(nalu);
|
|
}
|
|
return nalus;
|
|
}
|