Refactor camera handling and video stream management in Assistant and MobileCall pages
- Remove direct camera start/stop calls and replace them with streamlined video stream handling. - Update DebugVideoPanel to accept a streamOverride for better flexibility in video source management. - Adjust MobileCallPage to utilize the new videoStream state, enhancing the call experience. - Introduce videoStream state management in useVoicePreview for improved media handling during voice interactions.
This commit is contained in:
@@ -2155,11 +2155,8 @@ function DebugDrawer({
|
|||||||
const [dynamicVariableValues, setDynamicVariableValues] = useState<
|
const [dynamicVariableValues, setDynamicVariableValues] = useState<
|
||||||
Record<string, string | number | boolean>
|
Record<string, string | number | boolean>
|
||||||
>({});
|
>({});
|
||||||
const stopCamera = camera.stop;
|
|
||||||
const startCamera = camera.start;
|
|
||||||
const recording =
|
const recording =
|
||||||
preview.status === "connecting" || preview.status === "connected";
|
preview.status === "connecting" || preview.status === "connected";
|
||||||
const shouldKeepCamera = vision && recording;
|
|
||||||
const dynamicVariableEntries = Object.entries(dynamicVariableDefinitions);
|
const dynamicVariableEntries = Object.entries(dynamicVariableDefinitions);
|
||||||
const resolvedDynamicVariables: Record<string, string | number | boolean> = {};
|
const resolvedDynamicVariables: Record<string, string | number | boolean> = {};
|
||||||
let dynamicVariablesError = "";
|
let dynamicVariablesError = "";
|
||||||
@@ -2174,18 +2171,10 @@ function DebugDrawer({
|
|||||||
resolvedDynamicVariables[name] = value;
|
resolvedDynamicVariables[name] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (shouldKeepCamera) {
|
|
||||||
void startCamera();
|
|
||||||
} else {
|
|
||||||
stopCamera();
|
|
||||||
}
|
|
||||||
}, [shouldKeepCamera, startCamera, stopCamera]);
|
|
||||||
|
|
||||||
const selectCamera = useCallback(
|
const selectCamera = useCallback(
|
||||||
async (deviceId: string) => {
|
async (deviceId: string) => {
|
||||||
const nextStream = await camera.selectCamera(deviceId);
|
await camera.selectCamera(deviceId);
|
||||||
await preview.replaceVideoStream(nextStream);
|
preview.selectCamera(deviceId);
|
||||||
},
|
},
|
||||||
[camera, preview],
|
[camera, preview],
|
||||||
);
|
);
|
||||||
@@ -2666,15 +2655,12 @@ function DebugVoicePanel({
|
|||||||
const startConversation = useCallback(async () => {
|
const startConversation = useCallback(async () => {
|
||||||
if (!assistantId || hasUnsavedChanges) return;
|
if (!assistantId || hasUnsavedChanges) return;
|
||||||
if (dynamicVariablesError) return;
|
if (dynamicVariablesError) return;
|
||||||
const videoStream = vision ? await camera.start() : null;
|
|
||||||
await connect({
|
await connect({
|
||||||
visionEnabled: vision,
|
visionEnabled: vision,
|
||||||
videoStream,
|
|
||||||
dynamicVariables,
|
dynamicVariables,
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
assistantId,
|
assistantId,
|
||||||
camera,
|
|
||||||
connect,
|
connect,
|
||||||
dynamicVariables,
|
dynamicVariables,
|
||||||
dynamicVariablesError,
|
dynamicVariablesError,
|
||||||
@@ -2695,7 +2681,7 @@ function DebugVoicePanel({
|
|||||||
connect={startConversation}
|
connect={startConversation}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DebugVideoPanel camera={camera} />
|
<DebugVideoPanel camera={camera} streamOverride={preview.videoStream} />
|
||||||
)
|
)
|
||||||
) : !SHOW_VOICE_VIZ || showTranscript ? (
|
) : !SHOW_VOICE_VIZ || showTranscript ? (
|
||||||
showIdleHub ? (
|
showIdleHub ? (
|
||||||
@@ -2955,19 +2941,27 @@ function DebugIdleHub({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DebugVideoPanel({ camera }: { camera: CameraPreview }) {
|
function DebugVideoPanel({
|
||||||
|
camera,
|
||||||
|
streamOverride,
|
||||||
|
}: {
|
||||||
|
camera: CameraPreview;
|
||||||
|
streamOverride?: MediaStream | null;
|
||||||
|
}) {
|
||||||
const { stream, error, starting, active } = camera;
|
const { stream, error, starting, active } = camera;
|
||||||
|
const effectiveStream = streamOverride ?? stream;
|
||||||
|
const effectiveActive = Boolean(effectiveStream) || active;
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoRef.current) videoRef.current.srcObject = stream;
|
if (videoRef.current) videoRef.current.srcObject = effectiveStream;
|
||||||
}, [stream]);
|
}, [effectiveStream]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden",
|
"relative flex min-h-0 flex-1 items-center justify-center overflow-hidden",
|
||||||
active ? "bg-black" : "bg-canvas-soft",
|
effectiveActive ? "bg-black" : "bg-canvas-soft",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
@@ -2977,10 +2971,10 @@ function DebugVideoPanel({ camera }: { camera: CameraPreview }) {
|
|||||||
muted
|
muted
|
||||||
className={[
|
className={[
|
||||||
"h-full w-full -scale-x-100 object-contain",
|
"h-full w-full -scale-x-100 object-contain",
|
||||||
active ? "" : "hidden",
|
effectiveActive ? "" : "hidden",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
/>
|
/>
|
||||||
{active ? (
|
{effectiveActive ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="absolute left-3 top-3 gap-1.5 rounded-full border border-hairline bg-card/80 px-2.5 py-1 text-[11px] font-medium text-muted-foreground shadow-none backdrop-blur"
|
className="absolute left-3 top-3 gap-1.5 rounded-full border border-hairline bg-card/80 px-2.5 py-1 text-[11px] font-medium text-muted-foreground shadow-none backdrop-blur"
|
||||||
|
|||||||
@@ -251,18 +251,16 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
callEnded,
|
callEnded,
|
||||||
networkQuality,
|
networkQuality,
|
||||||
connect,
|
connect,
|
||||||
replaceVideoStream,
|
videoStream,
|
||||||
|
selectCamera: selectTransportCamera,
|
||||||
disconnect,
|
disconnect,
|
||||||
audioRef,
|
audioRef,
|
||||||
} = preview;
|
} = preview;
|
||||||
const {
|
const {
|
||||||
stream: cameraStream,
|
|
||||||
error: cameraError,
|
error: cameraError,
|
||||||
starting: cameraStarting,
|
starting: cameraStarting,
|
||||||
devices: cameraDevices,
|
devices: cameraDevices,
|
||||||
deviceId: cameraDeviceId,
|
deviceId: cameraDeviceId,
|
||||||
start: startCamera,
|
|
||||||
stop: stopCamera,
|
|
||||||
selectCamera: changeCamera,
|
selectCamera: changeCamera,
|
||||||
} = camera;
|
} = camera;
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
@@ -271,29 +269,26 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
const inCall = !callEnded && (connecting || status === "connected");
|
const inCall = !callEnded && (connecting || status === "connected");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoRef.current) videoRef.current.srcObject = cameraStream;
|
if (videoRef.current) videoRef.current.srcObject = videoStream;
|
||||||
}, [cameraStream]);
|
}, [videoStream]);
|
||||||
|
|
||||||
const startCall = useCallback(async () => {
|
const startCall = useCallback(async () => {
|
||||||
const videoStream = await startCamera();
|
|
||||||
await connect({
|
await connect({
|
||||||
visionEnabled: true,
|
visionEnabled: true,
|
||||||
videoStream,
|
|
||||||
});
|
});
|
||||||
}, [connect, startCamera]);
|
}, [connect]);
|
||||||
|
|
||||||
const endCall = useCallback(() => {
|
const endCall = useCallback(() => {
|
||||||
setView("camera");
|
setView("camera");
|
||||||
disconnect();
|
disconnect();
|
||||||
stopCamera();
|
}, [disconnect]);
|
||||||
}, [disconnect, stopCamera]);
|
|
||||||
|
|
||||||
const selectCamera = useCallback(
|
const selectCamera = useCallback(
|
||||||
async (deviceId: string) => {
|
async (deviceId: string) => {
|
||||||
const nextStream = await changeCamera(deviceId);
|
await changeCamera(deviceId);
|
||||||
await replaceVideoStream(nextStream);
|
selectTransportCamera(deviceId);
|
||||||
},
|
},
|
||||||
[changeCamera, replaceVideoStream],
|
[changeCamera, selectTransportCamera],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -301,12 +296,6 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
// 首次打开页面时自动发起通话;hooks 会在卸载时各自释放媒体资源。
|
// 首次打开页面时自动发起通话;hooks 会在卸载时各自释放媒体资源。
|
||||||
}, [startCall]);
|
}, [startCall]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (callEnded || status === "idle" || status === "failed") {
|
|
||||||
stopCamera();
|
|
||||||
}
|
|
||||||
}, [callEnded, status, stopCamera]);
|
|
||||||
|
|
||||||
const error = previewError || cameraError;
|
const error = previewError || cameraError;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -323,7 +312,7 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
muted
|
muted
|
||||||
className={[
|
className={[
|
||||||
"absolute inset-0 h-full w-full -scale-x-100 object-cover transition-opacity duration-300 lg:object-contain",
|
"absolute inset-0 h-full w-full -scale-x-100 object-cover transition-opacity duration-300 lg:object-contain",
|
||||||
cameraStream && view === "camera" ? "opacity-100" : "opacity-0",
|
videoStream && view === "camera" ? "opacity-100" : "opacity-0",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -376,7 +365,7 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!cameraStream && inCall && view === "camera" && (
|
{!videoStream && inCall && view === "camera" && (
|
||||||
<div className="absolute inset-0 z-[2] flex items-center justify-center px-8 text-center">
|
<div className="absolute inset-0 z-[2] flex items-center justify-center px-8 text-center">
|
||||||
<div className="flex max-w-sm flex-col items-center gap-3">
|
<div className="flex max-w-sm flex-col items-center gap-3">
|
||||||
{cameraStarting ? (
|
{cameraStarting ? (
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export function useVoicePreview(
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [micWarning, setMicWarning] = useState<string | null>(null);
|
const [micWarning, setMicWarning] = useState<string | null>(null);
|
||||||
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
|
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
|
||||||
|
const [videoStream, setVideoStream] = useState<MediaStream | null>(null);
|
||||||
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
|
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
|
||||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
const [callEnded, setCallEnded] = useState(false);
|
const [callEnded, setCallEnded] = useState(false);
|
||||||
@@ -161,6 +162,7 @@ export function useVoicePreview(
|
|||||||
const disconnect = useCallback(() => {
|
const disconnect = useCallback(() => {
|
||||||
releaseResources();
|
releaseResources();
|
||||||
setLocalStream(null);
|
setLocalStream(null);
|
||||||
|
setVideoStream(null);
|
||||||
setRemoteStream(null);
|
setRemoteStream(null);
|
||||||
setMessages([]);
|
setMessages([]);
|
||||||
messageSeqRef.current = 0;
|
messageSeqRef.current = 0;
|
||||||
@@ -173,6 +175,7 @@ export function useVoicePreview(
|
|||||||
const fail = useCallback((message: string) => {
|
const fail = useCallback((message: string) => {
|
||||||
releaseResources();
|
releaseResources();
|
||||||
setLocalStream(null);
|
setLocalStream(null);
|
||||||
|
setVideoStream(null);
|
||||||
setRemoteStream(null);
|
setRemoteStream(null);
|
||||||
setError(message);
|
setError(message);
|
||||||
setNetworkQuality("unknown");
|
setNetworkQuality("unknown");
|
||||||
@@ -306,6 +309,10 @@ export function useVoicePreview(
|
|||||||
onDeviceError: (deviceError) => {
|
onDeviceError: (deviceError) => {
|
||||||
setMicWarning(deviceError.message || "无法访问麦克风。已尝试继续连接。");
|
setMicWarning(deviceError.message || "无法访问麦克风。已尝试继续连接。");
|
||||||
},
|
},
|
||||||
|
onCamUpdated: () => {
|
||||||
|
const track = transport.tracks().local.video;
|
||||||
|
setVideoStream(track ? new MediaStream([track]) : null);
|
||||||
|
},
|
||||||
onTransportStateChanged: (nextState) => {
|
onTransportStateChanged: (nextState) => {
|
||||||
if (nextState === "connected" || nextState === "ready") {
|
if (nextState === "connected" || nextState === "ready") {
|
||||||
setStatus("connected");
|
setStatus("connected");
|
||||||
@@ -331,7 +338,9 @@ export function useVoicePreview(
|
|||||||
await transport.updateMic(selectedDeviceIdRef.current);
|
await transport.updateMic(selectedDeviceIdRef.current);
|
||||||
}
|
}
|
||||||
const localAudio = transport.tracks().local.audio;
|
const localAudio = transport.tracks().local.audio;
|
||||||
|
const localVideo = transport.tracks().local.video;
|
||||||
setLocalStream(localAudio ? new MediaStream([localAudio]) : null);
|
setLocalStream(localAudio ? new MediaStream([localAudio]) : null);
|
||||||
|
setVideoStream(localVideo ? new MediaStream([localVideo]) : null);
|
||||||
void refreshDevices();
|
void refreshDevices();
|
||||||
|
|
||||||
const request = new Request(`${API_BASE}/api/webrtc/offer`, {
|
const request = new Request(`${API_BASE}/api/webrtc/offer`, {
|
||||||
@@ -377,6 +386,10 @@ export function useVoicePreview(
|
|||||||
if (transport && deviceId) await transport.updateCam(deviceId);
|
if (transport && deviceId) await transport.updateCam(deviceId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const selectCamera = useCallback((deviceId: string) => {
|
||||||
|
transportRef.current?.updateCam(deviceId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const selectDevice = useCallback(async (deviceId: string) => {
|
const selectDevice = useCallback(async (deviceId: string) => {
|
||||||
setSelectedDeviceId(deviceId);
|
setSelectedDeviceId(deviceId);
|
||||||
selectedDeviceIdRef.current = deviceId;
|
selectedDeviceIdRef.current = deviceId;
|
||||||
@@ -413,6 +426,7 @@ export function useVoicePreview(
|
|||||||
error,
|
error,
|
||||||
micWarning,
|
micWarning,
|
||||||
localStream,
|
localStream,
|
||||||
|
videoStream,
|
||||||
remoteStream,
|
remoteStream,
|
||||||
messages,
|
messages,
|
||||||
callEnded,
|
callEnded,
|
||||||
@@ -428,6 +442,7 @@ export function useVoicePreview(
|
|||||||
sendText,
|
sendText,
|
||||||
connect,
|
connect,
|
||||||
replaceVideoStream,
|
replaceVideoStream,
|
||||||
|
selectCamera,
|
||||||
disconnect,
|
disconnect,
|
||||||
audioRef,
|
audioRef,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user