feat: 收口视频访问与摄像头密钥输出

This commit is contained in:
weijuesen
2026-08-14 00:07:52 +08:00
parent 440cf803e4
commit 1a29def480
14 changed files with 494 additions and 78 deletions
+44 -50
View File
@@ -86,7 +86,11 @@ func listCameras(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
slog.Warn("同步 WVP 设备信息失败,保留 DB 状态", "error", err)
}
c.JSON(http.StatusOK, cameras)
publicCameras := make([]CameraPublic, 0, len(cameras))
for i := range cameras {
publicCameras = append(publicCameras, toCameraPublic(cameras[i]))
}
c.JSON(http.StatusOK, publicCameras)
}
}
@@ -99,18 +103,19 @@ func getCamera(db *gorm.DB) gin.HandlerFunc {
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
return
}
c.JSON(http.StatusOK, camera)
c.JSON(http.StatusOK, toCameraPublic(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 {
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"
@@ -137,7 +142,7 @@ func createCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
}
}
}
c.JSON(http.StatusCreated, camera)
c.JSON(http.StatusCreated, toCameraPublic(camera))
}
}
@@ -185,7 +190,7 @@ func updateCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
}
}
}
c.JSON(http.StatusOK, camera)
c.JSON(http.StatusOK, toCameraPublic(camera))
}
}
@@ -212,6 +217,12 @@ func deleteCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
// 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 {
@@ -223,71 +234,45 @@ func playCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
return
}
var body struct {
Format string `json:"format"`
token, err := IssueVideoToken(*userID, "camera", strconv.FormatUint(uint64(camera.ID), 10), videoTokenTTL)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成播放令牌失败"})
return
}
c.ShouldBindJSON(&body)
format := body.Format
if format == "" {
format = "hls"
}
expiresAt := time.Now().Add(30 * time.Minute).UTC().Format(time.RFC3339)
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 != "" {
result, err := media.StartPlay(*camera.GbDeviceID, *camera.GbChannelID)
if err != nil {
_, 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", err)
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "播放失败: " + err.Error()})
slog.Warn("StartPlay 失败", "deviceId", *camera.GbDeviceID, "channelId", *camera.GbChannelID, "error", playErr)
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "播放失败: " + playErr.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,
"format": "flv",
"url": streamURL,
"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
}
}
// Fallback 不再返回直连流地址,避免绕过令牌代理。
c.JSON(http.StatusOK, gin.H{
"cameraId": camera.ID,
"gbDeviceId": nil,
"gbChannelId": nil,
"format": format,
"url": url,
"format": "flv",
"url": streamURL,
"expiresAt": expiresAt,
"mock": url == "",
"mock": true,
})
}
}
@@ -295,6 +280,12 @@ func playCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
// 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
@@ -318,7 +309,11 @@ func playbackCamera(db *gorm.DB) gin.HandlerFunc {
// 为每个片段设置 playbackUrl
for i := range clips {
url := fmt.Sprintf("/api/v1/video/clips/%d/stream", clips[i].ID)
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
}
@@ -342,7 +337,6 @@ func getWvpConfig(media *service.MediaService) gin.HandlerFunc {
c.JSON(http.StatusOK, gin.H{
"sipId": sip["id"],
"sipDomain": sip["domain"],
"sipPassword": sip["password"],
"sipPort": sip["port"],
"sipShowIp": sip["showIp"],
})