Browse Source

use heuristic to work around some qsv av desync cases (#2829)

* check for multiple h264 profiles using qsv decoding

* fix build

* update changelog

* pass cancellation token
pull/2835/head
Jason Dove 6 months ago committed by GitHub
parent
commit
3e3bfbd5f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 30
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  3. 5
      ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs
  4. 2
      ErsatzTV.FFmpeg/MediaStream.cs
  5. 8
      ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs
  6. 62
      ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs

2
CHANGELOG.md

@ -27,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -27,6 +27,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Fixed
- Improve stability of playback orders `Shuffle` and `Shuffle in Order` over time
- 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)
## [26.2.0] - 2026-02-02
### Added

30
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -233,6 +233,16 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -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,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -248,7 +258,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
videoVersion.MediaVersion.DisplayAspectRatio,
new FrameRate(videoVersion.MediaVersion.RFrameRate),
videoPath != audioPath, // still image when paths are different
scanKind);
scanKind)
{
HasMultipleProfiles = hasMultipleProfiles
};
var videoInputFile = new VideoInputFile(
videoPath,
@ -405,8 +418,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -405,8 +418,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
graphicsElementContexts.AddRange(watermarks.Map(wm => new WatermarkElementContext(wm)));
}
HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings);
string videoFormat = GetVideoFormat(playbackSettings);
Option<string> maybeVideoProfile = GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile);
Option<string> maybeVideoPreset = GetVideoPreset(
@ -652,6 +663,19 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -652,6 +663,19 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
return result;
}
private async Task<bool> ProbeHasMultipleProfiles(
string ffmpegPath,
MediaItem mediaItem,
CancellationToken cancellationToken)
{
_logger.LogDebug("Will probe for h264 profile count");
Option<int> profileCount =
await _localStatisticsProvider.GetProfileCount(ffmpegPath, mediaItem, cancellationToken);
return await profileCount.IfNoneAsync(1) > 1;
}
public async Task<Command> ForError(
string ffmpegPath,
Channel channel,

5
ErsatzTV.Core/Interfaces/Metadata/ILocalStatisticsProvider.cs

@ -14,4 +14,9 @@ public interface ILocalStatisticsProvider @@ -14,4 +14,9 @@ public interface ILocalStatisticsProvider
string ffmpegPath,
MediaItem mediaItem,
CancellationToken cancellationToken);
Task<Option<int>> GetProfileCount(
string ffmpegPath,
MediaItem mediaItem,
CancellationToken cancellationToken);
}

2
ErsatzTV.FFmpeg/MediaStream.cs

@ -32,6 +32,8 @@ public record VideoStream( @@ -32,6 +32,8 @@ public record VideoStream(
{
public ColorParams ColorParams { get; private set; } = ColorParams;
public bool HasMultipleProfiles { get; set; }
public int BitDepth => PixelFormat.Map(pf => pf.BitDepth).IfNone(8);
public string SampleAspectRatio

8
ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs

@ -91,6 +91,14 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder @@ -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"));

62
ErsatzTV.Infrastructure/Metadata/LocalStatisticsProvider.cs

@ -181,6 +181,24 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider @@ -181,6 +181,24 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider
return Option<double>.None;
}
public async Task<Option<int>> GetProfileCount(
string ffmpegPath,
MediaItem mediaItem,
CancellationToken cancellationToken)
{
try
{
string filePath = await PathForMediaItem(mediaItem);
return await GetProfileCount(ffmpegPath, filePath, cancellationToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to check profile count for media item {Id}", mediaItem.Id);
}
return Option<int>.None;
}
private async Task<Either<BaseError, bool>> RefreshStatistics(
string ffmpegPath,
string ffprobePath,
@ -336,6 +354,50 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider @@ -336,6 +354,50 @@ public partial class LocalStatisticsProvider : ILocalStatisticsProvider
return stats;
}
private async Task<Option<int>> GetProfileCount(
string ffmpegPath,
string filePath,
CancellationToken cancellationToken)
{
string[] arguments =
[
"-hide_banner",
"-i", filePath,
"-c", "copy",
"-bsf:v", "trace_headers",
"-f", "null", "-"
];
var uniqueProfiles = new System.Collections.Generic.HashSet<string>();
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(cancellationToken);
if (traceHeaders.ExitCode != 0)
{
_logger.LogInformation(
"FFmpeg trace headers with arguments {Arguments} exited with code {ExitCode}",
arguments,
traceHeaders.ExitCode);
return Option<int>.None;
}
return uniqueProfiles.Count;
}
private async Task AnalyzeDuration(string ffmpegPath, string path, MediaVersion version)
{
try

Loading…
Cancel
Save