From 82a0cec38eea75e2640ad263f12eac49c4009961 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Tue, 2 Jun 2026 10:59:03 +0800 Subject: [PATCH] Replace camera switch button with device list dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The camera drawer now offers a select of available video input devices instead of a cycle button. "使用摄像头" starts the chosen camera and changing the dropdown switches the live stream; the list is populated from enumerateDevices (real labels after permission is granted). Co-Authored-By: Claude Opus 4.8 --- examples/webpage/app.js | 68 ++++++++++++++++++++++--------------- examples/webpage/index.html | 12 +++---- examples/webpage/styles.css | 22 +++++++++++- 3 files changed, 68 insertions(+), 34 deletions(-) diff --git a/examples/webpage/app.js b/examples/webpage/app.js index ef0ed72..c6218ad 100644 --- a/examples/webpage/app.js +++ b/examples/webpage/app.js @@ -78,7 +78,7 @@ const els = { cameraPhoto: document.getElementById("camera-photo"), cameraCanvas: document.getElementById("camera-canvas"), cameraStartBtn: document.getElementById("camera-start-btn"), - cameraFlipBtn: document.getElementById("camera-flip-btn"), + cameraDeviceSelect: document.getElementById("camera-device-select"), cameraUpload: document.getElementById("camera-upload"), cameraSamples: document.getElementById("camera-samples"), clearBtn: document.getElementById("clear-btn"), @@ -149,7 +149,6 @@ const state = { cameraActive: false, cameraFacing: "environment", videoDevices: [], - videoDeviceIndex: 0, pendingImage: null, samplesRendered: false, @@ -258,6 +257,9 @@ function syncCameraDrawer(value) { els.cameraQuestion.textContent = prompt; renderSampleThumbnails(); selectDefaultImage(); + refreshVideoDevices().then(() => { + if (!state.cameraActive) populateDeviceSelect(); + }); } else { els.cameraState.textContent = "State -"; els.cameraQuestion.textContent = ""; @@ -924,6 +926,30 @@ async function refreshVideoDevices() { } } +// Fill the camera dropdown from the enumerated devices. Labels are only exposed +// after camera permission has been granted, so before that we show generic +// names ("摄像头 1", …) or just the default option. +function populateDeviceSelect(activeDeviceId) { + const sel = els.cameraDeviceSelect; + sel.innerHTML = ""; + if (state.videoDevices.length === 0) { + const opt = document.createElement("option"); + opt.value = ""; + opt.textContent = "默认摄像头"; + sel.appendChild(opt); + sel.disabled = true; + return; + } + state.videoDevices.forEach((device, index) => { + const opt = document.createElement("option"); + opt.value = device.deviceId; + opt.textContent = device.label || `摄像头 ${index + 1}`; + sel.appendChild(opt); + }); + sel.disabled = false; + if (activeDeviceId) sel.value = activeDeviceId; +} + async function startCamera(deviceId) { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { addWsLog("system", "getUserMedia not available in this browser"); @@ -954,31 +980,16 @@ async function startCamera(deviceId) { els.cameraStartBtn.classList.add("is-active"); clearSampleSelection(); - // Device labels become available only after permission is granted. + // Device labels become available only after permission is granted; refresh + // the dropdown now and select whichever camera is actually streaming. await refreshVideoDevices(); - if (deviceId) { - const idx = state.videoDevices.findIndex((d) => d.deviceId === deviceId); - if (idx >= 0) state.videoDeviceIndex = idx; - } - els.cameraFlipBtn.disabled = state.videoDevices.length < 2; + const activeId = + state.cameraStream.getVideoTracks?.()[0]?.getSettings?.().deviceId || + deviceId; + populateDeviceSelect(activeId); setCameraButtonEnabled(); } -// Cycle to the next available camera, or flip facingMode when devices are -// unlabeled (e.g. before permission, or on mobile with front/back only). -async function switchCamera() { - if (!state.cameraActive) return; - if (state.videoDevices.length > 1) { - state.videoDeviceIndex = - (state.videoDeviceIndex + 1) % state.videoDevices.length; - await startCamera(state.videoDevices[state.videoDeviceIndex].deviceId); - } else { - state.cameraFacing = - state.cameraFacing === "environment" ? "user" : "environment"; - await startCamera(); - } -} - function stopCameraStream() { if (state.cameraStream) { state.cameraStream.getTracks().forEach((track) => track.stop()); @@ -987,7 +998,6 @@ function stopCameraStream() { els.cameraVideo.srcObject = null; state.cameraActive = false; els.cameraStartBtn.classList.remove("is-active"); - els.cameraFlipBtn.disabled = true; } function captureFromCamera() { @@ -1456,11 +1466,15 @@ els.cameraDoneBtn.addEventListener("click", () => { }); els.cameraStartBtn.addEventListener("click", () => { - startCamera(); + startCamera(els.cameraDeviceSelect.value || undefined); }); -els.cameraFlipBtn.addEventListener("click", () => { - switchCamera(); +els.cameraDeviceSelect.addEventListener("change", () => { + // Switching device only restarts the stream when the camera is already live; + // otherwise the choice is applied when "使用摄像头" is pressed. + if (state.cameraActive) { + startCamera(els.cameraDeviceSelect.value || undefined); + } }); els.cameraUpload.addEventListener("change", (event) => { diff --git a/examples/webpage/index.html b/examples/webpage/index.html index 0330f35..3c1c1c9 100644 --- a/examples/webpage/index.html +++ b/examples/webpage/index.html @@ -116,14 +116,14 @@ > 使用摄像头 - + +