mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-19 08:28:10 +02:00
fix(ui): release microphone stream on call teardown so a second test call works (#446)
* fix(ui): release microphone stream on call teardown The browser "Call Voice Agent" test call worked only once per page load. A second attempt failed (mic stuck / "Could not acquire media") until the user reloaded the page or cleared site data and re-granted mic permission. Root cause: the MediaStream from getUserMedia() was added to the peer connection but never retained or explicitly stopped on teardown. On hangup only sender.track.stop() (via pc.getSenders()) ran; browsers can keep the microphone device held through the original MediaStream reference, so the next getUserMedia() is blocked. Fix: keep the stream in localStreamRef and stop all of its tracks in cleanupConnection() (the shared teardown path) and in the unmount cleanup, so the device is fully released between calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(ui): harden microphone release in webrtc hook and embed widget Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: AYMENPAKISS2 <tech.nomatrade@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
This commit is contained in:
parent
951e73a645
commit
37c4bda0c7
2 changed files with 50 additions and 2 deletions
|
|
@ -633,6 +633,11 @@
|
|||
// Request microphone permission
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
// Release any stream still held from a prior attempt before retaining
|
||||
// the new one, so a re-entrant start can't leak the microphone.
|
||||
if (state.stream) {
|
||||
state.stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
state.stream = stream;
|
||||
} catch (micError) {
|
||||
// Handle specific microphone permission errors
|
||||
|
|
@ -660,6 +665,31 @@
|
|||
|
||||
} catch (error) {
|
||||
console.error('Dograh Widget: Failed to start call', error);
|
||||
|
||||
// Release anything acquired before the failure so a retry starts clean.
|
||||
// getUserMedia may have succeeded before a later step (WebSocket /
|
||||
// negotiation) threw, which would otherwise leave the mic held and block
|
||||
// the next getUserMedia(). Null the refs before close() so the peer/ws
|
||||
// state handlers short-circuit instead of re-entering teardown.
|
||||
if (state.stream) {
|
||||
state.stream.getTracks().forEach(track => track.stop());
|
||||
state.stream = null;
|
||||
}
|
||||
if (state.pc) {
|
||||
const pc = state.pc;
|
||||
state.pc = null;
|
||||
if (pc.signalingState !== 'closed') {
|
||||
pc.close();
|
||||
}
|
||||
}
|
||||
if (state.ws) {
|
||||
const ws = state.ws;
|
||||
state.ws = null;
|
||||
if (ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
|
||||
updateStatus('failed', 'Connection failed', error.message || 'Please check your microphone and try again');
|
||||
|
||||
// Trigger error callback
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
|
|||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const pcRef = useRef<RTCPeerConnection | null>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const localStreamRef = useRef<MediaStream | null>(null);
|
||||
const timeStartRef = useRef<number | null>(null);
|
||||
const onNodeTransitionRef = useRef(onNodeTransition);
|
||||
const connectionActiveRef = useRef(connectionActive);
|
||||
|
|
@ -153,6 +154,16 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
|
|||
}
|
||||
}, []);
|
||||
|
||||
const stopLocalStream = useCallback(() => {
|
||||
// Release the microphone so the device is freed for a subsequent call.
|
||||
// Stopping the sender tracks via pc.getSenders() alone can leave the
|
||||
// browser holding the mic, blocking the next getUserMedia().
|
||||
if (localStreamRef.current) {
|
||||
localStreamRef.current.getTracks().forEach((track) => track.stop());
|
||||
localStreamRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const cleanupConnection = useCallback((options: CleanupConnectionOptions = {}) => {
|
||||
const graceful = options.graceful ?? true;
|
||||
const status = options.status ?? (graceful ? 'idle' : 'failed');
|
||||
|
|
@ -173,10 +184,12 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
|
|||
wsRef.current = null;
|
||||
}
|
||||
|
||||
stopLocalStream();
|
||||
|
||||
if (options.closePeerConnection !== false) {
|
||||
closePeerConnection(pcRef.current, options.delayPeerClose ?? false);
|
||||
}
|
||||
}, [closePeerConnection]);
|
||||
}, [closePeerConnection, stopLocalStream]);
|
||||
|
||||
const createPeerConnection = () => {
|
||||
// Build ICE servers list
|
||||
|
|
@ -743,6 +756,10 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
|
|||
if (constraints.audio) {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
// Release any stream still held from a prior attempt before
|
||||
// retaining the new one, so re-entry can't leak a device.
|
||||
stopLocalStream();
|
||||
localStreamRef.current = stream;
|
||||
stream.getTracks().forEach((track) => {
|
||||
pc.addTrack(track, stream);
|
||||
});
|
||||
|
|
@ -770,6 +787,7 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
|
|||
// Cleanup on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
stopLocalStream();
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
|
|
@ -777,7 +795,7 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
|
|||
pcRef.current.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [stopLocalStream]);
|
||||
|
||||
return {
|
||||
audioRef,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue