A video file was picture-only: the decode pipe passed `-an`, so a starting-soon or BRB clip with music played silent everywhere. Adds sound to every clip and collapses the two decoders into one. - `Services/VideoClipPipe.cs`: the shared pipe behind `MediaVideoSource` (TASK 21) and `AlertClipDecoder` (TASK 47) — video (`rawvideo bgra`, scaled) + audio (`f32le -ac 2 -ar 48000`, 50 ms chunks, byte-carry across reads), both paced to real time, loop restarts both so picture and sound stay together. Alert behaviour is unchanged. - `MediaVideoSourceManager` applies the layer's Level/Mute per session and raises `AudioAvailable`; `MainViewModel` feeds it to the new `AudioMixer.EnqueueDesktopMediaAudio`, which joins the DESKTOP/GAME ring so the desktop fader and the mic auto-ducker apply exactly as they do for game audio and TRAX (creator ruling 2026-11-05: audio joins the group). - Per-layer `MediaMuted` + existing `MediaVolume` surfaced as Mute/Level in the properties panel; `MediaMuted` persisted (ALTER TABLE guard + SELECT/INSERT). - Level/mute are per SESSION because one decoder is shared per file path — the last layer touched wins; one decoder per layer would play the file twice. Test: `MediaSourceAudioTests` — the pipe emits 50 ms chunks at 48k stereo, the manager scales by level and drops when muted, and a media layer's sound moves the desktop meter. Alert/decode suites unchanged and green. Docs: task-21 closed on item 10, the "silent soundtrack" facts corrected in frame-pipeline/output/overlays, HANDOFF rewritten. verify.sh green (529 passed; the two documented pre-existing failures only).
34 lines
1.3 KiB
C#
34 lines
1.3 KiB
C#
using ytLive.Services.Audio;
|
|
|
|
namespace ytLive.Services;
|
|
|
|
/// <summary>
|
|
/// TASK 21: a media file decoder exposed to <see cref="MediaVideoSourceManager"/>.
|
|
/// Mirrors <c>IScreenCaptureSource</c>/<c>ICameraFrameSource</c> so the manager
|
|
/// (and tests) treat a local video file like any other frame source: start/stop
|
|
/// and a per-frame <see cref="FrameAvailable"/> event consumers coalesce.
|
|
/// </summary>
|
|
public interface IMediaFrameSource : IDisposable
|
|
{
|
|
string Key { get; }
|
|
|
|
/// <summary>When true, the decoder restarts from the beginning on natural EOF
|
|
/// instead of raising <see cref="Completed"/> (used for BRB/starting-soon loops).</summary>
|
|
bool Looping { get; set; }
|
|
|
|
/// <summary>Raised (on a worker thread) for each decoded frame.</summary>
|
|
event Action<VideoFrame>? FrameAvailable;
|
|
|
|
/// <summary>Raised (on a worker thread) per chunk of the file's own soundtrack
|
|
/// (48 kHz stereo float). The manager applies the layer's level/mute and hands it
|
|
/// to the mixer; a source with no audio simply never raises it.</summary>
|
|
event Action<AudioSample>? AudioAvailable;
|
|
|
|
/// <summary>Raised when the file finishes decoding (natural EOF) — not raised
|
|
/// while <see cref="Looping"/> is true.</summary>
|
|
event Action? Completed;
|
|
|
|
Task StartAsync(CancellationToken cancellationToken = default);
|
|
Task StopAsync();
|
|
}
|