Browse Source

add normalization options (#2802)

* add new fields to database

* update editor

* audio and video normalization settings appear to work

* implement optional color normalization

* fix transcoding tests

* update changelog
pull/2803/head
Jason Dove 6 months ago committed by GitHub
parent
commit
e10b28bc0b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs
  3. 99
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
  4. 3
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs
  5. 13
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
  6. 3
      ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs
  7. 3
      ErsatzTV.Application/FFmpegProfiles/Mapper.cs
  8. 8
      ErsatzTV.Core/Domain/FFmpegProfile.cs
  9. 6
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  10. 1
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs
  11. 5
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  12. 8
      ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
  13. 1
      ErsatzTV.FFmpeg/FrameState.cs
  14. 20
      ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs
  15. 6
      ErsatzTV.FFmpeg/Pipeline/AmfPipelineBuilder.cs
  16. 8
      ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs
  17. 38
      ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs
  18. 8
      ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs
  19. 6
      ErsatzTV.FFmpeg/Pipeline/RkmppPipelineBuilder.cs
  20. 8
      ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs
  21. 6
      ErsatzTV.FFmpeg/Pipeline/V4l2m2mPipelineBuilder.cs
  22. 14
      ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs
  23. 6
      ErsatzTV.FFmpeg/Pipeline/VideoToolboxPipelineBuilder.cs
  24. 7039
      ErsatzTV.Infrastructure.MySql/Migrations/20260127040133_Add_FFmpegProfileNormalizationOptions.Designer.cs
  25. 51
      ErsatzTV.Infrastructure.MySql/Migrations/20260127040133_Add_FFmpegProfileNormalizationOptions.cs
  26. 15
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  27. 6866
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260127040052_Add_FFmpegProfileNormalizationOptions.Designer.cs
  28. 51
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260127040052_Add_FFmpegProfileNormalizationOptions.cs
  29. 15
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  30. 9
      ErsatzTV.Infrastructure/Data/Configurations/FFmpegProfileConfiguration.cs
  31. 31
      ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs
  32. 30
      ErsatzTV/Pages/Channels.razor
  33. 454
      ErsatzTV/Pages/FFmpegEditor.razor
  34. 45
      ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

5
CHANGELOG.md

@ -24,6 +24,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -24,6 +24,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add `Marathon Group By` option `Director`
- This groups the *first* director on Movies, Episodes, Music Videos and Other Videos
- This is supported in classic schedules and sequential schedules
- Add FFmpeg Profile options:
- `Normalize Audio` (default: true) - normalizes audio streams, or stream copies when disabled
- `Normalize Video` (default: true) - normalizes video streams, or stream copies when disabled
- `Normalize Colors` (default: true) - normalizes color parameters when enabled
- Disabling any of these options may have a significant performance benefit *at the expense of stream stability*
### Changed
- Disable automatic artwork database cleanup

3
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs

@ -7,6 +7,8 @@ namespace ErsatzTV.Application.FFmpegProfiles; @@ -7,6 +7,8 @@ namespace ErsatzTV.Application.FFmpegProfiles;
public record CreateFFmpegProfile(
string Name,
int ThreadCount,
bool NormalizeAudio,
bool NormalizeVideo,
HardwareAccelerationKind HardwareAcceleration,
string VaapiDisplay,
VaapiDriver VaapiDriver,
@ -31,4 +33,5 @@ public record CreateFFmpegProfile( @@ -31,4 +33,5 @@ public record CreateFFmpegProfile(
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,
bool NormalizeColors,
bool DeinterlaceVideo) : IRequest<Either<BaseError, CreateFFmpegProfileResult>>;

99
ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs

@ -1,7 +1,6 @@ @@ -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;
@ -45,51 +44,63 @@ public class CreateFFmpegProfileHandler : @@ -45,51 +44,63 @@ public class CreateFFmpegProfileHandler :
CancellationToken cancellationToken) =>
(ValidateName(request), ValidateThreadCount(request),
await ResolutionMustExist(dbContext, request, cancellationToken))
.Apply((name, threadCount, resolutionId) => new FFmpegProfile
.Apply((name, threadCount, resolutionId) =>
{
Name = name,
ThreadCount = threadCount,
HardwareAcceleration = request.HardwareAcceleration,
VaapiDriver = request.VaapiDriver,
VaapiDevice = request.VaapiDevice,
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
ResolutionId = resolutionId,
ScalingBehavior = request.ScalingBehavior,
// only allow customization with VAAPI accel
PadMode = request.HardwareAcceleration switch
var hwAccel = request.NormalizeVideo
? request.HardwareAcceleration
: HardwareAccelerationKind.None;
return new FFmpegProfile
{
HardwareAccelerationKind.None => FilterMode.Software,
HardwareAccelerationKind.Vaapi => request.PadMode,
_ => FilterMode.HardwareIfPossible
},
VideoFormat = request.VideoFormat,
VideoProfile = request.VideoProfile,
VideoPreset = request.VideoPreset,
AllowBFrames = request.AllowBFrames,
// mpeg2video only supports 8-bit content
BitDepth = request.VideoFormat is FFmpegProfileVideoFormat.Mpeg2Video
? FFmpegProfileBitDepth.EightBit
: request.BitDepth,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
TonemapAlgorithm = request.TonemapAlgorithm,
AudioFormat = request.AudioFormat,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
NormalizeLoudnessMode = request.NormalizeLoudnessMode,
TargetLoudness = request.NormalizeLoudnessMode is NormalizeLoudnessMode.LoudNorm
? request.TargetLoudness
: null,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeFramerate = request.NormalizeFramerate,
DeinterlaceVideo = request.DeinterlaceVideo
Name = name,
ThreadCount = threadCount,
NormalizeAudio = request.NormalizeAudio,
NormalizeVideo = request.NormalizeVideo,
HardwareAcceleration = hwAccel,
VaapiDriver = request.VaapiDriver,
VaapiDevice = request.VaapiDevice,
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
ResolutionId = resolutionId,
ScalingBehavior = request.ScalingBehavior,
// only allow customization with VAAPI accel
PadMode = hwAccel switch
{
HardwareAccelerationKind.None => FilterMode.Software,
HardwareAccelerationKind.Vaapi => request.PadMode,
_ => FilterMode.HardwareIfPossible
},
VideoFormat = request.NormalizeVideo ? request.VideoFormat : FFmpegProfileVideoFormat.Copy,
VideoProfile = request.VideoProfile,
VideoPreset = request.VideoPreset,
AllowBFrames = request.AllowBFrames,
// mpeg2video only supports 8-bit content
BitDepth = request.VideoFormat is FFmpegProfileVideoFormat.Mpeg2Video
? FFmpegProfileBitDepth.EightBit
: request.BitDepth,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
TonemapAlgorithm = request.TonemapAlgorithm,
AudioFormat = request.NormalizeAudio ? request.AudioFormat : FFmpegProfileAudioFormat.Copy,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
NormalizeLoudnessMode = request.NormalizeLoudnessMode,
TargetLoudness = request.NormalizeLoudnessMode is NormalizeLoudnessMode.LoudNorm
? request.TargetLoudness
: null,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeFramerate = request.NormalizeFramerate,
NormalizeColors = request.NormalizeColors,
DeinterlaceVideo = request.DeinterlaceVideo
};
});
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>

3
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs

@ -8,6 +8,8 @@ public record UpdateFFmpegProfile( @@ -8,6 +8,8 @@ public record UpdateFFmpegProfile(
int FFmpegProfileId,
string Name,
int ThreadCount,
bool NormalizeAudio,
bool NormalizeVideo,
HardwareAccelerationKind HardwareAcceleration,
string VaapiDisplay,
VaapiDriver VaapiDriver,
@ -32,4 +34,5 @@ public record UpdateFFmpegProfile( @@ -32,4 +34,5 @@ public record UpdateFFmpegProfile(
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,
bool NormalizeColors,
bool DeinterlaceVideo) : IRequest<Either<BaseError, UpdateFFmpegProfileResult>>;

13
ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs

@ -27,9 +27,15 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa @@ -27,9 +27,15 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa
UpdateFFmpegProfile update,
CancellationToken cancellationToken)
{
var hwAccel = update.NormalizeVideo
? update.HardwareAcceleration
: HardwareAccelerationKind.None;
p.Name = update.Name;
p.ThreadCount = update.ThreadCount;
p.HardwareAcceleration = update.HardwareAcceleration;
p.NormalizeAudio = update.NormalizeAudio;
p.NormalizeVideo = update.NormalizeVideo;
p.HardwareAcceleration = hwAccel;
p.VaapiDisplay = update.VaapiDisplay;
p.VaapiDriver = update.VaapiDriver;
p.VaapiDevice = update.VaapiDevice;
@ -37,7 +43,7 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa @@ -37,7 +43,7 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa
p.ResolutionId = update.ResolutionId;
p.ScalingBehavior = update.ScalingBehavior;
p.PadMode = update.PadMode;
p.VideoFormat = update.VideoFormat;
p.VideoFormat = update.NormalizeVideo ? update.VideoFormat : FFmpegProfileVideoFormat.Copy;
p.VideoProfile = update.VideoProfile;
p.VideoPreset = update.VideoPreset;
p.AllowBFrames = update.AllowBFrames;
@ -67,7 +73,7 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa @@ -67,7 +73,7 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> 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;
@ -79,6 +85,7 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa @@ -79,6 +85,7 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa
p.AudioChannels = update.AudioChannels;
p.AudioSampleRate = update.AudioSampleRate;
p.NormalizeFramerate = update.NormalizeFramerate;
p.NormalizeColors = update.NormalizeColors;
p.DeinterlaceVideo = update.DeinterlaceVideo;
// don't save invalid preset

3
ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs

@ -8,6 +8,8 @@ public record FFmpegProfileViewModel( @@ -8,6 +8,8 @@ public record FFmpegProfileViewModel(
int Id,
string Name,
int ThreadCount,
bool NormalizeAudio,
bool NormalizeVideo,
HardwareAccelerationKind HardwareAcceleration,
string VaapiDisplay,
VaapiDriver VaapiDriver,
@ -32,4 +34,5 @@ public record FFmpegProfileViewModel( @@ -32,4 +34,5 @@ public record FFmpegProfileViewModel(
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,
bool NormalizeColors,
bool DeinterlaceVideo);

3
ErsatzTV.Application/FFmpegProfiles/Mapper.cs

@ -10,6 +10,8 @@ internal static class Mapper @@ -10,6 +10,8 @@ internal static class Mapper
profile.Id,
profile.Name,
profile.ThreadCount,
profile.NormalizeAudio,
profile.NormalizeVideo,
profile.HardwareAcceleration,
profile.VaapiDisplay ?? "drm",
profile.VaapiDriver,
@ -34,6 +36,7 @@ internal static class Mapper @@ -34,6 +36,7 @@ internal static class Mapper
profile.AudioChannels,
profile.AudioSampleRate,
profile.NormalizeFramerate,
profile.NormalizeColors,
profile.DeinterlaceVideo == true);
internal static FFmpegProfileResponseModel ProjectToResponseModel(FFmpegProfile ffmpegProfile) =>

8
ErsatzTV.Core/Domain/FFmpegProfile.cs

@ -7,6 +7,8 @@ public record FFmpegProfile @@ -7,6 +7,8 @@ public record FFmpegProfile
public int Id { get; set; }
public string Name { get; set; }
public int ThreadCount { get; set; }
public bool NormalizeAudio { get; set; }
public bool NormalizeVideo { get; set; }
public HardwareAccelerationKind HardwareAcceleration { get; set; }
public string VaapiDisplay { get; set; }
public VaapiDriver VaapiDriver { get; set; }
@ -32,6 +34,7 @@ public record FFmpegProfile @@ -32,6 +34,7 @@ public record FFmpegProfile
public int AudioChannels { get; set; }
public int AudioSampleRate { get; set; }
public bool NormalizeFramerate { get; set; }
public bool NormalizeColors { get; set; }
public bool? DeinterlaceVideo { get; set; }
public static FFmpegProfile New(string name, Resolution resolution) =>
@ -59,6 +62,9 @@ public record FFmpegProfile @@ -59,6 +62,9 @@ public record FFmpegProfile
DeinterlaceVideo = true,
NormalizeFramerate = false,
HardwareAcceleration = HardwareAccelerationKind.None,
QsvExtraHardwareFrames = 64
QsvExtraHardwareFrames = 64,
NormalizeAudio = true,
NormalizeVideo = true,
NormalizeColors = true
};
}

6
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -517,10 +517,11 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -517,10 +517,11 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,
playbackSettings.VideoTrackTimeScale,
playbackSettings.NormalizeColors,
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);
@ -722,6 +723,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -722,6 +723,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,
playbackSettings.VideoTrackTimeScale,
playbackSettings.NormalizeColors,
playbackSettings.Deinterlace);
OutputFormatKind outputFormat = OutputFormatKind.MpegTs;

1
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs

@ -31,4 +31,5 @@ public class FFmpegPlaybackSettings @@ -31,4 +31,5 @@ public class FFmpegPlaybackSettings
public NormalizeLoudnessMode NormalizeLoudnessMode { get; set; }
public Option<double> TargetLoudness { get; set; }
public Option<FrameRate> FrameRate { get; set; }
public bool NormalizeColors { get; set; }
}

5
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -118,6 +118,11 @@ public static class FFmpegPlaybackSettingsCalculator @@ -118,6 +118,11 @@ public static class FFmpegPlaybackSettingsCalculator
result.FrameRate = targetFramerate;
}
if (ffmpegProfile.NormalizeColors)
{
result.NormalizeColors = true;
}
result.VideoTrackTimeScale = 90000;
foreach (MediaStream stream in videoStream.Where(s => !s.AttachedPic))

8
ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs

@ -73,6 +73,7 @@ public class PipelineBuilderBaseTests @@ -73,6 +73,7 @@ public class PipelineBuilderBaseTests
2000,
4000,
90_000,
false,
false);
var ffmpegState = new FFmpegState(
@ -175,6 +176,7 @@ public class PipelineBuilderBaseTests @@ -175,6 +176,7 @@ public class PipelineBuilderBaseTests
2000,
4000,
90_000,
false,
false);
var ffmpegState = new FFmpegState(
@ -335,6 +337,7 @@ public class PipelineBuilderBaseTests @@ -335,6 +337,7 @@ public class PipelineBuilderBaseTests
2000,
4000,
90_000,
false,
false);
var ffmpegState = new FFmpegState(
@ -387,7 +390,7 @@ public class PipelineBuilderBaseTests @@ -387,7 +390,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 -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");
"-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 -c:v copy -c:a copy -f mp4 pipe:1");
}
[Test]
@ -430,6 +433,7 @@ public class PipelineBuilderBaseTests @@ -430,6 +433,7 @@ public class PipelineBuilderBaseTests
2000,
4000,
90_000,
false,
false);
var ffmpegState = new FFmpegState(
@ -480,7 +484,7 @@ public class PipelineBuilderBaseTests @@ -480,7 +484,7 @@ public class PipelineBuilderBaseTests
string command = PrintCommand(videoInputFile, audioInputFile, None, 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 -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");
"-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 -c:v copy -c:a copy -f mp4 pipe:1");
}
[Test]

1
ErsatzTV.FFmpeg/FrameState.cs

@ -19,6 +19,7 @@ public record FrameState( @@ -19,6 +19,7 @@ public record FrameState(
Option<int> VideoBitrate,
Option<int> VideoBufferSize,
Option<int> VideoTrackTimeScale,
bool ColorsAreBt709,
bool Deinterlaced,
FrameDataLocation FrameDataLocation = FrameDataLocation.Unknown)
{

20
ErsatzTV.FFmpeg/OutputFormat/OutputFormatHls.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using ErsatzTV.FFmpeg.Environment;
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.OutputFormat;
@ -56,18 +57,27 @@ public class OutputFormatHls : IPipelineStep @@ -56,18 +57,27 @@ public class OutputFormatHls : IPipelineStep
? (int)Math.Round(frameRate.ParsedFrameRate)
: (int)Math.Round(frameRate.ParsedFrameRate * SegmentSeconds);
List<string> result =
List<string> 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";

6
ErsatzTV.FFmpeg/Pipeline/AmfPipelineBuilder.cs

@ -94,15 +94,15 @@ public class AmfPipelineBuilder : SoftwarePipelineBuilder @@ -94,15 +94,15 @@ public class AmfPipelineBuilder : SoftwarePipelineBuilder
protected override List<IPipelineFilterStep> SetPixelFormat(
VideoStream videoStream,
Option<IPixelFormat> desiredPixelFormat,
FrameState desiredState,
FrameState currentState,
ICollection<IPipelineStep> pipelineSteps)
{
var result = new List<IPipelineFilterStep>();
foreach (IPixelFormat pixelFormat in desiredPixelFormat)
foreach (IPixelFormat pixelFormat in desiredState.PixelFormat)
{
if (!videoStream.ColorParams.IsBt709)
if (desiredState.ColorsAreBt709 && !videoStream.ColorParams.IsBt709)
{
// _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat);

8
ErsatzTV.FFmpeg/Pipeline/NvidiaPipelineBuilder.cs

@ -347,7 +347,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder @@ -347,7 +347,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
List<IPipelineFilterStep> pixelFormatFilterSteps = SetPixelFormat(
videoStream,
desiredState.PixelFormat,
desiredState,
ffmpegState,
currentState,
context,
@ -392,7 +392,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder @@ -392,7 +392,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
private List<IPipelineFilterStep> SetPixelFormat(
VideoStream videoStream,
Option<IPixelFormat> desiredPixelFormat,
FrameState desiredState,
FFmpegState ffmpegState,
FrameState currentState,
PipelineContext context,
@ -400,7 +400,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder @@ -400,7 +400,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
{
var result = new List<IPipelineFilterStep>();
foreach (IPixelFormat pixelFormat in desiredPixelFormat)
foreach (IPixelFormat pixelFormat in desiredState.PixelFormat)
{
IPixelFormat format = pixelFormat;
@ -419,7 +419,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder @@ -419,7 +419,7 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
videoStream.ResetColorParams(ColorParams.Unknown);
}
if (!videoStream.ColorParams.IsBt709)
if (desiredState.ColorsAreBt709 && !videoStream.ColorParams.IsBt709)
{
// _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(currentState, videoStream, format);

38
ErsatzTV.FFmpeg/Pipeline/PipelineBuilderBase.cs

@ -214,10 +214,14 @@ public abstract class PipelineBuilderBase : IPipelineBuilder @@ -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 @@ -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 @@ -525,6 +534,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
private static void SetAudioSampleRate(AudioInputFile audioInputFile, List<IPipelineStep> 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 @@ -533,6 +547,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
private static void SetAudioBufferSize(AudioInputFile audioInputFile, List<IPipelineStep> 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 @@ -541,6 +560,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
private static void SetAudioBitrate(AudioInputFile audioInputFile, List<IPipelineStep> 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 @@ -549,6 +573,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
private static void SetAudioChannels(AudioInputFile audioInputFile, List<IPipelineStep> 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 @@ -878,6 +907,11 @@ public abstract class PipelineBuilderBase : IPipelineBuilder
FrameState desiredState,
List<IPipelineStep> 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)

8
ErsatzTV.FFmpeg/Pipeline/QsvPipelineBuilder.cs

@ -259,7 +259,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder @@ -259,7 +259,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
List<IPipelineFilterStep> pixelFormatFilterSteps = SetPixelFormat(
videoInputFile,
videoStream,
desiredState.PixelFormat,
desiredState,
ffmpegState,
currentState,
context,
@ -279,7 +279,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder @@ -279,7 +279,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
private List<IPipelineFilterStep> SetPixelFormat(
VideoInputFile videoInputFile,
VideoStream videoStream,
Option<IPixelFormat> desiredPixelFormat,
FrameState desiredState,
FFmpegState ffmpegState,
FrameState currentState,
PipelineContext context,
@ -287,7 +287,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder @@ -287,7 +287,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
{
var result = new List<IPipelineFilterStep>();
foreach (IPixelFormat pixelFormat in desiredPixelFormat)
foreach (IPixelFormat pixelFormat in desiredState.PixelFormat)
{
IPixelFormat format = pixelFormat;
@ -352,7 +352,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder @@ -352,7 +352,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
}
}
if (!videoStream.ColorParams.IsBt709 || usesVppQsv)
if (desiredState.ColorsAreBt709 && (!videoStream.ColorParams.IsBt709 || usesVppQsv))
{
// _logger.LogDebug("Adding colorspace filter");

6
ErsatzTV.FFmpeg/Pipeline/RkmppPipelineBuilder.cs

@ -126,15 +126,15 @@ public class RkmppPipelineBuilder : SoftwarePipelineBuilder @@ -126,15 +126,15 @@ public class RkmppPipelineBuilder : SoftwarePipelineBuilder
protected override List<IPipelineFilterStep> SetPixelFormat(
VideoStream videoStream,
Option<IPixelFormat> desiredPixelFormat,
FrameState desiredState,
FrameState currentState,
ICollection<IPipelineStep> pipelineSteps)
{
var result = new List<IPipelineFilterStep>();
foreach (IPixelFormat pixelFormat in desiredPixelFormat)
foreach (IPixelFormat pixelFormat in desiredState.PixelFormat)
{
if (!videoStream.ColorParams.IsBt709)
if (desiredState.ColorsAreBt709 && !videoStream.ColorParams.IsBt709)
{
// _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat);

8
ErsatzTV.FFmpeg/Pipeline/SoftwarePipelineBuilder.cs

@ -152,7 +152,7 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase @@ -152,7 +152,7 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase
List<IPipelineFilterStep> pixelFormatFilterSteps = SetPixelFormat(
videoStream,
desiredState.PixelFormat,
desiredState,
currentState,
pipelineSteps);
@ -169,15 +169,15 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase @@ -169,15 +169,15 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase
protected virtual List<IPipelineFilterStep> SetPixelFormat(
VideoStream videoStream,
Option<IPixelFormat> desiredPixelFormat,
FrameState desiredState,
FrameState currentState,
ICollection<IPipelineStep> pipelineSteps)
{
var result = new List<IPipelineFilterStep>();
foreach (IPixelFormat pixelFormat in desiredPixelFormat)
foreach (IPixelFormat pixelFormat in desiredState.PixelFormat)
{
if (!videoStream.ColorParams.IsBt709)
if (desiredState.ColorsAreBt709 && !videoStream.ColorParams.IsBt709)
{
// _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat);

6
ErsatzTV.FFmpeg/Pipeline/V4l2m2mPipelineBuilder.cs

@ -124,15 +124,15 @@ public class V4l2m2mPipelineBuilder : SoftwarePipelineBuilder @@ -124,15 +124,15 @@ public class V4l2m2mPipelineBuilder : SoftwarePipelineBuilder
protected override List<IPipelineFilterStep> SetPixelFormat(
VideoStream videoStream,
Option<IPixelFormat> desiredPixelFormat,
FrameState desiredState,
FrameState currentState,
ICollection<IPipelineStep> pipelineSteps)
{
var result = new List<IPipelineFilterStep>();
foreach (IPixelFormat pixelFormat in desiredPixelFormat)
foreach (IPixelFormat pixelFormat in desiredState.PixelFormat)
{
if (!videoStream.ColorParams.IsBt709)
if (desiredState.ColorsAreBt709 && !videoStream.ColorParams.IsBt709)
{
// _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat);

14
ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs

@ -286,7 +286,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder @@ -286,7 +286,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
List<IPipelineFilterStep> pixelFormatFilterSteps = SetPixelFormat(
videoStream,
desiredState.PixelFormat,
desiredState,
ffmpegState,
currentState,
pipelineSteps);
@ -313,14 +313,14 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder @@ -313,14 +313,14 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
private List<IPipelineFilterStep> SetPixelFormat(
VideoStream videoStream,
Option<IPixelFormat> desiredPixelFormat,
FrameState desiredState,
FFmpegState ffmpegState,
FrameState currentState,
ICollection<IPipelineStep> pipelineSteps)
{
var result = new List<IPipelineFilterStep>();
foreach (IPixelFormat pixelFormat in desiredPixelFormat)
foreach (IPixelFormat pixelFormat in desiredState.PixelFormat)
{
IPixelFormat format = pixelFormat;
@ -332,7 +332,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder @@ -332,7 +332,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
}
}
if (!videoStream.ColorParams.IsBt709)
if (desiredState.ColorsAreBt709 && !videoStream.ColorParams.IsBt709)
{
// _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(
@ -352,12 +352,12 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder @@ -352,12 +352,12 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
_logger.LogDebug(
"FrameDataLocation == FrameDataLocation.Hardware, {CurrentPixelFormat} bit => {DesiredPixelFormat}",
currentState.PixelFormat,
desiredPixelFormat);
desiredState.PixelFormat);
// don't try to download from 8-bit to 10-bit, or 10-bit to 8-bit
HardwareDownloadFilter hardwareDownload =
currentState.BitDepth == 8 && desiredPixelFormat.Map(pf => pf.BitDepth).IfNone(8) == 10 ||
currentState.BitDepth == 10 && desiredPixelFormat.Map(pf => pf.BitDepth).IfNone(10) == 8
currentState.BitDepth == 8 && desiredState.PixelFormat.Map(pf => pf.BitDepth).IfNone(8) == 10 ||
currentState.BitDepth == 10 && desiredState.PixelFormat.Map(pf => pf.BitDepth).IfNone(10) == 8
? new HardwareDownloadFilter(currentState)
: new HardwareDownloadFilter(currentState with { PixelFormat = Some(format) });

6
ErsatzTV.FFmpeg/Pipeline/VideoToolboxPipelineBuilder.cs

@ -123,15 +123,15 @@ public class VideoToolboxPipelineBuilder : SoftwarePipelineBuilder @@ -123,15 +123,15 @@ public class VideoToolboxPipelineBuilder : SoftwarePipelineBuilder
protected override List<IPipelineFilterStep> SetPixelFormat(
VideoStream videoStream,
Option<IPixelFormat> desiredPixelFormat,
FrameState desiredState,
FrameState currentState,
ICollection<IPipelineStep> pipelineSteps)
{
var result = new List<IPipelineFilterStep>();
foreach (IPixelFormat pixelFormat in desiredPixelFormat)
foreach (IPixelFormat pixelFormat in desiredState.PixelFormat)
{
if (!videoStream.ColorParams.IsBt709)
if (desiredState.ColorsAreBt709 && !videoStream.ColorParams.IsBt709)
{
// _logger.LogDebug("Adding colorspace filter");
var colorspace = new ColorspaceFilter(currentState, videoStream, pixelFormat);

7039
ErsatzTV.Infrastructure.MySql/Migrations/20260127040133_Add_FFmpegProfileNormalizationOptions.Designer.cs generated

File diff suppressed because it is too large Load Diff

51
ErsatzTV.Infrastructure.MySql/Migrations/20260127040133_Add_FFmpegProfileNormalizationOptions.cs

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_FFmpegProfileNormalizationOptions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "NormalizeAudio",
table: "FFmpegProfile",
type: "tinyint(1)",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<bool>(
name: "NormalizeColors",
table: "FFmpegProfile",
type: "tinyint(1)",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<bool>(
name: "NormalizeVideo",
table: "FFmpegProfile",
type: "tinyint(1)",
nullable: false,
defaultValue: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "NormalizeAudio",
table: "FFmpegProfile");
migrationBuilder.DropColumn(
name: "NormalizeColors",
table: "FFmpegProfile");
migrationBuilder.DropColumn(
name: "NormalizeVideo",
table: "FFmpegProfile");
}
}
}

15
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -757,6 +757,16 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -757,6 +757,16 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<string>("Name")
.HasColumnType("longtext");
b.Property<bool>("NormalizeAudio")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true);
b.Property<bool>("NormalizeColors")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true);
b.Property<bool>("NormalizeFramerate")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
@ -765,6 +775,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -765,6 +775,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("NormalizeLoudnessMode")
.HasColumnType("int");
b.Property<bool>("NormalizeVideo")
.ValueGeneratedOnAdd()
.HasColumnType("tinyint(1)")
.HasDefaultValue(true);
b.Property<int>("PadMode")
.HasColumnType("int");

6866
ErsatzTV.Infrastructure.Sqlite/Migrations/20260127040052_Add_FFmpegProfileNormalizationOptions.Designer.cs generated

File diff suppressed because it is too large Load Diff

51
ErsatzTV.Infrastructure.Sqlite/Migrations/20260127040052_Add_FFmpegProfileNormalizationOptions.cs

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_FFmpegProfileNormalizationOptions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "NormalizeAudio",
table: "FFmpegProfile",
type: "INTEGER",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<bool>(
name: "NormalizeColors",
table: "FFmpegProfile",
type: "INTEGER",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<bool>(
name: "NormalizeVideo",
table: "FFmpegProfile",
type: "INTEGER",
nullable: false,
defaultValue: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "NormalizeAudio",
table: "FFmpegProfile");
migrationBuilder.DropColumn(
name: "NormalizeColors",
table: "FFmpegProfile");
migrationBuilder.DropColumn(
name: "NormalizeVideo",
table: "FFmpegProfile");
}
}
}

15
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -726,6 +726,16 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -726,6 +726,16 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<bool>("NormalizeAudio")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<bool>("NormalizeColors")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<bool>("NormalizeFramerate")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@ -734,6 +744,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -734,6 +744,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("NormalizeLoudnessMode")
.HasColumnType("INTEGER");
b.Property<bool>("NormalizeVideo")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<int>("PadMode")
.HasColumnType("INTEGER");

9
ErsatzTV.Infrastructure/Data/Configurations/FFmpegProfileConfiguration.cs

@ -15,5 +15,14 @@ public class FFmpegProfileConfiguration : IEntityTypeConfiguration<FFmpegProfile @@ -15,5 +15,14 @@ public class FFmpegProfileConfiguration : IEntityTypeConfiguration<FFmpegProfile
builder.Property(p => p.DeinterlaceVideo)
.HasDefaultValue(true);
builder.Property(p => p.NormalizeAudio)
.HasDefaultValue(true);
builder.Property(p => p.NormalizeVideo)
.HasDefaultValue(true);
builder.Property(p => p.NormalizeColors)
.HasDefaultValue(true);
}
}

31
ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs

@ -203,9 +203,9 @@ public class TranscodingTests @@ -203,9 +203,9 @@ public class TranscodingTests
public static HardwareAccelerationKind[] TestAccelerations =
[
HardwareAccelerationKind.None,
//HardwareAccelerationKind.Nvenc
HardwareAccelerationKind.Vaapi,
HardwareAccelerationKind.Qsv,
HardwareAccelerationKind.Nvenc,
//HardwareAccelerationKind.Vaapi,
//HardwareAccelerationKind.Qsv,
// HardwareAccelerationKind.VideoToolbox,
// HardwareAccelerationKind.Amf
];
@ -217,6 +217,12 @@ public class TranscodingTests @@ -217,6 +217,12 @@ public class TranscodingTests
//StreamingMode.HttpLiveStreamingSegmenterV2
];
public static bool[] NormalizeColors =
[
false,
true
];
public static string[] FilesToTest => [string.Empty];
}
@ -234,7 +240,9 @@ public class TranscodingTests @@ -234,7 +240,9 @@ public class TranscodingTests
[ValueSource(typeof(TestData), nameof(TestData.TestAccelerations))]
HardwareAccelerationKind profileAcceleration,
[ValueSource(typeof(TestData), nameof(TestData.StreamingModes))]
StreamingMode streamingMode)
StreamingMode streamingMode,
[ValueSource(typeof(TestData), nameof(TestData.NormalizeColors))]
bool profileNormalizeColors)
{
var localFileSystem = new LocalFileSystem(
new RealFileSystem(),
@ -303,7 +311,8 @@ public class TranscodingTests @@ -303,7 +311,8 @@ public class TranscodingTests
VideoFormat = profileVideoFormat,
AudioFormat = FFmpegProfileAudioFormat.Aac,
DeinterlaceVideo = true,
BitDepth = profileBitDepth
BitDepth = profileBitDepth,
NormalizeColors = profileNormalizeColors
},
StreamingMode = streamingMode,
SubtitleMode = ChannelSubtitleMode.None
@ -423,6 +432,7 @@ public class TranscodingTests @@ -423,6 +432,7 @@ public class TranscodingTests
profileBitDepth,
profileVideoFormat,
profileAcceleration,
profileNormalizeColors,
VaapiDriver.iHD,
localStatisticsProvider,
streamingMode,
@ -455,7 +465,9 @@ public class TranscodingTests @@ -455,7 +465,9 @@ public class TranscodingTests
[ValueSource(typeof(TestData), nameof(TestData.TestAccelerations))]
HardwareAccelerationKind profileAcceleration,
[ValueSource(typeof(TestData), nameof(TestData.StreamingModes))]
StreamingMode streamingMode)
StreamingMode streamingMode,
[ValueSource(typeof(TestData), nameof(TestData.NormalizeColors))]
bool profileNormalizeColors)
{
try
{
@ -755,6 +767,7 @@ public class TranscodingTests @@ -755,6 +767,7 @@ public class TranscodingTests
profileBitDepth,
profileVideoFormat,
profileAcceleration,
profileNormalizeColors,
VaapiDriver.iHD,
localStatisticsProvider,
streamingMode,
@ -1026,6 +1039,7 @@ public class TranscodingTests @@ -1026,6 +1039,7 @@ public class TranscodingTests
FFmpegProfileBitDepth profileBitDepth,
FFmpegProfileVideoFormat profileVideoFormat,
HardwareAccelerationKind profileAcceleration,
bool profileNormalizeColors,
VaapiDriver vaapiDriver,
ILocalStatisticsProvider localStatisticsProvider,
StreamingMode streamingMode,
@ -1134,6 +1148,11 @@ public class TranscodingTests @@ -1134,6 +1148,11 @@ public class TranscodingTests
videoStream.PixelFormat.ShouldBe(expectedPixelFormat);
if (!profileNormalizeColors)
{
continue;
}
// verify colors
var colorParams = new ColorParams(
videoStream.ColorRange,

30
ErsatzTV/Pages/Channels.razor

@ -98,11 +98,25 @@ @@ -98,11 +98,25 @@
<div style="align-items: center; display: flex;">
@if (_channelsThatCanPreview.Contains(context.Id))
{
<MudTooltip Text="Preview Channel">
<MudIconButton Icon="@Icons.Material.Filled.PlayCircle"
OnClick="@(_ => PreviewChannel(context))">
</MudIconButton>
</MudTooltip>
Option<FFmpegProfileViewModel> maybeProfile = Optional(_ffmpegProfiles.Find(p => p.Id == context.FFmpegProfileId));
bool notNormalizing = maybeProfile.Any(p => !p.NormalizeVideo || !p.NormalizeAudio);
if (notNormalizing)
{
<MudTooltip Text="Preview Channel; audio and/or video may not work without normalization">
<MudIconButton Icon="@Icons.Material.Filled.PlayCircle"
OnClick="@(_ => PreviewChannel(context))"
Style="color: var(--mud-palette-warning-lighten);">
</MudIconButton>
</MudTooltip>
}
else
{
<MudTooltip Text="Preview Channel">
<MudIconButton Icon="@Icons.Material.Filled.PlayCircle"
OnClick="@(_ => PreviewChannel(context))">
</MudIconButton>
</MudTooltip>
}
}
else if (CanPreviewChannel(context) && _ffmpegProfilesThatCanPreview.ContainsKey(context.FFmpegProfileId) && !_ffmpegProfilesThatCanPreview[context.FFmpegProfileId])
{
@ -272,6 +286,12 @@ @@ -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;

454
ErsatzTV/Pages/FFmpegEditor.razor

@ -38,266 +38,302 @@ @@ -38,266 +38,302 @@
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preferred Resolution</MudText>
</div>
<MudSelect @bind-Value="_model.Resolution" For="@(() => _model.Resolution)">
@foreach (ResolutionViewModel resolution in _resolutions)
{
<MudSelectItem Value="@resolution">@resolution.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Scaling Behavior</MudText>
<MudText>Normalize Audio</MudText>
</div>
<MudSelect @bind-Value="_model.ScalingBehavior" For="@(() => _model.ScalingBehavior)">
<MudSelectItem Value="@ScalingBehavior.ScaleAndPad">Scale and Pad</MudSelectItem>
<MudSelectItem Value="@ScalingBehavior.Stretch">Stretch</MudSelectItem>
<MudSelectItem Value="@ScalingBehavior.Crop">Crop</MudSelectItem>
</MudSelect>
<MudCheckBox @bind-Value="@_model.NormalizeAudio" For="@(() => _model.NormalizeAudio)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Pad Mode</MudText>
<MudText>Normalize Video</MudText>
</div>
<MudSelect @bind-Value="_model.PadMode"
For="@(() => _model.PadMode)"
Disabled="_model.HardwareAcceleration is not HardwareAccelerationKind.Vaapi"
HelperText="If hardware padding is green, software padding should be used instead">
<MudSelectItem Value="@FilterMode.HardwareIfPossible">Hardware If Possible</MudSelectItem>
<MudSelectItem Value="@FilterMode.Software">Software</MudSelectItem>
</MudSelect>
<MudCheckBox @bind-Value="@_model.NormalizeVideo" For="@(() => _model.NormalizeVideo)" Dense="true"/>
</MudStack>
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Video</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Format</MudText>
<MudText>Preferred Resolution</MudText>
</div>
<MudSelect @bind-Value="_model.VideoFormat" For="@(() => _model.VideoFormat)">
<MudSelectItem Value="@FFmpegProfileVideoFormat.H264">h264</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileVideoFormat.Hevc">hevc</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileVideoFormat.Mpeg2Video">mpeg-2</MudSelectItem>
@if (_model.HardwareAcceleration is (HardwareAccelerationKind.Nvenc or HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv))
<MudSelect @bind-Value="_model.Resolution" For="@(() => _model.Resolution)">
@foreach (ResolutionViewModel resolution in _resolutions)
{
<MudSelectItem Value="@FFmpegProfileVideoFormat.Av1">av1</MudSelectItem>
<MudSelectItem Value="@resolution">@resolution.Name</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Profile</MudText>
</div>
<MudSelect @bind-Value="_model.VideoProfile"
For="@(() => _model.VideoProfile)"
Disabled="@(_model.VideoFormat != FFmpegProfileVideoFormat.H264 || !HardwareAccelSupportsH264Profile(_model.HardwareAcceleration))"
Clearable="true">
<MudSelectItem Value="@VideoProfile.Main">main</MudSelectItem>
<MudSelectItem Value="@VideoProfile.High">high</MudSelectItem>
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Nvenc)
{
<MudSelectItem Value="@VideoProfile.High444p">high444p</MudSelectItem>
}
else
{
<MudSelectItem Value="@VideoProfile.High10">high10</MudSelectItem>
@if (_model.NormalizeVideo)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Scaling Behavior</MudText>
</div>
<MudSelect @bind-Value="_model.ScalingBehavior" For="@(() => _model.ScalingBehavior)">
<MudSelectItem Value="@ScalingBehavior.ScaleAndPad">Scale and Pad</MudSelectItem>
<MudSelectItem Value="@ScalingBehavior.Stretch">Stretch</MudSelectItem>
<MudSelectItem Value="@ScalingBehavior.Crop">Crop</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Pad Mode</MudText>
</div>
<MudSelect @bind-Value="_model.PadMode"
For="@(() => _model.PadMode)"
Disabled="_model.HardwareAcceleration is not HardwareAccelerationKind.Vaapi"
HelperText="If hardware padding is green, software padding should be used instead">
<MudSelectItem Value="@FilterMode.HardwareIfPossible">Hardware If Possible</MudSelectItem>
<MudSelectItem Value="@FilterMode.Software">Software</MudSelectItem>
</MudSelect>
</MudStack>
}
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Video</MudText>
<MudDivider Class="mb-6"/>
@if (_model.NormalizeVideo)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Format</MudText>
</div>
<MudSelect @bind-Value="_model.VideoFormat" For="@(() => _model.VideoFormat)">
<MudSelectItem Value="@FFmpegProfileVideoFormat.H264">h264</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileVideoFormat.Hevc">hevc</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileVideoFormat.Mpeg2Video">mpeg-2</MudSelectItem>
@if (_model.HardwareAcceleration is (HardwareAccelerationKind.Nvenc or HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv))
{
<MudSelectItem Value="@FFmpegProfileVideoFormat.Av1">av1</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Profile</MudText>
</div>
<MudSelect @bind-Value="_model.VideoProfile"
For="@(() => _model.VideoProfile)"
Disabled="@(_model.VideoFormat != FFmpegProfileVideoFormat.H264 || !HardwareAccelSupportsH264Profile(_model.HardwareAcceleration))"
Clearable="true">
<MudSelectItem Value="@VideoProfile.Main">main</MudSelectItem>
<MudSelectItem Value="@VideoProfile.High">high</MudSelectItem>
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Nvenc)
{
<MudSelectItem Value="@VideoProfile.High444p">high444p</MudSelectItem>
}
else
{
<MudSelectItem Value="@VideoProfile.High10">high10</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preset</MudText>
</div>
@{
ICollection<string> presets = FFmpegLibraryHelper.PresetsForFFmpegProfile(
_model.HardwareAcceleration,
_model.VideoFormat,
_model.BitDepth);
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Preset</MudText>
</div>
@{
ICollection<string> presets = FFmpegLibraryHelper.PresetsForFFmpegProfile(
_model.HardwareAcceleration,
_model.VideoFormat,
_model.BitDepth);
}
<MudSelect @bind-Value="_model.VideoPreset"
For="@(() => _model.VideoPreset)"
Disabled="@(presets.Count == 0)"
Clearable="true">
@foreach (string preset in presets)
{
if (!string.IsNullOrWhiteSpace(preset))
<MudSelect @bind-Value="_model.VideoPreset"
For="@(() => _model.VideoPreset)"
Disabled="@(presets.Count == 0)"
Clearable="true">
@foreach (string preset in presets)
{
if (!string.IsNullOrWhiteSpace(preset))
{
<MudSelectItem Value="@preset">@preset</MudSelectItem>
}
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Allow B-Frames</MudText>
</div>
<MudCheckBox @bind-Value="@_model.AllowBFrames" For="@(() => _model.AllowBFrames)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bit Depth</MudText>
</div>
<MudSelect @bind-Value="_model.BitDepth" For="@(() => _model.BitDepth)">
<MudSelectItem Value="@FFmpegProfileBitDepth.EightBit">8-bit</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileBitDepth.TenBit">10-bit</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bitrate</MudText>
</div>
<MudTextField @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Buffer Size</MudText>
</div>
<MudTextField @bind-Value="_model.VideoBufferSize" For="@(() => _model.VideoBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Hardware Acceleration</MudText>
</div>
<MudSelect @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)">
@foreach (HardwareAccelerationKind hwAccel in _hardwareAccelerationKinds)
{
<MudSelectItem Value="@preset">@preset</MudSelectItem>
<MudSelectItem Value="@hwAccel">@hwAccel</MudSelectItem>
}
</MudSelect>
</MudStack>
@if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>VAAPI Driver</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDriver" For="@(() => _model.VaapiDriver)">
@foreach (VaapiDriver driver in Enum.GetValues<VaapiDriver>())
{
<MudSelectItem Value="@driver">@driver</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>VAAPI Display</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDisplay" For="@(() => _model.VaapiDisplay)">
@foreach (string display in _vaapiDisplays)
{
<MudSelectItem Value="@display">@display</MudSelectItem>
}
</MudSelect>
</MudStack>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Allow B-Frames</MudText>
</div>
<MudCheckBox @bind-Value="@_model.AllowBFrames" For="@(() => _model.AllowBFrames)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bit Depth</MudText>
</div>
<MudSelect @bind-Value="_model.BitDepth" For="@(() => _model.BitDepth)">
<MudSelectItem Value="@FFmpegProfileBitDepth.EightBit">8-bit</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileBitDepth.TenBit">10-bit</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bitrate</MudText>
</div>
<MudTextField @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Buffer Size</MudText>
</div>
<MudTextField @bind-Value="_model.VideoBufferSize" For="@(() => _model.VideoBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Hardware Acceleration</MudText>
</div>
<MudSelect @bind-Value="_model.HardwareAcceleration" For="@(() => _model.HardwareAcceleration)">
@foreach (HardwareAccelerationKind hwAccel in _hardwareAccelerationKinds)
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv)
{
<MudSelectItem Value="@hwAccel">@hwAccel</MudSelectItem>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>@(_model.HardwareAcceleration == HardwareAccelerationKind.Vaapi ? "VAAPI Device" : "QSV Device")</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDevice" For="@(() => _model.VaapiDevice)">
@foreach (string device in _vaapiDevices)
{
<MudSelectItem Value="@device">@device</MudSelectItem>
}
</MudSelect>
</MudStack>
}
</MudSelect>
</MudStack>
@if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi)
}
@if (_model.HardwareAcceleration == HardwareAccelerationKind.Qsv)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>VAAPI Driver</MudText>
<MudText>QSV Extra Hardware Frames</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDriver" For="@(() => _model.VaapiDriver)">
@foreach (VaapiDriver driver in Enum.GetValues<VaapiDriver>())
{
<MudSelectItem Value="@driver">@driver</MudSelectItem>
}
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>VAAPI Display</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDisplay" For="@(() => _model.VaapiDisplay)">
@foreach (string display in _vaapiDisplays)
{
<MudSelectItem Value="@display">@display</MudSelectItem>
}
</MudSelect>
<MudTextField @bind-Value="_model.QsvExtraHardwareFrames" For="@(() => _model.QsvExtraHardwareFrames)"/>
</MudStack>
}
@if (_model.HardwareAcceleration is HardwareAccelerationKind.Vaapi or HardwareAccelerationKind.Qsv)
else
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>@(_model.HardwareAcceleration == HardwareAccelerationKind.Vaapi ? "VAAPI Device" : "QSV Device")</MudText>
<MudText>Tonemap Algorithm</MudText>
</div>
<MudSelect @bind-Value="_model.VaapiDevice" For="@(() => _model.VaapiDevice)">
@foreach (string device in _vaapiDevices)
<MudSelect @bind-Value="_model.TonemapAlgorithm" For="@(() => _model.TonemapAlgorithm)">
@foreach (FFmpegProfileTonemapAlgorithm algorithm in Enum.GetValues<FFmpegProfileTonemapAlgorithm>())
{
<MudSelectItem Value="@device">@device</MudSelectItem>
<MudSelectItem Value="@algorithm">@algorithm</MudSelectItem>
}
</MudSelect>
</MudStack>
}
}
@if (_model.HardwareAcceleration == HardwareAccelerationKind.Qsv)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>QSV Extra Hardware Frames</MudText>
<MudText>Normalize Frame Rate</MudText>
</div>
<MudTextField @bind-Value="_model.QsvExtraHardwareFrames" For="@(() => _model.QsvExtraHardwareFrames)"/>
<MudCheckBox @bind-Value="@_model.NormalizeFramerate" For="@(() => _model.NormalizeFramerate)" Dense="true"/>
</MudStack>
}
else
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Tonemap Algorithm</MudText>
<MudText>Normalize Colors</MudText>
</div>
<MudSelect @bind-Value="_model.TonemapAlgorithm" For="@(() => _model.TonemapAlgorithm)">
@foreach (FFmpegProfileTonemapAlgorithm algorithm in Enum.GetValues<FFmpegProfileTonemapAlgorithm>())
{
<MudSelectItem Value="@algorithm">@algorithm</MudSelectItem>
}
</MudSelect>
<MudCheckBox @bind-Value="@_model.NormalizeColors" For="@(() => _model.NormalizeColors)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Auto Deinterlace Video</MudText>
</div>
<MudCheckBox @bind-Value="@_model.DeinterlaceVideo" For="@(() => _model.DeinterlaceVideo)" Dense="true"/>
</MudStack>
}
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Normalize Frame Rate</MudText>
</div>
<MudCheckBox @bind-Value="@_model.NormalizeFramerate" For="@(() => _model.NormalizeFramerate)" Dense="true"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Auto Deinterlace Video</MudText>
</div>
<MudCheckBox @bind-Value="@_model.DeinterlaceVideo" For="@(() => _model.DeinterlaceVideo)" Dense="true"/>
</MudStack>
else
{
<MudText>Video will be copied as-is, including timestamps, which *will* cause issues with most clients</MudText>
}
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Audio</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Format</MudText>
</div>
<MudSelect @bind-Value="_model.AudioFormat" For="@(() => _model.AudioFormat)">
<MudSelectItem Value="@FFmpegProfileAudioFormat.Aac">aac</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.Ac3">ac3</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.AacLatm">aac (latm)</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bitrate</MudText>
</div>
<MudTextField @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Buffer Size</MudText>
</div>
<MudTextField @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Channels</MudText>
</div>
<MudTextField @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Sample Rate</MudText>
</div>
<MudTextField @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Normalize Loudness</MudText>
</div>
<MudSelect @bind-Value="_model.NormalizeLoudnessMode" For="@(() => _model.NormalizeLoudnessMode)">
<MudSelectItem Value="@NormalizeLoudnessMode.Off">Off</MudSelectItem>
<MudSelectItem Value="@NormalizeLoudnessMode.LoudNorm">loudnorm</MudSelectItem>
</MudSelect>
</MudStack>
@if (_model.NormalizeLoudnessMode is NormalizeLoudnessMode.LoudNorm)
@if (_model.NormalizeAudio)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Target Loudness</MudText>
<MudText>Format</MudText>
</div>
<MudSelect @bind-Value="_model.AudioFormat" For="@(() => _model.AudioFormat)">
<MudSelectItem Value="@FFmpegProfileAudioFormat.Aac">aac</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.Ac3">ac3</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileAudioFormat.AacLatm">aac (latm)</MudSelectItem>
</MudSelect>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Bitrate</MudText>
</div>
<MudTextField @bind-Value="_model.AudioBitrate" For="@(() => _model.AudioBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Buffer Size</MudText>
</div>
<MudTextField @bind-Value="_model.AudioBufferSize" For="@(() => _model.AudioBufferSize)" Adornment="Adornment.End" AdornmentText="kBit"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Channels</MudText>
</div>
<MudTextField @bind-Value="_model.TargetLoudness" For="@(() => _model.TargetLoudness)" Adornment="Adornment.End" AdornmentText="LUFS"/>
<MudTextField @bind-Value="_model.AudioChannels" For="@(() => _model.AudioChannels)"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Sample Rate</MudText>
</div>
<MudTextField @bind-Value="_model.AudioSampleRate" For="@(() => _model.AudioSampleRate)" Adornment="Adornment.End" AdornmentText="kHz"/>
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Normalize Loudness</MudText>
</div>
<MudSelect @bind-Value="_model.NormalizeLoudnessMode" For="@(() => _model.NormalizeLoudnessMode)">
<MudSelectItem Value="@NormalizeLoudnessMode.Off">Off</MudSelectItem>
<MudSelectItem Value="@NormalizeLoudnessMode.LoudNorm">loudnorm</MudSelectItem>
</MudSelect>
</MudStack>
@if (_model.NormalizeLoudnessMode is NormalizeLoudnessMode.LoudNorm)
{
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Target Loudness</MudText>
</div>
<MudTextField @bind-Value="_model.TargetLoudness" For="@(() => _model.TargetLoudness)" Adornment="Adornment.End" AdornmentText="LUFS"/>
</MudStack>
}
}
else
{
<MudText>Audio will be copied as-is, including timestamps, which *will* cause issues with most clients</MudText>
}
</MudContainer>
</div>

45
ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

@ -23,11 +23,14 @@ public class FFmpegProfileEditViewModel @@ -23,11 +23,14 @@ public class FFmpegProfileEditViewModel
Id = viewModel.Id;
Name = viewModel.Name;
NormalizeFramerate = viewModel.NormalizeFramerate;
NormalizeColors = viewModel.NormalizeColors;
DeinterlaceVideo = viewModel.DeinterlaceVideo;
Resolution = viewModel.Resolution;
ScalingBehavior = viewModel.ScalingBehavior;
PadMode = viewModel.PadMode;
ThreadCount = viewModel.ThreadCount;
NormalizeAudio = viewModel.NormalizeAudio;
NormalizeVideo = viewModel.NormalizeVideo;
HardwareAcceleration = viewModel.HardwareAcceleration;
VaapiDisplay = viewModel.VaapiDisplay;
VaapiDriver = viewModel.VaapiDriver;
@ -46,7 +49,22 @@ public class FFmpegProfileEditViewModel @@ -46,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
@ -74,6 +92,7 @@ public class FFmpegProfileEditViewModel @@ -74,6 +92,7 @@ public class FFmpegProfileEditViewModel
public int Id { get; set; }
public string Name { get; set; }
public bool NormalizeFramerate { get; set; }
public bool NormalizeColors { get; set; }
public bool DeinterlaceVideo { get; set; }
public ResolutionViewModel Resolution { get; set; }
public ScalingBehavior ScalingBehavior { get; set; }
@ -92,6 +111,8 @@ public class FFmpegProfileEditViewModel @@ -92,6 +111,8 @@ public class FFmpegProfileEditViewModel
}
public int ThreadCount { get; set; }
public bool NormalizeAudio { get; set; }
public bool NormalizeVideo { get; set; }
public HardwareAccelerationKind HardwareAcceleration { get; set; }
public string VaapiDisplay { get; set; }
public VaapiDriver VaapiDriver { get; set; }
@ -99,7 +120,21 @@ public class FFmpegProfileEditViewModel @@ -99,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
{
@ -124,6 +159,8 @@ public class FFmpegProfileEditViewModel @@ -124,6 +159,8 @@ public class FFmpegProfileEditViewModel
new(
Name,
ThreadCount,
NormalizeAudio,
NormalizeVideo,
HardwareAcceleration,
VaapiDisplay,
VaapiDriver,
@ -148,6 +185,7 @@ public class FFmpegProfileEditViewModel @@ -148,6 +185,7 @@ public class FFmpegProfileEditViewModel
AudioChannels,
AudioSampleRate,
NormalizeFramerate,
NormalizeColors,
DeinterlaceVideo
);
@ -156,6 +194,8 @@ public class FFmpegProfileEditViewModel @@ -156,6 +194,8 @@ public class FFmpegProfileEditViewModel
Id,
Name,
ThreadCount,
NormalizeAudio,
NormalizeVideo,
HardwareAcceleration,
VaapiDisplay,
VaapiDriver,
@ -180,6 +220,7 @@ public class FFmpegProfileEditViewModel @@ -180,6 +220,7 @@ public class FFmpegProfileEditViewModel
AudioChannels,
AudioSampleRate,
NormalizeFramerate,
NormalizeColors,
DeinterlaceVideo
);
}

Loading…
Cancel
Save