Files
ytLlive/ai.md
T

14 KiB
Raw Blame History

ytLlive — AI Guide

Memory map entry point. Conventions live in schema.md; task status and YouTube API research in TASKS.md; directory maps in each folder's index.md. Reading order: this file → TASKS.md<dir>/index.md → source.

Response style

No default "Plans & Pitfalls" / planning boilerplate. Respond directly and concisely: do the queued work, then report what changed and what's next. Skip feature pitch, step-by-step implementation plans, pros/cons tables, and "potential pitfalls" sections unless the user explicitly asks for a plan first. A short diff-style summary beats a proposal document every time.

No-Fluff Mode (on demand)

Invoke with "no-fluff mode" (or similar) when you want ruthless review instead of reassurance. In that mode:

  • Strip all polite pleasantries, emojis, transitions, and conversational padding.
  • Treat the user's input as a draft to be methodically deconstructed or strengthened — argue, correct, and sharpen rather than agree.
  • Give unvarnished truth, not reassurance.

This is an occasional, explicitly-invoked mode — never the default. The default response style above stays in effect unless invoked.

Run

dotnet build   # Windows only — WPF requires Windows target
dotnet run

Note: EnableWindowsTargeting=true is set in ytLive.csproj, so the project can be restored/built from WSL, but running requires Windows.

Tests

xUnit in ytLive.Tests (net8.0-windows10.0.19041.0, matches the app TFM). Run on Windows — WSL can't run net8.0-windows tests:

dotnet.exe vstest "C:\...\ytLive.Tests\bin\Debug\net8.0-windows10.0.19041.0\ytLive.Tests.dll"

Good Dog Rule: ONE integration test per branch, ONE test per PR. Current: TokenStore DPAPI roundtrip/corrupt/missing/clear, OAuth exchange/refresh/ClearSession, CameraManager refcount + frame pump + failure handling (fakes for the WinRT seams) — 11 passing.

Architecture

C# / WPF (.NET 8) following MVVM:

Path Role
Models/ Plain data types — Scene, Source (incl. ClipShape, IsMirrored, VideoImageSource), QualityOption, StreamConfig, StreamHealth, YouTubeChannel, ChatMessage
ViewModels/ MainViewModel — exposes collections + commands for the UI; GoLiveViewModel, ReuseImageViewModel, CameraPickerViewModel
Services/ YouTube OAuth2, stream/broadcast management, live chat polling, LayoutStore (SQLite), webcam: VideoFrame seam + CameraDeviceInfo/ICameraEnumerator/ICameraFrameSource interfaces + MediaCaptureCameraEnumerator/MediaCaptureFrameSource (WinRT) + CameraManager
Helpers/ ViewModelBase (INotifyPropertyChanged), RelayCommand, ImageCache, AppLog (file logger), FocusPreservingListBox, OAuthCredentials, TokenStore (DPAPI session persistence), visibility converters
Themes/ Controls.xaml — the single dark-theme source, merged once in App.xaml (see Themes/index.md)
MainWindow.xaml Dark theme; layout: top bar (controls), center (preview), left (scenes/sources), right (chat), bottom (health)

Key patterns

  • ViewModelBase.SetProperty<T>() for property change notifications
  • RelayCommand for all button actions; commands gate on state (e.g. Start only when Offline)
  • ViewModels are constructed in XAML (<vm:MainViewModel/> as DataContext)
  • Services are currently instantiated in MainViewModel's constructor — no DI container yet
  • Layout persists to SQLite (Microsoft.Data.Sqlite); scenes/sources/asset bytes stored in the DB, asset identity is a SHA-256 content hash (1:M reuse, no file paths — assets are always available)
  • Theming: all custom styles live in Themes/Controls.xaml, merged in App.xaml — never duplicate styles per-window (dialog duplicates were consolidated into this dictionary)
  • Resolution tiers (bottom bar): 1080p60@8 (default) → 1080p30@8 → 720p60@6 → 720p30@6 → Vertical 1080p60@8 (9:16, 1080×1920). The composition master frame is always 1920×1080 — a tier is an output rect + target resolution over that master, so source geometry is never rewritten (no rounding drift). 16:9 tiers use the full frame; the vertical tier uses a centered 607×1080 window and the preview dims the cropped side strips at 55% black with an accent outline (semi-crop — the cut area stays visible). A resolution badge in the preview corner shows the active tier; the bottom bar shows bitrate/FPS. A tooltip explains finding upload bandwidth — an in-app speed test was deliberately dropped (unreliable). The future encoder crops the master to the rect and scales to the tier's Width×Height
  • Crash diagnosis: AppLog writes startup checkpoints to %APPDATA%\ytLlive\startup.log; App.xaml.cs logs DispatcherUnhandledException/AppDomain.UnhandledException. When WPF won't run from WSL, this log is how you find the failure (it caught the MenuItemRole.Separator XAML crash and the ComboBox SelectionBoxItem bug)

Current limitations / TODOs

  • Helpers/OAuthCredentials.cs contains the real ClientId/ClientSecret. Auth is complete and the session persists via Windows DPAPI (Helpers/TokenStore.cs%APPDATA%\ytLlive\ytLlive.auth, CurrentUser scope), reloaded best-effort at startup with a proactive refresh of a near-expiry access token. Sign-in/Change Account lives inside the Start Stream dialog (two-state flow — no separate Connect button). A graceful End Livestream signs out: StopStream() clears the session + token, so the next go-live needs a fresh sign-in; a crash never runs End, so the token survives and the creator stays signed in. YouTubeAuthService takes an optional HttpClient + sessionChanged callback (test seam + save hook; services are still constructed in MainViewModel)
  • Scene/source/asset layout persists (SQLite, schema v2); the OAuth session persists (DPAPI); the paid-unlock state does not (yet — itch.io key verification pending)
  • YouTubeStreamService uses hardcoded 1080p/60fps and per-broadcast streams — must switch to the v3 variable reusable stream
  • Webcam capture is shipped (milestone 1); screen capture, scene compositing/encoding, RTMP are next
  • StreamConfig defaults (TargetBitrate=6000, Resolution="1920x1080") are stale — the live dropdown drives StreamHealth.CurrentBitrate/FPS instead

Webcam capture (TASK 3 milestone 1)

  • Seam-first: everything above the WinRT layer speaks only VideoFrame (normalized tightly-packed BGRA8) + CameraDeviceInfo/ICameraEnumerator/ICameraFrameSource interfaces. Tests inject fakes; screen capture and background removal later feed the same seam.
  • CPU-first: MediaCaptureInitializationSettings { MemoryPreference = Cpu, StreamingCaptureMode = Video }, frames pulled via CreateFrameReaderAsync(colorSource, MediaEncodingSubtypes.Bgra8) — the pipeline does any format conversion, so every FrameArrived yields a ready BGRA8 SoftwareBitmap (bytes read via WindowsRuntimeMarshal.TryGetDataUnsafe, not marshalled copies).
  • TFM: net8.0-windows10.0.19041.0 (app + tests) pulls the WinRT projection from the SDK reference packs — no NuGet package, no capability manifest (unpackaged desktop app works; the Windows privacy camera toggle still applies). EnableWindowsTargeting keeps WSL builds working.
  • One camera app-wide: CameraManager refcounts sessions by DeviceId (a session is created with RefCount = 1; repeat acquire bumps it; the last release stops + disposes). The Add menu greys Webcam out once a webcam source exists anywhere (CanAddWebcam); the "OBS time" story is a one-camera limit.
  • Shared bitmap, coalesced updates: one WriteableBitmap per active camera, created on the UI thread at the device's frame size (first frame), forwarded to the single webcam Source.VideoImageSource via PreviewBitmapChanged. Frames arrive on a worker thread; CameraManager coalesces onto the dispatcher (at most one pending copy per session, at Render priority, always copying the latest frame) so a 60fps device never drowns the render thread.
  • Clip/mirror: per-Source ClipShape (Traditional rectangle / Round ellipse) + IsMirrored (ScaleX = -1). Rendered in the preview DataTemplate (Image for Traditional, ImageBrush inside an Ellipse for Round); toggled from the source chip; persisted in the layout DB. Round hit-testing is the same rectangle as Traditional (selection overlay is rectangular) — acceptable for now.
  • GPU posture: webcam frames are CPU (GPU-agnostic; WPF hardware-presents the preview anyway). Hardware encoders (NVENC/AMF/QSV) matter for the encoder task, not capture. D3DImage GPU compositing is deferred to the encoder task.
  • Background removal = milestone 2 — ONNX Runtime + DirectML (CPU fallback), MediaPipe Selfie Segmentation (Apache-2.0), wired into the same VideoFrame seam. Not part of milestone 1.

Design Principle

This software is so intuitive that even the most right-brained person can easily intuit and use it.

Apply this to every UI decision:

  • One-click go-live with working defaults
  • Prefilled YouTube defaults (RTMP URL, bitrate, resolution, latency)
  • Visual/drag-and-drop scene building over property panels
  • Every action produces a visible outcome — no dead ends

Monetization (design decision — the branding flash is the sword)

Free forever: all streams unlimited, no time caps, no subscription, no per-feature paywalls. The one paid line is a one-time unlock (delivered via itch.io — they handle hosting, payment, and key delivery; we never own a server or a key shop):

  • Free: a periodic full-frame branding flash — "made with ytLlive!" rendered big and centered at ~25% opacity for about one second (soft 250ms fade in/out), repeated every 300s, on the live output (and on v0.2 local recordings). Implemented as BrandFlashLayer in the preview compositor (MainWindow.xaml CanvasGrid) + BrandFlashTimer in MainViewModel — cadence 300s, first flash ~5s after go-live, only while live or recording. An always-on watermark can be cropped or covered; an intermittent full-frame flash can't be cropped and is impractical to edit around on a live feed.
  • Paid (one-time): branding flash removed (flips BrandFlashEnabled off) + Alerts (Super Chat / membership / subscribe pop-ins).

Deliberately rejected: always-on watermark (obscurable — replaced by the flash), hard stream-time cutoffs (the worst dead end — a stream dying mid-broadcast reads as broken, and YouTube streams routinely run 2-4 hours), soft-limit nagging, freemium tiers, and donation-only (relies on the kindness of strangers). Resolution/quality ceilings are deferred — that decision belongs to the resolution & streaming-constraints conversation, not monetization.

Auth gates Go Live, but not exploration

The app is fully usable without authentication: users can build scenes, add sources, compose previews, and audition the software with zero commitment. But going live requires authentication — it's the one capability gated behind YouTube sign-in. The sign-in should never pressure the user ("sign in (optional)", not a modal wall): the two-state top bar shows Start Stream (offline) / End Stream (live), and the Start Stream dialog hosts the account — a saved session appears as the default with "Change Account"; with none saved, a "Sign in to YouTube" button starts OAuth and the Start button stays disabled until signed in.

Account assumption (do not build an account setup flow)

Connecting uses Google OAuth ("Sign in with Google") to link an existing YouTube creator account. ytLlive never creates or sets up accounts — that is YouTube's job. If the creator has no YouTube channel, they go to YouTube first. This assumption is explicit and must never be silently replaced by an in-app account-creation step. Zero state = the Start Stream dialog's "Sign in to YouTube" button; going live is unreachable until an account is connected.

YouTube Live API — design constraints (do not violate)

These are the hard facts behind every decision. Full list in TASKS.md.

  • One-click go-live — never call transition(live). Insert the broadcast with enableAutoStart=true, enableAutoStop=true, enableMonitorStream=false, selfDeclaredMadeForKids=false, latencyPreference=low. The encoder starting brings YouTube live. enableMonitorStream=false is what lets us skip the testing stage.
  • Variable reusable streamliveStreams.insert once per channel with cdn.resolution=variable, cdn.frameRate=variable, isReusable=true; cache the ingestion URL + stream name and reuse for every broadcast. Any quality tier works without recreating the stream, and auto step-down is done by us dropping bitrate on the fly (zero API calls).
  • Quality is greyed out while live — resolution/frameRate/ingestionType are immutable after stream creation; editing title/description/privacy is fine at any time.
  • Report-by-exception health — poll liveStreams.list; render nothing on good/ok, surface a banner only on configurationIssues[] with warning/error severity. Bottom strip = YouTube logo
    • green/red connection dot (clickable → opens the dialog).
  • One dialog, three states — not connected / connected-offline (all editable) / live (title + description + visibility editable; quality + account greyed out). Both entry points (Start Stream button + bottom strip) open it; prefilled from saved session profile.
  • End stream — stop encoder → transition(complete), enableAutoStop as the safety net.
  • Encoder compliance — keyframes ≤ 4s (gopSizeLong), closed GOP, H.264, AAC/MP3 @ 44.1/48kHz, mono/stereo only. YouTube flags violations via health status.
  • Broadcast ID == Video ID — one ID tracks status, health, and the auto-created VOD (recordFromStart + enableDvr default true).