Começar a usar dispositivos de mídia

Ao desenvolver para a Web, o padrão WebRTC fornece APIs para acessar câmeras e microfones conectados ao computador ou smartphone. Eles são geralmente chamados de dispositivos de mídia e podem ser acessados com JavaScript pelo objeto navigator.mediaDevices, que implementa a interface MediaDevices. A partir desse objeto, podemos enumerar todos os dispositivos conectados, detectar mudanças de dispositivo (quando um dispositivo está conectado ou desconectado) e abrir um dispositivo para recuperar um streaming de mídia (veja abaixo).

A maneira mais comum de usá-la é pela função getUserMedia(), que retorna uma promessa que será resolvida como um MediaStream para os dispositivos de mídia correspondentes. Essa função usa um único objeto MediaStreamConstraints que especifica os requisitos que temos. Por exemplo, para simplesmente abrir o microfone e a câmera padrão, faríamos o seguinte.

Como usar promessas

const constraints = {
    'video': true,
    'audio': true
}
navigator.mediaDevices.getUserMedia(constraints)
    .then(stream => {
        console.log('Got MediaStream:', stream);
    })
    .catch(error => {
        console.error('Error accessing media devices.', error);
    });

Como usar async/await

const openMediaDevices = async (constraints) => {
    return await navigator.mediaDevices.getUserMedia(constraints);
}

try {
    const stream = openMediaDevices({'video':true,'audio':true});
    console.log('Got MediaStream:', stream);
} catch(error) {
    console.error('Error accessing media devices.', error);
}

A chamada para getUserMedia() acionará uma solicitação de permissões. Se o usuário aceitar a permissão, a promessa será resolvida com um MediaStream contendo um vídeo e uma faixa de áudio. Se a permissão for negada, uma PermissionDeniedError será gerada. Caso não haja dispositivos correspondentes conectados, uma NotFoundError será gerada.

A referência completa da API para a interface MediaDevices está disponível nos documentos da Web do MDN.

Como consultar dispositivos de mídia

Em um aplicativo mais complexo, provavelmente é melhor verificar todas as câmeras e microfones conectados e fornecer o feedback adequado ao usuário. Para isso, chame a função enumerateDevices(). Isso retornará uma promessa que é resolvida em uma matriz de MediaDevicesInfo que descreve cada dispositivo de mídia conhecido. Podemos usar isso para apresentar uma interface ao usuário e permitir que ele escolha a que preferir. Cada MediaDevicesInfo contém uma propriedade chamada kind com o valor audioinput, audiooutput ou videoinput, indicando o tipo de dispositivo de mídia.

Como usar promessas

function getConnectedDevices(type, callback) {
    navigator.mediaDevices.enumerateDevices()
        .then(devices => {
            const filtered = devices.filter(device => device.kind === type);
            callback(filtered);
        });
}

getConnectedDevices('videoinput', cameras => console.log('Cameras found', cameras));

Como usar async/await

async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

const videoCameras = getConnectedDevices('videoinput');
console.log('Cameras found:', videoCameras);

Detectar mudanças em dispositivos

A maioria dos computadores oferece suporte a vários dispositivos durante a execução. Pode ser uma webcam conectada por USB, um fone de ouvido Bluetooth ou um conjunto de alto-falantes externos. Para oferecer suporte adequado, um aplicativo da Web precisa detectar as mudanças de dispositivos de mídia. Isso pode ser feito adicionando um listener ao navigator.mediaDevices para o evento devicechange.

// Updates the select element with the provided set of cameras
function updateCameraList(cameras) {
    const listElement = document.querySelector('select#availableCameras');
    listElement.innerHTML = '';
    cameras.map(camera => {
        const cameraOption = document.createElement('option');
        cameraOption.label = camera.label;
        cameraOption.value = camera.deviceId;
    }).forEach(cameraOption => listElement.add(cameraOption));
}

// Fetch an array of devices of a certain type
async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

// Get the initial set of cameras connected
const videoCameras = getConnectedDevices('videoinput');
updateCameraList(videoCameras);

// Listen for changes to media devices and update the list accordingly
navigator.mediaDevices.addEventListener('devicechange', event => {
    const newCameraList = getConnectedDevices('video');
    updateCameraList(newCameraList);
});

Restrições de mídia

O objeto de restrições, que precisa implementar a interface MediaStreamConstraints, que é transmitida como um parâmetro para getUserMedia(), permite abrir um dispositivo de mídia que corresponde a um determinado requisito. Esse requisito pode ser definido de forma muito vaga (áudio e/ou vídeo) ou muito específico (resolução mínima da câmera ou um ID exato do dispositivo). É recomendável que os aplicativos que usam a API getUserMedia() verifiquem primeiro os dispositivos atuais e, em seguida, especifiquem uma restrição que corresponda ao dispositivo exato usando a restrição deviceId. Os dispositivos também, se possível, serão configurados de acordo com as restrições. Podemos ativar o cancelamento de eco em microfones ou definir uma largura e altura específicas ou mínimas para o vídeo da câmera.

async function getConnectedDevices(type) {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === type)
}

// Open camera with at least minWidth and minHeight capabilities
async function openCamera(cameraId, minWidth, minHeight) {
    const constraints = {
        'audio': {'echoCancellation': true},
        'video': {
            'deviceId': cameraId,
            'width': {'min': minWidth},
            'height': {'min': minHeight}
            }
        }

    return await navigator.mediaDevices.getUserMedia(constraints);
}

const cameras = getConnectedDevices('videoinput');
if (cameras && cameras.length > 0) {
    // Open first available video camera with a resolution of 1280x720 pixels
    const stream = openCamera(cameras[0].deviceId, 1280, 720);
}

A documentação completa da interface MediaStreamConstraints pode ser encontrada nos documentos da Web MDN.

Reprodução local

Assim que um dispositivo de mídia for aberto e tivermos um MediaStream disponível, poderemos atribuí-lo a um elemento de vídeo ou áudio para reproduzir o stream localmente.

async function playVideoFromCamera() {
    try {
        const constraints = {'video': true, 'audio': true};
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        const videoElement = document.querySelector('video#localVideo');
        videoElement.srcObject = stream;
    } catch(error) {
        console.error('Error opening video camera.', error);
    }
}

O HTML necessário para um elemento de vídeo típico usado com getUserMedia() geralmente terá os atributos autoplay e playsinline. O atributo autoplay vai fazer com que os novos streams atribuídos ao elemento sejam reproduzidos automaticamente. O atributo playsinline permite que o vídeo seja reproduzido inline, em vez de apenas em tela inteira, em determinados navegadores para dispositivos móveis. Também é recomendável usar controls="false" para transmissões ao vivo, a menos que o usuário possa pausar as transmissões.

<html>
<head><title>Local video playback</title></head>
<body>
    <video id="localVideo" autoplay playsinline controls="false"/>
</body>
</html>