Tutorial

Como acessar as câmeras frontal e traseira com o getUserMedia() do JavaScript

JavaScript

Introdução

Juntamente com o HTML5 foram introduzidas as APIs com acesso aos dispositivos de hardware, incluindo a API MediaDevices. Essa API fornece acesso aos dispositivos de entrada de mídia como áudio e vídeo.

Com a ajuda dessa API, os desenvolvedores podem acessar dispositivos de áudio e vídeo para transmitir e exibir feeds de vídeo ao vivo no navegador. Neste tutorial, você irá acessar o feed de vídeo do dispositivo do usuário e exibi-lo no navegador usando o método getUserMedia.

A API getUserMedia utiliza os dispositivos de entrada de mídia para produzir um MediaStream (transmissão de mídia). Esse MediaStream contém os tipos de mídia solicitados, seja áudio ou vídeo. Ao usar a transmissão retornada da API, é possível exibir os feeds de vídeo no navegador, o que é útil na comunicação em tempo real no navegador.

Quando usado em conjunto com a API de gravação do MediaStream, é possível gravar e armazenar os dados de mídia capturados no navegador. Essa API só funciona em origens seguras assim como as APIs recentemente introduzidas, mas também funciona no localhost e URLs de arquivos.

Pré-requisitos

Este tutorial irá explicar inicialmente alguns conceitos e demonstrar exemplos com o Codepen. No passo final, você irá criar um feed de vídeo funcional para o navegador.

Passo 1 — Verificando o suporte de dispositivos

Primeiro, você verá como verificar se o navegador do usuário oferece suporte à API mediaDevices. Essa API existe dentra da interface do navegador e contém o estado atual e a identidade do agente do usuário. A verificação é realizada com o código a seguir que pode ser colado no Codepen:

if ('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices) {
  console.log("Let's get this party started")
}

Primeiro, ele verifica se a API mediaDevices existe dentro de navigator (navegador) e então verifica se a API getUserMedia está disponível dentro dos mediaDevices. Se o comando retorna true, podemos iniciar.

Passo 2 — Solicitando a permissão do usuário

Depois de confirmar que o navegador dá suporte à getUserMedia, é necessário solicitar a permissão para utilizar os dispositivos de entrada de mídia no agente do usuário. Normalmente, depois que o usuário concede a permissão, uma Promise é retornada e resolve para uma transmissão de mídia. Essa Promise não é retornada quando a permissão é negada pelo usuário, bloqueando o acesso a esses dispositivos.

Cole a linha a seguir no Codepen para solicitar a permissão:

navigator.mediaDevices.getUserMedia({video: true})

O objeto fornecido como um argumento para o método getUserMedia chama-se constraints (restrições). Ele determina quais os dispositivos de entrada de mídia os quais você está solicitando permissão para acessar. Por exemplo, se o objeto contém audio: true, o usuário será solicitado a conceder acesso ao dispositivo de entrada de áudio.

Passo 3 — Compreendendo as restrições de mídia

Esta seção irá abordar o conceito geral de contraints. O objeto constraints é um objeto MediaStreamConstraints que especifica os tipos de mídia para solicitar e os requisitos de cada tipo de mídia. É possível especificar os requisitos para a transmissão solicitada usando o objeto constraints, como a resolução da transmissão a ser usada (front, back).

É necessário especificar audio ou video ao fazer a solicitação. Um NotFoundError será retornado caso os tipos de mídia solicitados não possam ser encontrados no navegador do usuário.

Se você pretende solicitar uma transmissão de vídeo de resolução 1280 x 720, atualize o objeto constraints para que fique assim:

{
  video: {
    width: 1280,
    height: 720,
  }
}

Com essa atualização, o navegador tentará utilizar as configurações de qualidade especificadas para a transmissão. Se o dispositivo de vídeo não puder entregar essa resolução, o navegador retornará outras resoluções disponíveis.

Para garantir que o navegador retorne uma resolução que não seja inferior àquela fornecida, será necessário utilizar a propriedade min. Aqui está como atualizar o objeto constraints para incluir a propriedade min:

{
  video: {
    width: {
      min: 1280,
    },
    height: {
      min: 720,
    }
  }
}

Isso irá garantir que a resolução da transmissão retornada seja de pelo menos 1280 x 720. Caso esse requisito mínimo não possa ser atendido, a promessa será rejeitada com um OverconstrainedError.

Em alguns casos, você pode ter a preocupação de salvar dados e precisa que a transmissão não ultrapasse uma determinada resolução. Isso pode ser útil nos casos em que o usuário esteja em um plano limitado. Para habilitar essa funcionalidade, atualize o objeto de restrições para que contenha um campo max:

{
  video: {
    width: {
      min: 1280,
      max: 1920,
    },
    height: {
      min: 720,
      max: 1080
    }
  }
}

Com essas configurações, o navegador irá garantir que a transmissão de retorno não tenha resolução inferior a 1280 x 720 nem superior a 1920 x 1080.

Outros termos que podem ser utilizados incluem exact e ideal. A configuração ideal é normalmente usada juntamente com as propriedades min e max para encontrar a melhor resolução possível, o mais perto dos valores ideais fornecidos.

Atualize as restrições para incluir a palavra-chave ideal:

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

Para fazer o navegador usar a câmera frontal ou traseira (em portáteis) nos dispositivos, especifique uma propriedade facingMode no objeto video:

{
  video: {
    width: {
      min: 1280,
      ideal: 1920,
      max: 2560,
    },
    height: {
      min: 720,
      ideal: 1080,
      max: 1440
    },
    facingMode: 'user'
  }
}

Essa configuração irá utilizar a câmera frontal o tempo todo em todos os dispositivos. Para utilizar a câmera traseira em dispositivos móveis, altere a propriedade facingMode para environment.

{
  video: {
    ...
    facingMode: {
      exact: 'environment'
    }
  }
}

Passo 4 — Usando o método enumerateDevices

Quando o método enumerateDevices é chamado, ele retorna todos os dispositivos de entrada de mídia disponíveis no PC do usuário.

Com esse método, é possível oferecer opções ao usuário sobre qual dispositivo de entrada de mídia usar para a transmissão de conteúdo de áudio ou vídeo. Esse método retorna uma Promise resolvida para uma matriz MediaDeviceInfo contendo informações sobre cada dispositivo.

Um exemplo de como utilizar esse método é mostrado no trecho abaixo:

async function getDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();
}

Uma amostra de resposta para cada um dos dispositivos se pareceria com a seguinte:

{
  deviceId: "23e77f76e308d9b56cad920fe36883f30239491b8952ae36603c650fd5d8fbgj",
  groupId: "e0be8445bd846722962662d91c9eb04ia624aa42c2ca7c8e876187d1db3a3875",
  kind: "audiooutput",
  label: "",
}

Nota: um rótulo não será retornado a menos que uma transmissão esteja disponível, ou se o usuário tenha concedido permissões de acesso ao dispositivo.

Passo 5 — Exibindo a transmissão de vídeo no navegador

Até aqui, você passou pelo processo de solicitar e ganhar acesso aos dispositivos de mídia, configurou restrições para incluir as resoluções necessárias e selecionou a câmera que será utilizada para gravar o vídeo.

Depois de todos esses passos, você irá pelo menos querer ver se a transmissão está sendo realizada com base nas configurações definidas. Para garantir isso, o elemento <video> será usado para exibir a transmissão de vídeo no navegador.

Como mencionado anteriormente, o método getUserMedia retorna uma Promise que pode ser resolvida para uma transmissão. A transmissão retornada pode ser convertida em uma URL de objeto usando o método createObjectURL. Essa URL será definida como uma fonte de vídeo.

Você irá criar uma pequena demonstração na qual deixamos o usuário escolher de sua lista de dispositivos de vídeo disponíveis usando o método enumerateDevices.

Este é um método navigator.mediaDevices. Ele lista os dispositivos de mídia disponíveis, como microfones e câmeras. Depois retorna uma Promise resolvida para uma matriz de objetos detalhando os dispositivos de mídia disponíveis.

Crie um arquivo index.html e atualize o conteúdo com o código abaixo:

index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
    <link rel="stylesheet" href="style.css">
    <title>Document</title>
</head>
<body>
<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>

<script src="https://unpkg.com/feather-icons"></script>
<script src="script.js"></script>
</body>
</html>

No trecho acima, foram configurados os elementos que serão necessários e alguns controles para o vídeo. Também foi incluído um botão para tirar capturas de tela do feed de vídeo atual.

Agora, vamos adicionar um pouco de estilo a esses componentes.

Crie um arquivo style.css e copie os estilos a seguir nele. O Bootstrap foi incluído para reduzir a quantidade de CSS que você precisará escrever para que os componentes sejam iniciados.

style.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;
}

O próximo passo é adicionar funcionalidade à demonstração. Usando o método enumerateDevices, você irá obter os dispositivos de vídeo disponíveis e os definirá como opções dentro do elemento selecionado. Crie um arquivo chamado script.js e atualize-o com o seguinte trecho:

script.js
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 = async () => {
  const devices = await navigator.mediaDevices.enumerateDevices();
  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('');
};

play.onclick = () => {
  if (streamStarted) {
    video.play();
    play.classList.add('d-none');
    pause.classList.remove('d-none');
    return;
  }
  if ('mediaDevices' in navigator && navigator.mediaDevices.getUserMedia) {
    const updatedConstraints = {
      ...constraints,
      deviceId: {
        exact: cameraOptions.value
      }
    };
    startStream(updatedConstraints);
  }
};

const startStream = async (constraints) => {
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
  handleStream(stream);
};

const handleStream = (stream) => {
  video.srcObject = stream;
  play.classList.add('d-none');
  pause.classList.remove('d-none');
  screenshot.classList.remove('d-none');
  streamStarted = true;
};

getCameraSelection();

No trecho acima, algumas coisas estão acontecendo. Vamos dividi-las:

  1. feather.replace(): essa chamada de método cria uma instância de feather, que é um ícone definido para o desenvolvimento Web.
  2. A variável constraints contém a configuração inicial para a transmissão. Ela será estendida para incluir o dispositivo de mídia escolhido pelo usuário.
  3. getCameraSelection: essa função chama o método enumerateDevices. Em seguida, você filtra a matriz gerada a partir da Promise resolvida e seleciona os dispositivos de entrada de vídeo. A partir dos resultados filtrados, você cria <option> para o elemento <select>.
  4. Chamar o método getUserMedia acontece dentro do ouvinte onclick do botão play. Aqui, você irá verificar se esse método é suportado pelo navegador do usuário antes de iniciar a transmissão.
  5. Em seguida, você irá chamar a função startStream que recebe um argumento constraints. Ela chama o método getUserMedia com as constraints fornecidas. O handleStream é chamado usando a transmissão da Promise resolvida. Esse método define a transmissão retornada para o srcObject do elemento de vídeo.

Em seguida, você irá adicionar um listener de clique aos controles dos botões na página para pause, stop e tirar screenshots. Além disso, você irá adicionar um listener ao elemento <select> para atualizar as restrições da transmissão com o dispositivo de vídeo selecionado.

Atualize o arquivo script.js com o código abaixo:

script.js
...
cameraOptions.onchange = () => {
  const updatedConstraints = {
    ...constraints,
    deviceId: {
      exact: cameraOptions.value
    }
  };
  startStream(updatedConstraints);
};

const pauseStream = () => {
  video.pause();
  play.classList.remove('d-none');
  pause.classList.add('d-none');
};

const doScreenshot = () => {
  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');
};

pause.onclick = pauseStream;
screenshot.onclick = doScreenshot;

Agora, quando ao se abrir o arquivo index.html no navegador, clicar no botão Play irá iniciar a transmissão.

Aqui está uma demonstração completa:

https://codepen.io/chrisbeast/pen/ebYwpX

Conclusão

Esse tutorial introduziu a API getUserMedia. É uma adição interessante ao HTML5 que facilita o processo de captura de mídia na Web.

A API recebe um parâmetro (constraints) que pode ser usado para configurar o acesso aos dispositivos de entrada de áudio e vídeo. Ela também pode ser usada para especificar a resolução de vídeo necessária para o seu aplicativo.

É possível estender a demonstração ainda mais para dar ao usuário uma opção para salvar as capturas de tela feitas, bem como gravar e armazenar dados de vídeo e áudio com a ajuda da API MediaStream Recording.

Creative Commons License