mirror of
https://github.com/truewhile/MeBox.git
synced 2026-11-12 19:34:24 +08:00
182 lines
5.8 KiB
Go
182 lines
5.8 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 c.Request.Context().Err() != nil {
|
|
c.AbortWithStatus(499)
|
|
return
|
|
}
|
|
actualMediaID, err := svc.Cloud115.HLSProxy().SessionMediaID(c.Param("session"), c.Param("key"))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"code": "hls_session_not_found", "error": "hls session not found"})
|
|
return
|
|
}
|
|
if mediaID != actualMediaID {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "playback session media mismatch"})
|
|
return
|
|
}
|
|
if !enforceScopedPlaybackToken(c, actualMediaID) {
|
|
return
|
|
}
|
|
if svc.Repo == nil || svc.Repo.Media == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "media not found"})
|
|
return
|
|
}
|
|
m, findErr := svc.Repo.Media.FindByID(c.Request.Context(), actualMediaID)
|
|
if findErr != nil || m == nil || !mediaVisibleForRequest(c, svc, m) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "media not found"})
|
|
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()})
|
|
}
|
|
}
|
|
}
|