ulai-js-sdk 0.1.0 → 0.1.1
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 +557 -0
- package/dist/events.d.ts +13 -0
- package/dist/events.js +149 -42
- package/dist/room.d.ts +27 -1
- package/dist/room.js +208 -39
- package/dist/signaling.d.ts +11 -0
- package/dist/signaling.js +139 -24
- package/dist/types.d.ts +6 -4
- package/package.json +14 -4
- package/src/client.ts +0 -59
- package/src/events.ts +0 -64
- package/src/index.ts +0 -5
- package/src/room.ts +0 -163
- package/src/signaling.ts +0 -53
- package/src/types.ts +0 -44
- package/tsconfig.json +0 -15
package/README.md
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
# Ulai Cloud JavaScript/TypeScript Client SDK (`ulai-js-sdk`)
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/ulai-js-sdk)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
The official JavaScript and TypeScript client SDK for Ulai Cloud real-time WebRTC sessions. Enables web applications to establish ultra-low-latency, bidirectional audio streaming with AI voice agents, receive real-time streaming transcripts, and manage room lifecycle events.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Features](#features)
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Quick Start](#quick-start)
|
|
16
|
+
- [Pattern 1: Connect with Pre-provisioned Session Credentials (`Room`)](#pattern-1-connect-with-pre-provisioned-session-credentials-room)
|
|
17
|
+
- [Pattern 2: Connect using Ulai Cloud API Key (`UlaiClient`)](#pattern-2-connect-using-ulai-cloud-api-key-ulaiclient)
|
|
18
|
+
- [React / Next.js Example](#react--nextjs-example)
|
|
19
|
+
- [Vanilla JavaScript / HTML Example](#vanilla-javascript--html-example)
|
|
20
|
+
- [API Reference](#api-reference)
|
|
21
|
+
- [`Room`](#room)
|
|
22
|
+
- [Events](#events)
|
|
23
|
+
- [`UlaiClient`](#ulaiclient)
|
|
24
|
+
- [TypeScript Interfaces](#typescript-interfaces)
|
|
25
|
+
- [Handling Browser Audio & Autoplay Policies](#handling-browser-audio--autoplay-policies)
|
|
26
|
+
- [Network & ICE Configuration](#network--ice-configuration)
|
|
27
|
+
- [License](#license)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **W3C Perfect Negotiation:** Native polite-peer pattern implementation eliminates SDP glare and race conditions during dynamic renegotiation.
|
|
34
|
+
- **Resilient Audio Pipelines:** Automatic track wrapping and fallback streams compatible with Pion SFU standalone and grouped media tracks.
|
|
35
|
+
- **Dynamic Candidate Buffering:** Queues incoming ICE candidates until remote session descriptions are active, avoiding `InvalidStateError`.
|
|
36
|
+
- **Automatic Reconnection:** Exponential backoff with jitter for WebRTC signaling and Control Plane event sockets.
|
|
37
|
+
- **Message Outbox:** Buffers signaling messages until WebSocket connections reach an open state.
|
|
38
|
+
- **Live Transcripts:** Real-time event streaming for interim and final speech recognition deltas (`transcriptDelta`).
|
|
39
|
+
- **Audio Management:** Direct microphone capture with hardware-level mute controls (`setMuted()`), query methods (`isMuted()`), and pre-configured noise suppression, echo cancellation, and AGC.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# npm
|
|
47
|
+
npm install ulai-js-sdk
|
|
48
|
+
|
|
49
|
+
# yarn
|
|
50
|
+
yarn add ulai-js-sdk
|
|
51
|
+
|
|
52
|
+
# pnpm
|
|
53
|
+
pnpm add ulai-js-sdk
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### Pattern 1: Connect with Pre-provisioned Session Credentials (`Room`)
|
|
61
|
+
|
|
62
|
+
This is the standard pattern for web applications. Your application server provisions a session and returns join credentials (`roomId`, `ticket`, `nodeAddress`, `eventsToken`), which the client SDK uses to join:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { Room } from 'ulai-js-sdk';
|
|
66
|
+
|
|
67
|
+
// 1. Initialize Room with your Ulai Control Plane URL
|
|
68
|
+
const room = new Room('https://cp.ulai.co.in');
|
|
69
|
+
|
|
70
|
+
// 2. Set up event listeners
|
|
71
|
+
room.on('connected', () => {
|
|
72
|
+
console.log('Connected to Ulai WebRTC room!');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Play remote incoming audio (e.g., from an AI agent or participant)
|
|
76
|
+
room.on('trackSubscribed', (track: MediaStreamTrack, stream: MediaStream) => {
|
|
77
|
+
if (track.kind === 'audio') {
|
|
78
|
+
const audio = document.createElement('audio');
|
|
79
|
+
audio.srcObject = stream;
|
|
80
|
+
audio.autoplay = true;
|
|
81
|
+
audio.style.display = 'none';
|
|
82
|
+
document.body.appendChild(audio);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Receive live transcripts
|
|
87
|
+
room.on('transcriptDelta', (delta) => {
|
|
88
|
+
console.log(`[${delta.speaker}] ${delta.text} (final: ${delta.is_final})`);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
room.on('sessionTerminated', () => {
|
|
92
|
+
console.log('Call ended by remote party');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 3. Connect to WebRTC media and the Control Plane event feed
|
|
96
|
+
await room.connect({
|
|
97
|
+
roomId: 'session-xyz', // The session / room ID
|
|
98
|
+
ticket: 'ticket-token-abc', // SFU WebRTC signaling auth ticket
|
|
99
|
+
nodeAddress: 'sfu.ulai.co.in', // Address of the assigned SFU node
|
|
100
|
+
eventsToken: 'events-token-123' // Token for live events & transcripts
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 4. Request microphone access and stream audio to the room
|
|
104
|
+
await room.publishAudio();
|
|
105
|
+
|
|
106
|
+
// 5. Mute / Unmute controls
|
|
107
|
+
room.setMuted(true);
|
|
108
|
+
console.log('Is muted:', room.isMuted());
|
|
109
|
+
|
|
110
|
+
// 6. Graceful disconnect
|
|
111
|
+
room.disconnect();
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### Pattern 2: Connect using Ulai Cloud API Key (`UlaiClient`)
|
|
117
|
+
|
|
118
|
+
If you want the client to make direct join requests to the Ulai Cloud Control Plane:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { UlaiClient } from 'ulai-js-sdk';
|
|
122
|
+
|
|
123
|
+
const client = new UlaiClient({
|
|
124
|
+
url: 'https://cp.ulai.co.in',
|
|
125
|
+
apiKey: 'YOUR_ULAI_API_KEY', // Optional Bearer token
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Requests join credentials from the Control Plane and connects WebRTC + Events
|
|
129
|
+
const room = await client.joinRoom({
|
|
130
|
+
roomId: 'session-xyz',
|
|
131
|
+
participantId: 'user-01',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Hook into room events
|
|
135
|
+
room.on('trackSubscribed', (track, stream) => {
|
|
136
|
+
const audio = document.createElement('audio');
|
|
137
|
+
audio.srcObject = stream;
|
|
138
|
+
audio.autoplay = true;
|
|
139
|
+
document.body.appendChild(audio);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Publish microphone
|
|
143
|
+
await room.publishAudio();
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## React / Next.js Example
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
'use client';
|
|
152
|
+
|
|
153
|
+
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
154
|
+
import { Room, TranscriptDelta } from 'ulai-js-sdk';
|
|
155
|
+
|
|
156
|
+
interface TranscriptItem {
|
|
157
|
+
id: string;
|
|
158
|
+
speaker: string;
|
|
159
|
+
text: string;
|
|
160
|
+
isFinal: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function VoiceRoom({
|
|
164
|
+
controlPlaneUrl = 'https://cp.ulai.co.in',
|
|
165
|
+
fetchSessionCredentials,
|
|
166
|
+
}: {
|
|
167
|
+
controlPlaneUrl?: string;
|
|
168
|
+
fetchSessionCredentials: () => Promise<{
|
|
169
|
+
roomId: string;
|
|
170
|
+
ticket: string;
|
|
171
|
+
nodeAddress: string;
|
|
172
|
+
eventsToken?: string;
|
|
173
|
+
}>;
|
|
174
|
+
}) {
|
|
175
|
+
const [status, setStatus] = useState<'idle' | 'connecting' | 'connected' | 'ended'>('idle');
|
|
176
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
177
|
+
const [transcripts, setTranscripts] = useState<TranscriptItem[]>([]);
|
|
178
|
+
|
|
179
|
+
const roomRef = useRef<Room | null>(null);
|
|
180
|
+
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
181
|
+
|
|
182
|
+
const cleanup = useCallback(() => {
|
|
183
|
+
if (roomRef.current) {
|
|
184
|
+
roomRef.current.disconnect();
|
|
185
|
+
roomRef.current = null;
|
|
186
|
+
}
|
|
187
|
+
if (audioRef.current) {
|
|
188
|
+
audioRef.current.srcObject = null;
|
|
189
|
+
}
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
return () => cleanup();
|
|
194
|
+
}, [cleanup]);
|
|
195
|
+
|
|
196
|
+
const startSession = async () => {
|
|
197
|
+
try {
|
|
198
|
+
setStatus('connecting');
|
|
199
|
+
setTranscripts([]);
|
|
200
|
+
|
|
201
|
+
// 1. Fetch credentials from your backend
|
|
202
|
+
const creds = await fetchSessionCredentials();
|
|
203
|
+
|
|
204
|
+
// 2. Initialize Room
|
|
205
|
+
const room = new Room(controlPlaneUrl);
|
|
206
|
+
roomRef.current = room;
|
|
207
|
+
|
|
208
|
+
// 3. Register Event Handlers
|
|
209
|
+
room.on('connected', () => setStatus('connected'));
|
|
210
|
+
|
|
211
|
+
room.on('trackSubscribed', (track, stream) => {
|
|
212
|
+
if (track.kind === 'audio' && audioRef.current) {
|
|
213
|
+
audioRef.current.srcObject = stream;
|
|
214
|
+
audioRef.current.play().catch(console.error);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
room.on('transcriptDelta', (delta: TranscriptDelta) => {
|
|
219
|
+
setTranscripts((prev) => {
|
|
220
|
+
const existingIndex = prev.findIndex((t) => t.id === delta.segment_id);
|
|
221
|
+
if (existingIndex !== -1) {
|
|
222
|
+
const updated = [...prev];
|
|
223
|
+
updated[existingIndex] = {
|
|
224
|
+
...updated[existingIndex],
|
|
225
|
+
text: delta.text,
|
|
226
|
+
isFinal: delta.is_final,
|
|
227
|
+
};
|
|
228
|
+
return updated;
|
|
229
|
+
}
|
|
230
|
+
return [
|
|
231
|
+
...prev,
|
|
232
|
+
{ id: delta.segment_id, speaker: delta.speaker, text: delta.text, isFinal: delta.is_final },
|
|
233
|
+
];
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
room.on('sessionTerminated', () => {
|
|
238
|
+
setStatus('ended');
|
|
239
|
+
cleanup();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
room.on('disconnected', () => {
|
|
243
|
+
setStatus('idle');
|
|
244
|
+
cleanup();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 4. Connect WebRTC & Events
|
|
248
|
+
await room.connect({
|
|
249
|
+
roomId: creds.roomId,
|
|
250
|
+
ticket: creds.ticket,
|
|
251
|
+
nodeAddress: creds.nodeAddress,
|
|
252
|
+
eventsToken: creds.eventsToken,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// 5. Publish microphone
|
|
256
|
+
await room.publishAudio();
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.error('Failed to start session:', err);
|
|
259
|
+
setStatus('idle');
|
|
260
|
+
cleanup();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const endSession = () => {
|
|
265
|
+
cleanup();
|
|
266
|
+
setStatus('idle');
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const toggleMute = () => {
|
|
270
|
+
if (!roomRef.current) return;
|
|
271
|
+
const nextMuted = !isMuted;
|
|
272
|
+
roomRef.current.setMuted(nextMuted);
|
|
273
|
+
setIsMuted(nextMuted);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<div style={{ maxWidth: 480, padding: 24, borderRadius: 12, border: '1px solid #e2e8f0' }}>
|
|
278
|
+
<audio ref={audioRef} autoPlay playsInline style={{ display: 'none' }} />
|
|
279
|
+
|
|
280
|
+
<h3>Live Voice Session</h3>
|
|
281
|
+
<p>Status: <strong>{status.toUpperCase()}</strong></p>
|
|
282
|
+
|
|
283
|
+
<div style={{ display: 'flex', gap: 12, margin: '16px 0' }}>
|
|
284
|
+
{status === 'idle' || status === 'ended' ? (
|
|
285
|
+
<button onClick={startSession} style={{ padding: '8px 16px', background: '#2563eb', color: '#fff', borderRadius: 8 }}>
|
|
286
|
+
Start Session
|
|
287
|
+
</button>
|
|
288
|
+
) : (
|
|
289
|
+
<>
|
|
290
|
+
<button onClick={endSession} style={{ padding: '8px 16px', background: '#dc2626', color: '#fff', borderRadius: 8 }}>
|
|
291
|
+
End Session
|
|
292
|
+
</button>
|
|
293
|
+
<button onClick={toggleMute} style={{ padding: '8px 16px', borderRadius: 8 }}>
|
|
294
|
+
{isMuted ? 'Unmute Mic' : 'Mute Mic'}
|
|
295
|
+
</button>
|
|
296
|
+
</>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
<div style={{ height: 200, overflowY: 'auto', background: '#f8fafc', padding: 12, borderRadius: 8 }}>
|
|
301
|
+
{transcripts.map((t) => (
|
|
302
|
+
<div key={t.id} style={{ marginBottom: 6 }}>
|
|
303
|
+
<strong>{t.speaker}: </strong>
|
|
304
|
+
<span>{t.text}</span>
|
|
305
|
+
</div>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Vanilla JavaScript / HTML Example
|
|
316
|
+
|
|
317
|
+
```html
|
|
318
|
+
<!DOCTYPE html>
|
|
319
|
+
<html lang="en">
|
|
320
|
+
<head>
|
|
321
|
+
<meta charset="UTF-8">
|
|
322
|
+
<title>Ulai Voice Session</title>
|
|
323
|
+
</head>
|
|
324
|
+
<body>
|
|
325
|
+
<h2>Ulai Voice Client</h2>
|
|
326
|
+
<button id="connectBtn">Connect</button>
|
|
327
|
+
<button id="muteBtn" disabled>Mute</button>
|
|
328
|
+
<div id="transcriptBox" style="margin-top: 16px; border: 1px solid #ddd; height: 220px; overflow-y: auto; padding: 10px;"></div>
|
|
329
|
+
|
|
330
|
+
<audio id="remoteAudio" autoplay playsinline></audio>
|
|
331
|
+
|
|
332
|
+
<script type="module">
|
|
333
|
+
import { Room } from 'ulai-js-sdk';
|
|
334
|
+
|
|
335
|
+
let room = null;
|
|
336
|
+
const connectBtn = document.getElementById('connectBtn');
|
|
337
|
+
const muteBtn = document.getElementById('muteBtn');
|
|
338
|
+
const transcriptBox = document.getElementById('transcriptBox');
|
|
339
|
+
const remoteAudio = document.getElementById('remoteAudio');
|
|
340
|
+
|
|
341
|
+
connectBtn.addEventListener('click', async () => {
|
|
342
|
+
if (room) {
|
|
343
|
+
room.disconnect();
|
|
344
|
+
room = null;
|
|
345
|
+
connectBtn.textContent = 'Connect';
|
|
346
|
+
muteBtn.disabled = true;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
connectBtn.textContent = 'Connecting...';
|
|
351
|
+
|
|
352
|
+
// Obtain credentials from your backend API
|
|
353
|
+
const res = await fetch('/api/get-room-credentials');
|
|
354
|
+
const creds = await res.json();
|
|
355
|
+
|
|
356
|
+
room = new Room(creds.controlPlaneUrl || 'https://cp.ulai.co.in');
|
|
357
|
+
|
|
358
|
+
room.on('connected', () => {
|
|
359
|
+
connectBtn.textContent = 'Disconnect';
|
|
360
|
+
muteBtn.disabled = false;
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
room.on('trackSubscribed', (track, stream) => {
|
|
364
|
+
if (track.kind === 'audio') {
|
|
365
|
+
remoteAudio.srcObject = stream;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
room.on('transcriptDelta', (delta) => {
|
|
370
|
+
const item = document.createElement('div');
|
|
371
|
+
item.textContent = `[${delta.speaker}]: ${delta.text}`;
|
|
372
|
+
transcriptBox.appendChild(item);
|
|
373
|
+
transcriptBox.scrollTop = transcriptBox.scrollHeight;
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
room.on('sessionTerminated', () => {
|
|
377
|
+
room.disconnect();
|
|
378
|
+
room = null;
|
|
379
|
+
connectBtn.textContent = 'Connect';
|
|
380
|
+
muteBtn.disabled = true;
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await room.connect({
|
|
384
|
+
roomId: creds.roomId,
|
|
385
|
+
ticket: creds.ticket,
|
|
386
|
+
nodeAddress: creds.nodeAddress,
|
|
387
|
+
eventsToken: creds.eventsToken,
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await room.publishAudio();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
muteBtn.addEventListener('click', () => {
|
|
394
|
+
if (!room) return;
|
|
395
|
+
const next = !room.isMuted();
|
|
396
|
+
room.setMuted(next);
|
|
397
|
+
muteBtn.textContent = next ? 'Unmute' : 'Mute';
|
|
398
|
+
});
|
|
399
|
+
</script>
|
|
400
|
+
</body>
|
|
401
|
+
</html>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## API Reference
|
|
407
|
+
|
|
408
|
+
### `Room`
|
|
409
|
+
|
|
410
|
+
Primary session manager. Extends `EventEmitter<RoomEvent>`.
|
|
411
|
+
|
|
412
|
+
#### Constructor
|
|
413
|
+
```typescript
|
|
414
|
+
new Room(controlPlaneUrl: string)
|
|
415
|
+
```
|
|
416
|
+
- `controlPlaneUrl`: Base URL of the Ulai Control Plane (e.g. `'https://cp.ulai.co.in'`).
|
|
417
|
+
|
|
418
|
+
#### Methods
|
|
419
|
+
|
|
420
|
+
- **`connect(options: JoinRoomOptions): Promise<void>`**
|
|
421
|
+
Connects to the SFU signaling WebSocket and the Control Plane event feed.
|
|
422
|
+
- **`publishAudio(): Promise<MediaStream>`**
|
|
423
|
+
Requests microphone access (`navigator.mediaDevices.getUserMedia`) with echo cancellation, noise suppression, and auto gain control, and attaches the audio track to the WebRTC connection.
|
|
424
|
+
- **`setMuted(muted: boolean): void`**
|
|
425
|
+
Mutes or unmutes the active microphone audio track. Emits `'muteChanged'`.
|
|
426
|
+
- **`isMuted(): boolean`**
|
|
427
|
+
Returns `true` if local audio is muted or inactive.
|
|
428
|
+
- **`getLocalStream(): MediaStream | null`**
|
|
429
|
+
Returns the active local `MediaStream`.
|
|
430
|
+
- **`getRemoteStreams(): MediaStream[]`**
|
|
431
|
+
Returns all active subscribed remote `MediaStream`s.
|
|
432
|
+
- **`disconnect(): void`**
|
|
433
|
+
Stops local microphone tracks, closes the WebRTC peer connection, disconnects signaling and event sockets, and cleans up resources.
|
|
434
|
+
|
|
435
|
+
#### Properties
|
|
436
|
+
- **`roomId: string | null`**: Current room/session ID.
|
|
437
|
+
- **`participantId: string | null`**: Current participant ID.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
### Events
|
|
442
|
+
|
|
443
|
+
Register listeners using `room.on(eventName, handler)`.
|
|
444
|
+
|
|
445
|
+
| Event | Parameters | Description |
|
|
446
|
+
|---|---|---|
|
|
447
|
+
| `connected` | `()` | WebRTC connection successfully established. |
|
|
448
|
+
| `disconnected` | `()` | Connection closed or lost. |
|
|
449
|
+
| `reconnecting` | `()` | WebRTC connection interrupted; attempting reconnection. |
|
|
450
|
+
| `reconnected` | `()` | Reconnection established and media renegotiated. |
|
|
451
|
+
| `trackSubscribed` | `(track: MediaStreamTrack, stream: MediaStream)` | Remote audio track available. Attach `stream` to an `<audio>` element. |
|
|
452
|
+
| `trackUnsubscribed` | `(track: MediaStreamTrack, stream: MediaStream)` | Remote track ended or removed. |
|
|
453
|
+
| `transcriptDelta` | `(delta: TranscriptDelta)` | Streaming transcript update for user or remote speaker. |
|
|
454
|
+
| `sessionTerminated`| `(msg: any)` | Session ended by server/remote agent. SDK auto-disconnects. |
|
|
455
|
+
| `muteChanged` | `(muted: boolean)` | Local microphone mute status changed. |
|
|
456
|
+
| `participantJoined`| `(msg: any)` | A participant joined the session. |
|
|
457
|
+
| `participantLeft` | `(msg: any)` | A participant left the session. |
|
|
458
|
+
| `roster` | `(msg: RosterMessage)` | Current participant roster update. |
|
|
459
|
+
| `error` | `(error: any)` | Signaling, WebRTC, or event socket error. |
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
### `UlaiClient`
|
|
464
|
+
|
|
465
|
+
Utility class for directly requesting room join tickets and initializing a `Room`.
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
const client = new UlaiClient({
|
|
469
|
+
url: 'https://cp.ulai.co.in',
|
|
470
|
+
apiKey: 'optional_api_key'
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const room: Room = await client.joinRoom({
|
|
474
|
+
roomId: 'session-id',
|
|
475
|
+
participantId: 'user-01'
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
### TypeScript Interfaces
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
export interface JoinRoomOptions {
|
|
485
|
+
roomId: string;
|
|
486
|
+
participantId?: string;
|
|
487
|
+
eventsToken?: string;
|
|
488
|
+
ticket?: string;
|
|
489
|
+
nodeAddress?: string;
|
|
490
|
+
iceServers?: RTCIceServer[];
|
|
491
|
+
autoSubscribeAudio?: boolean;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export interface TranscriptDelta {
|
|
495
|
+
segment_id: string;
|
|
496
|
+
speaker: 'user' | 'agent' | string;
|
|
497
|
+
text: string;
|
|
498
|
+
is_final: boolean;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export interface ConnectOptions {
|
|
502
|
+
url: string;
|
|
503
|
+
apiKey?: string;
|
|
504
|
+
token?: string;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export interface RosterMessage {
|
|
508
|
+
participants: any[];
|
|
509
|
+
}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## Handling Browser Audio & Autoplay Policies
|
|
515
|
+
|
|
516
|
+
Modern web browsers restrict media playback until a user interaction occurs. Follow these rules to ensure smooth audio delivery:
|
|
517
|
+
|
|
518
|
+
1. **Invoke from a User Gesture:** Always call `room.connect()` and `room.publishAudio()` inside a direct user event handler (e.g., clicking a "Start Call" button).
|
|
519
|
+
2. **Audio Tag Attributes:** When attaching remote streams, ensure `<audio autoplay playsinline>` attributes are present.
|
|
520
|
+
3. **Catch Playback Errors:** Handle rejected audio play promises:
|
|
521
|
+
```typescript
|
|
522
|
+
audioElement.play().catch((err) => {
|
|
523
|
+
console.warn('Audio playback deferred by browser autoplay policy:', err);
|
|
524
|
+
});
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Network & ICE Configuration
|
|
530
|
+
|
|
531
|
+
Ulai Cloud WebRTC uses:
|
|
532
|
+
- **Signaling & Events:** Secure WebSockets (`wss://`) over port `443`.
|
|
533
|
+
- **Media (RTP/RTCP):** UDP media routing.
|
|
534
|
+
|
|
535
|
+
For environments requiring custom STUN/TURN traversal, provide custom `iceServers` in `JoinRoomOptions`:
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
await room.connect({
|
|
539
|
+
roomId: 'session-xyz',
|
|
540
|
+
ticket: 'ticket-token',
|
|
541
|
+
nodeAddress: 'sfu.ulai.co.in',
|
|
542
|
+
iceServers: [
|
|
543
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
544
|
+
{
|
|
545
|
+
urls: 'turn:turn.yourcompany.com:3478',
|
|
546
|
+
username: 'turnuser',
|
|
547
|
+
credential: 'turnpassword',
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## License
|
|
556
|
+
|
|
557
|
+
MIT © [Ulai Team](https://ulai.co.in)
|
package/dist/events.d.ts
CHANGED
|
@@ -2,7 +2,20 @@ import EventEmitter from 'eventemitter3';
|
|
|
2
2
|
export declare class EventsClient extends EventEmitter {
|
|
3
3
|
private ws;
|
|
4
4
|
private url;
|
|
5
|
+
private sessionId;
|
|
6
|
+
private eventsToken;
|
|
7
|
+
private lastEventId;
|
|
8
|
+
private isClosedManually;
|
|
9
|
+
private retryAttempts;
|
|
10
|
+
private readonly maxRetries;
|
|
11
|
+
private reconnectTimer;
|
|
12
|
+
private heartbeatTimer;
|
|
5
13
|
constructor(cpBase: string);
|
|
14
|
+
private buildUrl;
|
|
6
15
|
connect(sessionId: string, eventsToken: string): void;
|
|
16
|
+
private sendSubscribe;
|
|
17
|
+
private startHeartbeat;
|
|
18
|
+
private stopHeartbeat;
|
|
19
|
+
private cleanupSocket;
|
|
7
20
|
close(): void;
|
|
8
21
|
}
|