Renames AssemblyName to LlamaCasty before the first Steam depot — Steamworks is explicit that renaming a live exe is painful, so this had to land first, not later. Four assembly-qualified pack URIs carried the old name, not three: MainWindow (window icon), OverlayHost (logo), RenameRecordingDialog and KeepBroadcastDialog. Pack URIs resolve by assembly name at runtime, so a stale one compiles, passes every test and then silently renders nothing the first time that view opens. AssemblyIdentityTests guards both halves — the assembly name, and no source XAML still naming the legacy assembly. Packaging/Package.appxmanifest and package-msix.sh carry the exe name too. scripts/publish.sh replaces the by-hand publish: single-file win-x64, ReadyToRun, ffmpeg beside the exe, and it refuses to finish if ffmpeg, LICENSE, PRIVACY.md or THIRD-PARTY-NOTICES.txt are missing, or if a test assembly leaked in. That last guard exists because the first publish was wrong in a way no test could see: PublishSingleFile + IncludeNativeLibrariesForSelfExtract bundled the libav*.dll family into the exe — a 9-file output with no ffmpeg folder and a 182 MB binary, so the locator would have found nothing at runtime. ExcludeFromSingleFile="true" on the content keeps the folder a folder. Diffing against a control publish without the single-file flags found it; guessing at item globs wasted two attempts first (recorded in MyMistakes.md). Measured: 127 MB exe + 129 MB ffmpeg = 257 MB depot. Tests: AssemblyIdentity, StorePackaging, BuildStamp, BundledFonts all pass; app builds with 0 warnings.
315 lines
21 KiB
Markdown
315 lines
21 KiB
Markdown
# Product: brand, licensing, monetization & distribution
|
||
|
||
What the product is called, what it promises, what it may never do legally, and how it ships.
|
||
Read before branding, marketing, pricing, licensing or packaging work.
|
||
|
||
## Brand
|
||
|
||
**The UI design standard is [`DESIGN.md`](../../DESIGN.md)** — colours, type scale, spacing, radii
|
||
and elevation, all normative, implemented by `Themes/Tokens.xaml`. Accent blue `#38bdf8` is the only
|
||
interactive accent; red `#ff5451` means live/record and nothing else. The old brand red `#e94560` is
|
||
retired from app chrome (the wordmark/logo artwork is unchanged).
|
||
|
||
Marketing palette, official assets and copy live in `MARCOM.md` (gitignored — read it before any
|
||
branding or marketing work).
|
||
|
||
### Product name vs repo/assembly branding (2026-09-14)
|
||
|
||
The product is **llamacasty** (llamacasty.com, llamachile.tube, `llgit.llamachile.tube`). The
|
||
repo path, csproj `AssemblyName`/`RootNamespace`, DB/log paths (`%APPDATA%\ytLlive\...`),
|
||
and most internal namings are the legacy **ytLive/ytLlive** — a re-brand that never renamed
|
||
the internals. Treat the two as separate: external/user-facing language says "llamacasty",
|
||
code/assembly/repo names stay `ytLive`. If a full internal re-brand is ever done, this note
|
||
and the UI locator (Premium/wordmark) are the checklists; it is NOT a goal pre-1.0.
|
||
|
||
**Say "LlamaCasty" when reporting to the creator** — never "ytLive" in prose. `ytLive` is
|
||
the internal repo/assembly/namespace name only. Exact current split:
|
||
|
||
| surface | name | note |
|
||
|---|---|---|
|
||
| product / site / marketing | **LlamaCasty** | llamacasty.com, llamachile.tube |
|
||
| repo dir, csproj file, `RootNamespace` | `ytLive` | 230+ files: `namespace ytLive.*` |
|
||
| `AssemblyName` → shipped exe | **`LlamaCasty`** | `LlamaCasty.exe` — renamed 2026-11-01 |
|
||
| app data dir | `%APPDATA%\ytLlive` | **capital L**, drifts from `ytLive` |
|
||
|
||
**Consequences, so nobody re-hunts them:**
|
||
- The assembly rename landed 2026-11-01 (before the first Steam depot, because Steamworks warns
|
||
that renaming a live exe is painful). Namespaces stay `ytLive.*` — they are not user-visible
|
||
and renaming them would churn every file for nothing.
|
||
- An assembly-qualified pack URI spells the assembly name, so the rename **hard-breaks silently**
|
||
at runtime, not at compile time. Four carried it, not three:
|
||
`MainWindow.xaml:14` (window icon — taskbar/Alt-Tab), `OverlayHost.xaml:96`,
|
||
`RenameRecordingDialog.xaml:5`, and `KeepBroadcastDialog.xaml:5`.
|
||
`ytLive.Tests/AssemblyIdentityTests.cs` now guards both halves: the assembly name is
|
||
`LlamaCasty` and no source XAML still names the legacy assembly.
|
||
- `Packaging/Package.appxmanifest` (`Executable`) and `scripts/package-msix.sh` carry the exe
|
||
name too; the MSIX is unused but must not lie about the artifact.
|
||
- Renaming the `%APPDATA%\ytLlive` folder is **breaking** — it would orphan every existing
|
||
user's layout DB, `Assets` and settings. Never do it casually; needs a migration.
|
||
- No code compares its own assembly name (no `GetExecutingAssembly().GetName().Name` checks),
|
||
so the assembly name is otherwise free to change.
|
||
|
||
### AI transparency
|
||
|
||
This project is built through AI-assisted pair-programming. All code is generated
|
||
under the supervision of an experienced developer with decades of coding experience.
|
||
Architecture decisions, product direction, and quality gates are human-owned;
|
||
AI accelerates implementation. This is a testimonial to successful human-AI
|
||
collaboration, not autonomous code generation.
|
||
|
||
## Licensing — proprietary since 2026-10-30
|
||
|
||
**LlamaCasty is closed-source commercial software.** `LICENSE` is the EULA; the paid tier exists
|
||
because output quality is the paid delta — the free tier is capped at a 720p30 profile (creator
|
||
ruling 2026-10-30, superseding free-forever). Prices, entitlement and channels:
|
||
[`commercial.md`](commercial.md).
|
||
|
||
- **Every GPL build already published stays GPL for that snapshot.** Attribution/§7 terms
|
||
(`ADDITIONAL-TERMS.md`) are deleted and were GPL-only; the files are gone.
|
||
- **No GPL third-party component may be linked.** ffmpeg stays a separate subprocess (LGPL,
|
||
relinkable via the shared build), OpenH264 ships as Cisco's binary, the rest are MIT/Apache/CC0.
|
||
Never add a statically linked GPL library — that would make the commercial build undistributable.
|
||
- **Obligations that still bind** (they are third-party, not ours): keep `THIRD-PARTY-NOTICES.txt`
|
||
shipped and shown in About, ship full licence texts at v1, keep the ffmpeg pin immutable and
|
||
`lgpl-shared`, never use the `nonfree` variant, keep icon art CC0/public-domain, keep the ML model
|
||
provenance clean (Google MediaPipe, Apache-2.0 — no model-zoo re-hosts).
|
||
- **Fonts:** Inter + JetBrains Mono are OFL-1.1, bundled as separate files with their licence text —
|
||
allowed in a commercial product, but the fonts may not be sold on their own and must not be
|
||
modified under the reserved names.
|
||
- **Brand artwork** (name, icon, logo) is the publisher's property; the EULA grants no trademark
|
||
rights and forbids implying endorsement.
|
||
- **No source offer.** The GPL §6 source link is gone; the repo is private.
|
||
|
||
## Design Principle
|
||
|
||
> This software is so intuitive that even the most right-brained person can easily intuit and use it.
|
||
|
||
Apply this to every UI decision:
|
||
- One-click go-live with working defaults
|
||
- Prefilled YouTube defaults (RTMP URL, bitrate, resolution, latency)
|
||
- Visual/drag-and-drop scene building over property panels
|
||
- Every action produces a visible outcome — no dead ends
|
||
- **"We're not them" (2026-09-01):** assume the creator's hardware is mediocre, because it is.
|
||
Every feature spends the machine's budget once — never twice for the same result (record OR
|
||
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 — free tier + Pro output quality
|
||
|
||
**Every feature is free and ungated** (creator ruling 2026-10-08, TASK 54, kept by the 2026-10-30
|
||
freemium ruling): no feature lock, no watermark, and nothing licence-shaped in the output. The
|
||
paid delta is **output quality alone** — free is capped at a 720p30 profile, Pro unlocks the
|
||
higher tiers for **both the stream and the recording**. Prices, entitlement and channels:
|
||
[`commercial.md`](commercial.md). Support is email + GitHub issues; an in-app bug reporter is out
|
||
of product.
|
||
|
||
**The branding credit is retired as a paid delta and stays off.** `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."* Entitlement must never enable it.
|
||
`BrandFlashOutputTests` opts in by setting the switch before constructing a `MainWindow`;
|
||
`Credit_IsOffByDefault_EvenOnTheFreeTier` guards the default.
|
||
|
||
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.
|
||
|
||
Live prices, entitlement and channels: [`commercial.md`](commercial.md). Retired pricing, the
|
||
Store IAP plan and the rejected alternatives: [`decisions.md`](decisions.md).
|
||
|
||
|
||
**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
|
||
design, for **every** creator, free and ungated. It has three layers, all free — the capability no
|
||
competitor ships (the positioning order across features lives in `MARCOM.md`):
|
||
|
||
- **Reward-event capture (the data foundation).** The live-chat poll (`YouTubeChatService.Poll`)
|
||
receives every reward event over `liveChat/messages` and — since TASK 43 (2026-09-24) —
|
||
**decodes all six types**, not two: `superChatEvent`, `superStickerEvent`, `newSponsorEvent`,
|
||
`membershipGiftingEvent`, `giftMembershipReceivedEvent`, `memberMilestoneChatEvent` land as
|
||
`ChatMessage.Kind` (`ChatEventKind`) with their payload fields (display string, sticker
|
||
description, gift count, gifter name, milestone months); free subscriptions emit NO event
|
||
(API fact — a sub mention is a plain text row). The remaining half of the original capture
|
||
plan stays open (TASK 3 item 20): persist each event to the canonical SQLite `RewardEvents`
|
||
table (broadcastId, type, timestamp, amountMicros, currency, tier...) + `superChatEvents.list`
|
||
(30-day lookback) backfill — that persistence is what the session report and the journey
|
||
tracker read. Alerts did not wait on it: the shipped alert path (TASK 43) renders directly
|
||
from the in-memory `ChatMessage` feed.
|
||
- **Session / broadcast report.** A session is created at Go Live; the poll + End roll that session's
|
||
reward totals (new members, Super Chat $, stickers $, gifts, milestones). This is the in-app
|
||
replacement for the emailed external "stream activity report" — the incumbents (Streamlabs et al.)
|
||
email near-useless post-stream summaries; we track the same data natively and maintain it.
|
||
- **Journey tracker (TASK 39, slice 1 shipped 2026-09-22).** A **"YPP"** tab below the
|
||
Stream Settings pull-out (Live screen right edge) shows the connected creator's position
|
||
toward each YPP tier threshold. Slice 1 = current-scope data only: `channels.list?mine=true
|
||
&part=statistics,contentDetails` (subs/views/videos + uploads-playlist id) +
|
||
`playlistItems.list` for 90-day public uploads. Every refresh appends
|
||
a `YppSnapshot` to SQLite (schema v10) — the history a later slice needs for velocity/ETA.
|
||
**The `auditDetails` part is OUT (fixed 2026-09-23):** readding it 403s the WHOLE request —
|
||
that part alone requires the `youtubepartner-channel-audit` scope no app for normal creators
|
||
should request (the standing flags sat unreadable since slice 1 shipped; the app's `youtube` +
|
||
`youtube.force-ssl` scopes were never sufficient for them — the "no re-consent" slice-1 claim
|
||
was wrong for that part). Channel standing now deep-links to the Earn page; API stats come from
|
||
`statistics,contentDetails` only. YPP/API failures log the response body's `error.reason`.
|
||
**YPP thresholds are versioned, date-aware DATA, never constants** — the Tier-2 bar doubles
|
||
for new applicants on **2027-02-01** (long-form 4,000 → 8,000 qualified hrs / 365d; Shorts
|
||
10M → 20M / 90d), while the Tier-1 fan-funding bar (500 subs / 3,000 hrs / 3M Shorts) is
|
||
unchanged. Hardcoding the pre-2027 numbers would ship the wrong ETA. Watch-hours / Shorts /
|
||
velocity / ETA are **slice 2**: Analytics-API scopes (`yt-analytics.readonly`, additive
|
||
`yt-analytics-monetary.readonly`) behind the `IChannelStatsProvider` seam (additive
|
||
capability, requires an OAuth re-consent). Compliance items no API exposes (2FA, AdSense
|
||
linkage) are in-app self-reported checkboxes with deep links; channel standing (exposed only
|
||
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.
|
||
|
||
|
||
## Auth gates Go Live, but not exploration
|
||
|
||
The app is fully usable without authentication: users can build scenes, add sources, compose
|
||
previews, and audition the software with zero commitment. But **going live requires authentication** —
|
||
it's the one capability gated behind YouTube sign-in. The sign-in should never pressure the user
|
||
("sign in (optional)", not a modal wall): the two-state top bar shows **Start Stream** (offline) /
|
||
**End Stream** (live), and the Start Stream dialog hosts the account — a saved session appears as
|
||
the default with "Change Account"; with none saved, a "Sign in to YouTube" button starts OAuth and
|
||
the Start button stays disabled until signed in.
|
||
|
||
## Account assumption (do not build an account setup flow)
|
||
|
||
Connecting uses Google OAuth ("Sign in with Google") to link an **existing** YouTube creator
|
||
account. ytLlive **never creates or sets up accounts** — that is YouTube's job. If the creator has no
|
||
YouTube channel, they go to YouTube first. This assumption is explicit and must never be silently
|
||
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 (Steam only)
|
||
|
||
Live channel policy, the artifact and costs: [`commercial.md`](commercial.md). Build and depot
|
||
mechanics: [`Distribution.md`](../../Distribution.md). Retired routes and cause of death:
|
||
[`decisions.md`](decisions.md).
|
||
|
||
**One vector: Steam.** A free app on Steam with the paid edition as a Steam **DLC**; the monthly
|
||
plan is sold outside Steam and is never purchasable inside a Steam build (Steamworks requires the
|
||
Steam Wallet for in-app purchases). No direct download, no winget, no Microsoft Store.
|
||
|
||
**Consequences that are architecture, not packaging detail:**
|
||
|
||
- **⛔ There is no self-updater, and now no version banner either.** Steam delivers updates; the
|
||
app keeps nothing that downloads or executes, for the same Store-policy reason it always had.
|
||
`ytLive.Tests/StorePackagingTests.cs` must keep asserting that no self-updater returns.
|
||
- **⛔ `steam_api64.dll` is a functional dependency**, not the rejected "native helper DLL for IP
|
||
protection" — Steam DLC ownership cannot be read without it. The app must start without Steam
|
||
running and fall back to the free profile.
|
||
- **The MSIX package stays buildable but is now unused** — `scripts/package-msix.sh` (TASK 48
|
||
slice 2) still produces `artifacts/LlamaCasty.msix` from `Packaging/Package.appxmanifest` +
|
||
`Packaging/Assets/`. It was the Store channel's artifact; with Steam exclusive there is no
|
||
submission target. The full-trust `packagedClassicApp` findings below remain true and are still
|
||
worth reading before any packaged build is attempted.
|
||
- **⛔ Bundle ffmpeg — no runtime downloads** (was Store policy 10.2.2; still right for a Steam
|
||
depot, 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
|
||
is the win64 build, ONNX Runtime 1.31 ships no win-x86 native (only win-x64/win-arm64), and a
|
||
32-bit address space can't hold the 1080p frame buffers. Windows 11 is 64-bit-only anyway.
|
||
Public copy says "64-bit Windows 10 or 11".
|
||
- **Portable mode is a feature, not a channel:** `portable.txt` beside the exe keeps the layout DB
|
||
and settings in `.\data\` (hung off `LayoutPathOverride`), so a copied install carries someone's
|
||
scenes. Steam installs it; the marker is for afterwards.
|
||
|
||
**What does NOT change: no watermark and no feature gating.** The free tier is the whole app at a
|
||
720p30 profile. `Monetization` above governs.
|
||
|
||
### ⛔ Durable invariant: full trust, or recording silently breaks
|
||
|
||
MSIX packages run at one of two trust levels. A packaged WPF app declaring
|
||
`rescap:Capability Name="runFullTrust"` runs at **mediumIL — the same permissions as a
|
||
standard desktop app, NOT AppContainer** — and may spawn child processes, which is why
|
||
bundling ffmpeg is legal. Writes to user-profile locations (AppData, Downloads, MyVideos)
|
||
**pass through unvirtualized at mediumIL**; the package install folder is read-only.
|
||
|
||
**`ViewModels/MainViewModel.Recording.cs` writes recordings to
|
||
`%USERPROFILE%\Downloads` / `SpecialFolder.MyVideos` (`DefaultRecordFolder()`,
|
||
`ChooseRecordFolder()` via `Microsoft.Win32.OpenFolderDialog`), and the whole
|
||
temp-write-then-move lifecycle stays in one directory.** That works *only* because we are full
|
||
trust. No `broadFileSystemAccess` capability is needed, and none should be added.
|
||
|
||
⚠️ **If anyone flips the manifest to `appContainer`, `Directory.CreateDirectory` and
|
||
`File.Move` start resolving to a per-package virtualized location and output vanishes with no
|
||
error.** A comment at `DefaultRecordFolder()` is owed **when the manifest lands** (TASK 48
|
||
item 6) — not before; a comment describing a manifest that doesn't exist is worse than none.
|
||
|
||
### Signing — not needed, because Steam installs the app
|
||
|
||
Steam's client delivers the files, so there is no browser download, no Mark-of-the-Web and no
|
||
SmartScreen wall. **The certificate has no job in this channel.** It stays on the shelf for the day
|
||
a non-Steam download exists:
|
||
|
||
- **Azure Artifact Signing** (renamed from "Trusted Signing") — $9.99/month Basic, up to 5,000
|
||
signatures, **requires a paid Azure subscription**; individual identity validation is the slow
|
||
part and must be renewed.
|
||
- **Never EV** — Microsoft removed the EV SmartScreen instant-bypass in March 2024, so EV buys what
|
||
OV buys. **Never a certificate with an HSM** for this product.
|
||
- **Steam does not sign the exe.** A UAC prompt for the app is elevation, not publisher trust.
|
||
|
||
### Distribution and licensing are independent — the channel never decides billing
|
||
|
||
The Steam build sells the lifetime as **Steam DLC** and reads it with `BIsDlcInstalled`, while the
|
||
monthly plan is billed by **Polar** outside Steam. Steamworks forbids taking payment inside the app
|
||
with anything but the Steam Wallet, so the app states where the monthly lives and never sells it.
|
||
The lesson worth keeping: the licensing choice rides on a **monetization** decision, not on a
|
||
distribution decision.
|
||
|
||
### ⛔ "Only when the user says so" is enforced by Windows, not by us
|
||
|
||
Camera/mic consent is layered: Store policy **10.6** forbids circumventing OS checks; the
|
||
**Windows desktop-app camera/microphone toggle** (Settings → Privacy & security, since Win10
|
||
1903) is the gate; hardware lights and tray indicators are the OS's disclosure mechanism. So
|
||
**route everything through the standard MF/DShow/WASAPI paths and we never write that
|
||
certification ourselves** — the OS does it and gets neither our credit nor our blame.
|
||
|
||
⚠️ **Untested:** whether that toggle reliably gates a **DShow webcam and a capture card**.
|
||
Microsoft's own support page admits desktop apps "might still be able to access your camera
|
||
or microphone even when these settings are turned off." This is **not** a certification
|
||
failure (we declare `webcam` honestly and use supported APIs) but it is a real user-trust
|
||
issue — so **no privacy-forward marketing copy until it is measured.** Guard note added to
|
||
`MARCOM.md`.
|
||
|
||
### ⛔ Durable invariant: chat is rendered, never stored
|
||
|
||
`TASK 49`'s profanity filter is **local, on-device, opt-in, non-persistent**, with a
|
||
**user-supplied word list — never a hardcoded slur list baked into the binary** (unpleasant
|
||
artifact, false positives across dialects, extractable). It must **never match the SuperChat
|
||
amount or any reward field** — those are financial data under policy 10.5.5.
|
||
|
||
The non-persistence half is not just hygiene: it is the load-bearing half of the **11.12 UGC
|
||
certification answer** (mirrored YouTube chat is moderated at industrial scale upstream; ~1.6B
|
||
comments removed in Q1 2026 alone, child safety the #2 reason at 124.6M). **No future feature
|
||
— chat history, moderation logs, analytics, crash payloads — may quietly break that
|
||
assumption.** If one is ever proposed, the 11.12 position must be re-argued first.
|
||
|
||
### Minors
|
||
|
||
The **operator is 13+ by construction**, not by promise: LlamaCasty requires a YouTube
|
||
account to stream, and YouTube's ToS sets the account floor at **13** (regional variants reach
|
||
14; 13–17 need parent/guardian permission; under-13s are on supervised/YouTube Kids accounts).
|
||
So a COPPA theory premised on collecting PI from under-13s has no purchase against the
|
||
operator. The residual is **viewers** — still possible on supervised family accounts — and it
|
||
is weak: live chat is restricted there, YouTube moderates before we see it, and we persist
|
||
nothing. COPPA is very unlikely to bind (not child-directed), but note the enforcement trend
|
||
(FTC Sept 2025 *Apitor*: operators are responsible for what third-party components collect) —
|
||
so the answer is a documented inquiry, and ours is "no third-party analytics or ad SDKs, no
|
||
persisted chat or reward payloads."
|
||
|