fix(frontend): clean up voice preview listeners
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import Daily from "@daily-co/daily-js";
|
||||
import type { PipecatClientOptions, RTVIMessage } from "@pipecat-ai/client-js";
|
||||
import { SmallWebRTCTransport } from "@pipecat-ai/small-webrtc-transport";
|
||||
|
||||
@@ -101,6 +102,21 @@ function publicVariableSnapshot(
|
||||
class AppSmallWebRTCTransport extends SmallWebRTCTransport {
|
||||
onAppMessage?: (message: AppMessage) => void;
|
||||
|
||||
/**
|
||||
* SmallWebRTCTransport 的媒体管理器会复用 Daily 的全局 call object,
|
||||
* 但 disconnect() 不会移除构造时注册的设备和 track 监听器。销毁 call
|
||||
* object 后,下一次连接会得到干净的实例,避免监听器跨会话累积。
|
||||
*/
|
||||
async dispose(): Promise<void> {
|
||||
this.onAppMessage = undefined;
|
||||
try {
|
||||
await this.disconnect();
|
||||
} finally {
|
||||
const call = Daily.getCallInstance();
|
||||
if (call && !call.isDestroyed()) await call.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
override handleMessage(raw: string): void {
|
||||
try {
|
||||
const message = JSON.parse(raw) as AppMessage;
|
||||
@@ -262,6 +278,8 @@ export function useVoicePreview(
|
||||
const [selectedOutputDeviceId, setSelectedOutputDeviceId] = useState("");
|
||||
|
||||
const transportRef = useRef<AppSmallWebRTCTransport | null>(null);
|
||||
const cleanupPromiseRef = useRef<Promise<void>>(Promise.resolve());
|
||||
const connectionGenerationRef = useRef(0);
|
||||
const startingRef = useRef(false);
|
||||
const messageSeqRef = useRef(0);
|
||||
const networkStatsRef = useRef<NetworkStatsSample | null>(null);
|
||||
@@ -321,9 +339,14 @@ export function useVoicePreview(
|
||||
}, [refreshDevices]);
|
||||
|
||||
const releaseResources = useCallback(() => {
|
||||
connectionGenerationRef.current += 1;
|
||||
const transport = transportRef.current;
|
||||
transportRef.current = null;
|
||||
transport?.disconnect().catch(() => {});
|
||||
if (transport) {
|
||||
cleanupPromiseRef.current = cleanupPromiseRef.current
|
||||
.then(() => transport.dispose())
|
||||
.catch(() => {});
|
||||
}
|
||||
if (audioRef.current) audioRef.current.srcObject = null;
|
||||
startingRef.current = false;
|
||||
pendingAssistantTurnsRef.current.clear();
|
||||
@@ -585,6 +608,8 @@ export function useVoicePreview(
|
||||
}
|
||||
|
||||
startingRef.current = true;
|
||||
const generation = connectionGenerationRef.current + 1;
|
||||
connectionGenerationRef.current = generation;
|
||||
setStatus("connecting");
|
||||
setError(null);
|
||||
setMicWarning(null);
|
||||
@@ -594,10 +619,15 @@ export function useVoicePreview(
|
||||
setCallEnded(false);
|
||||
endedByServerRef.current = false;
|
||||
|
||||
await cleanupPromiseRef.current;
|
||||
if (connectionGenerationRef.current !== generation) return;
|
||||
|
||||
const iceServers = await webrtcApi
|
||||
.iceServers()
|
||||
.then((response) => response.iceServers)
|
||||
.catch(() => [{ urls: "stun:stun.l.google.com:19302" }]);
|
||||
if (connectionGenerationRef.current !== generation) return;
|
||||
|
||||
const transport = new AppSmallWebRTCTransport({ iceServers });
|
||||
transportRef.current = transport;
|
||||
transport.onAppMessage = handleAppMessage;
|
||||
@@ -648,9 +678,11 @@ export function useVoicePreview(
|
||||
|
||||
try {
|
||||
await transport.initDevices();
|
||||
if (connectionGenerationRef.current !== generation) return;
|
||||
if (selectedDeviceIdRef.current) {
|
||||
await transport.updateMic(selectedDeviceIdRef.current);
|
||||
}
|
||||
if (connectionGenerationRef.current !== generation) return;
|
||||
const localAudio = transport.tracks().local.audio;
|
||||
const localVideo = transport.tracks().local.video;
|
||||
setLocalStream(localAudio ? new MediaStream([localAudio]) : null);
|
||||
@@ -678,12 +710,17 @@ export function useVoicePreview(
|
||||
},
|
||||
},
|
||||
});
|
||||
if (connectionGenerationRef.current !== generation) return;
|
||||
transport.sendAppMessage({ type: "client-ready" });
|
||||
setStatus("connected");
|
||||
} catch (connectionError) {
|
||||
fail(errorMessage(connectionError, "无法连接语音服务。"));
|
||||
if (connectionGenerationRef.current === generation) {
|
||||
fail(errorMessage(connectionError, "无法连接语音服务。"));
|
||||
}
|
||||
} finally {
|
||||
startingRef.current = false;
|
||||
if (connectionGenerationRef.current === generation) {
|
||||
startingRef.current = false;
|
||||
}
|
||||
}
|
||||
}, [
|
||||
assistantId,
|
||||
|
||||
Reference in New Issue
Block a user