diff --git a/docs/PLUGIN_AUTHOR_GUIDE.md b/docs/PLUGIN_AUTHOR_GUIDE.md index 3749419..b0cfad0 100644 --- a/docs/PLUGIN_AUTHOR_GUIDE.md +++ b/docs/PLUGIN_AUTHOR_GUIDE.md @@ -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` | diff --git a/docs/WIRE_PROTOCOL.md b/docs/WIRE_PROTOCOL.md index 948e6d2..d6aa2bb 100644 --- a/docs/WIRE_PROTOCOL.md +++ b/docs/WIRE_PROTOCOL.md @@ -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` @@ -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"; + 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 diff --git a/examples/js/README.md b/examples/js/README.md index 07a033a..309ae8f 100644 --- a/examples/js/README.md +++ b/examples/js/README.md @@ -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. | diff --git a/examples/js/manual-video-settings/INSTRUCTIONS.md b/examples/js/manual-video-settings/INSTRUCTIONS.md index 7d20639..32e78bc 100644 --- a/examples/js/manual-video-settings/INSTRUCTIONS.md +++ b/examples/js/manual-video-settings/INSTRUCTIONS.md @@ -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 @@ -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 diff --git a/examples/js/manual-video-settings/README.md b/examples/js/manual-video-settings/README.md index 9f65a8e..d368cf5 100644 --- a/examples/js/manual-video-settings/README.md +++ b/examples/js/manual-video-settings/README.md @@ -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. diff --git a/examples/js/manual-video-settings/__tests__/admin.test.json b/examples/js/manual-video-settings/__tests__/admin.test.json index df51a4b..9e2ace0 100644 --- a/examples/js/manual-video-settings/__tests__/admin.test.json +++ b/examples/js/manual-video-settings/__tests__/admin.test.json @@ -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 } ] } }, @@ -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}]}" } } } @@ -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 } } @@ -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 } ] } ] diff --git a/examples/js/manual-video-settings/public/admin/index.html b/examples/js/manual-video-settings/public/admin/index.html index 98f11cc..4dc1377 100644 --- a/examples/js/manual-video-settings/public/admin/index.html +++ b/examples/js/manual-video-settings/public/admin/index.html @@ -60,8 +60,8 @@

Manual Video Settings

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 owncast.videoConfig.write. + autoplay, and each output variant's resolution / framerate / video bitrate + / CPU usage. Changes are applied via owncast.videoConfig.write.

@@ -89,6 +89,14 @@

Manual Video Settings

+
@@ -100,6 +108,7 @@

Manual Video Settings

Height FPS Video kbps + CPU usage Passthrough @@ -125,15 +134,12 @@

Manual Video Settings

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 = ` + @@ -149,12 +155,13 @@

Manual Video Settings

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, }; } @@ -164,6 +171,7 @@

Manual Video Settings

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 diff --git a/examples/js/manual-video-settings/src/plugin.js b/examples/js/manual-video-settings/src/plugin.js index d9e58fb..5cf870c 100644 --- a/examples/js/manual-video-settings/src/plugin.js +++ b/examples/js/manual-video-settings/src/plugin.js @@ -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), }; } @@ -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); } diff --git a/examples/js/stream-ops/__tests__/reads.test.json b/examples/js/stream-ops/__tests__/reads.test.json index ece01ac..8f654c6 100644 --- a/examples/js/stream-ops/__tests__/reads.test.json +++ b/examples/js/stream-ops/__tests__/reads.test.json @@ -40,14 +40,15 @@ "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 }, { @@ -55,7 +56,7 @@ "height": 720, "framerate": 30, "videoBitrate": 3000, - "audioBitrate": 128, + "cpuUsageLevel": 2, "isPassthrough": false } ] @@ -77,7 +78,7 @@ ], "expect": { "chatSends": [ - "latency 2, codec h264, 2 variant(s)" + "latency 2, codec libx264, 2 variant(s)" ] } } diff --git a/examples/python/README.md b/examples/python/README.md index 77b6f3f..03a2542 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -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. | diff --git a/examples/python/manual-video-settings/INSTRUCTIONS.md b/examples/python/manual-video-settings/INSTRUCTIONS.md index 790b9c4..b963c92 100644 --- a/examples/python/manual-video-settings/INSTRUCTIONS.md +++ b/examples/python/manual-video-settings/INSTRUCTIONS.md @@ -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 @@ -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 diff --git a/examples/python/manual-video-settings/README.md b/examples/python/manual-video-settings/README.md index 11c345d..b4ec7f6 100644 --- a/examples/python/manual-video-settings/README.md +++ b/examples/python/manual-video-settings/README.md @@ -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. diff --git a/examples/python/manual-video-settings/__tests__/admin.test.json b/examples/python/manual-video-settings/__tests__/admin.test.json index df51a4b..9e2ace0 100644 --- a/examples/python/manual-video-settings/__tests__/admin.test.json +++ b/examples/python/manual-video-settings/__tests__/admin.test.json @@ -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 } ] } }, @@ -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}]}" } } } @@ -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 } } @@ -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 } ] } ] diff --git a/examples/python/manual-video-settings/public/admin/index.html b/examples/python/manual-video-settings/public/admin/index.html index 98f11cc..4dc1377 100644 --- a/examples/python/manual-video-settings/public/admin/index.html +++ b/examples/python/manual-video-settings/public/admin/index.html @@ -60,8 +60,8 @@

Manual Video Settings

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 owncast.videoConfig.write. + autoplay, and each output variant's resolution / framerate / video bitrate + / CPU usage. Changes are applied via owncast.videoConfig.write.

@@ -89,6 +89,14 @@

Manual Video Settings

+
@@ -100,6 +108,7 @@

Manual Video Settings

Height FPS Video kbps + CPU usage Passthrough @@ -125,15 +134,12 @@

Manual Video Settings

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 = ` + @@ -149,12 +155,13 @@

Manual Video Settings

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, }; } @@ -164,6 +171,7 @@

Manual Video Settings

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 diff --git a/examples/python/manual-video-settings/src/plugin.py b/examples/python/manual-video-settings/src/plugin.py index 8297c72..1c343db 100644 --- a/examples/python/manual-video-settings/src/plugin.py +++ b/examples/python/manual-video-settings/src/plugin.py @@ -17,7 +17,7 @@ def parse_variant(v): "height": int(v.get("height") or 0), "framerate": int(v.get("framerate") or 0), "videoBitrate": int(v.get("videoBitrate") or 0), - "audioBitrate": int(v.get("audioBitrate") or 0), + "cpuUsageLevel": int(v.get("cpuUsageLevel") or 0), "isPassthrough": bool(v.get("isPassthrough")), } @@ -47,6 +47,9 @@ def set_config(req): codec = parsed.get("codec") if isinstance(codec, str) and len(codec) > 0: update["codec"] = codec + autoplay = parsed.get("autoplay") + if isinstance(autoplay, str) and len(autoplay) > 0: + update["autoplay"] = autoplay if isinstance(parsed.get("variants"), list): update["variants"] = [parse_variant(v) for v in parsed["variants"]] diff --git a/examples/python/stream-ops/__tests__/reads.test.json b/examples/python/stream-ops/__tests__/reads.test.json index ece01ac..8f654c6 100644 --- a/examples/python/stream-ops/__tests__/reads.test.json +++ b/examples/python/stream-ops/__tests__/reads.test.json @@ -40,14 +40,15 @@ "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 }, { @@ -55,7 +56,7 @@ "height": 720, "framerate": 30, "videoBitrate": 3000, - "audioBitrate": 128, + "cpuUsageLevel": 2, "isPassthrough": false } ] @@ -77,7 +78,7 @@ ], "expect": { "chatSends": [ - "latency 2, codec h264, 2 variant(s)" + "latency 2, codec libx264, 2 variant(s)" ] } } diff --git a/host-runtime/cmd/owncast-plugin-serve/main.go b/host-runtime/cmd/owncast-plugin-serve/main.go index 02395b9..4e5f5fa 100644 --- a/host-runtime/cmd/owncast-plugin-serve/main.go +++ b/host-runtime/cmd/owncast-plugin-serve/main.go @@ -137,10 +137,11 @@ func main() { VideoConfig: func() plugin.VideoConfig { return plugin.VideoConfig{ LatencyLevel: 2, - Codec: "h264", + Codec: "libx264", + Autoplay: "off", Variants: []plugin.StreamVariant{ - {Width: 1920, Height: 1080, Framerate: 30, VideoBitrate: 6000, AudioBitrate: 160}, - {Width: 1280, Height: 720, Framerate: 30, VideoBitrate: 3000, AudioBitrate: 128}, + {Width: 1920, Height: 1080, Framerate: 30, VideoBitrate: 6000, CPUUsageLevel: 2}, + {Width: 1280, Height: 720, Framerate: 30, VideoBitrate: 3000, CPUUsageLevel: 2}, }, } }, diff --git a/sdks/js/index.d.ts b/sdks/js/index.d.ts index 620e784..bd27f79 100644 --- a/sdks/js/index.d.ts +++ b/sdks/js/index.d.ts @@ -71,20 +71,36 @@ export interface StreamBroadcaster { bitrates?: number[]; } +/** Viewer autoplay behavior. */ +export type AutoplayMode = "off" | "always" | "sound-only"; + +/** H.264 encoder names accepted by Owncast video config writes. + * Hardware encoders must also be available in ffmpeg. */ +export type VideoCodec = + | "libx264" + | "h264_omx" + | "h264_vaapi" + | "h264_qsv" + | "h264_nvenc" + | "h264_v4l2m2m" + | "h264_videotoolbox"; + /** One configured output rendition, part of VideoConfig (owncast.videoConfig). */ export interface StreamVariant { width: number; height: number; framerate: number; videoBitrate: number; - audioBitrate: number; + cpuUsageLevel: number; isPassthrough: boolean; } /** The current video/transcoding config returned by owncast.videoConfig.read(). */ export interface VideoConfig { latencyLevel: number; + /** The configured encoder. Reads may report a legacy or newer host value. */ codec: string; + autoplay: AutoplayMode; variants: StreamVariant[]; } @@ -92,7 +108,8 @@ export interface VideoConfig { * are left unchanged. */ export interface VideoConfigUpdate { latencyLevel?: number; - codec?: string; + codec?: VideoCodec; + autoplay?: AutoplayMode; variants?: StreamVariant[]; }