@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.
Files changed (56) hide show
  1. package/dist/extension.d.ts +32 -0
  2. package/dist/extension.d.ts.map +1 -0
  3. package/dist/extension.js +133 -0
  4. package/dist/extension.js.map +1 -0
  5. package/dist/index.d.ts +15 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +19 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/rtcp/bye.d.ts +7 -0
  10. package/dist/rtcp/bye.d.ts.map +1 -0
  11. package/dist/rtcp/bye.js +59 -0
  12. package/dist/rtcp/bye.js.map +1 -0
  13. package/dist/rtcp/fb.d.ts +22 -0
  14. package/dist/rtcp/fb.d.ts.map +1 -0
  15. package/dist/rtcp/fb.js +171 -0
  16. package/dist/rtcp/fb.js.map +1 -0
  17. package/dist/rtcp/index.d.ts +23 -0
  18. package/dist/rtcp/index.d.ts.map +1 -0
  19. package/dist/rtcp/index.js +145 -0
  20. package/dist/rtcp/index.js.map +1 -0
  21. package/dist/rtcp/rr.d.ts +12 -0
  22. package/dist/rtcp/rr.d.ts.map +1 -0
  23. package/dist/rtcp/rr.js +65 -0
  24. package/dist/rtcp/rr.js.map +1 -0
  25. package/dist/rtcp/sdes.d.ts +7 -0
  26. package/dist/rtcp/sdes.d.ts.map +1 -0
  27. package/dist/rtcp/sdes.js +76 -0
  28. package/dist/rtcp/sdes.js.map +1 -0
  29. package/dist/rtcp/sr.d.ts +9 -0
  30. package/dist/rtcp/sr.d.ts.map +1 -0
  31. package/dist/rtcp/sr.js +59 -0
  32. package/dist/rtcp/sr.js.map +1 -0
  33. package/dist/rtp.d.ts +21 -0
  34. package/dist/rtp.d.ts.map +1 -0
  35. package/dist/rtp.js +149 -0
  36. package/dist/rtp.js.map +1 -0
  37. package/dist/sequence.d.ts +26 -0
  38. package/dist/sequence.d.ts.map +1 -0
  39. package/dist/sequence.js +53 -0
  40. package/dist/sequence.js.map +1 -0
  41. package/dist/types.d.ts +127 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +12 -0
  44. package/dist/types.js.map +1 -0
  45. package/package.json +57 -0
  46. package/src/extension.ts +153 -0
  47. package/src/index.ts +68 -0
  48. package/src/rtcp/bye.ts +73 -0
  49. package/src/rtcp/fb.ts +206 -0
  50. package/src/rtcp/index.ts +150 -0
  51. package/src/rtcp/rr.ts +82 -0
  52. package/src/rtcp/sdes.ts +94 -0
  53. package/src/rtcp/sr.ts +75 -0
  54. package/src/rtp.ts +176 -0
  55. package/src/sequence.ts +59 -0
  56. 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
+ }
@@ -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
+ }