From e97406d64920af7036283bb7fbe86f44997a9e09 Mon Sep 17 00:00:00 2001 From: gramps Date: Wed, 19 Aug 2026 11:51:32 -0700 Subject: [PATCH] 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 --- HANDOFF.md | 89 ++++--- TASKS.md | 278 +++++++++++++++++++- ViewModels/MainViewModel.cs | 1 - ViewModels/SocialsDialogViewModel.cs | 7 +- ai.md | 70 +++-- ytLive.Tests/SocialsDialogViewModelTests.cs | 33 +-- 6 files changed, 375 insertions(+), 103 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 4091e4f..cb71b73 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -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 diff --git a/TASKS.md b/TASKS.md index f6b4a0d..6a9b173 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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 --- diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index d4e4a21..b367556 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -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) diff --git a/ViewModels/SocialsDialogViewModel.cs b/ViewModels/SocialsDialogViewModel.cs index e070ad1..08864f2 100644 --- a/ViewModels/SocialsDialogViewModel.cs +++ b/ViewModels/SocialsDialogViewModel.cs @@ -204,7 +204,6 @@ public sealed class SocialsDialogViewModel : ViewModelBase private readonly ISocialValidator _validator; private readonly Func> _signInProvider; private readonly Func _signOutAction; - private readonly bool _isPremium; private readonly List _working = new(); private readonly CancellationTokenSource _cts = new(); private YouTubeChannel? _account; @@ -218,13 +217,11 @@ public sealed class SocialsDialogViewModel : ViewModelBase Func> signInProvider, Func 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; diff --git a/ai.md b/ai.md index 32cb647..79c64ec 100644 --- a/ai.md +++ b/ai.md @@ -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` → `/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` → `/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 diff --git a/ytLive.Tests/SocialsDialogViewModelTests.cs b/ytLive.Tests/SocialsDialogViewModelTests.cs index 1d03f1c..695a6f6 100644 --- a/ytLive.Tests/SocialsDialogViewModelTests.cs +++ b/ytLive.Tests/SocialsDialogViewModelTests.cs @@ -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(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);