add ffmpeg profile pad mode (#2775)

* add ffmpeg profile pad mode

* update changelog
This commit is contained in:
Jason Dove
2026-01-15 09:39:45 -06:00
committed by GitHub
parent 343a4619a6
commit ccb917d0df
22 changed files with 14038 additions and 28 deletions

View File

@@ -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 VAAPI acceleration (radeonsi or i965 drivers)
### Fixed
- Use code signing on all Windows executables (`ErsatzTV-Windows.exe`, `ErsatzTV.exe`, `ErsatzTV.Scanner.exe`)

View File

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

View File

@@ -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 :
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,

View File

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

View File

@@ -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
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;

View File

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

View File

@@ -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,

View File

@@ -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
ThreadCount = 0,
ResolutionId = resolution.Id,
Resolution = resolution,
ScalingBehavior = ScalingBehavior.ScaleAndPad,
PadMode = FilterMode.Software,
VideoFormat = FFmpegProfileVideoFormat.H264,
VideoProfile = "high",
VideoPreset = ErsatzTV.FFmpeg.Preset.VideoPreset.Unset,

View File

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

View File

@@ -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
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,

View File

@@ -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
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
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
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
Option<FrameSize>.None,
FFmpegFilterMode.HardwareIfPossible,
false,
Option<FrameRate>.None,
2000,

View File

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

View File

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

View File

@@ -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
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);

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -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");

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}
}

View File

@@ -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");

View File

@@ -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">

View File

@@ -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
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
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
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
{
(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
QsvExtraHardwareFrames,
Resolution.Id,
ScalingBehavior,
PadMode,
VideoFormat,
VideoProfile,
VideoPreset,
@@ -148,6 +163,7 @@ public class FFmpegProfileEditViewModel
QsvExtraHardwareFrames,
Resolution.Id,
ScalingBehavior,
PadMode,
VideoFormat,
VideoProfile,
VideoPreset,