Monetization policy locked + dev pause recorded (docs changeset, 2026-08-14): the one paid line becomes an annual subscription — early access $49.99/yr → $99/yr list at GA, grandfather-while-subscribed with lapse → list on renewal (no escalation matrix); free tier unchanged (branding flash is the free tier's billboard + no Alerts, which is the one paid feature); billing NOT locked to itch.io (hunted fact: itch.io has no native subscription billing — candidates Gumroad, whose native affiliate program is the tiebreaker, and Lemon Squeezy); support = in-app bug-report → git issues with a when-I-get-around-to-it cadence + emergency patches. ai.md Monetization section rewritten + stale paid-unlock seams fixed (L115 subscription-entitlement wording, the About hub's tabled billing PremiumUrl seam); TASKS.md gains TASK 23 (Monetization — billing provider decision, unlock mechanism with lapse→flash-returns enforcement, in-app bug-report loop, Unlock Premium seam lighting, Alerts gating, and line-item pointers for the early-access strategy, affiliate/ads phase 2, and the channel revival); HANDOFF.md records the decisions + the development pause (resume streaming = pair-programming dev streams via OBS + The Division 2 under the gramps/GOAT persona, 67 and gaming since 1981, 3D-printing content retired; dogfood switch to ytLlive once TASK 5 lands; resume point = TASK 5). Stale itch.io assertions corrected across comments and indexes (MainViewModel.cs PremiumUrl/IsPremium seams, MainWindow.xaml tooltip, AboutHubTests comment, ViewModels/index.md, TASK 9 out-of-scope note). Build 0 warnings, 196 tests passing

This commit is contained in:
2026-08-14 18:48:42 -07:00
parent f2f6401fef
commit 86fcd868a8
7 changed files with 110 additions and 47 deletions
+34 -30
View File
@@ -7,35 +7,39 @@
## Session state (last updated: 2026-08-14) ## Session state (last updated: 2026-08-14)
- **Branch:** `main` (audio milestone work is uncommitted, see "Uncommitted"). - **Branch:** `main`, tracking `origin/main` at **`f2f6401`** (pushed). Working tree was clean until the
- **AUDIO MILESTONE (TASK 9) — SHIPPED this session.** Real stream audio + voice filters + auto-duck monetization docs changeset below.
+ free TRAX background music. Build **0 warnings**, full suite **196 passing** (ONE integration - **AUDIO MILESTONE (TASK 9) — SHIPPED + COMMITTED + PUSHED this session.** Real stream audio + voice
test: `AudioPipelineTests.Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe`). Everything below in the filters + auto-duck + free TRAX background music. Build **0 warnings**, full suite **196 passing**
milestone section is DONE. **NOT yet committed** — next step is review + commit (with `ai.md`/ (ONE integration test: `AudioPipelineTests.Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe`). Commits
`TASKS.md`/this file already updated in the same changeset). **`6d71ace`** (milestone) + **`f2f6401`** (secrets cleanup), force-pushed (`725f5a7...f2f6401`).
- **Two build-time deviations from the settled plan (both recorded in ai.md + TASKS.md):** - **Secrets scrubbed from git history.** The DO token + passwords recorded in `HANDOFF.md` were removed
1. `MusicPlayer` uses **`WaveOutEvent`** (new sibling package `NAudio.WinMM` 2.2.1) — not `WasapiOut` from the working file and **purged from all history** via `git filter-branch` over `3d92bd0..main`
as planned; `WaveOutEvent` isn't in `NAudio.Wasapi`/`NAudio.Core`. + `git gc --prune=now --aggressive` (scan of all refs = 0 hits). They still exist in chat — **keep
2. No forced 48 kHz capture — NAudio 2.2.1's `WasapiCapture` has no overridable treating as compromised**; rotate the DO API token. Old objects remain in llgit's store until server
`GetDefaultMixFormat`, so both sources run the device mix format and the mixer's GC (unreachable, not fetchable). If a value is ever needed, `git reflog` is expired — it's gone.
`TinyResampler` normalizes any rate to 48 kHz (resampler IS the design, not a fallback). - **MONETIZATION LOCKED (2026-08-14, creator).** One paid line = **annual subscription**: early access
3. `FfmpegEncoder.cs` is **untouched** — the VM closes the audio pipe (`_audioMixer.StopLive()`) in **$49.99/yr** → **$99/yr list at GA**, **grandfather-while-subscribed**, lapse → list on renewal.
`StopStream` BEFORE the frame pump stops (audio EOF then video EOF, in order). Free tier unchanged (branding flash = the billboard + no Alerts; Alerts is the paid feature).
- **SERVER RECOVERY (llamachile.tube / YunoHost / DO droplet) — done earlier session.** Side task **Billing NOT itch-locked** — itch.io has no native subscription billing; candidates Gumroad (native
complete; **rotate the DO API token** (control-panel only) and any password no longer trusted. affiliates = tiebreaker) / Lemon Squeezy. Support = in-app bug-report → git issues, "when I get
- **Shipped, all pushed before this session:** TASK 21 (creator-hub About, `3d92bd0`), TASK 8 (meter around to it" + emergency patches. Full policy in `ai.md` → Monetization; scoped plan + line-items
+10 dB), TASK 4 ship step 7 (one-click go-live + private-only). **174 tests passing, 0 warnings.** in `TASKS.md` **TASK 23**.
- **Uncommitted (whole TASK 9 milestone):** `Services/Audio/` (new `VoiceFilterChain.cs`, - **DEVELOPMENT PAUSED (creator's call 2026-08-14) — resuming streaming instead.** Channel revival =
`AudioRingBuffer.cs`, `AutoDucker.cs`, `TinyResampler.cs`, `MusicPlayer.cs`, `NamedPipeAudioWriter.cs`, **pair-programming dev streams via OBS** (ytLlive can't go live yet: private-only + `_rtmpUrlProvider`
rewritten `AudioMixer.cs`, simplified `WasapiMicAudioSource.cs`), `Models/Music.cs` (new), null) + **The Division 2** gaming streams under the **"gramps/GOAT"** persona (67, gaming since 1981).
`FfmpegArgs.cs`, `EncoderOptions.cs`, `LayoutStore.cs` (v9), `MainViewModel.cs`, `MainWindow.xaml`, 3D-printing content retired (printer hard down, novelty gone). **Dogfood switch** later: when TASK 5 +
`MainWindow.xaml.cs`, `ytLive.csproj` (NAudio.WinMM), `ytLive.Tests/` (`AudioPipelineTests.cs` new, go-live capability land, stream *with* ytLlive — Division 2 (loud game + voice) is the demo for the
`FfmpegEncoderTests.cs` + `LayoutStorePersistenceTests.cs` updated), and docs `ai.md`, `TASKS.md`, ducking/voice-filter story. Early-access strategy + affiliate/ads are their own future line-item tasks.
`HANDOFF.md`. - **Docs changeset in flight (THIS changeset):** `ai.md` Monetization rewritten + stale paid-unlock seams
- **Next step:** commit the milestone (docs already staged in the same changeset), push, then start fixed; `TASKS.md` TASK 23 added + TASK 21's itch.io URL mention corrected; this file rewritten. Commit
**TASK 5** (reusable stream → `_rtmpUrlProvider` — the last blocker before go-live actually encodes). + push as one change.
Then **itch.io product page setup** (premium URL lands in `MainViewModel.PremiumUrl`, lights up - **Shipped, all pushed:** TASK 21 (creator-hub About, `3d92bd0`), TASK 8 (meter +10 dB), TASK 4 ship
Unlock Premium). Optional, not queued: rewriting the healed entry's `ProfileUrl` to step 7 (one-click go-live + private-only), TASK 9 audio milestone (`6d71ace`), secrets cleanup
(`f2f6401`).
- **Resume point (when dev starts again):** **TASK 5** — reusable stream → `_rtmpUrlProvider` (the last
blocker before go-live actually encodes + pushes). Then TASK 23 (billing + unlock + support).
Optional, not queued: rewriting the healed entry's `ProfileUrl` to
`https://mastodon.llamachile.tube/@gramps` (user must say the word). `https://mastodon.llamachile.tube/@gramps` (user must say the word).
## TASK 9 audio milestone — SHIPPED 2026-08-14 (what changed) ## TASK 9 audio milestone — SHIPPED 2026-08-14 (what changed)
@@ -67,7 +71,7 @@ and `ai.md` ("Live audio capture"). Highlights:
start-of-stream silence race was seen and eliminated). start-of-stream silence race was seen and eliminated).
**Out of scope this branch:** IP webcam, chat box, alt-key crop, credits, bg removal, music-off-VOD **Out of scope this branch:** IP webcam, chat box, alt-key crop, credits, bg removal, music-off-VOD
track (YouTube mutes VODs with copyrighted music — future feature), itch.io `PremiumUrl`. track (YouTube mutes VODs with copyrighted music — future feature), `PremiumUrl` (TASK 23 seam).
- **Landmines:** - **Landmines:**
- Never add another test that constructs `new App()` — use `RealAppHost.Run(...)` (shared STA host - Never add another test that constructs `new App()` — use `RealAppHost.Run(...)` (shared STA host
+1 -1
View File
@@ -1158,7 +1158,7 @@
<Button Content="Unlock Premium" Command="{Binding OpenPremiumCommand}" <Button Content="Unlock Premium" Command="{Binding OpenPremiumCommand}"
Style="{StaticResource YtButtonSecondary}" Style="{StaticResource YtButtonSecondary}"
IsEnabled="{Binding IsPremiumAvailable}" IsEnabled="{Binding IsPremiumAvailable}"
ToolTip="Coming soon — the itch.io unlock page is being set up"/> ToolTip="Coming soon — subscription billing is on the way (TASK 23)"/>
</StackPanel> </StackPanel>
<Button Content="Licenses &amp; legal" Command="{Binding OpenLicensesCommand}" <Button Content="Licenses &amp; legal" Command="{Binding OpenLicensesCommand}"
+47 -2
View File
@@ -115,7 +115,7 @@ Lifecycle: `created → ready → [testing] → live → complete` (transitional
18.**Chat box** — YouTube live chat rendered *on* the stream so viewers read along in-video 18.**Chat box** — YouTube live chat rendered *on* the stream so viewers read along in-video
19.**Background removal (milestone 2)** — ONNX Runtime + DirectML, MediaPipe Selfie Segmentation — deliberately NOT in this build 19.**Background removal (milestone 2)** — ONNX Runtime + DirectML, MediaPipe Selfie Segmentation — deliberately NOT in this build
20.**Alerts** — Super Chat / membership / subscribe pop-ins; build after the six; **the one paid feature** (see Monetization in `ai.md`) 20.**Alerts** — Super Chat / membership / subscribe pop-ins; build after the six; **the one paid feature** (see Monetization in `ai.md`)
21.**Logo + richer in-app About (2026-08-13, queued → 2026-08-14 SHIPPED)** — the ytLlive wordmark in the top bar opens the About overlay (already wired); the About overlay is now the **creator hub**: the real logo (the "llama fortnite superman logo" from the creator's vault, copied to `Assets/llama-logo.png` — the creator's own art, no third-party license), plus **creator-hub links** — llama chile shop on YouTube (`MainViewModel.ChannelUrl`), Mastodon (`https://mastodon.llamachile.tube/@gramps`), **Buy me a coffee** (`https://buymeacoffee.com/llamachiley` — live), and **Unlock Premium** (greyed "coming soon" — the itch.io product URL is a tabled seam, `PremiumUrl`, until the account is set up). A **Licenses & legal** button flips the About overlay to an in-app scrolling panel that loads the full `THIRD-PARTY-NOTICES.txt` text (`MainViewModel.ShowLicenses` reads the shipped file from the executable directory on first open; graceful "not found" fallback — **never the OS viewer, everything stays in-app**); "← Back to About" returns. The About overlay is also the in-app home of the notices — the top-bar About button that opened the file in the OS viewer was removed on 2026-08-13 for exactly this reason. Integration test (Good Dog Rule — ONE): `AboutHubTests.About_Opens_InApp_Licensing_Loads_Shipped_Notices` drives the real window + VM, asserting the hub opens, the link URLs are real, the licensing panel loads the shipped notices text (contains "Third-Party Notices" + "LGPL"), and back returns to the hub. 174 tests passing, 0 warnings 21.**Logo + richer in-app About (2026-08-13, queued → 2026-08-14 SHIPPED)** — the ytLlive wordmark in the top bar opens the About overlay (already wired); the About overlay is now the **creator hub**: the real logo (the "llama fortnite superman logo" from the creator's vault, copied to `Assets/llama-logo.png` — the creator's own art, no third-party license), plus **creator-hub links** — llama chile shop on YouTube (`MainViewModel.ChannelUrl`), Mastodon (`https://mastodon.llamachile.tube/@gramps`), **Buy me a coffee** (`https://buymeacoffee.com/llamachiley` — live), and **Unlock Premium** (greyed "coming soon" — the **billing** product URL is a tabled seam, `PremiumUrl`, until TASK 23 picks the subscription provider). A **Licenses & legal** button flips the About overlay to an in-app scrolling panel that loads the full `THIRD-PARTY-NOTICES.txt` text (`MainViewModel.ShowLicenses` reads the shipped file from the executable directory on first open; graceful "not found" fallback — **never the OS viewer, everything stays in-app**); "← Back to About" returns. The About overlay is also the in-app home of the notices — the top-bar About button that opened the file in the OS viewer was removed on 2026-08-13 for exactly this reason. Integration test (Good Dog Rule — ONE): `AboutHubTests.About_Opens_InApp_Licensing_Loads_Shipped_Notices` drives the real window + VM, asserting the hub opens, the link URLs are real, the licensing panel loads the shipped notices text (contains "Third-Party Notices" + "LGPL"), and back returns to the hub. 174 tests passing, 0 warnings
22.**Voice filters on the mic channel (2026-08-13 queued → SHIPPED 2026-08-14 inside TASK 9, the audio milestone)** — the standard four applied to the sound input path (before the meter/encoder mix): **bass boost, treble, noise suppression, compressor** (set decided with the creator 2026-08-13). Noise suppression = a **pure-C# noise gate** (creator chose over RNNoise / a second ffmpeg `afftdn` pipe, 2026-08-14 — KISS). Always-on — no UI knobs; the mic stays the creator's single audio control 22.**Voice filters on the mic channel (2026-08-13 queued → SHIPPED 2026-08-14 inside TASK 9, the audio milestone)** — the standard four applied to the sound input path (before the meter/encoder mix): **bass boost, treble, noise suppression, compressor** (set decided with the creator 2026-08-13). Noise suppression = a **pure-C# noise gate** (creator chose over RNNoise / a second ffmpeg `afftdn` pipe, 2026-08-14 — KISS). Always-on — no UI knobs; the mic stays the creator's single audio control
### The Minimal Source Set (design decision — do not expand casually) ### The Minimal Source Set (design decision — do not expand casually)
@@ -554,7 +554,7 @@ the validator → persisted), compositor bar overlay (top/bottom + above-flash),
**Testing (Good Dog Rule — ONE integration test):** `AudioPipelineTests.Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe` — real filter chain + mixer + ducker + `NamedPipeAudioWriter`, fake `IAudioSource`s, test-side `NamedPipeClientStream` reads the bytes and asserts post-filter/post-gain/post-duck mixed stereo (ring buffers pre-filled so the first pipe tick already carries real audio — avoids a start-of-stream silence race). Units: each DSP stage (known sine-in → expected gain), ring buffer wrap/underflow/overwrite, ducker envelope, resampler (down/up/stateful/identity), `FfmpegArgs` pipe+map, schema v9 roundtrip + no-music-row. Existing tests stay green. **Testing (Good Dog Rule — ONE integration test):** `AudioPipelineTests.Mix_WithFiltersDuckAndGain_Lands_On_AudioPipe` — real filter chain + mixer + ducker + `NamedPipeAudioWriter`, fake `IAudioSource`s, test-side `NamedPipeClientStream` reads the bytes and asserts post-filter/post-gain/post-duck mixed stereo (ring buffers pre-filled so the first pipe tick already carries real audio — avoids a start-of-stream silence race). Units: each DSP stage (known sine-in → expected gain), ring buffer wrap/underflow/overwrite, ducker envelope, resampler (down/up/stateful/identity), `FfmpegArgs` pipe+map, schema v9 roundtrip + no-music-row. Existing tests stay green.
**Out of scope (this branch):** IP webcam (video-only when it lands — never an audio input), chat box source (TASK 3 item 18), alt-key crop, credits, background removal, music-off-VOD, itch.io `PremiumUrl` (TASK 21 seam). **Out of scope (this branch):** IP webcam (video-only when it lands — never an audio input), chat box source (TASK 3 item 18), alt-key crop, credits, background removal, music-off-VOD, `PremiumUrl` (TASK 23 seam).
--- ---
@@ -590,6 +590,51 @@ the validator → persisted), compositor bar overlay (top/bottom + above-flash),
--- ---
## TASK 23 — Monetization: subscription billing + in-app unlock + support (2026-08-14, scoped)
**Goal:** turn the settled monetization policy (see `ai.md` → Monetization) into a paying product line:
annual subscription billing, an in-app unlock seam, and the in-app bug-report → git-issues support loop.
**Decisions already locked (2026-08-14, creator):** one paid line = **annual subscription** (early
access **$49.99/yr** → **$99/yr list at GA**; grandfather-while-subscribed, lapse → list on renewal;
no escalation matrix). Free tier unchanged (branding flash + no Alerts; the flash is the free tier's
billboard — distribution, not compromise). Billing **not locked to itch.io** (hunted fact: itch.io has
no native subscription billing); candidates **Gumroad** (native affiliates = tiebreaker) and **Lemon
Squeezy**. Support = in-app bug-report → git issues; "when I get around to it" cadence + emergency
patches. Unlock outcome: flash removal flips `BrandFlashEnabled` off + **Alerts** on.
### Status: 🔶 Scoped — nothing built; this entry records the plan + decisions
1. ☐ **Billing provider decision** — Gumroad vs Lemon Squeezy (annual subs + license-key verification
APIs; Gumroad's native affiliate program is the tiebreaker if the TASK 23 affiliate line-item matters)
2. ☐ **Unlock mechanism** — license-key entry + online/offline verification + persisted entitlement
(extends `LayoutStore`, schema bump) + renewal/lapse handling (**lapse → branding flash returns** —
the flash is the enforcement and the billboard)
3. ☐ **In-app bug-report → git issues** — a built-in report control that files an issue (title/body/
log attachment) into the creator's git repo; the support loop, per the settled support model
4. ☐ **"Unlock Premium" seam** — the About hub's greyed button (TASK 21; `PremiumUrl` is the tabled
billing URL) lights up and routes to the billing page / unlock dialog
5. ☐ **Alerts gating** — the Super Chat pop-ins (TASK 3 item 18 / minimal source set item 7) are the
one paid feature; subscription state gates them
6. **Line-item pointers (their own big tasks — do NOT start here):**
- ☐ **Early-access strategy** — the 90-day funnel (founder window $49.99 → GA $99), waitlist,
testimonials/QA, clips → Shorts. Separate planning effort.
- ☐ **Affiliate & ads (phase 2)** — own-channel gear affiliates from day one (organic), a
ytLlive referral program gated behind GA + happy users, YPP as the channel's ad revenue, paid
ads deferred.
- ☐ **Channel revival (the prequel to both)** — **resume streaming**: pair-programming dev
streams via **OBS** (ytLlive can't go live yet — private-only + `_rtmpUrlProvider` null) +
**The Division 2** gaming streams under the **"gramps/GOAT"** persona (67, gaming since 1981).
3D-printing content retired (printer hard down, novelty gone). **Dogfood switch** — once TASK 5 +
go-live capability land, stream *with* ytLlive; Division 2 (loud game + voice) is the demo for the
ducking/voice-filter story. The flash billboard + the grandfather promise are the monetization
hooks this line-item feeds.
**Out of scope (TASK 23):** the early-access strategy itself, the affiliate program, channel content
planning — those are separate line-item tasks. TASK 23 owns billing + unlock + support + gating only.
---
## TASK 6 — Layout Persistence (SQLite) ## TASK 6 — Layout Persistence (SQLite)
**Goal:** Scenes, sources, and asset bytes survive restarts; assets are always available. **Goal:** Scenes, sources, and asset bytes survive restarts; assets are always available.
+4 -4
View File
@@ -140,14 +140,14 @@ public class MainViewModel : ViewModelBase
// integration test can assert they point at real destinations. // integration test can assert they point at real destinations.
public const string ChannelUrl = "https://youtube.com/@llamachileshop"; public const string ChannelUrl = "https://youtube.com/@llamachileshop";
public const string MastodonUrl = "https://mastodon.llamachile.tube/@gramps"; public const string MastodonUrl = "https://mastodon.llamachile.tube/@gramps";
// Tabled 2026-08-14: the itch.io account was just created — the product page // Tabled 2026-08-14: the billing provider is not chosen yet (Gumroad/Lemon Squeezy — TASK 23);
// (premium unlock) lands here once set up. Coffee/thanks URL is live. // the subscription page (premium unlock) lands here once set up. Coffee/thanks URL is live.
public const string PremiumUrl = ""; public const string PremiumUrl = "";
public const string CoffeeUrl = "https://buymeacoffee.com/llamachiley"; public const string CoffeeUrl = "https://buymeacoffee.com/llamachiley";
public static string AppVersionLabel => $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}"; public static string AppVersionLabel => $"Version {typeof(MainViewModel).Assembly.GetName().Version?.ToString(3)}";
/// <summary>The About hub's Unlock Premium button lights up only once the /// <summary>The About hub's Unlock Premium button lights up only once the
/// itch.io product URL is set (tabled 2026-08-14 — account in setup).</summary> /// billing URL is set (tabled 2026-08-14 — provider undecided, TASK 23).</summary>
public bool IsPremiumAvailable => PremiumUrl.Length > 0; public bool IsPremiumAvailable => PremiumUrl.Length > 0;
/// <summary>Full in-app licensing/legal text, loaded on demand from the /// <summary>Full in-app licensing/legal text, loaded on demand from the
@@ -244,7 +244,7 @@ public class MainViewModel : ViewModelBase
public SocialsConfig? Socials => _socials; public SocialsConfig? Socials => _socials;
/// <summary>Freemium: 6 slots — YT + 1 other, the rest locked. Premium seam: all 6 open.</summary> /// <summary>Freemium: 6 slots — YT + 1 other, the rest locked. Premium seam: all 6 open.</summary>
private static bool IsPremium => false; // itch.io unlock deferred — seam only private static bool IsPremium => false; // unlock deferred (TASK 23) — seam only
/// <summary>Canvas.Top for the social bar: 0 = top, 1040 = bottom (40px from the 1080 edge).</summary> /// <summary>Canvas.Top for the social bar: 0 = top, 1040 = bottom (40px from the 1080 edge).</summary>
public double SocialBarTop => _socials?.BarPosition == SocialBarPosition.Top ? 0 : 1040; public double SocialBarTop => _socials?.BarPosition == SocialBarPosition.Top ? 0 : 1040;
+1 -1
View File
File diff suppressed because one or more lines are too long
+22 -8
View File
@@ -112,7 +112,7 @@ C# / WPF (.NET 8) following MVVM:
### Current limitations / TODOs ### 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`) - `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 — itch.io key verification pending) - 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 23)
- `YouTubeStreamService` uses hardcoded `1080p`/`60fps` and per-broadcast streams — must switch to the v3 `variable` reusable stream - `YouTubeStreamService` uses hardcoded `1080p`/`60fps` and per-broadcast streams — must switch to the v3 `variable` reusable stream
- 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) - 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 - `StreamConfig` defaults (`TargetBitrate=6000`, `Resolution="1920x1080"`) are stale — the live dropdown drives `StreamHealth.CurrentBitrate`/`FPS` instead
@@ -558,10 +558,10 @@ guardrails below — written down so a future "quick fix" never reintroduces a G
release blocker). Queued early is wrong; the release pass owns it. release blocker). Queued early is wrong; the release pass owns it.
- **Break the in-app About requirement** — since TASK 21 the About overlay is the app's in-app creator - **Break the in-app About requirement** — since TASK 21 the About overlay is the app's in-app creator
hub: the real logo, the creator links (YouTube channel, Mastodon, Buy me a coffee, and the greyed hub: the real logo, the creator links (YouTube channel, Mastodon, Buy me a coffee, and the greyed
"Unlock Premium" seam whose itch.io URL is tabled), and a **Licenses & legal** sub-panel that loads "Unlock Premium" seam whose billing URL is tabled), and a **Licenses & legal** sub-panel that loads
`THIRD-PARTY-NOTICES.txt` from the executable directory and renders it in a scrolling panel — `THIRD-PARTY-NOTICES.txt` from the executable directory and renders it in a scrolling panel —
**never the OS viewer, no Notepad, no browser tab**. The premium/coffee link constants live in **never the OS viewer, no Notepad, no browser tab**. The premium/coffee link constants live in
`MainViewModel` (`PremiumUrl`, `CoffeeUrl`, `ChannelUrl`, `MastodonUrl`); filling in the itch.io URL `MainViewModel` (`PremiumUrl`, `CoffeeUrl`, `ChannelUrl`, `MastodonUrl`); filling in the billing URL
lights up the premium button. Do not reintroduce an "open the notices in Notepad" button. lights up the premium button. Do not reintroduce an "open the notices in Notepad" button.
## Design Principle ## Design Principle
@@ -576,9 +576,8 @@ Apply this to every UI decision:
## Monetization (design decision — the branding flash is the sword) ## Monetization (design decision — the branding flash is the sword)
Free forever: all streams unlimited, no time caps, no subscription, no per-feature paywalls. The Free forever: all streams unlimited, no time caps, no per-feature paywalls. The **one paid line is an
**one paid line is a one-time unlock** (delivered via itch.io — they handle hosting, payment, and key annual subscription** (settled 2026-08-14 — replaces the earlier "one-time unlock" shape):
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 - **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 ~25% opacity for about one second (soft 250ms fade in/out), repeated every 300s, on the live output
@@ -586,8 +585,23 @@ delivery; we never own a server or a key shop):
(`MainWindow.xaml` CanvasGrid) + `BrandFlashTimer` in `MainViewModel` — cadence 300s, first flash (`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; ~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. 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** **The flash is also the free tier's billboard** — every free stream advertises ytLlive to its own
(Super Chat / membership / subscribe pop-ins). 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.
- **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). Candidates for annual subs + license-key verification:
**Gumroad** (its native affiliate program is a tiebreaker if the affiliate angle in TASK 23 matters)
and **Lemon Squeezy**. Decision deferred to TASK 23.
- **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 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 cutoffs (the worst dead end — a stream dying mid-broadcast reads as broken, and YouTube streams
+1 -1
View File
@@ -42,7 +42,7 @@ public sealed class AboutHubTests
Assert.False(vm.IsLicensesOpen); Assert.False(vm.IsLicensesOpen);
// The hub links resolve to real destinations (channel + mastodon + // The hub links resolve to real destinations (channel + mastodon +
// coffee are live; premium is the tabled itch.io seam). // coffee are live; premium is the tabled billing seam).
Assert.Equal("https://youtube.com/@llamachileshop", MainViewModel.ChannelUrl); Assert.Equal("https://youtube.com/@llamachileshop", MainViewModel.ChannelUrl);
Assert.Equal("https://mastodon.llamachile.tube/@gramps", MainViewModel.MastodonUrl); Assert.Equal("https://mastodon.llamachile.tube/@gramps", MainViewModel.MastodonUrl);
Assert.Equal("https://buymeacoffee.com/llamachiley", MainViewModel.CoffeeUrl); Assert.Equal("https://buymeacoffee.com/llamachiley", MainViewModel.CoffeeUrl);