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 }