mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* start to add ffmpeg library * start to hook ffmpeg lib into main app * improvements * more progress * make pipeline builder configurable * more options * move more logic down into ffmpeg lib * ffmpeg lib desired state refactoring * add software scaling and padding * add loudness normalization and software deinterlace * add metadata output options * add setsar filter * use built-in scaling logic * fixes * initial nvidia support * nvidia improvements * support hls mode * print old arguments at debug level * fix package reference * start to add qsv support * formatting * fix tests * add timeout to transcode tests * show successful ffmpeg arguments * add vaapi support * add more software decoders * add experimental transcoder option * call existing ffmpeg process service for unimplemented features * fix nvidia mpeg2video bug * update changelog * ignore some neglected unit testspull/631/head
149 changed files with 3331 additions and 87 deletions
@ -0,0 +1,240 @@
@@ -0,0 +1,240 @@
|
||||
using System; |
||||
using System.Diagnostics; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Text; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using ErsatzTV.Core.Interfaces.FFmpeg; |
||||
using ErsatzTV.FFmpeg; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
using ErsatzTV.FFmpeg.OutputFormat; |
||||
using LanguageExt; |
||||
using Microsoft.Extensions.Logging; |
||||
using static LanguageExt.Prelude; |
||||
using MediaStream = ErsatzTV.Core.Domain.MediaStream; |
||||
|
||||
namespace ErsatzTV.Core.FFmpeg; |
||||
|
||||
public class FFmpegLibraryProcessService : IFFmpegProcessService |
||||
{ |
||||
private readonly FFmpegProcessService _ffmpegProcessService; |
||||
private readonly FFmpegPlaybackSettingsCalculator _playbackSettingsCalculator; |
||||
private readonly IFFmpegStreamSelector _ffmpegStreamSelector; |
||||
private readonly ILogger<FFmpegLibraryProcessService> _logger; |
||||
|
||||
public FFmpegLibraryProcessService( |
||||
FFmpegProcessService ffmpegProcessService, |
||||
FFmpegPlaybackSettingsCalculator playbackSettingsCalculator, |
||||
IFFmpegStreamSelector ffmpegStreamSelector, |
||||
ILogger<FFmpegLibraryProcessService> logger) |
||||
{ |
||||
_ffmpegProcessService = ffmpegProcessService; |
||||
_playbackSettingsCalculator = playbackSettingsCalculator; |
||||
_ffmpegStreamSelector = ffmpegStreamSelector; |
||||
_logger = logger; |
||||
} |
||||
|
||||
public async Task<Process> ForPlayoutItem( |
||||
string ffmpegPath, |
||||
bool saveReports, |
||||
Channel channel, |
||||
MediaVersion videoVersion, |
||||
MediaVersion audioVersion, |
||||
string videoPath, |
||||
string audioPath, |
||||
DateTimeOffset start, |
||||
DateTimeOffset finish, |
||||
DateTimeOffset now, |
||||
Option<ChannelWatermark> globalWatermark, |
||||
VaapiDriver vaapiDriver, |
||||
string vaapiDevice, |
||||
bool hlsRealtime, |
||||
FillerKind fillerKind, |
||||
TimeSpan inPoint, |
||||
TimeSpan outPoint, |
||||
long ptsOffset, |
||||
Option<int> targetFramerate) |
||||
{ |
||||
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion); |
||||
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion); |
||||
|
||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings( |
||||
channel.StreamingMode, |
||||
channel.FFmpegProfile, |
||||
videoVersion, |
||||
videoStream, |
||||
maybeAudioStream, |
||||
start, |
||||
now, |
||||
inPoint, |
||||
outPoint, |
||||
hlsRealtime, |
||||
targetFramerate); |
||||
|
||||
var inputFiles = new List<InputFile> |
||||
{ |
||||
new( |
||||
videoVersion.MediaFiles.Head().Path, |
||||
new List<ErsatzTV.FFmpeg.MediaStream> |
||||
{ |
||||
new VideoStream( |
||||
videoStream.Index, |
||||
videoStream.Codec, |
||||
Some(AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat)), |
||||
new FrameSize(videoVersion.Width, videoVersion.Height), |
||||
videoVersion.RFrameRate) |
||||
}) |
||||
}; |
||||
|
||||
foreach (MediaStream audioStream in maybeAudioStream) |
||||
{ |
||||
inputFiles.Head().Streams |
||||
.Add(new AudioStream(audioStream.Index, audioStream.Codec, audioStream.Channels)); |
||||
} |
||||
|
||||
// TODO: need formats for these codecs
|
||||
string videoFormat = channel.FFmpegProfile.VideoCodec switch |
||||
{ |
||||
"libx265" or "hevc_nvenc" or "hevc_qsv" or "hevc_vaapi" => VideoFormat.Hevc, |
||||
"libx264" or "h264_nvenc" or "h264_qsv" or "h264_vaapi" => VideoFormat.H264, |
||||
"mpeg2video" => VideoFormat.Mpeg2Video, |
||||
_ => throw new ArgumentOutOfRangeException($"unexpected video codec {channel.FFmpegProfile.VideoCodec}") |
||||
}; |
||||
|
||||
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; |
||||
|
||||
var desiredState = new FrameState( |
||||
hwAccel, |
||||
playbackSettings.RealtimeOutput, |
||||
false, |
||||
playbackSettings.StreamSeek, |
||||
finish - now, |
||||
videoFormat, |
||||
Some(AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat)), |
||||
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, |
||||
channel.FFmpegProfile.AudioCodec, |
||||
channel.FFmpegProfile.AudioChannels, |
||||
playbackSettings.AudioBitrate, |
||||
playbackSettings.AudioBufferSize, |
||||
playbackSettings.AudioSampleRate, |
||||
videoPath == audioPath ? playbackSettings.AudioDuration : Option<TimeSpan>.None, |
||||
playbackSettings.NormalizeLoudness, |
||||
channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect, |
||||
"ErsatzTV", |
||||
channel.Name, |
||||
maybeAudioStream.Map(s => Optional(s.Language)).Flatten(), |
||||
outputFormat, |
||||
Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8"), |
||||
Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts"), |
||||
ptsOffset); |
||||
|
||||
return GetProcess(ffmpegPath, inputFiles, desiredState); |
||||
} |
||||
|
||||
public Task<Process> ForError( |
||||
string ffmpegPath, |
||||
Channel channel, |
||||
Option<TimeSpan> duration, |
||||
string errorMessage, |
||||
bool hlsRealtime, |
||||
long ptsOffset) => |
||||
_ffmpegProcessService.ForError(ffmpegPath, channel, duration, errorMessage, hlsRealtime, ptsOffset); |
||||
|
||||
public Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) |
||||
{ |
||||
var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height); |
||||
var desiredState = FrameState.Concat(channel.Name, resolution); |
||||
|
||||
var inputFiles = new List<InputFile> |
||||
{ |
||||
new ConcatInputFile($"http://localhost:{Settings.ListenPort}/ffmpeg/concat/{channel.Number}", resolution) |
||||
}; |
||||
|
||||
return GetProcess(ffmpegPath, inputFiles, desiredState); |
||||
} |
||||
|
||||
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host) => |
||||
_ffmpegProcessService.WrapSegmenter(ffmpegPath, saveReports, channel, scheme, host); |
||||
|
||||
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile) => |
||||
_ffmpegProcessService.ConvertToPng(ffmpegPath, inputFile, outputFile); |
||||
|
||||
public Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile) => |
||||
_ffmpegProcessService.ExtractAttachedPicAsPng(ffmpegPath, inputFile, streamIndex, outputFile); |
||||
|
||||
public Task<Either<BaseError, string>> GenerateSongImage( |
||||
string ffmpegPath, |
||||
Option<string> subtitleFile, |
||||
Channel channel, |
||||
Option<ChannelWatermark> globalWatermark, |
||||
MediaVersion videoVersion, |
||||
string videoPath, |
||||
bool boxBlur, |
||||
Option<string> watermarkPath, |
||||
ChannelWatermarkLocation watermarkLocation, |
||||
int horizontalMarginPercent, |
||||
int verticalMarginPercent, |
||||
int watermarkWidthPercent) => |
||||
_ffmpegProcessService.GenerateSongImage( |
||||
ffmpegPath, |
||||
subtitleFile, |
||||
channel, |
||||
globalWatermark, |
||||
videoVersion, |
||||
videoPath, |
||||
boxBlur, |
||||
watermarkPath, |
||||
watermarkLocation, |
||||
horizontalMarginPercent, |
||||
verticalMarginPercent, |
||||
watermarkWidthPercent); |
||||
|
||||
private Process GetProcess(string ffmpegPath, IList<InputFile> inputFiles, FrameState desiredState) |
||||
{ |
||||
var pipelineBuilder = new PipelineBuilder(inputFiles, _logger); |
||||
|
||||
IList<IPipelineStep> pipelineSteps = pipelineBuilder.Build(desiredState); |
||||
|
||||
IList<string> arguments = CommandGenerator.GenerateArguments(inputFiles, pipelineSteps); |
||||
|
||||
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 |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
using System; |
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.FFmpeg; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using LanguageExt; |
||||
using Microsoft.Extensions.DependencyInjection; |
||||
|
||||
namespace ErsatzTV.Core.FFmpeg; |
||||
|
||||
public class FFmpegProcessServiceFactory : IFFmpegProcessServiceFactory |
||||
{ |
||||
private readonly IConfigElementRepository _configElementRepository; |
||||
private readonly IServiceProvider _serviceProvider; |
||||
|
||||
public FFmpegProcessServiceFactory(IConfigElementRepository configElementRepository, IServiceProvider serviceProvider) |
||||
{ |
||||
_configElementRepository = configElementRepository; |
||||
_serviceProvider = serviceProvider; |
||||
} |
||||
|
||||
public async Task<IFFmpegProcessService> GetService() |
||||
{ |
||||
Option<bool> useExperimentalTranscoder = |
||||
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegUseExperimentalTranscoder); |
||||
|
||||
return await useExperimentalTranscoder.IfNoneAsync(false) |
||||
? _serviceProvider.GetRequiredService<FFmpegLibraryProcessService>() |
||||
: _serviceProvider.GetRequiredService<FFmpegProcessService>(); |
||||
} |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks; |
||||
using ErsatzTV.Core.Interfaces.FFmpeg; |
||||
|
||||
namespace ErsatzTV.Core.FFmpeg; |
||||
|
||||
public interface IFFmpegProcessServiceFactory |
||||
{ |
||||
Task<IFFmpegProcessService> GetService(); |
||||
} |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
||||
<PropertyGroup> |
||||
<TargetFramework>net6.0</TargetFramework> |
||||
<Nullable>enable</Nullable> |
||||
|
||||
<IsPackable>false</IsPackable> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<PackageReference Include="FluentAssertions" Version="6.4.0" /> |
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> |
||||
<PackageReference Include="Moq" Version="4.16.1" /> |
||||
<PackageReference Include="NUnit" Version="3.13.2" /> |
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> |
||||
<PackageReference Include="coverlet.collector" Version="3.1.0" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<ProjectReference Include="..\ErsatzTV.FFmpeg\ErsatzTV.FFmpeg.csproj" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<Reference Include="Microsoft.Extensions.Logging.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60"> |
||||
<HintPath>..\..\..\..\..\..\usr\share\dotnet\shared\Microsoft.AspNetCore.App\6.0.2\Microsoft.Extensions.Logging.Abstractions.dll</HintPath> |
||||
</Reference> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
using ErsatzTV.FFmpeg.OutputFormat; |
||||
using NUnit.Framework; |
||||
using FluentAssertions; |
||||
using LanguageExt; |
||||
using Microsoft.Extensions.Logging; |
||||
using Moq; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Tests; |
||||
|
||||
[TestFixture] |
||||
public class PipelineGeneratorTests |
||||
{ |
||||
private readonly ILogger _logger = new Mock<ILogger>().Object; |
||||
|
||||
[Test] |
||||
[Ignore("These aren't useful yet")] |
||||
public void Correct_Codecs_And_Pixel_Format_Should_Copy() |
||||
{ |
||||
var testFile = new InputFile( |
||||
"/tmp/whatever.mkv", |
||||
new List<MediaStream> |
||||
{ |
||||
new VideoStream(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "24"), |
||||
new AudioStream(1, AudioFormat.Aac, 2) |
||||
}); |
||||
|
||||
var inputFiles = new List<InputFile> { testFile }; |
||||
|
||||
var desiredState = new FrameState( |
||||
HardwareAccelerationMode.None, |
||||
true, |
||||
false, |
||||
Option<TimeSpan>.None, |
||||
Option<TimeSpan>.None, |
||||
VideoFormat.H264, |
||||
new PixelFormatYuv420P(), |
||||
new FrameSize(1920, 1080), |
||||
new FrameSize(1920, 1080), |
||||
Option<int>.None, |
||||
2000, |
||||
4000, |
||||
90_000, |
||||
false, |
||||
AudioFormat.Aac, |
||||
2, |
||||
320, |
||||
640, |
||||
48, |
||||
Option<TimeSpan>.None, |
||||
false, |
||||
false, |
||||
Option<string>.None, |
||||
Option<string>.None, |
||||
Option<string>.None, |
||||
OutputFormatKind.MpegTs, |
||||
Option<string>.None, |
||||
Option<string>.None, |
||||
0); |
||||
|
||||
var builder = new PipelineBuilder(inputFiles, _logger); |
||||
IList<IPipelineStep> result = builder.Build(desiredState); |
||||
|
||||
result.Should().HaveCountGreaterThan(0); |
||||
|
||||
PrintCommand(inputFiles, result); |
||||
} |
||||
|
||||
[Test] |
||||
[Ignore("These aren't useful yet")] |
||||
public void Incorrect_Video_Codec_Should_Use_Encoder() |
||||
{ |
||||
var testFile = new InputFile( |
||||
"/tmp/whatever.mkv", |
||||
new List<MediaStream> |
||||
{ |
||||
new VideoStream(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "24"), |
||||
new AudioStream(1, AudioFormat.Aac, 2) |
||||
}); |
||||
|
||||
var inputFiles = new List<InputFile> { testFile }; |
||||
|
||||
var desiredState = new FrameState( |
||||
HardwareAccelerationMode.None, |
||||
true, |
||||
false, |
||||
Option<TimeSpan>.None, |
||||
Option<TimeSpan>.None, |
||||
VideoFormat.Hevc, |
||||
new PixelFormatYuv420P(), |
||||
new FrameSize(1920, 1080), |
||||
new FrameSize(1920, 1080), |
||||
Option<int>.None, |
||||
2000, |
||||
4000, |
||||
90_000, |
||||
false, |
||||
AudioFormat.Aac, |
||||
2, |
||||
320, |
||||
640, |
||||
48, |
||||
Option<TimeSpan>.None, |
||||
false, |
||||
false, |
||||
Option<string>.None, |
||||
Option<string>.None, |
||||
Option<string>.None, |
||||
OutputFormatKind.MpegTs, |
||||
Option<string>.None, |
||||
Option<string>.None, |
||||
0); |
||||
|
||||
var builder = new PipelineBuilder(inputFiles, _logger); |
||||
IList<IPipelineStep> result = builder.Build(desiredState); |
||||
|
||||
result.Should().HaveCountGreaterThan(0); |
||||
|
||||
PrintCommand(inputFiles, result); |
||||
} |
||||
|
||||
[Test] |
||||
public void Concat_Test() |
||||
{ |
||||
var resolution = new FrameSize(1920, 1080); |
||||
var desiredState = FrameState.Concat("Some Channel", resolution); |
||||
|
||||
var inputFiles = new List<InputFile> |
||||
{ |
||||
new ConcatInputFile("http://localhost:8080/ffmpeg/concat/1", resolution) |
||||
}; |
||||
|
||||
var builder = new PipelineBuilder(inputFiles, _logger); |
||||
IList<IPipelineStep> result = builder.Build(desiredState); |
||||
|
||||
result.Should().HaveCountGreaterThan(0); |
||||
|
||||
PrintCommand(inputFiles, result); |
||||
} |
||||
|
||||
private static void PrintCommand(IEnumerable<InputFile> inputFiles, IList<IPipelineStep> pipeline) |
||||
{ |
||||
IList<string> arguments = CommandGenerator.GenerateArguments(inputFiles, pipeline); |
||||
Console.WriteLine($"Generated command: ffmpeg {string.Join(" ", arguments)}"); |
||||
} |
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
namespace ErsatzTV.FFmpeg; |
||||
|
||||
public static class CommandGenerator |
||||
{ |
||||
public static IList<string> GenerateArguments( |
||||
IEnumerable<InputFile> inputFiles, |
||||
IList<IPipelineStep> pipelineSteps) |
||||
{ |
||||
var arguments = new List<string>(); |
||||
|
||||
foreach (IPipelineStep step in pipelineSteps) |
||||
{ |
||||
arguments.AddRange(step.GlobalOptions); |
||||
} |
||||
|
||||
foreach (InputFile inputFile in inputFiles) |
||||
{ |
||||
foreach (IPipelineStep step in pipelineSteps) |
||||
{ |
||||
arguments.AddRange(step.InputOptions); |
||||
} |
||||
|
||||
arguments.AddRange(new[] { "-i", inputFile.Path }); |
||||
} |
||||
|
||||
foreach (IPipelineStep step in pipelineSteps) |
||||
{ |
||||
arguments.AddRange(step.FilterOptions); |
||||
} |
||||
|
||||
foreach (IPipelineStep step in pipelineSteps) |
||||
{ |
||||
arguments.AddRange(step.OutputOptions); |
||||
} |
||||
|
||||
return arguments; |
||||
} |
||||
} |
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
using ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
using ErsatzTV.FFmpeg.Decoder.Qsv; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public static class AvailableDecoders |
||||
{ |
||||
public static Option<IDecoder> ForVideoFormat(FrameState currentState, FrameState desiredState) |
||||
{ |
||||
return (currentState.HardwareAccelerationMode, currentState.VideoFormat, |
||||
currentState.PixelFormat.Match(pf => pf.Name, () => string.Empty)) switch |
||||
{ |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Hevc, _) => new DecoderHevcCuvid(desiredState), |
||||
|
||||
// nvenc doesn't support hardware decoding of 10-bit content
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.H264, PixelFormat.YUV420P10LE or PixelFormat.YUV444P10LE) |
||||
=> new DecoderH264(), |
||||
|
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.H264, _) => new DecoderH264Cuvid(desiredState), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg2Video, _) => new DecoderMpeg2Cuvid(desiredState), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Vc1, _) => new DecoderVc1Cuvid(desiredState), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Vp9, _) => new DecoderVp9Cuvid(desiredState), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg4, _) => new DecoderMpeg4Cuvid(desiredState), |
||||
|
||||
// hevc_qsv decoder sometimes causes green lines with 10-bit content
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Hevc, PixelFormat.YUV420P10LE) => new DecoderHevc(), |
||||
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Hevc, _) => new DecoderHevcQsv(), |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.H264, _) => new DecoderH264Qsv(), |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Mpeg2Video, _) => new DecoderMpeg2Qsv(), |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Vc1, _) => new DecoderVc1Qsv(), |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Vp9, _) => new DecoderVp9Qsv(), |
||||
|
||||
// vaapi should use implicit decoders
|
||||
(HardwareAccelerationMode.Vaapi, _, _) => new DecoderVaapi(), |
||||
|
||||
(_, VideoFormat.Hevc, _) => new DecoderHevc(), |
||||
(_, VideoFormat.H264, _) => new DecoderH264(), |
||||
(_, VideoFormat.Mpeg1Video, _) => new DecoderMpeg1Video(), |
||||
(_, VideoFormat.Mpeg2Video, _) => new DecoderMpeg2Video(), |
||||
(_, VideoFormat.Vc1, _) => new DecoderVc1(), |
||||
(_, VideoFormat.MsMpeg4V2, _) => new DecoderMsMpeg4V2(), |
||||
(_, VideoFormat.MsMpeg4V3, _) => new DecoderMsMpeg4V3(), |
||||
(_, VideoFormat.Mpeg4, _) => new DecoderMpeg4(), |
||||
(_, VideoFormat.Vp9, _) => new DecoderVp9(), |
||||
|
||||
(_, VideoFormat.Undetermined, _) => new DecoderImplicit(), |
||||
|
||||
// TODO: log warning and fall back to automatic decoder (i.e. None) instead of throwing?
|
||||
// maybe have a special "unknown decoder" that sets frame loc to software without specifying any decoder
|
||||
_ => throw new ArgumentOutOfRangeException( |
||||
nameof(currentState.VideoFormat), |
||||
currentState.VideoFormat, |
||||
null) |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderH264Cuvid : DecoderBase |
||||
{ |
||||
private readonly FrameState _desiredState; |
||||
|
||||
public DecoderH264Cuvid(FrameState desiredState) |
||||
{ |
||||
_desiredState = desiredState; |
||||
} |
||||
|
||||
public override string Name => "h264_cuvid"; |
||||
|
||||
public override IList<string> InputOptions |
||||
{ |
||||
get |
||||
{ |
||||
IList<string> result = base.InputOptions; |
||||
|
||||
if (_desiredState.Deinterlaced) |
||||
{ |
||||
result.Add("-deint"); |
||||
result.Add("2"); |
||||
} |
||||
|
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
|
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
|
||||
public override FrameState NextState(FrameState currentState) |
||||
{ |
||||
FrameState result = base.NextState(currentState); |
||||
return _desiredState.Deinterlaced ? result with { Deinterlaced = true } : result; |
||||
} |
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderHevcCuvid : DecoderBase |
||||
{ |
||||
private readonly FrameState _desiredState; |
||||
|
||||
public DecoderHevcCuvid(FrameState desiredState) |
||||
{ |
||||
_desiredState = desiredState; |
||||
} |
||||
|
||||
public override string Name => "hevc_cuvid"; |
||||
public override IList<string> InputOptions |
||||
{ |
||||
get |
||||
{ |
||||
IList<string> result = base.InputOptions; |
||||
|
||||
if (_desiredState.Deinterlaced) |
||||
{ |
||||
result.Add("-deint"); |
||||
result.Add("2"); |
||||
} |
||||
|
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
|
||||
public override FrameState NextState(FrameState currentState) |
||||
{ |
||||
FrameState result = base.NextState(currentState); |
||||
return _desiredState.Deinterlaced ? result with { Deinterlaced = true } : result; |
||||
} |
||||
} |
@ -0,0 +1,49 @@
@@ -0,0 +1,49 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderMpeg2Cuvid : DecoderBase |
||||
{ |
||||
private readonly FrameState _desiredState; |
||||
|
||||
public DecoderMpeg2Cuvid(FrameState desiredState) |
||||
{ |
||||
_desiredState = desiredState; |
||||
} |
||||
|
||||
public override string Name => "mpeg2_cuvid"; |
||||
public override IList<string> InputOptions |
||||
{ |
||||
get |
||||
{ |
||||
IList<string> result = base.InputOptions; |
||||
|
||||
if (_desiredState.Deinterlaced) |
||||
{ |
||||
result.Add("-deint"); |
||||
result.Add("2"); |
||||
|
||||
// make sure we decode into software
|
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("nv12"); |
||||
} |
||||
else |
||||
{ |
||||
// make sure we decode into hardware
|
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
|
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
|
||||
public override FrameState NextState(FrameState currentState) |
||||
{ |
||||
FrameState result = base.NextState(currentState); |
||||
return _desiredState.Deinterlaced |
||||
// when -deint is used, a hwupload_cuda is required to use more hw filters
|
||||
? result with { Deinterlaced = true, FrameDataLocation = FrameDataLocation.Software } |
||||
: result; |
||||
} |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderMpeg4Cuvid : DecoderBase |
||||
{ |
||||
private readonly FrameState _desiredState; |
||||
|
||||
public DecoderMpeg4Cuvid(FrameState desiredState) |
||||
{ |
||||
_desiredState = desiredState; |
||||
} |
||||
|
||||
public override string Name => "mpeg4_cuvid"; |
||||
|
||||
public override IList<string> InputOptions |
||||
{ |
||||
get |
||||
{ |
||||
IList<string> result = base.InputOptions; |
||||
|
||||
if (_desiredState.Deinterlaced) |
||||
{ |
||||
result.Add("-deint"); |
||||
result.Add("2"); |
||||
} |
||||
|
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
|
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
|
||||
public override FrameState NextState(FrameState currentState) |
||||
{ |
||||
FrameState result = base.NextState(currentState); |
||||
return _desiredState.Deinterlaced ? result with { Deinterlaced = true } : result; |
||||
} |
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderVc1Cuvid : DecoderBase |
||||
{ |
||||
private readonly FrameState _desiredState; |
||||
|
||||
public DecoderVc1Cuvid(FrameState desiredState) |
||||
{ |
||||
_desiredState = desiredState; |
||||
} |
||||
|
||||
public override string Name => "vc1_cuvid"; |
||||
public override IList<string> InputOptions |
||||
{ |
||||
get |
||||
{ |
||||
IList<string> result = base.InputOptions; |
||||
|
||||
if (_desiredState.Deinterlaced) |
||||
{ |
||||
result.Add("-deint"); |
||||
result.Add("2"); |
||||
} |
||||
|
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
|
||||
public override FrameState NextState(FrameState currentState) |
||||
{ |
||||
FrameState result = base.NextState(currentState); |
||||
return _desiredState.Deinterlaced ? result with { Deinterlaced = true } : result; |
||||
} |
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Cuvid; |
||||
|
||||
public class DecoderVp9Cuvid : DecoderBase |
||||
{ |
||||
private readonly FrameState _desiredState; |
||||
|
||||
public DecoderVp9Cuvid(FrameState desiredState) |
||||
{ |
||||
_desiredState = desiredState; |
||||
} |
||||
|
||||
public override string Name => "vp9_cuvid"; |
||||
public override IList<string> InputOptions |
||||
{ |
||||
get |
||||
{ |
||||
IList<string> result = base.InputOptions; |
||||
|
||||
if (_desiredState.Deinterlaced) |
||||
{ |
||||
result.Add("-deint"); |
||||
result.Add("2"); |
||||
} |
||||
|
||||
result.Add("-hwaccel_output_format"); |
||||
result.Add("cuda"); |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
|
||||
public override FrameState NextState(FrameState currentState) |
||||
{ |
||||
FrameState result = base.NextState(currentState); |
||||
return _desiredState.Deinterlaced ? result with { Deinterlaced = true } : result; |
||||
} |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public abstract class DecoderBase : IDecoder |
||||
{ |
||||
public abstract FrameDataLocation OutputFrameDataLocation { get; } |
||||
public IList<string> GlobalOptions => Array.Empty<string>(); |
||||
public virtual IList<string> InputOptions => new List<string> { "-c:v", Name }; |
||||
public IList<string> FilterOptions => Array.Empty<string>(); |
||||
public IList<string> OutputOptions => Array.Empty<string>(); |
||||
public virtual FrameState NextState(FrameState currentState) => |
||||
currentState with { FrameDataLocation = OutputFrameDataLocation }; |
||||
public abstract string Name { get; } |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderH264 : DecoderBase |
||||
{ |
||||
public override string Name => "h264"; |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderHevc : DecoderBase |
||||
{ |
||||
public override string Name => "hevc"; |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderImplicit : DecoderBase |
||||
{ |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; |
||||
public override string Name => string.Empty; |
||||
public override IList<string> InputOptions => Array.Empty<string>(); |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderMpeg1Video : DecoderBase |
||||
{ |
||||
public override string Name => "mpeg1video"; |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderMpeg2Video : DecoderBase |
||||
{ |
||||
public override string Name => "mpeg2video"; |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderMpeg4 : DecoderBase |
||||
{ |
||||
public override string Name => "mpeg4"; |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderMsMpeg4V2 : DecoderBase |
||||
{ |
||||
public override string Name => "msmpeg4v2"; |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderMsMpeg4V3 : DecoderBase |
||||
{ |
||||
public override string Name => "msmpeg4v3"; |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; |
||||
} |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderVaapi : DecoderBase |
||||
{ |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
public override string Name => "implicit_vaapi"; |
||||
public override IList<string> InputOptions => Array.Empty<string>(); |
||||
|
||||
public override FrameState NextState(FrameState currentState) |
||||
{ |
||||
FrameState nextState = base.NextState(currentState); |
||||
|
||||
return currentState.PixelFormat.Match( |
||||
pixelFormat => nextState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }, |
||||
() => nextState); |
||||
} |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderVc1 : DecoderBase |
||||
{ |
||||
public override string Name => "vc1"; |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public class DecoderVp9 : DecoderBase |
||||
{ |
||||
public override string Name => "vp9"; |
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Software; |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder; |
||||
|
||||
public interface IDecoder : IPipelineStep |
||||
{ |
||||
string Name { get; } |
||||
} |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Qsv; |
||||
|
||||
public class DecoderH264Qsv : DecoderBase |
||||
{ |
||||
public override string Name => "h264_qsv"; |
||||
|
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
|
||||
public override FrameState NextState(FrameState currentState) |
||||
{ |
||||
FrameState nextState = base.NextState(currentState); |
||||
|
||||
return currentState.PixelFormat.Match( |
||||
pixelFormat => nextState with { PixelFormat = new PixelFormatNv12(pixelFormat.Name) }, |
||||
() => nextState); |
||||
} |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Qsv; |
||||
|
||||
public class DecoderHevcQsv : DecoderBase |
||||
{ |
||||
public override string Name => "hevc_qsv"; |
||||
|
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Qsv; |
||||
|
||||
public class DecoderMpeg2Qsv : DecoderBase |
||||
{ |
||||
public override string Name => "mpeg2_qsv"; |
||||
|
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Qsv; |
||||
|
||||
public class DecoderVc1Qsv : DecoderBase |
||||
{ |
||||
public override string Name => "vc1_qsv"; |
||||
|
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Decoder.Qsv; |
||||
|
||||
public class DecoderVp9Qsv : DecoderBase |
||||
{ |
||||
public override string Name => "vp9_qsv"; |
||||
|
||||
public override FrameDataLocation OutputFrameDataLocation => FrameDataLocation.Hardware; |
||||
} |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
using ErsatzTV.FFmpeg.Encoder.Nvenc; |
||||
using ErsatzTV.FFmpeg.Encoder.Qsv; |
||||
using ErsatzTV.FFmpeg.Encoder.Vaapi; |
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public static class AvailableEncoders |
||||
{ |
||||
public static IEncoder ForVideoFormat(FrameState currentState, FrameState desiredState) => |
||||
(desiredState.HardwareAccelerationMode, desiredState.VideoFormat) switch |
||||
{ |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.Hevc) => new EncoderHevcNvenc(), |
||||
(HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new EncoderH264Nvenc(), |
||||
|
||||
(HardwareAccelerationMode.Qsv, VideoFormat.Hevc) => new EncoderHevcQsv(), |
||||
(HardwareAccelerationMode.Qsv, VideoFormat.H264) => new EncoderH264Qsv(), |
||||
|
||||
(HardwareAccelerationMode.Vaapi, VideoFormat.Hevc) => new EncoderHevcVaapi(currentState), |
||||
(HardwareAccelerationMode.Vaapi, VideoFormat.H264) => new EncoderH264Vaapi(currentState), |
||||
|
||||
(_, VideoFormat.Hevc) => new EncoderLibx265(), |
||||
(_, VideoFormat.H264) => new EncoderLibx264(), |
||||
(_, VideoFormat.Mpeg2Video) => new EncoderMpeg2Video(), |
||||
|
||||
(_, VideoFormat.Undetermined) => new EncoderImplicitVideo(), |
||||
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(desiredState.VideoFormat), desiredState.VideoFormat, null) |
||||
}; |
||||
|
||||
public static IEncoder ForAudioFormat(FrameState desiredState) |
||||
{ |
||||
return desiredState.AudioFormat.Match( |
||||
audioFormat => |
||||
audioFormat switch |
||||
{ |
||||
AudioFormat.Aac => (IEncoder)new EncoderAac(), |
||||
AudioFormat.Ac3 => new EncoderAc3(), |
||||
_ => throw new ArgumentOutOfRangeException(nameof(audioFormat), audioFormat, null) |
||||
}, |
||||
() => throw new ArgumentOutOfRangeException( |
||||
nameof(desiredState.AudioFormat), |
||||
desiredState.AudioFormat, |
||||
null)); |
||||
} |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public class EncoderAac : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState with { AudioFormat = AudioFormat.Aac }; |
||||
|
||||
public override string Name => "aac"; |
||||
|
||||
public override StreamKind Kind => StreamKind.Audio; |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public class EncoderAc3 : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState with { AudioFormat = AudioFormat.Ac3 }; |
||||
|
||||
public override string Name => "ac3"; |
||||
|
||||
public override StreamKind Kind => StreamKind.Audio; |
||||
} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public abstract class EncoderBase : IEncoder |
||||
{ |
||||
public IList<string> GlobalOptions => Array.Empty<string>(); |
||||
public IList<string> InputOptions => Array.Empty<string>(); |
||||
public IList<string> FilterOptions => Array.Empty<string>(); |
||||
public virtual IList<string> OutputOptions => new List<string> { Kind == StreamKind.Video ? "-c:v" : "-c:a", Name }; |
||||
public abstract FrameState NextState(FrameState currentState); |
||||
|
||||
public abstract string Name { get; } |
||||
public abstract StreamKind Kind { get; } |
||||
public virtual string Filter => string.Empty; |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public class EncoderCopyAll : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState; |
||||
public override string Name => "copy"; |
||||
public override StreamKind Kind => StreamKind.All; |
||||
public override IList<string> OutputOptions => new List<string> { "-c", Name }; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public class EncoderCopyAudio : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState; |
||||
public override string Name => "copy"; |
||||
public override StreamKind Kind => StreamKind.Audio; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public class EncoderCopyVideo : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState; |
||||
public override string Name => "copy"; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public class EncoderImplicitVideo : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState; |
||||
|
||||
public override string Name => string.Empty; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
public override IList<string> OutputOptions => Array.Empty<string>(); |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public class EncoderLibx264 : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState with { VideoFormat = VideoFormat.H264 }; |
||||
public override string Name => "libx264"; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public class EncoderLibx265 : EncoderBase |
||||
{ |
||||
// TODO: is tag:v needed for mpegts?
|
||||
public override IList<string> OutputOptions => new List<string> { "-c:v", Name, "-tag:v", "hvc1" }; |
||||
public override FrameState NextState(FrameState currentState) => currentState with { VideoFormat = VideoFormat.Hevc }; |
||||
public override string Name => "libx265"; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public class EncoderMpeg2Video : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState with { VideoFormat = VideoFormat.Mpeg2Video }; |
||||
public override string Name => "mpeg2video"; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Encoder; |
||||
|
||||
public interface IEncoder : IPipelineFilterStep |
||||
{ |
||||
string Name { get; } |
||||
StreamKind Kind { get; } |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder.Nvenc; |
||||
|
||||
public class EncoderH264Nvenc : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
VideoFormat = VideoFormat.H264, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
|
||||
public override string Name => "h264_nvenc"; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder.Nvenc; |
||||
|
||||
public class EncoderHevcNvenc : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
VideoFormat = VideoFormat.Hevc, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
|
||||
public override string Name => "hevc_nvenc"; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder.Qsv; |
||||
|
||||
public class EncoderH264Qsv : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
VideoFormat = VideoFormat.H264, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
|
||||
public override string Name => "h264_qsv"; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
} |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder.Qsv; |
||||
|
||||
public class EncoderHevcQsv : EncoderBase |
||||
{ |
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
VideoFormat = VideoFormat.Hevc, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
|
||||
public override string Name => "hevc_qsv"; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder.Vaapi; |
||||
|
||||
public class EncoderH264Vaapi : EncoderBase |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
|
||||
public EncoderH264Vaapi(FrameState currentState) |
||||
{ |
||||
_currentState = currentState; |
||||
} |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
VideoFormat = VideoFormat.H264, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
|
||||
public override string Name => "h264_vaapi"; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
|
||||
public override string Filter => _currentState.FrameDataLocation == FrameDataLocation.Software |
||||
? "hwupload" |
||||
: string.Empty; |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Encoder.Vaapi; |
||||
|
||||
public class EncoderHevcVaapi : EncoderBase |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
|
||||
public EncoderHevcVaapi(FrameState currentState) |
||||
{ |
||||
_currentState = currentState; |
||||
} |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
VideoFormat = VideoFormat.Hevc, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
|
||||
public override string Name => "hevc_vaapi"; |
||||
public override StreamKind Kind => StreamKind.Video; |
||||
|
||||
public override string Filter => _currentState.FrameDataLocation == FrameDataLocation.Software |
||||
? "hwupload" |
||||
: string.Empty; |
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
||||
<PropertyGroup> |
||||
<TargetFramework>net6.0</TargetFramework> |
||||
<ImplicitUsings>enable</ImplicitUsings> |
||||
<Nullable>enable</Nullable> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<PackageReference Include="LanguageExt.Core" Version="4.0.3" /> |
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<Compile Remove="Format\PixelFormatYuv444P10Le.cs" /> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
using System.Globalization; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class AudioPadFilter : BaseFilter |
||||
{ |
||||
private readonly TimeSpan _wholeDuration; |
||||
|
||||
public AudioPadFilter(TimeSpan wholeDuration) |
||||
{ |
||||
_wholeDuration = wholeDuration; |
||||
} |
||||
|
||||
public override string Filter |
||||
{ |
||||
get |
||||
{ |
||||
var durationString = _wholeDuration.TotalMilliseconds.ToString(NumberFormatInfo.InvariantInfo); |
||||
return $"apad=whole_dur={durationString}ms"; |
||||
} |
||||
} |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with { AudioDuration = _wholeDuration }; |
||||
} |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
using ErsatzTV.FFmpeg.Filter.Cuda; |
||||
using ErsatzTV.FFmpeg.Filter.Vaapi; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public static class AvailableDeinterlaceFilters |
||||
{ |
||||
public static IPipelineFilterStep ForAcceleration( |
||||
HardwareAccelerationMode accelMode, |
||||
FrameState currentState) => |
||||
accelMode switch |
||||
{ |
||||
HardwareAccelerationMode.Nvenc => new YadifCudaFilter(currentState), |
||||
HardwareAccelerationMode.Vaapi => new DeinterlaceVaapiFilter(currentState), |
||||
_ => new YadifFilter(currentState) |
||||
}; |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.FFmpeg.Filter.Cuda; |
||||
using ErsatzTV.FFmpeg.Filter.Qsv; |
||||
using ErsatzTV.FFmpeg.Filter.Vaapi; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public static class AvailableScaleFilters |
||||
{ |
||||
public static IPipelineFilterStep ForAcceleration( |
||||
HardwareAccelerationMode accelMode, |
||||
FrameState currentState, |
||||
FrameSize scaledSize, |
||||
FrameSize paddedSize) => |
||||
accelMode switch |
||||
{ |
||||
HardwareAccelerationMode.Nvenc => new ScaleCudaFilter(currentState, scaledSize, paddedSize), |
||||
HardwareAccelerationMode.Qsv => new ScaleQsvFilter(currentState, scaledSize), |
||||
HardwareAccelerationMode.Vaapi => new ScaleVaapiFilter(currentState, scaledSize, paddedSize), |
||||
_ => new ScaleFilter(currentState, scaledSize, paddedSize) |
||||
}; |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public abstract class BaseFilter : IPipelineFilterStep |
||||
{ |
||||
public virtual IList<string> GlobalOptions => Array.Empty<string>(); |
||||
public virtual IList<string> InputOptions => Array.Empty<string>(); |
||||
public virtual IList<string> FilterOptions => Array.Empty<string>(); |
||||
public virtual IList<string> OutputOptions => Array.Empty<string>(); |
||||
public abstract FrameState NextState(FrameState currentState); |
||||
|
||||
public abstract string Filter { get; } |
||||
} |
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class ComplexFilter : IPipelineStep |
||||
{ |
||||
private readonly IList<InputFile> _inputFiles; |
||||
private readonly IList<IPipelineFilterStep> _audioFilters; |
||||
private readonly IList<IPipelineFilterStep> _videoFilters; |
||||
|
||||
public ComplexFilter(IList<InputFile> inputFiles, IList<IPipelineFilterStep> audioFilters, IList<IPipelineFilterStep> videoFilters) |
||||
{ |
||||
_inputFiles = inputFiles; |
||||
_audioFilters = audioFilters; |
||||
_videoFilters = videoFilters; |
||||
} |
||||
|
||||
private IList<string> Arguments() |
||||
{ |
||||
var audioLabel = "0:a"; |
||||
var videoLabel = "0:v"; |
||||
|
||||
var result = new List<string>(); |
||||
|
||||
string audioFilterComplex = string.Empty; |
||||
string videoFilterComplex = string.Empty; |
||||
|
||||
for (var i = 0; i < _inputFiles.Count; i++) |
||||
{ |
||||
InputFile file = _inputFiles[i]; |
||||
for (var j = 0; j < file.Streams.Count; j++) |
||||
{ |
||||
MediaStream stream = file.Streams[j]; |
||||
switch (stream.Kind) |
||||
{ |
||||
case StreamKind.Audio: |
||||
audioLabel = $"{i}:{stream.Index}"; |
||||
if (_audioFilters.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) |
||||
{ |
||||
audioFilterComplex += $"[{i}:{stream.Index}]"; |
||||
audioFilterComplex += string.Join( |
||||
",", |
||||
_audioFilters.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
audioLabel = "[a]"; |
||||
audioFilterComplex += audioLabel; |
||||
} |
||||
break; |
||||
case StreamKind.Video: |
||||
videoLabel = $"{i}:{stream.Index}"; |
||||
if (_videoFilters.Any(f => !string.IsNullOrWhiteSpace(f.Filter))) |
||||
{ |
||||
videoFilterComplex += $"[{i}:{stream.Index}]"; |
||||
videoFilterComplex += string.Join( |
||||
",", |
||||
_videoFilters.Select(f => f.Filter).Filter(s => !string.IsNullOrWhiteSpace(s))); |
||||
videoLabel = "[v]"; |
||||
videoFilterComplex += videoLabel; |
||||
} |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (!string.IsNullOrWhiteSpace(audioFilterComplex) || !string.IsNullOrWhiteSpace(videoFilterComplex)) |
||||
{ |
||||
var filterComplex = string.Join( |
||||
";", |
||||
new[] { audioFilterComplex, videoFilterComplex }.Where(s => !string.IsNullOrWhiteSpace(s))); |
||||
|
||||
result.AddRange(new[] { "-filter_complex", filterComplex }); |
||||
} |
||||
|
||||
result.AddRange(new[] { "-map", audioLabel, "-map", videoLabel }); |
||||
|
||||
return result; |
||||
} |
||||
|
||||
public IList<string> GlobalOptions => Array.Empty<string>(); |
||||
public IList<string> InputOptions => Array.Empty<string>(); |
||||
public IList<string> FilterOptions => Arguments(); |
||||
public IList<string> OutputOptions => Array.Empty<string>(); |
||||
public FrameState NextState(FrameState currentState) => currentState; |
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter.Cuda; |
||||
|
||||
public class ScaleCudaFilter : BaseFilter |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
private readonly FrameSize _scaledSize; |
||||
private readonly FrameSize _paddedSize; |
||||
|
||||
public ScaleCudaFilter(FrameState currentState, FrameSize scaledSize, FrameSize paddedSize) |
||||
{ |
||||
_currentState = currentState; |
||||
_scaledSize = scaledSize; |
||||
_paddedSize = paddedSize; |
||||
} |
||||
|
||||
public override string Filter |
||||
{ |
||||
get |
||||
{ |
||||
string scale = string.Empty; |
||||
if (_currentState.ScaledSize == _scaledSize) |
||||
{ |
||||
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat) |
||||
{ |
||||
// don't need scaling, but still need pixel format
|
||||
scale = $"scale_cuda=format={pixelFormat.FFmpegName}"; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
string targetSize = $"{_paddedSize.Width}:{_paddedSize.Height}"; |
||||
string format = string.Empty; |
||||
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat) |
||||
{ |
||||
format = $":format={pixelFormat.FFmpegName}"; |
||||
} |
||||
|
||||
scale = $"scale_cuda={targetSize}:force_original_aspect_ratio=1{format}"; |
||||
} |
||||
|
||||
// TODO: this might not always upload to hardware, so NextState could be inaccurate
|
||||
if (string.IsNullOrWhiteSpace(scale)) |
||||
{ |
||||
return scale; |
||||
} |
||||
|
||||
return _currentState.FrameDataLocation == FrameDataLocation.Hardware |
||||
? scale |
||||
: $"hwupload_cuda,{scale}"; |
||||
} |
||||
} |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
ScaledSize = _scaledSize, |
||||
PaddedSize = _scaledSize, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
} |
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter.Cuda; |
||||
|
||||
public class YadifCudaFilter : BaseFilter |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
|
||||
public YadifCudaFilter(FrameState currentState) |
||||
{ |
||||
_currentState = currentState; |
||||
} |
||||
|
||||
public override string Filter => |
||||
_currentState.FrameDataLocation == FrameDataLocation.Hardware ? "yadif_cuda" : "hwupload_cuda,yadif_cuda"; |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
Deinterlaced = true, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class NormalizeLoudnessFilter : BaseFilter |
||||
{ |
||||
public override string Filter => "loudnorm=I=-16:TP=-1.5:LRA=11"; |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with { NormalizeLoudness = true }; |
||||
} |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class PadFilter : BaseFilter |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
private readonly FrameSize _paddedSize; |
||||
|
||||
public PadFilter(FrameState currentState, FrameSize paddedSize) |
||||
{ |
||||
_currentState = currentState; |
||||
_paddedSize = paddedSize; |
||||
} |
||||
|
||||
public override string Filter |
||||
{ |
||||
get |
||||
{ |
||||
string pad = $"pad={_paddedSize.Width}:{_paddedSize.Height}:-1:-1:color=black"; |
||||
string pixelFormat = _currentState.PixelFormat.Match(pf => pf.FFmpegName, () => string.Empty); |
||||
|
||||
return _currentState.FrameDataLocation == FrameDataLocation.Hardware && !string.IsNullOrWhiteSpace(pixelFormat) |
||||
? $"hwdownload,format={pixelFormat},{pad}" // TODO: does this apply to other accels?
|
||||
: pad; |
||||
} |
||||
} |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
PaddedSize = _paddedSize, |
||||
FrameDataLocation = FrameDataLocation.Software |
||||
}; |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter.Qsv; |
||||
|
||||
public class ScaleQsvFilter : BaseFilter |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
private readonly FrameSize _scaledSize; |
||||
|
||||
public ScaleQsvFilter(FrameState currentState, FrameSize scaledSize) |
||||
{ |
||||
_currentState = currentState; |
||||
_scaledSize = scaledSize; |
||||
} |
||||
|
||||
public override string Filter |
||||
{ |
||||
get |
||||
{ |
||||
string scale = string.Empty; |
||||
|
||||
if (_currentState.ScaledSize == _scaledSize) |
||||
{ |
||||
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat) |
||||
{ |
||||
// don't need scaling, but still need pixel format
|
||||
scale = $"scale_qsv=format={pixelFormat.FFmpegName}"; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
string format = string.Empty; |
||||
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat) |
||||
{ |
||||
format = $":format={pixelFormat.FFmpegName}"; |
||||
} |
||||
|
||||
string targetSize = $"{_scaledSize.Width}:{_scaledSize.Height}"; |
||||
scale = $"scale_qsv={targetSize}{format}"; |
||||
} |
||||
|
||||
// TODO: this might not always upload to hardware, so NextState could be inaccurate
|
||||
if (string.IsNullOrWhiteSpace(scale)) |
||||
{ |
||||
return scale; |
||||
} |
||||
|
||||
return _currentState.FrameDataLocation == FrameDataLocation.Hardware |
||||
? scale |
||||
: $"hwupload=extra_hw_frames=64,{scale}"; |
||||
} |
||||
} |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
ScaledSize = _scaledSize, |
||||
PaddedSize = _scaledSize, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
} |
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class ScaleFilter : BaseFilter |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
private readonly FrameSize _scaledSize; |
||||
private readonly FrameSize _paddedSize; |
||||
|
||||
public ScaleFilter(FrameState currentState, FrameSize scaledSize, FrameSize paddedSize) |
||||
{ |
||||
_currentState = currentState; |
||||
_scaledSize = scaledSize; |
||||
_paddedSize = paddedSize; |
||||
} |
||||
|
||||
public override string Filter |
||||
{ |
||||
get |
||||
{ |
||||
string scale = |
||||
$"scale={_paddedSize.Width}:{_paddedSize.Height}:flags=fast_bilinear:force_original_aspect_ratio=decrease"; |
||||
|
||||
string hwdownload = string.Empty; |
||||
if (_currentState.FrameDataLocation == FrameDataLocation.Hardware) |
||||
{ |
||||
hwdownload = "hwdownload,"; |
||||
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat) |
||||
{ |
||||
if (pixelFormat.FFmpegName == FFmpegFormat.NV12) |
||||
{ |
||||
hwdownload = "hwdownload,format=nv12,"; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return $"{hwdownload}{scale}"; |
||||
} |
||||
} |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
ScaledSize = _scaledSize, |
||||
PaddedSize = _scaledSize, |
||||
FrameDataLocation = FrameDataLocation.Software |
||||
}; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class SetSarFilter : BaseFilter |
||||
{ |
||||
public override string Filter => "setsar=1"; |
||||
public override FrameState NextState(FrameState currentState) => currentState; |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
namespace ErsatzTV.FFmpeg.Filter.Vaapi; |
||||
|
||||
public class DeinterlaceVaapiFilter : BaseFilter |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
|
||||
public DeinterlaceVaapiFilter(FrameState currentState) |
||||
{ |
||||
_currentState = currentState; |
||||
} |
||||
|
||||
public override string Filter => |
||||
_currentState.FrameDataLocation == FrameDataLocation.Hardware |
||||
? "deinterlace_vaapi" |
||||
: "hwupload,deinterlace_vaapi"; |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
Deinterlaced = true, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
} |
@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter.Vaapi; |
||||
|
||||
public class ScaleVaapiFilter : BaseFilter |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
private readonly FrameSize _scaledSize; |
||||
private readonly FrameSize _paddedSize; |
||||
|
||||
public ScaleVaapiFilter(FrameState currentState, FrameSize scaledSize, FrameSize paddedSize) |
||||
{ |
||||
_currentState = currentState; |
||||
_scaledSize = scaledSize; |
||||
_paddedSize = paddedSize; |
||||
} |
||||
|
||||
public override string Filter |
||||
{ |
||||
get |
||||
{ |
||||
string scale = string.Empty; |
||||
|
||||
if (_currentState.ScaledSize == _scaledSize) |
||||
{ |
||||
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat) |
||||
{ |
||||
// don't need scaling, but still need pixel format
|
||||
scale = $"scale_vaapi=format={pixelFormat.FFmpegName}"; |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
string format = string.Empty; |
||||
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat) |
||||
{ |
||||
format = $":format={pixelFormat.FFmpegName}"; |
||||
} |
||||
|
||||
string targetSize = $"{_paddedSize.Width}:{_paddedSize.Height}"; |
||||
scale = $"scale_vaapi={targetSize}:force_original_aspect_ratio=1:force_divisible_by=2{format}"; |
||||
} |
||||
|
||||
// TODO: this might not always upload to hardware, so NextState could be inaccurate
|
||||
if (string.IsNullOrWhiteSpace(scale)) |
||||
{ |
||||
return scale; |
||||
} |
||||
|
||||
return _currentState.FrameDataLocation == FrameDataLocation.Hardware |
||||
? scale |
||||
: $"hwupload,{scale}"; |
||||
} |
||||
} |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
ScaledSize = _scaledSize, |
||||
PaddedSize = _scaledSize, |
||||
FrameDataLocation = FrameDataLocation.Hardware |
||||
}; |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
using ErsatzTV.FFmpeg.Format; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Filter; |
||||
|
||||
public class YadifFilter : BaseFilter |
||||
{ |
||||
private readonly FrameState _currentState; |
||||
|
||||
public YadifFilter(FrameState currentState) |
||||
{ |
||||
_currentState = currentState; |
||||
} |
||||
|
||||
public override string Filter |
||||
{ |
||||
get |
||||
{ |
||||
string hwdownload = string.Empty; |
||||
if (_currentState.FrameDataLocation == FrameDataLocation.Hardware) |
||||
{ |
||||
hwdownload = "hwdownload,"; |
||||
foreach (IPixelFormat pixelFormat in _currentState.PixelFormat) |
||||
{ |
||||
if (pixelFormat.FFmpegName == FFmpegFormat.NV12) |
||||
{ |
||||
hwdownload = "hwdownload,format=nv12,"; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return $"{hwdownload}yadif=1"; |
||||
} |
||||
} |
||||
|
||||
public override FrameState NextState(FrameState currentState) => currentState with |
||||
{ |
||||
Deinterlaced = true, |
||||
FrameDataLocation = FrameDataLocation.Software |
||||
}; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public static class AudioFormat |
||||
{ |
||||
public const string Aac = "aac"; |
||||
public const string Ac3 = "ac3"; |
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using LanguageExt; |
||||
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public static class AvailablePixelFormats |
||||
{ |
||||
public static IPixelFormat ForPixelFormat(string pixelFormat) |
||||
{ |
||||
return pixelFormat switch |
||||
{ |
||||
PixelFormat.YUV420P => new PixelFormatYuv420P(), |
||||
PixelFormat.YUV420P10LE => new PixelFormatYuv420P10Le(), |
||||
PixelFormat.YUVJ420P => new PixelFormatYuvJ420P(), |
||||
PixelFormat.YUV444P => new PixelFormatYuv444P(), |
||||
_ => throw new ArgumentOutOfRangeException(nameof(pixelFormat), pixelFormat, null) |
||||
}; |
||||
} |
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class ConcatInputFormat : IPipelineStep |
||||
{ |
||||
public IList<string> GlobalOptions => Array.Empty<string>(); |
||||
|
||||
public IList<string> InputOptions => new List<string> |
||||
{ |
||||
"-f", "concat", |
||||
"-safe", "0", |
||||
"-protocol_whitelist", "file,http,tcp,https,tcp,tls", |
||||
"-probesize", "32" |
||||
}; |
||||
|
||||
public IList<string> FilterOptions => Array.Empty<string>(); |
||||
public IList<string> OutputOptions => Array.Empty<string>(); |
||||
public FrameState NextState(FrameState currentState) => currentState; |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class FFmpegFormat |
||||
{ |
||||
public const string YUV420P = "yuv420p"; |
||||
public const string YUV444P = "yuv444p"; |
||||
public const string P010LE = "p010le"; |
||||
public const string NV12 = "nv12"; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public interface IPixelFormat |
||||
{ |
||||
string Name { get; } |
||||
string FFmpegName { get; } |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public static class PixelFormat |
||||
{ |
||||
public const string YUV420P = "yuv420p"; |
||||
public const string YUV420P10LE = "yuv420p10le"; |
||||
public const string YUVJ420P = "yuvj420p"; |
||||
public const string YUV444P = "yuv444p"; |
||||
public const string YUV444P10LE = "yuv444p10le"; |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class PixelFormatNv12 : IPixelFormat |
||||
{ |
||||
public PixelFormatNv12(string name) |
||||
{ |
||||
Name = name; |
||||
} |
||||
|
||||
public string Name { get; } |
||||
|
||||
public string FFmpegName => "nv12"; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class PixelFormatUnknown : IPixelFormat |
||||
{ |
||||
public string Name => "unknown"; |
||||
public string FFmpegName => "unknown"; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class PixelFormatYuv420P : IPixelFormat |
||||
{ |
||||
public string Name => PixelFormat.YUV420P; |
||||
public string FFmpegName => FFmpegFormat.YUV420P; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class PixelFormatYuv420P10Le : IPixelFormat |
||||
{ |
||||
public string Name => PixelFormat.YUV420P10LE; |
||||
public string FFmpegName => FFmpegFormat.P010LE; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class PixelFormatYuv444P : IPixelFormat |
||||
{ |
||||
public string Name => PixelFormat.YUV444P; |
||||
public string FFmpegName => FFmpegFormat.YUV444P; |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class PixelFormatYuv444P10Le : IPixelFormat |
||||
{ |
||||
public string Name => PixelFormat.YUV444P10LE; |
||||
public string FFmpegName => FFmpegFormat.P010LE; |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public class PixelFormatYuvJ420P : IPixelFormat |
||||
{ |
||||
public string Name => PixelFormat.YUVJ420P; |
||||
|
||||
// always convert this to yuv420p in filter chains
|
||||
public string FFmpegName => FFmpegFormat.YUV420P; |
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
namespace ErsatzTV.FFmpeg.Format; |
||||
|
||||
public static class VideoFormat |
||||
{ |
||||
public const string Hevc = "hevc"; |
||||
public const string H264 = "h264"; |
||||
public const string Mpeg1Video = "mpeg1video"; |
||||
public const string Mpeg2Video = "mpeg2video"; |
||||
public const string MsMpeg4V2 = "msmpeg4v2"; |
||||
public const string MsMpeg4V3 = "msmpeg4v3"; |
||||
public const string Vc1 = "vc1"; |
||||
public const string Mpeg4 = "mpeg4"; |
||||
public const string Vp9 = "vp9"; |
||||
public const string Av1 = "av1"; |
||||
public const string MpegTs = "mpegts"; |
||||
|
||||
public const string Undetermined = ""; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.FFmpeg; |
||||
|
||||
public enum FrameDataLocation |
||||
{ |
||||
Unknown = 0, |
||||
Hardware = 1, |
||||
Software = 2 |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue