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).
68 lines
2.5 KiB
C#
68 lines
2.5 KiB
C#
using System;
|
|
using System.Threading;
|
|
using ytLive.Services.Audio;
|
|
using ytLive.Services.Encoder;
|
|
|
|
namespace ytLive.Services;
|
|
|
|
/// <summary>
|
|
/// TASK 47: one alert clip's playback — video frames + on-stream audio from the
|
|
/// same file, both paced to real time. The alert box owns a decoder per event
|
|
/// (start at play, dispose at drain) — unlike MediaSource's app-wide refcounted
|
|
/// session, an alert REPLAYS the same clip each event, so a fresh decode per
|
|
/// play is required. The fade is applied by the alert layer, not here.
|
|
/// <para>Since TASK 21's audio slice this is a thin wrapper over the shared
|
|
/// <see cref="VideoClipPipe"/> (the same pipe a media layer uses); the alert case
|
|
/// simply never sets <c>Looping</c>.</para>
|
|
/// </summary>
|
|
public interface IAlertClipDecoder : IDisposable
|
|
{
|
|
/// <summary>Starts the decode loop (video + audio); raises the events off the
|
|
/// calling thread. Paced to real time so playback matches wall clock.</summary>
|
|
void Start();
|
|
|
|
/// <summary>Stops and kills any running decode subprocesses.</summary>
|
|
void Stop();
|
|
|
|
/// <summary>One decoded video frame (box-sized, straight-alpha BGRA).</summary>
|
|
event Action<VideoFrame>? FrameAvailable;
|
|
|
|
/// <summary>One chunk of on-stream audio (48 kHz stereo float).</summary>
|
|
event Action<AudioSample>? AudioReady;
|
|
|
|
/// <summary>Raised once when both streams have fully played out.</summary>
|
|
event Action? Completed;
|
|
}
|
|
|
|
/// <summary>Default <see cref="IAlertClipDecoder"/>: the shared clip pipe with looping off.</summary>
|
|
public sealed class AlertClipDecoder : IAlertClipDecoder
|
|
{
|
|
public event Action<VideoFrame>? FrameAvailable;
|
|
public event Action<AudioSample>? AudioReady;
|
|
public event Action? Completed;
|
|
|
|
private readonly VideoClipPipe _clip;
|
|
|
|
public AlertClipDecoder(
|
|
string path,
|
|
int width,
|
|
int height,
|
|
IFfmpegLocator locator,
|
|
Func<IDecodeProcess> processFactory,
|
|
IFrameRateProbe? frameRateProbe = null,
|
|
Func<TimeSpan, CancellationToken, Task>? delay = null)
|
|
{
|
|
_clip = new VideoClipPipe(
|
|
path, width, height, locator, processFactory, frameRateProbe, delay, audio: true);
|
|
_clip.FrameAvailable += frame => FrameAvailable?.Invoke(frame);
|
|
_clip.AudioAvailable += sample => AudioReady?.Invoke(sample);
|
|
_clip.Completed += () => Completed?.Invoke();
|
|
}
|
|
|
|
public void Start() => _ = _clip.StartAsync();
|
|
|
|
public void Stop() => _ = _clip.StopAsync();
|
|
|
|
public void Dispose() => _clip.Dispose();
|
|
}
|