package handler import ( "io" "log/slog" "net/http" "strings" "silk-server-go/internal/config" "silk-server-go/internal/model" "silk-server-go/internal/service" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // RegisterVideoStreamRoutes 注册视频流代理路由;路由可免 Authorization,但必须校验 videoToken。 func RegisterVideoStreamRoutes(rg *gin.RouterGroup, transcode *service.TranscodeService, db *gorm.DB, media *service.MediaService, cfg *config.Config) { SetVideoTokenSecret(cfg.JWTSecret) rg.GET("/video/clips/:clipId/stream", streamClip(transcode)) rg.GET("/video/cameras/:id/live/stream", streamLive(db, media, cfg)) rg.GET("/video/cameras/:id/live/proxy", proxyLive(db, media, cfg)) } // streamClip 视频流代理 + ffmpeg 转码 func streamClip(transcode *service.TranscodeService) gin.HandlerFunc { return func(c *gin.Context) { clipId := c.Param("clipId") if !requireVideoToken(c, "clip", clipId) { return } c.Header("Content-Type", "video/mp4") if err := transcode.StreamClip(clipId, c.Writer); err != nil { slog.Error("视频流转码失败", "clipId", clipId, "error", err) if !c.Writer.Written() { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) } } } } // streamLive 实时直播流代理(直接转发,不转码) func streamLive(db *gorm.DB, media *service.MediaService, cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { cameraId := c.Param("id") if !requireVideoToken(c, "camera", cameraId) { return } var camera model.Camera if db.Where("id = ?", cameraId).First(&camera).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"}) return } if camera.GbDeviceID == nil || camera.GbChannelID == nil || *camera.GbDeviceID == "" || *camera.GbChannelID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "摄像头未配置 GB28181"}) return } // 调用 WVP 启动流(非阻塞,超时也不影响后续代理) sourceURL := "" result, err := media.StartPlay(*camera.GbDeviceID, *camera.GbChannelID) if err != nil { slog.Warn("直播流代理:StartPlay 失败,尝试直接代理", "cameraId", cameraId, "error", err) // StartPlay 失败时,直接构造 ZLM FLV 地址 sourceURL = strings.TrimSuffix(cfg.ZLMAPIBase, "/") + "/rtp/" + *camera.GbDeviceID + "_" + *camera.GbChannelID + ".live.flv?originTypeStr=rtp_push&videoCodec=H264" } else { sourceURL = result.FLV } if sourceURL == "" { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "无法获取视频流地址"}) return } // 确保地址是完整的 ZLM 地址(WVP 返回的可能是相对路径 /rtp/...) if strings.HasPrefix(sourceURL, "/") { sourceURL = strings.TrimSuffix(cfg.ZLMAPIBase, "/") + sourceURL } slog.Info("直播流代理", "cameraId", cameraId, "sourceURL", sourceURL) resp, err := http.Get(sourceURL) if err != nil { slog.Error("直播流代理:获取源流失败", "cameraId", cameraId, "error", err) c.JSON(http.StatusBadGateway, gin.H{"error": "获取视频流失败"}) return } defer resp.Body.Close() for key, values := range resp.Header { for _, value := range values { c.Header(key, value) } } c.Status(resp.StatusCode) _, err = io.Copy(c.Writer, resp.Body) if err != nil { slog.Error("直播流代理:转发流失败", "cameraId", cameraId, "error", err) } } } // proxyLive 实时直播流直接代理(不转码,性能更好) func proxyLive(db *gorm.DB, media *service.MediaService, cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { cameraId := c.Param("id") if !requireVideoToken(c, "camera", cameraId) { return } // 1. 查摄像头 var camera model.Camera if db.Where("id = ?", cameraId).First(&camera).Error != nil { c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"}) return } if camera.GbDeviceID == nil || camera.GbChannelID == nil || *camera.GbDeviceID == "" || *camera.GbChannelID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "摄像头未配置 GB28181"}) return } // 2. 确保 WVP 流在线 result, err := media.StartPlay(*camera.GbDeviceID, *camera.GbChannelID) if err != nil { slog.Warn("直播流代理:StartPlay 失败", "cameraId", cameraId, "error", err) c.JSON(http.StatusServiceUnavailable, gin.H{"error": "摄像头可能离线: " + err.Error()}) return } // 3. 获取 FLV 源地址 sourceURL := result.FLV if sourceURL == "" { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "无法获取视频流地址"}) return } // 4. 确保地址是完整的 ZLM 地址 if strings.HasPrefix(sourceURL, "/") { sourceURL = strings.TrimSuffix(cfg.ZLMAPIBase, "/") + sourceURL } slog.Info("直播流代理", "cameraId", cameraId, "sourceURL", sourceURL) // 5. 直接代理转发 resp, err := http.Get(sourceURL) if err != nil { slog.Error("直播流代理:获取源流失败", "cameraId", cameraId, "error", err) c.JSON(http.StatusBadGateway, gin.H{"error": "获取视频流失败"}) return } defer resp.Body.Close() // 复制响应头 for key, values := range resp.Header { for _, value := range values { c.Header(key, value) } } c.Status(resp.StatusCode) // 复制响应体 _, err = io.Copy(c.Writer, resp.Body) if err != nil { slog.Error("直播流代理:转发流失败", "cameraId", cameraId, "error", err) } } }