225 lines
7.2 KiB
C#
225 lines
7.2 KiB
C#
using System;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Imaging;
|
|
using System.Windows.Threading;
|
|
using Microsoft.Web.WebView2.Core;
|
|
using Microsoft.Web.WebView2.Wpf;
|
|
using ytLive.Helpers;
|
|
using ytLive.Models;
|
|
|
|
namespace ytLive.Services;
|
|
|
|
public sealed class WebView2Manager : IDisposable
|
|
{
|
|
private readonly Panel _hostPanel;
|
|
private readonly Dispatcher _dispatcher;
|
|
private readonly Dictionary<string, WebSourceSession> _sessions = new();
|
|
|
|
public event Action<string, WriteableBitmap>? PreviewBitmapChanged;
|
|
|
|
private sealed class WebSourceSession
|
|
{
|
|
public required WebView2 Control { get; init; }
|
|
public required DispatcherTimer CaptureTimer { get; init; }
|
|
public VideoFrame? LatestFrame;
|
|
public bool Disposed;
|
|
public bool Initialized;
|
|
}
|
|
|
|
public WebView2Manager(Panel hostPanel, Dispatcher dispatcher)
|
|
{
|
|
_hostPanel = hostPanel;
|
|
_dispatcher = dispatcher;
|
|
}
|
|
|
|
public void Register(Source source)
|
|
{
|
|
if (_sessions.ContainsKey(source.Id)) return;
|
|
|
|
// OBS model: the page renders ONCE at a stable viewport — the size the
|
|
// element was created at (snapshot, never tracked, so dragging the box
|
|
// never reflows/truncates the page and no scrollbars appear). The
|
|
// captured texture is then stretched by the display (Stretch=Fill) into
|
|
// whatever box the user draws, so the frame always bounds content with
|
|
// zero dead space. Geometry-agnostic: any widget, any box.
|
|
var webView = new WebView2
|
|
{
|
|
Width = Math.Max(1, source.Width),
|
|
Height = Math.Max(1, source.Height),
|
|
DefaultBackgroundColor = System.Drawing.Color.Transparent,
|
|
Visibility = System.Windows.Visibility.Visible,
|
|
};
|
|
Canvas.SetLeft(webView, -5000);
|
|
Canvas.SetTop(webView, -5000);
|
|
|
|
_hostPanel.Children.Add(webView);
|
|
|
|
var timer = new DispatcherTimer
|
|
{
|
|
Interval = TimeSpan.FromMilliseconds(100),
|
|
};
|
|
|
|
var session = new WebSourceSession
|
|
{
|
|
Control = webView,
|
|
CaptureTimer = timer,
|
|
};
|
|
|
|
_sessions[source.Id] = session;
|
|
|
|
timer.Tick += (_, _) =>
|
|
{
|
|
if (!session.Disposed && session.Initialized)
|
|
_ = CaptureFrame(source.Id);
|
|
};
|
|
|
|
_ = InitializeAsync(source, session);
|
|
}
|
|
|
|
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.Control.CoreWebView2?.NavigateToString(
|
|
"<html><body style='background:transparent;margin:0'></body></html>");
|
|
return;
|
|
}
|
|
|
|
Navigate(source.WebUri, session);
|
|
}
|
|
|
|
public VideoFrame? GetLatestFrame(string sourceId)
|
|
{
|
|
return _sessions.TryGetValue(sourceId, out var session) ? session.LatestFrame : null;
|
|
}
|
|
|
|
private async Task InitializeAsync(Source source, WebSourceSession session)
|
|
{
|
|
try
|
|
{
|
|
await session.Control.EnsureCoreWebView2Async();
|
|
|
|
var cws = session.Control.CoreWebView2!;
|
|
|
|
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.NavigationCompleted += (_, _) =>
|
|
{
|
|
_ = cws.ExecuteScriptAsync(
|
|
"document.documentElement.style.background='transparent';" +
|
|
"document.documentElement.style.overflow='hidden';" +
|
|
"document.body.style.background='transparent';" +
|
|
"document.body.style.overflow='hidden';");
|
|
_ = CaptureFrame(source.Id);
|
|
};
|
|
|
|
session.Initialized = true;
|
|
|
|
session.Control.CoreWebView2.NavigateToString(
|
|
"<html><body style='background:transparent;margin:0'></body></html>");
|
|
|
|
if (!string.IsNullOrEmpty(source.WebUri))
|
|
Navigate(source.WebUri, session);
|
|
|
|
session.CaptureTimer.Start();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
AppLog.Write($"WebView2Manager: init failed for '{source.Name}': {ex.Message}");
|
|
RemoveSession(source.Id);
|
|
}
|
|
}
|
|
|
|
private void Navigate(string url, WebSourceSession session)
|
|
{
|
|
try { session.Control.CoreWebView2?.Navigate(url); }
|
|
catch (Exception ex) { AppLog.Write($"WebView2Manager: navigate failed: {ex.Message}"); }
|
|
}
|
|
|
|
private async Task CaptureFrame(string sourceId)
|
|
{
|
|
if (!_sessions.TryGetValue(sourceId, out var session) || session.Disposed) return;
|
|
|
|
try
|
|
{
|
|
var webView = session.Control;
|
|
if (webView.CoreWebView2 == null) return;
|
|
|
|
using var ms = new MemoryStream();
|
|
await webView.CoreWebView2.CapturePreviewAsync(
|
|
CoreWebView2CapturePreviewImageFormat.Png, ms);
|
|
|
|
ms.Position = 0;
|
|
|
|
var bitmap = new BitmapImage();
|
|
bitmap.BeginInit();
|
|
bitmap.StreamSource = ms;
|
|
bitmap.CacheOption = BitmapCacheOption.OnLoad;
|
|
bitmap.EndInit();
|
|
bitmap.Freeze();
|
|
|
|
var formatted = new FormatConvertedBitmap(bitmap,
|
|
PixelFormats.Bgra32, null, 0);
|
|
|
|
var pixW = formatted.PixelWidth;
|
|
var pixH = formatted.PixelHeight;
|
|
|
|
if (pixW < 1 || pixH < 1) return;
|
|
|
|
var pixels = new byte[pixW * pixH * 4];
|
|
formatted.CopyPixels(pixels, pixW * 4, 0);
|
|
|
|
var stride = pixW * 4;
|
|
var wb = new WriteableBitmap(pixW, pixH, 96, 96, PixelFormats.Bgra32, null);
|
|
wb.WritePixels(new Int32Rect(0, 0, pixW, pixH), pixels, stride, 0);
|
|
wb.Freeze();
|
|
|
|
session.LatestFrame = new VideoFrame(pixW, pixH, pixels);
|
|
|
|
_ = _dispatcher.BeginInvoke(() =>
|
|
{
|
|
PreviewBitmapChanged?.Invoke(sourceId, wb);
|
|
});
|
|
}
|
|
catch (ObjectDisposedException) { }
|
|
catch (InvalidOperationException) { }
|
|
catch (Exception ex)
|
|
{
|
|
AppLog.Write($"WebView2Manager: capture failed for '{sourceId}': {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void RemoveSession(string sourceId)
|
|
{
|
|
if (!_sessions.TryGetValue(sourceId, out var session)) return;
|
|
session.Disposed = true;
|
|
session.CaptureTimer.Stop();
|
|
_hostPanel.Children.Remove(session.Control);
|
|
try { session.Control.Dispose(); } catch { }
|
|
_sessions.Remove(sourceId);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var id in _sessions.Keys.ToList())
|
|
RemoveSession(id);
|
|
}
|
|
}
|