Executando verificação de segurança...
9

🎥 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:

  1. Melhor Experiência de Desenvolvimento (DX): Trabalhar com as APIs mediaDevices e MediaRecorder é 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

Carregando publicação patrocinada...