fix: mute audio output during System Audio and Mic capture to prevent echo

Insert a zero-gain GainNode between ScriptProcessorNode and
audioContext.destination. The processor stays in the graph (so
onaudioprocess fires on all browsers) but zero volume reaches the
speakers, eliminating the echo/feedback loop during live capture.
This commit is contained in:
Woody 2026-05-18 14:04:42 +08:00
parent f637ab10a5
commit 80af17a255
1 changed files with 12 additions and 4 deletions

View File

@ -28,6 +28,7 @@ export function useMediaStreamASR({ wsUrl }: UseMediaStreamASRProps): UseMediaSt
const wsRef = useRef<WebSocket | null>(null)
const audioContextRef = useRef<AudioContext | null>(null)
const processorRef = useRef<ScriptProcessorNode | null>(null)
const gainNodeRef = useRef<GainNode | null>(null)
const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null)
const streamRef = useRef<MediaStream | null>(null)
const isStreamingRef = useRef(false)
@ -63,8 +64,10 @@ export function useMediaStreamASR({ wsUrl }: UseMediaStreamASRProps): UseMediaSt
}
processorRef.current?.disconnect()
gainNodeRef.current?.disconnect()
sourceRef.current?.disconnect()
processorRef.current = null
gainNodeRef.current = null
sourceRef.current = null
if (wsRef.current) {
@ -114,18 +117,22 @@ export function useMediaStreamASR({ wsUrl }: UseMediaStreamASRProps): UseMediaSt
const processor = audioContext.createScriptProcessor(4096, 1, 1)
processorRef.current = processor
// onaudioprocess — mirrors useVideoASR lines 126-132 exactly
// Zero-gain node mutes audio output to prevent echo/feedback. The processor
// must remain in the graph (connected to destination) so onaudioprocess fires.
const zeroGain = audioContext.createGain()
zeroGain.gain.value = 0
gainNodeRef.current = zeroGain
processor.onaudioprocess = (e) => {
const float32Data = e.inputBuffer.getChannelData(0)
const outputData = e.outputBuffer.getChannelData(0)
outputData.set(float32Data)
if (!isStreamingRef.current) return
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return
wsRef.current.send(float32Data.buffer)
}
source.connect(processor)
processor.connect(audioContext.destination)
processor.connect(zeroGain)
zeroGain.connect(audioContext.destination)
const ws = new WebSocket(wsUrl)
wsRef.current = ws
@ -181,6 +188,7 @@ export function useMediaStreamASR({ wsUrl }: UseMediaStreamASRProps): UseMediaSt
})
}
processorRef.current?.disconnect()
gainNodeRef.current?.disconnect()
sourceRef.current?.disconnect()
wsRef.current?.close()
audioContextRef.current?.close()