Files
fish03andClaude Opus 5.5 bef9f8b4ed Initial commit
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 14:43:16 +07:00

184 lines
4.7 KiB
Go

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
}