Serving HTTP via Plugins
I plugin possono servire i propri URL. Una volta dichiarato http.serve nel tuo manifest, lo spazio URL in /plugins/\<your-slug>/ appartiene a te: i file statici dalla tua directory public/ vengono serviti letteralmente, e tutto il resto passa al tuo gestore delle richieste.
Il codice è mostrato per entrambi gli SDK. Vedi JavaScript o Python per installare e configurare.
Instradamento
Una volta dichiarato http.serve, l'host instrada ogni richiesta sotto /plugins/\<your-slug>/ al tuo plugin:
- File statici. Qualsiasi cosa nella tua directory
public/è servita letteralmente. - Gestore dinamico. Qualsiasi altra cosa passa al gestore delle richieste del tuo plugin.
Il percorso di una richiesta è relativo allo spazio dei nomi del tuo plugin: una richiesta a /plugins/my-plugin/api/messages raggiunge il tuo gestore come /api/messages (la stringa di query è esclusa). Il gestore legge i parametri di query e il corpo della richiesta dalla richiesta e restituisce una risposta con uno stato, intestazioni opzionali e un corpo opzionale.
Ci sono due stili di routing. In JavaScript scrivi un singolo gestore onHttpRequest(req) e ti ramifichi su req.method / req.path. In Python dichiari i percorsi per metodo con i decoratori. Una richiesta il cui percorso corrisponde a un percorso ma non al suo metodo ottiene un automatico 405, e un percorso non corrispondente passa all'intercettatore generico, altrimenti 404.
- JavaScript
- Python
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 };
},
});
from owncast_plugin import plugin
@plugin.get("/api/messages")
def list_messages(req):
return {"status": 200, "headers": {"Content-Type": "application/json"}, "body": "[]"}
@plugin.post("/api/messages")
def add_message(req):
body = req.body # raw request body
return {"status": 201}
@plugin.on_http_request # bare: catch-all fallback (any method, any path)
def fallback(req):
return {"status": 404}
Le rotte sono esatte e relative al plugin. Leggi i parametri di query da req.query. Un gestore restituisce un dict ({status, body, headers}), un str (→ 200) o None (→ 204). @plugin.route(path, methods=[...]) copre più metodi su un percorso.
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.
File statici
La directory public/ contiene file serviti a /plugins/\<your-slug>/\<path>. Una directory separata assets/ contiene file che l'host legge internamente per i campi del manifest che contengono contenuto in linea (styles, scripts, extraPageContent). Non possono essere raggiunti attraverso lo spazio URL del 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
Una richiesta a /plugins/my-plugin/ (senza percorso finale) serve automaticamente public/index.html.
Limiti di richiesta e risposta
- I corpi delle richieste sono limitati a 1 MB.
- I corpi delle risposte sono limitati a 10 MB.
- Il percorso di traversamento (
..) negli URL è bloccato a livello dell'host. Non lo vedrai mai nel percorso del tuo gestore. - Le intestazioni di risposta sono filtrate attraverso una lista di autorizzazione. Puoi impostare intestazioni
Content-Type,Content-Encoding,Content-Language,Cache-Control,Set-Cookie,Location,ETag,Last-Modified,Vary,Linke CORS (Access-Control-*). Le intestazioni di proprietà di Owncast (Server,Content-Security-Policy,Strict-Transport-Security,X-Frame-Options) sono bloccate. - I cookie impostati si applicano per impostazione predefinita allo spazio URL del tuo plugin (
/plugins/\<your-slug>/). Se vuoi che un cookie venga inviato con richieste al di fuori di quel percorso, imposta esplicitamentePath=.... Altrimenti, il browser lo limita al tuo spazio dei nomi e non lo farà trapelare in altri plugin o nei percorsi di Owncast. - Ogni richiesta ha un limite di tempo di 5 secondi prima che l'host restituisca un
504e scarti la tua risposta.
Pubblico vs. autenticato
Gli endpoint sono pubblici per impostazione predefinita. 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).
Per le richieste effettuate da un utente chat con un token utente valido, la richiesta porta con sé l'identità dell'utente (id, nome visualizzato e scopes). Utile per dashboard personalizzate per utente o strumenti solo per moderatori:
- JavaScript
- Python
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}` };
},
});
@plugin.get("/my-data")
def my_data(req):
if not req.user: # not signed in
return {"status": 401}
if "MODERATOR" not in (req.user.scopes or []):
return {"status": 403}
return {"status": 200, "body": f"hello {req.user.display_name}"}
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.
Aggiornamenti in tempo reale (Eventi inviati dal server)
Per inviare aggiornamenti dal vivo a un browser (un overlay che reagisce alla chat, un dashboard che aggiorna il conteggio degli spettatori, un widget di avviso) dichiara http.sse e utilizza owncast.sse.send.
Non apri o mantieni tu stesso la connessione. Il tuo gestore delle richieste non può inviare stream: ogni chiamata è una singola richiesta/risposta bufferizzata. L'host possiede la connessione a lungo termine ed espone un endpoint predefinito su /plugins/\<your-slug>/_sse/\<channel>. Il tuo plugin invia. L'host distribuisce ogni messaggio a ogni browser connesso.
Lato plugin
Invia da qualsiasi gestore, ad esempio, dal tuo gestore di chat, chiamando owncast.sse.send(channel, event, data):
- JavaScript
- Python
const { definePlugin, owncast } = require('@owncast/plugin-sdk');
module.exports = definePlugin({
onChatMessage(msg) {
owncast.sse.send('overlay', 'chat', {
from: msg.user?.displayName,
body: msg.body,
});
},
});
from owncast_plugin import plugin, owncast
@plugin.on_chat_message
def push(msg):
owncast.sse.send("overlay", "chat", {
"from": msg.user.display_name if msg.user else None,
"body": msg.body,
})
channel: quale stream inviare. I browser si iscrivono per canale, quindi puoi eseguire diversi stream indipendenti ("overlay","admin-stats") da un plugin. Usa""per un singolo canale predefinito.event: il nome dell'evento al quale il browser si iscrive (addEventListener("chat", ...)). Passa""per l'eventomessagepredefinito del browser.data: il payload. Le stringhe sono inviate così come sono. Qualsiasi altra cosa è codificata in JSON per te.
Gli invii sono di tipo fire-and-forget. La chiamata restituisce immediatamente e non blocca mai, anche se nessuno è connesso o un client è lento. Client lenti rimuovono i frame piuttosto che bloccare il tuo plugin. Ci sono anche eventi del ciclo di vita della connessione SSE (apertura e chiusura dello stream di un visualizzatore) a cui puoi iscriverti: vedi il riferimento ai gestori.
Lato browser
API standard EventSource nella pagina del visualizzatore. Nessuna libreria. Questo viene eseguito nel browser, quindi è sempre JavaScript indipendentemente dalla lingua in cui è scritto il tuo plugin:
<!-- 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>
Note
- Fino a 64 connessioni simultanee per plugin. Oltre, l'endpoint restituisce
503.EventSourcesi riconnette automaticamente. - If the channel matches a key in
admin.pages, it's auth-gated like any admin route. Utile per uno stream di statistiche accessibile solo agli amministratori. - L'endpoint è di proprietà dell'host. Il tuo gestore delle richieste non vede mai le richieste
/_sse/..., e non puoi servire il tuo percorso lì.
Mettere tutto insieme: un plugin overlay completo
Il manifesto dichiara le due autorizzazioni di cui ha bisogno l'overlay:
{
"api": "1",
"name": "Chat Overlay",
"slug": "overlay",
"version": "0.1.0",
"permissions": ["http.serve", "http.sse"]
}
Il plugin si iscrive ai messaggi di chat e invia ognuno al canale SSE overlay:
- JavaScript
- Python
// 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,
});
},
});
# src/plugin.py
from owncast_plugin import plugin, owncast
@plugin.on_chat_message
def push(msg):
owncast.sse.send("overlay", "chat", {
"from": msg.user.display_name if msg.user else None,
"body": msg.body,
})
La pagina del visualizzatore è lo stesso snippet EventSource mostrato sopra, puntato all'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>
Costruisci, impacchetta, installa. Apri /plugins/overlay/ in OBS come sorgente browser.
Improve this page
See something missing or incorrect? Edit this page and improve the documentation for everyone.
Gabe Kangas