blog@matheusmaydana:~$

← voltar ao blog

MayScreen: compartilhar a tela depois que o Discord perdeu o Go Live

· Projeto

Quem joga com amigos no Discord conhece o ritual: entra na call, clica em transmitir e todo mundo vê a sua tela, com o som do jogo. Desde 17 de agosto isso não existe mais para quem está no Brasil. Então fiz o MayScreen: você escolhe um nickname, dá um nome para a transmissão e manda o link. Quem abrir assiste na hora, com áudio, sem instalar nada e sem criar conta.

O que aconteceu com o Discord

No dia 6 de agosto, depois da morte de uma adolescente de 13 anos em Mato Grosso do Sul, num caso ligado ao Discord, a primeira-dama Janja pediu publicamente que a plataforma saísse do ar. No dia 12, a ANPD adotou uma medida preventiva com base no ECA Digital: o Discord teve de suspender no Brasil o Go Live e qualquer recurso equivalente de transmissão ou compartilhamento de vídeo. A suspensão vale desde o dia 17 e só termina quando a empresa demonstrar medidas de proteção que a ANPD aceite (TechTudo, Discord).

O Discord não foi banido: texto, voz e servidores continuam funcionando. Mas o recurso que o meu grupo mais usava, mostrar o jogo enquanto conversa, sumiu sem data para voltar.

A segurança de crianças e adolescentes nas plataformas é um assunto sério e merece mais do que um parágrafo num post técnico. Este post é sobre o buraco prático que ficou e sobre como eu o preenchi.

O que o MayScreen faz

Como funciona

O vídeo não passa pelo servidor. Ele vai direto do navegador de quem transmite para o de cada espectador, por WebRTC. O servidor só ajuda os navegadores a se encontrarem e guarda o que precisa persistir: canais, histórico, fotos de perfil e replays.

 quem transmite                     Cloudflare                   quem assiste
┌──────────────────┐    ┌────────────────────────────────┐    ┌────────────────┐
│ tela + áudio     │ ws │ Worker ─► Durable Object       │ ws │ navegador      │
│ + microfone      │◄──►│ (uma sala por transmissão)     │◄──►│ (sem conta)    │
│ (Web Audio mixa) │    │ D1: canais · R2: fotos/replay  │    │                │
└────────┬─────────┘    └────────────────────────────────┘    └───────▲────────┘
         │                                                            │
         └────────── vídeo + áudio por WebRTC, direto (P2P) ──────────┘

Captura e áudio

A tela vem de getDisplayMedia e o microfone, de getUserMedia. Em vez de mandar duas faixas de áudio, um mixer feito com Web Audio junta as duas numa só. Assim, ligar ou desligar o microfone no meio da transmissão é só mudar um ganho, sem renegociar a conexão com cada espectador. O SDP também é reescrito para o Opus sair em estéreo a 128 kbps, porque o padrão do WebRTC é mono, pensado para voz, e não para a trilha de um jogo.

Sinalização

Antes de trocar vídeo, os navegadores precisam trocar ofertas, respostas e candidatos de rede (offer, answer e ICE). Isso passa por um WebSocket. Cada transmissão tem a sua sala: um Durable Object da Cloudflare, que recebe os sockets de quem transmite e de quem assiste e repassa as mensagens entre eles.

A sala usa a API de hibernação. O estado de cada conexão fica no próprio socket, e o objeto dorme enquanto ninguém fala. O keepalive dos clientes é respondido pelo runtime sem acordá-lo. Se quem transmite some por 120 segundos, um alarme do próprio objeto encerra a transmissão.

Mídia em mesh

Quem transmite abre uma conexão WebRTC por espectador, com vídeo de até 2,5 Mbps a 30 fps e áudio de 128 kbps. É a topologia mais simples que existe e não custa nada de servidor, mas o upload de quem transmite cresce a cada pessoa que entra: com 5 espectadores são uns 13 Mbps, com 25 passa de 65 Mbps. Por isso o limite é de 25 espectadores por transmissão.

Replay

Se o dono liga “Gravar para replay”, o navegador dele grava MP4 (H.264 + AAC, cerca de 1,5 Mbps) com MediaRecorder e envia o arquivo em partes de 8 MiB enquanto transmite. O Worker grava cada parte no R2 como um multipart upload, então nunca precisa segurar o vídeo inteiro. Como o MediaRecorder não acompanha a troca de faixa quando a pessoa para e volta a compartilhar, cada trecho contínuo vira um segmento, e o player toca os segmentos em sequência.

Um Cron Trigger diário completa uploads abandonados e apaga replays com mais de 30 dias. A soma de todos os replays tem um teto de 9 GB, para ficar dentro dos 10 GB gratuitos do R2.

Por que essas tecnologias

WebRTC
É o jeito de capturar tela e áudio no navegador e entregar com menos de um segundo de atraso, sem instalar nada. Um fluxo com OBS, RTMP e HLS teria vários segundos de atraso e precisaria de um servidor recebendo e redistribuindo o vídeo.
Mesh em vez de SFU
Um SFU (como o Cloudflare Realtime ou o LiveKit) recebe o vídeo uma vez e redistribui, o que acaba com o limite de upload. Mas é mais uma peça para operar e pagar. Para um grupo de amigos, a mesh resolve. E tem um efeito colateral bom: como a mídia não passa pelo servidor, ele não vê nem guarda a transmissão, a não ser que o dono peça o replay.
Cloudflare Workers
A primeira versão rodava num servidor Node com Express, ws e SQLite. Funcionava, mas era uma máquina para manter ligada. Na Cloudflare, o frontend sai como Static Assets, que não contam como requisição de Worker, e a API roda num Worker com Hono, que tem a cara do Express. O plano gratuito dá 100 mil requisições de Worker por dia, o que cobre o uso de um grupo de amigos.
Durable Objects
A sinalização precisa de um lugar onde todos os sockets de uma transmissão se encontrem. Num servidor comum, isso é um objeto em memória; em serverless, costuma virar Redis ou pub/sub. O Durable Object é exatamente esse lugar: uma instância única por nome, com WebSockets, alarmes e armazenamento próprios. Com a hibernação, uma sala ociosa não fica cobrando tempo de execução.
D1 e R2
O D1 é SQLite, o mesmo banco que a versão Node já usava. A sala grava nele o número de espectadores e se a tela está ativa, e a API lê de lá sem precisar acordar a sala. O R2 guarda fotos e replays, é acessado pelo binding do Worker, sem chaves de API no código, e não cobra tráfego de saída, o que pesa quando o conteúdo é vídeo.
React e Vite
No frontend, uma SPA com React, Vite e CSS escrito à mão, sem biblioteca de componentes.

Como eu testo

Os testes de unidade e de API rodam o bundle real do Worker dentro do workerd, via Miniflare, com D1, R2 e Durable Objects emulados. O teste de ponta a ponta usa o Playwright num Chromium com uma tela falsa desenhada em canvas: um transmissor e dois espectadores conectam por WebRTC de verdade, e o teste confere se o vídeo que chega tem conteúdo e se o áudio do sistema e o do microfone chegam, e somem quando silenciados.

Limites

Se a audiência crescer, o próximo passo é trocar a mesh por um SFU. Por enquanto, o MayScreen está no ar em may-screen.matheusmaydana.com.br, e quem quiser trocar ideia sobre ele pode entrar no servidor do Discord. Por texto, claro.

  • webrtc
  • cloudflare
  • durable-objects
  • react