5e78065c2d
The recording is 60fps but WebView2 capture was a blind 100ms DispatcherTimer = 10Hz; each captured web frame repeated ~6x into the file caps web animation at the capture rate, not the page's (user: 'the animation appears to be too slow'). - New CaptureScheduler (Services/CaptureScheduler.cs): per-session dispatcher timer that DROPS a tick while a capture is in flight (latest-wins, never queues) — the guard that makes a higher cadence safe: concurrent full-HD PNG CapturePreviewAsync calls (~10-30ms each, slow per WebView2Feedback#20) would stack CPU and publish stale-after-fresh. Effective cadence = max(interval, capture duration). - Cadence: SetCaptureInterval(33) on record/stream start, (200) idle — applied via MainViewModel.Streaming.Operations.cs. - De-throttle the hidden page: shared CoreWebView2Environment created BEFORE EnsureCoreWebView2Async with --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-features=CalculateNativeWinOcclusion. Off-screen WebView2 is a hidden page when the host window is unfocused/covered and Chromium then parks rAF and clamps timers to ~1s (WebView2Feedback#1172/#3070, Chrome-88 timer-throttling blog). - Telemetry: first 30 captures per session log elapsed ms (PNG encode + decode) to startup.log — that decides whether ~30Hz stays or drops to ~20Hz; the FramePump drops frames (never time-lapses, slice 10) if UI-thread GC churn starves it. - ONE test: CaptureScheduler_Drops_Ticks_While_Capture_InFlight_And_Resumes (deterministic TCS-driven, no WebView2 runtime). Suite 290/291 — sole failure the pre-existing compositor pixel test. - Docs same-commit: ai.md slice 11, MyMistakes.md, HANDOFF. References: https://github.com/MicrosoftEdge/WebView2Feedback/issues/1172 https://github.com/MicrosoftEdge/WebView2Feedback/issues/3070 https://github.com/MicrosoftEdge/WebView2Feedback/issues/20 https://developer.chrome.com/blog/timer-throttling-in-chrome-88
186 lines
5.4 KiB
C#
186 lines
5.4 KiB
C#
using System.Drawing;
|
|
using System.Linq;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Threading;
|
|
using Microsoft.Web.WebView2.Wpf;
|
|
using Xunit;
|
|
using ytLive.Models;
|
|
using ytLive.Services;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
[Collection("RealApp")]
|
|
public sealed class WebView2ManagerTests
|
|
{
|
|
private readonly RealAppHost _app;
|
|
|
|
public WebView2ManagerTests(RealAppHost app)
|
|
{
|
|
_app = app;
|
|
}
|
|
|
|
[Fact]
|
|
public void WebView2_Control_Size_Is_Canvas_Resolution_Regardless_Of_Source()
|
|
{
|
|
_app.Run(() =>
|
|
{
|
|
var panel = new Canvas();
|
|
var source = new Source
|
|
{
|
|
Type = SourceType.WebSource,
|
|
Width = 640,
|
|
Height = 480,
|
|
};
|
|
|
|
using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher);
|
|
manager.Register(source);
|
|
|
|
var webView = panel.Children.OfType<WebView2>().First();
|
|
Assert.Equal(1920.0, webView.Width);
|
|
Assert.Equal(1080.0, webView.Height);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void WebView2_Control_Size_Does_Not_Track_Box_After_Register()
|
|
{
|
|
_app.Run(() =>
|
|
{
|
|
var panel = new Canvas();
|
|
var source = new Source
|
|
{
|
|
Type = SourceType.WebSource,
|
|
Width = 640,
|
|
Height = 480,
|
|
};
|
|
|
|
using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher);
|
|
manager.Register(source);
|
|
|
|
var webView = panel.Children.OfType<WebView2>().First();
|
|
|
|
source.Width = 1024;
|
|
source.Height = 768;
|
|
|
|
Assert.Equal(1920.0, webView.Width);
|
|
Assert.Equal(1080.0, webView.Height);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void WebView2_Control_Size_Ignores_Zero_Sized_Source()
|
|
{
|
|
_app.Run(() =>
|
|
{
|
|
var panel = new Canvas();
|
|
var source = new Source
|
|
{
|
|
Type = SourceType.WebSource,
|
|
Width = 0,
|
|
Height = 0,
|
|
};
|
|
|
|
using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher);
|
|
manager.Register(source);
|
|
|
|
var webView = panel.Children.OfType<WebView2>().First();
|
|
Assert.Equal(1920.0, webView.Width);
|
|
Assert.Equal(1080.0, webView.Height);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void WebView2_DefaultBackgroundColor_Is_Transparent()
|
|
{
|
|
_app.Run(() =>
|
|
{
|
|
var panel = new Canvas();
|
|
var source = new Source
|
|
{
|
|
Type = SourceType.WebSource,
|
|
Width = 640,
|
|
Height = 480,
|
|
};
|
|
|
|
using var manager = new WebView2Manager(panel, Dispatcher.CurrentDispatcher);
|
|
manager.Register(source);
|
|
|
|
var webView = panel.Children.OfType<WebView2>().First();
|
|
|
|
Assert.Equal(System.Drawing.Color.Transparent, webView.DefaultBackgroundColor);
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void FindContentBounds_Returns_Bounding_Box_Of_NonTransparent_Pixels()
|
|
{
|
|
const int w = 10, h = 6;
|
|
var pixels = new byte[w * h * 4];
|
|
for (int yy = 2; yy <= 4; yy++)
|
|
for (int xx = 3; xx <= 6; xx++)
|
|
{
|
|
var i = (yy * w + xx) * 4;
|
|
pixels[i] = 255; // B
|
|
pixels[i + 1] = 0; // G
|
|
pixels[i + 2] = 0; // R
|
|
pixels[i + 3] = 255; // A
|
|
}
|
|
|
|
var (x, y, bw, bh) = WebView2Manager.FindContentBounds(pixels, w, h, w * 4);
|
|
|
|
Assert.Equal((3, 2, 4, 3), (x, y, bw, bh));
|
|
}
|
|
|
|
[Fact]
|
|
public void FindContentBounds_No_Alpha_Means_No_Crop()
|
|
{
|
|
const int w = 10, h = 6;
|
|
var pixels = new byte[w * h * 4];
|
|
|
|
var (x, y, bw, bh) = WebView2Manager.FindContentBounds(pixels, w, h, w * 4);
|
|
|
|
Assert.Equal((0, 0, w, h), (x, y, bw, bh));
|
|
}
|
|
|
|
[Fact]
|
|
public void FindContentBounds_Full_Opacity_Means_No_Crop()
|
|
{
|
|
const int w = 10, h = 6;
|
|
var pixels = new byte[w * h * 4];
|
|
for (var i = 3; i < pixels.Length; i += 4)
|
|
pixels[i] = 255;
|
|
|
|
var (x, y, bw, bh) = WebView2Manager.FindContentBounds(pixels, w, h, w * 4);
|
|
|
|
Assert.Equal((0, 0, w, h), (x, y, bw, bh));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CaptureScheduler_Drops_Ticks_While_Capture_InFlight_And_Resumes()
|
|
{
|
|
// The ~30Hz recording cadence would stack concurrent CapturePreviewAsync calls
|
|
// (full-HD PNG encode is ~10-30ms) without the latest-wins drop: ticks that
|
|
// arrive while a capture is in flight must be skipped, never queued, and a
|
|
// capture can always start once the previous one has finished.
|
|
var calls = 0;
|
|
var signal = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var scheduler = new CaptureScheduler(Dispatcher.CurrentDispatcher, 200, async () =>
|
|
{
|
|
calls++;
|
|
await signal.Task;
|
|
});
|
|
|
|
scheduler.Tick();
|
|
scheduler.Tick();
|
|
scheduler.Tick();
|
|
Assert.Equal(1, calls); // overlapping ticks dropped, never queued
|
|
|
|
signal.SetResult();
|
|
await scheduler.Pending; // durable: Pending completes only after the drop resets
|
|
|
|
scheduler.Tick();
|
|
Assert.Equal(2, calls); // once idle, the next tick captures again
|
|
await scheduler.Pending;
|
|
}
|
|
}
|