normalize bit depth (#1031)

* normalize bit depth and color for nvenc

* fix hls direct

* update changelog

* add bit depth option to ffmpeg profile
This commit is contained in:
Jason Dove
2022-11-21 20:20:07 -06:00
committed by GitHub
parent 6a640d3708
commit 7439ded43d
42 changed files with 13456 additions and 101 deletions
+2
View File
@@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix parsing song metadata from OGG audio files
### Added
- Add (required) bit depth normalization option to ffmpeg profile
- This can help if your card only supports e.g. h264 encoding, normalizing to 8 bits will allow the hardware encoder to be used
- Add audio stream selector scripts for episodes and movies
- This will let you customize which audio stream is selected for playback
- Episodes are passed the following data:
@@ -13,6 +13,7 @@ public record CreateFFmpegProfile(
int? QsvExtraHardwareFrames,
int ResolutionId,
FFmpegProfileVideoFormat VideoFormat,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileAudioFormat AudioFormat,
@@ -47,6 +47,7 @@ public class CreateFFmpegProfileHandler :
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
ResolutionId = resolutionId,
VideoFormat = request.VideoFormat,
BitDepth = request.BitDepth,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
AudioFormat = request.AudioFormat,
@@ -14,6 +14,7 @@ public record UpdateFFmpegProfile(
int? QsvExtraHardwareFrames,
int ResolutionId,
FFmpegProfileVideoFormat VideoFormat,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileAudioFormat AudioFormat,
@@ -36,6 +36,7 @@ public class
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
p.ResolutionId = update.ResolutionId;
p.VideoFormat = update.VideoFormat;
p.BitDepth = update.BitDepth;
p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize;
p.AudioFormat = update.AudioFormat;
@@ -14,6 +14,7 @@ public record FFmpegProfileViewModel(
int? QsvExtraHardwareFrames,
ResolutionViewModel Resolution,
FFmpegProfileVideoFormat VideoFormat,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileAudioFormat AudioFormat,
@@ -17,6 +17,7 @@ internal static class Mapper
profile.QsvExtraHardwareFrames,
Project(profile.Resolution),
profile.VideoFormat,
profile.BitDepth,
profile.VideoBitrate,
profile.VideoBufferSize,
profile.AudioFormat,
+24 -16
View File
@@ -113,26 +113,26 @@ public class TranscodingTests
public static InputFormat[] InputFormats =
{
new("libx264", "yuv420p"),
new("libx264", "yuvj420p"),
// new("libx264", "yuvj420p"),
new("libx264", "yuv420p10le"),
// new("libx264", "yuv444p10le"),
new("mpeg1video", "yuv420p"),
new("mpeg2video", "yuv420p"),
// new("mpeg1video", "yuv420p"),
//
// new("mpeg2video", "yuv420p"),
new("libx265", "yuv420p"),
new("libx265", "yuv420p10le"),
new("mpeg4", "yuv420p"),
new("libvpx-vp9", "yuv420p"),
// new("libaom-av1", "yuv420p")
// av1 yuv420p10le 51
new("msmpeg4v2", "yuv420p"),
new("msmpeg4v3", "yuv420p")
// new("mpeg4", "yuv420p"),
//
// new("libvpx-vp9", "yuv420p"),
//
// // new("libaom-av1", "yuv420p")
// // av1 yuv420p10le 51
//
// new("msmpeg4v2", "yuv420p"),
// new("msmpeg4v3", "yuv420p")
// wmv3 yuv420p 1
};
@@ -143,6 +143,12 @@ public class TranscodingTests
new() { Width = 1280, Height = 720 }
};
public static FFmpegProfileBitDepth[] BitDepths =
{
FFmpegProfileBitDepth.EightBit,
FFmpegProfileBitDepth.TenBit
};
public static HardwareAccelerationKind[] NoAcceleration =
{
HardwareAccelerationKind.None
@@ -187,6 +193,8 @@ public class TranscodingTests
InputFormat inputFormat,
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
Resolution profileResolution,
[ValueSource(typeof(TestData), nameof(TestData.BitDepths))]
FFmpegProfileBitDepth profileBitDepth,
[ValueSource(typeof(TestData), nameof(TestData.Paddings))]
Padding padding,
[ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))]
@@ -213,8 +221,7 @@ public class TranscodingTests
}
}
string name = GetStringSha256Hash(
$"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{watermark}_{subtitle}_{profileResolution}_{profileVideoFormat}_{profileAcceleration}");
string name = GetStringSha256Hash($"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}");
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
if (!File.Exists(file))
@@ -478,7 +485,8 @@ public class TranscodingTests
HardwareAcceleration = profileAcceleration,
VideoFormat = profileVideoFormat,
AudioFormat = FFmpegProfileAudioFormat.Aac,
DeinterlaceVideo = true
DeinterlaceVideo = true,
BitDepth = profileBitDepth
},
StreamingMode = StreamingMode.TransportStream,
SubtitleMode = subtitleMode
+1
View File
@@ -14,6 +14,7 @@ public record FFmpegProfile
public int ResolutionId { get; set; }
public Resolution Resolution { get; set; }
public FFmpegProfileVideoFormat VideoFormat { get; set; }
public FFmpegProfileBitDepth BitDepth { get; set; }
public int VideoBitrate { get; set; }
public int VideoBufferSize { get; set; }
public FFmpegProfileAudioFormat AudioFormat { get; set; }
@@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Domain;
public enum FFmpegProfileBitDepth
{
EightBit = 0,
TenBit = 1
}
@@ -14,6 +14,10 @@ public class MediaStream
public bool Forced { get; set; }
public bool AttachedPic { get; set; }
public string PixelFormat { get; set; }
public string ColorRange { get; set; }
public string ColorSpace { get; set; }
public string ColorTransfer { get; set; }
public string ColorPrimaries { get; set; }
public int BitsPerRawSample { get; set; }
public string FileName { get; set; }
public string MimeType { get; set; }
@@ -147,6 +147,11 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
videoStream.Index,
videoStream.Codec,
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
new ColorParams(
videoStream.ColorRange,
videoStream.ColorSpace,
videoStream.ColorTransfer,
videoStream.ColorPrimaries),
new FrameSize(videoVersion.Width, videoVersion.Height),
videoVersion.SampleAspectRatio,
videoVersion.DisplayAspectRatio,
@@ -208,14 +213,14 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
: Option<string>.None;
// normalize songs to yuv420p
Option<IPixelFormat> desiredPixelFormat =
videoPath == audioPath ? ffmpegVideoStream.PixelFormat : new PixelFormatYuv420P();
IPixelFormat desiredPixelFormat =
videoPath == audioPath ? playbackSettings.PixelFormat : new PixelFormatYuv420P();
var desiredState = new FrameState(
playbackSettings.RealtimeOutput,
false, // TODO: fallback filler needs to loop
videoFormat,
desiredPixelFormat,
Optional(desiredPixelFormat),
ffmpegVideoStream.SquarePixelFrameSize(
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height)),
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height),
@@ -343,6 +348,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
0,
VideoFormat.GeneratedImage,
new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p
ColorParams.Default,
new FrameSize(videoVersion.Width, videoVersion.Height),
videoVersion.SampleAspectRatio,
videoVersion.DisplayAspectRatio,
@@ -437,7 +443,19 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
{
var videoInputFile = new VideoInputFile(
inputFile,
new List<VideoStream> { new(0, string.Empty, None, FrameSize.Unknown, string.Empty, string.Empty, None, true) });
new List<VideoStream>
{
new(
0,
string.Empty,
None,
ColorParams.Default,
FrameSize.Unknown,
string.Empty,
string.Empty,
None,
true)
});
var pipelineBuilder = new PipelineBuilder(
_runtimeInfo,
@@ -517,6 +535,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
options.ImageStreamIndex.IfNone(0),
"unknown",
new PixelFormatUnknown(),
ColorParams.Default,
new FrameSize(1, 1),
string.Empty,
string.Empty,
@@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.Core.FFmpeg;
@@ -14,6 +15,7 @@ public class FFmpegPlaybackSettings
public Option<IDisplaySize> ScaledSize { get; set; }
public bool PadToDesiredResolution { get; set; }
public FFmpegProfileVideoFormat VideoFormat { get; set; }
public IPixelFormat PixelFormat { get; set; }
public Option<int> VideoBitrate { get; set; }
public Option<int> VideoBufferSize { get; set; }
public Option<int> AudioBitrate { get; set; }
@@ -20,6 +20,7 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.Core.FFmpeg;
@@ -144,6 +145,13 @@ public class FFmpegPlaybackSettingsCalculator
};
}
result.PixelFormat = ffmpegProfile.BitDepth switch
{
FFmpegProfileBitDepth.TenBit => new PixelFormatYuv420P10Le(),
_ => new PixelFormatYuv420P()
// _ => new PixelFormatYuv420P10Le()
};
result.AudioFormat = ffmpegProfile.AudioFormat;
result.AudioBitrate = ffmpegProfile.AudioBitrate;
result.AudioBufferSize = ffmpegProfile.AudioBufferSize;
@@ -399,7 +399,11 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
Index = videoStream.index,
Codec = videoStream.codec_name,
Profile = (videoStream.profile ?? string.Empty).ToLowerInvariant(),
PixelFormat = (videoStream.pix_fmt ?? string.Empty).ToLowerInvariant()
PixelFormat = (videoStream.pix_fmt ?? string.Empty).ToLowerInvariant(),
ColorRange = (videoStream.color_range ?? string.Empty).ToLowerInvariant(),
ColorSpace = (videoStream.color_space ?? string.Empty).ToLowerInvariant(),
ColorTransfer = (videoStream.color_transfer ?? string.Empty).ToLowerInvariant(),
ColorPrimaries = (videoStream.color_primaries ?? string.Empty).ToLowerInvariant()
};
if (int.TryParse(videoStream.bits_per_raw_sample, out int bitsPerRawSample))
@@ -538,6 +542,10 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
string sample_aspect_ratio,
string display_aspect_ratio,
string pix_fmt,
string color_range,
string color_space,
string color_transfer,
string color_primaries,
string field_order,
string r_frame_rate,
string bits_per_raw_sample,
@@ -26,7 +26,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
new List<VideoStream>
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "1:1", "16:9", "24", false) });
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), ColorParams.Default, new FrameSize(1920, 1080), "1:1", "16:9", "24", false) });
var audioInputFile = new AudioInputFile(
"/tmp/whatever.mkv",
@@ -99,7 +99,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
new List<VideoStream>
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "1:1", "16:9", "24", false) });
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), ColorParams.Default, new FrameSize(1920, 1080), "1:1", "16:9", "24", false) });
var audioInputFile = new AudioInputFile(
"/tmp/whatever.mkv",
@@ -198,7 +198,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
new List<VideoStream>
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "1:1", "16:9", "24", false) });
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), ColorParams.Default, new FrameSize(1920, 1080), "1:1", "16:9", "24", false) });
var audioInputFile = new AudioInputFile(
"/tmp/whatever.mkv",
@@ -216,7 +216,7 @@ public class PipelineGeneratorTests
true,
false,
VideoFormat.Copy,
new PixelFormatYuv420P(),
Option<IPixelFormat>.None,
new FrameSize(1920, 1080),
new FrameSize(1920, 1080),
false,
@@ -273,7 +273,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv",
new List<VideoStream>
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), new FrameSize(1920, 1080), "1:1", "16:9", "24", false) });
{ new(0, VideoFormat.H264, new PixelFormatYuv420P(), ColorParams.Default, new FrameSize(1920, 1080), "1:1", "16:9", "24", false) });
Option<AudioInputFile> audioInputFile = Option<AudioInputFile>.None;
@@ -341,7 +341,7 @@ public class PipelineGeneratorTests
"/test/input/file.png",
new List<VideoStream>
{
new(0, string.Empty, Option<IPixelFormat>.None, FrameSize.Unknown, string.Empty, string.Empty, Option<string>.None, true)
new(0, string.Empty, Option<IPixelFormat>.None, ColorParams.Default, FrameSize.Unknown, string.Empty, string.Empty, Option<string>.None, true)
});
var pipelineBuilder = new PipelineBuilder(
+14
View File
@@ -0,0 +1,14 @@
namespace ErsatzTV.FFmpeg;
public record ColorParams(string ColorRange, string ColorSpace, string ColorTransfer, string ColorPrimaries)
{
public static readonly ColorParams Default = new("tv", "bt709", "bt709", "bt709");
public bool IsHdr => ColorTransfer is "arib-std-b67" or "smpte2084";
public bool IsUnknown => string.IsNullOrWhiteSpace(ColorSpace) &&
string.IsNullOrWhiteSpace(ColorTransfer) &&
string.IsNullOrWhiteSpace(ColorPrimaries);
public bool IsBt709 => this == Default;
}
+14 -2
View File
@@ -1,4 +1,6 @@
using ErsatzTV.FFmpeg.Environment;
using ErsatzTV.FFmpeg.Encoder;
using ErsatzTV.FFmpeg.Environment;
using ErsatzTV.FFmpeg.Filter;
using ErsatzTV.FFmpeg.Option;
namespace ErsatzTV.FFmpeg;
@@ -80,8 +82,18 @@ public static class CommandGenerator
arguments.AddRange(step.FilterOptions);
}
foreach (IPipelineStep step in pipelineSteps.Filter(s => s is not StreamSeekFilterOption))
// rearrange complex filter output options directly after video encoder
var sortedSteps = pipelineSteps.Filter(s => s is not StreamSeekFilterOption && s is not ComplexFilter).ToList();
Option<IPipelineStep> maybeComplex = pipelineSteps.Find(s => s is ComplexFilter);
foreach (IPipelineStep complex in maybeComplex)
{
int encoderIndex = sortedSteps.FindIndex(s => s is EncoderBase { Kind: StreamKind.Video });
sortedSteps.Insert(encoderIndex + 1, complex);
}
foreach (IPipelineStep step in sortedSteps)
{
arguments.AddRange(step.OutputOptions);
}
@@ -44,6 +44,6 @@ public class EncoderH264Nvenc : EncoderBase
public override FrameState NextState(FrameState currentState) => currentState with
{
VideoFormat = VideoFormat.H264,
FrameDataLocation = FrameDataLocation.Hardware
// FrameDataLocation = FrameDataLocation.Hardware
};
}
@@ -44,6 +44,6 @@ public class EncoderHevcNvenc : EncoderBase
public override FrameState NextState(FrameState currentState) => currentState with
{
VideoFormat = VideoFormat.Hevc,
FrameDataLocation = FrameDataLocation.Hardware
// FrameDataLocation = FrameDataLocation.Hardware
};
}
@@ -0,0 +1,43 @@
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.Filter;
public class ColorspaceFilter : BaseFilter
{
private readonly VideoStream _videoStream;
private readonly IPixelFormat _desiredPixelFormat;
public ColorspaceFilter(VideoStream videoStream, IPixelFormat desiredPixelFormat)
{
_videoStream = videoStream;
_desiredPixelFormat = desiredPixelFormat;
}
public override FrameState NextState(FrameState currentState) =>
currentState with
{
PixelFormat = Some(_desiredPixelFormat),
FrameDataLocation = FrameDataLocation.Software
};
public override string Filter
{
get
{
string setParams = string.Empty;
if (_videoStream.ColorParams.IsUnknown)
{
setParams = "setparams=range=tv:colorspace=bt709:color_trc=bt709:color_primaries=bt709";
}
string colorspace = _desiredPixelFormat.BitDepth switch
{
10 when !_videoStream.ColorParams.IsUnknown => "colorspace=all=bt709:format=yuv420p10",
8 when !_videoStream.ColorParams.IsUnknown => "colorspace=all=bt709:format=yuv420p",
_ => string.Empty
};
return string.Join(',', new[] { setParams, colorspace }.Filter(s => !string.IsNullOrWhiteSpace(s)));
}
}
}
+120 -46
View File
@@ -12,9 +12,11 @@ public class ComplexFilter : IPipelineStep
private readonly ILogger _logger;
private readonly Option<AudioInputFile> _maybeAudioInputFile;
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
private readonly Option<IPixelFormat> _desiredPixelFormat;
private readonly Option<VideoInputFile> _maybeVideoInputFile;
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
private readonly FrameSize _resolution;
private readonly List<string> _outputOptions;
public ComplexFilter(
FrameState currentState,
@@ -23,6 +25,7 @@ public class ComplexFilter : IPipelineStep
Option<AudioInputFile> maybeAudioInputFile,
Option<WatermarkInputFile> maybeWatermarkInputFile,
Option<SubtitleInputFile> maybeSubtitleInputFile,
Option<IPixelFormat> desiredPixelFormat,
FrameSize resolution,
string fontsDir,
ILogger logger)
@@ -33,20 +36,25 @@ public class ComplexFilter : IPipelineStep
_maybeAudioInputFile = maybeAudioInputFile;
_maybeWatermarkInputFile = maybeWatermarkInputFile;
_maybeSubtitleInputFile = maybeSubtitleInputFile;
_desiredPixelFormat = desiredPixelFormat;
_resolution = resolution;
_fontsDir = fontsDir;
_logger = logger;
_outputOptions = new List<string>();
}
public IList<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>();
public IList<string> GlobalOptions => Array.Empty<string>();
public IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>();
public IList<string> FilterOptions => Arguments();
public IList<string> OutputOptions => Array.Empty<string>();
public IList<string> OutputOptions => _outputOptions;
public FrameState NextState(FrameState currentState) => currentState;
private IList<string> Arguments()
{
var state = _currentState;
var audioLabel = "0:a";
var videoLabel = "0:v";
string watermarkLabel;
@@ -60,6 +68,7 @@ public class ComplexFilter : IPipelineStep
string watermarkOverlayFilterComplex = string.Empty;
string subtitleFilterComplex = string.Empty;
string subtitleOverlayFilterComplex = string.Empty;
string pixelFormatFilterComplex = string.Empty;
var distinctPaths = new List<string>();
foreach ((string path, _) in _maybeVideoInputFile)
@@ -152,54 +161,56 @@ public class ComplexFilter : IPipelineStep
}
foreach (VideoInputFile videoInputFile in _maybeVideoInputFile)
foreach (VideoStream stream in videoInputFile.VideoStreams)
{
foreach (VideoStream stream in videoInputFile.VideoStreams)
IPipelineFilterStep overlayFilter = AvailableWatermarkOverlayFilters.ForAcceleration(
_ffmpegState.EncoderHardwareAccelerationMode,
watermarkInputFile.DesiredState,
_resolution,
stream.SquarePixelFrameSize(_resolution),
_logger);
if (overlayFilter.Filter != string.Empty)
{
IPipelineFilterStep overlayFilter = AvailableWatermarkOverlayFilters.ForAcceleration(
_ffmpegState.EncoderHardwareAccelerationMode,
watermarkInputFile.DesiredState,
_resolution,
stream.SquarePixelFrameSize(_resolution),
_logger);
state = overlayFilter.NextState(state);
if (overlayFilter.Filter != string.Empty)
string tempVideoLabel = string.IsNullOrWhiteSpace(videoFilterComplex)
? $"[{videoLabel}]"
: videoLabel;
// vaapi uses software overlay and needs to upload
// videotoolbox seems to require a hwupload for hevc
// also wait to upload if a subtitle overlay is coming
string uploadDownloadFilter = string.Empty;
if (_maybeSubtitleInputFile.IsNone &&
(_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi ||
_ffmpegState.EncoderHardwareAccelerationMode ==
HardwareAccelerationMode.VideoToolbox &&
state.VideoFormat == VideoFormat.Hevc))
{
string tempVideoLabel = string.IsNullOrWhiteSpace(videoFilterComplex)
? $"[{videoLabel}]"
: videoLabel;
// vaapi uses software overlay and needs to upload
// videotoolbox seems to require a hwupload for hevc
// also wait to upload if a subtitle overlay is coming
string uploadDownloadFilter = string.Empty;
if (_maybeSubtitleInputFile.IsNone &&
(_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi ||
_ffmpegState.EncoderHardwareAccelerationMode ==
HardwareAccelerationMode.VideoToolbox &&
_currentState.VideoFormat == VideoFormat.Hevc))
{
uploadDownloadFilter = new HardwareUploadFilter(_ffmpegState).Filter;
}
if (_maybeSubtitleInputFile.Map(s => !s.IsImageBased).IfNone(false) &&
_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Vaapi &&
_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.VideoToolbox &&
_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Amf)
{
uploadDownloadFilter = new HardwareDownloadFilter(_currentState).Filter;
}
if (!string.IsNullOrWhiteSpace(uploadDownloadFilter))
{
uploadDownloadFilter = "," + uploadDownloadFilter;
}
watermarkOverlayFilterComplex =
$"{tempVideoLabel}{watermarkLabel}{overlayFilter.Filter}{uploadDownloadFilter}[vf]";
// change the mapped label
videoLabel = "[vf]";
uploadDownloadFilter = new HardwareUploadFilter(_ffmpegState).Filter;
state = state with { FrameDataLocation = FrameDataLocation.Hardware };
}
if (_maybeSubtitleInputFile.Map(s => !s.IsImageBased).IfNone(false) &&
_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Vaapi &&
_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.VideoToolbox &&
_ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Amf)
{
uploadDownloadFilter = new HardwareDownloadFilter(state).Filter;
state = state with { FrameDataLocation = FrameDataLocation.Software };
}
if (!string.IsNullOrWhiteSpace(uploadDownloadFilter))
{
uploadDownloadFilter = "," + uploadDownloadFilter;
}
watermarkOverlayFilterComplex =
$"{tempVideoLabel}{watermarkLabel}{overlayFilter.Filter}{uploadDownloadFilter}[vf]";
// change the mapped label
videoLabel = "[vf]";
}
}
}
@@ -230,12 +241,14 @@ public class ComplexFilter : IPipelineStep
{
IPipelineFilterStep overlayFilter = AvailableSubtitleOverlayFilters.ForAcceleration(
_ffmpegState.EncoderHardwareAccelerationMode);
state = overlayFilter.NextState(state);
filter = overlayFilter.Filter;
}
else
{
subtitleLabel = string.Empty;
var subtitlesFilter = new SubtitlesFilter(_fontsDir, subtitleInputFile);
state = subtitlesFilter.NextState(state);
filter = subtitlesFilter.Filter;
}
@@ -250,9 +263,13 @@ public class ComplexFilter : IPipelineStep
string uploadFilter = string.Empty;
if (_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi
|| _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox &&
_currentState.VideoFormat == VideoFormat.Hevc)
state.VideoFormat == VideoFormat.Hevc)
{
uploadFilter = new HardwareUploadFilter(_ffmpegState).Filter;
if (!string.IsNullOrWhiteSpace(uploadFilter))
{
state = state with { FrameDataLocation = FrameDataLocation.Hardware };
}
}
if (!string.IsNullOrWhiteSpace(uploadFilter))
@@ -268,6 +285,62 @@ public class ComplexFilter : IPipelineStep
}
}
foreach (VideoStream videoStream in _maybeVideoInputFile.Map(vif => vif.VideoStreams).Flatten())
foreach (IPixelFormat pixelFormat in _desiredPixelFormat)
{
_logger.LogDebug("Desired pixel format {PixelFormat}", pixelFormat);
string tempVideoLabel = videoLabel.StartsWith("[") && videoLabel.EndsWith("]")
? videoLabel
: $"[{videoLabel}]";
// normalize pixel format and color params
string filter = string.Empty;
if (!videoStream.ColorParams.IsBt709)
{
_logger.LogDebug("Adding colorspace filter");
filter = new ColorspaceFilter(videoStream, pixelFormat).Filter;
}
if (state.PixelFormat.Map(f => f.FFmpegName) != pixelFormat.FFmpegName)
{
_logger.LogDebug(
"Format {A} doesn't equal {B}",
state.PixelFormat.Map(f => f.FFmpegName),
pixelFormat.FFmpegName);
if (videoStream.ColorParams.IsHdr)
{
_logger.LogWarning("HDR tone mapping is not implemented; colors may appear incorrect");
continue;
}
if (state.FrameDataLocation == FrameDataLocation.Software)
{
_logger.LogDebug("Frame data location is SOFTWARE");
_outputOptions.AddRange(new[] { "-pix_fmt", pixelFormat.FFmpegName });
}
else
{
_logger.LogDebug("Frame data location is HARDWARE");
filter = _ffmpegState.EncoderHardwareAccelerationMode switch
{
HardwareAccelerationMode.Nvenc => $"{filter},scale_cuda=format={pixelFormat.FFmpegName}",
_ => filter
};
}
}
if (!string.IsNullOrWhiteSpace(filter))
{
pixelFormatFilterComplex = $"{tempVideoLabel}{filter}[vpf]";
// change the mapped label
videoLabel = "[vpf]";
}
}
var filterComplex = string.Join(
";",
new[]
@@ -277,7 +350,8 @@ public class ComplexFilter : IPipelineStep
watermarkFilterComplex,
subtitleFilterComplex,
watermarkOverlayFilterComplex,
subtitleOverlayFilterComplex
subtitleOverlayFilterComplex,
pixelFormatFilterComplex
}.Where(
s => !string.IsNullOrWhiteSpace(s)));
@@ -19,5 +19,6 @@ public class OverlayWatermarkCudaFilter : OverlayWatermarkFilter
public override string Filter => $"overlay_cuda={Position}";
public override FrameState NextState(FrameState currentState) => currentState;
public override FrameState NextState(FrameState currentState) =>
currentState with { FrameDataLocation = FrameDataLocation.Hardware };
}
@@ -46,7 +46,8 @@ public class OverlayWatermarkFilter : BaseFilter
}
}
public override FrameState NextState(FrameState currentState) => currentState;
public override FrameState NextState(FrameState currentState) =>
currentState with { FrameDataLocation = FrameDataLocation.Software };
private WatermarkMargins NormalMargins()
{
@@ -19,5 +19,6 @@ public class OverlayWatermarkQsvFilter : OverlayWatermarkFilter
public override string Filter => $"overlay_qsv={Position}";
public override FrameState NextState(FrameState currentState) => currentState;
public override FrameState NextState(FrameState currentState) =>
currentState with { FrameDataLocation = FrameDataLocation.Hardware };
}
+2 -1
View File
@@ -40,5 +40,6 @@ public class SubtitlesFilter : BaseFilter
}
}
public override FrameState NextState(FrameState currentState) => currentState;
public override FrameState NextState(FrameState currentState) =>
currentState with { FrameDataLocation = FrameDataLocation.Software };
}
+45
View File
@@ -0,0 +1,45 @@
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg.Filter;
public class TonemapFilter : BaseFilter
{
private readonly FrameState _currentState;
private readonly IPixelFormat _desiredPixelFormat;
public TonemapFilter(FrameState currentState, IPixelFormat desiredPixelFormat)
{
_currentState = currentState;
_desiredPixelFormat = desiredPixelFormat;
}
public override FrameState NextState(FrameState currentState) =>
currentState with
{
PixelFormat = Some(_desiredPixelFormat),
FrameDataLocation = FrameDataLocation.Software
};
public override string Filter
{
get
{
string pixelFormat = _currentState.PixelFormat.Match(pf => pf.FFmpegName, () => string.Empty);
var tonemap =
$"setparams=colorspace=bt2020c,zscale=transfer=linear,tonemap=hable,zscale=transfer=bt709,format={_desiredPixelFormat.FFmpegName}";
if (_currentState.FrameDataLocation == FrameDataLocation.Hardware)
{
if (!string.IsNullOrWhiteSpace(pixelFormat))
{
return $"hwdownload,format={pixelFormat},{tonemap}";
}
return $"hwdownload,{tonemap}";
}
return tonemap;
}
}
}
+1
View File
@@ -18,6 +18,7 @@ public record ConcatInputFile(string Url, FrameSize Resolution) : InputFile(
0,
string.Empty,
Option<IPixelFormat>.None,
ColorParams.Default,
Resolution,
string.Empty,
string.Empty,
+2 -2
View File
@@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations;
using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg;
@@ -14,6 +13,7 @@ public record VideoStream(
int Index,
string Codec,
Option<IPixelFormat> PixelFormat,
ColorParams ColorParams,
FrameSize FrameSize,
string SampleAspectRatio,
string DisplayAspectRatio,
+3 -14
View File
@@ -123,6 +123,8 @@ public class PipelineBuilder
public FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState)
{
var originalDesiredPixelFormat = desiredState.PixelFormat;
if (ffmpegState.Start.Exists(s => s > TimeSpan.Zero) && desiredState.Realtime)
{
_logger.LogInformation(
@@ -520,20 +522,6 @@ public class PipelineBuilder
_pipelineSteps.Add(step);
}
}
foreach (IPixelFormat desiredPixelFormat in desiredState.PixelFormat)
{
if (currentState.PixelFormat.Map(pf => pf.FFmpegName) != desiredPixelFormat.FFmpegName)
{
// qsv doesn't seem to like this
if (ffmpegState.EncoderHardwareAccelerationMode != HardwareAccelerationMode.Qsv)
{
IPipelineStep step = new PixelFormatOutputOption(desiredPixelFormat);
currentState = step.NextState(currentState);
_pipelineSteps.Add(step);
}
}
}
}
// TODO: if all video filters are software, use software pixel format for hwaccel output
@@ -787,6 +775,7 @@ public class PipelineBuilder
_audioInputFile,
_watermarkInputFile,
_subtitleInputFile,
originalDesiredPixelFormat,
currentState.PaddedSize,
_fontsFolder,
_logger);
@@ -170,6 +170,10 @@ public class MetadataRepository : IMetadataRepository
existingStream.Forced = incomingStream.Forced;
existingStream.AttachedPic = incomingStream.AttachedPic;
existingStream.PixelFormat = incomingStream.PixelFormat;
existingStream.ColorRange = incomingStream.ColorRange;
existingStream.ColorSpace = incomingStream.ColorSpace;
existingStream.ColorTransfer = incomingStream.ColorTransfer;
existingStream.ColorPrimaries = incomingStream.ColorPrimaries;
existingStream.BitsPerRawSample = incomingStream.BitsPerRawSample;
existingStream.FileName = incomingStream.FileName;
existingStream.MimeType = incomingStream.MimeType;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_FFmpegProfileBitDepth : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "BitDepth",
table: "FFmpegProfile",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BitDepth",
table: "FFmpegProfile");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Add_MediaStreamColor : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ColorPrimaries",
table: "MediaStream",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ColorRange",
table: "MediaStream",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ColorSpace",
table: "MediaStream",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ColorTransfer",
table: "MediaStream",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ColorPrimaries",
table: "MediaStream");
migrationBuilder.DropColumn(
name: "ColorRange",
table: "MediaStream");
migrationBuilder.DropColumn(
name: "ColorSpace",
table: "MediaStream");
migrationBuilder.DropColumn(
name: "ColorTransfer",
table: "MediaStream");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Migrations
{
public partial class Reset_AllStatistics20221103 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// reset all statistics EXCEPT songs since they don't contain video streams
migrationBuilder.Sql("UPDATE MediaVersion SET DateUpdated = '0001-01-01 00:00:00' WHERE SongId IS NULL");
migrationBuilder.Sql("UPDATE LibraryFolder SET Etag = NULL WHERE Id IN (SELECT LF.Id FROM LibraryFolder LF INNER JOIN LibraryPath LP on LP.Id = LF.LibraryPathId INNER JOIN Library L on L.Id = LP.LibraryId WHERE MediaKind != 5)");
migrationBuilder.Sql("UPDATE EmbyMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbySeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE EmbyEpisode SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinMovie SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinShow SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinSeason SET Etag = NULL");
migrationBuilder.Sql("UPDATE JellyfinEpisode SET Etag = NULL");
migrationBuilder.Sql("UPDATE LibraryPath SET LastScan = '0001-01-01 00:00:00' WHERE Id IN (SELECT LP.Id FROM LibraryPath LP INNER JOIN Library L on L.Id = LP.LibraryId WHERE MediaKind != 5)");
migrationBuilder.Sql("UPDATE Library SET LastScan = '0001-01-01 00:00:00' WHERE MediaKind != 5");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
@@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.9");
modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{
@@ -548,6 +548,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("AudioSampleRate")
.HasColumnType("INTEGER");
b.Property<int>("BitDepth")
.HasColumnType("INTEGER");
b.Property<bool?>("DeinterlaceVideo")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
@@ -961,6 +964,18 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("Codec")
.HasColumnType("TEXT");
b.Property<string>("ColorPrimaries")
.HasColumnType("TEXT");
b.Property<string>("ColorRange")
.HasColumnType("TEXT");
b.Property<string>("ColorSpace")
.HasColumnType("TEXT");
b.Property<string>("ColorTransfer")
.HasColumnType("TEXT");
b.Property<bool>("Default")
.HasColumnType("INTEGER");
+2 -2
View File
@@ -27,8 +27,8 @@ Global
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Release|Any CPU.Build.0 = Release|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.ActiveCfg = Debug No Sync|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.Build.0 = Debug No Sync|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.ActiveCfg = Debug No Sync|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.Build.0 = Debug No Sync|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+1
View File
@@ -60,6 +60,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=setsar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=showtitle/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=strm/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tonemap/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Trakt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=transcoded/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tvdb/@EntryIndexedValue">True</s:Boolean>
+4 -1
View File
@@ -4,7 +4,6 @@
@using ErsatzTV.Application.Resolutions
@using ErsatzTV.Core.FFmpeg
@using ErsatzTV.Application.FFmpegProfiles
@using ErsatzTV.Infrastructure.Runtime
@using System.Runtime.InteropServices
@implements IDisposable
@inject NavigationManager _navigationManager
@@ -48,6 +47,10 @@
<MudSelectItem Value="@FFmpegProfileVideoFormat.Hevc">hevc</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileVideoFormat.Mpeg2Video">mpeg-2</MudSelectItem>
</MudSelect>
<MudSelect Label="Bit Depth" @bind-Value="_model.BitDepth" For="@(() => _model.BitDepth)">
<MudSelectItem Value="@FFmpegProfileBitDepth.EightBit">8-bit</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileBitDepth.TenBit">10-bit</MudSelectItem>
</MudSelect>
<MudElement HtmlTag="div" Class="mt-3">
<MudTextField Label="Bitrate" @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudElement>
@@ -32,6 +32,7 @@ public class FFmpegProfileEditViewModel
VideoBitrate = viewModel.VideoBitrate;
VideoBufferSize = viewModel.VideoBufferSize;
VideoFormat = viewModel.VideoFormat;
BitDepth = viewModel.BitDepth;
}
public int AudioBitrate { get; set; }
@@ -53,6 +54,7 @@ public class FFmpegProfileEditViewModel
public int VideoBitrate { get; set; }
public int VideoBufferSize { get; set; }
public FFmpegProfileVideoFormat VideoFormat { get; set; }
public FFmpegProfileBitDepth BitDepth { get; set; }
public CreateFFmpegProfile ToCreate() =>
new(
@@ -64,6 +66,7 @@ public class FFmpegProfileEditViewModel
QsvExtraHardwareFrames,
Resolution.Id,
VideoFormat,
BitDepth,
VideoBitrate,
VideoBufferSize,
AudioFormat,
@@ -87,6 +90,7 @@ public class FFmpegProfileEditViewModel
QsvExtraHardwareFrames,
Resolution.Id,
VideoFormat,
BitDepth,
VideoBitrate,
VideoBufferSize,
AudioFormat,