Merge pull request #9907 from nyanmisaka/av1e
This commit is contained in:
commit
e53e53eb29
|
@ -211,6 +211,7 @@ public class DynamicHlsHelper
|
||||||
|
|
||||||
// Provide SDR HEVC entrance for backward compatibility.
|
// Provide SDR HEVC entrance for backward compatibility.
|
||||||
if (encodingOptions.AllowHevcEncoding
|
if (encodingOptions.AllowHevcEncoding
|
||||||
|
&& !encodingOptions.AllowAv1Encoding
|
||||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
&& state.VideoStream.VideoRange == VideoRange.HDR
|
&& state.VideoStream.VideoRange == VideoRange.HDR
|
||||||
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||||
|
@ -252,7 +253,9 @@ public class DynamicHlsHelper
|
||||||
// Provide Level 5.0 entrance for backward compatibility.
|
// Provide Level 5.0 entrance for backward compatibility.
|
||||||
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
|
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
|
||||||
// but in fact it is capable of playing videos up to Level 6.1.
|
// but in fact it is capable of playing videos up to Level 6.1.
|
||||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
if (encodingOptions.AllowHevcEncoding
|
||||||
|
&& !encodingOptions.AllowAv1Encoding
|
||||||
|
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
&& state.VideoStream.Level.HasValue
|
&& state.VideoStream.Level.HasValue
|
||||||
&& state.VideoStream.Level > 150
|
&& state.VideoStream.Level > 150
|
||||||
&& state.VideoStream.VideoRange == VideoRange.SDR
|
&& state.VideoStream.VideoRange == VideoRange.SDR
|
||||||
|
@ -554,6 +557,12 @@ public class DynamicHlsHelper
|
||||||
levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
|
levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
|
||||||
levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
|
levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
levelString = state.GetRequestedLevel("av1") ?? "19";
|
||||||
|
levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
|
if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
|
||||||
|
@ -565,11 +574,11 @@ public class DynamicHlsHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the H.26X profile of the output video stream.
|
/// Get the profile of the output video stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="state">StreamState of the current stream.</param>
|
/// <param name="state">StreamState of the current stream.</param>
|
||||||
/// <param name="codec">Video codec.</param>
|
/// <param name="codec">Video codec.</param>
|
||||||
/// <returns>H.26X profile of the output video stream.</returns>
|
/// <returns>Profile of the output video stream.</returns>
|
||||||
private string GetOutputVideoCodecProfile(StreamState state, string codec)
|
private string GetOutputVideoCodecProfile(StreamState state, string codec)
|
||||||
{
|
{
|
||||||
string profileString = string.Empty;
|
string profileString = string.Empty;
|
||||||
|
@ -587,7 +596,8 @@ public class DynamicHlsHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
profileString ??= "main";
|
profileString ??= "main";
|
||||||
}
|
}
|
||||||
|
@ -657,9 +667,9 @@ public class DynamicHlsHelper
|
||||||
{
|
{
|
||||||
if (level == 0)
|
if (level == 0)
|
||||||
{
|
{
|
||||||
// This is 0 when there's no requested H.26X level in the device profile
|
// This is 0 when there's no requested level in the device profile
|
||||||
// and the source is not encoded in H.26X
|
// and the source is not encoded in H.26X or AV1
|
||||||
_logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist");
|
_logger.LogError("Got invalid level when building CODECS field for HLS master playlist");
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -676,6 +686,22 @@ public class DynamicHlsHelper
|
||||||
return HlsCodecStringHelpers.GetH265String(profile, level);
|
return HlsCodecStringHelpers.GetH265String(profile, level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
string profile = GetOutputVideoCodecProfile(state, "av1");
|
||||||
|
|
||||||
|
// Currently we only transcode to 8 bits AV1
|
||||||
|
int bitDepth = 8;
|
||||||
|
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
|
&& state.VideoStream != null
|
||||||
|
&& state.VideoStream.BitDepth.HasValue)
|
||||||
|
{
|
||||||
|
bitDepth = state.VideoStream.BitDepth.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HlsCodecStringHelpers.GetAv1String(profile, level, false, bitDepth);
|
||||||
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -179,4 +179,62 @@ public static class HlsCodecStringHelpers
|
||||||
|
|
||||||
return result.ToString();
|
return result.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an AV1 codec string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="profile">AV1 profile.</param>
|
||||||
|
/// <param name="level">AV1 level.</param>
|
||||||
|
/// <param name="tierFlag">AV1 tier flag.</param>
|
||||||
|
/// <param name="bitDepth">AV1 bit depth.</param>
|
||||||
|
/// <returns>The AV1 codec string.</returns>
|
||||||
|
public static string GetAv1String(string? profile, int level, bool tierFlag, int bitDepth)
|
||||||
|
{
|
||||||
|
// https://aomedia.org/av1/specification/annex-a/
|
||||||
|
// FORMAT: [codecTag].[profile].[level][tier].[bitDepth]
|
||||||
|
StringBuilder result = new StringBuilder("av01", 13);
|
||||||
|
|
||||||
|
if (string.Equals(profile, "Main", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.Append(".0");
|
||||||
|
}
|
||||||
|
else if (string.Equals(profile, "High", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.Append(".1");
|
||||||
|
}
|
||||||
|
else if (string.Equals(profile, "Professional", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.Append(".2");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default to Main
|
||||||
|
result.Append(".0");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level <= 0
|
||||||
|
|| level > 31)
|
||||||
|
{
|
||||||
|
// Default to the maximum defined level 6.3
|
||||||
|
level = 19;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitDepth != 8
|
||||||
|
&& bitDepth != 10
|
||||||
|
&& bitDepth != 12)
|
||||||
|
{
|
||||||
|
// Default to 8 bits
|
||||||
|
bitDepth = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Append('.')
|
||||||
|
.Append(level)
|
||||||
|
.Append(tierFlag ? 'H' : 'M');
|
||||||
|
|
||||||
|
string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture);
|
||||||
|
result.Append('.')
|
||||||
|
.Append(bitDepthD2);
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -430,12 +430,17 @@ public static class StreamingHelpers
|
||||||
{
|
{
|
||||||
var videoCodec = state.Request.VideoCodec;
|
var videoCodec = state.Request.VideoCodec;
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||||
string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
return ".ts";
|
return ".ts";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return ".mp4";
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return ".ogv";
|
return ".ogv";
|
||||||
|
|
|
@ -46,6 +46,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
|
private readonly Version _minFFmpegImplictHwaccel = new Version(6, 0);
|
||||||
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
||||||
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
|
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
|
||||||
|
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
|
||||||
|
|
||||||
private static readonly string[] _videoProfilesH264 = new[]
|
private static readonly string[] _videoProfilesH264 = new[]
|
||||||
{
|
{
|
||||||
|
@ -65,6 +66,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
"Main10"
|
"Main10"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly string[] _videoProfilesAv1 = new[]
|
||||||
|
{
|
||||||
|
"Main",
|
||||||
|
"High",
|
||||||
|
"Professional",
|
||||||
|
};
|
||||||
|
|
||||||
private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> _mp4ContainerNames = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
"mp4",
|
"mp4",
|
||||||
|
@ -113,12 +121,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
|
public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
|
||||||
=> GetH264OrH265Encoder("libx264", "h264", state, encodingOptions);
|
=> GetH26xOrAv1Encoder("libx264", "h264", state, encodingOptions);
|
||||||
|
|
||||||
public string GetH265Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
|
public string GetH265Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
|
||||||
=> GetH264OrH265Encoder("libx265", "hevc", state, encodingOptions);
|
=> GetH26xOrAv1Encoder("libx265", "hevc", state, encodingOptions);
|
||||||
|
|
||||||
private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions)
|
public string GetAv1Encoder(EncodingJobInfo state, EncodingOptions encodingOptions)
|
||||||
|
=> GetH26xOrAv1Encoder("libsvtav1", "av1", state, encodingOptions);
|
||||||
|
|
||||||
|
private string GetH26xOrAv1Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions)
|
||||||
{
|
{
|
||||||
// Only use alternative encoders for video files.
|
// Only use alternative encoders for video files.
|
||||||
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
|
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
|
||||||
|
@ -266,6 +277,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(codec))
|
if (!string.IsNullOrEmpty(codec))
|
||||||
{
|
{
|
||||||
|
if (string.Equals(codec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return GetAv1Encoder(state, encodingOptions);
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
@ -565,6 +581,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
return Array.FindIndex(_videoProfilesH265, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
|
return Array.FindIndex(_videoProfilesH265, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals("av1", videoCodec, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return Array.FindIndex(_videoProfilesAv1, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1204,6 +1225,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
return FormattableString.Invariant($" -b:v {bitrate}");
|
return FormattableString.Invariant($" -b:v {bitrate}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(videoCodec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return FormattableString.Invariant($" -b:v {bitrate} -bufsize {bufsize}");
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
@ -1211,14 +1237,16 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(videoCodec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(videoCodec, "hevc_amf", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoCodec, "av1_amf", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Override the too high default qmin 18 in transcoding preset
|
// Override the too high default qmin 18 in transcoding preset
|
||||||
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
|
return FormattableString.Invariant($" -rc cbr -qmin 0 -qmax 32 -b:v {bitrate} -maxrate {bitrate} -bufsize {bufsize}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(videoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(videoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(videoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoCodec, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// VBR in i965 driver may result in pixelated output.
|
// VBR in i965 driver may result in pixelated output.
|
||||||
if (_mediaEncoder.IsVaapiDeviceInteli965)
|
if (_mediaEncoder.IsVaapiDeviceInteli965)
|
||||||
|
@ -1236,14 +1264,23 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
{
|
{
|
||||||
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
|
if (double.TryParse(level, CultureInfo.InvariantCulture, out double requestLevel))
|
||||||
{
|
{
|
||||||
if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Transcode to level 5.3 (15) and lower for maximum compatibility.
|
||||||
|
// https://en.wikipedia.org/wiki/AV1#Levels
|
||||||
|
if (requestLevel < 0 || requestLevel >= 15)
|
||||||
|
{
|
||||||
|
return "15";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Transcode to level 5.0 and lower for maximum compatibility.
|
// Transcode to level 5.0 and lower for maximum compatibility.
|
||||||
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
|
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
|
||||||
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
||||||
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
|
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
|
||||||
if (requestLevel >= 150)
|
if (requestLevel < 0 || requestLevel >= 150)
|
||||||
{
|
{
|
||||||
return "150";
|
return "150";
|
||||||
}
|
}
|
||||||
|
@ -1253,7 +1290,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
// Transcode to level 5.1 and lower for maximum compatibility.
|
// Transcode to level 5.1 and lower for maximum compatibility.
|
||||||
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
|
// h264 4k 30fps requires at least level 5.1 otherwise it will break on safari fmp4.
|
||||||
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
|
// https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels
|
||||||
if (requestLevel >= 51)
|
if (requestLevel < 0 || requestLevel >= 51)
|
||||||
{
|
{
|
||||||
return "51";
|
return "51";
|
||||||
}
|
}
|
||||||
|
@ -1391,14 +1428,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(codec, "av1_qsv", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(codec, "av1_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(codec, "av1_amf", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(codec, "libsvtav1", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
args += gopArg;
|
args += gopArg;
|
||||||
}
|
}
|
||||||
else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase)
|
else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(codec, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
args += keyFrameArg;
|
args += keyFrameArg;
|
||||||
|
|
||||||
|
@ -1534,18 +1575,60 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
param += " -crf " + defaultCrf;
|
param += " -crf " + defaultCrf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
|
else if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase))
|
||||||
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv)
|
|
||||||
{
|
{
|
||||||
string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
|
// Default to use the recommended preset 10.
|
||||||
|
// Omit presets < 5, which are too slow for on the fly encoding.
|
||||||
|
// https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Ffmpeg.md
|
||||||
|
param += encodingOptions.EncoderPreset switch
|
||||||
|
{
|
||||||
|
"veryslow" => " -preset 5",
|
||||||
|
"slower" => " -preset 6",
|
||||||
|
"slow" => " -preset 7",
|
||||||
|
"medium" => " -preset 8",
|
||||||
|
"fast" => " -preset 9",
|
||||||
|
"faster" => " -preset 10",
|
||||||
|
"veryfast" => " -preset 11",
|
||||||
|
"superfast" => " -preset 12",
|
||||||
|
"ultrafast" => " -preset 13",
|
||||||
|
_ => " -preset 10"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// -compression_level is not reliable on AMD.
|
||||||
|
if (_mediaEncoder.IsVaapiDeviceInteliHD)
|
||||||
|
{
|
||||||
|
param += encodingOptions.EncoderPreset switch
|
||||||
|
{
|
||||||
|
"veryslow" => " -compression_level 1",
|
||||||
|
"slower" => " -compression_level 2",
|
||||||
|
"slow" => " -compression_level 3",
|
||||||
|
"medium" => " -compression_level 4",
|
||||||
|
"fast" => " -compression_level 5",
|
||||||
|
"faster" => " -compression_level 6",
|
||||||
|
"veryfast" => " -compression_level 7",
|
||||||
|
"superfast" => " -compression_level 7",
|
||||||
|
"ultrafast" => " -compression_level 7",
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
|
||||||
|
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase) // hevc (hevc_qsv)
|
||||||
|
|| string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)) // av1 (av1_qsv)
|
||||||
|
{
|
||||||
|
string[] valid_presets = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
|
||||||
|
|
||||||
if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase))
|
if (valid_presets.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
param += " -preset " + encodingOptions.EncoderPreset;
|
param += " -preset " + encodingOptions.EncoderPreset;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
param += " -preset 7";
|
param += " -preset veryfast";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only h264_qsv has look_ahead option
|
// Only h264_qsv has look_ahead option
|
||||||
|
@ -1555,7 +1638,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|
||||||
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
|
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase) // hevc (hevc_nvenc)
|
||||||
|
|| string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)) // av1 (av1_nvenc)
|
||||||
{
|
{
|
||||||
switch (encodingOptions.EncoderPreset)
|
switch (encodingOptions.EncoderPreset)
|
||||||
{
|
{
|
||||||
|
@ -1595,7 +1679,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
|
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
|
||||||
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf)
|
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase) // hevc (hevc_amf)
|
||||||
|
|| string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase)) // av1 (av1_amf)
|
||||||
{
|
{
|
||||||
switch (encodingOptions.EncoderPreset)
|
switch (encodingOptions.EncoderPreset)
|
||||||
{
|
{
|
||||||
|
@ -1622,9 +1707,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
param += " -header_insertion_mode gop";
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
param += " -header_insertion_mode gop -gops_per_idr 1";
|
param += " -gops_per_idr 1";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
|
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // vp8
|
||||||
|
@ -1755,6 +1846,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
profile = "high";
|
profile = "high";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We only need Main profile of AV1 encoders.
|
||||||
|
if (videoEncoder.Contains("av1", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& (profile.Contains("high", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| profile.Contains("professional", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
profile = "main";
|
||||||
|
}
|
||||||
|
|
||||||
// h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
|
// h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
|
||||||
// which is compatible (and ugly).
|
// which is compatible (and ugly).
|
||||||
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||||
|
@ -1822,19 +1921,41 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
param += " -level " + (hevcLevel / 3);
|
param += " -level " + (hevcLevel / 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (string.Equals(videoEncoder, "av1_qsv", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// libsvtav1 and av1_qsv use -level 60 instead of -level 16
|
||||||
|
// https://aomedia.org/av1/specification/annex-a/
|
||||||
|
if (int.TryParse(level, NumberStyles.Any, CultureInfo.InvariantCulture, out int av1Level))
|
||||||
|
{
|
||||||
|
var x = 2 + (av1Level >> 2);
|
||||||
|
var y = av1Level & 3;
|
||||||
|
var res = (x * 10) + y;
|
||||||
|
param += " -level " + res;
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoEncoder, "av1_amf", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
param += " -level " + level;
|
param += " -level " + level;
|
||||||
}
|
}
|
||||||
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase))
|
||||||
|| string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
// level option may cause NVENC to fail.
|
// level option may cause NVENC to fail.
|
||||||
// NVENC cannot adjust the given level, just throw an error.
|
// NVENC cannot adjust the given level, just throw an error.
|
||||||
|
}
|
||||||
|
else if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoEncoder, "av1_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
// level option may cause corrupted frames on AMD VAAPI.
|
// level option may cause corrupted frames on AMD VAAPI.
|
||||||
|
if (_mediaEncoder.IsVaapiDeviceInteliHD || _mediaEncoder.IsVaapiDeviceInteli965)
|
||||||
|
{
|
||||||
|
param += " -level " + level;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
|
else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
@ -1856,6 +1977,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
param += " -x265-params:0 no-info=1";
|
param += " -x265-params:0 no-info=1";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(videoEncoder, "libsvtav1", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& _mediaEncoder.EncoderVersion >= _minFFmpegSvtAv1Params)
|
||||||
|
{
|
||||||
|
param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0";
|
||||||
|
}
|
||||||
|
|
||||||
return param;
|
return param;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3645,7 +3772,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
mainFilters.Add(swDeintFilter);
|
mainFilters.Add(swDeintFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p";
|
var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
|
||||||
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
|
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
|
||||||
// sw scale
|
// sw scale
|
||||||
mainFilters.Add(swScaleFilter);
|
mainFilters.Add(swScaleFilter);
|
||||||
|
@ -3846,7 +3973,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
mainFilters.Add(swDeintFilter);
|
mainFilters.Add(swDeintFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
var outFormat = doOclTonemap ? "yuv420p10le" : "yuv420p";
|
var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
|
||||||
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
|
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
|
||||||
// sw scale
|
// sw scale
|
||||||
mainFilters.Add(swScaleFilter);
|
mainFilters.Add(swScaleFilter);
|
||||||
|
@ -5810,19 +5937,25 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
|
|
||||||
private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions)
|
private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions)
|
||||||
{
|
{
|
||||||
// Shift hevc/h265 to the end of list if hevc encoding is not allowed.
|
|
||||||
if (encodingOptions.AllowHevcEncoding)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No need to shift if there is only one supported video codec.
|
// No need to shift if there is only one supported video codec.
|
||||||
if (videoCodecs.Count < 2)
|
if (videoCodecs.Count < 2)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var shiftVideoCodecs = new[] { "hevc", "h265" };
|
// Shift codecs to the end of list if it's not allowed.
|
||||||
|
var shiftVideoCodecs = new List<string>();
|
||||||
|
if (!encodingOptions.AllowHevcEncoding)
|
||||||
|
{
|
||||||
|
shiftVideoCodecs.Add("hevc");
|
||||||
|
shiftVideoCodecs.Add("h265");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!encodingOptions.AllowAv1Encoding)
|
||||||
|
{
|
||||||
|
shiftVideoCodecs.Add("av1");
|
||||||
|
}
|
||||||
|
|
||||||
if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -52,6 +52,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
{
|
{
|
||||||
"libx264",
|
"libx264",
|
||||||
"libx265",
|
"libx265",
|
||||||
|
"libsvtav1",
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
"msmpeg4",
|
"msmpeg4",
|
||||||
"libvpx",
|
"libvpx",
|
||||||
|
@ -69,12 +70,16 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
"srt",
|
"srt",
|
||||||
"h264_amf",
|
"h264_amf",
|
||||||
"hevc_amf",
|
"hevc_amf",
|
||||||
|
"av1_amf",
|
||||||
"h264_qsv",
|
"h264_qsv",
|
||||||
"hevc_qsv",
|
"hevc_qsv",
|
||||||
|
"av1_qsv",
|
||||||
"h264_nvenc",
|
"h264_nvenc",
|
||||||
"hevc_nvenc",
|
"hevc_nvenc",
|
||||||
|
"av1_nvenc",
|
||||||
"h264_vaapi",
|
"h264_vaapi",
|
||||||
"hevc_vaapi",
|
"hevc_vaapi",
|
||||||
|
"av1_vaapi",
|
||||||
"h264_v4l2m2m",
|
"h264_v4l2m2m",
|
||||||
"h264_videotoolbox",
|
"h264_videotoolbox",
|
||||||
"hevc_videotoolbox"
|
"hevc_videotoolbox"
|
||||||
|
|
|
@ -49,6 +49,7 @@ public class EncodingOptions
|
||||||
EnableIntelLowPowerHevcHwEncoder = false;
|
EnableIntelLowPowerHevcHwEncoder = false;
|
||||||
EnableHardwareEncoding = true;
|
EnableHardwareEncoding = true;
|
||||||
AllowHevcEncoding = false;
|
AllowHevcEncoding = false;
|
||||||
|
AllowAv1Encoding = false;
|
||||||
EnableSubtitleExtraction = true;
|
EnableSubtitleExtraction = true;
|
||||||
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
|
AllowOnDemandMetadataBasedKeyframeExtractionForExtensions = new[] { "mkv" };
|
||||||
HardwareDecodingCodecs = new string[] { "h264", "vc1" };
|
HardwareDecodingCodecs = new string[] { "h264", "vc1" };
|
||||||
|
@ -249,6 +250,11 @@ public class EncodingOptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AllowHevcEncoding { get; set; }
|
public bool AllowHevcEncoding { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether AV1 encoding is enabled.
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowAv1Encoding { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether subtitle extraction is enabled.
|
/// Gets or sets a value indicating whether subtitle extraction is enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -73,27 +73,5 @@ namespace MediaBrowser.Model.Dlna
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double GetVideoBitrateScaleFactor(string codec)
|
|
||||||
{
|
|
||||||
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return .6;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int ScaleBitrate(int bitrate, string inputVideoCodec, string outputVideoCodec)
|
|
||||||
{
|
|
||||||
var inputScaleFactor = GetVideoBitrateScaleFactor(inputVideoCodec);
|
|
||||||
var outputScaleFactor = GetVideoBitrateScaleFactor(outputVideoCodec);
|
|
||||||
var scaleFactor = outputScaleFactor / inputScaleFactor;
|
|
||||||
var newBitrate = scaleFactor * bitrate;
|
|
||||||
|
|
||||||
return Convert.ToInt32(newBitrate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ namespace MediaBrowser.Model.Dlna
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly ITranscoderSupport _transcoderSupport;
|
private readonly ITranscoderSupport _transcoderSupport;
|
||||||
private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc" };
|
private static readonly string[] _supportedHlsVideoCodecs = new string[] { "h264", "hevc", "av1" };
|
||||||
private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" };
|
private static readonly string[] _supportedHlsAudioCodecsTs = new string[] { "aac", "ac3", "eac3", "mp3" };
|
||||||
private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" };
|
private static readonly string[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" };
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user