Skip to content

Commit 6ad8760

Browse files
committed
Golive
1 parent 9a5b646 commit 6ad8760

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+10072
-249
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ Repo-specific design rules:
324324
- Go Live chrome MUST stay operational and generic; do not surface the loaded script title or script preview subtitle in the Go Live header/session bar just because a script is open.
325325
- Go Live back navigation MUST return to the actual previous in-app screen when known, and only fall back to library when there is no valid in-app return target; it must never hardcode teleprompter as the back target.
326326
- Go Live local recording MUST capture the same composed program feed that the active live/record session publishes; recording a black frame or a different source than the current program feed is a regression.
327+
- Go Live local recording artifacts MUST contain both decodable video and decodable audio from the real program feed, and their saved resolution/quality must match the active source or chosen output profile instead of silently degrading to a lower-quality fallback.
327328
- Go Live recording status and runtime panels MUST show the real recording details the browser runtime knows, including the resolved output profile and live session/file telemetry when available; blank recording metadata during an active local recording is a regression.
328329
- Go Live audio meters MUST show real browser audio activity for microphone, program, and recording paths; static placeholder bars or seeded fake levels in the Audio tab are regressions.
329330
- Go Live MUST have one active browser-side broadcast spine per setup: choose LiveKit or VDO.Ninja for the upstream transport, but do not run both as the same session's primary publish path at once.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Architecture map: [docs/Architecture.md](docs/Architecture.md)
3131
- TPS-focused editor for prompt-ready scripts
3232
- RSVP/Learn mode with ORP-style word rendering
3333
- browser-side media scene, device setup, and live preview
34-
- Go Live runtime with LiveKit and VDO.Ninja outputs
34+
- Go Live runtime with VDO.Ninja-first standalone output plus optional LiveKit mode
3535
- browser-local document and settings storage
3636
- browser-realistic acceptance tests through Playwright
3737

docs/Features/GoLiveRuntime.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The current page layout is a production-style studio surface:
1515
- right rail for the current live preview plus a larger stream, audio, and runtime panel that can comfortably show real output telemetry, metadata, and mix controls
1616
- source-card `ON AIR` badges and the preview red live dot only turn on when recording or streaming is actually active; idle routing and armed sources stay visually non-live
1717
- full-program mode collapses both side rails so the center canvas follows the design's focused monitor state
18-
- destination cards are built from persisted local outputs plus a dynamic browser-stored `ExternalDestinations` list; seeded fake provider rows are forbidden
18+
- destination cards are built only from the persisted browser-stored `ExternalDestinations` list; local recording is controlled from `REC` plus runtime metadata, not rendered as a fake destination row
1919

2020
The runtime now owns real browser media outputs for the composed program scene and the current audio bus:
2121

@@ -24,19 +24,22 @@ The runtime now owns real browser media outputs for the composed program scene a
2424
- `Go Live` builds one browser-side program stream from the scene camera cards by drawing the selected primary camera full-frame and then layering additional included cameras as positioned overlays on a canvas
2525
- the scene `AudioBus` is mixed into one program audio track through `AudioContext`, delay, and gain nodes before the final program stream is published or recorded
2626
- OBS browser output stays browser-only and exposes the composed program audio inside an OBS Browser Source environment
27-
- LiveKit publishing uses the vendored browser SDK and publishes real composed `MediaStreamTrack` objects for video and audio
27+
- `VDO.Ninja` publishing uses the vendored official browser SDK and publishes the same composed `MediaStream` that powers preview, recording, and OBS
28+
- `LiveKit` publishing remains available only as an explicit optional managed-transport mode
2829
- local recording uses the browser `MediaRecorder` API against the same composed program stream and prefers `showSaveFilePicker()` for real local file writing when the browser allows it, with download fallback otherwise
30+
- saved local recording artifacts must decode into visible program video plus audible program audio; a black frame or silent file is a runtime regression even if metadata looks populated
2931
- recording export profiles are honest browser probes only: the runtime checks `MediaRecorder.isTypeSupported()` and `MediaCapabilities.encodingInfo()` before choosing the resolved MIME type and falls back to a supported browser codec/container when the requested profile is unavailable
3032
- stream start, stop, recording start, recording stop, and source switching update the active browser output session instead of only flipping local UI state
3133
- the session timer and right-rail `Status` / `Runtime` cards are driven from live session/runtime state, not hardcoded demo telemetry
3234
- active local recording surfaces resolved runtime metadata in the right rail, including the recording profile, save mode, and live captured file size when the browser exposes those details
3335

34-
Relay-only destinations stay configuration surfaces:
36+
Downstream destinations stay browser-honest configuration surfaces:
3537

36-
- YouTube, Twitch, custom RTMP, and similar RTMP-style targets still persist credentials and routing in browser storage
38+
- YouTube, Twitch, custom RTMP, and similar targets still persist credentials and routing in browser storage
3739
- `Go Live` only exposes quick arm/disarm toggles and readiness summaries for those targets
3840
- detailed destination credentials, ingest URLs, and provider-specific configuration live in `Settings`
39-
- these targets do not publish directly from the browser runtime; they require an external relay or ingest layer outside this standalone WASM app, and `Go Live` must not mark the session live unless a direct browser live output actually starts
41+
- direct browser publish is allowed only when the active standalone spine really supports it, with `VDO.Ninja` treated as the primary path for browser-driven `YouTube` / `Twitch` workflows
42+
- custom RTMP remains visually constrained unless a real browser-compatible publish path is confirmed
4043

4144
## Broadcast Spine Decision
4245

@@ -54,7 +57,8 @@ Current implementation focus:
5457

5558
- browser-local recording from the composed program feed
5659
- OBS browser output
57-
- LiveKit publishing in the current code path
60+
- `VDO.Ninja` publishing in the current code path
61+
- `LiveKit` kept only as an explicit optional managed mode
5862
- no shipped universal browser-only path yet for every final platform target
5963

6064
Target rollout after this architecture pass:
@@ -118,13 +122,13 @@ sequenceDiagram
118122
Settings->>Studio: Persist device preferences
119123
Settings->>Scene: Persist scene cameras and audio bus
120124
User->>Settings: Configure the active spine and browser-compatible publish targets
121-
Settings->>Studio: Persist local outputs and external destination list
125+
Settings->>Studio: Persist external destination list and recording/export preferences
122126
User->>GoLive: Open Go Live
123127
GoLive->>Studio: Load live routing settings
124128
GoLive->>Scene: Load current scene sources
125129
GoLive->>Sources: Render real scene cameras and mic status
126130
GoLive->>Preview: Mount the current on-air scene camera
127-
GoLive->>Sidebar: Summarize persisted destination readiness
131+
GoLive->>Sidebar: Summarize persisted external destination readiness
128132
User->>GoLive: Arm one or more destinations
129133
GoLive->>Studio: Persist output targets
130134
User->>Sources: Select a different scene camera
@@ -219,7 +223,7 @@ flowchart LR
219223
Vdo["VDO.Ninja publish / WHIP"]
220224
LiveKit["LiveKit publishTrack(...)"]
221225
Targets["Browser-compatible publish targets"]
222-
Platforms["YouTube / Twitch / RTMP<br/>constrained unless browser path exists"]
226+
Platforms["YouTube / Twitch / RTMP<br/>only when the active spine exposes a real browser path"]
223227
Obs["OBS browser audio bridge"]
224228
225229
Scene --> Factory
@@ -248,7 +252,7 @@ flowchart LR
248252
- `Settings` must expose a visible CTA into `Go Live` so device setup and live routing stay discoverable as separate flows.
249253
- the shared header shell must keep `Go Live` reachable from every non-`Go Live` routed page because it is a primary studio action
250254
- `Go Live` may arm local recording and downstream relay targets at the same time, but only one upstream browser transport spine may be active for a session.
251-
- hardcoded destination instances are forbidden; the external destination list must come from persisted browser settings and may contain zero, one, or many platform entries
255+
- hardcoded destination instances are forbidden; the right-rail destination list must come from persisted external destinations only and may contain zero, one, or many platform entries
252256
- `Go Live` must reuse the browser-composed scene and not invent a separate media graph.
253257
- `Go Live` must auto-seed the first available browser camera into the scene when the scene is empty and devices are available.
254258
- `Go Live` must show the selected program source in the center monitor and the currently on-air source in the right preview rail until the operator explicitly takes the selected source live.
@@ -262,13 +266,15 @@ flowchart LR
262266
- `VirtualCamera` mode normalizes to OBS armed by default, so browser sessions keep the legacy desktop-capture workflow unless the user explicitly turns OBS off
263267
- Camera source inclusion is persisted through `MediaSceneState`.
264268
- Destination credentials and endpoints are persisted only in browser storage for this standalone runtime.
269+
- `VDO.Ninja` browser publishing must use the vendored official SDK shipped in the repo, not a CDN copy.
265270
- LiveKit browser publishing must use the vendored SDK shipped in the repo, not a CDN copy.
266271
- `VDO.Ninja` is the default production upstream spine for the strict standalone browser mode; `LiveKit` is reserved for explicit optional managed-transport modes and must not quietly become a second co-equal default publish path.
267272
- OBS browser integration must stay a thin browser bridge; no server relay or backend media graph is introduced.
268-
- local recording must stay browser-local and use the same active program media session as OBS / LiveKit so record and source switching stay in sync
273+
- local recording must stay browser-local and use the same active program media session as OBS / `VDO.Ninja` / optional `LiveKit` so record and source switching stay in sync
269274
- local recording must prefer real local file writing through the File System Access API when the browser exposes it, but must fall back to browser download instead of pretending save-to-disk is universally available
270275
- recording codec/container export must never advertise unsupported browser encoders as if they are guaranteed; the runtime must probe support and choose a real fallback profile
271276
- local recording metadata shown in `Go Live` must come from browser runtime state, including honest file-size/save-mode/profile details when available, instead of placeholder labels or blank cards
277+
- browser acceptance for local recording must verify the saved artifact itself, not only the in-page runtime metadata, so regressions such as black video or silent audio cannot hide behind populated telemetry
272278
- the `Audio` tab must project real browser telemetry: the microphone row comes from a direct browser microphone monitor, while `Program` and `Recording` come from the mixed program audio feed and must fall back to idle instead of seeded percentages when no signal is active
273279
- right-rail telemetry must never show fake packet-loss, jitter, ping, or upload metrics when the browser runtime does not actually own those measurements
274280
- downstream platforms such as YouTube and Twitch may be shown as first-class live-publish targets when the chosen VDO-driven standalone path supports them; custom RTMP must stay visibly constrained unless a real browser-compatible path is confirmed

docs/Features/VendoredStreamingSdkReleases.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
- `livekit/client-sdk-js`
88
- `steveseguin/vdo.ninja`
9+
- `steveseguin/ninjasdk`
910

10-
The repo pins both SDKs to exact GitHub release tags and exact GitHub release URLs. The vendored files live under `src/PrompterOne.Shared/wwwroot/vendor/` so the browser runtime does not depend on floating CDN or `latest` endpoints.
11+
The repo pins these runtime artifacts to exact GitHub release tags and exact GitHub release URLs. The vendored files live under `src/PrompterOne.Shared/wwwroot/vendor/` so the browser runtime does not depend on floating CDN or `latest` endpoints.
1112

1213
## Source Of Truth
1314

@@ -20,6 +21,7 @@ The repo pins both SDKs to exact GitHub release tags and exact GitHub release UR
2021

2122
- LiveKit Client SDK JS: `v2.18.0`
2223
- VDO.Ninja: `v29.0`
24+
- VDO.Ninja SDK: `v1.3.18`
2325

2426
## Flow
2527

@@ -51,4 +53,5 @@ flowchart LR
5153
## Notes
5254

5355
- LiveKit does not ship the built browser bundle as a GitHub release asset in the current release format, so the sync flow builds the exact pinned release tag from the tagged source tarball and copies only the resulting browser artifacts into the repo.
54-
- VDO.Ninja is vendored as a runtime JS tree from the exact pinned release source tarball because its browser runtime spans multiple JS entrypoints and runtime-loaded dependencies.
56+
- VDO.Ninja is vendored as a runtime JS tree from the exact pinned release source tarball because its broader browser runtime spans multiple JS entrypoints and runtime-loaded dependencies.
57+
- The official standalone browser publishing SDK comes from `steveseguin/ninjasdk` and is vendored from exact pinned GitHub release assets plus the matching license files from the release tarball.

scripts/vendored_streaming_sdks.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,23 @@ def sync_vdo_ninja(sdk: dict) -> None:
155155
copy_js_tree(source_root / runtime_directory, vendor_directory / runtime_directory)
156156

157157

158+
def sync_release_assets_with_license_tarball(sdk: dict) -> None:
159+
vendor_directory = REPO_ROOT / sdk["vendorDirectory"]
160+
reset_directory(vendor_directory)
161+
162+
with tempfile.TemporaryDirectory(prefix=f"{sdk['id']}-") as temp_dir_name:
163+
temp_dir = Path(temp_dir_name)
164+
archive_path = temp_dir / "release.tar.gz"
165+
download_to(sdk["sourceTarballUrl"], archive_path)
166+
source_root = extract_tarball(archive_path, temp_dir)
167+
168+
for license_file in sdk.get("licenseFiles", []):
169+
copy_file(source_root / license_file, vendor_directory / Path(license_file).name)
170+
171+
for asset in sdk.get("assets", []):
172+
download_to(asset["url"], vendor_directory / asset["name"])
173+
174+
158175
def sync_sdk(sdk: dict) -> None:
159176
strategy = sdk["syncStrategy"]
160177
print(f"Syncing {sdk['id']} from {sdk['releaseTag']} using {strategy}")
@@ -167,6 +184,10 @@ def sync_sdk(sdk: dict) -> None:
167184
sync_vdo_ninja(sdk)
168185
return
169186

187+
if strategy == "release-assets-with-license-tarball":
188+
sync_release_assets_with_license_tarball(sdk)
189+
return
190+
170191
raise SystemExit(f"Unsupported sync strategy: {strategy}")
171192

172193

@@ -187,6 +208,11 @@ def verify_sdk(sdk: dict) -> list[str]:
187208
if not (vendor_directory / artifact_name).exists():
188209
errors.append(f"{sdk['id']}: missing artifact {artifact_name}")
189210

211+
for asset in sdk.get("assets", []):
212+
asset_name = asset["name"]
213+
if not (vendor_directory / asset_name).exists():
214+
errors.append(f"{sdk['id']}: missing asset {asset_name}")
215+
190216
for root_file in sdk.get("rootFiles", []):
191217
if not (vendor_directory / root_file).exists():
192218
errors.append(f"{sdk['id']}: missing root file {root_file}")

src/PrompterOne.App/wwwroot/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ <h2 id="app-shell-error-title">PrompterOne hit a shell error</h2>
4242
<script src="_content/PrompterOne.Shared/media/go-live-media-compositor.js"></script>
4343
<script src="_content/PrompterOne.Shared/media/go-live-output-support.js"></script>
4444
<script src="_content/PrompterOne.Shared/vendor/livekit-client/v2.18.0/livekit-client.umd.js"></script>
45+
<script src="_content/PrompterOne.Shared/vendor/vdo-ninja-sdk/v1.3.18/vdoninja-sdk.js"></script>
46+
<script src="_content/PrompterOne.Shared/media/go-live-output-vdo-ninja.js"></script>
4547
<script src="_content/PrompterOne.Shared/media/go-live-output.js"></script>
4648
<script src="_content/PrompterOne.Shared/editor/editor-source-panel.js"></script>
4749
<script src="_content/PrompterOne.Shared/learn/learn-rsvp-layout.js"></script>

src/PrompterOne.Shared/GoLive/Models/GoLiveText.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,9 @@ public static class Surface
109109
public const string RuntimeEngineLiveKitValue = "LiveKit";
110110
public const string RuntimeEngineObsBrowserValue = "OBS browser";
111111
public const string RuntimeEngineObsLiveKitValue = "OBS + LiveKit";
112+
public const string RuntimeEngineObsVdoNinjaValue = "OBS + VDO.Ninja";
112113
public const string RuntimeEngineRecorderValue = "Recorder";
114+
public const string RuntimeEngineVdoNinjaValue = "VDO.Ninja";
113115
public const string SceneSlidesId = "scene-slides";
114116
public const string SceneSlidesLabel = "Slides";
115117
public const string SecondarySceneId = "scene-secondary";

0 commit comments

Comments
 (0)