package m3u8 import ( "91porn-server/app/appg" "91porn-server/common" "91porn-server/common/constant" "91porn-server/common/constant/redisconst" "91porn-server/common/httputil" "91porn-server/common/log" "91porn-server/common/stderr" "91porn-server/models/commod" "91porn-server/web/webg" "bytes" "context" "crypto/md5" "crypto/sha256" "encoding/hex" "errors" "fmt" "net/http" "net/url" "path" "path/filepath" "strings" "time" "github.com/grafov/m3u8" ) func GetAPPM3u8(source, fileName, _ string, cdn string, fsIo func(source, mds string) (data []byte, err error)) (*bytes.Buffer, error) { mds := GetMediaResouce(source) key := m3u8PureCacheKey(source, mds) m3u8redis, err := appg.Redis.Get(key) var m3u8Byte []byte if m3u8redis != nil && err == nil { //判断是否是m3u8文件 if IsM3u8([]byte(*m3u8redis)) { m3u8Byte = []byte(*m3u8redis) } else { _, _ = appg.Redis.Del(key) } } if mds == constant.MediaSourceLaoSiJi { source = normalizeLaosijiM3u8Source(source) } if m3u8Byte == nil || len(m3u8Byte) == 0 { m3u8Byte, err = fsIo(source, mds) if err != nil { return nil, err } _ = appg.Redis.Set(key, string(m3u8Byte), redisconst.M3u8CacheExpire) } // TS 分片鉴权签名密钥:优先取配置(appg.Conf.Base.TsAuth),未配置时回退内置默认(版本 default + 内置常量) keyVersion, authKey := appg.Conf.Base.TsAuth.Resolve() var bytebuff *bytes.Buffer if mds == constant.MediaSourcePMS { bytebuff = DecodeFromReader(m3u8Byte, "", "/api/app/vid", "", mds, authKey, keyVersion, source) } else if mds == constant.MediaSourceSP { bytebuff = DecodeFromReader(m3u8Byte, cdn+strings.TrimSuffix(source, fileName), "/api/app/vid/sec", "", mds, authKey, keyVersion, source) } else if mds == constant.MediaSourceLaoSiJi { bytebuff = DecodeFromReader(m3u8Byte, cdn, "/api/app/vid/lsjsec", "", mds, authKey, keyVersion, source) } else if mds == constant.MediaSourceJH1B { bytebuff = DecodeFromReader(m3u8Byte, cdn+strings.TrimSuffix(source, fileName), "/api/app/vid/m3u8sec", "", mds, authKey, keyVersion, source) } if bytebuff == nil { log.Warn("can't create m3u8 file", log.Any("source", source)) return nil, errors.New(stderr.CodeEmptyData.Msg()) } return bytebuff, nil } func normalizeLaosijiM3u8Source(source string) string { source = strings.TrimLeft(strings.TrimSpace(source), "/") return strings.TrimPrefix(source, "laosiji/") } // m3u8PureCacheKey isolates raw playlists by media source and their complete // normalized source path. Hashing keeps the Redis key bounded while avoiding // collisions between common basenames such as index.m3u8. func m3u8PureCacheKey(source, mds string) string { normalizedSource := normalizeM3u8CacheSource(source) sum := sha256.Sum256([]byte(mds + "\x00" + normalizedSource)) return redisconst.M3u8PureCacheFmt(mds + ":" + hex.EncodeToString(sum[:])) } func normalizeM3u8CacheSource(source string) string { source = strings.TrimSpace(source) parsed, err := url.Parse(source) if err != nil { return path.Clean("/" + strings.TrimLeft(strings.ReplaceAll(source, "\\", "/"), "/")) } parsed.Scheme = strings.ToLower(parsed.Scheme) parsed.Host = strings.ToLower(parsed.Host) parsed.Fragment = "" parsed.Path = path.Clean("/" + strings.TrimLeft(strings.ReplaceAll(parsed.Path, "\\", "/"), "/")) parsed.RawPath = "" if parsed.RawQuery != "" { if query, queryErr := url.ParseQuery(parsed.RawQuery); queryErr == nil { parsed.RawQuery = query.Encode() } } return parsed.String() } func DecodeFromReader(reader []byte, cdnUrl, serUrl, key, mds, authKey, keyVersion, source string) *bytes.Buffer { p, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false) if err != nil { log.Error("m3u8 decodeFrom error", log.E(err)) return nil } switch listType { case m3u8.MEDIA: return Create(p.(*m3u8.MediaPlaylist), cdnUrl, serUrl, key, mds, authKey, keyVersion, source) case m3u8.MASTER: return p.(*m3u8.MasterPlaylist).Encode() } return nil } // RewriteMasterPlaylist rewrites every child playlist URI in a master HLS // playlist. Media playlists are returned unchanged. The signed H.265 pull // endpoint uses this so variants and alternate renditions do not lose their // HMAC when the cloud transcoder follows a relative child URI. func RewriteMasterPlaylist(reader []byte, rewrite func(string) (string, error)) (*bytes.Buffer, error) { if rewrite == nil { return bytes.NewBuffer(reader), nil } playlist, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false) if err != nil { return nil, err } if listType != m3u8.MASTER { return bytes.NewBuffer(reader), nil } master := playlist.(*m3u8.MasterPlaylist) rewritten := make(map[string]string) rewriteURI := func(rawURI string) (string, error) { rawURI = strings.TrimSpace(rawURI) if rawURI == "" { return "", nil } if value, ok := rewritten[rawURI]; ok { return value, nil } value, rewriteErr := rewrite(rawURI) if rewriteErr != nil { return "", rewriteErr } rewritten[rawURI] = value return value, nil } seenAlternatives := make(map[*m3u8.Alternative]struct{}) for _, variant := range master.Variants { if variant == nil { continue } variant.URI, err = rewriteURI(variant.URI) if err != nil { return nil, fmt.Errorf("rewrite HLS variant: %w", err) } for _, alternative := range variant.Alternatives { if alternative == nil { continue } if _, ok := seenAlternatives[alternative]; ok { continue } seenAlternatives[alternative] = struct{}{} alternative.URI, err = rewriteURI(alternative.URI) if err != nil { return nil, fmt.Errorf("rewrite HLS alternative: %w", err) } } } return master.Encode(), nil } func Create(src *m3u8.MediaPlaylist, cdnUrl, serUrl, key, mds, authKey, keyVersion, source string) *bytes.Buffer { p, e := m3u8.NewMediaPlaylist(src.WinSize(), src.Count()) if e != nil { log.Error(fmt.Sprintf("Creating of media playlist failed: %s", e)) return nil } p.SetVersion(src.Version()) p.SeqNo = src.SeqNo p.DiscontinuitySeq = src.DiscontinuitySeq p.StartTime = src.StartTime p.StartTimePrecise = src.StartTimePrecise p.MediaType = src.MediaType p.Iframe = src.Iframe p.Args = src.Args p.WV = src.WV for _, customTag := range src.Custom { p.SetCustomTag(customTag) } now := time.Now() var activeMap *m3u8.Map if src.Map != nil { activeMap = src.Map p.SetDefaultMap( rewritePlaylistMediaURI(src.Map.URI, cdnUrl, authKey, keyVersion, mds, source, now), src.Map.Limit, src.Map.Offset, ) } activeKey := src.Key for _, v := range src.Segments { if v != nil { currentMapURI := "" if activeMap != nil { currentMapURI = activeMap.URI } if v.Map != nil { currentMapURI = v.Map.URI } tsurl := resolvePlaylistMediaURI(v.URI, currentMapURI) tsurl = rewritePlaylistMediaURI(tsurl, cdnUrl, authKey, keyVersion, mds, source, now) if err := p.Append(tsurl, v.Duration, v.Title); err != nil { log.Error(fmt.Sprintf("Appending of media playlist failed: %s", err)) return nil } if v.Limit > 0 { if err := p.SetRange(v.Limit, v.Offset); err != nil { log.Error(fmt.Sprintf("Setting media byte range failed: %s", err)) return nil } } if v.Discontinuity { if err := p.SetDiscontinuity(); err != nil { log.Error(fmt.Sprintf("Setting media discontinuity failed: %s", err)) return nil } } if !v.ProgramDateTime.IsZero() { if err := p.SetProgramDateTime(v.ProgramDateTime); err != nil { log.Error(fmt.Sprintf("Setting media program date failed: %s", err)) return nil } } if v.SCTE != nil { if err := p.SetSCTE35(v.SCTE); err != nil { log.Error(fmt.Sprintf("Setting media SCTE tag failed: %s", err)) return nil } } for _, customTag := range v.Custom { if err := p.SetCustomSegmentTag(customTag); err != nil { log.Error(fmt.Sprintf("Setting media custom tag failed: %s", err)) return nil } } if v.Map != nil && !playlistMapsEqual(v.Map, activeMap) { if err := p.SetMap( rewritePlaylistMediaURI(v.Map.URI, cdnUrl, authKey, keyVersion, mds, source, now), v.Map.Limit, v.Map.Offset, ); err != nil { log.Error(fmt.Sprintf("Setting map of media playlist failed: %s", err)) return nil } activeMap = v.Map } if v.Key != nil && !playlistKeysEqual(v.Key, activeKey) { if err := p.SetKey( v.Key.Method, playlistKeyURI(v.Key.URI, serUrl, mds), playlistKeyIV(v.Key.IV, key), v.Key.Keyformat, v.Key.Keyformatversions, ); err != nil { log.Error(fmt.Sprintf("Setting segment key failed: %s", err)) return nil } activeKey = v.Key } } } if src.Key != nil { _ = p.SetDefaultKey( src.Key.Method, playlistKeyURI(src.Key.URI, serUrl, mds), playlistKeyIV(src.Key.IV, key), src.Key.Keyformat, src.Key.Keyformatversions, ) } if src.TargetDuration > p.TargetDuration { p.TargetDuration = src.TargetDuration } p.Close() return p.Encode() } func playlistMapsEqual(a, b *m3u8.Map) bool { if a == nil || b == nil { return a == b } return a.URI == b.URI && a.Limit == b.Limit && a.Offset == b.Offset } func playlistKeysEqual(a, b *m3u8.Key) bool { if a == nil || b == nil { return a == b } return a.Method == b.Method && a.URI == b.URI && a.IV == b.IV && a.Keyformat == b.Keyformat && a.Keyformatversions == b.Keyformatversions } func playlistKeyURI(sourceKeyURI, serverURL, mds string) string { if mds != constant.MediaSourcePMS { return serverURL } if strings.Contains(sourceKeyURI, "/mt/enkeymt") { return serverURL + "/pms/mt_sec" } return serverURL + "/pms/sec" } func playlistKeyIV(sourceIV, override string) string { if override != "" { return override } return sourceIV } // resolvePlaylistMediaURI uses an absolute EXT-X-MAP URI as the base for // relative fMP4/CMAF segment paths. func resolvePlaylistMediaURI(mediaURI, mapURI string) string { if strings.TrimSpace(mediaURI) == "" || strings.TrimSpace(mapURI) == "" { return mediaURI } parsedMediaURI, err := url.Parse(mediaURI) if err == nil && parsedMediaURI.IsAbs() { return mediaURI } parsedMapURI, err := url.Parse(mapURI) if err != nil || !parsedMapURI.IsAbs() { return mediaURI } parsedMapURI.RawQuery = "" parsedMapURI.Fragment = "" ref, err := url.Parse(mediaURI) if err != nil { return mediaURI } return parsedMapURI.ResolveReference(ref).String() } // rewritePlaylistMediaURI applies the same source, auth and CDN rewriting to // regular media segments and EXT-X-MAP initialization segments. func rewritePlaylistMediaURI(mediaURI, cdnUrl, authKey, keyVersion, mds, source string, now time.Time) string { mediaURL := mediaURI if mds == constant.MediaSourceLaoSiJi { mediaURL = removeDomainPrefix(mediaURL) mediaURL = filepath.Join("laosiji", mediaURL) } if len(authKey) > 0 { uri := filepath.Join(filepath.Dir(source), mediaURL) if mds == constant.MediaSourceLaoSiJi { uri = "/" + mediaURL } urlAuth := generateUrlAuth(now, uri, authKey, keyVersion) mediaURL = fmt.Sprintf("%s%s", mediaURL, urlAuth) } if cdnUrl != "" { mediaURL = common.BindUrl(cdnUrl, mediaURL) } return mediaURL } func GetTsUrlFromReader(reader []byte, preUrl, cdnUrl string) []string { p, listType, err := m3u8.DecodeFrom(bytes.NewReader(reader), false) if err != nil { log.Error("m3u8 decodeFrom error", log.E(err)) return []string{} } var tsUrl []string switch listType { case m3u8.MEDIA: src := p.(*m3u8.MediaPlaylist) tsUrl = make([]string, 0, len(src.Segments)) for _, v := range src.Segments { if v != nil { tsUrl = append(tsUrl, common.BindUrl(cdnUrl, preUrl, v.URI)) } } } return tsUrl } func IsM3u8(reader []byte) bool { _, _, err := m3u8.DecodeFrom(bytes.NewReader(reader), false) return err == nil } const timeout = 5 * time.Second // UploadSuccess 上传成功回调, 通知文件服务, 文件上传完成 func UploadSuccess(id string) (code stderr.Code) { var params = map[string]interface{}{ "id": id, } c, cancle := context.WithTimeout(context.Background(), timeout) defer cancle() respBody := commod.Resp{} //请求 httpStatus, err := httputil.DefaultClientPostJsonWithRespWithCtx(c, &respBody, webg.Conf.URL.FileInfoUrl, nil, params) log.Info("http method UploadSuccess response code ==>", log.Any("httpStatus", httpStatus), log.Any("respCode", respBody.Code)) if err != nil { log.Error("UploadSuccess POSTJsonWithJResp error", log.E(err)) return stderr.ErrConnectToFs } if respBody.Code != http.StatusOK { log.Error("UploadSuccess status error", log.Any("respBody.Code", respBody.Code), log.E(err)) return stderr.ErrFsServerFile } return stderr.Success } // 媒体资源库选择 func GetMediaResouce(source string) string { if strings.HasPrefix(source, "v1/") || strings.HasPrefix(source, "/v1/") || strings.HasPrefix(source, "v2/") || strings.HasPrefix(source, "/v2/") || strings.HasPrefix(source, "v3/") || strings.HasPrefix(source, "/v3/") { return constant.MediaSourceJH1B } if strings.HasPrefix(source, constant.MediaSourcePMSPrefixPath) { return constant.MediaSourcePMS } if strings.HasPrefix(source, constant.MediaSourceSPPrefixPath) { return constant.MediaSourceSP } if strings.Contains(source, "laosiji") { return constant.MediaSourceLaoSiJi } return constant.MediaSourceSP } // 生成鉴权url func generateUrlAuth(now time.Time, path, authKey, keyVersion string) string { timestamp := now.Unix() // randId := 0 //signStr := fmt.Sprintf("%s%s%d", authKey, path, timestamp) //md5Str := getMD5Sign(signStr) //urlAuth := fmt.Sprintf("?t=%d&k=%s", timestamp, md5Str) signStr := fmt.Sprintf("%s-%d-0-0-%s", path, timestamp, authKey) md5Str := getMD5Sign(signStr) // c={appid}(commod.KFK_APPID) 供 CDN 按应用区分统计/路由;v={keyVersion} 供 CDN 按版本选择校验密钥 urlAuth := fmt.Sprintf("?md=%d-0-0-%s&c=%d&v=%s", timestamp, md5Str, 209, 1) return urlAuth } // getMD5Sign 得到签名 func getMD5Sign(buf string) string { md5Ctx := md5.New() md5Ctx.Write([]byte(buf)) cipherStr := md5Ctx.Sum(nil) nsign := hex.EncodeToString(cipherStr) return nsign } func replaceDomainAndPath(originalURL, newBase string) string { // 解析原始 URL parsedURL, err := url.Parse(originalURL) if err != nil { return originalURL } // 解析新基础 URL newBaseURL, err := url.Parse(newBase) if err != nil { return originalURL } // 替换协议、主机和基础路径 parsedURL.Scheme = newBaseURL.Scheme parsedURL.Host = newBaseURL.Host // 构建新路径:/laosiji + 原始路径(去掉旧域名部分) oldBasePath := "" parsedURL.Path = path.Join(newBaseURL.Path, strings.TrimPrefix(parsedURL.Path, oldBasePath)) // 移除查询参数 parsedURL.RawQuery = "" return parsedURL.String() } func removeDomainPrefix(originalURL string) string { // 解析原始 URL parsedURL, err := url.Parse(originalURL) if err != nil { return originalURL } oldBasePath := "" return strings.TrimPrefix(parsedURL.Path, oldBasePath) }