Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/PLUGIN_AUTHOR_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,8 @@ Each method requires the matching permission in your manifest:
| `owncast.server.emotes()`, custom chat emotes `[{name, url}]` | `server.read` |
| `owncast.server.federation()`, `{enabled, username, isPrivate}` | `server.read` |
| `owncast.server.tags()`, `[string]` | `server.read` |
| `owncast.videoConfig.read()`, `{latencyLevel, codec, variants}` | `videoconfig.read` |
| `owncast.videoConfig.write({latencyLevel?, codec?, variants?})`, partial update, throws on failure | `videoconfig.write` |
| `owncast.videoConfig.read()`, `{latencyLevel, codec, autoplay, variants}` | `videoconfig.read` |
| `owncast.videoConfig.write({latencyLevel?, codec?, autoplay?, variants?})`, partial update, throws on failure | `videoconfig.write` |
| `owncast.notifications.discord(text)` | `notifications.send` |
| `owncast.notifications.browserPush({title, body, url?})` | `notifications.send` |
| `owncast.notifications.fediverse({type, body, image?, link?})` | `notifications.send` |
Expand Down
27 changes: 24 additions & 3 deletions docs/WIRE_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ plugin should store values above `Number.MAX_SAFE_INTEGER` (2^53 - 1) as TEXT.
### `videoconfig.read`

- `owncast_video_config_read(): PTR`. Input: none. Output: JSON `VideoConfig`
with `latencyLevel`, `codec`, and `variants`.
with `latencyLevel`, `codec`, `autoplay`, and `variants`.

### `videoconfig.write`

Expand Down Expand Up @@ -869,28 +869,49 @@ type FederationInfo = {
isPrivate?: boolean;
};

type AutoplayMode = "off" | "always" | "sound-only";

type VideoCodec =
| "libx264"
| "h264_omx"
| "h264_vaapi"
| "h264_qsv"
| "h264_nvenc"
| "h264_v4l2m2m"
| "h264_videotoolbox";
Comment thread
gabek marked this conversation as resolved.

type StreamVariant = {
width: number;
height: number;
framerate: number;
videoBitrate: number;
audioBitrate: number;
cpuUsageLevel: number;
isPassthrough: boolean;
};

type VideoConfig = {
latencyLevel: number;
codec: string;
autoplay: AutoplayMode;
variants: StreamVariant[];
};

type VideoConfigUpdate = {
latencyLevel?: number;
codec?: string;
codec?: VideoCodec;
autoplay?: AutoplayMode;
variants?: StreamVariant[];
};
```

`VideoConfig.codec` can report a legacy value or an encoder added by a newer
host. `VideoConfigUpdate.codec` accepts the `VideoCodec` values listed above.

`cpuUsageLevel` accepts `0` through `4`, from lowest to highest CPU usage.
Audio settings are not exposed. A variant update preserves the host's existing
audio configuration for that output.
Hardware codecs require the matching encoder in the host's ffmpeg build.

### Fediverse and notifications

```ts
Expand Down
2 changes: 1 addition & 1 deletion examples/js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ One self-contained npm project per directory. Each has its own `README.md` with
| [overlay](./overlay/) | `http.serve`, static files from `public/` + dynamic JSON endpoint. |
| [stream-tracker](./stream-tracker/) | Every typed lifecycle / chat-user handler + read APIs. |
| [stream-ops](./stream-ops/) | Broadcast telemetry (`server.read`) + video config read/write (`videoconfig.read`/`videoconfig.write`). |
| [manual-video-settings](./manual-video-settings/) | Admin form for the video config: latency, codec, and per-variant resolution / framerate / bitrate. |
| [manual-video-settings](./manual-video-settings/) | Admin form for video latency, codec, autoplay, and output variants. |
| [engagement-bot](./engagement-bot/) | Discord + browser-push + fediverse notifier on stream / fediverse events, with a small inline spam filter. |
| [admin-demo](./admin-demo/) | `manifest.admin.pages`, host-gated admin routes. |
| [file-manager](./file-manager/) | `storage.fs`, admin page to browse/upload/download/delete files in the plugin's private sandbox. |
Expand Down
7 changes: 4 additions & 3 deletions examples/js/manual-video-settings/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ A hand-edit form for the Owncast transcoding pipeline. Use it when you want to t
Opening **Manual Video Settings** under the admin sidebar shows a single form with:

- **Latency level**: `0` (lowest) through `4` (highest). Lower means viewers see the stream sooner but rebuffer more on flaky networks.
- **Codec**: the FFmpeg encoder Owncast invokes (`libx264` software, plus the usual hardware variants: `h264_vaapi`, `h264_nvenc`, `h264_qsv`, `h264_omx`, `h264_v4l2m2m`, `h264_videotoolbox`). If your live config uses something not in the dropdown, this plugin shows it as a `(current)` option so saving doesn't silently overwrite it.
- **Output variants**: one row per HLS rendition, with editable **width**, **height**, **FPS**, **video kbps**, and a **passthrough** checkbox. Add or remove rows from the form. Each variant's audio bitrate is preserved across saves (it's part of the host's data model, just not surfaced here).
- **Codec**: the FFmpeg encoder Owncast invokes (`libx264` software, plus `h264_vaapi`, `h264_nvenc`, `h264_qsv`, `h264_omx`, `h264_v4l2m2m`, and `h264_videotoolbox`). Hardware encoders must be available in the host's ffmpeg build.
- **Autoplay**: `off`, `always`, or `sound-only`.
- **Output variants**: one row per HLS rendition, with editable **width**, **height**, **FPS**, **video kbps**, **CPU usage**, and a **passthrough** checkbox. Add or remove rows from the form.

## How to use it

Expand All @@ -17,7 +18,7 @@ Opening **Manual Video Settings** under the admin sidebar shows a single form wi
3. Edit fields, then click **Save**. The status line confirms `Saved.` or shows the host's error message if a setting was rejected.
4. **Reload** discards in-form edits and re-fetches the live config.

Changes apply on the next stream segment Owncast encodes. An active broadcast does not need to be restarted, but viewers may see a brief quality switch.
Changes apply when the next stream starts. The host does not restart an active broadcast.

## Safety notes

Expand Down
2 changes: 1 addition & 1 deletion examples/js/manual-video-settings/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# manual-video-settings

Admin-only form at `/plugins/manual-video-settings/admin/` for hand-editing the host's transcoding config: HLS latency level, output codec, and each output variant's resolution / framerate / video bitrate (plus the passthrough flag). The form reads the current state via `owncast.videoConfig.read()` and POSTs partial updates through `owncast.videoConfig.write()`.
Admin-only form at `/plugins/manual-video-settings/admin/` for hand-editing the host's transcoding config: HLS latency level, output codec, autoplay, and each output variant's resolution, framerate, video bitrate, CPU usage level, and passthrough flag. The form reads the current state via `owncast.videoConfig.read()` and POSTs partial updates through `owncast.videoConfig.write()`.

**Demonstrates:** combining `manifest.admin.pages` (host-gated routes, no auth code in the plugin) with the `videoconfig.read` / `videoconfig.write` permission split. The HTTP handler accepts a partial `VideoConfigUpdate` so fields the form didn't touch are left untouched by the host.

Expand Down
14 changes: 8 additions & 6 deletions examples/js/manual-video-settings/__tests__/admin.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@
"videoConfig": {
"latencyLevel": 2,
"codec": "libx264",
"autoplay": "sound-only",
"variants": [
{ "width": 1920, "height": 1080, "framerate": 30, "videoBitrate": 6000, "audioBitrate": 160, "isPassthrough": false },
{ "width": 1280, "height": 720, "framerate": 30, "videoBitrate": 3000, "audioBitrate": 128, "isPassthrough": false }
{ "width": 1920, "height": 1080, "framerate": 30, "videoBitrate": 6000, "cpuUsageLevel": 3, "isPassthrough": false },
{ "width": 1280, "height": 720, "framerate": 30, "videoBitrate": 3000, "cpuUsageLevel": 2, "isPassthrough": false }
]
}
},
Expand All @@ -45,7 +46,7 @@
"expect": {
"status": 200,
"headers": { "content-type": "application/json" },
"body": "{\"latencyLevel\":2,\"codec\":\"libx264\",\"variants\":[{\"width\":1920,\"height\":1080,\"framerate\":30,\"videoBitrate\":6000,\"audioBitrate\":160,\"isPassthrough\":false},{\"width\":1280,\"height\":720,\"framerate\":30,\"videoBitrate\":3000,\"audioBitrate\":128,\"isPassthrough\":false}]}"
"body": "{\"latencyLevel\":2,\"codec\":\"libx264\",\"autoplay\":\"sound-only\",\"variants\":[{\"width\":1920,\"height\":1080,\"framerate\":30,\"videoBitrate\":6000,\"cpuUsageLevel\":3,\"isPassthrough\":false},{\"width\":1280,\"height\":720,\"framerate\":30,\"videoBitrate\":3000,\"cpuUsageLevel\":2,\"isPassthrough\":false}]}"
}
}
}
Expand Down Expand Up @@ -90,7 +91,7 @@
"method": "POST",
"path": "/admin/api/config",
"headers": { "content-type": "application/json" },
"body": "{\"latencyLevel\":2,\"codec\":\"libx264\",\"variants\":[{\"width\":1920,\"height\":1080,\"framerate\":30,\"videoBitrate\":6000,\"audioBitrate\":160,\"isPassthrough\":false},{\"width\":1280,\"height\":720,\"framerate\":30,\"videoBitrate\":3000,\"audioBitrate\":128,\"isPassthrough\":false}]}",
"body": "{\"latencyLevel\":2,\"codec\":\"libx264\",\"autoplay\":\"sound-only\",\"variants\":[{\"width\":1920,\"height\":1080,\"framerate\":30,\"videoBitrate\":6000,\"cpuUsageLevel\":3,\"isPassthrough\":false},{\"width\":1280,\"height\":720,\"framerate\":30,\"videoBitrate\":3000,\"cpuUsageLevel\":2,\"isPassthrough\":false}]}",
"authenticated": true,
"expect": { "status": 204 }
}
Expand All @@ -101,9 +102,10 @@
{
"latencyLevel": 2,
"codec": "libx264",
"autoplay": "sound-only",
"variants": [
{ "width": 1920, "height": 1080, "framerate": 30, "videoBitrate": 6000, "audioBitrate": 160, "isPassthrough": false },
{ "width": 1280, "height": 720, "framerate": 30, "videoBitrate": 3000, "audioBitrate": 128, "isPassthrough": false }
{ "width": 1920, "height": 1080, "framerate": 30, "videoBitrate": 6000, "cpuUsageLevel": 3, "isPassthrough": false },
{ "width": 1280, "height": 720, "framerate": 30, "videoBitrate": 3000, "cpuUsageLevel": 2, "isPassthrough": false }
]
}
]
Expand Down
22 changes: 15 additions & 7 deletions examples/js/manual-video-settings/public/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@
<h1>Manual Video Settings</h1>
<p class="lead">
Edit the host's live transcoding config: HLS latency level, output codec,
and each output variant's resolution / framerate / video bitrate. Changes
are applied via <code>owncast.videoConfig.write</code>.
autoplay, and each output variant's resolution / framerate / video bitrate
/ CPU usage. Changes are applied via <code>owncast.videoConfig.write</code>.
</p>

<form id="form">
Expand Down Expand Up @@ -89,6 +89,14 @@ <h1>Manual Video Settings</h1>
<option value="h264_videotoolbox">h264_videotoolbox (macOS)</option>
</select>
</label>
<label>
<span>Autoplay</span>
<select name="autoplay">
<option value="off">Never</option>
<option value="always">Always</option>
<option value="sound-only">Only if sound is available</option>
</select>
</label>
</fieldset>

<fieldset>
Expand All @@ -100,6 +108,7 @@ <h1>Manual Video Settings</h1>
<th>Height</th>
<th>FPS</th>
<th>Video kbps</th>
<th>CPU usage</th>
<th>Passthrough</th>
<th class="row-actions"></th>
</tr>
Expand All @@ -125,15 +134,12 @@ <h1>Manual Video Settings</h1>

function variantRow(v) {
const tr = document.createElement("tr");
// Audio bitrate is part of the host's variant shape but isn't
// exposed in this form. Stash it on the row so save() echoes it
// back instead of clobbering it with 0.
tr.dataset.audioBitrate = String(v.audioBitrate ?? 0);
tr.innerHTML = `
<td><input type="number" name="width" min="0" value="${v.width ?? 0}" /></td>
<td><input type="number" name="height" min="0" value="${v.height ?? 0}" /></td>
<td><input type="number" name="framerate" min="0" value="${v.framerate ?? 0}" /></td>
<td><input type="number" name="videoBitrate" min="0" value="${v.videoBitrate ?? 0}" /></td>
<td><input type="number" name="cpuUsageLevel" min="0" max="4" value="${v.cpuUsageLevel ?? 0}" /></td>
<td><input type="checkbox" name="isPassthrough" ${v.isPassthrough ? "checked" : ""} /></td>
<td class="row-actions">
<button type="button" class="danger" data-action="remove">Remove</button>
Expand All @@ -149,12 +155,13 @@ <h1>Manual Video Settings</h1>
height: Number(row.querySelector("[name=height]").value) || 0,
framerate: Number(row.querySelector("[name=framerate]").value) || 0,
videoBitrate: Number(row.querySelector("[name=videoBitrate]").value) || 0,
audioBitrate: Number(row.dataset.audioBitrate) || 0,
cpuUsageLevel: Number(row.querySelector("[name=cpuUsageLevel]").value) || 0,
isPassthrough: row.querySelector("[name=isPassthrough]").checked,
}));
return {
latencyLevel: Number(data.get("latencyLevel")),
codec: data.get("codec"),
autoplay: data.get("autoplay"),
variants,
};
}
Expand All @@ -164,6 +171,7 @@ <h1>Manual Video Settings</h1>
if ([...latency.options].some((o) => o.value === String(config.latencyLevel))) {
latency.value = String(config.latencyLevel);
}
form.querySelector("[name=autoplay]").value = config.autoplay || "off";
const codec = form.querySelector("[name=codec]");
if (![...codec.options].some((o) => o.value === config.codec) && config.codec) {
// Show a custom codec if the host reports something the dropdown
Expand Down
5 changes: 4 additions & 1 deletion examples/js/manual-video-settings/src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function parseVariant(v) {
height: Number(v.height) || 0,
framerate: Number(v.framerate) || 0,
videoBitrate: Number(v.videoBitrate) || 0,
audioBitrate: Number(v.audioBitrate) || 0,
cpuUsageLevel: Number(v.cpuUsageLevel) || 0,
isPassthrough: Boolean(v.isPassthrough),
};
}
Expand Down Expand Up @@ -46,6 +46,9 @@ module.exports = definePlugin({
if (typeof parsed.codec === "string" && parsed.codec.length > 0) {
update.codec = parsed.codec;
}
if (typeof parsed.autoplay === "string" && parsed.autoplay.length > 0) {
update.autoplay = parsed.autoplay;
}
if (Array.isArray(parsed.variants)) {
update.variants = parsed.variants.map(parseVariant);
}
Expand Down
9 changes: 5 additions & 4 deletions examples/js/stream-ops/__tests__/reads.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,23 @@
"given": {
"videoConfig": {
"latencyLevel": 2,
"codec": "h264",
"codec": "libx264",
"autoplay": "off",
"variants": [
{
"width": 1920,
"height": 1080,
"framerate": 30,
"videoBitrate": 6000,
"audioBitrate": 160,
"cpuUsageLevel": 3,
"isPassthrough": false
},
{
"width": 1280,
"height": 720,
"framerate": 30,
"videoBitrate": 3000,
"audioBitrate": 128,
"cpuUsageLevel": 2,
"isPassthrough": false
}
]
Expand All @@ -77,7 +78,7 @@
],
"expect": {
"chatSends": [
"latency 2, codec h264, 2 variant(s)"
"latency 2, codec libx264, 2 variant(s)"
]
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ One self-contained plugin per directory, authored in Python and compiled to wasm
| [overlay](./overlay/) | `http.serve`, static files from `public/` + dynamic JSON endpoint. |
| [stream-tracker](./stream-tracker/) | Every typed lifecycle / chat-user handler + read APIs. |
| [stream-ops](./stream-ops/) | Broadcast telemetry (`server.read`) + video config read/write (`videoconfig.read`/`videoconfig.write`). |
| [manual-video-settings](./manual-video-settings/) | Admin form for the video config: latency, codec, and per-variant resolution / framerate / bitrate. |
| [manual-video-settings](./manual-video-settings/) | Admin form for video latency, codec, autoplay, and output variants. |
| [engagement-bot](./engagement-bot/) | Discord + browser-push + fediverse notifier on stream / fediverse events, with a small inline spam filter. |
| [admin-demo](./admin-demo/) | `manifest.admin.pages`, host-gated admin routes. |
| [file-manager](./file-manager/) | `storage.fs`, admin page to browse/upload/download/delete files in the plugin's private sandbox. |
Expand Down
7 changes: 4 additions & 3 deletions examples/python/manual-video-settings/INSTRUCTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ A hand-edit form for the Owncast transcoding pipeline. Use it when you want to t
Opening **Manual Video Settings** under the admin sidebar shows a single form with:

- **Latency level**: `0` (lowest) through `4` (highest). Lower means viewers see the stream sooner but rebuffer more on flaky networks.
- **Codec**: the FFmpeg encoder Owncast invokes (`libx264` software, plus the usual hardware variants: `h264_vaapi`, `h264_nvenc`, `h264_qsv`, `h264_omx`, `h264_v4l2m2m`, `h264_videotoolbox`). If your live config uses something not in the dropdown, this plugin shows it as a `(current)` option so saving doesn't silently overwrite it.
- **Output variants**: one row per HLS rendition, with editable **width**, **height**, **FPS**, **video kbps**, and a **passthrough** checkbox. Add or remove rows from the form. Each variant's audio bitrate is preserved across saves (it's part of the host's data model, just not surfaced here).
- **Codec**: the FFmpeg encoder Owncast invokes (`libx264` software, plus `h264_vaapi`, `h264_nvenc`, `h264_qsv`, `h264_omx`, `h264_v4l2m2m`, and `h264_videotoolbox`). Hardware encoders must be available in the host's ffmpeg build.
- **Autoplay**: `off`, `always`, or `sound-only`.
- **Output variants**: one row per HLS rendition, with editable **width**, **height**, **FPS**, **video kbps**, **CPU usage**, and a **passthrough** checkbox. Add or remove rows from the form.

## How to use it

Expand All @@ -17,7 +18,7 @@ Opening **Manual Video Settings** under the admin sidebar shows a single form wi
3. Edit fields, then click **Save**. The status line confirms `Saved.` or shows the host's error message if a setting was rejected.
4. **Reload** discards in-form edits and re-fetches the live config.

Changes apply on the next stream segment Owncast encodes. An active broadcast does not need to be restarted, but viewers may see a brief quality switch.
Changes apply when the next stream starts. The host does not restart an active broadcast.

## Safety notes

Expand Down
2 changes: 1 addition & 1 deletion examples/python/manual-video-settings/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# manual-video-settings

Admin-only form at `/plugins/manual-video-settings/admin/` for hand-editing the host's transcoding config: HLS latency level, output codec, and each output variant's resolution / framerate / video bitrate (plus the passthrough flag). The form reads the current state via `owncast.video_config.read()` and POSTs partial updates through `owncast.video_config.write()`.
Admin-only form at `/plugins/manual-video-settings/admin/` for hand-editing the host's transcoding config: HLS latency level, output codec, autoplay, and each output variant's resolution, framerate, video bitrate, CPU usage level, and passthrough flag. The form reads the current state via `owncast.video_config.read()` and POSTs partial updates through `owncast.video_config.write()`.

**Demonstrates:** combining `manifest.admin.pages` (host-gated routes, no auth code in the plugin) with the `videoconfig.read` / `videoconfig.write` permission split. The handler accepts a partial `VideoConfigUpdate` so fields the form didn't touch are left untouched by the host.

Expand Down
Loading
Loading