揭秘B站视频秒播技术:m4s与SourceBuffer的奥秘_mp4读取 206 状态
为什么 b 站视频播放的那么快
m4s 分段存储视频,通过 range 请求动态下载某个视频片段,然后通过 SourceBuffer 来动态播放这个片段。
我们分析了 b 站、知乎视频播放速度很快的原因。
结论是通过 range 动态请求视频的某个片段,然后通过 SourceBuffer 来动态播放这个片段。
这个 range 是提前确定好的,会根据进度条来计算下载哪个 range 的视频。
播放的时候,会边播边下载后面的 range,而调整进度的时候,也会从对应的 range 开始下载。
服务端存储这些视频片段的方式,b 站使用的 m4s,当然也可以用 m3u8,或者像知乎那样,动态读取 mp4 文件的部分内容返回。
除了结论之外,调试过程也是很重要的:
我们通过 status-code 的过滤器来过滤出了 206 状态码的请求。
通过自定义列在列表中直接显示了 Content-Range:
通过 command + f 搜索了响应的内容:
range 请求
Content-Range 头部的作用
-
标识返回的字节范围
:
-
格式:
Content-Range: bytes -/
-
示例:
Content-Range: bytes 13965476-14514678/44620616
:13,965,476(返回数据的起始字节偏移量,从 0 开始)。
:14,514,678(返回数据的结束字节偏移量)。
:44,620,616(文件的总大小,单位:字节)。
-
-
返回的数据长度
:
- 实际返回的数据长度为
end - start + 1
,即14514678 - 13965476 + 1 = 549,203
字节。
- 实际返回的数据长度为
与 Range
请求的协作流程
-
客户端发起
Range
请求:
-
示例请求:
GET /video.mp4 HTTP/1.1Range: bytes=13965476-14514678
-
-
服务器响应
206 Partial Content
:
-
服务器检查文件是否存在、
Range
是否有效,并返回:
- 状态码
206
(部分内容)。 Content-Range
头部描述返回范围。Content-Length
头部描述返回数据的实际大小。
- 状态码
-
示例响应:
HTTP/1.1 206 Partial ContentContent-Range: bytes 13965476-14514678/44620616Content-Length: 549203Content-Type: video/mp4
-
-
客户端处理响应
:
- 播放器或下载工具根据
Content-Range
更新进度或拼接数据流。
- 播放器或下载工具根据
Range服务器支持
(1) 服务器支持
-
必须支持
Range
请求:
- 服务器需正确处理
Range
头部并返回206
状态码。若不支持,可能返回200 OK
和整个文件。
- 服务器需正确处理
-
配置示例(Nginx)
:
location / { add_header \'Accept-Ranges\' \'bytes\'; # 声明支持 Range 请求 root /var/www/html;}
(2) 字节范围有效性
-
边界检查
:
-
服务器需验证请求的
和
是否在文件范围内。若越界,可返回
416 Requested Range Not Satisfiable
。 -
示例错误响应:
HTTP/1.1 416 Requested Range Not SatisfiableContent-Range: bytes */44620616
-
(3) 多范围请求(Multi-Range)
-
支持多个范围
:
-
客户端可通过
Range: bytes=0-999,2000-2999
请求多个不连续的范围。 -
服务器需返回
multipart/byteranges
类型的响应,每个部分包含自己的Content-Range
头部。 -
示例响应:
HTTP/1.1 206 Partial ContentContent-Type: multipart/byteranges; boundary=3d6b6a416f9b5 --3d6b6a416f9b5Content-Type: video/mp4Content-Range: bytes 0-999/44620616 [0-999字节的数据]--3d6b6a416f9b5Content-Type: video/mp4Content-Range: bytes 2000-2999/44620616 [2000-2999字节的数据]--3d6b6a416f9b5--
-
(4) 缓存控制
-
缓存部分内容:
-
服务器可通过Cache-Control头部控制Range响应的缓存行为。例如:
httpCache-Control: public, max-age=3600
-
浏览器或 CDN 可能缓存
206
响应,但需注意缓存一致性(如文件更新时需失效缓存)。
-
调试与验证
-
使用
curl
测试:
bashcurl -I -H \"Range: bytes=13965476-14514678\" https://example.com/video.mp4
-
预期响应:
HTTP/1.1 206 Partial ContentContent-Range: bytes 13965476-14514678/44620616
-
-
浏览器开发者工具
:
- 在 Network 面板中查看视频片段请求的
Range
头部和响应的Content-Range
头部。
- 在 Network 面板中查看视频片段请求的
SourceBuffer中的视频数据存储位置
使用SourceBuffer
处理视频时,视频数据并不会直接“保存”到传统意义上的本地文件系统路径中,而是存储在内存中,通过浏览器提供的机制进行管理和操作,具体分析如下:
SourceBuffer中的视频数据存储位置
- 内存中的临时存储:
SourceBuffer
是MediaSource
API的一部分,用于在内存中动态构建媒体数据流。它接收音视频片段(MediaSegment
)并通过appendBuffer()
方法将这些片段添加到缓冲区中。这些数据不会直接写入硬盘,而是以二进制形式暂存于内存,供标签实时解码和播放。
- 依赖
MediaSource
对象:MediaSource
对象作为数据源与标签关联,其内部通过
SourceBuffer
管理多个媒体轨道(如音频、视频)。每个SourceBuffer
实例对应一个轨道,所有片段按时间戳或顺序排列,形成完整的播放流。
数据生命周期与释放机制
- 内存释放:
SourceBuffer
中的数据在播放过程中持续占用内存,直至被显式移除或页面卸载。通过remove()
方法可清理指定时间范围内的片段,或调用abort()
终止所有操作。关闭页面或刷新时,内存中的数据会自动释放。 - 无持久化存储:由于数据仅存在于内存,页面关闭后无法直接恢复。若需长期保存,需通过
MediaRecorder
API录制标签的播放内容,生成
Blob
对象后下载为文件,或调用captureStream()
获取流数据并处理。
与本地存储的区别
- 非文件系统存储:
SourceBuffer
不涉及硬盘文件操作,所有数据均通过JavaScript动态操作。若需将视频保存到本地,需结合MediaRecorder
或文件系统访问API(如Chrome的showSaveFilePicker
)实现。 - 实时处理特性:其设计初衷是支持流式播放和动态调整(如自适应码率),而非持久化存储。数据在内存中的高效管理确保了低延迟播放,但牺牲了长期存储能力。
获取视频和音频url
代码
public class Demo { public static void main(String[] args) { //保存路径 String savePath = \"E:\\\\\"; //视频处理 String videoUrl1 = \"https://cn-hncs-cu-01-09.bilivideo.com/upgcxcode/47/98/1044339847/1044339847-1-30077.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEqxTEto8BTrNvN0GvT90W5JZMkX_YN0MvXg8gNEV4NC8xNEV4N03eN0B5tZlqNxTEto8BTrNvNeZVuJ10Kj_g2UB02J0mN0B5tZlqNCNEto8BTrNvNC7MTX502C8f2jmMQJ6mqF2fka1mqx6gqj0eN0B599M=&tag=&nbs=1&oi=2748115477&mid=476134903&gen=playurlv3&og=hw&deadline=1746870842&platform=pc&trid=0000300b3d6e4130479cb8e8b369ca44b4bu&os=bcache&uipk=5&upsig=da32130624c313a1e7cc066d25375cef&uparams=e,tag,nbs,oi,mid,gen,og,deadline,platform,trid,os,uipk&cdnid=34209&bvc=vod&nettype=0&bw=323854&f=u_0_0&agrr=1&buvid=CFA17265-C236-754B-2EAB-19F445F8089E87661infoc&build=0&dl=0&orderid=0,3\"; // 文件名称 建议和链接保持一致 String name1 = \"1044339847-1-30077.m4s\"; downloadMovie(videoUrl1, savePath, name1); //音频处理 String videoUrl2 = \"https://cn-hncs-cu-01-09.bilivideo.com/upgcxcode/47/98/1044339847/1044339847_nb3-1-30280.m4s?e=ig8euxZM2rNcNbdlhoNvNC8BqJIzNbfqXBvEqxTEto8BTrNvN0GvT90W5JZMkX_YN0MvXg8gNEV4NC8xNEV4N03eN0B5tZlqNxTEto8BTrNvNeZVuJ10Kj_g2UB02J0mN0B5tZlqNCNEto8BTrNvNC7MTX502C8f2jmMQJ6mqF2fka1mqx6gqj0eN0B599M=&platform=pc&trid=0000300b3d6e4130479cb8e8b369ca44b4bu&mid=476134903&tag=&uipk=5&gen=playurlv3&os=bcache&oi=2748115477&deadline=1746870842&nbs=1&og=cos&upsig=42018fc6e33142ec3085bbf058137512&uparams=e,platform,trid,mid,tag,uipk,gen,os,oi,deadline,nbs,og&cdnid=34209&bvc=vod&nettype=0&bw=137033&build=0&dl=0&f=u_0_0&agrr=1&buvid=CFA17265-C236-754B-2EAB-19F445F8089E87661infoc&orderid=0,3\" ; // 文件名称 建议和链接保持一致 String name2 = \"1044339847_nb3-1-30280.m4s\"; downloadMovie(videoUrl2, savePath, name2); // ffmpeg -i E:\\1044339847-1-30077.m4s -i 1044339847_nb3-1-30280.m4s -codec copy E:\\video.mp4 } public static void downloadMovie(String BLUrl, String savePath, String fileName) { InputStream inputStream = null; try { URL url = new URL(BLUrl); URLConnection urlConnection = url.openConnection(); urlConnection.setRequestProperty(\"Referer\", \"https://www.bilibili.com/video/BV1j8411N7Bm\"); // 填需要爬取的bv号 urlConnection.setRequestProperty(\"Sec-Fetch-Mode\", \"no-cors\"); urlConnection.setRequestProperty(\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36\"); urlConnection.setRequestProperty(\"User-Agent\", \"Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)\"); urlConnection.setConnectTimeout(10 * 1000); inputStream = urlConnection.getInputStream(); } catch (IOException e) { e.printStackTrace(); } File file = new File(savePath+fileName); int i = 1; try { BufferedInputStream bis = new BufferedInputStream(inputStream); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); byte[] bys = new byte[1024]; int len = 0; while ((len = bis.read(bys)) != -1) { bos.write(bys, 0, len); } bis.close(); bos.close(); } catch (Exception e) { e.printStackTrace(); } }}
合并视频和音频
ffmpeg -i E:\\1044339847-1-30077.m4s -i 1044339847_nb3-1-30280.m4s -codec copy E:\\video.mp4
播放视频
播放软件:PotPlayer