diff --git a/CHANGELOG.md b/CHANGELOG.md index aa554d68c..272e52807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Clean transcode cache folder on startup and after `HLS Segmenter` session terminates for any reason +### Changed +- Remove thread limitation for scenarios where it is not required + - This should give a performance boost to installations that don't use hardware acceleration + ## [0.5.7-beta] - 2022-05-14 ### Fixed - Reduce memory use due to library scan operations diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index 0f36a9e17..2644ba2b1 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -28,10 +28,10 @@ public class HlsSessionWorker : IHlsSessionWorker private string _channelNumber; private bool _firstProcess; private DateTimeOffset _lastAccess; + private DateTimeOffset _playlistStart; private Option _targetFramerate; private Timer _timer; private DateTimeOffset _transcodedUntil; - private DateTimeOffset _playlistStart; public HlsSessionWorker( IHlsPlaylistFilter hlsPlaylistFilter, @@ -357,14 +357,14 @@ public class HlsSessionWorker : IHlsSessionWorker .ToList(); var toDelete = allSegments.Filter(s => s.SequenceNumber < trimResult.Sequence).ToList(); - if (toDelete.Count > 0) - { - _logger.LogDebug( - "Deleting HLS segments {Min} to {Max} (less than {StartSequence})", - toDelete.Map(s => s.SequenceNumber).Min(), - toDelete.Map(s => s.SequenceNumber).Max(), - trimResult.Sequence); - } + // if (toDelete.Count > 0) + // { + // _logger.LogDebug( + // "Deleting HLS segments {Min} to {Max} (less than {StartSequence})", + // toDelete.Map(s => s.SequenceNumber).Min(), + // toDelete.Map(s => s.SequenceNumber).Max(), + // trimResult.Sequence); + // } foreach (Segment segment in toDelete) { diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index bae888adb..95aedec0c 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -234,7 +234,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService outputFormat, hlsPlaylistPath, hlsSegmentTemplate, - ptsOffset); + ptsOffset, + playbackSettings.ThreadCount); _logger.LogDebug("FFmpeg desired state {FrameState}", desiredState); @@ -327,7 +328,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService var ffmpegVideoStream = new VideoStream( 0, VideoFormat.GeneratedImage, - new PixelFormatYuv420P(), + new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p new FrameSize(videoVersion.Width, videoVersion.Height), None, true); @@ -348,7 +349,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService outputFormat, hlsPlaylistPath, hlsSegmentTemplate, - ptsOffset); + ptsOffset, + Option.None); var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(0, "ass", StreamKind.Video); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs index 6159809e5..d797500eb 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs @@ -177,7 +177,6 @@ public class FFmpegPlaybackSettingsCalculator new() { HardwareAcceleration = HardwareAccelerationKind.None, - ThreadCount = 1, FormatFlags = CommonFormatFlags, VideoFormat = ffmpegProfile.VideoFormat, VideoBitrate = ffmpegProfile.VideoBitrate, @@ -192,7 +191,8 @@ public class FFmpegPlaybackSettingsCalculator StreamingMode.HttpLiveStreamingSegmenter => hlsRealtime, _ => true }, - VideoTrackTimeScale = 90000 + VideoTrackTimeScale = 90000, + FrameRate = 24 }; private static bool NeedToScale(FFmpegProfile ffmpegProfile, MediaVersion version) => diff --git a/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs b/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs index de51f45ad..08834e9e0 100644 --- a/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs +++ b/ErsatzTV.Core/FFmpeg/HlsPlaylistFilter.cs @@ -25,13 +25,13 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter { try { - _logger.LogDebug( - "TrimPlaylist - Start {PlaylistStart}, FilterBefore {FilterBefore}, MaxSegments {MaxSegments}, EndWithDiscontinuity {EndWithDiscontinuity}", - playlistStart, - filterBefore, - maxSegments, - endWithDiscontinuity); - + // _logger.LogDebug( + // "TrimPlaylist - Start {PlaylistStart}, FilterBefore {FilterBefore}, MaxSegments {MaxSegments}, EndWithDiscontinuity {EndWithDiscontinuity}", + // playlistStart, + // filterBefore, + // maxSegments, + // endWithDiscontinuity); + DateTimeOffset currentTime = playlistStart; DateTimeOffset nextPlaylistStart = DateTimeOffset.MaxValue; @@ -120,8 +120,7 @@ public class HlsPlaylistFilter : IHlsPlaylistFilter playlist += "#EXT-X-DISCONTINUITY" + Environment.NewLine; } - // if (playlist.Trim().Split(Environment.NewLine).All(l => string.IsNullOrWhiteSpace(l) || l.StartsWith('#'))) - if (playlist.Trim().Split(Environment.NewLine).All(l => l.StartsWith('#'))) + if (playlist.Trim().Split(Environment.NewLine).All(l => string.IsNullOrWhiteSpace(l) || l.StartsWith('#'))) { throw new Exception("Trimming playlist to nothing"); } diff --git a/ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs b/ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs index 4034028e6..7a39d55d1 100644 --- a/ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs +++ b/ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs @@ -56,7 +56,7 @@ public class PipelineGeneratorTests HardwareAccelerationMode.None, Option.None, Option.None, - Option.None, + TimeSpan.FromSeconds(1), Option.None, false, Option.None, @@ -65,7 +65,8 @@ public class PipelineGeneratorTests OutputFormatKind.MpegTs, Option.None, Option.None, - 0); + 0, + Option.None); var builder = new PipelineBuilder(videoInputFile, audioInputFile, None, None, "", "", _logger); FFmpegPipeline result = builder.Build(ffmpegState, desiredState); @@ -73,7 +74,8 @@ public class PipelineGeneratorTests result.PipelineSteps.Should().HaveCountGreaterThan(0); result.PipelineSteps.Should().Contain(ps => ps is EncoderLibx265); - PrintCommand(videoInputFile, audioInputFile, None, None, result); + string command = PrintCommand(videoInputFile, audioInputFile, None, None, result); + command.Should().Be("-threads 1 -nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -ss 00:00:01 -c:v h264 -re -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -video_track_timescale 90000 -b:v 2000k -maxrate:v 2000k -bufsize:v 4000k -c:a aac -ac 2 -b:a 320k -maxrate:a 320k -bufsize:a 640k -ar 48k -c:v libx265 -tag:v hvc1 -x265-params log-level=error -f mpegts -mpegts_flags +initial_discontinuity pipe:1"); } [Test] @@ -90,7 +92,7 @@ public class PipelineGeneratorTests string command = PrintCommand(None, None, None, concatInputFile, result); command.Should().Be( - "-threads 1 -nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -f concat -safe 0 -protocol_whitelist file,http,tcp,https,tcp,tls -probesize 32 -re -stream_loop -1 -i http://localhost:8080/ffmpeg/concat/1 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -c copy -map_metadata -1 -metadata service_provider=\"ErsatzTV\" -metadata service_name=\"Some Channel\" -f mpegts -mpegts_flags +initial_discontinuity pipe:1"); + "-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -f concat -safe 0 -protocol_whitelist file,http,tcp,https,tcp,tls -probesize 32 -re -stream_loop -1 -i http://localhost:8080/ffmpeg/concat/1 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -c copy -map_metadata -1 -metadata service_provider=\"ErsatzTV\" -metadata service_name=\"Some Channel\" -f mpegts -mpegts_flags +initial_discontinuity pipe:1"); } [Test] @@ -140,7 +142,8 @@ public class PipelineGeneratorTests OutputFormatKind.MpegTs, Option.None, Option.None, - 0); + 0, + Option.None); var builder = new PipelineBuilder(videoInputFile, audioInputFile, None, None, "", "", _logger); FFmpegPipeline result = builder.Build(ffmpegState, desiredState); @@ -152,7 +155,7 @@ public class PipelineGeneratorTests string command = PrintCommand(videoInputFile, audioInputFile, None, None, result); command.Should().Be( - "-threads 1 -nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mpegts -mpegts_flags +initial_discontinuity pipe:1"); + "-nostdin -hide_banner -nostats -loglevel error -fflags +genpts+discardcorrupt+igndts -i /tmp/whatever.mkv -map 0:1 -map 0:0 -muxdelay 0 -muxpreload 0 -movflags +faststart -flags cgop -sc_threshold 0 -c:v copy -c:a copy -f mpegts -mpegts_flags +initial_discontinuity pipe:1"); } [Test] diff --git a/ErsatzTV.FFmpeg/FFmpegState.cs b/ErsatzTV.FFmpeg/FFmpegState.cs index e15f1b235..ae534c907 100644 --- a/ErsatzTV.FFmpeg/FFmpegState.cs +++ b/ErsatzTV.FFmpeg/FFmpegState.cs @@ -16,7 +16,8 @@ public record FFmpegState( OutputFormatKind OutputFormat, Option HlsPlaylistPath, Option HlsSegmentTemplate, - long PtsOffset) + long PtsOffset, + Option ThreadCount) { public static FFmpegState Concat(bool saveReport, string channelName) => new( @@ -33,5 +34,6 @@ public record FFmpegState( OutputFormatKind.MpegTs, Option.None, Option.None, - 0); + 0, + Option.None); } diff --git a/ErsatzTV.FFmpeg/PipelineBuilder.cs b/ErsatzTV.FFmpeg/PipelineBuilder.cs index 66b0ccb51..afb2a2936 100644 --- a/ErsatzTV.FFmpeg/PipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/PipelineBuilder.cs @@ -36,7 +36,6 @@ public class PipelineBuilder { _pipelineSteps = new List { - new ThreadCountOption(1), // try everything single-threaded new NoStandardInputOption(), new HideBannerOption(), new NoStatsOption(), @@ -80,6 +79,11 @@ public class PipelineBuilder concatInputFile.AddOption(new RealtimeInputOption()); concatInputFile.AddOption(new InfiniteLoopInputOption(HardwareAccelerationMode.None)); + foreach (int threadCount in ffmpegState.ThreadCount) + { + _pipelineSteps.Insert(0, new ThreadCountOption(threadCount)); + } + _pipelineSteps.Add(new NoSceneDetectOutputOption(0)); _pipelineSteps.Add(new EncoderCopyAll()); @@ -111,6 +115,22 @@ public class PipelineBuilder public FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState) { + if (ffmpegState.Start.Exists(s => s > TimeSpan.Zero) && desiredState.Realtime) + { + _logger.LogInformation( + "Forcing {Threads} ffmpeg thread due to buggy combination of stream seek and realtime output", + 1); + + _pipelineSteps.Insert(0, new ThreadCountOption(1)); + } + else + { + foreach (int threadCount in ffmpegState.ThreadCount) + { + _pipelineSteps.Insert(0, new ThreadCountOption(threadCount)); + } + } + var allVideoStreams = _videoInputFile.SelectMany(f => f.VideoStreams).ToList(); // -sc_threshold 0 is unsupported with mpeg2video