Web source: geometry-agnostic viewport==box model — viewport locked to element box, Stretch=Fill, no dead space

This commit is contained in:
2026-08-28 10:48:53 -07:00
parent 9d4103c44b
commit 8d793af91a
5 changed files with 121 additions and 82 deletions
+31 -21
View File
@@ -15,20 +15,6 @@ namespace ytLive.Services;
public sealed class WebView2Manager : IDisposable
{
// Eliminates the wrapper padding/whitespace between the box and the page:
// clears UA margins/paddings, forces html/body to fill the viewport and
// anchors content at (0,0) so the rendered frame matches the widget's size.
private const string CssResetInjection =
"(function(){" +
"var s=document.createElement('style');" +
"s.textContent='html,body{margin:0!important;padding:0!important;" +
"width:100%!important;height:100%!important;overflow:hidden!important;" +
"background:transparent!important}';" +
"(document.head||document.documentElement).appendChild(s);" +
"if(document.documentElement)document.documentElement.style.background='transparent';" +
"if(document.body)document.body.style.background='transparent';" +
"})();";
private readonly Panel _hostPanel;
private readonly Dispatcher _dispatcher;
private readonly Dictionary<string, WebSourceSession> _sessions = new();
@@ -54,13 +40,14 @@ public sealed class WebView2Manager : IDisposable
{
if (_sessions.ContainsKey(source.Id)) return;
// OBS pattern: the browser viewport is the canvas size (1920×1080),
// never the element box. The page renders full at its designed size;
// the preview scales the captured frame independently (Stretch=Uniform).
// Geometry-agnostic OBS model: the browser viewport IS the element box.
// The page renders at exactly Source.Width × Source.Height, so the
// captured frame and the selection rect are always the same rectangle —
// no fixed render size, no aspect assumptions, no dead space or padding.
var webView = new WebView2
{
Width = 1920,
Height = 1080,
Width = Math.Max(1, source.Width),
Height = Math.Max(1, source.Height),
DefaultBackgroundColor = System.Drawing.Color.Transparent,
Visibility = System.Windows.Visibility.Visible,
};
@@ -82,6 +69,22 @@ public sealed class WebView2Manager : IDisposable
_sessions[source.Id] = session;
// Interop sync: keep the viewport locked to the element box so the
// selection rect always bounds the actual rendered content.
source.PropertyChanged += (_, e) =>
{
if (e.PropertyName is not (nameof(Source.Width) or nameof(Source.Height))) return;
if (session.Disposed) return;
if (Math.Abs(session.Control.Width - source.Width) > 0.5 ||
Math.Abs(session.Control.Height - source.Height) > 0.5)
{
session.Control.Width = Math.Max(1, source.Width);
session.Control.Height = Math.Max(1, source.Height);
if (session.Initialized)
_ = CaptureFrame(source.Id);
}
};
timer.Tick += (_, _) =>
{
if (!session.Disposed && session.Initialized)
@@ -122,11 +125,18 @@ public sealed class WebView2Manager : IDisposable
var cws = session.Control.CoreWebView2!;
await cws.AddScriptToExecuteOnDocumentCreatedAsync(CssResetInjection);
cws.NavigationStarting += (_, _) =>
{
_ = cws.ExecuteScriptAsync(
"document.documentElement.style.background='transparent';" +
"document.body.style.background='transparent';");
};
cws.NavigationCompleted += (_, _) =>
{
_ = cws.ExecuteScriptAsync(CssResetInjection);
_ = cws.ExecuteScriptAsync(
"document.documentElement.style.background='transparent';" +
"document.body.style.background='transparent';");
_ = CaptureFrame(source.Id);
};