Files
LlamaCasty/MainWindow.xaml.cs
T
gramps c01206fb8a feat: web sources frame-captured via composition (CoreWebView2CompositionController → Windows.Graphics.Capture); PNG poll + CaptureScheduler deleted
The ~30Hz CapturePreviewAsync PNG poll capped real cadence at ~20Hz
(35–165ms full-HD encode+decode), so a 60fps widget still juddered at ~1/6
speed. Replaced polling with frame-driven capture of the composition
controller's root visual — the mechanism WebView2CompositionControl and
Flutter's webview_windows use (graphics_context.cc captures the root
surface_ visual via CreateGraphicsCaptureItemFromVisual; reference:
github.com/microsoft/Windows.UI.Composition.WinUI / flutter-internal
webview_windows). Frames now arrive at the renderer's own pace; capture
memory is epoch'd ring reuse + one crop-sized shared WriteableBitmap.

New Services/WebCaptureFrameSource.cs owns GraphicsCaptureItem + free-
threaded Direct3D11CaptureFramePool + session (Straight alpha readback,
per-frame FindContentBounds → CropBounds). WebView2Manager reworked around
per-session composition controllers + one UI-thread Compositor created via
the CoreMessaging CreateDispatcherQueueController P/Invoke (the 19041
projection lacks CreateOnCurrentThread); internal seam ctor
(Dispatcher, Func<string,IScreenCaptureSource>?) for hermetic tests.
CaptureScheduler.cs deleted; the three SetCaptureInterval cadence hooks
removed; InitWebView2() moved from MainWindow ctor to Loaded (a parent
HWND must exist for the composition controller); the hidden WebViewHostPanel
overlay deleted. TransparentBackgroundScript unchanged.

Tests: WebView2ManagerTests reworked — 4 control-size + scheduler tests
dropped, FindContentBounds tests moved to WebCaptureFrameSource, ONE
integration test (Frames_PublishCroppedPreview_And_CoalesceToLatest_CarryingCropBounds)
drives the seam with a FakeWebSource + background-STA DispatcherPump.
Suite 293/293, 0 warnings.

NOTE: composition path NOT yet verified on a device — the take is the
next step. Web work committed locally only (no push per standing rule).

Docs same-commit: ai.md Slice 14 + supersede marker on Slice 11, HANDOFF,
MyMistakes (CoreMessaging DQ + namespace-landmine recipe), TASK 17,
Controls/ViewModels/Services indexes.
2026-09-14 16:14:00 -07:00

164 lines
5.7 KiB
C#

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using ytLive.Helpers;
using ytLive.ViewModels;
namespace ytLive;
public partial class MainWindow : Window
{
private readonly MainViewModel _viewModel;
private Controls.TopBar _topBar = null!;
private Controls.PreviewPane _previewPane = null!;
private Controls.LeftPanel _leftPanel = null!;
public MainWindow()
{
AppLog.Write("MainWindow ctor: before InitializeComponent");
InitializeComponent();
AppLog.Write("MainWindow ctor: after InitializeComponent");
_viewModel = (MainViewModel)DataContext;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
_topBar = (Controls.TopBar)FindName("TopBar")!;
_previewPane = (Controls.PreviewPane)FindName("PreviewPane")!;
_leftPanel = (Controls.LeftPanel)FindName("LeftPanel")!;
UpdateTaskbarOverlay();
UpdateSelectionOverlay();
_topBar.RefreshAvatar();
RestoreWindowGeometry();
AppLog.Write("MainWindow ctor: end");
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
AppLog.Write("MainWindow loaded");
// Web sources need the window HWND as the composition controller's parent,
// which does not exist in the ctor — InitWebView2 runs here (Loaded), after
// SourceInitialized. Nothing renders from web frames before this.
_viewModel.InitWebView2();
}
// ─── Window geometry persistence (launch size/position) ───
// Saved on close (RestoreBounds so a maximized window remembers its
// un-maximize rect); restored in the ctor before the window is shown.
// Values clamp to the minimums and to the primary work area — a position
// saved on a since-disconnected secondary monitor falls back on-screen.
private void RestoreWindowGeometry()
{
try
{
var saved = _viewModel.RestoreWindowState();
if (saved == null) return;
var (left, top, width, height, state) = saved.Value;
var work = SystemParameters.WorkArea;
width = Math.Clamp(width, MinWidth, work.Width);
height = Math.Clamp(height, MinHeight, work.Height);
left = Math.Clamp(left, work.Left - width + 120, work.Right - 120);
top = Math.Clamp(top, work.Top, work.Bottom - 80);
Left = left;
Top = top;
Width = width;
Height = height;
if (state == "Maximized") WindowState = WindowState.Maximized;
}
catch (Exception ex)
{
AppLog.Write($"Window geometry restore failed: {ex.Message}");
}
}
private void SaveWindowGeometry()
{
try
{
var bounds = RestoreBounds;
var state = WindowState == WindowState.Maximized ? "Maximized" : "Normal";
_viewModel.PersistWindowState(bounds.Left, bounds.Top, bounds.Width, bounds.Height, state);
}
catch (Exception ex)
{
AppLog.Write($"Window geometry save failed: {ex.Message}");
}
}
private ytLive.Services.GlobalHotkeyManager? _hotkeys;
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
_hotkeys = new ytLive.Services.GlobalHotkeyManager();
_hotkeys.Activated += id => _viewModel.HandleHotkey(id);
_hotkeys.Attach(this, _viewModel.CurrentHotkeyBindings);
_viewModel.HotkeyBindingsChanged = bindings => _hotkeys.Reattach(this, bindings);
}
private void Window_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (!_viewModel.IsEditMode)
e.Handled = true;
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainViewModel.IsLive))
UpdateTaskbarOverlay();
else if (e.PropertyName == nameof(MainViewModel.SelectedElement))
UpdateSelectionOverlay();
else if (e.PropertyName == nameof(MainViewModel.AccountAvatarUrl)
|| e.PropertyName == nameof(MainViewModel.IsConnected))
_topBar.RefreshAvatar();
}
private void MainWindow_Closing(object? sender, CancelEventArgs e)
{
SaveWindowGeometry();
_viewModel.Shutdown();
}
private void MainWindow_Activated(object? sender, EventArgs e)
{
_viewModel.RefreshBackgroundAutoCapture();
}
private void MainWindow_Deactivated(object? sender, EventArgs e)
{
}
private void UpdateTaskbarOverlay()
{
TaskbarInfo.Overlay = _viewModel.IsLive
? (ImageSource)FindResource("LiveOverlay")
: null;
}
private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.OriginalSource is not DependencyObject original) return;
var form = _viewModel.BroadcastForm;
if (form.IsDrawerOpen && !_previewPane.IsClickInsideDrawer(original))
form.CloseDrawerCommand.Execute(null);
if (_previewPane.IsClickInsidePreview(original)) return;
if (_leftPanel.IsClickInside(original)) return;
_viewModel.SelectedElement = null;
}
private void UpdateSelectionOverlay()
{
if (!_viewModel.IsEditMode)
{
_previewPane.UpdateSelectionOverlay(false);
return;
}
var selected = _viewModel.SelectedElement is { } s && Controls.PreviewPane.IsDraggableElement(s);
_previewPane.UpdateSelectionOverlay(selected);
_leftPanel.OnSelectionChanged(_viewModel);
}
}