TASK 21 Increment A: media-source model + persistence (schema migration + round-trip test)
SourceType.MediaSource added; MediaSourceType (Video/Audio) and MediaPlaybackState (Stopped/Playing/Paused) enums. Source gains MediaPath, MediaIsLooping, MediaVolume (0-1 clamped), MediaPlaybackState. LayoutStore: MigrateSourceTable adds 4 guarded ALTERs (MediaPath, MediaIsLooping, MediaVolume, MediaPlaybackState); SELECT appends indices 26-29, INSERT adds 4 columns + params. Round-trip test MediaSource_Config_Survives_Save_And_Reload passes. Build 0 warnings, 245/247 tests (2 known pre-existing). Gated on creator run for later integration (ffmpeg).
This commit is contained in:
+37
-57
@@ -2,75 +2,55 @@
|
|||||||
|
|
||||||
## Branch / Commit State
|
## Branch / Commit State
|
||||||
|
|
||||||
**`main`**, **working tree CLEAN.** TASK 18 (record-only pipeline + top-bar pills/light/Start-End button UX
|
**`main`**, last pushed commit **`46f4696`** (manual-rename modal). Working tree is **DIRTY** —
|
||||||
+ Sign In label/spacing) **plus** the memory-map/derived-solution fix **and** the README hero image were
|
TASK 21 Increment A (model + persistence for media sources) is fully implemented, verified, and
|
||||||
committed and pushed as **`a9eb360`** (2026-08-29). `origin/main` and head both at **`a9eb360`**.
|
ready to commit. See "In flight" below.
|
||||||
|
|
||||||
No feature branches pre-1.0: all work lands on `main` per work unit.
|
No feature branches pre-1.0: all work lands on `main` per work unit.
|
||||||
|
|
||||||
## Memory-map corrections (2026-08-29 — "don't re-derive" fix)
|
## In flight: TASK 21 — next bounded slice = Increment B (FFmpeg video decoder, items 4–5)
|
||||||
|
|
||||||
- **Incident:** the image-shrink recipe (for `docs/ytLlive-preview.jpg`) was never recorded when first
|
**What is DONE (uncommitted, ready to commit):** Increment A — `SourceType.MediaSource` enum,
|
||||||
done, so it was re-derived from scratch. Fixed by:
|
`MediaSourceType`/`MediaPlaybackState` enums, media config fields on `Source` (`MediaPath`,
|
||||||
- **`AGENTS.md`** → Working rules: new **🔬 Derived-solution rule** (record a one-off solution in
|
`MediaIsLooping`, `MediaVolume`, `MediaPlaybackState`), LayoutStore migration + SELECT + INSERT,
|
||||||
`MyMistakes.md` the same session; grep it first on any "did this before?" wall).
|
round-trip test (`MediaSource_Config_Survives_Save_And_Reload`). Build 0 warnings, 245/247 tests
|
||||||
- **`MyMistakes.md`** → contract expanded: per-task failure log **+ RECIPES REGISTRY** (permanent, NOT
|
(2 known failures), scope check clean.
|
||||||
truncated); recorded the verified image-shrink recipe.
|
|
||||||
- **`schema.md`** → file inventory now rows `MyMistakes.md` as the recipes registry.
|
|
||||||
- **`ai.md`** → Derivative-work section notes the derived-solution rule (registry reachable from map home).
|
|
||||||
- **`README.md`** + `docs/ytLlive-preview.jpg` → README hero image replaced (screenshot downscaled to
|
|
||||||
1400×794, 188KB JPEG).
|
|
||||||
|
|
||||||
|
**What is NOT started (Increment B):** `MediaVideoDecoder` — FFmpeg-based video decoder → `VideoFrame`.
|
||||||
|
Needs ffmpeg present on Windows PATH or in `%APPDATA%\ytLlive\tools` to test. Currently absent:
|
||||||
|
neither `ffmpeg.exe` on PATH nor in tools cache (locator's network pull hasn't run). This is a
|
||||||
|
**run-the-app wall** — not blocked from writing the code, but blocked from headless integration
|
||||||
|
testing. Can be shipped as a compile-verified code commit gated on creator run, or deferred until
|
||||||
|
ffmpeg is present.
|
||||||
|
|
||||||
## TASK 18 — Local recording (shipped `a9eb360`, 2026-08-29)
|
## Landmines (unchanged)
|
||||||
|
|
||||||
**Shipped (committed + pushed):**
|
|
||||||
- `EncoderOptions` + `FfmpegArgs` + `FfmpegEncoder`: independent `StreamEnabled`/`RecordEnabled`/
|
|
||||||
`RecordPath`; per-output blocks (`-f flv` stream, `-f mp4` record); encoder throws unless ≥1 output.
|
|
||||||
- `MainViewModel`: REC + ON-AIR pill toggles, status lights, dynamic `PrimaryButtonText`
|
|
||||||
(Start Streaming / Start Recording), `ShowPrimaryStartButton`/`ShowLogInButton`/`ShowEndStreamButton`,
|
|
||||||
`CanStartSession`, `AccountStatusLightToolTip`; `IsEditMode` += `!IsRecording`.
|
|
||||||
- `StartSession()` routes: ON-AIR → GoLive dialog then stream (± record); only-REC → `BeginRecordOnly()`
|
|
||||||
(no YouTube dialog). `StopStream()` no longer signs the creator out (explicit logout only) and runs
|
|
||||||
**rename-on-stop** to `ty-<yyyymmdd>-<HHmm start>-<hh2mm2 length>.mp4`.
|
|
||||||
- `Services/RecordingFile` (pure): auto-name `ty-…-0000.mp4` → rename-on-stop with hh2mm2 length.
|
|
||||||
- `LayoutStore.Load/SaveRecordFolder` + `ChooseRecordFolderCommand` (`OpenFolderDialog`), default
|
|
||||||
`%APPDATA%\ytLlive\recordings\`.
|
|
||||||
- `MainWindow.xaml`: REC/ON-AIR pill `ToggleButton`s + status lights, dynamic button, status-light tooltip,
|
|
||||||
About overlay border 520→650 / MaxHeight 560→700 / padding 24→28, About grid background `#171928`.
|
|
||||||
- Tests: `FfmpegEncoderTests` record-only + dual-output arg tests; `RecordingFileTests` (naming rules).
|
|
||||||
|
|
||||||
**Not done / parked:** manual rename dialog for the user (planned as a modal, needs a trigger since the
|
|
||||||
toast service has no actions) — parked; running-app verification of rename + dual output.
|
|
||||||
|
|
||||||
## Key facts / landmines
|
|
||||||
|
|
||||||
- **Pre-existing test failures (do not chase):** `AudioPipelineTests.Mix_HonorsProviderGains…` +
|
- **Pre-existing test failures (do not chase):** `AudioPipelineTests.Mix_HonorsProviderGains…` +
|
||||||
`RoundClipInteractionTests.Round_Clip_Corner_Is_Grabbable…` — fail on `main` with this change unapplied.
|
`RoundClipInteractionTests.Round_Clip_Corner_Is_Grabbable…`.
|
||||||
All other tests pass (incl. the 7 new recording tests).
|
|
||||||
- **App running locks `ytLive.exe`** → build fails MSB3027 until the creator exits the app.
|
- **App running locks `ytLive.exe`** → build fails MSB3027 until the creator exits the app.
|
||||||
- **Startup diagnostics:** `%APPDATA%\ytLlive\startup.log` (`Helpers/AppLog.cs`).
|
|
||||||
- **Real-MainWindow tests MUST set `LayoutPathOverride`** — DB pollution wiped real data once.
|
- **Real-MainWindow tests MUST set `LayoutPathOverride`** — DB pollution wiped real data once.
|
||||||
- **Good Dog Rule:** ONE integration test per change; no feature branches pre-1.0.
|
- **Pre-commit gate:** run `./scripts/verify.sh <scope...>` — clean build (0 warnings) + full tests +
|
||||||
- **Scope Lock:** declared list below; run `scripts/scope-check.sh` before committing.
|
scope check. Do NOT trust an incremental build's "0 warnings".
|
||||||
- **Every feature change ships memory updates in the SAME commit** — `TASKS.md` status + this file.
|
- **Good Dog Rule:** ONE integration test per change.
|
||||||
- **Pre-commit gate:** run `./scripts/verify.sh <scope...>` — clean build (0 warnings) + full tests + scope check. Do NOT trust an incremental build's "0 warnings".
|
- **🔬 Derived-solution rule:** record one-off solutions in `MyMistakes.md` recipes registry same session.
|
||||||
- No `CODING.md`; conventions live in `AGENTS.md` + `ai.md`.
|
- Startup diagnostics: `%APPDATA%\ytLlive\startup.log`.
|
||||||
|
- Media-source feature is large (12 items); each later slice (decode, compositor wiring, loop/volume,
|
||||||
|
UI) is its own bounded increment — decoder needs ffmpeg present, can't be headless-tested unless
|
||||||
|
ffmpeg is installed (currently absent from Windows PATH + empty tools cache).
|
||||||
|
|
||||||
## Commit scope (declared)
|
## Todo state
|
||||||
|
|
||||||
`Services/Encoder/EncoderOptions.cs Services/Encoder/FfmpegArgs.cs Services/Encoder/FfmpegEncoder.cs
|
- TASK 18: fully shipped (`a9eb360` recording, `c8374bd` docs, `be529a4` verify.sh, `46f4696` rename
|
||||||
Services/Encoder/FramePump.cs Services/RecordingFile.cs Services/LayoutStore.cs
|
modal). Only remaining = **running-app verification** (creator must run the WPF app).
|
||||||
ViewModels/MainViewModel.cs MainWindow.xaml ytLive.Tests/FfmpegEncoderTests.cs
|
- TASK 21: Increment A (model + persistence) done and ready to commit. Increment B (ffmpeg decoder)
|
||||||
ytLive.Tests/FramePumpTests.cs ytLive.Tests/RecordingFileTests.cs
|
is the next bounded slice — compile-verifiable without ffmpeg, but integration test is walled.
|
||||||
AGENTS.md TASKS.md ai.md HANDOFF.md MyMistakes.md schema.md README.md
|
- TASK 22 (audio sync offset): not started.
|
||||||
docs/ytLlive-preview.jpg docs/ytLlive-preview.png(deleted)`
|
|
||||||
|
|
||||||
## Next steps
|
## Next steps
|
||||||
|
|
||||||
1. **Running-app verification** of TASK 18 (the one real gap): confirm rename-on-stop produces
|
1. Commit + push Increment A (the code is ready, gates passed, TASKS/HANDOFF docs updated).
|
||||||
`ty-…-<len>.mp4` and dual stream+record output actually works live. Requires the creator to run the app.
|
2. Commit + push the handoff correction (HANDOFF says done, not partial).
|
||||||
2. ~~Manual rename modal~~ **DONE** — `RenameRecordingDialog` (themed modal, pre-filled auto-stem,
|
3. Running-app verification of TASK 18 rename/dual-output (creator) — one of two run-app walls.
|
||||||
OK/Enter / Cancel/Esc, invalid-char + empty = keep-auto validation) shown at finalize in
|
4. Increment B: write `MediaVideoDecoder` (FFmpeg → BGRA8 `VideoFrame`), compile-verified, gated on
|
||||||
`FinalizeRecordingAsync`; reuses `YtTextBox`/`YtButton` styles.
|
creator run for integration test (ffmpeg not present headlessly).
|
||||||
3. Continue queue: TASK 21 media source, TASK 22 audio sync offset.
|
5. Later TASK 21 slices + TASK 22.
|
||||||
|
|||||||
+59
-1
@@ -12,7 +12,23 @@ public enum SourceType
|
|||||||
TextOverlay,
|
TextOverlay,
|
||||||
Countdown,
|
Countdown,
|
||||||
WebSource,
|
WebSource,
|
||||||
ChatBox
|
ChatBox,
|
||||||
|
MediaSource
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Distinguishes how a media source presents its content (TASK 21).</summary>
|
||||||
|
public enum MediaSourceType
|
||||||
|
{
|
||||||
|
Video,
|
||||||
|
Audio
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Playback state of a media source (TASK 21).</summary>
|
||||||
|
public enum MediaPlaybackState
|
||||||
|
{
|
||||||
|
Stopped,
|
||||||
|
Playing,
|
||||||
|
Paused
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ClipShape
|
public enum ClipShape
|
||||||
@@ -113,6 +129,48 @@ public class Source : SceneElement
|
|||||||
private string? _webUri;
|
private string? _webUri;
|
||||||
public string? WebUri { get => _webUri; set => Set(ref _webUri, value); }
|
public string? WebUri { get => _webUri; set => Set(ref _webUri, value); }
|
||||||
|
|
||||||
|
// Media source config (TASK 21: video/audio file playback)
|
||||||
|
private MediaSourceType _mediaType = MediaSourceType.Video;
|
||||||
|
public MediaSourceType MediaType
|
||||||
|
{
|
||||||
|
get => _mediaType;
|
||||||
|
set => Set(ref _mediaType, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? _mediaPath;
|
||||||
|
public string? MediaPath
|
||||||
|
{
|
||||||
|
get => _mediaPath;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (Set(ref _mediaPath, value))
|
||||||
|
Raise(nameof(IsMediaSource));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _mediaIsLooping;
|
||||||
|
public bool MediaIsLooping
|
||||||
|
{
|
||||||
|
get => _mediaIsLooping;
|
||||||
|
set => Set(ref _mediaIsLooping, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _mediaVolume = 1.0;
|
||||||
|
public double MediaVolume
|
||||||
|
{
|
||||||
|
get => _mediaVolume;
|
||||||
|
set => Set(ref _mediaVolume, Math.Clamp(value, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaPlaybackState _mediaPlaybackState = MediaPlaybackState.Stopped;
|
||||||
|
public MediaPlaybackState MediaPlaybackState
|
||||||
|
{
|
||||||
|
get => _mediaPlaybackState;
|
||||||
|
set => Set(ref _mediaPlaybackState, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMediaSource => Type == SourceType.MediaSource;
|
||||||
|
|
||||||
// Chat box config (TASK 3.18)
|
// Chat box config (TASK 3.18)
|
||||||
private double _chatFontSize = 16;
|
private double _chatFontSize = 16;
|
||||||
public double ChatFontSize
|
public double ChatFontSize
|
||||||
|
|||||||
+45
-3
@@ -281,6 +281,32 @@ public class LayoutStore : IDisposable
|
|||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TASK 21: Media source (video/audio file playback)
|
||||||
|
if (!columns.Contains("MediaPath"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN MediaPath TEXT;";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
if (!columns.Contains("MediaIsLooping"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN MediaIsLooping INTEGER NOT NULL DEFAULT 0;";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
if (!columns.Contains("MediaVolume"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN MediaVolume REAL NOT NULL DEFAULT 1;";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
if (!columns.Contains("MediaPlaybackState"))
|
||||||
|
{
|
||||||
|
using var cmd = _connection.CreateCommand();
|
||||||
|
cmd.CommandText = "ALTER TABLE Source ADD COLUMN MediaPlaybackState TEXT NOT NULL DEFAULT 'Stopped';";
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
// Rename legacy "Backdrop" display name to "Background" in existing rows
|
// Rename legacy "Backdrop" display name to "Background" in existing rows
|
||||||
using (var cmd = _connection.CreateCommand())
|
using (var cmd = _connection.CreateCommand())
|
||||||
{
|
{
|
||||||
@@ -599,7 +625,7 @@ public class LayoutStore : IDisposable
|
|||||||
IsBackground, CaptureKey, ShowDesktop,
|
IsBackground, CaptureKey, ShowDesktop,
|
||||||
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
|
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
|
||||||
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor,
|
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor,
|
||||||
WebUri
|
WebUri, MediaPath, MediaIsLooping, MediaVolume, MediaPlaybackState
|
||||||
FROM Source ORDER BY SortOrder
|
FROM Source ORDER BY SortOrder
|
||||||
""";
|
""";
|
||||||
using var reader = cmd.ExecuteReader();
|
using var reader = cmd.ExecuteReader();
|
||||||
@@ -637,6 +663,14 @@ public class LayoutStore : IDisposable
|
|||||||
ChatMemberColor = reader.IsDBNull(23) ? "#e94560" : reader.GetString(23),
|
ChatMemberColor = reader.IsDBNull(23) ? "#e94560" : reader.GetString(23),
|
||||||
ChatSuperChatColor = reader.IsDBNull(24) ? "#eab308" : reader.GetString(24),
|
ChatSuperChatColor = reader.IsDBNull(24) ? "#eab308" : reader.GetString(24),
|
||||||
WebUri = reader.IsDBNull(25) ? null : reader.GetString(25),
|
WebUri = reader.IsDBNull(25) ? null : reader.GetString(25),
|
||||||
|
MediaPath = reader.IsDBNull(26) ? null : reader.GetString(26),
|
||||||
|
MediaIsLooping = reader.IsDBNull(27) ? false : reader.GetInt32(27) != 0,
|
||||||
|
MediaVolume = reader.IsDBNull(28) ? 1.0 : reader.GetDouble(28),
|
||||||
|
MediaPlaybackState = reader.IsDBNull(29)
|
||||||
|
? MediaPlaybackState.Stopped
|
||||||
|
: Enum.TryParse<MediaPlaybackState>(reader.GetString(29), out var ps)
|
||||||
|
? ps
|
||||||
|
: MediaPlaybackState.Stopped,
|
||||||
};
|
};
|
||||||
if (!sourcesByScene.TryGetValue(sceneId, out var list))
|
if (!sourcesByScene.TryGetValue(sceneId, out var list))
|
||||||
sourcesByScene[sceneId] = list = new List<Source>();
|
sourcesByScene[sceneId] = list = new List<Source>();
|
||||||
@@ -871,13 +905,13 @@ public class LayoutStore : IDisposable
|
|||||||
ClipShape, IsMirrored, IsBackground, CaptureKey, ShowDesktop, SortOrder,
|
ClipShape, IsMirrored, IsBackground, CaptureKey, ShowDesktop, SortOrder,
|
||||||
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
|
ChatFontSize, ChatFontColor, ChatBackgroundColor, ChatMaxMessages,
|
||||||
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor,
|
ChatShowTimestamps, ChatShowBadges, ChatMemberColor, ChatSuperChatColor,
|
||||||
WebUri)
|
WebUri, MediaPath, MediaIsLooping, MediaVolume, MediaPlaybackState)
|
||||||
VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled,
|
VALUES ($id, $sceneId, $assetId, $type, $name, $isEnabled,
|
||||||
$x, $y, $w, $h, $opacity, $monitor,
|
$x, $y, $w, $h, $opacity, $monitor,
|
||||||
$clip, $mirrored, $isBackground, $captureKey, $showDesktop, $sort,
|
$clip, $mirrored, $isBackground, $captureKey, $showDesktop, $sort,
|
||||||
$chatFontSize, $chatFontColor, $chatBgColor, $chatMaxMsgs,
|
$chatFontSize, $chatFontColor, $chatBgColor, $chatMaxMsgs,
|
||||||
$chatTimestamps, $chatBadges, $chatMemberColor, $chatSuperChatColor,
|
$chatTimestamps, $chatBadges, $chatMemberColor, $chatSuperChatColor,
|
||||||
$webUri)
|
$webUri, $mediaPath, $mediaIsLooping, $mediaVolume, $mediaPlaybackState)
|
||||||
""";
|
""";
|
||||||
cmd.Transaction = tx;
|
cmd.Transaction = tx;
|
||||||
var idP = cmd.Parameters.Add("$id", SqliteType.Text);
|
var idP = cmd.Parameters.Add("$id", SqliteType.Text);
|
||||||
@@ -907,6 +941,10 @@ public class LayoutStore : IDisposable
|
|||||||
var chatMemberColorP = cmd.Parameters.Add("$chatMemberColor", SqliteType.Text);
|
var chatMemberColorP = cmd.Parameters.Add("$chatMemberColor", SqliteType.Text);
|
||||||
var chatSuperChatColorP = cmd.Parameters.Add("$chatSuperChatColor", SqliteType.Text);
|
var chatSuperChatColorP = cmd.Parameters.Add("$chatSuperChatColor", SqliteType.Text);
|
||||||
var webUriP = cmd.Parameters.Add("$webUri", SqliteType.Text);
|
var webUriP = cmd.Parameters.Add("$webUri", SqliteType.Text);
|
||||||
|
var mediaPathP = cmd.Parameters.Add("$mediaPath", SqliteType.Text);
|
||||||
|
var mediaIsLoopingP = cmd.Parameters.Add("$mediaIsLooping", SqliteType.Integer);
|
||||||
|
var mediaVolumeP = cmd.Parameters.Add("$mediaVolume", SqliteType.Real);
|
||||||
|
var mediaPlaybackStateP = cmd.Parameters.Add("$mediaPlaybackState", SqliteType.Text);
|
||||||
|
|
||||||
foreach (var scene in scenes)
|
foreach (var scene in scenes)
|
||||||
{
|
{
|
||||||
@@ -941,6 +979,10 @@ public class LayoutStore : IDisposable
|
|||||||
chatMemberColorP.Value = source.ChatMemberColor;
|
chatMemberColorP.Value = source.ChatMemberColor;
|
||||||
chatSuperChatColorP.Value = source.ChatSuperChatColor;
|
chatSuperChatColorP.Value = source.ChatSuperChatColor;
|
||||||
webUriP.Value = (object?)source.WebUri ?? DBNull.Value;
|
webUriP.Value = (object?)source.WebUri ?? DBNull.Value;
|
||||||
|
mediaPathP.Value = (object?)source.MediaPath ?? DBNull.Value;
|
||||||
|
mediaIsLoopingP.Value = source.MediaIsLooping ? 1 : 0;
|
||||||
|
mediaVolumeP.Value = source.MediaVolume;
|
||||||
|
mediaPlaybackStateP.Value = source.MediaPlaybackState.ToString();
|
||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1035,11 +1035,11 @@ The tasks below are ordered by dependency and risk. Each task builds on the prev
|
|||||||
|
|
||||||
**Goal:** play video files (MP4, MOV, AVI) into scenes — starting soon videos, BRB loops, intro/outro clips.
|
**Goal:** play video files (MP4, MOV, AVI) into scenes — starting soon videos, BRB loops, intro/outro clips.
|
||||||
|
|
||||||
### Status: ☐ Not started — required for v1
|
### Status: 🔶 In progress — Increment A shipped (model + persistence), slices 4–12 open
|
||||||
|
|
||||||
1. ☐ `MediaSourceType` enum: `Video`, `Audio` (audio-only files via media source)
|
1. ✅ `MediaSourceType` enum: `Video`, `Audio` (audio-only files via media source)
|
||||||
2. ☐ `SourceType.MediaSource` addition to the enum
|
2. ✅ `SourceType.MediaSource` addition to the enum
|
||||||
3. ☐ `MediaSourceModel`: `FilePath`, `IsLooping`, `Volume` (0-1), `PlaybackState`
|
3. ✅ `MediaSourceModel`: `MediaPath`, `MediaIsLooping`, `MediaVolume` (0-1), `MediaPlaybackState` — persisted in LayoutStore (schema migration + SELECT/INSERT + round-trip test)
|
||||||
4. ☐ `VideoFrameSource`: FFmpeg-based video decoder → `VideoFrame` pipeline
|
4. ☐ `VideoFrameSource`: FFmpeg-based video decoder → `VideoFrame` pipeline
|
||||||
5. ☐ Frame capture from video file (decode at native FPS, output BGRA8 frames)
|
5. ☐ Frame capture from video file (decode at native FPS, output BGRA8 frames)
|
||||||
6. ☐ Wire into `FramePump` resolver — `Source { Type: MediaSource }` → latest video frame
|
6. ☐ Wire into `FramePump` resolver — `Source { Type: MediaSource }` → latest video frame
|
||||||
|
|||||||
@@ -388,4 +388,47 @@ public class LayoutStorePersistenceTests
|
|||||||
try { File.Delete(path); } catch { /* best-effort cleanup */ }
|
try { File.Delete(path); } catch { /* best-effort cleanup */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TASK 21: media-source config (Path, Looping, Volume, PlaybackState) must
|
||||||
|
// survive a save + reload through the full DELETE-all/re-insert cycle.
|
||||||
|
[Fact]
|
||||||
|
public void MediaSource_Config_Survives_Save_And_Reload()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var store = new LayoutStore(path);
|
||||||
|
var scene = new Scene { Name = "Starting" };
|
||||||
|
var source = new Source
|
||||||
|
{
|
||||||
|
Name = "Starting Loop",
|
||||||
|
Type = SourceType.MediaSource,
|
||||||
|
Width = 1920,
|
||||||
|
Height = 1080,
|
||||||
|
MediaPath = @"C:\videos\starting.mp4",
|
||||||
|
MediaIsLooping = true,
|
||||||
|
MediaVolume = 0.75,
|
||||||
|
MediaPlaybackState = MediaPlaybackState.Playing,
|
||||||
|
};
|
||||||
|
scene.Elements.Add(source);
|
||||||
|
|
||||||
|
store.Save(new[] { scene }, null, null);
|
||||||
|
|
||||||
|
var reloaded = store.Load();
|
||||||
|
var loaded = Assert.Single(reloaded[0].Elements);
|
||||||
|
var loadedSource = Assert.IsType<Source>(loaded);
|
||||||
|
|
||||||
|
Assert.Equal(SourceType.MediaSource, loadedSource.Type);
|
||||||
|
Assert.True(loadedSource.IsMediaSource);
|
||||||
|
Assert.Equal(@"C:\videos\starting.mp4", loadedSource.MediaPath);
|
||||||
|
Assert.True(loadedSource.MediaIsLooping);
|
||||||
|
Assert.Equal(0.75, loadedSource.MediaVolume);
|
||||||
|
Assert.Equal(MediaPlaybackState.Playing, loadedSource.MediaPlaybackState);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
try { File.Delete(path); } catch { /* best-effort cleanup */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user