using ytLive.Models; namespace ytLive.Services; /// /// The three v1 transition types. Clicking a scene thumbnail triggers the selected /// transition; Go Live always uses Cut for a clean start. /// public enum TransitionType { Cut, Fade, Move } /// /// Manages the transition state between StagedScene and LiveScene. When a /// transition is active, FramePump calls each tick /// to get the blended output. When the transition completes, /// becomes false and FramePump reverts to a direct LiveScene render. /// public sealed class TransitionService { private const double DefaultDurationMs = 300; /// True while a transition is in progress. public bool Active { get; private set; } /// The transition type for the current in-progress transition. public TransitionType CurrentType { get; private set; } = TransitionType.Cut; /// Duration of the current transition in milliseconds. public double DurationMs { get; private set; } = DefaultDurationMs; /// Elapsed time in milliseconds (updated by caller). public double ElapsedMs { get; private set; } /// 0.0 = fully "from" scene, 1.0 = fully "to" scene. public float Progress => DurationMs <= 0 ? 1f : Math.Clamp((float)(ElapsedMs / DurationMs), 0f, 1f); /// /// The "from" frame (captured at transition start). Populated by the caller /// before starting the transition. /// public VideoFrame? FromFrame { get; private set; } /// Begin a transition. Caller should capture FromFrame from the current LiveScene render. 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; } } /// /// Advance the transition clock. Returns true while the transition is still active. /// public bool Tick(double elapsedMs) { if (!Active) return false; ElapsedMs += elapsedMs; if (ElapsedMs >= DurationMs) { ElapsedMs = DurationMs; Active = false; return false; } return true; } /// /// 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. /// 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); } }