391 lines
13 KiB
Go
391 lines
13 KiB
Go
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)
|
||
// 同步共有参数到 WVP(silk → 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})
|
||
}
|
||
}
|