// zlib License // // Copyright (c) 2022 Dan Ferguson, Victor Hugo Soliz Kuncar, Jason Dove // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // 3. This notice may not be removed or altered from any source distribution. using System.Diagnostics; using System.Globalization; using System.Text; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.FFmpeg; namespace ErsatzTV.Core.FFmpeg; internal class FFmpegProcessBuilder { private static readonly string[] QuietArguments = { "-hide_banner", "-loglevel", "error", "-nostats" }; private readonly List _arguments = new(); private readonly string _ffmpegPath; private FFmpegComplexFilterBuilder _complexFilterBuilder = new(); public FFmpegProcessBuilder(string ffmpegPath) => _ffmpegPath = ffmpegPath; public FFmpegProcessBuilder WithThreads(int threads) { _arguments.Add("-threads"); _arguments.Add($"{threads}"); return this; } public FFmpegProcessBuilder WithWatermark( Option watermarkOptions, Option> maybeFadePoints, IDisplaySize resolution) { ChannelWatermarkMode maybeWatermarkMode = watermarkOptions.Map(wmo => wmo.Watermark.Map(wm => wm.Mode)) .Flatten() .IfNone(ChannelWatermarkMode.None); // skip watermark if intermittent and no fade points if (maybeWatermarkMode != ChannelWatermarkMode.None && (maybeWatermarkMode != ChannelWatermarkMode.Intermittent || maybeFadePoints.Map(fp => fp.Count > 0).IfNone(false))) { foreach (WatermarkOptions options in watermarkOptions) { foreach (string path in options.ImagePath) { if (options.IsAnimated) { _arguments.Add("-ignore_loop"); _arguments.Add("0"); } // when we have fade points, we need to loop the static watermark image else if (maybeFadePoints.Map(fp => fp.Count).IfNone(0) > 0) { _arguments.Add("-stream_loop"); _arguments.Add("-1"); } _arguments.Add("-i"); _arguments.Add(path); _complexFilterBuilder = _complexFilterBuilder.WithWatermark( options.Watermark, maybeFadePoints, resolution, options.ImageStreamIndex); } } } return this; } public FFmpegProcessBuilder WithSubtitleFile(Option subtitleFile) { _complexFilterBuilder = _complexFilterBuilder.WithSubtitleFile(subtitleFile); return this; } public FFmpegProcessBuilder WithSongInput( string videoPath, Option pixelFormat, bool boxBlur) { _complexFilterBuilder = _complexFilterBuilder .WithInputPixelFormat(pixelFormat) .WithBoxBlur(boxBlur); _arguments.Add("-i"); _arguments.Add(videoPath); return this; } public FFmpegProcessBuilder WithFormatFlags(IEnumerable formatFlags) { _arguments.Add("-fflags"); _arguments.Add(string.Join(string.Empty, formatFlags)); return this; } public FFmpegProcessBuilder WithScaling(IDisplaySize displaySize) { _complexFilterBuilder = _complexFilterBuilder.WithScaling(displaySize); return this; } public FFmpegProcessBuilder WithBlackBars(IDisplaySize displaySize) { _complexFilterBuilder = _complexFilterBuilder.WithBlackBars(displaySize); return this; } public FFmpegProcessBuilder WithOutputFormat(string format, string output, params string[] options) { foreach (string option in options) { _arguments.Add(option); } _arguments.Add("-f"); _arguments.Add(format); _arguments.Add("-y"); _arguments.Add(output); return this; } public FFmpegProcessBuilder WithFilterComplex( MediaStream videoStream, Option maybeAudioStream, string videoPath, Option audioPath) { int videoStreamIndex = videoStream.Index; Option maybeIndex = maybeAudioStream.Map(ms => ms.Index); var videoIndex = 0; var audioIndex = 0; if (audioPath.IsNone) { // no audio index, so use same as video audioIndex = 0; } else if (audioPath.IfNone("NotARealPath") != videoPath) { audioIndex = 1; } var videoLabel = $"{videoIndex}:{videoStreamIndex}"; var audioLabel = $"{audioIndex}:{maybeIndex.Match(i => i.ToString(CultureInfo.InvariantCulture), () => "a")}"; Option maybeFilter = _complexFilterBuilder.Build( audioPath.IsNone, videoIndex, videoStreamIndex, audioIndex, maybeIndex, audioPath.IsSome && videoPath != audioPath.IfNone("NotARealPath")); maybeFilter.IfSome( filter => { _arguments.Add("-filter_complex"); _arguments.Add(filter.ComplexFilter); videoLabel = filter.VideoLabel; audioLabel = filter.AudioLabel; }); _arguments.Add("-map"); _arguments.Add(videoLabel); foreach (string _ in audioPath) { _arguments.Add("-map"); _arguments.Add(audioLabel); } return this; } public FFmpegProcessBuilder WithQuiet() { _arguments.AddRange(QuietArguments); return this; } public Process Build() { var startInfo = new ProcessStartInfo { FileName = _ffmpegPath, RedirectStandardOutput = true, RedirectStandardError = false, UseShellExecute = false, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8 }; startInfo.ArgumentList.Add("-nostdin"); foreach (string argument in _arguments) { startInfo.ArgumentList.Add(argument); } return new Process { StartInfo = startInfo }; } }