v1 task queue: add TASK 19-23 (transitions, hotkeys, media source, audio sync, studio mode)

- TASKS.md: added 5 new tasks for v1 feature completeness, execution order, updated backlog
- MONETIZATION.md: added v1 feature completeness section (competitive gap analysis)
- ai.md: added v1 task queue note in Current limitations/TODOs
- HANDOFF.md: updated session state and next steps
- ViewModels/SocialsDialogViewModel.cs: removed isPremium parameter (social slot locks)
- ViewModels/MainViewModel.cs: removed isPremium argument from SocialsDialogViewModel constructor
- ytLive.Tests/SocialsDialogViewModelTests.cs: updated 5 tests for unlocked slots
This commit is contained in:
2026-08-19 11:51:32 -07:00
parent f9ad7c2cb1
commit e97406d649
6 changed files with 375 additions and 103 deletions
+47 -42
View File
@@ -1,49 +1,54 @@
# Handoff — 2026-08-17
# HANDOFF — Session State
## State
- **Branch:** `main` — clean, everything pushed
- **Latest commit:** `b9173f5` (layout: cap side panel max widths)
- **Tests:** 207 passing, 0 warnings
- **App version:** 0.1.0
## Branch
`main` — dirty (social slot lock removal + doc updates)
## What shipped today
## What shipped this session
- **Branding palette + assets recorded** — `ai.md` Brand section (references `MARCOM.md`), `MARCOM.md` gets the full hex palette and asset inventory
- **AI transparency statement** — `ai.md` Brand section documents AI-assisted pair-programming
- **Monetization model updated** — `ai.md` Monetization section rewritten: no feature lock, only watermark difference; all features free for everyone
- **TASKS.md updated** — TASK 10 re-scoped (Polar billing, watermark-only), tasks 15-18 added (stock bg images, infinity display fix, web source WebView2, local recording)
- **TASK 10c shipped** — social slot locks removed: `SocialsDialogViewModel` no longer takes `isPremium` parameter, slots 3-6 always available, tests updated
- **v1 feature completeness** — competitive gap analysis completed; 5 new tasks added (19-23) for table-stakes features: scene transitions, hotkeys, media source, audio sync offset, studio mode
- **MONETIZATION.md updated** — v1 feature completeness section added with competitive context
- **ai.md updated** — v1 task queue documented in Current limitations/TODOs section
### Branding (`2d5d2e9`)
- Replaced all llama logo assets with new ytLlive branded icons
- `tyllive-icon.png` → window icons + About overlay
- `tyllive-icon.ico` → Windows app icon (multi-size)
- `ytLlive-logo.png` → full branded logo with tagline
## Polar.sh resources (from earlier session)
| Resource | ID |
|---|---|
| Product | `d105dfa1-497e-423b-8cd4-e0ee2e3abbc0` |
| Price | `7c1b6f70-cf8c-46df-9bc8-6703f9fa64d0` |
| Benefit | `80f66bc1-d5a3-44a1-b9ea-1709ae508535` |
| Checkout | `70e037cd-9bcb-414e-8b26-c82c1d268f65` |
| LLAMAFOUNDER | `01a8c29f-9bb8-4e96-9cbe-417a6a5cec9b` |
| LLAMA50 | `8cff803e-053f-49b4-8d12-d74d468f2943` |
| Checkout URL | `https://buy.polar.sh/polar_cl_ueMy8AyAClO0o0yo19IroZwKTbD587gzUsjEW3bFapw` |
### Creator feedback batch (`28765a0`) — 9 items
1. About box bg → `#0A0C1A` (matches icon bg)
2. Elements panel collapses when nothing selected / scene changes
3. OpacityChip moved from preview overlay into Elements panel
4. Color picker fix: `is not SceneElement` (was `Source`, silently blocked `WebcamSceneConfig`)
5. Red left accent bar on selected items in Scene/Source lists
6. ✓ character → Path checkmark icon
7. Removed ToolTip from green check buttons
8. Created `bugs.md` with first entry (iTunes audio on mic meter)
9. Full logo in README + About box uses `ytLlive-logo.png`
## Build
```bash
taskkill.exe /F /IM ytLive.exe # if running
"/mnt/c/Program Files/dotnet/dotnet.exe" build "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.csproj"
"/mnt/c/Program Files/dotnet/dotnet.exe" vstest "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.Tests\bin\Debug\net8.0-windows10.0.19041.0\ytLive.Tests.dll"
```
### Layout (`32f8923` → `b9173f5`)
- Proportional column sizing: left/right at `1*`, preview at `2*` (50% growth rate)
- Side panels capped: left 360px max, right 400px max
## Next steps
1. **Build + test** — verify social slot lock removal compiles and tests pass
2. **TASK 9 item 4** — live chat (poll + render + badges) — already coded, just needs wiring
3. **TASK 20** — hotkeys (global keyboard shortcuts) — small scope, huge UX impact
4. **TASK 19** — scene transitions (Cut/Fade/Move/Custom) — already scoped
5. **TASK 17** — web source WebView2 (enables alert ecosystem)
6. **TASK 18** — local recording (independent)
7. **TASK 21** — media source (video file playback) — pairs with TRAX for non-Live scenes
8. **TASK 22** — audio sync offset — small, quality-of-life
9. **TASK 23** — studio mode (lightweight preview)
10. **TASK 15** — wait for user to provide stock scene background images
11. **TASK 16** — kill infinity display (depends on TASK 15)
## New files
- `Helpers/NullToVisibilityConverter.cs` — shows element when null
- `bugs.md` — bug tracker
- `Assets/tyllive-icon.png`, `Assets/tyllive-icon.ico`, `Assets/ytLlive-logo.png`
## v1 vision
> A streaming tool that's intuitive and ready to go out-the-gate with nothing held back.
> Minimal configuration — they're ready to go live or record. Eliminate the OBS pain-point completely.
## Modified files
- `Models/SceneElement.cs` — added `IsDraggable` virtual property
- `Models/Source.cs` — `IsDraggable => Type == SourceType.Image`
- `Models/WebcamSceneConfig.cs` — `IsDraggable => true`
- `ViewModels/MainViewModel.cs` — `IsElementsPanelEmpty` property
- `Themes/Controls.xaml` — accent bar on selected ListBoxItem
- `MainWindow.xaml` — elements restructure, layout columns, About bg
- `MainWindow.xaml.cs` — OpacityChip removal, color picker fix
- `README.md` — centered logo
## Next up
- **TASK 9 item 4 — live chat** (`liveChat/messages` poll, right-panel render, Super Chat + membership badges)
- Item 8 (iTunes on mic meter) — logged in `bugs.md`, likely acoustic coupling, not a code bug
## Open items
- Polar checkout page ~80% configured — needs logo upload via dashboard
- Logo upload API was broken (Polar server 500 on `/v1/files/{id}/uploaded`) — upload via dashboard instead
- Full codebase rename (ytLlive → LlamaCasty) still pending
+265 -13
View File
@@ -694,19 +694,27 @@ the validator → persisted), compositor bar overlay (top/bottom + above-flash),
---
## TASK 10 — Monetization: subscription billing + in-app unlock + support
## TASK 10 — Monetization: watermark-only subscription + Polar billing
**Goal:** annual subscription billing, in-app unlock seam, and in-app bug-report → git-issues support loop.
**Goal:** annual subscription via Polar.sh that removes the branding watermark. All features are
free — the only difference between free and paid is the watermark.
**Business details (pricing, billing research, revenue projections) in `MONETIZATION.md` (gitignored).**
**Business details (pricing, Polar product/checkout/discounts) in `MONETIZATION.md` (gitignored).**
### Status: 🔶 Scoped — nothing built
**Polar product:** `d105dfa1-497e-423b-8cd4-e0ee2e3abbc0` (LlamaCasty, $99/yr, currently `private`)
**Checkout:** `llamacasty.com` → Polar hosted page (~80% configured)
**Discounts:** `LLAMAFOUNDER` (100% off, 50 uses, `once`), `LLAMA50` (50% off, 12 months, unlimited)
1. ☐ **Billing provider decision** — Gumroad vs Lemon Squeezy (see MONETIZATION.md for comparison)
2. ☐ **Unlock mechanism** — license-key entry + online/offline verification + persisted entitlement (extends `LayoutStore`, schema bump) + renewal/lapse handling
3. ☐ **In-app bug-report → git issues** — built-in report control; good error codes first, optional log attachment with transparency about what's being sent
4. ☐ **"Unlock Premium" seam** — the About hub's greyed button (`PremiumUrl`) lights up and routes to billing/unlock
5. ☐ **Alerts gating** — Super Chat pop-ins gated behind subscription state
### Status: 🔶 Scoped — Polar infra live, in-app plumbing not built
1. ✅ **Billing provider** — Polar (polar.sh) chosen and configured. Product, price, license key benefit, discounts, checkout link all created.
2. ☐ **License key entry + verification UI** — About overlay or dedicated dialog; key entry field + validate against Polar API
3. ☐ **Offline entitlement** — cached license state in `LayoutStore` (schema bump); online check at startup, offline grace period
4. ☐ **BrandFlash → watermark toggle** — `BrandFlashEnabled = false` when licensed; the flash asset image needs to be created (the XAML `TextBlock` is preview-only, the compositor needs a `VideoFrame`)
5. ☐ **Remove social slot locks** — all 6 social bar slots open to everyone (no `IsPremium` gating on slots 3-6)
6. ☐ **"Unlock Premium" button** — `PremiumUrl` set to `llamacasty.com` checkout; `IsPremiumAvailable` becomes true
7. ☐ **Renewal/lapse handling** — key expiry check, grace period, re-prompt
8. ☐ **In-app bug-report → git issues** — built-in report control; good error codes first, optional log attachment
---
@@ -832,10 +840,254 @@ No changes to `MusicPlayer`, the 0.20 cap, the ducker, or the meter zones. Build
---
## Backlog (future versions)
## TASK 15 — Stock scene background images
1. v0.2 — Recording to local file (recordings carry the branding flash — see TASK 3 / `ai.md` Monetization)
2. v0.4 — Multi-destination restreaming (if needed; casual streamers may outgrow ytLlive first — "congrats, you're ready for OBS")
3. v0.5 — Stream clipping
**Goal:** branded background images for each canonical scene (Starting, Live, BRB, Chat, Ending)
that both provide working hints to creators and self-promote LlamaCasty.
### Status: ☐ Waiting on user to provide images
1. ☐ User provides 5 background images (one per canonical scene), PNG format, 1920×1080
2. ☐ Wire images as default `Background` sources, auto-populated on DB seed (empty DB → each scene gets its branded bg)
3. ☐ Assets stored in DB via `Asset` table (BLOB, SHA-256 keyed) — survives file deletion
4. ☐ Used by TASK 16 (infinity display fix) as the static fallback when no game is detected
### Design decisions
- Images are user-provided, not generated — the creator owns the brand look
- Each scene gets its own image (Starting = "Starting Soon", Live = live branding, BRB = "Be Right Back", etc.)
- The images double as the no-game backdrop fallback (TASK 16), so the Live scene image should work as a desktop replacement
---
## TASK 16 — Kill infinity display (no-game backdrop fallback)
**Goal:** when no full-screen game is detected, show a static branded placeholder instead of
capturing the primary display (which causes the infinity mirror effect).
### Status: ☐ Not started — depends on TASK 15
1. ☐ Modify `ResolveAutoCaptureKey()` (`MainViewModel.cs:1482`) — return null when no game detected
instead of falling back to primary monitor
2. ☐ Modify `EnsureBackdrop()` — when `CaptureKey` is null, show the scene's `Background` image
(the stock image from TASK 15) instead of creating a capture session
3. ☐ "Change Capture…" still available — user can manually pick a display/window via the picker
4. ☐ `ReacquireScreenCaptures()` handles null capture key gracefully (no `ScreenCaptureManager.AcquireAsync` call)
5. ☐ Update `ai.md` screen backdrop section to document the null-capture-key behavior
### Design decisions
- **Option A from user review:** static placeholder when no game, not multi-monitor fallback
- The user-provided stock bg image from TASK 15 IS the placeholder — no separate asset needed
- Manual capture pick still works — the user can always force a display capture via right-click → "Change Capture…"
- Single-monitor setups no longer show the infinity mirror
---
## TASK 17 — Web source rendering (WebView2)
**Goal:** make the web source actually render URLs into the preview and stream output.
### Status: ☐ Not started — required for v1
1. ☐ Add `Microsoft.Web.WebView2` NuGet package
2. ☐ Schema v10: `WebUri TEXT` column on `Source` table + migration in `LayoutStore.cs`
3. ☐ Persist `Source.WebUri` on save/load (currently in-memory only — lost on restart)
4. ☐ Hidden `WebView2` control per web source — navigates to `WebUri`, renders in-app
5. ☐ Frame capture from WebView2 (`CoreWebView2.CapturePreviewAsync` or `CompositionSurface`) → `VideoFrame` (BGRA8)
6. ☐ Wire into `FramePump` resolver — `Source { Type: WebSource }` → latest WebView2 frame
7. ☐ Wire into `SceneCompositor` — render web source as an image element at its position/size
8. ☐ Preview shows live web content (not just a blank rectangle)
9. ☐ Handle navigation errors, invalid URIs, timeout gracefully
### Design decisions
- WebView2 is the only option for Windows — it's pre-installed on Windows 10 20H2+ and Windows 11
- The web source is a standard element — positioned/sized/opacitied like any image source
- Frame capture rate can be lower than video FPS (5-10 fps for web content is fine)
- This enables Streamlabs/StreamElements overlays via web URLs
---
## TASK 18 — Local recording
**Goal:** record the stream output to a local file, with or without simultaneously streaming.
### Status: ☐ Not started — required for v1
1. ☐ Extend `EncoderOptions` with `OutputPath?` and recording mode flags
2. ☐ `FfmpegArgs.Build` gains a local-file branch: MP4/MKV container for file output
3. ☐ Three modes:
- **Record only** — no RTMP push, just local file (for pre-recorded content)
- **Stream only** — RTMP push, no local file (current behavior)
- **Stream + record** — dual output via `-f flv rtmp://... -f mp4 file.mp4`
4. ☐ Recording controls in UI (REC button, file path picker, duration display)
5. ☐ Schema migration for recording preferences (default output path, format)
6. ☐ Branding flash appears in local recordings too (free tier)
7. ☐ Output folder: `%APPDATA%\ytLlive\recordings\` with timestamped filenames
### Design decisions
- FFmpeg supports multiple outputs natively (`-f flv rtmp://... -f mp4 file.mp4`) — no second subprocess needed
- MP4 is the default container (widely compatible); MKV as an option (crash-safe, can be remuxed)
- Record-only mode is useful for pre-recorded content or testing without going live
- The branding flash carries into local recordings (free tier billboard extends to recordings)
---
## v1 execution order
The tasks below are ordered by dependency and risk. Each task builds on the previous.
1. **TASK 9.4** — Live chat (right panel) — wire `YouTubeChatService.Start()`, parse messages, render in panel. Foundation for chat box source.
2. **TASK 20** — Hotkeys — global keyboard shortcuts. Small scope, huge UX impact.
3. **TASK 19** — Scene transitions — Cut/Fade/Move/Custom. Depends on compositor.
4. **TASK 17** — Web source (WebView2) — enables alert ecosystem.
5. **TASK 18** — Local recording — independent, but pairs with stream.
6. **TASK 21** — Media source — video file playback for non-Live scenes.
7. **TASK 22** — Audio sync offset — small, quality-of-life.
8. **TASK 23** — Studio mode — lightweight preview before live.
9. **TASK 3.18** — Chat box source — renders chat ON the stream. Depends on TASK 9.4 (same message parsing).
10. **TASK 15** — Stock bg images — waiting on user.
11. **TASK 16** — Infinity display fix — depends on TASK 15.
12. **TASK 10** — Polar billing — in-app license key entry + watermark toggle.
13. **TASK 13** — Social media launch kit — after all v1 features ship.
---
## TASK 19 — Scene transitions
**Goal:** smooth visual transitions between scenes — the four types from the design spec (Cut, Fade, Move, Custom).
### Status: ☐ Not started — required for v1
1. ☐ `TransitionType` enum: `Cut`, `Fade`, `Move`, `Custom`
2. ☐ Global transition setting (`Cut` default, 300ms) with per-scene override
3. ☐ `TransitionDuration` property (configurable, default 300ms)
4. ☐ Compositor support — blend two frames during transition:
- **Cut:** instant switch (current behavior, zero cost)
- **Fade:** crossfade — source fades out, destination fades in
- **Move:** simple positional transition — source slides out, destination slides in (economical)
- **Custom:** media/stinger playback during transition (heavier, but creators pay for these)
5. ☐ Preview shows the transition (WYSIWYG) — the preview panel renders the same blend
6. ☐ Encoder output renders the transition too (the live stream sees it)
7. ☐ Transition state management in MainViewModel (`_isTransitioning`, current/next scene tracking)
8. ☐ Custom transitions: media resource loading (video file → frame sequence during transition)
9. ☐ UI: transition type picker (dropdown or buttons), duration slider
10. ☐ Tests: transition timing, frame blending, Cut is instant, Fade produces blended frames
### Design decisions
- **Cut is free** — instant switch, no blending, zero CPU cost. This is the default.
- **Fade is the minimum expectation** — 300ms crossfade is universal. Every streaming tool ships this.
- **Move is economical** — simple slide animation, no video decoding needed. Good middle ground.
- **Custom is premium** — media/stinger transitions require video playback. Heavy but expected by mid-tier streamers.
- Transitions happen in the compositor, not post-encode. The preview sees the same blend as the live output.
---
## TASK 20 — Hotkeys (keyboard shortcuts)
**Goal:** keyboard shortcuts for scene switching and common actions — the single biggest UX gap.
### Status: ☐ Not started — required for v1
1. ☐ `HotkeyManager` service — register/unregister hotkeys per scene + common actions
2. ☐ Scene switching hotkeys — F1-F5 for the 5 canonical scenes (configurable)
3. ☐ Common action hotkeys — Start/Stop stream, Mute/Unmute mic, Mute/Unmute game, Toggle TRAX
4. ☐ Hotkey configuration UI — Settings dialog or per-scene hotkey picker
5. ☐ Global hotkeys — work even when app is not focused (for gaming)
6. ☐ Conflict detection — warn when two actions share the same hotkey
7. ☐ Schema migration for hotkey bindings (persist across restarts)
8. ☐ Tests: hotkey registration, conflict detection, scene switch via hotkey
### Design decisions
- **Global hotkeys are mandatory** — streamers are in-game and cannot alt-tab. F1-F5 must work from anywhere.
- **Simple config** — one row per action: [Action] [Current Key] [Change]. No complex macro system.
- **No Stream Deck yet** — bare keyboard first. Stream Deck support (physical devices) is v1.1+.
---
## TASK 21 — Media source (video file playback)
**Goal:** play video files (MP4, MOV, AVI) into scenes — starting soon videos, BRB loops, intro/outro clips.
### Status: ☐ Not started — required for v1
1. ☐ `MediaSourceType` enum: `Video`, `Audio` (audio-only files via media source)
2. ☐ `SourceType.MediaSource` addition to the enum
3. ☐ `MediaSourceModel`: `FilePath`, `IsLooping`, `Volume` (0-1), `PlaybackState`
4. ☐ `VideoFrameSource`: FFmpeg-based video decoder → `VideoFrame` pipeline
5. ☐ Frame capture from video file (decode at native FPS, output BGRA8 frames)
6. ☐ Wire into `FramePump` resolver — `Source { Type: MediaSource }` → latest video frame
7. ☐ Wire into `SceneCompositor` — render media source as an image element at its position/size
8. ☐ Loop control — `IsLooping` property, restart on end
9. ☐ Volume control — per-source volume slider for audio playback
10. ☐ UI: file picker (filtered to video formats), loop toggle, volume slider
11. ☐ Schema migration for media source settings (file path, loop, volume)
12. ☐ Tests: video frame extraction, loop behavior, volume scaling, file validation
### Design decisions
- **FFmpeg handles all formats** — no codec-specific code. FFmpeg already in the project.
- **Audio plays through the desktop channel** — media source audio is captured by the WASAPI loopback (like TRAX/game audio). No separate audio routing needed.
- **This pairs with TRAX** — TRAX is background music, media source is background video. Together they make non-Live scenes (Starting/BRB/Ending) feel polished.
- **Not a full NLE** — no trimming, no multi-track, no effects. Just "play this video in the scene."
---
## TASK 22 — Audio sync offset
**Goal:** per-source audio delay compensation to prevent lip-sync drift from USB mics and capture cards.
### Status: ☐ Not started — required for v1
1. ☐ `AudioSyncOffset` property on AudioSource models (default 0ms, range -500ms to +500ms)
2. ☐ Apply offset in `AudioMixer` — delay or advance audio samples relative to video
3. ☐ UI: offset slider per audio source (or global offset for simplicity)
4. ☐ Persist offset in `LayoutStore` (schema migration)
5. ☐ Tests: offset application, positive/negative delay, boundary values
### Design decisions
- **Global offset first** — one setting for all audio sources. Per-source is v1.1+.
- **Simple slider** — -500ms to +500ms, default 0. No numeric input needed.
- **Visual feedback** — show a "sync OK" indicator when offset is applied.
---
## TASK 23 — Studio mode (lightweight preview)
**Goal:** preview the next scene before transitioning — lightweight version of OBS Studio Mode.
### Status: ☐ Not started — required for v1
1. ☐ "Preview" button per scene — clicking shows that scene in a preview pane without going live
2. ☐ Preview pane — dedicated area showing the selected scene (not the current live scene)
3. ☐ "Go" button — transitions from preview to live (applies the scene change)
4. ☐ Transition applies when switching — if Fade is selected, the preview-to-live transition uses it
5. ☐ Disable during live transitions — prevent rapid-fire preview switching while transitioning
6. ☐ Tests: preview isolation, go button triggers transition, disable during transition
### Design decisions
- **Lightweight implementation** — not the full OBS dual-canvas setup. Just a "preview this scene" button + a preview pane.
- **Optional feature** — creators can ignore it and just switch scenes directly (current behavior).
- **Pairs with transitions** — studio mode + transitions = professional workflow. Preview the fade, then commit.
---
## Backlog (future versions)
1. v1.1 — Stream Deck / Loupedeck integration (requires hotkey foundation from TASK 20)
2. v1.1 — Per-source audio sync offset (global offset ships in TASK 22)
3. v1.1 — Multiple profiles/presets (save different configs for different stream types)
4. v1.1 — Chroma key filter (green screen removal, or ONNX background removal)
5. v1.1 — Virtual camera output (Zoom/Discord/Teams)
6. v1.1 — Replay buffer (instant replay with hotkey)
7. v2 — Multi-destination restreaming (if needed; casual streamers may outgrow LlamaCasty first)
8. v2 — Stream clipping
9. v2 — Export/import settings
---
-1
View File
@@ -2626,7 +2626,6 @@ public class MainViewModel : ViewModelBase
SignInAsync,
SignOutYouTubeAsync,
_youtubeAuth.CurrentChannel,
IsPremium,
_socials);
var window = new ytLive.SocialsDialog(dialog) { Owner = Application.Current.MainWindow };
if (window.ShowDialog() == true)
+2 -5
View File
@@ -204,7 +204,6 @@ public sealed class SocialsDialogViewModel : ViewModelBase
private readonly ISocialValidator _validator;
private readonly Func<Task<YouTubeChannel?>> _signInProvider;
private readonly Func<Task> _signOutAction;
private readonly bool _isPremium;
private readonly List<DialogEntry> _working = new();
private readonly CancellationTokenSource _cts = new();
private YouTubeChannel? _account;
@@ -218,13 +217,11 @@ public sealed class SocialsDialogViewModel : ViewModelBase
Func<Task<YouTubeChannel?>> signInProvider,
Func<Task> signOutAction,
YouTubeChannel? account,
bool isPremium,
SocialsConfig? current = null)
{
_validator = validator;
_signInProvider = signInProvider;
_signOutAction = signOutAction;
_isPremium = isPremium;
_barEnabled = current?.BarEnabled ?? true;
_account = account;
_isSignedIn = account != null;
@@ -494,9 +491,9 @@ public sealed class SocialsDialogViewModel : ViewModelBase
slot.ProfileUrl = entry.ProfileUrl;
slot.FediverseSoftware = entry.FediverseSoftware;
}
else if (!_isPremium && i >= 2)
else if (i >= 2)
{
slot.IsLocked = true;
// Slot is empty — ready for user to add
}
}
slot.PropertyChanged += OnSlotPropertyChanged;
+48 -22
View File
@@ -1,8 +1,10 @@
# 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.
> status and YouTube API research in [`TASKS.md`](TASKS.md); brand/marketing
> palette and launch strategy in [`MARCOM.md`](MARCOM.md) (gitignored — read
> it before any branding or marketing work); directory maps in each folder's
> `index.md`. Reading order: this file → `TASKS.md` → `MARCOM.md` → `<dir>/index.md` → source.
## Response style
@@ -25,6 +27,20 @@ of reassurance. In that mode:
This is an occasional, explicitly-invoked mode — **never the default**. The
default response style above stays in effect unless invoked.
## Brand
Brand palette and official assets live in `MARCOM.md` (gitignored — read it for hex values,
asset file names, and marketing copy). The brand red `#e94560` is the primary accent used
throughout the UI.
### AI transparency
This project is built through AI-assisted pair-programming. All code is generated
under the supervision of an experienced developer with decades of coding experience.
Architecture decisions, product direction, and quality gates are human-owned;
AI accelerates implementation. This is a testimonial to successful human-AI
collaboration, not autonomous code generation.
## Run
```bash
@@ -113,9 +129,16 @@ C# / WPF (.NET 8) following MVVM:
- `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 + the social bar persist (SQLite, schema v8); the OAuth session persists (DPAPI); the paid-unlock state does not (yet — subscription entitlement verification pending, TASK 10)
- `YouTubeStreamService` manages the **variable reusable stream** (shipped 2026-08-16): `GetOrCreateReusableStreamAsync` lists `liveStreams?mine=true` and reuses the existing `cdn.isReusable` stream, creating it only on first use (`resolution=variable`, `frameRate=variable`); the stream is cached via `LayoutStore` (`SaveReusableStream`/`LoadReusableStream`, Settings table) and bound at broadcast insert (`contentDetails.boundStreamId`). Health (shipped 2026-08-16): `GetStreamHealthAsync(streamId)` polls `liveStreams?part=status` for `healthStatus` + `configurationIssues[]` → `StreamHealth`; banner decision in pure `StreamHealthReporter`
- `YouTubeStreamService` manages the **variable reusable stream** (shipped 2026-08-16): `GetOrCreateReusableStreamAsync` lists `liveStreams?mine=true` and reuses the existing `cdn.isReusable` stream, creating it only on first use (`resolution=variable`, `frameRate=variable`); the stream is cached via `LayoutStore` (`SaveReusableStream`/`LoadReusableStream`, Settings table) and bound at broadcast insert (`boundStreamId`). Health (shipped 2026-08-16): `GetStreamHealthAsync(streamId)` polls `liveStreams?part=status` for `healthStatus` + `configurationIssues[]` → `StreamHealth`; banner decision in pure `StreamHealthReporter`
- Webcam capture is shipped (milestone 1); the live desktop/game backdrop is shipped (ship task #1); **the output compositor (TASK 4 ship step 1) is SHIPPED**, **the FFmpeg locator (TASK 4 ship step 2) is SHIPPED**, **the encoder + RTMP push (TASK 4 ship step 3) is SHIPPED**, **WASAPI audio capture (TASK 4 ship step 4) is SHIPPED** — full plan in `TASKS.md`; the frame-pipeline wiring follows (its own PR)
- `StreamConfig` defaults (`TargetBitrate=6000`, `Resolution="1920x1080"`) are stale — the live dropdown drives `StreamHealth.CurrentBitrate`/`FPS` instead
- **v1 task queue (2026-08-19):** 5 new tasks added for v1 feature completeness:
- TASK 19: Scene transitions (Cut/Fade/Move/Custom) — required for professional polish
- TASK 20: Hotkeys (global keyboard shortcuts) — the single biggest UX gap
- TASK 21: Media source (video file playback) — starting soon videos, BRB loops
- TASK 22: Audio sync offset — lip-sync correction for USB mics/capture cards
- TASK 23: Studio mode (lightweight preview) — preview next scene before live
- These were identified through competitive analysis and are table-stakes for any streaming software in 2025-2026. Full details in `TASKS.md`.
### Screen backdrop capture (TASK 3 ship task #1)
@@ -589,38 +612,41 @@ Apply this to every UI decision:
## Monetization (design decision — the branding flash is the sword)
Free forever: all streams unlimited, no time caps, no per-feature paywalls. The **one paid line is an
annual subscription** (settled 2026-08-14 — replaces the earlier "one-time unlock" shape):
Full pricing, discount codes, and Polar product details in `MONETIZATION.md` (gitignored).
Brand palette and assets in `MARCOM.md` (gitignored). This section covers the in-app model.
- **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
Free forever: **all features unlocked for everyone** — no feature lock between free and paid.
The **only difference is watermarking**. This is deliberate: no creator will tolerate a watermark,
and as a good-guy developer, we give them complete access to every feature so no one can call us
crooked.
- **Free:** a periodic full-frame branding flash — "made with LlamaCasty!" 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 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.
**The flash is also the free tier's billboard** — every free stream advertises ytLlive to its own
viewers; the free tier is distribution, not compromise.
- **Paid (annual subscription):** branding flash removed (flips `BrandFlashEnabled` off) + **Alerts**
(Super Chat / membership / subscribe pop-ins). Alerts is the recurring-value engine — a live-API
feature we maintain forever, which is the strongest argument for a yearly bill.
**The flash is also the free tier's billboard** — every free stream advertises LlamaCasty to its
own viewers; the free tier is distribution, not compromise.
- **Paid (annual subscription):** branding flash removed (flips `BrandFlashEnabled` off). That's it.
No feature gating. Alerts, social bar slots, voice filters, TRAX, recording — everything is free.
- **Pricing:** early-access founders rate **$49.99/yr** → **$99/yr list at GA** (v1). **Grandfathering:
early adopters keep $49.99/yr for as long as the subscription is maintained**; a lapse means renewal
at list. That's the whole policy — no escalation matrix (a realistic product lifetime is a few years;
keep the promise simple).
- **Billing:** NOT locked to itch.io (creator's call 2026-08-14). Hunted fact — **itch.io has no native
subscription billing** (no annual/recurring product billing; only pay-what-you-want, pre-orders,
early-access, keys, and a Patreon integration). **Chosen platform: Polar (polar.sh)** — open-source
MoR (Apache 2.0), handles payments, subscriptions, license keys, and global tax compliance.
Startup Program gives Scale plan free for 12 months. Details in `MONETIZATION.md`.
- **Billing:** **Polar (polar.sh)** — open-source MoR (Apache 2.0), handles payments, subscriptions,
license keys, and global tax compliance. Startup Program gives Scale plan free for 12 months.
Product: `d105dfa1-497e-423b-8cd4-e0ee2e3abbc0`. Checkout: `llamacasty.com` → Polar hosted page.
Discounts: `LLAMAFOUNDER` (100% off, 50 uses), `LLAMA50` (50% off, 12 months). Details in `MONETIZATION.md`.
- **Support (creator's model):** in-app bug-reporting mechanism → issues into git; most queries are
how-tos / feature requests / manual-skimmers. Maintenance cadence = "when I get around to it" with
emergency patches; not a 24/7 service promise.
Deliberately rejected: always-on watermark (obscurable — replaced by the flash), hard stream-time
**What was 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.
routinely run 2-4 hours), soft-limit nagging, freemium feature 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
+13 -20
View File
@@ -77,7 +77,6 @@ public class SocialsDialogViewModelTests
ISocialValidator? validator = null,
SignInFake? signIn = null,
YouTubeChannel? account = null,
bool isPremium = false,
SocialsConfig? current = null)
{
return new SocialsDialogViewModel(
@@ -85,7 +84,6 @@ public class SocialsDialogViewModelTests
signIn != null ? signIn.Next : () => Task.FromResult<YouTubeChannel?>(null),
SignOut,
account,
isPremium,
current);
}
@@ -98,14 +96,14 @@ public class SocialsDialogViewModelTests
var signIn = new SignInFake();
var vm = Create(validator, signIn);
// Gate: signed out, freemium → row 0 is the sign-in prompt, rows 2-5 locked.
// Gate: signed out → row 0 is the sign-in prompt, all slots available.
Assert.True(vm.ShowSignInBanner);
Assert.Equal(6, vm.Slots.Count);
Assert.True(vm.Slots[0].IsSignIn);
Assert.False(vm.Slots[0].IsLocked);
Assert.False(vm.Slots[1].IsLocked);
Assert.True(vm.Slots[2].IsLocked);
Assert.True(vm.Slots[5].IsLocked);
Assert.False(vm.Slots[2].IsLocked);
Assert.False(vm.Slots[5].IsLocked);
Assert.True(vm.CanSave);
// Row 1: add a validated X handle. Save is blocked while input is unvalidated.
@@ -121,16 +119,15 @@ public class SocialsDialogViewModelTests
Assert.False(vm.Slots[1].IsEditing);
Assert.True(vm.CanSave);
// Rows 2-5 are locked on the free tier — starting an edit there is blocked.
// Rows 2-5 are available — starting an edit there works.
vm.StartEdit(2);
Assert.False(vm.Slots[2].IsEditing);
Assert.True(vm.Slots[2].IsLocked);
Assert.True(vm.Slots[2].IsEditing);
Assert.False(vm.Slots[2].IsLocked);
// Delete the X entry: the slot empties and unlocks nothing else.
// Delete the X entry: the slot empties.
await vm.DeleteSlotAsync(1);
Assert.False(vm.Slots[1].IsFilled);
Assert.False(vm.Slots[1].IsLocked);
Assert.True(vm.Slots[2].IsLocked);
// Delete row 0 while signed out is a no-op.
await vm.DeleteSlotAsync(0);
@@ -195,16 +192,12 @@ public class SocialsDialogViewModelTests
// ── Unit tests ──
[Fact]
public void Freemium_LocksSlotsBeyondTwo_PremiumUnlocksAll()
public void AllSlots_AlwaysUnlocked()
{
var free = Create();
Assert.True(free.Slots[2].IsLocked);
Assert.True(free.Slots[5].IsLocked);
Assert.False(free.Slots[1].IsLocked);
var premium = Create(isPremium: true);
Assert.All(premium.Slots, s => Assert.False(s.IsLocked));
Assert.False(premium.Slots[5].IsLocked);
var vm = Create();
Assert.All(vm.Slots, s => Assert.False(s.IsLocked));
Assert.False(vm.Slots[2].IsLocked);
Assert.False(vm.Slots[5].IsLocked);
}
[Fact]
@@ -268,7 +261,7 @@ public class SocialsDialogViewModelTests
Assert.Equal("Fresh Channel", vm.Slots[0].Handle);
Assert.Equal(SocialService.Twitch, vm.Slots[1].Service);
Assert.Equal("oldstreamer", vm.Slots[1].Handle);
Assert.True(vm.Slots[2].IsLocked);
Assert.False(vm.Slots[2].IsLocked);
vm.SaveCommand.Execute(null);
Assert.Equal(2, vm.CommittedEntries!.Count);