gin.Context不支持直接流式返回视频二进制数据,因c.Data()全量加载内存、c.Stream()被默认中间件缓存阻塞;必须用c.Writer手动写入+Flush(),并禁用Logger/Recovery。

gin.Context 本身不支持直接流式返回视频二进制数据(如 MP4、HLS 或 WebRTC 媒体流),必须手动设置响应头、禁用默认中间件缓冲,并按 chunk 写入。否则会卡住、超时或返回空响应。
为什么 c.Data() 和 c.Stream() 都不能直接用于持续视频流
常见误解是调用 c.Data(200, "video/mp4", data) 就能“返回视频”,但这是全量内存加载——对大文件或实时流完全不可行;c.Stream() 虽然支持逐块写入,但它依赖于 http.ResponseWriter 的底层 flush 机制,而 Gin 默认启用了 gin.Recovery() 和 gin.Logger(),它们会拦截并缓存响应体,导致流无法及时到达前端。
必须关闭 Gin 的默认响应缓冲和日志中间件
在注册路由前,需显式禁用可能阻塞流的中间件:
- 用
gin.New()替代gin.Default(),避免自动注入Logger()和Recovery() - 若仍需日志,改用
gin.LoggerWithWriter()并确保不缓冲响应体(比如输出到os.Stderr) - 务必调用
c.Writer.Flush()每次写完一个 chunk 后,否则浏览器收不到数据
c.Writer 手动写入 HLS 或 MP4 流的最小可行代码
以返回本地 video.mp4 文件流为例(非内存加载):
func serveVideo(c *gin.Context) {
file, err := os.Open("video.mp4")
if err != nil {
c.AbortWithStatus(404)
return
}
defer file.Close()
// 获取文件大小用于 Content-Length(可选,但推荐)
stat, _ := file.Stat()
c.Header("Content-Type", "video/mp4")
c.Header("Content-Length", fmt.Sprintf("%d", stat.Size()))
// 禁用 Gin 的响应体缓存
c.Status(200)
buf := make([]byte, 4096)
for {
n, err := file.Read(buf)
if n > 0 {
c.Writer.Write(buf[:n])
c.Writer.Flush() // 关键:必须 flush
}
if err == io.EOF {
break
}
if err != nil {
break
}
}
}
注意:c.Writer 是原始 http.ResponseWriter,不经过 Gin 的 JSON/HTML 渲染流程,因此不能混用 c.JSON() 或 c.String()。
HLS 流需额外处理 .m3u8 和 .ts 分片请求
单纯返回一个 .m3u8 文件不够——浏览器会按列表里的 segment.ts 地址发起多次 GET 请求。你得提供两个路由:
-
GET /playlist.m3u8:返回 m3u8 文本内容,含相对路径如segment-001.ts -
GET /segment-*.ts:每个 ts 文件都要单独打开、设置Content-Type: video/MP2T、逐块 flush
若用 ffmpeg 动态转码生成 HLS,建议用临时目录 + 定时清理,避免磁盘爆满;同时注意并发读取同一 ts 文件时的文件锁问题(Go 默认无锁,但多个 goroutine 同时 os.Open 同一文件是安全的)。
c.Writer、是否正确设置 Content-Type 和 Transfer-Encoding、以及底层文件 I/O 是否阻塞。很多“流不动”的问题,其实卡在没调 Flush(),或者被中间件悄悄缓存了。


















