chore: 初始化仓库基线(AGENTS.md、git 规范、敏感文件排除)
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user