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:
@@ -5,67 +5,67 @@ using ytLive.ViewModels;
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The backdrop is a permanent, non-deletable bottom layer holding live
|
||||
/// The background is a permanent, non-deletable bottom layer holding live
|
||||
/// desktop/game capture, and it exists only in the canonical Live scene (see
|
||||
/// <see cref="SceneCatalog"/>). Every backdrop-enabled scene
|
||||
/// (<see cref="Scene.HasBackdrop"/>) is healed to have exactly one, inserted
|
||||
/// <see cref="SceneCatalog"/>). Every background-enabled scene
|
||||
/// (<see cref="Scene.HasBackground"/>) is healed to have exactly one, inserted
|
||||
/// first; it is never reorderable or removable by the UI. Scenes without the
|
||||
/// flag never get one.
|
||||
/// </summary>
|
||||
public class BackdropTests
|
||||
public class BackgroundTests
|
||||
{
|
||||
[Fact]
|
||||
public void EnsureBackdrop_OnEmptyScene_InsertsFullFrameBackdrop()
|
||||
public void EnsureBackground_OnEmptyScene_InsertsFullFrameBackground()
|
||||
{
|
||||
var scene = new Scene { Name = "S", HasBackdrop = true };
|
||||
var scene = new Scene { Name = "S", HasBackground = true };
|
||||
|
||||
var backdrop = MainViewModel.EnsureBackdrop(scene);
|
||||
var background = MainViewModel.EnsureBackground(scene);
|
||||
|
||||
Assert.NotNull(backdrop);
|
||||
Assert.NotNull(background);
|
||||
Assert.Single(scene.Elements);
|
||||
Assert.Same(backdrop, scene.Elements[0]);
|
||||
Assert.True(backdrop.IsBackdrop);
|
||||
Assert.True(backdrop.IsEnabled);
|
||||
Assert.Equal(SourceType.DisplayCapture, backdrop.Type);
|
||||
Assert.Equal(0, backdrop.X);
|
||||
Assert.Equal(0, backdrop.Y);
|
||||
Assert.Equal(1920, backdrop.Width);
|
||||
Assert.Equal(1080, backdrop.Height);
|
||||
Assert.Same(background, scene.Elements[0]);
|
||||
Assert.True(background.IsBackground);
|
||||
Assert.True(background.IsEnabled);
|
||||
Assert.Equal(SourceType.DisplayCapture, background.Type);
|
||||
Assert.Equal(0, background.X);
|
||||
Assert.Equal(0, background.Y);
|
||||
Assert.Equal(1920, background.Width);
|
||||
Assert.Equal(1080, background.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureBackdrop_ExistingBackdrop_IsIdempotent()
|
||||
public void EnsureBackground_ExistingBackground_IsIdempotent()
|
||||
{
|
||||
var scene = new Scene { Name = "S", HasBackdrop = true };
|
||||
var first = MainViewModel.EnsureBackdrop(scene);
|
||||
var second = MainViewModel.EnsureBackdrop(scene);
|
||||
var scene = new Scene { Name = "S", HasBackground = true };
|
||||
var first = MainViewModel.EnsureBackground(scene);
|
||||
var second = MainViewModel.EnsureBackground(scene);
|
||||
|
||||
Assert.Same(first, second);
|
||||
Assert.Single(scene.Elements);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureBackdrop_HealsMissingBackdropAtIndexZero()
|
||||
public void EnsureBackground_HealsMissingBackgroundAtIndexZero()
|
||||
{
|
||||
var scene = new Scene { Name = "S", HasBackdrop = true };
|
||||
var scene = new Scene { Name = "S", HasBackground = true };
|
||||
var image = new Source { Name = "Logo", Type = SourceType.Image };
|
||||
scene.Elements.Add(image);
|
||||
|
||||
MainViewModel.EnsureBackdrop(scene);
|
||||
MainViewModel.EnsureBackground(scene);
|
||||
|
||||
Assert.Equal(2, scene.Elements.Count);
|
||||
Assert.True(((Source)scene.Elements[0]).IsBackdrop);
|
||||
Assert.True(((Source)scene.Elements[0]).IsBackground);
|
||||
Assert.Same(image, scene.Elements[1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnsureBackdrop_SceneWithoutHasBackdrop_ReturnsNullAndInsertsNothing()
|
||||
public void EnsureBackground_SceneWithoutHasBackground_ReturnsNullAndInsertsNothing()
|
||||
{
|
||||
var scene = new Scene { Name = "Ending", HasBackdrop = false };
|
||||
var scene = new Scene { Name = "Ending", HasBackground = false };
|
||||
|
||||
var backdrop = MainViewModel.EnsureBackdrop(scene);
|
||||
var background = MainViewModel.EnsureBackground(scene);
|
||||
|
||||
Assert.Null(backdrop);
|
||||
Assert.Null(background);
|
||||
Assert.Empty(scene.Elements);
|
||||
}
|
||||
|
||||
@@ -59,10 +59,10 @@ public class FramePumpTests
|
||||
public void RaiseHealth(StreamHealth health) => HealthUpdated?.Invoke(this, health);
|
||||
}
|
||||
|
||||
private static Scene BackdropScene()
|
||||
private static Scene BackgroundScene()
|
||||
{
|
||||
var scene = new Scene { Name = "Live" };
|
||||
scene.Elements.Add(new Source { Type = SourceType.DisplayCapture, IsBackdrop = true, CaptureKey = "monitor:0" });
|
||||
scene.Elements.Add(new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" });
|
||||
return scene;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class FramePumpTests
|
||||
Func<(VideoFrame? Frame, SocialBarPosition Position)>? socialBar = null)
|
||||
{
|
||||
return new FramePump(
|
||||
sceneProvider: scene ?? (() => BackdropScene()),
|
||||
sceneProvider: scene ?? (() => BackgroundScene()),
|
||||
frameResolver: resolve ?? (_ => null),
|
||||
compositorOptions: () => new CompositorOptions
|
||||
{
|
||||
@@ -166,7 +166,7 @@ public class FramePumpTests
|
||||
var red = SceneCompositorTests.Solid(64, 48, 255, 0, 0);
|
||||
var encoder = new FakeEncoder();
|
||||
using var pump = NewPump(encoder,
|
||||
resolve: e => e is Source { IsBackdrop: true } ? red : null);
|
||||
resolve: e => e is Source { IsBackground: true } ? red : null);
|
||||
|
||||
await pump.StartAsync();
|
||||
Assert.True(pump.IsRunning);
|
||||
@@ -177,7 +177,7 @@ public class FramePumpTests
|
||||
var frame = encoder.Frames[0];
|
||||
Assert.Equal(64, frame.Width);
|
||||
Assert.Equal(48, frame.Height);
|
||||
AssertColor(frame, 0, 0, 255, 0, 0); // the backdrop really was composited in
|
||||
AssertColor(frame, 0, 0, 255, 0, 0); // the background really was composited in
|
||||
|
||||
await pump.StopAsync();
|
||||
Assert.False(pump.IsRunning);
|
||||
@@ -236,7 +236,7 @@ public class FramePumpTests
|
||||
var encoder = new FakeEncoder();
|
||||
var position = SocialBarPosition.Top;
|
||||
using var pump = NewPump(encoder,
|
||||
resolve: e => e is Source { IsBackdrop: true } ? red : null,
|
||||
resolve: e => e is Source { IsBackground: true } ? red : null,
|
||||
socialBar: () => (bar, position));
|
||||
|
||||
await pump.StartAsync();
|
||||
@@ -246,7 +246,7 @@ public class FramePumpTests
|
||||
|
||||
// Flip to Bottom: the pump re-reads the seam each frame, so a later frame
|
||||
// lands the bar at the bottom edge (SourceRectHeight - bar height) and the
|
||||
// top corner clears back to backdrop.
|
||||
// top corner clears back to background.
|
||||
position = SocialBarPosition.Bottom;
|
||||
VideoFrame? flipped = null;
|
||||
var deadline = DateTime.UtcNow.AddSeconds(5);
|
||||
|
||||
@@ -78,21 +78,21 @@ public class LayoutStorePersistenceTests
|
||||
}
|
||||
}
|
||||
|
||||
// The backdrop (schema v5): permanent bottom layer holding live desktop/game
|
||||
// capture. IsBackdrop + CaptureKey must survive a save + reload so a
|
||||
// The background (schema v5): permanent bottom layer holding live desktop/game
|
||||
// capture. IsBackground + CaptureKey must survive a save + reload so a
|
||||
// re-designated target isn't lost on restart.
|
||||
[Fact]
|
||||
public void Backdrop_IsBackdrop_And_CaptureKey_Survive_Save_And_Reload()
|
||||
public void Background_IsBackground_And_CaptureKey_Survive_Save_And_Reload()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
|
||||
try
|
||||
{
|
||||
using var store = new LayoutStore(path);
|
||||
var backdrop = new Source
|
||||
var background = new Source
|
||||
{
|
||||
Name = "Game Capture",
|
||||
Type = SourceType.DisplayCapture,
|
||||
IsBackdrop = true,
|
||||
IsBackground = true,
|
||||
IsEnabled = true,
|
||||
X = 0,
|
||||
Y = 0,
|
||||
@@ -102,13 +102,13 @@ public class LayoutStorePersistenceTests
|
||||
CaptureKey = "monitor:1",
|
||||
};
|
||||
var scene = new Scene { Name = "Starting" };
|
||||
scene.Elements.Add(backdrop);
|
||||
scene.Elements.Add(background);
|
||||
store.Save(new[] { scene }, null, null);
|
||||
|
||||
var reloaded = store.Load();
|
||||
var restored = Assert.IsType<Source>(Assert.Single(reloaded[0].Elements));
|
||||
|
||||
Assert.True(restored.IsBackdrop);
|
||||
Assert.True(restored.IsBackground);
|
||||
Assert.Equal("monitor:1", restored.CaptureKey);
|
||||
Assert.Equal(1, restored.MonitorIndex);
|
||||
Assert.Equal(SourceType.DisplayCapture, restored.Type);
|
||||
@@ -120,23 +120,23 @@ public class LayoutStorePersistenceTests
|
||||
}
|
||||
}
|
||||
|
||||
// Per-scene backdrop switch (schema v6): a scene whose backdrop was turned
|
||||
// Per-scene background switch (schema v6): a scene whose background was turned
|
||||
// off must stay off across a save + reload.
|
||||
[Fact]
|
||||
public void Scene_HasBackdrop_Survives_Save_And_Reload()
|
||||
public void Scene_HasBackground_Survives_Save_And_Reload()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
|
||||
try
|
||||
{
|
||||
using var store = new LayoutStore(path);
|
||||
var on = new Scene { Name = "Live", HasBackdrop = true };
|
||||
var off = new Scene { Name = "Ending", HasBackdrop = false };
|
||||
var on = new Scene { Name = "Live", HasBackground = true };
|
||||
var off = new Scene { Name = "Ending", HasBackground = false };
|
||||
store.Save(new[] { on, off }, null, null);
|
||||
|
||||
var reloaded = store.Load();
|
||||
|
||||
Assert.True(reloaded.First(s => s.Name == "Live").HasBackdrop);
|
||||
Assert.False(reloaded.First(s => s.Name == "Ending").HasBackdrop);
|
||||
Assert.True(reloaded.First(s => s.Name == "Live").HasBackground);
|
||||
Assert.False(reloaded.First(s => s.Name == "Ending").HasBackground);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -145,12 +145,12 @@ public class LayoutStorePersistenceTests
|
||||
}
|
||||
}
|
||||
|
||||
// v5 → v6 one-time backfill: pre-v6 DBs already have backdrops sitting in the
|
||||
// non-Live canonical scenes (Starting/BRB/Chat/Ending). Adding the HasBackdrop
|
||||
// column turns those scenes off and drops their backdrop sources; Live keeps
|
||||
// its backdrop.
|
||||
// v5 → v6 one-time backfill: pre-v6 DBs already have backgrounds sitting in the
|
||||
// non-Live canonical scenes (Starting/BRB/Chat/Ending). Adding the HasBackground
|
||||
// column turns those scenes off and drops their background sources; Live keeps
|
||||
// its background.
|
||||
[Fact]
|
||||
public void V5_Database_Backfill_Removes_NonLive_Backdrops()
|
||||
public void V5_Database_Backfill_Removes_NonLive_Backgrounds()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
|
||||
try
|
||||
@@ -183,19 +183,19 @@ public class LayoutStorePersistenceTests
|
||||
DeviceId TEXT,
|
||||
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
|
||||
IsMirrored INTEGER NOT NULL DEFAULT 0,
|
||||
IsBackdrop INTEGER NOT NULL DEFAULT 0,
|
||||
IsBackground INTEGER NOT NULL DEFAULT 0,
|
||||
CaptureKey TEXT,
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-starting', 'Starting', 0);
|
||||
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-live', 'Live', 1);
|
||||
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-chat', 'Chat', 2);
|
||||
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
|
||||
VALUES ('b-starting', 's-starting', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
|
||||
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
|
||||
VALUES ('b-live', 's-live', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
|
||||
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
|
||||
VALUES ('b-chat', 's-chat', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
|
||||
INSERT INTO Source (Id, SceneId, Type, Name, IsBackground, CaptureKey, SortOrder)
|
||||
VALUES ('b-starting', 's-starting', 'DisplayCapture', 'Background', 1, 'monitor:0', 0);
|
||||
INSERT INTO Source (Id, SceneId, Type, Name, IsBackground, CaptureKey, SortOrder)
|
||||
VALUES ('b-live', 's-live', 'DisplayCapture', 'Background', 1, 'monitor:0', 0);
|
||||
INSERT INTO Source (Id, SceneId, Type, Name, IsBackground, CaptureKey, SortOrder)
|
||||
VALUES ('b-chat', 's-chat', 'DisplayCapture', 'Background', 1, 'monitor:0', 0);
|
||||
PRAGMA user_version = 5;
|
||||
""";
|
||||
cmd.ExecuteNonQuery();
|
||||
@@ -205,16 +205,16 @@ public class LayoutStorePersistenceTests
|
||||
var scenes = store.Load();
|
||||
|
||||
var starting = Assert.Single(scenes, s => s.Name == "Starting");
|
||||
Assert.False(starting.HasBackdrop);
|
||||
Assert.DoesNotContain(starting.Elements, e => e is Source { IsBackdrop: true });
|
||||
Assert.False(starting.HasBackground);
|
||||
Assert.DoesNotContain(starting.Elements, e => e is Source { IsBackground: true });
|
||||
|
||||
var chat = Assert.Single(scenes, s => s.Name == "Chat");
|
||||
Assert.False(chat.HasBackdrop);
|
||||
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackdrop: true });
|
||||
Assert.False(chat.HasBackground);
|
||||
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackground: true });
|
||||
|
||||
var live = Assert.Single(scenes, s => s.Name == "Live");
|
||||
Assert.True(live.HasBackdrop);
|
||||
Assert.Single(live.Elements, e => e is Source { IsBackdrop: true });
|
||||
Assert.True(live.HasBackground);
|
||||
Assert.Single(live.Elements, e => e is Source { IsBackground: true });
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class RoundClipInteractionTests
|
||||
window.Show();
|
||||
window.UpdateLayout();
|
||||
|
||||
var scene = vm.ActiveScene!;
|
||||
var scene = vm.StagedScene!;
|
||||
var webcam = new WebcamSceneConfig
|
||||
{
|
||||
WebcamId = "test-camera",
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace ytLive.Tests;
|
||||
/// <summary>
|
||||
/// The app is a five-scene product: Starting/Live/BRB/Chat/Ending. Scenes are
|
||||
/// worked on by name; you can delete them but never add beyond the set, and the
|
||||
/// live backdrop belongs to Live alone.
|
||||
/// live background belongs to Live alone.
|
||||
/// </summary>
|
||||
public class SceneCatalogTests
|
||||
{
|
||||
@@ -40,38 +40,38 @@ public class SceneCatalogTests
|
||||
[InlineData("BRB", false)]
|
||||
[InlineData("Chat", false)]
|
||||
[InlineData("Ending", false)]
|
||||
public void SceneCatalog_HasBackdrop_Only_For_Live(string name, bool expected)
|
||||
public void SceneCatalog_HasBackground_Only_For_Live(string name, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, SceneCatalog.HasBackdrop(name));
|
||||
Assert.Equal(expected, SceneCatalog.HasBackground(name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnforceBackdropPolicy_Removes_Backdrop_From_NonLive_Scenes()
|
||||
public void EnforceBackgroundPolicy_Removes_Background_From_NonLive_Scenes()
|
||||
{
|
||||
var chat = new Scene { Name = "Chat", HasBackdrop = true };
|
||||
var live = new Scene { Name = "Live", HasBackdrop = true };
|
||||
var chatBackdrop = MainViewModel.EnsureBackdrop(chat)!;
|
||||
var liveBackdrop = MainViewModel.EnsureBackdrop(live)!;
|
||||
Assert.Contains(chat.Elements, e => ReferenceEquals(e, chatBackdrop));
|
||||
var chat = new Scene { Name = "Chat", HasBackground = true };
|
||||
var live = new Scene { Name = "Live", HasBackground = true };
|
||||
var chatBackground = MainViewModel.EnsureBackground(chat)!;
|
||||
var liveBackground = MainViewModel.EnsureBackground(live)!;
|
||||
Assert.Contains(chat.Elements, e => ReferenceEquals(e, chatBackground));
|
||||
|
||||
MainViewModel.EnforceBackdropPolicy(new[] { chat, live });
|
||||
MainViewModel.EnforceBackgroundPolicy(new[] { chat, live });
|
||||
|
||||
Assert.False(chat.HasBackdrop);
|
||||
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackdrop: true });
|
||||
Assert.True(live.HasBackdrop);
|
||||
Assert.Contains(live.Elements, e => ReferenceEquals(e, liveBackdrop));
|
||||
Assert.False(chat.HasBackground);
|
||||
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackground: true });
|
||||
Assert.True(live.HasBackground);
|
||||
Assert.Contains(live.Elements, e => ReferenceEquals(e, liveBackground));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnforceBackdropPolicy_Marks_Live_And_Clears_A_Pre_Policy_Database()
|
||||
public void EnforceBackgroundPolicy_Marks_Live_And_Clears_A_Pre_Policy_Database()
|
||||
{
|
||||
var starting = new Scene { Name = "Starting", HasBackdrop = true };
|
||||
MainViewModel.EnsureBackdrop(starting);
|
||||
var live = new Scene { Name = "Live", HasBackdrop = false };
|
||||
var starting = new Scene { Name = "Starting", HasBackground = true };
|
||||
MainViewModel.EnsureBackground(starting);
|
||||
var live = new Scene { Name = "Live", HasBackground = false };
|
||||
|
||||
MainViewModel.EnforceBackdropPolicy(new[] { starting, live });
|
||||
MainViewModel.EnforceBackgroundPolicy(new[] { starting, live });
|
||||
|
||||
Assert.False(starting.HasBackdrop);
|
||||
Assert.True(live.HasBackdrop);
|
||||
Assert.False(starting.HasBackground);
|
||||
Assert.True(live.HasBackground);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class SourceNamingTests
|
||||
var vm = (MainViewModel)window.DataContext;
|
||||
try
|
||||
{
|
||||
var scene = vm.ActiveScene!;
|
||||
var scene = vm.StagedScene!;
|
||||
|
||||
vm.AddSourceCommand.Execute(SourceType.TextOverlay);
|
||||
vm.AddSourceCommand.Execute(SourceType.TextOverlay);
|
||||
|
||||
Reference in New Issue
Block a user