docs: bank the Windows Store + signing research, and fix the dead EV-certificate line
The distribution answer existed only in conversation, so every session re-derived it. It is now in the map, and the route decision is explicitly parked as the creator's. new TASKS/research-store-certification.md — Store Policies 7.20 + MSIX packaging: which policies bind, which don't (and why), cert economics, camera/mic gating layers, YouTube age + COPPA, the 11.12 UGC judgment call. Two real defects surfaced, neither fixed (docs-only unit): - FfmpegLocator downloads an unsigned exe from GitHub and runs it. That is policy 10.2.2 (dynamic code inclusion) verbatim, and it is the root cause of the 2026-09-01 404 — the pin aged out of BtbN's 14-day retention on the creator's first real recording attempt. -> TASK 48 item 1, not Store-conditional. - Distribution.md:318 recommended a $400+/yr EV cert for a SmartScreen bypass Microsoft removed in March 2024. Fixed; had it shipped it would have cost $400+/yr to buy what $150 buys. Also new: TASKS/task-48 (checklist, carved out of TASK 36 item 6) and TASKS/task-49 (chat profanity filter, not blocked). ai.md gains the durable invariants — full trust or recording breaks silently, chat is rendered never stored — plus a correction to the FFmpeg locator section. MyMistakes.md records the lesson: a policy citation is a claim about scope, not just text. MARCOM.md got the privacy-copy guard but is gitignored by design, so that edit stays local and did not travel here.
This commit is contained in:
+107
-126
@@ -1,146 +1,127 @@
|
||||
# HANDOFF — current state
|
||||
|
||||
**Branch:** `main` (pre-1.0, no feature branches). **Last pushed:** `7f14ffb`.
|
||||
**This session's work is COMMITTED LOCALLY, NOT PUSHED** — **four** commits ahead of `origin/main`
|
||||
(`fae8ec5`, `5aedca7`, `f5a9d46`, `de81fa3`) plus this unit. Push only when the creator says so.
|
||||
**Branch:** `main` (pre-1.0, no feature branches — creator ruling 2026-08-24).
|
||||
**Last pushed:** `938c5b3` (alert ticker). This unit (distribution/certification research) is
|
||||
**docs-only** — no code touched, no build, no tests run.
|
||||
|
||||
## The 2026-09-26 proof-of-concept round (creator reporting)
|
||||
## This unit: distribution & Store certification research is WRITTEN DOWN
|
||||
|
||||
Context: **no YouTube private test recordings are being saved**, so the creator is judging
|
||||
compositing from **local recordings**. They assumed the live path needed a separate "redirect" —
|
||||
**it does not, and that is verified, not assumed:** one `_framePump` is constructed in the
|
||||
`MainViewModel` ctor with a single `brandFlash:` callback, and both `Streaming.Operations.cs:68`
|
||||
(go-live) and `:199` (record) call that same `StartAsync`. Record+simulcast is one ffmpeg with two
|
||||
outputs. Five items, tracked in `TASKS.md` → "Creator-reported batch".
|
||||
The session's open question was "how do we get this in front of users without a SmartScreen
|
||||
scarewall" and it had been answered **in conversation only** — which meant the next session
|
||||
would re-derive it. It is now in the map, and the conversation can be dropped.
|
||||
|
||||
### Done: the alert ticker draws inside the Stream Alerts box
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `TASKS/research-store-certification.md` | **new** — the authoritative research. Store Policies **7.20** (effective 2026-10-22, re-check the version), MSIX full-trust vs AppContainer, cert economics, a ⛔/✅ table of which policies bind and which don't, the camera/mic gating layers, the YouTube age + COPPA analysis, the 11.12 UGC judgment call, and the filter design constraints |
|
||||
| `TASKS/task-48-distribution-msix.md` | **new** — the executable checklist. Carved out of TASK 36 item 6 (code signing / installer / Velopack URL) so distribution has one owner |
|
||||
| `TASKS/task-49-chat-profanity-filter.md` | **new** — the chat filter checklist. Not blocked |
|
||||
| `Distribution.md` §4.3 | **corrected** — the EV recommendation was dead (§ below) |
|
||||
| `ai.md` | new "Windows packaging & distribution" section (durable invariants) + a correction on the FFmpeg locator |
|
||||
| `MyMistakes.md` | the policy-applicability lesson |
|
||||
| `TASKS.md` | rows 48/49, a research index, and the TASK 36 item 6 split |
|
||||
| `MARCOM.md` | ⛔ **privacy-copy guard — gitignored, so this edit is local-only and did not travel with the push** |
|
||||
|
||||
Was a **global 1920px bar pinned to the top edge**; the creator wants it "over the stream alerts
|
||||
video, not over the entire preview window". The strip is now rendered at the alert box's own size
|
||||
and the box's origin travels on the frame in a new `VideoFrame.Placement` (`(int X, int Y)?`, with
|
||||
`OriginX`/`OriginY` defaulting to 0 so full-canvas overlays are unaffected). Every blit site now
|
||||
reads `tickerFrame.OriginX/OriginY` instead of a literal `0, 0` — `SceneCompositor` ×2 plus
|
||||
`FramePump`'s static-bake `Overlay` path — and `PreviewPane.xaml`'s `AlertTickerElement` binds the
|
||||
same rect (`AlertTickerLeft/Top/Width/Height`), so preview and output can't drift.
|
||||
### Two real defects the research surfaced (both now recorded, neither fixed — no code this unit)
|
||||
|
||||
Design notes: the position rides on the frame rather than in a full-canvas frame because a
|
||||
1920×1080 overlay is 8.3MB of LOH garbage per tick (~2.5GB over one 10s alert); the strip is
|
||||
~370KB. `CopyStrip` now clips **rows** to the target height — the pill rasterises at 48px, so a
|
||||
shorter box would have written past the buffer. **No alert box in the scene ⇒ no ticker.**
|
||||
1. **⛔ `FfmpegLocator` downloads an unsigned exe from GitHub and runs it.** That is Store
|
||||
policy **10.2.2** (dynamic code inclusion) verbatim, *and* it is the root cause of the
|
||||
2026-09-01 failure: the previous daily pin aged out of BtbN's 14-day retention and the
|
||||
download **404'd on the creator's first real recording attempt**. `FfmpegLocator.cs:30-35`
|
||||
records the incident but still reads like a design choice. → **TASK 48 item 1.** Not
|
||||
Store-conditional: bundling kills this whole class of cold-start failure and deletes the
|
||||
startup network dependency. **This is the highest-value unblocked item in the repo.**
|
||||
2. **⛔ `Distribution.md:318` recommended a $400+/yr EV certificate for a benefit Microsoft
|
||||
deleted in March 2024.** Fixed. Had it shipped, it would have cost $400+/yr to buy exactly
|
||||
what $150 buys. Lesson in `MyMistakes.md` — the recurring shape is *citing a policy or a
|
||||
platform fact from memory instead of re-verifying it in the document itself.*
|
||||
|
||||
### Done: the branding flash is no longer a go-live-only behaviour
|
||||
### ⛔ The decision that is NOT made, and belongs to the creator
|
||||
|
||||
The presenter was `Start()`/`Stop()`-ed from `UpdateLiveVisuals()`'s `IsLive` branch, so a
|
||||
recording made **without ever going live** carried no credit — exactly the creator's complaint.
|
||||
It is now started **once in the `MainViewModel` ctor** and never stopped on live-state churn, so it
|
||||
runs in every scene, reaches the preview, and rides the same `FramePump` into the stream and the
|
||||
recording. The licence gate needs no live branch: `IsPremium`'s setter already pushes
|
||||
`BrandFlashPresenter.Enabled`.
|
||||
**The distribution route.** Research is done and MSIX is the front-runner (full trust is viable;
|
||||
we trip **none** of the four MSIX disqualifiers — no driver installed, no per-user service, no
|
||||
elevation, no shell extension). Four real combinations, costed in
|
||||
`TASKS/research-store-certification.md` §3:
|
||||
|
||||
**Trap that came with it (fixed):** `Enabled = false` stops the presenter's `DispatcherTimer`.
|
||||
When `Start()` was per-go-live, the next go-live restarted it; with one app-lifetime `Start()`
|
||||
nothing would, so a key entered mid-session would leave the credit dead until the process
|
||||
restarted. The setter now restarts the timer when re-enabling while `_running`.
|
||||
| # | Route | Cert/yr | SmartScreen | Notes |
|
||||
|---|---|---|---|---|
|
||||
| A | Store MSIX + Store IAP | **$0** | none | Polar gets deleted; revenue cut; public listing |
|
||||
| B | Store MSIX + **Polar** | **$0** | none | free signing **and** keep 100% — two systems to maintain |
|
||||
| C | **Polar file hosting** + own cert | $120–300 | warning ramp | **the status quo already sketched in `Distribution.md:14`/`:296`** |
|
||||
| D | Store EXE (policy 10.2.9) | $120–300 | warning ramp | **dominated** — cert anyway, silent install, own URL |
|
||||
|
||||
Test-arithmetic trap, hit twice now: `Advance(5.1)` in one call can never observe a credit —
|
||||
`Advance` opens *and* ages the presentation by the same delta, so it jumps the 2s window. Step at
|
||||
1/30s like the real timer (the `Step` helpers).
|
||||
**Nothing else was allowed to block on this** — the user is right that most of the research is
|
||||
route-independent and worth banking regardless. So: research banked, dead line fixed, and only
|
||||
the packaging work is parked.
|
||||
|
||||
### Done this unit: recording save dialog — Cancel now discards
|
||||
**Also still open, deliberately:** pricing (one-time vs one-time+monthly), the license provider,
|
||||
and therefore all licensing copy. `OverlayHost.xaml` still claims Polar unlocks alerts — wrong,
|
||||
alerts are not gated. Unresolved, queued, **not** to be papered over.
|
||||
|
||||
`RenameRecordingDialog` was always correct (Enter → `DialogResult=true`, Cancel/X/Escape → `false`).
|
||||
**The caller was not**: `FinalizeRecordingAsync` only overwrote `stem` when the dialog returned true
|
||||
and then ran `File.Move` **unconditionally**, so a cancelled dialog silently saved the recording
|
||||
under the default name. Extracted the decision into internal
|
||||
`MainViewModel.CompleteRecordingSave(startPath, dir, autoStem, chosenStem, creatorSaved)`:
|
||||
### The two findings most likely to be forgotten
|
||||
|
||||
- `creatorSaved == false` (Cancel/Escape/X) → **delete the temp file**; the videos folder is left
|
||||
empty. A failed delete returns `DiscardFailed` and the toast names the file + folder.
|
||||
- `creatorSaved == true` (Save, or Enter on the pre-filled default) → `File.Move` to the final name.
|
||||
Blank box still means "keep the auto name" — but only on an explicit Save.
|
||||
|
||||
Test: `ytLive.Tests/RecordingSaveDialogTests.cs` (5 facts, real temp files, no WPF needed) — the
|
||||
headline asserts Cancel leaves the directory **empty**, not merely "the stem is unchanged".
|
||||
Lesson recorded in `MyMistakes.md` (falsy `ShowDialog()` falling through into a side effect).
|
||||
|
||||
## Two earlier work units (committed)
|
||||
|
||||
### 1. Branding flash is now composited into the OUTPUT (TASK 36 shipped)
|
||||
|
||||
It used to be a WPF `BrandFlashLayer` TextBlock in the preview at 25% opacity on a 300s timer — a
|
||||
credit the creator could see and **no viewer ever could**. Now one rendered `VideoFrame` goes to
|
||||
**both** the frame pump and the preview, so they cannot drift:
|
||||
|
||||
- `Services/Compositor/BrandFlashPresenter.cs` (new) — neon raster (white core + feathered red/blue
|
||||
halo, channels split opposite), 2s envelope, random on-canvas placement per presentation,
|
||||
licence gate re-checked on every read.
|
||||
- `ViewModels/MainViewModel.BrandFlash.cs` (new) — `BrandFlashFrame()` for the pump +
|
||||
`PublishBrandFlashPreview()` for the pane.
|
||||
- `FramePump` gained `brandFlash:` → the existing per-frame `flashFrame` slot, and mixes it into the
|
||||
C4 cache signature. `SceneCompositor.Overlay` is now `internal` for the shortcut path.
|
||||
- `PreviewPane.xaml`: `BrandFlashLayer` → `BrandFlashElement` (bound to the published frame).
|
||||
- `BrandFlashEnabled` is derived `!IsPremium` and no longer assignable.
|
||||
|
||||
Spec met: 500ms in / 1000ms hold / 500ms out, first credit ~5s after go-live then every
|
||||
`rand(30s)+30s`, single unwrapped line, random spot every time, fully on canvas.
|
||||
|
||||
### 2. Dev-only: two instances side by side
|
||||
|
||||
`Helpers/InstanceProfile.cs` (new). Set `YTLIVE_INSTANCE=<id>` and that process gets a private
|
||||
`%APPDATA%\ytLlive\instances/<id>/` root for the **layout DB, auth file, startup.log and the
|
||||
WebView2 user data folder**. Chromium locks that folder exclusively — without this the second
|
||||
instance does not start at all. Recording folder and the ffmpeg `tools` cache stay shared on
|
||||
purpose; global hotkeys stay un-namespaced.
|
||||
|
||||
**Usage:** `$env:YTLIVE_INSTANCE=2; dotnet run`
|
||||
|
||||
The entire implementation is inside `#if DEBUG`. Release compiles to `DataRoot => DefaultRoot` +
|
||||
`WebViewDataFolder => null`; the call sites are unconditional so Release cannot drift.
|
||||
|
||||
## Bugs found and fixed along the way
|
||||
|
||||
- **Pre-existing, not mine:** `FramePump`'s **fully-static shortcut** stretched the cached bake and
|
||||
returned it, silently discarding **every per-frame overlay** — the social bar and the alert ticker
|
||||
were already lost there, not just the flash. Now composites overlays onto a copy before stretching.
|
||||
- **Cadence bug caught by my own test:** the interval was an absolute deadline, not a period, so
|
||||
gaps collapsed (6.7s / 3.5s / 2.8s instead of 30–60s). Fixed.
|
||||
- `BrandFlashPresenter` recycles one 8MB master buffer and **must** stamp `VideoFrame.Epoch` per
|
||||
read, or the paste cache freezes the credit. It does.
|
||||
|
||||
## Test state — VERIFIED
|
||||
|
||||
- **357 total, 356 pass.** 18 new facts (10 brand flash, 8 instance isolation).
|
||||
- The one failure is `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`,
|
||||
and it is **flaky by environment, not a regression** — 2 fail / 1 pass in isolation. Its own doc
|
||||
comment: "Requires an interactive desktop session: if the window is covered or the session is
|
||||
locked, the pointer no-ops and the drag never lands." The symptom matches exactly (order
|
||||
unchanged). **Run the suite with ytLive CLOSED** (see `MyMistakes.md` 2026-08 era note).
|
||||
- `RealAppHost.RunAsync` was added for this: a frame-pump test MUST `await` inside it. A blocking
|
||||
wait in `RealAppHost.Run` occupies the one shared STA thread and hangs the whole suite with no
|
||||
output. **Always background a `vstest` run and poll the log** — foreground piped `vstest` returns
|
||||
nothing in this shell even on success.
|
||||
- **Distribution and licensing are independent.** Policy 10.8.1 lets a **Store-distributed**
|
||||
app verify licenses with **Polar**. So "should we use the Store?" does not imply "drop Polar?"
|
||||
Only route A removes Polar, and that is a licensing decision riding on a distribution one.
|
||||
- **⛴ Two invariants that will break silently if touched:**
|
||||
- **Full trust, or recording breaks.** `DefaultRecordFolder()` writes to Downloads/MyVideos;
|
||||
that passes through unvirtualized **only** at `mediumIL`. Flip the manifest to
|
||||
`appContainer` and output vanishes with no error. A comment is owed there **when the
|
||||
manifest lands** (TASK 48 item 6) — not before.
|
||||
- **Chat is rendered, never stored.** That non-persistence half is the load-bearing part of
|
||||
the 11.12 certification answer. No chat history, moderation log, analytics, or crash payload
|
||||
may be added without re-arguing 11.12 first.
|
||||
|
||||
## Landmines
|
||||
|
||||
- A stale `ytLive.exe` (PID 2544) locked `bin/.../ytLive.exe` and broke `dotnet run` with MSB3027.
|
||||
Killed. **If the build fails to copy `ytLive.exe`, check for a running instance first.**
|
||||
- `GlobalHotkeys`: two instances registering the SAME global hotkey — Windows refuses the second.
|
||||
Left alone deliberately.
|
||||
- `OverlayHost.xaml` says Polar unlocks alerts too; alerts are not gated. Unresolved, queued.
|
||||
- **Stale fact inside `TASKS.md` (left alone, flagged here per the Scope Lock).** The "1.0 gates"
|
||||
section asserts "**TASK 36 is now shipped**", but the catalog row 36 reads **☐ Queued** and
|
||||
`task-36-gold-pass.md` still shows items 1–6 unchecked. The line most likely means *item 2*
|
||||
(branding flash goes live) shipped at `9761b1d`. **Needs a one-line fix, not a code change** —
|
||||
flagging rather than fixing because it is not this unit's job.
|
||||
- **A stale `ytLive.exe` (PID 2544) locked `bin/.../ytLive.exe`** and broke `dotnet run` with
|
||||
MSB3027. Killed. **If the build fails to copy `ytLive.exe`, check for a running instance first.**
|
||||
- **`LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder` is flaky by
|
||||
environment** (needs an interactive desktop session; 2 fail / 1 pass in isolation). **Run the
|
||||
suite with ytLive CLOSED.** Not a regression.
|
||||
- **`RealAppHost.RunAsync` exists for a reason** — a frame-pump test MUST `await` inside it. A
|
||||
blocking wait occupies the one shared STA thread and **hangs the whole suite with no output.**
|
||||
Always background a `vstest` run and poll the log; a foreground piped `vstest` returns nothing
|
||||
in this shell even on success.
|
||||
- **`GlobalHotkeys`: two instances registering the SAME global hotkey** — Windows refuses the
|
||||
second. Left alone deliberately.
|
||||
- **MSIX writes under a real package identity are unverified.** The docs say full trust passes
|
||||
user-profile writes through, but confirm on a real packaged build before trusting it with
|
||||
recordings (TASK 48 item 6).
|
||||
|
||||
## Test state
|
||||
|
||||
**367/367 as of `938c5b3`.** Not re-run for this unit — docs only, no code touched.
|
||||
|
||||
## Uncommitted / untracked
|
||||
|
||||
- `MARCOM.md` — the privacy-copy guard was added but the file is **gitignored by design**
|
||||
(confidential business file, `.gitignore:11`). It stays local, as intended. Same for
|
||||
`MONETIZATION.md` and `CREDENTIALS.md` — **never commit those.**
|
||||
|
||||
## Next
|
||||
|
||||
1. **Multi-instance** (#4) — route `FfmpegLocator._toolsDir` through `InstanceProfile`, and confirm
|
||||
the launch method: `dotnet run` while the first app holds `bin/…/ytLive.exe` fails with MSB3027
|
||||
*before any instance starts* (a build-output lock, not a log conflict). `$env:YTLIVE_INSTANCE=2`
|
||||
+ `dotnet run --no-build` in a second shell is the known-good path.
|
||||
2. **Post-session efficacy report** (#5) — roll up `CurrentHealth` (dropped frames, duration, health
|
||||
message) when a stream or recording ends.
|
||||
3. **The creator's own eyes on the output** — the ticker-placement and brand-flash changes are proven
|
||||
by unit tests and the compositor, but the final proof is a local recording, since no YouTube
|
||||
private test recordings are being saved.
|
||||
4. Push the commits when the creator asks.
|
||||
5. Test-console chat UX (queued): don't clear the chat window, 20px right padding on the pull-out
|
||||
input, Enter inserts a newline.
|
||||
6. Decide the alert-gating copy question above.
|
||||
7. Ship-checklist items live in `TASKS.md` → "1.0 gates".
|
||||
1. **Bundle ffmpeg (TASK 48 item 1)** — unblocked, recommended on every route, and it fixes a
|
||||
real user-facing failure. *This is the next code unit.*
|
||||
2. **The route decision** (creator) — A/B/C above, once the ffmpeg fix is out of the way.
|
||||
3. **Vertical recording verification** — the compositor tier is proven by
|
||||
`Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop`; the *record* path has never
|
||||
been run. `FramePump.cs:645` flags off-size tiers as a known follow-up.
|
||||
4. **Multi-instance** — route `FfmpegLocator._toolsDir` through `InstanceProfile` (same shared
|
||||
extraction race as #1, and it becomes moot if ffmpeg is bundled). Confirm the launch method:
|
||||
`$env:YTLIVE_INSTANCE=2` + `dotnet run --no-build` in a second shell is the known-good path.
|
||||
5. **Post-session efficacy report** — roll up `CurrentHealth` (dropped frames, duration, health
|
||||
message) when a stream or recording ends. `SessionTeardownTests` is the natural home.
|
||||
6. **`scripts/publish.sh` + Debug-only `InternalsVisibleTo` + the `LlamaCasty.exe` rename** —
|
||||
still queued from 2026-09-26; do **NOT** add `-p:PublishTrimmed=true` (WPF fails at runtime,
|
||||
not build time). See `TASKS.md` → "Shipping / release build".
|
||||
7. **The alert-gating copy** (`OverlayHost.xaml`) — fix the copy or gate the alerts; do not leave
|
||||
it lying. Blocked behind the pricing ruling.
|
||||
8. **The camera-privacy measurement** — does the Win11 desktop-app camera toggle actually gate a
|
||||
DShow webcam and a capture card? Gates all privacy-forward copy (`MARCOM.md` guard).
|
||||
9. **TASK 36:212 stale "shipped" line** — one-line fix, needs a hand.
|
||||
10. Ship-checklist items live in `TASKS.md` → "1.0 gates".
|
||||
|
||||
Reference in New Issue
Block a user