Escolhendo câmeras em JavaScript com a API mediaDevices

April 19, 2018
Escrito por
Phil Nash
Twilion

Escolhendo câmeras em JavaScript com a API mediaDevices - Mesa com 3 celulares

A maioria dos smartphones vêm com uma câmera frontal e traseira, e quando você está construindo uma aplicação de vídeo para mobile, você pode querer escolher ou alternar entre elas.

Se você estiver construindo uma aplicação de chat, provavelmente vai querer a câmera frontal, mas se estiver construindo um aplicativo de câmeras, talvez esteja mais interessado na câmera traseira. Neste post, vamos ver como escolher ou alternar entre as câmeras usando a API mediaDevices e restrições de mídia.

Do que você vai precisar

Para prosseguir neste post, você vai precisar de:

  • Um iOS ou Android com duas câmeras para fazer testes. Se você tiver duas webcams no notebook, também serve.
  • ngrok para que você possa facilmente acessar o projeto do seu aparelho móvel (e porque eu acho o ngrok incrível).
  • O código deste repositório do GitHub para começar

 

Para pegar o código, clone o projeto e dê um checkout no projeto:

git clone https://github.com/philnash/mediadevices-camera-selection.git -b initial-project
cd mediadevices-camera-selection

O projeto inicial vai te dar alguns arquivos HTML e CSS para que você possa concentrar no JavaScript. Você pode abrir o arquivo index.html diretamente, mas eu te recomendo disponibilizar estes arquivos em um webserver. Eu gosto de usar o módulo serve do npm. Eu também incluí o módulo serve no repositório. Para usá-lo, instale a dependência com o npm e inicie o servidor.

npm install
npm start

Uma vez que tiver o servidor rodando, abra um túnel usando o ngrok. O serve disponibiliza os arquivos na porta 5000, para vincular o túnel desta porta com o ngrok, rode a linha de comando a seguir em uma nova janela:

ngrok http 5000

Agora você tem uma versão pública disponível do site que você pode abrir em seu aparelho móvel para testá-lo mais tarde. Certifique-se de que você está abrindo a URL HTTPS já que as APIs que estamos usando só rodam em um ambiente seguro.

Tela com o link público do ngrok

O app deve se parecer com a imagem a seguir:

Tela com visualização do site no estágio atual

Pegando o Stream de mídia

Nosso primeiro desafio é pegar o stream de vídeo de qualquer câmera na tela. Uma vez que isso estiver completo, vamos investigar as opções para selecionar uma câmera específica. Abra o app.js e comece selecionando os elementos de vídeo e de botão do DOM:

// app.js
const video = document.getElementById('video');
const button = document.getElementById('button');

Vamos pedir acesso à câmera usando a API da mediaDevices, quando o usuário clicar no botão. Para isso, chame o navigator.mediaDevices.getUserMedia() passando um objeto de restrições de mídia. Vamos começar com uma simples configuração de restrições, neste caso filtrando apenas por vídeos, então vamos configurar vídeo em true e áudio em false.

getUserMedia retorna uma promise que, quando resolvida, nos dá acesso ao streaming de mídia da câmera.

Defina o srcObj do vídeo com stream e teremos o seguinte código:

button.addEventListener('click', event => {
  const constraints = {
    video: true,
    audio: false
  };
  navigator.mediaDevices
    .getUserMedia(constraints)
    .then(stream => {
      video.srcObject = stream;
    })
    .catch(error => {
      console.error(error);
    });
});

Salve o arquivo, recarregue a página e clique no botão. Agora deve aparecer uma caixa de confirmação solicitando acesso à sua câmera. Uma vez que as permissões forem dadas, seu vídeo vai aparecer na tela.

Experimente em seu computador e telefone. Quando eu tentei com meu iPhone, a câmera selecionada foi a câmera frontal.

Tela com novo estágio exibindo a câmera frontal

Se estiver usando um iPhone, certifique-se de checar no Safari, já que aparentemente não está funcionando em outros navegadores.

Quais câmeras estão disponíveis?

A API da mediaDevices nos dá uma maneira de listar todos os dispositivos disponíveis para entrada de áudio e vídeo. Vamos usar o enumerateDevices para construir um conjunto de opções em uma caixa de listagem para que possamos escolher a câmera que queremos ver. Abra o app.js novamente e comece selecionando o elemento do DOM da caixa de seleção:

const video = document.getElementById('video');
const button = document.getElementById('button');
const select = document.getElementById('select');

enumerateDevices retorna uma promise, então vamos escrever uma função que possamos usar para receber os resultados da promise. A função vai receber uma lista de aparelhos de mídia como argumento.

A primeira coisa a se fazer é esvaziar o <select> de qualquer opção existente e anexar uma <option> vazia. E depois percorrer a lista de dispositivos, filtrando qualquer um que não seja do tipo “videoinput”. Depois criamos uma <option> usando o ID do dispositivo como o valor e o label do dispositivo para o texto. Também vamos lidar com o caso onde um aparelho não tiver um label definido, criando um texto “Camera n” para este caso.

const video = document.getElementById('video');
const button = document.getElementById('button');
const select = document.getElementById('select');

function gotDevices(mediaDevices) {
  select.innerHTML = '';
  select.appendChild(document.createElement('option'));
  let count = 1;
  mediaDevices.forEach(mediaDevice => {
    if (mediaDevice.kind === 'videoinput') {
      const option = document.createElement('option');
      option.value = mediaDevice.deviceId;
      const label = mediaDevice.label || `Camera ${count++}`;
      const textNode = document.createTextNode(label);
      option.appendChild(textNode);
      select.appendChild(option);
    }
  });
}

No final do app.js, faça a chamada para enumerateDevices.

navigator.mediaDevices.enumerateDevices().then(gotDevices);

Atualize a página e dê uma olhada na caixa de seleção próximo ao botão. Se estiver no Android, ou usando o Chrome ou Firefox, você vai ver o nome das câmeras que estiverem disponíveis.

No iPhone, você vai ver o nome genérico “Camera 1” e “Camera 2” da nossa função. No iOS, você não vai receber labels das câmeras até que tenha dado permissão ao site para usar pelo menos uma das câmeras. Isso torna nossa interface menos útil ao selecionar as câmeras, e mesmo que você receba o ID dos aparelhos, você não conseguirá saber qual câmera é qual.

Tela do celular com a opção de seleção de câmera ativa

Ainda não fizemos o código para trocar a câmera ao mudar a caixa de seleção. Antes disso, vamos dar uma olhada em outra maneira para podermos escolher qual câmera queremos usar.

Modo de Face

Uma alternativa que podemos usar para selecionar a câmera é a restrição facingMode. Esta é uma maneira menos exata de selecionar uma câmera do que pegando seu ID do enumerateDevices, mas funciona muito bem para dispositivos móveis. Existem quatro opções que você pode usar para a restrição: user, environemnt, left e right. As restrições são explicadas na documentação MDN, e para este post, vamos usar user e environment já que eles mapeiam bem as câmeras dianteiras e traseiras em smartphones.

Para usar a restrição facingMode, precisamos mudar as constraints que usamos em nossa chamada para o getUserMedia. Ao invés de apenas dizer true para vídeo, precisamos de um objeto para essas restrições. Atualize o código do botão para selecionar a câmera dianteira:

button.addEventListener('click', event => {
  const videoConstraints = {
    facingMode: 'user'
  };
  const constraints = {
    video: videoConstraints,
    audio: false
  };

Teste no seu celular agora. Você vai ver a câmera frontal selecionada. Faça a atualização do facingMode para environment e tente novamente. Agora a câmera de trás deve estar selecionada.

Vamos juntar o código com os resultados do enumerateDevices acima para construir um alternador de câmeras, uma vez que tivermos permissões para ler os dados de câmera.

Alternando Câmeras

Temos o código para selecionar uma câmera de usuário ou ambiente na primeira seleção, mas se quisermos alternar entre câmeras, precisamos de um pouco mais.

Primeiro, deveríamos manter uma referência do stream atual para que possamos pará-lo quando trocamos para outro dispositivo. Adicione mais uma variável e uma função para parar a stream no topo do app.js.

const video = document.getElementById('video');
const button = document.getElementById('button');
const select = document.getElementById('select');
let currentStream;

function stopMediaTracks(stream) {
  stream.getTracks().forEach(track => {
    track.stop();
  });
}

A função stopMediaTracks recebe uma stream como parâmetro e percorre todas as trilhas disponíveis na stream, parando cada uma delas.

Vamos mudar de câmera quando apertamos no mesmo botão, então precisamos atualizar o event listener, mas se tivermos um currentStream definido, devemos pará-lo primeiro. Depois, vamos checar o <select> para ver se estamos escolhendo um dispositivo em particular e construir as restrições de vídeo baseadas na seleção.

Atualize o handler de clique do botão e vamos ter o seguinte:

button.addEventListener('click', event => {
  if (typeof currentStream !== 'undefined') {
    stopMediaTracks(currentStream);
  }
  const videoConstraints = {};
  if (select.value === '') {
    videoConstraints.facingMode = 'environment';
  } else {
    videoConstraints.deviceId = { exact: select.value };
  }
  const constraints = {
    video: videoConstraints,
    audio: false
  };

Quando quisermos selecionar um aparelho por seu deviceId, usamos a restrição exact. Evitamos isso para o facingMode, já que isso poderia falhar em um aparelho que não reconheça user ou environment, nos deixando sem nenhuma mídia.

Ainda dentro do handler de click do botão, quando tivermos permissão de usar o vídeo, vamos mudar mais algumas coisas. Configure o currentStream para o novo stream passado pela função, para que possamos pará-lo depois, e chame o enumerateDevices novamente.

Como o enumerateDevices retorna uma promise, vamos acioná-lo como um retorno do nosso then da getUserMedia e ligá-lo a um novo then com o resultado que será repassado para nossa função gotDevices.

Substitua a chamada existente por getUserMedia com o seguinte:

button.addEventListener('click', event => {
  if (typeof currentStream !== 'undefined') {
    stopMediaTracks(currentStream);
  }
  const videoConstraints = {};
  if (select.value === '') {
    videoConstraints.facingMode = 'environment';
  } else {
    videoConstraints.deviceId = { exact: select.value };
  }
  const constraints = {
    video: videoConstraints,
    audio: false
  };

  navigator.mediaDevices
    .getUserMedia(constraints)
    .then(stream => {
      currentStream = stream;
      video.srcObject = stream;
      return navigator.mediaDevices.enumerateDevices();
    })
    .then(gotDevices)
    .catch(error => {
      console.error(error);
    });
});

Quando adicionar todo o código, seu app.js deve parecer com este código completo. Atualize a página para poder brincar, selecionando e mudando as câmeras. Isso funciona no dispositivo móvel e na web também.

Tela do celular com a demonstração completa de escolha de câmera

Próximos passos

Acabamos de ver como selecionar uma câmera de usuário com a facingMode ou deviceId. Lembre-se, facingMode é mais confiável antes de você ter permissões, mas selecionar um deviceId é mais preciso. Você pode pegar todo o código deste post no repositório do GitHub ou experimentar a aplicação ao vivo aqui.

Se estiver usando a Twilio Video para construir uma aplicação de vídeo, você pode usar essas restrições ao chamar os comandos connect ou createLocalVideoTrack.

Selecionar ou alternar câmeras é uma funcionalidade útil para chat em vídeo, permitindo que usuários selecionem a câmera exata que eles querem usar na sua interface, e pode ser muito útil ao compartilhar sua tela durante uma chamada de vídeo também.

Existem outras funcionalidades de vídeo que você gostaria de ver que seriam úteis em chats de vídeo ou tem alguma pergunta sobre essa funcionalidade? Me conte no Twitter @luisleao.

Este post foi traduzido e revisado do artigo em Inglês  "Choosing cameras in JavaScript with the mediaDevices API".