跳至主要内容

Serving HTTP via Plugins

插件可以提供自己的 URL。 一旦您在清单中声明了 http.serve/plugins/\<your-slug>/ 下的 URL 空间就是您的:public/ 目录中的静态文件会原样输出,其他内容将交由您的请求处理程序处理。

代码是在两个 SDK 中显示的。 请参见 JavaScriptPython 以获取安装和设置说明。

路由

一旦声明了 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/ 目录保存主机内部用于清单字段的文件,这些字段包含内联内容(stylesscriptsextraPageContent)。 这些文件无法通过插件的 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 MB。
  • 响应体限制为 10 MB。
  • 路径遍历(..)在 URL 中在主机级别被阻止。 您永远不会在处理程序的路径中看到它。
  • 响应头通过允许列表过滤。 您可以设置 Content-TypeContent-EncodingContent-LanguageCache-ControlSet-CookieLocationETagLast-ModifiedVaryLink 和 CORS(Access-Control-*)头。 Owncast 拥有的头(ServerContent-Security-PolicyStrict-Transport-SecurityX-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 连接生命周期事件(查看者的流打开和关闭)您可以订阅:请参见 处理程序参考

浏览器端

在查看页面上使用标准的 EventSource API。 无需库。 这在浏览器中运行,因此不论您的插件用何种语言编写,始终使用 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 个。 超过则端点返回 503EventSource 会自动重连。
  • 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>

构建、打包、安装。 在 OBS 中以浏览器源打开 /plugins/overlay/


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