add tonemap algorithm setting to ffmpeg profile (#2039)

This commit is contained in:
Jason Dove
2025-06-15 00:38:37 +00:00
committed by GitHub
parent 69a1e718df
commit ea46a7a5ca
31 changed files with 11771 additions and 23 deletions
+2 -1
View File
@@ -26,7 +26,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- VAAPI may use hardware-accelerated tone mapping (when opencl accel is also available)
- NVIDIA may use hardware-accelerated tone mapping (when vulkan accel and libplacebo filter are also available)
- QSV may use hardware-accelerated tone mapping (when hardware decoding is used)
- In all other cases, HDR content will use a software pipeline and the linear algorithm
- In all other cases, HDR content will use a software pipeline
- The tonemap algorithm can be configured in the ffmpeg profile
- Use hardware-accelerated padding with VAAPI
- Add environment variable `ETV_DISABLE_VULKAN`
- Any non-empty value will disable use of Vulkan acceleration and force software tonemapping
@@ -21,6 +21,7 @@ public record CreateFFmpegProfile(
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
@@ -60,6 +60,7 @@ public class CreateFFmpegProfileHandler :
BitDepth = request.BitDepth,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
TonemapAlgorithm = request.TonemapAlgorithm,
AudioFormat = request.AudioFormat,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
@@ -22,6 +22,7 @@ public record UpdateFFmpegProfile(
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
@@ -54,6 +54,7 @@ public class
p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize;
p.TonemapAlgorithm = update.TonemapAlgorithm;
p.AudioFormat = update.AudioFormat;
p.AudioBitrate = update.AudioBitrate;
p.AudioBufferSize = update.AudioBufferSize;
@@ -22,6 +22,7 @@ public record FFmpegProfileViewModel(
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
@@ -24,6 +24,7 @@ internal static class Mapper
profile.BitDepth,
profile.VideoBitrate,
profile.VideoBufferSize,
profile.TonemapAlgorithm,
profile.AudioFormat,
profile.AudioBitrate,
profile.AudioBufferSize,
@@ -54,6 +55,7 @@ internal static class Mapper
(int)ffmpegProfile.VideoFormat,
ffmpegProfile.VideoBitrate,
ffmpegProfile.VideoBufferSize,
(int)ffmpegProfile.TonemapAlgorithm,
(int)ffmpegProfile.AudioFormat,
ffmpegProfile.AudioBitrate,
ffmpegProfile.AudioBufferSize,
@@ -12,6 +12,7 @@ public record FFmpegFullProfileResponseModel(
int VideoFormat,
int VideoBitrate,
int VideoBufferSize,
int TonemapAlgorithm,
int AudioFormat,
int AudioBitrate,
int AudioBufferSize,
+2
View File
@@ -22,6 +22,7 @@ public record FFmpegProfile
public FFmpegProfileBitDepth BitDepth { get; set; }
public int VideoBitrate { get; set; }
public int VideoBufferSize { get; set; }
public FFmpegProfileTonemapAlgorithm TonemapAlgorithm { get; set; }
public FFmpegProfileAudioFormat AudioFormat { get; set; }
public int AudioBitrate { get; set; }
public int AudioBufferSize { get; set; }
@@ -45,6 +46,7 @@ public record FFmpegProfile
AudioFormat = FFmpegProfileAudioFormat.Aac,
VideoBitrate = 2000,
VideoBufferSize = 4000,
TonemapAlgorithm = FFmpegProfileTonemapAlgorithm.Linear,
AudioBitrate = 192,
AudioBufferSize = 384,
NormalizeLoudnessMode = NormalizeLoudnessMode.Off,
@@ -0,0 +1,11 @@
namespace ErsatzTV.Core.Domain;
public enum FFmpegProfileTonemapAlgorithm
{
Linear = 0,
Clip = 1,
Gamma = 2,
Reinhard = 3,
Mobius = 4,
Hable = 5
}
@@ -399,7 +399,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playbackSettings.ThreadCount,
qsvExtraHardwareFrames,
videoVersion is BackgroundImageMediaVersion { IsSongWithProgress: true },
IsHdrTonemap: false);
IsHdrTonemap: false,
GetTonemapAlgorithm(playbackSettings));
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
@@ -554,7 +555,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
Option<int>.None,
qsvExtraHardwareFrames,
IsSongWithProgress: false,
IsHdrTonemap: false);
IsHdrTonemap: false,
GetTonemapAlgorithm(playbackSettings));
var ffmpegSubtitleStream = new ErsatzTV.FFmpeg.MediaStream(0, "ass", StreamKind.Video);
@@ -749,7 +751,8 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
playbackSettings.ThreadCount,
Optional(channel.FFmpegProfile.QsvExtraHardwareFrames),
IsSongWithProgress: false,
IsHdrTonemap: false);
IsHdrTonemap: false,
GetTonemapAlgorithm(playbackSettings));
_logger.LogDebug("FFmpeg desired state {FrameState}", desiredState);
@@ -1039,6 +1042,18 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
_ => throw new ArgumentOutOfRangeException($"unexpected video format {playbackSettings.VideoFormat}")
};
private static string GetTonemapAlgorithm(FFmpegPlaybackSettings playbackSettings) =>
playbackSettings.TonemapAlgorithm switch
{
FFmpegProfileTonemapAlgorithm.Linear => TonemapAlgorithm.Linear,
FFmpegProfileTonemapAlgorithm.Clip => TonemapAlgorithm.Clip,
FFmpegProfileTonemapAlgorithm.Gamma => TonemapAlgorithm.Gamma,
FFmpegProfileTonemapAlgorithm.Reinhard => TonemapAlgorithm.Reinhard,
FFmpegProfileTonemapAlgorithm.Mobius => TonemapAlgorithm.Mobius,
FFmpegProfileTonemapAlgorithm.Hable => TonemapAlgorithm.Hable,
_ => throw new ArgumentOutOfRangeException($"unexpected tonemap algorithm {playbackSettings.TonemapAlgorithm}")
};
private static Option<string> GetVideoProfile(string videoFormat, string videoProfile) =>
(videoFormat, (videoProfile ?? string.Empty).ToLowerInvariant()) switch
{
@@ -18,6 +18,7 @@ public class FFmpegPlaybackSettings
public IPixelFormat PixelFormat { get; set; }
public Option<int> VideoBitrate { get; set; }
public Option<int> VideoBufferSize { get; set; }
public FFmpegProfileTonemapAlgorithm TonemapAlgorithm { get; set; }
public Option<int> AudioBitrate { get; set; }
public Option<int> AudioBufferSize { get; set; }
public Option<int> AudioChannels { get; set; }
@@ -116,6 +116,7 @@ public static class FFmpegPlaybackSettingsCalculator
result.VideoFormat = ffmpegProfile.VideoFormat;
result.VideoBitrate = ffmpegProfile.VideoBitrate;
result.VideoBufferSize = ffmpegProfile.VideoBufferSize;
result.TonemapAlgorithm = ffmpegProfile.TonemapAlgorithm;
result.VideoDecoder =
(result.HardwareAcceleration, stream.Codec, stream.PixelFormat) switch
@@ -184,6 +185,7 @@ public static class FFmpegPlaybackSettingsCalculator
VideoFormat = ffmpegProfile.VideoFormat,
VideoBitrate = ffmpegProfile.VideoBitrate,
VideoBufferSize = ffmpegProfile.VideoBufferSize,
TonemapAlgorithm = ffmpegProfile.TonemapAlgorithm,
VideoDecoder = null,
PixelFormat = ffmpegProfile.BitDepth switch
{
@@ -94,7 +94,8 @@ public class PipelineBuilderBaseTests
Option<int>.None,
Option<int>.None,
false,
false);
false,
"clip");
var builder = new SoftwarePipelineBuilder(
new DefaultFFmpegCapabilities(),
@@ -189,7 +190,8 @@ public class PipelineBuilderBaseTests
Option<int>.None,
Option<int>.None,
false,
false);
false,
"clip");
var builder = new SoftwarePipelineBuilder(
new DefaultFFmpegCapabilities(),
@@ -340,7 +342,8 @@ public class PipelineBuilderBaseTests
Option<int>.None,
Option<int>.None,
false,
false);
false,
"clip");
var builder = new SoftwarePipelineBuilder(
new DefaultFFmpegCapabilities(),
@@ -429,7 +432,8 @@ public class PipelineBuilderBaseTests
Option<int>.None,
Option<int>.None,
false,
false);
false,
"clip");
var builder = new SoftwarePipelineBuilder(
new DefaultFFmpegCapabilities(),
+4 -2
View File
@@ -23,7 +23,8 @@ public record FFmpegState(
Option<int> ThreadCount,
Option<int> MaybeQsvExtraHardwareFrames,
bool IsSongWithProgress,
bool IsHdrTonemap)
bool IsHdrTonemap,
string TonemapAlgorithm)
{
public int QsvExtraHardwareFrames => MaybeQsvExtraHardwareFrames.IfNone(64);
@@ -49,5 +50,6 @@ public record FFmpegState(
Option<int>.None,
Option<int>.None,
false,
false);
false,
"linear");
}
@@ -2,10 +2,10 @@ using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.Filter.Cuda;
public class TonemapCudaFilter(IPixelFormat desiredPixelFormat) : BaseFilter
public class TonemapCudaFilter(FFmpegState ffmpegState, IPixelFormat desiredPixelFormat) : BaseFilter
{
public override string Filter =>
$"libplacebo=tonemapping=auto:colorspace=bt709:color_primaries=bt709:color_trc=bt709:format={desiredPixelFormat.FFmpegName},hwupload_cuda";
$"libplacebo=tonemapping={ffmpegState.TonemapAlgorithm}:colorspace=bt709:color_primaries=bt709:color_trc=bt709:format={desiredPixelFormat.FFmpegName},hwupload_cuda";
public override FrameState NextState(FrameState currentState) =>
currentState with
+4 -2
View File
@@ -4,11 +4,13 @@ namespace ErsatzTV.FFmpeg.Filter;
public class TonemapFilter : BaseFilter
{
private readonly FFmpegState _ffmpegState;
private readonly FrameState _currentState;
private readonly IPixelFormat _desiredPixelFormat;
public TonemapFilter(FrameState currentState, IPixelFormat desiredPixelFormat)
public TonemapFilter(FFmpegState ffmpegState, FrameState currentState, IPixelFormat desiredPixelFormat)
{
_ffmpegState = ffmpegState;
_currentState = currentState;
_desiredPixelFormat = desiredPixelFormat;
}
@@ -20,7 +22,7 @@ public class TonemapFilter : BaseFilter
string pixelFormat = _currentState.PixelFormat.Match(pf => pf.FFmpegName, () => string.Empty);
var tonemap =
$"zscale=transfer=linear,tonemap=linear,zscale=transfer=bt709,format={_desiredPixelFormat.FFmpegName}";
$"zscale=transfer=linear,tonemap={_ffmpegState.TonemapAlgorithm},zscale=transfer=bt709,format={_desiredPixelFormat.FFmpegName}";
if (_currentState.FrameDataLocation == FrameDataLocation.Hardware)
{
@@ -1,8 +1,8 @@
namespace ErsatzTV.FFmpeg.Filter.Vaapi;
public class TonemapVaapiFilter : BaseFilter
public class TonemapVaapiFilter(FFmpegState ffmpegState) : BaseFilter
{
public override string Filter => "hwupload=derive_device=vaapi,hwmap=derive_device=opencl,tonemap_opencl,hwmap=derive_device=vaapi:reverse=1";
public override string Filter => $"hwupload=derive_device=vaapi,hwmap=derive_device=opencl,tonemap_opencl=tonemap={ffmpegState.TonemapAlgorithm},hwmap=derive_device=vaapi:reverse=1";
public override FrameState NextState(FrameState currentState) =>
currentState with
@@ -752,14 +752,14 @@ public class NvidiaPipelineBuilder : SoftwarePipelineBuilder
{
if (ffmpegState.IsHdrTonemap)
{
var filter = new TonemapCudaFilter(pixelFormat);
var filter = new TonemapCudaFilter(ffmpegState, pixelFormat);
currentState = filter.NextState(currentState);
videoStream.ResetColorParams(ColorParams.Default);
videoInputFile.FilterSteps.Add(filter);
}
else
{
var filter = new TonemapFilter(currentState, pixelFormat);
var filter = new TonemapFilter(ffmpegState, currentState, pixelFormat);
currentState = filter.NextState(currentState);
videoStream.ResetColorParams(ColorParams.Default);
videoInputFile.FilterSteps.Add(filter);
@@ -660,7 +660,7 @@ public class QsvPipelineBuilder : SoftwarePipelineBuilder
}
else
{
var filter = new TonemapFilter(currentState, pixelFormat);
var filter = new TonemapFilter(ffmpegState, currentState, pixelFormat);
currentState = filter.NextState(currentState);
videoStream.ResetColorParams(ColorParams.Default);
videoInputFile.FilterSteps.Add(filter);
@@ -105,7 +105,7 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase
SetDeinterlace(videoInputFile, context, currentState);
currentState = SetScale(videoInputFile, videoStream, desiredState, currentState);
currentState = SetTonemap(videoInputFile, videoStream, desiredState, currentState);
currentState = SetTonemap(videoInputFile, videoStream, ffmpegState, desiredState, currentState);
currentState = SetPad(videoInputFile, videoStream, desiredState, currentState);
currentState = SetCrop(videoInputFile, desiredState, currentState);
SetStillImageLoop(videoInputFile, videoStream, ffmpegState, desiredState, pipelineSteps);
@@ -321,6 +321,7 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase
private static FrameState SetTonemap(
VideoInputFile videoInputFile,
VideoStream videoStream,
FFmpegState ffmpegState,
FrameState desiredState,
FrameState currentState)
{
@@ -328,7 +329,7 @@ public class SoftwarePipelineBuilder : PipelineBuilderBase
{
foreach (IPixelFormat pixelFormat in desiredState.PixelFormat)
{
var tonemapStep = new TonemapFilter(currentState, pixelFormat);
var tonemapStep = new TonemapFilter(ffmpegState, currentState, pixelFormat);
currentState = tonemapStep.NextState(currentState);
videoStream.ResetColorParams(ColorParams.Default);
videoInputFile.FilterSteps.Add(tonemapStep);
@@ -638,14 +638,14 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder
{
if (ffmpegState.DecoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi && _ffmpegCapabilities.HasFilter(FFmpegKnownFilter.TonemapOpenCL))
{
var filter = new TonemapVaapiFilter();
var filter = new TonemapVaapiFilter(ffmpegState);
currentState = filter.NextState(currentState);
videoStream.ResetColorParams(ColorParams.Default);
videoInputFile.FilterSteps.Add(filter);
}
else
{
var filter = new TonemapFilter(currentState, pixelFormat);
var filter = new TonemapFilter(ffmpegState, currentState, pixelFormat);
currentState = filter.NextState(currentState);
videoStream.ResetColorParams(ColorParams.Default);
videoInputFile.FilterSteps.Add(filter);
+11
View File
@@ -0,0 +1,11 @@
namespace ErsatzTV.FFmpeg;
public static class TonemapAlgorithm
{
public const string Linear = "linear";
public const string Clip = "clip";
public const string Gamma = "gamma";
public const string Reinhard = "reinhard";
public const string Mobius = "mobius";
public const string Hable = "hable";
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_FFmpegProfile_TonemapAlgorithm : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "TonemapAlgorithm",
table: "FFmpegProfile",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TonemapAlgorithm",
table: "FFmpegProfile");
}
}
}
@@ -637,6 +637,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int>("ThreadCount")
.HasColumnType("int");
b.Property<int>("TonemapAlgorithm")
.HasColumnType("int");
b.Property<string>("VaapiDevice")
.HasColumnType("longtext");
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_FFmpegProfile_TonemapAlgorithm : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "TonemapAlgorithm",
table: "FFmpegProfile",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "TonemapAlgorithm",
table: "FFmpegProfile");
}
}
}
@@ -606,6 +606,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int>("ThreadCount")
.HasColumnType("INTEGER");
b.Property<int>("TonemapAlgorithm")
.HasColumnType("INTEGER");
b.Property<string>("VaapiDevice")
.HasColumnType("TEXT");
+13
View File
@@ -154,6 +154,19 @@
<MudTextField Disabled="@(_model.HardwareAcceleration != HardwareAccelerationKind.Qsv)" Label="QSV Extra Hardware Frames" @bind-Value="_model.QsvExtraHardwareFrames" For="@(() => _model.QsvExtraHardwareFrames)"/>
</MudElement>
}
else
{
<MudElement HtmlTag="div" Class="mt-3">
<MudSelect Label="Tonemap Algorithm"
@bind-Value="_model.TonemapAlgorithm"
For="@(() => _model.TonemapAlgorithm)">
@foreach (FFmpegProfileTonemapAlgorithm algorithm in Enum.GetValues<FFmpegProfileTonemapAlgorithm>())
{
<MudSelectItem Value="@algorithm">@algorithm</MudSelectItem>
}
</MudSelect>
</MudElement>
}
<MudElement HtmlTag="div" Class="mt-3">
<MudCheckBox Label="Normalize Frame Rate" @bind-Value="@_model.NormalizeFramerate" For="@(() => _model.NormalizeFramerate)"/>
</MudElement>
@@ -38,6 +38,7 @@ public class FFmpegProfileEditViewModel
VideoPreset = viewModel.VideoPreset;
AllowBFrames = viewModel.AllowBFrames;
BitDepth = viewModel.BitDepth;
TonemapAlgorithm = viewModel.TonemapAlgorithm;
}
public int AudioBitrate { get; set; }
@@ -65,6 +66,7 @@ public class FFmpegProfileEditViewModel
public string VideoPreset { get; set; }
public bool AllowBFrames { get; set; }
public FFmpegProfileBitDepth BitDepth { get; set; }
public FFmpegProfileTonemapAlgorithm TonemapAlgorithm { get; set; }
public CreateFFmpegProfile ToCreate() =>
new(
@@ -84,6 +86,7 @@ public class FFmpegProfileEditViewModel
BitDepth,
VideoBitrate,
VideoBufferSize,
TonemapAlgorithm,
AudioFormat,
AudioBitrate,
AudioBufferSize,
@@ -113,6 +116,7 @@ public class FFmpegProfileEditViewModel
BitDepth,
VideoBitrate,
VideoBufferSize,
TonemapAlgorithm,
AudioFormat,
AudioBitrate,
AudioBufferSize,