18a21010bb
Software compositor that renders a scene into the encoder's master VideoFrame, mirroring the XAML preview minus editing chrome: backdrop -> background -> elements (UniformToFill cover-crop, round clip, mirror, opacity, border) -> branding flash. NOTE FOR USERS: this change shows NO difference in the app's UI — it is pure backend scaffolding laying the groundwork for live video capture/streaming. The preview you see is unchanged. - Services/Compositor/: SceneCompositor (Render(scene, frameFor resolver, flashFrame, CompositorOptions)), CompositorOptions (source rect + output size; 16:9 full master, vertical 607x1080 -> 1080x1920), StretchMath (pure UniformToFill + bilinear), StaticPixelCache (asset bytes -> BGRA8 frame) - Frame sources injected via Func<SceneElement, VideoFrame?> resolver, so the compositor is pure, WPF-free, and hermetic to test (D3D11 upgrade behind the same seam later) - SceneElement.TryGetBorderColor public (shared hex parse), stale MainViewModel comment fixed, pre-existing CS1998 in YouTubeAuthServiceTests cleaned up - Tests: SceneCompositorTests integration (full scene + vertical tier + flash) + StretchMath units, docs updated (72 tests passing, 0 warnings)
77 lines
3.2 KiB
C#
77 lines
3.2 KiB
C#
namespace ytLive.Services.Compositor;
|
||
|
||
/// <summary>
|
||
/// Pure pixel math shared by the compositor: the WPF "UniformToFill" cover-crop
|
||
/// (what the preview's <c>Stretch="UniformToFill"</c> does) and a clamped bilinear
|
||
/// sample/scale. Pure and deterministic — the unit-tested half of the compositor.
|
||
/// </summary>
|
||
public static class StretchMath
|
||
{
|
||
/// <summary>
|
||
/// UniformToFill: the drawn content covers the destination while preserving the
|
||
/// source aspect, centered; overflow is cropped. Returns the scale and the
|
||
/// source-origin offset (in destination pixels) of the drawn content within the
|
||
/// destination rect.
|
||
/// </summary>
|
||
public static (float Scale, float OffsetX, float OffsetY) UniformToFill(float dstW, float dstH, int srcW, int srcH)
|
||
{
|
||
var scale = Math.Max(dstW / srcW, dstH / srcH);
|
||
var drawnW = srcW * scale;
|
||
var drawnH = srcH * scale;
|
||
return (scale, (dstW - drawnW) / 2f, (dstH - drawnH) / 2f);
|
||
}
|
||
|
||
/// <summary>Clamped bilinear scale into a new tightly-packed BGRA8 frame; returns the input unchanged when the sizes already match.</summary>
|
||
public static VideoFrame BilinearScale(VideoFrame src, int outW, int outH)
|
||
{
|
||
if (outW == src.Width && outH == src.Height) return src;
|
||
|
||
var outPixels = new byte[outW * outH * 4];
|
||
for (var y = 0; y < outH; y++)
|
||
{
|
||
var sy = (y + 0.5f) / outH * src.Height - 0.5f;
|
||
for (var x = 0; x < outW; x++)
|
||
{
|
||
var sx = (x + 0.5f) / outW * src.Width - 0.5f;
|
||
var (b, g, r, a) = SampleBgra(src.BgraPixels, src.Width, src.Height, sx, sy);
|
||
var di = (y * outW + x) * 4;
|
||
outPixels[di] = b;
|
||
outPixels[di + 1] = g;
|
||
outPixels[di + 2] = r;
|
||
outPixels[di + 3] = a;
|
||
}
|
||
}
|
||
return new VideoFrame(outW, outH, outPixels);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Clamped bilinear sample of one BGRA8 pixel at fractional (sx, sy). Coordinates
|
||
/// outside [0, w-1]×[0, h-1] clamp to the edge, so a caller can sample freely
|
||
/// without bounds checks.
|
||
/// </summary>
|
||
public static (byte B, byte G, byte R, byte A) SampleBgra(byte[] src, int w, int h, float sx, float sy)
|
||
{
|
||
sx = Math.Clamp(sx, 0, w - 1);
|
||
sy = Math.Clamp(sy, 0, h - 1);
|
||
var x0 = (int)sx;
|
||
var y0 = (int)sy;
|
||
var x1 = Math.Min(x0 + 1, w - 1);
|
||
var y1 = Math.Min(y0 + 1, h - 1);
|
||
var fx = sx - x0;
|
||
var fy = sy - y0;
|
||
|
||
var p00 = (y0 * w + x0) * 4;
|
||
var p10 = (y0 * w + x1) * 4;
|
||
var p01 = (y1 * w + x0) * 4;
|
||
var p11 = (y1 * w + x1) * 4;
|
||
|
||
float r = Lerp(Lerp(src[p00 + 2], src[p10 + 2], fx), Lerp(src[p01 + 2], src[p11 + 2], fx), fy);
|
||
float g = Lerp(Lerp(src[p00 + 1], src[p10 + 1], fx), Lerp(src[p01 + 1], src[p11 + 1], fx), fy);
|
||
float b = Lerp(Lerp(src[p00], src[p10], fx), Lerp(src[p01], src[p11], fx), fy);
|
||
float a = Lerp(Lerp(src[p00 + 3], src[p10 + 3], fx), Lerp(src[p01 + 3], src[p11 + 3], fx), fy);
|
||
return ((byte)Math.Round(b), (byte)Math.Round(g), (byte)Math.Round(r), (byte)Math.Round(a));
|
||
}
|
||
|
||
private static float Lerp(float a, float b, float t) => a + (b - a) * t;
|
||
}
|