fix(frontend): clean up voice preview listeners
This commit is contained in:
1
frontend/package-lock.json
generated
1
frontend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "ai-video-admin-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@daily-co/daily-js": "^0.90.0",
|
||||
"@pipecat-ai/client-js": "^1.12.0",
|
||||
"@pipecat-ai/small-webrtc-transport": "^1.10.5",
|
||||
"@xyflow/react": "^12.11.0",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@daily-co/daily-js": "^0.90.0",
|
||||
"@pipecat-ai/client-js": "^1.12.0",
|
||||
"@pipecat-ai/small-webrtc-transport": "^1.10.5",
|
||||
"@xyflow/react": "^12.11.0",
|
||||
|
||||
@@ -176,6 +176,10 @@ export function WorkflowCanvas({
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [addSourceId, setAddSourceId] = useState<string | null>(null);
|
||||
const [addPosition, setAddPosition] = useState<{ x: number; y: number } | null>(null);
|
||||
// Fast Refresh 会替换模块级对象,而保留 React Flow 实例。用 state 固定本次
|
||||
// 挂载使用的映射,避免热更新后被误判为每次渲染都在创建新对象。
|
||||
const [flowNodeTypes] = useState(nodeTypes);
|
||||
const [flowEdgeTypes] = useState(edgeTypes);
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
|
||||
// 只在可持久化的画布内容真正变化时回传。React Flow 挂载时会写入节点尺寸等
|
||||
@@ -569,8 +573,8 @@ export function WorkflowCanvas({
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
nodeTypes={flowNodeTypes}
|
||||
edgeTypes={flowEdgeTypes}
|
||||
onNodesChange={handleNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
|
||||
@@ -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