揭秘B站视频秒播技术:m4s与SourceBuffer的奥秘
为什么 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
返回的数据长度
:
实际返回的数据长度为 end - start + 1,即 14514678 - 13965476 + 1 = 549,203 字节。
与 Range 请求的协作流程
客户端发起 Range 请求
:
示例请求:
GET /video.mp4 HTTP/1.1
Range: bytes=13965476-14514678
服务器响应 206 Partial Content
:
服务器检查文件是否存在、
Range
是否有效,并返回:
状态码 206(部分内容)。Content-Range 头部描述返回范围。Content-Length 头部描述返回数据的实际大小。
示例响应:
HTTP/1.1 206 Partial Content
Content-Range: bytes 13965476-14514678/44620616
Content-Length: 549203
Content-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) 字节范围有效性
边界检查
:
服务器需验证请求的
示例错误响应:
HTTP/1.1 416 Requested Range Not Satisfiable
Content-Range: bytes */44620616
(3) 多范围请求(Multi-Range)
支持多个范围
:
客户端可通过 Range: bytes=0-999,2000-2999 请求多个不连续的范围。
服务器需返回 multipart/byteranges 类型的响应,每个部分包含自己的 Content-Range 头部。
示例响应:
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=3d6b6a416f9b5
--3d6b6a416f9b5
Content-Type: video/mp4
Content-Range: bytes 0-999/44620616
[0-999字节的数据]
--3d6b6a416f9b5
Content-Type: video/mp4
Content-Range: bytes 2000-2999/44620616
[2000-2999字节的数据]
--3d6b6a416f9b5--
(4) 缓存控制
缓存部分内容:
服务器可通过Cache-Control头部控制Range响应的缓存行为。例如:
http
Cache-Control: public, max-age=3600
浏览器或 CDN 可能缓存 206 响应,但需注意缓存一致性(如文件更新时需失效缓存)。
调试与验证
使用 curl 测试
:
bash
curl -I -H "Range: bytes=13965476-14514678" https://example.com/video.mp4
预期响应:
HTTP/1.1 206 Partial Content
Content-Range: bytes 13965476-14514678/44620616
浏览器开发者工具
:
在 Network 面板中查看视频片段请求的 Range 头部和响应的 Content-Range 头部。
SourceBuffer中的视频数据存储位置
使用SourceBuffer处理视频时,视频数据并不会直接“保存”到传统意义上的本地文件系统路径中,而是存储在内存中,通过浏览器提供的机制进行管理和操作,具体分析如下:
SourceBuffer中的视频数据存储位置
内存中的临时存储:SourceBuffer是MediaSource API的一部分,用于在内存中动态构建媒体数据流。它接收音视频片段(MediaSegment)并通过appendBuffer()方法将这些片段添加到缓冲区中。这些数据不会直接写入硬盘,而是以二进制形式暂存于内存,供
数据生命周期与释放机制
内存释放:SourceBuffer中的数据在播放过程中持续占用内存,直至被显式移除或页面卸载。通过remove()方法可清理指定时间范围内的片段,或调用abort()终止所有操作。关闭页面或刷新时,内存中的数据会自动释放。无持久化存储:由于数据仅存在于内存,页面关闭后无法直接恢复。若需长期保存,需通过MediaRecorder API录制
与本地存储的区别
非文件系统存储: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