@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
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Wrap-around aware sequence number utilities for RTP (RFC 3550 Section A.1).
3
+ * Sequence numbers are 16-bit unsigned (0–65535).
4
+ */
5
+ const MAX_SEQ = 0x10000; // 65536
6
+ const HALF_MAX_SEQ = 0x8000; // 32768
7
+ /**
8
+ * Returns a - b, wrap-around aware.
9
+ * Result is in range (-32768, 32768].
10
+ */
11
+ export function seqDiff(a, b) {
12
+ const diff = ((a - b) & 0xffff) >>> 0;
13
+ if (diff === 0)
14
+ return 0;
15
+ // If diff >= half the range, it wrapped around in the negative direction
16
+ return diff < HALF_MAX_SEQ ? diff : diff - MAX_SEQ;
17
+ }
18
+ /** Returns true if a < b (wrap-around aware) */
19
+ export function seqLt(a, b) {
20
+ return seqDiff(a, b) < 0;
21
+ }
22
+ /** Returns true if a <= b (wrap-around aware) */
23
+ export function seqLte(a, b) {
24
+ return seqDiff(a, b) <= 0;
25
+ }
26
+ /** Returns true if a > b (wrap-around aware) */
27
+ export function seqGt(a, b) {
28
+ return seqDiff(a, b) > 0;
29
+ }
30
+ // NTP epoch is Jan 1, 1900; Unix epoch is Jan 1, 1970 — difference in seconds
31
+ const NTP_UNIX_OFFSET_S = BigInt(70 * 365 * 24 * 3600 + 17 * 24 * 3600); // 70 years + 17 leap days
32
+ /**
33
+ * Convert a 64-bit NTP timestamp to Unix milliseconds.
34
+ * NTP format: upper 32 bits = seconds since 1900-01-01,
35
+ * lower 32 bits = fractional seconds.
36
+ */
37
+ export function ntpToUnix(ntp) {
38
+ const seconds = (ntp >> 32n) - NTP_UNIX_OFFSET_S;
39
+ const fraction = ntp & 0xffffffffn;
40
+ const ms = (fraction * 1000n) >> 32n;
41
+ return Number(seconds) * 1000 + Number(ms);
42
+ }
43
+ /**
44
+ * Convert Unix milliseconds to a 64-bit NTP timestamp.
45
+ */
46
+ export function unixToNtp(ms) {
47
+ const totalMs = BigInt(ms);
48
+ const seconds = totalMs / 1000n + NTP_UNIX_OFFSET_S;
49
+ const remainder = totalMs % 1000n;
50
+ const fraction = (remainder * (1n << 32n)) / 1000n;
51
+ return (seconds << 32n) | fraction;
52
+ }
53
+ //# sourceMappingURL=sequence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequence.js","sourceRoot":"","sources":["../src/sequence.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,QAAQ;AACjC,MAAM,YAAY,GAAG,MAAM,CAAC,CAAC,QAAQ;AAErC;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,CAAS,EAAE,CAAS;IAC1C,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACzB,yEAAyE;IACzE,OAAO,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,OAAO,CAAC;AACrD,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,KAAK,CAAC,CAAS,EAAE,CAAS;IACxC,OAAO,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,MAAM,CAAC,CAAS,EAAE,CAAS;IACzC,OAAO,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,KAAK,CAAC,CAAS,EAAE,CAAS;IACxC,OAAO,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED,8EAA8E;AAC9E,MAAM,iBAAiB,GAAG,MAAM,CAAC,EAAE,GAAG,GAAG,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,0BAA0B;AAEnG;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,iBAAiB,CAAC;IACjD,MAAM,QAAQ,GAAG,GAAG,GAAG,WAAW,CAAC;IACnC,MAAM,EAAE,GAAG,CAAC,QAAQ,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC;IACrC,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,MAAM,OAAO,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3B,MAAM,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,iBAAiB,CAAC;IACpD,MAAM,SAAS,GAAG,OAAO,GAAG,KAAK,CAAC;IAClC,MAAM,QAAQ,GAAG,CAAC,SAAS,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;IACnD,OAAO,CAAC,OAAO,IAAI,GAAG,CAAC,GAAG,QAAQ,CAAC;AACrC,CAAC"}
@@ -0,0 +1,127 @@
1
+ export interface RtpPacket {
2
+ version: 2;
3
+ padding: boolean;
4
+ extension: boolean;
5
+ csrcCount: number;
6
+ marker: boolean;
7
+ payloadType: number;
8
+ sequenceNumber: number;
9
+ timestamp: number;
10
+ ssrc: number;
11
+ csrcs: number[];
12
+ headerExtension?: RtpHeaderExtension;
13
+ payload: Buffer;
14
+ }
15
+ export interface RtpHeaderExtension {
16
+ id: number;
17
+ values: RtpExtensionValue[];
18
+ }
19
+ export interface RtpExtensionValue {
20
+ id: number;
21
+ data: Buffer;
22
+ }
23
+ export declare enum RtcpPacketType {
24
+ SR = 200,
25
+ RR = 201,
26
+ SDES = 202,
27
+ BYE = 203,
28
+ APP = 204,
29
+ TransportFeedback = 205,
30
+ PayloadFeedback = 206
31
+ }
32
+ export interface RtcpHeader {
33
+ version: 2;
34
+ padding: boolean;
35
+ count: number;
36
+ packetType: RtcpPacketType;
37
+ length: number;
38
+ }
39
+ export interface RtcpSenderReport {
40
+ ssrc: number;
41
+ ntpTimestamp: bigint;
42
+ rtpTimestamp: number;
43
+ packetCount: number;
44
+ octetCount: number;
45
+ reportBlocks: ReportBlock[];
46
+ }
47
+ export interface RtcpReceiverReport {
48
+ ssrc: number;
49
+ reportBlocks: ReportBlock[];
50
+ }
51
+ export interface ReportBlock {
52
+ ssrc: number;
53
+ fractionLost: number;
54
+ cumulativeLost: number;
55
+ extendedHighestSeq: number;
56
+ jitter: number;
57
+ lastSR: number;
58
+ delaySinceLastSR: number;
59
+ }
60
+ export interface RtcpSdes {
61
+ chunks: SdesChunk[];
62
+ }
63
+ export interface SdesChunk {
64
+ ssrc: number;
65
+ items: SdesItem[];
66
+ }
67
+ export interface SdesItem {
68
+ type: number;
69
+ text: string;
70
+ }
71
+ export interface RtcpBye {
72
+ ssrcs: number[];
73
+ reason?: string;
74
+ }
75
+ export interface RtcpNack {
76
+ senderSsrc: number;
77
+ mediaSsrc: number;
78
+ pid: number;
79
+ blp: number;
80
+ }
81
+ export interface RtcpPli {
82
+ senderSsrc: number;
83
+ mediaSsrc: number;
84
+ }
85
+ export interface RtcpFir {
86
+ senderSsrc: number;
87
+ entries: FirEntry[];
88
+ }
89
+ export interface FirEntry {
90
+ ssrc: number;
91
+ seqNumber: number;
92
+ }
93
+ export interface RtcpRemb {
94
+ senderSsrc: number;
95
+ mediaSsrc: number;
96
+ bitrate: number;
97
+ ssrcs: number[];
98
+ }
99
+ export type RtcpPacket = {
100
+ type: 'sr';
101
+ packet: RtcpSenderReport;
102
+ } | {
103
+ type: 'rr';
104
+ packet: RtcpReceiverReport;
105
+ } | {
106
+ type: 'sdes';
107
+ packet: RtcpSdes;
108
+ } | {
109
+ type: 'bye';
110
+ packet: RtcpBye;
111
+ } | {
112
+ type: 'nack';
113
+ packet: RtcpNack;
114
+ } | {
115
+ type: 'pli';
116
+ packet: RtcpPli;
117
+ } | {
118
+ type: 'fir';
119
+ packet: RtcpFir;
120
+ } | {
121
+ type: 'remb';
122
+ packet: RtcpRemb;
123
+ } | {
124
+ type: 'unknown';
125
+ raw: Buffer;
126
+ };
127
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ // RFC 3550 RTP/RTCP types
2
+ export var RtcpPacketType;
3
+ (function (RtcpPacketType) {
4
+ RtcpPacketType[RtcpPacketType["SR"] = 200] = "SR";
5
+ RtcpPacketType[RtcpPacketType["RR"] = 201] = "RR";
6
+ RtcpPacketType[RtcpPacketType["SDES"] = 202] = "SDES";
7
+ RtcpPacketType[RtcpPacketType["BYE"] = 203] = "BYE";
8
+ RtcpPacketType[RtcpPacketType["APP"] = 204] = "APP";
9
+ RtcpPacketType[RtcpPacketType["TransportFeedback"] = 205] = "TransportFeedback";
10
+ RtcpPacketType[RtcpPacketType["PayloadFeedback"] = 206] = "PayloadFeedback";
11
+ })(RtcpPacketType || (RtcpPacketType = {}));
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,0BAA0B;AA2B1B,MAAM,CAAN,IAAY,cAQX;AARD,WAAY,cAAc;IACxB,iDAAQ,CAAA;IACR,iDAAQ,CAAA;IACR,qDAAU,CAAA;IACV,mDAAS,CAAA;IACT,mDAAS,CAAA;IACT,+EAAuB,CAAA;IACvB,2EAAqB,CAAA;AACvB,CAAC,EARW,cAAc,KAAd,cAAc,QAQzB"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@agentdance/node-webrtc-rtp",
3
+ "version": "1.0.0",
4
+ "description": "RFC 3550 RTP/RTCP codec — header encode/decode, CSRC, header extensions, SR/RR/SDES/BYE/NACK/PLI/FIR/REMB. Part of the @agentdance/node-webrtc stack.",
5
+ "keywords": [
6
+ "webrtc",
7
+ "rtp",
8
+ "rtcp",
9
+ "rfc3550",
10
+ "nack",
11
+ "remb",
12
+ "typescript",
13
+ "node"
14
+ ],
15
+ "license": "MIT",
16
+ "homepage": "https://github.com/agent-dance/node-webrtc#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/agent-dance/node-webrtc.git",
20
+ "directory": "packages/rtp"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/agent-dance/node-webrtc/issues"
24
+ },
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "type": "module",
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "import": "./dist/index.js",
34
+ "types": "./dist/index.d.ts"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src"
40
+ ],
41
+ "sideEffects": false,
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "registry": "https://registry.npmjs.org/"
45
+ },
46
+ "devDependencies": {
47
+ "typescript": "*",
48
+ "vitest": "*",
49
+ "@types/node": "*"
50
+ },
51
+ "scripts": {
52
+ "build": "tsc",
53
+ "test": "vitest run",
54
+ "typecheck": "tsc --noEmit",
55
+ "clean": "rm -rf dist"
56
+ }
57
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * RTP header extension parsing and serialization.
3
+ * Supports one-byte header (RFC 5285, profile 0xBEDE) and
4
+ * two-byte header (RFC 5285, profile 0x1000).
5
+ */
6
+
7
+ import type { RtpExtensionValue, RtpHeaderExtension } from './types.js';
8
+
9
+ export const ONE_BYTE_PROFILE = 0xbede;
10
+ export const TWO_BYTE_PROFILE = 0x1000;
11
+
12
+ /**
13
+ * Parse the extension payload (after the 4-byte profile+length header).
14
+ * @param profile - The extension profile value (0xBEDE or 0x1000)
15
+ * @param data - Raw bytes of the extension body (without the 4-byte profile/length header)
16
+ */
17
+ export function parseExtensionValues(
18
+ profile: number,
19
+ data: Buffer,
20
+ ): RtpExtensionValue[] {
21
+ const values: RtpExtensionValue[] = [];
22
+
23
+ if (profile === ONE_BYTE_PROFILE) {
24
+ // One-byte header: | 0001 | id(4) | len-1(4) | data ... |
25
+ let i = 0;
26
+ while (i < data.length) {
27
+ const byte = data[i];
28
+ if (byte === undefined) break;
29
+ if (byte === 0x00) {
30
+ // Padding
31
+ i++;
32
+ continue;
33
+ }
34
+ if (byte === 0xff) {
35
+ // End marker
36
+ break;
37
+ }
38
+ const id = (byte >> 4) & 0x0f;
39
+ const len = (byte & 0x0f) + 1;
40
+ i++;
41
+ if (i + len > data.length) break;
42
+ values.push({ id, data: Buffer.from(data.subarray(i, i + len)) });
43
+ i += len;
44
+ }
45
+ } else if ((profile & 0xfff0) === TWO_BYTE_PROFILE) {
46
+ // Two-byte header: | id(8) | len(8) | data ... |
47
+ let i = 0;
48
+ while (i < data.length) {
49
+ const id = data[i];
50
+ if (id === undefined) break;
51
+ if (id === 0x00) {
52
+ // Padding
53
+ i++;
54
+ continue;
55
+ }
56
+ i++;
57
+ if (i >= data.length) break;
58
+ const len = data[i];
59
+ if (len === undefined) break;
60
+ i++;
61
+ if (len === 0) {
62
+ values.push({ id, data: Buffer.alloc(0) });
63
+ continue;
64
+ }
65
+ if (i + len > data.length) break;
66
+ values.push({ id, data: Buffer.from(data.subarray(i, i + len)) });
67
+ i += len;
68
+ }
69
+ }
70
+
71
+ return values;
72
+ }
73
+
74
+ /**
75
+ * Serialize extension values into a Buffer (without the 4-byte profile/length header).
76
+ * The result is padded to a 4-byte boundary.
77
+ */
78
+ export function serializeExtensionValues(
79
+ profile: number,
80
+ values: RtpExtensionValue[],
81
+ ): Buffer {
82
+ const parts: Buffer[] = [];
83
+
84
+ if (profile === ONE_BYTE_PROFILE) {
85
+ for (const ext of values) {
86
+ const len = ext.data.length;
87
+ if (len < 1 || len > 16) {
88
+ throw new RangeError(
89
+ `One-byte extension id=${ext.id}: data length must be 1-16 bytes, got ${len}`,
90
+ );
91
+ }
92
+ if (ext.id < 1 || ext.id > 14) {
93
+ throw new RangeError(
94
+ `One-byte extension id must be 1-14, got ${ext.id}`,
95
+ );
96
+ }
97
+ const header = ((ext.id & 0x0f) << 4) | ((len - 1) & 0x0f);
98
+ parts.push(Buffer.from([header]));
99
+ parts.push(Buffer.from(ext.data));
100
+ }
101
+ } else if ((profile & 0xfff0) === TWO_BYTE_PROFILE) {
102
+ for (const ext of values) {
103
+ const len = ext.data.length;
104
+ if (ext.id < 1 || ext.id > 255) {
105
+ throw new RangeError(
106
+ `Two-byte extension id must be 1-255, got ${ext.id}`,
107
+ );
108
+ }
109
+ parts.push(Buffer.from([ext.id, len]));
110
+ if (len > 0) parts.push(Buffer.from(ext.data));
111
+ }
112
+ }
113
+
114
+ const body = Buffer.concat(parts);
115
+ // Pad to 4-byte boundary
116
+ const padLen = (4 - (body.length % 4)) % 4;
117
+ const padding = Buffer.alloc(padLen, 0x00);
118
+ return Buffer.concat([body, padding]);
119
+ }
120
+
121
+ /**
122
+ * Serialize a complete RtpHeaderExtension (profile/length word + body).
123
+ */
124
+ export function serializeExtension(ext: RtpHeaderExtension): Buffer {
125
+ const body = serializeExtensionValues(ext.id, ext.values);
126
+ // Length field = number of 32-bit words in the body
127
+ const lengthWords = body.length / 4;
128
+ const header = Buffer.allocUnsafe(4);
129
+ header.writeUInt16BE(ext.id, 0);
130
+ header.writeUInt16BE(lengthWords, 2);
131
+ return Buffer.concat([header, body]);
132
+ }
133
+
134
+ /**
135
+ * Get the first extension value matching a given id, or undefined.
136
+ */
137
+ export function getExtensionValue(
138
+ ext: RtpHeaderExtension,
139
+ id: number,
140
+ ): RtpExtensionValue | undefined {
141
+ return ext.values.find((v) => v.id === id);
142
+ }
143
+
144
+ /**
145
+ * Return a new RtpHeaderExtension with the given id set (inserted or replaced).
146
+ */
147
+ export function setExtensionValue(
148
+ ext: RtpHeaderExtension,
149
+ value: RtpExtensionValue,
150
+ ): RtpHeaderExtension {
151
+ const filtered = ext.values.filter((v) => v.id !== value.id);
152
+ return { id: ext.id, values: [...filtered, value] };
153
+ }
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @agentdance/node-webrtc-rtp — RFC 3550 RTP/RTCP implementation
3
+ */
4
+
5
+ export type {
6
+ RtpPacket,
7
+ RtpHeaderExtension,
8
+ RtpExtensionValue,
9
+ RtcpHeader,
10
+ RtcpSenderReport,
11
+ RtcpReceiverReport,
12
+ ReportBlock,
13
+ RtcpSdes,
14
+ SdesChunk,
15
+ SdesItem,
16
+ RtcpBye,
17
+ RtcpNack,
18
+ RtcpPli,
19
+ RtcpFir,
20
+ FirEntry,
21
+ RtcpRemb,
22
+ RtcpPacket,
23
+ } from './types.js';
24
+
25
+ export { RtcpPacketType } from './types.js';
26
+
27
+ // RTP
28
+ export { encodeRtp, decodeRtp, isRtpPacket } from './rtp.js';
29
+
30
+ // RTCP compound
31
+ export { encodeRtcp, decodeRtcp, isRtcpPacket } from './rtcp/index.js';
32
+
33
+ // RTCP individual encoders/decoders
34
+ export { encodeSr, decodeSr } from './rtcp/sr.js';
35
+ export { encodeRr, decodeRr, encodeReportBlock, decodeReportBlock } from './rtcp/rr.js';
36
+ export { encodeSdes, decodeSdes } from './rtcp/sdes.js';
37
+ export { encodeBye, decodeBye } from './rtcp/bye.js';
38
+ export {
39
+ encodeNack,
40
+ decodeNack,
41
+ encodePli,
42
+ decodePli,
43
+ encodeFir,
44
+ decodeFir,
45
+ encodeRemb,
46
+ decodeRemb,
47
+ } from './rtcp/fb.js';
48
+
49
+ // Header extensions
50
+ export {
51
+ ONE_BYTE_PROFILE,
52
+ TWO_BYTE_PROFILE,
53
+ parseExtensionValues,
54
+ serializeExtensionValues,
55
+ serializeExtension,
56
+ getExtensionValue,
57
+ setExtensionValue,
58
+ } from './extension.js';
59
+
60
+ // Sequence utilities
61
+ export {
62
+ seqDiff,
63
+ seqLt,
64
+ seqLte,
65
+ seqGt,
66
+ ntpToUnix,
67
+ unixToNtp,
68
+ } from './sequence.js';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * RTCP BYE — RFC 3550 Section 6.6
3
+ */
4
+
5
+ import type { RtcpBye } from '../types.js';
6
+
7
+ const BYE_HEADER_SIZE = 4;
8
+
9
+ export function encodeBye(bye: RtcpBye): Buffer {
10
+ const sc = bye.ssrcs.length;
11
+
12
+ // Reason string (optional)
13
+ let reasonBuf: Buffer = Buffer.alloc(0);
14
+ if (bye.reason !== undefined) {
15
+ const text = Buffer.from(bye.reason, 'utf8');
16
+ const reasonLen = Math.min(text.length, 255);
17
+ // 1 byte length prefix + text + padding to 4-byte boundary
18
+ const raw = Buffer.allocUnsafe(1 + reasonLen);
19
+ raw[0] = reasonLen;
20
+ text.copy(raw, 1, 0, reasonLen);
21
+ const padded = reasonLen + 1;
22
+ const padLen = (4 - (padded % 4)) % 4;
23
+ reasonBuf = Buffer.concat([raw, Buffer.alloc(padLen, 0x00)]);
24
+ }
25
+
26
+ const totalBytes = BYE_HEADER_SIZE + sc * 4 + reasonBuf.length;
27
+ const buf = Buffer.allocUnsafe(totalBytes);
28
+
29
+ buf[0] = (2 << 6) | (sc & 0x1f);
30
+ buf[1] = 203;
31
+ buf.writeUInt16BE(totalBytes / 4 - 1, 2);
32
+
33
+ let offset = BYE_HEADER_SIZE;
34
+ for (const ssrc of bye.ssrcs) {
35
+ buf.writeUInt32BE(ssrc >>> 0, offset);
36
+ offset += 4;
37
+ }
38
+
39
+ if (reasonBuf.length > 0) {
40
+ reasonBuf.copy(buf, offset);
41
+ }
42
+
43
+ return buf;
44
+ }
45
+
46
+ export function decodeBye(buf: Buffer): RtcpBye {
47
+ if (buf.length < BYE_HEADER_SIZE) {
48
+ throw new RangeError(`BYE packet too short: ${buf.length}`);
49
+ }
50
+
51
+ const sc = buf[0]! & 0x1f;
52
+ const totalBytes = (buf.readUInt16BE(2) + 1) * 4;
53
+
54
+ const ssrcs: number[] = [];
55
+ let offset = BYE_HEADER_SIZE;
56
+
57
+ for (let i = 0; i < sc; i++) {
58
+ if (offset + 4 > buf.length) break;
59
+ ssrcs.push(buf.readUInt32BE(offset));
60
+ offset += 4;
61
+ }
62
+
63
+ let reason: string | undefined;
64
+ if (offset < totalBytes && offset < buf.length) {
65
+ const reasonLen = buf[offset];
66
+ if (reasonLen !== undefined && reasonLen > 0) {
67
+ offset++;
68
+ reason = buf.subarray(offset, offset + reasonLen).toString('utf8');
69
+ }
70
+ }
71
+
72
+ return { ssrcs, ...(reason !== undefined ? { reason } : {}) };
73
+ }