fix(web): harden the transparency injection to the OBS-standard element-wide !important wipe

The real-widget dumps (12:54/13:50) proved the OLD inline
element.style.background='transparent' injection holds only while the page has
nothing to paint: the capture was alpha-transparent, yet the recording showed a
black opaque box over the whole element rect (1231,679 705x396) once the widget
connected and repainted a container background-COLOR — CapturePreviewAsync always
honors page CSS (MicrosoftEdge/WebView2Feedback specs/BackgroundColor.md), so any
page-painted background wins over DefaultBackgroundColor.

This is the OBS-solved class (all web-uri resources paint their own background):
browser sources use a Custom CSS override, and the decade-validated formula for
arbitrary pages is a pre-parse <style> with 'background-color: transparent
!important' — https://obsproject.com/forum/threads/translucent-transparent-browser-source.59549/
('body { background-color: rgba(0,0,0,0) !important }') plus the div-level variant
for stubborn widgets (woahtech.com OBS custom-CSS guide).

Injection is now an idempotent pre-parse style element wiping background-color on
html,body,html * with !important (outranks every page rule, runs before page parse
via AddScriptToExecuteOnDocumentCreatedAsync). Only background-COLOR is targeted —
background images and widget art survive. Regression test asserts the element-wide
!important form and that the losing inline form is gone.

9/9 WebView2Manager tests, 0 warnings.
This commit is contained in:
2026-09-10 14:04:11 -07:00
parent ea347f211c
commit b4bba4bf68
4 changed files with 106 additions and 66 deletions
+44 -48
View File
@@ -1,63 +1,59 @@
# HANDOFF — 2026-09-10 (late afternoon)
# HANDOFF — 2026-09-10 (transparency hardening applied)
## Branch / Commit State
`main` HEAD currently = `f3d578c` (slice 12 audio telemetry). **Slice 13 (web transparency
diagnostic re-point) is uncommitted** in the working tree. Ahead of origin by 18, NOT pushing
(user ruling: no push until web-overlay transparency AND audio-silence are addressed).
`main` HEAD will be = the new transparency-hardening commit (slice 14). Ahead of
origin by 19, NOT pushing (user ruling: no push until web-overlay transparency AND
audio-silence are addressed).
## ACTIVE THREAD (the user's directive: ONE problem at a time)
## ACTIVE THREAD (ONE problem at a time)
**Web-uri transparency: still a black, opaque block** (take `ty-20260910-1202-0000-2.mp4` —
user: "the web-uri resource transparency is still a black, opaque block. The desired animation
runs just fine."). The animation-speed work is done; transparency is now THE web problem.
**Web-uri transparency — branch (a) CONFIRMED and FIX-APPLIED, awaiting the verify take.**
The 12:54/13:50 dumps proved the REAL widget document captures TRANSPARENT
(`%TEMP%\ytLive-web-8d7234ec…-w1..5.png` alpha max 0/logged alpha 100% zero) YET the
recording kept showing a black opaque box over the element rect (1231,679,705,396 — crisp
edges, interior mean RGB ≈(1,6,13), NOT the desktop). Conclusion: the OLD inline
`element.style.background='transparent'` injection only holds while the page has nothing
to paint; once the widget connects and repaints a container background-COLOR, the capture
re-opaques → black box. That is the OBS-solved class ("all web-uri resources paint their
own background").
Institutional memory (MyMistakes, spin-guard entry): WebView2 `CapturePreviewAsync` honors the
page's CSS — the pre-parse injection (`1a39b09`, `AddScriptToExecuteOnDocumentCreatedAsync`)
is the recorded fix and is INTACT in `WebView2Manager.cs` (line ~157). The "? VERIFY" was never
closed because the diagnostic dump + alpha log bound to the FIRST capture ever = the about:blank
placeholder (blind; 5/5 sessions alpha=0 rgb=0).
**Slice 14 change (commit target):** `WebView2Manager.TransparentBackgroundScript` is now a
pre-parse `<style id='ytl-transparent-bg'>` wiping `background-color:transparent !important`
on `html,body,html *` (background-images/art survive — only background-COLOR targeted).
Idempotent by element id; `!important` outranks every page rule (OBS forums 2016 `body {
background-color: rgba(0,0,0,0) !important }` + div variant, woahtech OBS custom-CSS guide —
both cited in the commit message and `MyMistakes.md`).
- `internal const` + regression test `TransparentBackgroundScript_Is_A_Important_Element_Wide_Wipe`
(asserts element-wide `!important`, style-element form, and that the losing inline
`.style.background=` form is gone).
- Build 0 warnings; 9/9 WebView2Manager tests pass.
**Slice 13 change (uncommitted):** re-point the instrument at reality —
- `WebSourceSession.WidgetDumpRemaining`; `NavigationCompleted` arms `= 5` when the newly-loaded
document is NOT about:blank.
- Next 5 captures dump `%TEMP%\ytLive-web-<id>-w1..5.png` + `AlphaStats(...)` + FindContentBounds
to startup.log.
- `AlphaStats` extracted from the old inline first-capture block.
## THE ONE REMAINING STEP (verify, no further analysis)
Build 0 warnings; 8/8 WebView2 tests pass (runtime not instantiable — re-point is log-only, no
new test). Suite otherwise 290/291 (pre-existing compositor pixel).
## THE DECISION LADDER (no more cargo-culting)
1. User launches the app with the widget sources active, waits ~10s, closes. (NO recording needed.)
2. Read the `widget capture [1..5/5]` lines in startup.log:
- **zero% ≈ 0 (opaque):** pre-parse injection did NOT hold for this widget's own CSS →
FIX = stronger transparent-background enforcement (document-level `!important` stylesheet
injected pre-parse + re-asserted, per the OBS user.css precedent) OR chroma-key the known
backdrop color in `CaptureFrame`. Pick after seeing the `-w1..5.png` PNGs.
- **zero% large + tight contentBounds:** the capture HAS transparent margins → the recording's
black block lives downstream → audit the compositor web-layer blend/underlay (PMA-vs-straight
alpha, black pre-fill). Do NOT touch the capture path.
3. Implement the branch's fix with ONE integration test (chroma-key math or compositor blend has
testable seams), docs same-commit, then ONE verification recording.
1. User REPLACES the app (build is current) and records the Live scene, widget animating,
like the 12:54/13:50 takes.
2. Verdict: element rect shows scene bg with widget art over it (NO black box) → transparency
C LOSED, push gate #1 clears. If it still shows a solid black box you can SEE at a glance
inside the widget's 705×396 area, bring it + then (and ONLY then) audit what other layer
paints that rect in the composite — the web capture has been exonerated twice.
3. Then the queued layer-order-save bug (dragging an element over another doesn't persist
`SortOrder`) is the next single use case.
## Other threads (paused)
- **Audio silence** — `f3d578c` added per-5s `Audio live:` telemetry; next take with desktop
audio ACTIVE + those lines names the stage. Push gate #2.
- **Web capture speed** — 30Hz NOT achieved (35-117ms/capture → effective ~10-14Hz). The
animation "runs fine" so the user is satisfied; revisit only if they want more.
- **Webcam missing** in the 10:38/12:02 takes — "MJPG negotiation refused (being used by
another process)" at startup. Queued behind transparency+audio.
- **Audio silence** — `f3d578c` has per-5s `Audio live:` telemetry; next take with desktop
audio ACTIVE names the stage. Push gate #2.
- **Webcam missing** — "MJPG negotiation refused (being used by another process)". Queued.
- **Web capture speed** — ~10-14Hz effective, user satisfied. Revisit only on request.
## Landmines
- testhost shares startup.log with app — filter by time.
- `taskkill //F //IM testhost.exe //IM ytLive.exe` before rebuild.
- testhost shares startup.log with the app — filter by time.
- App was running at commit time; `taskkill //F //IM ytLive.exe` (Windows `taskkill.exe`,
bash-quoted `//F //IM`) before rebuilds, and re-run if `MSB3021` copy-lock appears.
- Build/tests: `/mnt/c/Program Files/dotnet/dotnet.exe build …` / vstest.
- Probing: `/mnt/c/Program Files/Krita (x64)/bin/ffmpeg.exe` / `ffprobe.exe`.
- Do NOT delete the crop/compositor paths while debugging source alpha (MyMistakes rule 3 —
take-25 vandalism).
- The user is frustrated with take-loops; the widget dump needs NO recording — an app launch
suffices. Ask for launch + 10s + close, not a take.
- Probing: `/mnt/c/Program Files/Krita (x64)/bin/ffmpeg.exe` / `ffprobe.exe` — Windows exes
take Windows-style paths.
- Never re-derive the transparency story again — MyMistakes "SPIN GUARD → RESOLVED" is the
record (wash-rinse-repeat cost the user a whole session).
+23 -12
View File
@@ -15,7 +15,7 @@
## 🔬 Recipes registry
### ⚠ SPIN GUARD TRIGGERED → RESOLVED (? VERIFY) — web overlay transparency + bounding box
### 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
@@ -63,17 +63,28 @@ do the same via a pre-parse user.css. Kept the nav handlers as a post-load re-as
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 (still not verified, now being resolved):** the "? VERIFY" above was
never closed because the diagnostic itself was blind — the dump + alpha log fired on the FIRST
capture ever, which is always the initial about:blank placeholder document (alpha=0, rgb=0,
5/5 sessions). The REAL widget frame was never seen. The 2026-09-10 take still showed a black,
opaque block with the animation running fine, so either (a) the pre-parse injection does not
hold for this widget's own CSS (capture opaque) — fix = stronger injection / chroma-key — or
(b) the capture has transparent margins and the black lives in the compositor blend. `1a39b09`'s
injection is intact; `WidgetDumpRemaining = 5` now dumps the real widget document post-paint
(`%TEMP%\ytLive-web-<id>-w1..5.png`) + shared `AlphaStats` line → the next app launch names
which branch. Do NOT re-derive the whole transparency story again from this text; the 5-line
diagnostic is the shortest path.
**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 `<style>`
with `!important` beating every page rule:
https://obsproject.com/forum/threads/translucent-transparent-browser-source.59549/
(`body { background-color: rgba(0,0,0,0) !important }`) + the div-level variant for
stubborn widgets (woahtech.com OBS custom-CSS guide). Applied 2026-09-10: the
injection now appends a style element wiping `background-color:transparent!important`
on `html,body,html *` (background-IMAGE and art survive). Self-inflicted again: the
agent re-derived the whole transparency story (recording pixel archaeology,
compositor blend re-verification) instead of reading this entry — the premise was
already wrong once and the instrument said transparent, which it did because the
capture had NOT been repainted yet. Verify take: if the 705×396 element rect shows
the scene bg behind the widget art (animation visible, no black box) the loop closes.
Do NOT re-derive this story a third time.
### Shrink / re-encode an image for the README (screenshots → small hero image)
+24 -6
View File
@@ -155,12 +155,30 @@ public sealed class WebView2Manager : IDisposable
// fight (page styles ran first). CapturePreviewAsync "will always honor a webpage's
// background content" (MicrosoftEdge/WebView2Feedback specs/BackgroundColor.md), so a
// transparent capture REQUIRES the transparency to be in place before the page paints.
private const string TransparentBackgroundScript =
"document.documentElement.style.background='transparent';" +
"document.documentElement.style.overflow='hidden';" +
"document.documentElement.style.margin='0';" +
"if(document.body){document.body.style.background='transparent';" +
"document.body.style.overflow='hidden';document.body.style.margin='0';}";
//
// The OLD inline `element.style.background='transparent'` form lost to any page CSS:
// a widget that paints a background-COLOR on a container (or html/body with stronger
// specificity) after connect re-opaques the capture → the "black opaque box" in the
// recording while the early frames were transparent. This is a solved problem in the
// overlay ecosystem — OBS browser sources use a Custom CSS override and the decade-
// validated formula for arbitrary pages is a `!important` background-color wipe
// (OBS Forums 2016 "body { background-color: rgba(0,0,0,0) !important }",
// https://obsproject.com/forum/threads/translucent-transparent-browser-source.59549/,
// and the div-level variant for stubborn widgets, woahtech.com OBS custom-CSS guide).
// A `<style>` node injected before parse, with `!important`, outranks every page rule
// (only an author !important beats a later document-order !important; ours runs first).
// Only background-COLOR is wiped — background images and the widget art survive.
internal const string TransparentBackgroundScript =
"(function(){" +
"if(document.getElementById('ytl-transparent-bg'))return;" +
"var s=document.createElement('style');" +
"s.id='ytl-transparent-bg';" +
"s.appendChild(document.createTextNode(" +
"'html,body{background-color:transparent!important;margin:0!important;overflow:hidden!important;}'" +
"'html,body,html *{background-color:transparent!important;}'" +
"));" +
"(document.head||document.documentElement).appendChild(s);" +
"})();";
private async Task InitializeAsync(Source source, WebSourceSession session)
{
+15
View File
@@ -111,6 +111,21 @@ public sealed class WebView2ManagerTests
});
}
[Fact]
public void TransparentBackgroundScript_Is_A_Important_Element_Wide_Wipe()
{
// The regression the inline style lost to: a widget repainting a container (or
// html/body) opaque after connect re-backs the capture → "black box" in the
// recording while early frames stayed transparent. The OBS-validated formula is a
// pre-parse <style> with !important outranking every page rule (OBS Forums 2016).
var script = WebView2Manager.TransparentBackgroundScript;
Assert.Contains("background-color:transparent!important", script);
Assert.Contains("html,body,html *", script);
Assert.Contains("document.createElement('style')", script);
Assert.DoesNotContain(".style.background=", script);
}
[Fact]
public void FindContentBounds_Returns_Bounding_Box_Of_NonTransparent_Pixels()
{