@ariaflowagents/livekit-plugin-transport-sip 0.9.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/README.md +22 -0
- package/dist/audio_input.d.ts +24 -0
- package/dist/audio_input.d.ts.map +1 -0
- package/dist/audio_input.js +58 -0
- package/dist/audio_input.js.map +1 -0
- package/dist/audio_output.d.ts +34 -0
- package/dist/audio_output.d.ts.map +1 -0
- package/dist/audio_output.js +133 -0
- package/dist/audio_output.js.map +1 -0
- package/dist/codec/g711.d.ts +17 -0
- package/dist/codec/g711.d.ts.map +1 -0
- package/dist/codec/g711.js +123 -0
- package/dist/codec/g711.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/jssip_signaling.d.ts +76 -0
- package/dist/jssip_signaling.d.ts.map +1 -0
- package/dist/jssip_signaling.js +255 -0
- package/dist/jssip_signaling.js.map +1 -0
- package/dist/rtp/jitter_buffer.d.ts +19 -0
- package/dist/rtp/jitter_buffer.d.ts.map +1 -0
- package/dist/rtp/jitter_buffer.js +51 -0
- package/dist/rtp/jitter_buffer.js.map +1 -0
- package/dist/rtp/rtp_packet.d.ts +23 -0
- package/dist/rtp/rtp_packet.d.ts.map +1 -0
- package/dist/rtp/rtp_packet.js +63 -0
- package/dist/rtp/rtp_packet.js.map +1 -0
- package/dist/rtp/rtp_session.d.ts +54 -0
- package/dist/rtp/rtp_session.d.ts.map +1 -0
- package/dist/rtp/rtp_session.js +162 -0
- package/dist/rtp/rtp_session.js.map +1 -0
- package/dist/sdp_g711.d.ts +18 -0
- package/dist/sdp_g711.d.ts.map +1 -0
- package/dist/sdp_g711.js +74 -0
- package/dist/sdp_g711.js.map +1 -0
- package/dist/server.d.ts +76 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +197 -0
- package/dist/server.js.map +1 -0
- package/dist/sip_signaling.d.ts +73 -0
- package/dist/sip_signaling.d.ts.map +1 -0
- package/dist/sip_signaling.js +553 -0
- package/dist/sip_signaling.js.map +1 -0
- package/dist/text_output.d.ts +16 -0
- package/dist/text_output.d.ts.map +1 -0
- package/dist/text_output.js +35 -0
- package/dist/text_output.js.map +1 -0
- package/dist/transport_adapter.d.ts +22 -0
- package/dist/transport_adapter.d.ts.map +1 -0
- package/dist/transport_adapter.js +44 -0
- package/dist/transport_adapter.js.map +1 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +34 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SIP Signaling implementation using JsSIP library for WebSocket transport.
|
|
3
|
+
*
|
|
4
|
+
* @experimental
|
|
5
|
+
*
|
|
6
|
+
* WARNING: JsSIP is a WebRTC-only library. Its `session.answer()` API is a
|
|
7
|
+
* WebRTC API and cannot bridge to the raw RTP pipeline used by `SIPAudioInput`
|
|
8
|
+
* and `SIPAudioOutput`. This file is kept for reference but the WebSocket SIP
|
|
9
|
+
* signaling path is architecturally incompatible with the RTP transport.
|
|
10
|
+
*
|
|
11
|
+
* For production use, use `sip_signaling.ts` (UDP-based, compatible with RTP).
|
|
12
|
+
*
|
|
13
|
+
* JsSIP supports:
|
|
14
|
+
* - SIP over WebSocket (secure WSS and non-secure WS)
|
|
15
|
+
* - WebRTC for media (NOT compatible with raw RTP)
|
|
16
|
+
* - Full SIP protocol compliance (INVITE, ACK, BYE, etc.)
|
|
17
|
+
*/
|
|
18
|
+
import { UA, WebSocketInterface } from 'jssip';
|
|
19
|
+
/**
|
|
20
|
+
* SDP offer for G.711 μ-law audio.
|
|
21
|
+
*/
|
|
22
|
+
const G711_SDP_OFFER = (localAddress, rtpPort) => `v=0\r\n` +
|
|
23
|
+
`o=- ${Date.now()} ${Date.now()} IN IP4 ${localAddress}\r\n` +
|
|
24
|
+
`s=AriaFlow Voice Agent\r\n` +
|
|
25
|
+
`c=IN IP4 ${localAddress}\r\n` +
|
|
26
|
+
`t=0 0\r\n` +
|
|
27
|
+
`m=audio ${rtpPort} RTP/AVP 0 8\r\n` + // 0 = G.711 μ-law (PCMU), 8 = G.711 A-law (PCMA)
|
|
28
|
+
`a=rtpmap:0 PCMU/8000\r\n` +
|
|
29
|
+
`a=rtpmap:8 PCMA/8000\r\n` +
|
|
30
|
+
`a=fmtp:0\r\n` +
|
|
31
|
+
`a=fmtp:8\r\n` +
|
|
32
|
+
`a=ptime:20\r\n` +
|
|
33
|
+
`a=sendrecv\r\n`;
|
|
34
|
+
/**
|
|
35
|
+
* SIP Signaling handler using JsSIP over WebSocket.
|
|
36
|
+
*
|
|
37
|
+
* This implementation uses the JsSIP library for WebSocket-based SIP
|
|
38
|
+
* signaling, commonly used with WebRTC and modern SIP providers.
|
|
39
|
+
*/
|
|
40
|
+
export class JsSIPSignaling {
|
|
41
|
+
options;
|
|
42
|
+
ua = null;
|
|
43
|
+
activeSessions = new Map();
|
|
44
|
+
nextRtpPort;
|
|
45
|
+
localAddress;
|
|
46
|
+
constructor(options) {
|
|
47
|
+
this.options = options;
|
|
48
|
+
this.localAddress = options.localAddress;
|
|
49
|
+
this.nextRtpPort = options.rtpPortStart ?? 10000;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Start the SIP user agent and begin listening for calls over WebSocket.
|
|
53
|
+
*
|
|
54
|
+
* @param onInvite - Callback when a new INVITE is received
|
|
55
|
+
* @param onBye - Callback when a BYE is received (remote hangup)
|
|
56
|
+
*/
|
|
57
|
+
async start(onInvite, onBye) {
|
|
58
|
+
// Create WebSocket URL
|
|
59
|
+
const wsProtocol = this.options.secureWebSocket === false ? 'ws' : 'wss';
|
|
60
|
+
const wsUrl = `${wsProtocol}://${this.options.wsServerHost || this.localAddress}:${this.options.wsServerPort || 8080}/ws`;
|
|
61
|
+
// Create SIP URI
|
|
62
|
+
const sipUri = `sip:${this.options.sipUsername || 'agent'}@${this.options.sipDomain || this.localAddress}`;
|
|
63
|
+
// Create JsSIP User Agent
|
|
64
|
+
this.ua = new UA({
|
|
65
|
+
uri: sipUri,
|
|
66
|
+
sockets: [new WebSocketInterface(wsUrl)],
|
|
67
|
+
password: this.options.sipPassword,
|
|
68
|
+
register: this.options.shouldRegister !== false, // Register by default
|
|
69
|
+
});
|
|
70
|
+
// Set up event handlers for incoming calls
|
|
71
|
+
this.ua.on('newRTCSession', (data) => {
|
|
72
|
+
this.handleIncomingInvite(data.session, onInvite, onBye);
|
|
73
|
+
});
|
|
74
|
+
// Set up connection event handlers
|
|
75
|
+
this.ua.on('connected', () => {
|
|
76
|
+
console.log(`[JsSIPSignaling] Connected to SIP server: ${wsUrl}`);
|
|
77
|
+
});
|
|
78
|
+
this.ua.on('disconnected', () => {
|
|
79
|
+
console.warn('[JsSIPSignaling] Disconnected from SIP server');
|
|
80
|
+
});
|
|
81
|
+
this.ua.on('registered', () => {
|
|
82
|
+
console.log(`[JsSIPSignaling] Registered as: ${sipUri}`);
|
|
83
|
+
});
|
|
84
|
+
this.ua.on('registrationFailed', () => {
|
|
85
|
+
console.error('[JsSIPSignaling] Registration failed');
|
|
86
|
+
});
|
|
87
|
+
// Start the user agent
|
|
88
|
+
this.ua.start();
|
|
89
|
+
console.log(`[JsSIPSignaling] Started with WebSocket: ${wsUrl}`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Handle an incoming INVITE request.
|
|
93
|
+
*/
|
|
94
|
+
async handleIncomingInvite(session, onInvite, onBye) {
|
|
95
|
+
const callId = session.request?.callId || `call-${Date.now()}`;
|
|
96
|
+
const remoteSdp = session.request?.body || '';
|
|
97
|
+
console.log(`[JsSIPSignaling] Received INVITE for call: ${callId}`);
|
|
98
|
+
console.log(`[JsSIPSignaling] From: ${session.remote_identity?.uri?.toString() || session}`);
|
|
99
|
+
// Allocate RTP port for this call
|
|
100
|
+
const rtpPort = this.nextRtpPort;
|
|
101
|
+
this.nextRtpPort += 2; // RTP uses even ports
|
|
102
|
+
// Generate SDP answer with our RTP port
|
|
103
|
+
const sdpAnswer = G711_SDP_OFFER(this.localAddress, rtpPort);
|
|
104
|
+
// Set up session state handlers
|
|
105
|
+
session.on('progress', () => {
|
|
106
|
+
console.log(`[JsSIPSignaling] Call ${callId} in progress`);
|
|
107
|
+
});
|
|
108
|
+
session.on('confirmed', () => {
|
|
109
|
+
console.log(`[JsSIPSignaling] Call ${callId} established`);
|
|
110
|
+
});
|
|
111
|
+
session.on('ended', () => {
|
|
112
|
+
console.log(`[JsSIPSignaling] Call ${callId} ended`);
|
|
113
|
+
this.activeSessions.delete(callId);
|
|
114
|
+
if (onBye) {
|
|
115
|
+
onBye(callId);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
session.on('failed', (error) => {
|
|
119
|
+
console.error(`[JsSIPSignaling] Call ${callId} failed:`, error);
|
|
120
|
+
this.activeSessions.delete(callId);
|
|
121
|
+
});
|
|
122
|
+
// Accept the invitation with our SDP
|
|
123
|
+
try {
|
|
124
|
+
await session.answer({
|
|
125
|
+
extraHeaders: ['Content-Type: application/sdp'],
|
|
126
|
+
});
|
|
127
|
+
// Track the active session
|
|
128
|
+
this.activeSessions.set(callId, { session, rtpPort });
|
|
129
|
+
// Notify the application about the new call
|
|
130
|
+
onInvite(callId, remoteSdp, rtpPort);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.error(`[JsSIPSignaling] Error accepting call ${callId}:`, error);
|
|
134
|
+
try {
|
|
135
|
+
session.terminate();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Ignore terminate errors
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Hang up an active call.
|
|
144
|
+
*/
|
|
145
|
+
async hangup(callId) {
|
|
146
|
+
const activeSession = this.activeSessions.get(callId);
|
|
147
|
+
if (!activeSession) {
|
|
148
|
+
console.warn(`[JsSIPSignaling] No active session for call: ${callId}`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await activeSession.session.terminate();
|
|
153
|
+
this.activeSessions.delete(callId);
|
|
154
|
+
console.log(`[JsSIPSignaling] Hung up call: ${callId}`);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error(`[JsSIPSignaling] Error hanging up call ${callId}:`, error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Make an outgoing call.
|
|
162
|
+
*/
|
|
163
|
+
async makeCall(targetUri, onInvite, onBye) {
|
|
164
|
+
if (!this.ua) {
|
|
165
|
+
throw new Error('[JsSIPSignaling] User agent not started');
|
|
166
|
+
}
|
|
167
|
+
// Allocate RTP port for this call
|
|
168
|
+
const rtpPort = this.nextRtpPort;
|
|
169
|
+
this.nextRtpPort += 2;
|
|
170
|
+
// Generate SDP offer with our RTP port
|
|
171
|
+
const sdpOffer = G711_SDP_OFFER(this.localAddress, rtpPort);
|
|
172
|
+
// Create the call session
|
|
173
|
+
const session = this.ua.call(targetUri, {
|
|
174
|
+
extraHeaders: ['Content-Type: application/sdp'],
|
|
175
|
+
eventHandlers: {
|
|
176
|
+
progress: () => {
|
|
177
|
+
const callId = session.request?.callId || `call-${Date.now()}`;
|
|
178
|
+
console.log(`[JsSIPSignaling] Outgoing call ${callId} in progress`);
|
|
179
|
+
},
|
|
180
|
+
confirmed: () => {
|
|
181
|
+
const callId = session.request?.callId || `call-${Date.now()}`;
|
|
182
|
+
console.log(`[JsSIPSignaling] Outgoing call ${callId} accepted`);
|
|
183
|
+
this.activeSessions.set(callId, { session, rtpPort });
|
|
184
|
+
if (onInvite) {
|
|
185
|
+
onInvite(callId, session.request?.body || '', rtpPort);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
ended: () => {
|
|
189
|
+
const callId = session.request?.callId || `call-${Date.now()}`;
|
|
190
|
+
console.log(`[JsSIPSignaling] Outgoing call ${callId} ended`);
|
|
191
|
+
this.activeSessions.delete(callId);
|
|
192
|
+
if (onBye) {
|
|
193
|
+
onBye(callId);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
failed: (error) => {
|
|
197
|
+
const callId = session.request?.callId || `call-${Date.now()}`;
|
|
198
|
+
console.error(`[JsSIPSignaling] Outgoing call ${callId} failed:`, error);
|
|
199
|
+
this.activeSessions.delete(callId);
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
return session.request?.callId || `call-${Date.now()}`;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Stop the SIP user agent and hang up all active calls.
|
|
207
|
+
*/
|
|
208
|
+
async stop() {
|
|
209
|
+
if (!this.ua) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Hang up all active calls
|
|
213
|
+
const hangupPromises = Array.from(this.activeSessions.entries()).map(async ([callId, { session }]) => {
|
|
214
|
+
try {
|
|
215
|
+
await session.terminate();
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.error(`[JsSIPSignaling] Error hanging up call ${callId} during shutdown:`, error);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
await Promise.allSettled(hangupPromises);
|
|
222
|
+
this.activeSessions.clear();
|
|
223
|
+
// Stop the user agent
|
|
224
|
+
this.ua.stop();
|
|
225
|
+
this.ua = null;
|
|
226
|
+
console.log('[JsSIPSignaling] Stopped');
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Get the RTP port allocated for a specific call.
|
|
230
|
+
*/
|
|
231
|
+
getRtpPort(callId) {
|
|
232
|
+
return this.activeSessions.get(callId)?.rtpPort;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Check if the user agent is registered.
|
|
236
|
+
*/
|
|
237
|
+
get isRegistered() {
|
|
238
|
+
return this.ua?.isRegistered() ?? false;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Get the user agent status.
|
|
242
|
+
*/
|
|
243
|
+
get status() {
|
|
244
|
+
if (!this.ua)
|
|
245
|
+
return 'disconnected';
|
|
246
|
+
const uaStatus = this.ua.status;
|
|
247
|
+
// JsSIP UA status: 0 = Init, 1 = Ready, 2 = UserRegistered, 3 = Unregistered
|
|
248
|
+
if (uaStatus === 0)
|
|
249
|
+
return 'connecting';
|
|
250
|
+
if (uaStatus === 1 || uaStatus === 2)
|
|
251
|
+
return 'connected';
|
|
252
|
+
return 'disconnected';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=jssip_signaling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jssip_signaling.js","sourceRoot":"","sources":["../src/jssip_signaling.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RtpPacket } from './rtp_packet.js';
|
|
2
|
+
/**
|
|
3
|
+
* Simple jitter buffer that reorders RTP packets by sequence number.
|
|
4
|
+
*
|
|
5
|
+
* Fixed depth (default 3 packets = 60ms at 20ms/packet).
|
|
6
|
+
* Late packets are dropped. Missing packets cause the buffer to skip ahead
|
|
7
|
+
* when it reaches max depth.
|
|
8
|
+
*/
|
|
9
|
+
export declare class JitterBuffer {
|
|
10
|
+
private buffer;
|
|
11
|
+
private lastDeliveredSeq;
|
|
12
|
+
private nextExpectedSeq;
|
|
13
|
+
private maxDepth;
|
|
14
|
+
constructor(maxDepth?: number);
|
|
15
|
+
push(packet: RtpPacket): void;
|
|
16
|
+
pull(): RtpPacket | null;
|
|
17
|
+
private isOlder;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=jitter_buffer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jitter_buffer.d.ts","sourceRoot":"","sources":["../../src/rtp/jitter_buffer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;;;;GAMG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,gBAAgB,CAAc;IACtC,OAAO,CAAC,eAAe,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,GAAE,MAAU;IAIhC,IAAI,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IAe7B,IAAI,IAAI,SAAS,GAAG,IAAI;IA0BxB,OAAO,CAAC,OAAO;CAIhB"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple jitter buffer that reorders RTP packets by sequence number.
|
|
3
|
+
*
|
|
4
|
+
* Fixed depth (default 3 packets = 60ms at 20ms/packet).
|
|
5
|
+
* Late packets are dropped. Missing packets cause the buffer to skip ahead
|
|
6
|
+
* when it reaches max depth.
|
|
7
|
+
*/
|
|
8
|
+
export class JitterBuffer {
|
|
9
|
+
buffer = new Map();
|
|
10
|
+
lastDeliveredSeq = -1;
|
|
11
|
+
nextExpectedSeq = -1;
|
|
12
|
+
maxDepth;
|
|
13
|
+
constructor(maxDepth = 3) {
|
|
14
|
+
this.maxDepth = maxDepth;
|
|
15
|
+
}
|
|
16
|
+
push(packet) {
|
|
17
|
+
if (this.lastDeliveredSeq >= 0 &&
|
|
18
|
+
this.isOlder(packet.sequenceNumber, this.lastDeliveredSeq)) {
|
|
19
|
+
return; // Late packet, drop
|
|
20
|
+
}
|
|
21
|
+
this.buffer.set(packet.sequenceNumber, packet);
|
|
22
|
+
if (this.nextExpectedSeq < 0) {
|
|
23
|
+
this.nextExpectedSeq = packet.sequenceNumber;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
pull() {
|
|
27
|
+
if (this.buffer.has(this.nextExpectedSeq)) {
|
|
28
|
+
const packet = this.buffer.get(this.nextExpectedSeq);
|
|
29
|
+
this.buffer.delete(this.nextExpectedSeq);
|
|
30
|
+
this.lastDeliveredSeq = this.nextExpectedSeq;
|
|
31
|
+
this.nextExpectedSeq = (this.nextExpectedSeq + 1) & 0xffff;
|
|
32
|
+
return packet;
|
|
33
|
+
}
|
|
34
|
+
// Buffer full -- skip to oldest available
|
|
35
|
+
if (this.buffer.size >= this.maxDepth) {
|
|
36
|
+
const sorted = Array.from(this.buffer.keys()).sort((a, b) => this.isOlder(a, b) ? -1 : 1);
|
|
37
|
+
const oldestSeq = sorted[0];
|
|
38
|
+
const packet = this.buffer.get(oldestSeq);
|
|
39
|
+
this.buffer.delete(oldestSeq);
|
|
40
|
+
this.lastDeliveredSeq = oldestSeq;
|
|
41
|
+
this.nextExpectedSeq = (oldestSeq + 1) & 0xffff;
|
|
42
|
+
return packet;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
isOlder(a, b) {
|
|
47
|
+
const diff = (b - a) & 0xffff;
|
|
48
|
+
return diff > 0 && diff < 0x8000;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=jitter_buffer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jitter_buffer.js","sourceRoot":"","sources":["../../src/rtp/jitter_buffer.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,MAAM,OAAO,YAAY;IACf,MAAM,GAA2B,IAAI,GAAG,EAAE,CAAC;IAC3C,gBAAgB,GAAW,CAAC,CAAC,CAAC;IAC9B,eAAe,GAAW,CAAC,CAAC,CAAC;IAC7B,QAAQ,CAAS;IAEzB,YAAY,WAAmB,CAAC;QAC9B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,IAAI,CAAC,MAAiB;QACpB,IACE,IAAI,CAAC,gBAAgB,IAAI,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAC,EAC1D,CAAC;YACD,OAAO,CAAC,oBAAoB;QAC9B,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QAE/C,IAAI,IAAI,CAAC,eAAe,GAAG,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,cAAc,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;YAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,CAAE,CAAC;YACtD,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;YACzC,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,eAAe,CAAC;YAC7C,IAAI,CAAC,eAAe,GAAG,CAAC,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;YAC3D,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,0CAA0C;QAC1C,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACtC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAC1D,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAC5B,CAAC;YAEF,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAE,CAAC;YAC3C,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC9B,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAC;YAClC,IAAI,CAAC,eAAe,GAAG,CAAC,SAAS,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;YAChD,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAEO,OAAO,CAAC,CAAS,EAAE,CAAS;QAClC,MAAM,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC;QAC9B,OAAO,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,MAAM,CAAC;IACnC,CAAC;CACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTP packet parser and builder (RFC 3550).
|
|
3
|
+
*
|
|
4
|
+
* For G.711 at 8000 Hz with 20ms packets:
|
|
5
|
+
* Payload: 160 bytes (160 samples * 1 byte per G.711 sample)
|
|
6
|
+
* Timestamp increment: 160 per packet
|
|
7
|
+
* Total: 12 (header) + 160 (payload) = 172 bytes
|
|
8
|
+
*/
|
|
9
|
+
export interface RtpPacket {
|
|
10
|
+
version: number;
|
|
11
|
+
padding: boolean;
|
|
12
|
+
extension: boolean;
|
|
13
|
+
csrcCount: number;
|
|
14
|
+
marker: boolean;
|
|
15
|
+
payloadType: number;
|
|
16
|
+
sequenceNumber: number;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
ssrc: number;
|
|
19
|
+
payload: Uint8Array;
|
|
20
|
+
}
|
|
21
|
+
export declare function parseRtpPacket(data: Buffer): RtpPacket | null;
|
|
22
|
+
export declare function buildRtpPacket(payloadType: number, sequenceNumber: number, timestamp: number, ssrc: number, payload: Uint8Array, marker?: boolean): Buffer;
|
|
23
|
+
//# sourceMappingURL=rtp_packet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rtp_packet.d.ts","sourceRoot":"","sources":["../../src/rtp/rtp_packet.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,UAAU,CAAC;CACrB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAuD7D;AAED,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,UAAU,EACnB,MAAM,GAAE,OAAe,GACtB,MAAM,CAUR"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RTP packet parser and builder (RFC 3550).
|
|
3
|
+
*
|
|
4
|
+
* For G.711 at 8000 Hz with 20ms packets:
|
|
5
|
+
* Payload: 160 bytes (160 samples * 1 byte per G.711 sample)
|
|
6
|
+
* Timestamp increment: 160 per packet
|
|
7
|
+
* Total: 12 (header) + 160 (payload) = 172 bytes
|
|
8
|
+
*/
|
|
9
|
+
export function parseRtpPacket(data) {
|
|
10
|
+
if (data.length < 12)
|
|
11
|
+
return null;
|
|
12
|
+
const firstByte = data[0];
|
|
13
|
+
const version = (firstByte >> 6) & 0x03;
|
|
14
|
+
if (version !== 2)
|
|
15
|
+
return null;
|
|
16
|
+
const padding = ((firstByte >> 5) & 0x01) === 1;
|
|
17
|
+
const extension = ((firstByte >> 4) & 0x01) === 1;
|
|
18
|
+
const csrcCount = firstByte & 0x0f;
|
|
19
|
+
const secondByte = data[1];
|
|
20
|
+
const marker = ((secondByte >> 7) & 0x01) === 1;
|
|
21
|
+
const payloadType = secondByte & 0x7f;
|
|
22
|
+
const sequenceNumber = data.readUInt16BE(2);
|
|
23
|
+
const timestamp = data.readUInt32BE(4);
|
|
24
|
+
const ssrc = data.readUInt32BE(8);
|
|
25
|
+
const headerLength = 12 + csrcCount * 4;
|
|
26
|
+
let payloadOffset = headerLength;
|
|
27
|
+
if (extension && data.length > headerLength + 4) {
|
|
28
|
+
const extensionLength = data.readUInt16BE(headerLength + 2);
|
|
29
|
+
payloadOffset = headerLength + 4 + extensionLength * 4;
|
|
30
|
+
}
|
|
31
|
+
if (payloadOffset >= data.length)
|
|
32
|
+
return null;
|
|
33
|
+
let payloadLength = data.length - payloadOffset;
|
|
34
|
+
if (padding && payloadLength > 0) {
|
|
35
|
+
const paddingLength = data[data.length - 1];
|
|
36
|
+
payloadLength -= paddingLength;
|
|
37
|
+
}
|
|
38
|
+
if (payloadLength <= 0)
|
|
39
|
+
return null;
|
|
40
|
+
const payload = new Uint8Array(data.buffer, data.byteOffset + payloadOffset, payloadLength);
|
|
41
|
+
return {
|
|
42
|
+
version,
|
|
43
|
+
padding,
|
|
44
|
+
extension,
|
|
45
|
+
csrcCount,
|
|
46
|
+
marker,
|
|
47
|
+
payloadType,
|
|
48
|
+
sequenceNumber,
|
|
49
|
+
timestamp,
|
|
50
|
+
ssrc,
|
|
51
|
+
payload,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function buildRtpPacket(payloadType, sequenceNumber, timestamp, ssrc, payload, marker = false) {
|
|
55
|
+
const header = Buffer.alloc(12);
|
|
56
|
+
header[0] = 0x80; // Version=2
|
|
57
|
+
header[1] = (marker ? 0x80 : 0x00) | (payloadType & 0x7f);
|
|
58
|
+
header.writeUInt16BE(sequenceNumber & 0xffff, 2);
|
|
59
|
+
header.writeUInt32BE(timestamp >>> 0, 4);
|
|
60
|
+
header.writeUInt32BE(ssrc >>> 0, 8);
|
|
61
|
+
return Buffer.concat([header, Buffer.from(payload)]);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=rtp_packet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rtp_packet.js","sourceRoot":"","sources":["../../src/rtp/rtp_packet.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { type Codec } from '@ariaflow/livekit-plugin/codec/g711';
|
|
3
|
+
export interface RtpSessionOptions {
|
|
4
|
+
localPort: number;
|
|
5
|
+
jitterDepth?: number;
|
|
6
|
+
packetDurationMs?: number;
|
|
7
|
+
/**
|
|
8
|
+
* When true, outbound RTP is clocked at `packetDurationMs` (default 20ms)
|
|
9
|
+
* regardless of application timing: queue drains one frame per tick; idle
|
|
10
|
+
* ticks send codec-encoded silence. Default false preserves immediate
|
|
11
|
+
* `sendAudio` behavior.
|
|
12
|
+
*/
|
|
13
|
+
continuousPacing?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Manages a bidirectional RTP stream over UDP.
|
|
17
|
+
*
|
|
18
|
+
* Inbound: receives UDP datagrams, parses RTP, passes through jitter buffer,
|
|
19
|
+
* decodes with codec selected by RTP payload type (PCMU=0, PCMA=8), emits
|
|
20
|
+
* 'audio' events with PCM Int16Array data.
|
|
21
|
+
*
|
|
22
|
+
* Outbound: accepts PCM Int16Array, encodes with the negotiated outbound
|
|
23
|
+
* codec, builds RTP packets, sends as UDP datagrams.
|
|
24
|
+
*/
|
|
25
|
+
export declare class RtpSession extends EventEmitter {
|
|
26
|
+
private socket;
|
|
27
|
+
private jitterBuffer;
|
|
28
|
+
private outboundCodec;
|
|
29
|
+
private sendSequenceNumber;
|
|
30
|
+
private sendTimestamp;
|
|
31
|
+
private sendSsrc;
|
|
32
|
+
private remoteAddress;
|
|
33
|
+
private remotePort;
|
|
34
|
+
private samplesPerPacket;
|
|
35
|
+
private packetDurationMs;
|
|
36
|
+
private continuousPacing;
|
|
37
|
+
private pacedQueue;
|
|
38
|
+
private pacePending;
|
|
39
|
+
private paceTimer;
|
|
40
|
+
private nextPaceTime;
|
|
41
|
+
constructor(codec: Codec, options: RtpSessionOptions);
|
|
42
|
+
private decodeByPayloadType;
|
|
43
|
+
setRemote(address: string, port: number): void;
|
|
44
|
+
private sendRtpPayload;
|
|
45
|
+
/**
|
|
46
|
+
* Append PCM to the paced buffer and queue full 20ms (or packetDurationMs) frames.
|
|
47
|
+
*/
|
|
48
|
+
private enqueuePacedPcm;
|
|
49
|
+
private runPacedTick;
|
|
50
|
+
private schedulePaceTick;
|
|
51
|
+
sendAudio(pcm: Int16Array): void;
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=rtp_session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rtp_session.d.ts","sourceRoot":"","sources":["../../src/rtp/rtp_session.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAO3C,OAAO,EAGL,KAAK,KAAK,EACX,MAAM,qCAAqC,CAAC;AAE7C,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,qBAAa,UAAW,SAAQ,YAAY;IAC1C,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,aAAa,CAAQ;IAE7B,OAAO,CAAC,kBAAkB,CAAa;IACvC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,UAAU,CAAa;IAE/B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,gBAAgB,CAAS;IAEjC,OAAO,CAAC,gBAAgB,CAAU;IAClC,OAAO,CAAC,UAAU,CAAoB;IACtC,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,YAAY,CAAa;gBAErB,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,iBAAiB;IAkDpD,OAAO,CAAC,mBAAmB;IAa3B,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAK9C,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACH,OAAO,CAAC,eAAe;IAmBvB,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,gBAAgB;IAiBxB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI;IAUhC,KAAK,IAAI,IAAI;CAad"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import * as dgram from 'node:dgram';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { parseRtpPacket, buildRtpPacket, } from './rtp_packet.js';
|
|
4
|
+
import { JitterBuffer } from './jitter_buffer.js';
|
|
5
|
+
import { PCMU, PCMA, } from '@ariaflow/livekit-plugin/codec/g711';
|
|
6
|
+
/**
|
|
7
|
+
* Manages a bidirectional RTP stream over UDP.
|
|
8
|
+
*
|
|
9
|
+
* Inbound: receives UDP datagrams, parses RTP, passes through jitter buffer,
|
|
10
|
+
* decodes with codec selected by RTP payload type (PCMU=0, PCMA=8), emits
|
|
11
|
+
* 'audio' events with PCM Int16Array data.
|
|
12
|
+
*
|
|
13
|
+
* Outbound: accepts PCM Int16Array, encodes with the negotiated outbound
|
|
14
|
+
* codec, builds RTP packets, sends as UDP datagrams.
|
|
15
|
+
*/
|
|
16
|
+
export class RtpSession extends EventEmitter {
|
|
17
|
+
socket;
|
|
18
|
+
jitterBuffer;
|
|
19
|
+
outboundCodec;
|
|
20
|
+
sendSequenceNumber = 0;
|
|
21
|
+
sendTimestamp = 0;
|
|
22
|
+
sendSsrc;
|
|
23
|
+
remoteAddress = '';
|
|
24
|
+
remotePort = 0;
|
|
25
|
+
samplesPerPacket;
|
|
26
|
+
packetDurationMs;
|
|
27
|
+
continuousPacing;
|
|
28
|
+
pacedQueue = [];
|
|
29
|
+
pacePending = new Int16Array(0);
|
|
30
|
+
paceTimer = null;
|
|
31
|
+
nextPaceTime = 0;
|
|
32
|
+
constructor(codec, options) {
|
|
33
|
+
super();
|
|
34
|
+
this.outboundCodec = codec;
|
|
35
|
+
this.jitterBuffer = new JitterBuffer(options.jitterDepth ?? 3);
|
|
36
|
+
this.sendSsrc = Math.floor(Math.random() * 0xffffffff);
|
|
37
|
+
this.packetDurationMs = options.packetDurationMs ?? 20;
|
|
38
|
+
this.samplesPerPacket =
|
|
39
|
+
(codec.sampleRate * this.packetDurationMs) / 1000;
|
|
40
|
+
this.continuousPacing = options.continuousPacing === true;
|
|
41
|
+
this.socket = dgram.createSocket('udp4');
|
|
42
|
+
this.socket.on('message', (msg, rinfo) => {
|
|
43
|
+
if (!this.remoteAddress) {
|
|
44
|
+
this.remoteAddress = rinfo.address;
|
|
45
|
+
this.remotePort = rinfo.port;
|
|
46
|
+
}
|
|
47
|
+
const packet = parseRtpPacket(msg);
|
|
48
|
+
if (!packet)
|
|
49
|
+
return;
|
|
50
|
+
this.jitterBuffer.push(packet);
|
|
51
|
+
let buffered;
|
|
52
|
+
while ((buffered = this.jitterBuffer.pull()) !== null) {
|
|
53
|
+
const pcm = this.decodeByPayloadType(buffered.payloadType, buffered.payload);
|
|
54
|
+
this.emit('audio', pcm);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
this.socket.on('error', (err) => {
|
|
58
|
+
console.error('[RtpSession] UDP socket error:', err.message);
|
|
59
|
+
});
|
|
60
|
+
this.socket.bind(options.localPort);
|
|
61
|
+
if (this.continuousPacing) {
|
|
62
|
+
// First `schedulePaceTick` advances by one `packetDurationMs`; start from "now"
|
|
63
|
+
// so the initial delay is exactly one frame (not two).
|
|
64
|
+
this.nextPaceTime = performance.now();
|
|
65
|
+
this.schedulePaceTick();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
decodeByPayloadType(payloadType, payload) {
|
|
69
|
+
if (payloadType === PCMU.payloadType) {
|
|
70
|
+
return PCMU.decode(payload);
|
|
71
|
+
}
|
|
72
|
+
if (payloadType === PCMA.payloadType) {
|
|
73
|
+
return PCMA.decode(payload);
|
|
74
|
+
}
|
|
75
|
+
return this.outboundCodec.decode(payload);
|
|
76
|
+
}
|
|
77
|
+
setRemote(address, port) {
|
|
78
|
+
this.remoteAddress = address;
|
|
79
|
+
this.remotePort = port;
|
|
80
|
+
}
|
|
81
|
+
sendRtpPayload(encoded, samplesThisPacket) {
|
|
82
|
+
if (!this.remoteAddress || !this.remotePort)
|
|
83
|
+
return;
|
|
84
|
+
const packet = buildRtpPacket(this.outboundCodec.payloadType, this.sendSequenceNumber, this.sendTimestamp, this.sendSsrc, encoded, this.sendSequenceNumber === 0);
|
|
85
|
+
this.socket.send(packet, this.remotePort, this.remoteAddress);
|
|
86
|
+
this.sendSequenceNumber = (this.sendSequenceNumber + 1) & 0xffff;
|
|
87
|
+
this.sendTimestamp =
|
|
88
|
+
(this.sendTimestamp + samplesThisPacket) >>> 0;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Append PCM to the paced buffer and queue full 20ms (or packetDurationMs) frames.
|
|
92
|
+
*/
|
|
93
|
+
enqueuePacedPcm(pcm) {
|
|
94
|
+
const merged = this.pacePending.length === 0
|
|
95
|
+
? pcm
|
|
96
|
+
: concatInt16(this.pacePending, pcm);
|
|
97
|
+
let offset = 0;
|
|
98
|
+
while (offset + this.samplesPerPacket <= merged.length) {
|
|
99
|
+
this.pacedQueue.push(merged.subarray(offset, offset + this.samplesPerPacket));
|
|
100
|
+
offset += this.samplesPerPacket;
|
|
101
|
+
}
|
|
102
|
+
this.pacePending =
|
|
103
|
+
offset === merged.length
|
|
104
|
+
? new Int16Array(0)
|
|
105
|
+
: merged.subarray(offset);
|
|
106
|
+
}
|
|
107
|
+
runPacedTick() {
|
|
108
|
+
if (!this.remoteAddress || !this.remotePort) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const pcm = this.pacedQueue.length > 0
|
|
112
|
+
? this.pacedQueue.shift()
|
|
113
|
+
: new Int16Array(this.samplesPerPacket);
|
|
114
|
+
const encoded = this.outboundCodec.encode(pcm);
|
|
115
|
+
this.sendRtpPayload(encoded, pcm.length);
|
|
116
|
+
}
|
|
117
|
+
schedulePaceTick() {
|
|
118
|
+
const now = performance.now();
|
|
119
|
+
const drift = now - this.nextPaceTime;
|
|
120
|
+
if (drift > 2 * this.packetDurationMs) {
|
|
121
|
+
this.nextPaceTime = now + this.packetDurationMs;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.nextPaceTime += this.packetDurationMs;
|
|
125
|
+
}
|
|
126
|
+
const delay = Math.max(0, this.nextPaceTime - performance.now());
|
|
127
|
+
this.paceTimer = setTimeout(() => {
|
|
128
|
+
this.paceTimer = null;
|
|
129
|
+
this.runPacedTick();
|
|
130
|
+
this.schedulePaceTick();
|
|
131
|
+
}, delay);
|
|
132
|
+
}
|
|
133
|
+
sendAudio(pcm) {
|
|
134
|
+
if (this.continuousPacing) {
|
|
135
|
+
this.enqueuePacedPcm(pcm);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const encoded = this.outboundCodec.encode(pcm);
|
|
139
|
+
this.sendRtpPayload(encoded, this.samplesPerPacket);
|
|
140
|
+
}
|
|
141
|
+
close() {
|
|
142
|
+
if (this.paceTimer !== null) {
|
|
143
|
+
clearTimeout(this.paceTimer);
|
|
144
|
+
this.paceTimer = null;
|
|
145
|
+
}
|
|
146
|
+
this.pacedQueue = [];
|
|
147
|
+
this.pacePending = new Int16Array(0);
|
|
148
|
+
try {
|
|
149
|
+
this.socket.close();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Already closed
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function concatInt16(a, b) {
|
|
157
|
+
const out = new Int16Array(a.length + b.length);
|
|
158
|
+
out.set(a, 0);
|
|
159
|
+
out.set(b, a.length);
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=rtp_session.js.map
|