diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f791aa..0b189a6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -159,6 +159,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix block playout EPG generation to use `XMLTV Time Zone` setting - Fix adding "official" Trakt lists - Fix searching for `collection` names with spaces or other special characters, e.g. `collection:"Movies - Action"` +- Fix QSV transcoding errors when scaling +- Fix QSV frame freezing in browser ## [25.2.0] - 2025-06-24 ### Added diff --git a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs index 4b362d3b..1654244a 100644 --- a/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs +++ b/ErsatzTV.Application/Troubleshooting/Commands/PrepareTroubleshootingPlaybackHandler.cs @@ -101,7 +101,10 @@ public class PrepareTroubleshootingPlaybackHandler( if (!hlsRealtime && !request.StartFromBeginning) { inPoint = TimeSpan.FromSeconds(version.Duration.TotalSeconds / 2.0); - duration = TimeSpan.FromSeconds(duration.TotalSeconds / 2.0); + if (inPoint.TotalSeconds < 30) + { + duration = inPoint; + } outPoint = inPoint + duration; } diff --git a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs index 64e9fc05..80421fd3 100644 --- a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs +++ b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs @@ -116,7 +116,7 @@ public class PipelineBuilderBaseTests string command = PrintCommand(videoInputFile, audioInputFile, None, None, result); command.ShouldBe( - "-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -readrate 1.0 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1:first_pts=0[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 -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -readrate 1.0 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[a];[0:0]setpts=PTS-STARTPTS[vpf] -map [vpf] -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] @@ -213,7 +213,7 @@ public class PipelineBuilderBaseTests string command = PrintCommand(videoInputFile, audioInputFile, None, None, result); command.ShouldBe( - "-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -readrate 1.0 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1:first_pts=0[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 -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -readrate 1.0 -i /tmp/whatever.mkv -filter_complex [0:1]aresample=async=1:first_pts=0,asetpts=PTS-STARTPTS[a];[0:0]setpts=PTS-STARTPTS[vpf] -map [vpf] -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] diff --git a/ErsatzTV.FFmpeg/Filter/AudioSetPtsFilter.cs b/ErsatzTV.FFmpeg/Filter/AudioSetPtsFilter.cs new file mode 100644 index 00000000..849922e6 --- /dev/null +++ b/ErsatzTV.FFmpeg/Filter/AudioSetPtsFilter.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.FFmpeg.Filter; + +public class AudioSetPtsFilter : BaseFilter +{ + public override string Filter => "asetpts=PTS-STARTPTS"; + + public override FrameState NextState(FrameState currentState) => currentState; +} diff --git a/ErsatzTV.FFmpeg/Filter/VideoSetPtsFilter.cs b/ErsatzTV.FFmpeg/Filter/VideoSetPtsFilter.cs new file mode 100644 index 00000000..e2a43948 --- /dev/null +++ b/ErsatzTV.FFmpeg/Filter/VideoSetPtsFilter.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.FFmpeg.Filter; + +public class VideoSetPtsFilter : BaseFilter +{ + public override string Filter => "setpts=PTS-STARTPTS"; + + public override FrameState NextState(FrameState currentState) => currentState; +} diff --git a/ErsatzTV.FFmpeg/FilterChain.cs b/ErsatzTV.FFmpeg/FilterChain.cs index de5fc409..6fdc0007 100644 --- a/ErsatzTV.FFmpeg/FilterChain.cs +++ b/ErsatzTV.FFmpeg/FilterChain.cs @@ -8,11 +8,5 @@ public record FilterChain( List SubtitleOverlayFilterSteps, List PixelFormatFilterSteps) { - public static readonly FilterChain Empty = new( - new List(), - new List(), - new List(), - new List(), - new List(), - new List()); + public static readonly FilterChain Empty = new([], [], [], [], [], []); } diff --git a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs index a98055df..d466b04e 100644 --- a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs @@ -306,6 +306,8 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder context, pipelineSteps); + pixelFormatFilterSteps.Add(new VideoSetPtsFilter()); + return new FilterChain( videoInputFile.FilterSteps, watermarkInputFile.Map(wm => wm.FilterSteps).IfNone(new List()), diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs index f617a7d2..559c7f99 100644 --- a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs +++ b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs @@ -327,6 +327,9 @@ public abstract class PipelineBuilderBase : IPipelineBuilder { foreach (string segmentTemplate in ffmpegState.HlsSegmentTemplate) { + //bool oneSecondGop = ffmpegState.EncoderHardwareAccelerationMode is HardwareAccelerationMode.Qsv; + var oneSecondGop = false; + pipelineSteps.Add( new OutputFormatHls( desiredState, @@ -334,7 +337,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder segmentTemplate, playlistPath, ffmpegState.PtsOffset == 0, - ffmpegState.EncoderHardwareAccelerationMode is HardwareAccelerationMode.Qsv, + oneSecondGop, ffmpegState.IsTroubleshooting)); } } @@ -412,16 +415,21 @@ public abstract class PipelineBuilderBase : IPipelineBuilder SetAudioPad(audioInputFile, pipelineSteps); } - private void SetAudioPad(AudioInputFile audioInputFile, List pipelineSteps) + private static void SetAudioPad(AudioInputFile audioInputFile, List pipelineSteps) { if (pipelineSteps.All(ps => ps is not EncoderCopyAudio)) { - _audioInputFile.Iter(f => f.FilterSteps.Add(new AudioFirstPtsFilter(0))); + audioInputFile.FilterSteps.Add(new AudioFirstPtsFilter(0)); } foreach (TimeSpan _ in audioInputFile.DesiredState.AudioDuration) { - _audioInputFile.Iter(f => f.FilterSteps.Add(new AudioPadFilter())); + audioInputFile.FilterSteps.Add(new AudioPadFilter()); + } + + if (pipelineSteps.All(ps => ps is not EncoderCopyAudio)) + { + audioInputFile.FilterSteps.Add(new AudioSetPtsFilter()); } } diff --git a/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs index 5141bc8c..2456ed18 100644 --- a/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs @@ -248,6 +248,8 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder context, pipelineSteps); + pixelFormatFilterSteps.Add(new VideoSetPtsFilter()); + return new FilterChain( videoInputFile.FilterSteps, watermarkInputFile.Map(wm => wm.FilterSteps).IfNone(new List()), diff --git a/ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs index a2cb959b..9b26f1af 100644 --- a/ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs @@ -148,10 +148,12 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase currentState, pipelineSteps); + pixelFormatFilterSteps.Add(new VideoSetPtsFilter()); + return new FilterChain( videoInputFile.FilterSteps, - watermarkInputFile.Map(wm => wm.FilterSteps).IfNone(new List()), - subtitleInputFile.Map(st => st.FilterSteps).IfNone(new List()), + watermarkInputFile.Map(wm => wm.FilterSteps).IfNone([]), + subtitleInputFile.Map(st => st.FilterSteps).IfNone([]), watermarkOverlayFilterSteps, subtitleOverlayFilterSteps, pixelFormatFilterSteps); diff --git a/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs index 8fe4f532..0f0f86bc 100644 --- a/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs @@ -260,6 +260,8 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder currentState, pipelineSteps); + pixelFormatFilterSteps.Add(new VideoSetPtsFilter()); + return new FilterChain( videoInputFile.FilterSteps, watermarkInputFile.Map(wm => wm.FilterSteps).IfNone(new List()),