Stream custom live channels using your own media
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

// 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
}
}
}