Capturando e gravando imagens, vídeo e áudio com getUserMedia

Hey there .o/

Continuando o que havíamos começado no artigo 1001 formas de fazer Input de arquivos com JavaScript, é hora de falarmos sobre como usar a API do getUserMedia para capturarmos imagens, vídeos ou áudios do usuário utilizando o seu dispositivo.

Para seguir essa receita, você vai precisar de:

  • Um botão para iniciar a câmera

  • Um elemento do tipo vídeo

  • Um botão para iniciar a gravação do áudio

  • Um botão para tirar uma foto

  • Um canvas

  • Um botão para parar a gravação (seja do áudio ou do vídeo)

  • 3/4 de uma xícara de farinha

 <div class="btn record-audio" title='Enviar um áudio'> 🎤 </div> <div class="btn start-video" title='Câmera'>Câmera</div> <div class="btn stop-video" title='Stop'>Parar</div> <div class="btn take-picture" title='Tirar uma foto'> 📷 </div> <div class="btn record-video" title='Gravar vídeo'> ⏺ </div>  <video src="" id="videoFeed" muted autoplay></video> <canvas id="picture-canvas"></canvas>  

Note que o vídeo precisa ter o atributo autoplay, caso contrário, você só verá a primeira imagem capturada pela câmera.

Note o uso do atributo muted no vídeo. Isso é porque, enquanto estamos gravando o vídeo, não queremos que ele siga replicando o áudio como um eco (que acaba ficando com um efeito "infinito", como em uma sala de espelhos).

Adicione CSS a gosto.

Mas acrescente esta pitada ao seu CSS:

 .picture-canvas {   display: none; } 

Depois de pronto, sinta-se à vontade para usar CSS para esconder ou exibir o vídeo ou os botões, por exemplo, e acrescentar outras animações.

Acessando câmera e microfone

Primeiro, temos que conseguir ligar e desligar a câmera e, para isso, usaremos a nova API do mediaDevices.

Essa API tem o método getUserMedia, que utilizaremos para solicitar permissão à câmera para o usuário, que só precisa responder na primeira vez. Este método nos devolve uma promise que resolverá em um objeto do tipo stream.

Quando estivermos com o stream em mãos, podemos colocá-lo diretamente em nosso elemento de vídeo em sua propriedade srcObject.

O método getUserMedia aceita um objeto de configurações que nos permite informar o que exatamente estamos querendo. Este objeto segue o padrão abaixo:

  • video: boolean ou Object com opções para o vídeo

  • audio: boolean ou Object com opções para o áudio

Na qual, para o vídeo, temos as opções:

  • facingMode: qual câmera damos preferência. Pode ser "environment" para câmera traseira, ou "user" para câmera frontal.

  • width/height: qual o tamanho (sim, resolução) preferencial para o feed de vídeo. E o mais interessante é que podemos passar aqui um valor fixo ou um outro objeto especificando min, ideal e max. Se especificarmos um valor fixo, ele será tratado como "ideal".

Por exemplo:

{   audio: true,   video: {     facingMode: "user",     width: { min: 1024, ideal: 1280, max: 1920 },     height: { min: 776, ideal: 720, max: 1080 }   } } 

Note que o facingMode é apenas uma "sugestão" de qual câmera é de nossa preferência. Caso queira exigir que a câmera seja uma ou outra, passe um objeto com { exact: 'user' }, por exemplo.

Podemos especificar o frame-rate da câmera também:

{   audio: true,   video: {     frameRate: { ideal: 10, max: 15 }   } } 

Outra coisa importante é que, em um dado momento, precisaremos desligar a câmera.

Para isso, vamos usar o método getVideoTracks para percorrer todas as trilhas do vídeo atual, parando-as.

Nosso código ficará assim:

 function startCamera () {     navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: true })         .then((stream) => {             document.getElementById('videoFeed').srcObject = stream         }) } function stopCamera () {     document.getElementById('videoFeed')         .srcObject         .getVideoTracks()         .forEach(track => track.stop()) }  document.querySelector('.start-video').addEventListener('click', event => {     startCamera() }) document.querySelector('.stop-video').addEventListener('click', event => {     stopCamera() }) 

Tirando uma foto

Para tirarmos uma foto, faremos o seguinte:

  • Ligamos a câmera do usuário

  • Conectamos a câmera a um elemento vídeo

  • Quando o usuário clicar o botão capturar foto, enviamos os bytes da imagem para um canvas

  • Coletamos o blob do canvas

  • Desligamos a câmera

"Como?" você pergunta. Fácil. Nosso método startCamera já executa os dois primeiros passos para nós.

A função (abaixo) é bastante autoexplicativa:

 document.querySelector('.take-picture').addEventListener('click', event => {     // coletamos os elementos que precisamos referenciar     const canvas = document.getElementById('picture-canvas')     const context = canvas.getContext('2d')     const video = document.getElementById('videoFeed')     // o canvas terá o mesmo tamanho do vídeo     canvas.width = video.offsetWidth     canvas.height = video.offsetHeight     // e então, desenhamos o que houver no vídeo, no canvas     context.drawImage(video, 0, 0, canvas.width, canvas.height)      // olha que barbada, o canvas tem um método toBlob!     canvas.toBlob(function(blob){         const url = URL.createObjectURL(blob)         // podemos usar esta URL em um elemento de vídeo, ou fazer upload do blob, etc.         // e então, não precisamos mais da câmera         stopCamera()     }, 'image/jpeg', 0.95)     closeCamera() }) 

Usamos o método toBlob do canvas para termos acesso ao formato blob da imagem armazenada nele. Isso nos possibilita usar esta imagem de várias maneiras diferentes.

Para este método, passamos o formato dele (neste caso, 'image/jpeg') e a qualidade da imagem.

Gravando um vídeo

Mas, Felipe, é impossível gravar um vídeo em front-end, por exemplo.

Rá! Não tema, porque hoje já é possível, sim!

Infelizmente, o suporte ainda não é pleno, mas no Firefox e no Chrome, já é bem estável há algum tempo.

Suporte da geração de gravação de vídeo e áudio, com JavaScript

Vou explicar como ele funciona.

Passaremos o nosso stream para o construtor new MediaRecorder.

É isso :)

A instância de um MediaRecorder tem os seguinte métodos:

  • isTypeSupported: verifica se um MIMEtype é suportado pelo navegador

  • pause: hum... Pausa

  • resume: "despausa"

  • start: Começa a gravar. Rápido, alguém grita "AÇÃO!"

  • stop: Finaliza a gravação atual.

Existem alguns eventos também, como 'onpause', 'onresume', 'onstop' e 'onerror', mas tem um mais peculiar, o ondataavailable.

Este evento é disparado várias vezes sempre que um novo chunk do vídeo está disponível. Ele é disparado também quando o evento de stop acontece, nos enviando o último chunk.

Esses chunks terão o tamanho do timeslice passado no método start.

 const recorder = new MediaRecorder(stream) recorder.start(3000) recorder.ondataavailable(chunk -> {     // cada chunk terá no máximo 3 segundos de duração }) 

Lembrando que o timeslice é opcional e se não for informado, teremos um único chunk com todo o vídeo.

Para gravarmos nosso vídeo, usaremos a mesma stream que já temos do método startCamera.

 let videoRecorder = null document.querySelector('.record-video').addEventListener('click', event => {     let chunks = []     const videoFeed = document.getElementById('videoFeed')     // caso não estejamos gravando, começaremos     if (!videoRecorder) {         // vamos usar o mesmo stream que já está ativo em nosso vídeo         const stream = videoFeed.srcObject          videoRecorder = new MediaRecorder(stream)         videoRecorder.start(3000)          // sempre que um novo chunk estiver pronto, ou         // quando a gravação for finalizada         videoRecorder.ondataavailable = event => {             // nós simplesmente armazenaremos o novo chunk             chunks.push(event.data)         }             // e, finalmente, quando a gravação é finalizada         videoRecorder.onstop = event => {             // nós montaremos um blob a partir de nossos chunks             // nesse caso, no formato de vídeo/mp4             let blob = new Blob(chunks, { 'type' : 'video/mp4' })             // e podemos usar o nosso blob, aqui, à vontade         }      } else {         // se o vídeo estava sendo gravado, quer dizer que o usuário         // quer finalizar a gravação         videoRecorder.stop()         // e podemos também finalizar a câmera         stopCamera()     } }) 

Viu só que fácil? :)

De Blob para URL

Caso queira testar pra ver se sua gravação está funcionando, você pode tornar seu blob em uma URL e usá-lo em um elemento HTML do tipo vídeo ou áudio.

Fazemos isso utilizando o objeto URL e seu método createObjectURL.

 URL.createObjectURL(blob) 

Como lembrado pelo Nilton Cesar nos comentários, é muito importante também usarmos o método URL.revokeObjectURL() passando para ele, a URL criada anteriormente a partir do blob. Isso por que quando criamos um ObjectURL, estamos pedindo para o browser manter este "arquivo" vivo na memória referenciado por aquele endereço até a página não precisar mais dele, o que somente acontecerá quando usarmos o revokeObjectURL ou quando o usuário recarregar a página. Como SPAs(Single Page Applications) normalmente evitam ao máximo o re-carregamento da página e procuram manter o usuário na mesma página o maior tempo possível, isso poderia ocasionar em memory leaks.

Mas não vá achando que vai escapar assim tão fácil, não! Tenho um desafio para ti. É hora de você construir a gravação de áudio!

A ideia é seguir esta receita, assim como fizemos para gravar um vídeo, para gravarmos o áudio.

Mais material

Apesar de estas serem APIs relativamente novas, tem muito material disponível.

E não perca a oportunidade de deixar um comentário bem bacana, em especial se tiver conseguido finalizar o desafio que passei.