🎥 Estudei APIs nativas do browser e FFMPEG para criar meu próprio OBS
Fala pessoal, tudo certo?
Pra hoje venho compartilhar aqui meu aplicativo gratuito de gravações: Kronos. O app que grava seus videos, organiza em pastas customizadas por você e (se você quiser), automaticamente faz o upload para o Youtube, criando uma playlist com o seu workspace escolhido.
Como ele nasceu e qual dor resolve?
Sempre usei o OBS para gravar reuniões, bugs, demonstração de features, etc. Contudo, a UI não me agradava 100%, então pensei em uma UI/UX do zero.
E eu gostava de organizar em pastas as minhas gravações, pra eu não me perder nos arquivos ou poder sempre consultar quando necessário. Problema disso? Era bem massante e muitas vezes eu apenas desistia de organizar, deixando tudo bagunçado na minha pasta local de videos.
Com isso em mente, me surgiu uma ideia de criar o meu próprio OBS. Pesquisei várias coisas, começando, claro, pelo FFMPEG!
Porém, pasmem, não estou usando ele pra gravar diretamente os vídeos, apenas para pegar informações dos arquivos gravados. E por qual motivo? Bom, vamos la:
APIs nativas do Browser e do Electron
Usei diversas APIs para gravação de vídeo neste projeto, algumas delas, mas não limitadas a, são:
- mediaDevices
- AudioContext
- MediaStream
- MediaRecorder
- desktopCapturer
Essas interfaces me ajudam a chegar no meu objetivo principal: Mostrar preview do monitor/janela que a pessoa quiser e gravar video, áudio e microfone. E isso foi uma aventura! Algumas coisas (principalmente de áudio), estavam complicadas de entenderem, mas vou exemplificar aqui com foi feito, bora lá
Como a mágica da gravação acontece? (Com código!)
Pense nisso como montar um set de filmagem com peças de LEGO.
Passo 1: Apontando a Câmera para a Tela
Primeiro, eu peço ao Electron uma lista de telas e janelas. Quando você escolhe uma (screenSourceId
), eu uso a API getUserMedia
para capturá-la.
// Pede ao navegador para criar um "stream" (fluxo de dados)
// da tela que o usuário escolheu.
const desktopStream = await navigator.mediaDevices.getUserMedia({
audio: {
mandatory: {
// Pede para capturar o áudio que sai dos alto-falantes
chromeMediaSource: 'desktop'
}
},
video: {
mandatory: {
chromeMediaSource: 'desktop', // Diz que queremos uma fonte de vídeo do desktop
chromeMediaSourceId: screenSourceId // O ID específico da tela ou janela
}
}
});
Resultado: Temos um objeto desktopStream
com a faixa de vídeo e a faixa de áudio do seu computador.
Passo 2: Ligando o Microfone
Agora, capturamos a sua voz em um stream separado.
// Pede ao navegador para criar um stream de áudio do microfone
const micStream = await navigator.mediaDevices.getUserMedia({
audio: true, // Só queremos áudio
video: false // ...e nada de vídeo desta vez
});
Resultado: Temos um micStream
separado, contendo apenas o som da sua voz.
Passo 3: A Mesa de Som Virtual com AudioContext
Aqui vem a parte mais legal: juntar o som da tela com o som do microfone. Faço isso em tempo real.
// Primeiro, separamos a faixa de vídeo original, pois ela não vai mudar
const [videoTrack] = desktopStream.getVideoTracks();
// Agora, a mágica do áudio começa criando um "contexto" de áudio
const audioContext = new AudioContext();
// Criamos um "destino" para o nosso áudio já mixado
const destinationNode = audioContext.createMediaStreamDestination();
// Adicionamos o áudio do desktop como uma fonte no nosso "mixer"
// (se ele existir, claro)
if (desktopStream.getAudioTracks().length > 0) {
const desktopSource = audioContext.createMediaStreamSource(desktopStream);
desktopSource.connect(destinationNode);
}
// E também adicionamos o áudio do microfone como outra fonte
const micSource = audioContext.createMediaStreamSource(micStream);
micSource.connect(destinationNode);
// Pegamos a faixa de áudio já mixada do nosso nó de destino
const [mixedAudioTrack] = destinationNode.stream.getAudioTracks();
// Por fim, criamos um novo stream final, combinando:
// a faixa de vídeo original + a nova faixa de áudio mixada
const finalStream = new MediaStream([videoTrack, mixedAudioTrack]);
Resultado: Tenho um finalStream
perfeito, com o vídeo da tela e o áudio combinado do sistema e do microfone.
Passo 4: O Botão de Gravar (REC) com MediaRecorder
Com tudo pronto e mixado, entrego o finalStream
para o MediaRecorder
.
// Cria uma nova instância do gravador com o stream final
const mediaRecorder = new MediaRecorder(finalStream, {
mimeType: 'video/webm; codecs=vp9,opus'
});
// Uma lista para guardar os "pedaços" do vídeo que serão gravados
const recordedChunks = [];
// Diz ao gravador o que fazer quando ele tiver um novo pedaço de vídeo
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
// Adiciona o novo pedaço à nossa lista
recordedChunks.push(event.data);
}
};
// Aperta o "REC"! A gravação começa agora.
mediaRecorder.start();
Resultado: O MediaRecorder
começa a gerar dados de vídeo, que eu vou guardando na lista recordedChunks
.
Passo 5: Juntando os Pedaços e Salvando
Quando você para a gravação, eu executo o código final dentro do evento onstop
do gravador.
// Para o gravador de forma segura. Quando ele parar, o `onstop` será chamado.
mediaRecorder.stop();
// Define o que acontece assim que a gravação for finalizada
mediaRecorder.onstop = async () => {
// Junta todos os pedaços de vídeo em um único arquivo em memória (Blob)
const videoBlob = new Blob(recordedChunks, {
type: 'video/webm'
});
// Converte o Blob para um formato que o Electron consegue manipular
const videoBuffer = await videoBlob.arrayBuffer();
// Envia o buffer do vídeo para o processo principal do Electron
// para que ele possa ser salvo no disco do usuário
await window.video.saveFile(videoBuffer, /* ... infos do vídeo ... */);
console.log('Vídeo salvo com sucesso!');
};
Resultado Final: Um arquivo .webm
é salvo no seu computador. Missão cumprida!
E é isso! Unindo essas peças de código, o Kronos consegue fazer todo o trabalho pesado para você.
Por que não usei o FFMPEG para gravar?
Muitos podem se perguntar: "Se o FFMPEG é tão poderoso, por que não usá-lo para a gravação em si?". A resposta é simples: as APIs nativas que eu escolhi tornaram o processo mais fácil, leve e direto ao ponto.
Aqui estão os principais motivos:
- Melhor Experiência de Desenvolvimento (DX): Trabalhar com as APIs
mediaDevices
eMediaRecorder
é como usar blocos de LEGO. Elas são bem documentadas, fáceis de entender e se integram perfeitamente com o JavaScript. Fazer a mesma coisa com FFMPEG exigiria lidar com processos complexos no terminal, linhas de comando enormes e uma curva de aprendizado muito maior. - Compatibilidade por Padrão: A maior vantagem é que essas APIs são parte da web moderna. Isso significa que meu código funciona em Windows, macOS e Linux sem nenhuma alteração. O Electron simplesmente "expõe" essas APIs para o meu aplicativo. Com FFMPEG, eu teria que me preocupar em empacotar binários diferentes para cada sistema operacional, o que é um grande trabalho extra.
- Menor Consumo de Recursos: Iniciar uma gravação com as APIs nativas é quase instantâneo e consome menos CPU e memória. O FFMPEG, sendo um processo externo, precisaria ser iniciado, configurado e gerenciado, o que pode ser mais lento e pesado para o sistema, especialmente em computadores menos potentes.
- Integração Perfeita com a Interface: Como tudo é JavaScript, eu consigo conectar facilmente a lógica de gravação com os botões, pré-visualizações de vídeo e controles de volume na tela. Mostrar um preview da tela, por exemplo, é trivial com um
MediaStream
. Com FFMPEG, isso seria um pesadelo para implementar. - Facilidade na Manipulação de Áudio: Como mostrei, a
AudioContext
me deu um controle incrível sobre as fontes de áudio, permitindo mixar o som do sistema e do microfone em tempo real. Fazer essa mixagem com FFMPEG "ao vivo" durante a gravação é possível, mas infinitamente mais complexo.
Em resumo, enquanto o FFMPEG é uma ferramenta fantástica e indispensável para processar vídeos depois que eles são criados (como para converter formatos ou extrair informações), para a tarefa de capturar e gravar, as APIs nativas foram a minha escolha da vez!
Kronos tem muitas outras features atuais e futuras que eu não falei em extensão aqui (talvez em posts posteriores), mas por hoje, é isso!
Abraços!
Baixem o Kronos em: https://kronosrec.com