Перейти к основному содержимому

Serving HTTP via Plugins

Плагины могут обслуживать свои собственные URL-адреса. Как только вы объявите http.serve в вашем манифесте, пространство URL на /plugins/\<your-slug>/ будет вашим: статические файлы из вашего каталога public/ передаются без изменений, а все остальное проходит через ваш обработчик запросов.

Код показан для обоих SDK. Смотрите JavaScript или Python для установки и настройки.

Маршрутизация

Как только http.serve объявлен, хост направляет каждый запрос под /plugins/\<your-slug>/ к вашему плагину:

  1. Статические файлы. Всё в вашем каталоге public/ обслуживается без изменений.
  2. Динамический обработчик. Всё остальное передаётся вашему обработчику запросов плагина.

Путь запроса относительно пространства имен вашего плагина: запрос к /plugins/my-plugin/api/messages достигает вашего обработчика как /api/messages (строка запроса не включена). Обработчик читает параметры запроса и тело запроса из запроса и возвращает ответ со статусом, необязательными заголовками и необязательным телом.

Существуют два стиля маршрутизации. В JavaScript вы пишете один onHttpRequest(req) обработчик и ветвите по req.method / req.path. В Python вы объявляете маршруты для каждого метода с помощью декораторов. Запрос, путь которого соответствует маршруту, но не его методу, получает автоматический 405, а неподходящий путь передаётся в универсальный обработчик, иначе 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.

Статические файлы

Каталог 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). Полезно для панелей управления для пользователей или инструментов только для модераторов:

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.

Обновления в реальном времени (События, отправленные сервером)

Чтобы передавать живые обновления в браузер (наложение, которое реагирует на чат, панель, показывающая количество зрителей, виджет оповещения), объявите http.sse и используйте owncast.sse.send.

Вы сами не открываете или не удерживаете соединение. Ваш обработчик запросов не может вести поток: каждый вызов — это единый буферизованный запрос/ответ. Хост владеет долговременным соединением и предоставляет готовую конечную точку по адресу /plugins/\<your-slug>/_sse/\<channel>. Ваш плагин передаёт. Хост рассылает каждое сообщение всем подключённым браузерам.

Сторона плагина

Отправляйте из любого обработчика, например, из вашего обработчика чата, вызывая owncast.sse.send(channel, event, data):

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

module.exports = definePlugin({
onChatMessage(msg) {
owncast.sse.send('overlay', 'chat', {
from: msg.user?.displayName,
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:

// 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,
});
},
});

Страница зрителя это тот же фрагмент 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.

Contributors to this documentation
Gabe KangasGabe Kangas
G
Gabe Kangas