Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

M3U8 improvements #917

Merged
merged 9 commits into from
Dec 30, 2023
152 changes: 152 additions & 0 deletions TwitchDownloaderCore.Tests/M3U8ExtensionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using TwitchDownloaderCore.Extensions;
using TwitchDownloaderCore.Tools;

namespace TwitchDownloaderCore.Tests
{
public static class M3U8ExtensionTests
{
[Theory]
[InlineData("720", "720p30")]
[InlineData("720p", "720p30")]
[InlineData("720p30", "720p30")]
[InlineData("1280x720", "720p30")]
[InlineData("1280x720p", "720p30")]
[InlineData("1280x720p30", "720p30")]
[InlineData("720p60", "720p60")]
[InlineData("1280x720p60", "720p60")]
[InlineData("1080", "1080p60")]
[InlineData("1080p", "1080p60")]
[InlineData("1080p60", "1080p60")]
[InlineData("1920x1080", "1080p60")]
[InlineData("1920x1080p", "1080p60")]
[InlineData("1920x1080p60", "1080p60")]
[InlineData("Source", "1080p60")]
public static void CorrectlyFindsStreamOfQualityFromLiveM3U8Response(string qualityString, string expectedPath)
{
var m3u8 = new M3U8(new M3U8.Metadata(), new[]
{
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "chunked", "1080p60 (source)", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.4D401F,mp4a.40.2", (1920, 1080), "chunked", 60),
"1080p60"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "720p60", "720p60", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.4D401F,mp4a.40.2", (1280, 720), "720p60", 60),
"720p60"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "720p30", "720p", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.4D401F,mp4a.40.2", (1280, 720), "720p30", 30),
"720p30")
});

var selectedQuality = m3u8.GetStreamOfQuality(qualityString);
Assert.Equal(expectedPath, selectedQuality.Path);
}

[Theory]
[InlineData("1080", "1080p60")]
[InlineData("1080p", "1080p60")]
[InlineData("1080p60", "1080p60")]
[InlineData("1920x1080", "1080p60")]
[InlineData("1920x1080p", "1080p60")]
[InlineData("1920x1080p60", "1080p60")]
[InlineData("720p60", "720p60")]
[InlineData("1280x720p60", "720p60")]
[InlineData("720", "720p30")]
[InlineData("720p", "720p30")]
[InlineData("720p30", "720p30")]
[InlineData("1280x720", "720p30")]
[InlineData("1280x720p", "720p30")]
[InlineData("1280x720p30", "720p30")]
[InlineData("audio", "audio_only")]
[InlineData("Audio", "audio_only")]
[InlineData("Audio Only", "audio_only")]
public static void CorrectlyFindsStreamOfQualityFromOldM3U8Response(string qualityString, string expectedPath)
{
var m3u8 = new M3U8(new M3U8.Metadata(), new[]
{
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "chunked", "Source", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.4D401F,mp4a.40.2", (1920, 1080), "chunked", 58.644M),
"1080p60"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "720p60", "720p60", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.4D401F,mp4a.40.2", (1280, 720), "720p60", 58.644M),
"720p60"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "720p30", "720p", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.4D401F,mp4a.40.2", (1280, 720), "720p30", 28.814M),
"720p30"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "480p30", "480p", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.42C01E,mp4a.40.2", (852, 480), "480p30", 30.159M),
"480p30"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "360p30", "360p", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.42C01E,mp4a.40.2", (640, 360), "360p30", 30.159M),
"360p30"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "144p30", "144p", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.42C00C,mp4a.40.2", (256, 144), "144p30", 30.159M),
"144p30"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "audio_only", "Audio Only", false, false),
new M3U8.Stream.ExtStreamInfo(0, 1, "mp4a.40.2", (256, 144), "audio_only", 0),
"audio_only")
});

var selectedQuality = m3u8.GetStreamOfQuality(qualityString);
Assert.Equal(expectedPath, selectedQuality.Path);
}

[Theory]
[InlineData("480p60", "1080p60")]
[InlineData("852x480p60", "1080p60")]
[InlineData("Source", "1080p60")]
public static void ReturnsHighestQualityWhenDesiredQualityNotFoundForOldM3U8Response(string qualityString, string expectedPath)
{
var m3u8 = new M3U8(new M3U8.Metadata(), new[]
{
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "chunked", "Source", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.4D401F,mp4a.40.2", (1920, 1080), "chunked", 58.644M),
"1080p60"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "720p60", "720p60", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.4D401F,mp4a.40.2", (1280, 720), "720p60", 58.644M),
"720p60"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "720p30", "720p", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.4D401F,mp4a.40.2", (1280, 720), "720p30", 28.814M),
"720p30"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "480p30", "480p", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.42C01E,mp4a.40.2", (852, 480), "480p30", 30.159M),
"480p30"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "360p30", "360p", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.42C01E,mp4a.40.2", (640, 360), "360p30", 30.159M),
"360p30"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "144p30", "144p", true, true),
new M3U8.Stream.ExtStreamInfo(0, 1, "avc1.42C00C,mp4a.40.2", (256, 144), "144p30", 30.159M),
"144p30"),
new M3U8.Stream(
new M3U8.Stream.ExtMediaInfo(M3U8.Stream.ExtMediaInfo.MediaType.Video, "audio_only", "Audio Only", false, false),
new M3U8.Stream.ExtStreamInfo(0, 1, "mp4a.40.2", (256, 144), "audio_only", 0),
"audio_only")
});

var selectedQuality = m3u8.GetStreamOfQuality(qualityString);
Assert.Equal(expectedPath, selectedQuality.Path);
}

[Fact]
public static void ThrowsWhenNoStreamsArePresent()
{
var m3u8 = new M3U8(new M3U8.Metadata(), Array.Empty<M3U8.Stream>());

Assert.Throws<ArgumentException>(() => m3u8.GetStreamOfQuality(""));
}
}
}
93 changes: 77 additions & 16 deletions TwitchDownloaderCore.Tests/M3U8Tests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using TwitchDownloaderCore.Tools;
using System.Text;
using TwitchDownloaderCore.Tools;

namespace TwitchDownloaderCore.Tests
{
// ReSharper disable StringLiteralTypo
public class M3U8Tests
{
[Fact]
public void CorrectlyParsesTwitchM3U8OfTransportStreams()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void CorrectlyParsesTwitchM3U8OfTransportStreams(bool useStream)
{
const string ExampleM3U8Twitch =
"#EXTM3U" +
Expand All @@ -25,7 +28,17 @@ public void CorrectlyParsesTwitchM3U8OfTransportStreams()
"\n#EXTINF:10.000,\n40.ts\n#EXTINF:10.000,\n41.ts\n#EXTINF:10.000,\n42.ts\n#EXTINF:10.000,\n43.ts\n#EXTINF:10.000,\n44.ts\n#EXTINF:10.000,\n45.ts\n#EXTINF:10.000,\n46.ts\n#EXTINF:10.000,\n47.ts" +
"\n#EXTINF:10.000,\n48.ts\n#EXTINF:10.000,\n49.ts\n#EXT-X-ENDLIST";

var m3u8 = M3U8.Parse(ExampleM3U8Twitch);
M3U8 m3u8;
if (useStream)
{
var bytes = Encoding.Unicode.GetBytes(ExampleM3U8Twitch);
using var ms = new MemoryStream(bytes);
m3u8 = M3U8.Parse(ms, Encoding.Unicode);
}
else
{
m3u8 = M3U8.Parse(ExampleM3U8Twitch);
}

Assert.Equal(3u, m3u8.FileMetadata.Version);
Assert.Equal(10u, m3u8.FileMetadata.StreamTargetDuration);
Expand All @@ -45,8 +58,10 @@ public void CorrectlyParsesTwitchM3U8OfTransportStreams()
}
}

[Fact]
public void CorrectlyParsesTwitchM3U8OfLiveStreams()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void CorrectlyParsesTwitchM3U8OfLiveStreams(bool useStream)
{
const string ExampleM3U8Twitch =
"#EXTM3U" +
Expand Down Expand Up @@ -98,7 +113,17 @@ public void CorrectlyParsesTwitchM3U8OfLiveStreams()
(DateTimeOffset.Parse("2023-09-17T02:32:16.242Z"), 2.000m, true, "https://video-edge-foo.bar.abs.hls.ttvnw.net/v1/segment/hij-567KLM_890.ts")
};

var m3u8 = M3U8.Parse(ExampleM3U8Twitch);
M3U8 m3u8;
if (useStream)
{
var bytes = Encoding.Unicode.GetBytes(ExampleM3U8Twitch);
using var ms = new MemoryStream(bytes);
m3u8 = M3U8.Parse(ms, Encoding.Unicode);
}
else
{
m3u8 = M3U8.Parse(ExampleM3U8Twitch);
}

Assert.Equal(3u, m3u8.FileMetadata.Version);
Assert.Equal(5u, m3u8.FileMetadata.StreamTargetDuration);
Expand All @@ -120,8 +145,10 @@ public void CorrectlyParsesTwitchM3U8OfLiveStreams()
}
}

[Fact]
public void CorrectlyParsesTwitchM3U8OfPlaylists()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void CorrectlyParsesTwitchM3U8OfPlaylists(bool useStream)
{
const string ExampleM3U8Twitch =
"#EXTM3U" +
Expand Down Expand Up @@ -167,7 +194,17 @@ public void CorrectlyParsesTwitchM3U8OfPlaylists()
"https://abc123def456gh.cloudfront.net/123abc456def789ghi01_streamer42_12345678901_1234567890/160p30/index-dvr.m3u8")
};

var m3u8 = M3U8.Parse(ExampleM3U8Twitch);
M3U8 m3u8;
if (useStream)
{
var bytes = Encoding.Unicode.GetBytes(ExampleM3U8Twitch);
using var ms = new MemoryStream(bytes);
m3u8 = M3U8.Parse(ms, Encoding.Unicode);
}
else
{
m3u8 = M3U8.Parse(ExampleM3U8Twitch);
}

Assert.Equal(streams.Length, m3u8.Streams.Length);
Assert.Equivalent(streams[0], m3u8.Streams[0], true);
Expand Down Expand Up @@ -214,8 +251,10 @@ public void CorrectlyParsesTwitchM3U8StreamInfo(string streamInfoString, int ban
Assert.Equal(framerate, streamInfo.Framerate);
}

[Fact]
public void CorrectlyParsesKickM3U8OfTransportStreams()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void CorrectlyParsesKickM3U8OfTransportStreams(bool useStream)
{
const string ExampleM3U8Kick =
"#EXTM3U" +
Expand Down Expand Up @@ -274,7 +313,17 @@ public void CorrectlyParsesKickM3U8OfTransportStreams()
(DateTimeOffset.Parse("2023-11-16T05:35:07.97Z"), (1506068, 6462876), "506.ts")
};

var m3u8 = M3U8.Parse(ExampleM3U8Kick);
M3U8 m3u8;
if (useStream)
{
var bytes = Encoding.Unicode.GetBytes(ExampleM3U8Kick);
using var ms = new MemoryStream(bytes);
m3u8 = M3U8.Parse(ms, Encoding.Unicode);
}
else
{
m3u8 = M3U8.Parse(ExampleM3U8Kick);
}

Assert.Equal(4u, m3u8.FileMetadata.Version);
Assert.Equal(2u, m3u8.FileMetadata.StreamTargetDuration);
Expand All @@ -293,8 +342,10 @@ public void CorrectlyParsesKickM3U8OfTransportStreams()
}
}

[Fact]
public void CorrectlyParsesKickM3U8OfPlaylists()
[Theory]
[InlineData(false)]
[InlineData(true)]
public void CorrectlyParsesKickM3U8OfPlaylists(bool useStream)
{
const string ExampleM3U8Kick =
"#EXTM3U" +
Expand Down Expand Up @@ -335,7 +386,17 @@ public void CorrectlyParsesKickM3U8OfPlaylists()
"160p30/playlist.m3u8")
};

var m3u8 = M3U8.Parse(ExampleM3U8Kick);
M3U8 m3u8;
if (useStream)
{
var bytes = Encoding.Unicode.GetBytes(ExampleM3U8Kick);
using var ms = new MemoryStream(bytes);
m3u8 = M3U8.Parse(ms, Encoding.Unicode);
}
else
{
m3u8 = M3U8.Parse(ExampleM3U8Kick);
}

Assert.Equal(streams.Length, m3u8.Streams.Length);
Assert.Equivalent(streams[0], m3u8.Streams[0], true);
Expand Down
19 changes: 19 additions & 0 deletions TwitchDownloaderCore/Extensions/LinqExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace TwitchDownloaderCore.Extensions
{
public static class LinqExtensions
{
public static IEnumerable<TSource> WhereOnlyIf<TSource>(this IEnumerable<TSource> enumerable, Func<TSource, bool> predicate, bool shouldFilter)
{
if (shouldFilter)
{
return enumerable.Where(predicate);
}

return enumerable;
}
}
}
Loading