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) } c.JSON(http.StatusOK, cameras) } } // 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, camera) } } // createCamera 新建摄像头 func createCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc { return func(c *gin.Context) { var camera model.Camera if err := c.ShouldBindJSON(&camera); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } 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, 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, 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) { 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 } var body struct { Format string `json:"format"` } c.ShouldBindJSON(&body) format := body.Format if format == "" { format = "hls" } expiresAt := time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339) // GB28181 摄像头:通过 WVP 媒体服务器播放 if camera.GbDeviceID != nil && camera.GbChannelID != nil && *camera.GbDeviceID != "" && *camera.GbChannelID != "" { result, err := media.StartPlay(*camera.GbDeviceID, *camera.GbChannelID) if err != nil { // StartPlay 失败不等于摄像头离线,可能是 WVP/ZLM 瞬时问题,不修改 is_online slog.Warn("StartPlay 失败", "deviceId", *camera.GbDeviceID, "channelId", *camera.GbChannelID, "error", err) c.JSON(http.StatusServiceUnavailable, gin.H{"error": "播放失败: " + err.Error()}) return } url := result.HLS if format == "flv" && result.FLV != "" { url = result.FLV } else if format == "webrtc" && result.WebRtc != "" { url = result.WebRtc } c.JSON(http.StatusOK, gin.H{ "cameraId": camera.ID, "gbDeviceId": camera.GbDeviceID, "gbChannelId": camera.GbChannelID, "format": format, "url": url, "expiresAt": expiresAt, }) return } // Fallback:使用摄像头自身的流地址 var url string switch format { case "flv": if camera.FlvURL != nil { url = *camera.FlvURL } case "webrtc": if camera.WebrtcURL != nil { url = *camera.WebrtcURL } default: if camera.HlsURL != nil { url = *camera.HlsURL } } c.JSON(http.StatusOK, gin.H{ "cameraId": camera.ID, "gbDeviceId": nil, "gbChannelId": nil, "format": format, "url": url, "expiresAt": expiresAt, "mock": url == "", }) } } // playbackCamera 查询摄像头的历史录像片段(query: from/to/limit) func playbackCamera(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { 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 { url := fmt.Sprintf("/api/v1/video/clips/%d/stream", clips[i].ID) 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"], "sipPassword": sip["password"], "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}) } }