A plugin that contributes viewer action buttons through its manifest, and ships an admin page that lets the streamer add an extra button at runtime. The host's effective list for each plugin is manifest.actions ++ whatever the plugin has added at runtime via owncast.actions.add(...). Both are merged into the viewer's externalActions array on /api/config, so plugin-contributed buttons appear next to admin-defined ones with no extra wiring.
Demonstrates: the actions manifest field, the URL-vs-HTML variants, the openExternally flag, the color styling hook, the ui.modify permission, the runtime owncast.actions.add / .clear API, an admin page (manifest.admin.pages), and a custom HTTP API served via @plugin.get / @plugin.post.
Action buttons place UI inside Owncast's own viewer chrome, so the manifest must declare "ui.modify" in its permissions array. The host rejects a manifest at load time if actions is set without ui.modify, and the runtime owncast.actions.add / .clear calls raise the same permission error if it isn't granted.
- The plugin declares any always-on buttons under
manifest.actions[]. - On load (or reload), the host parses the manifest and validates each entry: title is required, exactly one of
urlorhtmlmust be present, relative URLs are rewritten into this plugin's namespace, cross-plugin URLs are rejected. - At runtime,
owncast.actions.add(buttons)appends to the plugin's effective list. The host runs the same validation on each entry and persists the result in the plugin's config. The call raises a descriptive error when validation or persistence fails, and rejects the entire batch. owncast.actions.clear()drops the runtime additions. Only the manifest's defaults remain.- On every viewer
/api/configrequest, the host returnsmanifest.actions++ the runtime list, projected into Owncast's existingExternalActionshape.
This plugin's manifest also declares admin.pages and asks for http.serve + storage.kv. src/plugin.py handles two endpoints:
GET /admin/api/custom-buttonreturns the streamer's saved title + url from plugin config (or empty strings if none).POST /admin/api/custom-buttonaccepts{ title, url }, persists the value withowncast.kv.set, then publishes it to the host viaowncast.actions.clear()followed byowncast.actions.add({ title, url, ... }).
The admin form (public/admin/index.html) is auto-themed by the host's plugin-iframe stylesheet, so plain <input> and <button> controls look like the surrounding Owncast admin without any plugin-side CSS.
{ "actions": [ { "title": "Owncast", // required "description": "...", // optional, shown in the modal "url": "https://owncast.online", // exactly one of url/html "openExternally": true, // optional: new tab vs in-modal "color": "#24292e", // optional: button bg color "icon": "/star.png" // optional: image URL; a relative path // ("/star.png") resolves to this // plugin's static assets, and any // "https://..." URL is left alone }, { "title": "About this stream", "html": "<div>...</div>" // inline HTML modal body } ] }