package m3u8 import ( "bytes" "strings" "testing" "91porn-server/common/constant" "91porn-server/common/constant/redisconst" grafovm3u8 "github.com/grafov/m3u8" ) func TestM3u8PureCacheKeyUsesFullSource(t *testing.T) { tests := []struct { name string sourceA string sourceB string }{ { name: "same basename in different directories", sourceA: "/sp/movie-a/index.m3u8", sourceB: "/sp/movie-b/index.m3u8", }, { name: "filenames previously collapsed by strings.Trim", sourceA: "/sp/movie/movie.m3u8", sourceB: "/sp/movie/ovie.m3u8", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { keyA := m3u8PureCacheKey(tt.sourceA, constant.MediaSourceSP) keyB := m3u8PureCacheKey(tt.sourceB, constant.MediaSourceSP) if keyA == keyB { t.Fatalf("different playlist sources share cache key %q", keyA) } }) } } func TestNormalizeLaosijiM3u8Source(t *testing.T) { for _, source := range []string{ "laosiji/m3m/movie/index.m3u8", "/laosiji/m3m/movie/index.m3u8", } { if got := normalizeLaosijiM3u8Source(source); got != "m3m/movie/index.m3u8" { t.Fatalf("normalizeLaosijiM3u8Source(%q) = %q", source, got) } } } func TestM3u8PureCacheKeyNormalizesSourceAndIncludesMediaSource(t *testing.T) { const source = "/sp/movie/season/../index.m3u8?b=2&a=1" normalizedVariant := " /sp/movie/index.m3u8?a=1&b=2#ignored " spKey := m3u8PureCacheKey(source, constant.MediaSourceSP) if got := m3u8PureCacheKey(normalizedVariant, constant.MediaSourceSP); got != spKey { t.Fatalf("equivalent sources should share a cache key: %q != %q", got, spKey) } if got := m3u8PureCacheKey(source, constant.MediaSourcePMS); got == spKey { t.Fatalf("different media sources should not share a cache key: %q", got) } if !strings.HasPrefix(spKey, redisconst.M3u8PureCacheFmt(constant.MediaSourceSP+":")) { t.Fatalf("cache key should include media source prefix, got %q", spKey) } } func TestDecodeFromReaderKeepsHevcMap(t *testing.T) { data := []byte(`#EXTM3U #EXT-X-TARGETDURATION:6 #EXT-X-VERSION:6 #EXT-X-MEDIA-SEQUENCE:1 #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-MAP:URI="https://cdn.g3ejjm8m.com/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_0.m4s" #EXTINF:6.006, 683a_1.m4s #EXTINF:6.006, 683a_2.m4s #EXT-X-ENDLIST `) buf := DecodeFromReader( data, "https://rs.hyxrp.cn", "/api/app/vid/lsjsec", "", constant.MediaSourceLaoSiJi, constant.MediaSourceAuthKey, constant.DefaultTsAuthKeyVersion, "m3m/demo.m3u8", ) if buf == nil { t.Fatal("DecodeFromReader returned nil") } got := buf.String() for _, want := range []string{ `#EXT-X-MAP:URI="https://rs.hyxrp.cn/laosiji/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_0.m4s?md=`, `https://rs.hyxrp.cn/laosiji/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_1.m4s?md=`, `https://rs.hyxrp.cn/laosiji/rk130/m3u8-v5/f98/f98495114eca4ae66714b0d0249d8980/683a_2.m4s?md=`, } { if !strings.Contains(got, want) { t.Fatalf("rewritten playlist missing %q:\n%s", want, got) } } if strings.Contains(got, "#EXT-X-KEY") { t.Fatalf("unencrypted HEVC playlist should not add EXT-X-KEY:\n%s", got) } if strings.Contains(got, "/laosiji/683a_") { t.Fatalf("relative HEVC segments were not resolved against EXT-X-MAP:\n%s", got) } } func TestDecodeFromReaderKeepsEncryptedH264Key(t *testing.T) { data := []byte(`#EXTM3U #EXT-X-TARGETDURATION:6 #EXT-X-VERSION:3 #EXT-X-KEY:METHOD=AES-128,URI="source.key",IV=0x00000000000000000000000000000001 #EXTINF:6, segment.ts #EXT-X-ENDLIST `) buf := DecodeFromReader( data, "https://cdn.example.com/video", "/api/app/vid/sec", "", constant.MediaSourceSP, "", constant.DefaultTsAuthKeyVersion, "sp/demo.m3u8", ) if buf == nil { t.Fatal("DecodeFromReader returned nil") } got := buf.String() for _, want := range []string{ `#EXT-X-KEY:METHOD=AES-128,URI="/api/app/vid/sec",IV=0x00000000000000000000000000000001`, "https://cdn.example.com/video/segment.ts", } { if !strings.Contains(got, want) { t.Fatalf("rewritten playlist missing %q:\n%s", want, got) } } } func TestCreateResolvesFirstSegmentAgainstSegmentMap(t *testing.T) { src, err := grafovm3u8.NewMediaPlaylist(1, 1) if err != nil { t.Fatalf("create source playlist: %v", err) } if err = src.Append("first.m4s", 6, ""); err != nil { t.Fatalf("append source segment: %v", err) } mapURI := "https://cdn.g3ejjm8m.com/hevc/movie/init.m4s" if err = src.SetMap(mapURI, 0, 0); err != nil { t.Fatalf("set source segment map: %v", err) } if src.Map != nil { t.Fatal("test setup must use a segment-level map") } buf := Create( src, "https://rs.hyxrp.cn", "/api/app/vid/lsjsec", "", constant.MediaSourceLaoSiJi, constant.MediaSourceAuthKey, constant.DefaultTsAuthKeyVersion, "m3m/demo.m3u8", ) if buf == nil { t.Fatal("Create returned nil") } got := buf.String() for _, want := range []string{ `#EXT-X-MAP:URI="https://rs.hyxrp.cn/laosiji/hevc/movie/init.m4s?md=`, "https://rs.hyxrp.cn/laosiji/hevc/movie/first.m4s?md=", } { if !strings.Contains(got, want) { t.Fatalf("rewritten playlist missing %q:\n%s", want, got) } } if strings.Contains(got, "/laosiji/first.m4s") { t.Fatalf("first relative segment was not resolved against its map:\n%s", got) } } func TestDecodeFromReaderPreservesCMAFSegmentMetadata(t *testing.T) { data := []byte(`#EXTM3U #EXT-X-VERSION:6 #EXT-X-TARGETDURATION:6 #EXT-X-KEY:METHOD=AES-128,URI="first.key",IV=0x00000000000000000000000000000001 #EXT-X-MAP:URI="https://cdn.example.com/cmaf/init.mp4" #EXT-X-BYTERANGE:100@0 #EXTINF:6, chunk.mp4 #EXT-X-DISCONTINUITY #EXT-X-KEY:METHOD=AES-128,URI="second.key",IV=0x00000000000000000000000000000002 #EXT-X-BYTERANGE:120@100 #EXTINF:6, chunk.mp4 #EXT-X-ENDLIST `) buf := DecodeFromReader( data, "https://play.example.com", "/api/app/vid/sec", "", constant.MediaSourceSP, "", constant.DefaultTsAuthKeyVersion, "sp/cmaf/index.m3u8", ) if buf == nil { t.Fatal("DecodeFromReader returned nil") } got := buf.String() for _, want := range []string{ "#EXT-X-BYTERANGE:100@0", "#EXT-X-BYTERANGE:120@100", "#EXT-X-DISCONTINUITY", "IV=0x00000000000000000000000000000001", "IV=0x00000000000000000000000000000002", } { if !strings.Contains(got, want) { t.Fatalf("rewritten playlist missing %q:\n%s", want, got) } } if count := strings.Count(got, "#EXT-X-KEY:"); count != 2 { t.Fatalf("rewritten playlist key count = %d, want 2:\n%s", count, got) } } func TestRewriteMasterPlaylist(t *testing.T) { input := []byte(`#EXTM3U #EXT-X-VERSION:3 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="main",DEFAULT=YES,AUTOSELECT=YES,URI="audio/index.m3u8" #EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720,AUDIO="audio" video/720/index.m3u8 `) output, err := RewriteMasterPlaylist(input, func(rawURI string) (string, error) { return "/signed/" + rawURI, nil }) if err != nil { t.Fatalf("RewriteMasterPlaylist failed: %v", err) } playlist, listType, err := grafovm3u8.DecodeFrom(bytes.NewReader(output.Bytes()), false) if err != nil { t.Fatalf("decode rewritten master: %v", err) } if listType != grafovm3u8.MASTER { t.Fatalf("list type = %v, want master", listType) } master := playlist.(*grafovm3u8.MasterPlaylist) if len(master.Variants) != 1 || master.Variants[0].URI != "/signed/video/720/index.m3u8" { t.Fatalf("variant was not rewritten: %+v", master.Variants) } if len(master.Variants[0].Alternatives) != 1 || master.Variants[0].Alternatives[0].URI != "/signed/audio/index.m3u8" { t.Fatalf("alternative was not rewritten: %+v", master.Variants[0].Alternatives) } } func TestRewriteMasterPlaylistLeavesMediaPlaylistUnchanged(t *testing.T) { input := []byte("#EXTM3U\n#EXT-X-TARGETDURATION:4\n#EXTINF:4,\nsegment.ts\n#EXT-X-ENDLIST\n") output, err := RewriteMasterPlaylist(input, func(string) (string, error) { t.Fatal("media playlist URI rewriter must not be called") return "", nil }) if err != nil { t.Fatalf("RewriteMasterPlaylist failed: %v", err) } if !bytes.Equal(output.Bytes(), input) { t.Fatalf("media playlist changed:\n%s", output.String()) } }