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:
@@ -9,14 +9,14 @@ namespace ytLive.Services.Compositor;
|
||||
/// editing view; this is the output view — see TASKS.md "Ship step 1 — Scene compositor".
|
||||
///
|
||||
/// Frame sources are supplied by a <see cref="Func{SceneElement, VideoFrame}"/> resolver
|
||||
/// (the caller maps webcam → DeviceId, images → AssetId, backdrop → CaptureKey), keeping
|
||||
/// (the caller maps webcam → DeviceId, images → AssetId, background → CaptureKey), keeping
|
||||
/// the compositor pure and free of WPF and of the capture managers.
|
||||
/// </summary>
|
||||
public sealed class SceneCompositor
|
||||
{
|
||||
/// <summary>
|
||||
/// Composite <paramref name="scene"/> into the tier's output frame. Layer order (back →
|
||||
/// front): live backdrop (the scene's <c>IsBackdrop</c> source) → background image →
|
||||
/// front): live background (the scene's <c>IsBackground</c> source) → background image →
|
||||
/// visible elements (z-order = <c>Elements</c> order, mirroring the XAML DataTemplate) →
|
||||
/// branding flash → social bar (a global overlay; spans the master width at
|
||||
/// <paramref name="socialBarTop"/>). Transparent regions read opaque black.
|
||||
@@ -45,10 +45,10 @@ public sealed class SceneCompositor
|
||||
|
||||
var elements = scene.Elements;
|
||||
|
||||
var backdrop = elements.OfType<Source>().FirstOrDefault(s => s.IsBackdrop);
|
||||
var backdropFrame = backdrop != null && backdrop.IsVisible ? frameFor(backdrop) : null;
|
||||
if (backdropFrame != null)
|
||||
BlitContent(buffer, cropW, cropH, 0, 0, cropW, cropH, backdropFrame, 1f, false, false);
|
||||
var liveCapture = elements.OfType<Source>().FirstOrDefault(s => s.IsBackground);
|
||||
var liveCaptureFrame = liveCapture != null && liveCapture.IsVisible ? frameFor(liveCapture) : null;
|
||||
if (liveCaptureFrame != null)
|
||||
BlitContent(buffer, cropW, cropH, 0, 0, cropW, cropH, liveCaptureFrame, 1f, false, false);
|
||||
|
||||
var background = elements.OfType<Source>().FirstOrDefault(s => s.Type == SourceType.Background);
|
||||
var backgroundFrame = background != null ? frameFor(background) : null;
|
||||
@@ -60,10 +60,10 @@ public sealed class SceneCompositor
|
||||
if (!element.IsVisible) continue;
|
||||
switch (element)
|
||||
{
|
||||
case Source { IsBackdrop: true }:
|
||||
case Source { IsBackground: true }:
|
||||
case Source { Type: SourceType.Background }:
|
||||
case Source { Type: SourceType.TextOverlay }:
|
||||
continue; // backdrop/background are their own layers; Text isn't shipped
|
||||
continue; // background/background are their own layers; Text isn't shipped
|
||||
}
|
||||
|
||||
var frame = frameFor(element);
|
||||
@@ -91,6 +91,34 @@ public sealed class SceneCompositor
|
||||
new VideoFrame(cropW, cropH, buffer), options.OutputWidth, options.OutputHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render a cross-fade blend between two scenes. Used by <see cref="TransitionService"/>
|
||||
/// during fade/move transitions: the caller renders each scene individually, then blends
|
||||
/// the two results at the given alpha (0 = fully from, 1 = fully to).
|
||||
/// </summary>
|
||||
public static VideoFrame BlendFrames(VideoFrame from, VideoFrame to, float alpha)
|
||||
{
|
||||
if (from.Width != to.Width || from.Height != to.Height)
|
||||
{
|
||||
if (alpha <= 0.5f) return from;
|
||||
return to;
|
||||
}
|
||||
|
||||
var count = from.BgraPixels.Length;
|
||||
var result = new byte[count];
|
||||
var inv = 1f - alpha;
|
||||
|
||||
for (var i = 0; i < count; i += 4)
|
||||
{
|
||||
result[i] = (byte)(from.BgraPixels[i] * inv + to.BgraPixels[i] * alpha);
|
||||
result[i + 1] = (byte)(from.BgraPixels[i + 1] * inv + to.BgraPixels[i + 1] * alpha);
|
||||
result[i + 2] = (byte)(from.BgraPixels[i + 2] * inv + to.BgraPixels[i + 2] * alpha);
|
||||
result[i + 3] = 255;
|
||||
}
|
||||
|
||||
return new VideoFrame(from.Width, from.Height, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UniformToFill blit of a source frame into element rect (ex, ey, ew, eh), with
|
||||
/// straight-alpha source-over, optional round clip (true circle, hard edge) and
|
||||
|
||||
Reference in New Issue
Block a user