c.file() 不适合音视频流式传输,因其强制 stat 文件、不支持 range 请求导致拖动失败,且无法处理动态流(如转码/管道),需手动解析 range、分块写入并正确设置响应头。

为什么c.File()不适合音视频流式传输
因为 c.File() 会调用 os.Open + 全文件读入内存或直接 io.Copy 到 response writer,但不设 Content-Length 时它仍会尝试 stat 文件——这在音视频源是管道、加密流、实时生成内容(如转码中)时直接 panic 或返回 500。更关键的是,它无法响应 Range 请求,浏览器进度条卡死、无法拖动、无法暂停续播。
手动实现支持 Range 的流式响应
音视频必须支持 HTTP Range 请求,否则现代播放器(Chrome/Safari/移动端 WebView)拒绝加载或仅播放开头几秒。Gin 本身不自动处理 Range,需自己解析 c.Request.Header.Get("Range") 并计算 offset/length。
- 先用
os.Stat获取文件大小(若源可 seek),否则放弃 Range 支持,只走 chunked - 解析
Range: bytes=1024-2047→ 提取start和end,注意end可能省略(bytes=1024-) - 设置状态码为
http.StatusPartialContent(206),头字段:Content-Range、Accept-Ranges: bytes、Content-Length(本次返回字节数) - 用
file.Seek(start, io.SeekStart)定位,再用io.CopyN(c.Writer, file, length)流式写入,避免整块读内存
Chunked 传输下 Content-Length 不能硬设
当音视频来自实时转码(ffmpeg stdout)、加密解密流、或网络拉流(如 RTMP 转 HLS 片段)时,总长度未知。此时必须省略 Content-Length 头,让 Go http server 自动启用 Transfer-Encoding: chunked。但要注意:
-
c.Header("Content-Length", "...")绝对不能调用——Gin 的 Header 是 lazy write,c.Data()或io.Copy会覆盖它,且设了但值错会导致客户端截断 -
c.Data()和c.File()都会破坏流式语义,必须用底层c.Writer+flush控制输出节奏 - 若需控制缓冲节奏(比如防网卡打满),可在每次
io.CopyN后调用c.Writer.Flush()
中文文件名在 Content-Disposition 中的编码陷阱
音视频下载常带中文名(如 "预告片_夏季限定.mp4"),直接塞进 Content-Disposition: attachment; filename="..." 会导致 Safari/Edge 下载名变乱码或截断。
- 兼容性最好的方式是用 RFC 5987 编码:
filename*=UTF-8''%E9%A2%84%E5%91%8A%E7%89%87_%E5%A4%8F%E5%AD%A3%E9%99%90%E5%AE%9A.mp4 - Go 标准库无现成函数,需手动 URL 编码:用
url.PathEscape处理文件名(不是QueryEscape),再拼接前缀 - 不要同时设
filename和filename*,旧浏览器优先读前者,新浏览器读后者;只设filename*即可
流式传输音视频真正难的不是“怎么发”,而是“怎么让每个播放器都正常起播、拖动、暂停”——Range 解析边界、flush 时机、编码格式头(如 MP4 的 moov 块位置)、以及 Content-Disposition 的实际渲染效果,缺一不可。











