You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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');
}

问题原因

  1. 旧媒体流未关闭:每次切换摄像头时,新流被赋值给video.srcObject,但旧流仍在占用摄像头资源,浏览器会优先保留已激活的流,导致新流无法正常输出画面。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 22:13:09