docs: decisions log, queue trim, HANDOFF rewrite — the map stops being a treadmill
TASKS.md 467 → 66 lines: one line per live item, detail stays in TASKS/ task-*.md. The old catalogue (63 rows of paragraph status prose plus the historical narration) is preserved verbatim as TASKS/archive.md — a 2026-10-29 snapshot, not the live queue. The "out of product" list and every retired decision moved to docs/ai/decisions.md with cause of death. HANDOFF.md 440 → 40 lines, and it is now OVERWRITTEN, not appended: branch, what's in flight, next action, live landmines, pending creator checks. History belongs to git log; the previous handoff is reachable at ee3c592:HANDOFF.md. docs/ai/decisions.md (new) collects what no longer governs: the paid tier and its pricing, Polar, Store IAP, the Store route and why it died, the deleted Velopack updater, signing economics, the retired monetization ideas, and the permanent out-of-product list. Facts that lived in the old ai.md "Current limitations" section were re-homed rather than dropped with the router rewrite: OAuth/DPAPI/session behaviour → docs/ai/youtube-api.md; paths, persistence and the Google project number → docs/ai/environment.md. verify.sh caps now cover all four always-read files (AGENTS 210, ai 100, HANDOFF 40, TASKS 120). Docs-only unit: scope-check run, no build.
This commit is contained in:
+30
-430
@@ -1,440 +1,40 @@
|
||||
# HANDOFF — 2026-10-28 (selected-layer stacking and App Settings UX; suite green)
|
||||
# HANDOFF — 2026-10-29 (memory map restructured; text layer is next)
|
||||
|
||||
## Current state
|
||||
- **Branch `main`, tracking `origin/main`; the selected-layer fix and test refresh were pushed as
|
||||
`769c616`.** The five
|
||||
feature commits since the previous `7ae30d4` checkpoint are `90cd039` (fade starts from outgoing picture), `f67a263`
|
||||
(transition picker), `b86b6bb` (Wipe), `841c905` (brand logo layer), and `7da2872` (preview and
|
||||
program transitions); they are included in the pushed history.
|
||||
- **Four UI-redesign commits already on `origin/main` were missing from the older handoff:**
|
||||
`24aca7d` moved the scene strip above the preview and removed the default CRT/on-air image seeds;
|
||||
`3239808` added the Scene Layers header/count and layer icons plus scene-card names, hotkeys, and
|
||||
PROGRAM badge; `a8814b4` added the explicit EDIT toggle and fixed the scene-card/left-column
|
||||
sizing; `7ae30d4` moved readouts/resolution into the top-left cell, added the Socials rail drawer,
|
||||
and restored webcam/chat border controls and vertical 1080p30. `b7ab169` added [`DESIGN.md`](DESIGN.md)
|
||||
with exploratory color/type tokens; it explicitly is not a layout spec or a theme implementation.
|
||||
`ai.md` and `Controls/index.md` now describe the landed surface. No new task was added to the queue.
|
||||
- **Shipped in those commits:** type/duration picker persisted with the layout; CPU left-to-right
|
||||
Wipe; one locked Brand logo image layer per scene; preview transition animation in every mode;
|
||||
program transitions while recording or streaming. The 7da2872 commit omitted a memory update;
|
||||
`TASKS.md` and `ai.md` now reflect it.
|
||||
- **Current work unit:** when a draggable element is selected from the layer list, `PreviewPane`
|
||||
temporarily raises its item container and gives it priority in the custom preview hit test. On
|
||||
deselection, normal preview stacking returns; the scene collection order and saved/program z-order
|
||||
never change. `SelectedLayerPreviewHitTestingTests` verifies the overlapping Brand logo/webcam
|
||||
visual hit, custom hit priority, and deselection restore.
|
||||
- **The six prior suite failures are resolved in their tests, not by reverting current behavior:**
|
||||
LayerReorder now accounts for the permanent Brand logo after Background; BorderColorPicker validates
|
||||
every palette picker and scopes its rendered-widget lookup to the visible webcam control; both
|
||||
thumbnail cases persist explicit tile assets instead of relying on removed default seed files;
|
||||
SourceNaming only considers user-created Text overlays; ZoomBox locates its placeholder by type,
|
||||
not a list index changed by Brand logo.
|
||||
- **The App Settings UX update is now committed and pushed:** `Controls/OverlayHost.xaml` groups
|
||||
settings into Recordings, Brand logo, Preview, and Scene transition sections with compact controls.
|
||||
Right-clicking the gear opens App Settings directly; left-click still opens the existing menu.
|
||||
The on-screen verification is still pending.
|
||||
- **The three edits that were previously uncommitted are now included in that update:**
|
||||
`Controls/OverlayHost.xaml` groups App Settings into Recordings, Brand logo, Preview, and Scene
|
||||
transition sections with compact controls; `Controls/TopBar.xaml` wires right-click on the gear;
|
||||
`Controls/TopBar.xaml.cs` opens App Settings directly on right-click while left-click keeps the
|
||||
menu.
|
||||
- **Worktree is clean after the push.**
|
||||
- **Next:** verify the App Settings/gear changes, transitions, and Brand logo on-screen. The earlier
|
||||
vertical-webcam hands-on check is also still
|
||||
pending unless the creator confirms it was completed. Move's persistent-layer behavior and Cube
|
||||
remain queued; distribution remains deferred until near v1.
|
||||
- **Verification state:** `verify.sh` passed: clean build with 0 warnings, **444/444 tests passed**,
|
||||
and scope check passed. The new selected-layer integration test also passes by itself.
|
||||
- **Follow-up, not fixed here:** `ytLive.csproj`'s `GenerateBuildStamp` comment still describes a
|
||||
commit-count-based `+1 per commit` counter, but the target now reads/writes local `BuildCounter.txt`
|
||||
and increments it per build. `ai.md` records the code's current behavior; correct the stale source
|
||||
comment in a separately scoped change. `ViewModels/MainViewModel.Socials.cs` also still calls the
|
||||
Socials surface a modal dialog in its method comment; the committed UI is now a preview-rail drawer.
|
||||
Correct that source comment separately if the file is next in scope. The new deterministic WPF
|
||||
hit-test recipe is in `MyMistakes.md`; the final commit cites its Microsoft reference.
|
||||
- **WPF references:** [`Panel.ZIndex`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.panel.zindex?view=windowsdesktop-10.0)
|
||||
governs preview container order; [`PreviewMouseRightButtonDown`](https://learn.microsoft.com/en-us/dotnet/api/system.windows.uielement.previewmouserightbuttondown?view=windowsdesktop-10.0)
|
||||
is used for the gear's direct settings action. The selected-layer fix also prioritizes the selected
|
||||
element in `HitElementAt`, because the custom hit test does not consult WPF's visual z-order. The
|
||||
deterministic test recipe and reference are recorded in `MyMistakes.md`.
|
||||
## State
|
||||
|
||||
---
|
||||
- Branch `main`, worktree clean after this commit. **Docs-only session — not pushed** (push at a
|
||||
checkpoint the creator names).
|
||||
- Commits: `a311c8f` partition `ai.md` into five subsystem docs · `c9ac348` router + invariants +
|
||||
doc-budget gate · this commit = decisions log, queue trim, handoff rewrite.
|
||||
|
||||
## Historical notes (retained reference; current checkout state is above)
|
||||
## What changed
|
||||
|
||||
### At a glance (2026-10-26 docs session)
|
||||
- **`ai.md` is being split into `docs/ai/*.md`** (user-approved: verbatim move, staged units, "ai.md
|
||||
only"). Three units landed as separate commits, **docs only, not pushed**:
|
||||
`804b1d2` (frame pipeline: cadence/timing/budget/compositor/pump),
|
||||
`096bf6d` (capture: backdrop/webcam/background effects/vertical tier),
|
||||
`a4dfca6` (output: ffmpeg/encoder/recording/audio). Each move is byte-identical in the new file;
|
||||
`ai.md` keeps a short stub with the load-bearing invariants + a link. `docs/ai/index.md` routes.
|
||||
**`ai.md` is now 1,289 lines (~32k tokens), down from 2,702 (~64k)** — more than half the
|
||||
mandatory-read cost is gone.
|
||||
- **Also landed (docs only, not pushed):** `ef60f1f` — distribution & packaging deferred until near
|
||||
v1 (creator ruling 2026-10-26); see "Next step" below for the still-open route contradiction.
|
||||
- **Remaining split candidates in `ai.md`** (not yet moved — smaller, more interleaved with law that
|
||||
should stay): Architecture's "Social bar" (46) + "Toast notifications" (24) + "Licensing — do not
|
||||
violate" (60); "YouTube Live API — design constraints" incl. "Native events & alerts" (349 total,
|
||||
99 of it alerts); "Monetization" (136); "Windows packaging & distribution" (135, frozen per the
|
||||
deferral above). None of these is urgent — pick back up only if the user asks to continue the split.
|
||||
- **Scope-check passed on all three commits**; no source touched this session.
|
||||
- `ai.md` 1,308 → 64 lines (router). Subsystem truth: `docs/ai/{capture,frame-pipeline,output,
|
||||
overlays,youtube-api,ui-surface,product,environment,decisions}.md`.
|
||||
- `AGENTS.md` is the single always-read file: rules + the 20 invariants.
|
||||
- `TASKS.md` 467 → ~90 lines (active queue); the old catalogue is `TASKS/archive.md`.
|
||||
- `scripts/verify.sh` now runs doc budgets first (AGENTS 210, ai 100, HANDOFF 40, TASKS 120).
|
||||
- Creator's 2026-10-29 report: App Settings/gear, transitions and Brand logo **all working** — closed.
|
||||
|
||||
---
|
||||
## Next action
|
||||
|
||||
### Earlier detailed state (2026-10-22) — properties-panel asks shipped, incl. TASK 63, and pushed; vv webcam Units 1+2 state below
|
||||
**TASK 3·17 — the Text layer**: content, font family/size/weight/style, colour, alignment,
|
||||
drag/resize, persisted; renderer mirrors `Services/ChatBoxRenderer.cs`. The type today is a
|
||||
placeholder with no code behind it. One Good Dog test, code + memory in one commit.
|
||||
|
||||
## At a glance
|
||||
- **Branch** main, tracking `origin/main`. **The six-commit properties-panel batch is PUSHED**
|
||||
(2026-10-22, as a sub-milestone): TASK 58 the theme sweep, TASK 59 the properties hierarchy,
|
||||
TASK 60 the colour names, a docs commit, TASK 61+62 the geometry field widths and the colour
|
||||
name beside the hex, and TASK 63 the border-colour picker as the app's own drop-down — the
|
||||
creator's properties-panel asks, done in one batch. `origin/main` is now `987fdda`.
|
||||
- The vv feature (Units 1+2) **was** pushed at its close (2026-10-21 — the creator approved
|
||||
the Unit 1 push after testing it, and the completed feature is a milestone).
|
||||
- **The push path changed:** `origin` is now
|
||||
`https://llgit.llamachile.tube/gramps/LlamaCasty.git` (HTTPS + the credential helper
|
||||
`/home/gramps/bin/gitea-credential`, which reads the password from the gitignored
|
||||
`CREDENTIALS.md`; plain `git push` works). The old SSH remote was broken twice over: the repo
|
||||
was **renamed `gramps/ytLlive` → `gramps/LlamaCasty`**, and `llgit:2214` refuses the publickey
|
||||
of this machine's key even though it IS registered (account key id 6 "Corsair VSC"). Ask the
|
||||
Gitea REST API, don't do key archaeology — recipe in `MyMistakes.md` → 2026-10-21.
|
||||
- **This session shipped vv webcam Units 1+2** (the vertical 9:16 tier keeps its OWN camera placement
|
||||
per scene; 16:9 restores the desktop layout exactly; the Chat scene in vv is rect-only and can be
|
||||
enlarged to fill a whole 607×540 area). Code + memory in ONE commit each; the ONE Good Dog tests
|
||||
are `ytLive.Tests/VerticalWebcamLayoutTests.cs` (Unit 1) and
|
||||
`ytLive.Tests/VerticalChatCameraLayoutTests.cs` (Unit 2). **Only the creator's hands-on check is
|
||||
left** — see the vv section.
|
||||
- **TASK 58 — the properties panel now wears the theme** (shipped 2026-10-21). The creator's
|
||||
"why is the drop-down not in the same style as the others?" was a real defect: those drop-downs
|
||||
(and every check box, and every properties field) had been hand-painted with a `Background`/
|
||||
`Foreground` instead of a style, so WPF drew the **light system template** on top of the dark
|
||||
panel. Now: `ComboBox` → `YtComboBox`, `CheckBox` → `YtCheckBox` (new), properties fields →
|
||||
`YtTextBoxCompact` (new, BasedOn `YtTextBox`). Guard:
|
||||
`ytLive.Tests/PropertiesStyleTests.cs`.
|
||||
- **TASK 59 — the properties panel is a hierarchy** (shipped 2026-10-21). A title owns its
|
||||
content and the content is indented one level (10px): `Shape` (new title) over the
|
||||
Mirror switch and the shape choice, the drag hint indented under that row, Geometry /
|
||||
Border Color / Border Thickness as titles with their fields indented, Background's
|
||||
Remove + Blur indented, and Strength one level deeper under Blur. **The shape became a
|
||||
CHOICE**: `Rect` | `Circle` as two segment options with **Rect selected by default**
|
||||
(the old single button showed the *next* action, "Round"). The mutual exclusion lives
|
||||
in the model (`IsRectShape`/`IsRoundShape` on `SceneElement`) — a WPF `RadioButton`
|
||||
group would kill the bindings. Guard: `ytLive.Tests/PropertiesHierarchyTests.cs`.
|
||||
- **TASK 60 — the border-colour drop-down names every colour** (shipped 2026-10-21). The creator:
|
||||
*"the #hex values are good, but add a corresponding color name/label next to the value"* — each row
|
||||
now reads `#e94560 Crimson` (the name dimmed, the tooltip "Crimson (#e94560)"). The 14 colours
|
||||
moved out of the click handler into `Models/BorderColorPalette.cs` (`NamedColor(Hex, Name)` +
|
||||
`NameOf(hex)`), so the list is testable and extensible. Guard:
|
||||
`ytLive.Tests/BorderColorPaletteTests.cs`.
|
||||
- **TASK 61 + 62 — the geometry fields and the border-colour field** (shipped 2026-10-21).
|
||||
- **61:** the X/Y/W/H fields share ONE fixed width (44px — the largest geometry value, 1920,
|
||||
plus a sign) and their rows set `LastChildFill="False"`, so Y/H no longer stretch to fill
|
||||
the row while X/W sized themselves to their text. Guard: `ytLive.Tests/GeometryFieldWidthTests.cs`
|
||||
(it MEASURES the widest value with WPF's `FormattedText` — measure, don't guess).
|
||||
- **62:** the border-colour field shows the palette's **name beside the hex**
|
||||
(`SceneElement.BorderColorName`, empty for a custom hex), and the hex field keeps a fixed
|
||||
width so the name stays next to the hex. The field stays editable (a custom hex can
|
||||
still be typed). Guard: `ytLive.Tests/BorderColorNameTests.cs`.
|
||||
- **TASK 63 - the border-colour picker IS the app's drop-down** (shipped 2026-10-21; **popup click
|
||||
deselection fixed 2026-10-22**). The creator:
|
||||
*"why does the drop down under the web cam properties not show the same style for the border
|
||||
color drop down list box?"* and *"we established look-and-feel standards. And yet you keep
|
||||
ignoring these and I'm wondering why."* **Root cause of the miss:** the TASK 58 sweep looked for
|
||||
control **types** (`ComboBox`/`CheckBox`/`TextBox`) without a theme style - and the border picker
|
||||
was NONE of those: it was a `...` `Button` whose `Click` handler built a `ContextMenu` in code.
|
||||
It was never in the sweep's scope, and `PropertiesStyleTests.cs` (a type-based guard) could not
|
||||
see it either. Now: the picker is a **`YtComboBox`** bound to the palette
|
||||
(`SelectedElement.BorderColorOptions` / `BorderColorEntry`), the **closed box shows the swatch +
|
||||
the colour's NAME**, the **rows show swatch + hex + name**, the hex field stays for a custom
|
||||
colour, the swatch is painted by the theme's `HexToBrush` converter, and the hand-built menu +
|
||||
`WebcamBorderColorPicker_Click` are **gone**. The closed box != the rows only through
|
||||
`Helpers/ClosedBoxTemplateSelector.cs` (WPF copies `ItemTemplate` into the read-only
|
||||
`SelectionBoxItemTemplate`; the selector is told them apart by the `ContentPresenter`'s
|
||||
templated parent - `ComboBox` = closed box, `ComboBoxItem` = a row). **2026-10-22 creator bug fix:**
|
||||
clicking a colour in the open `ComboBox` popup registered as an exit from the properties listing
|
||||
because `LeftPanel.IsDescendantOf` used `child is Visual ? VisualTreeHelper.GetParent(child) :
|
||||
LogicalTreeHelper.GetParent(child)` — at `PopupRoot` (which IS a `Visual`),
|
||||
`VisualTreeHelper.GetParent` returns `null`, so `IsDescendantOf` stopped without crossing
|
||||
`PopupRoot`'s logical parent (`PART_Popup` → `ComboBox` → `ElementsScrollViewer`), and
|
||||
`MainWindow.Window_PreviewMouseLeftButtonDown` cleared `SelectedElement = null`. Fixed by falling
|
||||
back to `LogicalTreeHelper.GetParent` / `TemplatedParent` when `VisualTreeHelper.GetParent`
|
||||
returns `null`. Guard: `ytLive.Tests/BorderColorPickerTests.cs` (model + panel + the **real
|
||||
widget**, including `LeftPanel.IsClickInside` on an open popup row and clicking a row to change
|
||||
`BorderColor`). The TASK 60
|
||||
guard (`BorderColorPaletteTests`) was updated to guard the new wiring (the drop-down is bound to
|
||||
the palette; no second list of colours anywhere).
|
||||
- **TASK 55 slice 1 — the log is capped (shipped 2026-10-22).** `startup.log` was uncapped (23 MB
|
||||
and growing every few seconds — ffmpeg logs every stderr line). Now it rolls to `startup.1.log`
|
||||
at 5 MB (one rolled copy kept), so the growth is bounded. The policy is
|
||||
`Helpers/LogRotation.cs` (a pure function over a path — `AppLog`'s 156-call-site surface is
|
||||
unchanged; the seams are two `internal` properties). Guard:
|
||||
`ytLive.Tests/AppLogRotationTests.cs`. Slices 2 (temp alert-clip cleanup) and 3 (low-disk
|
||||
guard before recording) are still open.
|
||||
- **TASK 48 slice 2 — the app PACKS as a Store MSIX (shipped 2026-10-22).** `Packaging/Package.appxmanifest`
|
||||
(full trust: `Windows.FullTrustApplication` + `uap10:TrustLevel=mediumIL` +
|
||||
`uap10:RuntimeBehavior=packagedClassicApp` + `rescap runFullTrust`, plus webcam/microphone
|
||||
`DeviceCapability`s, x64, en-US only) + `Packaging/Assets/` (four logo tiles generated from the
|
||||
app's own artwork by `scripts/make-store-logos.py`) + `scripts/package-msix.sh` (publish
|
||||
`Release -r win-x64 --self-contained` → stage → `makeappx pack`, optional `MSIX_PFX` signing).
|
||||
**First real package built: `artifacts/LlamaCasty.msix`, 159 MB, 509 files, `Package creation
|
||||
succeeded`** (`artifacts/` is gitignored). Guard:
|
||||
`StorePackagingTests.The_package_manifest_declares_full_trust_camera_and_microphone`.
|
||||
⚠️ Before submission the manifest's `Identity/@Publisher` and `PublisherDisplayName` must be
|
||||
set to the exact values Partner Center shows — a mismatch is a rejection.
|
||||
- **TASK 48 slice 3 — the self-updater is gone (shipped 2026-10-22).** The Store route (creator
|
||||
ruling 2026-09-27) means the Store updates the app, so the Velopack package + bootstrap were
|
||||
**deleted** (`ytLive.csproj`, `App.xaml.cs`) — a compliance action, not cleanup: the updater
|
||||
downloaded *and executed*, which Store policy 10.2.2 does not permit. The DO update-URL droplet
|
||||
dies with it. Guard: `ytLive.Tests/StorePackagingTests.cs` (no updater package, no updater API
|
||||
in any app source file). The app was smoke-tested after the removal: `MainWindow loaded`, no
|
||||
exceptions. **Still open on TASK 48:** slice 2 (the `Package.appxmanifest` + packaging project),
|
||||
slice 6 (the full-trust recording invariant comment), slice 7 (disqualifier re-check), slice 8
|
||||
(the pre-submission gates) — and slices 4/5 are the creator's (Certification Kit, Partner
|
||||
Center listing, the demo account).
|
||||
- **The previous session (HA lights) produced NO ytLive code** — it was spent on the creator's Home
|
||||
Assistant light alert system (off-repo, `192.168.50.145`). See `MyMistakes.md` → 2026-10-21: the plan
|
||||
was one tweak to a verified-working script; the script was rewritten twice, the alert path broke, and
|
||||
the day was burned. **The HA work landed OK in the end** (recipes + measurements now in
|
||||
`MyMistakes.md`). Records below so the next session does not re-hunt.
|
||||
## Landmines (live, not historical)
|
||||
|
||||
## HA light alert system — current, verified state (2026-10-21)
|
||||
Five Tapo bulbs: `light.br1`, `light.br2`, `light.officeone`, `light.officetwo`,
|
||||
`light.death_star_light`. Purpose: the creator is **deaf** — lights are the alert channel; the phone
|
||||
(`notify.mobile_app_mis_lx9`) is secondary. **The iPhone is gone — never notify it.**
|
||||
- `%APPDATA%\ytLlive` (capital L) — never rename it; it holds the layout DB, `Assets` and the auth
|
||||
token. Paths: `docs/ai/environment.md`.
|
||||
- The **full test suite hangs** (WASAPI teardown, pre-existing); per-class runs work and are the way
|
||||
to validate. `RealMouseDrag_OnTheLayerList_…` is an environment flake, not a regression.
|
||||
- `verify.sh`'s known-failure whitelist still matches on the two old test names; every other FAIL is
|
||||
a real regression.
|
||||
- `stash@{0}` ("WIP on main: 0464593 Compositor fast-path") predates this work — do not drop blindly.
|
||||
- Off-repo ops notes (HA lights, Pi-hole clock, GOG/Galaxy, sudo rotation) live in the previous
|
||||
handoff: `git show ee3c592:HANDOFF.md`.
|
||||
|
||||
| State | br1 / officeone | br2 / officetwo | Death Star |
|
||||
|---|---|---|---|
|
||||
| Idle | off | off | off |
|
||||
| Pump running | off | **off** | **blue only** (creator directive) |
|
||||
| Leak | warm-white 100% | purple | purple |
|
||||
| Alarm | warm-white 100% | red | red |
|
||||
| Doorbell | green, 8s | green, 8s | green, 8s |
|
||||
| Doorbell during an alert | — | suppressed — | — |
|
||||
| Clear | off | off | off |
|
||||
## Pending creator checks
|
||||
|
||||
**Architecture:** `script.alerts_arm` / `alerts_clear` write only `input_text.light_alert`
|
||||
(`none|leak|alarm|motion`, restart-surviving). **`script.alerts_render_lights` is the single writer of
|
||||
the bulbs** — flat top-level `if`/`else` blocks (nested `if` and `choose:` both silently lit nothing;
|
||||
do not "improve" the shape). Priority: an alert always beats pump status.
|
||||
|
||||
**⛔ THE ENTITY-ID TRAP (cost this session hours):** the YAML key is `alerts_render` but the entity id is
|
||||
**`script.alerts_render_lights`**. `script.turn_on` on a non-existent entity returns **HTTP 200 and does
|
||||
nothing** — no error, no log. All callers now use the `_lights` id. Check this first on any "script does
|
||||
nothing" symptom.
|
||||
|
||||
**Pump status** is a *status*, not an alert (auto-clearing), and is keyed off
|
||||
`sensor.water_pump_current_consumption` — **never** `switch.water_pump_2`, which was observed latched
|
||||
`on` for hours at 0.0A draw and painted the bedroom blue at 5am.
|
||||
|
||||
**HA files touched** (`/config`, root-owned → writes need `sudo`): `scripts.yaml`, `automations.yaml`,
|
||||
`configuration.yaml` (added `input_text.light_alert` / `_prev`). Backups: `scripts.yaml.bak-alertbuild`,
|
||||
`automations.yaml.bak-alertbuild`, `automations.yaml.bak-notify-20261020`, `configuration.yaml.bak-tplinkfix`.
|
||||
Zombie entities deleted from the registry: `script.leak_strobe`, `script.doorbell_flash` (old one),
|
||||
`automation.leak_strobe_notify_clear_restore`, `script.alerts_render_lights_2`.
|
||||
**Leak sensor is healthy** (`binary_sensor.pump_leak_moisture` = off, battery 100, signal 3) — an earlier
|
||||
"unavailable" claim was a bad read during a reload; `alert_leak_on` now also triggers on the latched
|
||||
`sensor.pump_leak_last_water_leak_alert`.
|
||||
|
||||
## vv webcam feature — locked rules (creator, 2026-10-17) — **Units 1+2 shipped 2026-10-21**
|
||||
| mode | shape | size | position |
|
||||
|---|---|---|---|
|
||||
| 16:9 | as today | as today | as today |
|
||||
| **vv · Live** | **circle OR rect, creator's choice** | small tile | one of six spots: top/bottom × L/C/R |
|
||||
| **vv · Chat** | **rect only — no circle option** | normal, or enlarged to fill one area (607×540) | enlarged → top or bottom |
|
||||
| Starting / BRB / Ending | unchanged | unchanged | unchanged |
|
||||
|
||||
- **Placement is set by tap/click-drag with snap, exactly like the socials today**, plus manual entry in
|
||||
properties. **vv keeps its own camera position, size and shape per scene** — switching back to 16:9
|
||||
restores the desktop layout exactly.
|
||||
- **No explicit borderless/no-background mode is needed** (round + border/bg options already allow it).
|
||||
- Creator's "square" means the rectangular view (the camera's own aspect), not a literal 1:1 box.
|
||||
|
||||
### What the code already gives us (do not re-hunt)
|
||||
- **vv = the active tier's centred band crop.** 1080p60-vertical: master band **x 656.25–1263.75
|
||||
(607.5 wide), full height**, upscaled to 1080×1920 (`MainViewModel.Streaming.cs` `ApplyStreamQuality`
|
||||
→ `OutputRect*`). The band splits into **two 607×540 areas** (top/bottom).
|
||||
- **The Live webcam sits at x≈1425 → entirely outside the band** — in vv it is invisible. That is the
|
||||
whole motivation.
|
||||
- **Geometry only — no render-path change.** Preview (`Controls/PreviewPane.xaml` 144–230) and compositor
|
||||
(`Services/Compositor/SceneCompositor.cs` 389–397) both read the element's own `X/Y/W/H` + `ClipShape`.
|
||||
- **Round / border / background are independent:** `ClipShape.Round`; `HasBorder` = width > 0 AND valid
|
||||
6-hex colour; `BackgroundEffect` = None/Remove/Blur.
|
||||
- **Per-scene camera size policy exists**: `MaxWebcamWidthFor/HeightFor` (Chat 1358×764, others 960×540)
|
||||
+ `ClampWebcamToBounds`. **A 607×540 tile sits inside both → no clamp change.**
|
||||
- **Storage:** `WebcamSceneConfig` table; new columns via `MigrateWebcamConfigTable()` as idempotent
|
||||
`ALTER TABLE … ADD COLUMN` guarded on `PRAGMA table_info` + `LayoutStore.Save.cs` INSERT and
|
||||
`LayoutStore.Load.cs` SELECT.
|
||||
- **Precedent for remembered geometry:** `RectWidth/RectHeight` + `ToggleClipShape`
|
||||
(`Models/SceneElement.cs` 124–152).
|
||||
|
||||
### What shipped in Unit 1 (2026-10-21) — do not re-hunt
|
||||
- **Geometry only — NO render-path change.** The tier switch swaps the element's LIVE geometry; the
|
||||
preview and the compositor read `X/Y/W/H` + `ClipShape` exactly as before.
|
||||
- `Models/VerticalCameraLayout.cs` (new): the pure math — band x 656.25, w 607.5, h 1080; two
|
||||
607×540 areas; six spots; default tile **270×152 master px** (= 480×270 in the 1080×1920 output),
|
||||
**bottom-right** spot; inset 32 master px.
|
||||
- `Models/WebcamSceneConfig.cs`: the vertical placement is persisted in **seven additive columns**
|
||||
(`VerticalX/Y/Width/Height/VerticalClipShape/VerticalRectWidth/VerticalRectHeight`, NULL = "this
|
||||
scene has no vertical placement yet"); the 16:9 layout stays in the old columns and is **parked** in
|
||||
the transient `Landscape*` fields while the vertical tier is live. `BeginVerticalLayout` /
|
||||
`EndVerticalLayout` / `CommitLiveLayoutToVertical` do the swap.
|
||||
- `Models/SceneElement.cs`: `X/Y/Width/Height` are now **`virtual`** so the webcam clamps EVERY write
|
||||
(drag, resize, typed fields) into the band — outside the band = invisible in vv.
|
||||
- `ViewModels/MainViewModel.Webcam.cs`: `IsVerticalTier`/`VerticalBand` + `ResolveVerticalCameraLayouts`
|
||||
(the tier swap; the **Live scene gets its tile automatically** on the first vertical switch) +
|
||||
`MoveWebcamInVerticalTier`/`SnapWebcamInVerticalTier` (the six-spot snap, by the tile's CENTRE).
|
||||
`MainViewModel.Streaming.cs` `ApplyStreamQuality` is the tier-change hook.
|
||||
- `Controls/LeftPanel.xaml`: the webcam properties gained a **Geometry** section (X/Y/W/H) bound to
|
||||
the live placement, gated on the VM's `IsWebcamSelected` (NOT `SelectedElement.IsWebcam` — the TASK 51
|
||||
test finds that panel by binding text and would match twice).
|
||||
- `Controls/PreviewPane.xaml.cs`: dragging/resizing a webcam under the vertical tier routes through the
|
||||
VM's snap instead of free-floating over the master.
|
||||
- Test seam: the drag lives in a **VM method**, so the test drives it without synthesising mouse input.
|
||||
|
||||
### What shipped in Unit 2 (the Chat scene, 2026-10-21) — do not re-hunt
|
||||
- **Rect only, and the lock is on the MODEL, not the UI.** `WebcamSceneConfig.VerticalShapeLocked`
|
||||
(set per scene by the VM) **overrides the `ClipShape` property** → a locked camera cannot become
|
||||
Round by any writer (the toggle, the Shape button, the placement swap). `SceneElement.ClipShape`
|
||||
and `SceneElement.ToggleClipShape()` are `virtual` for this. The lock is cleared **before** the
|
||||
16:9 layout comes back, so the desktop shape is restored.
|
||||
- **The circle is not OFFERED:** `MainViewModel.CanToggleClipShape` is false for a Chat camera in the
|
||||
vertical tier → the Shape button in the webcam's properties **disappears** (not just disabled).
|
||||
`MainViewModel.cs` raises it on the selection change; `MainViewModel.Streaming.cs` on the tier change.
|
||||
- **Two sizes, snapped by the nearer one:** `VerticalCameraLayout.SizeFor(band, requestedWidth)` →
|
||||
the small tile (270×152) or one whole area (607.5×540). The resize handle therefore has two
|
||||
detents instead of any size in between.
|
||||
- **An enlarged camera fills the area its centre is in** — top or bottom (`VerticalCameraLayout.AreaFor`).
|
||||
- **The Chat camera is auto-tiled** like the Live one when the vertical tier is picked (otherwise the
|
||||
vertical chat stream has no camera at all). `ResolveVerticalCameraLayout` now auto-tiles Live **and**
|
||||
Chat; Starting/BRB/Ending keep their 16:9 placement.
|
||||
- The preview's resize routes through `MainViewModel.ResizeWebcamInVerticalTier` (the Chat camera's
|
||||
discrete size + the snap); the drag routes through `MoveWebcamInVerticalTier` (assign → clamp →
|
||||
snap, so the enlarged camera can be dragged from the top area to the bottom one).
|
||||
- *Test:* `ytLive.Tests/VerticalChatCameraLayoutTests.cs` — **PASSES** (in Chat under the vertical
|
||||
tier, enlarging fills the full top area and the circle option is not offered).
|
||||
|
||||
### Units (each: ONE Good Dog integration test, own commit, memory in the same commit)
|
||||
1. ✅ **vv camera layout (Unit 1)** — shipped 2026-10-21. Stored separately per scene, resolved when the
|
||||
tier changes. Live: small tile (circle or rect) snapped to one of six spots; drag like the socials +
|
||||
properties entry.
|
||||
*Test:* `ytLive.Tests/VerticalWebcamLayoutTests.cs` — with the vertical tier active, dragging the Live
|
||||
camera to top-right places it top-right inside the band; switching back to 16:9 restores the original
|
||||
layout exactly; both placements survive a save + reload. **PASSES.**
|
||||
2. ✅ **vv · Chat (Unit 2)** — shipped 2026-10-21. Rect enforced with the circle option unavailable, plus
|
||||
enlarge-to-one-area (607×540) filling the top or the bottom area by where the camera is.
|
||||
*Test:* `ytLive.Tests/VerticalChatCameraLayoutTests.cs` — in Chat under the vertical tier, enlarge sets
|
||||
the full top area and the circle option is not offered. **PASSES.**
|
||||
|
||||
## Historical next step (2026-10-22; superseded by Current state above)
|
||||
- **✅ TASK 63 + popup-click fix verified by creator (2026-10-22)** — clicking a colour in the
|
||||
Border Color `YtComboBox` dropdown now stays inside `LeftPanel` (`IsDescendantOf` crosses
|
||||
`PopupRoot` → `PART_Popup` via `LogicalTreeHelper.GetParent`), updates the webcam border colour,
|
||||
and does not exit the properties listing.
|
||||
- **Creator check (needs the creator's hands)** — the last piece of the vv feature: switch the
|
||||
resolution dropdown to "Vertical 1080p60", confirm the Live camera tile lands inside the band and
|
||||
the **Circle** option (TASK 59 turned the Shape button into a Rect | Circle choice) turns the tile
|
||||
into a circle there, open the **Chat** scene and confirm the **Circle** option is **gone** (rect only)
|
||||
and that dragging the tile up and enlarging it fills a whole 607×540 area, then
|
||||
switch back and confirm the desktop layout is untouched. The unit tests cover the geometry, not the
|
||||
eyeball.
|
||||
- **⏸ Distribution & packaging is DEFERRED (creator ruling 2026-10-26) — do not pick it up until
|
||||
we are close to v1.** The creator's reasons: the remaining programming tasks are far from
|
||||
release, and a **complete UX redesign is about to start** — so packaging/signing/updater/route
|
||||
decisions would chase scope that is about to move. Nothing distribution-related is urgent now.
|
||||
Frozen: TASK 48 items 6-8, the signing decision, the updater choice, and the **route contradiction**
|
||||
(`ai.md` says Store route DEAD → direct + Steam; `TASKS.md`/`TASKS/task-48-…`/this file still say
|
||||
Store MSIX + Store IAP). That contradiction is to be settled in ONE distribution sweep at the
|
||||
revisit, not patched piecemeal in the meantime. Detail: `TASKS.md` → Open items → the ⏸ TASK 48
|
||||
bullet.
|
||||
- Nothing else is queued for llamacasty — the creator queues work.
|
||||
|
||||
## Open (operational)
|
||||
- **Follow-up (not fixed — out of scope for TASK 61):** the **web source** block's geometry rows in
|
||||
`Controls/LeftPanel.xaml` (the `SelectedElement.IsWebSource` panel, ~lines 387-402) have the
|
||||
identical shape and the identical defect as the webcam's did — the Y/H fields stretch to fill the
|
||||
row. Same one-line-per-field fix as TASK 61, when the creator wants it.
|
||||
- **✅ FIXED in TASK 63:** the border-colour swatches now carry a 1px `BorderBrush="#2a3a5e"`, so
|
||||
the black (`#000000`) and dark swatches are visible against the dark menu (`#16213e`).
|
||||
- **Follow-up spotted while doing TASK 63 (not fixed — out of scope for that change):** the theme has
|
||||
**no implicit `Button` style**, so every plain `<Button>` in the app still renders with the light
|
||||
WPF system template (`Controls/LeftPanel.xaml` ×7, `Controls/CameraPickerDialog.xaml` ×2). The
|
||||
border picker's `...` button was one of them and is gone; the rest are still there. A theme-level
|
||||
implicit `Button` style would fix them all at once — that is a creator-queued task, not a silent edit.
|
||||
- **GOG Galaxy launcher shows "offline" (2026-10-21) — GOG's own overnight outage, NOT Windows.** GOG
|
||||
put `www.gog.com` behind a maintenance page (503) and their Fastly services for `auth.gog.com` /
|
||||
`api.gog.com` / `remote-config.gog.com` return `500 "Fastly error: unknown domain"`; the page itself
|
||||
says *"Galaxy Launcher will also be down."* **Window: 00:00–08:11 local on 2026-10-21** — the Wayback
|
||||
snapshot of `www.gog.com/en/` at 06:56 UTC is HTTP 200 with the full live storefront. `status.gog.com`
|
||||
is NXDOMAIN globally. Local exoneration chain: genuine `GOG sp. z o.o → DigiCert` cert (no MITM),
|
||||
local DNS == Google's authoritative answers, real Fastly POP headers, and GOG's own non-Fastly servers
|
||||
still answering. **Nothing to fix locally** — wait for GOG. Galaxy's logs (incl. the rotated ones) show
|
||||
no API-status failures between Oct 10 and Oct 20. Diagnosis recipe + the `webfetch`-is-not-an-
|
||||
independent-egress trap: `MyMistakes.md` → 2026-10-21 GOG Galaxy "offline". Galaxy 2.0 logs live in
|
||||
`C:\ProgramData\GOG.com\Galaxy\logs\` (NOT `%LOCALAPPDATA%`). ⚠️ that folder also holds a **2.7 GB
|
||||
`cef.log`** (since Jul 13) — disk hygiene, see TASK 55.
|
||||
- **verify.sh's known-failure whitelist is too loose** (confirmed twice 2026-10-16): require EVERY failure
|
||||
line to be whitelisted, and add `MasterLimiter_CapsTheLiveMix_OnThePipe` +
|
||||
`RealMouseDrag_OnTheLayerList_PersistsTheReorder` to the list.
|
||||
- **A pre-existing stash is still sitting there:** `stash@{0}` = "WIP on main: 0464593 Compositor:
|
||||
fast-path opaque 1:1 when source fills canvas" (NOT this session's; do not drop blindly).
|
||||
- The judder reproduction take in the FAILING geometry (vertical 1080p60 first) is still outstanding — no
|
||||
fix until a take names the stage.
|
||||
- ⛔ Sudo password rotation pending (user; then update both `CREDENTIALS.md` password lines).
|
||||
- ✅ **TASK 56 secret scan — DONE 2026-10-22, CLEAN.** 432 reachable commits (all branches, 35
|
||||
tags, reflog, stash) + 63 dangling commits + the worktree: **0 real secrets**. 4 rule hits, all
|
||||
false positives (`README.md:79` = the `your-client-secret` doc placeholder;
|
||||
`TokenStoreTests.cs:22` = a 9-char test dummy). `CREDENTIALS.md`/`MARCOM.md`/
|
||||
`Helpers/OAuthCredentials.cs` were never tracked. ⚠️ the repo was **already public** (anonymous
|
||||
GET → 200, API `private = false`) when the scan ran — the gate came late, but nothing leaked.
|
||||
Method + verdict: `TASKS/task-56-secret-scan.md`.
|
||||
- TASK 55 disk hygiene: slice 1 (log rotation) **shipped 2026-10-22** — see the At-a-glance bullet.
|
||||
Slices 2 (temp alert-clip cleanup) + 3 (low-disk guard before recording) are open. (The GOG
|
||||
Galaxy `cef.log` at 2.7 GB is NOT ours.)
|
||||
|
||||
## Tests
|
||||
- Last full run: **438/438 passed, 0 warnings** (2026-10-22 — after TASK 48 slice 2's
|
||||
manifest guard; the run before was 437/437 after slice 3's `StorePackagingTests`, then
|
||||
436/436 after TASK 55 slice 1's `AppLogRotationTests` and 435/435 after TASK 63). The one
|
||||
failure seen earlier in the
|
||||
session was `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`,
|
||||
which **passes in isolation** (re-run: 3/3) — the known real-mouse flake, not a regression.
|
||||
- Known flaky-by-environment (all pass in isolation): `AudioPipelineTests.Mix_HonorsProviderGains…`,
|
||||
`MasterLimiter_CapsTheLiveMix_OnThePipe`, `LayerReorderPersistenceTests.RealMouseDrag…`,
|
||||
`Round_Clip_Corner…`.
|
||||
- ⚠️ **`LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder` is NOT
|
||||
"passes in isolation" any more.** During this session it FAILED ALONE on a **clean tree** at
|
||||
`58b9a7d` (the vv work stashed) with the same "Expected: ImgC / Actual: ImgA" — i.e. it is an
|
||||
environment/real-mouse flake, **not** a regression from the vv work — and it PASSES in the
|
||||
full-suite run (429/429). Treat it as environment-dependent, and don't chase it as a regression
|
||||
without stashing first.
|
||||
|
||||
## Secrets / paths (do not re-hunt)
|
||||
- Layout DB: `%APPDATA%\ytLlive\ytLlive.db` (SQLite, full-rewrite per save). Auth:
|
||||
`%APPDATA%\ytLlive\ytLlive.auth` (DPAPI). Startup log: `%APPDATA%\ytLlive\startup.log`
|
||||
(23MB — now CAPPED at 5 MB with one rolled copy, TASK 55).
|
||||
- Recordings land in `C:\Users\gramp\Downloads\recordings\` (`ty-<date>-<time>-<ms>-<seq>.mp4`; a
|
||||
duplicate name gets a `-2` suffix).
|
||||
- **Conversation history:** `~/.local/share/opencode/opencode.db`, table **`session_message`** — creator
|
||||
turns are `type='user'` with text in `data.text`; my turns are `type='assistant'` with reasoning AND the
|
||||
rendered reply concatenated into `content[0].text`. The **reply marker's verbatim string is recorded in
|
||||
`MyMistakes.md`**.
|
||||
- LAN credentials: `CREDENTIALS.md` (gitignored) — **⚠️ stale for HA: it says `192.168.50.144`, the real
|
||||
Home Assistant is `192.168.50.145`** (`homeassistant.local`, HAOS 18.2, Core 2026.10.1, SSH
|
||||
`gramps`/`einstein`, `/config` root-owned). `gordito` is `.144`. Fix this file when next editing it.
|
||||
- Google Cloud OAuth project number `198374416003`, owned by `llamachile.shop@gmail.com` — verify at
|
||||
`https://console.cloud.google.com/apis/credentials?project=198374416003`.
|
||||
|
||||
## Open (operational) — officerFriendly / PS5 "DNS errors" (2026-10-17, off-repo)
|
||||
- **Root cause: a ~43 h-wrong system clock on officerFriendly × `dnssec = true`.** Pi 400, **no RTC
|
||||
hardware at all**, healthy oscillator (−7.39 ppm). Journal shows +54 min, +7 h, +42.9 h steps.
|
||||
`pihole-FTL.db`: **1,688,396 `dnssec=3` rows on 10-11, 839,276 on 10-12 vs ≤23 other days**, hitting
|
||||
PSN endpoints = the PS5's "DNS errors". KSK-rollover theory **retracted**.
|
||||
- **Pending creator decision (do not apply unilaterally):** tighten
|
||||
`/etc/systemd/timesyncd.conf.d/lan.conf` to `PollIntervalMinSec=16` / `PollIntervalMaxSec=64` /
|
||||
`ConnectionRetrySec=8` / `SaveIntervalSec=30`, and/or add a DS3231 RTC (`i2c-tools` already installed).
|
||||
- **LAN DNS routing vs creator intent:** DHCP still hands out `.26` + `.1` (clients query Pi-hole
|
||||
directly) whereas the creator wants everything → `.1` with BBR forwarding. Also `dnssec = true` on
|
||||
Pi-hole duplicates dnscrypt-proxy's `require_dnssec = true`. Both offered-for-veto, not done.
|
||||
- Router is the DHCP server (ASUS ROG GT-BE98 Pro `.1`) — **Pi-hole `.26` is DNS only**, its
|
||||
`dhcp.leases` is 0 bytes.
|
||||
Vertical-tier camera geometry (Live tile in the band, Chat rect-only); and a Release webcam-cadence
|
||||
take in the failing geometry, which is what blocks cadence Units 1–2.
|
||||
|
||||
@@ -1,467 +1,66 @@
|
||||
# ytLlive — Task List
|
||||
# TASKS — the active queue
|
||||
|
||||
> **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).
|
||||
> One line per live item; the detail lives in `TASKS/task-*.md`. Done work is in
|
||||
> [`TASKS/archive.md`](TASKS/archive.md) (2026-10-29 snapshot of the old catalogue). Retired
|
||||
> decisions are in [`docs/ai/decisions.md`](docs/ai/decisions.md).
|
||||
> Markers: ☐ queued · 🔶 in progress · ⏳ blocked · ⏸ deferred · ❌ retired.
|
||||
|
||||
> **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)
|
||||
## Next up
|
||||
|
||||
> **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 3·17 — the Text layer.** The `TextOverlay` source type exists as a placeholder and nothing
|
||||
else does: `AddSource` creates it 0×0, all four compositor paths `continue` past it, `FramePump`'s
|
||||
hash skips it, it is not draggable, and it has no text/font/colour properties or persistence.
|
||||
The unit: text content, font family/size/weight/style, colour, alignment, drag/resize, persisted;
|
||||
renderer mirrors `Services/ChatBoxRenderer.cs`. One Good Dog test.
|
||||
|
||||
---
|
||||
## Queue
|
||||
|
||||
## Task catalog
|
||||
| Task | Status | Scope | Detail |
|
||||
|---|---|---|---|
|
||||
| 3·17 | ☐ **NEXT** | **Text layer** — content, font family/size/weight/style, colour, alignment, drag/resize, persisted | [`task-03`](TASKS/task-03-capture-pipeline.md) |
|
||||
| 21 | 🔶 | Media source: acquisition wiring + file picker + loop/volume UI (decoder, manager, pacing, loop all shipped — **no `AcquireAsync` caller exists**) | [`task-21`](TASKS/task-21-media-source.md) |
|
||||
| — | ☐ | Transitions **Move**: per-layer carry-across-scenes flag, per-scene placement, fixed z band | [`ui-surface`](docs/ai/ui-surface.md) |
|
||||
| 55·2–3 | ☐ | Disk hygiene: delete `%TEMP%` alert clips on close + sweep at startup; low-disk guard before REC (slice 1 shipped) | [`task-55`](TASKS/task-55-disk-hygiene.md) |
|
||||
| 40·A/C/D | ☐ | App Settings: active-camera picker; persist output resolution + surface transition defaults + confirm-before-End; accent consolidation | [`task-40`](TASKS/task-40-app-settings-round.md) |
|
||||
| 3·20 | ☐ | RewardEvent persistence: canonical `RewardEvents` table + `superChatEvents.list` 30-day backfill (the session report and journey read it) | [`task-03`](TASKS/task-03-capture-pipeline.md) |
|
||||
| 32 | ☐ | Stream resilience: blip retry → measured grace + amber "RECONNECTING" → one-click Back on air → fragmented MP4 → ingest pre-flight | [`task-32`](TASKS/task-32-stream-resilience.md) |
|
||||
| 33 | ☐ | Bandwidth auto step-down: pure policy over dropped-frame rate, one-way within a broadcast | [`task-33`](TASKS/task-33-bandwidth-step-down.md) |
|
||||
| 34 | ☐ | Scheduled streams (Text-drawer version) | [`task-34`](TASKS/task-34-scheduled-streams.md) |
|
||||
| 35 | ☐ | Scene-linked audio | [`task-35`](TASKS/task-35-scene-linked-audio.md) |
|
||||
| 37 | ☐ | Defaults vs current layout split (build-id); enables one-click revert to defaults | [`task-37`](TASKS/task-37-defaults-current-split.md) |
|
||||
| 39·2 | ☐ | YPP slice 2: Analytics scopes, watch-hours/Shorts velocity + ETA | [`task-39`](TASKS/task-39-ypp-journey-tracker.md) |
|
||||
| 49 | ☐ | Chat profanity filter: local, opt-in, **non-persistent**, user-supplied word list | [`task-49`](TASKS/task-49-chat-profanity-filter.md) |
|
||||
| 50 | ☐ | One-button publish of the local recording (resumable upload) | [`task-50`](TASKS/task-50-publish-recording.md) |
|
||||
| 53 | ☐ | Output-format contract: pin every tier with a geometry test, prove 9:16 end-to-end, recorded file matches output | [`task-53`](TASKS/task-53-output-formats.md) |
|
||||
| 54 | 🔶 | Marcom: free-forever copy live; merch POD + About links queued | [`task-54`](TASKS/task-54-marcom-free-merch.md) |
|
||||
| 9·5 | ☐ | Webcam identity key reconciliation | [`task-09`](TASKS/task-09-live-stream-management.md) |
|
||||
| 14 | 🔶 | Creator feedback batch — branch 3+ | [`task-14`](TASKS/task-14-creator-feedback-batch.md) |
|
||||
| — | ☐ | Small: `StreamConfig` defaults (`TargetBitrate=6000`, `1920x1080`) are stale — the dropdown drives `StreamHealth` | this line |
|
||||
| 36 | ☐ | **Gold pass** — the v1 gate, always the last unit (see 1.0 gates) | [`task-36`](TASKS/task-36-gold-pass.md) |
|
||||
| 48 | ⏸ | Distribution & packaging — deferred to near-v1 (creator ruling 2026-10-26); route contradiction settled in one sweep then | [`task-48`](TASKS/task-48-distribution-msix.md) |
|
||||
| 10 | ❌ | Retired with free-forever; a Polar teardown runs only if a paid tier ever returns | [`task-10`](TASKS/task-10-monetization.md) |
|
||||
|
||||
| # | 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 paid delta (branding flash) | 🔶 **TEARDOWN, not a build** — Polar deleted in favour of Store IAP (2026-09-27); `$10/mo` + `$400` lifetime, no annual. **SUPERSEDED 2026-10-08** — free-forever ruling (watermark-free, gratuity/merch only); see TASK 54 | [`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 | ✅ Shipped 2026-08-15 — `Services/Audio/MasterLimiter.cs` (−1 dBFS ceiling, instant attack, smoothed release) applied at the end of `AudioMixer.FillAndMix` | [`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** | ⏸ **DEFERRED 2026-10-26 — revisit near v1, not now** (creator: the remaining programming work is far from release, and a complete UX redesign comes first). Frozen until then: items 6-8, the signing decision, the updater choice, and the route question itself. History: **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28**; **item 2 (`Package.appxmanifest` + `scripts/package-msix.sh`) SHIPPED 2026-10-22 — a real 159 MB `LlamaCasty.msix` packs**; **item 3 (remove the Velopack updater) SHIPPED 2026-10-22** — the Store updates the app. Open: items 6-8 (recording invariant, disqualifiers, pre-submission gates); items 4/5 are the creator's (Certification Kit, Partner Center listing) | [`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) |
|
||||
| 50 | One-button "Publish this video" (resumable upload of the local recording) | ☐ Queued (2026-09-28) | [`TASKS/task-50-publish-recording.md`](TASKS/task-50-publish-recording.md) |
|
||||
| 51 | **Webcam background effects: Remove + Blur** | ✅ **Built 2026-10-09** (v1 requirement) — bundled MediaPipe Selfie Segmenter → ONNX (verified vs Google's runtime), Remove + Blur with strength, per-scene config persisted, one processing point in CameraManager, radio-pill UI in the webcam's Layers properties. Creator check 2026-10-09: **Remove jitter FIXED** (temporal mask smoothing 0.75/0.25, OBS/Jitsi-derived) and **Blur "heavy" FIXED** (quadratic radius 1→6 small px, default 0.5 = 2, StreamFX-consensus); recheck on the real camera pending | [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md) |
|
||||
| 52 | **Zoom box (anchored 2× magnifier)** | ✅ Done — shipped 2026-10-08 (3 creator corrections during the build: loupe → anchored capture over the full composite, drawn last → never shows the dim shading) | [`TASKS/task-52-zoom-box.md`](TASKS/task-52-zoom-box.md) |
|
||||
| 53 | **Output-format verification: Shorts/TikTok geometry (9:16)** | ☐ Queued (2026-10-08) — output formats never tested end-to-end; pin + prove the vertical geometry | [`TASKS/task-53-output-formats.md`](TASKS/task-53-output-formats.md) |
|
||||
| 54 | **Marcom round: free-forever positioning + merch storefront** | 🔶 In progress — llamacasty.com copy live 2026-10-08 (pricing stripped); merch POD + app About links queued | [`TASKS/task-54-marcom-free-merch.md`](TASKS/task-54-marcom-free-merch.md) |
|
||||
| 55 | **Disk hygiene: log rotation + low-disk guard** | 🔶 In progress — **slice 1 shipped 2026-10-22** (`startup.log` rolls at 5 MB to `startup.1.log`, one copy kept — it was uncapped at 23 MB and growing every few seconds); slices 2 (temp alert-clip cleanup) + 3 (low-disk guard) open | [`TASKS/task-55-disk-hygiene.md`](TASKS/task-55-disk-hygiene.md) |
|
||||
| 56 | **Pre-publication secret scan (full git history)** | ✅ Done — 2026-10-22, **CLEAN** (432 reachable commits + 63 dangling + worktree; 4 hits, all false positives: a `your-client-secret` doc placeholder and a test dummy — nothing to rotate). ⚠️ the repo was **already public** when the scan ran (anonymous GET → 200), so the gate came late — but nothing was exposed | [`TASKS/task-56-secret-scan.md`](TASKS/task-56-secret-scan.md) |
|
||||
| 57 | **Vertical (9:16) tier camera placement — the "vv" feature** | ✅ **Both units shipped 2026-10-21** (Unit 1 = Live scene: own placement per tier — small tile, circle-or-rect, one of six spots; drag-snap + manual entry; 16:9 restored exactly. Unit 2 = Chat scene: rect only — the circle is not offered — the tile or one whole 607×540 area) | [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md) |
|
||||
| 58 | **Properties-panel style consistency (the theme owns the look)** | ✅ Done — 2026-10-21 (the creator's "why is the drop-down not in the same style as the others?" — the drop-downs/fields/check boxes had been hand-painted instead of themed, so WPF drew the light system template; now every one carries a theme style, guarded by a test) | [`TASKS/task-58-properties-style-consistency.md`](TASKS/task-58-properties-style-consistency.md) |
|
||||
| 59 | **The properties panel is a hierarchy (titles own their content)** | ✅ Done — 2026-10-21 (the creator's ruling: a "Shape" title above the Mirror/Rect row, the drag hint indented under it, the same for Geometry / Border Color / Border Thickness, Remove + Blur under Background, Strength under Blur — and the shape became a CHOICE: Rect | Circle with Rect selected by default) | [`TASKS/task-59-properties-hierarchy.md`](TASKS/task-59-properties-hierarchy.md) |
|
||||
| 60 | **The border-colour drop-down names every colour** | ✅ Done — 2026-10-21 (the creator's "the #hex values are good, but add a corresponding color name/label next to the value" — the 14 colours moved out of the click handler into `Models/BorderColorPalette.cs`, and each row now reads "#e94560 Crimson") | [`TASKS/task-60-border-color-names.md`](TASKS/task-60-border-color-names.md) |
|
||||
| 61 | **The geometry fields share one fixed width** | ✅ Done — 2026-10-21 (the creator's "the Y and H boxes should be the same width as the X and W boxes and those widths should be fixed to accomodate the max number of possible digits" — the Y/H fields used to stretch to fill the row; all four are now 44px, wide enough for 1920 plus a sign) | [`TASKS/task-61-geometry-field-widths.md`](TASKS/task-61-geometry-field-widths.md) |
|
||||
| 62 | **The border-colour field shows the colour's name next to the hex** | ✅ Done — 2026-10-21 (the creator: "the border color drop down box content should also include the name of the color next to the hex value as they in the drop-down list box" — the field keeps the hex editable and the palette's name sits right beside it, empty for a custom hex) | [`TASKS/task-62-border-colour-name-in-the-field.md`](TASKS/task-62-border-colour-name-in-the-field.md) |
|
||||
| 63 | **The border-colour picker is the app's own drop-down** | ✅ Done — 2026-10-21 (popup row click deselection fixed 2026-10-22: `LeftPanel.IsDescendantOf` now crosses `PopupRoot` → `PART_Popup` via `LogicalTreeHelper.GetParent` when `VisualTreeHelper.GetParent` returns `null`, so clicking a colour in the open `YtComboBox` updates `BorderColor` instead of exiting properties) | [`TASKS/task-63-border-colour-app-dropdown.md`](TASKS/task-63-border-colour-app-dropdown.md) |
|
||||
## Pending the creator's hands (not programming)
|
||||
|
||||
---
|
||||
- ✅ App Settings grouping + right-click gear — reported working 2026-10-29.
|
||||
- ✅ Transition picker/durations, Wipe, preview + program transitions — reported working 2026-10-29.
|
||||
- ✅ Brand logo layer — reported working 2026-10-29.
|
||||
- ⏳ Vertical-tier camera check (Live tile inside the band, Chat rect-only) — outstanding.
|
||||
- ⏳ **Webcam cadence**: a Release take in the failing geometry (vertical tier first) blocks Units 1–2.
|
||||
No fix until a reading names the stage.
|
||||
|
||||
## Research index
|
||||
## 1.0 gates (before the first shipped build)
|
||||
|
||||
| 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 |
|
||||
- **Visibility picker** — replace the always-Private lock, hide the TEST button, and wire
|
||||
`UpdateBroadcast` in the same unit (it is the only code that can change a broadcast's privacy).
|
||||
- **Kill the dev multi-instance affordance** — `Helpers/InstanceProfile.cs` is already `#if DEBUG`;
|
||||
re-prove it on a Release build.
|
||||
- **Release engineering** — `scripts/publish.sh` (self-contained win-x64, no test artifacts),
|
||||
Debug-only `InternalsVisibleTo`, the `LlamaCasty.exe` assembly rename (three pack URIs, incl.
|
||||
`MainWindow.xaml:14`), full license texts beside `THIRD-PARTY-NOTICES.txt` (TASK 4 req 9).
|
||||
- **Gold pass** (TASK 36): the screens/layers audit, native-Windows verification, and the
|
||||
branding-flash decision.
|
||||
|
||||
---
|
||||
## Research
|
||||
|
||||
## 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** — the Velopack update URL is **RETIRED (2026-10-22)**: the updater was deleted (TASK 48 slice 3) and the Store updates the app
|
||||
- **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`. ⛔ **The Polar
|
||||
File-Download benefit is dead** — the Store hosts and updates the app, so there is no upload
|
||||
step (TASK 48). 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)
|
||||
- **✅ TASK 57 — the "vv" vertical camera layout** (both units shipped 2026-10-21). The vertical
|
||||
tier is a centred band crop of the same master, so the 16:9 camera placement falls outside the
|
||||
cropped window and the creator is not in the vertical stream at all. **Unit 1:** each scene keeps
|
||||
its own vertical placement (position, size, shape); the Live scene gets a small tile in one of
|
||||
six spots (top/bottom × left/centre/right), circle or rect, placed by drag-snap or typed into the
|
||||
new Geometry fields; switching back to 16:9 restores the desktop layout exactly. **Unit 2:** the
|
||||
Chat scene in the vertical tier — rect only (the circle option is not offered and the model
|
||||
refuses it), the small tile or enlarged to fill one whole 607×540 area, top or bottom by where the
|
||||
camera is (the creator's table, 2026-10-17). **Left:** the creator's hands-on check of the whole
|
||||
vv feature. See [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md)
|
||||
and `ai.md` → "Vertical (9:16) tier camera placement".
|
||||
- **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.
|
||||
- **🔶 Webcam cadence → resolution — IN PROGRESS, creator-queued 2026-10-05** (DHH/lipreading: the
|
||||
webcam judders, ~1 hitch in 5, invisible in the output file). **UNIT 0 shipped** (counters +
|
||||
`Helpers/FrameCadence.cs`; see [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) → "Frame cadence"). **Needs a creator recording before the
|
||||
next unit.** Remaining units, all agreed 2026-10-05, one Good Dog test each:
|
||||
- **⛔ UNIT 1 IS NOT NEXT — re-scoped 2026-10-05.** The first real reading exonerated the camera:
|
||||
it delivers 30.0/s with zero hitches; the compositor manages **6.8 composites/s at ~141ms each**,
|
||||
so the file is **88.7% duplicate frames** and that is the judder. 720p triples the bytes stretched
|
||||
per render on an already-over-budget path. Keep `ExclusiveControl` + 720p as a **sharpness** win
|
||||
and schedule it AFTER the compositor work.
|
||||
- **✅ SHIPPED 2026-10-16 (measurement only): per-element composite timing + a scene dump.**
|
||||
`CompositeTimings` + the pump's `FramePump parts (N renders/5s): …` line and
|
||||
`FramePump scene dump:` block (see [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) → "Per-element composite timing + scene dump").
|
||||
One integration test: `ytLive.Tests/CompositeTimingTests.cs`. **NOW WAITING ON A CREATOR TAKE
|
||||
(Release):** the parts line must name which of the 6 elements eats the ~140ms and the dump must
|
||||
show the shape of the slow take vs the fast one (10:03 → 12:01, 29–34ms → 143–155ms, same shape).
|
||||
**No fix until a real reading names the element** — the next unit is the fix that reading dictates.
|
||||
- **📖 FIRST INSTRUMENTED TAKE READ (2026-10-16, Release, recording, 1920×1080 1:1 master tier):
|
||||
THE JUDDER DID NOT REPRODUCE — and no element is expensive.** 60 composites/s, avg render
|
||||
1.0–1.1ms, biggest element in the scene = webcam **0.4ms** (raster 0.2ms), backdrop 0.2ms, the
|
||||
other four under the 0.1ms floor; webcam 30.0/s at the resolver (longest gap 50ms); the FILE holds
|
||||
29.6 distinct webcam images/s with 82% of images held exactly 2 frames. **Nothing to fix yet.**
|
||||
The historic slow numbers are different configurations, not a slow scene — Oct 9 19:48 was the
|
||||
**vertical** tier (`comp=608x1080`, 45ms) and Oct 5 12:01's geometry was never logged (the
|
||||
`scene=/comp=` suffix didn't exist); every slow window shows `cache xR/0H` (the whole-frame cache
|
||||
never reusing) while the healthy one shows `151R/150H`, and one Oct 5 take latches fast→slow
|
||||
mid-take. **NEXT: a Release take in the failing geometry** — vertical tier first, then the 21:9
|
||||
display config — and only that take's `parts` line names the stage. See `ai.md` → "✅ What the
|
||||
first instrumented take said".
|
||||
- **Unit 2 (later, unchanged):** blinking amber **"SD" badge**, preview only, never in the
|
||||
recording/stream, when a better format was available and not obtained. Top-right corner, or the
|
||||
1:30 rim point when the clip is round.
|
||||
- **Deferred to after Unit 1:** `MediaCaptureSharingMode.ExclusiveControl` + MJPG ≥30fps up to
|
||||
1280×720 via the existing `SetMediaStreamPropertiesAsync` path (refused only because
|
||||
`SharedReadOnly`); same-camera `SharedReadOnly` retry. **The creator reversed the ban on taking an
|
||||
exclusive camera lock.** API probe already done — see HANDOFF.md, incl. the `SetFormatAsync`
|
||||
dead end.
|
||||
- ⛔ **No fallback-camera feature, ever** — *"that's bypassing the user's explicit choice."*
|
||||
- **Background removal + blur = v1 requirement (2026-10-08)** — reverses the 2026-10-05
|
||||
exclusion. Bundled ONNX + onnxruntime, no downloads, no GPU requirement. Per-element
|
||||
Remove/Blur + strength. See [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md).
|
||||
- **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*.
|
||||
- **Both formerly-open questions are now CLOSED (2026-09-27).** Store revenue share is
|
||||
**verified at 15%, subscriptions included** (ADA v8.10 §6(b)(i) — read from the PDF, not a
|
||||
search excerpt), and the IAP tier shape is **decided: `$10/mo` subscription + `$400`
|
||||
lifetime, no annual**, declared in Partner Center as two products. Full detail in
|
||||
`TASKS/task-48-distribution-msix.md` item 0. ⚠️ Do **not** re-open the fee question without
|
||||
re-reading the ADA, and do not *assume* 15% if the terms ever move.
|
||||
- **✅ item 1 SHIPPED 2026-09-28 — bundle ffmpeg instead of downloading it.** The app no
|
||||
longer fetches an executable at runtime (Store policy 10.2.2, and the pin 404'd on the
|
||||
creator's first real recording on 2026-09-01). `scripts/fetch-ffmpeg.sh` populates a
|
||||
gitignored `ffmpeg/` from a **SHA-256-verified** pinned month-end BtbN artifact; the csproj
|
||||
copies it to output/publish and **fails the build** if it is missing; `FfmpegLocator` lost its
|
||||
downloader and now resolves **bundle → PATH**. Immutable tag recorded per the licensing rule:
|
||||
`autobuild-2026-08-31-13-27`. `ffplay.exe` is not shipped. Verified: ffmpeg was the **only**
|
||||
download-and-execute third-party binary — `e_sqlite3.dll`/`WebView2Loader.dll` already ship
|
||||
via NuGet and OpenH264 is compiled into `avcodec-63.dll`. **Remaining gaps are not bundling
|
||||
problems:** the WebView2 Evergreen *runtime* is a machine prerequisite (item 2), and
|
||||
**Velopack's updater downloads and executes — its teardown is a compliance action.**
|
||||
Detail: `TASKS/task-48-distribution-msix.md` §1.
|
||||
- **⏸ TASK 48 / ALL distribution & packaging work — DEFERRED 2026-10-26 (creator ruling). Do not
|
||||
start, plan, or re-litigate any of it until we are close to v1.** Verbatim reason: *"we're not
|
||||
anywhere close to release just considering the remaining programming tasks"* and *"I am getting
|
||||
ready to do a complete UX redesign"* — that redesign began 2026-10-26 (see [`HANDOFF.md`](HANDOFF.md)
|
||||
for landed UI slices and current worktree). Packaging, signing, installer and updater decisions
|
||||
would chase UI and scope that are still changing, so the urgency is withdrawn, not the task.
|
||||
What is frozen:
|
||||
- **TASK 48 items 6-8** (recording invariant comment, disqualifier re-check, pre-submission gates)
|
||||
and the creator's items 4/5 (Certification Kit, Partner Center listing).
|
||||
- **The route question itself.** The map currently contradicts itself: `ai.md` →
|
||||
"Windows packaging & distribution" says the **Microsoft Store route is dead (2026-10-22)** —
|
||||
direct download from `llamacasty.com` + Steam, ship unsigned — and `MONETIZATION.md`/`MARCOM.md`
|
||||
agree, while *this file*, `TASKS/task-48-distribution-msix.md` and `HANDOFF.md` still read
|
||||
"Microsoft Store / Store IAP decided". **Resolve it in ONE distribution sweep at the revisit —
|
||||
do not patch it piecemeal now.**
|
||||
- **Code signing** (ship unsigned vs buy OV/EV) — current recorded posture: ship unsigned and
|
||||
accept the "unrecognized publisher" warning until download reputation accrues.
|
||||
- **The updater.** Deleting Velopack (TASK 48 slice 3) left the app with **no update path at all**,
|
||||
and `ytLive.Tests/StorePackagingTests.cs` currently *asserts that no updater exists*. That guard
|
||||
must be **rewritten with the chosen updater** at the revisit, never left to veto one.
|
||||
- The MSIX package + `scripts/package-msix.sh` still build (159 MB artifact) — a working artefact,
|
||||
submitted nowhere. Leave it alone.
|
||||
- **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 — ✅ DECIDED 2026-09-27: `$10/mo` subscription, or `$400` lifetime. No annual.**
|
||||
`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 ruled against
|
||||
it and back to a subscription, anchored on **Meld** (`$20/mo = $240/yr` entry — the only
|
||||
competitor sharing our shape: streaming tool, subscription, YouTube-native). The creator's own
|
||||
read: **"XSplit only has legs because of their vcam product"** — its price is carried by a
|
||||
system-wide driver, and **virtual camera output is permanently out of product**, so XSplit is
|
||||
not a pricing comparator and its "lifetime ≈ 2x one year of sub" anchor is retired.
|
||||
- **The finding that drove no-annual (2026-09-27): Microsoft Store does not pro-rate.**
|
||||
Pro-rated refunds on cancellation exist only in a narrow set of countries, and
|
||||
**"monthly subscriptions and initial (pre-renewal) purchases aren't eligible for a prorated
|
||||
refund"** — so a first-year annual subscriber who cancels loses the remaining months in most
|
||||
countries, **and the developer cannot refund it**. That is verbatim the grievance the creator
|
||||
raised, so it is exactly the trap to avoid. Sources in `MONETIZATION.md` → "No annual tier".
|
||||
- ⛔ **Clean numbers, no `.99`** (creator ruling: *"Let's stop with the x.99 stuff - I find that
|
||||
irritating. Just say: ten bucks a month"*). The `$x.99` convention only makes a price *look*
|
||||
cheaper and is incoherent for a brand sold on honesty and no-dark-patterns. `MONETIZATION.md`
|
||||
→ "Clean numbers".
|
||||
- **No feature gating** — pre-existing repo posture (TASK 36 monetization stance: free gets
|
||||
everything, the branding flash is the only paid delta), noted here because it gains a
|
||||
**revenue** argument: the free tier's reach is the funnel and the flash is an ad running
|
||||
inside other people's content. Not a new ruling.
|
||||
- ✅ **Store revenue share — VERIFIED 15%, subscriptions included.** From the ADA v8.10 PDF
|
||||
itself, §6(b)(i): 15% applies to "any Apps (and any In-App Products in such Apps) that are
|
||||
not listed in Section 6(b)(iii)". LlamaCasty is a Windows PC App — not a Game, not Xbox
|
||||
console, not Windows 8 — so **15% governs both tiers; there is no subscription surcharge.**
|
||||
The agreement's own changelog (v8.0, Oct 26 2017) says it outright: *"implement the 85/15
|
||||
revenue share for non-Game subscriptions."* ⇒ **`$10/mo` nets `$8.50`; `$400` nets `$340`.**
|
||||
- ⛔ **Two obligations the subscription creates (ADA §6(h))**, which drive the lifetime tier's
|
||||
value as a hedge: (1) we must **fulfil the subscription for the whole period as marketed**,
|
||||
and on breach Microsoft may refund *"the full amount, plus taxes... in Microsoft's sole
|
||||
discretion"*; (2) **raising the price disables auto-renew**, so `$10` is effectively locked
|
||||
for the product's life.
|
||||
- **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)
|
||||
|
||||
- **⛔ Replace the always-Private broadcast lock with a real visibility option (creator ruling,
|
||||
restated 2026-10-04).** `privacyStatus = "private"` hardcoded in
|
||||
`YouTubeStreamService.CreateBroadcast` is deliberate and correct for the whole pre-v1 test
|
||||
phase — it is the reason test VODs of a still-breaking product stay off the channel. At v1 it
|
||||
becomes a user-selectable visibility option in the Go-Live dialog. **Before v1, do not touch it and do not raise it as a
|
||||
question, observation or suggestion — it has been asked and answered repeatedly.** Full rationale
|
||||
and the retired "can we delete the test VODs?" question: `ai.md` → YouTube Live API design
|
||||
constraints.
|
||||
- **Verified 2026-10-04: a Go Live video IS private.** Three independent hardcodes
|
||||
(`CreateBroadcast` → `"private"`, `GoLiveViewModel.Visibility => "Private"`,
|
||||
`BeginTestStream` → `StreamVisibility = "Private"`) and the one method that could change it,
|
||||
`YouTubeStreamService.UpdateBroadcast`, has **no production caller** — only
|
||||
`BroadcastPullOutTests.cs:120`. No path exists today by which an app-created broadcast becomes
|
||||
non-private.
|
||||
- **Two coupled v1 items — both are the deliberate end of that guarantee:**
|
||||
1. **Hide the TEST button.** It exists as the guaranteed-private path; once visibility is
|
||||
selectable, a separate safe button is redundant. One-line change —
|
||||
`ShowTestButton => ShowStreamActions && IsConnected` is a computed predicate, not a setting.
|
||||
2. **This is the unit that first wires `UpdateBroadcast` into production.** It is the only code
|
||||
that can change a broadcast's privacy, and it is currently dead. Wiring it is exactly when
|
||||
private stops being guaranteed and becomes a user-selectable default. Correct at v1,
|
||||
ruinous before — and "it's dead code, let me call it" is not a reason to do it early.
|
||||
- A **dry run** may still be worth having at v1 on its own merits; that is a separate question
|
||||
from the privacy mechanism and is **not** decided by the above.
|
||||
|
||||
- **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`).
|
||||
- ✅ **RESOLVED — do not re-open.** The `OverlayHost.xaml` copy claimed Polar unlocked *alerts*;
|
||||
alerts were never gated by `IsPremium`. The file is **deleted outright** by the TASK 10 teardown
|
||||
(TASK 48 item 0), so the copy is gone with it. The "or gate the alerts" option is **permanently
|
||||
closed** — there is no feature gating, ever.
|
||||
|
||||
---
|
||||
|
||||
## 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 + blur** (ONNX/DirectML; TASK 3 item 19) — ⛔ ruled out 2026-10-05
|
||||
(*"no - not doing it... a vcam app does it free; an in-app model is a runtime, a GPU/driver
|
||||
matrix, per-scene lighting tuning"*). **REVERSED 2026-10-08 — now a v1 requirement**
|
||||
([`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md)): bundled
|
||||
~2–5 MB ONNX model + onnxruntime, CPU-capable, no downloads, no NVIDIA. NVIDIA VFX SDK
|
||||
(~1 GB per-client redistributable, RTX 2000+) remains ruled out.
|
||||
- **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)
|
||||
|
||||
## Transitions & brand logo (status updated 2026-10-28)
|
||||
|
||||
- ✅ **Fade fix** — swaps fade from the outgoing picture (`TransitionFadeTests`). On-screen check pending.
|
||||
- ✅ **Transition picker** in App Settings (type + duration), saved with the layout (`TransitionPickerTests`).
|
||||
- ✅ **Wipe** — CPU blend, left-to-right reveal (`WipeTransitionTests`). Direction setting not yet added.
|
||||
- ✅ **Transitions in edit mode and on output** — preview animates the outgoing picture for scene changes in every mode; program output transitions while recording or streaming. Fade/Move fade out, Wipe shrinks the outgoing picture, Cut is immediate. Guard: `PreviewTransitionTests`.
|
||||
- ☐ **Move** — persistent layers (per-layer "carry across scenes" flag), per-scene placement, fixed z band.
|
||||
- ✅ **Brand logo** — one locked "Brand logo" image layer per scene, directly above the background; file in
|
||||
App Settings (imported to the asset table, the setting keeps the asset id); per-scene eye toggle; top-left
|
||||
default. Tests: `BrandLogoTests`. On-screen check pending.
|
||||
- ☐ **Cube** — GPU path gated on capability; CPU path only if it holds 60fps.
|
||||
- Gear: right-click opens App Settings (built, on-screen check pending). Left-click menu stays for now.
|
||||
[`research-youtube-api.md`](TASKS/research-youtube-api.md) · [`research-store-certification.md`](TASKS/research-store-certification.md)
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
# TASKS archive — snapshot 2026-10-29
|
||||
|
||||
The catalogue as it stood before the queue trim: 63 task rows with paragraph-length status prose,
|
||||
plus the historical "open items" narration. Kept so nothing is lost; **not** the live queue — see
|
||||
[`../TASKS.md`](../TASKS.md). Decisions that no longer govern are in
|
||||
[`../docs/ai/decisions.md`](../docs/ai/decisions.md).
|
||||
|
||||
---
|
||||
|
||||
# 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 paid delta (branding flash) | 🔶 **TEARDOWN, not a build** — Polar deleted in favour of Store IAP (2026-09-27); `$10/mo` + `$400` lifetime, no annual. **SUPERSEDED 2026-10-08** — free-forever ruling (watermark-free, gratuity/merch only); see TASK 54 | [`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 | ✅ Shipped 2026-08-15 — `Services/Audio/MasterLimiter.cs` (−1 dBFS ceiling, instant attack, smoothed release) applied at the end of `AudioMixer.FillAndMix` | [`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** | ⏸ **DEFERRED 2026-10-26 — revisit near v1, not now** (creator: the remaining programming work is far from release, and a complete UX redesign comes first). Frozen until then: items 6-8, the signing decision, the updater choice, and the route question itself. History: **✅ route decided 2026-09-27 (Store MSIX + Store IAP)**; **item 1 (bundle ffmpeg) SHIPPED 2026-09-28**; **item 2 (`Package.appxmanifest` + `scripts/package-msix.sh`) SHIPPED 2026-10-22 — a real 159 MB `LlamaCasty.msix` packs**; **item 3 (remove the Velopack updater) SHIPPED 2026-10-22** — the Store updates the app. Open: items 6-8 (recording invariant, disqualifiers, pre-submission gates); items 4/5 are the creator's (Certification Kit, Partner Center listing) | [`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) |
|
||||
| 50 | One-button "Publish this video" (resumable upload of the local recording) | ☐ Queued (2026-09-28) | [`TASKS/task-50-publish-recording.md`](TASKS/task-50-publish-recording.md) |
|
||||
| 51 | **Webcam background effects: Remove + Blur** | ✅ **Built 2026-10-09** (v1 requirement) — bundled MediaPipe Selfie Segmenter → ONNX (verified vs Google's runtime), Remove + Blur with strength, per-scene config persisted, one processing point in CameraManager, radio-pill UI in the webcam's Layers properties. Creator check 2026-10-09: **Remove jitter FIXED** (temporal mask smoothing 0.75/0.25, OBS/Jitsi-derived) and **Blur "heavy" FIXED** (quadratic radius 1→6 small px, default 0.5 = 2, StreamFX-consensus); recheck on the real camera pending | [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md) |
|
||||
| 52 | **Zoom box (anchored 2× magnifier)** | ✅ Done — shipped 2026-10-08 (3 creator corrections during the build: loupe → anchored capture over the full composite, drawn last → never shows the dim shading) | [`TASKS/task-52-zoom-box.md`](TASKS/task-52-zoom-box.md) |
|
||||
| 53 | **Output-format verification: Shorts/TikTok geometry (9:16)** | ☐ Queued (2026-10-08) — output formats never tested end-to-end; pin + prove the vertical geometry | [`TASKS/task-53-output-formats.md`](TASKS/task-53-output-formats.md) |
|
||||
| 54 | **Marcom round: free-forever positioning + merch storefront** | 🔶 In progress — llamacasty.com copy live 2026-10-08 (pricing stripped); merch POD + app About links queued | [`TASKS/task-54-marcom-free-merch.md`](TASKS/task-54-marcom-free-merch.md) |
|
||||
| 55 | **Disk hygiene: log rotation + low-disk guard** | 🔶 In progress — **slice 1 shipped 2026-10-22** (`startup.log` rolls at 5 MB to `startup.1.log`, one copy kept — it was uncapped at 23 MB and growing every few seconds); slices 2 (temp alert-clip cleanup) + 3 (low-disk guard) open | [`TASKS/task-55-disk-hygiene.md`](TASKS/task-55-disk-hygiene.md) |
|
||||
| 56 | **Pre-publication secret scan (full git history)** | ✅ Done — 2026-10-22, **CLEAN** (432 reachable commits + 63 dangling + worktree; 4 hits, all false positives: a `your-client-secret` doc placeholder and a test dummy — nothing to rotate). ⚠️ the repo was **already public** when the scan ran (anonymous GET → 200), so the gate came late — but nothing was exposed | [`TASKS/task-56-secret-scan.md`](TASKS/task-56-secret-scan.md) |
|
||||
| 57 | **Vertical (9:16) tier camera placement — the "vv" feature** | ✅ **Both units shipped 2026-10-21** (Unit 1 = Live scene: own placement per tier — small tile, circle-or-rect, one of six spots; drag-snap + manual entry; 16:9 restored exactly. Unit 2 = Chat scene: rect only — the circle is not offered — the tile or one whole 607×540 area) | [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md) |
|
||||
| 58 | **Properties-panel style consistency (the theme owns the look)** | ✅ Done — 2026-10-21 (the creator's "why is the drop-down not in the same style as the others?" — the drop-downs/fields/check boxes had been hand-painted instead of themed, so WPF drew the light system template; now every one carries a theme style, guarded by a test) | [`TASKS/task-58-properties-style-consistency.md`](TASKS/task-58-properties-style-consistency.md) |
|
||||
| 59 | **The properties panel is a hierarchy (titles own their content)** | ✅ Done — 2026-10-21 (the creator's ruling: a "Shape" title above the Mirror/Rect row, the drag hint indented under it, the same for Geometry / Border Color / Border Thickness, Remove + Blur under Background, Strength under Blur — and the shape became a CHOICE: Rect | Circle with Rect selected by default) | [`TASKS/task-59-properties-hierarchy.md`](TASKS/task-59-properties-hierarchy.md) |
|
||||
| 60 | **The border-colour drop-down names every colour** | ✅ Done — 2026-10-21 (the creator's "the #hex values are good, but add a corresponding color name/label next to the value" — the 14 colours moved out of the click handler into `Models/BorderColorPalette.cs`, and each row now reads "#e94560 Crimson") | [`TASKS/task-60-border-color-names.md`](TASKS/task-60-border-color-names.md) |
|
||||
| 61 | **The geometry fields share one fixed width** | ✅ Done — 2026-10-21 (the creator's "the Y and H boxes should be the same width as the X and W boxes and those widths should be fixed to accomodate the max number of possible digits" — the Y/H fields used to stretch to fill the row; all four are now 44px, wide enough for 1920 plus a sign) | [`TASKS/task-61-geometry-field-widths.md`](TASKS/task-61-geometry-field-widths.md) |
|
||||
| 62 | **The border-colour field shows the colour's name next to the hex** | ✅ Done — 2026-10-21 (the creator: "the border color drop down box content should also include the name of the color next to the hex value as they in the drop-down list box" — the field keeps the hex editable and the palette's name sits right beside it, empty for a custom hex) | [`TASKS/task-62-border-colour-name-in-the-field.md`](TASKS/task-62-border-colour-name-in-the-field.md) |
|
||||
| 63 | **The border-colour picker is the app's own drop-down** | ✅ Done — 2026-10-21 (popup row click deselection fixed 2026-10-22: `LeftPanel.IsDescendantOf` now crosses `PopupRoot` → `PART_Popup` via `LogicalTreeHelper.GetParent` when `VisualTreeHelper.GetParent` returns `null`, so clicking a colour in the open `YtComboBox` updates `BorderColor` instead of exiting properties) | [`TASKS/task-63-border-colour-app-dropdown.md`](TASKS/task-63-border-colour-app-dropdown.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** — the Velopack update URL is **RETIRED (2026-10-22)**: the updater was deleted (TASK 48 slice 3) and the Store updates the app
|
||||
- **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`. ⛔ **The Polar
|
||||
File-Download benefit is dead** — the Store hosts and updates the app, so there is no upload
|
||||
step (TASK 48). 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)
|
||||
- **✅ TASK 57 — the "vv" vertical camera layout** (both units shipped 2026-10-21). The vertical
|
||||
tier is a centred band crop of the same master, so the 16:9 camera placement falls outside the
|
||||
cropped window and the creator is not in the vertical stream at all. **Unit 1:** each scene keeps
|
||||
its own vertical placement (position, size, shape); the Live scene gets a small tile in one of
|
||||
six spots (top/bottom × left/centre/right), circle or rect, placed by drag-snap or typed into the
|
||||
new Geometry fields; switching back to 16:9 restores the desktop layout exactly. **Unit 2:** the
|
||||
Chat scene in the vertical tier — rect only (the circle option is not offered and the model
|
||||
refuses it), the small tile or enlarged to fill one whole 607×540 area, top or bottom by where the
|
||||
camera is (the creator's table, 2026-10-17). **Left:** the creator's hands-on check of the whole
|
||||
vv feature. See [`TASKS/task-57-vertical-camera-layout.md`](TASKS/task-57-vertical-camera-layout.md)
|
||||
and `ai.md` → "Vertical (9:16) tier camera placement".
|
||||
- **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.
|
||||
- **🔶 Webcam cadence → resolution — IN PROGRESS, creator-queued 2026-10-05** (DHH/lipreading: the
|
||||
webcam judders, ~1 hitch in 5, invisible in the output file). **UNIT 0 shipped** (counters +
|
||||
`Helpers/FrameCadence.cs`; see [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) → "Frame cadence"). **Needs a creator recording before the
|
||||
next unit.** Remaining units, all agreed 2026-10-05, one Good Dog test each:
|
||||
- **⛔ UNIT 1 IS NOT NEXT — re-scoped 2026-10-05.** The first real reading exonerated the camera:
|
||||
it delivers 30.0/s with zero hitches; the compositor manages **6.8 composites/s at ~141ms each**,
|
||||
so the file is **88.7% duplicate frames** and that is the judder. 720p triples the bytes stretched
|
||||
per render on an already-over-budget path. Keep `ExclusiveControl` + 720p as a **sharpness** win
|
||||
and schedule it AFTER the compositor work.
|
||||
- **✅ SHIPPED 2026-10-16 (measurement only): per-element composite timing + a scene dump.**
|
||||
`CompositeTimings` + the pump's `FramePump parts (N renders/5s): …` line and
|
||||
`FramePump scene dump:` block (see [`docs/ai/frame-pipeline.md`](docs/ai/frame-pipeline.md) → "Per-element composite timing + scene dump").
|
||||
One integration test: `ytLive.Tests/CompositeTimingTests.cs`. **NOW WAITING ON A CREATOR TAKE
|
||||
(Release):** the parts line must name which of the 6 elements eats the ~140ms and the dump must
|
||||
show the shape of the slow take vs the fast one (10:03 → 12:01, 29–34ms → 143–155ms, same shape).
|
||||
**No fix until a real reading names the element** — the next unit is the fix that reading dictates.
|
||||
- **📖 FIRST INSTRUMENTED TAKE READ (2026-10-16, Release, recording, 1920×1080 1:1 master tier):
|
||||
THE JUDDER DID NOT REPRODUCE — and no element is expensive.** 60 composites/s, avg render
|
||||
1.0–1.1ms, biggest element in the scene = webcam **0.4ms** (raster 0.2ms), backdrop 0.2ms, the
|
||||
other four under the 0.1ms floor; webcam 30.0/s at the resolver (longest gap 50ms); the FILE holds
|
||||
29.6 distinct webcam images/s with 82% of images held exactly 2 frames. **Nothing to fix yet.**
|
||||
The historic slow numbers are different configurations, not a slow scene — Oct 9 19:48 was the
|
||||
**vertical** tier (`comp=608x1080`, 45ms) and Oct 5 12:01's geometry was never logged (the
|
||||
`scene=/comp=` suffix didn't exist); every slow window shows `cache xR/0H` (the whole-frame cache
|
||||
never reusing) while the healthy one shows `151R/150H`, and one Oct 5 take latches fast→slow
|
||||
mid-take. **NEXT: a Release take in the failing geometry** — vertical tier first, then the 21:9
|
||||
display config — and only that take's `parts` line names the stage. See `ai.md` → "✅ What the
|
||||
first instrumented take said".
|
||||
- **Unit 2 (later, unchanged):** blinking amber **"SD" badge**, preview only, never in the
|
||||
recording/stream, when a better format was available and not obtained. Top-right corner, or the
|
||||
1:30 rim point when the clip is round.
|
||||
- **Deferred to after Unit 1:** `MediaCaptureSharingMode.ExclusiveControl` + MJPG ≥30fps up to
|
||||
1280×720 via the existing `SetMediaStreamPropertiesAsync` path (refused only because
|
||||
`SharedReadOnly`); same-camera `SharedReadOnly` retry. **The creator reversed the ban on taking an
|
||||
exclusive camera lock.** API probe already done — see HANDOFF.md, incl. the `SetFormatAsync`
|
||||
dead end.
|
||||
- ⛔ **No fallback-camera feature, ever** — *"that's bypassing the user's explicit choice."*
|
||||
- **Background removal + blur = v1 requirement (2026-10-08)** — reverses the 2026-10-05
|
||||
exclusion. Bundled ONNX + onnxruntime, no downloads, no GPU requirement. Per-element
|
||||
Remove/Blur + strength. See [`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md).
|
||||
- **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*.
|
||||
- **Both formerly-open questions are now CLOSED (2026-09-27).** Store revenue share is
|
||||
**verified at 15%, subscriptions included** (ADA v8.10 §6(b)(i) — read from the PDF, not a
|
||||
search excerpt), and the IAP tier shape is **decided: `$10/mo` subscription + `$400`
|
||||
lifetime, no annual**, declared in Partner Center as two products. Full detail in
|
||||
`TASKS/task-48-distribution-msix.md` item 0. ⚠️ Do **not** re-open the fee question without
|
||||
re-reading the ADA, and do not *assume* 15% if the terms ever move.
|
||||
- **✅ item 1 SHIPPED 2026-09-28 — bundle ffmpeg instead of downloading it.** The app no
|
||||
longer fetches an executable at runtime (Store policy 10.2.2, and the pin 404'd on the
|
||||
creator's first real recording on 2026-09-01). `scripts/fetch-ffmpeg.sh` populates a
|
||||
gitignored `ffmpeg/` from a **SHA-256-verified** pinned month-end BtbN artifact; the csproj
|
||||
copies it to output/publish and **fails the build** if it is missing; `FfmpegLocator` lost its
|
||||
downloader and now resolves **bundle → PATH**. Immutable tag recorded per the licensing rule:
|
||||
`autobuild-2026-08-31-13-27`. `ffplay.exe` is not shipped. Verified: ffmpeg was the **only**
|
||||
download-and-execute third-party binary — `e_sqlite3.dll`/`WebView2Loader.dll` already ship
|
||||
via NuGet and OpenH264 is compiled into `avcodec-63.dll`. **Remaining gaps are not bundling
|
||||
problems:** the WebView2 Evergreen *runtime* is a machine prerequisite (item 2), and
|
||||
**Velopack's updater downloads and executes — its teardown is a compliance action.**
|
||||
Detail: `TASKS/task-48-distribution-msix.md` §1.
|
||||
- **⏸ TASK 48 / ALL distribution & packaging work — DEFERRED 2026-10-26 (creator ruling). Do not
|
||||
start, plan, or re-litigate any of it until we are close to v1.** Verbatim reason: *"we're not
|
||||
anywhere close to release just considering the remaining programming tasks"* and *"I am getting
|
||||
ready to do a complete UX redesign"* — that redesign began 2026-10-26 (see [`HANDOFF.md`](HANDOFF.md)
|
||||
for landed UI slices and current worktree). Packaging, signing, installer and updater decisions
|
||||
would chase UI and scope that are still changing, so the urgency is withdrawn, not the task.
|
||||
What is frozen:
|
||||
- **TASK 48 items 6-8** (recording invariant comment, disqualifier re-check, pre-submission gates)
|
||||
and the creator's items 4/5 (Certification Kit, Partner Center listing).
|
||||
- **The route question itself.** The map currently contradicts itself: `ai.md` →
|
||||
"Windows packaging & distribution" says the **Microsoft Store route is dead (2026-10-22)** —
|
||||
direct download from `llamacasty.com` + Steam, ship unsigned — and `MONETIZATION.md`/`MARCOM.md`
|
||||
agree, while *this file*, `TASKS/task-48-distribution-msix.md` and `HANDOFF.md` still read
|
||||
"Microsoft Store / Store IAP decided". **Resolve it in ONE distribution sweep at the revisit —
|
||||
do not patch it piecemeal now.**
|
||||
- **Code signing** (ship unsigned vs buy OV/EV) — current recorded posture: ship unsigned and
|
||||
accept the "unrecognized publisher" warning until download reputation accrues.
|
||||
- **The updater.** Deleting Velopack (TASK 48 slice 3) left the app with **no update path at all**,
|
||||
and `ytLive.Tests/StorePackagingTests.cs` currently *asserts that no updater exists*. That guard
|
||||
must be **rewritten with the chosen updater** at the revisit, never left to veto one.
|
||||
- The MSIX package + `scripts/package-msix.sh` still build (159 MB artifact) — a working artefact,
|
||||
submitted nowhere. Leave it alone.
|
||||
- **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 — ✅ DECIDED 2026-09-27: `$10/mo` subscription, or `$400` lifetime. No annual.**
|
||||
`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 ruled against
|
||||
it and back to a subscription, anchored on **Meld** (`$20/mo = $240/yr` entry — the only
|
||||
competitor sharing our shape: streaming tool, subscription, YouTube-native). The creator's own
|
||||
read: **"XSplit only has legs because of their vcam product"** — its price is carried by a
|
||||
system-wide driver, and **virtual camera output is permanently out of product**, so XSplit is
|
||||
not a pricing comparator and its "lifetime ≈ 2x one year of sub" anchor is retired.
|
||||
- **The finding that drove no-annual (2026-09-27): Microsoft Store does not pro-rate.**
|
||||
Pro-rated refunds on cancellation exist only in a narrow set of countries, and
|
||||
**"monthly subscriptions and initial (pre-renewal) purchases aren't eligible for a prorated
|
||||
refund"** — so a first-year annual subscriber who cancels loses the remaining months in most
|
||||
countries, **and the developer cannot refund it**. That is verbatim the grievance the creator
|
||||
raised, so it is exactly the trap to avoid. Sources in `MONETIZATION.md` → "No annual tier".
|
||||
- ⛔ **Clean numbers, no `.99`** (creator ruling: *"Let's stop with the x.99 stuff - I find that
|
||||
irritating. Just say: ten bucks a month"*). The `$x.99` convention only makes a price *look*
|
||||
cheaper and is incoherent for a brand sold on honesty and no-dark-patterns. `MONETIZATION.md`
|
||||
→ "Clean numbers".
|
||||
- **No feature gating** — pre-existing repo posture (TASK 36 monetization stance: free gets
|
||||
everything, the branding flash is the only paid delta), noted here because it gains a
|
||||
**revenue** argument: the free tier's reach is the funnel and the flash is an ad running
|
||||
inside other people's content. Not a new ruling.
|
||||
- ✅ **Store revenue share — VERIFIED 15%, subscriptions included.** From the ADA v8.10 PDF
|
||||
itself, §6(b)(i): 15% applies to "any Apps (and any In-App Products in such Apps) that are
|
||||
not listed in Section 6(b)(iii)". LlamaCasty is a Windows PC App — not a Game, not Xbox
|
||||
console, not Windows 8 — so **15% governs both tiers; there is no subscription surcharge.**
|
||||
The agreement's own changelog (v8.0, Oct 26 2017) says it outright: *"implement the 85/15
|
||||
revenue share for non-Game subscriptions."* ⇒ **`$10/mo` nets `$8.50`; `$400` nets `$340`.**
|
||||
- ⛔ **Two obligations the subscription creates (ADA §6(h))**, which drive the lifetime tier's
|
||||
value as a hedge: (1) we must **fulfil the subscription for the whole period as marketed**,
|
||||
and on breach Microsoft may refund *"the full amount, plus taxes... in Microsoft's sole
|
||||
discretion"*; (2) **raising the price disables auto-renew**, so `$10` is effectively locked
|
||||
for the product's life.
|
||||
- **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)
|
||||
|
||||
- **⛔ Replace the always-Private broadcast lock with a real visibility option (creator ruling,
|
||||
restated 2026-10-04).** `privacyStatus = "private"` hardcoded in
|
||||
`YouTubeStreamService.CreateBroadcast` is deliberate and correct for the whole pre-v1 test
|
||||
phase — it is the reason test VODs of a still-breaking product stay off the channel. At v1 it
|
||||
becomes a user-selectable visibility option in the Go-Live dialog. **Before v1, do not touch it and do not raise it as a
|
||||
question, observation or suggestion — it has been asked and answered repeatedly.** Full rationale
|
||||
and the retired "can we delete the test VODs?" question: `ai.md` → YouTube Live API design
|
||||
constraints.
|
||||
- **Verified 2026-10-04: a Go Live video IS private.** Three independent hardcodes
|
||||
(`CreateBroadcast` → `"private"`, `GoLiveViewModel.Visibility => "Private"`,
|
||||
`BeginTestStream` → `StreamVisibility = "Private"`) and the one method that could change it,
|
||||
`YouTubeStreamService.UpdateBroadcast`, has **no production caller** — only
|
||||
`BroadcastPullOutTests.cs:120`. No path exists today by which an app-created broadcast becomes
|
||||
non-private.
|
||||
- **Two coupled v1 items — both are the deliberate end of that guarantee:**
|
||||
1. **Hide the TEST button.** It exists as the guaranteed-private path; once visibility is
|
||||
selectable, a separate safe button is redundant. One-line change —
|
||||
`ShowTestButton => ShowStreamActions && IsConnected` is a computed predicate, not a setting.
|
||||
2. **This is the unit that first wires `UpdateBroadcast` into production.** It is the only code
|
||||
that can change a broadcast's privacy, and it is currently dead. Wiring it is exactly when
|
||||
private stops being guaranteed and becomes a user-selectable default. Correct at v1,
|
||||
ruinous before — and "it's dead code, let me call it" is not a reason to do it early.
|
||||
- A **dry run** may still be worth having at v1 on its own merits; that is a separate question
|
||||
from the privacy mechanism and is **not** decided by the above.
|
||||
|
||||
- **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`).
|
||||
- ✅ **RESOLVED — do not re-open.** The `OverlayHost.xaml` copy claimed Polar unlocked *alerts*;
|
||||
alerts were never gated by `IsPremium`. The file is **deleted outright** by the TASK 10 teardown
|
||||
(TASK 48 item 0), so the copy is gone with it. The "or gate the alerts" option is **permanently
|
||||
closed** — there is no feature gating, ever.
|
||||
|
||||
---
|
||||
|
||||
## 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 + blur** (ONNX/DirectML; TASK 3 item 19) — ⛔ ruled out 2026-10-05
|
||||
(*"no - not doing it... a vcam app does it free; an in-app model is a runtime, a GPU/driver
|
||||
matrix, per-scene lighting tuning"*). **REVERSED 2026-10-08 — now a v1 requirement**
|
||||
([`TASKS/task-51-webcam-bg-effects.md`](TASKS/task-51-webcam-bg-effects.md)): bundled
|
||||
~2–5 MB ONNX model + onnxruntime, CPU-capable, no downloads, no NVIDIA. NVIDIA VFX SDK
|
||||
(~1 GB per-client redistributable, RTX 2000+) remains ruled out.
|
||||
- **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)
|
||||
|
||||
## Transitions & brand logo (status updated 2026-10-28)
|
||||
|
||||
- ✅ **Fade fix** — swaps fade from the outgoing picture (`TransitionFadeTests`). On-screen check pending.
|
||||
- ✅ **Transition picker** in App Settings (type + duration), saved with the layout (`TransitionPickerTests`).
|
||||
- ✅ **Wipe** — CPU blend, left-to-right reveal (`WipeTransitionTests`). Direction setting not yet added.
|
||||
- ✅ **Transitions in edit mode and on output** — preview animates the outgoing picture for scene changes in every mode; program output transitions while recording or streaming. Fade/Move fade out, Wipe shrinks the outgoing picture, Cut is immediate. Guard: `PreviewTransitionTests`.
|
||||
- ☐ **Move** — persistent layers (per-layer "carry across scenes" flag), per-scene placement, fixed z band.
|
||||
- ✅ **Brand logo** — one locked "Brand logo" image layer per scene, directly above the background; file in
|
||||
App Settings (imported to the asset table, the setting keeps the asset id); per-scene eye toggle; top-left
|
||||
default. Tests: `BrandLogoTests`. On-screen check pending.
|
||||
- ☐ **Cube** — GPU path gated on capability; CPU path only if it holds 60fps.
|
||||
- Gear: right-click opens App Settings (built, on-screen check pending). Left-click menu stays for now.
|
||||
@@ -0,0 +1,89 @@
|
||||
# Decisions log — retired paths, kept so they are not re-litigated
|
||||
|
||||
One dated line per decision that **no longer governs**, plus the reason it died. Read only when
|
||||
you are about to re-open one of them. Live rules live in the subsystem files; this is the
|
||||
graveyard with cause of death.
|
||||
|
||||
## Monetization — all superseded by the free-forever ruling (2026-10-08, TASK 54)
|
||||
|
||||
- **2026-09-21** one-time perpetual licence `$29` founder → `$49` list — superseded 6 days later.
|
||||
- **2026-09-27** `$10/mo` subscription or `$400` lifetime, **no annual**. Store revenue share was
|
||||
verified at **15%** (ADA v8.10 §6(b)(i) → `$8.50` / `$340` net), including subscriptions.
|
||||
- **No annual tier** because Microsoft does not pro-rate: *"monthly subscriptions and initial
|
||||
(pre-renewal) purchases aren't eligible for a prorated refund"* — a first-year subscriber who
|
||||
cancels loses the remaining months and the developer cannot refund it.
|
||||
- **Clean numbers, no `.99`** (creator: *"Let's stop with the x.99 stuff"*). The convention exists
|
||||
only to make a price look cheaper, which is incoherent for a brand sold on honesty.
|
||||
- **Escalation model** (2026-09-01): the credit was deliberately obnoxious self-promo; the cadence
|
||||
*is* the nagware — no modals, no feature locks, no stream-time cutoffs.
|
||||
- **Pre-GA posture — preview-only flash** (reversed 2026-09-26): the credit used to render in the
|
||||
preview and never on the broadcast, to keep test VODs clean. The creator ruled the free tier must
|
||||
be honest advertising; it now composites into output and recordings when enabled.
|
||||
- **Rejected**: always-on watermark (obscurable — replaced by the flash), hard stream-time cutoffs
|
||||
(a stream dying mid-broadcast reads as broken), soft-limit nagging, freemium feature tiers,
|
||||
donation-only, and any resolution/quality ceiling. Auto step-down (TASK 33) is a *protect the
|
||||
stream* feature, never a monetization penalty.
|
||||
- **Polar** — $50 already paid, 5% + $0.50 Starter, merchant of record. Dormant, not
|
||||
deleted-in-principle: no product, org ID, key prefix or discount code exists
|
||||
(`d105dfa1-…`, `c05fb364-…`, `LCYT-`, `LLAMAFOUNDER`/`LLAMA50`, the portal-validate endpoint and
|
||||
the 14-day offline-grace machine are all gone). `Distribution.md` §4.2/§4.5 keeps the design.
|
||||
- **Store IAP** was never implemented — `Windows.Services.Store` appears nowhere in the repo.
|
||||
- ⚠️ **Two obligations any future subscription would create** (ADA §6(h)): fulfil the whole period
|
||||
as marketed (breach → Microsoft may refund in full at its discretion), and **raising the price
|
||||
disables auto-renew** — the launch price is effectively locked for the product's life.
|
||||
|
||||
## Distribution — Store route withdrawn 2026-10-22
|
||||
|
||||
- **2026-09-27 Store MSIX + Store IAP decided; 2026-10-22 withdrawn.** A brand name on the
|
||||
Store's *publisher* line requires a **Company** account → a registered legal entity (LLC ~$85
|
||||
New Mexico with a ~2-week SOS backlog, ~$143 Wyoming). Partner Center does **not** convert an
|
||||
Individual account to a Company account later, and an Individual account publishes under the
|
||||
verified legal name. The creator will not fund an entity for a nameplate.
|
||||
- **2026-10-22 Velopack deleted** — it downloaded and executed, which Store policy 10.2.2 forbids.
|
||||
The Store was going to update the app, so that premise died with the route: **the app now has no
|
||||
update path**, and `StorePackagingTests` currently asserts that no updater exists — rewrite that
|
||||
guard with the chosen updater, never leave it to veto one.
|
||||
- **The MSIX package still builds** (159 MB `artifacts/LlamaCasty.msix`, submitted nowhere). The
|
||||
route contradiction between `ai.md`'s older text and the TASKS files is settled in one sweep at
|
||||
the distribution revisit, not patched piecemeal.
|
||||
- **Signing**: Microsoft removed the EV instant-bypass for SmartScreen in March 2024, so EV buys
|
||||
what OV buys; the 2026-10-22 ruling is **ship unsigned** and accept the "unrecognized publisher"
|
||||
warning until download reputation accrues. Deferred, not rejected.
|
||||
- **Steam when $100 exists**: Steam Direct $100/product (recoupable after $1,000 AGR), categories
|
||||
*Audio/Video Production* / *Player Tools* (OBS precedent), developer/publisher name self-chosen
|
||||
(no entity needed), 5+ 1920×1080 screenshots + capsule art, a build Valve runs, a 2-week live
|
||||
page, 30% cut.
|
||||
|
||||
## Product surface — retired
|
||||
|
||||
- **2026-09-24 StreamElements replaced by native alerts** (TASK 43). No third-party widget feed:
|
||||
the chat poll *is* the event feed.
|
||||
- **2026-09-01 the screens/layers settings audit** was retired into the gold pass (TASK 36 item 3)
|
||||
rather than run as session debt.
|
||||
- **2026-10-04 launch position**: restoring the saved position was rejected for a derived
|
||||
top-left placement. The live rule is in `ui-surface.md`; the old clamp's tolerance range is why
|
||||
it must not come back.
|
||||
- **D3D11 swap compositor** — superseded by the software compositor (TASK 3 item 16).
|
||||
- **Windows 8 / 32-bit support** — 64-bit Windows 10/11 only (creator ruling 2026-10-09).
|
||||
- **"There is no v1.x"** (2026-09-01): everything queues to v1 or to *out of product,
|
||||
permanently* — no feature parks "for a later version".
|
||||
|
||||
## Out of product — permanently
|
||||
|
||||
Do not build these; each was explicitly ruled out.
|
||||
|
||||
- D3D11 swap compositor (superseded by the software compositor).
|
||||
- OBS-style source expansion — game capture, browser-source zoo, media playlist, VLC, colour-key,
|
||||
MIDI. The minimal source set is deliberate; creators who need more have graduated to OBS.
|
||||
- Multi-layout / profiles / presets — that is what OBS is for.
|
||||
- Cross-process stream resume (impossible via the API; creator ruling 2026-09-01).
|
||||
- Stream Deck / Loupedeck integration — the device sends keystrokes, and global hotkeys already
|
||||
*are* the integration.
|
||||
- Per-source audio sync offset — global offset shipped (TASK 22); per-source is the OBS mixer
|
||||
rabbit hole and breaks the one-knob audio rule.
|
||||
- Chroma key / NVIDIA-only background removal (the bundled ONNX path is the one shipped).
|
||||
- Virtual camera output — a different product and a device-driver swamp; our output is one
|
||||
YouTube stream.
|
||||
- Replay buffer, stream clipping, multi-destination restreaming, an in-app bug reporter, and an
|
||||
advanced broadcast-form tab (latency/DVR/embed/projection/CC/region are fixed at sane defaults;
|
||||
every exposed field is a support ticket).
|
||||
@@ -138,3 +138,19 @@ save/load round trips are exact: an element removed in the UI (`RemoveElement`
|
||||
`scene.Elements.Remove` → `OnElementsChanged` → debounced `ScheduleSave`, plus `Shutdown` on
|
||||
close) does **not** come back after reload (`LayoutStorePersistenceTests` guards this).
|
||||
|
||||
|
||||
## Persistence & external accounts (lookup)
|
||||
|
||||
- Layout: `%APPDATA%\ytLlive\ytLlive.db` (SQLite, schema v8 — scenes, sources, assets by SHA-256
|
||||
content hash, socials, settings; **full rewrite per save**).
|
||||
- Auth session: `%APPDATA%\ytLlive\ytLlive.auth` (DPAPI, CurrentUser scope).
|
||||
- Startup log: `%APPDATA%\ytLlive\startup.log`, capped at 5 MB with one rolled copy
|
||||
(`Helpers/LogRotation.cs`, TASK 55 slice 1).
|
||||
- **Google Cloud project `198374416003`** — the OAuth ClientId prefix is the project number. It is
|
||||
owned by **`llamachile.shop@gmail.com`** ("gramps"), which is **NOT** the Google account a creator
|
||||
signs into YouTube with; console access and channel identity are separate logins. Direct link:
|
||||
`https://console.cloud.google.com/apis/credentials?project=198374416003` — if the picker shows no
|
||||
match it defaults to the **"Recent"** tab, so switch to **"All"**.
|
||||
- LAN credentials live in the gitignored `CREDENTIALS.md`. ⚠️ It says Home Assistant is
|
||||
`192.168.50.144`; the real HA is `192.168.50.145` (`.144` is `gordito`). Fix that file when next
|
||||
editing it.
|
||||
|
||||
@@ -16,6 +16,7 @@ Read the file whose row matches the task. There is no required reading order.
|
||||
| [`product.md`](product.md) | Brand/naming split, AI transparency, the GPL licensing prohibitions, the free-forever monetization posture, auth/account assumptions, and packaging/distribution | Branding, marketing, pricing, licensing, or packaging decisions |
|
||||
| [`youtube-api.md`](youtube-api.md) | Go-live ordering + the broadcast-binding fix, the private-only lock, health parsing, end-stream pre-flight, Test mode, and chat-insert shape | Touching go-live, broadcast lifecycle, health polling, streaming close-out, or the chat service |
|
||||
| [`overlays.md`](overlays.md) | The social bar, the native alert box (six animations, alert clip, ticker) and the chat box conventions — everything composited over the frame | Adding or changing anything drawn on top of the output |
|
||||
| [`decisions.md`](decisions.md) | Retired paths with cause of death: the paid tier and pricing, Polar, Store IAP, the Store route, the deleted updater, the rejected monetization ideas, and the permanent out-of-product list | You are about to re-open a decision that was already settled |
|
||||
| [`ui-surface.md`](ui-surface.md) | The control surface, scene strip, preview rail, hotkeys, broadcast drawer, toasts, transitions, brand logo, launch geometry, and the theme/properties-panel conventions | Changing anything the creator sees: layout, controls, theming, properties panel |
|
||||
|
||||
## Split-out yet to come
|
||||
|
||||
+32
-95
@@ -123,90 +123,38 @@ Apply this to every UI decision:
|
||||
stream, not both; one reusable stream; one webcam; two mixer inputs). If a feature only sings on
|
||||
a high-end chassis, it doesn't ship — the OBS escape hatch is open by design.
|
||||
|
||||
## Monetization (design decision — the branding flash is the sword)
|
||||
## Monetization — free forever, no watermark
|
||||
|
||||
> **⛔ Superseded 2026-10-08/09 — free forever, no branding on the stream.** The credit is now
|
||||
> **OFF by default**: `MainViewModel.ShowBrandFlash` (internal static, default `false`) is the
|
||||
> master switch, and `BrandFlashEnabled => ShowBrandFlash && !IsPremium` feeds the presenter at
|
||||
> construction and on every licence change. The code (`BrandFlashPresenter`, the FramePump
|
||||
> `brandFlash:` seam, the preview layer) is kept, not deleted — creator ruling: "don't delete it,
|
||||
> just make it conditionally displayed". `BrandFlashOutputTests` opt in by setting the switch
|
||||
> before constructing a `MainWindow`; `Credit_IsOffByDefault_EvenOnTheFreeTier` guards the default.
|
||||
> Everything below is the historical paid-tier design.
|
||||
**All features are free and ungated** (creator ruling 2026-10-08, TASK 54): no feature lock, no paid
|
||||
tier, no watermark. Money comes from gratuity and merch, not gating. **No licence chatter reaches a
|
||||
user** — no keys, no expiry, no licence UI. Support is email + GitHub issues; an in-app bug reporter
|
||||
is out of product. `IsPremium` gates nothing in v1.
|
||||
|
||||
Full pricing and the Year 1 revenue forecast in `MONETIZATION.md` (gitignored). ⚠️ **No Polar
|
||||
product, discount code, or license key exists any more** — nothing bills today; Polar is dormant.
|
||||
Brand palette and assets in `MARCOM.md` (gitignored). This section covers the in-app model.
|
||||
**The branding credit is OFF by default.** `MainViewModel.ShowBrandFlash` (internal static, default
|
||||
`false`) is the master switch; `BrandFlashEnabled => ShowBrandFlash && !IsPremium` feeds the
|
||||
presenter. The code is kept, not deleted — creator ruling: *"don't delete it, just make it
|
||||
conditionally displayed."* `BrandFlashOutputTests` opts in by setting the switch before constructing
|
||||
a `MainWindow`; `Credit_IsOffByDefault_EvenOnTheFreeTier` guards the default.
|
||||
|
||||
Free forever: **all features unlocked for everyone** — no feature lock, no paid tier, and since
|
||||
2026-10-08 **no watermark either** (the credit is OFF by default; see the banner above). No
|
||||
creator will tolerate a watermark, and as a good-guy developer we give everyone complete access
|
||||
so no one can call us crooked. Money comes from gratuity/merch, not gating (TASK 54).
|
||||
If it is ever enabled, the mechanics that matter:
|
||||
|
||||
- Full-frame neon "made with LlamaCasty!" at a **random position per presentation**, held **2s**
|
||||
(500ms in / 1000ms full / 500ms out), first ~5s after start then every `rand(30s)+30s`.
|
||||
- Published to the frame pump and the preview from the **same** `VideoFrame`, so preview and
|
||||
broadcast cannot drift. Deliberately **not** folded into `SceneGraph.GetBakedBase` — the bake
|
||||
cache compares element ids and would freeze the credit in place.
|
||||
- **App-lifetime cadence:** the presenter is `Start()`ed once in the `MainViewModel` ctor, not per
|
||||
go-live, because go-live and local recording share one `FramePump`. `IsPremium`'s setter therefore
|
||||
restarts the cadence timer when re-enabling (`IsCadenceTimerEnabled` is the test seam).
|
||||
- **It composites into the recording and the stream** (reversed 2026-09-26 — the free tier must be
|
||||
honest advertising). Test arithmetic trap: `Advance(5.1)` in one call blows through the 2s window
|
||||
(it both opens and ages the presentation) — step in 1/30s increments like `BrandFlashOutputTests`.
|
||||
- **A mid-credit downgrade** cuts the advertisement on the next tick: the presenter re-checks its own
|
||||
`Enabled` gate every frame rather than letting the credit finish.
|
||||
|
||||
Pricing, billing, the Store IAP plan and the rejected alternatives are history →
|
||||
[`decisions.md`](decisions.md).
|
||||
|
||||
- **Free:** a periodic branding credit — "made with LlamaCasty!" in **full-frame neon** (white core,
|
||||
feathered red/blue halo with the channels split in opposite directions), at a **random position
|
||||
inside the frame on every presentation** (single pre-measured line, so it never wraps; travel range
|
||||
is `0 … 1920−textWidth`), held **2s** (500ms fade in, 1000ms full, 500ms fade out), first credit
|
||||
~5s after go-live then **every `rand(30s)+30s`** (30–60s), on the live output **and** local
|
||||
recordings. 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 LlamaCasty to its own
|
||||
viewers; the free tier is distribution, not compromise.
|
||||
Implemented as `Services/Compositor/BrandFlashPresenter.cs` (raster + envelope + placement + licence
|
||||
gate), published to **both** the frame pump (`FramePump(brandFlash:)` → the compositor's per-frame
|
||||
`flashFrame` slot) and the preview (`BrandFlashElement`, bound to `BrandFlashImageSource`) from the
|
||||
**same** `VideoFrame`, so the preview cannot drift from the broadcast. Deliberately NOT the
|
||||
`flashFrame` parameter of `SceneGraph.GetBakedBase` — `SceneRegion.Matches` compares element ids
|
||||
only, so a credit folded into the cached bake would freeze there and never expire.
|
||||
**Pre-GA posture — REVERSED 2026-09-26 (TASK 36 shipped).** This used to render *in the preview
|
||||
only*, never on the broadcast, so test VODs stayed clean. The creator ruled that the free tier must
|
||||
be honest advertising: it now composites into the recording and the stream. Test VODs from an
|
||||
unlicensed instance carry the credit — use a license key or the paid build for clean captures.
|
||||
**Escalation model (2026-09-01, creator decision):** the cadence is *obnoxiously* self-promoting.
|
||||
License activation still flips exactly one bit: `IsPremium` → flash off. Nothing else changes
|
||||
between free and paid, ever.
|
||||
**Cadence is APP-LIFETIME, not go-live (creator ruling 2026-09-26):** the presenter is
|
||||
`Start()`ed **once in the `MainViewModel` ctor**, not from `UpdateLiveVisuals()`. It used to be
|
||||
gated on `IsLive`, which meant a recording made without ever going live carried no credit — the
|
||||
creator's ruling: *"the made with llamacasty flash should appear in all scenes, not just live"*
|
||||
and *"should also appear in recordings"*. One start now covers every scene, the preview, the
|
||||
stream and the recording, because go-live and local recording are the **same `FramePump`**
|
||||
(`Streaming.Operations.cs:68` and `:199` both call `StartAsync` with the same `brandFlash:`
|
||||
delegate) — verified, not assumed; there is no second encoder path needing a "redirect". The
|
||||
licence gate needs no live branch: `IsPremium`'s setter pushes `Enabled` from anywhere.
|
||||
**Consequence of app-lifetime — the premium edge restarts the timer.** `Enabled = false` stops
|
||||
the `DispatcherTimer` (cutting the advertisement mid-credit). Because `Start()` is no longer
|
||||
called per go-live, nothing would ever restart it, so a key entered mid-session would leave the
|
||||
credit dead until the process restarted. The setter therefore restarts it when re-enabling while
|
||||
`_running` (`IsCadenceTimerEnabled` is the test seam).
|
||||
**Test-arithmetic trap, hit again 2026-09-26:** `Advance(5.1)` in ONE call cannot observe a
|
||||
credit — `Advance` both *opens* the presentation and *ages* it by the same delta, so a single
|
||||
5.1s step blows clean through the 2s window. Always step in 1/30s increments like the real timer
|
||||
(the `Step` helpers in `BrandFlashOutputTests`).
|
||||
- **Paid:** branding flash removed. `BrandFlashEnabled` is now a **derived, non-assignable**
|
||||
`!IsPremium` and the presenter's own `Enabled` gate is re-checked on every frame, so the
|
||||
entitlement changing mid-credit cuts the advertisement on the next tick rather than letting it
|
||||
finish. That's it. No feature gating. Alerts, social bar slots, voice filters, TRAX, recording —
|
||||
everything is free. **How `IsPremium` is obtained is a billing concern and has changed twice;
|
||||
what it gates has not.**
|
||||
- **Pricing: ⛔ SUPERSEDED 2026-10-08 by the free-forever ruling (TASK 54).** Retained as
|
||||
reasoning only: `$10/mo` subscription or `$400` lifetime, no annual, ruled 2026-09-27 and
|
||||
overtaken ten days later. If a paid tier ever returns, these are the numbers to argue from.
|
||||
**How `IsPremium` would be obtained is a billing concern; what it gates is not.**
|
||||
- **Billing: ⏸ dormant — no paid tier exists** (free-forever ruling 2026-10-08, TASK 54). The
|
||||
Store IAP plan of 2026-09-27 was never implemented (`Windows.Services.Store` appears nowhere
|
||||
in the repo) and was withdrawn with the Store route on 2026-10-22. If a paid tier ever
|
||||
returns, the provider is **Polar** — merchant of record for VAT/sales tax, 5% + $0.50 Starter,
|
||||
**$50 already paid**; `Distribution.md` §4.2/§4.5 holds the design. ⚠️ No Polar product, org
|
||||
ID, key prefix, or discount code exists (`d105dfa1-…`, `c05fb364-…`, `LCYT-`, `LLAMAFOUNDER`/
|
||||
`LLAMA50`, the customer-portal validate endpoint, and the 14-day offline-grace machine are all
|
||||
gone).
|
||||
- **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. There is **no licence chatter at all** — the app
|
||||
is free, there are no keys, and no user sees licence UI. TASK 36 item 5 (expiry reminder) is
|
||||
struck.
|
||||
|
||||
**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
|
||||
@@ -252,12 +200,6 @@ removal above). It has three layers, all free — this is the product's differen
|
||||
via the partner-audit scope) deep-links to the Earn page. Slice 2's once-daily refresh gate +
|
||||
snapshot history were baked into slice 1.
|
||||
|
||||
**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 feature tiers, and donation-only (relies on
|
||||
the kindness of strangers). Resolution/quality ceilings stay rejected (fixed 2026-09-01, no longer
|
||||
"deferred"): the free tier never loses quality — auto step-down (**TASK 33**) is a *protect the
|
||||
stream* feature, never a monetization penalty.
|
||||
|
||||
## Auth gates Go Live, but not exploration
|
||||
|
||||
@@ -277,15 +219,10 @@ YouTube channel, they go to YouTube first. This assumption is explicit and must
|
||||
replaced by an in-app account-creation step. Zero state = the Start Stream dialog's "Sign in to
|
||||
YouTube" button; going live is unreachable until an account is connected.
|
||||
|
||||
## Windows packaging & distribution (⛔ REVERSED 2026-10-22 — direct + Steam; Store dropped)
|
||||
## Windows packaging & distribution (direct + Steam; Store dropped 2026-10-22)
|
||||
|
||||
**The Microsoft Store route is dead.** A brand name on the Store's *publisher* line requires a
|
||||
**Company** account, which requires a registered legal entity (an LLC — ~$85 New Mexico with a
|
||||
~2-week SOS backlog, ~$143 Wyoming). Partner Center does **not** convert an Individual account to
|
||||
a Company account later, and an Individual account publishes under the verified legal name. The
|
||||
creator will not fund an entity for a nameplate, so the 2026-09-27 Store ruling was withdrawn on
|
||||
2026-10-22. Store IAP was never implemented either — `Windows.Services.Store` appears nowhere in
|
||||
the repo.
|
||||
**The Microsoft Store route is dead** — a brand on the publisher line needs a Company account and
|
||||
a legal entity. Cause of death and the abandoned routes: [`decisions.md`](decisions.md).
|
||||
|
||||
**The route now:**
|
||||
|
||||
@@ -317,7 +254,7 @@ the repo.
|
||||
`runFullTrust` + webcam + microphone. `Square310x310Logo`/`Wide150x150Logo` traps are in
|
||||
`MyMistakes.md`. Logos come from the app's own artwork via `scripts/make-store-logos.py`.
|
||||
- **⛔ Bundle ffmpeg — no runtime downloads** (was Store policy 10.2.2; still right for a direct
|
||||
download, and it fixes the 2026-09-01 404). See [`docs/ai/output.md`](docs/ai/output.md) → "FFmpeg locator".
|
||||
download, and it fixes the 2026-09-01 404). See [`output.md`](output.md) → "FFmpeg locator".
|
||||
- **⛔ 64-bit Windows only (creator ruling 2026-10-09).** Both csproj files set
|
||||
`<PlatformTarget>x64</PlatformTarget>` (the test project too — an AnyCPU test project
|
||||
referencing an x64 app raises MSB3270 and breaks the 0-warning gate). Why: the bundled ffmpeg
|
||||
|
||||
@@ -253,3 +253,25 @@ These are the hard facts behind every decision. Full list in `TASKS.md`.
|
||||
already cover insert. **Real stinger/TTS alert widgets are DONE (TASK 43, shipped
|
||||
2026-09-24) as the native alert box** — see the next section; Text source (TASK 3 item 16)
|
||||
and the RewardEvent persistence (item 20) remain open.
|
||||
|
||||
## Auth & session (YouTube OAuth)
|
||||
|
||||
- `Helpers/OAuthCredentials.cs` holds 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** — a two-state flow, no separate
|
||||
Connect button. Sign-out is **explicit only**: `StopStream()` does NOT clear the session
|
||||
("stopping a recording leaves the creator signed in", 2026-08-29).
|
||||
- `YouTubeAuthService` takes an optional `HttpClient` + `sessionChanged` + `openBrowser` callback
|
||||
(test seams + save hook; services are constructed in `MainViewModel`).
|
||||
- **Change Account must never sign out first** (fixed 2026-09-27): `SignOutYouTubeAsync` →
|
||||
`TokenStore.Clear()` **deletes** `ytLlive.auth`, so cancelling in the Google chooser left the
|
||||
creator logged out of a working account. It is now just `await SignInAsync()` — success overwrites
|
||||
the token through the `sessionChanged` hook; cancel/failure leaves the session untouched.
|
||||
- **The OAuth flow releases its loopback port on retry:** an abandoned browser attempt holds
|
||||
`localhost:8765` for its 5-minute timeout, so `AuthenticateAsync` cancels and awaits the abandoned
|
||||
attempt before binding, serialised by a `SemaphoreSlim` (`_authGate`) — without the gate, two
|
||||
overlapping calls both see a free port and race to bind it.
|
||||
- Test seam: `YouTubeAuthService.SetSession()` is public — inject a channel with a future
|
||||
`TokenExpiry` so `EnsureToken()` passes offline, paired with `HttpClient(new StubHandler())`.
|
||||
|
||||
@@ -35,6 +35,8 @@ check_budget() {
|
||||
}
|
||||
check_budget "AGENTS.md" 210
|
||||
check_budget "ai.md" 100
|
||||
check_budget "HANDOFF.md" 40
|
||||
check_budget "TASKS.md" 120
|
||||
|
||||
echo "══ 2/4 CLEAN BUILD (expect 0 warnings) ══"
|
||||
"$DOTNET" clean "$ROOTWIN\\ytLive.csproj" > /dev/null
|
||||
|
||||
Reference in New Issue
Block a user