mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
12 KiB
382 lines
12 KiB
// zlib License |
|
// |
|
// Copyright (c) 2021 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; |
|
using System.Collections.Generic; |
|
using System.Diagnostics; |
|
using System.Linq; |
|
using System.Text; |
|
using ErsatzTV.Core.Domain; |
|
using ErsatzTV.Core.Interfaces.FFmpeg; |
|
using LanguageExt; |
|
|
|
namespace ErsatzTV.Core.FFmpeg |
|
{ |
|
internal class FFmpegProcessBuilder |
|
{ |
|
private readonly List<string> _arguments = new(); |
|
private readonly Queue<string> _audioFilters = new(); |
|
private readonly string _ffmpegPath; |
|
private readonly Queue<string> _videoFilters = new(); |
|
|
|
public FFmpegProcessBuilder(string ffmpegPath) => _ffmpegPath = ffmpegPath; |
|
|
|
public FFmpegProcessBuilder WithThreads(int threads) |
|
{ |
|
_arguments.Add("-threads"); |
|
_arguments.Add($"{threads}"); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithRealtimeOutput(bool realtimeOutput) |
|
{ |
|
if (realtimeOutput) |
|
{ |
|
_arguments.Add("-re"); |
|
} |
|
|
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithSeek(Option<TimeSpan> maybeStart) |
|
{ |
|
maybeStart.IfSome( |
|
start => |
|
{ |
|
_arguments.Add("-ss"); |
|
_arguments.Add($"{start:c}"); |
|
}); |
|
|
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithInfiniteLoop() |
|
{ |
|
_arguments.Add("-stream_loop"); |
|
_arguments.Add("-1"); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithLoopedImage(string input) |
|
{ |
|
_arguments.Add("-loop"); |
|
_arguments.Add("1"); |
|
return WithInput(input); |
|
} |
|
|
|
|
|
public FFmpegProcessBuilder WithPipe() |
|
{ |
|
_arguments.Add("pipe:1"); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithPixfmt(string pixfmt) |
|
{ |
|
_arguments.Add("-pix_fmt"); |
|
_arguments.Add(pixfmt); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithLibavfilter() |
|
{ |
|
_arguments.Add("-f"); |
|
_arguments.Add("lavfi"); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithInput(string input) |
|
{ |
|
_arguments.Add("-i"); |
|
_arguments.Add($"{input}"); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithFiltergraph(string graph) |
|
{ |
|
_arguments.Add("-vf"); |
|
_arguments.Add($"{graph}"); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithFilterComplex(string filter, string finalVideo, string finalAudio) |
|
{ |
|
_arguments.Add("-filter_complex"); |
|
_arguments.Add($"{filter}"); |
|
_arguments.Add("-map"); |
|
_arguments.Add(finalVideo); |
|
_arguments.Add("-map"); |
|
_arguments.Add(finalAudio); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithConcat(string concatPlaylist) |
|
{ |
|
var arguments = new List<string> |
|
{ |
|
"-f", "concat", |
|
"-safe", "0", |
|
"-protocol_whitelist", "file,http,tcp,https,tcp,tls", |
|
"-probesize", "32", |
|
"-i", concatPlaylist, |
|
"-map", "0:v", |
|
"-map", "0:a", |
|
"-c", "copy", |
|
"-muxdelay", "0", |
|
"-muxpreload", "0" |
|
}; |
|
_arguments.AddRange(arguments); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithMetadata(Channel channel) |
|
{ |
|
var arguments = new List<string> |
|
{ |
|
"-metadata", "service_provider=\"ErsatzTV\"", |
|
"-metadata", $"service_name=\"{channel.Name}\"" |
|
}; |
|
_arguments.AddRange(arguments); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithFormatFlags(IEnumerable<string> formatFlags) |
|
{ |
|
_arguments.Add("-fflags"); |
|
_arguments.Add(string.Join(string.Empty, formatFlags)); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithText(string text) |
|
{ |
|
const string FONT_FILE = "fontfile=Resources/Roboto-Regular.ttf"; |
|
const string FONT_SIZE = "fontsize=30"; |
|
const string FONT_COLOR = "fontcolor=white"; |
|
const string X = "x=(w-text_w)/2"; |
|
const string Y = "y=(h-text_h)/2"; |
|
|
|
return WithFiltergraph($"drawtext={FONT_FILE}:{FONT_SIZE}:{FONT_COLOR}:{X}:{Y}:text='{text}'"); |
|
} |
|
|
|
public FFmpegProcessBuilder WithDuration(TimeSpan duration) => |
|
// _arguments.Add("-t"); |
|
// _arguments.Add($"{duration:c}"); |
|
this; |
|
|
|
public FFmpegProcessBuilder WithFormat(string format) |
|
{ |
|
_arguments.Add("-f"); |
|
_arguments.Add($"{format}"); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithPlaybackArgs(FFmpegPlaybackSettings playbackSettings) |
|
{ |
|
var arguments = new List<string> |
|
{ |
|
"-c:v", playbackSettings.VideoCodec, |
|
"-flags", "cgop", |
|
"-sc_threshold", "1000000000" |
|
}; |
|
|
|
string[] videoBitrateArgs = playbackSettings.VideoBitrate.Match( |
|
bitrate => |
|
new[] |
|
{ |
|
"-b:v", $"{bitrate}k", |
|
"-maxrate:v", $"{bitrate}k" |
|
}, |
|
Array.Empty<string>()); |
|
arguments.AddRange(videoBitrateArgs); |
|
|
|
playbackSettings.VideoBufferSize |
|
.IfSome(bufferSize => arguments.AddRange(new[] { "-bufsize:v", $"{bufferSize}k" })); |
|
|
|
string[] audioBitrateArgs = playbackSettings.AudioBitrate.Match( |
|
bitrate => |
|
new[] |
|
{ |
|
"-b:a", $"{bitrate}k", |
|
"-maxrate:a", $"{bitrate}k" |
|
}, |
|
Array.Empty<string>()); |
|
arguments.AddRange(audioBitrateArgs); |
|
|
|
playbackSettings.AudioBufferSize |
|
.IfSome(bufferSize => arguments.AddRange(new[] { "-bufsize:a", $"{bufferSize}k" })); |
|
|
|
playbackSettings.AudioChannels |
|
.IfSome(channels => arguments.AddRange(new[] { "-ac", $"{channels}" })); |
|
|
|
playbackSettings.AudioSampleRate |
|
.IfSome(sampleRate => arguments.AddRange(new[] { "-ar", $"{sampleRate}k" })); |
|
|
|
arguments.AddRange( |
|
new[] |
|
{ |
|
"-c:a", playbackSettings.AudioCodec, |
|
"-map_metadata", "-1", |
|
"-movflags", "+faststart", |
|
"-muxdelay", "0", |
|
"-muxpreload", "0" |
|
}); |
|
|
|
_arguments.AddRange(arguments); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithScaling(IDisplaySize displaySize, string algorithm) |
|
{ |
|
_videoFilters.Enqueue($"scale={displaySize.Width}:{displaySize.Height}:flags={algorithm}"); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithBlackBars(IDisplaySize displaySize) |
|
{ |
|
_videoFilters.Enqueue($"pad={displaySize.Width}:{displaySize.Height}:(ow-iw)/2:(oh-ih)/2"); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithAlignedAudio(Option<TimeSpan> audioDuration) |
|
{ |
|
audioDuration.IfSome(duration => _audioFilters.Enqueue($"apad=whole_dur={duration.TotalMilliseconds}ms")); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithDeinterlace(bool deinterlace, string algorithm = "yadif=1") |
|
{ |
|
if (deinterlace) |
|
{ |
|
_videoFilters.Enqueue(algorithm); |
|
} |
|
|
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithSAR() |
|
{ |
|
// TODO: minsiz? |
|
_videoFilters.Enqueue("setsar=1"); |
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithFilterComplex() |
|
{ |
|
var complexFilter = new StringBuilder(); |
|
var videoLabel = "0:v"; |
|
var audioLabel = "0:a"; |
|
bool hasVideoFilters = _videoFilters.Any(); |
|
if (hasVideoFilters) |
|
{ |
|
(string filter, string finalLabel) = GenerateFilter(_videoFilters, StreamType.Video); |
|
complexFilter.Append(filter); |
|
videoLabel = finalLabel; |
|
} |
|
|
|
if (_audioFilters.Any()) |
|
{ |
|
if (hasVideoFilters) |
|
{ |
|
complexFilter.Append(';'); |
|
} |
|
|
|
(string filter, string finalLabel) = GenerateFilter(_audioFilters, StreamType.Audio); |
|
complexFilter.Append(filter); |
|
audioLabel = finalLabel; |
|
} |
|
|
|
var complex = complexFilter.ToString(); |
|
|
|
if (!string.IsNullOrWhiteSpace(complex)) |
|
{ |
|
_arguments.Add("-filter_complex"); |
|
_arguments.Add(complex); |
|
} |
|
|
|
_arguments.Add("-map"); |
|
_arguments.Add(videoLabel); |
|
|
|
_arguments.Add("-map"); |
|
_arguments.Add(audioLabel); |
|
|
|
return this; |
|
} |
|
|
|
public FFmpegProcessBuilder WithQuiet() |
|
{ |
|
_arguments.AddRange(new[] { "-hide_banner", "-loglevel", "panic", "-nostats" }); |
|
return this; |
|
} |
|
|
|
public Process Build() |
|
{ |
|
var startInfo = new ProcessStartInfo |
|
{ |
|
FileName = _ffmpegPath, |
|
RedirectStandardOutput = true, |
|
RedirectStandardError = false, |
|
UseShellExecute = false, |
|
CreateNoWindow = true, |
|
StandardOutputEncoding = Encoding.UTF8 |
|
}; |
|
|
|
foreach (string argument in _arguments) |
|
{ |
|
startInfo.ArgumentList.Add(argument); |
|
} |
|
|
|
return new Process |
|
{ |
|
StartInfo = startInfo |
|
}; |
|
} |
|
|
|
private FilterResult GenerateFilter(Queue<string> filterQueue, StreamType streamType) |
|
{ |
|
var filter = new StringBuilder(); |
|
var index = 0; |
|
string nullFilter = streamType switch |
|
{ |
|
StreamType.Audio => "anull", |
|
StreamType.Video => "null" |
|
}; |
|
char av = streamType switch |
|
{ |
|
StreamType.Audio => 'a', |
|
StreamType.Video => 'v' |
|
}; |
|
filter.Append($"[0:{av}]{nullFilter}[{av}{index}]"); |
|
while (filterQueue.TryDequeue(out string result)) |
|
{ |
|
filter.Append($";[{av}{index}]{result}[{av}{++index}]"); |
|
} |
|
|
|
return new FilterResult(filter.ToString(), $"[{av}{index}]"); |
|
} |
|
|
|
private record FilterResult(string Filter, string FinalLabel); |
|
|
|
private enum StreamType |
|
{ |
|
Audio, |
|
Video |
|
} |
|
} |
|
}
|
|
|