Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
184 lines
4.7 KiB
Go
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
|
|
}
|