nvidia - decode 10-bit h264 in software (#2833)
* output progress/speed even when copying video * nvidia - decode 10-bit h264 in software * fixes * fix tests
This commit is contained in:
@@ -33,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Fix Trakt list sync
|
||||
- Fix some cases of QSV audio/video desync when *not* seeking by using software decode
|
||||
- This only applies to content that *might* be problematic (using a heuristic)
|
||||
- NVIDIA: force software decode of 10-bit h264 content since hardware decode is unsupported by ffmpeg until version 8
|
||||
|
||||
## [26.2.0] - 2026-02-02
|
||||
### Added
|
||||
|
||||
@@ -26,7 +26,7 @@ public class RefreshFFmpegCapabilitiesHandler(
|
||||
|
||||
foreach (string ffmpegPath in maybeFFmpegPath)
|
||||
{
|
||||
_ = await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
|
||||
_ = await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath, cancellationToken);
|
||||
|
||||
Option<string> maybeFFprobePath = await dbContext.ConfigElements
|
||||
.GetValue<string>(ConfigElementKey.FFprobePath, cancellationToken)
|
||||
|
||||
@@ -31,15 +31,18 @@ public class
|
||||
Validation<BaseError, string> validation = await Validate(dbContext, cancellationToken);
|
||||
|
||||
return await validation.Match(
|
||||
GetHardwareAccelerationKinds,
|
||||
ffmpegPath => GetHardwareAccelerationKinds(ffmpegPath, cancellationToken),
|
||||
_ => Task.FromResult(new List<HardwareAccelerationKind> { HardwareAccelerationKind.None }));
|
||||
}
|
||||
|
||||
private async Task<List<HardwareAccelerationKind>> GetHardwareAccelerationKinds(string ffmpegPath)
|
||||
private async Task<List<HardwareAccelerationKind>> GetHardwareAccelerationKinds(
|
||||
string ffmpegPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<HardwareAccelerationKind> { HardwareAccelerationKind.None };
|
||||
|
||||
IFFmpegCapabilities ffmpegCapabilities = await _hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
|
||||
IFFmpegCapabilities ffmpegCapabilities =
|
||||
await _hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath, cancellationToken);
|
||||
|
||||
if (ffmpegCapabilities.HasHardwareAcceleration(HardwareAccelerationMode.Nvenc))
|
||||
{
|
||||
|
||||
@@ -74,7 +74,8 @@ public class
|
||||
ffmpegPath,
|
||||
originalPath,
|
||||
withExtension,
|
||||
request.MaxHeight.Value);
|
||||
request.MaxHeight.Value,
|
||||
cancellationToken);
|
||||
|
||||
CommandResult resize = await process.ExecuteAsync(cancellationToken);
|
||||
|
||||
|
||||
@@ -36,7 +36,8 @@ public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetCo
|
||||
saveReports,
|
||||
channel,
|
||||
request.Scheme,
|
||||
request.Host);
|
||||
request.Host,
|
||||
cancellationToken);
|
||||
|
||||
return new PlayoutItemProcessModel(
|
||||
process,
|
||||
|
||||
@@ -32,7 +32,8 @@ public class GetErrorProcessHandler(
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
|
||||
cancellationToken);
|
||||
|
||||
return new PlayoutItemProcessModel(
|
||||
process,
|
||||
|
||||
@@ -337,7 +337,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
|
||||
cancellationToken);
|
||||
|
||||
return new PlayoutItemProcessModel(
|
||||
doesNotExistProcess,
|
||||
@@ -534,7 +535,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
|
||||
cancellationToken);
|
||||
|
||||
return new PlayoutItemProcessModel(
|
||||
offlineProcess,
|
||||
@@ -558,7 +560,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
|
||||
cancellationToken);
|
||||
|
||||
return new PlayoutItemProcessModel(
|
||||
doesNotExistProcess,
|
||||
@@ -582,7 +585,8 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
|
||||
channel.FFmpegProfile.VaapiDisplay,
|
||||
channel.FFmpegProfile.VaapiDriver,
|
||||
channel.FFmpegProfile.VaapiDevice,
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames));
|
||||
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
|
||||
cancellationToken);
|
||||
|
||||
return new PlayoutItemProcessModel(
|
||||
errorProcess,
|
||||
|
||||
@@ -21,19 +21,21 @@ public class GetSeekTextSubtitleProcessHandler(
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
Validation<BaseError, string> validation = await Validate(dbContext, cancellationToken);
|
||||
return await validation.Match(
|
||||
ffmpegPath => GetProcess(request, ffmpegPath),
|
||||
ffmpegPath => GetProcess(request, ffmpegPath, cancellationToken),
|
||||
error => Task.FromResult<Either<BaseError, SeekTextSubtitleProcess>>(error.Join()));
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, SeekTextSubtitleProcess>> GetProcess(
|
||||
GetSeekTextSubtitleProcess request,
|
||||
string ffmpegPath)
|
||||
string ffmpegPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Command process = await ffmpegProcessService.SeekTextSubtitle(
|
||||
ffmpegPath,
|
||||
request.PathAndCodec.Path,
|
||||
request.PathAndCodec.Codec,
|
||||
request.Seek);
|
||||
request.Seek,
|
||||
cancellationToken);
|
||||
|
||||
return new SeekTextSubtitleProcess(process);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ public class GetSlugProcessByChannelNumberHandler(
|
||||
request.Now,
|
||||
duration,
|
||||
request.HlsRealtime,
|
||||
request.PtsOffset);
|
||||
request.PtsOffset,
|
||||
cancellationToken);
|
||||
|
||||
var result = new PlayoutItemProcessModel(
|
||||
playoutItemResult,
|
||||
|
||||
@@ -161,7 +161,8 @@ public class GetTroubleshootingInfoHandler : IRequestHandler<GetTroubleshootingI
|
||||
videoToolboxCapabilities.AppendLine();
|
||||
}
|
||||
|
||||
var ffmpegCapabilities = await _hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath.Value);
|
||||
var ffmpegCapabilities =
|
||||
await _hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath.Value, cancellationToken);
|
||||
aviSynthDemuxer = ffmpegCapabilities.HasDemuxFormat(FFmpegKnownFormat.AviSynth);
|
||||
aviSynthInstalled = _hardwareCapabilitiesFactory.IsAviSynthInstalled();
|
||||
}
|
||||
|
||||
@@ -604,7 +604,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
VaapiDeviceName(hwAccel, vaapiDevice),
|
||||
await customReportsFolder.IfNoneAsync(FileSystemLayout.FFmpegReportsFolder),
|
||||
FileSystemLayout.FontsCacheFolder,
|
||||
ffmpegPath);
|
||||
ffmpegPath,
|
||||
cancellationToken);
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
|
||||
|
||||
@@ -687,7 +688,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
string vaapiDisplay,
|
||||
VaapiDriver vaapiDriver,
|
||||
string vaapiDevice,
|
||||
Option<int> qsvExtraHardwareFrames)
|
||||
Option<int> qsvExtraHardwareFrames,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettingsCalculator.CalculateGeneratedImageSettings(
|
||||
channel.StreamingMode,
|
||||
@@ -830,7 +832,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
? FileSystemLayout.TranscodeTroubleshootingFolder
|
||||
: FileSystemLayout.FFmpegReportsFolder,
|
||||
FileSystemLayout.FontsCacheFolder,
|
||||
ffmpegPath);
|
||||
ffmpegPath,
|
||||
cancellationToken);
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
|
||||
|
||||
@@ -843,7 +846,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
DateTimeOffset now,
|
||||
TimeSpan duration,
|
||||
bool hlsRealtime,
|
||||
TimeSpan ptsOffset)
|
||||
TimeSpan ptsOffset,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettingsCalculator.CalculateGeneratedImageSettings(
|
||||
channel.StreamingMode,
|
||||
@@ -960,7 +964,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
? FileSystemLayout.TranscodeTroubleshootingFolder
|
||||
: FileSystemLayout.FFmpegReportsFolder,
|
||||
FileSystemLayout.FontsCacheFolder,
|
||||
ffmpegPath);
|
||||
ffmpegPath,
|
||||
cancellationToken);
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState);
|
||||
|
||||
@@ -972,7 +977,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
bool saveReports,
|
||||
Channel channel,
|
||||
string scheme,
|
||||
string host)
|
||||
string host,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var resolution = new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height);
|
||||
|
||||
@@ -993,7 +999,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
Option<string>.None,
|
||||
FileSystemLayout.FFmpegReportsFolder,
|
||||
FileSystemLayout.FontsCacheFolder,
|
||||
ffmpegPath);
|
||||
ffmpegPath,
|
||||
cancellationToken);
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.Concat(
|
||||
concatInputFile,
|
||||
@@ -1063,7 +1070,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
Option<string>.None,
|
||||
FileSystemLayout.FFmpegReportsFolder,
|
||||
FileSystemLayout.FontsCacheFolder,
|
||||
ffmpegPath);
|
||||
ffmpegPath,
|
||||
cancellationToken);
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.WrapSegmenter(
|
||||
concatInputFile,
|
||||
@@ -1072,7 +1080,12 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
return GetCommand(ffmpegPath, None, None, None, concatInputFile, None, pipeline);
|
||||
}
|
||||
|
||||
public async Task<Command> ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height)
|
||||
public async Task<Command> ResizeImage(
|
||||
string ffmpegPath,
|
||||
string inputFile,
|
||||
string outputFile,
|
||||
int height,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var videoInputFile = new VideoInputFile(
|
||||
inputFile,
|
||||
@@ -1105,7 +1118,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
Option<string>.None,
|
||||
FileSystemLayout.FFmpegReportsFolder,
|
||||
FileSystemLayout.FontsCacheFolder,
|
||||
ffmpegPath);
|
||||
ffmpegPath,
|
||||
cancellationToken);
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.Resize(outputFile, new FrameSize(-1, height));
|
||||
|
||||
@@ -1141,7 +1155,12 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
watermarkWidthPercent,
|
||||
cancellationToken);
|
||||
|
||||
public async Task<Command> SeekTextSubtitle(string ffmpegPath, string inputFile, string codec, TimeSpan seek)
|
||||
public async Task<Command> SeekTextSubtitle(
|
||||
string ffmpegPath,
|
||||
string inputFile,
|
||||
string codec,
|
||||
TimeSpan seek,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var videoInputFile = new VideoInputFile(
|
||||
inputFile,
|
||||
@@ -1174,7 +1193,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
|
||||
Option<string>.None,
|
||||
FileSystemLayout.FFmpegReportsFolder,
|
||||
FileSystemLayout.FontsCacheFolder,
|
||||
ffmpegPath);
|
||||
ffmpegPath,
|
||||
cancellationToken);
|
||||
|
||||
FFmpegPipeline pipeline = pipelineBuilder.Seek(inputFile, codec, seek);
|
||||
|
||||
|
||||
@@ -56,7 +56,8 @@ public interface IFFmpegProcessService
|
||||
string vaapiDisplay,
|
||||
VaapiDriver vaapiDriver,
|
||||
string vaapiDevice,
|
||||
Option<int> qsvExtraHardwareFrames);
|
||||
Option<int> qsvExtraHardwareFrames,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<Command> Slug(
|
||||
string ffmpegPath,
|
||||
@@ -64,9 +65,16 @@ public interface IFFmpegProcessService
|
||||
DateTimeOffset now,
|
||||
TimeSpan duration,
|
||||
bool hlsRealtime,
|
||||
TimeSpan ptsOffset);
|
||||
TimeSpan ptsOffset,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<Command> ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||
Task<Command> ConcatChannel(
|
||||
string ffmpegPath,
|
||||
bool saveReports,
|
||||
Channel channel,
|
||||
string scheme,
|
||||
string host,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<Command> WrapSegmenter(
|
||||
string ffmpegPath,
|
||||
@@ -77,7 +85,12 @@ public interface IFFmpegProcessService
|
||||
string accessToken,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<Command> ResizeImage(string ffmpegPath, string inputFile, string outputFile, int height);
|
||||
Task<Command> ResizeImage(
|
||||
string ffmpegPath,
|
||||
string inputFile,
|
||||
string outputFile,
|
||||
int height,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<Either<BaseError, string>> GenerateSongImage(
|
||||
string ffmpegPath,
|
||||
@@ -94,5 +107,10 @@ public interface IFFmpegProcessService
|
||||
int watermarkWidthPercent,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<Command> SeekTextSubtitle(string ffmpegPath, string inputFile, string codec, TimeSpan seek);
|
||||
Task<Command> SeekTextSubtitle(
|
||||
string ffmpegPath,
|
||||
string inputFile,
|
||||
string codec,
|
||||
TimeSpan seek,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ public class PipelineBuilderBaseTests
|
||||
|
||||
string command = PrintCommand(videoInputFile, audioInputFile, None, None, None, result);
|
||||
command.ShouldBe(
|
||||
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -progress - -ss 00:00:01 -c:v h264 -readrate 1.05 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1[a] -map 0:0 -map [a] -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -bf 0 -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -c:a aac -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
||||
"-nostdin -hide_banner -nostats -progress - -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -readrate 1.05 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1[a] -map 0:0 -map [a] -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -bf 0 -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -c:a aac -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -225,7 +225,7 @@ public class PipelineBuilderBaseTests
|
||||
|
||||
string command = PrintCommand(videoInputFile, audioInputFile, None, None, None, result);
|
||||
command.ShouldBe(
|
||||
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -progress - -ss 00:00:01 -c:v h264 -readrate 1.05 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1[a] -map 0:0 -map [a] -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -bf 0 -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -c:a aac -ac 6 -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
||||
"-nostdin -hide_banner -nostats -progress - -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -readrate 1.05 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1[a] -map 0:0 -map [a] -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -bf 0 -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -c:a aac -ac 6 -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -f mpegts -mpegts_flags +initial_discontinuity pipe:1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -390,7 +390,7 @@ public class PipelineBuilderBaseTests
|
||||
|
||||
// 0.4.0 reference: "-nostdin -threads 1 -hide_banner -loglevel error -nostats -fflags +genpts+discardcorrupt+igndts -re -ss 00:14:33.6195516 -i /tmp/whatever.mkv -map 0:0 -map 0:a -c:v copy -flags cgop -sc_threshold 0 -c:a copy -movflags +faststart -metadata service_provider="ErsatzTV" -metadata service_name="ErsatzTV" -t 00:06:39.6934484 -f mpegts -mpegts_flags +initial_discontinuity pipe:1"
|
||||
command.ShouldBe(
|
||||
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -readrate 1.0 -i /tmp/whatever.mkv -map 0:0 -map 0:1 -muxdelay 0 -muxpreload 0 -movflags +faststart+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov+delay_moov -c:v copy -c:a copy -f mp4 pipe:1");
|
||||
"-nostdin -hide_banner -nostats -progress - -loglevel error -fflags +genpts+discardcorrupt+igndts -readrate 1.0 -i /tmp/whatever.mkv -map 0:0 -map 0:1 -muxdelay 0 -muxpreload 0 -movflags +faststart+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov+delay_moov -c:v copy -c:a copy -f mp4 pipe:1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -484,7 +484,7 @@ public class PipelineBuilderBaseTests
|
||||
string command = PrintCommand(videoInputFile, audioInputFile, None, None, None, result);
|
||||
|
||||
command.ShouldBe(
|
||||
"-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -readrate 1.0 -i /tmp/whatever.mkv -map 0:0 -map 0:a -muxdelay 0 -muxpreload 0 -movflags +faststart+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov+delay_moov -c:v copy -c:a copy -f mp4 pipe:1");
|
||||
"-nostdin -hide_banner -nostats -progress - -loglevel error -fflags +genpts+discardcorrupt+igndts -readrate 1.0 -i /tmp/whatever.mkv -map 0:0 -map 0:a -muxdelay 0 -muxpreload 0 -movflags +faststart+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov+delay_moov -c:v copy -c:a copy -f mp4 pipe:1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -556,6 +556,7 @@ public class PipelineBuilderBaseTests
|
||||
}
|
||||
|
||||
public class DefaultFFmpegCapabilities() : FFmpegCapabilities(
|
||||
string.Empty,
|
||||
new System.Collections.Generic.HashSet<string>(),
|
||||
new System.Collections.Generic.HashSet<string>(),
|
||||
new System.Collections.Generic.HashSet<string>(),
|
||||
|
||||
@@ -4,6 +4,7 @@ using ErsatzTV.FFmpeg.Format;
|
||||
namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
public class FFmpegCapabilities(
|
||||
string ffmpegVersion,
|
||||
IReadOnlySet<string> ffmpegHardwareAccelerations,
|
||||
IReadOnlySet<string> ffmpegDecoders,
|
||||
IReadOnlySet<string> ffmpegFilters,
|
||||
@@ -12,6 +13,8 @@ public class FFmpegCapabilities(
|
||||
IReadOnlySet<string> ffmpegDemuxFormats)
|
||||
: IFFmpegCapabilities
|
||||
{
|
||||
public string Version => ffmpegVersion;
|
||||
|
||||
public bool HasHardwareAcceleration(HardwareAccelerationMode hardwareAccelerationMode)
|
||||
{
|
||||
// AMF isn't a "hwaccel" in ffmpeg, so check for presence of encoders
|
||||
|
||||
@@ -25,6 +25,7 @@ public partial class HardwareCapabilitiesFactory(
|
||||
: IHardwareCapabilitiesFactory
|
||||
{
|
||||
private const string CudaDeviceKey = "ffmpeg.hardware.cuda.device";
|
||||
private const string FFmpegVersionKey = "ffmpeg.version";
|
||||
|
||||
private static readonly CompositeFormat VaapiCacheKeyFormat =
|
||||
CompositeFormat.Parse("ffmpeg.hardware.vaapi.{0}.{1}.{2}");
|
||||
@@ -46,6 +47,7 @@ public partial class HardwareCapabilitiesFactory(
|
||||
|
||||
public void ClearCache()
|
||||
{
|
||||
memoryCache.Remove(FFmpegVersionKey);
|
||||
memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "hwaccels"));
|
||||
memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "decoders"));
|
||||
memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "filters"));
|
||||
@@ -54,33 +56,39 @@ public partial class HardwareCapabilitiesFactory(
|
||||
memoryCache.Remove(string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "formats"));
|
||||
}
|
||||
|
||||
public async Task<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath)
|
||||
public async Task<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath, CancellationToken cancellationToken)
|
||||
{
|
||||
// TODO: validate videotoolbox somehow
|
||||
// TODO: validate amf somehow
|
||||
|
||||
string ffmpegVersion = await GetFFmpegVersion(ffmpegPath, cancellationToken);
|
||||
|
||||
IReadOnlySet<string> ffmpegHardwareAccelerations =
|
||||
await GetFFmpegCapabilities(ffmpegPath, "hwaccels", ParseFFmpegAccelLine)
|
||||
await GetFFmpegCapabilities(ffmpegPath, "hwaccels", ParseFFmpegAccelLine, cancellationToken)
|
||||
.Map(set => set.Intersect(FFmpegKnownHardwareAcceleration.AllAccels).ToImmutableHashSet());
|
||||
|
||||
IReadOnlySet<string> ffmpegDecoders = await GetFFmpegCapabilities(ffmpegPath, "decoders", ParseFFmpegLine)
|
||||
.Map(set => set.Intersect(FFmpegKnownDecoder.AllDecoders).ToImmutableHashSet());
|
||||
IReadOnlySet<string> ffmpegDecoders =
|
||||
await GetFFmpegCapabilities(ffmpegPath, "decoders", ParseFFmpegLine, cancellationToken)
|
||||
.Map(set => set.Intersect(FFmpegKnownDecoder.AllDecoders).ToImmutableHashSet());
|
||||
|
||||
IEnumerable<string> allFilterNames =
|
||||
FFmpegKnownFilter.AllFilters.Union(FFmpegKnownFilter.RequiredFilters.Select(f => f.Name));
|
||||
IReadOnlySet<string> ffmpegFilters = await GetFFmpegCapabilities(ffmpegPath, "filters", ParseFFmpegLine)
|
||||
.Map(set => set.Intersect(allFilterNames).ToImmutableHashSet());
|
||||
IReadOnlySet<string> ffmpegFilters =
|
||||
await GetFFmpegCapabilities(ffmpegPath, "filters", ParseFFmpegLine, cancellationToken)
|
||||
.Map(set => set.Intersect(allFilterNames).ToImmutableHashSet());
|
||||
|
||||
IReadOnlySet<string> ffmpegEncoders = await GetFFmpegCapabilities(ffmpegPath, "encoders", ParseFFmpegLine)
|
||||
.Map(set => set.Intersect(FFmpegKnownEncoder.AllEncoders).ToImmutableHashSet());
|
||||
IReadOnlySet<string> ffmpegEncoders =
|
||||
await GetFFmpegCapabilities(ffmpegPath, "encoders", ParseFFmpegLine, cancellationToken)
|
||||
.Map(set => set.Intersect(FFmpegKnownEncoder.AllEncoders).ToImmutableHashSet());
|
||||
|
||||
IReadOnlySet<string> ffmpegOptions = await GetFFmpegOptions(ffmpegPath)
|
||||
IReadOnlySet<string> ffmpegOptions = await GetFFmpegOptions(ffmpegPath, cancellationToken)
|
||||
.Map(set => set.Intersect(FFmpegKnownOption.AllOptions).ToImmutableHashSet());
|
||||
|
||||
IReadOnlySet<string> ffmpegDemuxFormats = await GetFFmpegFormats(ffmpegPath, "D")
|
||||
IReadOnlySet<string> ffmpegDemuxFormats = await GetFFmpegFormats(ffmpegPath, "D", cancellationToken)
|
||||
.Map(set => set.Intersect(FFmpegKnownFormat.AllFormats).ToImmutableHashSet());
|
||||
|
||||
return new FFmpegCapabilities(
|
||||
ffmpegVersion,
|
||||
ffmpegHardwareAccelerations,
|
||||
ffmpegDecoders,
|
||||
ffmpegFilters,
|
||||
@@ -339,10 +347,40 @@ public partial class HardwareCapabilitiesFactory(
|
||||
return memoryCache.TryGetValue(cacheKey, out bool installed) && installed;
|
||||
}
|
||||
|
||||
public async Task<string> GetFFmpegVersion(string ffmpegPath, CancellationToken cancellationToken)
|
||||
{
|
||||
if (memoryCache.TryGetValue(FFmpegVersionKey, out string? ffmpegVersion) &&
|
||||
ffmpegVersion is not null)
|
||||
{
|
||||
return ffmpegVersion;
|
||||
}
|
||||
|
||||
string[] arguments = ["-version"];
|
||||
|
||||
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
|
||||
.WithArguments(arguments)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
|
||||
|
||||
string output = string.IsNullOrWhiteSpace(result.StandardOutput)
|
||||
? result.StandardError
|
||||
: result.StandardOutput;
|
||||
|
||||
string versionResult = await output.Split("\n").Map(s => s.Trim())
|
||||
.Bind(l => ParseFFmpegVersionLine(l))
|
||||
.HeadOrNone()
|
||||
.IfNoneAsync(string.Empty);
|
||||
|
||||
memoryCache.Set(FFmpegVersionKey, versionResult);
|
||||
|
||||
return versionResult;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlySet<string>> GetFFmpegCapabilities(
|
||||
string ffmpegPath,
|
||||
string capabilities,
|
||||
Func<string, Option<string>> parseLine)
|
||||
Func<string, Option<string>> parseLine,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, capabilities);
|
||||
if (memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
|
||||
@@ -356,7 +394,7 @@ public partial class HardwareCapabilitiesFactory(
|
||||
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
|
||||
.WithArguments(arguments)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteBufferedAsync(Encoding.UTF8);
|
||||
.ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
|
||||
|
||||
string output = string.IsNullOrWhiteSpace(result.StandardOutput)
|
||||
? result.StandardError
|
||||
@@ -371,7 +409,7 @@ public partial class HardwareCapabilitiesFactory(
|
||||
return capabilitiesResult;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlySet<string>> GetFFmpegOptions(string ffmpegPath)
|
||||
private async Task<IReadOnlySet<string>> GetFFmpegOptions(string ffmpegPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "options");
|
||||
if (memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
|
||||
@@ -385,7 +423,7 @@ public partial class HardwareCapabilitiesFactory(
|
||||
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
|
||||
.WithArguments(arguments)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteBufferedAsync(Encoding.UTF8);
|
||||
.ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
|
||||
|
||||
string output = string.IsNullOrWhiteSpace(result.StandardOutput)
|
||||
? result.StandardError
|
||||
@@ -400,7 +438,10 @@ public partial class HardwareCapabilitiesFactory(
|
||||
return capabilitiesResult;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlySet<string>> GetFFmpegFormats(string ffmpegPath, string muxDemux)
|
||||
private async Task<IReadOnlySet<string>> GetFFmpegFormats(
|
||||
string ffmpegPath,
|
||||
string muxDemux,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cacheKey = string.Format(CultureInfo.InvariantCulture, FFmpegCapabilitiesCacheKeyFormat, "formats");
|
||||
if (memoryCache.TryGetValue(cacheKey, out IReadOnlySet<string>? cachedCapabilities) &&
|
||||
@@ -414,7 +455,7 @@ public partial class HardwareCapabilitiesFactory(
|
||||
BufferedCommandResult result = await Cli.Wrap(ffmpegPath)
|
||||
.WithArguments(arguments)
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteBufferedAsync(Encoding.UTF8);
|
||||
.ExecuteBufferedAsync(Encoding.UTF8, cancellationToken);
|
||||
|
||||
string output = string.IsNullOrWhiteSpace(result.StandardOutput)
|
||||
? result.StandardError
|
||||
@@ -431,6 +472,12 @@ public partial class HardwareCapabilitiesFactory(
|
||||
return capabilitiesResult;
|
||||
}
|
||||
|
||||
private static Option<string> ParseFFmpegVersionLine(string input)
|
||||
{
|
||||
Match match = VersionRegex().Match(input);
|
||||
return match.Success ? match.Groups[1].Value : Option<string>.None;
|
||||
}
|
||||
|
||||
private static Option<string> ParseFFmpegAccelLine(string input)
|
||||
{
|
||||
Match match = AccelRegex().Match(input);
|
||||
@@ -661,6 +708,9 @@ public partial class HardwareCapabilitiesFactory(
|
||||
return new NoHardwareCapabilities();
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"version\s+([^\s]+)")]
|
||||
private static partial Regex VersionRegex();
|
||||
|
||||
[GeneratedRegex(@"^([\w]+)$")]
|
||||
private static partial Regex AccelRegex();
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
public interface IFFmpegCapabilities
|
||||
{
|
||||
string Version { get; }
|
||||
bool HasHardwareAcceleration(HardwareAccelerationMode hardwareAccelerationMode);
|
||||
bool HasDecoder(FFmpegKnownDecoder decoder);
|
||||
bool HasEncoder(FFmpegKnownEncoder encoder);
|
||||
|
||||
@@ -6,7 +6,9 @@ public interface IHardwareCapabilitiesFactory
|
||||
{
|
||||
void ClearCache();
|
||||
|
||||
Task<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath);
|
||||
Task<IFFmpegCapabilities> GetFFmpegCapabilities(string ffmpegPath, CancellationToken cancellationToken);
|
||||
|
||||
Task<string> GetFFmpegVersion(string ffmpegPath, CancellationToken cancellationToken);
|
||||
|
||||
Task<IHardwareCapabilities> GetHardwareCapabilities(
|
||||
IFFmpegCapabilities ffmpegCapabilities,
|
||||
|
||||
@@ -57,6 +57,18 @@ public class NvidiaHardwareCapabilities(CudaDevice cudaDevice, IFFmpegCapabiliti
|
||||
isHardware = false;
|
||||
}
|
||||
|
||||
// 10-bit h264 hardware decode is not supported until ffmpeg 8
|
||||
if (isHardware && videoFormat is VideoFormat.H264 && bitDepth == 10)
|
||||
{
|
||||
bool isVersion8OrNewer = int.TryParse(
|
||||
ffmpegCapabilities.Version.Split('.')[0].TrimStart('n', 'N'),
|
||||
out int major) && major >= 8;
|
||||
if (!isVersion8OrNewer)
|
||||
{
|
||||
isHardware = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isHardware)
|
||||
{
|
||||
return videoFormat switch
|
||||
|
||||
@@ -15,5 +15,6 @@ public interface IPipelineBuilderFactory
|
||||
Option<string> vaapiDevice,
|
||||
string reportsFolder,
|
||||
string fontsFolder,
|
||||
string ffmpegPath);
|
||||
string ffmpegPath,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
|
||||
new NoStandardInputOption(),
|
||||
new HideBannerOption(),
|
||||
new NoStatsOption(),
|
||||
new ProgressOption(),
|
||||
new LoglevelErrorOption(),
|
||||
new StandardFormatFlags(),
|
||||
new NoDemuxDecodeDelayOutputOption(),
|
||||
@@ -220,7 +221,6 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
|
||||
if (desiredState.VideoFormat != VideoFormat.Copy)
|
||||
{
|
||||
pipelineSteps.Add(new ClosedGopOutputOption());
|
||||
pipelineSteps.Add(new ProgressOption());
|
||||
}
|
||||
|
||||
if (desiredState.VideoFormat != VideoFormat.Copy && !desiredState.AllowBFrames)
|
||||
|
||||
@@ -29,9 +29,11 @@ public class PipelineBuilderFactory : IPipelineBuilderFactory
|
||||
Option<string> vaapiDevice,
|
||||
string reportsFolder,
|
||||
string fontsFolder,
|
||||
string ffmpegPath)
|
||||
string ffmpegPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IFFmpegCapabilities ffmpegCapabilities = await _hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
|
||||
IFFmpegCapabilities ffmpegCapabilities =
|
||||
await _hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath, cancellationToken);
|
||||
|
||||
IHardwareCapabilities capabilities = await _hardwareCapabilitiesFactory.GetHardwareCapabilities(
|
||||
ffmpegCapabilities,
|
||||
|
||||
@@ -23,7 +23,8 @@ public class FFmpegCapabilitiesHealthCheck(IConfigElementRepository configElemen
|
||||
|
||||
foreach (ConfigElement ffmpegPath in maybeFFmpegPath)
|
||||
{
|
||||
var ffmpegCapabilities = await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath.Value);
|
||||
var ffmpegCapabilities =
|
||||
await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath.Value, cancellationToken);
|
||||
|
||||
List<string> missingFilters = [];
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Health;
|
||||
using ErsatzTV.Core.Health.Checks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.FFmpeg.Capabilities;
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Health.Checks;
|
||||
|
||||
public class FFmpegVersionHealthCheck(IConfigElementRepository configElementRepository)
|
||||
public class FFmpegVersionHealthCheck(
|
||||
IConfigElementRepository configElementRepository,
|
||||
IHardwareCapabilitiesFactory hardwareCapabilitiesFactory)
|
||||
: BaseHealthCheck, IFFmpegVersionHealthCheck
|
||||
{
|
||||
private const string BundledVersion = "7.1.1";
|
||||
private const string BundledVersionVaapi = "7.1.1";
|
||||
private const string WindowsVersionPrefix = "n7.1.1";
|
||||
|
||||
private static readonly string[] FFmpegVersionArguments = { "-version" };
|
||||
|
||||
public override string Title => "FFmpeg Version";
|
||||
|
||||
public async Task<HealthCheckResult> Check(CancellationToken cancellationToken)
|
||||
@@ -37,8 +37,9 @@ public class FFmpegVersionHealthCheck(IConfigElementRepository configElementRepo
|
||||
|
||||
foreach (ConfigElement ffmpegPath in maybeFFmpegPath)
|
||||
{
|
||||
Option<string> maybeVersion = await GetVersion(ffmpegPath.Value, cancellationToken);
|
||||
if (maybeVersion.IsNone)
|
||||
Option<string> maybeVersion =
|
||||
await hardwareCapabilitiesFactory.GetFFmpegVersion(ffmpegPath.Value, cancellationToken);
|
||||
if (maybeVersion.IsNone || maybeVersion.Exists(string.IsNullOrWhiteSpace))
|
||||
{
|
||||
return WarningResult("Unable to determine ffmpeg version", "Unable to determine ffmpeg version", link);
|
||||
}
|
||||
@@ -54,8 +55,9 @@ public class FFmpegVersionHealthCheck(IConfigElementRepository configElementRepo
|
||||
|
||||
foreach (ConfigElement ffprobePath in maybeFFprobePath)
|
||||
{
|
||||
Option<string> maybeVersion = await GetVersion(ffprobePath.Value, cancellationToken);
|
||||
if (maybeVersion.IsNone)
|
||||
Option<string> maybeVersion =
|
||||
await hardwareCapabilitiesFactory.GetFFmpegVersion(ffprobePath.Value, cancellationToken);
|
||||
if (maybeVersion.IsNone || maybeVersion.Exists(string.IsNullOrWhiteSpace))
|
||||
{
|
||||
return WarningResult(
|
||||
"Unable to determine ffprobe version",
|
||||
@@ -101,21 +103,4 @@ public class FFmpegVersionHealthCheck(IConfigElementRepository configElementRepo
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
private static async Task<Option<string>> GetVersion(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<string> maybeLine = await GetProcessOutput(path, FFmpegVersionArguments, cancellationToken)
|
||||
.Map(s => s.Split("\n").HeadOrNone().Map(h => h.Trim()));
|
||||
foreach (string line in maybeLine)
|
||||
{
|
||||
const string PATTERN = @"version\s+([^\s]+)";
|
||||
Match match = Regex.Match(line, PATTERN);
|
||||
if (match.Success)
|
||||
{
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class VaapiDriverHealthCheck(
|
||||
foreach (string ffmpegPath in maybeFFmpegPath)
|
||||
{
|
||||
IFFmpegCapabilities ffmpegCapabilities =
|
||||
await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
|
||||
await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath, cancellationToken);
|
||||
foreach (FFmpegProfile profile in activeFFmpegProfiles)
|
||||
{
|
||||
Option<string> vaapiDriver = VaapiDriverName(profile.VaapiDriver);
|
||||
|
||||
@@ -26,7 +26,7 @@ public class RefreshFFmpegCapabilitiesHandler(
|
||||
|
||||
foreach (string ffmpegPath in maybeFFmpegPath)
|
||||
{
|
||||
_ = await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath);
|
||||
_ = await hardwareCapabilitiesFactory.GetFFmpegCapabilities(ffmpegPath, cancellationToken);
|
||||
|
||||
Option<string> maybeFFprobePath = await dbContext.ConfigElements
|
||||
.GetValue<string>(ConfigElementKey.FFprobePath, cancellationToken)
|
||||
|
||||
Reference in New Issue
Block a user