fix(web): inject transparency BEFORE page scripts — CapturePreviewAsync honors page CSS
The widget page paints html/body opaque, so CapturePreviewAsync output had
zero alpha-0 margins — every take since bccdb48 built the crop on 'the
capture is transparent' and never verified it. WebView2 spec is explicit:
'WebView will always honor a webpage's background content' and
DefaultBackgroundColor only shows through pages with no background style
(MicrosoftEdge/WebView2Feedback specs/BackgroundColor.md). The late
ExecuteScriptAsync injection (NavStarting/NavCompleted) ran after page CSS
and lost the fight.
Fix via CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync — the
documented pre-parse hook ('before the HTML document has been parsed and
before any other script included by the HTML document is run'), the same
mechanism OBS user.css uses. Nav handlers kept as post-load re-assertion.
Restores the take-23/24 stride-correct crop path (my take-25 removal was
wrong — it regressed the bounding box into a solid black box).
Adds one-shot diagnostics: raw capture PNG -> %TEMP%/ytLive-web-<id>.png
plus alpha min/max/mean/%zero and FindContentBounds result in startup.log,
so the next run proves the capture is transparent instead of guessing.
Build 0 warnings, 289/289 tests pass. MyMistakes.md records the full chain.
This commit is contained in:
+57
-10
@@ -42,6 +42,7 @@ public sealed class WebView2Manager : IDisposable
|
||||
public int OutRingNext;
|
||||
public long Epoch;
|
||||
public WriteableBitmap? PreviewBitmap;
|
||||
public bool DebugPngWritten;
|
||||
|
||||
public byte[] RentOutBuffer(int size)
|
||||
{
|
||||
@@ -138,6 +139,19 @@ public sealed class WebView2Manager : IDisposable
|
||||
return _sessions.TryGetValue(sourceId, out var session) ? session.LatestFrame : null;
|
||||
}
|
||||
|
||||
// Injected BEFORE the document is parsed (AddScriptToExecuteOnDocumentCreatedAsync) so
|
||||
// the widget page's own CSS/scripts cannot repaint html/body opaque — the reason the
|
||||
// late NavigationStarting/NavigationCompleted ExecuteScriptAsync injection lost the
|
||||
// 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';}";
|
||||
|
||||
private async Task InitializeAsync(Source source, WebSourceSession session)
|
||||
{
|
||||
try
|
||||
@@ -146,22 +160,19 @@ public sealed class WebView2Manager : IDisposable
|
||||
|
||||
var cws = session.Control.CoreWebView2!;
|
||||
|
||||
// Pre-page hook: runs after the global object exists but BEFORE the document
|
||||
// parses / any page script runs — must complete before navigating away from
|
||||
// about:blank for it to apply to the widget document.
|
||||
await cws.AddScriptToExecuteOnDocumentCreatedAsync(TransparentBackgroundScript);
|
||||
|
||||
cws.NavigationStarting += (_, _) =>
|
||||
{
|
||||
_ = cws.ExecuteScriptAsync(
|
||||
"document.documentElement.style.background='transparent';" +
|
||||
"document.documentElement.style.overflow='hidden';" +
|
||||
"document.body.style.background='transparent';" +
|
||||
"document.body.style.overflow='hidden';");
|
||||
_ = cws.ExecuteScriptAsync(TransparentBackgroundScript);
|
||||
};
|
||||
|
||||
cws.NavigationCompleted += (_, _) =>
|
||||
{
|
||||
_ = cws.ExecuteScriptAsync(
|
||||
"document.documentElement.style.background='transparent';" +
|
||||
"document.documentElement.style.overflow='hidden';" +
|
||||
"document.body.style.background='transparent';" +
|
||||
"document.body.style.overflow='hidden';");
|
||||
_ = cws.ExecuteScriptAsync(TransparentBackgroundScript);
|
||||
_ = CaptureFrame(source.Id);
|
||||
};
|
||||
|
||||
@@ -231,6 +242,19 @@ public sealed class WebView2Manager : IDisposable
|
||||
await webView.CoreWebView2.CapturePreviewAsync(
|
||||
CoreWebView2CapturePreviewImageFormat.Png, ms);
|
||||
|
||||
if (!session.DebugPngWritten)
|
||||
{
|
||||
session.DebugPngWritten = true;
|
||||
try
|
||||
{
|
||||
ms.Position = 0;
|
||||
var dbg = Path.Combine(Path.GetTempPath(), $"ytLive-web-{sourceId}.png");
|
||||
File.WriteAllBytes(dbg, ms.ToArray());
|
||||
AppLog.Write($"WebView2Manager: debug capture dump -> {dbg}");
|
||||
}
|
||||
catch (Exception dex) { AppLog.Write($"WebView2Manager: debug dump failed: {dex.Message}"); }
|
||||
}
|
||||
|
||||
ms.Position = 0;
|
||||
|
||||
var bitmap = new BitmapImage();
|
||||
@@ -258,6 +282,29 @@ public sealed class WebView2Manager : IDisposable
|
||||
|
||||
var (cropX, cropY, cropW, cropH) = FindContentBounds(pixels, pixW, pixH, stride);
|
||||
|
||||
if (session.Epoch == 0)
|
||||
{
|
||||
int minA = 255, maxA = 0;
|
||||
long sumA = 0, nA = 0, zeroA = 0;
|
||||
for (var y = 0; y < pixH; y += 16)
|
||||
{
|
||||
var row = y * stride;
|
||||
for (var x = 0; x < pixW; x += 16)
|
||||
{
|
||||
var a = pixels[row + x * 4 + 3];
|
||||
if (a < minA) minA = a;
|
||||
if (a > maxA) maxA = a;
|
||||
sumA += a;
|
||||
if (a == 0) zeroA++;
|
||||
nA++;
|
||||
}
|
||||
}
|
||||
AppLog.Write(
|
||||
$"WebView2Manager: first capture for '{sourceId}' {pixW}x{pixH} " +
|
||||
$"alpha[min={minA},max={maxA},mean={sumA / (double)nA:F1},zero={100.0 * zeroA / nA:F1}%] " +
|
||||
$"contentBounds=({cropX},{cropY},{cropW},{cropH})");
|
||||
}
|
||||
|
||||
var outPixels = session.RentOutBuffer(cropW * cropH * 4);
|
||||
if (cropW == pixW && cropH == pixH && cropX == 0 && cropY == 0)
|
||||
Buffer.BlockCopy(pixels, 0, outPixels, 0, outPixels.Length);
|
||||
|
||||
Reference in New Issue
Block a user