音视频不能直接用 r.static() 是因为 gin 默认不支持 http range 请求,导致浏览器无法分片加载、拖动失效;必须手动解析 range 头并用 http.servecontent() 或 c.datafromreader() 实现流式响应。

直接用 r.Static() 托管音视频文件,大概率会卡顿、拖动失效、无法分片播放——这不是你代码写错了,是 Gin 默认不支持 HTTP Range 请求,而浏览器播放器依赖这个机制做流式加载和进度跳转。
为什么音视频不能直接用 r.Static()
浏览器在播放 MP4、WebM、MP3 等资源时,会主动发起带 Range: bytes=0- 或 Range: bytes=1024-2047 的请求。Gin 原生 r.Static() 和 r.StaticFS() 不处理这个头,直接返回 200 + 全量文件,导致:
- 播放器反复下载整个大文件(比如 500MB 视频),拖动进度条就卡住
- Chrome/Safari 显示 “Failed to load because no supported source was found”
- 移动端 Safari 完全无法播放,连加载图标都不转
必须用 c.DataFromReader() 手动支持 Range
核心思路:不走静态路由,改用处理函数,自己解析 Range 请求头,用 http.ServeContent() 或 c.DataFromReader() 返回分片内容。Gin 没封装 Range 支持,得自己桥接标准库逻辑。
关键点:
- 读取文件前先用
os.Stat()获取大小,否则无法构造Content-Range头 - 必须设置
c.Header("Accept-Ranges", "bytes"),否则播放器不发 Range 请求 - 不要用
c.File()—— 它不支持 Range,且路径校验可能误拦合法请求 - 示例片段(托管单个 MP4):
func serveVideo(c *gin.Context) {
path := "./videos/demo.mp4"
f, err := os.Open(path)
if err != nil {
c.AbortWithStatus(404)
return
}
defer f.Close()
fi, _ := f.Stat()
c.Header("Accept-Ranges", "bytes")
http.ServeContent(c.Writer, c.Request, "demo.mp4", fi.ModTime(), f)
}
生产环境建议用 staticfs 第三方包
重复造轮子容易漏掉边界情况(比如多 Range、不合法偏移、并发读)。推荐用社区验证过的 github.com/go-dev-frame/sponge/pkg/gin/staticfs,它内部已封装 http.ServeContent 并自动处理 Range、缓存头、ETag。
使用方式:
- 替换原生
r.Static():调用staticfs.StaticFS(r, "/video", "./videos", staticfs.WithStaticFSIndexFile("")) - 它默认开启
Accept-Ranges,且对.mp4、.webm、.m4a等后缀自动设正确Content-Type - 注意:目录里不能放同名的
index.html,否则访问/video/会返回 HTML 而非文件列表(如果需要列表,得额外配 UI)
路径安全与性能陷阱
音视频文件通常体积大、并发高,两个细节极易被忽略:
-
os.Open()后必须defer f.Close(),否则 fd 泄露,Linux 下撑不过几千并发就会报too many open files - 别把音视频和静态资源混在同一个
r.Static()目录下——Gin 对所有子路径统一走静态逻辑,哪怕你只想要一个 MP4,整个目录都失去 Range 支持 - 如果文件路径来自 URL 参数(如
/play?id=xxx),务必用filepath.Clean()校验,防止../../etc/passwd类路径遍历
真要动态选文件,就老实用 os.Open() + http.ServeContent(),别试图绕过路径检查去“优化”。











