> 技术文档 > 揭秘B站视频秒播技术:m4s与SourceBuffer的奥秘_mp4读取 206 状态

揭秘B站视频秒播技术:m4s与SourceBuffer的奥秘_mp4读取 206 状态


为什么 b 站视频播放的那么快

m4s 分段存储视频,通过 range 请求动态下载某个视频片段,然后通过 SourceBuffer 来动态播放这个片段。

我们分析了 b 站、知乎视频播放速度很快的原因。

结论是通过 range 动态请求视频的某个片段,然后通过 SourceBuffer 来动态播放这个片段。

这个 range 是提前确定好的,会根据进度条来计算下载哪个 range 的视频。

播放的时候,会边播边下载后面的 range,而调整进度的时候,也会从对应的 range 开始下载。

服务端存储这些视频片段的方式,b 站使用的 m4s,当然也可以用 m3u8,或者像知乎那样,动态读取 mp4 文件的部分内容返回。

除了结论之外,调试过程也是很重要的:

我们通过 status-code 的过滤器来过滤出了 206 状态码的请求。

揭秘B站视频秒播技术:m4s与SourceBuffer的奥秘_mp4读取 206 状态

通过自定义列在列表中直接显示了 Content-Range:

揭秘B站视频秒播技术:m4s与SourceBuffer的奥秘_mp4读取 206 状态

通过 command + f 搜索了响应的内容:

揭秘B站视频秒播技术:m4s与SourceBuffer的奥秘_mp4读取 206 状态

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 请求的协作流程

  1. 客户端发起 Range 请求

    • 示例请求:

      GET /video.mp4 HTTP/1.1Range: bytes=13965476-14514678
  2. 服务器响应 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
  3. 客户端处理响应

    • 播放器或下载工具根据 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 头部。

SourceBuffer中的视频数据存储位置

使用SourceBuffer处理视频时,视频数据并不会直接“保存”到传统意义上的本地文件系统路径中,而是存储在内存中,通过浏览器提供的机制进行管理和操作,具体分析如下:

SourceBuffer中的视频数据存储位置

  • 内存中的临时存储SourceBufferMediaSource API的一部分,用于在内存中动态构建媒体数据流。它接收音视频片段(MediaSegment)并通过appendBuffer()方法将这些片段添加到缓冲区中。这些数据不会直接写入硬盘,而是以二进制形式暂存于内存,供标签实时解码和播放。
  • 依赖MediaSource对象MediaSource对象作为数据源与标签关联,其内部通过SourceBuffer管理多个媒体轨道(如音频、视频)。每个SourceBuffer实例对应一个轨道,所有片段按时间戳或顺序排列,形成完整的播放流。

数据生命周期与释放机制

  • 内存释放SourceBuffer中的数据在播放过程中持续占用内存,直至被显式移除或页面卸载。通过remove()方法可清理指定时间范围内的片段,或调用abort()终止所有操作。关闭页面或刷新时,内存中的数据会自动释放。
  • 无持久化存储:由于数据仅存在于内存,页面关闭后无法直接恢复。若需长期保存,需通过MediaRecorder API录制标签的播放内容,生成Blob对象后下载为文件,或调用captureStream()获取流数据并处理。

与本地存储的区别

  • 非文件系统存储SourceBuffer不涉及硬盘文件操作,所有数据均通过JavaScript动态操作。若需将视频保存到本地,需结合MediaRecorder或文件系统访问API(如Chrome的showSaveFilePicker)实现。
  • 实时处理特性:其设计初衷是支持流式播放和动态调整(如自适应码率),而非持久化存储。数据在内存中的高效管理确保了低延迟播放,但牺牲了长期存储能力。

获取视频和音频url

揭秘B站视频秒播技术:m4s与SourceBuffer的奥秘_mp4读取 206 状态

代码

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