@@ -0,0 +1,503 @@
|
||||
package m3u8
|
||||
|
||||
import (
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/httputil"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/stderr"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/web/webg"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafov/m3u8"
|
||||
)
|
||||
|
||||
func GetAPPM3u8(source, fileName, _ string, cdn string, fsIo func(source, mds string) (data []byte, err error)) (*bytes.Buffer, error) {
|
||||
mds := GetMediaResouce(source)
|
||||
key := m3u8PureCacheKey(source, mds)
|
||||
m3u8redis, err := appg.Redis.Get(key)
|
||||
var m3u8Byte []byte
|
||||
if m3u8redis != nil && err == nil {
|
||||
//判断是否是m3u8文件
|
||||
if IsM3u8([]byte(*m3u8redis)) {
|
||||
m3u8Byte = []byte(*m3u8redis)
|
||||
} else {
|
||||
_, _ = appg.Redis.Del(key)
|
||||
}
|
||||
}
|
||||
if mds == constant.MediaSourceLaoSiJi {
|
||||
source = normalizeLaosijiM3u8Source(source)
|
||||
}
|
||||
if m3u8Byte == nil || len(m3u8Byte) == 0 {
|
||||
m3u8Byte, err = fsIo(source, mds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = appg.Redis.Set(key, string(m3u8Byte), redisconst.M3u8CacheExpire)
|
||||
}
|
||||
// TS 分片鉴权签名密钥:优先取配置(appg.Conf.Base.TsAuth),未配置时回退内置默认(版本 default + 内置常量)
|
||||
keyVersion, authKey := appg.Conf.Base.TsAuth.Resolve()
|
||||
var bytebuff *bytes.Buffer
|
||||
if mds == constant.MediaSourcePMS {
|
||||
bytebuff = DecodeFromReader(m3u8Byte, "", "/api/app/vid", "", mds, authKey, keyVersion, source)
|
||||
} else if mds == constant.MediaSourceSP {
|
||||
bytebuff = DecodeFromReader(m3u8Byte, cdn+strings.TrimSuffix(source, fileName), "/api/app/vid/sec", "", mds, authKey, keyVersion, source)
|
||||
} else if mds == constant.MediaSourceLaoSiJi {
|
||||
bytebuff = DecodeFromReader(m3u8Byte, cdn, "/api/app/vid/lsjsec", "", mds, authKey, keyVersion, source)
|
||||
} else if mds == constant.MediaSourceJH1B {
|
||||
bytebuff = DecodeFromReader(m3u8Byte, cdn+strings.TrimSuffix(source, fileName), "/api/app/vid/m3u8sec", "", mds, authKey, keyVersion, source)
|
||||
}
|
||||
if bytebuff == nil {
|
||||
log.Warn("can't create m3u8 file", log.Any("source", source))
|
||||
return nil, errors.New(stderr.CodeEmptyData.Msg())
|
||||
}
|
||||
return bytebuff, nil
|
||||
}
|
||||
|
||||
func normalizeLaosijiM3u8Source(source string) string {
|
||||
source = strings.TrimLeft(strings.TrimSpace(source), "/")
|
||||
return strings.TrimPrefix(source, "laosiji/")
|
||||
}
|
||||
|
||||
// m3u8PureCacheKey isolates raw playlists by media source and their complete
|
||||
// normalized source path. Hashing keeps the Redis key bounded while avoiding
|
||||
// collisions between common basenames such as index.m3u8.
|
||||
func m3u8PureCacheKey(source, mds string) string {
|
||||
normalizedSource := normalizeM3u8CacheSource(source)
|
||||
sum := sha256.Sum256([]byte(mds + "\x00" + normalizedSource))
|
||||
return redisconst.M3u8PureCacheFmt(mds + ":" + hex.EncodeToString(sum[:]))
|
||||
}
|
||||
|
||||
func normalizeM3u8CacheSource(source string) string {
|
||||
source = strings.TrimSpace(source)
|
||||
parsed, err := url.Parse(source)
|
||||
if err != nil {
|
||||
return path.Clean("/" + strings.TrimLeft(strings.ReplaceAll(source, "\\", "/"), "/"))
|
||||
}
|
||||
|
||||
parsed.Scheme = strings.ToLower(parsed.Scheme)
|
||||
parsed.Host = strings.ToLower(parsed.Host)
|
||||
parsed.Fragment = ""
|
||||
parsed.Path = path.Clean("/" + strings.TrimLeft(strings.ReplaceAll(parsed.Path, "\\", "/"), "/"))
|
||||
parsed.RawPath = ""
|
||||
if parsed.RawQuery != "" {
|
||||
if query, queryErr := url.ParseQuery(parsed.RawQuery); queryErr == nil {
|
||||
parsed.RawQuery = query.Encode()
|
||||
}
|
||||
}
|
||||
return parsed.String()
|
||||
}
|
||||
|
||||
func DecodeFromReader(reader []byte, cdnUrl, serUrl, key, mds, authKey, keyVersion, source string) *bytes.Buffer {
|
||||
p, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false)
|
||||
if err != nil {
|
||||
log.Error("m3u8 decodeFrom error", log.E(err))
|
||||
return nil
|
||||
}
|
||||
switch listType {
|
||||
case m3u8.MEDIA:
|
||||
return Create(p.(*m3u8.MediaPlaylist), cdnUrl, serUrl, key, mds, authKey, keyVersion, source)
|
||||
case m3u8.MASTER:
|
||||
return p.(*m3u8.MasterPlaylist).Encode()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RewriteMasterPlaylist rewrites every child playlist URI in a master HLS
|
||||
// playlist. Media playlists are returned unchanged. The signed H.265 pull
|
||||
// endpoint uses this so variants and alternate renditions do not lose their
|
||||
// HMAC when the cloud transcoder follows a relative child URI.
|
||||
func RewriteMasterPlaylist(reader []byte, rewrite func(string) (string, error)) (*bytes.Buffer, error) {
|
||||
if rewrite == nil {
|
||||
return bytes.NewBuffer(reader), nil
|
||||
}
|
||||
playlist, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if listType != m3u8.MASTER {
|
||||
return bytes.NewBuffer(reader), nil
|
||||
}
|
||||
|
||||
master := playlist.(*m3u8.MasterPlaylist)
|
||||
rewritten := make(map[string]string)
|
||||
rewriteURI := func(rawURI string) (string, error) {
|
||||
rawURI = strings.TrimSpace(rawURI)
|
||||
if rawURI == "" {
|
||||
return "", nil
|
||||
}
|
||||
if value, ok := rewritten[rawURI]; ok {
|
||||
return value, nil
|
||||
}
|
||||
value, rewriteErr := rewrite(rawURI)
|
||||
if rewriteErr != nil {
|
||||
return "", rewriteErr
|
||||
}
|
||||
rewritten[rawURI] = value
|
||||
return value, nil
|
||||
}
|
||||
|
||||
seenAlternatives := make(map[*m3u8.Alternative]struct{})
|
||||
for _, variant := range master.Variants {
|
||||
if variant == nil {
|
||||
continue
|
||||
}
|
||||
variant.URI, err = rewriteURI(variant.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rewrite HLS variant: %w", err)
|
||||
}
|
||||
for _, alternative := range variant.Alternatives {
|
||||
if alternative == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seenAlternatives[alternative]; ok {
|
||||
continue
|
||||
}
|
||||
seenAlternatives[alternative] = struct{}{}
|
||||
alternative.URI, err = rewriteURI(alternative.URI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rewrite HLS alternative: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return master.Encode(), nil
|
||||
}
|
||||
|
||||
func Create(src *m3u8.MediaPlaylist, cdnUrl, serUrl, key, mds, authKey, keyVersion, source string) *bytes.Buffer {
|
||||
p, e := m3u8.NewMediaPlaylist(src.WinSize(), src.Count())
|
||||
if e != nil {
|
||||
log.Error(fmt.Sprintf("Creating of media playlist failed: %s", e))
|
||||
return nil
|
||||
}
|
||||
p.SetVersion(src.Version())
|
||||
p.SeqNo = src.SeqNo
|
||||
p.DiscontinuitySeq = src.DiscontinuitySeq
|
||||
p.StartTime = src.StartTime
|
||||
p.StartTimePrecise = src.StartTimePrecise
|
||||
p.MediaType = src.MediaType
|
||||
p.Iframe = src.Iframe
|
||||
p.Args = src.Args
|
||||
p.WV = src.WV
|
||||
for _, customTag := range src.Custom {
|
||||
p.SetCustomTag(customTag)
|
||||
}
|
||||
now := time.Now()
|
||||
var activeMap *m3u8.Map
|
||||
if src.Map != nil {
|
||||
activeMap = src.Map
|
||||
p.SetDefaultMap(
|
||||
rewritePlaylistMediaURI(src.Map.URI, cdnUrl, authKey, keyVersion, mds, source, now),
|
||||
src.Map.Limit,
|
||||
src.Map.Offset,
|
||||
)
|
||||
}
|
||||
activeKey := src.Key
|
||||
for _, v := range src.Segments {
|
||||
if v != nil {
|
||||
currentMapURI := ""
|
||||
if activeMap != nil {
|
||||
currentMapURI = activeMap.URI
|
||||
}
|
||||
if v.Map != nil {
|
||||
currentMapURI = v.Map.URI
|
||||
}
|
||||
tsurl := resolvePlaylistMediaURI(v.URI, currentMapURI)
|
||||
tsurl = rewritePlaylistMediaURI(tsurl, cdnUrl, authKey, keyVersion, mds, source, now)
|
||||
if err := p.Append(tsurl, v.Duration, v.Title); err != nil {
|
||||
log.Error(fmt.Sprintf("Appending of media playlist failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
if v.Limit > 0 {
|
||||
if err := p.SetRange(v.Limit, v.Offset); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting media byte range failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if v.Discontinuity {
|
||||
if err := p.SetDiscontinuity(); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting media discontinuity failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !v.ProgramDateTime.IsZero() {
|
||||
if err := p.SetProgramDateTime(v.ProgramDateTime); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting media program date failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if v.SCTE != nil {
|
||||
if err := p.SetSCTE35(v.SCTE); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting media SCTE tag failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
for _, customTag := range v.Custom {
|
||||
if err := p.SetCustomSegmentTag(customTag); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting media custom tag failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if v.Map != nil && !playlistMapsEqual(v.Map, activeMap) {
|
||||
if err := p.SetMap(
|
||||
rewritePlaylistMediaURI(v.Map.URI, cdnUrl, authKey, keyVersion, mds, source, now),
|
||||
v.Map.Limit,
|
||||
v.Map.Offset,
|
||||
); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting map of media playlist failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
activeMap = v.Map
|
||||
}
|
||||
if v.Key != nil && !playlistKeysEqual(v.Key, activeKey) {
|
||||
if err := p.SetKey(
|
||||
v.Key.Method,
|
||||
playlistKeyURI(v.Key.URI, serUrl, mds),
|
||||
playlistKeyIV(v.Key.IV, key),
|
||||
v.Key.Keyformat,
|
||||
v.Key.Keyformatversions,
|
||||
); err != nil {
|
||||
log.Error(fmt.Sprintf("Setting segment key failed: %s", err))
|
||||
return nil
|
||||
}
|
||||
activeKey = v.Key
|
||||
}
|
||||
}
|
||||
}
|
||||
if src.Key != nil {
|
||||
_ = p.SetDefaultKey(
|
||||
src.Key.Method,
|
||||
playlistKeyURI(src.Key.URI, serUrl, mds),
|
||||
playlistKeyIV(src.Key.IV, key),
|
||||
src.Key.Keyformat,
|
||||
src.Key.Keyformatversions,
|
||||
)
|
||||
}
|
||||
if src.TargetDuration > p.TargetDuration {
|
||||
p.TargetDuration = src.TargetDuration
|
||||
}
|
||||
p.Close()
|
||||
return p.Encode()
|
||||
}
|
||||
|
||||
func playlistMapsEqual(a, b *m3u8.Map) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return a.URI == b.URI && a.Limit == b.Limit && a.Offset == b.Offset
|
||||
}
|
||||
|
||||
func playlistKeysEqual(a, b *m3u8.Key) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
return a.Method == b.Method &&
|
||||
a.URI == b.URI &&
|
||||
a.IV == b.IV &&
|
||||
a.Keyformat == b.Keyformat &&
|
||||
a.Keyformatversions == b.Keyformatversions
|
||||
}
|
||||
|
||||
func playlistKeyURI(sourceKeyURI, serverURL, mds string) string {
|
||||
if mds != constant.MediaSourcePMS {
|
||||
return serverURL
|
||||
}
|
||||
if strings.Contains(sourceKeyURI, "/mt/enkeymt") {
|
||||
return serverURL + "/pms/mt_sec"
|
||||
}
|
||||
return serverURL + "/pms/sec"
|
||||
}
|
||||
|
||||
func playlistKeyIV(sourceIV, override string) string {
|
||||
if override != "" {
|
||||
return override
|
||||
}
|
||||
return sourceIV
|
||||
}
|
||||
|
||||
// resolvePlaylistMediaURI uses an absolute EXT-X-MAP URI as the base for
|
||||
// relative fMP4/CMAF segment paths.
|
||||
func resolvePlaylistMediaURI(mediaURI, mapURI string) string {
|
||||
if strings.TrimSpace(mediaURI) == "" || strings.TrimSpace(mapURI) == "" {
|
||||
return mediaURI
|
||||
}
|
||||
parsedMediaURI, err := url.Parse(mediaURI)
|
||||
if err == nil && parsedMediaURI.IsAbs() {
|
||||
return mediaURI
|
||||
}
|
||||
parsedMapURI, err := url.Parse(mapURI)
|
||||
if err != nil || !parsedMapURI.IsAbs() {
|
||||
return mediaURI
|
||||
}
|
||||
parsedMapURI.RawQuery = ""
|
||||
parsedMapURI.Fragment = ""
|
||||
ref, err := url.Parse(mediaURI)
|
||||
if err != nil {
|
||||
return mediaURI
|
||||
}
|
||||
return parsedMapURI.ResolveReference(ref).String()
|
||||
}
|
||||
|
||||
// rewritePlaylistMediaURI applies the same source, auth and CDN rewriting to
|
||||
// regular media segments and EXT-X-MAP initialization segments.
|
||||
func rewritePlaylistMediaURI(mediaURI, cdnUrl, authKey, keyVersion, mds, source string, now time.Time) string {
|
||||
mediaURL := mediaURI
|
||||
if mds == constant.MediaSourceLaoSiJi {
|
||||
mediaURL = removeDomainPrefix(mediaURL)
|
||||
mediaURL = filepath.Join("laosiji", mediaURL)
|
||||
}
|
||||
if len(authKey) > 0 {
|
||||
uri := filepath.Join(filepath.Dir(source), mediaURL)
|
||||
if mds == constant.MediaSourceLaoSiJi {
|
||||
uri = "/" + mediaURL
|
||||
}
|
||||
urlAuth := generateUrlAuth(now, uri, authKey, keyVersion)
|
||||
mediaURL = fmt.Sprintf("%s%s", mediaURL, urlAuth)
|
||||
}
|
||||
if cdnUrl != "" {
|
||||
mediaURL = common.BindUrl(cdnUrl, mediaURL)
|
||||
}
|
||||
return mediaURL
|
||||
}
|
||||
|
||||
func GetTsUrlFromReader(reader []byte, preUrl, cdnUrl string) []string {
|
||||
p, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false)
|
||||
if err != nil {
|
||||
log.Error("m3u8 decodeFrom error", log.E(err))
|
||||
return []string{}
|
||||
}
|
||||
var tsUrl []string
|
||||
switch listType {
|
||||
case m3u8.MEDIA:
|
||||
src := p.(*m3u8.MediaPlaylist)
|
||||
tsUrl = make([]string, 0, len(src.Segments))
|
||||
for _, v := range src.Segments {
|
||||
if v != nil {
|
||||
tsUrl = append(tsUrl, common.BindUrl(cdnUrl, preUrl, v.URI))
|
||||
}
|
||||
}
|
||||
}
|
||||
return tsUrl
|
||||
}
|
||||
|
||||
func IsM3u8(reader []byte) bool {
|
||||
_, _, err := m3u8.DecodeFrom(bytes.NewReader(reader), false)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
const timeout = 5 * time.Second
|
||||
|
||||
// UploadSuccess 上传成功回调, 通知文件服务, 文件上传完成
|
||||
func UploadSuccess(id string) (code stderr.Code) {
|
||||
var params = map[string]interface{}{
|
||||
"id": id,
|
||||
}
|
||||
c, cancle := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancle()
|
||||
respBody := commod.Resp{}
|
||||
//请求
|
||||
httpStatus, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &respBody, webg.Conf.URL.FileInfoUrl, nil, params)
|
||||
log.Info("http method UploadSuccess response code ==>", log.Any("httpStatus", httpStatus), log.Any("respCode", respBody.Code))
|
||||
if err != nil {
|
||||
log.Error("UploadSuccess POSTJsonWithJResp error", log.E(err))
|
||||
return stderr.ErrConnectToFs
|
||||
}
|
||||
if respBody.Code != http.StatusOK {
|
||||
log.Error("UploadSuccess status error", log.Any("respBody.Code", respBody.Code), log.E(err))
|
||||
return stderr.ErrFsServerFile
|
||||
}
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
// 媒体资源库选择
|
||||
func GetMediaResouce(source string) string {
|
||||
if strings.HasPrefix(source, "v1/") || strings.HasPrefix(source, "/v1/") ||
|
||||
strings.HasPrefix(source, "v2/") || strings.HasPrefix(source, "/v2/") ||
|
||||
strings.HasPrefix(source, "v3/") || strings.HasPrefix(source, "/v3/") {
|
||||
return constant.MediaSourceJH1B
|
||||
}
|
||||
|
||||
if strings.HasPrefix(source, constant.MediaSourcePMSPrefixPath) {
|
||||
return constant.MediaSourcePMS
|
||||
}
|
||||
if strings.HasPrefix(source, constant.MediaSourceSPPrefixPath) {
|
||||
return constant.MediaSourceSP
|
||||
}
|
||||
if strings.Contains(source, "laosiji") {
|
||||
return constant.MediaSourceLaoSiJi
|
||||
}
|
||||
return constant.MediaSourceSP
|
||||
}
|
||||
|
||||
// 生成鉴权url
|
||||
func generateUrlAuth(now time.Time, path, authKey, keyVersion string) string {
|
||||
timestamp := now.Unix()
|
||||
// randId := 0
|
||||
//signStr := fmt.Sprintf("%s%s%d", authKey, path, timestamp)
|
||||
//md5Str := getMD5Sign(signStr)
|
||||
//urlAuth := fmt.Sprintf("?t=%d&k=%s", timestamp, md5Str)
|
||||
signStr := fmt.Sprintf("%s-%d-0-0-%s", path, timestamp, authKey)
|
||||
md5Str := getMD5Sign(signStr)
|
||||
// c={appid}(commod.KFK_APPID) 供 CDN 按应用区分统计/路由;v={keyVersion} 供 CDN 按版本选择校验密钥
|
||||
urlAuth := fmt.Sprintf("?md=%d-0-0-%s&c=%d&v=%s", timestamp, md5Str, commod.KFK_APPID, keyVersion)
|
||||
return urlAuth
|
||||
}
|
||||
|
||||
// getMD5Sign 得到签名
|
||||
func getMD5Sign(buf string) string {
|
||||
md5Ctx := md5.New()
|
||||
md5Ctx.Write([]byte(buf))
|
||||
cipherStr := md5Ctx.Sum(nil)
|
||||
nsign := hex.EncodeToString(cipherStr)
|
||||
return nsign
|
||||
}
|
||||
|
||||
func replaceDomainAndPath(originalURL, newBase string) string {
|
||||
// 解析原始 URL
|
||||
parsedURL, err := url.Parse(originalURL)
|
||||
if err != nil {
|
||||
return originalURL
|
||||
}
|
||||
|
||||
// 解析新基础 URL
|
||||
newBaseURL, err := url.Parse(newBase)
|
||||
if err != nil {
|
||||
return originalURL
|
||||
}
|
||||
|
||||
// 替换协议、主机和基础路径
|
||||
parsedURL.Scheme = newBaseURL.Scheme
|
||||
parsedURL.Host = newBaseURL.Host
|
||||
|
||||
// 构建新路径:/laosiji + 原始路径(去掉旧域名部分)
|
||||
oldBasePath := ""
|
||||
parsedURL.Path = path.Join(newBaseURL.Path, strings.TrimPrefix(parsedURL.Path, oldBasePath))
|
||||
|
||||
// 移除查询参数
|
||||
parsedURL.RawQuery = ""
|
||||
|
||||
return parsedURL.String()
|
||||
}
|
||||
func removeDomainPrefix(originalURL string) string {
|
||||
// 解析原始 URL
|
||||
parsedURL, err := url.Parse(originalURL)
|
||||
if err != nil {
|
||||
return originalURL
|
||||
}
|
||||
oldBasePath := ""
|
||||
return strings.TrimPrefix(parsedURL.Path, oldBasePath)
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user