メインコンテンツへスキップ

Serving HTTP via Plugins

プラグインは独自のURLを提供できます。 http.serveをマニフェストで宣言すると、/plugins/\<your-slug>/のURLスペースはあなたのものになります:public/ディレクトリの静的ファイルはそのまま配信され、それ以外のものはリクエストハンドラに渡されます。

コードは両方のSDK用に示されています。 JavaScriptまたはPythonを参照して、インストールと設定を行います。

ルーティング

http.serveが宣言されると、ホストはすべてのリクエストを/plugins/\<your-slug>/の下にルーティングします:

  1. 静的ファイル。 public/ディレクトリ内のすべてがそのまま配信されます。
  2. 動的ハンドラ。 それ以外はプラグインのリクエストハンドラに渡されます。

リクエストのパスはプラグインの名前空間に対して相対です:/plugins/my-plugin/api/messagesへのリクエストは、あなたのハンドラには/api/messagesとして到達します(クエリ文字列は除外されます)。 ハンドラはリクエストからクエリパラメータとリクエストボディを読み込み、ステータス、オプショナルヘッダー、およびオプショナルボディを持つレスポンスを返します。

ルーティングスタイルは2種類あります。 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を提供します。

リクエストとレスポンスの制限

  • リクエストボディは1MBに制限されています。
  • レスポンスボディは10MBに制限されています。
  • URL内のパスのトラバーサル(..)はホストレベルでブロックされています。 あなたのハンドラのパスでそれを見ることはありません。
  • レスポンスヘッダーは許可リストを通過します。 Content-TypeContent-EncodingContent-LanguageCache-ControlSet-CookieLocationETagLast-ModifiedVaryLink、およびCORS(Access-Control-*)ヘッダーを設定できます。 Owncast所有のヘッダー(ServerContent-Security-PolicyStrict-Transport-SecurityX-Frame-Options)はブロックされます。
  • 設定したクッキーはデフォルトでプラグインのURLスペース(/plugins/\<your-slug>/)に適用されます。 もし、クッキーをそのパス外のリクエストで送信したい場合は、Path=...を明示的に設定してください。 そうでなければ、ブラウザはそれをあなたの名前空間にスコープ設定し、他のプラグインやOwncastの独自のパスに漏れることはありません。
  • 各リクエストは、ホストが504を返し、レスポンスを破棄する前に5秒間に制限されています。

パブリックと認証済み

エンドポイントはデフォルトでパブリックです。 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:どのストリームにプッシュするか。 ブラウザはチャンネルごとに購読するため、1つのプラグインから複数の独立したストリーム("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の同時接続。 それ以上の場合、エンドポイントは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,
});
},
});

視聴者ページは、相対的な./_sse/overlayエンドポイントを指す同じEventSourceスニペットです:

<!-- 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