Control Surface UX: pill toggle, background rename, per-scene properties

- Sliding pill toggle (green ON / red OFF) replaces checkbox
- Geometry mismatch error for custom backgrounds
- Backdrop -> Background: C# rename, SQL column rename with migration
- Source.IsBackground: new -> override (fixes WPF binding)
- Legacy 'Backdrop' display name migrated to 'Background'
- Per-scene UseDefaultBackground + CustomBackgroundPath
- Asset files renamed backdrop -> background
- StagedScene/LiveScene split, thumbnail strip, edit gating
- TransitionService (Cut/Fade/Move)
- Context menu: Capture Desktop, Refresh Desktop
- Resolution badge removed, ON-AIR status light added
This commit is contained in:
2026-08-22 18:10:59 -07:00
parent e06bfa42a8
commit 4b9a8e515f
35 changed files with 1406 additions and 717 deletions
+18 -18
View File
@@ -8,7 +8,7 @@ namespace ytLive.Tests;
/// <summary>
/// The output compositor (TASK 4 ship step 1): renders a scene into the encoder's
/// master frame, mirroring the XAML preview minus the editing chrome. The integration
/// test composites a full scene (backdrop + round webcam + images + mirror + border)
/// test composites a full scene (background + round webcam + images + mirror + border)
/// and asserts per-layer probe pixels; the vertical-tier test asserts the 9:16 crop
/// and upscale. Frames are pushed by the test — no capture managers involved.
/// </summary>
@@ -60,11 +60,11 @@ public class SceneCompositorTests
[Fact]
public void Composite_FullScene_MasterPixels()
{
var red = Solid(1920, 1080, 255, 0, 0); // backdrop
var red = Solid(1920, 1080, 255, 0, 0); // background
var green = Solid(1280, 720, 0, 255, 0); // webcam
var split = Split(100, 100, 0, 255, 255, 255, 0, 255); // image: left cyan, right magenta
var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
var roundWebcam = new WebcamSceneConfig { X = 100, Y = 100, Width = 300, Height = 300, ClipShape = ClipShape.Round };
var imageA = new Source { Type = SourceType.Image, X = 1200, Y = 600, Width = 200, Height = 200 };
var imageB = new Source
@@ -74,7 +74,7 @@ public class SceneCompositorTests
};
var scene = new Scene { Name = "Live" };
scene.Elements.Add(backdrop);
scene.Elements.Add(background);
scene.Elements.Add(roundWebcam);
scene.Elements.Add(imageA);
scene.Elements.Add(imageB);
@@ -82,7 +82,7 @@ public class SceneCompositorTests
VideoFrame? FrameFor(SceneElement e) => e switch
{
WebcamSceneConfig => green,
Source { IsBackdrop: true } => red,
Source { IsBackground: true } => red,
Source { Type: SourceType.Image } => split,
_ => null,
};
@@ -98,11 +98,11 @@ public class SceneCompositorTests
Assert.Equal(1920, output.Width);
Assert.Equal(1080, output.Height);
// backdrop at the frame corner
// background at the frame corner
AssertColor(output, 0, 0, 255, 0, 0);
// round webcam: center is the (square-cropped) webcam feed
AssertColor(output, 250, 250, 0, 255, 0);
// round webcam: element-square corner is OUTSIDE the circle -> backdrop shows
// round webcam: element-square corner is OUTSIDE the circle -> background shows
AssertColor(output, 101, 101, 255, 0, 0);
// imageA (unmirrored): left half cyan, right half magenta
AssertColor(output, 1220, 700, 0, 255, 255);
@@ -120,16 +120,16 @@ public class SceneCompositorTests
var red = Solid(1920, 1080, 255, 0, 0);
var green = Solid(1280, 720, 0, 255, 0);
var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
var roundWebcam = new WebcamSceneConfig { X = 656, Y = 100, Width = 300, Height = 300, ClipShape = ClipShape.Round };
var scene = new Scene { Name = "Live" };
scene.Elements.Add(backdrop);
scene.Elements.Add(background);
scene.Elements.Add(roundWebcam);
VideoFrame? FrameFor(SceneElement e) => e switch
{
WebcamSceneConfig => green,
Source { IsBackdrop: true } => red,
Source { IsBackground: true } => red,
_ => null,
};
@@ -143,11 +143,11 @@ public class SceneCompositorTests
Assert.Equal(1080, output.Width);
Assert.Equal(1920, output.Height);
// top-left of the crop is pure backdrop (webcam starts at crop y=100, inset from the corner)
// top-left of the crop is pure background (webcam starts at crop y=100, inset from the corner)
AssertColor(output, 0, 0, 255, 0, 0);
// the webcam center (crop 150,250) scales to output ~(267,444) and stays green
AssertColor(output, 267, 444, 0, 255, 0);
// far from the webcam, still backdrop
// far from the webcam, still background
AssertColor(output, 978, 1688, 255, 0, 0);
}
@@ -155,9 +155,9 @@ public class SceneCompositorTests
public void Composite_WithFlash_BlendsOverContent()
{
var red = Solid(1920, 1080, 255, 0, 0);
var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
var scene = new Scene { Name = "Live" };
scene.Elements.Add(backdrop);
scene.Elements.Add(background);
// flash: a semi-transparent white pixel at the center of a master-sized frame
var flash = new VideoFrame(1920, 1080, new byte[1920 * 1080 * 4]);
@@ -185,9 +185,9 @@ public class SceneCompositorTests
public void Composite_WithSocialBar_OverlaysAboveFlash_AtTopOrBottom()
{
var red = Solid(1920, 1080, 255, 0, 0);
var backdrop = new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" };
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
var scene = new Scene { Name = "Live" };
scene.Elements.Add(backdrop);
scene.Elements.Add(background);
var options = new CompositorOptions
{
@@ -210,11 +210,11 @@ public class SceneCompositorTests
var top = compositor.Render(scene, _ => red, flash, options, bar, 0);
AssertColor(top, 0, 0, 0, 255, 0); // bar above flash at the top-left
AssertColor(top, 960, 540, 255, 127, 127); // 50% white over red
AssertColor(top, 100, 100, 255, 0, 0); // empty overlay area: backdrop
AssertColor(top, 100, 100, 255, 0, 0); // empty overlay area: background
var bottom = compositor.Render(scene, _ => red, flash, options, bar, 1040);
AssertColor(bottom, 0, 1040, 0, 255, 0); // bar drawn at the bottom edge
AssertColor(bottom, 0, 1039, 255, 0, 0); // backdrop just above the bar
AssertColor(bottom, 0, 1039, 255, 0, 0); // background just above the bar
AssertColor(bottom, 960, 540, 255, 0, 0); // bar region moved away from center
}
}