Files
silk/server-go/internal/handler/video_clip.go
T
2026-08-17 21:43:26 +08:00

196 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"fmt"
"net/http"
"strconv"
"time"
"silk-server-go/internal/config"
"silk-server-go/internal/middleware"
"silk-server-go/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// RegisterVideoClipRoutes 注册录像片段管理路由
func RegisterVideoClipRoutes(rg *gin.RouterGroup, db *gorm.DB, cfg *config.Config) {
readPerm := middleware.RequirePermission(db, "video:read")
rg.GET("/video/clips", readPerm, listClips(db))
rg.GET("/video/clips/:clipId/play", readPerm, playClip(db))
rg.POST("/video/clips/internal", createClipInternal(db, cfg)) // 白名单接口,无需权限
}
// listClips 录像片段列表(query: cameraId/from/to/limit,按 startAt DESC
func listClips(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
userID := currentUserID(c)
if userID == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供用户信息"})
return
}
limit := 50
if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 {
limit = l
if limit > 200 {
limit = 200
}
}
q := applyRoomScope(db.Model(&model.VideoClip{}), c, "room_id").Order("start_at DESC").Limit(limit)
if cameraId := c.Query("cameraId"); cameraId != "" {
q = q.Where("camera_id = ?", cameraId)
}
if from := c.Query("from"); from != "" {
q = q.Where("start_at >= ?", from)
}
if to := c.Query("to"); to != "" {
q = q.Where("start_at <= ?", to)
}
clips := make([]model.VideoClip, 0)
q.Find(&clips)
// 为每个片段设置 playbackUrl
for i := range clips {
token, err := IssueVideoToken(*userID, "clip", strconv.FormatUint(uint64(clips[i].ID), 10), videoTokenTTL)
if err != nil {
continue
}
url := fmt.Sprintf("/api/v1/video/clips/%d/stream?videoToken=%s", clips[i].ID, token)
clips[i].PlaybackURL = &url
}
c.JSON(http.StatusOK, gin.H{"items": clips, "total": len(clips)})
}
}
// playClip 获取片段播放地址
func playClip(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
userID := currentUserID(c)
if userID == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供用户信息"})
return
}
clipId := c.Param("clipId")
if !requireObjectAccess(c, db, "video_clip", clipId) {
return
}
var clip model.VideoClip
if db.Where("id = ?", clipId).First(&clip).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "video clip not found"})
return
}
format := clip.Format
if format == "" {
format = "mp4"
}
token, err := IssueVideoToken(*userID, "clip", strconv.FormatUint(uint64(clip.ID), 10), videoTokenTTL)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成播放令牌失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"clipId": clip.ID,
"url": fmt.Sprintf("/api/v1/video/clips/%d/stream?videoToken=%s", clip.ID, token),
"format": format,
"expiresAt": time.Now().Add(videoTokenTTL).UTC().Format(time.RFC3339),
})
}
}
// createClipInternal 内部接口创建录像片段记录(校验 x-api-key header
func createClipInternal(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
return func(c *gin.Context) {
// 校验 x-api-key
apiKey := c.GetHeader("x-api-key")
if apiKey != cfg.InternalAPIKey {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid api key"})
return
}
var body struct {
CameraID string `json:"cameraId"`
Trigger string `json:"trigger"`
Format string `json:"format"`
StartAt string `json:"startAt"`
DurationSec float64 `json:"durationSec"`
SizeBytes string `json:"sizeBytes"`
S3Bucket string `json:"s3Bucket"`
S3Key string `json:"s3Key"`
Resolution string `json:"resolution"`
Notes string `json:"notes"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 去重:同一 s3Key 不重复创建
var existing model.VideoClip
if db.Where("s3_key = ?", body.S3Key).First(&existing).Error == nil {
c.JSON(http.StatusOK, existing)
return
}
// 解析开始时间
startAt, err := time.Parse(time.RFC3339, body.StartAt)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid startAt, expected RFC3339 format"})
return
}
// 设置默认值
trigger := body.Trigger
if trigger == "" {
trigger = "schedule"
}
format := body.Format
if format == "" {
format = "mp4"
}
notes := body.Notes
if notes == "" {
notes = "自动录制归档"
}
clip := model.VideoClip{
CameraID: body.CameraID,
Trigger: trigger,
Format: format,
StartAt: startAt,
DurationSec: body.DurationSec,
S3Bucket: &body.S3Bucket,
S3Key: &body.S3Key,
Notes: &notes,
}
// 从 camera 查询 room_id
var camera model.Camera
if err := db.Where("id = ?", body.CameraID).First(&camera).Error; err == nil {
clip.RoomID = camera.RoomID
}
if body.SizeBytes != "" {
if n, err := strconv.ParseInt(body.SizeBytes, 10, 64); err == nil {
clip.SizeBytes = &n
}
}
if body.Resolution != "" {
clip.Resolution = &body.Resolution
}
if err := db.Create(&clip).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, clip)
}
}