mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 08:30:29 +00:00
253 lines
7.9 KiB
Go
253 lines
7.9 KiB
Go
package hls
|
|
|
|
import (
|
|
"net/url"
|
|
"testing"
|
|
)
|
|
|
|
func mustURL(t *testing.T, raw string) *url.URL {
|
|
t.Helper()
|
|
u, err := url.Parse(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return u
|
|
}
|
|
|
|
func TestParseMediaTS(t *testing.T) {
|
|
content := "#EXTM3U\n" +
|
|
"#EXT-X-TARGETDURATION:20\n" +
|
|
"#EXT-X-VERSION:3\n" +
|
|
"#EXT-X-MEDIA-SEQUENCE:1\n" +
|
|
"#EXTINF:20.000,\n" +
|
|
"seg-1.ts\n" +
|
|
"#EXTINF:20.000,\n" +
|
|
"seg-2.ts\n" +
|
|
"#EXT-X-ENDLIST\n"
|
|
media, err := ParseMedia(content, mustURL(t, "https://cdn.example.com/vod/index.m3u8"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if media.Live {
|
|
t.Error("VOD playlist should not be live")
|
|
}
|
|
if media.IsFMP4 {
|
|
t.Error("TS playlist should not be fMP4")
|
|
}
|
|
if len(media.Segments) != 2 {
|
|
t.Fatalf("want 2 segments, got %d", len(media.Segments))
|
|
}
|
|
want := "https://cdn.example.com/vod/seg-1.ts"
|
|
if media.Segments[0].URI != want {
|
|
t.Errorf("want uri %s, got %s", want, media.Segments[0].URI)
|
|
}
|
|
if media.Segments[0].Sequence != 1 {
|
|
t.Errorf("want sequence 1, got %d", media.Segments[0].Sequence)
|
|
}
|
|
if media.Segments[0].Key != nil {
|
|
t.Error("segment should be unencrypted")
|
|
}
|
|
}
|
|
|
|
func TestParseMediaWithoutExtinf(t *testing.T) {
|
|
// Some playlists list segment URIs without EXTINF lines.
|
|
content := "#EXTM3U\nseg-1.ts\nseg-2.ts\n#EXT-X-ENDLIST\n"
|
|
media, err := ParseMedia(content, mustURL(t, "https://cdn.example.com/a/index.m3u8"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(media.Segments) != 2 {
|
|
t.Fatalf("want 2 segments, got %d", len(media.Segments))
|
|
}
|
|
}
|
|
|
|
func TestParseMediaLive(t *testing.T) {
|
|
content := "#EXTM3U\n#EXT-X-TARGETDURATION:6\n#EXTINF:6.0,\nseg-1.ts\n"
|
|
media, err := ParseMedia(content, mustURL(t, "https://cdn.example.com/live/index.m3u8"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !media.Live {
|
|
t.Error("playlist without ENDLIST should be live")
|
|
}
|
|
}
|
|
|
|
func TestParseMediaFMP4(t *testing.T) {
|
|
content := "#EXTM3U\n" +
|
|
"#EXT-X-VERSION:6\n" +
|
|
"#EXT-X-MAP:URI=\"init.mp4\"\n" +
|
|
"#EXTINF:5.0,\n" +
|
|
"seg-1.m4s\n" +
|
|
"#EXT-X-ENDLIST\n"
|
|
media, err := ParseMedia(content, mustURL(t, "https://cdn.example.com/vod/index.m3u8"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !media.IsFMP4 {
|
|
t.Error("playlist with MAP should be fMP4")
|
|
}
|
|
if len(media.Segments) != 2 {
|
|
t.Fatalf("want 2 segments (init + 1), got %d", len(media.Segments))
|
|
}
|
|
if !media.Segments[0].IsInit {
|
|
t.Error("first segment should be the init section")
|
|
}
|
|
if media.Segments[0].URI != "https://cdn.example.com/vod/init.mp4" {
|
|
t.Errorf("init uri wrong: %s", media.Segments[0].URI)
|
|
}
|
|
if media.OutputExtension() != ".mp4" {
|
|
t.Errorf("want .mp4, got %s", media.OutputExtension())
|
|
}
|
|
}
|
|
|
|
func TestParseMediaMultiMapError(t *testing.T) {
|
|
content := "#EXTM3U\n" +
|
|
"#EXT-X-MAP:URI=\"init1.mp4\"\n" +
|
|
"#EXTINF:5.0,\nseg-1.m4s\n" +
|
|
"#EXT-X-DISCONTINUITY\n" +
|
|
"#EXT-X-MAP:URI=\"init2.mp4\"\n" +
|
|
"#EXTINF:5.0,\nseg-2.m4s\n" +
|
|
"#EXT-X-ENDLIST\n"
|
|
if _, err := ParseMedia(content, mustURL(t, "https://cdn.example.com/vod/index.m3u8")); err == nil {
|
|
t.Error("multi-period fMP4 should be rejected")
|
|
}
|
|
}
|
|
|
|
func TestParseMediaKeyRotation(t *testing.T) {
|
|
content := "#EXTM3U\n" +
|
|
"#EXT-X-KEY:METHOD=AES-128,URI=\"key1.bin\",IV=0x00000000000000000000000000000001\n" +
|
|
"#EXTINF:5.0,\nseg-1.ts\n" +
|
|
"#EXT-X-KEY:METHOD=AES-128,URI=\"key2.bin\"\n" +
|
|
"#EXTINF:5.0,\nseg-2.ts\n" +
|
|
"#EXT-X-KEY:METHOD=NONE\n" +
|
|
"#EXTINF:5.0,\nseg-3.ts\n" +
|
|
"#EXT-X-ENDLIST\n"
|
|
media, err := ParseMedia(content, mustURL(t, "https://cdn.example.com/vod/index.m3u8"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if media.Segments[0].Key == nil || media.Segments[0].Key.URI != "https://cdn.example.com/vod/key1.bin" {
|
|
t.Fatalf("segment 1 key wrong: %+v", media.Segments[0].Key)
|
|
}
|
|
if len(media.Segments[0].Key.IV) != 16 || media.Segments[0].Key.IV[15] != 1 {
|
|
t.Error("explicit IV not parsed")
|
|
}
|
|
if media.Segments[1].Key == nil || media.Segments[1].Key.URI != "https://cdn.example.com/vod/key2.bin" {
|
|
t.Fatalf("segment 2 key wrong: %+v", media.Segments[1].Key)
|
|
}
|
|
if media.Segments[1].Key.IV != nil {
|
|
t.Error("key without IV should leave IV nil (sequence-derived)")
|
|
}
|
|
if media.Segments[2].Key != nil {
|
|
t.Error("segment 3 should be unencrypted after METHOD=NONE")
|
|
}
|
|
}
|
|
|
|
func TestParseMediaSampleAESError(t *testing.T) {
|
|
content := "#EXTM3U\n#EXT-X-KEY:METHOD=SAMPLE-AES,URI=\"skd://key\"\n#EXTINF:5.0,\nseg-1.ts\n"
|
|
if _, err := ParseMedia(content, mustURL(t, "https://cdn.example.com/vod/index.m3u8")); err == nil {
|
|
t.Error("SAMPLE-AES should be rejected")
|
|
}
|
|
}
|
|
|
|
func TestParseMediaByterange(t *testing.T) {
|
|
content := "#EXTM3U\n" +
|
|
"#EXT-X-BYTERANGE:1000@0\nseg.ts\n" +
|
|
"#EXT-X-BYTERANGE:2000\nseg.ts\n" +
|
|
"#EXT-X-BYTERANGE:3000\nseg.ts\n" +
|
|
"#EXT-X-ENDLIST\n"
|
|
media, err := ParseMedia(content, mustURL(t, "https://cdn.example.com/vod/index.m3u8"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ranges := [][2]int64{{0, 1000}, {1000, 2000}, {3000, 3000}}
|
|
for i, want := range ranges {
|
|
br := media.Segments[i].Byterange
|
|
if br == nil {
|
|
t.Fatalf("segment %d byterange missing", i)
|
|
}
|
|
if br.Offset != want[0] || br.Length != want[1] {
|
|
t.Errorf("segment %d: want offset %d length %d, got %d/%d", i, want[0], want[1], br.Offset, br.Length)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseMediaGap(t *testing.T) {
|
|
content := "#EXTM3U\n#EXTINF:5.0,\nseg-1.ts\n#EXT-X-GAP\n#EXTINF:5.0,\nseg-2.ts\n#EXT-X-ENDLIST\n"
|
|
media, err := ParseMedia(content, mustURL(t, "https://cdn.example.com/vod/index.m3u8"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if media.Segments[0].Gap || !media.Segments[1].Gap {
|
|
t.Error("gap flags wrong")
|
|
}
|
|
}
|
|
|
|
func TestParseMediaInvalidHeader(t *testing.T) {
|
|
if _, err := ParseMedia("not a playlist", mustURL(t, "https://cdn.example.com/a.m3u8")); err == nil {
|
|
t.Error("invalid content should be rejected")
|
|
}
|
|
}
|
|
|
|
func TestParseMasterPickBest(t *testing.T) {
|
|
content := "#EXTM3U\n" +
|
|
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud\",NAME=\"en\",DEFAULT=YES,URI=\"audio/index.m3u8\"\n" +
|
|
"#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360\n" +
|
|
"360p/index.m3u8\n" +
|
|
"#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1920x1080,CODECS=\"avc1.640028,mp4a.40.2\"\n" +
|
|
"1080p/index.m3u8?sign=abc\n" +
|
|
"#EXT-X-ENDLIST\n"
|
|
variants, err := ParseMaster(content, mustURL(t, "https://cdn.example.com/vod/master.m3u8"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(variants) != 2 {
|
|
t.Fatalf("want 2 variants, got %d", len(variants))
|
|
}
|
|
best := PickBestVariant(variants)
|
|
if best.Bandwidth != 2500000 {
|
|
t.Errorf("want highest bandwidth variant, got %d", best.Bandwidth)
|
|
}
|
|
if best.URI != "https://cdn.example.com/vod/1080p/index.m3u8?sign=abc" {
|
|
t.Errorf("variant uri wrong: %s", best.URI)
|
|
}
|
|
}
|
|
|
|
func TestResolveURLQueryNotInherited(t *testing.T) {
|
|
base := mustURL(t, "https://cdn.example.com/vod/index.m3u8?sign=abc")
|
|
got := resolveURL(base, "seg-1.ts")
|
|
if got != "https://cdn.example.com/vod/seg-1.ts" {
|
|
t.Errorf("relative resolution should follow RFC3986 (no query inheritance), got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestDeriveOutputName(t *testing.T) {
|
|
media := &Media{}
|
|
if got := DeriveOutputName("https://cdn.example.com/ts2/a/index-v1-a1.m3u8", media); got != "index-v1-a1.ts" {
|
|
t.Errorf("want index-v1-a1.ts, got %s", got)
|
|
}
|
|
fmp4 := &Media{IsFMP4: true}
|
|
if got := DeriveOutputName("https://cdn.example.com/ts2/a/index.m3u8", fmp4); got != "index.mp4" {
|
|
t.Errorf("want index.mp4, got %s", got)
|
|
}
|
|
if got := DeriveOutputName("https://cdn.example.com/play?token=x", media); got != "play.ts" {
|
|
t.Errorf("want play.ts, got %s", got)
|
|
}
|
|
if got := DeriveOutputName("https://cdn.example.com/", media); got != "cdn.example.com.ts" {
|
|
t.Errorf("want host fallback, got %s", got)
|
|
}
|
|
}
|
|
|
|
func TestParseAttributesQuotedComma(t *testing.T) {
|
|
attrs := parseAttributes(`URI="a,b.ts",IV=0xABCDEF00000000000000000000000001,NAME="x"`)
|
|
if attrs["URI"] != "a,b.ts" {
|
|
t.Errorf("quoted comma value wrong: %q", attrs["URI"])
|
|
}
|
|
if attrs["IV"] != "0xABCDEF00000000000000000000000001" {
|
|
t.Errorf("hex value wrong: %q", attrs["IV"])
|
|
}
|
|
if attrs["NAME"] != "x" {
|
|
t.Errorf("quoted value wrong: %q", attrs["NAME"])
|
|
}
|
|
}
|