feat(65): the replay layer exists in the model; the exclusion problem is solved by the guard

Slice 5a, scaffolding only. 607 green.

- `Source.IsReplayLayer` + schema column + roundtrip: a media layer that shows saved clips instead of a
  chosen file. The compositor needs to know nothing — it is a media layer whose path the VM owns.
- **Two spec corrections from the creator (2026-11-10), recorded in the task:**
  1. The immediate replay of a fresh clip happens **only in the Live scene**. While it plays, **F10 is
     inert** ("replay in progress").
  2. That guard replaces the "layer excluded from its own clip" idea, which is **impossible** with one
     encoder and two muxes: the clip is a second mux of the same frames, so it necessarily contains what
     viewers saw. No second encode, no fiction about a clean feed.

Remaining for 65: the add-layer menu entry, `ClipSaved` → play (Live: once then hide; elsewhere: loop the
kept set), `SessionCompleted` → advance or hide, and silent-by-default audio.
This commit is contained in:
2026-11-10 12:48:41 -08:00
parent 28657918f9
commit a5c2061b55
5 changed files with 34 additions and 5 deletions
+3 -1
View File
@@ -117,7 +117,7 @@ public partial class LayoutStore : IDisposable
TextContent, TextFontFamily, TextFontSize, TextColor, TextBold, TextItalic,
TextUnderline, TextAlignment, TextVerticalAlignment, MediaMuted,
ChromaKeyEnabled, ChromaKeyColor, ChromaKeySimilarity, ChromaKeySoftness,
ChromaKeySpill, ChromaScreen
ChromaKeySpill, ChromaScreen, IsReplayLayer
FROM Source ORDER BY SortOrder
""";
using var reader = cmd.ExecuteReader();
@@ -198,6 +198,8 @@ public partial class LayoutStore : IDisposable
ChromaScreen = reader.IsDBNull(reader.GetOrdinal("ChromaScreen"))
? ChromaScreen.Green
: (ChromaScreen)reader.GetInt32(reader.GetOrdinal("ChromaScreen")),
IsReplayLayer = !reader.IsDBNull(reader.GetOrdinal("IsReplayLayer"))
&& reader.GetInt32(reader.GetOrdinal("IsReplayLayer")) != 0,
};
if (!sourcesByScene.TryGetValue(sceneId, out var list))
sourcesByScene[sceneId] = list = new List<(Source, int)>();
+9
View File
@@ -48,6 +48,7 @@ public partial class LayoutStore : IDisposable
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
IsMirrored INTEGER NOT NULL DEFAULT 0,
IsBackground INTEGER NOT NULL DEFAULT 0,
IsReplayLayer INTEGER NOT NULL DEFAULT 0,
CaptureKey TEXT,
SortOrder INTEGER NOT NULL DEFAULT 0
);
@@ -320,6 +321,14 @@ public partial class LayoutStore : IDisposable
cmd.CommandText = "ALTER TABLE Source ADD COLUMN ChatSuperChatColor TEXT NOT NULL DEFAULT '#eab308';";
cmd.ExecuteNonQuery();
}
// TASK 65: a media layer that plays the replay clips instead of a chosen file.
if (!columns.Contains("IsReplayLayer"))
{
using var cmd = _connection.CreateCommand();
cmd.CommandText = "ALTER TABLE Source ADD COLUMN IsReplayLayer INTEGER NOT NULL DEFAULT 0;";
cmd.ExecuteNonQuery();
}
if (!columns.Contains("ShowDesktop"))
{
using var cmd = _connection.CreateCommand();
+4 -2
View File
@@ -106,7 +106,7 @@ public partial class LayoutStore : IDisposable
TextContent, TextFontFamily, TextFontSize, TextColor, TextBold, TextItalic,
TextUnderline, TextAlignment, TextVerticalAlignment, MediaMuted,
ChromaKeyEnabled, ChromaKeyColor, ChromaKeySimilarity,
ChromaKeySoftness, ChromaKeySpill, ChromaScreen)
ChromaKeySoftness, ChromaKeySpill, ChromaScreen, IsReplayLayer)
VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled,
$x, $y, $w, $h, $opacity, $monitor,
$clip, $mirrored, $isBackground, $captureKey, $showDesktop, $sort,
@@ -118,7 +118,7 @@ public partial class LayoutStore : IDisposable
$textContent, $textFontFamily, $textFontSize, $textColor, $textBold, $textItalic,
$textUnderline, $textAlignment, $textVerticalAlignment, $mediaMuted,
$chromaEnabled, $chromaColor, $chromaSimilarity, $chromaSoftness,
$chromaSpill, $chromaScreen)
$chromaSpill, $chromaScreen, $isReplayLayer)
""";
cmd.Transaction = tx;
var idP = cmd.Parameters.Add("$id", SqliteType.Text);
@@ -158,6 +158,7 @@ public partial class LayoutStore : IDisposable
var chromaSoftnessP = cmd.Parameters.Add("$chromaSoftness", SqliteType.Real);
var chromaSpillP = cmd.Parameters.Add("$chromaSpill", SqliteType.Integer);
var chromaScreenP = cmd.Parameters.Add("$chromaScreen", SqliteType.Integer);
var replayLayerP = cmd.Parameters.Add("$isReplayLayer", SqliteType.Integer);
var mediaPlaybackStateP = cmd.Parameters.Add("$mediaPlaybackState", SqliteType.Text);
var alertVideoAssetIdP = cmd.Parameters.Add("$alertVideoAssetId", SqliteType.Text);
var alertVideoPathP = cmd.Parameters.Add("$alertVideoPath", SqliteType.Text);
@@ -226,6 +227,7 @@ public partial class LayoutStore : IDisposable
chromaSoftnessP.Value = source.ChromaKeySoftness;
chromaSpillP.Value = source.ChromaKeySpill ? 1 : 0;
chromaScreenP.Value = (int)source.ChromaScreen;
replayLayerP.Value = source.IsReplayLayer ? 1 : 0;
mediaPlaybackStateP.Value = source.MediaPlaybackState.ToString();
alertVideoAssetIdP.Value = (object?)source.AlertVideoAssetId ?? DBNull.Value;
alertVideoPathP.Value = (object?)source.AlertVideoPath ?? DBNull.Value;