feat(65): the replay layer plays — once in Live, looping everywhere else
608 green, 0 warnings. Completes the layer half of TASK 65. - **(+) → Replay Clip** adds an ordinary media layer with an app-owned path, one per scene, silent by default (`MediaMuted`) and looping only outside Live — the compositor needs no special case. - **F10 saves → the Live layer plays it once, then hides** (`SessionCompleted` → invisible, guard cleared). Every other scene with a replay layer keeps looping the kept set, advancing on each clip's completion, wrapping at the end. - **The guard the creator asked for:** while that Live replay is playing, **F10 is inert** with a plain explanation. That is what replaces the impossible "exclude the layer from its own clip" — with one encoder and two muxes the clip *is* the program, so the fix is to not clip a clip. `ReplayLayerTests` drives the real VM: one layer per scene, muted by default, once-through in Live, looping in BRB, and a stray F10 with clips off writes nothing.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Xunit;
|
||||
using ytLive.Models;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// TASK 65 slice 5 — the replay layer as a thing the creator can add, and the one guard that matters.
|
||||
///
|
||||
/// <para>The layer is an ordinary media layer with an app-owned path, so the test only has to prove the
|
||||
/// flags: one per scene, silent by default, once-through in Live and looping everywhere else. Playback
|
||||
/// itself rides the media manager that TASK 21 already covers.</para>
|
||||
/// </summary>
|
||||
[Collection("RealApp")]
|
||||
public sealed class ReplayLayerTests
|
||||
{
|
||||
private readonly RealAppHost _app;
|
||||
|
||||
public ReplayLayerTests(RealAppHost app) => _app = app;
|
||||
|
||||
[Fact]
|
||||
public void AddReplayLayer_IsOnePerScene_SilentByDefault_AndLoopsOutsideLive() => _app.Run(() =>
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-replay-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
try
|
||||
{
|
||||
var vm = new MainViewModel();
|
||||
|
||||
vm.StagedScene = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Live));
|
||||
vm.AddReplayLayer();
|
||||
vm.AddReplayLayer(); // second add is a no-op
|
||||
|
||||
var layers = vm.Scenes.SelectMany(s => s.Elements).OfType<Source>()
|
||||
.Where(s => s.IsReplayLayer).ToList();
|
||||
Assert.Single(layers);
|
||||
|
||||
var liveLayer = layers[0];
|
||||
Assert.True(liveLayer.MediaMuted); // silent by default
|
||||
Assert.False(liveLayer.MediaIsLooping); // Live plays once
|
||||
|
||||
// Any other scene loops the kept set.
|
||||
vm.StagedScene = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Brb));
|
||||
vm.AddReplayLayer();
|
||||
var brbLayer = vm.Scenes.SelectMany(s => s.Elements).OfType<Source>()
|
||||
.Single(s => s.IsReplayLayer && s != liveLayer);
|
||||
Assert.True(brbLayer.MediaIsLooping);
|
||||
Assert.True(brbLayer.MediaMuted);
|
||||
|
||||
// Clips off by default, and with no ring nothing is written by a stray F10.
|
||||
Assert.False(vm.ClipsEnabled);
|
||||
vm.SaveReplayClip();
|
||||
var clipsFolder = Path.Combine(Path.GetDirectoryName(tempDb)!, "clips");
|
||||
Assert.False(Directory.Exists(clipsFolder) && Directory.EnumerateFiles(clipsFolder).Any());
|
||||
}
|
||||
finally
|
||||
{
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best effort */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user