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

Em:

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

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.

Patrocinadores BrazilJS

Gold

Silver

Bronze

Apoio

BrazilJS® é uma iniciativa NASC.     Hosted by Getup