ttp-agent-sdk 2.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/GETTING_STARTED.md +429 -0
- package/README.md +303 -0
- package/dist/agent-widget.js +3 -0
- package/dist/agent-widget.js.LICENSE.txt +21 -0
- package/dist/agent-widget.js.map +1 -0
- package/dist/examples/react-example.html +455 -0
- package/dist/examples/react-example.jsx +307 -0
- package/dist/examples/test.html +235 -0
- package/dist/examples/vanilla-example.html +464 -0
- package/dist/index.html +224 -0
- package/examples/react-example.html +455 -0
- package/examples/react-example.jsx +307 -0
- package/examples/test.html +235 -0
- package/examples/vanilla-example.html +464 -0
- package/package.json +63 -0
- package/src/core/AudioPlayer.js +185 -0
- package/src/core/AudioRecorder.js +128 -0
- package/src/core/ConnectionManager.js +86 -0
- package/src/core/EventEmitter.js +53 -0
- package/src/core/VoiceSDK.js +390 -0
- package/src/core/WebSocketManager.js +218 -0
- package/src/core/WebSocketManagerV2.js +211 -0
- package/src/core/WebSocketSingleton.js +171 -0
- package/src/index.js +64 -0
- package/src/legacy/AgentSDK.js +462 -0
- package/src/react/VoiceButton.jsx +163 -0
- package/src/vanilla/VoiceButton.js +190 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AudioRecorder - Handles audio recording using AudioWorklet
|
|
3
|
+
*/
|
|
4
|
+
import EventEmitter from './EventEmitter.js';
|
|
5
|
+
|
|
6
|
+
export default class AudioRecorder extends EventEmitter {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
super();
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.audioContext = null;
|
|
11
|
+
this.audioWorkletNode = null;
|
|
12
|
+
this.mediaStream = null;
|
|
13
|
+
this.isRecording = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Start audio recording
|
|
18
|
+
*/
|
|
19
|
+
async start() {
|
|
20
|
+
try {
|
|
21
|
+
// Get user media
|
|
22
|
+
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
23
|
+
audio: {
|
|
24
|
+
sampleRate: this.config.sampleRate,
|
|
25
|
+
channelCount: 1,
|
|
26
|
+
echoCancellation: true,
|
|
27
|
+
noiseSuppression: true,
|
|
28
|
+
autoGainControl: true
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Create AudioContext
|
|
33
|
+
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({
|
|
34
|
+
sampleRate: this.config.sampleRate
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Resume AudioContext if suspended
|
|
38
|
+
if (this.audioContext.state === 'suspended') {
|
|
39
|
+
await this.audioContext.resume();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Load AudioWorklet module
|
|
43
|
+
await this.audioContext.audioWorklet.addModule('/audio-processor.js');
|
|
44
|
+
|
|
45
|
+
// Create AudioWorklet node
|
|
46
|
+
this.audioWorkletNode = new AudioWorkletNode(this.audioContext, 'audio-processor');
|
|
47
|
+
|
|
48
|
+
// Create media stream source
|
|
49
|
+
const source = this.audioContext.createMediaStreamSource(this.mediaStream);
|
|
50
|
+
source.connect(this.audioWorkletNode);
|
|
51
|
+
|
|
52
|
+
// Handle messages from AudioWorklet
|
|
53
|
+
this.audioWorkletNode.port.onmessage = (event) => {
|
|
54
|
+
const { type, data } = event.data;
|
|
55
|
+
|
|
56
|
+
if (type === 'pcm_audio_data') {
|
|
57
|
+
this.emit('audioData', data);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Enable continuous mode
|
|
62
|
+
this.audioWorkletNode.port.postMessage({
|
|
63
|
+
type: 'setForceContinuous',
|
|
64
|
+
data: { enabled: true }
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.isRecording = true;
|
|
68
|
+
this.emit('recordingStarted');
|
|
69
|
+
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.emit('error', error);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Stop audio recording
|
|
78
|
+
*/
|
|
79
|
+
async stop() {
|
|
80
|
+
if (!this.isRecording) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Flush any remaining audio data
|
|
86
|
+
if (this.audioWorkletNode) {
|
|
87
|
+
this.audioWorkletNode.port.postMessage({ type: 'flush' });
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Disconnect and cleanup
|
|
92
|
+
if (this.mediaStream) {
|
|
93
|
+
this.mediaStream.getTracks().forEach(track => track.stop());
|
|
94
|
+
this.mediaStream = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (this.audioContext && this.audioContext.state !== 'closed') {
|
|
98
|
+
await this.audioContext.close();
|
|
99
|
+
this.audioContext = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.audioWorkletNode = null;
|
|
103
|
+
this.isRecording = false;
|
|
104
|
+
this.emit('recordingStopped');
|
|
105
|
+
|
|
106
|
+
} catch (error) {
|
|
107
|
+
this.emit('error', error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get recording status
|
|
114
|
+
*/
|
|
115
|
+
getStatus() {
|
|
116
|
+
return {
|
|
117
|
+
isRecording: this.isRecording,
|
|
118
|
+
audioContextState: this.audioContext ? this.audioContext.state : 'closed'
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Cleanup resources
|
|
124
|
+
*/
|
|
125
|
+
destroy() {
|
|
126
|
+
this.stop();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectionManager - Global connection manager to prevent multiple connections to the same URL
|
|
3
|
+
*/
|
|
4
|
+
class ConnectionManager {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.connections = new Map(); // Map of URL -> connection info
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Register a connection attempt
|
|
11
|
+
*/
|
|
12
|
+
registerConnection(url, connectionId) {
|
|
13
|
+
if (!this.connections.has(url)) {
|
|
14
|
+
this.connections.set(url, {
|
|
15
|
+
connectionId,
|
|
16
|
+
timestamp: Date.now(),
|
|
17
|
+
count: 1
|
|
18
|
+
});
|
|
19
|
+
console.log(`🔌 ConnectionManager: Registered connection ${connectionId} for ${url}`);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const existing = this.connections.get(url);
|
|
24
|
+
const timeSinceLastConnection = Date.now() - existing.timestamp;
|
|
25
|
+
|
|
26
|
+
// If it's been more than 30 seconds since the last connection, allow it
|
|
27
|
+
if (timeSinceLastConnection > 30000) {
|
|
28
|
+
this.connections.set(url, {
|
|
29
|
+
connectionId,
|
|
30
|
+
timestamp: Date.now(),
|
|
31
|
+
count: 1
|
|
32
|
+
});
|
|
33
|
+
console.log(`🔌 ConnectionManager: Allowed new connection ${connectionId} for ${url} (old connection was ${timeSinceLastConnection}ms ago)`);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Otherwise, prevent the connection
|
|
38
|
+
existing.count++;
|
|
39
|
+
console.log(`🔌 ConnectionManager: Blocked connection ${connectionId} for ${url} (${existing.count} attempts in ${timeSinceLastConnection}ms)`);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Unregister a connection
|
|
45
|
+
*/
|
|
46
|
+
unregisterConnection(url, connectionId) {
|
|
47
|
+
const existing = this.connections.get(url);
|
|
48
|
+
if (existing && existing.connectionId === connectionId) {
|
|
49
|
+
this.connections.delete(url);
|
|
50
|
+
console.log(`🔌 ConnectionManager: Unregistered connection ${connectionId} for ${url}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a connection is allowed
|
|
56
|
+
*/
|
|
57
|
+
isConnectionAllowed(url) {
|
|
58
|
+
const existing = this.connections.get(url);
|
|
59
|
+
if (!existing) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const timeSinceLastConnection = Date.now() - existing.timestamp;
|
|
64
|
+
return timeSinceLastConnection > 30000; // Allow if more than 30 seconds ago
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get connection info
|
|
69
|
+
*/
|
|
70
|
+
getConnectionInfo(url) {
|
|
71
|
+
return this.connections.get(url);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Clear all connections (useful for testing)
|
|
76
|
+
*/
|
|
77
|
+
clearAll() {
|
|
78
|
+
this.connections.clear();
|
|
79
|
+
console.log('🔌 ConnectionManager: Cleared all connections');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Global instance
|
|
84
|
+
const connectionManager = new ConnectionManager();
|
|
85
|
+
|
|
86
|
+
export default connectionManager;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventEmitter - Simple event system for the VoiceSDK
|
|
3
|
+
*/
|
|
4
|
+
export default class EventEmitter {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.events = {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Add event listener
|
|
11
|
+
*/
|
|
12
|
+
on(event, callback) {
|
|
13
|
+
if (!this.events[event]) {
|
|
14
|
+
this.events[event] = [];
|
|
15
|
+
}
|
|
16
|
+
this.events[event].push(callback);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Remove event listener
|
|
21
|
+
*/
|
|
22
|
+
off(event, callback) {
|
|
23
|
+
if (!this.events[event]) return;
|
|
24
|
+
|
|
25
|
+
this.events[event] = this.events[event].filter(cb => cb !== callback);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Emit event
|
|
30
|
+
*/
|
|
31
|
+
emit(event, ...args) {
|
|
32
|
+
if (!this.events[event]) return;
|
|
33
|
+
|
|
34
|
+
this.events[event].forEach(callback => {
|
|
35
|
+
try {
|
|
36
|
+
callback(...args);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(`Error in event listener for ${event}:`, error);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Remove all listeners for an event
|
|
45
|
+
*/
|
|
46
|
+
removeAllListeners(event) {
|
|
47
|
+
if (event) {
|
|
48
|
+
delete this.events[event];
|
|
49
|
+
} else {
|
|
50
|
+
this.events = {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
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.bidme.co.il/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
|
+
}
|