Transitions: fade starts from the outgoing picture (was an instant cut)
The transition started with no from-frame, so BlendFrame returned the new scene at once. Start now requests a capture and the frame pump supplies its last output on the next tick. Test: TransitionFadeTests. Docs: ai.md, TASKS.md. Not pushed.
This commit is contained in:
@@ -32,6 +32,7 @@ public sealed class FramePump : IDisposable
|
|||||||
private readonly Func<VideoFrame?>? _alertTicker;
|
private readonly Func<VideoFrame?>? _alertTicker;
|
||||||
private readonly Func<VideoFrame?>? _brandFlash;
|
private readonly Func<VideoFrame?>? _brandFlash;
|
||||||
private readonly TransitionService? _transition;
|
private readonly TransitionService? _transition;
|
||||||
|
private VideoFrame? _lastOutput;
|
||||||
private readonly SceneGraph? _sceneGraph;
|
private readonly SceneGraph? _sceneGraph;
|
||||||
private readonly SceneCompositor _compositor = new();
|
private readonly SceneCompositor _compositor = new();
|
||||||
|
|
||||||
@@ -497,6 +498,9 @@ public sealed class FramePump : IDisposable
|
|||||||
socialBarTicks += sw.ElapsedTicks;
|
socialBarTicks += sw.ElapsedTicks;
|
||||||
|
|
||||||
VideoFrame frame;
|
VideoFrame frame;
|
||||||
|
if (_transition is { CaptureRequested: true } capture && _lastOutput != null)
|
||||||
|
capture.SupplyFromFrame(new VideoFrame(_lastOutput.Width, _lastOutput.Height,
|
||||||
|
(byte[])_lastOutput.BgraPixels.Clone()));
|
||||||
var scratchSize = compositorOptions.SourceRectWidth
|
var scratchSize = compositorOptions.SourceRectWidth
|
||||||
* compositorOptions.SourceRectHeight * 4;
|
* compositorOptions.SourceRectHeight * 4;
|
||||||
renderSw.Restart();
|
renderSw.Restart();
|
||||||
@@ -567,6 +571,7 @@ public sealed class FramePump : IDisposable
|
|||||||
// tick's buffers are recyclable once the enqueue snapshot them.
|
// tick's buffers are recyclable once the enqueue snapshot them.
|
||||||
// The free-list Contains guard keeps the Cut path (BlendFrame
|
// The free-list Contains guard keeps the Cut path (BlendFrame
|
||||||
// returns toFrame itself, aliasing scratch) safe.
|
// returns toFrame itself, aliasing scratch) safe.
|
||||||
|
_lastOutput = frame;
|
||||||
ReleaseScratch(frame.BgraPixels);
|
ReleaseScratch(frame.BgraPixels);
|
||||||
ReleaseScratch(scratch);
|
ReleaseScratch(scratch);
|
||||||
|
|
||||||
|
|||||||
@@ -45,12 +45,23 @@ public sealed class TransitionService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public VideoFrame? FromFrame { get; private set; }
|
public VideoFrame? FromFrame { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>True when a transition started without its "from" picture; the frame pump
|
||||||
|
/// supplies the outgoing program frame on its next tick via <see cref="SupplyFromFrame"/>.</summary>
|
||||||
|
public bool CaptureRequested { get; private set; }
|
||||||
|
|
||||||
|
public void SupplyFromFrame(VideoFrame frame)
|
||||||
|
{
|
||||||
|
FromFrame = frame;
|
||||||
|
CaptureRequested = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Begin a transition. Caller should capture FromFrame from the current LiveScene render.</summary>
|
/// <summary>Begin a transition. Caller should capture FromFrame from the current LiveScene render.</summary>
|
||||||
public void Start(TransitionType type, double durationMs, VideoFrame? fromFrame)
|
public void Start(TransitionType type, double durationMs, VideoFrame? fromFrame)
|
||||||
{
|
{
|
||||||
CurrentType = type;
|
CurrentType = type;
|
||||||
DurationMs = durationMs > 0 ? durationMs : DefaultDurationMs;
|
DurationMs = durationMs > 0 ? durationMs : DefaultDurationMs;
|
||||||
FromFrame = fromFrame;
|
FromFrame = fromFrame;
|
||||||
|
CaptureRequested = fromFrame == null && type != TransitionType.Cut;
|
||||||
ElapsedMs = 0;
|
ElapsedMs = 0;
|
||||||
Active = true;
|
Active = true;
|
||||||
|
|
||||||
|
|||||||
@@ -451,3 +451,14 @@ These were explicitly ruled out:
|
|||||||
- **OBS-style source expansion** (game capture, browser source, media playlist, VLC, color-key, MIDI — the minimal source set is deliberate; creators who need more have graduated to OBS)
|
- **OBS-style source expansion** (game capture, browser source, media playlist, VLC, color-key, MIDI — the minimal source set is deliberate; creators who need more have graduated to OBS)
|
||||||
- **Multi-layout** (auto-save, single layout; multi-layout = OBS territory)
|
- **Multi-layout** (auto-save, single layout; multi-layout = OBS territory)
|
||||||
- **Cross-process stream resume** (API makes it impossible; creator ruling 2026-09-01)
|
- **Cross-process stream resume** (API makes it impossible; creator ruling 2026-09-01)
|
||||||
|
|
||||||
|
## Transitions & brand logo (queued 2026-10-27)
|
||||||
|
|
||||||
|
- ✅ **Fade fix** — swaps fade from the outgoing picture (`TransitionFadeTests`). On-screen check pending.
|
||||||
|
- ☐ **Transition picker** in App Settings (type + duration).
|
||||||
|
- ☐ **Wipe** — CPU blend, direction setting.
|
||||||
|
- ☐ **Move** — persistent layers (per-layer "carry across scenes" flag), per-scene placement, fixed z band.
|
||||||
|
- ☐ **Brand logo** — permanent "Brand logo" layer above background in every scene; file in App Settings
|
||||||
|
(imported to the asset table); per-scene eye toggle; top-left default.
|
||||||
|
- ☐ **Cube** — GPU path gated on capability; CPU path only if it holds 60fps.
|
||||||
|
- Gear: right-click opens App Settings (built, on-screen check pending). Left-click menu stays for now.
|
||||||
|
|||||||
@@ -1287,3 +1287,12 @@ nothing. COPPA is very unlikely to bind (not child-directed), but note the enfor
|
|||||||
(FTC Sept 2025 *Apitor*: operators are responsible for what third-party components collect) —
|
(FTC Sept 2025 *Apitor*: operators are responsible for what third-party components collect) —
|
||||||
so the answer is a documented inquiry, and ours is "no third-party analytics or ad SDKs, no
|
so the answer is a documented inquiry, and ours is "no third-party analytics or ad SDKs, no
|
||||||
persisted chat or reward payloads."
|
persisted chat or reward payloads."
|
||||||
|
|
||||||
|
## Transitions (2026-10-27)
|
||||||
|
|
||||||
|
- **Fade starts from the outgoing picture.** `TransitionService.Start` with no "from" frame sets
|
||||||
|
`CaptureRequested`; the frame pump copies its last output at the top of its next tick and calls
|
||||||
|
`SupplyFromFrame`. Before that fix the blend had no starting frame and a scene swap was instant.
|
||||||
|
Guard: `ytLive.Tests/TransitionFadeTests.cs`.
|
||||||
|
- **Planned next (see TASKS.md):** transition picker in App Settings, Wipe, Move (persistent layers),
|
||||||
|
brand logo layer, GPU cube (capability-gated).
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using ytLive.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ytLive.Tests;
|
||||||
|
|
||||||
|
public class TransitionFadeTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Fade_Without_A_From_Frame_Requests_Capture_And_Blends_Once_Supplied()
|
||||||
|
{
|
||||||
|
var transition = new TransitionService();
|
||||||
|
var to = new VideoFrame(2, 2, Enumerable.Repeat((byte)255, 16).ToArray());
|
||||||
|
var from = new VideoFrame(2, 2, new byte[16]);
|
||||||
|
|
||||||
|
transition.Start(TransitionType.Fade, 300, null);
|
||||||
|
Assert.True(transition.CaptureRequested);
|
||||||
|
|
||||||
|
transition.SupplyFromFrame(from);
|
||||||
|
Assert.False(transition.CaptureRequested);
|
||||||
|
|
||||||
|
transition.Tick(150);
|
||||||
|
var blended = transition.BlendFrame(to);
|
||||||
|
Assert.NotSame(to, blended);
|
||||||
|
Assert.NotEqual((byte)255, blended.BgraPixels[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user