ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

B站网页版视频分片抓取与零工具下载:DASH、fMP4与边播边录实践

B站网页版视频分片抓取与零工具下载:DASH、fMP4与边播边录实践

1. 先把这件事想明白:网页版为什么“看起来不能下载”

B站网页版没有给普通用户一个显眼的“下载到本地”按钮,这件事困扰过很多人。我第一次认真研究这个问题,是因为要给一个线下的分享会准备素材,会场网络很差,必须提前把几段视频存到本地。当时第一反应是找现成的下载工具,但装了三四个都不太满意:有的要装客户端,有的对分辨率有限制,有的下载完音画不同步。折腾半天之后我换了个思路——浏览器自己就能把视频文件“看”到,那我为什么不让浏览器自己把文件交出来。这个思路一旦成立,后面所有操作都不需要任何第三方软件,只用浏览器自带的开发者工具和下载管理器就够了。

先把结论摆出来:B站网页版采用的是DASH 流媒体传输,播放器不会一次性拿到一个完整的 mp4,而是一边播一边向服务器要一小段一小段的数据(业内叫分片,通常是 m4s 后缀),视频轨和音频轨还是分开两条线传输的。所以你打开开发者工具看到的不是一个大文件,而是几十上百条小请求。这也解释了为什么“右键另存为”在播放器上永远是灰的——页面上根本没有一个完整的视频文件可以被另存。

这套机制本身没有对错,它是目前主流视频站的通用做法:分片传输能自适应码率、能秒开、能拖进度条。我们要做的事,本质上就是把播放器悄悄拉取的这些小分片,按顺序收集起来,重新拼成一个完整文件。整件事拆开只有三步:抓到分片地址清单、按顺序请求这些地址、把二进制数据首尾相接保存下来。

这篇内容适合三类人看:一类是完全不懂前端、但愿意照着步骤点鼠标的朋友;一类是有一点 JavaScript 基础、想搞明白原理的开发者;还有一类是之前用过各种工具但总踩坑、想找一个更可控方案的人。我会把三条路线都讲透——从纯手工点击的笨办法,到几行控制台脚本一把梭,再到连脚本都不想写的“边播边录”救急方案。所有操作只用浏览器原生能力,不装扩展、不装客户端。

1.1 播放器里的视频不是一个文件,而是一条流水线

要理解后面的所有操作,得先接受一个心智模型的转变:你看到的“一个视频”,在网络上其实是几百个碎片。

打开任意一个视频页,按下 F12 打开开发者工具,切到 Network(网络)面板,然后刷新页面、点播放,你会看到请求列表像瀑布一样滚下来。里面混杂着图片、脚本、接口请求,真正跟视频内容相关的是后缀为.m4s的那一批,以及可能出现的.mp4初始化片段。它们的特征很明显:体积普遍在几百 KB 到两三 MB 之间,文件名里带一串数字编号,请求类型显示为media或xhr。

这里有个很多人忽略的细节:视频轨和音频轨是两条独立的流水线。你会在请求列表里看到两组编号不同的分片,一组的码率编号偏大(视频),另一组偏小(音频)。播放器在本地把这两条流实时“叠”在一起,所以你听着是有声音的,但实际上你下载下来的视频轨文件是没有声音的。这是新手最容易翻车的地方——兴冲冲拼完一个文件,打开一看画面正常、静音。

还有一个概念叫init 段(初始化段)。DASH 的每个分片并不是自包含的完整 mp4,它们共享一个“文件头”,里面记录了编码参数、时长、轨道信息等元数据。这个头通常作为第 0 号分片存在,也可能是一个独立的init.mp4。拼接的时候如果漏掉它,得到的就是一堆无法被播放器识别的裸数据——文件大小看着挺大,双击打开直接报错。所以后面所有拼接操作,init 段必须排在第一位。

注意:不同页面、不同清晰度下,init 段的存在形式不一样。有的是-1-30080.m4s这种带序号的,序号从 1 开始,init 单独一个请求;有的则是序号从 0 开始,第 0 个就是 init。抓包时先确认清楚,别凭感觉。

1.2 三条零工具路线,以及我怎么选

明确了原理,方案就自然浮出来了。我把可行路线归成三类,你可以对照自己的情况挑:

路线核心手段上手难度适合场景主要限制
路线一:手工抓取开发者工具复制 URL,逐条或批量下载低分片数量少、短视频分片多了会点到手软
路线二:控制台脚本fetch 批量请求 + Blob 拼接 + 触发下载中中长视频、需要完整音画需要会复制粘贴代码
路线三:边播边录captureStream + MediaRecorder低应急、脚本跑不通时耗时等于视频时长

我自己的选择逻辑是这样的:十分钟以内的短视频走路线一,因为分片可能就十几个,复制粘贴两分钟搞定;中长视频一律走路线二,一次性把视频轨拼成一个文件、音频轨拼成另一个文件,再用系统自带的播放器或者浏览器端的能力合成;路线二因为各种原因跑不通的时候(比如请求被拦、脚本报错),才退回路线三,虽然慢,但它绕开了所有网络请求层面的限制,只要画面能播出来,录屏就一定能拿到东西。

这里要提前说清楚一件事,免得后面产生误解:路线二拼出来的视频轨和音频轨,是两个独立文件。要合成一个带声音的 mp4,严格来说需要封装工具。但“不用下载工具”的边界我理解为“不安装第三方下载软件”,所以如果你手边有系统自带的命令行环境,做一次无损封装是很轻的操作;如果完全没有,那就用路线三直接录一个带声音的,或者接受“画面和声音分两个文件”这个结果。这个取舍我在第 4 节会展开讲。

2. 动手前的准备:环境、认知与一条安全底线

正式开始之前,有几件事得先交代清楚,否则操作到一半卡住会很难受。这一节看起来像“准备工作”,实际上是我踩过坑之后总结出来的前置检查清单,很多人失败不是因为步骤错,而是因为环境不对。

2.1 你需要准备什么

浏览器建议用桌面版的 Chromium 内核浏览器(比如 Chrome、Edge 这类)。原因很实际:它们的开发者工具面板功能最全,Network 面板的筛选、右键复制 URL、控制台执行异步代码这些都支持得很好。部分国产浏览器虽然也是 Chromium 内核,但开发者工具被裁剪过,Network 面板可能少了“复制所有 URL”这类关键功能,会让你多花很多时间。

页面必须是登录状态、并且能正常播放。这一点经常被忽略。很多分片请求是带鉴权参数的,URL 里那串deadline、upsig之类的东西就是签名,签名和你的登录态、过期时间绑定。如果你在未登录状态下抓到的地址,过一会儿再去请求,很可能直接返回 403。所以正确顺序是:先登录、再打开视频页、再开始抓包。

提前把清晰度固定下来。自适应码率是个双刃剑——它会让播放器在播放过程中根据网速切换清晰度,这意味着你抓到的分片可能前半段是 1080P、后半段掉到 720P,拼出来的文件在切换点会花屏甚至直接崩掉。稳妥做法是在播放器里手动锁定一个清晰度(比如 1080P),然后再开始抓。如果页面没有手动锁定选项,那就等播放稳定几十秒、确认码率不再变化之后再动手。

预留足够的磁盘空间和内存。一个 30 分钟的中等清晰度视频,视频轨加音频轨加起来可能有一两个 GB。而且路线二的内存占用是“全部数据先进内存再落盘”,换句话说,文件多大,峰值内存就差不多多大。8G 内存的机器处理两个小时的视频会很吃力,这一点在第 6 节讲内存爆掉的时候我会详细说。

2.2 fMP4 分片和 init 段:拼接能成功的关键

这一小节稍微偏技术,但它是决定你拼出来的文件“能不能播”的分水岭,值得花三分钟理解。

B站用的分片格式是fMP4(fragmented MP4),中文一般叫“分片式 MP4”。它和普通的 mp4 有个本质区别:普通 mp4 的文件头里包含了完整的索引表(moov box),播放器读到这个表才知道每一帧数据在文件的哪个位置;而 fMP4 的索引是每个分片自带一小段,配合最前面的 init 段使用。

这个设计带来一个非常好的副作用:fMP4 的分片可以直接二进制首尾相接。你不需要做任何转码、不需要重新封装,只要保证顺序对、init 段在最前面,把字节流拼起来就是一个合法的 fMP4 文件,绝大多数现代播放器(浏览器的 video 标签、系统的默认播放器、常见的第三方播放器)都能直接识别。

但这里有个隐藏的坑:分片的顺序不能靠文件名里的数字直接排。有些页面的编号是补零的(00001、00002),字符串排序没问题;有些是不补零的(1、2、10、11),你按字符串排就会得到1, 10, 11, 2这种灾难性顺序,拼出来的文件前几秒正常、后面全乱。所以我在脚本里一律用数值排序,而不是默认的字符串排序。这个细节我在第 4 节的代码里会标出来。

提示:判断拼接是否成功有个很快的自检方法——把拼好的文件拖进浏览器新标签页。浏览器能播、进度条长度和原视频对得上,基本就说明顺序和 init 段都没问题。播不了,第一件事就是检查 init 段有没有放第一位。

2.3 动手前必须明确的使用边界

这一条我必须放在显眼位置,因为它决定了这件事能不能心安理得地做。

网页版抓下来的内容,只适合个人在离线环境下观看——比如通勤路上没网、会场网络差、想反复看某段讲解做笔记。这些都属于合理的个人使用范畴。但有几条线不能碰:不要二次上传到任何平台、不要用于商业用途、不要剪辑后当作自己的作品发布、不要批量抓取他人内容做聚合站。平台的分片鉴权机制本身就是一种保护,绕过它去做规模化的事情,性质就变了。

另外,抓取频率要克制。脚本里我会加延时,不是为了“绕过限制”,而是避免给服务器造成不必要的压力,同时也降低自己 IP 被临时限流的概率。一口气发几百个并发请求,对谁都没好处,你自己也更容易遇到请求失败。

3. 路线一:开发者工具里“捞”出分片清单

先从最朴素的方法讲起。这条路线的价值不只是“能下载短视频”,更重要的是它能让你亲眼看见整个机制是怎么运转的。理解了这一步,后面写脚本、排查问题都会顺很多。

3.1 打开 Network 面板并过滤出媒体请求

在视频播放页按F12(或者右键 → 检查),默认会打开 Elements 面板,点顶部的Network(网络)标签切过去。面板刚打开时是空的,这是正常的,它只记录打开之后的请求,所以先开面板、再刷新页面。

刷新之后请求列表会瞬间刷屏。这时候别急着翻,先用筛选器。有两个高效的筛法:

第一种是用顶部的过滤输入框,输入m4s,列表会立刻收窄到只剩分片请求。这个筛法最直接,缺点是如果页面用了别的后缀(比如某些情况下是.mp4的 init 段),会被过滤掉,所以建议先不加过滤看一眼全貌,再收窄。

第二种是用类型筛选按钮,点Media(媒体)。这个按钮会把所有媒体类型的请求单独拎出来,通常就是你要找的那批。我个人更常用这个,因为它不会因为后缀变化而漏掉东西。

筛完之后,如果列表还是空的,说明播放器还没开始加载。这时候点一下播放按钮,或者在进度条上拖一下,让播放器“以为”你要看后面的内容,它就会去请求对应的分片。这是最关键的一个动作——分片是按需加载的,你不播到那儿,它就不会请求。想要完整的视频,就得让进度条从头走到尾,慢慢走一遍。

3.2 让播放器把分片“吐”出来

这里有个实操技巧:不要用 2 倍速刷。倍速播放确实能让进度条走得快,但播放器会跳过一些中间分片(因为解码器只请求需要的部分),你最后会得到一份有缺口的清单。正确的做法是让视频以正常速度从头播到尾,播的过程中你可以去干别的,让它在后台跑。

如果视频很长,正常速度播完要很久,有个折中方案:分段拖动。先让它播前 20%,然后把进度条拖到 40%、60%、80%、95%,每个位置停留十几秒让分片加载出来。这样能覆盖绝大部分分片,漏掉几个的话,后面拼接时会发现文件时长对不上,可以针对性补抓。

播放过程中你会看到列表不断增长。每一行都是一条请求,点开任意一条,右侧会显示详细信息:Request URL(请求地址)、Status(状态码)、Size(大小)、Type(类型)。Request URL 就是我们要的东西,它包含了完整的鉴权参数,直接复制出来就能用。

这里要特别留意Status 这一列。正常加载的分片应该是 200 或者 206(部分内容)。如果你看到一片 403、404,说明鉴权过期或者地址不对,这时候抓下来的清单是废的,得刷新页面重来。我一般会先扫一眼状态码列,全是绿的才继续往下走。

3.3 把清单变成可复制的 URL 数组

一条一条复制 URL 显然不现实,分片动辄几十上百个。好在 Network 面板支持批量操作:

在请求列表里按住 Shift 点第一条、再点最后一条(或者 Ctrl+A 全选,但要注意别把非分片请求也选进去),然后右键 → Copy → Copy all URLs。这一步会把选中的所有请求地址复制到剪贴板,一行一个。

拿到这堆地址之后,粘到一个临时文本文件里,做个简单的整理。整理的目标是得到两样东西:视频轨的地址列表和音频轨的地址列表。怎么区分?看 URL 里的码率编号段——通常视频轨的编号偏大(表示更高的视频码率),音频轨的编号偏小,而且同一组音频分片的 URL 除了序号部分,其余完全一致。你可以按 URL 前缀分个组,很快就看出规律了。

整理的时候顺手做一个检查:把地址按序号数值排一遍序,确认首尾序号是连续的、中间没有大段缺失。如果发现 1 到 30 有,31 到 50 没有,51 之后又有,说明中间那段你没播到,得回去补。

提示:复制出来的 URL 里通常带deadline这样的时间戳参数,意味着这份清单是有有效期的。整理完尽快用,别放到第二天。过期了也不用心急,重新抓一遍就行,分片地址的规律是一样的。

4. 路线二:控制台一把梭,请求、拼接、下载一条龙

路线一适合十几个分片的短视频,一旦分片上百,手工复制粘贴就变成了体力活。路线二把整个流程压缩成一次控制台执行——你把地址清单喂进去,剩下的请求、排序、拼接、触发下载全自动完成。这一节是全文的核心,我会把代码逐段拆开讲,包括每一行为什么这么写。

4.1 脚本的三段式结构

先讲清楚整体设计,免得你看代码时一脸茫然。整个脚本分成三段:

第一段是数据准备。把第 3 节抓到的 URL 清单粘进来,变成一个数组。这里有个小技巧——直接从 Network 面板 Copy all URLs 出来的内容是换行分隔的,粘进代码里需要转成数组格式,可以先用一个字符串模板接住,再用split('\n')切开,比手动加引号加逗号省事得多。

第二段是顺序请求。用一个循环依次 fetch 每个地址,拿回arrayBuffer,转成Uint8Array存进数组。为什么用Uint8Array而不是直接存ArrayBuffer?因为最后拼接需要按字节往一个大的缓冲区里set,Uint8Array支持这个操作,而且能算准总长度。

第三段是拼接与落盘。先算出所有分片的字节总数,分配一个足够大的Uint8Array,然后按顺序把每个分片set到对应偏移量上,最后包成Blob,用URL.createObjectURL生成一个本地地址,挂到一个临时<a>标签上点击触发下载。

这里有个关键点:请求必须串行,不能并发。并发虽然快,但几百个请求同时发出去,浏览器自身的连接数限制会让一部分请求排队超时,而且服务器端也可能判定为异常流量。串行慢一点,但稳。如果嫌慢,可以控制在 3 到 5 个并发,用简单的分批处理实现,但对大多数场景来说,串行加一点延时已经够用。

4.2 完整代码与逐行拆解

下面这段是可直接执行的版本,在视频播放页的控制台里跑。执行前先确认一件事:控制台当前上下文是这个视频页,别在别的标签页里跑。

(async () => { // ===== 第一段:数据准备 ===== // 把 Copy all URLs 得到的内容原样粘进下面这个模板字符串 const rawList = ` https://example-cdn.com/path/xxxx-1-30080.m4s?deadline=1700000000&upsig=aaa https://example-cdn.com/path/xxxx-2-30080.m4s?deadline=1700000000&upsig=bbb https://example-cdn.com/path/xxxx-3-30080.m4s?deadline=1700000000&upsig=ccc `; // 按行切开、去空白、去空行 const urls = rawList .split('\n') .map(s => s.trim()) .filter(Boolean); // 按 URL 里的分片序号做数值排序,避免字符串排序导致的乱序 const pickIndex = (u) => { const m = u.match(/-(\d+)-\d+\.m4s/); return m ? parseInt(m[1], 10) : 0; }; urls.sort((a, b) => pickIndex(a) - pickIndex(b)); console.log(`共 ${urls.length} 个分片,开始拉取…`); // ===== 第二段:顺序请求 ===== const chunks = []; const failed = []; for (let i = 0; i < urls.length; i++) { try { const resp = await fetch(urls[i], { credentials: 'include', // 带上登录态相关的凭证 referrer: location.href // 补齐来源信息,避免被拦 }); if (!resp.ok) { failed.push({ index: i, status: resp.status }); console.warn(`第 ${i + 1} 个失败,状态码 ${resp.status}`); continue; } const buf = new Uint8Array(await resp.arrayBuffer()); chunks.push(buf); console.log(`进度 ${i + 1}/${urls.length},本片 ${buf.byteLength} 字节`); } catch (err) { failed.push({ index: i, status: 'network-error' }); console.warn(`第 ${i + 1} 个异常`, err); } // 控制节奏,别把请求打成一片 await new Promise(r => setTimeout(r, 120)); } if (failed.length) { console.warn('有分片没拿到,清单如下:', failed); } // ===== 第三段:拼接与落盘 ===== const total = chunks.reduce((n, c) => n + c.byteLength, 0); console.log(`拼接开始,总大小约 ${(total / 1024 / 1024).toFixed(2)} MB`); const merged = new Uint8Array(total); let offset = 0; for (const c of chunks) { merged.set(c, offset); offset += c.byteLength; } const blob = new Blob([merged], { type: 'video/mp4' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'video-track.mp4'; document.body.appendChild(a); a.click(); a.remove(); // 稍后释放,避免立即回收导致下载中断 setTimeout(() => URL.revokeObjectURL(url), 60_000); })();

逐段说几个容易出错的地方。

pickIndex那个正则是我改过好几版才定下来的。最开始我用的是简单的字符串排序,结果拼出来的文件前 10 秒正常、后面直接花屏,排查了半天才发现是10排在了2前面。正则里的-(\d+)-\d+\.m4s是去匹配 URL 中“分片序号-码率编号.m4s”这个结构,如果你的地址结构和这个不一样,得相应调整。这一步一定要先打印几个序号看看对不对,可以在脚本开头加一行console.log(urls.slice(0, 5).map(pickIndex))自查。

credentials: 'include'这一行别省。虽然分片地址本身带了签名,但保险起见把凭证带上,能减少因为鉴权判断导致的 403。referrer同理,有些服务端会检查来源,补上更稳。

最后的setTimeout里那个 60 秒延迟释放,是我吃过亏之后加的。早期版本我在click()之后立刻revokeObjectURL,结果下载刚开始就被掐断,得到一个 0 字节的文件。Blob的下载是异步的,浏览器需要时间去读取,给它留足时间。

4.3 大文件怎么办:分批下载再有序合并

上面的代码有个硬伤:所有分片都堆在内存里。一个 1.5 GB 的视频,峰值内存占用可能到 2 GB 以上,加上浏览器本身的开销,8G 内存的机器很容易卡死,页面直接崩掉,前面的活儿全白干。

解决办法是分批。把地址列表切成若干份,每份控制在 200 MB 左右的产出体积,每批单独拼成一个 Blob 下载下来,得到part-01.mp4、part-02.mp4这样的文件。因为 fMP4 的特性,这些分段文件每个都能独立播放(从第二段开始可能没有文件头,播放器兼容性参差,但数据是完整的)。最后要合成一个完整文件,做一个二进制级别的顺序拼接即可。

批的大小怎么定?我的经验是按分片的平均体积估算。先跑前 10 个分片,算出平均每片多少 MB,然后用 200 除以这个平均值,就是每批的分片数量。举个例子,平均每片 1.2 MB,那每批放 160 个左右比较安全。这个数不用太精确,保守一点没坏处。

分批之后还有个好处:失败了不用从头再来。某一批里有几个分片拿不到,只需要重跑那一批,其他批次已经落盘了。这对于网络不稳定的环境特别友好。

4.4 音轨单独处理与最终合成

现在你手上有一个video-track.mp4,打开一看——没声音。别慌,这是预期内的。回到第 2 节讲的:音频是另一条独立的流水线。

处理方法完全一样:回到 Network 面板,这次筛选出音频分片(URL 里码率编号偏小的那一组),Copy all URLs,粘进同一个脚本,把下载文件名改成audio-track.mp4,重跑一遍。你会得到两个文件。

接下来是合成。这里把选择权交给你:

如果你有系统自带的命令行工具,一次无损封装就能搞定,不重新编码、不损失画质,几秒钟的事。命令的核心是告诉工具“第一个输入是画面、第二个输入是声音,把两路流原样装进一个容器”。参数上要特别注意加“直接复制流”的选项,千万别让它重新编码,否则时间会翻好几倍,画质还会掉一档。封装完之后用播放器打开验证一下,拖动进度条看音画是否同步——如果不同步,通常是两条轨的起始时间戳有偏差,加一个偏移修正参数就能解决。

如果你完全不想碰命令行,那有两个替代思路。一是接受“画面和声音分开”这个结果,用支持双文件播放的播放器同时加载,虽然别扭但能用。二是干脆放弃路线二,直接走路线三的边播边录,一步到位拿到带声音的文件,代价是耗时长。

注意:合成之前先分别验证两个文件。视频轨能播、时长正确;音频轨能播、时长和视频轨基本一致。如果两者时长差得比较多(比如差了几十秒),说明抓的时候漏了分片,得回去补抓,别急着合成,合出来一定对不上。

5. 路线三:零脚本的“边播边录”,适合救急

路线二虽然高效,但它依赖几个前提:能抓到完整清单、请求不被拦、内存扛得住。这三个前提任何一个不成立,你就得换路子。路线三是我的兜底方案——它绕开了网络请求这一层,直接对着播放器“拍下来”。听起来土,但成功率极高。

5.1 captureStream + MediaRecorder 到底在做什么

这两个 API 是浏览器原生能力,不需要任何扩展。captureStream()挂在<video>元素上,作用是把正在播放的画面和声音“导”出来,变成一个媒体流对象;MediaRecorder则负责把这个流按帧编码成视频文件。整个过程完全在浏览器内部完成,不经过网络。

用生活化的比喻:路线二是去后厨把食材原料一样样搬回家自己做;路线三则是坐在餐厅里,用摄像机对着桌上拍的菜录一遍。前者拿到的是原始素材,后者拿到的是成品,但成品质量受“拍摄条件”影响——你播得多流畅,录出来就多流畅。

在控制台执行的核心代码就几行:

// 找到页面上的 video 元素 const video = document.querySelector('video'); // 导出媒体流(部分浏览器需要加前缀写法) const stream = video.captureStream ? video.captureStream() : video.mozCaptureStream(); // 配置录制器 const recorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 8_000_000, audioBitsPerSecond: 128_000 }); const parts = []; recorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) parts.push(e.data); }; recorder.onstop = () => { const blob = new Blob(parts, { type: 'video/webm' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'recorded.webm'; a.click(); setTimeout(() => URL.revokeObjectURL(url), 60_000); }; // 开始录,每 1 秒切一个数据块 recorder.start(1000); video.play(); console.log('录制已开始,播完记得执行 recorder.stop()');

播完之后,在控制台执行recorder.stop(),文件就会自动下载下来。

5.2 参数怎么调才不掉帧

这段代码能跑,但默认参数下画质和流畅度未必理想,几个参数值得调。

videoBitsPerSecond是码率,直接决定清晰度。设太低画面会糊成一片,设太高文件巨大而且浏览器可能跟不上编码速度导致丢帧。我的经验值:720P 用 4 Mbps 左右,1080P 用 8 Mbps 左右,再高就要看机器性能了。这个数不是越高越好,编码器扛不住的时候,宁可按实际能力降一档。

mimeType的选择也有讲究。vp9压缩率高、体积小,但编码开销大,老机器上容易掉帧;换vp8会稳一些,代价是同等画质下文件更大。如果你的浏览器不支持指定的编码格式,MediaRecorder会直接抛错,所以执行前最好先探测一下支持列表。

recorder.start(1000)里的 1000 是切片间隔(毫秒)。设太小会产生大量小片段,最后合并时开销大;设太大则内存里堆积的数据多,中途崩了会丢掉一大截。1000 到 5000 之间都算合理。

还有一个非常重要的注意事项:录制期间不要切标签页、不要让页面进入后台。浏览器对后台标签页有资源调度限制,视频解码会被降频甚至暂停,录出来的文件会出现卡顿、音画不同步。整个录制过程老老实实把页面放在前台,可以去倒杯水,但别去干别的。

5.3 这条路线适合谁、不适合谁

适合的场景很明确:应急。比如你只需要视频里的一小段(五分钟的讲解),懒得抓上百个分片;比如路线二怎么都跑不通,请求一直被拦;比如你就是不想碰代码逻辑,只想点两下。

不适合的场景同样明确。长视频绝对不适合,因为录制耗时等于视频时长,一小时的视频就得录一小时,中间还不能动页面。对画质有要求的也不适合,重编码必然带来损失,而且受实时编码能力限制,画质上限比原片低。需要精确剪辑的也不适合,录制出来的文件时间轴和原始时间轴有细微偏差,剪起来对不齐。

6. 踩坑实录:常见问题与排查速查表

下面这些坑,一部分是我自己踩的,一部分是帮别人看问题时遇到的。按现象归类整理成一张速查表,遇到问题时对着找。

现象最可能的原因排查动作解决办法
请求返回 403签名过期或登录态失效看 URL 里的时间戳参数刷新页面重新抓清单
下载得到 0 字节文件Blob URL 被过早释放检查 revoke 的时机延迟释放,或干脆不释放
文件能播但没声音只抓了视频轨看清单里是否有第二组 URL单独抓音频轨
播放到某处花屏/卡死分片顺序错乱或缺失打印序号序列检查数值排序,补抓缺失分片
拼接后总时长偏短中间有分片没抓到数一下实际分片数与预期拖进度条补抓后重跑
页面卡死、控制台无响应内存占用过高看任务管理器的内存曲线改成分批下载
录制文件卡顿、音画不同步录制时页面在后台回放录制过程的操作保持页面前台,降低码率
拖动进度条后清单断档跳播导致中间分片未加载检查序号连续性正常速度完整播放一遍

挑三个最有代表性的展开说。

6.1 403、0 字节、下载直接失败

403 是最高频的问题,本质就一句话:你手里的地址“过期”了。分片 URL 里的签名参数通常有个有效期,短则几十分钟,长则几小时。抓完清单放着不动,过一阵再跑脚本,前半段能下、后半段全 403,非常典型。解决办法只有一个:刷新页面,重新抓一份新鲜清单,抓完立刻用。别偷懒想着“攒着一起弄”。

0 字节文件那个坑我在 4.2 节提过,但值得再强调一次。除了 Blob URL 过早释放,还有一种可能是浏览器拦截了自动下载。现在主流浏览器对“页面自动触发下载”有拦截策略,尤其是短时间内连续下载多个文件。表现就是第一个文件正常,第二个开始全部静默失败。可以在地址栏右侧看有没有拦截提示,手动允许一下;或者改成一次只下载一个文件,中间隔几秒。

6.2 合并后没声音、卡在开头、进度条乱跳

没声音的原因前面说过,是只抓了视频轨。这里补充一个变体情况:抓了音频轨,但音频轨的分片数量明显比视频轨少很多。这时候要警惕,可能是音频用了不同的分片策略(比如一个分片覆盖更长的时间),也可能你抓的时候音频还没加载完。判断方法是看抓到的音频总大小和视频总大小的比例,正常的音视频体积比大概在 1:10 到 1:20 之间,差得太离谱就说明没抓全。

卡在开头、进度条乱跳,八成是init 段缺失或顺序错误。fMP4 没有 init 段就像一本书没有目录和版权页,播放器不知道该怎么解析。回去检查清单里有没有序号为 0 的那一条,或者有没有独立的init.mp4请求,确保它排在最前面。

还有一种更隐蔽的情况:拼出来的文件能播,但拖到某个位置就卡住。这往往不是顺序问题,而是那个位置附近有分片缺失。播放器顺序播放时能“跳”过一小段空洞,但一旦你精确拖到空洞位置,解码器就拿不到数据了。解决办法是把进度条拖到卡住的位置附近,触发那一段的分片加载,补抓后重新拼接。

6.3 内存爆掉、页面卡死、下载中途断掉

内存问题在长视频上是必然会遇到的,不是“可能”。判断信号很明显:脚本跑着跑着控制台不再输出、页面开始转圈、任务管理器里浏览器进程的内存曲线一路飙到几个 G。这时候别等,直接关标签页重来,改成分批方案。

分批的粒度我一般按“单批产出不超过 200 MB”来定。为什么是 200 MB?因为浏览器在拼接阶段需要分配一块和总大小相等的连续内存,再加上各个分片本身占用的内存,实际峰值大概是总大小的 2 倍左右。200 MB 的产出对应 400 MB 峰值,加上页面本身的开销,1 GB 以内能打住,这个量级在老机器上也扛得住。

下载中途断掉还有个小概率原因:页面被切换到了后台标签。浏览器会降低后台页面的定时器精度,setTimeout的 120 毫秒延时可能被拉长到 1 秒以上,整个脚本慢得让人以为卡死了。跑脚本的时候把页面放前台,别切走。

7. 几句实操之后的心里话

这套方法我用挺久了,从最早一条条手点,到后来写脚本,再到现在会根据视频长度自动选路线,中间踩的坑基本都写在上面的表格里了。有几件事是我做完这些之后才真正想明白的。

第一件是**“知道原理”比“记住步骤”值钱得多**。分片、init 段、音视频分离这三个概念,只要你真的理解了,遇到任何变体都能自己推出来该怎么处理。我见过太多人照着教程一步步做,结果页面一改版、URL 结构一变,立刻就不会了。反而是那些搞懂了 fMP4 拼接原理的人,看到新结构能自己调正则、自己改排序逻辑。

第二件是工具的边界感。写着写着脚本,很容易产生一种“我要把它做成一个通用工具”的冲动,加参数、加界面、加进度条。但冷静下来想想,我一年也用不了几次,做成工具的成本远高于收益,而且做出来之后难免会想“分享出去”,那就越界了。保持在一个“自己够用”的粗糙程度上,反而是最舒服的状态。脚本丑一点没关系,能跑就行。

第三件是关于取舍。路线二拿到的是原始质量,但要多花步骤合成;路线三一步到位,但画质有损、耗时等于时长。没有哪个绝对更好,取决于你这次是要存档还是要快速看一眼。我现在判断的标准很简单:这个视频我以后还会不会再打开。会,就走路线二老老实实拼;不会,路线三录完就完事。

最后分享一个自查的小习惯。每次拼完文件,我都会先做三个动作再关页面:把文件拖进浏览器播一遍、拖到中间和结尾各看几秒、看一下文件属性里的时长和原视频对不对得上。这三步花不了一分钟,但能挡掉绝大部分“拼完了才发现白干”的情况。尤其是时长这一项,差个几秒可能就是漏了一两个分片,早发现早补,比事后重新抓一遍省事得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进