可行但需绕开HTTP限制,用WebSocket转发MPEG1流:Gin仅建立连接,FFmpeg单例输出至videoChan,JSMpeg前端需audio:true、canvas宽高、ws协议及Nginx透传Upgrade头。

直接用 Gin 做实时视频流输出是可行的,但必须绕开 http.ResponseWriter 的常规响应模式——它不支持长连接流式写入(比如持续写入 MPEG-TS 或 MJPEG 帧),默认会缓冲并关闭连接。真要跑通,得手动接管底层 net.Conn、禁用 HTTP/1.1 的 chunked 编码和超时,并配合合适的前端解码器(如 JSMpeg)。
为什么不能直接 return c.Data() 或 c.Stream()?
Gin 的 c.Data() 和 c.Stream() 本质仍是标准 HTTP 响应:写完就结束连接,无法维持 WebSocket 或 raw TCP 流;c.Stream() 虽然支持逐块写入,但底层仍走 http.ResponseWriter.Write(),受 HTTP 协议限制(如无 keep-alive 时连接会断、浏览器可能缓存首帧、无法跨域推送二进制帧)。实测中,用它推 MPEG1 视频流,Chrome 会卡在第一帧或报 net::ERR_INCOMPLETE_CHUNKED_ENCODING。
常见错误现象:
- 前端 canvas 显示黑屏,控制台无报错但
ws.onmessage不触发 - FFmpeg 推流正常,Gin 日志显示“write: broken pipe”
- 多个客户端连接后,新连接立即被旧连接挤掉(因复用同一 HTTP 连接未隔离)
正确做法:用 Gin 启一个 WebSocket 服务端
核心不是“输出视频流”,而是“转发视频流”——Gin 只负责建立 WebSocket 连接,把 FFMPEG 解出的 MPEG1/MP2 数据原样广播给所有订阅该地址的客户端。JSMpeg 客户端只认 WebSocket + MPEG1 视频流,不接受 HTTP 流。
实操要点:
- 用
gorilla/websocket(非 Gin 自带的gin-contrib/websocket)——后者封装过深,无法控制Upgrader.CheckOrigin和WriteMessage的 buffer 策略 - 升级连接前必须显式设置
Upgrader.CheckOrigin = func(r *http.Request) bool { return true },否则跨域请求直接 403 - 每个 RTSP 地址对应唯一 WebSocket path(如
/ws/cam1),避免不同摄像头流混在一起 - FFmpeg 命令必须指定
-f mpeg1video -ar 22050 -ac 1 -f mp2输出格式,JSMpeg 只认这个组合
示例关键片段:
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
r.GET("/ws/:camid", func(c *gin.Context) {
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil { return }
defer conn.Close()
<pre class="brush:php;toolbar:false;">camID := c.Param("camid")
// 从全局 map 获取对应 camID 的 videoChan <p>})
</p>FFmpeg 启动与生命周期管理容易踩的坑
别在 HTTP handler 里直接 exec.Command 启动 FFMPEG——每次新连接都拉起一个进程,CPU 爆满且无法复用流。必须按“RTSP URL → FFMPEG 实例”做单例管理,靠引用计数决定启停。
典型陷阱:
- 没设
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true},导致 Ctrl+C 无法终止子进程 - stdout pipe 未设置
bufio.NewReaderSize(..., 64*1024),小帧频繁 read 导致 goroutine 阻塞 - 超时控制用
time.AfterFunc而非context.WithTimeout,导致 cancel 信号无法透传到 FFMPEG 进程组 - 没捕获
cmd.Process.Signal(syscall.SIGTERM),强行 kill -9 会残留僵尸进程
建议用 golang.org/x/sync/errgroup 统一管控 FFMPEG stdin/stdout/stderr 三路管道,失败时能同步 cancel 所有 goroutine。
前端 JSMpeg 初始化必须匹配后端参数
JSMpeg 的 new JSMpeg.Player(wsUrl, { ... }) 不是万能胶水,几个硬性约束常被忽略:
-
audio必须为true,哪怕你只传视频——因为 JSMpeg 解码器内部强制读取 MP2 音频头,设false会导致解码器跳帧甚至崩溃 -
canvas元素需提前设置宽高(如style="width:640px;height:480px"),否则渲染区域为 0×0 -
url必须是ws://或wss://,不能是http://——哪怕 Gin 路由写了/ws/xxx,前端也得拼完整 ws 地址 - 若用 Nginx 反向代理 WebSocket,必须显式透传
Upgrade和Connection头,否则握手失败
最简可用初始化:
const player = new JSMpeg.Player('ws://localhost:8080/ws/cam1', {
canvas: document.getElementById('video-canvas'),
audio: true,
autoplay: true,
loop: false,
preserveDrawingBuffer: false
});
真正麻烦的从来不是“怎么连上”,而是“怎么让 20 个摄像头流同时稳定跑 72 小时不丢帧、不爆内存”——这取决于 FFMPEG 参数调优(-vf fps=25 控制帧率)、WebSocket 写缓冲区大小(conn.SetWriteDeadline)、以及是否对 GOP 结构做对齐。这些细节,调试时看 ffmpeg -v debug 输出比看 Gin 日志有用十倍。











