using ErsatzTV.FFmpeg.Environment; using ErsatzTV.FFmpeg.Pipeline; namespace ErsatzTV.FFmpeg.Filter; public class ComplexFilter : IPipelineStep { private readonly Option _maybeAudioInputFile; private readonly Option _maybeSubtitleInputFile; private readonly Option _maybeVideoInputFile; private readonly Option _maybeWatermarkInputFile; private readonly PipelineContext _pipelineContext; public ComplexFilter( Option maybeVideoInputFile, Option maybeAudioInputFile, Option maybeWatermarkInputFile, Option maybeSubtitleInputFile, PipelineContext pipelineContext, FilterChain filterChain) { _maybeVideoInputFile = maybeVideoInputFile; _maybeAudioInputFile = maybeAudioInputFile; _maybeWatermarkInputFile = maybeWatermarkInputFile; _maybeSubtitleInputFile = maybeSubtitleInputFile; _pipelineContext = pipelineContext; FilterChain = filterChain; FilterOptions = Arguments(); } // for testing public FilterChain FilterChain { get; } public EnvironmentVariable[] EnvironmentVariables => Array.Empty(); public string[] GlobalOptions => Array.Empty(); public string[] InputOptions(InputFile inputFile) => Array.Empty(); public string[] FilterOptions { get; } public string[] OutputOptions => Array.Empty(); public FrameState NextState(FrameState currentState) => currentState; private string[] Arguments() { var audioLabel = "0:a"; var videoLabel = "0:v"; string? watermarkLabel = null; string? subtitleLabel = null; var result = new List(); string audioFilterComplex = string.Empty; string videoFilterComplex = string.Empty; string watermarkFilterComplex = string.Empty; string watermarkOverlayFilterComplex = string.Empty; string subtitleFilterComplex = string.Empty; string subtitleOverlayFilterComplex = string.Empty; string pixelFormatFilterComplex = string.Empty; var distinctPaths = new List(); foreach ((string path, _) in _maybeVideoInputFile) { if (!distinctPaths.Contains(path)) { distinctPaths.Add(path); } } foreach ((string path, _) in _maybeAudioInputFile) { if (!distinctPaths.Contains(path) || // use audio as a separate input with intel vaapi/qsv _pipelineContext.IsIntelVaapiOrQsv) { distinctPaths.Add(path); } } foreach ((string path, _) in _maybeWatermarkInputFile) { if (!distinctPaths.Contains(path)) { distinctPaths.Add(path); } } foreach ((string path, _) in _maybeSubtitleInputFile) { if (!distinctPaths.Contains(path)) { distinctPaths.Add(path); } } foreach (VideoInputFile videoInputFile in _maybeVideoInputFile) { int inputIndex = distinctPaths.IndexOf(videoInputFile.Path); foreach ((int index, _, _) in videoInputFile.Streams) { videoLabel = $"{inputIndex}:{index}"; if (FilterChain.VideoFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { videoFilterComplex += $"[{inputIndex}:{index}]"; videoFilterComplex += string.Join( ",", FilterChain.VideoFilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); videoLabel = "[v]"; videoFilterComplex += videoLabel; } } } foreach (WatermarkInputFile watermarkInputFile in _maybeWatermarkInputFile) { int inputIndex = distinctPaths.IndexOf(watermarkInputFile.Path); foreach ((int index, _, _) in watermarkInputFile.Streams) { watermarkLabel = $"{inputIndex}:{index}"; if (FilterChain.WatermarkFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { watermarkFilterComplex += $"[{inputIndex}:{index}]"; watermarkFilterComplex += string.Join( ",", FilterChain.WatermarkFilterSteps.Select(f => f.Filter) .Filter(s => !string.IsNullOrWhiteSpace(s))); watermarkLabel = "[wm]"; watermarkFilterComplex += watermarkLabel; } else { watermarkLabel = $"[{watermarkLabel}]"; } } } foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter( s => s.Method == SubtitleMethod.Burn)) { int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path); foreach ((int index, _, _) in subtitleInputFile.Streams) { subtitleLabel = $"{inputIndex}:{index}"; if (FilterChain.SubtitleFilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { subtitleFilterComplex += $"[{inputIndex}:{index}]"; subtitleFilterComplex += string.Join( ",", FilterChain.SubtitleFilterSteps.Select(f => f.Filter) .Filter(s => !string.IsNullOrWhiteSpace(s))); subtitleLabel = "[st]"; subtitleFilterComplex += subtitleLabel; } else { subtitleLabel = $"[{subtitleLabel}]"; } } } // overlay subtitle if (!string.IsNullOrWhiteSpace(subtitleLabel) && FilterChain.SubtitleOverlayFilterSteps.Count != 0) { subtitleOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(subtitleLabel)}"; subtitleOverlayFilterComplex += string.Join( ",", FilterChain.SubtitleOverlayFilterSteps.Select(f => f.Filter) .Filter(s => !string.IsNullOrWhiteSpace(s))); videoLabel = "[vst]"; subtitleOverlayFilterComplex += videoLabel; } // overlay watermark if (!string.IsNullOrWhiteSpace(watermarkLabel) && FilterChain.WatermarkOverlayFilterSteps.Count != 0) { watermarkOverlayFilterComplex += $"{ProperLabel(videoLabel)}{ProperLabel(watermarkLabel)}"; watermarkOverlayFilterComplex += string.Join( ",", FilterChain.WatermarkOverlayFilterSteps.Select(f => f.Filter) .Filter(s => !string.IsNullOrWhiteSpace(s))); videoLabel = "[vwm]"; watermarkOverlayFilterComplex += videoLabel; } // pixel format if (FilterChain.PixelFormatFilterSteps.Count != 0) { pixelFormatFilterComplex += $"{ProperLabel(videoLabel)}"; pixelFormatFilterComplex += string.Join( ",", FilterChain.PixelFormatFilterSteps.Select(f => f.Filter) .Filter(s => !string.IsNullOrWhiteSpace(s))); videoLabel = "[vpf]"; pixelFormatFilterComplex += videoLabel; } foreach (AudioInputFile audioInputFile in _maybeAudioInputFile) { int inputIndex = distinctPaths.LastIndexOf(audioInputFile.Path); foreach ((int index, _, _) in audioInputFile.Streams) { audioLabel = $"{inputIndex}:{index}"; if (audioInputFile.FilterSteps.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) { audioFilterComplex += $"[{inputIndex}:{index}]"; audioFilterComplex += string.Join( ",", audioInputFile.FilterSteps.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); audioLabel = "[a]"; audioFilterComplex += audioLabel; } } } var filterComplex = string.Join( ";", new[] { audioFilterComplex, videoFilterComplex, subtitleFilterComplex, watermarkFilterComplex, subtitleOverlayFilterComplex, watermarkOverlayFilterComplex, pixelFormatFilterComplex }.Where(s => !string.IsNullOrWhiteSpace(s))); if (!string.IsNullOrWhiteSpace(filterComplex)) { result.AddRange(new[] { "-filter_complex", filterComplex }); } result.AddRange(new[] { "-map", videoLabel, "-map", audioLabel }); foreach (SubtitleInputFile subtitleInputFile in _maybeSubtitleInputFile.Filter( s => s.Method == SubtitleMethod.Copy || s is { IsImageBased: true, Method: SubtitleMethod.Convert })) // TODO: support converting text subtitles? { if (subtitleInputFile.Streams.Any()) { int inputIndex = distinctPaths.IndexOf(subtitleInputFile.Path); foreach ((int index, _, _) in subtitleInputFile.Streams) { subtitleLabel = $"{inputIndex}:{index}"; result.AddRange(new[] { "-map", subtitleLabel }); } } } return result.ToArray(); } private static string ProperLabel(string label) => label.StartsWith("[", StringComparison.OrdinalIgnoreCase) ? label : $"[{label}]"; }