init: web_hgcg_server 初始化提交

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 16:44:55 +08:00
co-authored by Claude Opus 4.7
commit 189556e702
627 changed files with 126228 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
package m3u8
import (
"bytes"
"context"
"io/ioutil"
"sync"
)
var (
cacheEnkey []byte
cacheLsjEnkey []byte
initOnce sync.Once
cacheNewEnkey []byte
newEnkeyErr error
newEnkeyOnce sync.Once
)
func initEnkey() {
initOnce.Do(func() {
if len(cacheEnkey) == 0 {
data, err := ioutil.ReadFile("conf/m3u8.key")
if err != nil {
panic(err)
}
cacheEnkey = data
}
if len(cacheLsjEnkey) == 0 {
data, err := ioutil.ReadFile("conf/lsjm3u8.key")
if err != nil {
panic(err)
}
cacheLsjEnkey = data
}
})
}
func Enkey(ctx context.Context) (buffer *bytes.Buffer, err error) {
if cacheEnkey == nil {
initEnkey()
}
buffer = bytes.NewBuffer(cacheEnkey)
return
}
// NewEnkey 返回短剧原片使用的新 M3U8 解密密钥。
func NewEnkey(ctx context.Context) (buffer *bytes.Buffer, err error) {
newEnkeyOnce.Do(func() {
cacheNewEnkey, newEnkeyErr = ioutil.ReadFile("conf/newm3u8.key")
})
if newEnkeyErr != nil {
return nil, newEnkeyErr
}
return bytes.NewBuffer(cacheNewEnkey), nil
}
func LsjEnkey(ctx context.Context) (buffer *bytes.Buffer, err error) {
if cacheLsjEnkey == nil {
initEnkey()
}
buffer = bytes.NewBuffer(cacheLsjEnkey)
return
}
+192
View File
@@ -0,0 +1,192 @@
package m3u8
import (
"net/url"
"path"
"strings"
"time"
"videoweb/pub/common/utils"
"github.com/grafov/m3u8"
)
func makeMediaPlayList(from *m3u8.MediaPlaylist, cdnDomain string, dirPath string, keyUri string, authKey string) (media *m3u8.MediaPlaylist, err error) {
media, err = m3u8.NewMediaPlaylist(0, from.Count())
if err != nil {
return
}
base := utils.JoinUrlPath(cdnDomain, dirPath)
now := time.Now()
for _, segment := range from.Segments {
if segment == nil {
continue
}
sg := new(m3u8.MediaSegment)
if strings.Contains(dirPath, "laosiji") {
segment.URI = removeDomainPrefix(segment.URI)
}
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", keyUri, "", "", "")
media.Close()
return
}
func makeMediaPlayListLaosiji(from *m3u8.MediaPlaylist, cdnDomain string, dirPath string, keyUri string, authKey string) (media *m3u8.MediaPlaylist, err error) {
media, err = m3u8.NewMediaPlaylist(0, from.Count())
if err != nil {
return
}
now := time.Now()
isFMP4 := from.Map != nil
media.SetVersion(from.Version())
if isFMP4 && media.Version() < 5 {
media.SetVersion(5)
}
media.SeqNo = from.SeqNo
media.MediaType = from.MediaType
media.DiscontinuitySeq = from.DiscontinuitySeq
media.StartTime = from.StartTime
media.StartTimePrecise = from.StartTimePrecise
media.Iframe = from.Iframe
if from.Map != nil {
mapURI, mapErr := rewriteLaosijiMediaURI(from.Map.URI, cdnDomain, dirPath, "")
if mapErr != nil {
return nil, mapErr
}
mapURI = appendLaosijiURLAuth(mapURI, now, authKey)
media.SetDefaultMap(mapURI, from.Map.Limit, from.Map.Offset)
}
for _, segment := range from.Segments {
if segment == nil {
continue
}
sg := new(m3u8.MediaSegment)
sg.URI, err = rewriteLaosijiMediaURI(segment.URI, cdnDomain, dirPath, fromMapURI(from))
if err != nil {
return nil, err
}
sg.URI = appendLaosijiURLAuth(sg.URI, now, authKey)
if from.Map == nil && segment.Map != nil {
segmentMapURI, mapErr := rewriteLaosijiMediaURI(segment.Map.URI, cdnDomain, dirPath, "")
if mapErr != nil {
return nil, mapErr
}
sg.Map = &m3u8.Map{
URI: appendLaosijiURLAuth(segmentMapURI, now, authKey),
Limit: segment.Map.Limit,
Offset: segment.Map.Offset,
}
isFMP4 = true
}
sg.Title = segment.Title
sg.Duration = segment.Duration
sg.Limit = segment.Limit
sg.Offset = segment.Offset
sg.Discontinuity = segment.Discontinuity
if err = media.AppendSegment(sg); err != nil {
return
}
}
if from.TargetDuration > media.TargetDuration {
media.TargetDuration = from.TargetDuration
}
if isFMP4 && media.Version() < 5 {
media.SetVersion(5)
}
// 老司机旧 TS 仍使用 AES-128 key;fMP4/HEVC 依赖 EXT-X-MAP,不能再强制插入 EXT-X-KEY。
if !isFMP4 {
_ = media.SetDefaultKey("AES-128", keyUri, "0x64cf692cebc516e4a86ac6081aae50ce", "", "")
}
media.Close()
return
}
func rewriteLaosijiMediaURI(originalURI, cdnDomain, playlistDir, mapURI string) (string, error) {
originalURI = strings.TrimSpace(originalURI)
if originalURI == "" {
return "", nil
}
parsed, err := url.Parse(originalURI)
if err != nil {
return "", err
}
if !parsed.IsAbs() {
if mapURL, mapErr := url.Parse(strings.TrimSpace(mapURI)); mapErr == nil && mapURL.IsAbs() {
base := *mapURL
base.RawQuery = ""
base.Fragment = ""
base.Path = path.Dir(base.Path) + "/"
parsed = base.ResolveReference(parsed)
} else {
cleanDir := strings.TrimPrefix(strings.TrimSpace(playlistDir), "/")
cleanDir = strings.TrimPrefix(cleanDir, "laosiji/m3m/")
cleanDir = strings.TrimPrefix(cleanDir, "laosiji/")
cleanDir = strings.TrimPrefix(cleanDir, "m3m/")
parsed.Path = path.Join("/", cleanDir, parsed.Path)
}
}
return replaceDomainAndPath(parsed.String(), utils.JoinUrlPath(cdnDomain, "/laosiji"))
}
func appendLaosijiURLAuth(mediaURL string, now time.Time, authKey string) string {
if mediaURL == "" || authKey == "" {
return mediaURL
}
signPath := mediaURL
if parsed, err := url.Parse(mediaURL); err == nil {
signPath = parsed.Path
}
return mediaURL + generateUrlAuth(now, signPath, authKey)
}
func fromMapURI(playlist *m3u8.MediaPlaylist) string {
if playlist == nil || playlist.Map == nil {
return ""
}
return playlist.Map.URI
}
func replaceDomainAndPath(originalURL, newBase string) (string, error) {
parsedURL, err := url.Parse(originalURL)
if err != nil {
return "", err
}
newBaseURL, err := url.Parse(newBase)
if err != nil {
return "", err
}
parsedURL.Scheme = newBaseURL.Scheme
parsedURL.Host = newBaseURL.Host
parsedURL.Path = path.Join(newBaseURL.Path, strings.TrimPrefix(parsedURL.Path, ""))
parsedURL.RawQuery = ""
return parsedURL.String(), nil
}
func removeDomainPrefix(originalURL string) string {
parsedURL, err := url.Parse(originalURL)
if err != nil {
return originalURL
}
return strings.TrimPrefix(parsedURL.Path, "")
}
+199
View File
@@ -0,0 +1,199 @@
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
}
+116
View File
@@ -0,0 +1,116 @@
package m3u8
import (
"bytes"
"fmt"
"strings"
"videoweb/pub/common/errors"
"videoweb/pub/common/utils"
"videoweb/pub/storage/proxy"
"github.com/grafov/m3u8"
)
const lsjKeyIV = "0x64cf692cebc516e4a86ac6081aae50ce"
// TransformForTranscode 生成转码专用播放列表,供老司机转码服务器拉取。
func TransformForTranscode(name, origDomain, cdnDomain, webM3u8Host, lsjM3u8Yuan, lsjTsYuan string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
if strings.Contains(name, "hc237") {
name = "/laosiji" + name
}
if strings.Contains(name, "laosiji") {
return transcodeLaosiji(name, webM3u8Host, lsjM3u8Yuan, lsjTsYuan)
}
return transcodeNormal(name, origDomain, cdnDomain, webM3u8Host)
}
func transcodeLaosiji(name, webM3u8Host, lsjM3u8Yuan, lsjTsYuan string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
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
}
switch t {
case m3u8.MEDIA:
pl := p.(*m3u8.MediaPlaylist)
keyURI := utils.JoinUrlPath(webM3u8Host, keyUriWebLsj)
// ts 走 CDN(TsYuan/laosiji),与正常播放的 CDN 路径一致。
media, err = makeTranscodeSegList(pl, utils.JoinUrlPath(lsjTsYuan, "/laosiji"), "", keyURI, lsjKeyIV, true)
if err != nil {
return
}
buff = media.Encode()
case m3u8.MASTER:
buff = p.(*m3u8.MasterPlaylist).Encode()
default:
err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
}
return
}
func transcodeNormal(name, origDomain, cdnDomain, webM3u8Host string) (media *m3u8.MediaPlaylist, buff *bytes.Buffer, err error) {
if v := proxy.AppendsMediaS3Version(name); len(v) > 0 {
name = v + 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
}
switch t {
case m3u8.MEDIA:
pl := p.(*m3u8.MediaPlaylist)
dirPath := utils.PathForInfo(name).Dir
keyURI := utils.JoinUrlPath(webM3u8Host, webkeyUri)
// ts 走 CDN(CdnDomain),与正常播放一致。
media, err = makeTranscodeSegList(pl, cdnDomain, dirPath, keyURI, "", false)
if err != nil {
return
}
buff = media.Encode()
case m3u8.MASTER:
buff = p.(*m3u8.MasterPlaylist).Encode()
default:
err = errors.MakeWithMsg(errors.M3u8FileName, fmt.Sprintf("invalid m3u8 type %d", t))
}
return
}
func makeTranscodeSegList(from *m3u8.MediaPlaylist, srcDomain, dirPath, keyURI, iv string, laosiji bool) (media *m3u8.MediaPlaylist, err error) {
media, err = m3u8.NewMediaPlaylist(0, from.Count())
if err != nil {
return
}
base := utils.JoinUrlPath(srcDomain, dirPath)
for _, segment := range from.Segments {
if segment == nil {
continue
}
sg := new(m3u8.MediaSegment)
if laosiji {
if u, e := replaceDomainAndPath(segment.URI, srcDomain); e == nil {
sg.URI = u
} else {
sg.URI = segment.URI
}
} else {
sg.URI = utils.JoinUrlPath(base, segment.URI)
}
sg.Title = segment.Title
sg.Duration = segment.Duration
if err = media.AppendSegment(sg); err != nil {
return
}
}
_ = media.SetDefaultKey("AES-128", keyURI, iv, "", "")
media.Close()
return
}
+153
View File
@@ -0,0 +1,153 @@
package m3u8
import (
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
"videoweb/pub/common/errors"
"videoweb/pub/common/utils"
"videoweb/pub/pubdef"
"videoweb/pub/storage/proxy"
"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
}
+13
View File
@@ -0,0 +1,13 @@
package m3u8
type Req struct {
Token string `json:"token" form:"token"`
VId int64 `json:"vId" form:"vId"`
Name string `json:"name" form:"name"`
NeedHeader bool `json:"needHeader" form:"needHeader"`
}
type PostVideoReq struct {
Token string `json:"token" form:"token"`
Name string `json:"name" form:"name" binding:"required" comment:"must be set"`
}
+18
View File
@@ -0,0 +1,18 @@
package m3u8
//sErr "sz-app-server/pub/common/errors"
// func CheckParams(req *Req) (err error) {
// if req.Token == "" {
// err = sErr.MakeWithMsg(sErr.NoToken, "no token")
// return
// }
// if req.VId <= 0 {
// err = sErr.MakeWithMsg(sErr.VideoId, "invalid video id")
// return
// }
// if req.Name == "" {
// err = sErr.MakeWithMsg(sErr.VideoName, "invalid name")
// }
// return
// }
+96
View File
@@ -0,0 +1,96 @@
package m3u8
import (
"bufio"
"bytes"
"io"
"strings"
"videoweb/pub/common/errors"
"videoweb/pub/common/utils"
"videoweb/pub/storage/proxy"
)
// TransformVtt 读取雪碧图 vtt 文件,并把其中的缩略图路径替换为图片 cdn 地址。
// 兼容现有 /laosiji 路径资源:laosiji 的 vtt 需移除 /laosiji 前缀后,从 lsjImageYuan 源站读取。
// imgCdn 图片 cdn 域名(缩略图访问地址)
// origDomain 普通媒资的源站域名
// lsjImageYuan 老司机图片源站域名(由 app/web 项目从配置 lsjMediaConf.imageYuan 传入)
func TransformVtt(name, imgCdn, origDomain, lsjImageYuan string) (buff []byte, err error) {
if name == "" {
err = errors.MakeWithMsg(errors.M3u8FileName, "vtt file name is empty")
return
}
if imgCdn == "" || origDomain == "" {
err = errors.MakeWithMsg(errors.DomainErr, "imgCdn or origDomain is empty")
return
}
mediaVersion := proxy.AppendsMediaS3Version(name)
if len(mediaVersion) > 0 {
name = mediaVersion + name
}
originName := name
if strings.Contains(name, "laosiji") {
if lsjImageYuan == "" {
err = errors.MakeWithMsg(errors.DomainErr, "lsjImageYuan is empty")
return
}
originName = stripLaosijiPrefix(name)
origDomain = lsjImageYuan
}
srcReader, err := readFromRedis(originName, origDomain)
if err != nil {
return
}
raw, err := io.ReadAll(srcReader)
if err != nil {
return nil, errors.MakeWithMsg(errors.M3u8Read, "read vtt reader fail: "+err.Error())
}
dirPath := utils.PathForInfo(name).Dir
buff = rewriteVttWithCDN(raw, imgCdn, dirPath)
return
}
func stripLaosijiPrefix(name string) string {
p := strings.TrimPrefix(name, "/")
p = strings.TrimPrefix(p, "laosiji")
p = strings.TrimPrefix(p, "/")
return p
}
func rewriteVttWithCDN(vttData []byte, imgCdn, dirPath string) []byte {
imgCdn = strings.TrimRight(strings.TrimSpace(imgCdn), "/")
var out bytes.Buffer
scanner := bufio.NewScanner(bytes.NewReader(vttData))
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
trim := strings.TrimSpace(line)
if idx := strings.Index(trim, "#xywh="); idx >= 0 {
p := strings.TrimSpace(trim[:idx])
xywh := trim[idx:]
var newPath string
switch {
case strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://"):
newPath = p
case strings.HasPrefix(p, "/"):
newPath = imgCdn + p
default:
newPath = utils.JoinUrlPath(utils.JoinUrlPath(imgCdn, strings.TrimRight(dirPath, "/")), p)
}
out.WriteString(newPath + xywh + "\n")
continue
}
out.WriteString(line + "\n")
}
return out.Bytes()
}