Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
6.5 KiB
Plano — Compartilhar tela do Quest 3 via link público
Permitir que o inspetor no Quest gere um link tipo tracksteelxr.lovable.app/watch/AB12CD, mande no WhatsApp/email, e até 3 espectadores assistam ao vivo (vídeo + áudio do Quest) em qualquer celular ou PC.
Como vai funcionar para o usuário
No Quest (broadcaster):
- No HUD do
/xraparece novo botão "Compartilhar" (ícone Share/Cast) - Ao clicar: app gera código de 6 caracteres, mostra QR + link encurtado
- Botões "Copiar link" e "Compartilhar" (Web Share API → abre WhatsApp/email do sistema)
- Indicador vermelho 🔴 LIVE fica visível enquanto transmite, com contador de viewers
- Botão "Encerrar" para parar
No celular/PC (viewer):
- Abre o link → tela cheia com vídeo da sessão XR
- Sem login, sem instalar nada
- Mostra "Aguardando inspetor…" se a sala não estiver ativa
- Read-only: só assiste
Arquitetura técnica
Quest 3 (broadcaster) Lovable Cloud Viewers (celular/PC)
┌──────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ /xr │ │ Supabase Realtime │ │ /watch/AB12CD │
│ - canvas WebXR │ ──SDP──▶│ (signaling channel) │◀──SDP── │ - <video> tag │
│ - getDisplayMedia│ │ rooms table │ │ - peer connection│
│ - RTCPeerConn x3 │◀════════ ICE candidates ═════════════▶ │ │
└──────────────────┘ │ viewers count │ └──────────────────┘
║ └─────────────────────┘ ▲
╚════════ WebRTC P2P media stream (vídeo H264) ════════════════╝
Stack:
- Lovable Cloud (precisa habilitar): tabela
share_rooms+ canal Realtime para signaling - WebRTC nativo do browser (
RTCPeerConnection) — sem libs pesadas - Captura no Quest:
navigator.mediaDevices.getDisplayMedia()no browser do Quest captura a janela atual (inclui o canvas 3D). Em sessão XR imersiva, alternativa é usarcanvas.captureStream(30)direto do canvas WebGL — funciona melhor e não precisa de prompt - Sem servidor de mídia (SFU): P2P 1→3 viewers, banda do Quest aguenta tranquilo (~2 Mbps por viewer)
Estrutura de arquivos
Novos:
src/pages/Watch.tsx— página pública do viewer (rota/watch/:code)src/components/ShareButton.tsx— botão + dialog com QR/link no HUDsrc/lib/webrtc/broadcaster.ts— lógica do lado Quest (cria offer, gerencia múltiplos peers)src/lib/webrtc/viewer.ts— lógica do lado viewer (recebe offer, monta<video>)src/lib/webrtc/signaling.ts— wrapper sobre Supabase Realtime para troca SDP/ICE
Editados:
src/App.tsx— adicionar rota/watch/:code(pública, fora do layout XR)src/components/XRHud.tsx— adicionar<ShareButton />na toolbarsrc/pages/XRSession.tsx— botão também visível fora do HUDsrc/stores/useModelStore.ts— estadoshareRoom: { code, viewerCount, isLive }
Schema do banco (Lovable Cloud)
create table public.share_rooms (
code text primary key, -- 6 chars, ex: 'AB12CD'
created_at timestamptz default now(),
expires_at timestamptz default now() + interval '4 hours',
is_active boolean default true,
viewer_count int default 0
);
-- RLS: leitura pública (viewers precisam saber se sala existe), escrita só anon controlada
alter table public.share_rooms enable row level security;
create policy "anyone reads active rooms" on public.share_rooms
for select using (is_active and expires_at > now());
create policy "anyone creates room" on public.share_rooms
for insert with check (true);
create policy "anyone updates count" on public.share_rooms
for update using (true);
Signaling propriamente dito (SDP offers/answers, ICE candidates) acontece via Supabase Realtime broadcast channel room:${code} — não persiste em tabela, é efêmero.
STUN/TURN
- STUN público do Google (
stun:stun.l.google.com:19302) — grátis, resolve ~80% dos casos (NAT comum) - TURN: se inspetor estiver atrás de NAT simétrico (raro em fábrica), conexão falha. Para v1 deixamos só STUN. Se precisar TURN depois, integramos Twilio TURN (~$0,40/GB) ou Metered.ca (free tier 50GB/mês)
Captura no Quest — detalhes importantes
Tem dois cenários:
- Fora da sessão XR (modo viewer 2D):
canvas.captureStream(30)— fácil, funciona já - Dentro da sessão XR imersiva (passthrough AR): aqui o canvas é renderizado para o display do headset, não para a tela visível. Solução: usar
XRWebGLLayer.framebuffere renderizar uma view extra "spectator" para um canvas offscreen que vira o stream. Esta parte tem risco — pode precisar fallback paragetDisplayMedia()que pede permissão do usuário no Quest
Plano: começar com cenário 1 (já cobre uso fora do AR imersivo), e cenário 2 numa segunda iteração se necessário.
Limitações honestas
- Latência: 200-500ms (não é instantâneo, mas serve para acompanhamento)
- Sem áudio do microfone do Quest na v1 (adicionar depois com
getUserMedia({audio:true})) - Sala expira em 4h — geração de novo código se sessão durar mais
- Link público: qualquer um com a URL vê. Se vazar, código pode ser revogado pelo inspetor
- Não grava — só transmissão ao vivo. Gravação seria feature separada
Pré-requisito
Precisa habilitar Lovable Cloud (atualmente o projeto não tem). Vou pedir confirmação antes de habilitar, pois cria infraestrutura backend.
Escopo desta implementação
✅ Botão "Compartilhar" no HUD do XR + tela inicial
✅ Geração de código + QR + link curto + Web Share API
✅ Página /watch/:code pública com vídeo
✅ Signaling via Supabase Realtime
✅ WebRTC P2P 1→3 viewers
✅ Contador de viewers em tempo real para o broadcaster
✅ Captura via canvas.captureStream (cenário fora do XR imersivo)
❌ Captura dentro da sessão XR imersiva (segunda iteração)
❌ Áudio do mic (segunda iteração)
❌ Chat / interação (você optou por read-only)
❌ TURN server (só se aparecer problema de NAT)
❌ Gravação
Aprovando o plano, eu habilito o Lovable Cloud e implemento.