Skip to content

feat(streaming): ferramentas de live para os streamers da comunidade - #576

Merged
danielhe4rt merged 39 commits into
4.xfrom
feat/streaming-tooling
Oct 5, 2026
Merged

danielhe4rt merged 39 commits into
4.xfrom
feat/streaming-tooling

Conversation

@danielhe4rt

Copy link
Copy Markdown
Contributor

Contexto

Eu faço live com um toolkit que roda na minha máquina: alerta de follow, chat na tela, cena de "a live vai começar"... Funciona, mas só serve pra mim. Se alguém da comunidade quer fazer live com a cara da He4rt, tem que montar tudo do zero ou depender de ferramenta de terceiro.

Esse PR traz isso pro site. Quem tem a role streamer conecta a Twitch na Minha Live (/app), cola o link da cena no OBS e pronto: alerta e chat chegam na overlay em tempo real, sem rodar nada local.

Se liga no caminho de um evento:

  Twitch (EventSub)
       │ webhook
       ▼
  integration-twitch    grava no lake (twitch_event_logs) e roda o ETL no próprio request
       │ Actions do streaming
       ▼
  streaming             sessão, evento de stream, chat (activity.messages), configurações
       │ broadcast no canal privado do streamer (Reverb)
       ▼
  panel-overlays        cenas do OBS em Inertia + React, em /overlay/{token}/{cena}

  panel-app (/app)      Minha Live: Painel · Overlays · Chat · Lives

O que a pessoa streamer ganha:

  • Painel: card "Ao vivo" com os números da sessão, ou a última live com as setas em relação à anterior. Do lado, a conta, a saúde da integração (conta, webhook, inscrições, último evento, overlays abertas) com botão de reparo quando algo quebra, e a atividade recente com "repetir alerta".
  • Overlays: link de cada cena (Coworking, A live vai começar, Sala de voz e Chat), prévia em escala com dados de exemplo, contador de overlays abertas agora, menu "Testar alerta" e a configuração de cada cena.
  • Chat: o chat da live atualizando sozinho, com ocultar mensagem, silenciar chatter e limpar o chat só na overlay. Na Twitch nada muda.
  • Lives: o histórico com as somas de cada live e o detalhe com linha do tempo, quem mais falou e a comparação com a última live com dados.
  • Cena de chat com fundo transparente, genérica pra qualquer ocasião, com 6 estilos, tempo pra sumir, tamanho, largura, direção e alinhamento.
  • Ban, timeout, /clear e mensagem apagada pela moderação da Twitch limpam a overlay também.

Alterações

streaming (módulo novo, domínio)

  • Streamer 1:1 com o usuário, nasce no primeiro acesso à Minha Live e segue a role. Token da overlay criptografado, com busca pelo hash e canal privado derivado dele.
  • Fontes (streamer_sources): cada conta de live conectada vira uma fonte, com toggle e leitor do chat (a própria conta ou a conta bot).
  • Sessões e eventos de stream como fatos imutáveis. Uma sessão aberta por identidade (índice parcial), details tipado por evento (sub, gift sub, bits, raid) e dedupe por origem.
  • Chat em activity.messages. Quem só assiste vira uma identidade sem conta, com XP 0.
  • StreamerSettings em jsonb tipado, um VO por cena. Valor inválido vira o padrão na leitura.
  • Controle do chat na overlay: ocultar, silenciar e limpar, sem tocar na Twitch.
  • Saúde: contrato HealthCheck e a contagem de overlays abertas pelo subscription_count do Reverb.
  • Lives: StreamSessionTotals (somas por subselect), StreamSessionHistory (live aberta, última encerrada, vizinhas e base da comparação) e StreamSessionChat (ritmo e quem mais falou).

panel-overlays (módulo novo)

  • Cenas em Inertia + React portadas do toolkit, sem SSR, em rota pública por token.
  • POST /overlay/{token}/broadcasting/auth assina só o canal do próprio streamer, recusa token regenerado e tem limite de 30 por minuto.
  • Estado inicial no mesmo formato dos broadcasts: recarregar a fonte no OBS mantém o chat e não repete alerta. Com ?demo, a cena usa o feed de exemplo e não assina o canal.

integration-twitch

  • ETL ProjectTwitchEventToStreaming do lake pro streaming, rodando no request do webhook com ShouldBroadcastNow. Antes, o alerta podia levar uns 6 segundos pra chegar, por causa dos dois saltos de fila.
  • Inscrições EventSub por fonte (9 de alerta, mais 4 de chat quando tem leitor), marcadas com a fonte. Desligar um streamer remove só as dele.
  • Reconciliação com a Twitch e "Reparar inscrições": confere o status remoto, adota as que são nossas e recria as que falharam.
  • Conta bot pra ler o chat de quem escolhe "Lido pela he4rtdevs", com renovação do token.
  • Features no OAuth: a Twitch pede só os escopos do que a pessoa escolheu. O token de usuário é renovado no 401.
  • Imagens dos badges do chat pelo catálogo da Helix, com cache de 6 horas.

identity / activity / panel-admin

  • Role streamer e a ability use-streamer-tools. O admin consegue se dar papéis comuns sem mexer no próprio super admin.
  • Card de conexões pede reautorização quando faltam escopos.
  • messages.platform, e o PersistMessage ficou idempotente pelo provider_message_id.

Correções que vieram no caminho (vale olhar com carinho)

  • O state do OAuth agora é amarrado à sessão que começou o fluxo. Antes, um callback gerado em outra sessão podia vincular a conta de outra pessoa.
  • Desconectar uma conta pelo id agora só alcança as conexões de quem pede.
  • Erros da Helix (4xx/5xx) passavam como sucesso. O "Sync from Twitch" com token inválido chegava a apagar todas as inscrições locais.
  • Inscrição verificada fica enabled (antes ficava pendente pra sempre). A revogação atualiza a inscrição em vez de virar evento.
  • Twitch sem credenciais não derruba mais o login com 500.

Migrations

  • streaming: streamers, streamer_sources, stream_sessions, stream_events, chat_cleared_at em streamers e o índice de stream_session_id.
  • identity: role streamer e índice único parcial pra identidade sem dono.
  • activity: platform em messages (default discord).
  • integration-twitch: streamer_source_id em twitch_subscriptions.

Pra subir

  • Rodar o reverb:start junto da aplicação, com as variáveis REVERB_* e VITE_REVERB_* (estão no .env.example).
  • TWITCH_EVENTSUB_SECRET e callback HTTPS público. Sem o segredo, todo webhook é recusado.
  • Conta bot é opcional (TWITCH_BOT_USER_ID e TWITCH_BOT_REFRESH_TOKEN).
  • O painel e o WebSocket do Reverb dividem o prefixo /app. Se o Reverb ficar no mesmo host, o proxy manda só o upgrade de WebSocket pra ele. A outra saída é um host próprio pro Reverb.
  • Dar a role streamer pelo admin pra quem vai usar.

Docs

  • CONTEXT.md e README do streaming (o caminho do evento e o passo a passo local com Reverb e twitch-cli), ADR-0001 do modelo de dados e os planos em streaming/docs/plans/.

Plano de Testes

  • make check (rector, pint e phpstan)
  • vendor/bin/pest --parallel: suíte completa, 2198 testes
  • Suítes dos módulos: panel-app (184), streaming (125), integration-twitch (130) e panel-overlays (24)
  • Overlay de ponta a ponta num navegador headless, com streamer temporário: canal privado assinado, chat chegando, limpeza por chatter e geral, e o chat limpo que não volta ao recarregar
  • Telas da Minha Live no navegador, em desktop e celular, claro e escuro
  • No meu canal, com o webhook público: inscrições ativas e evento real chegando no Painel

Pra testar na mão, o README do streaming tem o passo a passo:

  1. Suba o Reverb (php artisan reverb:start).
  2. Dê a role streamer pro seu usuário e abra Minha Live › Painel.
  3. Conecte a Twitch e cole o link de uma cena no navegador.
  4. Mande eventos com o twitch-cli (twitch event trigger channel.follow ...) ou use o menu Testar alerta em Overlays.

Evidências

Os prints vão num comentário logo abaixo: Painel offline e ao vivo, celular, Lives, detalhe da live e o menu "Testar alerta".


Issues Relacionadas

Sem issue: isso veio da minha própria live.

…ar evento

Uma mensagem `revocation` era gravada em twitch_event_logs com o
event_type da inscrição (ex.: stream.online) e disparava
TwitchEventReceived. Qualquer listener futuro (a overlay, por exemplo)
trataria a revogação como um evento real da live.

Agora a revogação só espelha o status enviado pela Twitch em
twitch_subscriptions, criando a linha se ela não existir localmente.
Nada vai para o log de eventos e nenhum evento é disparado.
O TwitchHelixConnector não lançava exceção em respostas 4xx/5xx, então
todos os `catch (RequestException)` em volta dele nunca rodavam:

- "Sync from Twitch" com token inválido recebia `data` vazio e apagava
  todas as inscrições locais, com notificação de sucesso.
- `twitch:subscribe` listava criações recusadas como "created".
- o mapeamento 403/409 do RegisterTwitchSubscriptionsAction nunca
  acontecia.

O conector passa a usar AlwaysThrowOnErrors. O delete no admin só apaga
a linha local quando a Twitch confirma, ou quando a inscrição já não
existe lá (404); nos outros erros, avisa e cancela.
…abled

O challenge de verificação era respondido sem tocar em
twitch_subscriptions, então toda inscrição criada pelo painel ficava em
webhook_callback_verification_pending para sempre. Como o registro só
pula inscrições `enabled`, rodar de novo tentava recriar todas e
recebia 409.

A Twitch habilita a inscrição quando o challenge é respondido. Agora o
webhook espelha a inscrição como `enabled` nesse momento, inclusive as
criadas fora do painel (`twitch:subscribe`).

O registro passa a usar firstOrCreate: se a verificação chegar antes da
resposta do CreateSubscription, o status `enabled` não é rebaixado para
pending.
…om 500

Com TWITCH_OAUTH_ENABLED ligado (o padrão) e sem client id/secret, o
botão "Continuar com Twitch" dava 500: o provider lia as credenciais com
config()->string(), que lança InvalidArgumentException, e o
OAuthController só trata RuntimeException. Valor vazio passava direto e
mandava o usuário para a Twitch com client_id em branco.

O provider passa a lançar RuntimeException quando a credencial falta ou
está vazia, como já faz a integração do GitHub. O login volta para a
home e registra o aviso no log.

O .env.example ganha o bloco da Twitch: OAuth desligado por padrão,
segredo do EventSub e callback, que precisa ser HTTPS público.
O state era só criptografado, sem nada que o ligasse a quem começou o
login. Um callback com state e code válidos, gerados em outra sessão,
era aceito: no intent `link`, a conta do provider (Discord, GitHub,
Twitch) era vinculada ao usuário logado, que depois podia ser acessado
por esse provider. O returnUrl do state também redirecionava para fora.

O redirect agora gera um nonce, guarda na sessão por provider e o envia
no state. O callback web consome o nonce (uso único) e recusa o state
que não bate, voltando para a home sem vincular nada. O fluxo mobile não
tem sessão e segue como antes.
Nova role `streamer`, concedida pelo admin no UserResource como o super
admin já é. Ela libera a ability `use-streamer-tools`, que vai decidir
quem pede escopos de broadcaster na Twitch e quem acessa as telas de
live. Super admin passa pela ability via Gate::before, já que o admin
não consegue alterar os próprios papéis.

O RolesSeeder só roda em dev, então uma migration cria a role em
produção. Sem ela, as telas de usuário do admin quebrariam no
UserRole::from() ao encontrar a role no banco.
…oadcaster

Até aqui o conjunto de escopos vinha só do painel: no /app todo mundo
pedia `user:read:email`, e os escopos de alerta (follows, subs, bits)
só existiam no fluxo do admin.

Agora quem tem `use-streamer-tools` e conecta a Twitch fora do /admin
pede o conjunto `streamer` (TWITCH_OAUTH_SCOPES_STREAMER). O login de
visitante e os membros comuns continuam só com `user:read:email`.

TwitchScopes concentra a regra e é usada pelo redirect e pelo card de
conexões, então o card mostra exatamente o que a Twitch vai pedir.
…opos

Quem já tinha a Twitch conectada (pelo login, por exemplo) e depois
recebe a role streamer ficava preso: o card só mostrava "Disconnect" e
não havia como pedir os escopos novos sem desconectar.

A conexão OAuth passa a guardar os escopos concedidos em
`metadata.granted_scopes`, a partir do `scope` que a Twitch devolve no
token. O card compara com o conjunto que o usuário precisa e, quando
falta algo, mostra "Reautorizar" e a lista do que falta. Conexões
antigas, sem escopos guardados, contam como o conjunto padrão de login,
então membros comuns não veem o aviso.
…er admin

A lista de papéis ficava toda travada quando o admin editava o próprio
usuário. Com a role streamer isso virou atrito: o admin que faz live
precisava de outro admin, ou do banco, só para se marcar como streamer.

Agora só a opção de super admin fica travada no próprio form. O
servidor também ignora o que vier nela, então nem um request montado à
mão tira o super admin de quem está logado, nem promove alguém a super
admin pelo próprio perfil. A ADR 0002 passa a registrar a regra nova.
Novo cluster "Minha Live" no /app, visível só para quem tem a ability
`use-streamer-tools`. Ele começa com duas telas:

- Painel: o canal da Twitch conectado (ou o convite para conectar),
  aviso quando faltam escopos, números dos últimos 30 dias, botões
  para testar cada alerta e a atividade recente.
- Overlays: uma cena por card (Coworking, A live vai começar, Alertas
  e Chat), com o link para o OBS mascarado, copiar, e uma ação para
  gerar novos links.

Só a conexão da Twitch é dado real. Números, atividade e links vêm de
StreamingPreviewData, e um aviso de prévia deixa isso claro na tela.
A ideia é validar layout e fluxo antes de modelar overlay e alertas.
…live

O card da Twitch no Painel mandava o streamer para o perfil para
conectar ou reautorizar. Agora as três ações ficam no próprio card:

- Conectar Twitch, quando não há conexão.
- Reautorizar, quando faltam escopos de streamer.
- Desconectar, com confirmação, quando há conexão.

Conectar e reautorizar apontam para o mesmo fluxo OAuth do /app, que
devolve o streamer ao Painel no fim. Desconectar só alcança a conexão
do usuário logado, e cada ação some quando não faz sentido, então o
Filament recusa a chamada mesmo vinda de um request montado à mão.
…m pede

O disconnectById do ConnectionHub buscava a identidade só pelo id, sem
olhar o dono. Qualquer usuário logado podia chamar o método pelo
Livewire, inclusive a partir do perfil em /app, e desconectar a conta de
outra pessoa (Discord, GitHub, Twitch) só com o id dela.

A busca agora fica restrita às identidades do próprio usuário. O super
admin também alcança as conexões de tenant, que são as que a tela do
painel admin lista. Fora desse escopo, a resposta é a mesma de um id que
não existe: o aviso "Connection not found".

O dono continua desconectando a própria conta, e o super admin continua
desconectando as conexões de tenant pelo painel admin.
A sub-navegação da Minha Live sai da lateral e vira abas acima do
conteúdo, que ocupa a largura toda da página.

O tema do /app passa a ler o módulo panel-app inteiro, então as classes
das páginas do cluster também entram no build do Tailwind.
…m React

Novo módulo de apresentação para as overlays que o streamer coloca no
OBS. Diferente dos painéis Filament, ele vai renderizar páginas com
Inertia e React, aproveitando o Vite que o projeto já tem.

Entram o inertiajs/inertia-laravel no Composer e o @inertiajs/react,
react, react-dom e @vitejs/plugin-react no npm, com versão fixa como o
resto das dependências de front. O módulo também ganha a linha
`mod:panel-overlays` na tabela de labels de triagem.
…exemplo

As cenas do toolkit local de live chegam ao site como páginas Inertia:
Coworking, A live vai começar e Sala de voz. Cada uma abre em
/overlay/{token}/{cena}, sem login, como fonte de navegador do OBS.

Do toolkit vieram só os componentes, hooks e o CSS da overlay. O único
ponto que falava com a rede era o useOverlayFeed; agora ele assina um
feed de exemplo que gera chat, alertas, música, nível e sala de voz.
Quando o tempo real chegar, a troca acontece nesse mesmo ponto.

- O Inertia roda só nessas rotas, com template próprio e sem SSR.
- O Vite ganha o plugin do React e a entrada do módulo, e o TypeScript
  com os tipos do React entram para checar o código portado.
- O token por enquanto só precisa ter o formato certo, e o canal é fixo.
- O catálogo de cenas da Minha Live passa a listar as três que existem.
- O Prettier do lint-staged passa a formatar arquivos .tsx.
Novo módulo de domínio para o backend das ferramentas de streamer:
quem é streamer, de quais plataformas transmite, sessões de live,
eventos de alerta e configurações das overlays. Ele não tem rota, view
nem Filament. Os módulos integration-* e os painéis dependem dele.

O módulo entra no composer.json da raiz com `^1.0.0`, como os outros
he4rt/*, e ganha a linha `mod:streaming` na tabela de labels de triagem.
O CONTEXT.md fixa o vocabulário do módulo e as fronteiras com identity,
activity, integration-twitch e os painéis. Os termos incluem streamer,
fonte, leitor do chat, sessão, evento de stream, identidade solta e
token da overlay.

A ADR-0001 registra o modelo de dados:

- o streamer é a raiz, 1:1 com o usuário;
- as plataformas são as ExternalIdentity que ele já conectou, com um
  toggle por fonte em `streamer_sources`;
- sessões e eventos são fatos imutáveis;
- as configurações das cenas ficam num jsonb tipado;
- o chat vai para `activity.messages`, com o espectador como identidade
  sem conta, e `messages` ganha uma coluna `platform`;
- o tempo real vai num canal privado do Reverb, autenticado pelo token
  da overlay.

A ADR também lista o que muda em outros módulos e o que ficou para
depois: retenção do lake, XP de live, doações e YouTube.
O plano leva a ADR-0001 até o código em 7 fases: pré-requisitos fora do
módulo, núcleo do streaming, sessões e eventos, chat, ingestão da
Twitch, tempo real no panel-overlays e a Minha Live com dados reais.

Cada passo tem o contexto com arquivo e linha, o código antes e depois
e os cenários BDD. O plano também registra problemas achados ao
planejar:

- o formulário de usuário do admin não dispara os eventos de role do
  spatie;
- o mesmo canal pode ter inscrições da comunidade e do streamer;
- o PersistMessage quebra num retry da fila.
Primeira fase do plano do streaming. Prepara identity, activity e
integration-twitch para receber dados de live, sem comportamento novo
para quem usa o site.

- identity: `IdentityProvider::streamingPlatforms()` diz quais contas
  transmitem ao vivo. Hoje só a Twitch.
- identity: a desconexão passa a ser a Action
  `DisconnectExternalIdentity`, que emite `ExternalIdentityDisconnected`.
  O ConnectionHub e a Minha Live usam a Action.
- identity: quem conecta uma conta que já existe como identidade sem
  dono assume essa identidade, com o histórico junto. Um índice parcial
  único impede duas identidades sem dono para a mesma conta.
- activity: `messages` ganha a coluna `platform`, com default `discord`.
  O PersistMessage grava a plataforma da mensagem, e os 8 leitores que
  contam mensagens como Discord passam a filtrar por ela.
- integration-twitch: a listagem de inscrições EventSub filtra pelo
  broadcaster e lê todas as páginas.
Segunda fase do plano. O módulo ganha o registro do streamer, sem
ingestão e sem tempo real ainda.

- Enums do domínio no `streaming`: `StreamEventType` (era o
  `StreamAlertType` do panel-app, e `gift-sub` vira `gift_sub`),
  `OverlayScene`, `StreamerStatus`, `ChatReader`, `SubTier` e
  `VoiceLayout`. O panel-app passa a importar deles.
- `StreamerSettings` tipado em jsonb, com um VO por cena e os alertas
  ligados por padrão. Valor inválido vira o padrão na leitura.
- Tabelas `streamers` e `streamer_sources`, com FK restritiva e soft
  delete. O token da overlay fica criptografado, e a busca usa o hash.
- `RegenerateOverlayToken`, `ResolveOverlayToken` e o nome do canal
  privado com a impressão do token.
- O streamer nasce no primeiro acesso à Minha Live e segue a role. O
  listener lê o gate depois do commit, então o super-admin não perde o
  streamer e trocar outras roles não gera evento.
- O spatie passa a emitir eventos de role, e o formulário de usuário do
  admin salva com `syncRoles` numa transação, porque o `sync` direto na
  relação não emite nada.
- Cada conta de live conectada vira uma fonte. Reconectar restaura a
  fonte e mantém o toggle.
Terceira fase do plano. O módulo passa a gravar as lives e os eventos
do canal, e decide quando um evento vira alerta na overlay.

- Tabelas `stream_sessions` e `stream_events`. Os dois são fatos
  imutáveis: o evento não tem `updated_at`, e a identidade tem só uma
  sessão aberta por vez, garantida por índice parcial.
- O `details` do evento é um VO por tipo (sub, gift sub, bits e raid),
  com o cast lendo o `type` da própria linha. Follow não tem detalhes.
- `StartStreamSession`, `UpdateStreamSession` e `EndStreamSession`. Um
  online repetido não duplica a sessão, e um online novo fecha a
  sessão que perdeu o offline.
- `RecordStreamEvent` grava cada evento uma vez por origem e só alerta
  quando a fonte e o tipo de alerta estão ligados. Streamer desativado
  e canal sem fonte não gravam nada.
- Broadcasts `alert.triggered`, `session.started` e `session.ended` no
  canal privado do streamer. Os de sessão respeitam o toggle da fonte.
- O alerta de teste da Minha Live dispara o broadcast marcado como
  teste, sem gravar evento.
Quarta fase do plano. O chat das lives entra em `activity.messages`,
do mesmo jeito que o Discord, sem criar conta para quem só assiste.

- `PersistMessage` passa a ser idempotente pelo `provider_message_id`
  e aceita metadata. Um retry da fila não quebra mais no índice único.
- `RecordChatMessage` liga a mensagem à identidade do membro, quando
  ele tem a Twitch conectada. Se não tem, cria uma identidade solta,
  sem `User` e com XP 0. Várias mensagens do mesmo espectador usam a
  mesma identidade.
- `ChatMessageMetadata` guarda nome, cor, badges e fragmentos de texto
  ou emote. O `streaming` só lê e grava a metadata por esse VO.
- `DeleteChatMessage` marca a mensagem apagada pela moderação e mantém
  a linha no banco.
- Broadcasts `chat.message` e `chat.message-deleted` no canal do
  streamer, só quando a fonte está ligada e tem leitor de chat.
… bot

Quinta fase do plano. Os eventos da Twitch saem do lake e viram sessão,
alerta e chat no `streaming`.

- `ProjectTwitchEventToStreaming` lê cada `TwitchEventLog` e chama a
  Action certa do `streaming`. O `stream.online` busca título e
  categoria na Helix só quando o canal tem fonte ativa. Se a Helix
  falhar, a sessão abre sem título.
- O gift de N subs grava um evento só. Os subs de presente que chegam
  depois são descartados.
- `SyncStreamerTwitchSubscriptions` mantém as inscrições EventSub de
  cada fonte: 9 de alerta, mais 2 de chat quando a fonte tem leitor.
  As inscrições ficam marcadas com a fonte, então desligar o streamer
  remove só as dele. As do canal da comunidade continuam.
- A sincronização roda quando a conexão, o streamer ou a fonte mudam,
  e só com o segredo do EventSub configurado.
- `TwitchBotTokenService` renova o token da conta bot e guarda o
  refresh token novo que a Twitch devolve.
- O streamer escolhe as features no OAuth (alertas, chat pela própria
  conta, chat pela conta bot) e a Twitch pede só os escopos delas. A
  reautorização mantém os escopos já concedidos. Na primeira conexão,
  o leitor do chat sai dos escopos concedidos.
- `services.twitch.scopes.streamer` sai da config. Sem escolha, o
  streamer recebe os escopos de alerta, que eram o mesmo conjunto.
Sexta fase do plano. A overlay do OBS deixa o canal fixo de exemplo e
passa a abrir com os dados do streamer dono do token.

- Reverb como driver de broadcast. A config foi publicada sozinha, e o
  `.env.example` traz as variáveis do ambiente local. `laravel-echo` e
  `pusher-js` entram com versão fixa para o front da overlay.
- A página da overlay resolve o token pelo `streaming` e responde 404
  para token desconhecido ou streamer desativado. As props trazem o
  canal privado, as configurações da cena, as 30 últimas mensagens do
  chat e a live aberta.
- O estado inicial usa o mesmo formato dos broadcasts. Recarregar a
  fonte no OBS mantém o chat na tela e não repete alertas antigos.
- `POST /overlay/{token}/broadcasting/auth` assina o canal privado sem
  sessão de usuário. Só libera o canal do próprio streamer, recusa o
  token antigo depois de regenerado e tem limite de 30 pedidos por
  minuto.
- O enum de cena do `panel-overlays` sai. O controller usa o
  `OverlayScene` do `streaming`.

O adaptador do `useOverlayFeed` (5.4) fica com o front da overlay. O
plano traz a tabela dos eventos e das props iniciais.
Sétima fase do plano. A Minha Live deixa os dados de exemplo e passa a
ler e gravar no `streaming`.

- O Painel mostra follows, subs (com os gifts), bits e raids dos
  últimos 30 dias, e os 10 eventos mais novos com um resumo de cada.
  Sem eventos, a atividade mostra um estado vazio.
- "Conectar Twitch" abre um modal com alertas e chat. A opção de chat
  pela he4rtdevs só aparece com a conta bot configurada.
- A lista de fontes liga e desliga cada canal e troca quem lê o chat.
  Se os escopos já cobrem o leitor novo, a troca vale na hora. Se não,
  o streamer vai para a Twitch pedir só o que falta.
- Overlays usa o token real do streamer. Gerar novos links invalida os
  antigos na hora.
- "A live vai começar" e "Sala de voz" ganham configuração, e a
  overlay aberta no OBS recebe a mudança pelo `settings.updated`. Os
  alertas ganham um toggle por tipo.
- `StreamingPreviewData` e o aviso de prévia saem.
Oitava e última fase do plano. Os documentos passam a descrever o que
existe, não o que estava previsto.

- README do `streaming`, com o caminho de um evento até a overlay e o
  passo a passo local: Reverb, streamer, fonte, twitch-cli e conta bot.
  Ele também lista os limites atuais, como o adaptador do
  `useOverlayFeed`, que ainda é do front.
- CONTEXT do `streaming` com a estrutura real e o status implementado.
- CONTEXT do `integration-twitch` com o ETL, as inscrições por fonte, a
  conta bot e as features do streamer. O aviso antigo sobre o raid sai,
  porque o controller já grava o canal de destino.
- A ADR-0001 ganha uma nota com os fatos que mudaram depois da decisão.
  A tabela original fica como estava.
A overlay assina o canal privado do streamer pelo Echo e traduz os
eventos do Laravel para os DTOs do feed.ts: alertas, chat, mensagem
apagada e configurações da cena chegam sem recarregar o OBS.

O chat recente e as configurações da cena entram como estado inicial,
então a cena já abre com o título e o horário do painel. A barra do
topo mostra o usuário do streamer, e não mais o nome do canal privado.
Com ?demo na URL, a overlay segue usando o feed de exemplo.
A EventSub manda só o set_id e a versão de cada badge, sem imagem. A
mensagem era gravada com a URL nula, e a overlay montava um <img> sem
src para cada badge.

O ETL agora busca a imagem no catálogo da Helix: os badges globais e os
do canal, que trocam o global (o badge de sub é de cada canal). O
catálogo fica em cache por 6 horas e só é consultado quando a mensagem
tem badge e o canal tem fonte ativa. Se a Helix falhar, a mensagem
entra com o badge sem imagem, e a overlay não mostra esse badge.
…fila

Um evento da Twitch passava por dois saltos de fila antes da overlay: o
ETL e o broadcast. Com o worker do Redis dormindo até 3 segundos quando a
fila está vazia, o alerta podia chegar 6 segundos depois.

Agora o ETL roda dentro do request do webhook, e os broadcasts saem na
hora (ShouldBroadcastNow). Os eventos de uma live são poucos, então o
request continua curto.

Uma falha no ETL é reportada e a Twitch recebe 204 mesmo assim, porque o
evento já está no lake. Um 500 faria a Twitch reenviar, o reenvio cai no
dedupe, e falhas seguidas revogam a inscrição. Os broadcasts usam
ShouldRescue: com o Reverb fora do ar, o webhook e o painel seguem
funcionando.
…rlay

Só a mensagem apagada chegava na overlay. Um ban, um timeout ou um
/clear não chegavam, e o spam de quem foi banido ficava na tela.

A fonte com leitor de chat agora assina channel.chat.clear_user_messages
e channel.chat.clear. O ban marca as mensagens do chatter nas últimas 24
horas e tira as linhas dele da overlay. O /clear grava
streamers.chat_cleared_at e esvazia o chat; ao recarregar, a overlay não
traz de volta o que foi limpo.

O chat.message agora leva o chatterId, para a overlay saber de quem é
cada linha. As fontes que já existem ganham as inscrições novas com
php artisan twitch:sync-streamer-subscriptions.

Também entra o plano das próximas fases da Minha Live e do chat
transparente.
Uma cena nova, só com o chat da live, para pôr por cima de qualquer
cena do OBS. São seis estilos: Linhas (o padrão), Balões, Painel,
Destaque, Terminal e Vidro.

Pelo painel, o streamer escolhe o estilo, o tempo para cada linha sumir
(0 = nunca), o tamanho da fonte, a largura da coluna, a direção e o
alinhamento. A overlay aberta no OBS muda na hora pelo
settings.updated.

Três comportamentos ficam fixos, sem ajuste: a linha que não cabe na
fonte sai inteira, nunca cortada no topo; o nome com cor escura demais
é clareado até ficar legível; e quem não tem cor ganha uma cor fixa a
partir do login.

O Vidro não desfoca a cena, porque a fonte de navegador do OBS não vê o
que está atrás dela. Ele usa uma faixa clara translúcida com sombra
forte no texto.
A página Minha Live › Chat mostra as últimas 50 mensagens e deixa o
streamer agir só na overlay, sem mexer na Twitch:

- ocultar uma mensagem (metadata.hidden_at, separado do deleted_at da
  moderação);
- silenciar um chatter (streamers.settings.muted_chatters) e tirar o
  silêncio;
- limpar o chat da overlay, com o mesmo ClearChat do /clear.

O StreamerChatMessages vira a consulta única do chat do streamer: a
página, o recentChat da overlay e as Actions usam a mesma regra, e uma
mensagem de outro canal responde 404. A lista atualiza a cada 3 s e
pausa com o mouse em cima.
O Painel ganha uma seção com uma verificação por item: conta da Twitch,
endereço do webhook, inscrições, último evento e overlays conectadas.
Cada uma diz a causa e, quando dá, traz o conserto ao lado.

- Reparar inscrições confere o status real pelo ListSubscriptions,
  apaga o que a Twitch não tem mais, adota as do nosso callback e só
  então sincroniza. A sincronização passa a recriar as inscrições com
  falha e as pendentes há mais de 10 minutos.
- O token do streamer vence em poucas horas e nada o renovava. A
  checagem da conta tenta renovar no 401 antes de pedir reconexão.
- A seção carrega depois da página. O selo vermelho no menu usa só as
  verificações que leem o banco, sem chamar a Twitch a cada página.
…rlays

Cada card mostra a própria cena num iframe com ?demo, em escala. A
prévia nunca vai ao ar, porque o modo demo não assina o canal. O Chat
renderiza em 960 × 540 para a coluna ficar legível no card, e as cenas
transparentes trocam o fundo entre escuro, claro e xadrez.

Salvar uma configuração muda o v da URL da prévia, e o iframe carrega
de novo com o estilo novo.

O topo da página diz quantas overlays estão abertas agora, com a mesma
contagem do Reverb da saúde da integração. O número atualiza a cada
5 s sem renderizar a página de novo.
…o detalhe

Durante a live, o Painel mostra um card com as somas da sessão aberta: follows, subs, bits,
raids, mensagens, ritmo do chat nos últimos 5 min, chatters e overlays abertas. O alerta de
teste pede confirmação só quando há live, e cada item da atividade recente tem "Repetir
alerta", que manda o evento gravado de novo para a overlay sem gravar outro.

A página Lives lista as sessões da mais nova para a mais antiga, com as somas de cada uma.
Uma sessão sem evento e sem mensagem mostra "—", porque o mais provável é a Twitch não ter
entregado os eventos. O detalhe traz a linha do tempo, quem mais falou e a comparação com a
última live com dados, para que uma live que caiu não infle as setas.
…guração

O Painel empilhava seis blocos com o mesmo peso. A saúde ocupava cinco linhas verdes quando
estava tudo certo, a conta aparecia de novo em Fontes, e os números de 30 dias ficavam
zerados fora da live. Agora a coluna larga mostra a live: o card "Ao vivo" durante a
transmissão, ou a última live com as setas em relação à anterior com dados. A atividade vem
logo abaixo. A coluna estreita junta a conta, a integração e as outras fontes. Um item ok
da integração mostra só o título, e um item com problema mostra o detalhe e o botão de
reparo. No celular, a conta e a integração vêm antes da atividade.

O alerta de teste foi para a página de Overlays, num menu ao lado do contador de overlays
abertas. O tempo relativo agora sai em pt-BR.
@danielhe4rt
danielhe4rt requested a review from a team October 4, 2026 23:00
Traz da 4.x os avatares pelas contas vinculadas e os endpoints mobile
da timeline.

- ExternalIdentity e IdentityProvider ficam com os dois lados: o
  avatarUrl() da 4.x e o missingScopes() dos escopos da Twitch.
- composer.lock parte do lock da 4.x e só acrescenta os pacotes desta
  branch (inertia, reverb, streaming e panel-overlays).
- Shards regerados com a suíte completa depois do merge.
@danielhe4rt
danielhe4rt merged commit e3114c9 into 4.x Oct 5, 2026
8 checks passed
@danielhe4rt
danielhe4rt deleted the feat/streaming-tooling branch October 5, 2026 00:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants