@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/rtp.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTP packet encode/decode — RFC 3550 Section 5.1
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RtpPacket } from './types.js';
|
|
6
|
+
import {
|
|
7
|
+
parseExtensionValues,
|
|
8
|
+
serializeExtension,
|
|
9
|
+
ONE_BYTE_PROFILE,
|
|
10
|
+
TWO_BYTE_PROFILE,
|
|
11
|
+
} from './extension.js';
|
|
12
|
+
|
|
13
|
+
/** Minimum RTP header size (no CSRC, no extension) */
|
|
14
|
+
const RTP_MIN_HEADER = 12;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns true if the buffer looks like an RTP packet:
|
|
18
|
+
* - version == 2
|
|
19
|
+
* - payload type NOT in the RTCP range 200–204
|
|
20
|
+
* - at least 12 bytes
|
|
21
|
+
*/
|
|
22
|
+
export function isRtpPacket(buf: Buffer): boolean {
|
|
23
|
+
if (buf.length < RTP_MIN_HEADER) return false;
|
|
24
|
+
const firstByte = buf[0];
|
|
25
|
+
if (firstByte === undefined) return false;
|
|
26
|
+
const version = (firstByte >> 6) & 0x03;
|
|
27
|
+
if (version !== 2) return false;
|
|
28
|
+
const secondByte = buf[1];
|
|
29
|
+
if (secondByte === undefined) return false;
|
|
30
|
+
// RTCP PT occupies the full second byte (200–207); check raw value, not masked
|
|
31
|
+
if (secondByte >= 200 && secondByte <= 207) return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Decode an RTP packet from a Buffer.
|
|
37
|
+
* Throws if the buffer is malformed or too short.
|
|
38
|
+
*/
|
|
39
|
+
export function decodeRtp(buf: Buffer): RtpPacket {
|
|
40
|
+
if (buf.length < RTP_MIN_HEADER) {
|
|
41
|
+
throw new RangeError(`RTP buffer too short: ${buf.length}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const byte0 = buf[0]!;
|
|
45
|
+
const byte1 = buf[1]!;
|
|
46
|
+
|
|
47
|
+
const version = (byte0 >> 6) & 0x03;
|
|
48
|
+
if (version !== 2) {
|
|
49
|
+
throw new Error(`Invalid RTP version: ${version}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const padding = Boolean(byte0 & 0x20);
|
|
53
|
+
const hasExtension = Boolean(byte0 & 0x10);
|
|
54
|
+
const csrcCount = byte0 & 0x0f;
|
|
55
|
+
|
|
56
|
+
const marker = Boolean(byte1 & 0x80);
|
|
57
|
+
const payloadType = byte1 & 0x7f;
|
|
58
|
+
|
|
59
|
+
const sequenceNumber = buf.readUInt16BE(2);
|
|
60
|
+
const timestamp = buf.readUInt32BE(4);
|
|
61
|
+
const ssrc = buf.readUInt32BE(8);
|
|
62
|
+
|
|
63
|
+
let offset = RTP_MIN_HEADER;
|
|
64
|
+
|
|
65
|
+
// CSRC list
|
|
66
|
+
if (buf.length < offset + csrcCount * 4) {
|
|
67
|
+
throw new RangeError('RTP buffer too short for CSRC list');
|
|
68
|
+
}
|
|
69
|
+
const csrcs: number[] = [];
|
|
70
|
+
for (let i = 0; i < csrcCount; i++) {
|
|
71
|
+
csrcs.push(buf.readUInt32BE(offset));
|
|
72
|
+
offset += 4;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Header extension
|
|
76
|
+
let headerExtension: RtpPacket['headerExtension'];
|
|
77
|
+
if (hasExtension) {
|
|
78
|
+
if (buf.length < offset + 4) {
|
|
79
|
+
throw new RangeError('RTP buffer too short for extension header');
|
|
80
|
+
}
|
|
81
|
+
const extProfile = buf.readUInt16BE(offset);
|
|
82
|
+
const extLengthWords = buf.readUInt16BE(offset + 2);
|
|
83
|
+
offset += 4;
|
|
84
|
+
const extBodyLen = extLengthWords * 4;
|
|
85
|
+
if (buf.length < offset + extBodyLen) {
|
|
86
|
+
throw new RangeError('RTP buffer too short for extension body');
|
|
87
|
+
}
|
|
88
|
+
const extBody = buf.subarray(offset, offset + extBodyLen) as Buffer;
|
|
89
|
+
offset += extBodyLen;
|
|
90
|
+
|
|
91
|
+
const values = parseExtensionValues(extProfile, extBody);
|
|
92
|
+
headerExtension = { id: extProfile, values };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Payload
|
|
96
|
+
let payloadEnd = buf.length;
|
|
97
|
+
if (padding) {
|
|
98
|
+
const padLen = buf[buf.length - 1]!;
|
|
99
|
+
payloadEnd = buf.length - padLen;
|
|
100
|
+
}
|
|
101
|
+
const payload = Buffer.from(buf.subarray(offset, payloadEnd));
|
|
102
|
+
|
|
103
|
+
const result: RtpPacket = {
|
|
104
|
+
version: 2,
|
|
105
|
+
padding,
|
|
106
|
+
extension: hasExtension,
|
|
107
|
+
csrcCount,
|
|
108
|
+
marker,
|
|
109
|
+
payloadType,
|
|
110
|
+
sequenceNumber,
|
|
111
|
+
timestamp,
|
|
112
|
+
ssrc,
|
|
113
|
+
csrcs,
|
|
114
|
+
payload,
|
|
115
|
+
};
|
|
116
|
+
if (headerExtension !== undefined) {
|
|
117
|
+
result.headerExtension = headerExtension;
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Encode an RTP packet into a Buffer.
|
|
124
|
+
*/
|
|
125
|
+
export function encodeRtp(packet: RtpPacket): Buffer {
|
|
126
|
+
const csrcCount = packet.csrcs.length;
|
|
127
|
+
|
|
128
|
+
// Build extension bytes if present
|
|
129
|
+
let extBuf: Buffer = Buffer.alloc(0);
|
|
130
|
+
if (packet.headerExtension) {
|
|
131
|
+
extBuf = serializeExtension(packet.headerExtension);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build padding
|
|
135
|
+
let padBuf: Buffer = Buffer.alloc(0);
|
|
136
|
+
if (packet.padding) {
|
|
137
|
+
// Add minimal 1-byte padding (caller controls actual padding amount via payload)
|
|
138
|
+
// The last byte of padding holds the count.
|
|
139
|
+
// We always emit padding as part of the payload in this implementation.
|
|
140
|
+
// If the caller set padding=true the payload should already include it.
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const headerLen = RTP_MIN_HEADER + csrcCount * 4;
|
|
144
|
+
const totalLen = headerLen + extBuf.length + packet.payload.length + padBuf.length;
|
|
145
|
+
const buf = Buffer.allocUnsafe(totalLen);
|
|
146
|
+
|
|
147
|
+
const hasExt = packet.headerExtension !== undefined || packet.extension;
|
|
148
|
+
|
|
149
|
+
const byte0 =
|
|
150
|
+
(2 << 6) |
|
|
151
|
+
(packet.padding ? 0x20 : 0) |
|
|
152
|
+
(hasExt ? 0x10 : 0) |
|
|
153
|
+
(csrcCount & 0x0f);
|
|
154
|
+
const byte1 = ((packet.marker ? 1 : 0) << 7) | (packet.payloadType & 0x7f);
|
|
155
|
+
|
|
156
|
+
buf[0] = byte0;
|
|
157
|
+
buf[1] = byte1;
|
|
158
|
+
buf.writeUInt16BE(packet.sequenceNumber, 2);
|
|
159
|
+
buf.writeUInt32BE(packet.timestamp >>> 0, 4);
|
|
160
|
+
buf.writeUInt32BE(packet.ssrc >>> 0, 8);
|
|
161
|
+
|
|
162
|
+
let offset = RTP_MIN_HEADER;
|
|
163
|
+
for (const csrc of packet.csrcs) {
|
|
164
|
+
buf.writeUInt32BE(csrc >>> 0, offset);
|
|
165
|
+
offset += 4;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (extBuf.length > 0) {
|
|
169
|
+
extBuf.copy(buf, offset);
|
|
170
|
+
offset += extBuf.length;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
packet.payload.copy(buf, offset);
|
|
174
|
+
|
|
175
|
+
return buf;
|
|
176
|
+
}
|
package/src/sequence.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
|
|
6
|
+
const MAX_SEQ = 0x10000; // 65536
|
|
7
|
+
const HALF_MAX_SEQ = 0x8000; // 32768
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns a - b, wrap-around aware.
|
|
11
|
+
* Result is in range (-32768, 32768].
|
|
12
|
+
*/
|
|
13
|
+
export function seqDiff(a: number, b: number): number {
|
|
14
|
+
const diff = ((a - b) & 0xffff) >>> 0;
|
|
15
|
+
if (diff === 0) return 0;
|
|
16
|
+
// If diff >= half the range, it wrapped around in the negative direction
|
|
17
|
+
return diff < HALF_MAX_SEQ ? diff : diff - MAX_SEQ;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Returns true if a < b (wrap-around aware) */
|
|
21
|
+
export function seqLt(a: number, b: number): boolean {
|
|
22
|
+
return seqDiff(a, b) < 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Returns true if a <= b (wrap-around aware) */
|
|
26
|
+
export function seqLte(a: number, b: number): boolean {
|
|
27
|
+
return seqDiff(a, b) <= 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Returns true if a > b (wrap-around aware) */
|
|
31
|
+
export function seqGt(a: number, b: number): boolean {
|
|
32
|
+
return seqDiff(a, b) > 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// NTP epoch is Jan 1, 1900; Unix epoch is Jan 1, 1970 — difference in seconds
|
|
36
|
+
const NTP_UNIX_OFFSET_S = BigInt(70 * 365 * 24 * 3600 + 17 * 24 * 3600); // 70 years + 17 leap days
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert a 64-bit NTP timestamp to Unix milliseconds.
|
|
40
|
+
* NTP format: upper 32 bits = seconds since 1900-01-01,
|
|
41
|
+
* lower 32 bits = fractional seconds.
|
|
42
|
+
*/
|
|
43
|
+
export function ntpToUnix(ntp: bigint): number {
|
|
44
|
+
const seconds = (ntp >> 32n) - NTP_UNIX_OFFSET_S;
|
|
45
|
+
const fraction = ntp & 0xffffffffn;
|
|
46
|
+
const ms = (fraction * 1000n) >> 32n;
|
|
47
|
+
return Number(seconds) * 1000 + Number(ms);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convert Unix milliseconds to a 64-bit NTP timestamp.
|
|
52
|
+
*/
|
|
53
|
+
export function unixToNtp(ms: number): bigint {
|
|
54
|
+
const totalMs = BigInt(ms);
|
|
55
|
+
const seconds = totalMs / 1000n + NTP_UNIX_OFFSET_S;
|
|
56
|
+
const remainder = totalMs % 1000n;
|
|
57
|
+
const fraction = (remainder * (1n << 32n)) / 1000n;
|
|
58
|
+
return (seconds << 32n) | fraction;
|
|
59
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// RFC 3550 RTP/RTCP types
|
|
2
|
+
|
|
3
|
+
export interface RtpPacket {
|
|
4
|
+
version: 2;
|
|
5
|
+
padding: boolean;
|
|
6
|
+
extension: boolean;
|
|
7
|
+
csrcCount: number;
|
|
8
|
+
marker: boolean;
|
|
9
|
+
payloadType: number;
|
|
10
|
+
sequenceNumber: number;
|
|
11
|
+
timestamp: number;
|
|
12
|
+
ssrc: number;
|
|
13
|
+
csrcs: number[];
|
|
14
|
+
headerExtension?: RtpHeaderExtension;
|
|
15
|
+
payload: Buffer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RtpHeaderExtension {
|
|
19
|
+
id: number; // profile: 0xBEDE = one-byte, 0x1000 = two-byte
|
|
20
|
+
values: RtpExtensionValue[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RtpExtensionValue {
|
|
24
|
+
id: number;
|
|
25
|
+
data: Buffer;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export enum RtcpPacketType {
|
|
29
|
+
SR = 200,
|
|
30
|
+
RR = 201,
|
|
31
|
+
SDES = 202,
|
|
32
|
+
BYE = 203,
|
|
33
|
+
APP = 204,
|
|
34
|
+
TransportFeedback = 205,
|
|
35
|
+
PayloadFeedback = 206,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RtcpHeader {
|
|
39
|
+
version: 2;
|
|
40
|
+
padding: boolean;
|
|
41
|
+
count: number; // also FMT for feedback packets
|
|
42
|
+
packetType: RtcpPacketType;
|
|
43
|
+
length: number; // in 32-bit words minus 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RtcpSenderReport {
|
|
47
|
+
ssrc: number;
|
|
48
|
+
ntpTimestamp: bigint; // 64-bit NTP timestamp
|
|
49
|
+
rtpTimestamp: number;
|
|
50
|
+
packetCount: number;
|
|
51
|
+
octetCount: number;
|
|
52
|
+
reportBlocks: ReportBlock[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RtcpReceiverReport {
|
|
56
|
+
ssrc: number;
|
|
57
|
+
reportBlocks: ReportBlock[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ReportBlock {
|
|
61
|
+
ssrc: number;
|
|
62
|
+
fractionLost: number; // 8-bit fraction
|
|
63
|
+
cumulativeLost: number; // 24-bit signed
|
|
64
|
+
extendedHighestSeq: number;
|
|
65
|
+
jitter: number;
|
|
66
|
+
lastSR: number;
|
|
67
|
+
delaySinceLastSR: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RtcpSdes {
|
|
71
|
+
chunks: SdesChunk[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SdesChunk {
|
|
75
|
+
ssrc: number;
|
|
76
|
+
items: SdesItem[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SdesItem {
|
|
80
|
+
type: number; // 1=CNAME, 2=NAME, 3=EMAIL, 4=PHONE, 5=LOC, 6=TOOL, 7=NOTE, 8=PRIV
|
|
81
|
+
text: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface RtcpBye {
|
|
85
|
+
ssrcs: number[];
|
|
86
|
+
reason?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Feedback messages (RFC 4585)
|
|
90
|
+
export interface RtcpNack {
|
|
91
|
+
senderSsrc: number;
|
|
92
|
+
mediaSsrc: number;
|
|
93
|
+
pid: number;
|
|
94
|
+
blp: number; // bitmask of following lost packets
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface RtcpPli {
|
|
98
|
+
senderSsrc: number;
|
|
99
|
+
mediaSsrc: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface RtcpFir {
|
|
103
|
+
senderSsrc: number;
|
|
104
|
+
entries: FirEntry[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface FirEntry {
|
|
108
|
+
ssrc: number;
|
|
109
|
+
seqNumber: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// REMB (draft-alvestrand-rmcat-remb)
|
|
113
|
+
export interface RtcpRemb {
|
|
114
|
+
senderSsrc: number;
|
|
115
|
+
mediaSsrc: number;
|
|
116
|
+
bitrate: number; // bits per second
|
|
117
|
+
ssrcs: number[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type RtcpPacket =
|
|
121
|
+
| { type: 'sr'; packet: RtcpSenderReport }
|
|
122
|
+
| { type: 'rr'; packet: RtcpReceiverReport }
|
|
123
|
+
| { type: 'sdes'; packet: RtcpSdes }
|
|
124
|
+
| { type: 'bye'; packet: RtcpBye }
|
|
125
|
+
| { type: 'nack'; packet: RtcpNack }
|
|
126
|
+
| { type: 'pli'; packet: RtcpPli }
|
|
127
|
+
| { type: 'fir'; packet: RtcpFir }
|
|
128
|
+
| { type: 'remb'; packet: RtcpRemb }
|
|
129
|
+
| { type: 'unknown'; raw: Buffer };
|