147 lines
10 KiB
Markdown
147 lines
10 KiB
Markdown
# ytLlive — AI Guide
|
||
|
||
> Memory map entry point. Conventions live in [`schema.md`](schema.md); task
|
||
> status and YouTube API research in [`TASKS.md`](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
|
||
|
||
```bash
|
||
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
|
||
|
||
No test framework set up yet. When added: `dotnet test`.
|
||
|
||
## Architecture
|
||
|
||
C# / WPF (.NET 8) following MVVM:
|
||
|
||
| Path | Role |
|
||
|------|------|
|
||
| `Models/` | Plain data types — Scene, Source, QualityOption, StreamConfig, StreamHealth, YouTubeChannel, ChatMessage |
|
||
| `ViewModels/` | MainViewModel — exposes collections + commands for the UI; GoLiveViewModel, ReuseImageViewModel |
|
||
| `Services/` | YouTube OAuth2, stream/broadcast management, live chat polling, LayoutStore (SQLite) |
|
||
| `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); 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
|
||
- No capture/encoding/RTMP yet
|
||
- `StreamConfig` defaults (`TargetBitrate=6000`, `Resolution="1920x1080"`) are stale — the live dropdown drives `StreamHealth.CurrentBitrate`/`FPS` instead
|
||
|
||
## 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 stream** — `liveStreams.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).
|