The logs answered the question and closed off a whole branch of investigation. The long run: broadcast inserted 14:17:04.577, ffmpeg to rtmp://a.rtmp.youtube.com/live2/5f6z-... at 14:17:05.686, and ffmpeg's own muxing summary at 14:18:56.473 reads video:101229KiB audio:1419KiB muxing overhead: 0.202949% ~99MB of real video plus audio over 112s, which is what 8000+128 kb/s is supposed to produce. No socket error, no 4xx from ingest, no reconnect. And the broadcast still never left `ready`; close-out 403'd invalidTransition again. The 8s run behaved the same way, so "YouTube needed longer" is disproven — 112s is longer. So the RTMP path is provably healthy. That is the finding worth keeping: it kills the entire "the push is broken" theory, which is where this had been heading for a week. The break is between ingest being accepted and the broadcast being told to start. Which puts the binding in frame, and the app was structurally blind to it. Google documents contentDetails.boundStreamId and says enableAutoStart fires "when you start streaming video on the bound live stream" — both valid on insert, neither restricted against reusable streams. But the migration guide also says a broadcast "is not bound to any streams at the time that it's created", and the failure that produces is invisible from the RTMP side precisely because a reusable stream will happily accept bytes for an unbound broadcast. Ingest succeeding proves nothing. Hence LogBroadcastDiagnosticsAsync, riding the existing health poll rather than adding a timer. One greppable line: lifecycle status, privacy, recording, boundStreamId *compared against the stream id we are actually pushing* (BoundMatches: yes / MISMATCH / NO-BINDING / unknown), boundStreamLastUpdate- TimeMs, autoStart/Stop, liveChatId presence, the stream's own streamStatus and activeLiveChatId, and the CDN settings we declared. Deliberately self-limiting: _broadcastSeenLive latches on live/liveStarting so the diagnostic shouts only while the broadcast is stuck and goes quiet once it is healthy. A permanent per-tick log line would be noise; this is the opposite. One test, per the Good Dog rule: BroadcastBindingTests pins the matrix and specifically pins `unknown` when there is no stream id to compare against, so a missing expectation can never be misreported as a match — that is the failure that would make this diagnostic lie. Suite 387/387, 0 warnings.
ytLlive
A creator-proof live-streaming and recording app for YouTube — native Windows (C# / WPF / .NET 8).
Screenshot
Design Principle
This software is so intuitive that even the most right-brained person can easily intuit and use it.
Every feature is measured against this. One-click go-live, sane YouTube defaults, visual scene building, and no dead ends — every action has a visible outcome.
Why ytLlive
OBS treats YouTube as an afterthought; SLOBS is Twitch-first with YouTube bolted on. ytLlive is designed and dedicated to YouTube livestreaming and recording, and to YouTube's specific quirks.
Account Assumption
ytLlive assumes you already have a YouTube creator account — connecting uses "Sign in with Google" to link that existing account. ytLlive does not create accounts. If you don't have a YouTube channel yet, set one up on YouTube first, then connect it here.
Audio Assumption
ytLlive uses your default microphone and desktop/game output — it never hunts for a device or contends for one. It assumes your audio is already configured and working (default input and output set, not hijacked by another app). Audio issues are a Windows/device matter outside ytLlive's scope: we don't debug, detect, or try to fix them. If it doesn't sound right, fix the system's audio settings first — it's on the OS, not on us.
Run
dotnet build # Windows only — WPF
dotnet run
Version Roadmap
v1 is the finished product (creator ruling, 2026-09-01 — there are no v1.x releases).
Everything still queueing lands in v1; anything not on the feature list is on the closed
"Out of product — permanently" list in TASKS.md — read that page before asking for a feature.
What v1 ships: the five-scene director's control surface, webcam + screen capture, media + web + text sources, chat on-stream, the social bar, mic/desktop audio with auto-duck and TRAX, local recording, variable quality tiers with auto step-down, one-click go-live + scheduling, stream resilience (auto-reconnect inside YouTube's grace, one-click back on air), built-in monetization awareness (reward capture, session reports, YPP journey tracker), Alerts — all free and ungated; the paid tier only removes the branding flash.
Structure
| Path | Role |
|---|---|
Models/ |
Scene, Source, WebcamSceneConfig, SceneElement, SceneCatalog, QualityOption, StreamConfig/Health, chat, Socials, BroadcastMetadata, Music, MicStatus |
ViewModels/ |
MainViewModel (partial classes per area) + dialogs: GoLive, CameraPicker, MicPicker, HotkeyConfig, Socials, LiveBroadcastForm |
Services/ |
YouTube (OAuth, streams/broadcasts, chat polling), LayoutStore (SQLite), capture (camera/screen-capture/media-file via FFmpeg), SceneCompositor + FramePump, FFmpeg encoder + RTMP, audio pipeline (WASAPI loopback + mic, mixer, filters, ducker, limiter, TRAX), SocialValidator, notifications, hotkeys, SceneGraph, ChatOverlayLayer |
Helpers/ |
ViewModelBase, RelayCommand, ImageCache, AppLog, TokenStore (DPAPI), OAuthCredentials, converters |
Themes/ |
Controls.xaml — the dark-theme control styles, merged once in App.xaml |
MainWindow.xaml |
Dark-theme control surface: thumbnails + central preview, two-state left panel (layers/properties ↔ live chat), top bar, footer |
Distribution.md |
GA-time plan: signing, installer, EULA, hardening — executed as TASK 36 |
Docs (memory map)
schema.md defines the conventions; ai.md is the AI/session guide;
TASKS.md is the task queue. Each code directory carries an index.md.

