mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-01 08:59:46 +02:00
feat: persist split user and bot audio
This commit is contained in:
parent
dd3f2e7323
commit
3d1886c450
30 changed files with 1322 additions and 253 deletions
|
|
@ -26,10 +26,12 @@
|
|||
stream: null,
|
||||
sessionToken: null,
|
||||
workflowRunId: null,
|
||||
pcId: null,
|
||||
connectionStatus: 'idle', // idle, connecting, connected, failed
|
||||
audioElement: null,
|
||||
turnCredentials: null, // TURN server credentials
|
||||
callStartedAt: null, // Timestamp when call connected (for duration tracking)
|
||||
gracefulDisconnect: false,
|
||||
callbacks: {
|
||||
onReady: null,
|
||||
onCallStart: null,
|
||||
|
|
@ -611,6 +613,7 @@
|
|||
* Start voice call
|
||||
*/
|
||||
async function startCall() {
|
||||
state.gracefulDisconnect = false;
|
||||
updateStatus('connecting', 'Connecting...', 'Please wait while we establish the connection');
|
||||
|
||||
if (state.callbacks.onCallStart) {
|
||||
|
|
@ -766,45 +769,69 @@
|
|||
};
|
||||
|
||||
// Monitor connection state
|
||||
state.pc.oniceconnectionstatechange = () => {
|
||||
console.log('ICE connection state:', state.pc.iceConnectionState);
|
||||
state.pc.oniceconnectionstatechange = handlePeerConnectionStateChange;
|
||||
state.pc.onconnectionstatechange = handlePeerConnectionStateChange;
|
||||
state.pc.onicecandidate = sendIceCandidate;
|
||||
}
|
||||
|
||||
if (state.pc.iceConnectionState === 'connected' || state.pc.iceConnectionState === 'completed') {
|
||||
const wasAlreadyConnected = state.callStartedAt !== null;
|
||||
updateStatus('connected', 'Connected', 'Your voice call is now active');
|
||||
if (!wasAlreadyConnected) {
|
||||
state.callStartedAt = Date.now();
|
||||
if (state.callbacks.onCallConnected) {
|
||||
state.callbacks.onCallConnected({
|
||||
agentId: state.config.workflowId || null,
|
||||
token: state.config.token || null,
|
||||
workflowRunId: state.workflowRunId || null
|
||||
});
|
||||
}
|
||||
function handlePeerConnectionStateChange() {
|
||||
const pc = state.pc;
|
||||
if (!pc) return;
|
||||
|
||||
console.log('Peer connection state:', pc.connectionState, 'ICE:', pc.iceConnectionState);
|
||||
|
||||
if (pc.connectionState === 'connected' || pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') {
|
||||
const wasAlreadyConnected = state.callStartedAt !== null;
|
||||
updateStatus('connected', 'Connected', 'Your voice call is now active');
|
||||
if (!wasAlreadyConnected) {
|
||||
state.callStartedAt = Date.now();
|
||||
if (state.callbacks.onCallConnected) {
|
||||
state.callbacks.onCallConnected({
|
||||
agentId: state.config.workflowId || null,
|
||||
token: state.config.token || null,
|
||||
workflowRunId: state.workflowRunId || null
|
||||
});
|
||||
}
|
||||
} else if (state.pc.iceConnectionState === 'failed' || state.pc.iceConnectionState === 'disconnected') {
|
||||
updateStatus('failed', 'Connection lost', 'The call has been disconnected');
|
||||
stopCall();
|
||||
}
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (pc.connectionState === 'failed' || pc.iceConnectionState === 'failed') {
|
||||
stopCall({
|
||||
graceful: false,
|
||||
status: 'failed',
|
||||
text: 'Connection lost',
|
||||
subtext: 'The call has been disconnected'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
pc.connectionState === 'closed' ||
|
||||
pc.connectionState === 'disconnected' ||
|
||||
pc.iceConnectionState === 'closed' ||
|
||||
pc.iceConnectionState === 'disconnected'
|
||||
) {
|
||||
stopCall({ graceful: true });
|
||||
}
|
||||
}
|
||||
|
||||
function sendIceCandidate(event) {
|
||||
// Handle ICE candidates for trickling
|
||||
state.pc.onicecandidate = (event) => {
|
||||
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
type: 'ice-candidate',
|
||||
payload: {
|
||||
candidate: event.candidate ? {
|
||||
candidate: event.candidate.candidate,
|
||||
sdpMid: event.candidate.sdpMid,
|
||||
sdpMLineIndex: event.candidate.sdpMLineIndex
|
||||
} : null,
|
||||
pc_id: state.pcId
|
||||
}
|
||||
};
|
||||
state.ws.send(JSON.stringify(message));
|
||||
}
|
||||
};
|
||||
if (state.ws && state.ws.readyState === WebSocket.OPEN) {
|
||||
const message = {
|
||||
type: 'ice-candidate',
|
||||
payload: {
|
||||
candidate: event.candidate ? {
|
||||
candidate: event.candidate.candidate,
|
||||
sdpMid: event.candidate.sdpMid,
|
||||
sdpMLineIndex: event.candidate.sdpMLineIndex
|
||||
} : null,
|
||||
pc_id: state.pcId
|
||||
}
|
||||
};
|
||||
state.ws.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -828,9 +855,16 @@
|
|||
reject(error);
|
||||
};
|
||||
|
||||
state.ws.onclose = () => {
|
||||
state.ws.onclose = (event) => {
|
||||
console.log('WebSocket closed');
|
||||
if (state.connectionStatus === 'connected') {
|
||||
state.ws = null;
|
||||
|
||||
if (event.reason === 'call ended') {
|
||||
stopCall({ graceful: true, closeWebSocket: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.connectionStatus === 'connected' && !state.gracefulDisconnect) {
|
||||
updateStatus('failed', 'Connection lost', 'The call has been disconnected');
|
||||
}
|
||||
};
|
||||
|
|
@ -882,6 +916,11 @@
|
|||
updateStatus('failed', 'Server error', message.payload.message || 'An error occurred');
|
||||
break;
|
||||
|
||||
case 'call-ended':
|
||||
console.log('Call ended by server:', message.payload);
|
||||
stopCall({ graceful: true });
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('Unknown message type:', message.type);
|
||||
}
|
||||
|
|
@ -913,7 +952,15 @@
|
|||
/**
|
||||
* Stop voice call
|
||||
*/
|
||||
function stopCall() {
|
||||
function stopCall(options = {}) {
|
||||
const graceful = options.graceful !== false;
|
||||
const closeWebSocket = options.closeWebSocket !== false;
|
||||
const status = options.status || 'idle';
|
||||
const text = options.text || 'Call ended';
|
||||
const subtext = options.subtext || 'Click below to start a new call';
|
||||
|
||||
state.gracefulDisconnect = graceful;
|
||||
|
||||
// Fire onCallDisconnected only if the call had actually connected, with
|
||||
// identifiers and duration. Must run before we clear callStartedAt.
|
||||
if (state.callStartedAt && state.callbacks.onCallDisconnected) {
|
||||
|
|
@ -927,15 +974,20 @@
|
|||
}
|
||||
state.callStartedAt = null;
|
||||
|
||||
updateStatus('idle', 'Call ended', 'Click below to start a new call');
|
||||
updateStatus(status, text, subtext);
|
||||
|
||||
if (state.callbacks.onCallEnd) {
|
||||
state.callbacks.onCallEnd();
|
||||
}
|
||||
|
||||
// Close WebSocket
|
||||
if (state.ws) {
|
||||
state.ws.close();
|
||||
if (closeWebSocket && state.ws) {
|
||||
const ws = state.ws;
|
||||
state.ws = null;
|
||||
if (ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
|
||||
ws.close();
|
||||
}
|
||||
} else if (!closeWebSocket) {
|
||||
state.ws = null;
|
||||
}
|
||||
|
||||
|
|
@ -947,8 +999,11 @@
|
|||
|
||||
// Close peer connection
|
||||
if (state.pc) {
|
||||
state.pc.close();
|
||||
const pc = state.pc;
|
||||
state.pc = null;
|
||||
if (pc.signalingState !== 'closed') {
|
||||
pc.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear audio
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue