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.
603 lines
24 KiB
603 lines
24 KiB
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.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; |
|
|
|
public FFmpegLibraryProcessService( |
|
FFmpegProcessService ffmpegProcessService, |
|
FFmpegPlaybackSettingsCalculator playbackSettingsCalculator, |
|
IFFmpegStreamSelector ffmpegStreamSelector, |
|
ITempFilePool tempFilePool, |
|
ILogger<FFmpegLibraryProcessService> logger) |
|
{ |
|
_ffmpegProcessService = ffmpegProcessService; |
|
_playbackSettingsCalculator = playbackSettingsCalculator; |
|
_ffmpegStreamSelector = ffmpegStreamSelector; |
|
_tempFilePool = tempFilePool; |
|
_logger = logger; |
|
} |
|
|
|
public async Task<Command> ForPlayoutItem( |
|
string ffmpegPath, |
|
string ffprobePath, |
|
bool saveReports, |
|
Channel channel, |
|
MediaVersion videoVersion, |
|
MediaVersion audioVersion, |
|
string videoPath, |
|
string audioPath, |
|
List<Subtitle> subtitles, |
|
string preferredAudioLanguage, |
|
string preferredSubtitleLanguage, |
|
ChannelSubtitleMode subtitleMode, |
|
DateTimeOffset start, |
|
DateTimeOffset finish, |
|
DateTimeOffset now, |
|
Option<ChannelWatermark> playoutItemWatermark, |
|
Option<ChannelWatermark> globalWatermark, |
|
VaapiDriver vaapiDriver, |
|
string vaapiDevice, |
|
bool hlsRealtime, |
|
FillerKind fillerKind, |
|
TimeSpan inPoint, |
|
TimeSpan outPoint, |
|
long ptsOffset, |
|
Option<int> targetFramerate, |
|
bool disableWatermarks) |
|
{ |
|
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(videoVersion); |
|
Option<MediaStream> maybeAudioStream = |
|
await _ffmpegStreamSelector.SelectAudioStream( |
|
audioVersion, |
|
channel.StreamingMode, |
|
channel.Number, |
|
preferredAudioLanguage); |
|
Option<Subtitle> maybeSubtitle = |
|
await _ffmpegStreamSelector.SelectSubtitleStream( |
|
videoVersion, |
|
subtitles, |
|
channel.StreamingMode, |
|
channel.Number, |
|
preferredSubtitleLanguage, |
|
subtitleMode); |
|
|
|
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings( |
|
channel.StreamingMode, |
|
channel.FFmpegProfile, |
|
videoVersion, |
|
videoStream, |
|
maybeAudioStream, |
|
start, |
|
now, |
|
inPoint, |
|
outPoint, |
|
hlsRealtime, |
|
targetFramerate); |
|
|
|
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 FrameSize(videoVersion.Width, videoVersion.Height), |
|
videoVersion.RFrameRate, |
|
videoPath != audioPath); // still image when paths are different |
|
|
|
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 = playbackSettings.HardwareAcceleration switch |
|
{ |
|
HardwareAccelerationKind.Nvenc => HardwareAccelerationMode.Nvenc, |
|
HardwareAccelerationKind.Qsv => HardwareAccelerationMode.Qsv, |
|
HardwareAccelerationKind.Vaapi => HardwareAccelerationMode.Vaapi, |
|
HardwareAccelerationKind.VideoToolbox => HardwareAccelerationMode.VideoToolbox, |
|
_ => HardwareAccelerationMode.None |
|
}; |
|
|
|
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 |
|
Option<IPixelFormat> desiredPixelFormat = |
|
videoPath == audioPath ? ffmpegVideoStream.PixelFormat : new PixelFormatYuv420P(); |
|
|
|
var desiredState = new FrameState( |
|
playbackSettings.RealtimeOutput, |
|
false, // TODO: fallback filler needs to loop |
|
videoFormat, |
|
desiredPixelFormat, |
|
await playbackSettings.ScaledSize.Map(ss => new FrameSize(ss.Width, ss.Height)) |
|
.IfNoneAsync(new FrameSize(videoVersion.Width, videoVersion.Height)), |
|
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height), |
|
playbackSettings.FrameRate, |
|
playbackSettings.VideoBitrate, |
|
playbackSettings.VideoBufferSize, |
|
playbackSettings.VideoTrackTimeScale, |
|
playbackSettings.Deinterlace); |
|
|
|
var ffmpegState = new FFmpegState( |
|
saveReports, |
|
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); |
|
|
|
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState); |
|
|
|
var pipelineBuilder = new PipelineBuilder( |
|
videoInputFile, |
|
audioInputFile, |
|
watermarkInputFile, |
|
subtitleInputFile, |
|
FileSystemLayout.FFmpegReportsFolder, |
|
FileSystemLayout.FontsCacheFolder, |
|
_logger); |
|
|
|
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState); |
|
|
|
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) |
|
{ |
|
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, |
|
playbackSettings.NormalizeLoudness); |
|
|
|
var desiredState = new FrameState( |
|
playbackSettings.RealtimeOutput, |
|
false, |
|
GetVideoFormat(playbackSettings), |
|
new PixelFormatYuv420P(), |
|
new FrameSize(desiredResolution.Width, desiredResolution.Height), |
|
new FrameSize(desiredResolution.Width, desiredResolution.Height), |
|
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 PixelFormatYuv420P(), |
|
new FrameSize(videoVersion.Width, videoVersion.Height), |
|
None, |
|
true); |
|
|
|
var videoInputFile = new VideoInputFile(videoPath, new List<VideoStream> { ffmpegVideoStream }); |
|
|
|
var ffmpegState = new FFmpegState( |
|
false, |
|
HardwareAccelerationMode.None, |
|
None, |
|
None, |
|
playbackSettings.StreamSeek, |
|
duration, |
|
channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect, |
|
"ErsatzTV", |
|
channel.Name, |
|
None, |
|
outputFormat, |
|
hlsPlaylistPath, |
|
hlsSegmentTemplate, |
|
ptsOffset); |
|
|
|
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); |
|
|
|
var pipelineBuilder = new PipelineBuilder( |
|
videoInputFile, |
|
audioInputFile, |
|
None, |
|
subtitleInputFile, |
|
FileSystemLayout.FFmpegReportsFolder, |
|
FileSystemLayout.FontsCacheFolder, |
|
_logger); |
|
|
|
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState); |
|
|
|
return GetCommand(ffmpegPath, videoInputFile, audioInputFile, None, None, pipeline); |
|
} |
|
|
|
public 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); |
|
|
|
var pipelineBuilder = new PipelineBuilder( |
|
None, |
|
None, |
|
None, |
|
None, |
|
FileSystemLayout.FFmpegReportsFolder, |
|
FileSystemLayout.FontsCacheFolder, |
|
_logger); |
|
|
|
FFmpegPipeline pipeline = pipelineBuilder.Concat( |
|
concatInputFile, |
|
FFmpegState.Concat(saveReports, channel.Name)); |
|
|
|
return GetCommand(ffmpegPath, None, None, None, concatInputFile, pipeline); |
|
} |
|
|
|
public Command WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) => |
|
_ffmpegProcessService.WrapSegmenter(ffmpegPath, saveReports, channel, scheme, host); |
|
|
|
public Command ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height) |
|
{ |
|
var videoInputFile = new VideoInputFile( |
|
inputFile, |
|
new List<VideoStream> { new(0, string.Empty, None, FrameSize.Unknown, None, true) }); |
|
|
|
var pipelineBuilder = new PipelineBuilder( |
|
videoInputFile, |
|
None, |
|
None, |
|
None, |
|
FileSystemLayout.FFmpegReportsFolder, |
|
FileSystemLayout.FontsCacheFolder, |
|
_logger); |
|
|
|
FFmpegPipeline pipeline = pipelineBuilder.Resize(outputFile, new FrameSize(-1, height)); |
|
|
|
return GetCommand(ffmpegPath, videoInputFile, None, None, None, pipeline, false); |
|
} |
|
|
|
public Command ConvertToPng(string ffmpegPath, string inputFile, string outputFile) => |
|
_ffmpegProcessService.ConvertToPng(ffmpegPath, inputFile, outputFile); |
|
|
|
public Command ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile) => |
|
_ffmpegProcessService.ExtractAttachedPicAsPng(ffmpegPath, inputFile, streamIndex, outputFile); |
|
|
|
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(), |
|
new FrameSize(1, 1), |
|
Option<string>.None, |
|
!options.IsAnimated) |
|
}, |
|
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)); |
|
|
|
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> loggedVideoFilters = |
|
videoInputFile.Map(f => f.FilterSteps.Map(vf => vf.GetType().Name)).Flatten(); |
|
IEnumerable<string> loggedAudioFilters = |
|
audioInputFile.Map(f => f.FilterSteps.Map(af => af.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 ? 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}") |
|
}; |
|
}
|
|
|