feat: 实现摄像头和麦克风设备选择功能

- 新增 useDeviceList hook,枚举音视频输入设备并监听热插拔
- CameraManager/MicManager 的 startCamera/startMic 支持可选 deviceId 参数
- useVisionSession 集成设备选择:授权后自动枚举、切换设备时热重启
- 连接后显示设备下拉选择器,未连接时隐藏(避免未授权时空列表)
- SessionConfig 新增 cameraDeviceId/micDeviceId 持久化到 localStorage
This commit is contained in:
2026-06-21 16:49:48 +08:00
parent eea5c07eaa
commit 6967dd7b2e
9 changed files with 128 additions and 37 deletions

View File

@@ -96,6 +96,9 @@ function AppContent() {
toggleCamera,
toggleMic,
sendTextMessage,
cameras,
mics,
switchDevice,
} = useVisionSession(accessToken, activeSessionId);
const isConnected = connectionStatus === "connected";
@@ -406,21 +409,6 @@ function AppContent() {
<button className="btn btn--primary btn--lg" onClick={startSession}>
{connectionStatus === "connecting" ? tr("controls.connecting") : tr("controls.startVideo")}
</button>
{/* 设备选择器 */}
<div className="video-controls__devices">
<div className="device-select-wrapper">
<label className="device-select-label">📷 {tr("controls.device.camera")}</label>
<select className="device-select" defaultValue="default">
<option value="default">{tr("controls.device.default")}</option>
</select>
</div>
<div className="device-select-wrapper">
<label className="device-select-label">🎤 {tr("controls.device.mic")}</label>
<select className="device-select" defaultValue="default">
<option value="default">{tr("controls.device.default")}</option>
</select>
</div>
</div>
</>
) : isCameraOn ? (
<>
@@ -457,6 +445,35 @@ function AppContent() {
{tr("controls.stopVideo")}
</button>
</div>
{/* 设备选择器 */}
<div className="video-controls__devices">
<div className="device-select-wrapper">
<label className="device-select-label">📷</label>
<select
className="device-select"
value={config.cameraDeviceId || "default"}
onChange={(e) => switchDevice("camera", e.target.value === "default" ? "" : e.target.value)}
>
<option value="default">{tr("controls.device.default")}</option>
{cameras.map((d) => (
<option key={d.deviceId} value={d.deviceId}>{d.label}</option>
))}
</select>
</div>
<div className="device-select-wrapper">
<label className="device-select-label">🎤</label>
<select
className="device-select"
value={config.micDeviceId || "default"}
onChange={(e) => switchDevice("mic", e.target.value === "default" ? "" : e.target.value)}
>
<option value="default">{tr("controls.device.default")}</option>
{mics.map((d) => (
<option key={d.deviceId} value={d.deviceId}>{d.label}</option>
))}
</select>
</div>
</div>
</>
) : (
<>