diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 22584711f..2c4e73f64 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -233,6 +233,16 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService scanKind = await ProbeScanKind(ffmpegPath, videoVersion.MediaItem, cancellationToken); } + HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings); + + // QSV may have sync issues with h264 files that have multiple profiles + // check and flag here so software decoding can be used if needed + bool hasMultipleProfiles = false; + if (hwAccel is HardwareAccelerationMode.Qsv && videoStream.Codec is VideoFormat.H264) + { + hasMultipleProfiles = await ProbeHasMultipleProfiles(ffmpegPath, videoVersion.MediaItem, cancellationToken); + } + var ffmpegVideoStream = new VideoStream( videoStream.Index, videoStream.Codec, @@ -248,7 +258,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService videoVersion.MediaVersion.DisplayAspectRatio, new FrameRate(videoVersion.MediaVersion.RFrameRate), videoPath != audioPath, // still image when paths are different - scanKind); + scanKind, + hasMultipleProfiles); var videoInputFile = new VideoInputFile( videoPath, @@ -405,8 +416,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService graphicsElementContexts.AddRange(watermarks.Map(wm => new WatermarkElementContext(wm))); } - HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings); - string videoFormat = GetVideoFormat(playbackSettings); Option maybeVideoProfile = GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile); Option maybeVideoPreset = GetVideoPreset( @@ -652,6 +661,19 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService return result; } + private async Task ProbeHasMultipleProfiles( + string ffmpegPath, + MediaItem mediaItem, + CancellationToken cancellationToken) + { + _logger.LogDebug("Will probe for h264 profile count"); + + Option profileCount = + await _localStatisticsProvider.GetProfileCount(ffmpegPath, mediaItem, cancellationToken); + + return await profileCount.IfNoneAsync(1) > 1; + } + public async Task ForError( string ffmpegPath, Channel channel, diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs index 933b64b33..0c882064a 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs @@ -14,4 +14,9 @@ public interface ILocalStatisticsProvider string ffmpegPath, MediaItem mediaItem, CancellationToken cancellationToken); + + Task> GetProfileCount( + string ffmpegPath, + MediaItem mediaItem, + CancellationToken cancellationToken); } diff --git a/ErsatzTV.FFmpeg/MediaStream.cs b/ErsatzTV.FFmpeg/MediaStream.cs index 224c8b7ac..e6f0a4422 100644 --- a/ErsatzTV.FFmpeg/MediaStream.cs +++ b/ErsatzTV.FFmpeg/MediaStream.cs @@ -25,7 +25,8 @@ public record VideoStream( string DisplayAspectRatio, Option FrameRate, bool StillImage, - ScanKind ScanKind) : MediaStream( + ScanKind ScanKind, + bool HasMultipleProfiles) : MediaStream( Index, Codec, StreamKind.Video) diff --git a/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs index 1275a9742..2e3970f81 100644 --- a/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs @@ -91,6 +91,14 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder decodeCapability = FFmpegCapability.Software; } + // QSV cannot always decode properly when *not* seeking, so check if software is needed + if (decodeCapability == FFmpegCapability.Hardware && ffmpegState.Start.Filter(s => s > TimeSpan.Zero).IsNone && + videoStream.HasMultipleProfiles) + { + _logger.LogDebug("Forcing software decode when input file uses multiple h264 profiles"); + decodeCapability = FFmpegCapability.Software; + } + // give a bogus value so no cuda devices are visible to ffmpeg pipelineSteps.Add(new CudaVisibleDevicesVariable("999")); diff --git a/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs b/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs index cff4a5f58..7b67de330 100644 --- a/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs +++ b/ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs @@ -181,6 +181,24 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider return Option.None; } + public async Task> GetProfileCount( + string ffmpegPath, + MediaItem mediaItem, + CancellationToken cancellationToken) + { + try + { + string filePath = await PathForMediaItem(mediaItem); + return await GetProfileCount(ffmpegPath, filePath); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to check profile count for media item {Id}", mediaItem.Id); + } + + return Option.None; + } + private async Task> RefreshStatistics( string ffmpegPath, string ffprobePath, @@ -336,6 +354,46 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider return stats; } + private async Task> GetProfileCount(string ffmpegPath, string filePath) + { + string[] arguments = + [ + "-hide_banner", + "-i", filePath, + "-c", "copy", + "-bsf:v", "trace_headers", + "-f", "null", "-" + ]; + + var uniqueProfiles = new System.Collections.Generic.HashSet(); + + CommandResult traceHeaders = await Cli.Wrap(ffmpegPath) + .WithArguments(arguments) + .WithValidation(CommandResultValidation.None) + .WithStandardErrorPipe(PipeTarget.ToDelegate(line => + { + int idx = line.IndexOf("profile_idc", StringComparison.Ordinal); + if (idx >= 0) + { + uniqueProfiles.Add(line[idx..]); + } + })) + .ExecuteAsync(); + + if (traceHeaders.ExitCode != 0) + { + _logger.LogInformation( + "FFmpeg trace headers with arguments {Arguments} exited with code {ExitCode}", + arguments, + traceHeaders.ExitCode); + + return Option.None; + } + + return uniqueProfiles.Count; + } + + private async Task AnalyzeDuration(string ffmpegPath, string path, MediaVersion version) { try