705 lines
27 KiB
C#
705 lines
27 KiB
C#
using CliWrap;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Core.Domain.Filler;
|
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
|
using ErsatzTV.FFmpeg;
|
|
using ErsatzTV.FFmpeg.Environment;
|
|
using ErsatzTV.FFmpeg.Format;
|
|
using ErsatzTV.FFmpeg.OutputFormat;
|
|
using ErsatzTV.FFmpeg.Pipeline;
|
|
using ErsatzTV.FFmpeg.State;
|
|
using Microsoft.Extensions.Logging;
|
|
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
|
|
|
|
namespace ErsatzTV.Core.FFmpeg;
|
|
|
|
public class FFmpegLibraryProcessService : IFFmpegProcessService
|
|
{
|
|
private readonly FFmpegProcessService _ffmpegProcessService;
|
|
private readonly IFFmpegStreamSelector _ffmpegStreamSelector;
|
|
private readonly ILogger<FFmpegLibraryProcessService> _logger;
|
|
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator;
|
|
private readonly ITempFilePool _tempFilePool;
|
|
private readonly IPipelineBuilderFactory _pipelineBuilderFactory;
|
|
|
|
public FFmpegLibraryProcessService(
|
|
FFmpegProcessService ffmpegProcessService,
|
|
FFmpegPlaybackSettingsCalculator playbackSettingsCalculator,
|
|
IFFmpegStreamSelector ffmpegStreamSelector,
|
|
ITempFilePool tempFilePool,
|
|
IPipelineBuilderFactory pipelineBuilderFactory,
|
|
ILogger<FFmpegLibraryProcessService> logger)
|
|
{
|
|
_ffmpegProcessService = ffmpegProcessService;
|
|
_playbackSettingsCalculator = playbackSettingsCalculator;
|
|
_ffmpegStreamSelector = ffmpegStreamSelector;
|
|
_tempFilePool = tempFilePool;
|
|
_pipelineBuilderFactory = pipelineBuilderFactory;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<Command> ForPlayoutItem(
|
|
string ffmpegPath,
|
|
string ffprobePath,
|
|
bool saveReports,
|
|
Channel channel,
|
|
MediaVersion videoVersion,
|
|
MediaItemAudioVersion audioVersion,
|
|
string videoPath,
|
|
string audioPath,
|
|
Func<FFmpegPlaybackSettings, Task<List<Subtitle>>> getSubtitles,
|
|
string preferredAudioLanguage,
|
|
string preferredAudioTitle,
|
|
string preferredSubtitleLanguage,
|
|
ChannelSubtitleMode subtitleMode,
|
|
DateTimeOffset start,
|
|
DateTimeOffset finish,
|
|
DateTimeOffset now,
|
|
Option<ChannelWatermark> playoutItemWatermark,
|
|
Option<ChannelWatermark> globalWatermark,
|
|
VaapiDriver vaapiDriver,
|
|
string vaapiDevice,
|
|
Option<int> qsvExtraHardwareFrames,
|
|
bool hlsRealtime,
|
|
FillerKind fillerKind,
|
|
TimeSpan inPoint,
|
|
TimeSpan outPoint,
|
|
long ptsOffset,
|
|
Option<int> targetFramerate,
|
|
bool disableWatermarks,
|
|
Action<FFmpegPipeline> pipelineAction)
|
|
{
|
|
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(videoVersion);
|
|
Option<MediaStream> maybeAudioStream =
|
|
await _ffmpegStreamSelector.SelectAudioStream(
|
|
audioVersion,
|
|
channel.StreamingMode,
|
|
channel,
|
|
preferredAudioLanguage,
|
|
preferredAudioTitle);
|
|
|
|
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
|
|
channel.StreamingMode,
|
|
channel.FFmpegProfile,
|
|
videoVersion,
|
|
videoStream,
|
|
maybeAudioStream,
|
|
start,
|
|
now,
|
|
inPoint,
|
|
outPoint,
|
|
hlsRealtime,
|
|
targetFramerate);
|
|
|
|
Option<Subtitle> maybeSubtitle =
|
|
await _ffmpegStreamSelector.SelectSubtitleStream(
|
|
await getSubtitles(playbackSettings),
|
|
channel,
|
|
preferredSubtitleLanguage,
|
|
subtitleMode);
|
|
|
|
Option<WatermarkOptions> watermarkOptions = disableWatermarks
|
|
? None
|
|
: await _ffmpegProcessService.GetWatermarkOptions(
|
|
ffprobePath,
|
|
channel,
|
|
playoutItemWatermark,
|
|
globalWatermark,
|
|
videoVersion,
|
|
None,
|
|
None);
|
|
|
|
Option<List<FadePoint>> maybeFadePoints = watermarkOptions
|
|
.Map(o => o.Watermark)
|
|
.Flatten()
|
|
.Where(wm => wm.Mode == ChannelWatermarkMode.Intermittent)
|
|
.Map(
|
|
wm =>
|
|
WatermarkCalculator.CalculateFadePoints(
|
|
start,
|
|
inPoint,
|
|
outPoint,
|
|
playbackSettings.StreamSeek,
|
|
wm.FrequencyMinutes,
|
|
wm.DurationSeconds));
|
|
|
|
string audioFormat = playbackSettings.AudioFormat switch
|
|
{
|
|
FFmpegProfileAudioFormat.Aac => AudioFormat.Aac,
|
|
FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3,
|
|
FFmpegProfileAudioFormat.Copy => AudioFormat.Copy,
|
|
_ => throw new ArgumentOutOfRangeException($"unexpected audio format {playbackSettings.VideoFormat}")
|
|
};
|
|
|
|
var audioState = new AudioState(
|
|
audioFormat,
|
|
playbackSettings.AudioChannels,
|
|
playbackSettings.AudioBitrate,
|
|
playbackSettings.AudioBufferSize,
|
|
playbackSettings.AudioSampleRate,
|
|
videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None,
|
|
playbackSettings.NormalizeLoudness);
|
|
|
|
var ffmpegVideoStream = new VideoStream(
|
|
videoStream.Index,
|
|
videoStream.Codec,
|
|
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
|
|
new ColorParams(
|
|
videoStream.ColorRange,
|
|
videoStream.ColorSpace,
|
|
videoStream.ColorTransfer,
|
|
videoStream.ColorPrimaries),
|
|
new FrameSize(videoVersion.Width, videoVersion.Height),
|
|
videoVersion.SampleAspectRatio,
|
|
videoVersion.DisplayAspectRatio,
|
|
videoVersion.RFrameRate,
|
|
videoPath != audioPath, // still image when paths are different
|
|
videoVersion.VideoScanKind == VideoScanKind.Progressive ? ScanKind.Progressive : ScanKind.Interlaced);
|
|
|
|
var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream });
|
|
|
|
Option<AudioInputFile> audioInputFile = maybeAudioStream.Map(
|
|
audioStream =>
|
|
{
|
|
var ffmpegAudioStream = new AudioStream(audioStream.Index, audioStream.Codec, audioStream.Channels);
|
|
return new AudioInputFile(audioPath, new List<AudioStream> { ffmpegAudioStream }, audioState);
|
|
});
|
|
|
|
Option<SubtitleInputFile> subtitleInputFile = maybeSubtitle.Map<Option<SubtitleInputFile>>(
|
|
subtitle =>
|
|
{
|
|
if (!subtitle.IsImage && subtitle.SubtitleKind == SubtitleKind.Embedded && !subtitle.IsExtracted)
|
|
{
|
|
_logger.LogWarning("Subtitles are not yet available for this item");
|
|
return None;
|
|
}
|
|
|
|
var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(
|
|
subtitle.IsImage ? subtitle.StreamIndex : 0,
|
|
subtitle.Codec,
|
|
StreamKind.Video);
|
|
|
|
string path = subtitle.IsImage
|
|
? videoPath
|
|
: Path.Combine(FileSystemLayout.SubtitleCacheFolder, subtitle.Path);
|
|
|
|
return new SubtitleInputFile(
|
|
path,
|
|
new List<ErsatzTV.FFmpeg.MediaStream> { ffmpegSubtitleStream },
|
|
false);
|
|
|
|
// TODO: figure out HLS direct
|
|
// channel.StreamingMode == StreamingMode.HttpLiveStreamingDirect);
|
|
}).Flatten();
|
|
|
|
Option<WatermarkInputFile> watermarkInputFile = GetWatermarkInputFile(watermarkOptions, maybeFadePoints);
|
|
|
|
string videoFormat = GetVideoFormat(playbackSettings);
|
|
|
|
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings);
|
|
|
|
OutputFormatKind outputFormat = channel.StreamingMode == StreamingMode.HttpLiveStreamingSegmenter
|
|
? OutputFormatKind.Hls
|
|
: OutputFormatKind.MpegTs;
|
|
|
|
Option<string> hlsPlaylistPath = outputFormat == OutputFormatKind.Hls
|
|
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8")
|
|
: Option<string>.None;
|
|
|
|
Option<string> hlsSegmentTemplate = outputFormat == OutputFormatKind.Hls
|
|
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts")
|
|
: Option<string>.None;
|
|
|
|
// normalize songs to yuv420p
|
|
IPixelFormat desiredPixelFormat =
|
|
videoPath == audioPath ? playbackSettings.PixelFormat : new PixelFormatYuv420P();
|
|
|
|
var desiredState = new FrameState(
|
|
playbackSettings.RealtimeOutput,
|
|
fillerKind == FillerKind.Fallback,
|
|
videoFormat,
|
|
Optional(videoStream.Profile),
|
|
Optional(desiredPixelFormat),
|
|
ffmpegVideoStream.SquarePixelFrameSize(
|
|
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height)),
|
|
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height),
|
|
false,
|
|
playbackSettings.FrameRate,
|
|
playbackSettings.VideoBitrate,
|
|
playbackSettings.VideoBufferSize,
|
|
playbackSettings.VideoTrackTimeScale,
|
|
playbackSettings.Deinterlace);
|
|
|
|
var ffmpegState = new FFmpegState(
|
|
saveReports,
|
|
hwAccel,
|
|
hwAccel,
|
|
VaapiDriverName(hwAccel, vaapiDriver),
|
|
VaapiDeviceName(hwAccel, vaapiDevice),
|
|
playbackSettings.StreamSeek,
|
|
finish - now,
|
|
channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect,
|
|
"ErsatzTV",
|
|
channel.Name,
|
|
maybeAudioStream.Map(s => Optional(s.Language)).Flatten(),
|
|
outputFormat,
|
|
hlsPlaylistPath,
|
|
hlsSegmentTemplate,
|
|
ptsOffset,
|
|
playbackSettings.ThreadCount,
|
|
qsvExtraHardwareFrames);
|
|
|
|
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
|
|
|
|
IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder(
|
|
hwAccel,
|
|
videoInputFile,
|
|
audioInputFile,
|
|
watermarkInputFile,
|
|
subtitleInputFile,
|
|
VaapiDriverName(hwAccel, vaapiDriver),
|
|
VaapiDeviceName(hwAccel, vaapiDevice),
|
|
FileSystemLayout.FFmpegReportsFolder,
|
|
FileSystemLayout.FontsCacheFolder,
|
|
ffmpegPath);
|
|
|
|
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
|
|
|
|
pipelineAction?.Invoke(pipeline);
|
|
|
|
return GetCommand(ffmpegPath, videoInputFile, audioInputFile, watermarkInputFile, None, pipeline);
|
|
}
|
|
|
|
public async Task<Command> ForError(
|
|
string ffmpegPath,
|
|
Channel channel,
|
|
Option<TimeSpan> duration,
|
|
string errorMessage,
|
|
bool hlsRealtime,
|
|
long ptsOffset,
|
|
VaapiDriver vaapiDriver,
|
|
string vaapiDevice,
|
|
Option<int> qsvExtraHardwareFrames)
|
|
{
|
|
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateErrorSettings(
|
|
channel.StreamingMode,
|
|
channel.FFmpegProfile,
|
|
hlsRealtime);
|
|
|
|
IDisplaySize desiredResolution = channel.FFmpegProfile.Resolution;
|
|
|
|
var fontSize = (int)Math.Round(channel.FFmpegProfile.Resolution.Height / 20.0);
|
|
var margin = (int)Math.Round(channel.FFmpegProfile.Resolution.Height * 0.05);
|
|
|
|
string subtitleFile = await new SubtitleBuilder(_tempFilePool)
|
|
.WithResolution(desiredResolution)
|
|
.WithFontName("Roboto")
|
|
.WithFontSize(fontSize)
|
|
.WithAlignment(2)
|
|
.WithMarginV(margin)
|
|
.WithPrimaryColor("&HFFFFFF")
|
|
.WithFormattedContent(errorMessage.Replace(Environment.NewLine, "\\N"))
|
|
.BuildFile();
|
|
|
|
string audioFormat = playbackSettings.AudioFormat switch
|
|
{
|
|
FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3,
|
|
_ => AudioFormat.Aac
|
|
};
|
|
|
|
var audioState = new AudioState(
|
|
audioFormat,
|
|
playbackSettings.AudioChannels,
|
|
playbackSettings.AudioBitrate,
|
|
playbackSettings.AudioBufferSize,
|
|
playbackSettings.AudioSampleRate,
|
|
Option<TimeSpan>.None,
|
|
false);
|
|
|
|
var desiredState = new FrameState(
|
|
playbackSettings.RealtimeOutput,
|
|
false,
|
|
GetVideoFormat(playbackSettings),
|
|
VideoProfile.Main,
|
|
new PixelFormatYuv420P(),
|
|
new FrameSize(desiredResolution.Width, desiredResolution.Height),
|
|
new FrameSize(desiredResolution.Width, desiredResolution.Height),
|
|
false,
|
|
playbackSettings.FrameRate,
|
|
playbackSettings.VideoBitrate,
|
|
playbackSettings.VideoBufferSize,
|
|
playbackSettings.VideoTrackTimeScale,
|
|
playbackSettings.Deinterlace);
|
|
|
|
OutputFormatKind outputFormat = channel.StreamingMode == StreamingMode.HttpLiveStreamingSegmenter
|
|
? OutputFormatKind.Hls
|
|
: OutputFormatKind.MpegTs;
|
|
|
|
Option<string> hlsPlaylistPath = outputFormat == OutputFormatKind.Hls
|
|
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8")
|
|
: Option<string>.None;
|
|
|
|
Option<string> hlsSegmentTemplate = outputFormat == OutputFormatKind.Hls
|
|
? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts")
|
|
: Option<string>.None;
|
|
|
|
string videoPath = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "background.png");
|
|
|
|
var videoVersion = BackgroundImageMediaVersion.ForPath(videoPath, desiredResolution);
|
|
|
|
var ffmpegVideoStream = new VideoStream(
|
|
0,
|
|
VideoFormat.GeneratedImage,
|
|
new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p
|
|
ColorParams.Default,
|
|
new FrameSize(videoVersion.Width, videoVersion.Height),
|
|
videoVersion.SampleAspectRatio,
|
|
videoVersion.DisplayAspectRatio,
|
|
None,
|
|
true,
|
|
ScanKind.Progressive);
|
|
|
|
var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream });
|
|
|
|
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings);
|
|
|
|
var ffmpegState = new FFmpegState(
|
|
false,
|
|
hwAccel,
|
|
hwAccel,
|
|
VaapiDriverName(hwAccel, vaapiDriver),
|
|
VaapiDeviceName(hwAccel, vaapiDevice),
|
|
playbackSettings.StreamSeek,
|
|
duration,
|
|
channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect,
|
|
"ErsatzTV",
|
|
channel.Name,
|
|
None,
|
|
outputFormat,
|
|
hlsPlaylistPath,
|
|
hlsSegmentTemplate,
|
|
ptsOffset,
|
|
Option<int>.None,
|
|
qsvExtraHardwareFrames);
|
|
|
|
var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(0, "ass", StreamKind.Video);
|
|
|
|
var audioInputFile = new NullAudioInputFile(audioState);
|
|
|
|
var subtitleInputFile = new SubtitleInputFile(
|
|
subtitleFile,
|
|
new List<ErsatzTV.FFmpeg.MediaStream> { ffmpegSubtitleStream },
|
|
false);
|
|
|
|
_logger.LogDebug("FFmpeg desired error state {FrameState}", desiredState);
|
|
|
|
IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder(
|
|
hwAccel,
|
|
videoInputFile,
|
|
audioInputFile,
|
|
None,
|
|
subtitleInputFile,
|
|
VaapiDriverName(hwAccel, vaapiDriver),
|
|
VaapiDeviceName(hwAccel, vaapiDevice),
|
|
FileSystemLayout.FFmpegReportsFolder,
|
|
FileSystemLayout.FontsCacheFolder,
|
|
ffmpegPath);
|
|
|
|
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
|
|
|
|
return GetCommand(ffmpegPath, videoInputFile, audioInputFile, None, None, pipeline);
|
|
}
|
|
|
|
public async Task<Command> ConcatChannel(
|
|
string ffmpegPath,
|
|
bool saveReports,
|
|
Channel channel,
|
|
string scheme,
|
|
string host)
|
|
{
|
|
var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height);
|
|
|
|
var concatInputFile = new ConcatInputFile(
|
|
$"http://localhost:{Settings.ListenPort}/ffmpeg/concat/{channel.Number}",
|
|
resolution);
|
|
|
|
IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder(
|
|
HardwareAccelerationMode.None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
FileSystemLayout.FFmpegReportsFolder,
|
|
FileSystemLayout.FontsCacheFolder,
|
|
ffmpegPath);
|
|
|
|
FFmpegPipeline pipeline = pipelineBuilder.Concat(
|
|
concatInputFile,
|
|
FFmpegState.Concat(saveReports, channel.Name));
|
|
|
|
return GetCommand(ffmpegPath, None, None, None, concatInputFile, pipeline);
|
|
}
|
|
|
|
public async Task<Command> WrapSegmenter(
|
|
string ffmpegPath,
|
|
bool saveReports,
|
|
Channel channel,
|
|
string scheme,
|
|
string host)
|
|
{
|
|
var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height);
|
|
|
|
var concatInputFile = new ConcatInputFile(
|
|
$"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter",
|
|
resolution);
|
|
|
|
IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder(
|
|
HardwareAccelerationMode.None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
FileSystemLayout.FFmpegReportsFolder,
|
|
FileSystemLayout.FontsCacheFolder,
|
|
ffmpegPath);
|
|
|
|
FFmpegPipeline pipeline = pipelineBuilder.WrapSegmenter(
|
|
concatInputFile,
|
|
FFmpegState.Concat(saveReports, channel.Name));
|
|
|
|
return GetCommand(ffmpegPath, None, None, None, concatInputFile, pipeline);
|
|
}
|
|
|
|
public async Task<Command> ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height)
|
|
{
|
|
var videoInputFile = new VideoInputFile(
|
|
inputFile,
|
|
new List<VideoStream>
|
|
{
|
|
new(
|
|
0,
|
|
string.Empty,
|
|
None,
|
|
ColorParams.Default,
|
|
FrameSize.Unknown,
|
|
string.Empty,
|
|
string.Empty,
|
|
None,
|
|
true,
|
|
ScanKind.Progressive)
|
|
});
|
|
|
|
IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder(
|
|
HardwareAccelerationMode.None,
|
|
videoInputFile,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
FileSystemLayout.FFmpegReportsFolder,
|
|
FileSystemLayout.FontsCacheFolder,
|
|
ffmpegPath);
|
|
|
|
FFmpegPipeline pipeline = pipelineBuilder.Resize(outputFile, new FrameSize(-1, height));
|
|
|
|
return GetCommand(ffmpegPath, videoInputFile, None, None, None, pipeline, false);
|
|
}
|
|
|
|
public Task<Either<BaseError, string>> GenerateSongImage(
|
|
string ffmpegPath,
|
|
string ffprobePath,
|
|
Option<string> subtitleFile,
|
|
Channel channel,
|
|
Option<ChannelWatermark> playoutItemWatermark,
|
|
Option<ChannelWatermark> globalWatermark,
|
|
MediaVersion videoVersion,
|
|
string videoPath,
|
|
bool boxBlur,
|
|
Option<string> watermarkPath,
|
|
WatermarkLocation watermarkLocation,
|
|
int horizontalMarginPercent,
|
|
int verticalMarginPercent,
|
|
int watermarkWidthPercent,
|
|
CancellationToken cancellationToken) =>
|
|
_ffmpegProcessService.GenerateSongImage(
|
|
ffmpegPath,
|
|
ffprobePath,
|
|
subtitleFile,
|
|
channel,
|
|
playoutItemWatermark,
|
|
globalWatermark,
|
|
videoVersion,
|
|
videoPath,
|
|
boxBlur,
|
|
watermarkPath,
|
|
watermarkLocation,
|
|
horizontalMarginPercent,
|
|
verticalMarginPercent,
|
|
watermarkWidthPercent,
|
|
cancellationToken);
|
|
|
|
private Option<WatermarkInputFile> GetWatermarkInputFile(
|
|
Option<WatermarkOptions> watermarkOptions,
|
|
Option<List<FadePoint>> maybeFadePoints)
|
|
{
|
|
foreach (WatermarkOptions options in watermarkOptions)
|
|
{
|
|
foreach (ChannelWatermark watermark in options.Watermark)
|
|
{
|
|
// skip watermark if intermittent and no fade points
|
|
if (watermark.Mode != ChannelWatermarkMode.None &&
|
|
(watermark.Mode != ChannelWatermarkMode.Intermittent ||
|
|
maybeFadePoints.Map(fp => fp.Count > 0).IfNone(false)))
|
|
{
|
|
foreach (string path in options.ImagePath)
|
|
{
|
|
var watermarkInputFile = new WatermarkInputFile(
|
|
path,
|
|
new List<VideoStream>
|
|
{
|
|
new(
|
|
options.ImageStreamIndex.IfNone(0),
|
|
"unknown",
|
|
new PixelFormatUnknown(),
|
|
ColorParams.Default,
|
|
new FrameSize(1, 1),
|
|
string.Empty,
|
|
string.Empty,
|
|
Option<string>.None,
|
|
!options.IsAnimated,
|
|
ScanKind.Progressive)
|
|
},
|
|
new WatermarkState(
|
|
maybeFadePoints.Map(
|
|
lst => lst.Map(
|
|
fp =>
|
|
{
|
|
return fp switch
|
|
{
|
|
FadeInPoint fip => (WatermarkFadePoint)new WatermarkFadeIn(
|
|
fip.Time,
|
|
fip.EnableStart,
|
|
fip.EnableFinish),
|
|
FadeOutPoint fop => new WatermarkFadeOut(
|
|
fop.Time,
|
|
fop.EnableStart,
|
|
fop.EnableFinish),
|
|
_ => throw new NotSupportedException() // this will never happen
|
|
};
|
|
}).ToList()),
|
|
watermark.Location,
|
|
watermark.Size,
|
|
watermark.WidthPercent,
|
|
watermark.HorizontalMarginPercent,
|
|
watermark.VerticalMarginPercent,
|
|
watermark.Opacity,
|
|
watermark.PlaceWithinSourceContent));
|
|
|
|
return watermarkInputFile;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return None;
|
|
}
|
|
|
|
private Command GetCommand(
|
|
string ffmpegPath,
|
|
Option<VideoInputFile> videoInputFile,
|
|
Option<AudioInputFile> audioInputFile,
|
|
Option<WatermarkInputFile> watermarkInputFile,
|
|
Option<ConcatInputFile> concatInputFile,
|
|
FFmpegPipeline pipeline,
|
|
bool log = true)
|
|
{
|
|
IEnumerable<string> loggedSteps = pipeline.PipelineSteps.Map(ps => ps.GetType().Name);
|
|
IEnumerable<string> loggedAudioFilters =
|
|
audioInputFile.Map(f => f.FilterSteps.Map(af => af.GetType().Name)).Flatten();
|
|
IEnumerable<string> loggedVideoFilters =
|
|
videoInputFile.Map(f => f.FilterSteps.Map(vf => vf.GetType().Name)).Flatten();
|
|
|
|
if (log)
|
|
{
|
|
_logger.LogDebug(
|
|
"FFmpeg pipeline {PipelineSteps}, {AudioFilters}, {VideoFilters}",
|
|
loggedSteps,
|
|
loggedAudioFilters,
|
|
loggedVideoFilters
|
|
);
|
|
}
|
|
|
|
IList<EnvironmentVariable> environmentVariables =
|
|
CommandGenerator.GenerateEnvironmentVariables(pipeline.PipelineSteps);
|
|
IList<string> arguments = CommandGenerator.GenerateArguments(
|
|
videoInputFile,
|
|
audioInputFile,
|
|
watermarkInputFile,
|
|
concatInputFile,
|
|
pipeline.PipelineSteps);
|
|
|
|
if (environmentVariables.Any())
|
|
{
|
|
_logger.LogDebug("FFmpeg environment variables {EnvVars}", environmentVariables);
|
|
}
|
|
|
|
return Cli.Wrap(ffmpegPath)
|
|
.WithArguments(arguments)
|
|
.WithValidation(CommandResultValidation.None)
|
|
.WithStandardErrorPipe(PipeTarget.ToStream(Stream.Null))
|
|
.WithEnvironmentVariables(environmentVariables.ToDictionary(e => e.Key, e => e.Value));
|
|
}
|
|
|
|
private static Option<string> VaapiDriverName(HardwareAccelerationMode accelerationMode, VaapiDriver driver)
|
|
{
|
|
if (accelerationMode == HardwareAccelerationMode.Vaapi)
|
|
{
|
|
switch (driver)
|
|
{
|
|
case VaapiDriver.i965:
|
|
return "i965";
|
|
case VaapiDriver.iHD:
|
|
return "iHD";
|
|
case VaapiDriver.RadeonSI:
|
|
return "radeonsi";
|
|
case VaapiDriver.Nouveau:
|
|
return "nouveau";
|
|
}
|
|
}
|
|
|
|
return Option<string>.None;
|
|
}
|
|
|
|
private static Option<string> VaapiDeviceName(HardwareAccelerationMode accelerationMode, string vaapiDevice) =>
|
|
accelerationMode == HardwareAccelerationMode.Vaapi ||
|
|
OperatingSystem.IsLinux() && accelerationMode == HardwareAccelerationMode.Qsv
|
|
? vaapiDevice
|
|
: Option<string>.None;
|
|
|
|
private static string GetVideoFormat(FFmpegPlaybackSettings playbackSettings) =>
|
|
playbackSettings.VideoFormat switch
|
|
{
|
|
FFmpegProfileVideoFormat.Hevc => VideoFormat.Hevc,
|
|
FFmpegProfileVideoFormat.H264 => VideoFormat.H264,
|
|
FFmpegProfileVideoFormat.Mpeg2Video => VideoFormat.Mpeg2Video,
|
|
FFmpegProfileVideoFormat.Copy => VideoFormat.Copy,
|
|
_ => throw new ArgumentOutOfRangeException($"unexpected video format {playbackSettings.VideoFormat}")
|
|
};
|
|
|
|
private static HardwareAccelerationMode GetHardwareAccelerationMode(FFmpegPlaybackSettings playbackSettings) =>
|
|
playbackSettings.HardwareAcceleration switch
|
|
{
|
|
HardwareAccelerationKind.Nvenc => HardwareAccelerationMode.Nvenc,
|
|
HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv,
|
|
HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi,
|
|
HardwareAccelerationKind.VideoToolbox => HardwareAccelerationMode.VideoToolbox,
|
|
HardwareAccelerationKind.Amf => HardwareAccelerationMode.Amf,
|
|
_ => HardwareAccelerationMode.None
|
|
};
|
|
}
|