Serving HTTP via Plugins
Плагины могут обслуживать свои собственные URL-адреса. Как только вы объявите http.serve в вашем манифесте, пространство URL на /plugins/\<your-slug>/ будет вашим: статические файлы из вашего каталога public/ передаются без изменений, а все остальное проходит через ваш обработчик запросов.
Код показан для обоих SDK. Смотрите JavaScript или Python для установки и настройки.
Маршрутизация
Как только http.serve объявлен, хост направляет каждый запрос под /plugins/\<your-slug>/ к вашему плагину:
- Статические файлы. Всё в вашем каталоге
public/обслуживается без изменений. - Динамический обработчик. Всё остальное передаётся вашему обработчику запросов плагина.
Путь запроса относительно пространства имен вашего плагина: запрос к /plugins/my-plugin/api/messages достигает вашего обработчика как /api/messages (строка запроса не включена). Обработчик читает параметры запроса и тело запроса из запроса и возвращает ответ со статусом, необязательными заголовками и необязательным телом.
Существуют два стиля маршрутизации. В JavaScript вы пишете один onHttpRequest(req) обработчик и ветвите по req.method / req.path. В Python вы объявляете маршруты для каждого метода с помощью декораторов. Запрос, путь которого соответствует маршруту, но не его методу, получает автоматический 405, а неподходящий путь передаётся в универсальный обработчик, иначе 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}
Маршруты точные и относительны к плагину. Чтите параметры запроса из req.query. Обработчик возвращает dict ({status, body, headers}), str (→ 200) или None (→ 204). @plugin.route(path, methods=[...]) охватывает несколько методов на одном пути.
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.
Статические файлы
Каталог public/ содержит файлы, обслуживаемые по адресу /plugins/\<your-slug>/\<path>. Отдельный каталог assets/ содержит файлы, которые хост читает внутренне для полей манифеста, содержащих встроенный контент (styles, scripts, extraPageContent). Они недоступны через пространство URL плагина.
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
Запрос к /plugins/my-plugin/ (без завершающего пути) автоматически обслуживает public/index.html.
Ограничения запросов и ответов
- Тела запросов ограничены до 1 МБ.
- Тела ответов ограничены до 10 МБ.
- Путевое пересечение (
..) в URL заблокировано на уровне хоста. Вы никогда не увидите это в пути вашего обработчика. - Заголовки ответов фильтруются через список разрешенных. Вы можете установить заголовки
Content-Type,Content-Encoding,Content-Language,Cache-Control,Set-Cookie,Location,ETag,Last-Modified,Vary,Linkи CORS (Access-Control-*). Заголовки, принадлежащие Owncast (Server,Content-Security-Policy,Strict-Transport-Security,X-Frame-Options), заблокированы. - Файлы cookie, которые вы устанавливаете, применяются к пространству URL вашего плагина по умолчанию (
/plugins/\<your-slug>/). Если вы хотите, чтобы cookie отправлялось с запросами за пределами этого пути, явным образом установитеPath=.... В противном случае браузер ограничивает его вашим пространством имен и не пропустит его в другие плагины или в собственные пути Owncast. - Каждый запрос ограничен по времени до 5 секунд, прежде чем хост вернёт
504и отменит ваш ответ.
Публичный против аутентифицированного
Конечные точки по умолчанию публичные. 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).
Для запросов, сделанных пользователем чата с действительным токеном пользователя, запрос несёт идентичность пользователя (id, отображаемое имя и scopes). Полезно для панелей управления для пользователей или инструментов только для модераторов:
- 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.
Обновления в реальном времени (События, отправленные сервером)
Чтобы передавать живые обновления в браузер (наложение, которое реагирует на чат, панель, показывающая количество зрителей, виджет оповещения), объявите http.sse и используйте owncast.sse.send.
Вы сами не открываете или не удерживаете соединение. Ваш обработчик запросов не может вести поток: каждый вызов — это единый буферизованный запрос/ответ. Хост владеет долговременным соединением и предоставляет готовую конечную точку по адресу /plugins/\<your-slug>/_sse/\<channel>. Ваш плагин передаёт. Хост рассылает каждое сообщение всем подключённым браузерам.
Сторона плагина
Отправляйте из любого обработчика, например, из вашего обработчика чата, вызывая 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: какой поток передавать. Браузеры подписываются на каждый канал, поэтому вы можете запустить несколько независимых потоков ("overlay","admin-stats") из одного плагина. Используйте""для единственного канала по умолчанию.event: имя события, на которое браузер подписывается (addEventListener("chat", ...)). Передайте""для значения по умолчаниюmessageдля браузера.data: полезная нагрузка. Строки отправляются как есть. Всё остальное кодируется в JSON для вас.
Отправки выполняются без ожидания. Вызов возвращает немедленно и никогда не блокируется, даже если никто не подключен или клиент медленный. Медленные клиенты сбрасывают кадры вместо того, чтобы блокировать ваш плагин. Также есть события жизненного цикла SSE-соединения (открытие и закрытие потока зрителя), на которые вы можете подписаться: см. справочник обработчиков.
Сторона браузера
Стандартный API EventSource на странице зрителя. Нет библиотек. Это работает в браузере, поэтому это всегда JavaScript, независимо от языка, на котором написан ваш плагин:
<!-- 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>
Заметки
- До 64 одновременных подключений на плагин. Сверх этого конечная точка возвращает
503.EventSourceподключается автоматически. - If the channel matches a key in
admin.pages, it's auth-gated like any admin route. Удобно для канала статистики только для администраторов. - Конечная точка принадлежит хосту. Ваш обработчик запросов никогда не увидит запросы
/_sse/..., и вы не можете обслуживать собственный маршрут там.
Собирая вместе: полный плагин наложения
Манифест объявляет два разрешения, необходимых для наложения:
{
"api": "1",
"name": "Chat Overlay",
"slug": "overlay",
"version": "0.1.0",
"permissions": ["http.serve", "http.sse"]
}
Плагин подписывается на сообщения чата и отправляет каждое из них в канал overlay SSE:
- 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,
})
Страница зрителя это тот же фрагмент EventSource, показанный выше, направленный на относительную конечную точку ./_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>
Соберите, упакуйте, установите. Откройте /plugins/overlay/ в OBS как источник браузера.
Improve this page
See something missing or incorrect? Edit this page and improve the documentation for everyone.
Gabe Kangas