做前端或者成天折腾网页抓取的人,应该都对这种链接不陌生:blob:https://example.com/5f6d7a8b-9c10-4d1e-8f2a-3b4c5d6e7f8a。视频明明放得好好的,右键想复制播放地址,拿到的却是这串东西,粘到新标签页直接白屏。很多人第一反应是:“这又是哪家平台的防盗链新招数?”其实把blob当成防盗链,是一个很常见的误会。它背后真正牵着的,是一整套流媒体分片播放和 Media Source Extensions 的链路。这篇文章我想把 blob 链接的底细彻底拆开——它怎么来的、视频数据走的是什么路径、防盗链到底发生在哪个环节,再聊聊实际情况下保存视频时那些能落地的做法。
这篇文章适合这几类人:前端开发者想知道视频标签为什么挂了个诡异的 blob 地址;做爬虫和数据分析的同学想搞清楚视频真实地址在哪;还有视频站点的运营者,想弄明白自己辛辛苦苦做的防盗链到底防了什么、没防什么。看完你至少能判断一个网页视频能不能保存、该走哪条路去拿,以及哪些情况想都不要想。
1. 先拆开看:blob链接到底是什么东西
1.1 blob协议和http协议:一个门牌号,一个座位号
要理解 blob 链接,最核心的一点是:它不是网络地址,根本不指向任何服务器上的资源。http://开头的 URL,相当于一个餐厅的门牌号,浏览器拿着它走网络请求,服务器响应内容,才拿到数据。而blob:开头的 URL,是浏览器在本地创建的一个“座位号”——数据已经在你电脑的内存里了,这个地址只是告诉你“去第几桌找你那份菜”。
这个“座位号”由一行代码生成:
// 把一段二进制数据交给浏览器,生成一个本地引用地址 const videoBlob = new Blob([arrayBuffer], { type: 'video/mp4' }); const blobUrl = URL.createObjectURL(videoBlob); video.src = blobUrl;URL.createObjectURL()接收一个Blob或File对象,返回一个类似blob:https://域名/uuid的字符串。这个字符串的有效期和页面生命周期绑定,页面关掉,或者你调用URL.revokeObjectURL(),它就彻底失效。所以你会发现从旧页面复制的 blob 链接,换个地方打开必然白屏——因为那个“座位”在原地,不在新餐厅里。
这里有个值得注意的细节:blob 链接里带着当前页面的域名,所以它表面看像是一个指向这个域名的资源,其实域名只是用来标记这个 blob 的“归属地”,真正的数据此刻可能就躺在http://站点域名/对应的那个标签页进程的内存里。
1.2 视频为什么需要这种“临时门牌号”
很多人问:为什么视频播放不用正经的.mp4地址,要搞一个临时的 blob?答案要分几个场景看。
第一个场景是本地预览。用户上传了一个视频,你在没上传成功之前想让用户看到效果,这时候文件在浏览器本地,从input[type=file]拿到File对象,直接createObjectURL塞给<video>就能播,不需要先传服务器再拉回来。这一步几乎是所有带视频上传功能的站点都在用的方案。
第二个场景,也是更核心的场景:流媒体分片。当视频动辄几十分钟、几个GB,平台不可能让你一口气把整个文件拉下来。它们会把视频切成几百上千个小分片(TS 或 MP4 片段),浏览器拿到分片后,需要通过MediaSource对象把一块块数据“拼接”成一段连续的媒体流,再喂给<video>。而<video>的src属性只接受 URL,这时候就需要一个“本地地址”来占位指向这个不断在生长的 MediaSource——blob 就是干这个的。这个场景后面我会详细展开,它是理解整个问题的关键。
第三个场景是绕开跨域限制。如果你在一个网页里动态生成了一段音频或视频数据,想直接在页面上播放,用data:URI 会碰到长度限制和性能问题,用blob:则可以在不产生网络请求的前提下完成播放,而且不会触发跨域问题。
1.3 blob、data URI和File三者的关系
我画个表格放在这里,方便你从全局理解这几个概念的边界。
| 类型 | 地址/内容长度 | 数据存储位置 | 跨页面有效 | 典型用途 |
|---|---|---|---|---|
http(s):// | 短 | 服务器磁盘/缓存 | 长期,可被收藏分享 | 页面资源、API接口 |
blob:https://域名/xxx | 短 | 浏览器内存或临时文件 | 仅当前页面生命周期内 | 本地预览、MSE分片喂流 |
data:video/mp4;base64,.... | 随内容变长 | 当前文档内 | 单次页面内 | 小图片、小文件内嵌 |
File对象 | 不参与URL | 浏览器内存/用户磁盘 | 属于JS运行时对象 | 文件上传、本地处理 |
Blob是数据类型,File继承自Blob,data:URI 是把数据本身嵌入进地址,而blob:URI 只是一个引用。用在视频场景时,blob:几乎总是配合MediaSource使用——这一点在下一章会明白。
2. 防盗链和blob:很多人的第一个误会
2.1 传统防盗链手段都有哪些
在聊清楚误会之前,先看看真正的防盗链长什么样。防盗链的本质是两个问题:第一,“你有没有资格拿这个资源”;第二,“你能拿多久”。围绕这两个问题,业内有一套组合拳。
最基础的是Referer校验。服务器检查请求头里的Referer字段,如果发现来源页面不在白名单里,直接 403。这个方案因为Referer可以被伪造,现在已经只是入门级防护。
稍微进阶的是Token签名和时效 URL。服务器生成一个带过期时间的签名,塞进 URL 参数里,比如https://cdn.example.com/video.mp4?sign=abc&expires=1720000000。CDN 校验签名过期时间,过期就拒。这种方案的目的是防止有人把地址扒下来到处传播,但你拿到地址后在一定时间内还是能直接下载的。
再往上,是更严格的组合:请求头自定义字段、Cookie 鉴权、IP 限频、UA(User-Agent)校验。比如要求请求时必须带一个某某自定义的 Header,值为一段加密字符串。这种主要是为了防脚本,因为浏览器播放器本身发出的请求,所有 Header 都是相对固定的,而你用脚本去下载时很容易漏掉自定义字段。
注意,这些防盗链手段全部发生在网络请求层——也就是客户端向服务器拿数据的那一刻。它们的共同特点是:要么让你拿不到真实地址,要么让你拿到地址但没权限用,要么让你能用但在短时间内失效。
2.2 blob凭什么被当成防盗链
那么 blob 和防盗链有什么关系?答案是:没有直接关系,但它经常给人造成“防盗链很成功”的感觉。
场景还原一下:你打开一个视频页面,播放器正常播放,开发者工具里Network面板全是些看不懂的m4s或ts请求,视频标签的src却是blob:https://...。你没法从开发者工具里直接右键复制一个.mp4地址,也没法用下载工具直接抓到一个静态文件。看起来像是防盗链把你挡住了。
真实情况是:视频成功在你这播放了,数据也已经到了你电脑上。只是客户端用的是“播放器代码 + 分片请求 + 自定义协议”的方式在取数据,而不是简单地访问一个.mp4。blob 在里头只是一个“中转容器”,承载的是浏览器本地MediaSource的输出——它既不是服务器地址,也不是加密产物。
换句话说,blob 把“获取真实媒体地址”这件事变成了“必须搞清楚它的播放器逻辑才能拿到真实地址”,变相提高了门槛。但这只是门槛高,不是拿不到。真正的防盗链是那层在请求分片时校验的东西:比如动态签名的 m3u8 地址、几秒过期的一次性 token、要求带特定 Header 的 CORS 策略。你在网页里看见 blob,只能说明这个站点用了流媒体播放方式,不能说明它还额外加了几道锁。
2.3 想保护视频资源,真正该做的是这些
如果你是自己运营视频站点、正在纠结要不要把 mp4 换成 blob 来防盗链,我的建议是:把 blob 当“体验方案”,别当“安全方案”。
blob + MSE 解决的核心问题是:首屏秒开、按需加载、断网续播、码率自适应。这些是产品体验层面的价值。而在保护内容层面,你应该把精力放在这几个地方:
一是分片地址动态化。每个分片都用短时效签名,过期之后即使有人拿到 URL 也访问不了。这是目前性价比最高的手段,几乎所有主流平台都在用。
二是请求鉴权前移。播放前先让你拿到一个带权限的播放凭证,每个凭证绑定会话、设备指纹和过期时间。分片请求时校验凭证。一旦凭证过期或换设备,重新申请。
三是合理使用 DRM。对于高价值版权内容,使用 Widevine、FairPlay 这类 DRM 方案,结合硬件级的安全等级。但要注意,DRM 是有体验代价的,它限制你播放器和浏览器版本,还会带来兼容性问题。如果你不是平台级产品,其实不太需要上。
这层想明白之后,你就会懂得:看到 blob 不用觉得“完了这拿不到”,而是应该去想“服务器给的播放清单(m3u8/mpd)在哪、分片签名有效期多长”。顺着这个思路,保存视频就有了明确的切入点。
3. 流媒体分片:blob背后真正的数据管线
3.1 为什么主流视频平台不直接传MP4
假设你在写一个视频平台,简单粗暴的方案是:后端存一个movie.mp4,前端<video src="https://.../movie.mp4">完事。这个方案在视频很小、用户很少、网络环境理想的时候没问题,但一旦规模上来,痛点立刻出现。
首先是首播等待。一个 2GB 的 MP4,用户在弱网环境下要等 MP4 的moov元数据加载到足够位置才可能开始播放,那个转圈圈的几十秒足够劝退用户。其次是带宽浪费。用户只看前 3 分钟,服务器却要按最大码率把整个文件吐出来。然后是自适应做不到——4K 要推送 4K 分片,720p 要推送 720p 分片,整段文件不好灵活切换。
流媒体分片的核心思路,和杂志连载一个道理。你不一次性把整本书给读者,而是每期发一章。读者看了这章觉得好,再给他发下一章。视频平台把完整文件切成若干时长 2~10 秒的碎块,浏览器播完一个再请求下一个。同时视频和音频还可以分轨存储,视频轨一段段发,音频轨一段段发,播放器把它们对齐组合。分片模式下,用户看到第 5 秒时,浏览器才拉完第 1 个分片;看到第 2 分钟时,才拉完第 12 个分片。网络差就切换低码率分片,网络好就切高码率分片,这一切都在播放过程中无缝完成。
3.2 HLS和DASH:两套主流分片协议
目前最主流的两个分片协议分别是 HLS 和 DASH。
HLS(HTTP Live Streaming)是苹果提出的方案,分片格式为 TS(MPEG-TS)或较新的 FMP4,索引文件是m3u8。它的优势是生态成熟,iOS 和 Android 的浏览器几乎都原生支持,服务器端的基础设施也非常丰富。我们今天看很多视频站点的流地址,基本上都是.m3u8结尾。
DASH(Dynamic Adaptive Streaming over HTTP)是更通用的标准,索引文件叫 MPD,分片一般是 FMP4。它在多音轨、多字幕、多码率的组织上比 HLS 更灵活,标准也更现代,但因为各家浏览器支持不统一,在网页端通常需要搭配MediaSource和dash.js这类 SDK 来播放。
这两个协议的共同点是:都通过一个索引文件去描述“视频有哪些分片、每个分片 URL 是什么、音视频轨怎么组合”。也就是说,拿到了 m3u8 或 MPD,就等于拿到了整部视频的地址清单。
3.3 MSE:浏览器里的分片“装配线”
有了分片和索引,浏览器怎么把它们变成连续的视频流?这就是 Media Source Extensions(MSE)的活。
打个比方,<video>标签是一个需要被投喂的“放映机”,它不认一堆碎片文件,只认一段连续的媒体流。MSE 就好比在浏览器里搭了一个“装配车间”:你用 JS 创建一个MediaSource对象,等它触发sourceopen事件后,添加一个或多个SourceBuffer(分别用于视频轨道、音频轨道),然后不断把下载好的分片数据通过appendBuffer()塞进 SourceBuffer。装配线一边往缓冲池里加料,放映机一边播放。
MSE 和 blob 的关系在这一刻才真正显现:MediaSource对象本身不是一个 URL,但<video>的src需要一个 URL,所以浏览器用URL.createObjectURL(mediaSource)生成一个 blob 地址来占位。你在开发者工具里看到的blob:,其实指向的是这个正在被动态填充的 MediaSource。
完整的播放链路是这样的:
- 页面加载播放器 JS,拿到播放凭证;
- 播放器请求 m3u8 或 MPD 索引文件;
- 解析索引,拿到分片列表;
- 创建
MediaSource,生成 blob URL,赋给<video>; - 循环请求分片文件,
appendBuffer喂给 SourceBuffer; - 视频播放过程中,按需淘汰缓冲里面不需要的数据。
理解了这条链路,再去看 Network 面板里那些零零碎碎的视频请求,你就知道它们不是一个一个孤立的下载,而是整条流水线上的工序。想保存视频,本质上是把这条流水线上“喂”给浏览器的那份完整数据,在本地重新组装成文件。
4. 实操:怎么把blob视频完整保存下来
4.1 第一步:判断视频到底走没走MSE
保存视频之前,先判断你面对的是哪种情况。这决定了后续用什么手段。
打开开发者工具的Media面板(Chrome 系浏览器都有),播放视频,看看里面有没有显示Player Properties、Buffer、Audio/Video Tracks等信息。如果有,说明走的是 MSE 分片播放。这一步非常关键,甚至比抓地址还重要——它能帮你快速区分“真 blob(内存数据)”和“流媒体 blob(MSE 占位)”。
两种情况的表象都是src="blob:...",但本质完全不同。
- 纯 blob 视频:数据在浏览器内存里,可能来自用户上传、JS 拼接、Canvas 录制等。媒体面板里不会出现 MSE 信息。
- MSE 流媒体视频:分片从网络请求而来,通过 SourceBuffer 喂给播放器。媒体面板里会出现明确的分片缓冲情况和音视频轨道信息。
看Network面板也能辅助判断。如果视频在播放过程中持续出现大量.ts、.m4s、.m4a、.mp4的请求,且 URL 的域名和路径风格与普通静态资源明显不同,那基本就是流媒体。
4.2 纯blob视频的保存方法
如果是纯 blob 视频,保存思路最直接:数据本来就在浏览器内存里,你只需要把 blob 转成一个文件并触发下载。
在页面控制台里执行这样一段脚本,可以把这个 blob 视频导出来:
// 找到 video 标签 const video = document.querySelector('video'); // 拿到 blob url const blobUrl = video.src; // 拉取 blob 数据 const resp = await fetch(blobUrl); const blob = await resp.blob(); // 创建本地下载链接 const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'video.mp4'; a.click();这段脚本的原理是:fetch能直接请求 blob URL 并把数据以Blob形式返回,然后我们用URL.createObjectURL生成一个纯本地的下载链接,用<a download>触发下载。注意,download属性填的文件名后缀最好与blob.type一致,否则有的设备不认识。
如果页面有 JS 对 blob 数据做了分片拼接,你直接取video.src拿到的可能是不完整的。更稳妥的做法是找到页面代码里真正持有完整Blob或ArrayBuffer的那个对象,但这比较依赖具体页面结构,通常只在写定制脚本时才会去搞。普通场景下,直接下载就是最快路径。
这种场景常见于:用户在网页里本地预览上传视频、你用 Canvas + MediaRecorder 录制的 WebM 视频、以及某些通过 API 一次性返回完整视频二进制并在本地播放的轻量站点。
4.3 流媒体分片视频的抓流与合并
遇到流媒体分片视频,直接下载 blob 没有意义——因为那个 blob 指向的 MediaSource 是一个“活的水池”,数据流是被不断灌进去又放出来的,你 fetch 不到完整影片。正確的做法是去拿它背后的索引文件和分片列表。
操作路径通常是这样:
先在Network面板里过滤m3u8或mpd,刷新页面开始播放,找到那个索引文件请求。如果找不到,也可以搜m3u8、mpd、playlist这几个关键词。拿到索引 URL 后,直接在浏览器地址栏访问,可以看到里面是一长串分片地址列表。注意,很多平台给的 m3u8 地址带动态签名,有效期可能只有几分钟到几小时,所以拿到地址之后要尽快处理。
拿到索引地址之后,最简单省事的工具是 ffmpeg。
# 直接拉取整个流并合并,视频音频一起处理 ffmpeg -i "https://example.com/path/index.m3u8" -c copy -bsf:a aac_adtstoasc output.mp4-c copy表示不重新编码,直接拷贝原始数据流,速度很快且没有画质损失。-bsf:a aac_adtstoasc是为了处理 TS 里面的 AAC 音频流在封装到 MP4 时的一个格式转换问题,不加这条,部分视频放出来会没有声音或者音画不同步。
如果平台做了更严的防护,比如要求每个分片都有独立的临时签名,那就需要脚本监听Network面板,把分片请求的 URL 逐个保存下来,再用拼接工具合并。开源工具N_m3u8DL-RE是这方面比较靠谱的选择,它支持自定义请求头、token、多线程下载、自动合并,比 ffmpeg 在需要精细控制请求参数时更好用。
我自己在实际操作中遇到最多的情况是 M3U8 里有 KEY 加密或 parse 特殊字段的情况。此时 ffmpeg 会自动在解密时尝试读取 key 文件的地址,如果 key 地址也是动态的,就需要手动把 key 文件先保存到本地,再通过修改 m3u8 的#EXT-X-KEY那一行来指向本地文件。
# 先把 key 文件拿下来,然后改 m3u8 里的 key 地址为本地路径 ffmpeg -i "local_playlist.m3u8" -c copy output.mp4这种做法在抓自己账号有权访问的流时是可行的,但要注意:如果平台使用了 DRM(数字版权管理)加密,比如 Widevine 的pssh字段,这条路就走不通了。那意味着视频内容经过了硬件级加密,不是靠抓包能解的。任何声称能“解密 DRM”的工具,都不该碰。
4.4 “把blob url转file”的实现细节
“把 blob url 转 file”是很多人搜过的需求,也是处理上传业务时常见的逆向场景。它本质上是把blob:地址对应的二进制数据还原成一个File对象,方便塞进FormData提交给后端。
// 将 blob url 转为 File 对象 async function blobUrlToFile(blobUrl, filename) { const response = await fetch(blobUrl); const blob = await response.blob(); // File 继承自 Blob,可以直接构造 return new File([blob], filename, { type: blob.type }); } // 使用示例 const file = await blobUrlToFile('blob:https://example.com/xxxx', 'cover.jpg'); const formData = new FormData(); formData.append('file', file); await fetch('/api/upload', { method: 'POST', body: formData });这里有个容易被忽略的小坑:File构造函数里的filename不能随便写。如果后端按扩展名判断文件类型,而你的文件名没有后缀,后端可能识别失败。比较好的习惯是根据blob.type推断出扩展名,赋给filename。比如blob.type是image/jpeg,文件名就写成.jpg后缀。
另外,如果页面里做了 CSP(内容安全策略)限制,fetch(blobUrl)有可能被拦截。这时可以尝试直接用XHR请求,或者直接把Blob对象本身从页面上下文里取出来——这需要你熟悉页面的 JS 运行环境,一般配合油猴脚本才能搞定。
5. 常见报错与排查技巧实录
5.1 blob链接为什么换个页面就打不开
这是最经典的一个问题:你在视频页里把 blob 地址复制下来,粘到新标签页或者发给朋友,打开是白屏。原因前面已经说了,blob 是浏览器的本地临时地址,作用域仅限于创建它的标签页和对应的这些二进制对象。换页面等于换了餐厅,座位号当然失效。
知道了这个原理,你在调试时就该明白:别指望收藏 blob URL,也别问别人要 blob URL。正确做法是抓它背后的 m3u8 或分片地址,而不是 blob 地址本身。很多新手在这里绕了很久,以为是自己没抓到真正的资源,其实方向就错了。
5.2 合并完只有画面没有声音
用 ffmpeg 下载 m3u8 合并成 MP4 之后发现没声音,大概率是两个原因。
第一种原因是 TS 分片里音频是 AAC 格式,直接封装 MP4 需要aac_adtstoasc滤镜转换。解决方法是把命令改成-bsf:a aac_adtstoasc。第二种原因是播放器把视频轨和音频轨分成了两个不同的分片列表,你在 m3u8 里看到的是纯视频轨,音频轨是另一个 m3u8(文件名通常带audio字样)。解决方法是先把两条流都下载下来,再用 ffmpeg 合并:
# 先分别下载视频轨和音频轨 ffmpeg -i "video_only.m3u8" -c copy video.mp4 ffmpeg -i "audio_only.m3u8" -c copy audio.m4a # 再合并 ffmpeg -i video.mp4 -i audio.m4a -c copy merged.mp4这里建议先单独检查一下两个文件是否能正常播放,再合并,能更快定位问题出在哪一路。
5.3 播几秒黑屏:动态签名的锅
有些平台的 m3u8 或者分片 URL 里带一个很短的签名过期时间,比如 30 秒。你慢悠悠地打开 m3u8、看一会儿、再发起下载,结果发现用工具下载时前面几个分片下下来了,后面的全 403。
这种情况解决思路是:把整个下载过程自动化,拿到 m3u8 后立刻开始下载,中间不要停顿。如果你用的是 N_m3u8DL-RE,可以设置较高的并发数,它在分片过期前把任务拉完。如果手动用 ffmpeg,遇到 403 就立刻重新抓一次新签名,重新执行命令。多试几次你会发现,整个抓取过程最好限制在签名有效期内完成。
5.4 遇到DRM加密怎么办
有的视频服务用的是 Widevine 或 FairPlay 这类 DRM 方案,播放器请求的是加密分片,媒体数据在解密和渲染之前都是加密状态。你即使把分片全部下载下来,没有授权也就无法解码。这种情况下,网页端的 blob 只是表象,真正的版权保护在更底层。
面对 DRM,我的建议很简单:放弃。不是技术上的完全不可行,而是这个方向涉及的东西牵扯到法律风险和技术复杂度,普通人碰它百害无利。如果你的需求是合法的——比如你是某个平台的付费用户,想把视频存到本地缓存看——正常的平台都会提供官方离线缓存功能,请用官方渠道。如果是想录屏或二次分发,那本身就是违规甚至违法行为。
这个边界,我觉得每个做技术的人都得有。技术可以用来解决正当需求:比如自己拍的视频、自己网站的播放器调试、研究流媒体协议;但不要去拿它破解别人的版权保护机制。这不仅是对别人劳动成果的尊重,也是在保护你自己。
写在最后:一点经验之谈
这套东西我刚开始折腾的时候也绕了不少弯路。看到 blob 链接就以为是无解,后来弄明白 MSE 的原理才反应过来:blob 只是个影子,真正藏在水面下的,是那条“索引文件 + 分片 + 鉴权”的流媒体链路。现在我拿到一个网页视频,第一件事不是去复制地址,而是打开开发者工具的 Media 面板和 Network 面板,先判断走的是纯内存 blob 还是 MSE 流媒体,然后再决定下一步。这个判断做对了,后面 80% 的时间都能省下来。
最后再给你一个实操小技巧:处理这类问题时务必保持开发者工具全程开着,尤其是Network面板的Preserve log选项,一定要勾上。因为很多平台的播放器在首屏就请求了 m3u8,之后就不再请求,你不开 Preserve log,刷新后很容易眼睁睁看着那个关键请求消失,还得来回折腾几遍。这个细节我踩过太多次,希望你别再踩一遍。