From b8e44f75445df03f18610921e74109f9e5cfc182 Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:05:30 -0600 Subject: [PATCH] audio and video normalization settings appear to work --- .../Commands/CreateFFmpegProfileHandler.cs | 3 +- .../Commands/UpdateFFmpegProfileHandler.cs | 2 +- .../FFmpeg/FFmpegLibraryProcessService.cs | 4 +- .../OutputFormat/OutputFormatHls.cs | 20 +++++++--- .../Pipeline/PipelineBuilderBase.cs | 38 ++++++++++++++++++- ErsatzTV/Pages/Channels.razor | 30 ++++++++++++--- .../ViewModels/FFmpegProfileEditViewModel.cs | 33 +++++++++++++++- 7 files changed, 111 insertions(+), 19 deletions(-) diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs index a3f0c76dc..b38e8300f 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs @@ -1,7 +1,6 @@ using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Search; -using ErsatzTV.FFmpeg.Pipeline; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; @@ -87,7 +86,7 @@ public class CreateFFmpegProfileHandler : VideoBitrate = request.VideoBitrate, VideoBufferSize = request.VideoBufferSize, TonemapAlgorithm = request.TonemapAlgorithm, - AudioFormat = request.AudioFormat, + AudioFormat = request.NormalizeAudio ? request.AudioFormat : FFmpegProfileAudioFormat.Copy, AudioBitrate = request.AudioBitrate, AudioBufferSize = request.AudioBufferSize, diff --git a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs index f24a60663..4966d6155 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs @@ -73,7 +73,7 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory dbContextFa p.VideoBitrate = update.VideoBitrate; p.VideoBufferSize = update.VideoBufferSize; p.TonemapAlgorithm = update.TonemapAlgorithm; - p.AudioFormat = update.AudioFormat; + p.AudioFormat = update.NormalizeAudio ? update.AudioFormat : FFmpegProfileAudioFormat.Copy; p.AudioBitrate = update.AudioBitrate; p.AudioBufferSize = update.AudioBufferSize; diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index d3dca3f68..add48f315 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -519,8 +519,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService playbackSettings.VideoTrackTimeScale, playbackSettings.Deinterlace); - // only use graphics engine when we have elements - if (graphicsElementContexts.Count > 0 || graphicsElements.Count > 0) + // only use graphics engine when we have elements, and are normalizing video + if (videoFormat != VideoFormat.Copy && (graphicsElementContexts.Count > 0 || graphicsElements.Count > 0)) { FrameSize targetSize = await desiredState.CroppedSize.IfNoneAsync(desiredState.ScaledSize); diff --git a/ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs b/ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs index 396a8b5ed..d8d624db6 100644 --- a/ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs +++ b/ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs @@ -1,4 +1,5 @@ using ErsatzTV.FFmpeg.Environment; +using ErsatzTV.FFmpeg.Format; namespace ErsatzTV.FFmpeg.OutputFormat; @@ -56,18 +57,27 @@ public class OutputFormatHls : IPipelineStep ? (int)Math.Round(frameRate.ParsedFrameRate) : (int)Math.Round(frameRate.ParsedFrameRate * SegmentSeconds); - List result = + List result = []; + + if (_desiredState.VideoFormat != VideoFormat.Copy) + { + result.AddRange( + [ + "-g", $"{gop}", + "-keyint_min", $"{(int)Math.Round(frameRate.ParsedFrameRate * SegmentSeconds)}", + "-force_key_frames", $"expr:gte(t,n_forced*{SegmentSeconds})" + ]); + } + + result.AddRange( [ - "-g", $"{gop}", - "-keyint_min", $"{(int)Math.Round(frameRate.ParsedFrameRate * SegmentSeconds)}", - "-force_key_frames", $"expr:gte(t,n_forced*{SegmentSeconds})", "-f", "hls", "-hls_time", $"{SegmentSeconds}", "-hls_list_size", "0", "-segment_list_flags", "+live", "-hls_segment_filename", _segmentTemplate - ]; + ]); var independentSegments = "+independent_segments"; diff --git a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs index 2a74c1abe..7672023df 100644 --- a/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs +++ b/ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs @@ -214,10 +214,14 @@ public abstract class PipelineBuilderBase : IPipelineBuilder new LoglevelErrorOption(), new StandardFormatFlags(), new NoDemuxDecodeDelayOutputOption(), - outputOption, - new ClosedGopOutputOption() + outputOption }; + if (desiredState.VideoFormat != VideoFormat.Copy) + { + pipelineSteps.Add(new ClosedGopOutputOption()); + } + if (desiredState.VideoFormat != VideoFormat.Copy && !desiredState.AllowBFrames) { pipelineSteps.Add(new NoBFramesOutputOption()); @@ -512,6 +516,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder private void SetAudioLoudness(AudioInputFile audioInputFile) { + if (audioInputFile.DesiredState.AudioFormat == AudioFormat.Copy) + { + return; + } + if (audioInputFile.DesiredState.NormalizeLoudnessFilter is not AudioFilter.None) { var filter = new NormalizeLoudnessFilter( @@ -525,6 +534,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder private static void SetAudioSampleRate(AudioInputFile audioInputFile, List pipelineSteps) { + if (audioInputFile.DesiredState.AudioFormat == AudioFormat.Copy) + { + return; + } + foreach (int desiredSampleRate in audioInputFile.DesiredState.AudioSampleRate) { pipelineSteps.Add(new AudioSampleRateOutputOption(desiredSampleRate)); @@ -533,6 +547,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder private static void SetAudioBufferSize(AudioInputFile audioInputFile, List pipelineSteps) { + if (audioInputFile.DesiredState.AudioFormat == AudioFormat.Copy) + { + return; + } + foreach (int desiredBufferSize in audioInputFile.DesiredState.AudioBufferSize) { pipelineSteps.Add(new AudioBufferSizeOutputOption(desiredBufferSize)); @@ -541,6 +560,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder private static void SetAudioBitrate(AudioInputFile audioInputFile, List pipelineSteps) { + if (audioInputFile.DesiredState.AudioFormat == AudioFormat.Copy) + { + return; + } + foreach (int desiredBitrate in audioInputFile.DesiredState.AudioBitrate) { pipelineSteps.Add(new AudioBitrateOutputOption(desiredBitrate)); @@ -549,6 +573,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder private static void SetAudioChannels(AudioInputFile audioInputFile, List pipelineSteps) { + if (audioInputFile.DesiredState.AudioFormat == AudioFormat.Copy) + { + return; + } + foreach (AudioStream audioStream in audioInputFile.AudioStreams.HeadOrNone()) { foreach (int desiredAudioChannels in audioInputFile.DesiredState.AudioChannels) @@ -878,6 +907,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder FrameState desiredState, List pipelineSteps) { + if (desiredState.VideoFormat == VideoFormat.Copy) + { + return; + } + // -sc_threshold 0 is unsupported with mpeg2video if (videoStream.Codec == VideoFormat.Mpeg2Video || desiredState.VideoFormat == VideoFormat.Mpeg2Video || ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox) diff --git a/ErsatzTV/Pages/Channels.razor b/ErsatzTV/Pages/Channels.razor index d9d8af814..c19956229 100644 --- a/ErsatzTV/Pages/Channels.razor +++ b/ErsatzTV/Pages/Channels.razor @@ -98,11 +98,25 @@
@if (_channelsThatCanPreview.Contains(context.Id)) { - - - - + Option maybeProfile = Optional(_ffmpegProfiles.Find(p => p.Id == context.FFmpegProfileId)); + bool notNormalizing = maybeProfile.Any(p => !p.NormalizeVideo || !p.NormalizeAudio); + if (notNormalizing) + { + + + + + } + else + { + + + + + } } else if (CanPreviewChannel(context) && _ffmpegProfilesThatCanPreview.ContainsKey(context.FFmpegProfileId) && !_ffmpegProfilesThatCanPreview[context.FFmpegProfileId]) { @@ -272,6 +286,12 @@ //Console.WriteLine($"Checking video format {videoCodec} and audio format {audioCodec}"); + // good luck + if (profile.VideoFormat == FFmpegProfileVideoFormat.Copy || profile.AudioFormat == FFmpegProfileAudioFormat.Copy) + { + return true; + } + if (string.IsNullOrWhiteSpace(videoCodec) || string.IsNullOrWhiteSpace(audioCodec)) { return false; diff --git a/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs b/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs index 060d5ae91..44b7e11c4 100644 --- a/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs +++ b/ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs @@ -49,7 +49,22 @@ public class FFmpegProfileEditViewModel public int AudioBitrate { get; set; } public int AudioBufferSize { get; set; } public int AudioChannels { get; set; } - public FFmpegProfileAudioFormat AudioFormat { get; set; } + + public FFmpegProfileAudioFormat AudioFormat + { + get + { + if (field == FFmpegProfileAudioFormat.Copy && NormalizeAudio) + { + return FFmpegProfileAudioFormat.Aac; + } + + return field; + } + + set; + } + public int AudioSampleRate { get; set; } public NormalizeLoudnessMode NormalizeLoudnessMode @@ -105,7 +120,21 @@ public class FFmpegProfileEditViewModel public int? QsvExtraHardwareFrames { get; set; } public int VideoBitrate { get; set; } public int VideoBufferSize { get; set; } - public FFmpegProfileVideoFormat VideoFormat { get; set; } + + public FFmpegProfileVideoFormat VideoFormat + { + get + { + if (field == FFmpegProfileVideoFormat.Copy && NormalizeVideo) + { + return FFmpegProfileVideoFormat.H264; + } + + return field; + } + + set; + } public string VideoProfile {