Files
LlamaCasty/Services/MediaVideoSource.cs
T
gramps ea24405ba2 feat(chroma): offer to remove a green or blue screen, and key it in the decode
TASK 64 slices D and E: the feature is usable end to end.

- Add a video file → one frame is probed (new `IChromaProbe`/`FfmpegChromaProbe`, one ffmpeg
  pass at 160x90, off the UI thread) and the layer gets an offer: "Looks like a green screen —
  Remove it", with the measured colour as a swatch.
- Accepting it turns the offer into settings (Amount / Softness / Fringe) that reach ffmpeg as
  `colorkey` + `despill` + `format=bgra`, so the key is computed in the pipe and both consumers
  (the shared preview bitmap and the compositor's frame) carry the same alpha.
- Media sessions are now keyed by **path + look** (`MainViewModel.MediaSessionKey`), so two
  layers with the same settings still share one decoder, and changing the look releases the old
  session and starts the new one. Removal, load reconciliation, loop and audio settings all move
  to the same key.
- Settings persist on the `Source` row (six ALTER TABLE guards + SELECT/INSERT), so a keyed layer
  comes back keyed.
- Panel section is plain creator words: Green/Blue Screen, Amount, Softness, Fringe.

Test: `ChromaKeyApplyTests` — the pipe asks ffmpeg to key (and does not when off); a green-screen
clip is offered, accepting it starts a keyed session, the layer's resolver then reads that session
rather than the old one; the setting survives a save.

Docs: TASK 64 D/E ticked with slice F (auto-apply setting + alert clips) left open; frame-pipeline
records the decode-option design; HANDOFF rewritten. verify.sh: 544 passed, 0 failed, 0 warnings.
2026-11-05 08:15:32 -08:00

59 lines
2.2 KiB
C#

using ytLive.Services.Audio;
using ytLive.Services.Encoder;
namespace ytLive.Services;
/// <summary>
/// TASK 21: decodes a local media file (any format ffmpeg handles) into
/// <see cref="VideoFrame"/>s for a MediaSource. This is the <see cref="IMediaFrameSource"/>
/// shape over the shared <see cref="VideoClipPipe"/> — the same pipe an alert clip uses
/// (TASK 47), so the two features cannot drift apart again in how they decode.
/// <para>Video AND the file's own soundtrack are decoded. The manager scales the audio by
/// the layer's level/mute and hands it to the mixer's desktop group.</para>
/// <para><b>Keying (TASK 64) is a decode option</b>: pass <see cref="ChromaKeyOptions"/> and the
/// green/blue screen is removed inside ffmpeg's pipe, so the preview bitmap and the composited
/// frame both carry the keyed alpha. Two looks = two sources; identical looks share one.</para>
/// </summary>
public sealed class MediaVideoSource : IMediaFrameSource
{
public event Action<VideoFrame>? FrameAvailable;
public event Action<AudioSample>? AudioAvailable;
public event Action? Completed;
public string Key => _path;
public bool Looping
{
get => _clip.Looping;
set => _clip.Looping = value;
}
private readonly string _path;
private readonly VideoClipPipe _clip;
public MediaVideoSource(
string path,
int width,
int height,
IFfmpegLocator locator,
Func<IDecodeProcess> processFactory,
IFrameRateProbe? frameRateProbe = null,
Func<TimeSpan, CancellationToken, Task>? delay = null,
ChromaKeyOptions? chromaKey = null)
{
_path = path;
_clip = new VideoClipPipe(
path, width, height, locator, processFactory, frameRateProbe, delay,
audio: true, chromaKey: chromaKey);
_clip.FrameAvailable += frame => FrameAvailable?.Invoke(frame);
_clip.AudioAvailable += sample => AudioAvailable?.Invoke(sample);
_clip.Completed += () => Completed?.Invoke();
}
public Task StartAsync(CancellationToken cancellationToken = default) => _clip.StartAsync(cancellationToken);
public Task StopAsync() => _clip.StopAsync();
public void Dispose() => _clip.Dispose();
}