package handler import ( "crypto/subtle" "encoding/json" "io" "net/http" "net/url" "strconv" "strings" "time" "github.com/gin-gonic/gin" "laic-backend/common" "laic-backend/service" "laic-backend/vo" ) // GetLivePage 直播会话分页列表 func GetLivePage(c *gin.Context) { var req vo.LivePageReq _ = c.ShouldBindQuery(&req) if req.PageNum <= 0 { req.PageNum = 1 } if req.PageSize <= 0 { req.PageSize = 10 } page, busiErr := service.DefaultLiveService.GetPage(common.GetUserId(c), common.IsAdmin(c), &req) if busiErr != nil { common.FailWithBusiError(c, busiErr) return } common.OKWithData(c, page) } // CreateLiveSession 创建或加入机巢的唯一活动直播会话,并创建观看租约。 func CreateLiveSession(c *gin.Context) { dockID := c.Param("dockId") if dockID == "" { common.FailWithBusiError(c, common.ErrParam) return } var req vo.LiveSessionCreateReq _ = c.ShouldBindJSON(&req) result, busiErr := service.DefaultLiveService.Join(common.GetUserId(c), common.IsAdmin(c), dockID, &req) if busiErr != nil { common.FailWithBusiError(c, busiErr) return } common.OKWithData(c, result) } // GetLiveSession 获取指定直播会话。 func GetLiveSession(c *gin.Context) { dockID, sessionID := c.Param("dockId"), c.Param("streamSessionId") if dockID == "" || sessionID == "" { common.FailWithBusiError(c, common.ErrParam) return } session, busiErr := service.DefaultLiveService.GetSession(common.GetUserId(c), common.IsAdmin(c), dockID, sessionID) if busiErr != nil { common.FailWithBusiError(c, busiErr) return } common.OKWithData(c, session) } // HeartbeatLiveSession 续期当前用户的观看租约。 func HeartbeatLiveSession(c *gin.Context) { dockID, sessionID := c.Param("dockId"), c.Param("streamSessionId") if dockID == "" || sessionID == "" { common.FailWithBusiError(c, common.ErrParam) return } result, busiErr := service.DefaultLiveService.Heartbeat(common.GetUserId(c), common.IsAdmin(c), dockID, sessionID) if busiErr != nil { common.FailWithBusiError(c, busiErr) return } common.OKWithData(c, result) } // LeaveLiveSession 释放当前用户的观看租约。 func LeaveLiveSession(c *gin.Context) { dockID, sessionID := c.Param("dockId"), c.Param("streamSessionId") if dockID == "" || sessionID == "" { common.FailWithBusiError(c, common.ErrParam) return } if busiErr := service.DefaultLiveService.Leave(common.GetUserId(c), common.IsAdmin(c), dockID, sessionID); busiErr != nil { common.FailWithBusiError(c, busiErr) return } common.OK(c) } // StartLive 启动直播推流 func StartLive(c *gin.Context) { dockID := c.Param("dockId") if dockID == "" { common.FailWithBusiError(c, common.ErrParam) return } var req vo.LiveStartReq _ = c.ShouldBindJSON(&req) session, busiErr := service.DefaultLiveService.Start(common.GetUserId(c), common.IsAdmin(c), dockID, &req) if busiErr != nil { common.FailWithBusiError(c, busiErr) return } common.OKWithData(c, session) } func StopLive(c *gin.Context) { dockID := c.Param("dockId") if dockID == "" { common.FailWithBusiError(c, common.ErrParam) return } if busiErr := service.DefaultLiveService.Stop(common.GetUserId(c), common.IsAdmin(c), dockID); busiErr != nil { common.FailWithBusiError(c, busiErr) return } common.OK(c) } // GetLivePlayURL 获取直播播放地址 func GetLivePlayURL(c *gin.Context) { dockID, sessionID := c.Param("dockId"), c.Query("streamSessionId") // streamSessionId is optional for compatibility with older web clients. The // service resolves it from the caller's active viewer lease when omitted. if dockID == "" { common.FailWithBusiError(c, common.ErrParam) return } play, busiErr := service.DefaultLiveService.GetPlayURL(common.GetUserId(c), common.IsAdmin(c), dockID, sessionID) if busiErr != nil { common.FailWithBusiError(c, busiErr) return } common.OKWithData(c, play) } const maxAliyunCallbackBodyBytes = 64 * 1024 type aliyunLiveCallback struct { EventType string `json:"eventType"` AppName string `json:"appName"` StreamName string `json:"streamName"` EventTime int64 `json:"eventTime"` } func HandleAliyunLiveCallback(c *gin.Context) { token := common.AppConf.Live.CallbackAuthToken provided := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ") if token == "" || subtle.ConstantTimeCompare([]byte(provided), []byte(token)) != 1 { c.Status(http.StatusUnauthorized) return } body, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, maxAliyunCallbackBodyBytes)) if err != nil { c.Status(http.StatusRequestEntityTooLarge) return } callback, err := parseAliyunLiveCallback(c.GetHeader("Content-Type"), body) if err != nil || callback.EventType == "" || callback.AppName == "" || callback.StreamName == "" { c.Status(http.StatusBadRequest) return } if callback.EventTime > 0 && abs(time.Now().Unix()-normalizeAliyunCallbackTime(callback.EventTime)) > 300 { c.Status(http.StatusUnauthorized) return } if err := service.DefaultLiveService.HandleAliyunCallback(callback.EventType, callback.AppName, callback.StreamName, body); err != nil { c.Status(http.StatusInternalServerError) return } c.Status(http.StatusNoContent) } func parseAliyunLiveCallback(contentType string, body []byte) (aliyunLiveCallback, error) { var callback aliyunLiveCallback if strings.HasPrefix(contentType, "application/json") { err := json.Unmarshal(body, &callback) return callback, err } values, err := url.ParseQuery(string(body)) if err != nil { return callback, err } callback.EventType = values.Get("eventType") callback.AppName = values.Get("appName") callback.StreamName = values.Get("streamName") if value := values.Get("eventTime"); value != "" { callback.EventTime, err = strconv.ParseInt(value, 10, 64) } return callback, err } func normalizeAliyunCallbackTime(value int64) int64 { if value > 1_000_000_000_000 { return value / 1000 } return value } func abs(value int64) int64 { if value < 0 { return -value } return value }