using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; using Microsoft.Web.WebView2.Core; using Windows.Graphics.Capture; using Windows.System; using Windows.UI.Composition; using ytLive.Helpers; using ytLive.Models; namespace ytLive.Services; /// /// Owns the composition-based web sources (one per widget). A session renders /// its widget through a whose /// visual tree lives off-screen on the WPF UI thread (OBS web-source model: /// the page renders at the MASTER CANVAS size 1920×1080, stable, never tracked); /// the controller's root visual is captured by a /// at Chromium's own presentation cadence. The preview is crop-copied to a shared /// WriteableBitmap on the UI thread (dispatcher-coalesced latest-wins, mirror of /// ScreenCaptureManager); the FramePump reads . /// /// The composition stack needs a DispatcherQueue on this thread (CsWinRT does not /// project CreateOnCurrentThread on the 19041 SDK — only the dedicated /// variant — so it is installed via a CoreMessaging P/Invoke, the documented /// fallback the WebView2CompositionControl / webview_windows embedders use). /// Everything composition-side therefore runs on the WPF UI thread; only the /// frame conversion and alpha-bbox scan run on the capture worker. /// /// Seam: an internal Func<string, IScreenCaptureSource> source /// factory substitutes the whole WebView2+item creation in tests — the manager /// then owns the session/coalesce/crop/preview plumbing exactly as in production. /// public sealed class WebView2Manager : IDisposable { private readonly Dispatcher _dispatcher; private readonly Func? _sessionSourceFactory; private readonly Dictionary _sessions = new(); private Task? _environmentTask; private DispatcherQueueController? _dispatcherQueueController; private Windows.UI.Composition.Compositor? _compositor; public event Action? PreviewBitmapChanged; private sealed class WebSourceSession { public required string Key { get; init; } public CoreWebView2? Web; public CoreWebView2CompositionController? Controller; public IScreenCaptureSource? Source; public Action? FrameHandler; public VideoFrame? LatestFrame; public WriteableBitmap? PreviewBitmap; // Buffer reuse (2026-09-04 lesson): the preview crop copy reuses one // scratch array while the widget's extent holds — no per-frame LOH mint. public byte[]? CropScratch; public bool Disposed; public bool Initialized; public bool FramePending; public bool FirstCaptureLogged; public int WidgetLogsRemaining; } public WebView2Manager(Dispatcher dispatcher) : this(dispatcher, null) { } /// Test seam: the source factory replaces WebView2+composition /// creation, so the session/coalesce/crop/preview plumbing is testable /// without the runtime. internal WebView2Manager(Dispatcher dispatcher, Func? sessionSourceFactory) { _dispatcher = dispatcher; _sessionSourceFactory = sessionSourceFactory; } public void Register(Source source) { if (_sessions.ContainsKey(source.Id)) return; _sessions[source.Id] = new WebSourceSession { Key = source.Id }; _ = InitializeAsync(source, _sessions[source.Id]); } public void Unregister(string sourceId) => RemoveSession(sourceId); public void OnWebUriChanged(Source source) { if (!_sessions.TryGetValue(source.Id, out var session)) return; if (session.Disposed || !session.Initialized) return; if (string.IsNullOrEmpty(source.WebUri)) { session.LatestFrame = null; session.Web?.NavigateToString( ""); return; } Navigate(source.WebUri, session); } public VideoFrame? GetLatestFrame(string sourceId) { 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). A transparent capture REQUIRES the transparency in // place before the page paints, on the composition surface too. // // 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 `