Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
159 lines
4.2 KiB
Go
159 lines
4.2 KiB
Go
package videoser
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"fmt"
|
||
"io"
|
||
http2 "net/http"
|
||
"strings"
|
||
"videoweb/app/appg"
|
||
"videoweb/app/service/configser"
|
||
"videoweb/pub/common"
|
||
"videoweb/pub/common/enum/redisconst"
|
||
"videoweb/pub/common/errors"
|
||
"videoweb/pub/common/http"
|
||
"videoweb/pub/common/log"
|
||
"videoweb/pub/common/m3u8"
|
||
"videoweb/pub/common/srvdef"
|
||
"videoweb/pub/common/utils"
|
||
"videoweb/pub/redis"
|
||
"videoweb/pub/storage/proxy"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
func GetVTTEx(ctx *gin.Context) {
|
||
fn := func(c *gin.Context) (contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string, err error) {
|
||
vttName := ctx.Param("key")
|
||
var req m3u8.Req
|
||
if err = ctx.BindQuery(&req); err != nil {
|
||
return
|
||
}
|
||
|
||
req.Name = vttName
|
||
cfg := configser.GetConfigInfo()
|
||
cdnDomain := cfg.ImgCdn
|
||
originDomain := appg.Conf.OrigDomain
|
||
|
||
buff, err := TransformVttEx(req.Name, cdnDomain, originDomain)
|
||
if err != nil {
|
||
return
|
||
}
|
||
contentType = "application/vnd.apple.mpegurl"
|
||
reader = bytes.NewReader(buff)
|
||
contentLength = int64(len(buff))
|
||
extraHeaders = map[string]string{}
|
||
return
|
||
}
|
||
contentLength, contentType, reader, extraHeaders, err := fn(ctx)
|
||
if err != nil {
|
||
common.ServerJSON(ctx, srvdef.DownloadM3u8File, err)
|
||
return
|
||
}
|
||
ctx.DataFromReader(http2.StatusOK, contentLength, contentType, reader, extraHeaders)
|
||
}
|
||
|
||
func TransformVttEx(name, cdnDomain, originDomain string) (buff []byte, err error) {
|
||
if name == "" {
|
||
err = errors.MakeWithMsg(errors.M3u8FileName, "m3u8 file name is empty")
|
||
return
|
||
}
|
||
if cdnDomain == "" || originDomain == "" {
|
||
err = errors.MakeWithMsg(errors.DomainErr, "cdnDomain or origDomain is empty")
|
||
return
|
||
}
|
||
// 处理媒资库 桶1(v1) 桶2(v2) 桶3(v3)
|
||
mediaVerson := proxy.AppendsMediaS3Version(name)
|
||
if len(mediaVerson) > 0 {
|
||
name = mediaVerson + name
|
||
}
|
||
|
||
// 原始vtt
|
||
srcReader, err := readFromRedis(name, originDomain)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
raw, err := io.ReadAll(srcReader)
|
||
if err != nil {
|
||
return nil, errors.MakeWithMsg(errors.M3u8Read, "read vtt reader fail: "+err.Error())
|
||
}
|
||
// 替换为cdn路径
|
||
newData := rewriteVttWithCDN(raw, cdnDomain)
|
||
reader := bytes.NewReader(newData)
|
||
buff, err = io.ReadAll(reader)
|
||
if err != nil {
|
||
return nil, errors.MakeWithMsg(errors.M3u8Read, "read vtt fail: "+err.Error())
|
||
}
|
||
return
|
||
}
|
||
|
||
// 从redis缓存中读取
|
||
func readFromRedis(name, domain string) (reader io.Reader, err error) {
|
||
vttKey := redisconst.MediaM3U8Key(name)
|
||
data, err := redis.GetBytes(vttKey)
|
||
if err == nil && len(data) > 0 {
|
||
reader = bytes.NewReader(data)
|
||
return
|
||
}
|
||
requestUrl := utils.JoinUrlPath(domain, name)
|
||
resp, err := http.GetLargeFile(requestUrl, nil)
|
||
if err != nil {
|
||
fmt.Println("requestUrl111", requestUrl)
|
||
return
|
||
}
|
||
fmt.Println("requestUrl", requestUrl, resp.StatusCode)
|
||
if resp.StatusCode != 200 {
|
||
msg := fmt.Sprintf("read vtt fail, url: %s, status code %d", requestUrl, resp.StatusCode)
|
||
err = errors.MakeWithMsg(errors.M3u8Read, msg)
|
||
return
|
||
}
|
||
if err := redis.Set(vttKey, resp.Data,
|
||
redisconst.MediaM3U8CacheExpire); err != nil {
|
||
log.Info("vtt:readFromRedis redis set err", log.E(err))
|
||
}
|
||
reader = bytes.NewReader(resp.Data)
|
||
return
|
||
}
|
||
|
||
// rewriteVttWithCDN 将 vtt 内 "path#xywh=..." 的 path 改为 cdnDomain/path
|
||
func rewriteVttWithCDN(vttData []byte, cdnDomain string) []byte {
|
||
cdnDomain = strings.TrimSpace(cdnDomain)
|
||
cdnDomain = strings.TrimRight(cdnDomain, "/")
|
||
|
||
var out bytes.Buffer
|
||
scanner := bufio.NewScanner(bytes.NewReader(vttData))
|
||
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
trim := strings.TrimSpace(line)
|
||
|
||
// 只处理缩略图行:xxx#xywh=...
|
||
if strings.Contains(trim, "#xywh=") {
|
||
parts := strings.SplitN(trim, "#xywh=", 2)
|
||
if len(parts) == 2 {
|
||
p := strings.TrimSpace(parts[0]) // 原路径
|
||
xywh := parts[1]
|
||
|
||
// 已经是绝对URL则不改
|
||
if strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://") {
|
||
out.WriteString(p + "#xywh=" + xywh + "\n")
|
||
continue
|
||
}
|
||
|
||
// 处理相对路径,确保只有一个 /
|
||
p = strings.TrimLeft(p, "/")
|
||
newPath := cdnDomain + "/" + p
|
||
out.WriteString(newPath + "#xywh=" + xywh + "\n")
|
||
continue
|
||
}
|
||
}
|
||
|
||
out.WriteString(line + "\n")
|
||
}
|
||
|
||
// scanner.Err()
|
||
return out.Bytes()
|
||
}
|