Serving HTTP via Plugins
Plugins können ihre eigenen URLs bereitstellen. Sobald Sie http.serve in Ihrem Manifest deklarieren, gehört der URL-Bereich unter /plugins/\<your-slug>/ Ihnen: Statische Dateien aus Ihrem public/-Verzeichnis werden genau so ausgegeben, und alles andere fällt auf Ihren Anfragehandler zurück.
Der Code wird für beide SDKs angezeigt. Siehe JavaScript oder Python für Installation und Einrichtung.
Routing
Sobald http.serve deklariert ist, leitet der Host jede Anfrage unter /plugins/\<your-slug>/ an Ihr Plugin weiter:
- Statische Dateien. Alles im Verzeichnis
public/wird genau so bereitgestellt. - Dynamischer Handler. Alles andere fällt auf den Anfragehandler Ihres Plugins zurück.
Der Pfad einer Anfrage ist relativ zum Namensraum Ihres Plugins: eine Anfrage an /plugins/my-plugin/api/messages erreicht Ihren Handler als /api/messages (der Abfrageparameter ist ausgeschlossen). Der Handler liest Abfrageparameter und den Anfragekörper aus der Anfrage und gibt eine Antwort mit einem Status, optionalen Headern und einem optionalen Körper zurück.
Es gibt zwei Routing-Stile. In JavaScript schreiben Sie einen einzelnen onHttpRequest(req)-Handler und verzweigen nach req.method / req.path. In Python deklarieren Sie methode-spezifische Routen mit Dekoratoren. Eine Anfrage, deren Pfad mit einer Route übereinstimmt, jedoch nicht mit ihrer Methode, erhält automatisch eine 405, und ein nicht übereinstimmender Pfad fällt auf den allgemeinen Fangmechanismus zurück, andernfalls 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}
Routen sind genau und plugin-relativ. Lesen Sie Abfrageparameter von req.query. Ein Handler gibt ein dict ({status, body, headers}), einen str (→ 200) oder None (→ 204) zurück. @plugin.route(path, methods=[...]) deckt mehrere Methoden auf einem Pfad ab.
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.
Statische Dateien
Das Verzeichnis public/ enthält Dateien, die unter /plugins/\<your-slug>/\<path> bereitgestellt werden. Ein separates Verzeichnis assets/ enthält Dateien, die der Host intern für Manifestfelder liest, die inline Inhalte haben (styles, scripts, extraPageContent). Diese sind nicht über den URL-Bereich des Plugins erreichbar.
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
Eine Anfrage an /plugins/my-plugin/ (kein nachfolgender Pfad) stellt automatisch public/index.html bereit.
Anfrage- und Antwortgrenzen
- Anfragekörper sind auf 1 MB begrenzt.
- Antwortkörper sind auf 10 MB begrenzt.
- Pfad traversal (
..) in URLs wird auf Host-Ebene blockiert. Sie werden es niemals im Pfad Ihres Handlers sehen. - Antwortheader werden durch eine Erlaubenliste gefiltert. Sie können
Content-Type,Content-Encoding,Content-Language,Cache-Control,Set-Cookie,Location,ETag,Last-Modified,Vary,Linkund CORS (Access-Control-*) Header setzen. Von Owncast verwaltete Header (Server,Content-Security-Policy,Strict-Transport-Security,X-Frame-Options) sind blockiert. - Cookies, die Sie setzen, gelten standardmäßig für den URL-Bereich Ihres Plugins (
/plugins/\<your-slug>/). Wenn Sie möchten, dass ein Cookie bei Anfragen außerhalb dieses Pfades gesendet wird, setzen SiePath=...ausdrücklich. Andernfalls wird der Browser es auf Ihren Namensraum beschränken und nicht in andere Plugins oder in die eigenen Pfade von Owncast durchsickern lassen. - Jede Anfrage wird auf 5 Sekunden zeitlich begrenzt, bevor der Host einen
504zurückgibt und Ihre Antwort verwirft.
Öffentlich vs. authentifiziert
Endpunkte sind standardmäßig öffentlich. 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).
Für Anfragen, die von einem Chatbenutzer mit einem gültigen Benutzertoken ausgeführt werden, trägt die Anfrage die Identität des Benutzers (id, Anzeigename und scopes). Nützlich für benutzerbezogene Dashboards oder ausschließlich für Moderatoren zugängliche Tools:
- 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.
Echtzeit-Updates (Server-Sent Events)
Um Live-Updates an einen Browser zu pushen (ein Overlay, das auf Chat reagiert, ein Dashboard, das Zuschauerzahlen aktualisiert, ein Warn-Widget), deklarieren Sie http.sse und verwenden Sie owncast.sse.send.
Sie öffnen oder halten die Verbindung selbst nicht. Ihr Anfragehandler kann nicht streamen: jeder Aufruf ist eine einzelne gepufferte Anfrage/Aantwort. Der Host besitzt die langfristige Verbindung und bietet einen fertigen Endpunkt unter /plugins/\<your-slug>/_sse/\<channel> an. Ihr Plugin pusht. Der Host verteilt jede Nachricht an jeden verbundenen Browser.
Plugin-Seite
Pushen Sie von jedem Handler, zum Beispiel von Ihrem Chat-Handler, indem Sie owncast.sse.send(channel, event, data) aufrufen:
- 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: zu welchem Stream gepusht werden soll. Browser abonnieren nach Channel, sodass Sie mehrere unabhängige Streams ("overlay","admin-stats") von einem Plugin ausführen können. Verwenden Sie""für einen einzelnen Standardchannel.event: der Ereignisname, auf den der Browser hört (addEventListener("chat", ...)). Geben Sie""für das Standard-message-Ereignis des Browsers an.data: die Last. Zeichenfolgen werden so gesendet, wie sie sind. Alles andere wird für Sie in JSON kodiert.
Sendungen sind fire-and-forget. Der Aufruf gibt sofort zurück und blockiert nie, auch wenn niemand verbunden oder ein Client langsam ist. Langsame Clients fallen Frames ab, anstatt Ihr Plugin zu blockieren. Es gibt auch Lebenszyklusereignisse für die SSE-Verbindung (das Öffnen und Schließen des Streams eines Zuschauers), auf die Sie abonnieren können: siehe die Handler-Referenz.
Browser-Seite
Standard-EventSource API auf der Zuschauerseite. Keine Bibliothek. Das läuft im Browser, daher ist es immer JavaScript, unabhängig davon, in welcher Sprache Ihr Plugin geschrieben ist:
<!-- 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>
Hinweise
- Bis zu 64 gleichzeitige Verbindungen pro Plugin. Darüber hinaus gibt der Endpunkt
503zurück.EventSourcestellt automatisch die Verbindung wieder her. - If the channel matches a key in
admin.pages, it's auth-gated like any admin route. Praktisch für einen Stream von Statistiken, der nur für Admins zugänglich ist. - Der Endpunkt wird von dem Host verwaltet. Ihr Anfragehandler sieht niemals
/_sse/...-Anfragen, und Sie können dort keine eigene Route bereitstellen.
Zusammenfügen: ein vollständiges Overlay-Plugin
Das Manifest deklariert die beiden Berechtigungen, die das Overlay benötigt:
{
"api": "1",
"name": "Chat Overlay",
"slug": "overlay",
"version": "0.1.0",
"permissions": ["http.serve", "http.sse"]
}
Das Plugin abonniert Chatnachrichten und pusht jede einzelne an den overlay-SSE-Channel:
- 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,
})
Die Zuschauerseite ist der gleiche EventSource-Snippet, der oben gezeigt wurde, auf den relativen Endpunkt ./_sse/overlay gerichtet:
<!-- 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>
Erstellen, paketieren, installieren. Öffnen Sie /plugins/overlay/ in OBS als Browsersource.
Improve this page
See something missing or incorrect? Edit the English version of this page or help improve translations.
Gabe Kangas