From f2858878d166df214aee20f2dc0710b766285c91 Mon Sep 17 00:00:00 2001 From: Andreas B <6439218+YouKnowBlom@users.noreply.github.com> Date: Wed, 11 Mar 2020 18:14:36 +0100 Subject: [PATCH] Add CODECS field to HLS master playlist --- .../Playback/Hls/DynamicHlsService.cs | 110 +++++++++++++++ .../Playback/Hls/HlsCodecStringFactory.cs | 126 ++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index 8787eb2a3f..e6c9213912 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -722,6 +722,114 @@ namespace MediaBrowser.Api.Playback.Hls //return state.VideoRequest.VideoBitRate.HasValue; } + /// + /// Gets a formatted string of the output audio codec, for use in the CODECS field. + /// + /// + /// + /// StreamState of the current stream. + /// Formatted audio codec string. + 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; + } + + /// + /// Gets a formatted string of the output video codec, for use in the CODECS field. + /// + /// + /// + /// StreamState of the current stream. + /// Formatted video codec string. + 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; + } + + /// + /// Appends a CODECS field containing formatted strings of + /// the active streams output video and audio codecs. + /// + /// + /// + /// + /// StringBuilder to append the field to. + /// StreamState of the current stream. + 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) { 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)); //} + AppendPlaylistCodecsField(builder, state); + if (!string.IsNullOrWhiteSpace(subtitleGroup)) { builder.Append(",SUBTITLES=\"") diff --git a/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs b/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs new file mode 100644 index 0000000000..3bbb77a65e --- /dev/null +++ b/MediaBrowser.Api/Playback/Hls/HlsCodecStringFactory.cs @@ -0,0 +1,126 @@ +using System; +using System.Text; + + +namespace MediaBrowser.Api.Playback +{ + /// + /// Get various codec strings for use in HLS playlists. + /// + static class HlsCodecStringFactory + { + + /// + /// Gets a MP3 codec string. + /// + /// MP3 codec string. + public static string GetMP3String() + { + return "mp4a.40.34"; + } + + /// + /// Gets an AAC codec string. + /// + /// AAC profile. + /// AAC codec string. + 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(); + } + + /// + /// Gets a H.264 codec string. + /// + /// H.264 profile. + /// H.264 level. + /// H.264 string. + 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(); + } + + /// + /// Gets a H.265 codec string. + /// + /// H.265 profile. + /// H.265 level. + /// H.265 string. + 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(); + } + + /// + /// Gets an AC-3 codec string. + /// + /// AC-3 codec string. + public static string GetAC3String() + { + return "mp4a.a5"; + } + + /// + /// Gets an E-AC-3 codec string. + /// + /// E-AC-3 codec string. + public static string GetEAC3String() + { + return "mp4a.a6"; + } + } +}