Selaa lähdekoodia

Add CODECS field to HLS master playlist

Andreas B 5 vuotta sitten
vanhempi
sitoutus
f2858878d1

+ 110 - 0
MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs

@@ -722,6 +722,114 @@ namespace MediaBrowser.Api.Playback.Hls
             //return state.VideoRequest.VideoBitRate.HasValue;
             //return state.VideoRequest.VideoBitRate.HasValue;
         }
         }
 
 
+        /// <summary>
+        /// Gets a formatted string of the output audio codec, for use in the CODECS field.
+        /// </summary>
+        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+        /// <seealso cref="GetPlaylistVideoCodecs(StreamState)"/>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <returns>Formatted audio codec string.</returns>
+        private string GetPlaylistAudioCodecs(StreamState state)
+        {
+
+            if (string.Equals(state.ActualOutputAudioCodec, "aac", StringComparison.OrdinalIgnoreCase))
+            {
+                string profile = state.GetRequestedProfiles("aac").FirstOrDefault();
+
+                return HlsCodecStringFactory.GetAACString(profile);
+            }
+            else if (string.Equals(state.ActualOutputAudioCodec, "mp3", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringFactory.GetMP3String();
+            }
+            else if (string.Equals(state.ActualOutputAudioCodec, "ac3", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringFactory.GetAC3String();
+            }
+            else if (string.Equals(state.ActualOutputAudioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
+            {
+                return HlsCodecStringFactory.GetEAC3String();
+            }
+
+            return string.Empty;
+        }
+
+        /// <summary>
+        /// Gets a formatted string of the output video codec, for use in the CODECS field.
+        /// </summary>
+        /// <seealso cref="AppendPlaylistCodecsField(StringBuilder, StreamState)"/>
+        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+        /// <param name="state">StreamState of the current stream.</param>
+        /// <returns>Formatted video codec string.</returns>
+        private string GetPlaylistVideoCodecs(StreamState state)
+        {
+            int level = Convert.ToInt32(state.GetRequestedLevel(state.ActualOutputVideoCodec));
+
+            if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
+            {
+                string profile = state.GetRequestedProfiles("h264").FirstOrDefault();
+
+                return HlsCodecStringFactory.GetH264String(profile, level);
+            }
+            else if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
+                    || string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+            {
+                string profile = state.GetRequestedProfiles("h265").FirstOrDefault();
+
+                return HlsCodecStringFactory.GetH265String(profile, level);
+            }
+
+            return string.Empty;
+        }
+
+        /// <summary>
+        /// Appends a CODECS field containing formatted strings of
+        /// the active streams output video and audio codecs.
+        /// </summary>
+        /// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
+        /// <seealso cref="GetPlaylistVideoCodecs(StreamState)"/>
+        /// <seealso cref="GetPlaylistAudioCodecs(StreamState)"/>
+        /// <param name="builder">StringBuilder to append the field to.</param>
+        /// <param name="state">StreamState of the current stream.</param>
+        private void AppendPlaylistCodecsField(StringBuilder builder, StreamState state)
+        {
+            // Video
+            string videoCodecs = string.Empty;
+            if (!string.IsNullOrEmpty(state.ActualOutputVideoCodec))
+            {
+                videoCodecs = GetPlaylistVideoCodecs(state);
+            }
+
+            // Audio
+            string audioCodecs = string.Empty;
+            if (!string.IsNullOrEmpty(state.ActualOutputAudioCodec))
+            {
+                audioCodecs = GetPlaylistAudioCodecs(state);
+            }
+
+            if (!string.IsNullOrEmpty(videoCodecs) || !string.IsNullOrEmpty(audioCodecs))
+            {
+                builder.Append(",CODECS=\"");
+
+                if (!string.IsNullOrEmpty(videoCodecs) && !string.IsNullOrEmpty(audioCodecs))
+                {
+                    builder.Append(videoCodecs)
+                        .Append(',')
+                        .Append(audioCodecs);
+                }
+                else if (!string.IsNullOrEmpty(videoCodecs))
+                {
+                    builder.Append(videoCodecs);
+                }
+                else if (!string.IsNullOrEmpty(audioCodecs))
+                {
+                    builder.Append(audioCodecs);
+                }
+
+                builder.Append('"');
+            }
+        }
+
         private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup)
         private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string subtitleGroup)
         {
         {
             builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
             builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
@@ -735,6 +843,8 @@ namespace MediaBrowser.Api.Playback.Hls
             //    header += string.Format(",FRAME-RATE=\"{0}\"", state.TargetFramerate.Value.ToString(CultureInfo.InvariantCulture));
             //    header += string.Format(",FRAME-RATE=\"{0}\"", state.TargetFramerate.Value.ToString(CultureInfo.InvariantCulture));
             //}
             //}
 
 
+            AppendPlaylistCodecsField(builder, state);
+
             if (!string.IsNullOrWhiteSpace(subtitleGroup))
             if (!string.IsNullOrWhiteSpace(subtitleGroup))
             {
             {
                 builder.Append(",SUBTITLES=\"")
                 builder.Append(",SUBTITLES=\"")

+ 126 - 0
MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs

@@ -0,0 +1,126 @@
+using System;
+using System.Text;
+
+
+namespace MediaBrowser.Api.Playback
+{
+    /// <summary>
+    /// Get various codec strings for use in HLS playlists.
+    /// </summary>
+    static class HlsCodecStringFactory
+    {
+
+        /// <summary>
+        /// Gets a MP3 codec string.
+        /// </summary>
+        /// <returns>MP3 codec string.</returns>
+        public static string GetMP3String()
+        {
+            return "mp4a.40.34";
+        }
+
+        /// <summary>
+        /// Gets an AAC codec string.
+        /// </summary>
+        /// <param name="profile">AAC profile.</param>
+        /// <returns>AAC codec string.</returns>
+        public static string GetAACString(string profile)
+        {
+            StringBuilder result = new StringBuilder("mp4a", 9);
+
+            if (string.Equals(profile, "HE", StringComparison.OrdinalIgnoreCase))
+            {
+                result.Append(".40.5");
+            }
+            else
+            {
+                // Default to LC if profile is invalid
+                result.Append(".40.2");
+            }
+
+            return result.ToString();
+        }
+
+        /// <summary>
+        /// Gets a H.264 codec string.
+        /// </summary>
+        /// <param name="profile">H.264 profile.</param>
+        /// <param name="level">H.264 level.</param>
+        /// <returns>H.264 string.</returns>
+        public static string GetH264String(string profile, int level)
+        {
+            StringBuilder result = new StringBuilder("avc1", 11);
+
+            if (string.Equals(profile, "high", StringComparison.OrdinalIgnoreCase))
+            {
+                result.Append(".6400");
+            }
+            else if (string.Equals(profile, "main", StringComparison.OrdinalIgnoreCase))
+            {
+                result.Append(".4D40");
+            }
+            else if (string.Equals(profile, "baseline", StringComparison.OrdinalIgnoreCase))
+            {
+                result.Append(".42E0");
+            }
+            else
+            {
+                // Default to constrained baseline if profile is invalid
+                result.Append(".4240");
+            }
+
+            string levelHex = level.ToString("X2");
+            result.Append(levelHex);
+
+            return result.ToString();
+        }
+
+        /// <summary>
+        /// Gets a H.265 codec string.
+        /// </summary>
+        /// <param name="profile">H.265 profile.</param>
+        /// <param name="level">H.265 level.</param>
+        /// <returns>H.265 string.</returns>
+        public static string GetH265String(string profile, int level)
+        {
+            // The h265 syntax is a bit of a mystery at the time this comment was written.
+            // This is what I've found through various sources:
+            // FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
+            StringBuilder result = new StringBuilder("hev1", 16);
+
+            if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
+            {
+                result.Append(".2.6");
+            }
+            else
+            {
+                // Default to main if profile is invalid
+                result.Append(".1.6");
+            }
+
+            result.Append(".L")
+                .Append(level * 3)
+                .Append(".B0");
+
+            return result.ToString();
+        }
+
+        /// <summary>
+        /// Gets an AC-3 codec string.
+        /// </summary>
+        /// <returns>AC-3 codec string.</returns>
+        public static string GetAC3String()
+        {
+            return "mp4a.a5";
+        }
+
+        /// <summary>
+        /// Gets an E-AC-3 codec string.
+        /// </summary>
+        /// <returns>E-AC-3 codec string.</returns>
+        public static string GetEAC3String()
+        {
+            return "mp4a.a6";
+        }
+    }
+}