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
emax
. 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.