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() }