4b9a8e515f
- 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
155 lines
5.2 KiB
C#
155 lines
5.2 KiB
C#
using ytLive.Models;
|
|
|
|
namespace ytLive.Services;
|
|
|
|
/// <summary>
|
|
/// The three v1 transition types. Clicking a scene thumbnail triggers the selected
|
|
/// transition; Go Live always uses Cut for a clean start.
|
|
/// </summary>
|
|
public enum TransitionType
|
|
{
|
|
Cut,
|
|
Fade,
|
|
Move
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manages the transition state between StagedScene and LiveScene. When a
|
|
/// transition is active, FramePump calls <see cref="BlendFrame"/> each tick
|
|
/// to get the blended output. When the transition completes, <see cref="Active"/>
|
|
/// becomes false and FramePump reverts to a direct LiveScene render.
|
|
/// </summary>
|
|
public sealed class TransitionService
|
|
{
|
|
private const double DefaultDurationMs = 300;
|
|
|
|
/// <summary>True while a transition is in progress.</summary>
|
|
public bool Active { get; private set; }
|
|
|
|
/// <summary>The transition type for the current in-progress transition.</summary>
|
|
public TransitionType CurrentType { get; private set; } = TransitionType.Cut;
|
|
|
|
/// <summary>Duration of the current transition in milliseconds.</summary>
|
|
public double DurationMs { get; private set; } = DefaultDurationMs;
|
|
|
|
/// <summary>Elapsed time in milliseconds (updated by caller).</summary>
|
|
public double ElapsedMs { get; private set; }
|
|
|
|
/// <summary>0.0 = fully "from" scene, 1.0 = fully "to" scene.</summary>
|
|
public float Progress => DurationMs <= 0 ? 1f
|
|
: Math.Clamp((float)(ElapsedMs / DurationMs), 0f, 1f);
|
|
|
|
/// <summary>
|
|
/// The "from" frame (captured at transition start). Populated by the caller
|
|
/// before starting the transition.
|
|
/// </summary>
|
|
public VideoFrame? FromFrame { get; private set; }
|
|
|
|
/// <summary>Begin a transition. Caller should capture FromFrame from the current LiveScene render.</summary>
|
|
public void Start(TransitionType type, double durationMs, VideoFrame? fromFrame)
|
|
{
|
|
CurrentType = type;
|
|
DurationMs = durationMs > 0 ? durationMs : DefaultDurationMs;
|
|
FromFrame = fromFrame;
|
|
ElapsedMs = 0;
|
|
Active = true;
|
|
|
|
if (type == TransitionType.Cut)
|
|
{
|
|
Active = false;
|
|
ElapsedMs = DurationMs;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Advance the transition clock. Returns true while the transition is still active.
|
|
/// </summary>
|
|
public bool Tick(double elapsedMs)
|
|
{
|
|
if (!Active) return false;
|
|
ElapsedMs += elapsedMs;
|
|
if (ElapsedMs >= DurationMs)
|
|
{
|
|
ElapsedMs = DurationMs;
|
|
Active = false;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Blend the from-frame and the to-frame based on the current progress.
|
|
/// Caller passes the freshly-rendered "to" scene; we return the blended output.
|
|
/// </summary>
|
|
public VideoFrame BlendFrame(VideoFrame toFrame)
|
|
{
|
|
if (FromFrame == null || CurrentType == TransitionType.Cut)
|
|
return toFrame;
|
|
|
|
var t = Progress;
|
|
return CurrentType switch
|
|
{
|
|
TransitionType.Fade => BlendFade(FromFrame, toFrame, t),
|
|
TransitionType.Move => BlendMove(FromFrame, toFrame, t),
|
|
_ => toFrame
|
|
};
|
|
}
|
|
|
|
private static VideoFrame BlendFade(VideoFrame from, VideoFrame to, float t)
|
|
{
|
|
var w = from.Width;
|
|
var h = from.Height;
|
|
var src = from.BgraPixels;
|
|
var dst = to.BgraPixels;
|
|
var outPixels = new byte[src.Length];
|
|
|
|
for (var i = 0; i < outPixels.Length; i += 4)
|
|
{
|
|
outPixels[i] = (byte)(src[i] * (1 - t) + dst[i] * t);
|
|
outPixels[i + 1] = (byte)(src[i + 1] * (1 - t) + dst[i + 1] * t);
|
|
outPixels[i + 2] = (byte)(src[i + 2] * (1 - t) + dst[i + 2] * t);
|
|
outPixels[i + 3] = 255;
|
|
}
|
|
|
|
return new VideoFrame(w, h, outPixels);
|
|
}
|
|
|
|
private static VideoFrame BlendMove(VideoFrame from, VideoFrame to, float t)
|
|
{
|
|
var w = from.Width;
|
|
var h = from.Height;
|
|
var src = from.BgraPixels;
|
|
var dst = to.BgraPixels;
|
|
var outPixels = new byte[src.Length];
|
|
var offset = (int)(w * (1 - t));
|
|
|
|
for (var y = 0; y < h; y++)
|
|
{
|
|
for (var x = 0; x < w; x++)
|
|
{
|
|
var di = (y * w + x) * 4;
|
|
var fromX = x - offset;
|
|
var toX = x + offset;
|
|
|
|
var srcPixel = fromX >= 0 && fromX < w
|
|
? (src[(y * w + fromX) * 4], src[(y * w + fromX) * 4 + 1], src[(y * w + fromX) * 4 + 2])
|
|
: ((byte)0, (byte)0, (byte)0);
|
|
|
|
var dstPixel = toX >= 0 && toX < w
|
|
? (dst[(y * w + toX) * 4], dst[(y * w + toX) * 4 + 1], dst[(y * w + toX) * 4 + 2])
|
|
: ((byte)0, (byte)0, (byte)0);
|
|
|
|
var edgeFalloff = Math.Clamp((float)(offset) / (w * 0.3f), 0, 1);
|
|
var lerpF = Math.Clamp(1f - edgeFalloff, 0, 1);
|
|
|
|
outPixels[di] = (byte)(srcPixel.Item1 * (1 - lerpF) + dstPixel.Item1 * lerpF);
|
|
outPixels[di + 1] = (byte)(srcPixel.Item2 * (1 - lerpF) + dstPixel.Item2 * lerpF);
|
|
outPixels[di + 2] = (byte)(srcPixel.Item3 * (1 - lerpF) + dstPixel.Item3 * lerpF);
|
|
outPixels[di + 3] = 255;
|
|
}
|
|
}
|
|
|
|
return new VideoFrame(w, h, outPixels);
|
|
}
|
|
}
|