Files
web_huakui_server/pub/common/m3u8/transformer.go
T
rootandClaude Opus 4.7 85c0071fd8 feat: 从黄果 NET hgdjnet-server 9bfbafe 导入 HKDJ 官网后端
来源:hgdjnet-server 9bfbafe10d417deea959bd53117bc88a10e60a83
项目身份 HKDJ / JHA-216,独立官网仓库,Jenkins 守卫键 hkdjnet
测试目录 /server/hkdj_net,App/Web API 30331/30332

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-10-16 11:20:04 +08:00

154 lines
4.6 KiB
Go

package m3u8
import (
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
"hkdj-web-server/pub/common/errors"
"hkdj-web-server/pub/common/utils"
"hkdj-web-server/pub/pubdef"
"hkdj-web-server/pub/storage/proxy"
"strconv"
"strings"
"time"
"github.com/grafov/m3u8"
)
// TransformEx m3u8 文件转换。
// apiType(1:app,2:h5app,3:web)
// authKey: TS CDN 鉴权(空则回退默认 AuthKey)
// lsjM3u8Yuan / cdnAuthKey: 老司机源站与分片鉴权
// customKeyUri: 非空时直接使用,用于将鉴权参数嵌入 EXT-X-KEY URI
func TransformEx(name, cdnDomain, origDomain string, apiType int, authKey, lsjM3u8Yuan, cdnAuthKey string, customKeyUri ...string) (
media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
return transformEx(name, cdnDomain, origDomain, apiType, authKey, lsjM3u8Yuan, cdnAuthKey, true, customKeyUri...)
}
// TransformExOriginalPath 按数据库中的原始路径读取 m3u8,不自动追加 v1/v2/v3 前缀。
// 短剧水印未完成时仍播放原片,原片路径如 k12/...,需要保留原路径。
func TransformExOriginalPath(name, cdnDomain, origDomain string, apiType int, authKey, lsjM3u8Yuan, cdnAuthKey string, customKeyUri ...string) (
media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
return transformEx(name, cdnDomain, origDomain, apiType, authKey, lsjM3u8Yuan, cdnAuthKey, false, customKeyUri...)
}
func transformEx(name, cdnDomain, origDomain string, apiType int, authKey, lsjM3u8Yuan, cdnAuthKey string, appendMediaVersion bool, customKeyUri ...string) (
media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
if name == "" {
err = errors.MakeWithMsg(errors.M3u8FileName, "m3u8 file name is empty")
return
}
if cdnDomain == "" || origDomain == "" {
err = errors.MakeWithMsg(errors.DomainErr, "cdnDomain or origDomain is empty")
return
}
if strings.Contains(name, "hc237") {
name = "/laosiji" + name
}
if strings.Contains(name, "laosiji") {
return TransformLaosiji(name, cdnDomain, false, lsjM3u8Yuan, cdnAuthKey, customKeyUri...)
}
if appendMediaVersion {
mediaVerson := proxy.AppendsMediaS3Version(name)
if len(mediaVerson) > 0 {
name = mediaVerson + name
}
}
authKey = resolveTsAuthKey(authKey)
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
}
apiPath := keyUri
if len(customKeyUri) > 0 && customKeyUri[0] != "" {
apiPath = customKeyUri[0]
} else if apiType == 2 {
apiPath = h5KeyUri
} else if apiType == 3 {
apiPath = webkeyUri
}
switch t {
case m3u8.MEDIA:
pl := p.(*m3u8.MediaPlaylist)
dirPath := utils.PathForInfo(name).Dir
media, err = makeTSList(pl, cdnDomain, dirPath, apiPath, 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 makeTSList(from *m3u8.MediaPlaylist, cdnDomain string,
dirPath string, apiPath string, authKey string) (
media *m3u8.MediaPlaylist, err error) {
media, err = m3u8.NewMediaPlaylist(from.WinSize(), from.Count())
if err != nil {
return
}
now := time.Now()
base := utils.JoinUrlPath(cdnDomain, dirPath)
for _, segment := range from.Segments {
if segment == nil {
continue
}
sg := new(m3u8.MediaSegment)
sg.URI = utils.JoinUrlPath(base, segment.URI)
if len(authKey) > 0 {
urlPath := utils.JoinUrlPath(dirPath, segment.URI)
urlAuth := generateUrlAuth(now, urlPath, authKey)
sg.URI = sg.URI + urlAuth
}
sg.Title = segment.Title
sg.Duration = segment.Duration
if err = media.AppendSegment(sg); err != nil {
return
}
}
_ = media.SetDefaultKey("AES-128", apiPath, "", "", "")
media.Close()
return
}
func generateUrlAuth(now time.Time, path string, authKey string) string {
timestamp := now.Unix()
signStr := fmt.Sprintf("%s-%d-0-0-%s", path, timestamp, authKey)
md5Str := getMD5Sign(signStr)
// TS CDN 鉴权参数后追加应用标识,便于流量统计。
urlAuth := fmt.Sprintf("?md=%d-0-0-%s&c=%d", timestamp, md5Str, pubdef.AppId)
return urlAuth
}
func generateUrlAuthQiniu(now time.Time, path string, authKey string) string {
timestamp := strconv.FormatInt(now.Add(2*time.Hour).Unix(), 16)
base64 := path
signStr := authKey + base64 + timestamp
md5Str := getMD5Sign(signStr)
urlAuth := fmt.Sprintf("?sign=%s&t=%s", md5Str, timestamp)
return urlAuth
}
func getMD5Sign(buf string) string {
md5Ctx := md5.New()
md5Ctx.Write([]byte(buf))
cipherStr := md5Ctx.Sum(nil)
nsign := hex.EncodeToString(cipherStr)
return nsign
}