Files
2026-08-14 00:07:52 +08:00

173 lines
5.4 KiB
Go

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)
}
}
}