Browse Source

add ffmpeg profile pad mode

pull/2775/head
Jason Dove 7 months ago
parent
commit
5b8bc858bb
No known key found for this signature in database
  1. 5
      CHANGELOG.md
  2. 1
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs
  3. 10
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
  4. 1
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs
  5. 11
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
  6. 1
      ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs
  7. 1
      ErsatzTV.Application/FFmpegProfiles/Mapper.cs
  8. 3
      ErsatzTV.Core/Domain/FFmpegProfile.cs
  9. 7
      ErsatzTV.Core/Domain/FilterMode.cs
  10. 10
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  11. 4
      ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs
  12. 7
      ErsatzTV.FFmpeg/FFmpegFilterMode.cs
  13. 1
      ErsatzTV.FFmpeg/FrameState.cs
  14. 19
      ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs
  15. 7024
      ErsatzTV.Infrastructure.MySql/Migrations/20260115145631_Add_FFmpegProfilePadMode.Designer.cs
  16. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20260115145631_Add_FFmpegProfilePadMode.cs
  17. 3
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  18. 6851
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260115145551_Add_FFmpegProfilePadMode.Designer.cs
  19. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20260115145551_Add_FFmpegProfilePadMode.cs
  20. 3
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  21. 12
      ErsatzTV/Pages/FFmpegEditor.razor
  22. 34
      ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

5
CHANGELOG.md

@ -10,6 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -10,6 +10,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- fr-FR can match sunday using `day_of_week = 6`
- As a complete example, to match Saturday from 9pm (inclusive) to 11pm (exclusive), based on content start time
- `content_condition: day_of_week = 6 and (time_of_day_seconds >= 75600 and time_of_day_seconds < 82800)`
- Add `Pad Mode` to ffmpeg profile. Options are:
- `Hardware If Possible` - default/existing behavior when hardware acceleration is properly configured
- `Software` - force software padding
- This can be used to work around buggy GPU driver behavior where padding is green instead of black
- This is most often seen with AMD GPUs when using VAAPI acceleration
### Fixed
- Use code signing on all Windows executables (`ErsatzTV-Windows.exe`, `ErsatzTV.exe`, `ErsatzTV.Scanner.exe`)

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

@ -14,6 +14,7 @@ public record CreateFFmpegProfile( @@ -14,6 +14,7 @@ public record CreateFFmpegProfile(
int? QsvExtraHardwareFrames,
int ResolutionId,
ScalingBehavior ScalingBehavior,
FilterMode PadMode,
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,

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

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
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;
@ -54,6 +55,15 @@ public class CreateFFmpegProfileHandler : @@ -54,6 +55,15 @@ public class CreateFFmpegProfileHandler :
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
ResolutionId = resolutionId,
ScalingBehavior = request.ScalingBehavior,
// only allow customization with VAAPI accel
PadMode = request.HardwareAcceleration switch
{
HardwareAccelerationKind.None => FilterMode.Software,
HardwareAccelerationKind.Vaapi => request.PadMode,
_ => FilterMode.HardwareIfPossible
},
VideoFormat = request.VideoFormat,
VideoProfile = request.VideoProfile,
VideoPreset = request.VideoPreset,

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

@ -15,6 +15,7 @@ public record UpdateFFmpegProfile( @@ -15,6 +15,7 @@ public record UpdateFFmpegProfile(
int? QsvExtraHardwareFrames,
int ResolutionId,
ScalingBehavior ScalingBehavior,
FilterMode PadMode,
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,

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

@ -36,6 +36,7 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa @@ -36,6 +36,7 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
p.ResolutionId = update.ResolutionId;
p.ScalingBehavior = update.ScalingBehavior;
p.PadMode = update.PadMode;
p.VideoFormat = update.VideoFormat;
p.VideoProfile = update.VideoProfile;
p.VideoPreset = update.VideoPreset;
@ -53,6 +54,16 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa @@ -53,6 +54,16 @@ public class UpdateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFa
p.VideoFormat = FFmpegProfileVideoFormat.Hevc;
}
// only allow customization with VAAPI accel
if (p.HardwareAcceleration is HardwareAccelerationKind.None)
{
p.PadMode = FilterMode.Software;
}
else if (p.HardwareAcceleration is not HardwareAccelerationKind.Vaapi)
{
p.PadMode = FilterMode.HardwareIfPossible;
}
p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize;
p.TonemapAlgorithm = update.TonemapAlgorithm;

1
ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs

@ -15,6 +15,7 @@ public record FFmpegProfileViewModel( @@ -15,6 +15,7 @@ public record FFmpegProfileViewModel(
int? QsvExtraHardwareFrames,
ResolutionViewModel Resolution,
ScalingBehavior ScalingBehavior,
FilterMode PadMode,
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,

1
ErsatzTV.Application/FFmpegProfiles/Mapper.cs

@ -17,6 +17,7 @@ internal static class Mapper @@ -17,6 +17,7 @@ internal static class Mapper
profile.QsvExtraHardwareFrames,
Resolutions.Mapper.ProjectToViewModel(profile.Resolution),
profile.ScalingBehavior,
profile.PadMode,
profile.VideoFormat,
profile.VideoProfile,
profile.VideoPreset ?? string.Empty,

3
ErsatzTV.Core/Domain/FFmpegProfile.cs

@ -15,6 +15,7 @@ public record FFmpegProfile @@ -15,6 +15,7 @@ public record FFmpegProfile
public int ResolutionId { get; set; }
public Resolution Resolution { get; set; }
public ScalingBehavior ScalingBehavior { get; set; }
public FilterMode PadMode { get; set; }
public FFmpegProfileVideoFormat VideoFormat { get; set; }
public string VideoProfile { get; set; }
public string VideoPreset { get; set; }
@ -40,6 +41,8 @@ public record FFmpegProfile @@ -40,6 +41,8 @@ public record FFmpegProfile
ThreadCount = 0,
ResolutionId = resolution.Id,
Resolution = resolution,
ScalingBehavior = ScalingBehavior.ScaleAndPad,
PadMode = FilterMode.Software,
VideoFormat = FFmpegProfileVideoFormat.H264,
VideoProfile = "high",
VideoPreset = ErsatzTV.FFmpeg.Preset.VideoPreset.Unset,

7
ErsatzTV.Core/Domain/FilterMode.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Domain;
public enum FilterMode
{
HardwareIfPossible = 0,
Software = 1
}

10
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -509,7 +509,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -509,7 +509,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
scaledSize,
paddedSize,
cropSize,
false,
channel.FFmpegProfile.PadMode is FilterMode.HardwareIfPossible
? FFmpegFilterMode.HardwareIfPossible
: FFmpegFilterMode.Software,
IsAnamorphic: false,
playbackSettings.FrameRate,
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,
@ -711,7 +714,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService @@ -711,7 +714,10 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
new FrameSize(desiredResolution.Width, desiredResolution.Height),
new FrameSize(desiredResolution.Width, desiredResolution.Height),
Option<FrameSize>.None,
false,
channel.FFmpegProfile.PadMode is FilterMode.HardwareIfPossible
? FFmpegFilterMode.HardwareIfPossible
: FFmpegFilterMode.Software,
IsAnamorphic: false,
playbackSettings.FrameRate,
playbackSettings.VideoBitrate,
playbackSettings.VideoBufferSize,

4
ErsatzTV.FFmpeg.Tests/PipelineBuilderBaseTests.cs

@ -67,6 +67,7 @@ public class PipelineBuilderBaseTests @@ -67,6 +67,7 @@ public class PipelineBuilderBaseTests
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
Option<FrameSize>.None,
FFmpegFilterMode.HardwareIfPossible,
false,
Option<FrameRate>.None,
2000,
@ -168,6 +169,7 @@ public class PipelineBuilderBaseTests @@ -168,6 +169,7 @@ public class PipelineBuilderBaseTests
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
Option<FrameSize>.None,
FFmpegFilterMode.HardwareIfPossible,
false,
Option<FrameRate>.None,
2000,
@ -327,6 +329,7 @@ public class PipelineBuilderBaseTests @@ -327,6 +329,7 @@ public class PipelineBuilderBaseTests
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
Option<FrameSize>.None,
FFmpegFilterMode.HardwareIfPossible,
false,
Option<FrameRate>.None,
2000,
@ -421,6 +424,7 @@ public class PipelineBuilderBaseTests @@ -421,6 +424,7 @@ public class PipelineBuilderBaseTests
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
Option<FrameSize>.None,
FFmpegFilterMode.HardwareIfPossible,
false,
Option<FrameRate>.None,
2000,

7
ErsatzTV.FFmpeg/FFmpegFilterMode.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.FFmpeg;
public enum FFmpegFilterMode
{
HardwareIfPossible = 0,
Software = 1
}

1
ErsatzTV.FFmpeg/FrameState.cs

@ -13,6 +13,7 @@ public record FrameState( @@ -13,6 +13,7 @@ public record FrameState(
FrameSize ScaledSize,
FrameSize PaddedSize,
Option<FrameSize> CroppedSize,
FFmpegFilterMode PadMode,
bool IsAnamorphic,
Option<FrameRate> FrameRate,
Option<int> VideoBitrate,

19
ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs

@ -199,7 +199,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder @@ -199,7 +199,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
bool isHdrTonemap = videoStream.ColorParams.IsHdr;
currentState = SetTonemap(videoInputFile, videoStream, ffmpegState, desiredState, currentState);
currentState = SetPad(videoInputFile, ffmpegState, desiredState, currentState, isHdrTonemap);
currentState = SetPad(videoInputFile, desiredState, currentState, isHdrTonemap);
// _logger.LogDebug("After pad: {PixelFormat}", currentState.PixelFormat);
currentState = SetCrop(videoInputFile, desiredState, currentState);
@ -617,28 +617,13 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder @@ -617,28 +617,13 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
private static FrameState SetPad(
VideoInputFile videoInputFile,
FFmpegState ffmpegState,
FrameState desiredState,
FrameState currentState,
bool isHdrTonemap)
{
if (desiredState.CroppedSize.IsNone && currentState.PaddedSize != desiredState.PaddedSize)
{
// pad_vaapi seems to pad with green when input is HDR
// also green with i965 driver
// also green with radeonsi and h264 main profile
// so use software pad in these cases
bool is965 = ffmpegState.VaapiDriver
.IfNone(string.Empty)
.Contains("i965", StringComparison.OrdinalIgnoreCase);
bool isRadeonSiMain = ffmpegState.VaapiDriver.IfNone(string.Empty)
.Contains("radeonsi", StringComparison.OrdinalIgnoreCase)
&& ffmpegState.EncoderHardwareAccelerationMode is HardwareAccelerationMode.Vaapi
&& desiredState.VideoFormat is VideoFormat.H264
&& desiredState.VideoProfile.IfNone(string.Empty) is VideoProfile.Main;
if (isHdrTonemap || is965 || isRadeonSiMain)
if (desiredState.PadMode is FFmpegFilterMode.Software || isHdrTonemap)
{
var padStep = new PadFilter(currentState, desiredState.PaddedSize);
currentState = padStep.NextState(currentState);

7024
ErsatzTV.Infrastructure.MySql/Migrations/20260115145631_Add_FFmpegProfilePadMode.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20260115145631_Add_FFmpegProfilePadMode.cs

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

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

@ -765,6 +765,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -765,6 +765,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("NormalizeLoudnessMode")
.HasColumnType("int");
b.Property<int>("PadMode")
.HasColumnType("int");
b.Property<int?>("QsvExtraHardwareFrames")
.HasColumnType("int");

6851
ErsatzTV.Infrastructure.Sqlite/Migrations/20260115145551_Add_FFmpegProfilePadMode.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20260115145551_Add_FFmpegProfilePadMode.cs

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

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

@ -734,6 +734,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -734,6 +734,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("NormalizeLoudnessMode")
.HasColumnType("INTEGER");
b.Property<int>("PadMode")
.HasColumnType("INTEGER");
b.Property<int?>("QsvExtraHardwareFrames")
.HasColumnType("INTEGER");

12
ErsatzTV/Pages/FFmpegEditor.razor

@ -57,6 +57,18 @@ @@ -57,6 +57,18 @@
<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"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">

34
ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

@ -7,9 +7,6 @@ namespace ErsatzTV.ViewModels; @@ -7,9 +7,6 @@ namespace ErsatzTV.ViewModels;
public class FFmpegProfileEditViewModel
{
private string _videoProfile;
private NormalizeLoudnessMode _normalizeLoudnessMode;
public FFmpegProfileEditViewModel()
{
}
@ -29,6 +26,7 @@ public class FFmpegProfileEditViewModel @@ -29,6 +26,7 @@ public class FFmpegProfileEditViewModel
DeinterlaceVideo = viewModel.DeinterlaceVideo;
Resolution = viewModel.Resolution;
ScalingBehavior = viewModel.ScalingBehavior;
PadMode = viewModel.PadMode;
ThreadCount = viewModel.ThreadCount;
HardwareAcceleration = viewModel.HardwareAcceleration;
VaapiDisplay = viewModel.VaapiDisplay;
@ -53,13 +51,14 @@ public class FFmpegProfileEditViewModel @@ -53,13 +51,14 @@ public class FFmpegProfileEditViewModel
public NormalizeLoudnessMode NormalizeLoudnessMode
{
get => _normalizeLoudnessMode;
get;
set
{
if (_normalizeLoudnessMode != value)
if (field != value)
{
_normalizeLoudnessMode = value;
if (_normalizeLoudnessMode is NormalizeLoudnessMode.LoudNorm)
field = value;
if (field is NormalizeLoudnessMode.LoudNorm)
{
TargetLoudness = -16;
}
@ -78,6 +77,20 @@ public class FFmpegProfileEditViewModel @@ -78,6 +77,20 @@ public class FFmpegProfileEditViewModel
public bool DeinterlaceVideo { get; set; }
public ResolutionViewModel Resolution { get; set; }
public ScalingBehavior ScalingBehavior { get; set; }
public FilterMode PadMode
{
// only allow customization with VAAPI accel
get => HardwareAcceleration switch
{
HardwareAccelerationKind.None => FilterMode.Software,
HardwareAccelerationKind.Vaapi => field,
_ => FilterMode.HardwareIfPossible
};
set;
}
public int ThreadCount { get; set; }
public HardwareAccelerationKind HardwareAcceleration { get; set; }
public string VaapiDisplay { get; set; }
@ -95,10 +108,11 @@ public class FFmpegProfileEditViewModel @@ -95,10 +108,11 @@ public class FFmpegProfileEditViewModel
{
(HardwareAccelerationKind.Nvenc, FFmpegProfileVideoFormat.H264, FFmpegProfileBitDepth.TenBit) => FFmpeg
.Format.VideoProfile.High444p,
(_, FFmpegProfileVideoFormat.H264, _) => _videoProfile,
(_, FFmpegProfileVideoFormat.H264, _) => field,
_ => string.Empty
};
set => _videoProfile = value;
set;
}
public string VideoPreset { get; set; }
@ -117,6 +131,7 @@ public class FFmpegProfileEditViewModel @@ -117,6 +131,7 @@ public class FFmpegProfileEditViewModel
QsvExtraHardwareFrames,
Resolution.Id,
ScalingBehavior,
PadMode,
VideoFormat,
VideoProfile,
VideoPreset,
@ -148,6 +163,7 @@ public class FFmpegProfileEditViewModel @@ -148,6 +163,7 @@ public class FFmpegProfileEditViewModel
QsvExtraHardwareFrames,
Resolution.Id,
ScalingBehavior,
PadMode,
VideoFormat,
VideoProfile,
VideoPreset,

Loading…
Cancel
Save