diff --git a/TwitchDownloaderCore.Tests/ExtensionTests/M3U8ExtensionTests.cs b/TwitchDownloaderCore.Tests/ExtensionTests/M3U8ExtensionTests.cs index 79c6d00d..6f93149b 100644 --- a/TwitchDownloaderCore.Tests/ExtensionTests/M3U8ExtensionTests.cs +++ b/TwitchDownloaderCore.Tests/ExtensionTests/M3U8ExtensionTests.cs @@ -21,6 +21,9 @@ public static class M3U8ExtensionTests [InlineData("1920x1080p", "1080p60")] [InlineData("1920x1080p60", "1080p60")] [InlineData("Source", "1080p60")] + [InlineData("chunked", "1080p60")] + [InlineData("Best", "1080p60")] + [InlineData("Worst", "720p30")] public static void CorrectlyFindsStreamOfQualityFromLiveM3U8Response(string qualityString, string expectedPath) { var m3u8 = new M3U8(new M3U8.Metadata(), new[] @@ -61,6 +64,10 @@ public static void CorrectlyFindsStreamOfQualityFromLiveM3U8Response(string qual [InlineData("audio", "audio_only")] [InlineData("Audio", "audio_only")] [InlineData("Audio Only", "audio_only")] + [InlineData("Source", "1080p60")] + [InlineData("chunked", "1080p60")] + [InlineData("Best", "1080p60")] + [InlineData("Worst", "144p30")] public static void CorrectlyFindsStreamOfQualityFromOldM3U8Response(string qualityString, string expectedPath) { var m3u8 = new M3U8(new M3U8.Metadata(), new[] @@ -105,6 +112,10 @@ public static void CorrectlyFindsStreamOfQualityFromOldM3U8Response(string quali [InlineData("1080p60", "1080p60")] [InlineData("720p60", "720p60")] [InlineData("foo", "1080p60")] + [InlineData("Source", "1080p60")] + [InlineData("chunked", "1080p60")] + [InlineData("Best", "1080p60")] + [InlineData("Worst", "720p60")] public static void CorrectlyFindsStreamOfQualityFromM3U8ResponseWithoutFramerate(string qualityString, string expectedPath) { var m3u8 = new M3U8(new M3U8.Metadata(), new[] diff --git a/TwitchDownloaderCore/Extensions/M3U8Extensions.cs b/TwitchDownloaderCore/Extensions/M3U8Extensions.cs index afb00d7a..3ef590fb 100644 --- a/TwitchDownloaderCore/Extensions/M3U8Extensions.cs +++ b/TwitchDownloaderCore/Extensions/M3U8Extensions.cs @@ -26,33 +26,22 @@ public static void SortStreamsByQuality(this M3U8 m3u8) public static M3U8.Stream GetStreamOfQuality(this M3U8 m3u8, string qualityString) { - var streams = m3u8.Streams; - if (streams.Length == 0) + if (m3u8.Streams.Length == 0) { throw new ArgumentException(nameof(m3u8), "M3U8 does not contain any streams."); } - if (string.IsNullOrWhiteSpace(qualityString)) + if (TryGetKeywordStream(m3u8, qualityString, out var keywordStream)) { - return m3u8.BestQualityStream(); - } - - if (qualityString.Contains("source", StringComparison.OrdinalIgnoreCase) || qualityString.Contains("chunked", StringComparison.OrdinalIgnoreCase)) - { - return m3u8.BestQualityStream(); - } - - if (qualityString.Contains("audio", StringComparison.OrdinalIgnoreCase) && - streams.FirstOrDefault(x => x.MediaInfo.Name.Contains("audio", StringComparison.OrdinalIgnoreCase)) is { } audioStream) - { - return audioStream; + return keywordStream; } if (!qualityString.Contains('x') && qualityString.Contains('p')) { - foreach (var stream in streams) + foreach (var stream in m3u8.Streams) { - if (qualityString.Equals(stream.StreamInfo.Video, StringComparison.OrdinalIgnoreCase) || qualityString.Equals(stream.MediaInfo.Name, StringComparison.OrdinalIgnoreCase)) + if (qualityString.Equals(stream.StreamInfo.Video, StringComparison.OrdinalIgnoreCase) || + qualityString.Equals(stream.MediaInfo.Name, StringComparison.OrdinalIgnoreCase)) { return stream; } @@ -69,7 +58,7 @@ public static M3U8.Stream GetStreamOfQuality(this M3U8 m3u8, string qualityStrin var desiredHeight = qualityStringMatch.Groups["Height"]; var desiredFramerate = qualityStringMatch.Groups["Framerate"]; - var filteredStreams = streams + var filteredStreams = m3u8.Streams .WhereOnlyIf(x => x.StreamInfo.Resolution.Width == int.Parse(desiredWidth.ValueSpan), desiredWidth.Success) .WhereOnlyIf(x => x.StreamInfo.Resolution.Height == int.Parse(desiredHeight.ValueSpan), desiredHeight.Success) .WhereOnlyIf(x => Math.Abs(x.StreamInfo.Framerate - int.Parse(desiredFramerate.ValueSpan)) <= 2, desiredFramerate.Success) @@ -83,8 +72,41 @@ public static M3U8.Stream GetStreamOfQuality(this M3U8 m3u8, string qualityStrin }; } + private static bool TryGetKeywordStream(M3U8 m3u8, string qualityString, out M3U8.Stream stream) + { + if (string.IsNullOrWhiteSpace(qualityString)) + { + stream = m3u8.BestQualityStream(); + return true; + } + + if (qualityString.Contains("best", StringComparison.OrdinalIgnoreCase) + || qualityString.Contains("source", StringComparison.OrdinalIgnoreCase) + || qualityString.Contains("chunked", StringComparison.OrdinalIgnoreCase)) + { + stream = m3u8.BestQualityStream(); + return true; + } + + if (qualityString.Contains("worst", StringComparison.OrdinalIgnoreCase)) + { + stream = m3u8.WorstQualityStream(); + return true; + } + + if (qualityString.Contains("audio", StringComparison.OrdinalIgnoreCase) + && m3u8.Streams.FirstOrDefault(x => x.IsAudioOnly()) is { } audioStream) + { + stream = audioStream; + return true; + } + + stream = null; + return false; + } + /// - /// A representing the 's + /// A representing the 's /// and in the format of "{resolution}p{framerate}" or /// public static string GetResolutionFramerateString(this M3U8.Stream stream) @@ -92,7 +114,7 @@ public static string GetResolutionFramerateString(this M3U8.Stream stream) const string RESOLUTION_FRAMERATE_PATTERN = /*lang=regex*/@"\d{3,4}p\d{2,3}"; var mediaInfo = stream.MediaInfo; - if (mediaInfo.Name.Contains("audio", StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(mediaInfo.Name, RESOLUTION_FRAMERATE_PATTERN)) + if (stream.IsAudioOnly() || Regex.IsMatch(mediaInfo.Name, RESOLUTION_FRAMERATE_PATTERN)) { return mediaInfo.Name; } @@ -144,5 +166,20 @@ public static M3U8.Stream BestQualityStream(this M3U8 m3u8) internal static bool IsSource(this M3U8.Stream stream) => stream.MediaInfo.Name.Contains("source", StringComparison.OrdinalIgnoreCase) || stream.MediaInfo.GroupId.Equals("chunked", StringComparison.OrdinalIgnoreCase); + + /// + /// Returns the worst quality non-audio stream from the provided M3U8. + /// + public static M3U8.Stream WorstQualityStream(this M3U8 m3u8) + { + var worstQuality = m3u8.Streams + .Where(x => !x.IsSource() && !x.IsAudioOnly()) + .MinBy(x => x.StreamInfo.Resolution.Width * x.StreamInfo.Resolution.Height * x.StreamInfo.Framerate); + + return worstQuality ?? m3u8.Streams.First(); + } + + private static bool IsAudioOnly(this M3U8.Stream stream) + => stream.MediaInfo.Name.Contains("audio", StringComparison.OrdinalIgnoreCase); } } \ No newline at end of file