TASK 64 slice F — the two things left after the feature worked by hand. - App Settings → **Video clips → Auto Green/Blue Screen** (off by default). When on, a high-confidence detected screen is keyed as soon as the clip is added; the panel says "Removed automatically" so an automatic change is never silent. Medium confidence still asks, low says nothing — a wrong automatic key eats the picture. - **A clip is a clip:** the alert box's own replacement clip keys through the same settings block (one alert box per layout, so its settings are the ones the next alert uses), and choosing an alert clip runs the same detector. The Green/Blue Screen panel now appears for a video layer *or* an alert box with a custom clip (`Source.ShowsChromaKey`). - Spill ships as the creator-facing **Fringe** switch; nothing further to tune behind it. Test: `ChromaKeyApplyTests` gains the auto-apply case (no click, still explicit in the panel) and an alert-clip case (its pipe asks ffmpeg for colorkey+despill with the blue screen type). Docs: TASK 64 complete in TASKS.md, known limits recorded in the task file, HANDOFF rewritten. verify.sh: 546 passed, 0 failed, 0 warnings.
70 lines
2.6 KiB
C#
70 lines
2.6 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,
|
|
ChromaKeyOptions? chromaKey = null)
|
|
{
|
|
_clip = new VideoClipPipe(
|
|
path, width, height, locator, processFactory, frameRateProbe, delay,
|
|
audio: true, chromaKey: chromaKey);
|
|
_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();
|
|
}
|