85fad1ab79
Creator ruling 2026-09-27. Criteria, verbatim: "zero headaches, minimal maintenance (for me) while still providing accountability and a reasonably easy upgrade flow." Route A is the only combination where all four are solved by handing the work to Microsoft rather than to a certificate vendor: $0/yr, no certificate, no HSM, no annual renewal, no SmartScreen ramp — plus Store auto-update, Store-side payments/entitlements/refunds/support, and Microsoft review as the accountability layer. The rejected options and their reasons stay in research-store-certification.md §3 so a later session reads the ruling instead of re-deriving it. What this deletes: - The entire licensing backend. PolarLicenseService, PolarLicense, MainViewModel.License.cs (PremiumUrl, customer portal, the OfflineGracePeriod = 14 days subscription-era artifact, renewal/lapse copy) and the wrong "Polar unlocks alerts" string all become dead code. IsPremium is derived from the Store entitlement instead of an HTTP call, which also removes the whole "network flaky -> app thinks I'm expired" bug class. - Velopack, the update URL, and the self-hosted droplet — the Store updates. - Distribution.md's premise: Polar as the distribution backbone, Polar file hosting, and code signing as our problem. The IP-protection sections (1, 5, 6, 7) still stand and the build-posture ceiling is unchanged. What does NOT change: the entitlement. Free gets everything; the branding flash stays the only paid delta. Store IAP changes how IsPremium is obtained, never what it gates. Still open, deliberately: the price. The Store revenue share is unverified (do not assume a percentage), and MONETIZATION.md's $29 -> $49 one-time decision is re-opened against a fresh instinct toward ~$99/yr. No price encoded yet. The first code unit is unchanged: bundle ffmpeg (TASK 48 item 1). That clears the one hard certification gate and fixes a real user-facing 404. MARCOM.md and MONETIZATION.md were edited too but are gitignored by design, so those changes stayed local.
295 lines
26 KiB
Markdown
295 lines
26 KiB
Markdown
# ytLlive — Task List
|
||
|
||
> **Catalog:** task files live in [`TASKS/`](TASKS/) (one file per task). This file is the
|
||
> index. Memory-map conventions: [`schema.md`](schema.md); architecture/decisions: [`ai.md`](ai.md).
|
||
|
||
> **Checklist markers** — every task's Status line uses the same states:
|
||
> 1. ✅ — completed / shipped
|
||
> 2. 🔶 — in progress
|
||
> 3. ⏳ — blocked / waiting
|
||
> 4. ☐ — not started / queued
|
||
> 5. ❌ — exception (known-issue, deliberately excluded, or merged)
|
||
|
||
> **2026-09-01 — v1 = feature-complete ruling:** there is no v1.x. Everything queues to
|
||
> v1 or to **"Out of product — permanently"** (file end). Read those two sections before
|
||
> adding or reviving anything here.
|
||
|
||
---
|
||
|
||
## Task catalog
|
||
|
||
| # | Task | Status | File |
|
||
|---|------|--------|------|
|
||
| 01 | Initial Scaffold | ✅ Done | [`TASKS/task-01-initial-scaffold.md`](TASKS/task-01-initial-scaffold.md) |
|
||
| 02 | YouTube OAuth2 Authentication | ✅ Done | [`TASKS/task-02-oauth2-auth.md`](TASKS/task-02-oauth2-auth.md) |
|
||
| 03 | Capture Pipeline (Scenes/Sources) | 🔶 In progress — 27/30 items done; items 16, 17, 20 open | [`TASKS/task-03-capture-pipeline.md`](TASKS/task-03-capture-pipeline.md) |
|
||
| 04 | RTMP Ingest to YouTube | ✅ Done — all 7 items shipped | [`TASKS/task-04-rtmp-ingest.md`](TASKS/task-04-rtmp-ingest.md) |
|
||
| 05 | Layout Persistence (SQLite) | ✅ Done | [`TASKS/task-05-layout-persistence.md`](TASKS/task-05-layout-persistence.md) |
|
||
| 06 | UI polish batch | ✅ Done | [`TASKS/task-06-ui-polish-batch.md`](TASKS/task-06-ui-polish-batch.md) |
|
||
| 07 | Meter scaling amplification | ✅ Done | [`TASKS/task-07-meter-scaling.md`](TASKS/task-07-meter-scaling.md) |
|
||
| 08 | Audio milestone | ✅ SHIPPED 2026-08-14 | [`TASKS/task-08-audio-milestone.md`](TASKS/task-08-audio-milestone.md) |
|
||
| 09 | YouTube Live Stream Management | ⏳ In progress — items 1–3, 4 shipped; item 5 open; item 6 locked (TASK 36) | [`TASKS/task-09-live-stream-management.md`](TASKS/task-09-live-stream-management.md) |
|
||
| 10 | Monetization: watermark-only one-time license + Polar billing | 🔶 In progress — steps 1–7 shipped; **now a TEARDOWN: Polar is deleted in favour of Store IAP (2026-09-27)** | [`TASKS/task-10-monetization.md`](TASKS/task-10-monetization.md) |
|
||
| 11 | Post-pause polish batch (creator's 8 review issues) | ✅ SHIPPED 2026-08-15 | [`TASKS/task-11-polish-batch.md`](TASKS/task-11-polish-batch.md) |
|
||
| 12 | Master limiter on the live mix | ☐ Queued | [`TASKS/task-12-master-limiter.md`](TASKS/task-12-master-limiter.md) |
|
||
| 13 | Social media launch kit | 🔶 Scoped — queued after v1 | [`TASKS/task-13-social-launch-kit.md`](TASKS/task-13-social-launch-kit.md) |
|
||
| 14 | Creator feedback batch | 🔶 In progress — Branch 2 shipped | [`TASKS/task-14-creator-feedback-batch.md`](TASKS/task-14-creator-feedback-batch.md) |
|
||
| 15 | Stock scene background images | ✅ Shipped | [`TASKS/task-15-scene-backgrounds.md`](TASKS/task-15-scene-backgrounds.md) |
|
||
| 16 | Kill infinity display | ✅ Shipped | [`TASKS/task-16-kill-infinity-display.md`](TASKS/task-16-kill-infinity-display.md) |
|
||
| 17 | Web source rendering (WebView2) | ✅ Done | [`TASKS/task-17-web-source-rendering.md`](TASKS/task-17-web-source-rendering.md) |
|
||
| 18 | Local recording | ✅ Shipped `a9eb360` (2026-08-29) | [`TASKS/task-18-local-recording.md`](TASKS/task-18-local-recording.md) |
|
||
| 19 | Scene transitions | MERGED → TASK 23 (Control Surface UX) | [`TASKS/task-19-scene-transitions.md`](TASKS/task-19-scene-transitions.md) |
|
||
| 20 | Hotkeys (keyboard shortcuts) | ✅ Done — shipped 2026-08-26 | [`TASKS/task-20-hotkeys.md`](TASKS/task-20-hotkeys.md) |
|
||
| 21 | Media source (video file playback) | 🔶 In progress — UI picker remaining | [`TASKS/task-21-media-source.md`](TASKS/task-21-media-source.md) |
|
||
| 22 | Audio sync offset | ✅ Done (2026-08-31) | [`TASKS/task-22-audio-sync-offset.md`](TASKS/task-22-audio-sync-offset.md) |
|
||
| 23 | Studio mode | MERGED → TASK 23 (Control Surface UX) | [`TASKS/task-23-studio-mode.md`](TASKS/task-23-studio-mode.md) |
|
||
| 24 | Toast notifications | ✅ Done (2026-08-23) | [`TASKS/task-24-toast-notifications.md`](TASKS/task-24-toast-notifications.md) |
|
||
| 25 | Background consolidation | ✅ Done (2026-08-23) | [`TASKS/task-25-background-consolidation.md`](TASKS/task-25-background-consolidation.md) |
|
||
| 30 | Gear menu cleanup + Default Location for Recordings + preview layout | ✅ Done (2026-08-29) | [`TASKS/task-30-gear-cleanup-preview-layout.md`](TASKS/task-30-gear-cleanup-preview-layout.md) |
|
||
| 31 | SceneGraph component + static/dynamic compositor optimization | ✅ Done (2026-08-31) | [`TASKS/task-31-scenegraph-optimization.md`](TASKS/task-31-scenegraph-optimization.md) |
|
||
| 32 | Stream resilience | ☐ Queued (2026-09-01) | [`TASKS/task-32-stream-resilience.md`](TASKS/task-32-stream-resilience.md) |
|
||
| 33 | Bandwidth auto step-down | ☐ Queued (2026-09-01) | [`TASKS/task-33-bandwidth-step-down.md`](TASKS/task-33-bandwidth-step-down.md) |
|
||
| 34 | Scheduled streams (Text-drawer version) | ☐ Queued (2026-09-01) | [`TASKS/task-34-scheduled-streams.md`](TASKS/task-34-scheduled-streams.md) |
|
||
| 35 | Scene-linked audio | ☐ Queued (2026-09-01) | [`TASKS/task-35-scene-linked-audio.md`](TASKS/task-35-scene-linked-audio.md) |
|
||
| 36 | Gold pass | ☐ Queued (2026-09-01) | [`TASKS/task-36-gold-pass.md`](TASKS/task-36-gold-pass.md) |
|
||
| 37 | Defaults vs current layout split | ☐ Queued (2026-09-20) | [`TASKS/task-37-defaults-current-split.md`](TASKS/task-37-defaults-current-split.md) |
|
||
| 38 | Capture Window… backdrop (in-app window picker) | ✅ Done — shipped 2026-09-21 | [`TASKS/task-38-window-backdrop.md`](TASKS/task-38-window-backdrop.md) |
|
||
| 39 | YPP journey tracker (slice 1: current-scope data) | 🔶 Slice 1 ✅ — 2026-09-22; **refresh 403 fixed 2026-09-23** (auditDetails part dropped); slice 2 (Analytics + ETA) queued | [`TASKS/task-39-ypp-journey-tracker.md`](TASKS/task-39-ypp-journey-tracker.md) |
|
||
| 40 | App Settings round: active-camera picker + config dialog, gear move, defaults, accent | ☐ Queued (2026-09-22) — plan saved | [`TASKS/task-40-app-settings-round.md`](TASKS/task-40-app-settings-round.md) |
|
||
| 41 | Test Stream mode: private test broadcast + TEST drawer (mock chat input + simulated events) | ✅ Done (2026-09-22) | [`TASKS/task-41-test-stream-mode.md`](TASKS/task-41-test-stream-mode.md) |
|
||
| 42 | Top bar redesign: one REC-or-Stream surface, gear up top, sign-in/account zone world-gated | ✅ Done (2026-09-22) | [`TASKS/task-42-top-bar-redesign.md`](TASKS/task-42-top-bar-redesign.md) |
|
||
| 43 | Native events & alerts: chat parity (all six event types + server poll cadence) + six-animation native alert box | ✅ Done (2026-09-24) | [`TASKS/task-43-native-alerts.md`](TASKS/task-43-native-alerts.md) |
|
||
| 44 | TEST-tab chat fix: resolve liveChatId from snippet (not contentDetails) + poll until the broadcast is live | ✅ Done (2026-09-25); full suite 318/318 | [`TASKS/task-44-test-chat-fix.md`](TASKS/task-44-test-chat-fix.md) |
|
||
| 45 | TEST-tab chat fix #2: insert body must declare `snippet.type` (400 MISSING_REQUIRED_FIELD) | ✅ Done (2026-09-25) | [`TASKS/task-45-chat-insert-type.md`](TASKS/task-45-chat-insert-type.md) |
|
||
| 46 | Drawers: click outside the rail closes whichever is open — TEST added to the existing Stream Settings + YPP dismiss behavior | ✅ Done (2026-09-25) | [`TASKS/task-46-drawer-click-outside-close.md`](TASKS/task-46-drawer-click-outside-close.md) |
|
||
| 47 | Alert box video: built-in/custom alert clip (+ six-animation fallback) with read-time fade in/out + ticker (now in the preview too, 3 display methods) + alert volume in the live mix | ✅ Done (2026-09-26) | [`TASKS/task-47-alert-videos.md`](TASKS/task-47-alert-videos.md) |
|
||
| 48 | Distribution & packaging: **MSIX + Store IAP** | 🔶 In progress — **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; item 1 (bundle ffmpeg) is the next code unit | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) |
|
||
| 49 | Chat profanity filter (local, opt-in, non-persistent, user word list) | ☐ Queued (2026-09-27) | [`TASKS/task-49-chat-profanity-filter.md`](TASKS/task-49-chat-profanity-filter.md) |
|
||
|
||
---
|
||
|
||
## Research index
|
||
|
||
| Research | Covers |
|
||
|---|---|
|
||
| [`TASKS/research-youtube-api.md`](TASKS/research-youtube-api.md) | YouTube Live Streaming API v3 — authoritative facts for the v3 build |
|
||
| [`TASKS/research-store-certification.md`](TASKS/research-store-certification.md) | **Windows Store policies 7.20 + MSIX packaging + code-signing economics** (2026-09-27). Feeds TASK 48. Records which policies bind, which don't, and why — read it before re-litigating distribution |
|
||
|
||
---
|
||
|
||
## Open items (at a glance)
|
||
|
||
### Creator-reported batch (2026-09-26) — proof-of-concept round, recordings used as the reference
|
||
|
||
The creator is **not** getting YouTube private test recordings saved, so local recordings are the
|
||
proof of concept for compositing; the live path is assumed to share it. **Verified true, not assumed:**
|
||
one `_framePump` is built in the `MainViewModel` ctor with a single `brandFlash:` callback, and BOTH
|
||
`Streaming.Operations.cs:68` (go-live) and `:199` (record) call that same `StartAsync` — there is no
|
||
second encoder path needing a "redirect". Record+simulcast is one ffmpeg with two outputs.
|
||
|
||
1. ✅ **Recording save dialog: Cancel discards** — was silently saving under the default name. Enter
|
||
accepts the default; Cancel/Escape/X **deletes** the footage and names the file if the delete fails.
|
||
`MainViewModel.CompleteRecordingSave` (internal) + `ytLive.Tests/RecordingSaveDialogTests.cs`.
|
||
2. ✅ **Brand flash in ALL scenes + in recordings** — was gated on `IsLive` via
|
||
`UpdateLiveVisuals()`, so a recording made without ever going live carried no credit. The
|
||
presenter is now `Start()`ed once in the `MainViewModel` ctor and never stopped on live-state
|
||
churn; the licence gate rides on `IsPremium` → `Enabled` as before. Also fixed the
|
||
premium-edge/restart trap that app-lifetime exposed. Tests: `BrandFlashOutputTests`
|
||
(12 facts) incl. `Credit_IsComposited_WithNoLiveSession_AndSoARecordingCarriesIt` and
|
||
`ADowngradeMidSession_RestartsTheCadenceTimer`.
|
||
3. ✅ **Ticker confined to the alert box** — was a global 1920×48 bar at the top edge; the
|
||
creator wants it over the *Stream Alerts video*. The strip now renders at the alert box's own
|
||
size and carries the box's origin on a new `VideoFrame.Placement`; every blit site reads
|
||
`OriginX/OriginY` instead of a literal `0, 0`, and the preview element is bound to the same
|
||
rect so preview and output cannot drift. No alert box ⇒ no ticker. Height clamped to the 48px
|
||
strip; `CopyStrip` clips rows so a short box can't overrun the buffer. Tests: 3 new facts
|
||
incl. an output-side `SceneCompositor.Render` check that the pixels land in the box and NOT at
|
||
the top-left corner.
|
||
4. ☐ **Two instances still refuse to run.** `InstanceProfile` isolates layout/auth/log/WebView, but
|
||
`FfmpegLocator._toolsDir` is still the hardcoded shared `%APPDATA%\ytLlive\tools` and both
|
||
instances can `Directory.CreateDirectory` + `ExtractBinaries` into it. Also the launch method is
|
||
unconfirmed: `dotnet run` while the first app holds `bin/…/ytLive.exe` dies with `MSB3027` before
|
||
any instance starts — that is a build-output lock, not a log conflict.
|
||
5. ☐ **Post-session efficacy report** — on end of stream *or* recording, report what worked and what
|
||
failed, including the **dropped frames** the creator saw. `CurrentHealth` already tracks
|
||
`DroppedFrames`/`StreamDuration`; `SessionTeardownTests` is the natural home for the roll-up.
|
||
- **TASK 3** — items 16 (Text source) + 20 (RewardEvent capture → SQLite, Alerts' persistence half) open; **17 (Alerts) is DONE via TASK 43 (2026-09-24) — native six-event alert box**
|
||
- **TASK 9** — item 5 open (webcam identity key reconciliation)
|
||
- **TASK 10** — Velopack update URL pending → **now owned by TASK 48** (retires if the Store handles updates)
|
||
- **Shipping / release build (creator-queued 2026-09-26)** — no publish config exists yet:
|
||
the csproj has only `OutputType=WinExe` + `TargetFramework`, so a plain `dotnet publish`
|
||
is **framework-dependent** (customer needs the .NET 8 Desktop Runtime preinstalled).
|
||
`Distribution.md` targets a self-contained ~80–120MB `LlamaCasty.exe` uploaded to Polar
|
||
as a File Download benefit. Two deliverables:
|
||
1. **`scripts/publish.sh`** wrapping
|
||
`dotnet publish ytLive.csproj -c Release -r win-x64 --self-contained true -o ./publish/win-x64`,
|
||
and **asserting the output contains no test/xunit artifacts**. (The compile side needs
|
||
nothing — the tests are a separate project with a one-way reference, plus explicit
|
||
`<Compile Remove="ytLive.Tests\**" />` in `ytLive.csproj`; the risk is *packaging*:
|
||
`ytLive.Tests/bin/.../ytLive.exe` exists and would ship a Debug build + test DLLs if
|
||
anyone ever zips a bin folder by hand. Never copy folders; always publish.)
|
||
2. **Condition `InternalsVisibleTo("ytLive.Tests")` to Debug only** (`ytLive.csproj:78-82`
|
||
is unconditional, so it ships in Release) — it hands the test assembly name to anyone
|
||
decompiling and advertises that `internal` members are externally reachable, which
|
||
conflicts with the obfuscation posture in `Distribution.md` §1.2.
|
||
3. **Decide the shipped exe name.** `Distribution.md` promises the customer
|
||
`LlamaCasty.exe`; `<AssemblyName>ytLive</AssemblyName>` actually publishes `ytLive.exe`.
|
||
Renaming the AssemblyName is 4 lines (1 csproj + 3 pack URIs that spell the assembly
|
||
name — `MainWindow.xaml:14` is the **window icon**, so missing it ships a broken
|
||
taskbar/Alt-Tab icon). Namespaces stay `ytLive.*`; the 230-file re-brand is NOT
|
||
wanted. Do **NOT** touch the `%APPDATA%\ytLlive` folder — breaking (orphans users'
|
||
layout DB). See the naming table in `ai.md`.
|
||
Do **NOT** add `-p:PublishTrimmed=true`: WPF is not trim-compatible and it fails at
|
||
runtime (BAML/XAML resource resolution), not at build time. `PublishReadyToRun` is safe.
|
||
- **TASK 12** — queued future
|
||
- **TASK 13** — queued post-v1
|
||
- **TASK 14** — Branch 2 shipped; branch 3+ in progress
|
||
- **TASK 21** — UI picker slice remaining (Windows-only verification pending)
|
||
- **TASK 32–36** — all queued future work (2026-09-01)
|
||
- **Webcam resource lifecycle (2026-09-17)** — startup slice shipped (OS poll at start, single-camera
|
||
auto-lock, persistent red alert in the Layers panel on lock failure, re-polls until resolved); gate
|
||
slice shipped same day — webcam = app-level default, one per stream per scene, Add places the
|
||
default directly, offered only when a camera is ATTAINABLE (identity + live session, not just a
|
||
saved identity), app base lock keeps the session after the last placement is removed, startup
|
||
adopts a solo camera as default. TASK 26's app-wide gate superseded by creator directive. The App
|
||
Settings webcam selector is now **TASK 40 Unit A** (plan saved 2026-09-22). Remaining next slice
|
||
(user-queued): static (+) catalog rows (Background, YouTubeEvent, quoted labels) with reason-greying.
|
||
- **TASK 40 — App Settings round** — plan saved 2026-09-22 (`TASKS/task-40-app-settings-round.md`),
|
||
four units, one Good Dog test each: **A** active web camera section under Recordings (Change…
|
||
sub-menu = live enumeration incl. vcams; Tune… config dialog via `ICameraControlProbe`, live-only
|
||
tweaks), **B** gear moved top-left right of the wordmark (left-click App Settings, right-click
|
||
Bug/Feature/About), **C** persist output resolution default + surface default scene
|
||
transition/duration + confirm-before-End, **D** accent/theme color (consolidate 69 hardcoded
|
||
accent sites into a DynamicResource brush + picker). **Unit B (gear) is DONE early** — shipped 2026-09-22
|
||
as part of TASK 42 (top bar redesign): gear top-right of the brand, single click = the Settings/Bug/
|
||
Feature/About menu, removed from BottomBar. A + C + D remain queued.
|
||
- **TASK 37** — queued (2026-09-20): split the layout data into a `default` set (the established DB)
|
||
and a `current` set carrying the build-id; a saved `current` is honored only by the same build-id,
|
||
otherwise defaults load — enables one-click revert to defaults too. Capture needed work OUT of
|
||
current scope here; do not bolt it onto an in-flight change.
|
||
- **TASK 41 — Test Stream mode** — DONE 2026-09-22 (`TASKS/task-41-test-stream-mode.md`): a Test
|
||
button next to Start runs the real private-only go-live pipeline with `IsTestStream` set, and the
|
||
TEST drawer (right rail, third tab) hosts Mock Chat Input (real `liveChat/messages.insert` → ~2s
|
||
poll round-trip renders on the overlay) + simulated Subscriber/New Member/Super Chat events that
|
||
inject through the poller's `MessageReceived` seam (`IsSimulated` — YouTube's insert API only
|
||
creates text, so these are local-only by design; styled member/SuperChat rows validate the chat
|
||
overlay). **Real Stinger/TTS alert widgets resolved by TASK 43's native alert box.**
|
||
- **TASK 43 — Native events & alerts** — **DONE 2026-09-24** (`TASKS/task-43-native-alerts.md`):
|
||
StreamElements replaced with NATIVE YouTube events. Chat parity half: `YouTubeChatService` now
|
||
decodes all SIX `liveChat/messages` event types (`ChatEventKind` on `ChatMessage`) — the four
|
||
formerly-empty event rows fixed — and re-arms its poll on the server's `pollingIntervalMillis`
|
||
(streamList semantics, clamp 1000–6000ms, `maxResults=2000`). Alerts half: a new
|
||
**`SourceType.AlertBox`** ("Stream Alerts", one per layout) is an OBS-style celebration zone
|
||
where the six events play **six unique branded animations** (`AlertRenderer` + the
|
||
`AlertOverlayLayer` component; 33ms ticker, deterministic `Advance` test clock; idle =
|
||
transparent). Creator rulings baked in: free-sub mention = chat row only (no sub alert — YouTube
|
||
emits none), NO viewer count, every alert card carries the "made with LlamaCasty!" brand line.
|
||
- **TASK 44 — TEST-tab chat fix** — **DONE 2026-09-25** (`TASKS/task-44-test-chat-fix.md`):
|
||
the open creator report ("I still cannot post a chat message in the TEST tab") was a single
|
||
cause: `GetBroadcastLiveChatIdAsync` read `contentDetails.liveChatId` (lifeChatId lives in
|
||
**`snippet`**) AND ran before the broadcast was live (RTMP push flips ready→live via
|
||
enableAutoStart; YouTube only populates the id on live broadcasts — official GetLiveChatId.java
|
||
sample lists broadcastStatus=active). Fixed by reading `snippet.liveChatId` and polling with a
|
||
bounded retry after the frame pump starts; chat stays non-fatal.
|
||
- **TASK 47 take four (2026-09-26)** — the announcement strip was **output-only** (a
|
||
frame-pump callback with no preview consumer, since a master-width global overlay cannot
|
||
ride a per-element `Image`) and the marquee was paced at a fixed `140px/s` (~17s per
|
||
pass, so a 10s alert showed the text once). Now: a `tickerPreviewSink` publishes it to a
|
||
global `AlertTickerElement` in `PreviewPane.xaml` (mirroring `SocialBarElement`), and the
|
||
creator picks **Scroll / Flash / Solid** in the panel, with Scroll paced in *reads per
|
||
alert* (3 inside a 10s alert) rather than px/s. 15 new facts; suite 339/339.
|
||
- **TASK 47 — Alert box video** — **DONE 2026-09-26** (`TASKS/task-47-alert-videos.md`):
|
||
the TASK 43 alert box now plays a **video** on every alert — a shipped built-in mp4
|
||
(`Assets/alert-default.mp4`, stamped into the `Asset` table at startup) that the creator can
|
||
swap for their own file via the new Stream Alerts section (path only, never stored in the DB;
|
||
six `AlertRenderer` animations remain the fallback). Per-alert decode via a new
|
||
`IAlertClipDecoder` fx (ffmpeg rawvideo bgra + f32le pipes, real-time paced, disposed at
|
||
drain); ~0.3s fade-in/out rides the alpha envelope + scales the audio (mixed into the live
|
||
stream at unity — **no duck**, a per-alert Volume slider instead); an auto-composed message
|
||
**ticker** ("Funder — Super Chat · $10.00") scrolls along the very top of the frame
|
||
(never-baked dynamic overlay threaded through SceneCompositor + FramePump). Creator rulings:
|
||
custom + fallback (not either/or), ticker on top, no ducking.
|
||
- **TASK 36 item 6 (release engineering) is now TASK 48** (2026-09-27) — code signing, the
|
||
installer, and the Velopack update URL have one owner instead of three scattered mentions.
|
||
**EULA draft/review and the THIRD-PARTY-NOTICES license-texts gate stay in TASK 36 item 6**,
|
||
as does the agreed build-posture ceiling (HARDENED + MOCK_REWARDS, additive-only).
|
||
- **TASK 48 — distribution & packaging** — **✅ ROUTE DECIDED 2026-09-27: Microsoft Store,
|
||
MSIX package, Store IAP.** Creator's criteria, verbatim: *"zero headaches, minimal
|
||
maintenance (for me) while still providing accountability and a reasonably easy upgrade
|
||
flow."* Route A is the only option where all four are solved by handing the work to
|
||
Microsoft rather than to a certificate vendor — **$0/yr, no certificate, no HSM, no annual
|
||
renewal, no SmartScreen ramp**, plus Store auto-update, Store-side payments/entitlements/
|
||
refunds/support, and Microsoft review as the accountability layer. The rejected options
|
||
(Store+Polar, Polar-hosted + own cert, Store-EXE) are recorded with their reasons in
|
||
`TASKS/research-store-certification.md` §3 so the decision is not reopened.
|
||
- **The consequence that makes it cheap: the entire licensing subsystem is deleted.**
|
||
`Services/PolarLicenseService.cs` (HTTP validation, swallowed network failures, the
|
||
ignored `expires_at`), `Helpers/PolarLicense.cs`, `ViewModels/MainViewModel.License.cs`
|
||
(`PremiumUrl`, customer portal, the `OfflineGracePeriod = 14 days` subscription-era
|
||
artifact, renewal/lapse copy), and the incorrect "Polar unlocks alerts" string in
|
||
`Controls/OverlayHost.xaml` all become dead code. `IsPremium` is derived from the **Store
|
||
entitlement** instead of a remote HTTP call — one locally cached bit refreshed by the OS,
|
||
and the whole "network flaky → app thinks I'm expired" bug class disappears with the
|
||
offline-grace machine.
|
||
- **Unchanged:** the watermarks posture. Free gets everything; the branding flash stays the
|
||
ONLY paid delta (TASK 36 item 2). Store IAP changes how the bit is *obtained*, never what
|
||
it *gates*.
|
||
- **Still to verify (does not block packaging):** current Store revenue-share terms — ⚠️ do
|
||
not assume any percentage, verify before setting a price; and the one-time vs
|
||
subscription shape of the IAP tier (the storefront is decided, the price is not).
|
||
- **First code unit: item 1 — bundle ffmpeg instead of downloading it.** Unblocked,
|
||
recommended on every route, and it fixes a real user-facing failure.
|
||
- **TASK 10 is now a teardown, not a build** (2026-09-27) — Store IAP deletes the Polar
|
||
licensing path. The Polar fee tables, the perpetual-key model, and the customer-portal
|
||
plumbing in `MONETIZATION.md` (gitignored, local) are obsolete; the **watermark-only
|
||
entitlement and the branding-flash delta carry over unchanged**. The stale Polar product
|
||
(`$99/yr`, id `d105dfa1-…`) is not reused — Store IAP products are declared in Partner
|
||
Center instead.
|
||
- **Pricing is re-opened** (2026-09-27) — `MONETIZATION.md` carries a **one-time perpetual**
|
||
decision from 2026-09-21 (`$29` founder → `$49` list) that explicitly superseded the old
|
||
`$99/yr` subscription; the creator's current instinct is back toward a subscription.
|
||
**Unresolved — do not encode a price until it is settled**, and note that Store IAP moves
|
||
pricing into fixed Store tiers, so the "floor" (`~$69` per `MONETIZATION.md`) and the
|
||
"don't break the launch-price promise" note now refer to a different storefront.
|
||
- **TASK 49 — chat profanity filter** — queued, not blocked, size S. Local, on-device,
|
||
**non-persistent**, opt-in, **user-supplied word list (never a hardcoded slur list)**, and it
|
||
must **never match the SuperChat amount or reward fields** (financial data, Store 10.5.5).
|
||
The non-persistence half is the same property that makes the 11.12 UGC certification answer
|
||
strong, so **no future chat-history/moderation-log/analytics feature may quietly break it.**
|
||
|
||
---
|
||
|
||
## 1.0 gates (do these before the first shipped build)
|
||
|
||
- **Kill the dev multi-instance affordance.** `Helpers/InstanceProfile.cs` is already entirely
|
||
`#if DEBUG`, so a Release build ships as `DataRoot => DefaultRoot` / `WebViewDataFolder => null`.
|
||
Nothing to do but *keep it that way* — and re-prove it:
|
||
`InstanceIsolationTests.TheAlternateProfile_IsConfinedToTheDebugBuild` (source-level, always runs)
|
||
plus a Release build in which the `InstanceVariable` **field** is absent from metadata.
|
||
⚠️ Do NOT grep the binary for `YTLIVE_INSTANCE` — consts are inlined and appear in neither build.
|
||
- **TASK 36 is now shipped** — the branding flash composites into recordings and the stream. Test
|
||
VODs from an unlicensed instance carry the credit; use a license key for clean captures.
|
||
- Still queued from earlier: `scripts/publish.sh` (self-contained win-x64, no test artifacts,
|
||
Debug-only `InternalsVisibleTo`, no WPF trimming), and the `LlamaCasty.exe` assembly rename
|
||
(three pack URIs incl. `MainWindow.xaml:14` — do not rename `%APPDATA%\ytLlive`).
|
||
- Decide the open business question: `OverlayHost.xaml` copy says Polar unlocks *alerts* too, but
|
||
alerts are not gated by `IsPremium`. Either fix the copy or gate the alerts — do not leave it.
|
||
|
||
---
|
||
|
||
## YouTube Live API research facts
|
||
|
||
See [`TASKS/research-youtube-api.md`](TASKS/research-youtube-api.md) — authoritative facts for v3 build.
|
||
|
||
---
|
||
|
||
## Out of product — permanently
|
||
|
||
These were explicitly ruled out:
|
||
|
||
- **D3D11 swap compositor** (superseded by software compositor; TASK 3 item 16)
|
||
- **Background removal** (ONNX/DirectML; TASK 3 item 19)
|
||
- **OBS-style source expansion** (game capture, browser source, media playlist, VLC, color-key, MIDI — the minimal source set is deliberate; creators who need more have graduated to OBS)
|
||
- **Multi-layout** (auto-save, single layout; multi-layout = OBS territory)
|
||
- **Cross-process stream resume** (API makes it impossible; creator ruling 2026-09-01)
|