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 `