package router import ( "encoding/json" "net/http" "strings" "videoweb/app/appg" commonhttp "videoweb/pub/common/http" "videoweb/pub/common/log" "videoweb/pub/common/m3u8" "github.com/gin-gonic/gin" ) func registerM3U8Routes(r *gin.Engine) { r.GET("/h5/m3u8/*path", proxyM3U8) r.GET("/enkey", serveM3U8Key) r.GET("/newenkey", serveNewM3U8Key) } func proxyM3U8(c *gin.Context) { rawPath := strings.TrimPrefix(c.Param("path"), "/") if rawPath == "" { c.Status(http.StatusBadRequest) return } if c.Request.URL.RawQuery != "" { rawPath += "?" + c.Request.URL.RawQuery } serverAddr := strings.TrimRight(strings.TrimSpace(appg.Conf.ServerAddr), "/") if serverAddr == "" { log.Warn("proxyM3U8 empty serverAddr") c.Status(http.StatusBadGateway) return } targetURL := serverAddr + "/h5/m3u8/" + rawPath // 播放地址本身带签名 token;勿强制带登录 Authorization。 body, status, err := fetchM3U8Upstream(c, targetURL, "") if err != nil { log.Warn("proxyM3U8 request failed", log.Any("target", targetURL), log.E(err)) c.Status(http.StatusBadGateway) return } if status != http.StatusOK { c.Status(status) return } if code, ok := jsonErrorCode(body); ok { log.Warn("proxyM3U8 upstream error", log.Any("target", targetURL), log.Any("code", code), log.Any("body", string(body))) c.Data(http.StatusBadGateway, "application/json; charset=utf-8", body) return } contentType := detectMediaContentType(rawPath) if isM3U8Content(rawPath, body) { contentType = "application/vnd.apple.mpegurl" } c.Data(http.StatusOK, contentType, body) } func fetchM3U8Upstream(c *gin.Context, targetURL, auth string) ([]byte, int, error) { header := map[string]string{ "Accept": "*/*", } if auth != "" { header["Authorization"] = auth } applyClientForwardHeaders(header, c) resp, err := commonhttp.GetLargeFile(targetURL, header) if err != nil { return nil, 0, err } return resp.Data, resp.StatusCode, nil } func jsonErrorCode(body []byte) (int, bool) { if len(body) == 0 || body[0] != '{' { return 0, false } var payload struct { Code int `json:"code"` } if err := json.Unmarshal(body, &payload); err != nil { return 0, false } if payload.Code == 200 || payload.Code == 0 { return 0, false } return payload.Code, true } func isM3U8Content(rawPath string, body []byte) bool { if strings.Contains(strings.Split(rawPath, "?")[0], ".m3u8") { return true } return len(body) > 0 && body[0] == '#' } func detectMediaContentType(rawPath string) string { pathOnly := strings.Split(rawPath, "?")[0] switch { case strings.HasSuffix(pathOnly, ".m3u8"): return "application/vnd.apple.mpegurl" case strings.HasSuffix(pathOnly, ".ts"): return "video/mp2t" default: return "application/octet-stream" } } func serveM3U8Key(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Cache-Control", "no-store") if key, ok := fetchUpstreamEnkey(c, c.Request.URL.RawQuery); ok { c.Data(http.StatusOK, "application/octet-stream", key) return } buffer, err := m3u8.Enkey(c.Request.Context()) if err != nil || buffer == nil || buffer.Len() == 0 { log.Warn("serveM3U8Key local key unavailable", log.E(err)) c.Status(http.StatusBadGateway) return } c.Data(http.StatusOK, "application/octet-stream", buffer.Bytes()) } func serveNewM3U8Key(c *gin.Context) { c.Header("Access-Control-Allow-Origin", "*") c.Header("Cache-Control", "no-store") key, ok := fetchUpstreamKey(c, "/newenkey", c.Request.URL.RawQuery) if !ok { c.Status(http.StatusBadGateway) return } c.Data(http.StatusOK, "application/octet-stream", key) } func fetchUpstreamEnkey(c *gin.Context, rawQuery string) ([]byte, bool) { return fetchUpstreamKey(c, "/enkey", rawQuery) } func fetchUpstreamKey(c *gin.Context, keyPath, rawQuery string) ([]byte, bool) { serverAddr := strings.TrimRight(strings.TrimSpace(appg.Conf.ServerAddr), "/") if serverAddr == "" { return nil, false } targetURL := serverAddr + keyPath if rawQuery = strings.TrimSpace(rawQuery); rawQuery != "" { targetURL += "?" + rawQuery } header := map[string]string{ "Accept": "*/*", } applyClientForwardHeaders(header, c) resp, err := commonhttp.GetLargeFile(targetURL, header) if err != nil { log.Warn("fetchUpstreamKey request failed", log.Any("target", targetURL), log.E(err)) return nil, false } if resp.StatusCode != http.StatusOK || len(resp.Data) == 0 || jsonErrorCodeBody(resp.Data) { log.Warn("fetchUpstreamKey invalid response", log.Any("target", targetURL), log.Any("status", resp.StatusCode), log.Any("len", len(resp.Data))) return nil, false } return resp.Data, true } func jsonErrorCodeBody(body []byte) bool { _, ok := jsonErrorCode(body) return ok }