From c0b8ff1a0657d716a20ca1761ceb4672eea1840f Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sun, 15 Feb 2026 09:46:07 -0600 Subject: [PATCH] generate slug instead of probing and transcoding resource (#2824) * generate slug instead of probing and using slug resource * refactor * more fixes --- .../Streaming/HlsSessionWorker.cs | 4 +- .../GetSlugProcessByChannelNumberHandler.cs | 79 +----- .../FFmpeg/FFmpegLibraryProcessService.cs | 258 ++++++++++++++---- .../FFmpegPlaybackSettingsCalculator.cs | 2 +- ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs | 2 +- ErsatzTV.Core/FFmpeg/FFmpegProgress.cs | 2 +- .../FFmpeg/IFFmpegProcessService.cs | 8 + ErsatzTV.FFmpeg/InputFile.cs | 8 +- ErsatzTV/ErsatzTV.csproj | 1 - ErsatzTV/Resources/slug.mp4 | Bin 16005 -> 0 bytes .../RunOnce/ResourceExtractorService.cs | 1 - 11 files changed, 232 insertions(+), 133 deletions(-) delete mode 100644 ErsatzTV/Resources/slug.mp4 diff --git a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs index 16cbc4494..8a983323a 100644 --- a/ErsatzTV.Application/Streaming/HlsSessionWorker.cs +++ b/ErsatzTV.Application/Streaming/HlsSessionWorker.cs @@ -393,7 +393,8 @@ public class HlsSessionWorker : IHlsSessionWorker HlsSessionState.SlugAndRealtime => HlsSessionState.ZeroAndRealtime, // after completing the item, insert a slug - _ when isComplete && _slugSeconds.IsSome => HlsSessionState.SlugAndWorkAhead, + HlsSessionState.ZeroAndWorkAhead or HlsSessionState.SeekAndWorkAhead when isComplete && _slugSeconds.IsSome => HlsSessionState.SlugAndWorkAhead, + HlsSessionState.ZeroAndRealtime or HlsSessionState.SeekAndRealtime when isComplete && _slugSeconds.IsSome => HlsSessionState.SlugAndRealtime, // after seeking and completing the item, start at zero HlsSessionState.SeekAndWorkAhead => HlsSessionState.ZeroAndWorkAhead, @@ -454,6 +455,7 @@ public class HlsSessionWorker : IHlsSessionWorker { HlsSessionState.SeekAndWorkAhead => HlsSessionState.SeekAndRealtime, HlsSessionState.ZeroAndWorkAhead => HlsSessionState.ZeroAndRealtime, + HlsSessionState.SlugAndWorkAhead => HlsSessionState.SlugAndRealtime, _ => _state }; diff --git a/ErsatzTV.Application/Streaming/Queries/GetSlugProcessByChannelNumberHandler.cs b/ErsatzTV.Application/Streaming/Queries/GetSlugProcessByChannelNumberHandler.cs index e8eea5fa8..f53b2e948 100644 --- a/ErsatzTV.Application/Streaming/Queries/GetSlugProcessByChannelNumberHandler.cs +++ b/ErsatzTV.Application/Streaming/Queries/GetSlugProcessByChannelNumberHandler.cs @@ -1,22 +1,15 @@ -using System.IO.Abstractions; using ErsatzTV.Core; using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Domain.Filler; -using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; -using ErsatzTV.Core.Interfaces.Metadata; -using ErsatzTV.FFmpeg; +using ErsatzTV.Core.Interfaces.Streaming; using ErsatzTV.Infrastructure.Data; -using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; namespace ErsatzTV.Application.Streaming; public class GetSlugProcessByChannelNumberHandler( IDbContextFactory dbContextFactory, - IFileSystem fileSystem, - IFFmpegProcessService ffmpegProcessService, - ILocalStatisticsProvider localStatisticsProvider) + IFFmpegProcessService ffmpegProcessService) : FFmpegProcessHandler(dbContextFactory) { protected override async Task> GetProcess( @@ -27,82 +20,30 @@ public class GetSlugProcessByChannelNumberHandler( string ffprobePath, CancellationToken cancellationToken) { - string videoPath = fileSystem.Path.Combine(FileSystemLayout.ResourcesCacheFolder, "slug.mp4"); - - bool saveReports = await dbContext.ConfigElements - .GetValue(ConfigElementKey.FFmpegSaveReports, cancellationToken) - .Map(result => result.IfNone(false)); - - Either maybeVersion = - await localStatisticsProvider.GetStatistics(ffprobePath, videoPath); - foreach (var error in maybeVersion.LeftToSeq()) - { - return error; - } - - var version = maybeVersion.RightToSeq().Head(); - - var mediaItem = new OtherVideo - { - MediaVersions = [version] - }; - - TimeSpan duration = version.Duration; - foreach (double slugSeconds in request.SlugSeconds) + var duration = TimeSpan.FromSeconds(await request.SlugSeconds.IfNoneAsync(0)); + if (duration <= TimeSpan.Zero) { - TimeSpan seconds = TimeSpan.FromSeconds(slugSeconds); - if (seconds > TimeSpan.Zero && seconds < duration) - { - duration = seconds; - } + return BaseError.New("Slug seconds must be non-zero"); } DateTimeOffset finish = request.Now.Add(duration); - PlayoutItemResult playoutItemResult = await ffmpegProcessService.ForPlayoutItem( + var playoutItemResult = await ffmpegProcessService.Slug( ffmpegPath, - ffprobePath, - saveReports, channel, - new MediaItemVideoVersion(mediaItem, version), - new MediaItemAudioVersion(mediaItem, version), - videoPath, - videoPath, - _ => Task.FromResult>([]), - string.Empty, - string.Empty, - string.Empty, - ChannelSubtitleMode.None, - request.Now, - finish, request.Now, duration, - [], - [], - channel.FFmpegProfile.VaapiDisplay, - channel.FFmpegProfile.VaapiDriver, - channel.FFmpegProfile.VaapiDevice, - Optional(channel.FFmpegProfile.QsvExtraHardwareFrames), request.HlsRealtime, - StreamInputKind.Vod, - FillerKind.None, - inPoint: TimeSpan.Zero, - request.ChannelStartTime, - request.PtsOffset, - request.TargetFramerate, - Option.None, - _ => { }, - canProxy: true, - cancellationToken); + request.PtsOffset); var result = new PlayoutItemProcessModel( - playoutItemResult.Process, - playoutItemResult.GraphicsEngineContext, + playoutItemResult, + Option.None, duration, finish, isComplete: true, request.Now.ToUnixTimeSeconds(), - playoutItemResult.MediaItemId, + Option.None, Optional(channel.PlayoutOffset), !request.HlsRealtime); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index cbb8d1193..22584711f 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -665,7 +665,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService string vaapiDevice, Option qsvExtraHardwareFrames) { - FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettingsCalculator.CalculateErrorSettings( + FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettingsCalculator.CalculateGeneratedImageSettings( channel.StreamingMode, channel.FFmpegProfile, hlsRealtime); @@ -726,57 +726,6 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService playbackSettings.NormalizeColors, playbackSettings.Deinterlace); - OutputFormatKind outputFormat = OutputFormatKind.MpegTs; - switch (channel.StreamingMode) - { - case StreamingMode.HttpLiveStreamingSegmenter: - outputFormat = OutputFormatKind.Hls; - break; - } - - Option hlsPlaylistPath = outputFormat is OutputFormatKind.Hls or OutputFormatKind.HlsMp4 - ? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8") - : Option.None; - - long nowSeconds = now.ToUnixTimeSeconds(); - - Option hlsSegmentTemplate = outputFormat switch - { - OutputFormatKind.Hls => Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts"), - OutputFormatKind.HlsMp4 => Path.Combine( - FileSystemLayout.TranscodeFolder, - channel.Number, - $"live_{nowSeconds}_%06d.m4s"), - _ => Option.None - }; - - Option hlsInitTemplate = outputFormat switch - { - OutputFormatKind.HlsMp4 => $"{nowSeconds}_init.mp4", - _ => Option.None - }; - - Option hlsSegmentOptions = Option.None; - if (outputFormat is OutputFormatKind.Hls) - { - string options = string.Empty; - - if (ptsOffset == TimeSpan.Zero) - { - options += "+initial_discontinuity"; - } - - if (audioFormat == AudioFormat.AacLatm) - { - options += "+latm"; - } - - if (!string.IsNullOrWhiteSpace(options)) - { - hlsSegmentOptions = $"mpegts_flags={options}"; - } - } - string videoPath = _localFileSystem.GetCustomOrDefaultFile( FileSystemLayout.ResourcesCacheFolder, "background.png"); @@ -802,6 +751,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings); _logger.LogDebug("HW accel mode: {HwAccel}", hwAccel); + var hlsOptions = GetHlsOptions(channel, now, ptsOffset, audioFormat); + var ffmpegState = new FFmpegState( channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel, HardwareAccelerationMode.None, // no hw accel decode since errors loop @@ -816,11 +767,11 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService None, None, None, - outputFormat, - hlsPlaylistPath, - hlsSegmentTemplate, - hlsInitTemplate, - hlsSegmentOptions, + hlsOptions.OutputFormat, + hlsOptions.HlsPlaylistPath, + hlsOptions.HlsSegmentTemplate, + hlsOptions.HlsInitTemplate, + hlsOptions.HlsSegmentOptions, ptsOffset, Option.None, qsvExtraHardwareFrames, @@ -862,6 +813,136 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService return GetCommand(ffmpegPath, videoInputFile, audioInputFile, None, None, None, pipeline); } + public async Task Slug( + string ffmpegPath, + Channel channel, + DateTimeOffset now, + TimeSpan duration, + bool hlsRealtime, + TimeSpan ptsOffset) + { + FFmpegPlaybackSettings playbackSettings = FFmpegPlaybackSettingsCalculator.CalculateGeneratedImageSettings( + channel.StreamingMode, + channel.FFmpegProfile, + hlsRealtime); + + Resolution desiredResolution = channel.FFmpegProfile.Resolution; + + string audioFormat = playbackSettings.AudioFormat switch + { + FFmpegProfileAudioFormat.Ac3 => AudioFormat.Ac3, + FFmpegProfileAudioFormat.AacLatm => AudioFormat.AacLatm, + _ => AudioFormat.Aac + }; + + var audioState = new AudioState( + audioFormat, + playbackSettings.AudioChannels, + playbackSettings.AudioBitrate, + playbackSettings.AudioBufferSize, + playbackSettings.AudioSampleRate, + false, + AudioFilter.None, + playbackSettings.TargetLoudness); + + string videoFormat = GetVideoFormat(playbackSettings); + + var desiredState = new FrameState( + playbackSettings.RealtimeOutput, + InfiniteLoop: false, + videoFormat, + GetVideoProfile(videoFormat, channel.FFmpegProfile.VideoProfile), + VideoPreset.Unset, + channel.FFmpegProfile.AllowBFrames, + new PixelFormatYuv420P(), + new FrameSize(desiredResolution.Width, desiredResolution.Height), + new FrameSize(desiredResolution.Width, desiredResolution.Height), + Option.None, + channel.FFmpegProfile.PadMode is FilterMode.HardwareIfPossible + ? FFmpegFilterMode.HardwareIfPossible + : FFmpegFilterMode.Software, + IsAnamorphic: false, + Option.None, + playbackSettings.VideoBitrate, + playbackSettings.VideoBufferSize, + playbackSettings.VideoTrackTimeScale, + playbackSettings.NormalizeColors, + playbackSettings.Deinterlace); + + var frameRate = playbackSettings.FrameRate.IfNone(new FrameRate("24")); + + var ffmpegVideoStream = new VideoStream( + 0, + VideoFormat.GeneratedImage, + string.Empty, + new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p + ColorParams.Default, + desiredState.PaddedSize, + MaybeSampleAspectRatio: "1:1", + DisplayAspectRatio: string.Empty, + frameRate, + true, + ScanKind.Progressive); + + var videoInputFile = new LavfiInputFile( + $"color=c=black:s={desiredState.PaddedSize.Width}x{desiredState.PaddedSize.Height}:r={frameRate.FrameRateString}:d={duration.TotalSeconds}", + ffmpegVideoStream); + + HardwareAccelerationMode hwAccel = GetHardwareAccelerationMode(playbackSettings); + + var hlsOptions = GetHlsOptions(channel, now, ptsOffset, audioFormat); + + var ffmpegState = new FFmpegState( + channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel, + HardwareAccelerationMode.None, // no hw accel decode since errors loop + hwAccel, + VaapiDriverName(hwAccel, channel.FFmpegProfile.VaapiDriver), + VaapiDeviceName(hwAccel, channel.FFmpegProfile.VaapiDevice), + playbackSettings.StreamSeek, + duration, + channel.StreamingMode != StreamingMode.HttpLiveStreamingDirect, + "ErsatzTV", + channel.Name, + None, + None, + None, + hlsOptions.OutputFormat, + hlsOptions.HlsPlaylistPath, + hlsOptions.HlsSegmentTemplate, + hlsOptions.HlsInitTemplate, + hlsOptions.HlsSegmentOptions, + ptsOffset, + Option.None, + Optional(channel.FFmpegProfile.QsvExtraHardwareFrames), + false, + false, + GetTonemapAlgorithm(playbackSettings), + channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel); + + var audioInputFile = new NullAudioInputFile(audioState); + + IPipelineBuilder pipelineBuilder = await _pipelineBuilderFactory.GetBuilder( + hwAccel, + videoInputFile, + audioInputFile, + Option.None, + Option.None, + Option.None, + Option.None, + VaapiDisplayName(hwAccel, channel.FFmpegProfile.VaapiDisplay), + VaapiDriverName(hwAccel, channel.FFmpegProfile.VaapiDriver), + VaapiDeviceName(hwAccel, channel.FFmpegProfile.VaapiDevice), + channel.Number == FileSystemLayout.TranscodeTroubleshootingChannel + ? FileSystemLayout.TranscodeTroubleshootingFolder + : FileSystemLayout.FFmpegReportsFolder, + FileSystemLayout.FontsCacheFolder, + ffmpegPath); + + FFmpegPipeline pipeline = pipelineBuilder.Build(ffmpegState, desiredState); + + return GetCommand(ffmpegPath, videoInputFile, audioInputFile, None, None, None, pipeline); + } + public async Task ConcatChannel( string ffmpegPath, bool saveReports, @@ -1260,4 +1341,67 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService return false; } + + private static HlsOptions GetHlsOptions(Channel channel, DateTimeOffset now, TimeSpan ptsOffset, string audioFormat) + { + OutputFormatKind outputFormat = OutputFormatKind.MpegTs; + switch (channel.StreamingMode) + { + case StreamingMode.HttpLiveStreamingSegmenter: + outputFormat = OutputFormatKind.Hls; + break; + } + + Option hlsPlaylistPath = outputFormat is OutputFormatKind.Hls or OutputFormatKind.HlsMp4 + ? Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live.m3u8") + : Option.None; + + long nowSeconds = now.ToUnixTimeSeconds(); + + Option hlsSegmentTemplate = outputFormat switch + { + OutputFormatKind.Hls => Path.Combine(FileSystemLayout.TranscodeFolder, channel.Number, "live%06d.ts"), + OutputFormatKind.HlsMp4 => Path.Combine( + FileSystemLayout.TranscodeFolder, + channel.Number, + $"live_{nowSeconds}_%06d.m4s"), + _ => Option.None + }; + + Option hlsInitTemplate = outputFormat switch + { + OutputFormatKind.HlsMp4 => $"{nowSeconds}_init.mp4", + _ => Option.None + }; + + Option hlsSegmentOptions = Option.None; + if (outputFormat is OutputFormatKind.Hls) + { + string options = string.Empty; + + if (ptsOffset == TimeSpan.Zero) + { + options += "+initial_discontinuity"; + } + + if (audioFormat == AudioFormat.AacLatm) + { + options += "+latm"; + } + + if (!string.IsNullOrWhiteSpace(options)) + { + hlsSegmentOptions = $"mpegts_flags={options}"; + } + } + + return new HlsOptions(outputFormat, hlsPlaylistPath, hlsSegmentTemplate, hlsInitTemplate, hlsSegmentOptions); + } + + private sealed record HlsOptions( + OutputFormatKind OutputFormat, + Option HlsPlaylistPath, + Option HlsSegmentTemplate, + Option HlsInitTemplate, + Option HlsSegmentOptions); } diff --git a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs index 812da70de..c200ecada 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs @@ -181,7 +181,7 @@ public static class FFmpegPlaybackSettingsCalculator return result; } - public static FFmpegPlaybackSettings CalculateErrorSettings( + public static FFmpegPlaybackSettings CalculateGeneratedImageSettings( StreamingMode streamingMode, FFmpegProfile ffmpegProfile, bool hlsRealtime) => diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs index 42ced7862..f4ea620b5 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProcessService.cs @@ -71,7 +71,7 @@ public class FFmpegProcessService } FFmpegPlaybackSettings playbackSettings = - FFmpegPlaybackSettingsCalculator.CalculateErrorSettings( + FFmpegPlaybackSettingsCalculator.CalculateGeneratedImageSettings( StreamingMode.TransportStream, channel.FFmpegProfile, false); diff --git a/ErsatzTV.Core/FFmpeg/FFmpegProgress.cs b/ErsatzTV.Core/FFmpeg/FFmpegProgress.cs index 3b4665820..b2def683c 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegProgress.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegProgress.cs @@ -39,7 +39,7 @@ public partial class FFmpegProgress speed); } } - else if (speed < 1.0) + else if (speed < 0.99) { logger.LogWarning( "Media item {MediaItemId} on channel {Channel} transcoded at {Speed}x (throttled) which may not be fast enough to support playback", diff --git a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs index 79f3ac935..1a4114869 100644 --- a/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs +++ b/ErsatzTV.Core/Interfaces/FFmpeg/IFFmpegProcessService.cs @@ -58,6 +58,14 @@ public interface IFFmpegProcessService string vaapiDevice, Option qsvExtraHardwareFrames); + Task Slug( + string ffmpegPath, + Channel channel, + DateTimeOffset now, + TimeSpan duration, + bool hlsRealtime, + TimeSpan ptsOffset); + Task ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host); Task WrapSegmenter( diff --git a/ErsatzTV.FFmpeg/InputFile.cs b/ErsatzTV.FFmpeg/InputFile.cs index 930700d4b..0741a37f5 100644 --- a/ErsatzTV.FFmpeg/InputFile.cs +++ b/ErsatzTV.FFmpeg/InputFile.cs @@ -60,7 +60,7 @@ public record NullAudioInputFile : AudioInputFile DesiredState) => InputOptions.Add(new LavfiInputOption()); - public void Deconstruct(out AudioState DesiredState) => DesiredState = this.DesiredState; + public void Deconstruct(out AudioState desiredState) => desiredState = DesiredState; } public record VideoInputFile( @@ -79,6 +79,12 @@ public record VideoInputFile( } } +public record LavfiInputFile : VideoInputFile +{ + public LavfiInputFile(string parameters, VideoStream videoStream) : base(parameters, [videoStream]) + => InputOptions.Add(new LavfiInputOption()); +} + public record WatermarkInputFile(string Path, IList VideoStreams, WatermarkState DesiredState) : VideoInputFile(Path, VideoStreams); diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 77ca0eba6..d13c75219 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -119,7 +119,6 @@ ResXFileCodeGenerator Common.Designer.cs - diff --git a/ErsatzTV/Resources/slug.mp4 b/ErsatzTV/Resources/slug.mp4 deleted file mode 100644 index 94a335138879a7e044e054c0125be9f19cfc0a5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16005 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 diff --git a/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs b/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs index dcc035bb1..42f62beba 100644 --- a/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs +++ b/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs @@ -27,7 +27,6 @@ public class ResourceExtractorService : BackgroundService await ExtractResource(assembly, "sequential-schedule.schema.json", stoppingToken); await ExtractResource(assembly, "sequential-schedule-import.schema.json", stoppingToken); await ExtractResource(assembly, "test.avs", stoppingToken); - await ExtractResource(assembly, "slug.mp4", stoppingToken); await ExtractFontResource(assembly, "Sen.ttf", stoppingToken); await ExtractFontResource(assembly, "Roboto-Regular.ttf", stoppingToken);