c.File()和c.DataFromReader()均不适用于音视频流式输出:前者因调用os.Stat()和Seek()导致panic,后者因需预设size而引发chunked编码使video标签黑屏;必须直操作ResponseWriter,配合Flush逐帧推送,并确保Nginx关闭buffering、FFMPEG输出分片格式。

为什么 c.File() 和 c.DataFromReader 都不适合音视频流式输出
音视频流(尤其是 RTSP/HLS 源或实时转码流)不是静态文件,不能用 c.File()——它会尝试 os.Stat() 并读取整个文件,而流没有长度、不支持 Seek(),直接 panic;c.DataFromReader() 要求传入精确的 size,但实时音视频流长度未知,硬填 0 或估算会导致响应头 Content-Length 缺失,触发 Transfer-Encoding: chunked,而多数浏览器视频标签(<video></video>)拒绝接收 chunked 响应,表现为黑屏或“无法加载媒体”。
必须用 http.ResponseWriter.Write + Flush 实现逐帧推送
音视频流式输出本质是持续写入原始字节帧(如 MPEG-TS packet、H.264 NALU),靠客户端播放器按需消费。Gin 中唯一可控路径是绕过所有封装函数,直操作 http.ResponseWriter:
- 调用
w.Header().Set("Content-Type", "video/mp4")或"video/MP2T"(根据实际封装格式) - 禁用 Gin 自动设置
Content-Length:不要调c.DataFromReader(),也不要让任何中间件(如 gzip)包装w - 每次收到一帧数据(例如从 FFMPEG stdout 读到一个
[]byte),执行w.Write(frame),然后立即if f, ok := w.(http.Flusher); ok { f.Flush() } - 务必在首次
Write()前设置全部 header,包括Cache-Control: no-cache和Connection: keep-alive
FFMPEG 推流到 Gin 的典型管道结构
常见错误是让 FFMPEG 直连前端——这不可控且无法鉴权。正确做法是 Gin 启动子进程运行 FFMPEG,将输出 pipe 绑定到 handler 的 ResponseWriter:
- 启动命令示例:
ffmpeg -i rtsp://cam/ -f mp4 -movflags +frag_keyframe+empty_moov -vcodec copy -acodec copy - - 关键参数:
-movflags +frag_keyframe+empty_moov保证输出可被浏览器<video></video>流式解析;-f mp4输出 fragmented MP4(不是完整 MP4 文件) - Gin handler 中用
cmd.Stdout = w,但注意:必须手动处理http.Flusher,因为io.Copy()不会自动 flush;建议用 goroutine 分帧读取 stdout 并逐帧Write+Flush - 务必设
cmd.Stderr到日志,否则 FFMPEG 错误(如连接中断)完全静默
Nginx 反向代理下流式失效的三个硬坑
本地跑通 ≠ 上线可用。Nginx 默认行为会彻底破坏音视频流式传输:
-
proxy_buffering on(默认开启):Nginx 缓存整个响应体再吐给浏览器,导致首帧延迟数十秒——必须显式设为off -
proxy_cache或proxy_store开启:会尝试缓存流式响应,直接失败——确认配置中未启用 -
proxy_http_version 1.1必须显式设置,否则降级到 HTTP/1.0 会关闭 keep-alive,连接断开后流中断 - 额外提醒:Cloudflare 等 CDN 层根本不支持流式响应,音视频流路由必须绕过 CDN,直连应用服务器


















