如何用JavaScript切换摄像头源?切换失效问题的解决方法
多摄像头切换故障:Droidcam与PC摄像头无法切换的原因及解决方法
问题描述
使用以下网页代码调用摄像头时,添加Droidcam作为额外摄像头源后,无法在Droidcam与PC本地摄像头之间互相切换。已确认video.srcObject已更新为新的媒体流,但画面始终没有切换。
原代码
HTML代码
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"> <script src="https://unpkg.com/feather-icons"></script> <div class="display-cover"> <video autoplay></video> <canvas class="d-none"></canvas> <div class="video-options"> <select name="" id="" class="custom-select"> <option value="">Select camera</option> </select> </div> <img class="screenshot-image d-none" alt=""> <div class="controls"> <button class="btn btn-danger play" title="Play"><i data-feather="play-circle"></i></button> <button class="btn btn-info pause d-none" title="Pause"><i data-feather="pause"></i></button> <button class="btn btn-outline-success screenshot d-none" title="ScreenShot"><i data-feather="image"></i></button> </div> </div>
CSS代码
.screenshot-image { width: 150px; height: 90px; border-radius: 4px; border: 2px solid whitesmoke; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1); position: absolute; bottom: 5px; left: 10px; background: white; } .display-cover { display: flex; justify-content: center; align-items: center; width: 70%; margin: 5% auto; position: relative; } video { width: 100%; background: rgba(0, 0, 0, 0.2); } .video-options { position: absolute; left: 20px; top: 30px; } .controls { position: absolute; right: 20px; top: 20px; display: flex; } .controls>button { width: 45px; height: 45px; text-align: center; border-radius: 100%; margin: 0 6px; background: transparent; } .controls>button:hover svg { color: white !important; } @media (min-width: 300px) and (max-width: 400px) { .controls { flex-direction: column; } .controls button { margin: 5px 0 !important; } } .controls>button>svg { height: 20px; width: 18px; text-align: center; margin: 0 auto; padding: 0; } .controls button:nth-child(1) { border: 2px solid #D2002E; } .controls button:nth-child(1) svg { color: #D2002E; } .controls button:nth-child(2) { border: 2px solid #008496; } .controls button:nth-child(2) svg { color: #008496; } .controls button:nth-child(3) { border: 2px solid #00B541; } .controls button:nth-child(3) svg { color: #00B541; } .controls>button { width: 45px; height: 45px; text-align: center; border-radius: 100%; margin: 0 6px; background: transparent; } .controls>button:hover svg { color: white; }
JavaScript代码
feather.replace(); const controls = document.querySelector('.controls'); const cameraOptions = document.querySelector('.video-options>select'); const video = document.querySelector('video'); const canvas = document.querySelector('canvas'); const screenshotImage = document.querySelector('img'); const buttons = [...controls.querySelectorAll('button')]; let streamStarted = false; const [play, pause, screenshot] = buttons; const constraints = { video: { width: { min: 1280, ideal: 1920, max: 2560, }, height: { min: 720, ideal: 1080, max: 1440 }, } } const getCameraSelection = () => { navigator.mediaDevices.enumerateDevices().then((devices) => { const videoDevices = devices.filter(device => device.kind === 'videoinput'); const options = videoDevices.map(videoDevice => { return `<option value="${videoDevice.deviceId}">${videoDevice.label}</option>`; }); cameraOptions.innerHTML = options.join(''); }); } const startStream = (constraints, changeMode = false) => { try { navigator.mediaDevices.getUserMedia(constraints).then((stream) => { if (!changeMode) getCameraSelection(); video.srcObject = stream; console.log('video.srcObject :', video.srcObject); video.play(); play.classList.add('d-none'); pause.classList.remove('d-none'); screenshot.classList.remove('d-none'); streamStarted = true; }); } catch (e) { console.error(e); } } play.onclick = () => { if (streamStarted) { video.play(); play.classList.add('d-none'); pause.classList.remove('d-none'); return; } if ('mediaDevices' in navigator) { const updatedConstraints = { ...constraints, deviceId: { exact: cameraOptions.value } }; startStream(updatedConstraints); } } cameraOptions.onchange = () => { const updatedConstraints = { ...constraints, deviceId: { exact: cameraOptions.value } }; startStream(updatedConstraints, true); } pause.onclick = () => { video.pause(); play.classList.remove('d-none'); pause.classList.add('d-none'); } screenshot.onclick = () => { canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0); screenshotImage.src = canvas.toDataURL('image/webp'); screenshotImage.classList.remove('d-none'); }
问题原因
- 旧媒体流未关闭:每次切换摄像头时,新流被赋值给
video.srcObject,但旧流仍在占用摄像头资源,浏览器会优先保留已激活的流,导致新流无法正常输出画面。 - video元素状态未重置:切换时未暂停当前播放的视频,可能造成画面缓存或流切换不彻底。
解决方法
修改JavaScript代码,添加关闭旧流的逻辑,确保摄像头资源被释放后再加载新流:
修改后的核心代码
feather.replace(); const controls = document.querySelector('.controls'); const cameraOptions = document.querySelector('.video-options>select'); const video = document.querySelector('video'); const canvas = document.querySelector('canvas'); const screenshotImage = document.querySelector('img'); const buttons = [...controls.querySelectorAll('button')]; let streamStarted = false; const [play, pause, screenshot] = buttons; // 新增变量存储当前活跃流 let currentStream = null; const constraints = { video: { width: { min: 1280, ideal: 1920, max: 2560, }, height: { min: 720, ideal: 1080, max: 1440 }, } } const getCameraSelection = () => { navigator.mediaDevices.enumerateDevices().then((devices) => { const videoDevices = devices.filter(device => device.kind === 'videoinput'); const options = videoDevices.map(videoDevice => { return `<option value="${videoDevice.deviceId}">${videoDevice.label}</option>`; }); cameraOptions.innerHTML = options.join(''); }); } const startStream = (constraints, changeMode = false) => { try { // 关闭旧流,释放摄像头资源 if (currentStream) { currentStream.getTracks().forEach(track => track.stop()); } navigator.mediaDevices.getUserMedia(constraints).then((stream) => { currentStream = stream; // 保存新流引用 if (!changeMode) getCameraSelection(); video.srcObject = stream; video.play(); play.classList.add('d-none'); pause.classList.remove('d-none'); screenshot.classList.remove('d-none'); streamStarted = true; }); } catch (e) { console.error(e); } } // 其余逻辑保持不变 play.onclick = () => { if (streamStarted) { video.play(); play.classList.add('d-none'); pause.classList.remove('d-none'); return; } if ('mediaDevices' in navigator) { const updatedConstraints = { ...constraints, deviceId: { exact: cameraOptions.value } }; startStream(updatedConstraints); } } cameraOptions.onchange = () => { const updatedConstraints = { ...constraints, deviceId: { exact: cameraOptions.value } }; startStream(updatedConstraints, true); } pause.onclick = () => { video.pause(); play.classList.remove('d-none'); pause.classList.add('d-none'); } screenshot.onclick = () => { canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0); screenshotImage.src = canvas.toDataURL('image/webp'); screenshotImage.classList.remove('d-none'); }
关键修改点
- 新增
currentStream变量存储当前活跃的媒体流引用 - 在创建新流前,遍历旧流的所有轨道并调用
stop()方法,彻底释放摄像头资源 - 将新流赋值给
currentStream,以便下次切换时重复关闭逻辑
内容的提问来源于stack exchange,提问作者Jordy
相关产品推荐
相关产品推荐

