* move watermark options into watermark selector * fix graphics engine overlay performance * more refactoring * add some tests for existing watermark selector behavior * remove extra ffprobe call on all watermarks * remove a bunch of unused code; add failing tests * implement new watermark selection * add tests for new selector * probably sufficient (though verbose) test coverage * more tests * remove some unused code * simplify watermark selection * remove old selection code and tests * more tests
213 lines
6.4 KiB
C#
213 lines
6.4 KiB
C#
// 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<string> _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> watermarkOptions,
|
|
Option<List<FadePoint>> maybeFadePoints,
|
|
IDisplaySize resolution)
|
|
{
|
|
ChannelWatermarkMode maybeWatermarkMode = watermarkOptions.Map(wmo => wmo.Watermark.Mode)
|
|
.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)
|
|
{
|
|
_arguments.Add("-i");
|
|
_arguments.Add(options.ImagePath);
|
|
|
|
_complexFilterBuilder = _complexFilterBuilder.WithWatermark(
|
|
options.Watermark,
|
|
maybeFadePoints,
|
|
resolution,
|
|
options.ImageStreamIndex);
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
public FFmpegProcessBuilder WithSubtitleFile(Option<string> subtitleFile)
|
|
{
|
|
_complexFilterBuilder = _complexFilterBuilder.WithSubtitleFile(subtitleFile);
|
|
return this;
|
|
}
|
|
|
|
public FFmpegProcessBuilder WithSongInput(
|
|
string videoPath,
|
|
Option<string> pixelFormat,
|
|
bool boxBlur)
|
|
{
|
|
_complexFilterBuilder = _complexFilterBuilder
|
|
.WithInputPixelFormat(pixelFormat)
|
|
.WithBoxBlur(boxBlur);
|
|
|
|
_arguments.Add("-i");
|
|
_arguments.Add(videoPath);
|
|
|
|
return this;
|
|
}
|
|
|
|
public FFmpegProcessBuilder WithFormatFlags(IEnumerable<string> 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<MediaStream> maybeAudioStream,
|
|
string videoPath,
|
|
Option<string> audioPath)
|
|
{
|
|
int videoStreamIndex = videoStream.Index;
|
|
Option<int> 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<FFmpegComplexFilter> 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
|
|
};
|
|
}
|
|
}
|