ttp-agent-sdk 2.0.1 → 2.0.2
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/package.json +2 -3
- package/src/core/AudioPlayer.js +0 -185
- package/src/core/AudioRecorder.js +0 -128
- package/src/core/ConnectionManager.js +0 -86
- package/src/core/EventEmitter.js +0 -53
- package/src/core/VoiceSDK.js +0 -390
- package/src/core/WebSocketManager.js +0 -218
- package/src/core/WebSocketManagerV2.js +0 -211
- package/src/core/WebSocketSingleton.js +0 -171
- package/src/index.js +0 -64
- package/src/legacy/AgentSDK.js +0 -462
- package/src/react/VoiceButton.jsx +0 -163
- package/src/vanilla/VoiceButton.js +0 -190
package/src/core/VoiceSDK.js
DELETED
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VoiceSDK - Core voice interaction SDK
|
|
3
|
-
* Handles WebSocket connection, audio recording, and audio playback
|
|
4
|
-
*/
|
|
5
|
-
import EventEmitter from './EventEmitter.js';
|
|
6
|
-
import WebSocketManagerV2 from './WebSocketManagerV2.js';
|
|
7
|
-
import AudioRecorder from './AudioRecorder.js';
|
|
8
|
-
import AudioPlayer from './AudioPlayer.js';
|
|
9
|
-
|
|
10
|
-
export default class VoiceSDK extends EventEmitter {
|
|
11
|
-
constructor(config = {}) {
|
|
12
|
-
super();
|
|
13
|
-
|
|
14
|
-
// Configuration
|
|
15
|
-
this.config = {
|
|
16
|
-
websocketUrl: config.websocketUrl || 'wss://speech.talktopc.com/ws/conv',
|
|
17
|
-
agentId: config.agentId, // Optional - for direct agent access (unsecured method)
|
|
18
|
-
appId: config.appId, // User's app ID for authentication
|
|
19
|
-
ttpId: config.ttpId, // Optional - custom TTP ID (fallback if appId not provided)
|
|
20
|
-
voice: config.voice || 'default',
|
|
21
|
-
language: config.language || 'en',
|
|
22
|
-
sampleRate: config.sampleRate || 16000,
|
|
23
|
-
...config
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// State
|
|
27
|
-
this.isConnected = false;
|
|
28
|
-
this.isRecording = false;
|
|
29
|
-
this.isPlaying = false;
|
|
30
|
-
this.isDestroyed = false;
|
|
31
|
-
|
|
32
|
-
// Components
|
|
33
|
-
this.webSocketManager = new WebSocketManagerV2({
|
|
34
|
-
...this.config,
|
|
35
|
-
autoReconnect: this.config.autoReconnect !== false // Default to true unless explicitly disabled
|
|
36
|
-
});
|
|
37
|
-
this.audioRecorder = new AudioRecorder(this.config);
|
|
38
|
-
this.audioPlayer = new AudioPlayer(this.config);
|
|
39
|
-
|
|
40
|
-
// Bind event handlers
|
|
41
|
-
this.setupEventHandlers();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Setup event handlers for all components
|
|
46
|
-
*/
|
|
47
|
-
setupEventHandlers() {
|
|
48
|
-
// WebSocket events
|
|
49
|
-
this.webSocketManager.on('connected', () => {
|
|
50
|
-
this.isConnected = true;
|
|
51
|
-
this.sendHelloMessage();
|
|
52
|
-
this.emit('connected');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
this.webSocketManager.on('disconnected', () => {
|
|
56
|
-
this.isConnected = false;
|
|
57
|
-
this.emit('disconnected');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
this.webSocketManager.on('error', (error) => {
|
|
61
|
-
this.emit('error', error);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
this.webSocketManager.on('message', (message) => {
|
|
65
|
-
this.emit('message', message);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
this.webSocketManager.on('binaryAudio', (audioData) => {
|
|
69
|
-
this.audioPlayer.playAudio(audioData);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
this.webSocketManager.on('bargeIn', (message) => {
|
|
73
|
-
this.emit('bargeIn', message);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
this.webSocketManager.on('stopPlaying', (message) => {
|
|
77
|
-
this.emit('stopPlaying', message);
|
|
78
|
-
// Immediately stop all audio playback
|
|
79
|
-
this.audioPlayer.stopImmediate();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// Audio recorder events
|
|
83
|
-
this.audioRecorder.on('recordingStarted', () => {
|
|
84
|
-
this.isRecording = true;
|
|
85
|
-
this.emit('recordingStarted');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
this.audioRecorder.on('recordingStopped', () => {
|
|
89
|
-
this.isRecording = false;
|
|
90
|
-
this.emit('recordingStopped');
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
this.audioRecorder.on('audioData', (audioData) => {
|
|
94
|
-
if (this.isConnected) {
|
|
95
|
-
this.webSocketManager.sendBinary(audioData);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Audio player events
|
|
100
|
-
this.audioPlayer.on('playbackStarted', () => {
|
|
101
|
-
this.isPlaying = true;
|
|
102
|
-
this.emit('playbackStarted');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
this.audioPlayer.on('playbackStopped', () => {
|
|
106
|
-
this.isPlaying = false;
|
|
107
|
-
this.emit('playbackStopped');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
this.audioPlayer.on('playbackError', (error) => {
|
|
111
|
-
this.emit('playbackError', error);
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Connect to the voice server
|
|
117
|
-
*/
|
|
118
|
-
async connect() {
|
|
119
|
-
if (this.isDestroyed) {
|
|
120
|
-
return false; // Prevent connect after destroy
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
// Build WebSocket URL with query parameters if needed
|
|
125
|
-
const wsUrl = this.buildWebSocketUrl();
|
|
126
|
-
console.log('VoiceSDK: Using WebSocket URL:', wsUrl);
|
|
127
|
-
|
|
128
|
-
// Update the WebSocket manager with the URL that includes query parameters
|
|
129
|
-
this.webSocketManager.config.websocketUrl = wsUrl;
|
|
130
|
-
|
|
131
|
-
await this.webSocketManager.connect();
|
|
132
|
-
return true;
|
|
133
|
-
} catch (error) {
|
|
134
|
-
this.emit('error', error);
|
|
135
|
-
return false;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Build WebSocket URL with query parameters for authentication
|
|
141
|
-
*/
|
|
142
|
-
buildWebSocketUrl() {
|
|
143
|
-
let url = this.config.websocketUrl;
|
|
144
|
-
const params = new URLSearchParams();
|
|
145
|
-
|
|
146
|
-
// Add agentId as query parameter if provided
|
|
147
|
-
if (this.config.agentId) {
|
|
148
|
-
params.append('agentId', this.config.agentId);
|
|
149
|
-
console.log('VoiceSDK: Adding agentId to URL:', this.config.agentId);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Add appId as query parameter if provided
|
|
153
|
-
if (this.config.appId) {
|
|
154
|
-
params.append('appId', this.config.appId);
|
|
155
|
-
console.log('VoiceSDK: Adding appId to URL:', this.config.appId);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Add other parameters if needed
|
|
159
|
-
if (this.config.voice && this.config.voice !== 'default') {
|
|
160
|
-
params.append('voice', this.config.voice);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (this.config.language && this.config.language !== 'en') {
|
|
164
|
-
params.append('language', this.config.language);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Append query parameters to URL if any exist
|
|
168
|
-
if (params.toString()) {
|
|
169
|
-
const separator = url.includes('?') ? '&' : '?';
|
|
170
|
-
url += separator + params.toString();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return url;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Disconnect from the voice server
|
|
178
|
-
*/
|
|
179
|
-
disconnect() {
|
|
180
|
-
if (this.isDestroyed) {
|
|
181
|
-
console.log(`🎙️ VoiceSDK: Disconnect called but already destroyed`);
|
|
182
|
-
return; // Prevent disconnect after destroy
|
|
183
|
-
}
|
|
184
|
-
console.log(`🎙️ VoiceSDK: Disconnecting from voice server`);
|
|
185
|
-
this.stopRecording();
|
|
186
|
-
this.webSocketManager.disconnect();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Reset reconnection attempts (useful for manual reconnection)
|
|
191
|
-
*/
|
|
192
|
-
resetReconnectionAttempts() {
|
|
193
|
-
if (this.isDestroyed) {
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
this.webSocketManager.resetReconnectionAttempts();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Manually reconnect to the voice server
|
|
201
|
-
*/
|
|
202
|
-
async reconnect() {
|
|
203
|
-
if (this.isDestroyed) {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
this.disconnect();
|
|
208
|
-
this.resetReconnectionAttempts();
|
|
209
|
-
return await this.connect();
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Start voice recording and streaming
|
|
214
|
-
*/
|
|
215
|
-
async startRecording() {
|
|
216
|
-
if (!this.isConnected) {
|
|
217
|
-
throw new Error('Not connected to voice server');
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
try {
|
|
221
|
-
// Send start continuous mode message
|
|
222
|
-
this.webSocketManager.sendMessage({
|
|
223
|
-
t: 'start_continuous_mode',
|
|
224
|
-
ttpId: this.generateTtpId(),
|
|
225
|
-
voice: this.config.voice,
|
|
226
|
-
language: this.config.language
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// Start audio recording
|
|
230
|
-
await this.audioRecorder.start();
|
|
231
|
-
return true;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
this.emit('error', error);
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Stop voice recording and streaming
|
|
240
|
-
*/
|
|
241
|
-
async stopRecording() {
|
|
242
|
-
if (!this.isRecording) {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
// Send stop continuous mode message
|
|
248
|
-
this.webSocketManager.sendMessage({
|
|
249
|
-
t: 'stop_continuous_mode',
|
|
250
|
-
ttpId: this.generateTtpId()
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// Stop audio recording
|
|
254
|
-
await this.audioRecorder.stop();
|
|
255
|
-
|
|
256
|
-
// Stop audio playback immediately when stopping recording
|
|
257
|
-
this.audioPlayer.stopImmediate();
|
|
258
|
-
|
|
259
|
-
return true;
|
|
260
|
-
} catch (error) {
|
|
261
|
-
this.emit('error', error);
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Toggle recording state
|
|
268
|
-
*/
|
|
269
|
-
async toggleRecording() {
|
|
270
|
-
if (this.isRecording) {
|
|
271
|
-
return await this.stopRecording();
|
|
272
|
-
} else {
|
|
273
|
-
return await this.startRecording();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* Stop audio playback immediately (for barge-in scenarios)
|
|
279
|
-
*/
|
|
280
|
-
stopAudioPlayback() {
|
|
281
|
-
this.audioPlayer.stopImmediate();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Handle barge-in (user starts speaking while audio is playing)
|
|
286
|
-
*/
|
|
287
|
-
async handleBargeIn() {
|
|
288
|
-
// Stop current audio playback immediately
|
|
289
|
-
this.stopAudioPlayback();
|
|
290
|
-
|
|
291
|
-
// If not already recording, start recording
|
|
292
|
-
if (!this.isRecording) {
|
|
293
|
-
await this.startRecording();
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Get current connection status
|
|
299
|
-
*/
|
|
300
|
-
getStatus() {
|
|
301
|
-
return {
|
|
302
|
-
isConnected: this.isConnected,
|
|
303
|
-
isRecording: this.isRecording,
|
|
304
|
-
isPlaying: this.isPlaying
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Update configuration
|
|
310
|
-
*/
|
|
311
|
-
updateConfig(newConfig) {
|
|
312
|
-
this.config = { ...this.config, ...newConfig };
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Generate unique TTP ID
|
|
317
|
-
*/
|
|
318
|
-
generateTtpId() {
|
|
319
|
-
return 'sdk_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Send hello message with appropriate authentication
|
|
324
|
-
*/
|
|
325
|
-
sendHelloMessage() {
|
|
326
|
-
if (!this.isConnected) {
|
|
327
|
-
console.warn('VoiceSDK: Cannot send hello message - not connected');
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const helloMessage = {
|
|
332
|
-
t: "hello"
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
// Use app ID for authentication (preferred method)
|
|
336
|
-
if (this.config.appId) {
|
|
337
|
-
helloMessage.appId = this.config.appId;
|
|
338
|
-
console.log('VoiceSDK: Sending hello message with appId (app-based authentication)');
|
|
339
|
-
} else if (this.config.ttpId) {
|
|
340
|
-
// Fallback to custom TTP ID if app ID not provided
|
|
341
|
-
helloMessage.ttpId = this.config.ttpId;
|
|
342
|
-
console.log('VoiceSDK: Sending hello message with custom TTP ID (fallback method)');
|
|
343
|
-
} else {
|
|
344
|
-
// Generate TTP ID as last resort
|
|
345
|
-
helloMessage.ttpId = this.generateTtpId();
|
|
346
|
-
console.log('VoiceSDK: Sending hello message with generated TTP ID (last resort)');
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Note: agentId is now sent as query parameter in WebSocket URL, not in hello message
|
|
350
|
-
|
|
351
|
-
// Log authentication method for debugging
|
|
352
|
-
if (this.config.appId) {
|
|
353
|
-
console.log('VoiceSDK: Using app ID for authentication:', this.config.appId);
|
|
354
|
-
} else if (this.config.ttpId) {
|
|
355
|
-
console.log('VoiceSDK: Using custom TTP ID:', this.config.ttpId);
|
|
356
|
-
} else {
|
|
357
|
-
console.log('VoiceSDK: Using generated TTP ID:', helloMessage.ttpId);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
this.webSocketManager.sendMessage(helloMessage);
|
|
362
|
-
console.log('VoiceSDK: Hello message sent:', helloMessage);
|
|
363
|
-
} catch (error) {
|
|
364
|
-
console.error('VoiceSDK: Failed to send hello message:', error);
|
|
365
|
-
this.emit('error', error);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Cleanup resources
|
|
371
|
-
*/
|
|
372
|
-
destroy() {
|
|
373
|
-
if (this.isDestroyed) {
|
|
374
|
-
console.log(`🎙️ VoiceSDK: Destroy called but already destroyed`);
|
|
375
|
-
return; // Prevent multiple destroy calls
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
console.log(`🎙️ VoiceSDK: Destroying VoiceSDK instance`);
|
|
379
|
-
|
|
380
|
-
// Disconnect first, before setting isDestroyed
|
|
381
|
-
this.disconnect();
|
|
382
|
-
|
|
383
|
-
this.isDestroyed = true;
|
|
384
|
-
this.audioRecorder.destroy();
|
|
385
|
-
this.audioPlayer.destroy();
|
|
386
|
-
this.removeAllListeners();
|
|
387
|
-
|
|
388
|
-
console.log(`🎙️ VoiceSDK: VoiceSDK instance destroyed`);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocketManager - Handles WebSocket connection and message routing
|
|
3
|
-
*/
|
|
4
|
-
import EventEmitter from './EventEmitter.js';
|
|
5
|
-
import connectionManager from './ConnectionManager.js';
|
|
6
|
-
|
|
7
|
-
export default class WebSocketManager extends EventEmitter {
|
|
8
|
-
constructor(config) {
|
|
9
|
-
super();
|
|
10
|
-
this.config = config;
|
|
11
|
-
this.ws = null;
|
|
12
|
-
this.isConnected = false;
|
|
13
|
-
this.reconnectAttempts = 0;
|
|
14
|
-
this.maxReconnectAttempts = config.autoReconnect !== false ? 3 : 0; // Disable auto-reconnect if explicitly set to false
|
|
15
|
-
this.isReconnecting = false;
|
|
16
|
-
this.isConnecting = false; // Track if we're currently trying to connect
|
|
17
|
-
this.connectionId = null; // Unique ID for this connection attempt
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Connect to WebSocket
|
|
22
|
-
*/
|
|
23
|
-
async connect() {
|
|
24
|
-
return new Promise((resolve, reject) => {
|
|
25
|
-
try {
|
|
26
|
-
// Prevent multiple connections
|
|
27
|
-
if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
|
|
28
|
-
resolve();
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Prevent connection if already reconnecting
|
|
33
|
-
if (this.isReconnecting) {
|
|
34
|
-
resolve();
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Prevent connection if already connecting
|
|
39
|
-
if (this.isConnecting) {
|
|
40
|
-
resolve();
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Check if connection is allowed by global manager
|
|
45
|
-
if (!connectionManager.isConnectionAllowed(this.config.websocketUrl)) {
|
|
46
|
-
console.log(`🔌 WebSocketManager: Connection blocked by global manager for ${this.config.websocketUrl}`);
|
|
47
|
-
resolve();
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
this.isConnecting = true;
|
|
52
|
-
this.connectionId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
53
|
-
|
|
54
|
-
// Register with global connection manager
|
|
55
|
-
if (!connectionManager.registerConnection(this.config.websocketUrl, this.connectionId)) {
|
|
56
|
-
console.log(`🔌 WebSocketManager: Connection registration failed for ${this.connectionId}`);
|
|
57
|
-
this.isConnecting = false;
|
|
58
|
-
resolve();
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
console.log(`🔌 WebSocketManager: Starting connection attempt ${this.connectionId}`);
|
|
63
|
-
this.ws = new WebSocket(this.config.websocketUrl);
|
|
64
|
-
|
|
65
|
-
this.ws.onopen = () => {
|
|
66
|
-
console.log(`🔌 WebSocketManager: Connection successful ${this.connectionId}`);
|
|
67
|
-
this.isConnected = true;
|
|
68
|
-
this.reconnectAttempts = 0;
|
|
69
|
-
this.isReconnecting = false;
|
|
70
|
-
this.isConnecting = false;
|
|
71
|
-
this.emit('connected');
|
|
72
|
-
resolve();
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
this.ws.onmessage = (event) => {
|
|
76
|
-
this.handleMessage(event);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
this.ws.onclose = (event) => {
|
|
80
|
-
console.log(`🔌 WebSocketManager: Connection closed ${this.connectionId} (Code: ${event.code})`);
|
|
81
|
-
this.isConnected = false;
|
|
82
|
-
this.isConnecting = false;
|
|
83
|
-
this.emit('disconnected', event);
|
|
84
|
-
|
|
85
|
-
// Attempt reconnection if not intentional and not already reconnecting
|
|
86
|
-
if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts && !this.isReconnecting) {
|
|
87
|
-
this.isReconnecting = true;
|
|
88
|
-
this.reconnectAttempts++;
|
|
89
|
-
console.log(`🔌 WebSocketManager: Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
|
|
90
|
-
setTimeout(() => {
|
|
91
|
-
this.isReconnecting = false;
|
|
92
|
-
this.connect().catch(() => {
|
|
93
|
-
// Ignore reconnection errors to prevent infinite loops
|
|
94
|
-
});
|
|
95
|
-
}, 1000 * this.reconnectAttempts);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
this.ws.onerror = (error) => {
|
|
100
|
-
console.log(`🔌 WebSocketManager: Connection error ${this.connectionId}`, error);
|
|
101
|
-
this.isConnecting = false;
|
|
102
|
-
this.emit('error', error);
|
|
103
|
-
reject(error);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
} catch (error) {
|
|
107
|
-
console.log(`🔌 WebSocketManager: Connection failed ${this.connectionId}`, error);
|
|
108
|
-
this.isConnecting = false;
|
|
109
|
-
reject(error);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Disconnect from WebSocket
|
|
116
|
-
*/
|
|
117
|
-
disconnect() {
|
|
118
|
-
// Stop any reconnection attempts
|
|
119
|
-
this.isReconnecting = false;
|
|
120
|
-
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
|
|
121
|
-
|
|
122
|
-
// Unregister from global connection manager
|
|
123
|
-
if (this.connectionId) {
|
|
124
|
-
connectionManager.unregisterConnection(this.config.websocketUrl, this.connectionId);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
128
|
-
this.ws.close(1000, 'Intentional disconnect');
|
|
129
|
-
}
|
|
130
|
-
this.ws = null;
|
|
131
|
-
this.isConnected = false;
|
|
132
|
-
this.isConnecting = false;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Reset reconnection attempts (useful for manual reconnection)
|
|
137
|
-
*/
|
|
138
|
-
resetReconnectionAttempts() {
|
|
139
|
-
this.reconnectAttempts = 0;
|
|
140
|
-
this.isReconnecting = false;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Clear all global connections (useful for testing)
|
|
145
|
-
*/
|
|
146
|
-
static clearAllConnections() {
|
|
147
|
-
connectionManager.clearAll();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Send JSON message
|
|
152
|
-
*/
|
|
153
|
-
sendMessage(message) {
|
|
154
|
-
if (!this.isConnected || !this.ws) {
|
|
155
|
-
throw new Error('WebSocket not connected');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
this.ws.send(JSON.stringify(message));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Send binary data
|
|
163
|
-
*/
|
|
164
|
-
sendBinary(data) {
|
|
165
|
-
if (!this.isConnected || !this.ws) {
|
|
166
|
-
throw new Error('WebSocket not connected');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
this.ws.send(data);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Handle incoming WebSocket messages
|
|
174
|
-
*/
|
|
175
|
-
handleMessage(event) {
|
|
176
|
-
// Check if it's binary data first
|
|
177
|
-
if (event.data instanceof ArrayBuffer) {
|
|
178
|
-
this.emit('binaryAudio', event.data);
|
|
179
|
-
return;
|
|
180
|
-
} else if (event.data instanceof Blob) {
|
|
181
|
-
event.data.arrayBuffer().then(arrayBuffer => {
|
|
182
|
-
this.emit('binaryAudio', arrayBuffer);
|
|
183
|
-
}).catch(err => {
|
|
184
|
-
this.emit('error', err);
|
|
185
|
-
});
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Handle JSON messages
|
|
190
|
-
try {
|
|
191
|
-
const message = JSON.parse(event.data);
|
|
192
|
-
|
|
193
|
-
// Handle barge-in related messages
|
|
194
|
-
if (message.t === 'barge_in_ack' || message.t === 'stop_sending') {
|
|
195
|
-
this.emit('bargeIn', message);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Handle stop playing message
|
|
199
|
-
if (message.t === 'stop_playing') {
|
|
200
|
-
this.emit('stopPlaying', message);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
this.emit('message', message);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
this.emit('error', error);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Get connection status
|
|
211
|
-
*/
|
|
212
|
-
getStatus() {
|
|
213
|
-
return {
|
|
214
|
-
isConnected: this.isConnected,
|
|
215
|
-
readyState: this.ws ? this.ws.readyState : WebSocket.CLOSED
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
}
|