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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+4315
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4327
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4327
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
@@ -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
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
Reference in New Issue
Block a user