From ea5956a2687e28398c6146430fc9e00c0287952f Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Mon, 13 Oct 2025 16:26:06 -0500 Subject: [PATCH] improve stream startup (#2525) --- CHANGELOG.md | 1 + .../Streaming/HlsSessionWorker.cs | 4 +-- ...layoutItemProcessByChannelNumberHandler.cs | 18 ++++++++++- .../Pipeline/PipelineBuilderBase.cs | 31 +++++++++---------- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f36d61c1..c4d1c1c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix playback troubleshooting selecting a subtitle even with no subtitle stream selected in the UI - Fix intermittent watermark opacity - Improve reliability of live remote streams; they should transcode closer to realtime in most cases +- Dramatically improve stream startup time ### Changed - Do not use graphics engine for single, permanent watermark diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index 6a676e5f7..2a2ae3ca0 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -393,9 +393,7 @@ public class HlsSessionWorker : IHlsSessionWorker return result; } - private async Task Transcode( - bool realtime, - CancellationToken cancellationToken) + private async Task Transcode(bool realtime, CancellationToken cancellationToken) { try { diff --git a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs index 06cdc330b..f1858110f 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs @@ -281,6 +281,22 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< DateTimeOffset effectiveNow = request.StartAtZero ? start : now; TimeSpan duration = finish - effectiveNow; + bool isComplete = true; + + // if we are working ahead, limit to 45s + if (!request.HlsRealtime) + { + TimeSpan limit = TimeSpan.FromSeconds(45); + + if (duration > limit) + { + finish = effectiveNow + limit; + outPoint = inPoint + limit; + duration = limit; + isComplete = false; + } + } + if (_isDebugNoSync) { Command doesNotExistProcess = await _ffmpegProcessService.ForError( @@ -427,7 +443,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< playoutItemResult.GraphicsEngineContext, duration, finish, - true); + isComplete); return Right(result); } diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs index 1d31d16c5..d8e6207c0 100644 --- a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs +++ b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs @@ -563,7 +563,7 @@ public abstract class PipelineBuilderBase : IPipelineBuilder : SetDecoder(videoInputFile, videoStream, ffmpegState, context); //SetStillImageInfiniteLoop(videoInputFile, videoStream, ffmpegState); - SetRealtimeInput(videoInputFile, desiredState); + SetRealtimeInput(videoInputFile, ffmpegState, desiredState); SetInfiniteLoop(videoInputFile, videoStream, ffmpegState, desiredState); SetFrameRateOutput(desiredState, pipelineSteps); SetVideoTrackTimescaleOutput(desiredState, pipelineSteps); @@ -760,29 +760,26 @@ public abstract class PipelineBuilderBase : IPipelineBuilder } } - private void SetRealtimeInput(VideoInputFile videoInputFile, FrameState desiredState) + private void SetRealtimeInput(VideoInputFile videoInputFile, FFmpegState ffmpegState, FrameState desiredState) { - if (videoInputFile.StreamInputKind is StreamInputKind.Live) + if (videoInputFile.StreamInputKind is StreamInputKind.Live || !desiredState.Realtime) { return; } - int initialBurst; - if (!desiredState.Realtime) - { - initialBurst = 45; - } - else + AudioFilter filter = _audioInputFile + .Map(a => a.DesiredState.NormalizeLoudnessFilter) + .IfNone(AudioFilter.None); + + int initialBurst = filter switch { - AudioFilter filter = _audioInputFile - .Map(a => a.DesiredState.NormalizeLoudnessFilter) - .IfNone(AudioFilter.None); + AudioFilter.LoudNorm => 5, + _ => 0 + }; - initialBurst = filter switch - { - AudioFilter.LoudNorm => 5, - _ => 0 - }; + if (ffmpegState.Finish.Any(finish => finish.TotalSeconds < initialBurst)) + { + return; } _audioInputFile.Iter(a => a.AddOption(new ReadrateInputOption(_ffmpegCapabilities, initialBurst, _logger)));