feat(backdrop): Capture Window… in-app window pin (TASK 38, Good Dog ONE test)

Click-to-pin an open window as the full-bleed Live backdrop, via the existing
window:<hwnd> capture path (same render layer as game/desktop). Session-scoped:
window:/picker: keys heal to auto on reload (HWND-recycle hazard); pin wins
while the window enumerates, then auto-fallbacks (game → desktop → static); a
dead window's capture session heals to auto, no dead ends.

Fix: ScreenCaptureSourceFactory.Resolve hex-parsed "window:0x<Hwnd>" WITHOUT
stripping the 0x prefix — NumberStyles.HexNumber rejects it, so every pin
resolved null ("No capture target for this key") and the heal silently rolled
back. Same flaw in IsAliveWindowPin (pin-wins guard was dead). Both fixed.

Commit also carries an out-of-scope prerequisite: PillRadioTests.cs:105 had a
committed stray token ("...PrimaryStartButtonLabel soil;", CS1003) blocking the
whole test-project compile — token removed.

Plus the previous session's uncommitted pricing docs (one-time $29/$49) that
share TASKS.md/ai.md/HANDOFF.md.
This commit is contained in:
2026-09-22 07:00:47 -07:00
parent 523c17b7da
commit 4e0915a36e
19 changed files with 665 additions and 67 deletions
+32 -11
View File
@@ -348,6 +348,22 @@ This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Backgr
a source; `PickAsync()` shows the OS `GraphicsCapturePicker` ("Change Capture…", owner window set via the
`IInitializeWithWindow` ComImport) and returns a **transient** `picker:` key — a reload falls back to
auto-detection.
- **"Capture Window…" — in-app window pin (TASK 38, 2026-09-21):** the Live backdrop context menus
(PreviewPane canvas + layer-row) offer a submenu listing visible top-level windows of other processes
(`Win32WindowEnumerator`, an `EnumWindows` pass filtered like `Win32FullScreenDetector`:
visible, non-cloaked via `DWMWA_CLOAKED`, non-empty rect, other-process, titled). Picking one runs
`RedesignateBackgroundAsync("window:0x{hwnd:X}")` — the same full-bleed layer-0 capture path as the
game/desktop, keyed straight into `ScreenCaptureSourceFactory` (`window:` already resolved there).
**Session-scoped, never persisted:** `window:` (and `picker:`) keys are `IsTransientCaptureKey` —
`ReacquireScreenCaptures` strips them before auto-detection on every load, because an HWND can be
recycled to an unrelated window after restart (resurrecting a dead pin could capture the wrong
window). **Pin wins while alive:** `OnFullscreenMonitorChanged` skips re-designation while
`IsAliveWindowPin(current.CaptureKey)` is true — the auto game/desktop detector never steals an
explicit pin. **No dead ends:** if the pinned window's capture session dies mid-session,
`OnScreenCaptureFailed` (transient key only) re-runs `ReacquireScreenCaptures` → auto-fallback
(fullscreen game → desktop → static art). Seam: `IWindowEnumerator` + `WindowInfo`
(`Label = "Title — Process"`), `WindowEnumeratorOverride` static test seam mirrors
`CameraEnumeratorOverride`.
- **CsWinRT projection gaps hand-rolled:** `Windows.Graphics.Direct3D11.Direct3D11Helper` is not projected,
so `Direct3D11Helper` P/Invokes `d3d11.dll!D3D11CreateDevice` (hardware, BGRA_SUPPORT, explicit 11.1-first
feature array) → QI `IDXGIDevice` → the WinRT interop export
@@ -358,8 +374,8 @@ This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Backgr
`IInitializeWithWindow` are ComImports in `CaptureInterop.cs`. All WinRT projections were verified by
reflection against the built `Microsoft.Windows.SDK.NET.dll` before writing the interop.
- **Known v1 limits:** full-desktop captures are CPU-copied at native resolution (GPU downscale = encoder
task); window capture (`window:<hwnd>`) and multi-monitor live re-targeting beyond the auto-detected
game are behind the picker; picker-based captures don't survive reload.
task); multi-monitor live re-targeting beyond the auto-detected game is behind the picker; picker- and
window-pin captures don't survive reload (session-scoped by design — see the "Capture Window…" bullet).
- **Focus-loss capture lag (known OS limit, NOT an in-app throttle — deferred):** when the app window loses
focus the preview visibly slows (mouse-movement lag). Nothing in the repo checks focus to slow capture —
the only focus hooks (`FullscreenMonitorChanged` event + `RefreshBackdropAutoCapture`) re-target the backdrop, never
@@ -1384,22 +1400,27 @@ crooked.
never composited onto the live output or local recording — test VODs stay clean (same channel-
protection stance as the visibility lock), and creators can be shown what free looks like without
it ever touching a real broadcast. Flipping the flash live-on is a **TASK 36** unlock item.
- **Paid (annual subscription):** branding flash removed (flips `BrandFlashEnabled` off). That's it.
- **Paid (one-time perpetual license):** 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:** **Polar (polar.sh)** — open-source MoR (Apache 2.0), handles payments, subscriptions,
- **Pricing (2026-09-21 — switched from subscription to one-time "own it"):** **$29 lifetime** founder's
price (launch → 90 days) → **$49 lifetime** list at GA. The key is **perpetual** (`IsPremium` never
lapses; the old renewal/lapse path is dead). Rationale: a local app with no per-user server cost and a
renewal-averse, free-surrounded audience — minimize commitment size, don't charge rent. Accessibility is
the driver: the free tier is the *full* app (only the watermark differs), so a broke new streamer pays $0;
the low one-time price is the "no recurring bill" entry. Full reasoning, and the dropped $49.99/yr →
$99/yr subscription model, in `MONETIZATION.md`.
- **Billing:** **Polar (polar.sh)** — open-source MoR (Apache 2.0), handles payments, **one-time orders**,
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.
Product: `d105dfa1-497e-423b-8cd4-e0ee2e3abbc0` (⚠️ currently a $99/yr *subscription* product — must be
re-created as a one-time product for the new model). Checkout: `llamacasty.com` → Polar hosted page.
Org ID: `c05fb364-b967-4f6c-adf2-8a144e46d085` (org-scoped OAT — `organization_id` omitted from API calls).
Key prefix: `LCYT-`. Discounts: `LLAMAFOUNDER` (100% off, 50 uses), `LLAMA50` (50% off, 12 months). Details in `MONETIZATION.md`.
- **Support (creator's model, corrected 2026-09-01):** support = email + GitHub issues — the
once-planned in-app bug-reporter is **out of product** (TASKS.md → closed list). 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. The only license chatter is the paid-user expiry
reminder (TASK 36 item 5); active subscribers see zero license UI.
emergency patches; not a 24/7 service promise. The only license chatter is the paid-user **expiry
reminder — now moot** (perpetual keys never expire, one-time model 2026-09-21); paid users see zero
license UI. TASK 36 item 5 (expiry reminder) is superseded.
**Monetization awareness (built-in, ungated, free — 2026-09-01; v1 SCOPE per the complete-v1
ruling — build order: capture → report → journey → Alerts):** the app is monetization-aware by