Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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, "")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
// }
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user