mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-11 15:56:37 +08:00
5f4b613bde
115 云 HLS 会话只存在服务端内存里:服务重新部署、会话过期(30 分钟无活动)、 上游分片地址失效,都会让播放中的客户端在后续分片上拿到 404/410。原实现此时 直接退回本地转码,而低配宿主机本地转码追不上播放速度(实测 2 vCPU 只有 0.62x),用户看到的就是播放报废。 - 云 HLS 中途 fatal 时先重建会话续播(10 分钟内最多 3 次),仍失败才退回本地 - manifestLoadingMaxRetry 1 -> 4:master.m3u8 每次都要现调 115 接口,一次抖动 不该直接判死刑 - 播放列表重写改用上游地址哈希做 key:原来用自增序号,每次重写 entries 就膨胀 一轮、分片编号漂移(实测 762 段列表重写几次后 key 涨到 e2288..e3049) - 会话条目数加上限,异常播放列表不会无限占用内存 - 客户端断开(seek / 切清晰度取消在途请求)不再记成 502,改记 499 与 nginx 对齐;会话失效返回带 code 的 404/410,便于前端判断需要重建会话 - 修正进度/时长上报:云 HLS 时间轴起点恒为 0,不应再叠加 hlsStartSec(历史上 把 7624s 的片子记成 8346s 并标成「已看完」) - 切回直连 / 原画时保住播放位置:HLS 卸载会把 currentTime 归零,必须在切换前 先记下目标位置
160 lines
5.0 KiB
Go
160 lines
5.0 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/truewhile/MeBox/internal/service"
|
|
)
|
|
|
|
func mediaPlaybackInfoHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if svc == nil || svc.Cloud115 == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "cloud115 playback unavailable"})
|
|
return
|
|
}
|
|
m, err := svc.Media.GetMedia(c.Request.Context(), c.Param("id"))
|
|
if err != nil || m == nil || !mediaVisibleForRequest(c, svc, m) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
if !enforceScopedPlaybackToken(c, m.ID) {
|
|
return
|
|
}
|
|
quality, _ := strconv.Atoi(c.Query("quality"))
|
|
info, err := svc.Cloud115.PlaybackInfo(c.Request.Context(), m.ID, quality)
|
|
if err != nil {
|
|
if errors.Is(err, service.ErrMediaNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, info)
|
|
}
|
|
}
|
|
|
|
func mediaTranscodeHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if svc == nil || svc.Cloud115 == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "cloud115 playback unavailable"})
|
|
return
|
|
}
|
|
m, err := svc.Media.GetMedia(c.Request.Context(), c.Param("id"))
|
|
if err != nil || m == nil || !mediaVisibleForRequest(c, svc, m) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
if !enforceScopedPlaybackToken(c, m.ID) {
|
|
return
|
|
}
|
|
var req struct {
|
|
Definition int `json:"definition"`
|
|
}
|
|
_ = c.ShouldBindJSON(&req)
|
|
info, err := svc.Cloud115.StartTranscode(c.Request.Context(), m.ID, req.Definition)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, info)
|
|
}
|
|
}
|
|
|
|
func cloud115HLSMasterHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if svc == nil || svc.Cloud115 == nil || svc.Cloud115.HLSProxy() == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "cloud115 playback unavailable"})
|
|
return
|
|
}
|
|
m, err := svc.Media.GetMedia(c.Request.Context(), c.Param("id"))
|
|
if err != nil || m == nil || !mediaVisibleForRequest(c, svc, m) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
|
return
|
|
}
|
|
if !enforceScopedPlaybackToken(c, m.ID) {
|
|
return
|
|
}
|
|
definition, _ := strconv.Atoi(c.Query("definition"))
|
|
err = svc.Cloud115.HLSProxy().ServeMaster(c.Request.Context(), c.Writer, c.Request, m.ID, definition)
|
|
if err == nil {
|
|
return
|
|
}
|
|
if c.Writer.Written() {
|
|
return
|
|
}
|
|
if errors.Is(err, service.ErrCloud115TranscodePending) {
|
|
retryAfter := 5
|
|
message := "115 正在转码,请稍候"
|
|
var info *service.PlaybackInfo
|
|
if latest, infoErr := svc.Cloud115.PlaybackInfo(c.Request.Context(), m.ID, definition); infoErr == nil && latest != nil {
|
|
info = latest
|
|
if latest.Transcode.Message != "" {
|
|
message = latest.Transcode.Message
|
|
}
|
|
if latest.Transcode.RetryAfterSec > 0 {
|
|
retryAfter = latest.Transcode.RetryAfterSec
|
|
}
|
|
}
|
|
c.JSON(http.StatusConflict, gin.H{
|
|
"code": "transcode_pending",
|
|
"message": message,
|
|
"retry_after_sec": retryAfter,
|
|
"playback": info,
|
|
})
|
|
return
|
|
}
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
}
|
|
}
|
|
|
|
func cloud115HLSSessionHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
if svc == nil || svc.Cloud115 == nil || svc.Cloud115.HLSProxy() == nil {
|
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "cloud115 playback unavailable"})
|
|
return
|
|
}
|
|
mediaID := c.Query("media_id")
|
|
if mediaID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing media_id"})
|
|
return
|
|
}
|
|
if !enforceScopedPlaybackToken(c, mediaID) {
|
|
return
|
|
}
|
|
err := svc.Cloud115.HLSProxy().ServeChild(
|
|
c.Request.Context(),
|
|
c.Writer,
|
|
c.Request,
|
|
c.Param("session"),
|
|
c.Param("key"),
|
|
)
|
|
if err == nil || c.Writer.Written() {
|
|
return
|
|
}
|
|
// 客户端主动断开(播放器 seek、切切清晰度、关页面时取消在途请求)会让
|
|
// 上游请求以 context.Canceled 结束。这是正常现象,不是网关故障;以前
|
|
// 统一记成 502,既污染监控又和 nginx 的 499 对不上,这里按 499 记。
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) ||
|
|
c.Request.Context().Err() != nil {
|
|
c.AbortWithStatus(499)
|
|
return
|
|
}
|
|
switch {
|
|
case errors.Is(err, service.ErrCloud115HLSSessionNotFound):
|
|
// code 供前端判断「会话没了,需要重新拉一次 master.m3u8 建会话」,
|
|
// 而不是直接放弃云端播放退回本地转码。
|
|
c.JSON(http.StatusNotFound, gin.H{"code": "hls_session_not_found", "error": "hls session not found"})
|
|
case errors.Is(err, service.ErrCloud115HLSUpstreamExpired):
|
|
c.JSON(http.StatusGone, gin.H{"code": "hls_session_expired", "error": "hls upstream expired"})
|
|
default:
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
}
|
|
}
|
|
}
|