b00bf48b9d140ba1459c31a434a4d5d8b4c5b9d2
195
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6d1c982e26 |
feat(entitlement): IEntitlement seam — Steam DLC answers, everything fails closed
The first half of surge day 1. One question, "may this session emit Pro output?", with one method on the interface so nothing downstream knows which rail proved it. SteamEntitlement reads the lifetime SKU the way Valve intends: SteamAPI.Init() then SteamApps.BIsDlcInstalled(dlcAppId). Four different failure paths all resolve to the free tier rather than to Pro or to a crash — no DLC id configured, no Steam client, steam_api64.dll missing (a throw on first touch), and simply not owned. A copied folder run without Steam is the common case, not an exceptional one. NotConfigured() is the shipping default until Steamworks creates the DLC app; it answers false without ever touching the native library, so today's builds behave exactly as they did before this commit. HasProOutput is now IsPremium || DeveloperMode || rail, with an internal EntitlementOverride test seam (internal, because a public setter here is a licence bypass). Four tests: no id reads false, no client reads false without throwing, the fake reports what a test sets, and the view model takes its answer from the rail. Steamworks.NET 2024.8.0 added; the native DLL still comes from scripts/steam-sdk.sh, so the csproj change is three lines and no formatting churn. |
||
|
|
fdb27f0758 |
feat(ui): gear to the rail, EDIT on the panel edge, preview undo/redo
Three creator directives (2026-11-01), done ahead of the surge. **Gear moved to the preview rail** — first tab, orange (Brush.Attention), gear icon only. Left-click opens the same app menu the top bar gear owned (Settings / Bug / Feature / About), right-click goes straight to App Settings; both behaviours moved with it, so nothing lost an entry point. TopBar keeps the wordmark and EDIT. **EDIT right edge aligned to the layers panel.** The work area is margin 8 + a 360px column, the top bar is padding 16, so the shared edge is 352 inside the bar. TopBar column 0 is now a fixed 352 grid with EDIT right-aligned in it — alignment by arithmetic instead of by eye. **Preview undo/redo: Ctrl+Z / Ctrl+Shift+Z.** Models/PreviewHistory snapshots element REFERENCES plus geometry, visibility, order and membership — a deep clone would mirror every property of a dozen element subclasses and quietly miss the next one added. One snapshot per gesture (pushed at drag/resize start, never in the move loop), bounded at 50, redo survives multiple undos, cleared on layout load. Gated on EditingActive: undo while the preview shows program output would rewrite a scene nobody is looking at. Honest scope, in the type's doc comment: layout edits only. Colour/opacity/border/font edits are not captured yet, so Ctrl+Z after one of those restores the last layout change. 6 tests in PreviewHistoryTests; 19 pass across it and the neighbouring UI classes; Debug and Release both build at 0 warnings. |
||
|
|
600088c794 |
docs(tasks): six days, all remaining code — no week 2, no after-launch list
Correcting my own plan: the surge is six calendar days (Mon 2 - Sat 7), Saturday included, and it finishes the coding side. Every open row is assigned to a day; the week-2 and after-launch sections are gone because there are no such sections. Days: licensing (entitlement, licence UI, greyed tiers, system messages), release blockers (visibility picker, output contract, webcam identity, Move transitions), resilience (TASK 32, bandwidth step-down), content and media (text layer, media wiring, profanity filter, scheduled streams), data and settings (reward persistence, YPP slice 2, defaults split, scene audio, settings round, disk hygiene), finish and prove (publish recording, undo, gold pass, full sweep). Target 6-8 units a day against a measured baseline of 3-5 — that gap is the surge. If a day overruns, the smallest item moves; a launch blocker never does. Day 6 is the only polish day. |
||
|
|
b2e8b26b6e |
docs(tasks): the surge week, coding only — five days sized at 3-5 units each
Non-coding is parked on Valve (identity verification, store page, depot); the week is code, tests, debugging, polish. Ordered by what actually blocks a public build: entitlement seam and licence UI (days 1-2), greyed tiers (2), visibility picker (3), stream resilience (3-4), output contract (4), gold pass (5). Sized honestly against the measured rate: ~50 units remain, 15-25 fit in five days, so the plan names week-2 candidates and the five items that should not block launch at all (scheduled streams, scene audio, defaults split, YPP slice 2, feedback batch). Every unit carries the same definition of done — clean build, per-class tests, verify.sh on the declared scope, and the falsified doc corrected in the same commit. |
||
|
|
1fe84be64a |
build(steam): stage steam_api64.dll beside the exe — one file, not the whole SDK
The SDK zip is 24 MB of headers, examples and tools we never build. The runtime dependency is one native library, so scripts/steam-sdk.sh extracts exactly sdk/redistributable_bin/win64/steam_api64.dll into steam/ (gitignored, like ffmpeg/ — the script is the audit trail) and writes steam_appid.txt = 480, Valve's Spacewar app, so SteamAPI.Init() resolves an app id locally before LlamaCasty has one of its own. Two csproj details that would have cost a debugging session: - Link="steam_api64.dll" puts it BESIDE the exe; without Link, None items keep their relative path and it lands in steam/, where the loader does not look. - ExcludeFromSingleFile, for the same reason ffmpeg needed it: a native DLL otherwise gets swallowed into the single-file bundle and cannot be loaded at runtime. steam_appid.txt is Debug-only — Valve's guidance is not to ship it — and the publish output confirms it: dll present at the root, no appid file, exe still 127 MB (proof the DLL was not bundled). Also queued, as asked: preview undo (Ctrl+Z / Ctrl+Shift+Z) in edit mode, noted as the partner to the defaults-vs-current split — undo covers the slip, defaults cover the mess. |
||
|
|
27fc6fb507 |
feat(profile): the free budget is pixels and fps, enforced once — with a dev-only release hatch
The paid delta is output quality, so the rule needs to be measurable rather than a word:
921,600 pixels and 30 fps, fail-closed. Models/OutputProfile.cs is that rule as pure
policy — a permitted tier passes through untouched (Pro, or the free tier's own 720p30),
a disallowed one resolves to the best tier the budget allows and carries ITS bitrate, so
a capped user is never offered an 8 Mbps budget for a 720p frame.
Applied once, in BuildEncoderOptions. That is genuinely one place: the stream and the
local recording are two output blocks of ONE ffmpeg process fed by one EncoderOptions,
so there is no second encoder to forget. (commercial.md claimed "both encoders — never
two checks"; the code is stronger than the doc, and the doc is corrected.)
Developer override, as asked: a "Developer mode — all resolutions" checkbox in App
Settings, bound to MainViewModel.DeveloperMode, which feeds HasProOutput. It is not
hidden in a release build — Controls/DeveloperPanel's Page and Compile items are removed
when Configuration != Debug, and OverlayHost adds it at runtime inside #if DEBUG, so a
shipped exe has no BAML for it at all. The setter also refuses the value when
DevBuild.IsDevelopment is false, which is now the single source for that question (the
scoreboard partial had its own duplicate const).
Two traps worth their lines: a `const bool` dev flag makes `if (!Flag) return;` an
unreachable-code warning in one configuration or the other (CS0162), so DevBuild uses
static readonly; and a Condition on an ItemGroup is invisible to a test reading
item.Attribute("Condition") — read the parent.
Tests: 14 pass across OutputProfileTests, AssemblyIdentityTests and StorePackagingTests.
The invariant test is the one that matters: iterate every shipped tier under a free
entitlement and assert none emits more than the budget, so a future 4K tier fails here
instead of shipping to everyone. App builds Debug and Release at 0 warnings.
|
||
|
|
abfab25685 |
build: the exe is LlamaCasty.exe, and the publish script proves the artifact
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. |
||
|
|
8f79b20a18 |
docs(distribution): Steam is the only channel — the zip, winget and the Store are dead
The site said "Coming soon to Steam" while the app docs still said direct download plus winget primary with Steam parked. One of them had to move, and the creator's ruling is Steam exclusive: a free app on Steam with the paid edition as a Steam DLC, and the $10/month plan sold off-Steam by Polar. Records the constraint that shapes the code, not just the prose: - Steamworks requires in-app purchases to use the Steam Wallet (Microtransactions doc, quoted in commercial.md), so the monthly is *stated*, never *sold*, inside a Steam build; the lifetime is plain DLC bought on the store page and read with BIsDlcInstalled. - Entitlement is `steamDlcOwned || polarKeyValid`, both resolving to one OutputProfile. steam_api64.dll ships beside the exe and is a functional dependency, not the rejected "IP protection" native DLL — without it DLC ownership cannot be read. Launched without Steam, the app falls back to the free profile rather than refusing to start. - Signing is moot: Steam installs never meet SmartScreen. Artifact Signing stays on the shelf for the day a non-Steam download exists. Year-one fixed cost is ~$115. - The exe rename must land before the first depot upload — Steamworks warns about renaming a live exe. Distribution.md is rewritten to SteamPipe mechanics (publish output, depot, steamcmd, DLC wiring, clean-account purchase/refund test). product.md, decisions.md (a new "Steam exclusive" entry with every dead vector), TASKS.md (Steamworks entitlement row, Steam packaging row), ai.md, index.md and the TASK 48 banner follow. MyMistakes.md gains the Steam rules that cost real time today: installs skip SmartScreen, updates are free, the Wallet rule, and the recurring-billing caveat. |
||
|
|
57ef9e7a5e |
docs(commercial): the claim page was never needed, signing waits for revenue, and support is a filter
Four things derived this session superseded what the plan said twenty-four hours earlier,
and the docs are corrected where each one died:
**Licensing needs no server.** Polar's customer-portal endpoints authenticate on the key
itself plus the public organization_id; the org access token is only needed to create
products. Channel binding rides in `conditions: {channel_id}`, re-checked on every
validate — so the "claim page" (a hosted OAuth flow that wrote the UC id onto the key)
and the key database it implied are deleted. Device management, key rotation and
subscription auto-revocation are Polar's; four states, one lease, no callback endpoint.
**Signing is deferred until two subscribers.** It never removes the day-one SmartScreen
warning (EV's instant-bypass died in 2024), so buying it before revenue buys only the
publisher line. Renamed to Azure Artifact Signing, $9.99/mo Basic, and it requires a
paid Azure subscription. Year-one fixed cost drops from ~$135 to ~$15.
**The Store account is free.** Microsoft waived the $19 individual fee and removed the
$99 company fee in May 2026, which makes the Store the only zero-cost route to a
Microsoft-signed install — the free answer to SmartScreen, noted beside the paid one.
**Support is a filter, not a promise.** Email, best effort, no SLA, refunds on request
without argument; no training service, no helpdesk. A repeated question becomes a
training video on @llamacasty, which is content and monetization instead of a ticket.
GitHub issues on the releases repo are the deliberate friction.
Also settled: binaries live in a second Gitea repo (`llamacasty-releases`) push-mirrored
to a public GitHub repo — issues and assets public, code private, one publish direction
(a git mirror carries refs, not GitHub Release assets); `@llamacasty` is
standard-verified with intermediate features, so live streaming, >15 min uploads and
custom thumbnails work today and no SIM or ID is needed; and OAuth comes out of Testing
mode by publishing the app (100-user lifetime cap, no 7-day token death).
TASKS.md now opens with the four-day surge slice — artifact shape, output gate,
entitlement seam, real-machine day — because those are what the first public release
waits on. HANDOFF.md rewritten. MyMistakes.md gains the Polar recipe and the
"verify means three different things" entry. MARCOM.md gains the support→content loop
and the surface roles.
|
||
|
|
7827d239e2 |
docs(commercial): one source of truth for the money — prices, entitlement, channels
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. |
||
|
|
0498f7e130 |
legal: proprietary EULA + privacy policy; GPL terms retired
Licensing posture changes to closed-source commercial software (creator ruling 2026-10-30): free tier caps output at 720p30, a paid licence unlocks 1080p ($10/month or $400 lifetime). - LICENSE is now an End User License Agreement: grant, what you may do with your own content, no redistribution/reverse engineering, third-party components prevail, updates at publisher discretion, warranty and liability limits, termination, governing law. - ADDITIONAL-TERMS.md deleted — those were GPL §7 terms and have no meaning under the EULA. - THIRD-PARTY-NOTICES.txt keeps every third-party obligation and drops the GPL header and §6 source offer; it states plainly that LlamaCasty ships no source. - PRIVACY.md is new: exactly what is written locally (layout DB, DPAPI token, capped log, recordings you choose), what leaves the machine (Google's APIs only, with your account), and the standing promises (chat rendered never stored, no telemetry, no ad SDKs). - README, docs/ai/product.md, docs/ai/decisions.md and TASKS.md record the decision and the obligations that still bind (ffmpeg LGPL as a separate process, OFL fonts, CC0 icons, model provenance, notices in the About screen). Deliberately NOT decided here: key issuance/verification and the resolution gate itself. Earlier GPL builds remain GPL for the versions that shipped under it. |
||
|
|
5a102a4a37 |
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. |
||
|
|
769c616706 |
Preview: promote selected layer for hit testing
Temporarily raise the selected preview item and prioritize it in the custom hit test without changing scene order. Refresh integration-test expectations for Brand logo layers and explicit thumbnail assets. WPF references: https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.panel.zindex?view=windowsdesktop-10.0 https://learn.microsoft.com/en-us/dotnet/desktop/wpf/graphics-multimedia/hit-testing-in-the-visual-layer |
||
|
|
841c905c06 |
Brand logo: one locked image layer per scene above the background, image from App Settings
- Source.IsBrandLogo persisted (additive migration); layer healed on load, removal blocked - Image stored as an asset; settings keep the asset id and file name - App Settings: Brand Logo file field with Browse - Default placement top-left; per-scene eye uses the existing element visibility Test: BrandLogoTests; layout/background tests pass (34). Not pushed. |
||
|
|
b86b6bb601 |
Transitions: Wipe (left-to-right reveal), added to the App Settings picker
Test: WipeTransitionTests. Not pushed. |
||
|
|
f67a263ccd |
App Settings: transition type and length picker
Cut/Fade/Move and a 100-2000 ms length, bound to the view model; changes save with the layout. Test: TransitionPickerTests. Not pushed. |
||
|
|
90cd039d98 |
Transitions: fade starts from the outgoing picture (was an instant cut)
The transition started with no from-frame, so BlendFrame returned the new scene at once. Start now requests a capture and the frame pump supplies its last output on the next tick. Test: TransitionFadeTests. Docs: ai.md, TASKS.md. Not pushed. |
||
|
|
804b1d2f5f |
docs(memory): split the frame pipeline out of ai.md into docs/ai
ai.md is the mandatory read every session; at 2,702 lines / ~64k tokens it costs the whole subsystem history even when the task never touches it. This is a verbatim move, not a compression - nothing is deleted and each stub is written from the moved text. - docs/ai/frame-pipeline.md: 'Frame cadence', 'Per-element composite timing + scene dump', 'What the first instrumented take said', 'Render budget', 'Scene compositor' and 'Live frame pipeline', byte-identical, plus a short 'Related in ai.md' footer and two stale 'below' pointers corrected. - ai.md keeps two stubs carrying the invariants that must not be missed (overlay on every render path, VideoFrame.Epoch, StripPool threads, Release-only render numbers; arrival-stamped rawvideo, non-blocking submit, stop order, burned _outputIndex judge) and links the new file. - docs/ai/index.md + docs/index.md route the split; schema.md inventory lists the new tier. - 4 index-row references and 2 TASKS.md anchors repointed; ai.md's Services row too. Measured: ai.md 63.6k -> 47.1k tokens, 2,702 -> 1,975 lines. Progressive disclosure per schema.md convention 5. No source touched; no push. |
||
|
|
ef60f1f3b6 |
TASK 48: distribution & packaging is deferred until we are near v1
Creator ruling 2026-10-26: the remaining programming work is far from release and a complete UX redesign is about to start, so packaging, signing, installer and updater decisions would be chasing scope that is about to move. The task is deferred, not cancelled. Recorded what is frozen: TASK 48 items 6-8, the signing decision, the updater choice (Velopack's deletion left no update path, and StorePackagingTests currently vetoes a new one), and the route question itself - ai.md says the Store route is dead (direct + Steam) while TASKS.md/task-48/HANDOFF still read Store MSIX + Store IAP. That contradiction is to be settled in ONE sweep at the revisit, not patched piecemeal now. |
||
|
|
c768a8ee49 | fix(border-color): keep ComboBox popup clicks inside LeftPanel (TASK 63) | ||
|
|
daac7689fd |
TASK 48 slice 2: the app packs as a Microsoft Store MSIX
The manifest the Store route needs, and a real package built from it. Packaging/Package.appxmanifest is a full-trust packagedClassicApp: EntryPoint Windows.FullTrustApplication, uap10:TrustLevel=mediumIL, uap10:RuntimeBehavior=packagedClassicApp, the rescap runFullTrust capability, and webcam + microphone DeviceCapabilities. That combination is what lets the packaged app launch the bundled ffmpeg as a child process and write to the user's profile; an appContainer package would virtualise the recordings folder instead. x64 only, en-US only (policy 10.7). Packaging/Assets/ carries the four tiles the packer requires, generated from the app's own Assets/llamacasty-logo.jpg by scripts/make-store-logos.py - listing art and app art cannot drift. scripts/package-msix.sh publishes Release win-x64 self-contained into artifacts/msix-stage (a RID-less publish would copy every RID's ONNX native), stages the manifest as AppxManifest.xml + Assets, and runs the Windows SDK's makeappx. MSIX_PFX/MSIX_PFX_PASSWORD sign a package for a local test install; the Store signs submissions. artifacts/ is gitignored. First real package: artifacts/LlamaCasty.msix, 159 MB, 509 files, 'Package creation succeeded'. Schema traps found by packing (makeappx reports one error at a time): Square310x310Logo and Wide310x150Logo are NOT uap:VisualElements attributes - they live on uap:DefaultTile. Guard: StorePackagingTests.The_package_manifest_declares_full_trust_camera_and_ microphone - identity/architecture, the full-trust application element, the three capabilities, no appContainer VALUE (not the file text: the manifest's own comment says the word), en-US only, and every referenced logo present. Full suite 438/438, 0 warnings. Cold-start smoke test after the Velopack removal: MainWindow loaded, no exceptions. |
||
|
|
20cfadca0c |
TASK 48 slice 3: the self-updater is deleted - the Store updates the app
The creator's ruling (2026-09-27) is Store MSIX + Store IAP, so Microsoft delivers the updates and the in-app updater is gone rather than parked. This is a COMPLIANCE action, not cleanup: the Velopack bootstrap downloaded and executed a payload, which Store policy 10.2.2 does not permit. Three edit sites, as planned: the PackageReference in ytLive.csproj, 'using Velopack;' in App.xaml.cs, and the bootstrap block. The self-hosted DigitalOcean update-URL droplet dies with it. 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. The check matches CODE (a using directive, a type usage), not a bare word, so a file's prose may still name what it deliberately does not do. Smoke-tested after the removal: MainWindow loaded, 0 exceptions in startup.log. Full suite 437/437, 0 warnings. |
||
|
|
b1d6034662 |
TASK 55 slice 1: the startup log is size-capped
The creator (2026-10-09): 'we need some sort of log-rotation so that we don't inadvertently fill up a user's drive.' startup.log was uncapped - 23MB and growing every few seconds, because FfmpegEncoder logs every non-progress ffmpeg stderr line. The policy lives in a new Helpers/LogRotation.cs as a pure function over a path (RollIfNeeded(logPath, maxBytes, retainedFiles)), so AppLog's 156 call sites keep their surface and the rule is testable on its own. At 5MB the live file rolls to startup.1.log and one rolled copy is kept - the FILE SET is bounded, not just the live file, so a long stream cannot grow the log without limit. A failed rotation degrades to the old behaviour instead of taking the app down. Pattern reference (cited): Serilog's file sink pairs fileSizeLimitBytes (the cap) with retainedFileCountLimit (how many rolled files to keep) - https://github.com/serilog/serilog-sinks-file Guard: ytLive.Tests/AppLogRotationTests.cs - the ONE test drives the real AppLog.Write against a temp dir (two internal seams: PathOverride, MaxBytes) and asserts the roll, the fresh live file, and that after a SECOND roll the directory still holds exactly startup.log + startup.1.log. Also fixed a stale catalog row: TASK 12 (master limiter) shipped 2026-08-15 and was still marked 'Queued'. |
||
|
|
826cc17e47 |
TASK 56: the secret scan is done - and the history is clean
The release gate before the repo goes public. Scanned 432 reachable commits (all branches, 35 tags, the reflog, refs/stash) + the 63 dangling commits git fsck still holds + every tracked file in the worktree, for Google OAuth client secrets, API keys/tokens (Polar OAT, ghp_, AKIA, Slack, Stripe, OpenAI), private-key headers, assigned credentials, and any once-tracked CREDENTIALS.md / OAuthCredentials.cs / MARCOM.md. Report format is commit + path + rule only - no matched value ever reaches the screen. Result: 4 hits, ALL false positives - README.md:79 is the doc snippet's 'ClientSecret = your-client-secret' placeholder, TokenStoreTests.cs:22 is a 9-char test dummy. Nothing to rotate, no history rewrite. The three secret files were never tracked. Also recorded: the repo was ALREADY public when the scan ran (anonymous GET -> 200, API private=false), so the gate came late - but nothing leaked. The creator still owns the keep-it-public decision. Method + verdict in TASKS/task-56-secret-scan.md; the reusable recipe in MyMistakes.md -> 2026-10-22. |
||
|
|
987fddac24 |
TASK 63: the border-colour picker IS the app's own drop-down
The creator (2026-10-21), on Live -> Layers -> webcam -> Properties:
"why does the drop down under the web cam properties not show the same style for
the border color drop down list box?"
"one of the first things we did was define branding. We established look-and-feel
standards. And yet you keep ignoring these and I'm wondering why"
WHY IT WAS MISSED - the answer the creator is owed.
The TASK 58 sweep had already walked the whole app for un-themed controls and the
border picker STILL was not themed, because the sweep looked for control TYPES
(ComboBox / CheckBox / TextBox) carrying no theme style - and the border picker was
NONE of those. It was a "..." Button whose Click handler built a ContextMenu of
swatches in code. It was never in the sweep's scope, and PropertiesStyleTests.cs
(a type-based guard) could not see it either. A picker that LOOKS like a drop-down
is invisible to a type-based audit; the guard has to ask "is this the app's widget?".
WHAT CHANGED
- The picker is now the app's YtComboBox - the widget the Display and Font pickers
wear - bound to the palette (SelectedElement.BorderColorOptions) with its selection
bound by VALUE (SelectedItem -> BorderColorEntry), because WPF's SelectedValue
matching is ordinal and the palette mixes #FFFFFF and #e94560.
- The closed box shows the swatch + the colour's NAME; the rows show swatch + hex +
name (the rows the creator approved in TASK 60). A ComboBox cannot do that with one
ItemTemplate - WPF copies it into the read-only SelectionBoxItemTemplate - so
Helpers/ClosedBoxTemplateSelector.cs picks by the ContentPresenter's templated
parent: the ComboBox for the closed box, the ComboBoxItem for a row. Verified
against WPF with a throwaway probe, not assumed.
- The swatch is painted by the THEME (Helpers/HexToBrushConverter.cs, registered as
HexToBrush) - the hand-painted SolidColorBrush(Color.FromRgb(...)) in the old
click handler was the exact anti-pattern the theme forbids. The swatch also carries
a 1px #2a3a5e border now, so black and dark swatches are visible on the dark menu
(the follow-up TASK 60 left open).
- The hex field stays (60px, editable): a colour outside the palette can still be
typed, and the drop-down then carries that hex as a NAMELESS entry ("no name" is the
honest answer). The palette's name moved out of TASK 62's TextBlock into the
drop-down's closed box.
- The hand-built menu, its ContextMenu/MenuItem/SolidColorBrush code and
WebcamBorderColorPicker_Click are gone.
MODEL: SceneElement.BorderColorName -> BorderColorEntry (the palette entry for the
current hex, with its name; setting it writes the hex back) + BorderColorOptions (the
palette plus the custom entry), both raised when BorderColor changes.
BorderColorPalette gains EntryFor(hex) and OptionsFor(hex); NameOf delegates.
Guards: ytLive.Tests/BorderColorPickerTests.cs (the ONE test for this change - the
model, the panel, and the REAL widget: with the app's theme style and the panel's own
templates the closed box names the colour and the rows show the hex AND the name).
The TASK 60 guard (BorderColorPaletteTests) was updated because the mechanism it
asserted - a code-built menu - no longer exists; it now guards the new wiring (the
drop-down is bound to the palette and neither the panel nor the code-behind carries a
second list of colours).
Full suite: 435/435 passed, 0 warnings.
|
||
|
|
ef52508b1e |
TASK 61 + 62: the geometry fields share one width, and the border field names its colour
The creator (2026-10-21), on Live -> Layers -> webcam -> Properties: "the Y and H boxes under webcam properties 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 providing the geometry values" "also the border color drop drown box content should also include the name of the color next to the hex value as they in the drop-down list box" TASK 61 - one fixed width for the four geometry fields. The row is a DockPanel, and WPF sizes an element to its CONTENT unless it is told otherwise, while the LAST child of a DockPanel fills the remaining space. So X/W (not last) were as wide as their text and Y/H (last) stretched across the row - two widths for the same kind of field. Now all four carry Width="44" and the rows set LastChildFill="False" (without it the last field ignores its own Width and fills anyway). 44px is the measured width of "-1920" in the app's font at 11px (the largest geometry value - the master frame is 1920x1080 - plus a sign) plus the field's padding and border. Docs: https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.dockpanel.lastchildfill The guard MEASURES the widest value with WPF's FormattedText instead of asserting a magic number, so a font change cannot silently make the field too narrow: https://learn.microsoft.com/en-us/dotnet/api/system.windows.media.formattedtext TASK 62 - the border field names its colour. SceneElement.BorderColorName carries the palette's name for the current colour ("Crimson") and is raised when the colour changes; it is EMPTY for a custom hex the creator typed, because "no name" is the honest answer. BorderColorPalette.NameOf changed to match (it used to fall back to the hex itself; the TASK 60 test assertion was updated with it). The hex field keeps a fixed width (60) so the name sits right next to the hex instead of drifting to the row's far edge - the same trap as TASK 61. Guards: ytLive.Tests/GeometryFieldWidthTests.cs, ytLive.Tests/BorderColorNameTests.cs Full suite: 435/435 passed, 0 warnings. |
||
|
|
6271bf34c9 |
TASK 60: the border-colour drop-down names every colour
The creator: "under Live->Layers->webcam->Properties->border color: in the drop down box, the #hex values are good, but add a corresponding color name/label next to the value." Each row now reads "#e94560 Crimson" - the hex stays exactly as it was, the colour's name goes next to it (dimmed #a0a0b0 so the hex reads first), and the tooltip becomes "Crimson (#e94560)". The 14 colours were a hard-coded string[] inside the click handler - a list of bare strings cannot carry a name, and a list hidden in UI code cannot be asserted on. They now live in Models/BorderColorPalette.cs: NamedColor(Hex, Name) + BorderColorPalette.All (the single source of truth) + NameOf(hex), which answers case-insensitively and falls back to the hex for a colour outside the palette. The picker builds its menu from the catalogue and no longer knows any colour by heart. The names are chosen not to collide - the palette has two greens, two blues and two warm colours: #00FF00 is Lime and #22c55e Emerald, #0000FF is Blue and #3b82f6 Azure, #FF8800 is Orange and #f59e0b Amber. Guard: ytLive.Tests/BorderColorPaletteTests.cs - every entry valid + named, no hex or name repeated, the 14 legacy colours all still offered, NameOf answers, and the old list is gone from the handler. 433/433 green, 0 warnings. |
||
|
|
89f6aa3a67 |
TASK 59: the properties panel is a hierarchy, and the shape is a choice
The creator's ruling on the webcam's properties: a "Shape" title above the
Mirror/Rect row; the drag hint indented under that row "to show parentage";
the same for Geometry, Border Color, Border Thickness; Remove and Blur indented
under Background; Strength indented under Blur.
One rule covers it: a title owns its content and the content is indented one
level (10px) under it - two levels where a title is itself nested (Strength
under Blur under Background). The titles are SemiBold 11px #a0a0b0 like the
ones already there.
The shape also stops being a toggle button that shows the NEXT action ("Round"
when the camera was a rectangle) and becomes a CHOICE: two segment options,
Rect | Circle, with Rect selected by default - which is what the creator asked
for ("Rect should be selected as a default if none exists"). The old
ShapeButtonText is gone.
The mutual exclusion lives in the MODEL: IsRectShape/IsRoundShape on
SceneElement, each setter flipping the shape through ToggleClipShape() (so the
pre-round rect is still stored/restored) and always re-raising, so a refused
write - the vertical Chat layout is rect-only (TASK 57) - makes the UI snap
back instead of sticking. Deliberately NOT a WPF RadioButton group: a group
unchecks its siblings by setting IsChecked in code, which makes WPF drop the
binding (stackoverflow 7376373; the same trap measured in 50980687/57275046).
The Circle option stays hidden where CanToggleClipShape is false, so the circle
is still not offered in the vertical Chat layout.
Guard: ytLive.Tests/PropertiesHierarchyTests.cs - the model half (Rect by
default, pick circle, pick rect, the locked Chat layout refuses the circle) and
the panel half (the Shape title owns both options, the circle is the gated one,
the hint is indented and follows the row, every titled block's content is at
10px, Strength at 20px). 432/432 green, 0 warnings.
|
||
|
|
2273d20160 |
TASK 58: the properties panel wears the theme (no more light-system chrome)
The creator asked why the Stream Alerts "Display" drop-down was not in the same
style as the other drop-downs. It was never given the theme's YtComboBox style -
only a hand-painted Background/Foreground. Setting colours changes the paint, not
the template, so WPF still drew the light system ComboBox (grey border, system
arrow, white popup list) on top of the dark panel. The chat Font drop-down was a
copy of the same mistake, and every check box was the light system one.
The sweep of the whole properties panel (then the app):
- ComboBox x2 (chat Font, alert Display) -> YtComboBox, hand-painted colours gone
- TextBox x16 in the properties panel -> YtTextBoxCompact (new, BasedOn YtTextBox:
11px / padding 4,2 - the panel is dense, the full 13px box would double it)
- CheckBox x2 (chat) + x3 (preview broadcast) + x1 (socials dialog) -> YtCheckBox
(new: a 14x14 rounded box + tick path, accent fill when checked)
- the layer-rename field -> YtTextBoxCompact via BasedOn, still borderless
- buttons, pills, sliders: already themed, untouched
The sweep also killed a typo'd foreground (#e0d0e0 for #e0e0e0) and the only
remaining hand-painted colour is the deliberate dimming of the two read-only path
fields.
Guard: ytLive.Tests/PropertiesStyleTests.cs (the ONE Good Dog test) parses the app
XAML and fails if an unstyled drop-down, check box or properties field comes back.
431/431 green, 0 warnings.
|
||
|
|
3a77662a95 |
vv webcam Unit 2: the Chat scene in the vertical tier - rect only, tile or one whole 607x540 area
The creator's locked table (2026-10-17) gives the Chat scene its own rules in the 9:16 tier: RECT ONLY - the circle option is not offered - and a size that is either the normal small tile or ENLARGED to fill one whole 607x540 area (top or bottom). Unit 1 shipped the per-scene placement; this unit is the Chat scene's own variant of it. The rule lives on the MODEL, not on the button: - Models/SceneElement.cs: ClipShape and ToggleClipShape() are now virtual. - Models/WebcamSceneConfig.cs: VerticalShapeLocked (set per scene by the VM) OVERRIDES the ClipShape property, so no writer can slip a circle in - the toggle, the Shape button and the placement swap all go through it. The lock is cleared BEFORE the 16:9 layout comes back, so the desktop shape returns. - ViewModels/MainViewModel.Webcam.cs: CanToggleClipShape (the circle is NOT offered in the vertical Chat layout - the Shape button disappears, it is not merely greyed out), IsChatSceneCamera, ResizeWebcamInVerticalTier. - Models/VerticalCameraLayout.cs: FillsArea (the "enlarged" state, derived - no new column) and SizeFor - the Chat camera's TWO sizes, the tile or the whole area, whichever is nearer the requested width, so the resize handle has two detents instead of any size in between. - An enlarged camera FILLS the area its centre is in (top or bottom), so dragging it up/down swaps the areas. - 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. - Controls/PreviewPane.xaml.cs: the resize routes through the VM; the drag assigns, lets the model clamp into the band, then snaps (so the enlarged camera can be dragged from the top area to the bottom one). Test: ytLive.Tests/VerticalChatCameraLayoutTests.cs - the ONE Good Dog test for the unit: the vertical tier is picked -> the Chat camera comes into the band as a small RECT tile -> the circle option is not offered and cannot be set even on the model -> dragged up and enlarged it fills the TOP area exactly -> back to 16:9 the desktop layout is restored exactly and the circle option is offered again. 430/430 tests pass, 0 warnings. Research (the creator's table is the established practice, not our invention): - Streamlabs' vertical guide: "splitting the screen in half horizontally, placing the camera feed in the top half and the main content on the bottom (or vice versa) may be the bet", and a just-chatting vertical wants the camera "to take up most of the screen" (streamlabs.com/content-hub/post/overlays-vertical-and-horizontal-streaming) - Bloopbot's vertical overlay docs describe the same per-layer model we use at scene granularity: "move or resize a layer on Vertical and it stops being Auto-placed. It keeps the place you gave it, and the horizontal layout is not touched" (bloopbot.com/docs/vertical-overlays-twitch-dual-format) - our swap-at-tier-change, stored per scene. - Landmine recorded: "about the bottom 560 px of the vertical video can be covered by Twitch's chat and player controls on phones" (Bloopbot) - the Chat camera's enlarged BOTTOM area may be half-hidden by the platform chrome; TASK 53's safe-zone work decides that. Memory in the same commit: ai.md (the Chat rules, the shape lock, the tests), TASKS.md + TASKS/task-57-vertical-camera-layout.md (TASK 57 complete), HANDOFF.md, MyMistakes.md (3 recipes: lock the model property not the UI button; two detents for a discrete size set; the edit tool cannot match an oldString with an em dash), Models/ViewModels/Controls index rows. |
||
|
|
6be062c45f |
vv webcam Unit 1: the vertical (9:16) tier keeps its OWN camera placement per scene
The vertical tier is a centred band crop of the SAME 1920x1080 master (band x
656.25, w 607.5, h 1080), so the 16:9 camera placement (x~1408) falls outside the
cropped window and the creator is simply not in the vertical stream. Each scene
now keeps its own vertical position, size and shape; switching back to 16:9
restores the desktop layout exactly (the creator's locked table, 2026-10-17).
Geometry only - NO render-path change. The preview and the compositor both read
the element's own X/Y/W/H + ClipShape, so the tier switch swaps the LIVE geometry:
- Models/VerticalCameraLayout.cs (new): the pure math - the band, its two 607x540
areas, the six spots (top/bottom x left/centre/right), the 32px inset and the
270x152 default tile (480x270 in the 1080x1920 output) in the bottom-right spot,
the corner the 16:9 default webcam uses.
- Models/WebcamSceneConfig.cs: the vertical placement lives in seven additive
columns (VerticalX/Y/Width/Height/ClipShape/RectWidth/RectHeight; 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; Begin/EndVerticalLayout + CommitLiveLayoutToVertical do the swap. The
X/Y/W/H overrides clamp every write into the band - outside it the camera is
invisible in the vertical output.
- Models/SceneElement.cs: X/Y/Width/Height became virtual so the derived element
constrains every writer (drag, resize handle, typed fields) in ONE choke point
instead of three writers each re-implementing the rule.
- ViewModels/MainViewModel.Webcam.cs + .Streaming.cs: IsVerticalTier/VerticalBand,
ResolveVerticalCameraLayout(s) (the tier swap), MoveWebcamInVerticalTier /
SnapWebcamInVerticalTier (the six-spot snap, by the tile's centre),
EnsureVerticalCameraLayout; ApplyStreamQuality is the tier-change hook. The
Live scene gets its small tile automatically on the first vertical switch (the
vertical stream would otherwise have no camera at all); the other scenes keep
their 16:9 placement until the creator drags them into the band.
- Controls/PreviewPane.xaml.cs: the drag/resize route through the VM snap under
the vertical tier instead of free-floating over the master.
Controls/LeftPanel.xaml: the webcam properties gained a Geometry section
(X/Y/W/H) for the manual entry the creator asked for, 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).
- The store: seven guarded ADD COLUMNs, the INSERT/SELECT extended, and the
X/Y/W/H/ClipShape columns stay the 16:9 layout - while the vertical tier is
live the save writes the parked values there and the live ones into Vertical*.
Test: ytLive.Tests/VerticalWebcamLayoutTests.cs - the ONE Good Dog test. The
vertical tier brings the Live camera INTO the band as a small tile; dragging it to
the top-right lands it top-right inside the band; back to 16:9 the desktop layout
is restored exactly (1408/778/480x270); both placements survive a save + reload.
Full suite 429/429, 0 warnings. LayerReorderPersistenceTests.RealMouseDrag fails
ALONE on a clean tree in this environment - pre-existing flake, proven by stashing
this work, not a regression.
Derivative work: OBS has no built-in per-aspect-ratio layout storage - the
maintainers' answer is "create a second scene and add the scene you want to send
as source" (obsproject.com/forum/threads/161307) - and Streamyard's dual-aspect
guide says the same ("no built-in way to store separate layouts per aspect ratio",
so you rely on manual workarounds and plugins):
https://streamyard.com/blog/dual-aspect-ratio-streaming-tool
Our per-scene, per-tier stored placement, resolved on the tier change, is the
step past both.
Memory in the same commit: ai.md (new section), TASKS.md + TASKS/task-57-*.md
(Unit 1 shipped, Unit 2 = the Chat scene still open), HANDOFF.md, MyMistakes.md
(the per-tier geometry-swap recipe, the binding-text panel trap, the store's
webcam-identity rule) and the Models/Services/ViewModels/Controls index rows.
|
||
|
|
cdf82f688d |
Read the first instrumented take: the judder did not reproduce, no element is slow
The measurement unit existed to name which of the six scene elements eats ~140ms per composite. The creator's Release recording (ty-20261016-1841-559-0002.mp4, 174.5s, 1920x1080@60) names none of them — the whole composite costs ~1ms: composites 301 per 5s = 60/s, every slot, 0 dropped, 0 stalls after warm-up avg render 1.0-1.1ms (worst 2.5ms) = ~7% of the 16.67ms budget parts "#5 HD Pro Webcam C920 0.4ms, #5 ... raster 0.2ms, backdrop 0.2ms" - the other four elements never reach the 0.1ms print floor C4 cache 151R/150H: half the ticks were a whole-frame BlockCopy webcam 30.0/s at the resolver, longest gap 50.0ms (one 60Hz frame of sampling phase, not a drop) the FILE 29.6 distinct webcam images/s, 82% of images held exactly 2 frames, longest hold 267ms (the healthy 30fps-in-60fps signature) The two stalls are not composite cost: 124ms on the very first render (warm-up) and 53ms entering the "Chat" scene (a rebake). So the historic slow numbers are not a slow scene but different configurations: - Oct 9 19:48 (only bad take still on disk) was the VERTICAL tier: "FramePump started (1080x1920 @ 60 fps) scene='Live' comp=608x1080" - the 607x1080 centre crop upscaled to 2M px, 92 renders/5s, avg render 45.0ms, worst 278ms. - Oct 5 12:01 (141ms) logged no scene=/comp= at all - the suffix did not exist yet, so the map's "21:9 -> 1920x803" explanation stays a hypothesis, not a reading. - Every slow window shows "cache xR/0H" (the whole-frame cache never reusing) while this take shows 151R/150H, and one Oct 5 take latches mid-take (1.5ms -> 3.0ms -> 45.2ms in 15s, then stays). That latch is the lead for the next take; it is a hypothesis and the parts line is what will decide it. Corrections to the map (same commit, stale facts replaced not appended): - "It was never Debug-vs-Release" is WITHDRAWN: same 1920x1080 tier, Oct 9 = 10.4ms vs this Release take's 1.0ms, and Build <hash> carries no configuration - every pre-2026-10-16 render number is config-unknown. - The "same scene shape" 10:01/12:01 comparison is superseded: it was an eye comparison of takes whose geometry was never logged. New: scripts/webcam-cadence.py - the ground-truth probe for what the FILE holds, beside what the pump says it did. It took three wrong readings to get right (H.264 re-encoding defeats exact-pixel comparison; a round-clip crop's corners leak 60fps background; a fixed crop across a scene switch reads background), all recorded in MyMistakes.md as a recipe, including an ffmpeg-native cross-check (tblend=difference + signalstats.YAVG - the same metric, the technique already in this repo's A/V-sync recipe) which agreed: 52.2% duplicates / 28.6 distinct/s vs 50.7% / 29.6/s on different windows of the same take. Next: no fix is allowed yet - the creator makes a Release take in the failing geometry (vertical 1080p60 tier first), and that take's parts line names the stage. Docs in the same commit: ai.md (new "What the first instrumented take said" section, the two corrections, the parts-line denominators, the tool in the inventory), TASKS.md (reading outcome), HANDOFF.md (rewritten), MyMistakes.md (recipe). Gates: clean build 0 warnings, full suite 428/428, scope check passed. |
||
|
|
a3a4681aac |
Per-element composite timing + scene dump (measurement only)
The cadence work ended with a question the log could not answer: the compositor
manages 6.8 composites/s at ~141ms while the 10:03 take did 29-34ms on the "same
scene shape" (full-render split=0 elements=6 dynamic=5). Nothing in the stats line
said WHICH of the six elements cost what, and nothing recorded the scene itself, so
the two takes could only be compared by eye. This closes that gap and nothing else:
no fix is attempted until a real take names the culprit.
- Services/Compositor/CompositeTimings.cs (new): per-window collector.
Add(stage, ticks) accumulates; Describe(renders, minMs = 0.1) prints the averaged
breakdown slowest-first, dropping sub-0.1ms noise.
- SceneCompositor.Render / CompositeLayers / BakeStaticBase / BlitBakedElement /
BlitCachedLayer + SceneGraph.GetBakedBase: trailing optional CompositeTimings?
Every stage is timed (backdrop, base, flash, social, ticker, zoom, scale, overlay,
bake backdrop, bake overlays) and EVERY element gets its own entry keyed by
z-index + name ("#2 Webcam"), with the nested "raster" sub-stage recording the
paste-cache rasterize miss - the split between "re-samples every tick" and "slow
paste" without a second session. Bake-pass elements are prefixed "bake #..." so a
one-off rebake cannot masquerade as a per-composite cost. Callers that pass
nothing (thumbnails, existing tests) are unaffected and pay nothing.
- FramePump: owns the collector, threads it through RenderScene/RenderFull/
GetBakedBase/CompositeLayers, and logs "FramePump parts (N renders/5s): ..." once
per 5s window beside the stats line (averaged per render, so it compares directly
with "avg render"), then resets it. Also dumps the scene - "FramePump scene dump:"
header plus one line per element (z-index, name, kind, source type, visibility,
rect, opacity, clip) - at pump start and on every scene change.
- One Good Dog integration test (ytLive.Tests/CompositeTimingTests.cs): a real
composite of a full-canvas element plus a 64x64 one through the real collector
must rank the full-canvas element first, carry the raster sub-stage and the fixed
stages, and the dump must name both elements with z-index/type/rect and echo the
split point.
Derivative work (AGENTS.md): OBS's answer to "which source is slow" is the Source
Profiler - average execution time of each source's tick and render functions
(https://docs.obsproject.com/reference-libobs-util-source-profiler, util/profiler.h).
Same shape here: one timer per element per composite, reported as an average, plus
the stage split this case needs.
Docs in the same commit: ai.md (new subsection "Per-element composite timing + scene
dump", the closed gap corrected in "Frame cadence"), Services/index.md (new file row
+ FramePump/SceneCompositor rows), TASKS.md (unit marked shipped, next = creator
take), HANDOFF.md (rewritten: unpushed commit, next step, verify.sh whitelist hole).
Also recorded this session (from the creator): the launch command is
`dotnet run -c Release` - Release is the DEFAULT, Debug only when explicitly named,
and perf takes must read Release. ai.md -> Run updated.
Full suite 428 tests, 0 warnings; the only failures are the known audio env flakes
(both pass in isolation). Scope check passed.
|
||
|
|
c2d64dcd6d | TASK 51 blur 'heavy' fix: quadratic blur radius. BlurRadius(strength) = 1 + 5s^2, MaxBlurRadius 12 -> 6 downscaled px (~24 full px max). Default 0.5 is now radius 2 small px (was the old linear 6) - ~3x lighter perceived spread, matching the creator's real-camera finding. Range from the established tools: StreamFX defaults its Box blur Size to 5 and its author stops downscale-based blurs around 8px. One integration test pins the mapping (0 -> 1, 0.5 -> 2, 1 -> 6) and step-edge behaviour (light reaches 9px at default vs 26px at full); the existing slice-2 blur test passes unchanged. 419/419 pass. Refs: https://github.com/Vhonowslend/StreamFX-Public/wiki/Filter-Blur https://xaymar.com/articles/2020/04/26/optimizing-streamfx-in-your-obs-scenes/ (Unit: bg-blur heavy fix) | ||
|
|
305706956e | TASK 51 jitter fix: temporal mask smoothing. MaskSmoother blends each raw segmentation 0.75xnew + 0.25xprevious at the one CameraManager worker (state per CameraSession, not in the shared segmenter); null resets history, first frame passes through, input array never aliased, ping-pong buffers (no LOH alloc for a 147KB 256x144 mask). Creator's real-camera check: Remove was 'jittery' (per-frame independent segmentation). Weights from the OBS/Jitsi consensus. One integration test asserts the second frame keeps partial alpha (x=6 -> 58) where the raw mask cuts to 0. 416 pass / 2 known env flakes. Refs: https://deepwiki.com/royshil/obs-backgroundremoval/2-core-features https://github.com/jitsi/jitsi-meet/issues/17080 (Unit: bg-removal jitter fix) | ||
|
|
352f117cb2 | TASK 51 slice 5: webcam Background section in the Layers properties — Remove/Blur PillToggles (radio-exclusive) + Strength slider; strength changes never blank the webcam (new variant inherits the same-effect frame + bitmap). TASK 51 built (Unit: bg-effects UI) | ||
|
|
fbeb63ba79 | Docs: queue TASK 56 — pre-publication secret scan of the full git history (gate before the repo goes public) (Unit: task catalog) | ||
|
|
9f3c154d84 |
TASK 51 slice 1: background-effects engine — bundled MediaPipe Selfie Segmenter (ONNX) + Remove (Unit: bg-effects engine)
- Model: Google's official MediaPipe Selfie Segmenter (landscape, Apache-2.0 per model card), converted to ONNX by tools/selfie-seg/convert_tflite_to_onnx.py (custom op Convolution2DTransposeBias -> ConvTranspose) and verified against MediaPipe's own runtime (verify_against_mediapipe.py: max |diff| 5e-5, 100% binary agreement). Embedded resource, 430 KB. - Services/BackgroundEffects: IPersonSegmenter seam, OnnxPersonSegmenter (onnxruntime 1.31.0 CPU, never throws), BackgroundEffectProcessor (None passthrough, Remove = alpha mask). Not wired yet. - Tests: Remove integration test (fake segmenter) + bundled-model contract test vs MediaPipe reference mask. Full suite 407/408 — the one failure is the documented interactive-desktop flake RealMouseDrag_OnTheLayerList_PersistsTheReorder (passed in the run before; code path untouched). - Docs: ai.md reversal + section + licensing guard, THIRD-PARTY-NOTICES 11-12, TASK 51, indexes, MyMistakes recipe, HANDOFF. References: obs-backgroundremoval (approach only — ONNX Runtime on CPU, 2 threads; GPL-3.0, no code used) https://github.com/royshil/obs-backgroundremoval ; MediaPipe image segmenter https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter |
||
|
|
f8dd1079f3 | Docs: queue TASK 55 — disk hygiene (log rotation, temp alert-clip cleanup, low-disk recording guard) (Unit: task catalog) | ||
|
|
9fb628b473 | Docs: record Shorts/TikTok output-format verification (TASK 53) + free-forever/merch round (TASK 54); mark TASK 10 superseded (Unit: task catalog) | ||
|
|
fe80e8c994 |
Docs: record the zoom box as TASK 52 and fix the task-numbering drift (Unit: zoom docs)
The zoom feature shipped without a task record while its index rows carried the "TASK 51" label — which belongs to the webcam background effects (Remove + Blur). For an honest catalogue and handoff: - TASKS/task-52-zoom-box.md: feature provenance (creator-requested), the three 2026-10-08 corrections (loupe -> anchored capture over the full composite drawn last -> never show the dim shading), shapes, implementation map, tests, DONE. - TASKS.md: row 52 added (shipped); row 51 reworded — unblocked now that the zoom build shipped. - TASK 51 -> TASK 52 in Models/index.md, ViewModels/index.md, and the two code comments that pointed at the zoom as "TASK 51". - HANDOFF.md: pre-push readiness notes (5 commits ahead, push-safe). |
||
|
|
4b3d82ff0f |
Zoom: compose the box as a real 2x layer above the background (Unit: zoom layer + magnification proof)
Right-click now creates a transient ZoomSceneElement at z-index 1 whose pixels are a genuine 2x magnified sample of the backdrop (live capture under art), composited into stream/recording like any layer while the output crop stays the tier. All shapes are centered on the click and clamped to frame; healed on load/scene-create, never persisted, hidden from the layers list + element template (IsZoomLayer). BuildCompositorOptions is now internal = always tier crop (test seam). ZoomFrameBuilder is a pure static sampler (box/2 window centered on the box center, single-slot cache) proven by a banded-backdrop test; LayerReorderPersistenceTests drag resolves rows by item since the collapsed zoom row shifts container indices. Also books TASK 51 (webcam background Remove + Blur) and syncs memory docs (HANDOFF, MyMistakes, indexes). Full suite: 405 passed, 0 warnings. |
||
|
|
9d04aea55b |
UNIT 0 reading: the camera was never the problem, so Unit 1 is the wrong lever
Take ty-20261005-1201-556-0001.mp4 (75.3s) plus its log settle it. The C920 delivers 30.0/s with ZERO gaps over 50ms and its own device stamps agree - camera, USB and driver are clean. The pump managed 34 composites per 5s at ~141ms each, and the file holds 6.8 distinct webcam images per second at 88.7% duplicates with no arrival hitch whatsoever. The judder is us holding one face for 140ms and padding the rest of a 60fps container, which is precisely what a lipreader experiences as "cutting out". So 720p is a SHARPNESS win, not a smoothness win: it triples the bytes the compositor stretches per render on a path already 4.4x over budget. The exclusive camera lock stays agreed and stays queued, but it moves after the compositor work or it makes the judder measurably worse. The number still unexplained: the 10:01 take hit 25.6 images/s and this one 6.8, with the same scene shape (full-render split=0 elements=6 dynamic=5) and the same 640x480 camera - composites went 29-34ms to 143-155ms between sessions and nothing logged says why. There is no per-element composite breakdown and no scene dump at pump start, so the next unit is measurement, not a fix. Recorded the pump-counter overcount (13/s vs 6.8/s: one 141ms composite lets ~4 camera frames arrive, and the counter stamps the first epoch the resolver sees, not the frame actually drawn) so it is not re-derived as a bug later. |
||
|
|
55ca1029a3 |
UNIT 0: make a dropped webcam frame measurable, not just inferable
The creator is DHH and lipreads their own recordings, so a webcam that judders is an accessibility defect, not a polish item. Take ty-20261005-1001-821-0001.mp4 holds 25.6 distinct webcam images/s against the camera's 30fps with 281 gaps of 50-100ms - roughly one hitch in five - in a file that muxes flawlessly. Nothing in the app could see that: rawvideo carries no per-frame timestamps (ffmpeg stamps by ARRIVAL at the declared 60fps), and the stats line divided every average by the number of frames SUBMITTED, which deadline padding always fills to the full target. So the pump reported "300/300 frames, avg render 12.4ms" while producing 60 distinct composites. Helpers/FrameCadence.cs is one pure counter fed from three points: MediaCaptureFrameSource logs the device's own SystemRelativeTime stamps against our arrival times (TryAcquireLatestFrame keeps only the LATEST frame, so falling behind is silent by design), and FramePump logs the distinct webcam Epoch values that reached a composite. Read together they name the culprit - device/USB, our reader, or our compositor - instead of guessing. Render/resolve/wait now divide by the real render count, and "frames per 5s from R renders" is the duplicate-on-lag ratio stated honestly. An out-of-order timestamp counts as a frame but produces no gap and does not become the next interval's reference, so one bad stamp cannot invent a multi-second hitch twice. Also corrects a stale ai.md line (the "webcam renders at ~18fps" figure came from a different session and counted every newly rendered layer) and re-records the background-removal ruling as permanently out. Records the agreed-but-unbuilt Units 1-2, the no-fallback-camera ruling, and the SetFormatAsync dead end found by probing the 19041 projection. Suite 402/402, 0 warnings. The one red, LayerReorderPersistenceTests' RealMouseDrag, fails identically on a stashed clean tree - it needs an interactive desktop session this shell lacks. |
||
|
|
0fb2f9c6c3 |
docs: Go Live IS private (verified), and v1 is where that guarantee ends
The creator's standing doubt: "I wasn't sure that the go live video would be
private." Answered by tracing every path that can touch privacy, not by
reassuring:
CreateBroadcast -> privacyStatus = "private" hardcoded
GoLiveViewModel -> Visibility => "Private" hardcoded
BeginTestStream -> StreamVisibility = "Private" hardcoded
UpdateBroadcast -> the only code that can change it — NO CALLER
UpdateBroadcast's sole caller in the repo is BroadcastPullOutTests.cs:120. It
is dead code. So there is no path today by which a broadcast this app creates
becomes non-private, and the "do not populate my channel with videos showing
the app at its worst" concern is fully met. Verified, not assumed.
Which makes the safety accidental rather than structural, and that is the part
worth writing down. LiveBroadcastFormViewModel still offers
VisibilityOptions {private, unlisted, public}, and UpdateBroadcast would honour
it. So the v1 visibility option is precisely the change that first wires that
method up — v1 is the moment private stops being guaranteed and becomes a
user-selectable default. Intended at v1, ruinous before, and "it's dead code,
let me call it" is not a valid reason to do it early.
Also recorded, having been absent from the map entirely: the TEST button is
hidden at v1. Its real job is being the guaranteed-private path (hence the
hardcoded Private at BeginTestStream:105), which the existing "dry-run"
comment on ShowTestButton does not capture — that framing is now known to be
incomplete, and must not be used to re-derive why the button persists.
Corrects a fact I got wrong one commit ago: the method is UpdateBroadcast, not
"UpdateBroadcastMetadataAsync", and I described it as an available retrofit
path when it is in fact unreachable in production.
Open, deliberately, for v1 rather than now: whether a dry run is still wanted
on its own merits once Test is hidden.
|
||
|
|
74da99c988 |
docs: the always-Private lock is a decision, and it unlocks at v1
Asked twice about a decision that has been settled since 2026-09-01, so this is a documentation failure rather than a question worth answering a third time. The note was already there and it was accurate. It still didn't work, which is the whole lesson: every sentence in it was descriptive. It said why the lock exists and where the unlock lives, and not one sentence told the reader what to DO when it looks like a defect. I read it, recognised it, and asked anyway — because recognition is not instruction. It also pointed at TASK 36 for the unlock while the creator's actual answer is v1, so it was confidently wrong on top of being non-directive, which is worse than saying nothing. Rewritten as a prohibition carrying its own trigger: verdict in imperative voice, then the observations that make it look broken (privacy=private in the diag line, the PRIVATE badge, recordFromStart/DVR on, no delete call in the codebase) so recognition fires the instruction instead of prompting a question, then the retired delete question with its verified answer. Also: it belongs to v1, not TASK 36, and UpdateBroadcastMetadataAsync:112 is a retrofit path, not the product change. MyMistakes.md gets the transferable part, because the fix is not "write it down" — it is "write decisions as prohibitions, not rationale, and treat a repeated correction as a broken note rather than an impatient user". TASKS.md gets it under 1.0 gates so it is a tracked deliverable, not prose. |
||
|
|
f3fed0a2a8 |
docs: purge the dead $29/$49 + Polar model from every tracked file, add the Y1 forecast
The pricing ruling landed twice (2026-09-21 one-time, then 2026-09-27 Store IAP $10/mo + $400 lifetime) and the map never caught up. Six tracked files still told a future session that billing was Polar and the price was $29/$49. A stale memory map is worse than a stale comment: it is the file every session trusts, and this one was actively steering work toward a deleted provider. MONETIZATION.md (gitignored, so not in this commit) was rebuilt first: - "Revenue projections" now carries a modelled Year 1 forecast — pessimistic $4.0K / base $15.4K / optimistic $53.3K net, ~$22K weighted. Funnel is installs -> 35-45% activate -> 1.5-3% of active users pay. - Three findings recorded: Year 1 is ~97% lifetime transactions (subs net $392 of $15.4K, so MRR is not a Year-1 planning input); conversion dominates price (~1pp of conversion = ~$7K net, and ADA 6(h) locks $10 anyway); subscriber churn moves Year 1 by under $200 and is a Year-2 problem. - Break-even restated for $400: $25K net = 74 lifetime buyers, not 862. The price cut the volume requirement ~10x and raised the skill requirement to match. - Market sizing willingness-to-pay row rewritten. The 100K-400K TAM was price-bound to $29-49 and is wrong at $400; revised ~20K-60K, flagged as an estimate rather than a sourced number. - Flagged a real contradiction: $500K ARR needs ~4,900 concurrent subscribers, so the Exit section's ARR multiples do not describe what Years 1-3 produce. The defensible asset there is lifetime cash flow, not ARR. Tracked files corrected to match: - ai.md: paid-delta, pricing and billing bullets rewritten; Polar product id, org id, LCYT- key prefix and LLAMAFOUNDER/LLAMA50 marked as deleted; 15% share + net-after-tax recorded with the ADA 6(b)(i) citation. - TASKS.md: catalog row retitled as a teardown; the "still to verify — the price is not decided" note closed (it contradicted the ruling 20 lines below it); the OverlayHost "Polar unlocks alerts" open question marked resolved; Polar file-hosting deliverable removed from the release build. - task-10: reframed as a teardown. Steps 4-5 marked as surviving, 1-2-3-6-7 as deleting, 8 as a compliance action (Velopack downloads AND executes, which Store policy 10.2.2 does not permit). - task-36 item 5 (license-expiry reminders) struck — no key, no expiry, no lapse under Store IAP, so there is nothing to remind anyone about. - Distribution.md 4.2 / 4.4 / 4.5 marked obsolete; 4.5's "already implemented" corrected, since PolarLicenseService is being deleted rather than shipped. - README: "one-time license" -> "paid tier". Historical Polar sections are retained deliberately — the reasoning is reusable (merchant-of-record, fee comparison), only the conclusions are dead. Each is now labelled so it cannot be mistaken for the plan. No code touched. |
||
|
|
3637950df5 | task 50: one-button publish the local recording (creator ruling 2026-09-28) | ||
|
|
4fcf76289c |
TASK 48 item 1: bundle ffmpeg — the app no longer downloads an executable
Store policy 10.2.2 forbids dynamic code inclusion, and this was the only third-party binary the app ever downloaded and ran. It had already failed in production: the pin aged out of BtbN's 14-day retention and the download 404'd on the creator's first real recording attempt (2026-09-01). Bundling kills that failure class permanently and deletes the startup network dependency. - scripts/fetch-ffmpeg.sh populates a gitignored ./ffmpeg from a pinned month-end BtbN LGPL-shared artifact, SHA-256 verified before extraction, idempotent, FFMPEG_ZIP= override. 135 MB of DLLs in git would tax every commit on this 9p tree; the URL + hash are the audit trail LGPL §6 wants. - ytLive.csproj copies ffmpeg\** to output and publish (Always), and a GuardBundledFfmpeg target fails the BUILD if the folder is missing, so a fresh clone finds out before a take rather than during one. - FfmpegLocator loses the downloader, the zip extraction and the %APPDATA%\ytLlive\tools cache. Order is now bundle -> PATH, deliberately inverted: a PATH ffmpeg may be a different major version or a GPL build, and the bundle is what was pinned, license-vetted and tested. A missing bundle throws loudly; the old "found something on disk" path could satisfy a missing bundle with a stale copy, and silent drift is the worst failure mode for a dependency certification inspects. - 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 hash. OpenH264 made precise: compiled INTO avcodec-63.dll, no separate binary. Checked whether other third-party binaries needed bundling: no. e_sqlite3.dll and WebView2Loader.dll already ship via NuGet into runtimes\win-x64\native\, OpenH264 is inside avcodec-63.dll, the rest are managed. Two gaps remain that bundling cannot fix, recorded in the map: the WebView2 Evergreen runtime is a machine prerequisite (TASK 48 item 2), and Velopack's updater downloads and executes, so its teardown is a compliance action. Tests: 16 locator facts, suite 379/379, 0 warnings. The integration test runs the bundled ffmpeg -version, which is the only assertion that proves the sibling libav*.dll resolved — presence checks pass with the DLLs missing and a shared build then fails to start. TheLocatorCarriesNoNetworkCapability keeps 10.2.2 from rotting back. Analysis: TASKS/research-store-certification.md §4. |
||
|
|
e7cded6879 |
docs: pricing ruled — $10/mo + $400 lifetime, and the 15% fee is verified for subscriptions
Reconcile the pending doc edits with the creator's actual ruling, and close the revenue-share question that was blocking the monthly price. Verified from the App Developer Agreement v8.10 PDF itself (downloaded and text-extracted, not a search excerpt). Section 6(b) has only three tiers: 6(b)(i) 15% for Apps and their In-App Products *not listed in* 6(b)(iii); 6(b)(ii) 12% Games-only; 6(b)(iii) 30% for Xbox console apps/games, Xbox non-subscription IAP, and Windows 8/Phone 8. LlamaCasty is a Windows PC App, so 6(b)(i) governs BOTH tiers -- there is no subscription surcharge. The agreement's own changelog (v8.0, Oct 26 2017) states it outright: "implement the 85/15 revenue share for non-Game subscriptions." => $10/mo nets $8.50 (~$102/yr); $400 lifetime nets $340. Also confirmed: the 15% applies after VAT/GST (Net Receipts definition), payouts are monthly above a $50 threshold, and no better small-indie rate exists in the standard terms. Creator ruling recorded: $10/mo subscription or $400 lifetime, no annual, no .99. This supersedes the 2026-09-21 one-time $29 -> $49 model. Two obligations ADA 6(h) attaches to the recurring tier, recorded because they change its risk profile and are the reason lifetime is the hedge: we must fulfil the subscription for the entire period as marketed (on breach Microsoft may refund the full amount plus taxes in its sole discretion), and raising the price disables auto-renew -- so $10 is effectively locked for the product's life. Stale facts fixed in the same change rather than appended: - research-store-certification.md: IARC was 11.11.1/11.11.2 in one table and 10.11.1 in another; corrected to 10.11.x. - research-store-certification.md: policy 10.8.1/10.8.2 still asserted "Polar explicitly permitted" and "tick the third-party purchase box". Void now that Store IAP is the route and Polar is being torn down. - task-48: the working YouTube demo account (10.3.1) was accidentally dropped from the certification list during the Store-IAP edit. Restored -- a reviewer cannot use the app without one. - MyMistakes.md: the verified-fact-vs-decided-outcome lesson now closes its loop (the creator did rule the way the research pointed), and records the over-correction that followed. Docs-only. No code, no build, no tests. |