Fazendo um gravador de tela diretamente do browser com React

Contextualizando

O Gifcap é um dos meus sites favoritos, pra quem não conhece, é um projeto onde você pode gravar sua tela ou a tela de algum programa e transformar a gravação em um GIF, é um projeto bem completinho com a possibilidade de cortar o vídeo e o resultado final fica muito bacana! Recomendo darem uma olhada, uso muito pra fazer GIF dos meus projetos pessoais.

Esse projeto me inspirou a entender melhor como tudo isso funciona, e na realidade é relativamente simples, a gravação de tela é feita através da MediaRecorder API, uma API nativa dos navegadores que permite controlar a gravação e que é utilizada por vários e vários sites como Google Meet, Teams, etc.

Então nesse guia vou mostrar pra vocês o geral da API, criando um Hook personalizado em React para controlar a gravação de tela, o resultado final fica assim!

Alt Text

Mão na massa

Vamos começar iniciando um novo projeto React, pra isso irei utilizar o Vite, mas sinta-se a vontade pra usar outra ferramenta se preferir.

npm create vite@latest recorder-tutorial

Em seguida, escolha a framework React e a variante Typescript (pode usar JS puro se preferir). O próximo passo é entrar na pasta, instalar as dependencias, colocar o projeto pra rodar e começar a codar!

cd recorder-tutorial
npm install
npm run dev

Remova o arquivo App.css e deixe o arquivo App.tsx dessa forma:

function App() {
  return <div></div>;
}

export default App;

Vamos criar nosso hook personalizado para lidar com a MediaRecorder API, dentro de /src crie uma pasta chamada /utils e dentro dela uma arquivo useScreenRecorder.ts.

Dentro do arquivo criado, insira o código: (Calma que vou explicar o código).

import { useEffect, useState } from "react";

export default function useScreenRecorder() {
  // Definição dos estados
  const [isRecording, setisRecording] = useState<boolean>(false);
  const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(
    null
  );
  const [videoUrl, setvideoUrl] = useState<string>();

  useEffect(() => {
    // Adiciona os eventos de dataavailable e stop ao MediaRecorder
    if (mediaRecorder) {
      mediaRecorder.addEventListener("dataavailable", (e) => {
        const uri = URL.createObjectURL(e.data); // Cria um objeto URL para o Blob
        setvideoUrl(uri);
      });

      mediaRecorder.addEventListener("stop", () => {
        setMediaRecorder(null); // Limpa o estado do MediaRecorder
        setisRecording(false); // Limpa o estado de gravação
      });
    }
  }, [mediaRecorder, isRecording]);

  // Função para iniciar a gravação
  const starRecording = async () => {
    if (!isRecording && navigator.mediaDevices) {
      // Obtém o stream de vídeo e áudio, caso o navegador suporte

      const stream = await navigator.mediaDevices.getDisplayMedia({
        video: true, // Adiciona o vídeo
        audio: true, // Adiciona o áudio
      });

      // Cria uma instância do MediaRecorder
      const mediaRecorder = new MediaRecorder(stream);
      setMediaRecorder(mediaRecorder);

      // Inicia a gravação e atualiza estado
      mediaRecorder.start();
      setisRecording(true);
    }
  };

  // Função para parar a gravação
  const stopRecording = async () => {
    if (isRecording) {
      // Para a gravação e o estado
      // OBS: Isso irá ativar o trigger onstop definido acima
      mediaRecorder?.stop();
    }
  };

  // Export dos estados e funções
  return { isRecording, starRecording, stopRecording, videoUrl };
}

OK, vamos por partes, irei detalhar as partes mais importantes do código.

  useEffect(() => {
    if (mediaRecorder) {
      mediaRecorder.addEventListener("dataavailable", (e) => {
        const uri = URL.createObjectURL(e.data);
        setVideoData(uri);
      });

      mediaRecorder.addEventListener("stop", () => {
        setMediaRecorder(null);
        setisRecording(false); 
      });
    }
  }, [mediaRecorder, isRecording]);

A MediaRecorder API possui diversos eventos, nesse caso, nós estamos adicionando eventListeners para os eventos dataavailable e stop. O primeiro é chamado sempre que os dados do vídeo estão prontos, por padrão isso acontece quando a gravação é finalizada (isso pode ser configurado), já o outro evento acontece quando a gravação de fato para. É uma boa prática usar o dataavailable pois ele acontece após o stop e garante que os dados estarão prontos 100% do tempo.

  const startRecording = async () => {
    if (!isRecording && navigator.mediaDevices) {
 
      const stream = await navigator.mediaDevices.getDisplayMedia({
        video: true, // Adiciona o vídeo
        audio: true, // Adiciona o áudio
      });

      const mediaRecorder = new MediaRecorder(stream);
      setMediaRecorder(mediaRecorder);

      mediaRecorder.start();
      setisRecording(true);
    }
  };

Essa função faz a chamada para uma outra API, a de MediaDevices, é importante citar que aqui fazemos a verificação navigator.mediaDevices dentro do laço condicional, isso garante que o navegador suporta essa API, sendo interessante avisar o usuário caso não haja suporte! (Eu não fiz isso nesse exemplo).

Em seguida criamos uma stream de vídeo através da função getDisplayMedia() essa função é a que vai fazer com que o Pop-up de permissão apareça. Dessa forma é criado uma stream de dados do tipo MediaStream, que será utilizado na criação do nosso MediaRecorder, em seguida chamamos a função start() do nosso MediaRecorder e a mágica já começou! Todos os dados da tela que você selecionou já estão sendo gravados.

  // Função para parar a gravação
  const stopRecording = async () => {
    if (isRecording) {
      // Para a gravação e o estado
      // OBS: Isso irá ativar o trigger onstop definido acima
      mediaRecorder?.stop();
    }
  };

Essa função é simples de entender, chamamos a função stop() do nosso MediaRecorder, o que irá ativar os eventos dataavailable e stop já citados anteriormente.

Agora vamo colocar isso num front-end básico e testar?

import useScreenRecorder from "./utils/useScreenRecorder";

function App() {
  const { isRecording, startRecording, stopRecording, videoUrl } =
    useScreenRecorder();

  return (
    <div>
      <button onClick={startRecording}>Start Recording</button>

      <button onClick={stopRecording}>Stop Recording</button>

      {isRecording && <div>Recording...</div>}

      {videoUrl && (
        <>
          <video src={videoUrl} controls width={"500px"}></video>
          <a href={videoUrl} download="video.mp4">
            Download
          </a>
        </>
      )}
    </div>
  );
}

export default App;

Você pode aprimorar o front-end de diversas formas, não é o foco desse tutorial, se quiser ver como eu fiz usando TailwindCSS da uma olhada no meu repositório.

Essa foi a primeira vez que escrevo um Tutorial/Guia e fico aberto a críticas, gostei de escrever e pretendo fazer mais no futuro, obrigado a quem gostar!