Browse Source

normalize bit depth (#1031)

* normalize bit depth and color for nvenc

* fix hls direct

* update changelog

* add bit depth option to ffmpeg profile
pull/1032/head
Jason Dove 4 years ago committed by GitHub
parent
commit
7439ded43d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 1
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfile.cs
  3. 1
      ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
  4. 1
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfile.cs
  5. 1
      ErsatzTV.Application/FFmpegProfiles/Commands/UpdateFFmpegProfileHandler.cs
  6. 1
      ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs
  7. 1
      ErsatzTV.Application/FFmpegProfiles/Mapper.cs
  8. 40
      ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs
  9. 1
      ErsatzTV.Core/Domain/FFmpegProfile.cs
  10. 7
      ErsatzTV.Core/Domain/FFmpegProfileBitDepth.cs
  11. 4
      ErsatzTV.Core/Domain/MediaItem/MediaStream.cs
  12. 27
      ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs
  13. 2
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs
  14. 8
      ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs
  15. 10
      ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs
  16. 12
      ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs
  17. 14
      ErsatzTV.FFmpeg/ColorParams.cs
  18. 16
      ErsatzTV.FFmpeg/CommandGenerator.cs
  19. 2
      ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs
  20. 2
      ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs
  21. 43
      ErsatzTV.FFmpeg/Filter/ColorspaceFilter.cs
  22. 168
      ErsatzTV.FFmpeg/Filter/ComplexFilter.cs
  23. 3
      ErsatzTV.FFmpeg/Filter/Cuda/OverlayWatermarkCudaFilter.cs
  24. 3
      ErsatzTV.FFmpeg/Filter/OverlayWatermarkFilter.cs
  25. 3
      ErsatzTV.FFmpeg/Filter/Qsv/OverlayWatermarkQsvFilter.cs
  26. 3
      ErsatzTV.FFmpeg/Filter/SubtitlesFilter.cs
  27. 45
      ErsatzTV.FFmpeg/Filter/TonemapFilter.cs
  28. 1
      ErsatzTV.FFmpeg/InputFile.cs
  29. 4
      ErsatzTV.FFmpeg/MediaStream.cs
  30. 17
      ErsatzTV.FFmpeg/PipelineBuilder.cs
  31. 4
      ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs
  32. 4315
      ErsatzTV.Infrastructure/Migrations/20221029131840_Add_FFmpegProfileBitDepth.Designer.cs
  33. 26
      ErsatzTV.Infrastructure/Migrations/20221029131840_Add_FFmpegProfileBitDepth.cs
  34. 4327
      ErsatzTV.Infrastructure/Migrations/20221103002717_Add_MediaStreamColor.Designer.cs
  35. 55
      ErsatzTV.Infrastructure/Migrations/20221103002717_Add_MediaStreamColor.cs
  36. 4327
      ErsatzTV.Infrastructure/Migrations/20221104021107_Reset_AllStatistics20221103.Designer.cs
  37. 31
      ErsatzTV.Infrastructure/Migrations/20221104021107_Reset_AllStatistics20221103.cs
  38. 17
      ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs
  39. 4
      ErsatzTV.sln
  40. 1
      ErsatzTV.sln.DotSettings
  41. 5
      ErsatzTV/Pages/FFmpegEditor.razor
  42. 4
      ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

2
CHANGELOG.md

@ -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 - Fix parsing song metadata from OGG audio files
### Added ### 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 - Add audio stream selector scripts for episodes and movies
- This will let you customize which audio stream is selected for playback - This will let you customize which audio stream is selected for playback
- Episodes are passed the following data: - Episodes are passed the following data:

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

@ -13,6 +13,7 @@ public record CreateFFmpegProfile(
int? QsvExtraHardwareFrames, int? QsvExtraHardwareFrames,
int ResolutionId, int ResolutionId,
FFmpegProfileVideoFormat VideoFormat, FFmpegProfileVideoFormat VideoFormat,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate, int VideoBitrate,
int VideoBufferSize, int VideoBufferSize,
FFmpegProfileAudioFormat AudioFormat, FFmpegProfileAudioFormat AudioFormat,

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

@ -47,6 +47,7 @@ public class CreateFFmpegProfileHandler :
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames, QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
ResolutionId = resolutionId, ResolutionId = resolutionId,
VideoFormat = request.VideoFormat, VideoFormat = request.VideoFormat,
BitDepth = request.BitDepth,
VideoBitrate = request.VideoBitrate, VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize, VideoBufferSize = request.VideoBufferSize,
AudioFormat = request.AudioFormat, AudioFormat = request.AudioFormat,

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

@ -14,6 +14,7 @@ public record UpdateFFmpegProfile(
int? QsvExtraHardwareFrames, int? QsvExtraHardwareFrames,
int ResolutionId, int ResolutionId,
FFmpegProfileVideoFormat VideoFormat, FFmpegProfileVideoFormat VideoFormat,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate, int VideoBitrate,
int VideoBufferSize, int VideoBufferSize,
FFmpegProfileAudioFormat AudioFormat, FFmpegProfileAudioFormat AudioFormat,

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

@ -36,6 +36,7 @@ public class
p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames; p.QsvExtraHardwareFrames = update.QsvExtraHardwareFrames;
p.ResolutionId = update.ResolutionId; p.ResolutionId = update.ResolutionId;
p.VideoFormat = update.VideoFormat; p.VideoFormat = update.VideoFormat;
p.BitDepth = update.BitDepth;
p.VideoBitrate = update.VideoBitrate; p.VideoBitrate = update.VideoBitrate;
p.VideoBufferSize = update.VideoBufferSize; p.VideoBufferSize = update.VideoBufferSize;
p.AudioFormat = update.AudioFormat; p.AudioFormat = update.AudioFormat;

1
ErsatzTV.Application/FFmpegProfiles/FFmpegProfileViewModel.cs

@ -14,6 +14,7 @@ public record FFmpegProfileViewModel(
int? QsvExtraHardwareFrames, int? QsvExtraHardwareFrames,
ResolutionViewModel Resolution, ResolutionViewModel Resolution,
FFmpegProfileVideoFormat VideoFormat, FFmpegProfileVideoFormat VideoFormat,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate, int VideoBitrate,
int VideoBufferSize, int VideoBufferSize,
FFmpegProfileAudioFormat AudioFormat, FFmpegProfileAudioFormat AudioFormat,

1
ErsatzTV.Application/FFmpegProfiles/Mapper.cs

@ -17,6 +17,7 @@ internal static class Mapper
profile.QsvExtraHardwareFrames, profile.QsvExtraHardwareFrames,
Project(profile.Resolution), Project(profile.Resolution),
profile.VideoFormat, profile.VideoFormat,
profile.BitDepth,
profile.VideoBitrate, profile.VideoBitrate,
profile.VideoBufferSize, profile.VideoBufferSize,
profile.AudioFormat, profile.AudioFormat,

40
ErsatzTV.Core.Tests/FFmpeg/TranscodingTests.cs

@ -113,26 +113,26 @@ public class TranscodingTests
public static InputFormat[] InputFormats = public static InputFormat[] InputFormats =
{ {
new("libx264", "yuv420p"), new("libx264", "yuv420p"),
new("libx264", "yuvj420p"), // new("libx264", "yuvj420p"),
new("libx264", "yuv420p10le"), new("libx264", "yuv420p10le"),
// new("libx264", "yuv444p10le"), // new("libx264", "yuv444p10le"),
new("mpeg1video", "yuv420p"), // new("mpeg1video", "yuv420p"),
//
new("mpeg2video", "yuv420p"), // new("mpeg2video", "yuv420p"),
new("libx265", "yuv420p"), new("libx265", "yuv420p"),
new("libx265", "yuv420p10le"), new("libx265", "yuv420p10le"),
new("mpeg4", "yuv420p"), // new("mpeg4", "yuv420p"),
//
new("libvpx-vp9", "yuv420p"), // new("libvpx-vp9", "yuv420p"),
//
// new("libaom-av1", "yuv420p") // // new("libaom-av1", "yuv420p")
// av1 yuv420p10le 51 // // av1 yuv420p10le 51
//
new("msmpeg4v2", "yuv420p"), // new("msmpeg4v2", "yuv420p"),
new("msmpeg4v3", "yuv420p") // new("msmpeg4v3", "yuv420p")
// wmv3 yuv420p 1 // wmv3 yuv420p 1
}; };
@ -143,6 +143,12 @@ public class TranscodingTests
new() { Width = 1280, Height = 720 } new() { Width = 1280, Height = 720 }
}; };
public static FFmpegProfileBitDepth[] BitDepths =
{
FFmpegProfileBitDepth.EightBit,
FFmpegProfileBitDepth.TenBit
};
public static HardwareAccelerationKind[] NoAcceleration = public static HardwareAccelerationKind[] NoAcceleration =
{ {
HardwareAccelerationKind.None HardwareAccelerationKind.None
@ -187,6 +193,8 @@ public class TranscodingTests
InputFormat inputFormat, InputFormat inputFormat,
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))] [ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
Resolution profileResolution, Resolution profileResolution,
[ValueSource(typeof(TestData), nameof(TestData.BitDepths))]
FFmpegProfileBitDepth profileBitDepth,
[ValueSource(typeof(TestData), nameof(TestData.Paddings))] [ValueSource(typeof(TestData), nameof(TestData.Paddings))]
Padding padding, Padding padding,
[ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))] [ValueSource(typeof(TestData), nameof(TestData.VideoScanKinds))]
@ -213,8 +221,7 @@ public class TranscodingTests
} }
} }
string name = GetStringSha256Hash( string name = GetStringSha256Hash($"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}");
$"{inputFormat.Encoder}_{inputFormat.PixelFormat}_{videoScanKind}_{padding}_{watermark}_{subtitle}_{profileResolution}_{profileVideoFormat}_{profileAcceleration}");
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv"); string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
if (!File.Exists(file)) if (!File.Exists(file))
@ -478,7 +485,8 @@ public class TranscodingTests
HardwareAcceleration = profileAcceleration, HardwareAcceleration = profileAcceleration,
VideoFormat = profileVideoFormat, VideoFormat = profileVideoFormat,
AudioFormat = FFmpegProfileAudioFormat.Aac, AudioFormat = FFmpegProfileAudioFormat.Aac,
DeinterlaceVideo = true DeinterlaceVideo = true,
BitDepth = profileBitDepth
}, },
StreamingMode = StreamingMode.TransportStream, StreamingMode = StreamingMode.TransportStream,
SubtitleMode = subtitleMode SubtitleMode = subtitleMode

1
ErsatzTV.Core/Domain/FFmpegProfile.cs

@ -14,6 +14,7 @@ public record FFmpegProfile
public int ResolutionId { get; set; } public int ResolutionId { get; set; }
public Resolution Resolution { get; set; } public Resolution Resolution { get; set; }
public FFmpegProfileVideoFormat VideoFormat { get; set; } public FFmpegProfileVideoFormat VideoFormat { get; set; }
public FFmpegProfileBitDepth BitDepth { get; set; }
public int VideoBitrate { get; set; } public int VideoBitrate { get; set; }
public int VideoBufferSize { get; set; } public int VideoBufferSize { get; set; }
public FFmpegProfileAudioFormat AudioFormat { get; set; } public FFmpegProfileAudioFormat AudioFormat { get; set; }

7
ErsatzTV.Core/Domain/FFmpegProfileBitDepth.cs

@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Domain;
public enum FFmpegProfileBitDepth
{
EightBit = 0,
TenBit = 1
}

4
ErsatzTV.Core/Domain/MediaItem/MediaStream.cs

@ -14,6 +14,10 @@ public class MediaStream
public bool Forced { get; set; } public bool Forced { get; set; }
public bool AttachedPic { get; set; } public bool AttachedPic { get; set; }
public string PixelFormat { 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 int BitsPerRawSample { get; set; }
public string FileName { get; set; } public string FileName { get; set; }
public string MimeType { get; set; } public string MimeType { get; set; }

27
ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs

@ -147,6 +147,11 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
videoStream.Index, videoStream.Index,
videoStream.Codec, videoStream.Codec,
AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger), AvailablePixelFormats.ForPixelFormat(videoStream.PixelFormat, _logger),
new ColorParams(
videoStream.ColorRange,
videoStream.ColorSpace,
videoStream.ColorTransfer,
videoStream.ColorPrimaries),
new FrameSize(videoVersion.Width, videoVersion.Height), new FrameSize(videoVersion.Width, videoVersion.Height),
videoVersion.SampleAspectRatio, videoVersion.SampleAspectRatio,
videoVersion.DisplayAspectRatio, videoVersion.DisplayAspectRatio,
@ -208,14 +213,14 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
: Option<string>.None; : Option<string>.None;
// normalize songs to yuv420p // normalize songs to yuv420p
Option<IPixelFormat> desiredPixelFormat = IPixelFormat desiredPixelFormat =
videoPath == audioPath ? ffmpegVideoStream.PixelFormat : new PixelFormatYuv420P(); videoPath == audioPath ? playbackSettings.PixelFormat : new PixelFormatYuv420P();
var desiredState = new FrameState( var desiredState = new FrameState(
playbackSettings.RealtimeOutput, playbackSettings.RealtimeOutput,
false, // TODO: fallback filler needs to loop false, // TODO: fallback filler needs to loop
videoFormat, videoFormat,
desiredPixelFormat, Optional(desiredPixelFormat),
ffmpegVideoStream.SquarePixelFrameSize( ffmpegVideoStream.SquarePixelFrameSize(
new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height)), new FrameSize(channel.FFmpegProfile.Resolution.Width, channel.FFmpegProfile.Resolution.Height)),
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, 0,
VideoFormat.GeneratedImage, VideoFormat.GeneratedImage,
new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p new PixelFormatUnknown(), // leave this unknown so we convert to desired yuv420p
ColorParams.Default,
new FrameSize(videoVersion.Width, videoVersion.Height), new FrameSize(videoVersion.Width, videoVersion.Height),
videoVersion.SampleAspectRatio, videoVersion.SampleAspectRatio,
videoVersion.DisplayAspectRatio, videoVersion.DisplayAspectRatio,
@ -437,7 +443,19 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
{ {
var videoInputFile = new VideoInputFile( var videoInputFile = new VideoInputFile(
inputFile, 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( var pipelineBuilder = new PipelineBuilder(
_runtimeInfo, _runtimeInfo,
@ -517,6 +535,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService
options.ImageStreamIndex.IfNone(0), options.ImageStreamIndex.IfNone(0),
"unknown", "unknown",
new PixelFormatUnknown(), new PixelFormatUnknown(),
ColorParams.Default,
new FrameSize(1, 1), new FrameSize(1, 1),
string.Empty, string.Empty,
string.Empty, string.Empty,

2
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettings.cs

@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.Core.FFmpeg; namespace ErsatzTV.Core.FFmpeg;
@ -14,6 +15,7 @@ public class FFmpegPlaybackSettings
public Option<IDisplaySize> ScaledSize { get; set; } public Option<IDisplaySize> ScaledSize { get; set; }
public bool PadToDesiredResolution { get; set; } public bool PadToDesiredResolution { get; set; }
public FFmpegProfileVideoFormat VideoFormat { get; set; } public FFmpegProfileVideoFormat VideoFormat { get; set; }
public IPixelFormat PixelFormat { get; set; }
public Option<int> VideoBitrate { get; set; } public Option<int> VideoBitrate { get; set; }
public Option<int> VideoBufferSize { get; set; } public Option<int> VideoBufferSize { get; set; }
public Option<int> AudioBitrate { get; set; } public Option<int> AudioBitrate { get; set; }

8
ErsatzTV.Core/FFmpeg/FFmpegPlaybackSettingsCalculator.cs

@ -20,6 +20,7 @@
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.Core.FFmpeg; 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.AudioFormat = ffmpegProfile.AudioFormat;
result.AudioBitrate = ffmpegProfile.AudioBitrate; result.AudioBitrate = ffmpegProfile.AudioBitrate;
result.AudioBufferSize = ffmpegProfile.AudioBufferSize; result.AudioBufferSize = ffmpegProfile.AudioBufferSize;

10
ErsatzTV.Core/Metadata/LocalStatisticsProvider.cs

@ -399,7 +399,11 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
Index = videoStream.index, Index = videoStream.index,
Codec = videoStream.codec_name, Codec = videoStream.codec_name,
Profile = (videoStream.profile ?? string.Empty).ToLowerInvariant(), 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)) if (int.TryParse(videoStream.bits_per_raw_sample, out int bitsPerRawSample))
@ -538,6 +542,10 @@ public class LocalStatisticsProvider : ILocalStatisticsProvider
string sample_aspect_ratio, string sample_aspect_ratio,
string display_aspect_ratio, string display_aspect_ratio,
string pix_fmt, string pix_fmt,
string color_range,
string color_space,
string color_transfer,
string color_primaries,
string field_order, string field_order,
string r_frame_rate, string r_frame_rate,
string bits_per_raw_sample, string bits_per_raw_sample,

12
ErsatzTV.FFmpeg.Tests/PipelineBuilderTests.cs

@ -26,7 +26,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile( var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv", "/tmp/whatever.mkv",
new List<VideoStream> 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( var audioInputFile = new AudioInputFile(
"/tmp/whatever.mkv", "/tmp/whatever.mkv",
@ -99,7 +99,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile( var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv", "/tmp/whatever.mkv",
new List<VideoStream> 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( var audioInputFile = new AudioInputFile(
"/tmp/whatever.mkv", "/tmp/whatever.mkv",
@ -198,7 +198,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile( var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv", "/tmp/whatever.mkv",
new List<VideoStream> 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( var audioInputFile = new AudioInputFile(
"/tmp/whatever.mkv", "/tmp/whatever.mkv",
@ -216,7 +216,7 @@ public class PipelineGeneratorTests
true, true,
false, false,
VideoFormat.Copy, VideoFormat.Copy,
new PixelFormatYuv420P(), Option<IPixelFormat>.None,
new FrameSize(1920, 1080), new FrameSize(1920, 1080),
new FrameSize(1920, 1080), new FrameSize(1920, 1080),
false, false,
@ -273,7 +273,7 @@ public class PipelineGeneratorTests
var videoInputFile = new VideoInputFile( var videoInputFile = new VideoInputFile(
"/tmp/whatever.mkv", "/tmp/whatever.mkv",
new List<VideoStream> 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; Option<AudioInputFile> audioInputFile = Option<AudioInputFile>.None;
@ -341,7 +341,7 @@ public class PipelineGeneratorTests
"/test/input/file.png", "/test/input/file.png",
new List<VideoStream> 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( var pipelineBuilder = new PipelineBuilder(

14
ErsatzTV.FFmpeg/ColorParams.cs

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

16
ErsatzTV.FFmpeg/CommandGenerator.cs

@ -1,4 +1,6 @@
using ErsatzTV.FFmpeg.Environment; using ErsatzTV.FFmpeg.Encoder;
using ErsatzTV.FFmpeg.Environment;
using ErsatzTV.FFmpeg.Filter;
using ErsatzTV.FFmpeg.Option; using ErsatzTV.FFmpeg.Option;
namespace ErsatzTV.FFmpeg; namespace ErsatzTV.FFmpeg;
@ -80,8 +82,18 @@ public static class CommandGenerator
arguments.AddRange(step.FilterOptions); 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); arguments.AddRange(step.OutputOptions);
} }

2
ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderH264Nvenc.cs

@ -44,6 +44,6 @@ public class EncoderH264Nvenc : EncoderBase
public override FrameState NextState(FrameState currentState) => currentState with public override FrameState NextState(FrameState currentState) => currentState with
{ {
VideoFormat = VideoFormat.H264, VideoFormat = VideoFormat.H264,
FrameDataLocation = FrameDataLocation.Hardware // FrameDataLocation = FrameDataLocation.Hardware
}; };
} }

2
ErsatzTV.FFmpeg/Encoder/Nvenc/EncoderHevcNvenc.cs

@ -44,6 +44,6 @@ public class EncoderHevcNvenc : EncoderBase
public override FrameState NextState(FrameState currentState) => currentState with public override FrameState NextState(FrameState currentState) => currentState with
{ {
VideoFormat = VideoFormat.Hevc, VideoFormat = VideoFormat.Hevc,
FrameDataLocation = FrameDataLocation.Hardware // FrameDataLocation = FrameDataLocation.Hardware
}; };
} }

43
ErsatzTV.FFmpeg/Filter/ColorspaceFilter.cs

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

168
ErsatzTV.FFmpeg/Filter/ComplexFilter.cs

@ -12,9 +12,11 @@ public class ComplexFilter : IPipelineStep
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly Option<AudioInputFile> _maybeAudioInputFile; private readonly Option<AudioInputFile> _maybeAudioInputFile;
private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile; private readonly Option<SubtitleInputFile> _maybeSubtitleInputFile;
private readonly Option<IPixelFormat> _desiredPixelFormat;
private readonly Option<VideoInputFile> _maybeVideoInputFile; private readonly Option<VideoInputFile> _maybeVideoInputFile;
private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile; private readonly Option<WatermarkInputFile> _maybeWatermarkInputFile;
private readonly FrameSize _resolution; private readonly FrameSize _resolution;
private readonly List<string> _outputOptions;
public ComplexFilter( public ComplexFilter(
FrameState currentState, FrameState currentState,
@ -23,6 +25,7 @@ public class ComplexFilter : IPipelineStep
Option<AudioInputFile> maybeAudioInputFile, Option<AudioInputFile> maybeAudioInputFile,
Option<WatermarkInputFile> maybeWatermarkInputFile, Option<WatermarkInputFile> maybeWatermarkInputFile,
Option<SubtitleInputFile> maybeSubtitleInputFile, Option<SubtitleInputFile> maybeSubtitleInputFile,
Option<IPixelFormat> desiredPixelFormat,
FrameSize resolution, FrameSize resolution,
string fontsDir, string fontsDir,
ILogger logger) ILogger logger)
@ -33,20 +36,25 @@ public class ComplexFilter : IPipelineStep
_maybeAudioInputFile = maybeAudioInputFile; _maybeAudioInputFile = maybeAudioInputFile;
_maybeWatermarkInputFile = maybeWatermarkInputFile; _maybeWatermarkInputFile = maybeWatermarkInputFile;
_maybeSubtitleInputFile = maybeSubtitleInputFile; _maybeSubtitleInputFile = maybeSubtitleInputFile;
_desiredPixelFormat = desiredPixelFormat;
_resolution = resolution; _resolution = resolution;
_fontsDir = fontsDir; _fontsDir = fontsDir;
_logger = logger; _logger = logger;
_outputOptions = new List<string>();
} }
public IList<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>(); public IList<EnvironmentVariable> EnvironmentVariables => Array.Empty<EnvironmentVariable>();
public IList<string> GlobalOptions => Array.Empty<string>(); public IList<string> GlobalOptions => Array.Empty<string>();
public IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>(); public IList<string> InputOptions(InputFile inputFile) => Array.Empty<string>();
public IList<string> FilterOptions => Arguments(); public IList<string> FilterOptions => Arguments();
public IList<string> OutputOptions => Array.Empty<string>(); public IList<string> OutputOptions => _outputOptions;
public FrameState NextState(FrameState currentState) => currentState; public FrameState NextState(FrameState currentState) => currentState;
private IList<string> Arguments() private IList<string> Arguments()
{ {
var state = _currentState;
var audioLabel = "0:a"; var audioLabel = "0:a";
var videoLabel = "0:v"; var videoLabel = "0:v";
string watermarkLabel; string watermarkLabel;
@ -60,6 +68,7 @@ public class ComplexFilter : IPipelineStep
string watermarkOverlayFilterComplex = string.Empty; string watermarkOverlayFilterComplex = string.Empty;
string subtitleFilterComplex = string.Empty; string subtitleFilterComplex = string.Empty;
string subtitleOverlayFilterComplex = string.Empty; string subtitleOverlayFilterComplex = string.Empty;
string pixelFormatFilterComplex = string.Empty;
var distinctPaths = new List<string>(); var distinctPaths = new List<string>();
foreach ((string path, _) in _maybeVideoInputFile) foreach ((string path, _) in _maybeVideoInputFile)
@ -152,54 +161,56 @@ public class ComplexFilter : IPipelineStep
} }
foreach (VideoInputFile videoInputFile in _maybeVideoInputFile) 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( state = overlayFilter.NextState(state);
_ffmpegState.EncoderHardwareAccelerationMode,
watermarkInputFile.DesiredState, string tempVideoLabel = string.IsNullOrWhiteSpace(videoFilterComplex)
_resolution, ? $"[{videoLabel}]"
stream.SquarePixelFrameSize(_resolution), : videoLabel;
_logger);
// vaapi uses software overlay and needs to upload
if (overlayFilter.Filter != string.Empty) // 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))
{
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)
{ {
string tempVideoLabel = string.IsNullOrWhiteSpace(videoFilterComplex) uploadDownloadFilter = new HardwareDownloadFilter(state).Filter;
? $"[{videoLabel}]" state = state with { FrameDataLocation = FrameDataLocation.Software };
: 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]";
} }
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( IPipelineFilterStep overlayFilter = AvailableSubtitleOverlayFilters.ForAcceleration(
_ffmpegState.EncoderHardwareAccelerationMode); _ffmpegState.EncoderHardwareAccelerationMode);
state = overlayFilter.NextState(state);
filter = overlayFilter.Filter; filter = overlayFilter.Filter;
} }
else else
{ {
subtitleLabel = string.Empty; subtitleLabel = string.Empty;
var subtitlesFilter = new SubtitlesFilter(_fontsDir, subtitleInputFile); var subtitlesFilter = new SubtitlesFilter(_fontsDir, subtitleInputFile);
state = subtitlesFilter.NextState(state);
filter = subtitlesFilter.Filter; filter = subtitlesFilter.Filter;
} }
@ -250,9 +263,13 @@ public class ComplexFilter : IPipelineStep
string uploadFilter = string.Empty; string uploadFilter = string.Empty;
if (_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi if (_ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.Vaapi
|| _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox && || _ffmpegState.EncoderHardwareAccelerationMode == HardwareAccelerationMode.VideoToolbox &&
_currentState.VideoFormat == VideoFormat.Hevc) state.VideoFormat == VideoFormat.Hevc)
{ {
uploadFilter = new HardwareUploadFilter(_ffmpegState).Filter; uploadFilter = new HardwareUploadFilter(_ffmpegState).Filter;
if (!string.IsNullOrWhiteSpace(uploadFilter))
{
state = state with { FrameDataLocation = FrameDataLocation.Hardware };
}
} }
if (!string.IsNullOrWhiteSpace(uploadFilter)) 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( var filterComplex = string.Join(
";", ";",
new[] new[]
@ -277,7 +350,8 @@ public class ComplexFilter : IPipelineStep
watermarkFilterComplex, watermarkFilterComplex,
subtitleFilterComplex, subtitleFilterComplex,
watermarkOverlayFilterComplex, watermarkOverlayFilterComplex,
subtitleOverlayFilterComplex subtitleOverlayFilterComplex,
pixelFormatFilterComplex
}.Where( }.Where(
s => !string.IsNullOrWhiteSpace(s))); s => !string.IsNullOrWhiteSpace(s)));

3
ErsatzTV.FFmpeg/Filter/Cuda/OverlayWatermarkCudaFilter.cs

@ -19,5 +19,6 @@ public class OverlayWatermarkCudaFilter : OverlayWatermarkFilter
public override string Filter => $"overlay_cuda={Position}"; 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 };
} }

3
ErsatzTV.FFmpeg/Filter/OverlayWatermarkFilter.cs

@ -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() private WatermarkMargins NormalMargins()
{ {

3
ErsatzTV.FFmpeg/Filter/Qsv/OverlayWatermarkQsvFilter.cs

@ -19,5 +19,6 @@ public class OverlayWatermarkQsvFilter : OverlayWatermarkFilter
public override string Filter => $"overlay_qsv={Position}"; 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 };
} }

3
ErsatzTV.FFmpeg/Filter/SubtitlesFilter.cs

@ -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
ErsatzTV.FFmpeg/Filter/TonemapFilter.cs

@ -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
ErsatzTV.FFmpeg/InputFile.cs

@ -18,6 +18,7 @@ public record ConcatInputFile(string Url, FrameSize Resolution) : InputFile(
0, 0,
string.Empty, string.Empty,
Option<IPixelFormat>.None, Option<IPixelFormat>.None,
ColorParams.Default,
Resolution, Resolution,
string.Empty, string.Empty,
string.Empty, string.Empty,

4
ErsatzTV.FFmpeg/MediaStream.cs

@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations; using ErsatzTV.FFmpeg.Format;
using ErsatzTV.FFmpeg.Format;
namespace ErsatzTV.FFmpeg; namespace ErsatzTV.FFmpeg;
@ -14,6 +13,7 @@ public record VideoStream(
int Index, int Index,
string Codec, string Codec,
Option<IPixelFormat> PixelFormat, Option<IPixelFormat> PixelFormat,
ColorParams ColorParams,
FrameSize FrameSize, FrameSize FrameSize,
string SampleAspectRatio, string SampleAspectRatio,
string DisplayAspectRatio, string DisplayAspectRatio,

17
ErsatzTV.FFmpeg/PipelineBuilder.cs

@ -123,6 +123,8 @@ public class PipelineBuilder
public FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState) public FFmpegPipeline Build(FFmpegState ffmpegState, FrameState desiredState)
{ {
var originalDesiredPixelFormat = desiredState.PixelFormat;
if (ffmpegState.Start.Exists(s => s > TimeSpan.Zero) && desiredState.Realtime) if (ffmpegState.Start.Exists(s => s > TimeSpan.Zero) && desiredState.Realtime)
{ {
_logger.LogInformation( _logger.LogInformation(
@ -520,20 +522,6 @@ public class PipelineBuilder
_pipelineSteps.Add(step); _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 // TODO: if all video filters are software, use software pixel format for hwaccel output
@ -787,6 +775,7 @@ public class PipelineBuilder
_audioInputFile, _audioInputFile,
_watermarkInputFile, _watermarkInputFile,
_subtitleInputFile, _subtitleInputFile,
originalDesiredPixelFormat,
currentState.PaddedSize, currentState.PaddedSize,
_fontsFolder, _fontsFolder,
_logger); _logger);

4
ErsatzTV.Infrastructure/Data/Repositories/MetadataRepository.cs

@ -170,6 +170,10 @@ public class MetadataRepository : IMetadataRepository
existingStream.Forced = incomingStream.Forced; existingStream.Forced = incomingStream.Forced;
existingStream.AttachedPic = incomingStream.AttachedPic; existingStream.AttachedPic = incomingStream.AttachedPic;
existingStream.PixelFormat = incomingStream.PixelFormat; 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.BitsPerRawSample = incomingStream.BitsPerRawSample;
existingStream.FileName = incomingStream.FileName; existingStream.FileName = incomingStream.FileName;
existingStream.MimeType = incomingStream.MimeType; existingStream.MimeType = incomingStream.MimeType;

4315
ErsatzTV.Infrastructure/Migrations/20221029131840_Add_FFmpegProfileBitDepth.Designer.cs generated

File diff suppressed because it is too large Load Diff

26
ErsatzTV.Infrastructure/Migrations/20221029131840_Add_FFmpegProfileBitDepth.cs

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

4327
ErsatzTV.Infrastructure/Migrations/20221103002717_Add_MediaStreamColor.Designer.cs generated

File diff suppressed because it is too large Load Diff

55
ErsatzTV.Infrastructure/Migrations/20221103002717_Add_MediaStreamColor.cs

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

4327
ErsatzTV.Infrastructure/Migrations/20221104021107_Reset_AllStatistics20221103.Designer.cs generated

File diff suppressed because it is too large Load Diff

31
ErsatzTV.Infrastructure/Migrations/20221104021107_Reset_AllStatistics20221103.cs

@ -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)
{
}
}
}

17
ErsatzTV.Infrastructure/Migrations/TvContextModelSnapshot.cs

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); modelBuilder.HasAnnotation("ProductVersion", "6.0.10");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b => modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{ {
@ -548,6 +548,9 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<int>("AudioSampleRate") b.Property<int>("AudioSampleRate")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("BitDepth")
.HasColumnType("INTEGER");
b.Property<bool?>("DeinterlaceVideo") b.Property<bool?>("DeinterlaceVideo")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
@ -961,6 +964,18 @@ namespace ErsatzTV.Infrastructure.Migrations
b.Property<string>("Codec") b.Property<string>("Codec")
.HasColumnType("TEXT"); .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") b.Property<bool>("Default")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

4
ErsatzTV.sln

@ -27,8 +27,8 @@ Global
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Release|Any CPU.Build.0 = Release|Any CPU {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.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 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.ActiveCfg = Debug No Sync|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.Build.0 = Debug|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.ActiveCfg = Debug|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.Build.0 = 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 {C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU

1
ErsatzTV.sln.DotSettings

@ -60,6 +60,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=setsar/@EntryIndexedValue">True</s:Boolean> <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/=showtitle/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=strm/@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/=Trakt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=transcoded/@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> <s:Boolean x:Key="/Default/UserDictionary/Words/=tvdb/@EntryIndexedValue">True</s:Boolean>

5
ErsatzTV/Pages/FFmpegEditor.razor

@ -4,7 +4,6 @@
@using ErsatzTV.Application.Resolutions @using ErsatzTV.Application.Resolutions
@using ErsatzTV.Core.FFmpeg @using ErsatzTV.Core.FFmpeg
@using ErsatzTV.Application.FFmpegProfiles @using ErsatzTV.Application.FFmpegProfiles
@using ErsatzTV.Infrastructure.Runtime
@using System.Runtime.InteropServices @using System.Runtime.InteropServices
@implements IDisposable @implements IDisposable
@inject NavigationManager _navigationManager @inject NavigationManager _navigationManager
@ -48,6 +47,10 @@
<MudSelectItem Value="@FFmpegProfileVideoFormat.Hevc">hevc</MudSelectItem> <MudSelectItem Value="@FFmpegProfileVideoFormat.Hevc">hevc</MudSelectItem>
<MudSelectItem Value="@FFmpegProfileVideoFormat.Mpeg2Video">mpeg-2</MudSelectItem> <MudSelectItem Value="@FFmpegProfileVideoFormat.Mpeg2Video">mpeg-2</MudSelectItem>
</MudSelect> </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"> <MudElement HtmlTag="div" Class="mt-3">
<MudTextField Label="Bitrate" @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/> <MudTextField Label="Bitrate" @bind-Value="_model.VideoBitrate" For="@(() => _model.VideoBitrate)" Adornment="Adornment.End" AdornmentText="kBit/s"/>
</MudElement> </MudElement>

4
ErsatzTV/ViewModels/FFmpegProfileEditViewModel.cs

@ -32,6 +32,7 @@ public class FFmpegProfileEditViewModel
VideoBitrate = viewModel.VideoBitrate; VideoBitrate = viewModel.VideoBitrate;
VideoBufferSize = viewModel.VideoBufferSize; VideoBufferSize = viewModel.VideoBufferSize;
VideoFormat = viewModel.VideoFormat; VideoFormat = viewModel.VideoFormat;
BitDepth = viewModel.BitDepth;
} }
public int AudioBitrate { get; set; } public int AudioBitrate { get; set; }
@ -53,6 +54,7 @@ public class FFmpegProfileEditViewModel
public int VideoBitrate { get; set; } public int VideoBitrate { get; set; }
public int VideoBufferSize { get; set; } public int VideoBufferSize { get; set; }
public FFmpegProfileVideoFormat VideoFormat { get; set; } public FFmpegProfileVideoFormat VideoFormat { get; set; }
public FFmpegProfileBitDepth BitDepth { get; set; }
public CreateFFmpegProfile ToCreate() => public CreateFFmpegProfile ToCreate() =>
new( new(
@ -64,6 +66,7 @@ public class FFmpegProfileEditViewModel
QsvExtraHardwareFrames, QsvExtraHardwareFrames,
Resolution.Id, Resolution.Id,
VideoFormat, VideoFormat,
BitDepth,
VideoBitrate, VideoBitrate,
VideoBufferSize, VideoBufferSize,
AudioFormat, AudioFormat,
@ -87,6 +90,7 @@ public class FFmpegProfileEditViewModel
QsvExtraHardwareFrames, QsvExtraHardwareFrames,
Resolution.Id, Resolution.Id,
VideoFormat, VideoFormat,
BitDepth,
VideoBitrate, VideoBitrate,
VideoBufferSize, VideoBufferSize,
AudioFormat, AudioFormat,

Loading…
Cancel
Save