diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 4486954c62..b66aa69350 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -210,6 +210,7 @@ public class DynamicHlsHelper // Provide SDR HEVC entrance for backward compatibility. if (encodingOptions.AllowHevcEncoding + && !encodingOptions.AllowAv1Encoding && EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && !string.IsNullOrEmpty(state.VideoStream.VideoRange) && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) @@ -252,7 +253,9 @@ public class DynamicHlsHelper // 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, // 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 > 150 && !string.IsNullOrEmpty(state.VideoStream.VideoRange) @@ -555,6 +558,12 @@ public class DynamicHlsHelper levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120"; 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)) @@ -566,11 +575,11 @@ public class DynamicHlsHelper } /// - /// Get the H.26X profile of the output video stream. + /// Get the profile of the output video stream. /// /// StreamState of the current stream. /// Video codec. - /// H.26X profile of the output video stream. + /// Profile of the output video stream. private string GetOutputVideoCodecProfile(StreamState state, string codec) { string profileString = string.Empty; @@ -592,6 +601,11 @@ public class DynamicHlsHelper { profileString ??= "main"; } + + if (string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + profileString ??= "main"; + } } return profileString; @@ -658,9 +672,9 @@ public class DynamicHlsHelper { if (level == 0) { - // This is 0 when there's no requested H.26X level in the device profile - // and the source is not encoded in H.26X - _logger.LogError("Got invalid H.26X level when building CODECS field for HLS master playlist"); + // This is 0 when there's no requested level in the device profile + // and the source is not encoded in H.26X or AV1 + _logger.LogError("Got invalid level when building CODECS field for HLS master playlist"); return string.Empty; } @@ -677,6 +691,22 @@ public class DynamicHlsHelper 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; } diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs index 995488397e..ad1fce0f14 100644 --- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs @@ -179,4 +179,60 @@ public static class HlsCodecStringHelpers return result.ToString(); } + + /// + /// Gets a AV1 codec string. + /// + /// AV1 profile. + /// AV1 level. + /// AV1 tier flag. + /// AV1 bit depth. + /// AV1 string. + 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("." + level) + .Append(tierFlag ? "H" : "M"); + + string bitDepthD2 = bitDepth.ToString("D2", CultureInfo.InvariantCulture); + result.Append("." + bitDepthD2); + + return result.ToString(); + } } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 9c91dcc6fe..782cd65685 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -430,12 +430,17 @@ public static class StreamingHelpers { var videoCodec = state.Request.VideoCodec; - if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) || - string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) { return ".ts"; } + if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase)) + { + return ".mp4"; + } + if (string.Equals(videoCodec, "theora", StringComparison.OrdinalIgnoreCase)) { return ".ogv"; diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 0a955e917f..b82c9cf26b 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -23,7 +23,7 @@ namespace MediaBrowser.Model.Dlna private readonly ILogger _logger; 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[] _supportedHlsAudioCodecsMp4 = new string[] { "aac", "ac3", "eac3", "mp3", "alac", "flac", "opus", "dca", "truehd" };