- Introduce a new `NetworkQualityIndicator` component to visually represent network quality status during calls. - Integrate the `NetworkQualityIndicator` into `AssistantPage` and `MobileCallPage`, enhancing user awareness of connection quality. - Update `use-voice-preview` hook to manage network quality metrics, improving overall call experience and performance monitoring.
126 lines
3.8 KiB
TypeScript
126 lines
3.8 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* 摄像头预览:纯前端 getUserMedia 视频流,供调试抽屉的「视频流」视图使用。
|
|
*
|
|
* 与 use-voice-preview 分离:这里负责摄像头设备枚举与本地预览;开始视觉会话时,
|
|
* 调用方会把这条 video track 加进语音 WebRTC PeerConnection。
|
|
*/
|
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
|
const CAMERA_VIDEO_CONSTRAINTS: MediaTrackConstraints = {
|
|
width: { ideal: 640, max: 640 },
|
|
height: { ideal: 360, max: 480 },
|
|
frameRate: { ideal: 12, max: 15 },
|
|
};
|
|
|
|
function cameraErrorMessage(error: unknown): string {
|
|
if (error instanceof DOMException) {
|
|
if (error.name === "NotAllowedError")
|
|
return "摄像头权限被拒绝,请在浏览器网站设置中允许使用摄像头。";
|
|
if (error.name === "NotFoundError")
|
|
return "未检测到可用摄像头,请连接或启用摄像头后重试。";
|
|
if (error.name === "NotReadableError")
|
|
return "摄像头正被其他应用占用,或系统未允许浏览器访问摄像头。";
|
|
}
|
|
if (error instanceof Error && error.message) return error.message;
|
|
return "无法访问摄像头。";
|
|
}
|
|
|
|
export function useCameraPreview() {
|
|
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [starting, setStarting] = useState(false);
|
|
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
|
|
const [deviceId, setDeviceId] = useState("");
|
|
const streamRef = useRef<MediaStream | null>(null);
|
|
const deviceIdRef = useRef("");
|
|
|
|
const active = Boolean(stream);
|
|
|
|
const refreshCameras = useCallback(async () => {
|
|
if (!navigator.mediaDevices?.enumerateDevices) return;
|
|
try {
|
|
const list = await navigator.mediaDevices.enumerateDevices();
|
|
setDevices(list.filter((d) => d.kind === "videoinput" && d.deviceId));
|
|
} catch {
|
|
/* 忽略枚举失败 */
|
|
}
|
|
}, []);
|
|
|
|
const stop = useCallback(() => {
|
|
streamRef.current?.getTracks().forEach((track) => track.stop());
|
|
streamRef.current = null;
|
|
setStream(null);
|
|
}, []);
|
|
|
|
const start = useCallback(async (): Promise<MediaStream | null> => {
|
|
if (streamRef.current) return streamRef.current;
|
|
setError(null);
|
|
if (!window.isSecureContext || !navigator.mediaDevices?.getUserMedia) {
|
|
setError("当前页面无法访问摄像头,请通过 https 或 localhost 访问。");
|
|
return null;
|
|
}
|
|
setStarting(true);
|
|
try {
|
|
const id = deviceIdRef.current;
|
|
const videoConstraints: MediaTrackConstraints = {
|
|
...CAMERA_VIDEO_CONSTRAINTS,
|
|
...(id ? { deviceId: { exact: id } } : {}),
|
|
};
|
|
const media = await navigator.mediaDevices.getUserMedia({
|
|
video: videoConstraints,
|
|
});
|
|
streamRef.current = media;
|
|
setStream(media);
|
|
void refreshCameras();
|
|
return media;
|
|
} catch (mediaError) {
|
|
setError(cameraErrorMessage(mediaError));
|
|
return null;
|
|
} finally {
|
|
setStarting(false);
|
|
}
|
|
}, [refreshCameras]);
|
|
|
|
const selectCamera = useCallback(
|
|
async (id: string): Promise<MediaStream | null> => {
|
|
deviceIdRef.current = id;
|
|
setDeviceId(id);
|
|
if (streamRef.current) {
|
|
stop();
|
|
return await start();
|
|
}
|
|
return null;
|
|
},
|
|
[start, stop],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!navigator.mediaDevices?.enumerateDevices) return;
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
void refreshCameras();
|
|
navigator.mediaDevices.addEventListener("devicechange", refreshCameras);
|
|
return () => {
|
|
navigator.mediaDevices.removeEventListener("devicechange", refreshCameras);
|
|
};
|
|
}, [refreshCameras]);
|
|
|
|
useEffect(() => stop, [stop]);
|
|
|
|
return {
|
|
stream,
|
|
error,
|
|
starting,
|
|
devices,
|
|
deviceId,
|
|
active,
|
|
start,
|
|
stop,
|
|
selectCamera,
|
|
};
|
|
}
|
|
|
|
export type CameraPreview = ReturnType<typeof useCameraPreview>;
|