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!
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!