163 lines
5.1 KiB
Go
163 lines
5.1 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 注册视频流代理路由(公开接口,不需要 JWT)
|
|
func RegisterVideoStreamRoutes(rg *gin.RouterGroup, transcode *service.TranscodeService, db *gorm.DB, media *service.MediaService, cfg *config.Config) {
|
|
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")
|
|
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")
|
|
|
|
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")
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|