@agentdance/node-webrtc-rtp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extension.d.ts +32 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +133 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/rtcp/bye.d.ts +7 -0
- package/dist/rtcp/bye.d.ts.map +1 -0
- package/dist/rtcp/bye.js +59 -0
- package/dist/rtcp/bye.js.map +1 -0
- package/dist/rtcp/fb.d.ts +22 -0
- package/dist/rtcp/fb.d.ts.map +1 -0
- package/dist/rtcp/fb.js +171 -0
- package/dist/rtcp/fb.js.map +1 -0
- package/dist/rtcp/index.d.ts +23 -0
- package/dist/rtcp/index.d.ts.map +1 -0
- package/dist/rtcp/index.js +145 -0
- package/dist/rtcp/index.js.map +1 -0
- package/dist/rtcp/rr.d.ts +12 -0
- package/dist/rtcp/rr.d.ts.map +1 -0
- package/dist/rtcp/rr.js +65 -0
- package/dist/rtcp/rr.js.map +1 -0
- package/dist/rtcp/sdes.d.ts +7 -0
- package/dist/rtcp/sdes.d.ts.map +1 -0
- package/dist/rtcp/sdes.js +76 -0
- package/dist/rtcp/sdes.js.map +1 -0
- package/dist/rtcp/sr.d.ts +9 -0
- package/dist/rtcp/sr.d.ts.map +1 -0
- package/dist/rtcp/sr.js +59 -0
- package/dist/rtcp/sr.js.map +1 -0
- package/dist/rtp.d.ts +21 -0
- package/dist/rtp.d.ts.map +1 -0
- package/dist/rtp.js +149 -0
- package/dist/rtp.js.map +1 -0
- package/dist/sequence.d.ts +26 -0
- package/dist/sequence.d.ts.map +1 -0
- package/dist/sequence.js +53 -0
- package/dist/sequence.js.map +1 -0
- package/dist/types.d.ts +127 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
- package/src/extension.ts +153 -0
- package/src/index.ts +68 -0
- package/src/rtcp/bye.ts +73 -0
- package/src/rtcp/fb.ts +206 -0
- package/src/rtcp/index.ts +150 -0
- package/src/rtcp/rr.ts +82 -0
- package/src/rtcp/sdes.ts +94 -0
- package/src/rtcp/sr.ts +75 -0
- package/src/rtp.ts +176 -0
- package/src/sequence.ts +59 -0
- package/src/types.ts +129 -0
package/src/rtcp/fb.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTCP Feedback messages — RFC 4585
|
|
3
|
+
*
|
|
4
|
+
* Transport Layer Feedback (PT=205):
|
|
5
|
+
* FMT=1 NACK (Generic NACK)
|
|
6
|
+
* FMT=15 TWCC (Transport-wide CC) — not fully implemented here
|
|
7
|
+
*
|
|
8
|
+
* Payload-Specific Feedback (PT=206):
|
|
9
|
+
* FMT=1 PLI (Picture Loss Indication)
|
|
10
|
+
* FMT=4 FIR (Full Intra Request)
|
|
11
|
+
* FMT=15 REMB (Receiver Estimated Max Bitrate, draft-alvestrand-rmcat-remb)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { RtcpNack, RtcpPli, RtcpFir, FirEntry, RtcpRemb } from '../types.js';
|
|
15
|
+
|
|
16
|
+
/** Common feedback header size: 4 (common) + 4 (senderSSRC) + 4 (mediaSSRC) = 12 bytes */
|
|
17
|
+
const FB_HEADER_SIZE = 12;
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// NACK — RFC 4585 Section 6.2.1, PT=205, FMT=1
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export function encodeNack(nack: RtcpNack): Buffer {
|
|
24
|
+
// FCI = PID (16 bits) + BLP (16 bits) = 4 bytes
|
|
25
|
+
const totalBytes = FB_HEADER_SIZE + 4;
|
|
26
|
+
const buf = Buffer.allocUnsafe(totalBytes);
|
|
27
|
+
|
|
28
|
+
buf[0] = (2 << 6) | 1; // V=2, P=0, FMT=1
|
|
29
|
+
buf[1] = 205;
|
|
30
|
+
buf.writeUInt16BE(totalBytes / 4 - 1, 2);
|
|
31
|
+
buf.writeUInt32BE(nack.senderSsrc >>> 0, 4);
|
|
32
|
+
buf.writeUInt32BE(nack.mediaSsrc >>> 0, 8);
|
|
33
|
+
buf.writeUInt16BE(nack.pid & 0xffff, 12);
|
|
34
|
+
buf.writeUInt16BE(nack.blp & 0xffff, 14);
|
|
35
|
+
|
|
36
|
+
return buf;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function decodeNack(buf: Buffer): RtcpNack {
|
|
40
|
+
if (buf.length < FB_HEADER_SIZE + 4) {
|
|
41
|
+
throw new RangeError(`NACK packet too short: ${buf.length}`);
|
|
42
|
+
}
|
|
43
|
+
const senderSsrc = buf.readUInt32BE(4);
|
|
44
|
+
const mediaSsrc = buf.readUInt32BE(8);
|
|
45
|
+
const pid = buf.readUInt16BE(12);
|
|
46
|
+
const blp = buf.readUInt16BE(14);
|
|
47
|
+
return { senderSsrc, mediaSsrc, pid, blp };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// PLI — RFC 4585 Section 6.3.1, PT=206, FMT=1
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
export function encodePli(pli: RtcpPli): Buffer {
|
|
55
|
+
// No FCI
|
|
56
|
+
const totalBytes = FB_HEADER_SIZE;
|
|
57
|
+
const buf = Buffer.allocUnsafe(totalBytes);
|
|
58
|
+
|
|
59
|
+
buf[0] = (2 << 6) | 1; // V=2, P=0, FMT=1
|
|
60
|
+
buf[1] = 206;
|
|
61
|
+
buf.writeUInt16BE(totalBytes / 4 - 1, 2);
|
|
62
|
+
buf.writeUInt32BE(pli.senderSsrc >>> 0, 4);
|
|
63
|
+
buf.writeUInt32BE(pli.mediaSsrc >>> 0, 8);
|
|
64
|
+
|
|
65
|
+
return buf;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function decodePli(buf: Buffer): RtcpPli {
|
|
69
|
+
if (buf.length < FB_HEADER_SIZE) {
|
|
70
|
+
throw new RangeError(`PLI packet too short: ${buf.length}`);
|
|
71
|
+
}
|
|
72
|
+
const senderSsrc = buf.readUInt32BE(4);
|
|
73
|
+
const mediaSsrc = buf.readUInt32BE(8);
|
|
74
|
+
return { senderSsrc, mediaSsrc };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// FIR — RFC 5104 Section 4.3.1, PT=206, FMT=4
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** Each FIR entry: 4 bytes SSRC + 4 bytes (seqNumber | reserved) */
|
|
82
|
+
const FIR_ENTRY_SIZE = 8;
|
|
83
|
+
|
|
84
|
+
export function encodeFir(fir: RtcpFir): Buffer {
|
|
85
|
+
const totalBytes = FB_HEADER_SIZE + fir.entries.length * FIR_ENTRY_SIZE;
|
|
86
|
+
const buf = Buffer.allocUnsafe(totalBytes);
|
|
87
|
+
|
|
88
|
+
buf[0] = (2 << 6) | 4; // V=2, P=0, FMT=4
|
|
89
|
+
buf[1] = 206;
|
|
90
|
+
buf.writeUInt16BE(totalBytes / 4 - 1, 2);
|
|
91
|
+
buf.writeUInt32BE(fir.senderSsrc >>> 0, 4);
|
|
92
|
+
buf.writeUInt32BE(0, 8); // media SSRC = 0 for FIR
|
|
93
|
+
|
|
94
|
+
let offset = FB_HEADER_SIZE;
|
|
95
|
+
for (const entry of fir.entries) {
|
|
96
|
+
buf.writeUInt32BE(entry.ssrc >>> 0, offset);
|
|
97
|
+
buf.writeUInt32BE((entry.seqNumber & 0xff) << 24, offset + 4);
|
|
98
|
+
offset += FIR_ENTRY_SIZE;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return buf;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function decodeFir(buf: Buffer): RtcpFir {
|
|
105
|
+
if (buf.length < FB_HEADER_SIZE) {
|
|
106
|
+
throw new RangeError(`FIR packet too short: ${buf.length}`);
|
|
107
|
+
}
|
|
108
|
+
const senderSsrc = buf.readUInt32BE(4);
|
|
109
|
+
const entries: FirEntry[] = [];
|
|
110
|
+
|
|
111
|
+
let offset = FB_HEADER_SIZE;
|
|
112
|
+
while (offset + FIR_ENTRY_SIZE <= buf.length) {
|
|
113
|
+
const ssrc = buf.readUInt32BE(offset);
|
|
114
|
+
const seqNumber = (buf.readUInt32BE(offset + 4) >>> 24) & 0xff;
|
|
115
|
+
entries.push({ ssrc, seqNumber });
|
|
116
|
+
offset += FIR_ENTRY_SIZE;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { senderSsrc, entries };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// REMB — draft-alvestrand-rmcat-remb, PT=206, FMT=15
|
|
124
|
+
// Unique ID: "REMB" in ASCII at FCI[0..3]
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
const REMB_UNIQUE_ID = Buffer.from('REMB', 'ascii');
|
|
128
|
+
|
|
129
|
+
export function encodeRemb(remb: RtcpRemb): Buffer {
|
|
130
|
+
const ssrcCount = remb.ssrcs.length;
|
|
131
|
+
// FCI: 4 (REMB) + 1 (numSSRC) + 3 (BR exp+mantissa) + ssrcCount*4
|
|
132
|
+
const fciSize = 8 + ssrcCount * 4;
|
|
133
|
+
const totalBytes = FB_HEADER_SIZE + fciSize;
|
|
134
|
+
const buf = Buffer.allocUnsafe(totalBytes);
|
|
135
|
+
|
|
136
|
+
buf[0] = (2 << 6) | 15; // V=2, P=0, FMT=15
|
|
137
|
+
buf[1] = 206;
|
|
138
|
+
buf.writeUInt16BE(totalBytes / 4 - 1, 2);
|
|
139
|
+
buf.writeUInt32BE(remb.senderSsrc >>> 0, 4);
|
|
140
|
+
buf.writeUInt32BE(remb.mediaSsrc >>> 0, 8);
|
|
141
|
+
|
|
142
|
+
// "REMB" unique identifier
|
|
143
|
+
REMB_UNIQUE_ID.copy(buf, 12);
|
|
144
|
+
|
|
145
|
+
// Num SSRC (1 byte)
|
|
146
|
+
buf[16] = ssrcCount & 0xff;
|
|
147
|
+
|
|
148
|
+
// BR Exp (6 bits) + BR Mantissa (18 bits) = 24 bits
|
|
149
|
+
// bitrate = mantissa * 2^exp
|
|
150
|
+
const { exp, mantissa } = encodeBitrate(remb.bitrate);
|
|
151
|
+
buf[17] = ((exp & 0x3f) << 2) | ((mantissa >> 16) & 0x03);
|
|
152
|
+
buf[18] = (mantissa >> 8) & 0xff;
|
|
153
|
+
buf[19] = mantissa & 0xff;
|
|
154
|
+
|
|
155
|
+
let offset = 20;
|
|
156
|
+
for (const ssrc of remb.ssrcs) {
|
|
157
|
+
buf.writeUInt32BE(ssrc >>> 0, offset);
|
|
158
|
+
offset += 4;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return buf;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function encodeBitrate(bitrate: number): { exp: number; mantissa: number } {
|
|
165
|
+
if (bitrate === 0) return { exp: 0, mantissa: 0 };
|
|
166
|
+
let exp = 0;
|
|
167
|
+
let m = bitrate;
|
|
168
|
+
while (m >= (1 << 18)) {
|
|
169
|
+
m >>= 1;
|
|
170
|
+
exp++;
|
|
171
|
+
}
|
|
172
|
+
return { exp, mantissa: m & 0x3ffff };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function decodeRemb(buf: Buffer): RtcpRemb {
|
|
176
|
+
if (buf.length < FB_HEADER_SIZE + 8) {
|
|
177
|
+
throw new RangeError(`REMB packet too short: ${buf.length}`);
|
|
178
|
+
}
|
|
179
|
+
const senderSsrc = buf.readUInt32BE(4);
|
|
180
|
+
const mediaSsrc = buf.readUInt32BE(8);
|
|
181
|
+
|
|
182
|
+
// Verify "REMB" unique ID
|
|
183
|
+
const uid = buf.subarray(12, 16).toString('ascii');
|
|
184
|
+
if (uid !== 'REMB') {
|
|
185
|
+
throw new Error(`Not a REMB packet, got unique ID: ${uid}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const numSsrc = buf[16]!;
|
|
189
|
+
const byte17 = buf[17]!;
|
|
190
|
+
const byte18 = buf[18]!;
|
|
191
|
+
const byte19 = buf[19]!;
|
|
192
|
+
|
|
193
|
+
const exp = (byte17 >> 2) & 0x3f;
|
|
194
|
+
const mantissa = ((byte17 & 0x03) << 16) | (byte18 << 8) | byte19;
|
|
195
|
+
const bitrate = mantissa * Math.pow(2, exp);
|
|
196
|
+
|
|
197
|
+
const ssrcs: number[] = [];
|
|
198
|
+
let offset = 20;
|
|
199
|
+
for (let i = 0; i < numSsrc; i++) {
|
|
200
|
+
if (offset + 4 > buf.length) break;
|
|
201
|
+
ssrcs.push(buf.readUInt32BE(offset));
|
|
202
|
+
offset += 4;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { senderSsrc, mediaSsrc, bitrate, ssrcs };
|
|
206
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTCP compound packet dispatch — RFC 3550 Section 6.1
|
|
3
|
+
*
|
|
4
|
+
* Compound RTCP packets are multiple RTCP packets concatenated together
|
|
5
|
+
* in a single UDP datagram.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { RtcpPacket, RtcpPacketType as RtcpPT } from '../types.js';
|
|
9
|
+
import { RtcpPacketType } from '../types.js';
|
|
10
|
+
import { encodeSr, decodeSr } from './sr.js';
|
|
11
|
+
import { encodeRr, decodeRr } from './rr.js';
|
|
12
|
+
import { encodeSdes, decodeSdes } from './sdes.js';
|
|
13
|
+
import { encodeBye, decodeBye } from './bye.js';
|
|
14
|
+
import {
|
|
15
|
+
encodeNack,
|
|
16
|
+
decodeNack,
|
|
17
|
+
encodePli,
|
|
18
|
+
decodePli,
|
|
19
|
+
encodeFir,
|
|
20
|
+
decodeFir,
|
|
21
|
+
encodeRemb,
|
|
22
|
+
decodeRemb,
|
|
23
|
+
} from './fb.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns true if the buffer looks like an RTCP packet:
|
|
27
|
+
* - version == 2
|
|
28
|
+
* - payload type in 200–207
|
|
29
|
+
* - at least 4 bytes
|
|
30
|
+
*/
|
|
31
|
+
export function isRtcpPacket(buf: Buffer): boolean {
|
|
32
|
+
if (buf.length < 4) return false;
|
|
33
|
+
const byte0 = buf[0];
|
|
34
|
+
if (byte0 === undefined) return false;
|
|
35
|
+
const version = (byte0 >> 6) & 0x03;
|
|
36
|
+
if (version !== 2) return false;
|
|
37
|
+
const byte1 = buf[1];
|
|
38
|
+
if (byte1 === undefined) return false;
|
|
39
|
+
// RTCP PT is the full second byte value (200–207)
|
|
40
|
+
return byte1 >= 200 && byte1 <= 207;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Decode a (possibly compound) RTCP buffer into an array of RtcpPacket objects.
|
|
45
|
+
*/
|
|
46
|
+
export function decodeRtcp(buf: Buffer): RtcpPacket[] {
|
|
47
|
+
const packets: RtcpPacket[] = [];
|
|
48
|
+
let offset = 0;
|
|
49
|
+
|
|
50
|
+
while (offset < buf.length) {
|
|
51
|
+
if (offset + 4 > buf.length) break;
|
|
52
|
+
|
|
53
|
+
const byte0 = buf[offset]!;
|
|
54
|
+
const byte1 = buf[offset + 1]!;
|
|
55
|
+
const lengthWords = buf.readUInt16BE(offset + 2);
|
|
56
|
+
const packetByteLen = (lengthWords + 1) * 4;
|
|
57
|
+
|
|
58
|
+
if (offset + packetByteLen > buf.length) {
|
|
59
|
+
// Truncated — take what we have
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const packetBuf = buf.subarray(offset, offset + packetByteLen) as Buffer;
|
|
64
|
+
offset += packetByteLen;
|
|
65
|
+
|
|
66
|
+
const pt: number = byte1; // RTCP PT occupies the full second byte (no marker bit)
|
|
67
|
+
const fmt = byte0 & 0x1f; // lower 5 bits (also RC / SC / FMT)
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
if (pt === RtcpPacketType.SR) {
|
|
71
|
+
packets.push({ type: 'sr', packet: decodeSr(packetBuf) });
|
|
72
|
+
} else if (pt === RtcpPacketType.RR) {
|
|
73
|
+
packets.push({ type: 'rr', packet: decodeRr(packetBuf) });
|
|
74
|
+
} else if (pt === RtcpPacketType.SDES) {
|
|
75
|
+
packets.push({ type: 'sdes', packet: decodeSdes(packetBuf) });
|
|
76
|
+
} else if (pt === RtcpPacketType.BYE) {
|
|
77
|
+
packets.push({ type: 'bye', packet: decodeBye(packetBuf) });
|
|
78
|
+
} else if (pt === RtcpPacketType.TransportFeedback) {
|
|
79
|
+
// FMT=1: NACK
|
|
80
|
+
if (fmt === 1) {
|
|
81
|
+
packets.push({ type: 'nack', packet: decodeNack(packetBuf) });
|
|
82
|
+
} else {
|
|
83
|
+
packets.push({ type: 'unknown', raw: Buffer.from(packetBuf) });
|
|
84
|
+
}
|
|
85
|
+
} else if (pt === RtcpPacketType.PayloadFeedback) {
|
|
86
|
+
if (fmt === 1) {
|
|
87
|
+
packets.push({ type: 'pli', packet: decodePli(packetBuf) });
|
|
88
|
+
} else if (fmt === 4) {
|
|
89
|
+
packets.push({ type: 'fir', packet: decodeFir(packetBuf) });
|
|
90
|
+
} else if (fmt === 15) {
|
|
91
|
+
// Could be REMB — check unique identifier
|
|
92
|
+
if (packetBuf.length >= 16 && packetBuf.subarray(12, 16).toString('ascii') === 'REMB') {
|
|
93
|
+
packets.push({ type: 'remb', packet: decodeRemb(packetBuf) });
|
|
94
|
+
} else {
|
|
95
|
+
packets.push({ type: 'unknown', raw: Buffer.from(packetBuf) });
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
packets.push({ type: 'unknown', raw: Buffer.from(packetBuf) });
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
packets.push({ type: 'unknown', raw: Buffer.from(packetBuf) });
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
packets.push({ type: 'unknown', raw: Buffer.from(packetBuf) });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return packets;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Encode an array of RtcpPacket objects into a compound RTCP buffer.
|
|
113
|
+
*/
|
|
114
|
+
export function encodeRtcp(packets: RtcpPacket[]): Buffer {
|
|
115
|
+
const parts: Buffer[] = [];
|
|
116
|
+
|
|
117
|
+
for (const pkt of packets) {
|
|
118
|
+
switch (pkt.type) {
|
|
119
|
+
case 'sr':
|
|
120
|
+
parts.push(encodeSr(pkt.packet));
|
|
121
|
+
break;
|
|
122
|
+
case 'rr':
|
|
123
|
+
parts.push(encodeRr(pkt.packet));
|
|
124
|
+
break;
|
|
125
|
+
case 'sdes':
|
|
126
|
+
parts.push(encodeSdes(pkt.packet));
|
|
127
|
+
break;
|
|
128
|
+
case 'bye':
|
|
129
|
+
parts.push(encodeBye(pkt.packet));
|
|
130
|
+
break;
|
|
131
|
+
case 'nack':
|
|
132
|
+
parts.push(encodeNack(pkt.packet));
|
|
133
|
+
break;
|
|
134
|
+
case 'pli':
|
|
135
|
+
parts.push(encodePli(pkt.packet));
|
|
136
|
+
break;
|
|
137
|
+
case 'fir':
|
|
138
|
+
parts.push(encodeFir(pkt.packet));
|
|
139
|
+
break;
|
|
140
|
+
case 'remb':
|
|
141
|
+
parts.push(encodeRemb(pkt.packet));
|
|
142
|
+
break;
|
|
143
|
+
case 'unknown':
|
|
144
|
+
parts.push(pkt.raw);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return Buffer.concat(parts);
|
|
150
|
+
}
|
package/src/rtcp/rr.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTCP Receiver Report (RR) — RFC 3550 Section 6.4.2
|
|
3
|
+
* Also provides shared ReportBlock encode/decode helpers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RtcpReceiverReport, ReportBlock } from '../types.js';
|
|
7
|
+
|
|
8
|
+
/** Each report block is exactly 24 bytes (6 x 32-bit words) */
|
|
9
|
+
export const REPORT_BLOCK_SIZE = 24;
|
|
10
|
+
|
|
11
|
+
/** RR base size: 4 (common hdr) + 4 (SSRC) */
|
|
12
|
+
const RR_BASE_SIZE = 8;
|
|
13
|
+
|
|
14
|
+
export function encodeReportBlock(rb: ReportBlock, buf: Buffer, offset: number): void {
|
|
15
|
+
buf.writeUInt32BE(rb.ssrc >>> 0, offset);
|
|
16
|
+
|
|
17
|
+
// fractionLost (8 bits) | cumulativeLost (24 bits, two's complement)
|
|
18
|
+
const cumLost = rb.cumulativeLost & 0xffffff;
|
|
19
|
+
buf.writeUInt32BE(((rb.fractionLost & 0xff) << 24) | cumLost, offset + 4);
|
|
20
|
+
|
|
21
|
+
buf.writeUInt32BE(rb.extendedHighestSeq >>> 0, offset + 8);
|
|
22
|
+
buf.writeUInt32BE(rb.jitter >>> 0, offset + 12);
|
|
23
|
+
buf.writeUInt32BE(rb.lastSR >>> 0, offset + 16);
|
|
24
|
+
buf.writeUInt32BE(rb.delaySinceLastSR >>> 0, offset + 20);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function decodeReportBlock(buf: Buffer, offset: number): ReportBlock {
|
|
28
|
+
const ssrc = buf.readUInt32BE(offset);
|
|
29
|
+
const word2 = buf.readUInt32BE(offset + 4);
|
|
30
|
+
|
|
31
|
+
const fractionLost = (word2 >>> 24) & 0xff;
|
|
32
|
+
// cumulativeLost is 24-bit signed
|
|
33
|
+
let cumulativeLost = word2 & 0xffffff;
|
|
34
|
+
if (cumulativeLost & 0x800000) {
|
|
35
|
+
cumulativeLost = cumulativeLost - 0x1000000; // sign extend
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const extendedHighestSeq = buf.readUInt32BE(offset + 8);
|
|
39
|
+
const jitter = buf.readUInt32BE(offset + 12);
|
|
40
|
+
const lastSR = buf.readUInt32BE(offset + 16);
|
|
41
|
+
const delaySinceLastSR = buf.readUInt32BE(offset + 20);
|
|
42
|
+
|
|
43
|
+
return { ssrc, fractionLost, cumulativeLost, extendedHighestSeq, jitter, lastSR, delaySinceLastSR };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function encodeRr(rr: RtcpReceiverReport): Buffer {
|
|
47
|
+
const blockCount = rr.reportBlocks.length;
|
|
48
|
+
const totalBytes = RR_BASE_SIZE + blockCount * REPORT_BLOCK_SIZE;
|
|
49
|
+
const buf = Buffer.allocUnsafe(totalBytes);
|
|
50
|
+
|
|
51
|
+
buf[0] = (2 << 6) | (blockCount & 0x1f);
|
|
52
|
+
buf[1] = 201;
|
|
53
|
+
buf.writeUInt16BE(totalBytes / 4 - 1, 2);
|
|
54
|
+
buf.writeUInt32BE(rr.ssrc >>> 0, 4);
|
|
55
|
+
|
|
56
|
+
let offset = RR_BASE_SIZE;
|
|
57
|
+
for (const rb of rr.reportBlocks) {
|
|
58
|
+
encodeReportBlock(rb, buf, offset);
|
|
59
|
+
offset += REPORT_BLOCK_SIZE;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return buf;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function decodeRr(buf: Buffer): RtcpReceiverReport {
|
|
66
|
+
if (buf.length < RR_BASE_SIZE) {
|
|
67
|
+
throw new RangeError(`RR packet too short: ${buf.length}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rc = (buf[0]! & 0x1f);
|
|
71
|
+
const ssrc = buf.readUInt32BE(4);
|
|
72
|
+
|
|
73
|
+
const reportBlocks: ReportBlock[] = [];
|
|
74
|
+
let offset = RR_BASE_SIZE;
|
|
75
|
+
for (let i = 0; i < rc; i++) {
|
|
76
|
+
if (offset + REPORT_BLOCK_SIZE > buf.length) break;
|
|
77
|
+
reportBlocks.push(decodeReportBlock(buf, offset));
|
|
78
|
+
offset += REPORT_BLOCK_SIZE;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { ssrc, reportBlocks };
|
|
82
|
+
}
|
package/src/rtcp/sdes.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTCP SDES (Source Description) — RFC 3550 Section 6.5
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RtcpSdes, SdesChunk, SdesItem } from '../types.js';
|
|
6
|
+
|
|
7
|
+
/** SDES base header: 4 bytes (common header) */
|
|
8
|
+
const SDES_HEADER_SIZE = 4;
|
|
9
|
+
|
|
10
|
+
export function encodeSdes(sdes: RtcpSdes): Buffer {
|
|
11
|
+
const chunkBuffers: Buffer[] = [];
|
|
12
|
+
|
|
13
|
+
for (const chunk of sdes.chunks) {
|
|
14
|
+
const itemParts: Buffer[] = [];
|
|
15
|
+
|
|
16
|
+
// SSRC (4 bytes)
|
|
17
|
+
const ssrcBuf = Buffer.allocUnsafe(4);
|
|
18
|
+
ssrcBuf.writeUInt32BE(chunk.ssrc >>> 0, 0);
|
|
19
|
+
itemParts.push(ssrcBuf);
|
|
20
|
+
|
|
21
|
+
for (const item of chunk.items) {
|
|
22
|
+
const textBuf = Buffer.from(item.text, 'utf8');
|
|
23
|
+
const itemBuf = Buffer.allocUnsafe(2 + textBuf.length);
|
|
24
|
+
itemBuf[0] = item.type & 0xff;
|
|
25
|
+
itemBuf[1] = textBuf.length & 0xff;
|
|
26
|
+
textBuf.copy(itemBuf, 2);
|
|
27
|
+
itemParts.push(itemBuf);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// END item (type=0)
|
|
31
|
+
itemParts.push(Buffer.from([0x00]));
|
|
32
|
+
|
|
33
|
+
// Concatenate and pad to 4-byte boundary
|
|
34
|
+
const chunkBody = Buffer.concat(itemParts);
|
|
35
|
+
const padLen = (4 - (chunkBody.length % 4)) % 4;
|
|
36
|
+
const padding = Buffer.alloc(padLen, 0x00);
|
|
37
|
+
chunkBuffers.push(Buffer.concat([chunkBody, padding]));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const body = Buffer.concat(chunkBuffers);
|
|
41
|
+
const totalBytes = SDES_HEADER_SIZE + body.length;
|
|
42
|
+
|
|
43
|
+
const header = Buffer.allocUnsafe(SDES_HEADER_SIZE);
|
|
44
|
+
const sc = sdes.chunks.length;
|
|
45
|
+
header[0] = (2 << 6) | (sc & 0x1f);
|
|
46
|
+
header[1] = 202;
|
|
47
|
+
header.writeUInt16BE(totalBytes / 4 - 1, 2);
|
|
48
|
+
|
|
49
|
+
return Buffer.concat([header, body]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function decodeSdes(buf: Buffer): RtcpSdes {
|
|
53
|
+
if (buf.length < SDES_HEADER_SIZE) {
|
|
54
|
+
throw new RangeError(`SDES packet too short: ${buf.length}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const sc = buf[0]! & 0x1f;
|
|
58
|
+
// length word gives total 32-bit words - 1, so total bytes = (length+1)*4
|
|
59
|
+
const totalBytes = (buf.readUInt16BE(2) + 1) * 4;
|
|
60
|
+
|
|
61
|
+
const chunks: SdesChunk[] = [];
|
|
62
|
+
let offset = SDES_HEADER_SIZE;
|
|
63
|
+
|
|
64
|
+
for (let c = 0; c < sc; c++) {
|
|
65
|
+
if (offset + 4 > buf.length) break;
|
|
66
|
+
|
|
67
|
+
const ssrc = buf.readUInt32BE(offset);
|
|
68
|
+
offset += 4;
|
|
69
|
+
|
|
70
|
+
const items: SdesItem[] = [];
|
|
71
|
+
|
|
72
|
+
while (offset < totalBytes && offset < buf.length) {
|
|
73
|
+
const itemType = buf[offset];
|
|
74
|
+
if (itemType === undefined || itemType === 0x00) {
|
|
75
|
+
// END item — skip to 4-byte boundary
|
|
76
|
+
offset++;
|
|
77
|
+
const padTo = (offset + 3) & ~3;
|
|
78
|
+
offset = Math.min(padTo, buf.length);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
offset++;
|
|
82
|
+
const textLen = buf[offset];
|
|
83
|
+
if (textLen === undefined) break;
|
|
84
|
+
offset++;
|
|
85
|
+
const text = buf.subarray(offset, offset + textLen).toString('utf8');
|
|
86
|
+
offset += textLen;
|
|
87
|
+
items.push({ type: itemType, text });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
chunks.push({ ssrc, items });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { chunks };
|
|
94
|
+
}
|
package/src/rtcp/sr.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTCP Sender Report (SR) — RFC 3550 Section 6.4.1
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RtcpSenderReport, ReportBlock } from '../types.js';
|
|
6
|
+
import { encodeReportBlock, decodeReportBlock, REPORT_BLOCK_SIZE } from './rr.js';
|
|
7
|
+
|
|
8
|
+
/** Fixed size of SR sender info block (without report blocks) */
|
|
9
|
+
const SR_SENDER_INFO_SIZE = 20; // 5 x 32-bit words
|
|
10
|
+
|
|
11
|
+
/** Full SR packet size without report blocks: 4 (common hdr) + 4 (SSRC) + 20 (sender info) */
|
|
12
|
+
export const SR_BASE_SIZE = 28;
|
|
13
|
+
|
|
14
|
+
export function encodeSr(sr: RtcpSenderReport): Buffer {
|
|
15
|
+
const blockCount = sr.reportBlocks.length;
|
|
16
|
+
const totalBytes = SR_BASE_SIZE + blockCount * REPORT_BLOCK_SIZE;
|
|
17
|
+
const buf = Buffer.allocUnsafe(totalBytes);
|
|
18
|
+
|
|
19
|
+
// Common RTCP header
|
|
20
|
+
// V=2, P=0, RC=blockCount, PT=200
|
|
21
|
+
buf[0] = (2 << 6) | (blockCount & 0x1f);
|
|
22
|
+
buf[1] = 200;
|
|
23
|
+
// Length in 32-bit words minus 1
|
|
24
|
+
const lengthWords = totalBytes / 4 - 1;
|
|
25
|
+
buf.writeUInt16BE(lengthWords, 2);
|
|
26
|
+
|
|
27
|
+
// SSRC
|
|
28
|
+
buf.writeUInt32BE(sr.ssrc >>> 0, 4);
|
|
29
|
+
|
|
30
|
+
// NTP timestamp (64-bit)
|
|
31
|
+
buf.writeBigUInt64BE(sr.ntpTimestamp, 8);
|
|
32
|
+
|
|
33
|
+
// RTP timestamp
|
|
34
|
+
buf.writeUInt32BE(sr.rtpTimestamp >>> 0, 16);
|
|
35
|
+
|
|
36
|
+
// Packet count
|
|
37
|
+
buf.writeUInt32BE(sr.packetCount >>> 0, 20);
|
|
38
|
+
|
|
39
|
+
// Octet count
|
|
40
|
+
buf.writeUInt32BE(sr.octetCount >>> 0, 24);
|
|
41
|
+
|
|
42
|
+
// Report blocks
|
|
43
|
+
let offset = SR_BASE_SIZE;
|
|
44
|
+
for (const rb of sr.reportBlocks) {
|
|
45
|
+
encodeReportBlock(rb, buf, offset);
|
|
46
|
+
offset += REPORT_BLOCK_SIZE;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return buf;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function decodeSr(buf: Buffer): RtcpSenderReport {
|
|
53
|
+
if (buf.length < SR_BASE_SIZE) {
|
|
54
|
+
throw new RangeError(`SR packet too short: ${buf.length}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// byte[0]: V(2)|P(1)|RC(5) — RC = report count
|
|
58
|
+
const rc = (buf[0]! & 0x1f);
|
|
59
|
+
|
|
60
|
+
const ssrc = buf.readUInt32BE(4);
|
|
61
|
+
const ntpTimestamp = buf.readBigUInt64BE(8);
|
|
62
|
+
const rtpTimestamp = buf.readUInt32BE(16);
|
|
63
|
+
const packetCount = buf.readUInt32BE(20);
|
|
64
|
+
const octetCount = buf.readUInt32BE(24);
|
|
65
|
+
|
|
66
|
+
const reportBlocks: ReportBlock[] = [];
|
|
67
|
+
let offset = SR_BASE_SIZE;
|
|
68
|
+
for (let i = 0; i < rc; i++) {
|
|
69
|
+
if (offset + REPORT_BLOCK_SIZE > buf.length) break;
|
|
70
|
+
reportBlocks.push(decodeReportBlock(buf, offset));
|
|
71
|
+
offset += REPORT_BLOCK_SIZE;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { ssrc, ntpTimestamp, rtpTimestamp, packetCount, octetCount, reportBlocks };
|
|
75
|
+
}
|