javascripthtmlhtml5-videonavigatormediadevices

How to change camera source with Javascript?


I have a task to display a web camera through a website with some code like this:

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');
}
.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;
}
<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>

In order to get multiple camera sources, I added Droidcam. The problem is I can't change the camera source from droidcam to pc camera and vice versa. I have confirmed that video.srcObject has changed but still, the camera source is not changed.

fail change

Why did it happen? How to solve this? Any answer would be appreciated, thanks.


Solution

  • I just figured out the solution. The mistake occurred at the constraints object. The valid constraint looks like this:

    {
        video: {
            width: {
                min: 1280,
                ideal: 1920,
                max: 2560,
            },
            height: {
                min: 720,
                ideal: 1080,
                max: 1440
            },
            deviceId: null
        }
    }
    

    When selecting another input device, constraints.video.deviceId.exact should be updated with cameraOptions.value which is the selected option value (deviceId).

    Additional ways that can be applied for destroying MediaStreamTrack before changing the camera source is with MediaStreamTrack.stop().

    ...
    let currentStream = null;
    ...
    if (currentStream) {
        currentStream.getTracks().forEach(track => {
            track.stop();
        });
    }
    ...
    

    Full code:

    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
            },
            deviceId: null
        }
    }
    
    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) => {
                if (!changeMode) getCameraSelection();
                video.srcObject = stream;
                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) {
            startStream(constraints);
        }
    }
    
    cameraOptions.onchange = () => {
        constraints.video.deviceId = {
            exact: cameraOptions.value
        };
        startStream(constraints, 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');
    }
    .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;
    }
    <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>