Files
hadja_server/pub/common/m3u8/play_list.go
T
rootandClaude Opus 4.7 79ac89b458 Initial commit
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-11-07 10:47:18 +08:00

200 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package m3u8
import (
"bytes"
"fmt"
"io"
"strings"
"videoweb/pub/common/enum/redisconst"
"videoweb/pub/common/errors"
"videoweb/pub/common/http"
"videoweb/pub/common/log"
"videoweb/pub/common/utils"
"videoweb/pub/redis"
"videoweb/pub/storage/proxy"
"github.com/grafov/m3u8"
)
const (
keyUri = "/enkey"
webkeyUri = "/api/web/enkey"
h5KeyUri = "/enkey"
AuthKey = "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
keyUriLsj = "/lsjenkey"
keyUriWebLsj = "/api/web/lsjenkey"
)
// resolveTsAuthKey 优先使用传入的 tsAuthKey,为空则回退默认 AuthKey。
func resolveTsAuthKey(tsAuthKey string) string {
if key := strings.TrimSpace(tsAuthKey); key != "" {
return key
}
return AuthKey
}
var headerMediaPlayList *m3u8.MediaPlaylist
// TransFormWithSafely 被检查权限的播放列表。
// tsAuthKey: 普通 TS CDN 鉴权;lsjM3u8Yuan: 老司机源站;cdnAuthKey: 老司机分片 CDN 鉴权。
func TransFormWithSafely(req *Req, cdnDomain, origDomain string, web bool, tsAuthKey, lsjM3u8Yuan, cdnAuthKey string) (*m3u8.MediaPlaylist, *bytes.Buffer, error) {
if req.Name == "" {
return nil, nil, errors.MakeWithMsg(errors.M3u8FileName, "file name is empty")
}
if cdnDomain == "" || origDomain == "" {
return nil, nil, errors.MakeWithMsg(errors.DomainErr, "cdnDomain or origDomain is empty")
}
if strings.Contains(req.Name, "hc237") {
req.Name = "/laosiji" + req.Name
}
if strings.Contains(req.Name, "laosiji") {
return TransformLaosiji(req.Name, cdnDomain, web, lsjM3u8Yuan, cdnAuthKey)
}
return transform(req.Name, cdnDomain, origDomain, web, tsAuthKey)
}
func TransformLaosiji(name, cdnDomain string, web bool, lsjM3u8Yuan, cdnAuthKey string, customKeyUri ...string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
// fMP4/HEVC 分片 CDN 使用传入的视频 CDN(web 为 MediaWebCdn,app/h5 为 TsCdn),
// 与 mdsm GetCDNUrlList()[0] 一致;拉取源 m3u8 仍走 lsjM3u8Yuan。
m3u8reader, err := readFromRedis(laosijiUpstreamPath(name), lsjM3u8Yuan)
if err != nil {
return
}
p, t, err := m3u8.DecodeFrom(m3u8reader, false)
if err != nil {
err = errors.MakeWithErr(errors.M3u8Read, err)
return
}
authKey := strings.TrimSpace(cdnAuthKey)
keyurl := keyUriLsj
if web {
keyurl = keyUriWebLsj
}
if len(customKeyUri) > 0 && customKeyUri[0] != "" {
keyurl = customKeyUri[0]
}
switch t {
case m3u8.MEDIA:
pl := p.(*m3u8.MediaPlaylist)
dirPath := utils.PathForInfo(name).Dir
media, err = makeMediaPlayListLaosiji(pl, cdnDomain, dirPath, keyurl, authKey)
if err != nil {
return
}
buff = media.Encode()
case m3u8.MASTER:
mpl := p.(*m3u8.MasterPlaylist)
buff = mpl.Encode()
default:
err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
}
return
}
func transform(name, cdnDomain, origDomain string, web bool, tsAuthKey string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
mediaVerson := proxy.AppendsMediaS3Version(name)
if len(mediaVerson) > 0 {
name = mediaVerson + name
}
m3u8reader, err := readFromRedis(name, origDomain)
if err != nil {
return
}
p, t, err := m3u8.DecodeFrom(m3u8reader, false)
if err != nil {
err = errors.MakeWithErr(errors.M3u8Read, err)
return
}
authKey := resolveTsAuthKey(tsAuthKey)
keyurl := keyUri
if web {
keyurl = webkeyUri
}
switch t {
case m3u8.MEDIA:
pl := p.(*m3u8.MediaPlaylist)
dirPath := utils.PathForInfo(name).Dir
media, err = makeMediaPlayList(pl, cdnDomain, dirPath, keyurl, authKey)
if err != nil {
return
}
buff = media.Encode()
case m3u8.MASTER:
mpl := p.(*m3u8.MasterPlaylist)
buff = mpl.Encode()
default:
err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
}
return
}
func readFromCDN(name, domain string) (reader io.Reader, err error) {
requestUrl := utils.JoinUrlPath(domain, name)
resp, err := http.GetLargeFile(requestUrl, nil)
if err != nil {
return
}
if resp.StatusCode != 200 {
msg := fmt.Sprintf("read m3u8 fail, url: %s, status code %d", requestUrl, resp.StatusCode)
err = errors.MakeWithMsg(errors.M3u8Read, msg)
return
}
reader = bytes.NewReader(resp.Data)
return
}
func readFromRedis(name, domain string) (reader io.Reader, err error) {
m3u8Key := redisconst.MediaM3U8Key(name)
data, err := redis.GetBytes(m3u8Key)
if err == nil && len(data) > 0 {
reader = bytes.NewReader(data)
return
}
if strings.Contains(name, "hc237") {
name = "/m3m/" + name
}
requestUrl := utils.JoinUrlPath(domain, name)
resp, err := http.GetLargeFile(requestUrl, nil)
if err != nil {
return
}
if resp.StatusCode != 200 {
msg := fmt.Sprintf("read m3u8 fail, url: %s, status code %d", requestUrl, resp.StatusCode)
err = errors.MakeWithMsg(errors.M3u8Read, msg)
return
}
if err := redis.Set(m3u8Key, resp.Data,
redisconst.MediaM3U8CacheExpire); err != nil {
log.Info("m3u8:readFromRedis redis set err", log.E(err))
}
reader = bytes.NewReader(resp.Data)
return
}
func GetHeaderMediaSegUrls() []string {
urls := make([]string, 0)
if headerMediaPlayList == nil {
return urls
}
for _, segment := range headerMediaPlayList.Segments {
if segment == nil {
continue
}
urls = append(urls, segment.URI)
}
return urls
}
// laosijiUpstreamPath maps internal laosiji m3u8 path to upstream m3m path.
func laosijiUpstreamPath(name string) string {
name = strings.TrimPrefix(name, "/")
name = strings.TrimPrefix(name, "laosiji/m3m/")
name = strings.TrimPrefix(name, "laosiji/")
return "m3m/" + name
}