Files
silk/server-go/internal/handler/video_camera.go
T
2026-08-14 00:07:52 +08:00

391 lines
13 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"
"log/slog"
"net/http"
"strconv"
"time"
"silk-server-go/internal/middleware"
"silk-server-go/internal/model"
"silk-server-go/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// RegisterVideoCameraRoutes 注册摄像头管理路由
func RegisterVideoCameraRoutes(rg *gin.RouterGroup, db *gorm.DB, media *service.MediaService) {
readPerm := middleware.RequirePermission(db, "video:read")
writePerm := middleware.RequirePermission(db, "video:record")
rg.GET("/video/cameras", readPerm, listCameras(db, media))
rg.GET("/video/cameras/:id", readPerm, getCamera(db))
rg.POST("/video/cameras", writePerm, createCamera(db, media))
rg.PATCH("/video/cameras/:id", writePerm, updateCamera(db, media))
rg.DELETE("/video/cameras/:id", writePerm, deleteCamera(db, media))
rg.POST("/video/cameras/:id/play", readPerm, playCamera(db, media))
rg.POST("/video/cameras/:id/live", readPerm, playCamera(db, media))
rg.GET("/video/cameras/:id/playback", readPerm, playbackCamera(db))
rg.GET("/video/wvp-config", readPerm, getWvpConfig(media))
rg.GET("/video/wvp/devices", readPerm, listWvpDevices(media))
rg.GET("/video/wvp/devices/:deviceId/channels", readPerm, listWvpChannels(media))
rg.POST("/video/wvp/devices/:deviceId/sync", writePerm, syncWvpDevice(media))
}
// listCameras 摄像头列表(按 createdAt DESC,同步 WVP 设备信息:在线状态 + 共有参数)
func listCameras(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
var cameras []model.Camera
db.Order("created_at DESC").Find(&cameras)
// 先用 DB 中的 is_online 初始化 Online 字段(gorm:"-" 不会自动填充)
for i := range cameras {
cameras[i].Online = cameras[i].IsOnline
}
// 同步 WVP 设备信息(WVP → silk,失败时保留 DB 状态)
if devMap, err := media.SyncWvpDevices(); err == nil {
for i := range cameras {
if cameras[i].GbDeviceID == nil || *cameras[i].GbDeviceID == "" {
continue
}
info, ok := devMap[*cameras[i].GbDeviceID]
if !ok {
continue
}
// 同步在线状态
cameras[i].Online = info.OnLine
// 同步共有参数(WVP → silk),仅当 WVP 侧有值时才覆盖
updates := map[string]interface{}{}
if info.OnLine != cameras[i].IsOnline {
updates["is_online"] = info.OnLine
cameras[i].IsOnline = info.OnLine
}
if info.Name != "" && cameras[i].Name != info.Name {
updates["name"] = info.Name
cameras[i].Name = info.Name
}
if info.Manufacturer != "" {
if cameras[i].GbManufacturer == nil || *cameras[i].GbManufacturer != info.Manufacturer {
updates["gb_manufacturer"] = info.Manufacturer
cameras[i].GbManufacturer = &info.Manufacturer
}
}
if info.Password != "" {
if cameras[i].GbAuthPassword == nil || *cameras[i].GbAuthPassword != info.Password {
updates["gb_auth_password"] = info.Password
cameras[i].GbAuthPassword = &info.Password
}
}
if len(updates) > 0 {
db.Model(&model.Camera{}).Where("id = ?", cameras[i].ID).Updates(updates)
}
}
} else {
slog.Warn("同步 WVP 设备信息失败,保留 DB 状态", "error", err)
}
publicCameras := make([]CameraPublic, 0, len(cameras))
for i := range cameras {
publicCameras = append(publicCameras, toCameraPublic(cameras[i]))
}
c.JSON(http.StatusOK, publicCameras)
}
}
// getCamera 摄像头详情
func getCamera(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var camera model.Camera
if db.Where("id = ?", id).First(&camera).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
return
}
c.JSON(http.StatusOK, toCameraPublic(camera))
}
}
// createCamera 新建摄像头
func createCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
var input CameraInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
camera := input.toModel()
camera.ID = 0 // 让数据库自动生成
if camera.RoomID == nil {
defaultRoom := "1"
camera.RoomID = &defaultRoom
}
if err := db.Create(&camera).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
return
}
// 预添加设备到 WVP(设置独立密码,摄像头注册前 WVP 已有设备记录)
if camera.GbDeviceID != nil && *camera.GbDeviceID != "" {
manufacturer := ""
if camera.GbManufacturer != nil {
manufacturer = *camera.GbManufacturer
}
password := ""
if camera.GbAuthPassword != nil {
password = *camera.GbAuthPassword
}
if err := media.AddWvpDevice(*camera.GbDeviceID, camera.Name, manufacturer, password); err != nil {
// 设备可能已存在(之前添加过),尝试更新
if err2 := media.UpdateWvpDevice(*camera.GbDeviceID, camera.Name, manufacturer, password); err2 != nil {
slog.Warn("预添加/同步设备到 WVP 均失败", "deviceId", *camera.GbDeviceID, "addError", err, "updateError", err2)
}
}
}
c.JSON(http.StatusCreated, toCameraPublic(camera))
}
}
// updateCamera 更新摄像头
func updateCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var camera model.Camera
if db.Where("id = ?", id).First(&camera).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
return
}
updates, err := bindUpdates(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(updates) > 0 {
if err := db.Model(&model.Camera{}).Where("id = ?", id).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败: " + err.Error()})
return
}
}
db.Where("id = ?", id).First(&camera)
// 同步共有参数到 WVPsilk → WVP,更新了 name/manufacturer/password 任一字段就触发)
if camera.GbDeviceID != nil && *camera.GbDeviceID != "" {
needSync := false
for _, key := range []string{"name", "gb_manufacturer", "gb_auth_password"} {
if _, ok := updates[key]; ok {
needSync = true
break
}
}
if needSync {
manufacturer := ""
if camera.GbManufacturer != nil {
manufacturer = *camera.GbManufacturer
}
password := ""
if camera.GbAuthPassword != nil {
password = *camera.GbAuthPassword
}
if err := media.UpdateWvpDevice(*camera.GbDeviceID, camera.Name, manufacturer, password); err != nil {
slog.Warn("同步摄像头信息到 WVP 失败", "deviceId", *camera.GbDeviceID, "error", err)
}
}
}
c.JSON(http.StatusOK, toCameraPublic(camera))
}
}
// deleteCamera 删除摄像头
func deleteCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var camera model.Camera
if db.Where("id = ?", id).First(&camera).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
return
}
// 同步删除 WVP 中的设备
if camera.GbDeviceID != nil && *camera.GbDeviceID != "" {
if err := media.DeleteWvpDevice(*camera.GbDeviceID); err != nil {
slog.Warn("从 WVP 删除设备失败", "deviceId", *camera.GbDeviceID, "error", err)
}
}
db.Where("id = ?", id).Delete(&model.Camera{})
c.JSON(http.StatusOK, gin.H{"id": id})
}
}
// playCamera 播放摄像头实时流(body: {format},调用 media.StartPlay
func playCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
userID := currentUserID(c)
if userID == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供用户信息"})
return
}
id := c.Param("id")
var camera model.Camera
if db.Where("id = ?", id).First(&camera).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
return
}
if !camera.IsOnline {
c.JSON(http.StatusBadRequest, gin.H{"error": "摄像头离线,无法播放"})
return
}
token, err := IssueVideoToken(*userID, "camera", strconv.FormatUint(uint64(camera.ID), 10), videoTokenTTL)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成播放令牌失败"})
return
}
expiresAt := time.Now().Add(videoTokenTTL).UTC().Format(time.RFC3339)
streamURL := fmt.Sprintf("/api/v1/video/cameras/%d/live/proxy?videoToken=%s", camera.ID, token)
// GB28181 摄像头:通过 WVP 媒体服务器播放
if camera.GbDeviceID != nil && camera.GbChannelID != nil &&
*camera.GbDeviceID != "" && *camera.GbChannelID != "" {
_, playErr := media.StartPlay(*camera.GbDeviceID, *camera.GbChannelID)
if playErr != nil {
// StartPlay 失败不等于摄像头离线,可能是 WVP/ZLM 瞬时问题,不修改 is_online
slog.Warn("StartPlay 失败", "deviceId", *camera.GbDeviceID, "channelId", *camera.GbChannelID, "error", playErr)
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "播放失败: " + playErr.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"cameraId": camera.ID,
"gbDeviceId": camera.GbDeviceID,
"gbChannelId": camera.GbChannelID,
"format": "flv",
"url": streamURL,
"expiresAt": expiresAt,
})
return
}
// Fallback 不再返回直连流地址,避免绕过令牌代理。
c.JSON(http.StatusOK, gin.H{
"cameraId": camera.ID,
"gbDeviceId": nil,
"gbChannelId": nil,
"format": "flv",
"url": streamURL,
"expiresAt": expiresAt,
"mock": true,
})
}
}
// playbackCamera 查询摄像头的历史录像片段(query: from/to/limit
func playbackCamera(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
userID := currentUserID(c)
if userID == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供用户信息"})
return
}
id := c.Param("id")
limit := 50
if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 {
limit = l
if limit > 200 {
limit = 200
}
}
q := db.Where("camera_id = ?", id).Order("start_at DESC").Limit(limit)
if from := c.Query("from"); from != "" {
q = q.Where("start_at >= ?", from)
}
if to := c.Query("to"); to != "" {
q = q.Where("start_at <= ?", to)
}
var clips []model.VideoClip
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, clips)
}
}
// getWvpConfig 查询 WVP SIP 配置(供前端展示,便于配置摄像头硬件)
func getWvpConfig(media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
config, err := media.GetServerConfig()
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "获取 WVP 配置失败: " + err.Error()})
return
}
sip, ok := config["sip"].(map[string]interface{})
if !ok {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "WVP 配置中未找到 SIP 部分"})
return
}
c.JSON(http.StatusOK, gin.H{
"sipId": sip["id"],
"sipDomain": sip["domain"],
"sipPort": sip["port"],
"sipShowIp": sip["showIp"],
})
}
}
// listWvpDevices 查询 WVP 已注册设备列表
func listWvpDevices(media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
query := c.Query("query")
devices, err := media.ListWvpDevices(query)
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "获取 WVP 设备列表失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, devices)
}
}
// listWvpChannels 查询 WVP 设备的通道列表
func listWvpChannels(media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
deviceId := c.Param("deviceId")
if deviceId == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "deviceId is required"})
return
}
channels, err := media.ListWvpChannels(deviceId)
if err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "获取通道列表失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, channels)
}
}
// syncWvpDevice 触发 WVP 设备通道同步
func syncWvpDevice(media *service.MediaService) gin.HandlerFunc {
return func(c *gin.Context) {
deviceId := c.Param("deviceId")
if deviceId == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "deviceId is required"})
return
}
if err := media.SyncWvpDevice(deviceId); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "同步失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
}