# MyMistakes.md > **Two jobs**, distinguished by heading: > > 1. **Per-task failure log** β€” updated before every commit touching that task: > current iteration + why the last one failed. On task complete, committed AND > pushed β†’ truncate to this stub. A new task does NOT seed this file until its > first failure. > 2. **Recipes registry** (DERIVED-SOLUTION RULE, see `AGENTS.md` πŸ”¬) β€” the durable > home for one-off derived solutions, recipes, and how-tos. The moment you work > out a reusable solution, write it here **in the same session**. GREP THIS FILE > FIRST when you hit a "I've done this before but have to figure it out again" > wall. Recipe entries stay permanently (they are NOT truncated on task > completion) β€” only the failure log truncates. ## πŸ”¬ Recipes registry ### YOUTUBE STATISTICS ARE JSON STRINGS; TryGetInt64 THROWS ON STRINGS (RECIPE) YouTube Data API v3 returns `statistics.subscriberCount / viewCount / videoCount` as JSON **strings** (`"350"`), not numbers. (2026-09-23: the YPP parse bug surfaced the moment the auditDetails 403 stopped masking it.) Fix = a tolerant read that checks `ValueKind` FIRST, then `TryGetInt64` for `Number`, then `long.TryParse(GetString())` for `String`. Trap within the fix: **`JsonElement.TryGetInt64` THROWS `InvalidOperationException` on any non-Number token** ("requires an element of type 'Number'") β€” it is try-type-in, not try-catch. Guard on `value.ValueKind` before calling it; a leaked 403β†’parse chain means your "whole request failed" symptom can paper over a second crash that only appears once the 403 is fixed (test the full happy path, not just the error path). ### liveChatId LIVES IN SNIPPET AND ONLY EXISTS ONCE THE BROADCAST IS LIVE (RECIPE) YouTube Data API v3 `liveBroadcasts`. `liveChatId` is in **`snippet.liveChatId`** β€” `contentDetails` has NO such property. AND it is only populated once the broadcast is **live** (the official `GetLiveChatId.java` sample lists `broadcastStatus=active`); a fetch right after insert (lifecycleStatus `ready`) returns nothing. (2026-09-25: this cost a whole investigation β€” the TEST-tab "Chat polling couldn't start β€” Mock Chat Input is disabled" report β€” because the code read `part=contentDetails` + ran before the frame pump pushed RTMP.) Fix = read part=snippet + bounded retry AFTER the encoder starts (`GetBroadcastLiveChatIdAsync(broadcastId, maxAttempts=10, delayMs=2000)`). Debug lens: missing liveChatId β‰ˆ "broadcast not live yet", NOT an auth failure. ### liveChat/MESSAGES.INSERT BODY REQUIRES snippet.type (RECIPE) YouTube Data API v3 `liveChat/messages.insert` rejects the body with `400 MISSING_REQUIRED_FIELD` (`domain: youtube.api.v3.LiveChatMessageInsertResponse.Error`) unless the snippet declares `snippet.type` = `textMessageEvent` (or `pollEvent`) alongside `liveChatId` and `textMessageDetails.messageText` β€” the official insert reference lists `type` as a required property. (2026-09-25: the TEST-tab "YouTube rejected the message (error 400)" report β€” the liveChatId fix in TASK 44 had worked and the drawer's Mock Chat Input was issuing a real insert, but the body omitted `type`, and the TASK 41 test only asserted `liveChatId` + `messageText` were present so it stayed green while real YouTube rejected every send. Fix = add `type = "textMessageEvent"` to the body + assert it in the Good Dog test.) Debug lens: MISSING_REQUIRED_FIELD β‰  auth/scope β€” it means the request body shape is wrong, and the false-green test is the classic trap: assertion on the body was about WHAT WE SEND, so YouTube's required fields must be mirrored in the test. ### CHANNELS.LIST auditDetails PART 403s THE WHOLE REQUEST WITHOUT A PARTNER SCOPE (RECIPE) YouTube Data API v3 `channels.list` rejects the ENTIRE request with `403 insufficientPermissions` if your `part=` list includes `auditDetails` but the token lacks `https://www.googleapis.com/auth/youtubepartner-channel-audit` β€” the docs' exact words: "A request that retrieves the auditDetails part for a channel resource must provide an authorization token that contains the youtubepartner-channel-audit scope". That scope is MCN content-partner tooling (with a two-week token-revocation rule); normal-creator apps should NEVER ask for it. 2026-09-23 real-log evidence: YPP refresh 403'd three times in a row while the mock-fake tests stayed green ("current scopes suffice, no re-consent" was wrong). Fixes: (1) drop `auditDetails` from `part=`; (2) log the response BODY β€” the bare status code could not name `insufficientPermissions`, which is what made this failure undiagnosable for days; (3) when a feature needs data no ordinary scope grants, deep-link to the site instead of requesting the partner privilege. ### TRANSITION(COMPLETE) RACES AUTOSTOP: PRE-FLIGHT lifeCycleStatus (RECIPE) A blind `liveBroadcasts.transition?broadcastStatus=complete` POST can 403 `invalidTransition` even though the stream just ended normally β€” 2026-09-22 logged it on EVERY session end. Cause: the broadcast's own state moves toward complete via `enableAutoStop`/YouTube auto-complete; the transition method's errors doc (https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/transition) shows `invalidTransition` = "can't transition from its current status". A blind POST just races β€” read the broadcast's `lifeCycleStatus` first (`liveBroadcasts.list?part=status`) and only POST complete from `live`/`testing`; skip silently otherwise and let `enableAutoStop` finish it. Never throw on the close-out either way. ### YOUTUBE liveStreams LIST: healthStatus IS AN OBJECT, NOT A STRING (RECIPE) YouTube Data API v3 `liveStreams.list` nests health under `status.healthStatus = {status, lastUpdateTimeSeconds, configurationIssues[]}` β€” the `healthStatus` element is an **OBJECT**, and `configurationIssues[]` lives INSIDE it, not directly under `status`. Reading `healthStatus.GetString()` throws System.Text.Json's `The requested operation requires an element of type 'String', but the target element has type 'Object'` β€” the exact log line seen 2026-09-22 on every live health poll (two test sessions). The parse must read `healthStatus["status"]` (and nested `healthStatus["configurationIssues"]`). Something like REST-shape drift is a good reason to grep the API reference (https://developers.google.com/youtube/v3/live/docs/liveStreams) before writing parsers against a "remembered" shape β€” our own flat-string fixture was the wrong assumption the whole time. ### WINRT RESOURCE-ALLOCATION CALLS: WRAP PER-CALL, DEGRADE TO NEXT OPTION (RECIPE) Creator callout (2026-09-15): WinRT/COM calls that allocate or start a resource β€” `InitializeAsync`, `CreateFrameReaderAsync`, `StartAsync`, `CreateReaderAsync` & friends β€” are THROW-HEAVY. An unsupported subtype/format, a device that just vanished, an access mode rejected mid-flight: these surface as `ArgumentException`/`E_INVALIDARG` ("value does not fall within the expected range") or HRESULTs, NOT as a returned status you can switch on. Relying on ONE outer catch to "handle failures" is not handling β€” one rejected call inside a fallback ladder aborts the whole ladder and every untried option. Rule: - Each allocation/start call inside a try/catch of its OWN, so a throw on candidate N falls through to candidate N+1 (log each rejection with its message/status; collect them for the final error string). - `return`/`break` on success must be reached WITHOUT passing through a `finally` that disposes the resource you just committed (classic reader/capture dispose-after-commit bug). - Unsubscribe + dispose the partial resource in the catch block when the subscription happened before the throwing call. - The outer catch stays as the LAST-RESORT net for device-level errors, not the primary one. - Same discipline applies to the frame-consumption side: teardown races reader threads (see HANDOFF crash follow-up) β€” a frame callback can't assume the pipeline is alive. Applied in `MediaCaptureFrameSource`'s reader-subtype ladder (2026-09-15, webcam-take fix). ### "SAVE-RECORDING DIALOG IS CLIPPING CONTENT" β€” WHERE + HOW (RECIPE) **WHERE:** the end-of-recording modal (creator: "when I end a recording the app throws up a save-recording dialog with a default filename") is **`RenameRecordingDialog.xaml` at the repo ROOT** (title "Rename Recording", `x:Class="ytLive.RenameRecordingDialog"`). It is NOT one of the `*Dialog.xaml` files under `Controls/` β€” grep for the window title, not the filename, when the user names a dialog by what it does. Only fix dialogs the user actually named; do not "while I'm here" bump sibling dialogs. **HOW:** the dialog is `ResizeMode="NoResize"` with a fixed `Height` attribute; content client area β‰ˆ `Height βˆ’ ~37px` of window chrome. Sizing/margin changes pushed content down in Z-order until it clipped β€” first the file-name textbox (230 too short), and after the +20% bump (β†’276) the creator found the Cancel/Save button row had ALSO been obscured. Fixed `Height` to 304 (+10% more), `x:Name` the last interactive control (`SaveRecordingButton`) so a test can measure it. **Verify (never eyeball-predict):** `RenameRecordingDialogSizingTests` (RealApp host) instantiates the real dialog, `Show()` + `UpdateLayout()`, then asserts the BOTTOM EDGE of each named control (`TransformToAncestor` against the content root β†’ `TransformBounds(RenderSize).Bottom`) is ≀ the client `ActualHeight`. Every such dialog fix ships with that assertion for every control that was reported obscured. (Client-area math means the button row needs MORE slack than the textbox: its bottom sits deeper because of the `Margin="0,20,0,0"` above it.) ### SPIN GUARD β†’ RESOLVED β€” web overlay transparency + bounding box (RECIPE) **THE ONE ROOT CAUSE THAT EXPLAINS EVERY FAILED TAKE:** WebView2's `CapturePreviewAsync` produces an **OPAQUE** PNG. From the WebView2 spec (sender: MicrosoftEdge/WebView2Feedback `specs/BackgroundColor.md`): "WebView will always honor a webpage's background content." `DefaultBackgroundColor = Transparent` only shows through pages with NO background style β€” the widget's own CSS paints html/body opaque. Every take below built on the false premise "the capture has transparent margins, alpha=0"; it never did. `FindContentBounds` then had no alpha-0 margins to find β†’ wrong crop β†’ black bounding box. The compositor blend saw alpha=255 β†’ black over webcam = "transparency broken". Same bug, three symptoms. **THE FIX (the OBS way, applied 2026-09-08):** inject the transparency BEFORE the page parses using `CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync` β€” documented to run "before the HTML document has been parsed and before any other script included by the HTML document is run" (learn.microsoft.com/dotnet/api/microsoft.web.webview2.core.corewebview2.addscripttoexecuteondocumentcreatedasync). The old `ExecuteScriptAsync` on NavigationStarting/NavigationCompleted ran AFTER page scripts/CSS β†’ widget page repainted background opaque β†’ lost the fight. OBS browser sources do the same via a pre-parse user.css. Kept the nav handlers as a post-load re-assertion. **Take timeline (the honest record):** - `bccdb48` (Aug 28): added FindContentBounds crop β€” correct idea (tight bbox, no dead space), but the capture was OPAQUE so the bbox math was built on nothing. - `5348b5c` (Aug 28, 2 min later): reverted to full-frame no-crop β€” looked "good" for a full-bleed widget, but floating widgets regained dead space ("ghost boundary"). - take-21 (`e002847`) full canvas β†’ shrunken/offset widget (UnifomToFill of whole canvas into a small element = lost resizing). - take-22 (`ed9d7c1`) crop width used as canvas stride for buffer indexing β†’ garbage. - take-23 (`f6802c7`) stride fixed with `src.Width`; PasteKey lacked CropBounds β†’ stale raster cache β†’ stale crop. - take-24 (`081e4c1`) CropBounds in PasteKey β€” STILL BROKEN because the SOURCE ALPHA WAS NEVER REAL. - take-25 (**ME, this session β€” the user's "RE-INTRODUCING THE BOUNDING-BOX PROBLEM"**): I removed FindContentBounds + the crop path entirely, betting full-canvas UniformToFill was the answer. It WASN'T β€” the source is opaque-black, so the element rendered as a SOLID BLACK BOX (the user's screenshot: "a black box in the lower right corner"). Killed the crop β†’ dead space returned AND black box. The compositor math was ALREADY correct; gutting it was vandalism in response to a source-level bug. **Rules, self-inflicted:** 1. Instrument FIRST. This session added: first-capture PNG dump of the raw WebView2 PNG (to `%TEMP%\ytLive-web-.png`) + alpha min/max/mean/%zero + FindContentBounds result logged to startup.log once per session. That's the diff between a five-take loop and a five-minute diagnosis. 2. When the same symptom loops across takes, the PREMISE is wrong, not the code β€” the capture being transparent was the load-bearing premise and it was never verified. 3. Do not delete code paths that fix one axis (crop=bbox) while debugging another (source alpha). Revert scope creep; keep layer contributions separable. **2026-09-10 follow-up β†’ NOW VERIFIED and hardened.** The 12:54 and 13:50 sessions showed the REAL widget document capturing TRANSPARENT (widget dumps `w1..5` for `8d7234ec`: alpha 100% zero, content 12Γ—3; whole-file decodes of `%TEMP%\ytLive-web-*.png` alpha max = 0) while the RECORDING kept showing a black, opaque box over the element rect (crisp edges at the exact rect 1231,679 705Γ—396 β€” NOT the desktop showing through). Concluson: branch (a) β€” the OLD inline `element.style.background='transparent'` injection only wins while the page has nothing to paint; once the widget connects and paints its own container background-COLOR, the capture goes opaque again β†’ black box. The OBS-validated answer (valid for arbitrary pages for a decade) is an injected pre-parse `