Browse Source

start to use new ffmpeg library (#632)

* 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 tests
pull/631/head
Jason Dove 4 years ago committed by GitHub
parent
commit
5a442a06a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 4
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs
  3. 1
      ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs
  4. 5
      ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs
  5. 10
      ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs
  6. 17
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  7. 10
      ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs
  8. 132
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  9. 1
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  10. 5
      ErsatzTV.Core/ErsatzTV.Core.csproj
  11. 4
      ErsatzTV.Core/FFmpeg/ConcatPlaylist.cs
  12. 240
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  13. 2
      ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs
  14. 26
      ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs
  15. 31
      ErsatzTV.Core/FFmpeg/FFmpegProcessServiceFactory.cs
  16. 9
      ErsatzTV.Core/FFmpeg/IFFmpegProcessServiceFactory.cs
  17. 9
      ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs
  18. 12
      ErsatzTV.Core/Metadata/LocalFolderScanner.cs
  19. 5
      ErsatzTV.Core/Metadata/MovieFolderScanner.cs
  20. 5
      ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs
  21. 5
      ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs
  22. 5
      ErsatzTV.Core/Metadata/SongFolderScanner.cs
  23. 5
      ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs
  24. 29
      ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj
  25. 148
      ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs
  26. 38
      ErsatzTV.FFmpeg/CommandGenerator.cs
  27. 59
      ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs
  28. 40
      ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderH264Cuvid.cs
  29. 38
      ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderHevcCuvid.cs
  30. 49
      ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg2Cuvid.cs
  31. 40
      ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg4Cuvid.cs
  32. 38
      ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVc1Cuvid.cs
  33. 38
      ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVp9Cuvid.cs
  34. 13
      ErsatzTV.FFmpeg/Decoder/DecoderBase.cs
  35. 7
      ErsatzTV.FFmpeg/Decoder/DecoderH264.cs
  36. 7
      ErsatzTV.FFmpeg/Decoder/DecoderHevc.cs
  37. 8
      ErsatzTV.FFmpeg/Decoder/DecoderImplicit.cs
  38. 7
      ErsatzTV.FFmpeg/Decoder/DecoderMpeg1.cs
  39. 7
      ErsatzTV.FFmpeg/Decoder/DecoderMpeg2.cs
  40. 7
      ErsatzTV.FFmpeg/Decoder/DecoderMpeg4.cs
  41. 7
      ErsatzTV.FFmpeg/Decoder/DecoderMsMpeg4v2.cs
  42. 7
      ErsatzTV.FFmpeg/Decoder/DecoderMsMpeg4v3.cs
  43. 19
      ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs
  44. 7
      ErsatzTV.FFmpeg/Decoder/DecoderVc1.cs
  45. 7
      ErsatzTV.FFmpeg/Decoder/DecoderVp9.cs
  46. 6
      ErsatzTV.FFmpeg/Decoder/IDecoder.cs
  47. 19
      ErsatzTV.FFmpeg/Decoder/Qsv/DecoderH264Qsv.cs
  48. 8
      ErsatzTV.FFmpeg/Decoder/Qsv/DecoderHevcQsv.cs
  49. 8
      ErsatzTV.FFmpeg/Decoder/Qsv/DecoderMpeg2Qsv.cs
  50. 8
      ErsatzTV.FFmpeg/Decoder/Qsv/DecoderVc1Qsv.cs
  51. 8
      ErsatzTV.FFmpeg/Decoder/Qsv/DecoderVp9Qsv.cs
  52. 46
      ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs
  53. 12
      ErsatzTV.FFmpeg/Encoder/EncoderAac.cs
  54. 12
      ErsatzTV.FFmpeg/Encoder/EncoderAc3.cs
  55. 14
      ErsatzTV.FFmpeg/Encoder/EncoderBase.cs
  56. 9
      ErsatzTV.FFmpeg/Encoder/EncoderCopyAll.cs
  57. 8
      ErsatzTV.FFmpeg/Encoder/EncoderCopyAudio.cs
  58. 8
      ErsatzTV.FFmpeg/Encoder/EncoderCopyVideo.cs
  59. 10
      ErsatzTV.FFmpeg/Encoder/EncoderImplicitVideo.cs
  60. 10
      ErsatzTV.FFmpeg/Encoder/EncoderLibx264.cs
  61. 12
      ErsatzTV.FFmpeg/Encoder/EncoderLibx265.cs
  62. 10
      ErsatzTV.FFmpeg/Encoder/EncoderMpeg2Video.cs
  63. 7
      ErsatzTV.FFmpeg/Encoder/IEncoder.cs
  64. 15
      ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs
  65. 15
      ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs
  66. 15
      ErsatzTV.FFmpeg/Encoder/Qsv/EncoderH264Qsv.cs
  67. 15
      ErsatzTV.FFmpeg/Encoder/Qsv/EncoderHevcQsv.cs
  68. 26
      ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderH264Vaapi.cs
  69. 26
      ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderHevcVaapi.cs
  70. 18
      ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj
  71. 24
      ErsatzTV.FFmpeg/Filter/AudioPadFilter.cs
  72. 17
      ErsatzTV.FFmpeg/Filter/AvailableDeinterlaceFilters.cs
  73. 21
      ErsatzTV.FFmpeg/Filter/AvailableScaleFilters.cs
  74. 12
      ErsatzTV.FFmpeg/Filter/BaseFilter.cs
  75. 81
      ErsatzTV.FFmpeg/Filter/ComplexFilter.cs
  76. 61
      ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs
  77. 20
      ErsatzTV.FFmpeg/Filter/Cuda/YadifCudaFilter.cs
  78. 8
      ErsatzTV.FFmpeg/Filter/NormalizeLoudnessFilter.cs
  79. 32
      ErsatzTV.FFmpeg/Filter/PadFilter.cs
  80. 60
      ErsatzTV.FFmpeg/Filter/Qsv/ScaleQsvFilter.cs
  81. 48
      ErsatzTV.FFmpeg/Filter/ScaleFilter.cs
  82. 7
      ErsatzTV.FFmpeg/Filter/SetSarFilter.cs
  83. 22
      ErsatzTV.FFmpeg/Filter/Vaapi/DeinterlaceVaapiFilter.cs
  84. 62
      ErsatzTV.FFmpeg/Filter/Vaapi/ScaleVaapiFilter.cs
  85. 40
      ErsatzTV.FFmpeg/Filter/YadifFilter.cs
  86. 7
      ErsatzTV.FFmpeg/Format/AudioFormat.cs
  87. 18
      ErsatzTV.FFmpeg/Format/AvailablePixelFormats.cs
  88. 18
      ErsatzTV.FFmpeg/Format/ConcatInputFormat.cs
  89. 9
      ErsatzTV.FFmpeg/Format/FFmpegFormat.cs
  90. 7
      ErsatzTV.FFmpeg/Format/IPixelFormat.cs
  91. 10
      ErsatzTV.FFmpeg/Format/PixelFormat.cs
  92. 13
      ErsatzTV.FFmpeg/Format/PixelFormatNv12.cs
  93. 7
      ErsatzTV.FFmpeg/Format/PixelFormatUnknown.cs
  94. 7
      ErsatzTV.FFmpeg/Format/PixelFormatYuv420P.cs
  95. 7
      ErsatzTV.FFmpeg/Format/PixelFormatYuv420P10Le.cs
  96. 7
      ErsatzTV.FFmpeg/Format/PixelFormatYuv444P.cs
  97. 7
      ErsatzTV.FFmpeg/Format/PixelFormatYuv444P10Le.cs
  98. 9
      ErsatzTV.FFmpeg/Format/PixelFormatYuvJ420P.cs
  99. 18
      ErsatzTV.FFmpeg/Format/VideoFormat.cs
  100. 8
      ErsatzTV.FFmpeg/FrameDataLocation.cs
  101. Some files were not shown because too many files have changed in this diff Show More

1
CHANGELOG.md

@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Fixed
- Add improved but experimental transcoder logic, which can be toggled on and off in `Settings`
- Fix `HLS Segmenter` bug when source video packet contains no duration (`N/A`)
## [0.4.1-alpha] - 2022-02-10

4
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegSettingsHandler.cs

@ -85,6 +85,10 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands @@ -85,6 +85,10 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder);
}
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegUseExperimentalTranscoder,
request.Settings.UseExperimentalTranscoder.ToString());
await _configElementRepository.Upsert(
ConfigElementKey.FFmpegPreferredLanguageCode,
request.Settings.PreferredLanguageCode);

1
ErsatzTV.Application/FFmpegProfiles/FFmpegSettingsViewModel.cs

@ -12,5 +12,6 @@ @@ -12,5 +12,6 @@
public int HlsSegmenterIdleTimeout { get; set; }
public int WorkAheadSegmenterLimit { get; set; }
public int InitialSegmentCount { get; set; }
public bool UseExperimentalTranscoder { get; set; }
}
}

5
ErsatzTV.Application/FFmpegProfiles/Queries/GetFFmpegSettingsHandler.cs

@ -36,6 +36,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries @@ -36,6 +36,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegWorkAheadSegmenters);
Option<int> initialSegmentCount =
await _configElementRepository.GetValue<int>(ConfigElementKey.FFmpegInitialSegmentCount);
Option<bool> useExperimentalTranscoder =
await _configElementRepository.GetValue<bool>(ConfigElementKey.FFmpegUseExperimentalTranscoder);
var result = new FFmpegSettingsViewModel
{
@ -46,7 +48,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries @@ -46,7 +48,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Queries
PreferredLanguageCode = await preferredLanguageCode.IfNoneAsync("eng"),
HlsSegmenterIdleTimeout = await hlsSegmenterIdleTimeout.IfNoneAsync(60),
WorkAheadSegmenterLimit = await workAheadSegmenterLimit.IfNoneAsync(1),
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1)
InitialSegmentCount = await initialSegmentCount.IfNoneAsync(1),
UseExperimentalTranscoder = await useExperimentalTranscoder.IfNoneAsync(false)
};
foreach (int watermarkId in watermark)

10
ErsatzTV.Application/Streaming/Queries/GetConcatProcessByChannelNumberHandler.cs

@ -3,6 +3,7 @@ using System.Diagnostics; @@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -13,14 +14,14 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -13,14 +14,14 @@ namespace ErsatzTV.Application.Streaming.Queries
{
public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber>
{
private readonly IFFmpegProcessService _ffmpegProcessService;
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
public GetConcatProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory,
IFFmpegProcessService ffmpegProcessService)
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory)
: base(dbContextFactory)
{
_ffmpegProcessService = ffmpegProcessService;
_ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
}
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
@ -33,7 +34,8 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -33,7 +34,8 @@ namespace ErsatzTV.Application.Streaming.Queries
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false));
Process process = _ffmpegProcessService.ConcatChannel(
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
Process process = ffmpegProcessService.ConcatChannel(
ffmpegPath,
saveReports,
channel,

17
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain; @@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Jellyfin;
@ -30,15 +31,15 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -30,15 +31,15 @@ namespace ErsatzTV.Application.Streaming.Queries
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ITelevisionRepository _televisionRepository;
private readonly IArtistRepository _artistRepository;
private readonly IFFmpegProcessService _ffmpegProcessService;
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
private readonly ILocalFileSystem _localFileSystem;
private readonly IPlexPathReplacementService _plexPathReplacementService;
private readonly ISongVideoGenerator _songVideoGenerator;
public GetPlayoutItemProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory,
IFFmpegProcessService ffmpegProcessService,
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
ILocalFileSystem localFileSystem,
IPlexPathReplacementService plexPathReplacementService,
IJellyfinPathReplacementService jellyfinPathReplacementService,
@ -49,7 +50,7 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -49,7 +50,7 @@ namespace ErsatzTV.Application.Streaming.Queries
ISongVideoGenerator songVideoGenerator)
: base(dbContextFactory)
{
_ffmpegProcessService = ffmpegProcessService;
_ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
_localFileSystem = localFileSystem;
_plexPathReplacementService = plexPathReplacementService;
_jellyfinPathReplacementService = jellyfinPathReplacementService;
@ -111,6 +112,8 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -111,6 +112,8 @@ namespace ErsatzTV.Application.Streaming.Queries
maybePlayoutItem = await CheckForFallbackFiller(dbContext, channel, now);
}
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
return await maybePlayoutItem.Match(
async playoutItemWithPath =>
{
@ -141,7 +144,7 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -141,7 +144,7 @@ namespace ErsatzTV.Application.Streaming.Queries
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false));
Process process = await _ffmpegProcessService.ForPlayoutItem(
Process process = await ffmpegProcessService.ForPlayoutItem(
ffmpegPath,
saveReports,
channel,
@ -190,7 +193,7 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -190,7 +193,7 @@ namespace ErsatzTV.Application.Streaming.Queries
case UnableToLocatePlayoutItem:
if (channel.FFmpegProfile.Transcode)
{
Process errorProcess = await _ffmpegProcessService.ForError(
Process errorProcess = await ffmpegProcessService.ForError(
ffmpegPath,
channel,
maybeDuration,
@ -210,7 +213,7 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -210,7 +213,7 @@ namespace ErsatzTV.Application.Streaming.Queries
case PlayoutItemDoesNotExistOnDisk:
if (channel.FFmpegProfile.Transcode)
{
Process errorProcess = await _ffmpegProcessService.ForError(
Process errorProcess = await ffmpegProcessService.ForError(
ffmpegPath,
channel,
maybeDuration,
@ -230,7 +233,7 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -230,7 +233,7 @@ namespace ErsatzTV.Application.Streaming.Queries
default:
if (channel.FFmpegProfile.Transcode)
{
Process errorProcess = await _ffmpegProcessService.ForError(
Process errorProcess = await ffmpegProcessService.ForError(
ffmpegPath,
channel,
maybeDuration,

10
ErsatzTV.Application/Streaming/Queries/GetWrappedProcessByChannelNumberHandler.cs

@ -3,6 +3,7 @@ using System.Diagnostics; @@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Threading.Tasks;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
@ -13,14 +14,14 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -13,14 +14,14 @@ namespace ErsatzTV.Application.Streaming.Queries
{
public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetWrappedProcessByChannelNumber>
{
private readonly IFFmpegProcessService _ffmpegProcessService;
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
public GetWrappedProcessByChannelNumberHandler(
IDbContextFactory<TvContext> dbContextFactory,
IFFmpegProcessService ffmpegProcessService)
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory)
: base(dbContextFactory)
{
_ffmpegProcessService = ffmpegProcessService;
_ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
}
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
@ -33,7 +34,8 @@ namespace ErsatzTV.Application.Streaming.Queries @@ -33,7 +34,8 @@ namespace ErsatzTV.Application.Streaming.Queries
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
.Map(result => result.IfNone(false));
Process process = _ffmpegProcessService.WrapSegmenter(
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
Process process = ffmpegProcessService.WrapSegmenter(
ffmpegPath,
saveReports,
channel,

132
ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs

@ -4,6 +4,8 @@ using System.Diagnostics; @@ -4,6 +4,8 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
@ -37,6 +39,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -37,6 +39,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
Assert.Pass();
}
public record InputFormat(string Encoder, string PixelFormat);
public enum Padding
{
NoPadding,
@ -56,22 +60,32 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -56,22 +60,32 @@ namespace ErsatzTV.Core.Tests.FFmpeg
VideoScanKind.Progressive,
VideoScanKind.Interlaced
};
public static string[] InputCodecs =
{
"h264",
"mpeg2video",
"hevc",
"mpeg4"
};
public static string[] InputPixelFormats =
public static InputFormat[] InputFormats =
{
"yuv420p",
"yuv420p10le",
// "yuvj420p",
// "yuv444p",
// "yuv444p10le"
new("libx264", "yuv420p"),
new("libx264", "yuvj420p"),
new("libx264", "yuv420p10le"),
new("libx264", "yuv444p10le"),
new("mpeg1video", "yuv420p"),
new("mpeg2video", "yuv420p"),
new("libx265", "yuv420p"),
new("libx265", "yuv420p10le"),
new("mpeg4", "yuv420p"),
new("libvpx-vp9", "yuv420p"),
// new("libaom-av1", "yuv420p")
// av1 yuv420p10le 51
new("msmpeg4v2", "yuv420p"),
new("msmpeg4v3", "yuv420p")
// wmv3 yuv420p 1
};
public static Resolution[] Resolutions =
@ -127,10 +141,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -127,10 +141,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
[Test, Combinatorial]
public async Task Transcode(
[ValueSource(typeof(TestData), nameof(TestData.InputCodecs))]
string inputCodec,
[ValueSource(typeof(TestData), nameof(TestData.InputPixelFormats))]
string inputPixelFormat,
[ValueSource(typeof(TestData), nameof(TestData.InputFormats))]
InputFormat inputFormat,
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
Resolution profileResolution,
[ValueSource(typeof(TestData), nameof(TestData.Paddings))]
@ -146,8 +158,17 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -146,8 +158,17 @@ namespace ErsatzTV.Core.Tests.FFmpeg
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec,
// [ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
{
if (inputFormat.Encoder is "mpeg1video" or "msmpeg4v2" or "msmpeg4v3")
{
if (videoScanKind == VideoScanKind.Interlaced)
{
Assert.Inconclusive($"{inputFormat.Encoder} does not support interlaced content");
return;
}
}
string name = GetStringSha256Hash(
$"{inputCodec}_{inputPixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileCodec}_{profileAcceleration}");
$"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{profileResolution}_{profileCodec}_{profileAcceleration}");
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
if (!File.Exists(file))
@ -158,7 +179,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -158,7 +179,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
string flags = videoScanKind == VideoScanKind.Interlaced ? "-flags +ildct+ilme" : string.Empty;
string args =
$"-y -f lavfi -i anoisesrc=color=brown -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 {videoFilter} -c:a aac -c:v {inputCodec} -shortest -pix_fmt {inputPixelFormat} -strict -2 {flags} {file}";
$"-y -f lavfi -i anoisesrc=color=brown -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 {videoFilter} -c:a aac -c:v {inputFormat.Encoder} -shortest -pix_fmt {inputFormat.PixelFormat} -strict -2 {flags} {file}";
var p1 = new Process
{
StartInfo = new ProcessStartInfo
@ -175,19 +196,35 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -175,19 +196,35 @@ namespace ErsatzTV.Core.Tests.FFmpeg
p1.ExitCode.Should().Be(0);
}
var service = new FFmpegProcessService(
var oldService = new FFmpegProcessService(
new FFmpegPlaybackSettingsCalculator(),
new FakeStreamSelector(),
new Mock<IImageCache>().Object,
new Mock<ITempFilePool>().Object,
new Mock<ILogger<FFmpegProcessService>>().Object);
MediaVersion v = new MediaVersion();
var service = new FFmpegLibraryProcessService(
oldService,
new FFmpegPlaybackSettingsCalculator(),
new FakeStreamSelector(),
new Mock<ILogger<FFmpegLibraryProcessService>>().Object);
var v = new MediaVersion
{
MediaFiles = new List<MediaFile>
{
new() { Path = file }
}
};
var metadataRepository = new Mock<IMetadataRepository>();
metadataRepository
.Setup(r => r.UpdateLocalStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
.Callback<MediaItem, MediaVersion, bool>((_, version, _) => v = version);
.Callback<MediaItem, MediaVersion, bool>((_, version, _) =>
{
version.MediaFiles = v.MediaFiles;
v = version;
});
var localStatisticsProvider = new LocalStatisticsProvider(
metadataRepository.Object,
@ -217,6 +254,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -217,6 +254,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
false,
new Channel(Guid.NewGuid())
{
Number = "1",
FFmpegProfile = FFmpegProfile.New("test", profileResolution) with
{
HardwareAcceleration = profileAcceleration,
@ -242,25 +280,51 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -242,25 +280,51 @@ namespace ErsatzTV.Core.Tests.FFmpeg
None);
process.StartInfo.RedirectStandardError = true;
process.EnableRaisingEvents = true;
// Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}");
process.Start().Should().BeTrue();
process.BeginOutputReadLine();
string error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
// ReSharper disable once MethodHasAsyncOverload
process.WaitForExit();
string[] unsupportedMessages =
{
"No support for codec",
"No usable",
"Provided device doesn't support"
};
var errorBuffer = new StringBuilder();
if (profileAcceleration != HardwareAccelerationKind.None && unsupportedMessages.Any(error.Contains))
process.ErrorDataReceived += (_, errorLine) =>
{
string data = errorLine.Data ?? string.Empty;
errorBuffer.AppendLine(data);
};
process.BeginOutputReadLine();
process.BeginErrorReadLine();
// string error = await process.StandardError.ReadToEndAsync();
var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(30));
try
{
await process.WaitForExitAsync(timeoutSignal.Token);
// ReSharper disable once MethodHasAsyncOverload
process.WaitForExit();
}
catch (OperationCanceledException)
{
process.Kill();
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'");
Assert.Fail($"Transcode failure (timeout): ffmpeg {string.Join(" ", quotedArgs)}");
return;
}
var error = errorBuffer.ToString();
bool isUnsupported = unsupportedMessages.Any(error.Contains);
if (profileAcceleration != HardwareAccelerationKind.None && isUnsupported)
{
var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList();
process.ExitCode.Should().Be(1, $"Error message with successful exit code? {string.Join(" ", quotedArgs)}");
@ -273,8 +337,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg @@ -273,8 +337,12 @@ namespace ErsatzTV.Core.Tests.FFmpeg
}
else
{
IEnumerable<string> quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'");
process.ExitCode.Should().Be(0, error + Environment.NewLine + string.Join(" ", quotedArgs));
var quotedArgs = process.StartInfo.ArgumentList.Map(a => $"\'{a}\'").ToList();
process.ExitCode.Should().Be(0, errorBuffer + Environment.NewLine + string.Join(" ", quotedArgs));
if (process.ExitCode == 0)
{
Console.WriteLine(string.Join(" ", quotedArgs));
}
}
}

1
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
public static ConfigElementKey FFmpegSegmenterTimeout => new("ffmpeg.segmenter.timeout_seconds");
public static ConfigElementKey FFmpegWorkAheadSegmenters => new("ffmpeg.segmenter.work_ahead_limit");
public static ConfigElementKey FFmpegInitialSegmentCount => new("ffmpeg.segmenter.initial_segment_count");
public static ConfigElementKey FFmpegUseExperimentalTranscoder => new("ffmpeg.use_experimental_transcoder");
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count");
public static ConfigElementKey ChannelsPageSize => new("pages.channels.page_size");

5
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
<PackageReference Include="Flurl" Version="3.0.4" />
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
<PackageReference Include="MediatR" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
@ -25,5 +26,9 @@ @@ -25,5 +26,9 @@
<_Parameter1>ErsatzTV.Core.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ErsatzTV.FFmpeg\ErsatzTV.FFmpeg.csproj" />
</ItemGroup>
</Project>

4
ErsatzTV.Core/FFmpeg/ConcatPlaylist.cs

@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
{
public override string ToString() =>
$@"ffconcat version 1.0
file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}
file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}";
file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}?mode=ts-legacy
file http://localhost:{Settings.ListenPort}/ffmpeg/stream/{ChannelNumber}?mode=ts-legacy";
}
}

240
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -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
};
}
}

2
ErsatzTV.Core/FFmpeg/FFmpegProcessBuilder.cs

@ -222,7 +222,7 @@ namespace ErsatzTV.Core.FFmpeg @@ -222,7 +222,7 @@ namespace ErsatzTV.Core.FFmpeg
_arguments.Add($"{fr}");
_arguments.Add("-vsync");
_arguments.Add("1");
_arguments.Add("cfr");
}
return this;

26
ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs

@ -10,6 +10,7 @@ using ErsatzTV.Core.Interfaces.Images; @@ -10,6 +10,7 @@ using ErsatzTV.Core.Interfaces.Images;
using LanguageExt;
using Microsoft.Extensions.Logging;
using static LanguageExt.Prelude;
using MediaStream = ErsatzTV.Core.Domain.MediaStream;
namespace ErsatzTV.Core.FFmpeg
{
@ -182,23 +183,18 @@ namespace ErsatzTV.Core.FFmpeg @@ -182,23 +183,18 @@ namespace ErsatzTV.Core.FFmpeg
.WithMetadata(channel, maybeAudioStream)
.WithDuration(finish - now);
switch (channel.StreamingMode)
return channel.StreamingMode switch
{
// HLS needs to segment and generate playlist
case StreamingMode.HttpLiveStreamingSegmenter:
return builder.WithHls(
channel.Number,
videoVersion,
ptsOffset,
playbackSettings.VideoTrackTimeScale,
playbackSettings.FrameRate)
.Build();
default:
return builder.WithFormat("mpegts")
.WithInitialDiscontinuity()
.WithPipe()
.Build();
}
StreamingMode.HttpLiveStreamingSegmenter => builder.WithHls(
channel.Number,
videoVersion,
ptsOffset,
playbackSettings.VideoTrackTimeScale,
playbackSettings.FrameRate)
.Build(),
_ => builder.WithFormat("mpegts").WithInitialDiscontinuity().WithPipe().Build()
};
}
public async Task<Process> ForError(

31
ErsatzTV.Core/FFmpeg/FFmpegProcessServiceFactory.cs

@ -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>();
}
}

9
ErsatzTV.Core/FFmpeg/IFFmpegProcessServiceFactory.cs

@ -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();
}

9
ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs

@ -19,16 +19,16 @@ namespace ErsatzTV.Core.FFmpeg @@ -19,16 +19,16 @@ namespace ErsatzTV.Core.FFmpeg
private readonly ITempFilePool _tempFilePool;
private readonly IImageCache _imageCache;
private readonly IFFmpegProcessService _ffmpegProcessService;
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
public SongVideoGenerator(
ITempFilePool tempFilePool,
IImageCache imageCache,
IFFmpegProcessService ffmpegProcessService)
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory)
{
_tempFilePool = tempFilePool;
_imageCache = imageCache;
_ffmpegProcessService = ffmpegProcessService;
_ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
}
public async Task<Tuple<string, MediaVersion>> GenerateSongVideo(
@ -208,7 +208,8 @@ namespace ErsatzTV.Core.FFmpeg @@ -208,7 +208,8 @@ namespace ErsatzTV.Core.FFmpeg
new() { Path = videoPath }
};
Either<BaseError, string> maybeSongImage = await _ffmpegProcessService.GenerateSongImage(
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
Either<BaseError, string> maybeSongImage = await ffmpegProcessService.GenerateSongImage(
ffmpegPath,
subtitleFile,
channel,

12
ErsatzTV.Core/Metadata/LocalFolderScanner.cs

@ -50,7 +50,7 @@ namespace ErsatzTV.Core.Metadata @@ -50,7 +50,7 @@ namespace ErsatzTV.Core.Metadata
.ToList();
private readonly IImageCache _imageCache;
private readonly IFFmpegProcessService _ffmpegProcessService;
private readonly IFFmpegProcessServiceFactory _ffmpegProcessServiceFactory;
private readonly ITempFilePool _tempFilePool;
private readonly ILocalFileSystem _localFileSystem;
@ -65,7 +65,7 @@ namespace ErsatzTV.Core.Metadata @@ -65,7 +65,7 @@ namespace ErsatzTV.Core.Metadata
IMetadataRepository metadataRepository,
IMediaItemRepository mediaItemRepository,
IImageCache imageCache,
IFFmpegProcessService ffmpegProcessService,
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
ITempFilePool tempFilePool,
ILogger logger)
{
@ -74,7 +74,7 @@ namespace ErsatzTV.Core.Metadata @@ -74,7 +74,7 @@ namespace ErsatzTV.Core.Metadata
_metadataRepository = metadataRepository;
_mediaItemRepository = mediaItemRepository;
_imageCache = imageCache;
_ffmpegProcessService = ffmpegProcessService;
_ffmpegProcessServiceFactory = ffmpegProcessServiceFactory;
_tempFilePool = tempFilePool;
_logger = logger;
}
@ -156,12 +156,14 @@ namespace ErsatzTV.Core.Metadata @@ -156,12 +156,14 @@ namespace ErsatzTV.Core.Metadata
// if ffmpeg path is passed, we need pre-processing
foreach (string path in ffmpegPath)
{
IFFmpegProcessService ffmpegProcessService = await _ffmpegProcessServiceFactory.GetService();
artworkFile = await attachedPicIndex.Match(
async picIndex =>
{
// extract attached pic (and convert to png)
string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt);
using Process process = _ffmpegProcessService.ExtractAttachedPicAsPng(
using Process process = ffmpegProcessService.ExtractAttachedPicAsPng(
path,
artworkFile,
picIndex,
@ -175,7 +177,7 @@ namespace ErsatzTV.Core.Metadata @@ -175,7 +177,7 @@ namespace ErsatzTV.Core.Metadata
{
// no attached pic index means convert to png
string tempName = _tempFilePool.GetNextTempFile(TempFileCategory.CoverArt);
using Process process = _ffmpegProcessService.ConvertToPng(path, artworkFile, tempName);
using Process process = ffmpegProcessService.ConvertToPng(path, artworkFile, tempName);
process.Start();
await process.WaitForExitAsync();

5
ErsatzTV.Core/Metadata/MovieFolderScanner.cs

@ -4,6 +4,7 @@ using System.IO; @@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -41,7 +42,7 @@ namespace ErsatzTV.Core.Metadata @@ -41,7 +42,7 @@ namespace ErsatzTV.Core.Metadata
ILibraryRepository libraryRepository,
IMediaItemRepository mediaItemRepository,
IMediator mediator,
IFFmpegProcessService ffmpegProcessService,
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
ITempFilePool tempFilePool,
ILogger<MovieFolderScanner> logger)
: base(
@ -50,7 +51,7 @@ namespace ErsatzTV.Core.Metadata @@ -50,7 +51,7 @@ namespace ErsatzTV.Core.Metadata
metadataRepository,
mediaItemRepository,
imageCache,
ffmpegProcessService,
ffmpegProcessServiceFactory,
tempFilePool,
logger)
{

5
ErsatzTV.Core/Metadata/MusicVideoFolderScanner.cs

@ -4,6 +4,7 @@ using System.IO; @@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -42,7 +43,7 @@ namespace ErsatzTV.Core.Metadata @@ -42,7 +43,7 @@ namespace ErsatzTV.Core.Metadata
ILibraryRepository libraryRepository,
IMediaItemRepository mediaItemRepository,
IMediator mediator,
IFFmpegProcessService ffmpegProcessService,
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
ITempFilePool tempFilePool,
ILogger<MusicVideoFolderScanner> logger) : base(
localFileSystem,
@ -50,7 +51,7 @@ namespace ErsatzTV.Core.Metadata @@ -50,7 +51,7 @@ namespace ErsatzTV.Core.Metadata
metadataRepository,
mediaItemRepository,
imageCache,
ffmpegProcessService,
ffmpegProcessServiceFactory,
tempFilePool,
logger)
{

5
ErsatzTV.Core/Metadata/OtherVideoFolderScanner.cs

@ -4,6 +4,7 @@ using System.IO; @@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -40,7 +41,7 @@ namespace ErsatzTV.Core.Metadata @@ -40,7 +41,7 @@ namespace ErsatzTV.Core.Metadata
IOtherVideoRepository otherVideoRepository,
ILibraryRepository libraryRepository,
IMediaItemRepository mediaItemRepository,
IFFmpegProcessService ffmpegProcessService,
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
ITempFilePool tempFilePool,
ILogger<OtherVideoFolderScanner> logger) : base(
localFileSystem,
@ -48,7 +49,7 @@ namespace ErsatzTV.Core.Metadata @@ -48,7 +49,7 @@ namespace ErsatzTV.Core.Metadata
metadataRepository,
mediaItemRepository,
imageCache,
ffmpegProcessService,
ffmpegProcessServiceFactory,
tempFilePool,
logger)
{

5
ErsatzTV.Core/Metadata/SongFolderScanner.cs

@ -5,6 +5,7 @@ using System.Linq; @@ -5,6 +5,7 @@ using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -41,7 +42,7 @@ namespace ErsatzTV.Core.Metadata @@ -41,7 +42,7 @@ namespace ErsatzTV.Core.Metadata
ISongRepository songRepository,
ILibraryRepository libraryRepository,
IMediaItemRepository mediaItemRepository,
IFFmpegProcessService ffmpegProcessService,
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
ITempFilePool tempFilePool,
ILogger<SongFolderScanner> logger) : base(
localFileSystem,
@ -49,7 +50,7 @@ namespace ErsatzTV.Core.Metadata @@ -49,7 +50,7 @@ namespace ErsatzTV.Core.Metadata
metadataRepository,
mediaItemRepository,
imageCache,
ffmpegProcessService,
ffmpegProcessServiceFactory,
tempFilePool,
logger)
{

5
ErsatzTV.Core/Metadata/TelevisionFolderScanner.cs

@ -4,6 +4,7 @@ using System.IO; @@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Metadata;
@ -41,7 +42,7 @@ namespace ErsatzTV.Core.Metadata @@ -41,7 +42,7 @@ namespace ErsatzTV.Core.Metadata
ILibraryRepository libraryRepository,
IMediaItemRepository mediaItemRepository,
IMediator mediator,
IFFmpegProcessService ffmpegProcessService,
IFFmpegProcessServiceFactory ffmpegProcessServiceFactory,
ITempFilePool tempFilePool,
ILogger<TelevisionFolderScanner> logger) : base(
localFileSystem,
@ -49,7 +50,7 @@ namespace ErsatzTV.Core.Metadata @@ -49,7 +50,7 @@ namespace ErsatzTV.Core.Metadata
metadataRepository,
mediaItemRepository,
imageCache,
ffmpegProcessService,
ffmpegProcessServiceFactory,
tempFilePool,
logger)
{

29
ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj

@ -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>

148
ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs

@ -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)}");
}
}

38
ErsatzTV.FFmpeg/CommandGenerator.cs

@ -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;
}
}

59
ErsatzTV.FFmpeg/Decoder/AvailableDecoders.cs

@ -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)
};
}
}

40
ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderH264Cuvid.cs

@ -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;
}
}

38
ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderHevcCuvid.cs

@ -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;
}
}

49
ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg2Cuvid.cs

@ -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;
}
}

40
ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderMpeg4Cuvid.cs

@ -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;
}
}

38
ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVc1Cuvid.cs

@ -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;
}
}

38
ErsatzTV.FFmpeg/Decoder/Cuvid/DecoderVp9Cuvid.cs

@ -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;
}
}

13
ErsatzTV.FFmpeg/Decoder/DecoderBase.cs

@ -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; }
}

7
ErsatzTV.FFmpeg/Decoder/DecoderH264.cs

@ -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;
}

7
ErsatzTV.FFmpeg/Decoder/DecoderHevc.cs

@ -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;
}

8
ErsatzTV.FFmpeg/Decoder/DecoderImplicit.cs

@ -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>();
}

7
ErsatzTV.FFmpeg/Decoder/DecoderMpeg1.cs

@ -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;
}

7
ErsatzTV.FFmpeg/Decoder/DecoderMpeg2.cs

@ -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;
}

7
ErsatzTV.FFmpeg/Decoder/DecoderMpeg4.cs

@ -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;
}

7
ErsatzTV.FFmpeg/Decoder/DecoderMsMpeg4v2.cs

@ -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;
}

7
ErsatzTV.FFmpeg/Decoder/DecoderMsMpeg4v3.cs

@ -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;
}

19
ErsatzTV.FFmpeg/Decoder/DecoderVaapi.cs

@ -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);
}
}

7
ErsatzTV.FFmpeg/Decoder/DecoderVc1.cs

@ -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;
}

7
ErsatzTV.FFmpeg/Decoder/DecoderVp9.cs

@ -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;
}

6
ErsatzTV.FFmpeg/Decoder/IDecoder.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
namespace ErsatzTV.FFmpeg.Decoder;
public interface IDecoder : IPipelineStep
{
string Name { get; }
}

19
ErsatzTV.FFmpeg/Decoder/Qsv/DecoderH264Qsv.cs

@ -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);
}
}

8
ErsatzTV.FFmpeg/Decoder/Qsv/DecoderHevcQsv.cs

@ -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;
}

8
ErsatzTV.FFmpeg/Decoder/Qsv/DecoderMpeg2Qsv.cs

@ -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;
}

8
ErsatzTV.FFmpeg/Decoder/Qsv/DecoderVc1Qsv.cs

@ -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;
}

8
ErsatzTV.FFmpeg/Decoder/Qsv/DecoderVp9Qsv.cs

@ -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;
}

46
ErsatzTV.FFmpeg/Encoder/AvailableEncoders.cs

@ -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));
}
}

12
ErsatzTV.FFmpeg/Encoder/EncoderAac.cs

@ -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;
}

12
ErsatzTV.FFmpeg/Encoder/EncoderAc3.cs

@ -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;
}

14
ErsatzTV.FFmpeg/Encoder/EncoderBase.cs

@ -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;
}

9
ErsatzTV.FFmpeg/Encoder/EncoderCopyAll.cs

@ -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 };
}

8
ErsatzTV.FFmpeg/Encoder/EncoderCopyAudio.cs

@ -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;
}

8
ErsatzTV.FFmpeg/Encoder/EncoderCopyVideo.cs

@ -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;
}

10
ErsatzTV.FFmpeg/Encoder/EncoderImplicitVideo.cs

@ -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>();
}

10
ErsatzTV.FFmpeg/Encoder/EncoderLibx264.cs

@ -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;
}

12
ErsatzTV.FFmpeg/Encoder/EncoderLibx265.cs

@ -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;
}

10
ErsatzTV.FFmpeg/Encoder/EncoderMpeg2Video.cs

@ -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;
}

7
ErsatzTV.FFmpeg/Encoder/IEncoder.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.FFmpeg.Encoder;
public interface IEncoder : IPipelineFilterStep
{
string Name { get; }
StreamKind Kind { get; }
}

15
ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs

@ -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;
}

15
ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs

@ -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;
}

15
ErsatzTV.FFmpeg/Encoder/Qsv/EncoderH264Qsv.cs

@ -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;
}

15
ErsatzTV.FFmpeg/Encoder/Qsv/EncoderHevcQsv.cs

@ -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;
}

26
ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderH264Vaapi.cs

@ -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;
}

26
ErsatzTV.FFmpeg/Encoder/Vaapi/EncoderHevcVaapi.cs

@ -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;
}

18
ErsatzTV.FFmpeg/ErsatzTV.FFmpeg.csproj

@ -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>

24
ErsatzTV.FFmpeg/Filter/AudioPadFilter.cs

@ -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 };
}

17
ErsatzTV.FFmpeg/Filter/AvailableDeinterlaceFilters.cs

@ -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)
};
}

21
ErsatzTV.FFmpeg/Filter/AvailableScaleFilters.cs

@ -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)
};
}

12
ErsatzTV.FFmpeg/Filter/BaseFilter.cs

@ -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; }
}

81
ErsatzTV.FFmpeg/Filter/ComplexFilter.cs

@ -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;
}

61
ErsatzTV.FFmpeg/Filter/Cuda/ScaleCudaFilter.cs

@ -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
};
}

20
ErsatzTV.FFmpeg/Filter/Cuda/YadifCudaFilter.cs

@ -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
};
}

8
ErsatzTV.FFmpeg/Filter/NormalizeLoudnessFilter.cs

@ -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 };
}

32
ErsatzTV.FFmpeg/Filter/PadFilter.cs

@ -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
};
}

60
ErsatzTV.FFmpeg/Filter/Qsv/ScaleQsvFilter.cs

@ -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
};
}

48
ErsatzTV.FFmpeg/Filter/ScaleFilter.cs

@ -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
};
}

7
ErsatzTV.FFmpeg/Filter/SetSarFilter.cs

@ -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;
}

22
ErsatzTV.FFmpeg/Filter/Vaapi/DeinterlaceVaapiFilter.cs

@ -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
};
}

62
ErsatzTV.FFmpeg/Filter/Vaapi/ScaleVaapiFilter.cs

@ -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
};
}

40
ErsatzTV.FFmpeg/Filter/YadifFilter.cs

@ -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
};
}

7
ErsatzTV.FFmpeg/Format/AudioFormat.cs

@ -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";
}

18
ErsatzTV.FFmpeg/Format/AvailablePixelFormats.cs

@ -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)
};
}
}

18
ErsatzTV.FFmpeg/Format/ConcatInputFormat.cs

@ -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;
}

9
ErsatzTV.FFmpeg/Format/FFmpegFormat.cs

@ -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";
}

7
ErsatzTV.FFmpeg/Format/IPixelFormat.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.FFmpeg.Format;
public interface IPixelFormat
{
string Name { get; }
string FFmpegName { get; }
}

10
ErsatzTV.FFmpeg/Format/PixelFormat.cs

@ -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";
}

13
ErsatzTV.FFmpeg/Format/PixelFormatNv12.cs

@ -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";
}

7
ErsatzTV.FFmpeg/Format/PixelFormatUnknown.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.FFmpeg.Format;
public class PixelFormatUnknown : IPixelFormat
{
public string Name => "unknown";
public string FFmpegName => "unknown";
}

7
ErsatzTV.FFmpeg/Format/PixelFormatYuv420P.cs

@ -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;
}

7
ErsatzTV.FFmpeg/Format/PixelFormatYuv420P10Le.cs

@ -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;
}

7
ErsatzTV.FFmpeg/Format/PixelFormatYuv444P.cs

@ -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;
}

7
ErsatzTV.FFmpeg/Format/PixelFormatYuv444P10Le.cs

@ -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;
}

9
ErsatzTV.FFmpeg/Format/PixelFormatYuvJ420P.cs

@ -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;
}

18
ErsatzTV.FFmpeg/Format/VideoFormat.cs

@ -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 = "";
}

8
ErsatzTV.FFmpeg/FrameDataLocation.cs

@ -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…
Cancel
Save