Ir para o conteúdo principal

Serving HTTP via Plugins

Plugins podem servir suas próprias URLs. Uma vez que você declare http.serve em seu manifesto, o espaço de URL em /plugins/\<seu-slug>/ é seu: arquivos estáticos do seu diretório public/ são servidos literalmente, e tudo o mais passa pelo seu manipulador de requisições.

O código é mostrado para ambos os SDKs. Veja JavaScript ou Python para instalação e configuração.

Roteamento

Uma vez que http.serve é declarado, o host roteia cada requisição sob /plugins/\<seu-slug>/ para seu plugin:

  1. Arquivos estáticos. Qualquer coisa no seu diretório public/ é servida verbatim.
  2. Manipulador dinâmico. Qualquer outra coisa passa pelo manipulador de requisições do seu plugin.

O caminho de uma requisição é relativo ao espaço de nomes do seu plugin: uma requisição para /plugins/meu-plugin/api/messages chega ao seu manipulador como /api/messages (a string de consulta é excluída). O manipulador lê parâmetros de consulta e o corpo da requisição e retorna uma resposta com um status, headers opcionais e um corpo opcional.

Existem dois estilos de roteamento. Em JavaScript você escreve um único manipulador onHttpRequest(req) e ramifica em req.method / req.path. Em Python você declara rotas por método com decorators. Uma requisição cujo caminho corresponde a uma rota mas não ao seu método recebe um 405 automático, e um caminho não correspondente passa pelo catch-all puro; caso contrário, 404.

const { definePlugin } = require('@owncast/plugin-sdk');

module.exports = definePlugin({
onHttpRequest(req) {
// req: { method, path, headers, query, body, user? }
if (req.method === 'GET' && req.path === '/api/messages') {
return {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: '[]',
};
}
if (req.method === 'POST' && req.path === '/api/messages') {
const data = JSON.parse(req.body || '{}');
return { status: 201 };
}
return { status: 404 };
},
});

The manifest.admin.pages key match at the top is covered in UI: Admin pages. From the perspective of HTTP serving, it is a 401-before-your-handler-runs filter applied to paths matching one of the object's keys.

Arquivos estáticos

O diretório public/ contém arquivos servidos em /plugins/\<seu-slug>/\<caminho>. Um diretório separado assets/ contém arquivos que o host lê internamente para campos do manifesto que in-line conteúdo (styles, scripts, extraPageContent). Esses arquivos não são acessíveis pelo espaço de URL do plugin.

my-plugin/
└── public/
├── index.html → /plugins/my-plugin/index.html (and /plugins/my-plugin/)
├── style.css → /plugins/my-plugin/style.css
└── img/
└── logo.png → /plugins/my-plugin/img/logo.png

Uma requisição para /plugins/meu-plugin/ (sem caminho final) serve public/index.html automaticamente.

Limites de requisição e resposta

  • Os corpos das requisições são limitados a 1 MB.
  • Os corpos das respostas são limitados a 10 MB.
  • A travessia de caminho (..) em URLs é bloqueada no nível do host. Você nunca verá isso no caminho do seu manipulador.
  • Os headers de resposta são filtrados através de uma lista de permitidos. Você pode definir headers Content-Type, Content-Encoding, Content-Language, Cache-Control, Set-Cookie, Location, ETag, Last-Modified, Vary, Link, e headers CORS (Access-Control-*). Headers de propriedade do Owncast (Server, Content-Security-Policy, Strict-Transport-Security, X-Frame-Options) são bloqueados.
  • Cookies que você define aplicam-se ao espaço de URL do seu plugin por padrão (/plugins/\<seu-slug>/). Se você quiser que um cookie seja enviado em requisições fora desse caminho, defina explicitamente Path=.... Caso contrário, o browser o limita ao seu espaço de nomes e não o vaza para outros plugins ou para os próprios caminhos do Owncast.
  • Cada requisição é limitada a 5 segundos antes que o host retorne um 504 e descarte sua resposta.

Público vs. autenticado

Endpoints são públicos por padrão. To make something admin-only, either check whether the request is authenticated inside your handler and return 401 when it isn't, or add its path glob as a key in manifest.admin.pages and let the host gate it for you (see UI: Admin pages).

Para requisições feitas por um usuário de chat com um token de usuário válido, a requisição carrega a identidade do usuário (id, nome de exibição e scopes). Útil para dashboards por usuário ou ferramentas exclusivas para moderadores:

module.exports = definePlugin({
onHttpRequest(req) {
if (!req.user) return { status: 401 }; // not signed in
if (!req.user.scopes?.includes('MODERATOR')) return { status: 403 };
return { status: 200, body: `hello ${req.user.displayName}` };
},
});

For paths matching a key in manifest.admin.pages, the host returns 401 before your handler runs, so you don't have to check at all.

Atualizações em tempo real (Eventos Enviados pelo Servidor)

Para enviar atualizações ao vivo para um navegador (um overlay que reage ao chat, um painel que atualiza contagens de visualizadores, um widget de alerta) declare http.sse e use owncast.sse.send.

Você não abre ou mantém a conexão você mesmo. Seu manipulador de requisições não pode transmitir: cada chamada é uma requisição/resposta bufferizada única. O host possui a conexão de longa duração e expõe um endpoint pronto em /plugins/\<seu-slug>/_sse/\<canal>. Seu plugin envia. O host envia cada mensagem para todos os navegadores conectados.

Lado do plugin

Envie de qualquer manipulador, por exemplo, do seu manipulador de chat, chamando owncast.sse.send(canal, evento, dados):

const { definePlugin, owncast } = require('@owncast/plugin-sdk');

module.exports = definePlugin({
onChatMessage(msg) {
owncast.sse.send('overlay', 'chat', {
from: msg.user?.displayName,
body: msg.body,
});
},
});
  • canal: qual stream enviar. Browsers subscribe per channel, so you can run several independent streams ("overlay", "admin-stats") from one plugin. Use "" para um único canal padrão.
  • evento: o nome do evento que o navegador escuta (addEventListener("chat", ...)). Passe "" para o evento message padrão do navegador.
  • dados: os dados a serem enviados. Strings são enviadas como estão. Qualquer outra coisa é codificada em JSON para você.

Envios são fire-and-forget. A chamada retorna imediatamente e nunca bloqueia, mesmo que ninguém esteja conectado ou que um cliente esteja lento. Clientes lentos descartam quadros em vez de travar seu plugin. Existem também eventos de ciclo de vida de conexão SSE (abrindo e fechando a stream de um visualizador) aos quais você pode se inscrever: veja a referência de manipuladores.

Lado do navegador

API padrão EventSource na página do visualizador. Sem biblioteca. Isso roda no navegador, então é sempre JavaScript, não importa qual linguagem seu plugin seja escrito:

<!-- public/index.html, served at /plugins/my-plugin/ -->
<script>
const events = new EventSource('/plugins/my-plugin/_sse/overlay');
events.addEventListener('chat', e => {
const { from, body } = JSON.parse(e.data);
document.getElementById('feed').textContent = `${from}: ${body}`;
});
</script>

Notas

  • Até 64 conexões simultâneas por plugin. Acima disso, o endpoint retorna 503. EventSource reconecta automaticamente.
  • If the channel matches a key in admin.pages, it's auth-gated like any admin route. Útil para um stream de estatísticas apenas para administradores.
  • O endpoint é de propriedade do host. Seu manipulador de requisições nunca vê requisições /_sse/..., e você não pode servir sua própria rota lá.

Colocando tudo junto: um plugin completo de overlay

O manifesto declara as duas permissões que o overlay precisa:

{
"api": "1",
"name": "Chat Overlay",
"slug": "overlay",
"version": "0.1.0",
"permissions": ["http.serve", "http.sse"]
}

O plugin se inscreve em mensagens de chat e envia cada uma para o canal SSE overlay:

// src/plugin.js
const { definePlugin, owncast } = require('@owncast/plugin-sdk');

module.exports = definePlugin({
onChatMessage(msg) {
owncast.sse.send('overlay', 'chat', {
from: msg.user?.displayName,
body: msg.body,
});
},
});

A página do visualizador é o mesmo snippet EventSource mostrado acima, apontando para o endpoint relativo ./_sse/overlay:

<!-- public/index.html -->
<!doctype html>
<body>
<div id="feed"></div>
<script>
const events = new EventSource('./_sse/overlay');
events.addEventListener('chat', e => {
const { from, body } = JSON.parse(e.data);
document.getElementById('feed').textContent = `${from}: ${body}`;
});
</script>
</body>

Construa, empacote, instale. Abra /plugins/overlay/ no OBS como uma fonte de navegador.


Improve this page

See something missing or incorrect? Edit this page and improve the documentation for everyone.

Contributors to this documentation
Gabe KangasGabe Kangas
G
Gabe Kangas