必须后端显式解析range请求头并正确传字节范围给opendownloadstream(),否则浏览器拖动进度条时反复请求全量数据、seeked事件不触发、duration显示infinity;http range是包含首尾的闭区间,而opendownloadstream的end参数是左闭右开区间,需+1对齐;还需返回206状态码、规范content-range头、建mongodb索引并设置超时。

必须后端显式解析 Range 请求头,并将字节范围正确传给 openDownloadStream(),否则浏览器拖动进度条时反复请求全量数据,seeked 事件不触发,播放器 duration 显示为 Infinity。
HTTP Range 头和 GridFS 的字节范围参数不一致
浏览器发起的 Range: bytes=1000-1999 是包含首尾的(共 1000 字节),但 openDownloadStream({ start: 1000, end: 2000 }) 中的 end 是**不包含**的(即 [start, end))。直接把 1999 当作 end 会少传 1 字节,导致音视频元数据损坏、duration 无限增长。
- 错误写法:
end = parseInt(rangeEnd)→ 实际只读了1000–1998共 999 字节 - 正确写法:
end = parseInt(rangeEnd) + 1(当rangeEnd存在时) - 边界情况也要处理:
Range: bytes=500-应设start = 500,不传end;Range: bytes=-500需换算为start = total - 500
响应头必须返回 206 Partial Content 和完整 Content-Range
仅返回部分数据但状态码仍是 200 或缺少 Content-Range,浏览器会认为资源加载失败或不可 seek。尤其注意 Content-Range 的格式必须严格匹配规范:空格不能省,单位必须是 bytes,且总长度要准确。
- 必须设置状态码:
res.status(206) - 必须设置响应头:
res.setHeader('Content-Range', `bytes ${start}-${end - 1}/${total}`) - 必须设置:
res.setHeader('Accept-Ranges', 'bytes'),否则 Chrome 不会发后续 Range 请求 -
Content-Length必须等于end - start,不能是原始文件大小
GridFS 查询和流式下载必须加索引 + 限 range
未建索引或未传 start/end 参数,会导致整个文件从 MongoDB 拉到内存再裁剪,I/O 和内存压力陡增,同时拖慢数据库——尤其在并发拖动时,多个 openDownloadStream() 同时读大文件,可能触发连接池耗尽或超时。
- 为
fs.files建复合索引:db.fs.files.createIndex({ filename: 1, uploadDate: -1 }) - 为
fs.chunks建索引:db.fs.chunks.createIndex({ files_id: 1, n: 1 }) - 务必从
req.headers.range解析出start/end,并传入bucket.openDownloadStream(fileId, { start, end }) - 对 >50MB 的文件,建议加
stream.setTimeout(30000),防止慢客户端长期占住连接
最易被忽略的是字节偏移的“±1”问题:HTTP Range inclusive vs JS stream exclusive 这个错位,会让最后一帧元数据丢失,进而让播放器无法解析时长——它不是报错,而是静默失效,debug 时得盯住 Content-Range 响应头和实际返回字节数是否一致。











