The commercial plan was spread over five documents written in five different weeks, and they contradicted each other: product.md sold the Store, decisions.md called the Store dead then re-opened, Distribution.md (736 lines) still described Polar as a download host, MARCOM.md quoted "no subscription" next to a paid tier, and README.md promised "free forever". An outsider could not tell what ships. docs/ai/commercial.md is now the only place money is decided: free = whole app at a 921,600 px / 30 fps output profile covering the recording as well as the stream; Pro = $10/mo or $400 lifetime through Polar, key bound to the buyer's YouTube channel ID via a claim page, 2 activations, 7–14 day lease with offline grace, fail closed, never downgrade a running stream; one portable zip + winget primary, Steam and the Store parked; Azure Trusted Signing (~$9.99/mo), no EV; Founding 200 promo; ~$135/yr costs; anti-piracy posture and what is permanently rejected. Everything else now points at it instead of restating it: - product.md — money sections replaced with pointers; packaging rewritten to winget-first, Store parked; signing updated to Trusted Signing. - decisions.md — graveyard with cause of death for free-forever, the Store ordering, the updater, and the 2026-10-30 reversal of the quality-ceiling rejection. - Distribution.md — rewritten to release mechanics only (artifact, GitHub Releases, winget manifest, signing, checklist, takedown): 736 → 120 lines. - TASKS.md — TASK 54 rewritten, TASK 48's route settled, TASK 10 superseded, five new rows (entitlement seam, output-profile gate, licence UI, system messages, packaging). - README.md — "free forever" replaced with the real split. - capture.md — obs-backgroundremoval is GPL-3.0 and so licence-INCOMPATIBLE since the proprietary EULA; the old "compatible" line was falsified by that change. - TASKS/task-10, task-48, task-51, task-54 — superseded-banner at the top of each, so none of them reads as current policy. - HANDOFF.md — rewritten: plan settled, next code unit is the entitlement seam. MARCOM.md and MONETIZATION.md (gitignored) were corrected in place: policy pointer at the top of each, the flash removed from every live claim, the Polar fee break-even corrected to ~$900/mo, and the projections flagged as written against a dead model.
310 lines
19 KiB
Markdown
310 lines
19 KiB
Markdown
# TASK 48 — Distribution & packaging: route decision, MSIX, signing
|
|
|
|
> ⛔ **THE ROUTE IN THIS FILE IS SUPERSEDED — 2026-10-31.** Distribution is **direct download from
|
|
> `llamacasty.com` (GitHub Releases) + winget primary**; **Steam and the Microsoft Store are parked**,
|
|
> third and fourth. Store IAP is not the billing path — **Polar** is, and the paid delta is output
|
|
> quality. Live policy: [`docs/ai/commercial.md`](../docs/ai/commercial.md); mechanics:
|
|
> [`Distribution.md`](../Distribution.md); cause of death: [`docs/ai/decisions.md`](../docs/ai/decisions.md).
|
|
> The MSIX build steps below are still accurate and remain the parked Store channel's artifact.
|
|
|
|
> Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here.
|
|
> **Research: [`TASKS/research-store-certification.md`](research-store-certification.md)** — the "why".
|
|
> Carved out of TASK 36 item 6 (release engineering) on 2026-09-27 so distribution has one
|
|
> owner instead of three scattered mentions.
|
|
|
|
**Status:** ⏸ **DEFERRED 2026-10-26 (creator ruling) — revisit near v1, not now.** This file is the
|
|
record of what was decided and built, not an active queue: the remaining programming work is far
|
|
from release and a complete UX redesign comes first. See `TASKS.md` → Open items → the ⏸ TASK 48
|
|
bullet for exactly what is frozen (items 6-8, signing, updater, and the route contradiction against
|
|
`ai.md`). Do not start or re-litigate any of it before that revisit.
|
|
|
|
**Historical status:** 🔶 In progress — **✅ ROUTE DECIDED 2026-09-27: Microsoft Store MSIX + Store IAP.**
|
|
Polar is deleted; every licensing subsystem goes with it.
|
|
|
|
**Goal:** get LlamaCasty in front of a user without a SmartScreen scarewall, ideally without
|
|
an annual certificate bill, and without breaking recording, capture, or audio.
|
|
|
|
---
|
|
|
|
## 0. ✅ THE DECISION — Microsoft Store, MSIX, Store IAP (creator ruling 2026-09-27)
|
|
|
|
**Creator's stated criteria, verbatim: "zero headaches, minimal maintenance (for me) while
|
|
still providing accountability and a reasonably easy upgrade flow."**
|
|
|
|
**Ruling: distribute as an MSIX package through the Microsoft Store, and take payment through
|
|
Store IAP.** All four criteria are satisfied by the same mechanism:
|
|
|
|
| Criterion | How MSIX + Store IAP satisfies it |
|
|
|---|---|
|
|
| Zero headaches | **$0/yr** — Microsoft holds the signing certificate, the reputation, and the installer. No cert, no HSM, no annual renewal, no SmartScreen ramp |
|
|
| Minimal maintenance | Microsoft handles **updates**, **payments**, **entitlements**, **refunds**, and **customer support**. Nothing to run |
|
|
| Accountability | Microsoft reviews every submission — that *is* the accountability layer, and it is a real one |
|
|
| Easy upgrade flow | Store auto-update. **Velopack, the update URL, and the DO droplet all go** |
|
|
|
|
### The consequence that makes this cheap: the entire licensing subsystem is deleted
|
|
|
|
Choosing Store IAP over Store+Polar is what makes "minimal maintenance" real. Keeping Polar
|
|
would have left the creator maintaining a licensing backend, a customer portal, activation
|
|
limits, and an offline-grace machine — the exact burden the ruling was made to avoid.
|
|
|
|
**Dead code / deleted concepts (TASK 10 is now a teardown, not a build):**
|
|
- `Services/PolarLicenseService.cs` — HTTP validation, the swallowed network failures, the
|
|
ignored `expires_at`
|
|
- `Helpers/PolarLicense.cs` — the record type
|
|
- `ViewModels/MainViewModel.License.cs` — `PremiumUrl`, the customer portal, the
|
|
`OfflineGracePeriod = 14 days` subscription-era artifact, renewal/lapse copy
|
|
- `Controls/OverlayHost.xaml` — the incorrect "Polar unlocks alerts" copy resolves itself
|
|
- `ytLive.csproj:92` Velopack `PackageReference` + `App.xaml.cs:3,38-46` — see item 3
|
|
- The `MONETIZATION.md` provider sections (gitignored — local file)
|
|
|
|
**What replaces it:** `IsPremium` is derived from the **Store entitlement** instead of a
|
|
remote HTTP call. One bit, locally cached, refreshed by the OS. The offline-grace machine
|
|
disappears because the OS supplies license state — and with it the whole class of
|
|
"network flaky → app thinks I'm expired" bugs.
|
|
|
|
⚠️ **The watermarks posture is unchanged:** free gets everything; the branding flash stays the
|
|
ONLY paid delta (TASK 36 item 2). Store IAP changes how the bit is *obtained*, never what it
|
|
*gates*.
|
|
|
|
### Still to verify (does not block packaging work)
|
|
|
|
- ☑ **Current Store revenue-share terms — VERIFIED 15%, subscriptions included.** Read from the
|
|
ADA v8.10 PDF (downloaded and text-extracted), §6(b): 15% for Apps and their In-App Products
|
|
*not listed in* 6(b)(iii); 12% is Games-only; 30% is Xbox console / Xbox non-subscription IAP /
|
|
Windows 8. LlamaCasty is a Windows PC App, so **6(b)(i) 15% governs** ⇒ `$8.50/mo` net,
|
|
`$340` lifetime net. The agreement's own changelog (v8.0, Oct 26 2017) states it plainly:
|
|
*"implement the 85/15 revenue share for non-Game subscriptions."* Smaller-indie better rates
|
|
were **not** found in the standard terms. Microsoft also requires that any IAP products and
|
|
their pricing be declared in Partner Center.
|
|
- ☑ **Store IAP tier shape — ✅ DECIDED 2026-09-27: `$10/mo` subscription + `$400` lifetime,
|
|
no annual.** Declare **two** products in Partner Center: one auto-renewing monthly
|
|
subscription, one non-expiring lifetime IAP. ⛔ **No `.99` prices** (settled). ⛔ **No annual
|
|
product** — Microsoft does not pro-rate: *"monthly subscriptions and initial (pre-renewal)
|
|
purchases aren't eligible for a prorated refund"*, so an annual sub cancelled in year 1
|
|
forfeits the remainder **and the developer cannot refund it**. Net after the 15% Store Fee
|
|
(verified, §6(b)(i) — applies to non-Game subscriptions too): **`$8.50/mo` and `$340`**.
|
|
Note ADA §6(h): we must fulfil the subscription for the whole period, and **raising the price
|
|
disables auto-renew** — so `$10` is effectively locked for the product's life. ⛔ A discounted
|
|
subscriber→lifetime upgrade SKU is **not** approved; do not declare one.
|
|
- ☐ Individual vs Company Partner Center account (10.8.3 / 10.14 — see
|
|
`research-store-certification.md` §11 item 1). **Get this right before enrolling**; a
|
|
Store+IAP product needing financial info for primary functionality would require Company,
|
|
but a free product with a paid upgrade tier is the normal consumer shape and is fine on an
|
|
individual account.
|
|
|
|
---
|
|
|
|
## 1. ☑ Bundle ffmpeg instead of downloading it — **SHIPPED 2026-09-28**
|
|
|
|
**This is item 1 because it is genuinely first** — it fixes a user-facing failure on its own and is a hard certification gate.
|
|
|
|
`Services/Encoder/FfmpegLocator.cs` used to pin a GitHub release URL, download it on first run,
|
|
extract it, and execute the result. On a cold cache the app downloaded an unsigned executable from
|
|
the internet and ran it.
|
|
|
|
- **Store blocker:** policy 10.2.2 forbids dynamic code inclusion (download-and-execute).
|
|
- **Route-independent bug class:** the previous daily pin aged out of GitHub retention and
|
|
**404'd on the first real recording attempt (2026-09-01)**. Bundling kills this permanently and
|
|
removes the startup network dependency.
|
|
|
|
**What shipped (2026-09-28, `eb0dd91` docs + this unit):**
|
|
- `scripts/fetch-ffmpeg.sh` — populates `./ffmpeg` from a pinned month-end BtbN LGPL-shared
|
|
artifact, **SHA-256 verified before extraction**, idempotent, `FFMPEG_ZIP=` override. The
|
|
binaries are **gitignored** (135 MB of DLLs would tax every commit on the 9p tree); the pinned
|
|
URL + SHA-256 are the audit trail the LGPL written-offer actually needs.
|
|
- `ytLive.csproj` — copies `ffmpeg\**` to the output with `CopyToPublishDirectory="Always"` (the
|
|
MSIX one), plus a **`GuardBundledFfmpeg` target that fails the build** if the folder is missing,
|
|
so a fresh clone finds out before a take rather than during one.
|
|
- `FfmpegLocator` — downloader, zip extraction, and the `%APPDATA%\ytLlive\tools` cache are all
|
|
**gone**. Order is now **bundle → PATH** (deliberately inverted from "the user's install wins":
|
|
a PATH ffmpeg may be a different major version or a GPL build). A missing bundle throws a loud
|
|
`IOException` naming the folder and the fetch script — the "found something on disk" path that
|
|
could mask a missing bundle is deliberately deleted.
|
|
- `ffplay.exe` (18 MB) is **not** shipped; nothing calls it.
|
|
- `THIRD-PARTY-NOTICES.txt` — "downloads" → "bundled" throughout, and the stale build tag
|
|
`autobuild-2026-08-09-13-03` (**the tag that 404'd**) corrected to `autobuild-2026-08-31-13-27`
|
|
with the exact artifact name and archive SHA-256 recorded. OpenH264 item made precise: it is
|
|
**compiled into `avcodec-63.dll`**, not a separate binary.
|
|
|
|
**Immutable tag (licensing rule):** `autobuild-2026-08-31-13-27`, artifact
|
|
`ffmpeg-N-126342-gf88b741dbf-win64-lgpl-shared.zip`, SHA-256
|
|
`46ea6521ed3877689d2ff0bd03640ac2637104415f6237d42625d13408767146`. The URL is now a **provenance
|
|
record, not a runtime fetch**. Month-end on purpose: BtbN keeps ~14 dailies but month-end builds
|
|
~2 years — a daily pin is what aged out.
|
|
|
|
**Licensing:** the LGPL-shared build was chosen for LGPL §6 compliance (dynamic linking =
|
|
"license text + source offer"). That reasoning is **unaffected**, and the notices now cover the
|
|
bundled copy explicitly.
|
|
|
|
**Tests (16 facts, `FfmpegLocatorTests`, suite 379/379):** the Good Dog integration test walks the
|
|
real build output, asserts the 9 files ship, resolves via the locator with an empty PATH, and
|
|
**executes `ffmpeg -version`** — running it is the part that matters, because a shared build only
|
|
starts if every `libav*.dll` is a sibling. Plus: bundle-over-PATH, PATH fallback, zero-byte bundle,
|
|
the loud failure, `TheBuildRefusesToCompileWithoutTheBundle`, and
|
|
`TheLocatorCarriesNoNetworkCapability` — a source-level fact that no `HttpClient`/URL/downloader
|
|
can reappear, so 10.2.2 cannot rot back.
|
|
|
|
**Answering "were there other 3p binaries needing bundling?" (verified 2026-09-28): no.** ffmpeg was
|
|
the only download-and-execute. `e_sqlite3.dll` + `WebView2Loader.dll` already ship via NuGet into
|
|
`runtimes\win-x64\native\`; OpenH264 is inside `avcodec-63.dll`; everything else is managed. The two
|
|
real gaps are **not** bundling problems: the **WebView2 Evergreen runtime** is a machine prerequisite
|
|
(item 2's manifest dependency + a graceful-failure path), and **Velopack's updater downloads and
|
|
executes** — its teardown (item 3) is a compliance action, not just cleanup.
|
|
|
|
---
|
|
|
|
## 2. ✅ `Package.appxmanifest` — full trust, camera, microphone (DONE 2026-10-22)
|
|
|
|
There was **no `.manifest` file in the repo at all**, so this was purely additive.
|
|
|
|
**Shipped:** `Packaging/Package.appxmanifest` + `Packaging/Assets/` (the four logo tiles) +
|
|
`scripts/package-msix.sh` (publish → stage → `makeappx pack`, optional `signtool` via
|
|
`MSIX_PFX`). The first real package was built on 2026-10-22: **`Package creation succeeded`,
|
|
159 MB, 509 files** in `artifacts/LlamaCasty.msix` (gitignored). The logos are generated from
|
|
the app's own artwork by `scripts/make-store-logos.py`, so listing art and app art cannot drift.
|
|
|
|
- `rescap:Capability Name="runFullTrust"` — medium integrity, not AppContainer
|
|
- `DeviceCapability` **webcam** and **microphone**
|
|
- `uap10:TrustLevel="mediumIL"` and `uap10:RuntimeBehavior="packagedClassicApp"` on
|
|
`<Application>` — the latter is what allows launching package executables (the bundled
|
|
ffmpeg) as child processes
|
|
- **English only** (`<Resource Language="en-US"/>`) — 10.7: otherwise the description must be
|
|
localized into every declared language
|
|
- Block map SHA2-256 is generated by `makeappx`; no `StoreManifest` is needed for a plain
|
|
MSIX submission; 159 MB is far under the 25 GB cap
|
|
- **x64 only** (creator ruling 2026-10-09 — `PlatformTarget=x64` already in both csproj files):
|
|
`ProcessorArchitecture="x64"`, published `-r win-x64 --self-contained` so only the win-x64
|
|
native folders ship (an RID-less build copies every RID's ONNX Runtime native, ~17 MB each)
|
|
and the package carries its own .NET runtime. Store listing system requirements:
|
|
"64-bit Windows 10 or 11".
|
|
|
|
**Do NOT use `appContainer`** — see the invariant in item 6.
|
|
|
|
**Schema gotchas found the hard way** (the packer rejects the manifest, one error at a time):
|
|
`Square310x310Logo` and `Wide310x150Logo` are **NOT** attributes of `uap:VisualElements` in the
|
|
Windows 10 schema — they belong on `<uap:DefaultTile>`, a child of `VisualElements`. And
|
|
`Assert.DoesNotContain("appContainer", manifest.ToString())` fails on the manifest's own
|
|
comment: guard the **attribute values**, not the file text.
|
|
|
|
**Guard:** `ytLive.Tests/StorePackagingTests.The_package_manifest_declares_full_trust_camera_and_microphone`
|
|
— identity (x64), full-trust application element, the three capabilities, no `appContainer`
|
|
value, en-US only, and every referenced logo exists in `Packaging/` (makeappx refuses to pack
|
|
otherwise).
|
|
|
|
## 3. ✅ Remove Velopack — 3 edit sites (DONE 2026-10-22)
|
|
|
|
The Store handles updates, so this is simply **deleted**. Trivially removable; the code was
|
|
written defensively for exactly this.
|
|
|
|
- `ytLive.csproj:92` — `<PackageReference Include="Velopack" Version="1.2.0" />` — **REMOVED**
|
|
- `App.xaml.cs:3` — `using Velopack;` — **REMOVED**
|
|
- `App.xaml.cs:38-46` — the sole call site (already try/caught and commented
|
|
"(non-fatal) … when no URL is set, the check is a no-op") — **REMOVED**
|
|
|
|
**Guard:** `ytLive.Tests/StorePackagingTests.cs` — the ONE test asserts the app project
|
|
references no updater package (Velopack/Squirrel/NetSparkle/AutoUpdater/WinSparkle) and that no
|
|
app source file uses the updater API (matched as CODE — a `using` directive, a type usage — not
|
|
as a bare word, so a file's prose may still name what it deliberately does not do). The app was
|
|
smoke-tested after the removal: `MainWindow loaded`, no exceptions.
|
|
|
|
**Retires:** the pending "Velopack update URL" item (`TASKS.md` open items,
|
|
`task-10-monetization.md:43`) and the self-hosted DigitalOcean droplet.
|
|
|
|
## 4. ☐ Windows App Certification Kit
|
|
|
|
Run before submission. Technical compliance is tested by WACK; it is not optional. Known
|
|
relevant check: the app must start promptly, stay responsive, and shut down gracefully
|
|
(10.4.2).
|
|
|
|
## 5. ☐ Certification notes + the three documentation artifacts
|
|
|
|
**In Partner Center (submission):**
|
|
|
|
- ☐ **The IARC age-rating questionnaire** (10.11.1) — general audience, 12+ territory
|
|
- ☐ **A working YouTube demo account** (10.3.1) — the app is useless to a reviewer without one.
|
|
Must be a real, maintained account able to sign in and start a stream; a dead or
|
|
credentials-lapsed account is a rejection. ⛔ **This was accidentally dropped from this list**
|
|
during the Store-IAP edit — it is in `research-store-certification.md` §4 and must be done.
|
|
- ☐ **Declare the IAP products and their pricing** in Partner Center (required for Store IAP).
|
|
⛔ **No `.99` prices** — creator ruling, and incoherent for a no-dark-patterns brand. Confirm
|
|
the **net** after the revenue share, not the list.
|
|
- ☐ Note that the product is **general audience, not directed at under-13s**
|
|
- ☐ The 11.12 UGC position is **less load-bearing now that Polar is gone** — the strong part of
|
|
the original argument was "we render transiently, persist nothing, and provide no
|
|
user-to-user communication surface," which is *unaffected*. State it in full anyway; the
|
|
reasoning and the Q1-2026 YouTube enforcement numbers are in
|
|
`research-store-certification.md` §9
|
|
- ☐ The **YouTube age-gate argument** — the operator is 13+ by construction (§8)
|
|
|
|
**On `llamachile.shop`:**
|
|
|
|
- ☐ **Privacy policy** (10.5.1 — mandatory for Win32/Desktop Bridge). Must state: camera/mic
|
|
access is user-directed and OS-gated; **no retention** of chat or reward payloads; no
|
|
viewer data collected; **ffmpeg is bundled and nothing is fetched at runtime**
|
|
- ☐ **Code of conduct + content guidelines** (11.12 / 11.15)
|
|
- ☐ Confirm `THIRD-PARTY-NOTICES.txt` covers the bundled LGPL ffmpeg build
|
|
|
|
**In Partner Center (listing):**
|
|
|
|
- ☐ Title is exactly **`LlamaCasty`** — 10.1.1 forbids marketing text or extraneous keywords
|
|
- ☐ Search terms: max 7, no pricing terms, and **no other product titles** (10.1.3 — cannot
|
|
list `OBS` or `StreamYard`)
|
|
- ☐ Real listing content — 10.1.4 requires an active, substantive presence
|
|
- ☐ Review the **Individual vs Company** account question before enrolling
|
|
|
|
## 6. ☐ The full-trust recording invariant — **comment lands WITH this work**
|
|
|
|
`ViewModels/MainViewModel.Recording.cs:173-179` (`DefaultRecordFolder()`) writes to
|
|
`%USERPROFILE%\Downloads` or `SpecialFolder.MyVideos`; `ChooseRecordFolder()` (line 151)
|
|
uses `Microsoft.Win32.OpenFolderDialog`; the temp-write-then-move lifecycle stays in one
|
|
directory (line 131-132).
|
|
|
|
**This works only because the package is full trust.** At `mediumIL`, user-profile writes
|
|
pass through unvirtualized and `OpenFolderDialog` is an ordinary Win32 browser with no
|
|
capability token. No `broadFileSystemAccess` needed.
|
|
|
|
⚠️ **If anyone flips the manifest to `appContainer`, recording silently breaks** —
|
|
`Directory.CreateDirectory` and `File.Move` start resolving to a per-package virtualized
|
|
location and output vanishes.
|
|
|
|
**Add a comment at `DefaultRecordFolder()` in this task, not before.** A comment describing
|
|
a manifest that does not exist is worse than no comment.
|
|
|
|
**Also verify on a real packaged build** before trusting it with recordings — the docs say
|
|
full trust passes writes through, but that is worth confirming with an actual package
|
|
identity rather than an unpackaged run.
|
|
|
|
## 7. ☐ Confirm the disqualifiers stay clear after packaging
|
|
|
|
All four are currently clear (verified by grep 2026-09-27) — re-verify once the packaging
|
|
project exists:
|
|
|
|
- No Windows driver installed (we consume DShow filters, we do not install one)
|
|
- No per-user Windows service
|
|
- No elevation / `requireAdministrator` / UAC manifest
|
|
- No shell extension, no in-process module loading by outside processes, no jump list
|
|
|
|
## 8. ☐ Pre-submission gates
|
|
|
|
- ☐ 0 warnings, full suite green
|
|
- ☐ **Vertical-recording path verified end-to-end** (compositor tier is proven by
|
|
`Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop`; the *record* path has never
|
|
been exercised — `Services/Pump/FramePump.cs:645` flags off-size tiers as a known
|
|
follow-up)
|
|
- ☐ **The real-output confirmation** the creator has been doing manually
|
|
- ☐ Camera/mic/wasapi capture verified **from the packaged build**, not just unpackaged
|
|
- ☐ Clean uninstall verified (10.2.7)
|
|
- ☐ Notification-disabled path leaves the app functional (10.9)
|
|
|
|
---
|
|
|
|
## Not in this task (deliberately)
|
|
|
|
- **Velopack hardening / obfuscation / assembly splitting** — stays a GA-time decision per
|
|
TASK 36 item 6's agreed ceiling. Compile flags max at `HARDENED` + `MOCK_REWARDS`,
|
|
additive-only.
|
|
- **EULA draft/review and the THIRD-PARTY-NOTICES gate** — stay in TASK 36 item 6.
|
|
- **Vertical-canvas feature work** — the feature exists; only the *record-path test* above
|
|
is missing.
|
|
- **macOS / Avalonia port** — deferred; would be a second app, not a port.
|