diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cb13c27..4b78c06d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix building playouts with empty schedules - Fix schedule start time calculation when daily playout build goes beyond midnight and into a different alternate schedule - Fix compatibility with older NVIDIA devices (compute capability 3.0+) in unified docker image +- Fix transitions when using NVIDIA acceleration ### Changed - Always tell ffmpeg to stop encoding with a specific duration diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index bd1a6c12..4ba3c566 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -397,7 +397,7 @@ public class HlsSessionWorker : IHlsSessionWorker } } - double ptsOffset = await GetPtsOffset(_channelNumber, cancellationToken); + long ptsOffset = await GetPtsOffset(_channelNumber, cancellationToken); // _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset); _logger.LogDebug("HLS session state: {State}", _state); @@ -620,12 +620,12 @@ public class HlsSessionWorker : IHlsSessionWorker } } - private async Task GetPtsOffset(string channelNumber, CancellationToken cancellationToken) + private async Task GetPtsOffset(string channelNumber, CancellationToken cancellationToken) { await _slim.WaitAsync(cancellationToken); try { - double result = 0; + long result = 0; // if we haven't yet written any segments, start at zero if (!_hasWrittenSegments) @@ -644,7 +644,7 @@ public class HlsSessionWorker : IHlsSessionWorker foreach (PtsTime pts in queryResult.RightToSeq()) { - result = pts.Value + 0.01; + result = pts.Value; } return result; diff --git a/ErsatzTV.Application/Streaming/PtsTime.cs b/ErsatzTV.Application/Streaming/PtsTime.cs index f2f446d6..631d756e 100644 --- a/ErsatzTV.Application/Streaming/PtsTime.cs +++ b/ErsatzTV.Application/Streaming/PtsTime.cs @@ -2,15 +2,15 @@ namespace ErsatzTV.Application.Streaming; -public record PtsTime(double Value) +public record PtsTime(long Value) { public static readonly PtsTime Zero = new(0); public static PtsTime From(string ffprobeLine) { string[] split = ffprobeLine.Split("|"); - var ptsTime = double.Parse(split[0], CultureInfo.InvariantCulture); - if (double.TryParse(split[1], CultureInfo.InvariantCulture, out double duration)) + var ptsTime = long.Parse(split[0], CultureInfo.InvariantCulture); + if (long.TryParse(split[1], CultureInfo.InvariantCulture, out long duration)) { ptsTime += duration; } diff --git a/ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs b/ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs index 19c8c6c6..681b4bbd 100644 --- a/ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs +++ b/ErsatzTV.Application/Streaming/Queries/FFmpegProcessRequest.cs @@ -8,4 +8,4 @@ public record FFmpegProcessRequest( DateTimeOffset Now, bool StartAtZero, bool HlsRealtime, - double PtsOffset) : IRequest>; + long PtsOffset) : IRequest>; diff --git a/ErsatzTV.Application/Streaming/Queries/GetErrorProcess.cs b/ErsatzTV.Application/Streaming/Queries/GetErrorProcess.cs index aaa6ac6f..388746ca 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetErrorProcess.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetErrorProcess.cs @@ -4,7 +4,7 @@ public record GetErrorProcess( string ChannelNumber, string Mode, bool HlsRealtime, - double PtsOffset, + long PtsOffset, Option MaybeDuration, DateTimeOffset Until, string ErrorMessage) : FFmpegProcessRequest( diff --git a/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs index 3b40f791..d2745ff1 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetLastPtsTimeHandler.cs @@ -54,24 +54,20 @@ public class GetLastPtsTimeHandler : IRequestHandler maybeLastSegment = GetLastSegment(parameters.ChannelNumber); foreach (FileInfo segment in maybeLastSegment) { - PtsTime videoPts = await GetPts(parameters, segment, "v", cancellationToken).IfNoneAsync(PtsTime.Zero); - PtsTime audioPts = await GetPts(parameters, segment, "a", cancellationToken).IfNoneAsync(PtsTime.Zero); - return videoPts.Value > audioPts.Value ? videoPts : audioPts; + return await GetPts(parameters, segment, cancellationToken).IfNoneAsync(PtsTime.Zero); } return BaseError.New($"Failed to determine last pts duration for channel {parameters.ChannelNumber}"); } - private async Task> GetPts(RequestParameters parameters, FileInfo segment, string audioVideo, CancellationToken cancellationToken) + private async Task> GetPts(RequestParameters parameters, FileInfo segment, CancellationToken cancellationToken) { string[] argumentList = { "-v", "0", - "-select_streams", $"{audioVideo}:0", "-show_entries", - "packet=pts_time,duration_time", + "packet=pts,duration", "-of", "compact=p=0:nk=1", - // "-read_intervals", "999999", // read_intervals causes inconsistent behavior on windows segment.FullName }; diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs index 8974f59e..5a122329 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumber.cs @@ -6,7 +6,7 @@ public record GetPlayoutItemProcessByChannelNumber( DateTimeOffset Now, bool StartAtZero, bool HlsRealtime, - double PtsOffset, + long PtsOffset, Option TargetFramerate) : FFmpegProcessRequest( ChannelNumber, Mode, diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index 3a9d93b3..8f06d66b 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -72,7 +72,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService FillerKind fillerKind, TimeSpan inPoint, TimeSpan outPoint, - double ptsOffset, + long ptsOffset, Option targetFramerate, bool disableWatermarks, Option customReportsFolder, @@ -471,7 +471,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService Option duration, string errorMessage, bool hlsRealtime, - double ptsOffset, + long ptsOffset, string vaapiDisplay, VaapiDriver vaapiDriver, string vaapiDevice, diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs index 1405186c..b120488d 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs @@ -37,7 +37,7 @@ public interface IFFmpegProcessService FillerKind fillerKind, TimeSpan inPoint, TimeSpan outPoint, - double ptsOffset, + long ptsOffset, Option targetFramerate, bool disableWatermarks, Option customReportsFolder, @@ -49,7 +49,7 @@ public interface IFFmpegProcessService Option duration, string errorMessage, bool hlsRealtime, - double ptsOffset, + long ptsOffset, string vaapiDisplay, VaapiDriver vaapiDriver, string vaapiDevice, diff --git a/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs b/ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs index 3aa5b6cd..a6211778 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,asetpts=N/SR/TB[a];[0:0]setpts=PTS-STARTPTS[vpf] -map [vpf] -map [a] -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=N/SR/TB[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,asetpts=N/SR/TB[a];[0:0]setpts=PTS-STARTPTS[vpf] -map [vpf] -map [a] -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=N/SR/TB[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] @@ -370,7 +370,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 -movflags +faststart+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov+delay_moov -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mp4 pipe:1"); + "-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 -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mp4 pipe:1"); } [Test] @@ -459,7 +459,7 @@ public class PipelineBuilderBaseTests string command = PrintCommand(videoInputFile, audioInputFile, 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 -movflags +faststart+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov+delay_moov -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mp4 pipe:1"); + "-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 -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mp4 pipe:1"); } [Test] diff --git a/ErsatzTV.FFmpeg/FFmpegState.cs b/ErsatzTV.FFmpeg/FFmpegState.cs index d78ef5db..57ddb41b 100644 --- a/ErsatzTV.FFmpeg/FFmpegState.cs +++ b/ErsatzTV.FFmpeg/FFmpegState.cs @@ -19,7 +19,7 @@ public record FFmpegState( OutputFormatKind OutputFormat, Option HlsPlaylistPath, Option HlsSegmentTemplate, - double PtsOffset, + long PtsOffset, Option ThreadCount, Option MaybeQsvExtraHardwareFrames, bool IsSongWithProgress, diff --git a/ErsatzTV.FFmpeg/Filter/AudioPadFilter.cs b/ErsatzTV.FFmpeg/Filter/AudioPadFilter.cs index 3a153ed6..262b838f 100644 --- a/ErsatzTV.FFmpeg/Filter/AudioPadFilter.cs +++ b/ErsatzTV.FFmpeg/Filter/AudioPadFilter.cs @@ -1,10 +1,8 @@ -using System.Globalization; +namespace ErsatzTV.FFmpeg.Filter; -namespace ErsatzTV.FFmpeg.Filter; - -public class AudioPadFilter(TimeSpan audioDuration) : BaseFilter +public class AudioPadFilter : BaseFilter { - public override string Filter => $"apad=whole_dur={audioDuration.TotalMilliseconds.ToString(NumberFormatInfo.InvariantInfo)}ms"; + public override string Filter => "apad"; public override FrameState NextState(FrameState currentState) => currentState; } diff --git a/ErsatzTV.FFmpeg/OutputOption/OutputTsOffsetOption.cs b/ErsatzTV.FFmpeg/OutputOption/OutputTsOffsetOption.cs index e1da8751..ad86fd6b 100644 --- a/ErsatzTV.FFmpeg/OutputOption/OutputTsOffsetOption.cs +++ b/ErsatzTV.FFmpeg/OutputOption/OutputTsOffsetOption.cs @@ -2,12 +2,12 @@ namespace ErsatzTV.FFmpeg.OutputOption; -public class OutputTsOffsetOption(double ptsOffset) : OutputOption +public class OutputTsOffsetOption(long ptsOffset, int videoTrackTimeScale) : OutputOption { public override string[] OutputOptions => [ "-output_ts_offset", - $"{ptsOffset.ToString(NumberFormatInfo.InvariantInfo)}s" + $"{(ptsOffset / (double)videoTrackTimeScale).ToString(NumberFormatInfo.InvariantInfo)}" ]; // public override FrameState NextState(FrameState currentState) => currentState with { PtsOffset = _ptsOffset }; diff --git a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs index 63fadcb3..131085db 100644 --- a/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs @@ -87,7 +87,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder if (decodeCapability == FFmpegCapability.Hardware) { pipelineSteps.Add(new CudaHardwareAccelerationOption(isHdrTonemap)); - pipelineSteps.Add(new NoAutoScaleOutputOption()); + //pipelineSteps.Add(new NoAutoScaleOutputOption()); } // disable hw accel if decoder/encoder isn't supported @@ -120,6 +120,15 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder Option maybeDecoder = (ffmpegState.DecoderHardwareAccelerationMode, videoStream.Codec) switch { + (HardwareAccelerationMode.Nvenc, VideoFormat.Hevc) => new DecoderHevcCuvid(HardwareAccelerationMode.Nvenc), + (HardwareAccelerationMode.Nvenc, VideoFormat.H264) => new DecoderH264Cuvid(HardwareAccelerationMode.Nvenc), + (HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg2Video) => new DecoderMpeg2Cuvid( + HardwareAccelerationMode.Nvenc, context.ShouldDeinterlace), + (HardwareAccelerationMode.Nvenc, VideoFormat.Vc1) => new DecoderVc1Cuvid(HardwareAccelerationMode.Nvenc), + (HardwareAccelerationMode.Nvenc, VideoFormat.Vp9) => new DecoderVp9Cuvid(HardwareAccelerationMode.Nvenc), + (HardwareAccelerationMode.Nvenc, VideoFormat.Mpeg4) => + new DecoderMpeg4Cuvid(HardwareAccelerationMode.Nvenc), + (HardwareAccelerationMode.Nvenc, VideoFormat.Av1) => new DecoderAv1Cuvid(HardwareAccelerationMode.Nvenc), (HardwareAccelerationMode.Nvenc, _) => new DecoderImplicitCuda(), _ => GetSoftwareDecoder(videoStream) }; diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs index b08c9494..88d10004 100644 --- a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs +++ b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs @@ -193,6 +193,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder new NoStatsOption(), new LoglevelErrorOption(), new StandardFormatFlags(), + new NoDemuxDecodeDelayOutputOption(), outputOption, new ClosedGopOutputOption() }; @@ -446,10 +447,9 @@ public abstract class PipelineBuilderBase : IPipelineBuilder audioInputFile.FilterSteps.Add(new AudioSetPtsFilter()); } - foreach (TimeSpan audioDuration in audioInputFile.DesiredState.AudioDuration.Filter(d => d > TimeSpan.Zero)) + foreach (TimeSpan _ in audioInputFile.DesiredState.AudioDuration) { - audioInputFile.FilterSteps.Add(new AudioPadFilter(audioDuration)); - pipelineSteps.Add(new ShortestOutputOption()); + audioInputFile.FilterSteps.Add(new AudioPadFilter()); } } @@ -658,7 +658,10 @@ public abstract class PipelineBuilderBase : IPipelineBuilder if (ffmpegState.PtsOffset > 0) { - pipelineSteps.Add(new OutputTsOffsetOption(ffmpegState.PtsOffset)); + foreach (int videoTrackTimeScale in desiredState.VideoTrackTimeScale) + { + pipelineSteps.Add(new OutputTsOffsetOption(ffmpegState.PtsOffset, videoTrackTimeScale)); + } } }