用 Quetta 逛某个站的时候,遇到一件怪事。
页面上没有弹出下载按钮。 但视频确实在播。
于是我做了所有技术人都会做的那件事:F12 → Network。
然后我看到了一个很有意思的东西。
一、Network 面板里滚动的全是「图片」
随播放进度不断新增"] C --> E["没有任何 .mp4 / .m3u8 请求"] D --> F["视频明明在动
但网络里全是图片?"] E --> G["视频是逐段加载的
不是一次性一个文件"] F --> H["推断:服务端把视频分片
伪装成了图片"]
现象描述一下:播放过程中,Network 面板持续刷出大量 .jpg 请求,一个接一个;而列表里找不到任何 .mp4 或 .m3u8。
但视频确实在流畅播放。
这个矛盾只有一个解释:
视频不是以”视频”的形式传输的,而是被切成了很多小段,每一段以”图片”的名义加载。
再一深想就不对了 —— 一段视频帧,大小通常几十 KB 到几百 KB。CDN 上”图片”是极其便宜的资源类型(很多 CDN 对图片免流量费、有图片优化/尺寸变体、有独立的缓存规则)。把视频伪装成图片走,等于白嫖了图片托管的带宽和缓存。
这个思路和推理,问了下 AI,印证了:确实是这么干的。
二、动手:改后缀 + 补 BOM
知道原理之后操作就很简单了。
下载 F12 里抓到的那个”图片”,然后:
原始文件:segment-000123.ts (内容其实是 MPEG-TS 分片)改名 →:segment-000123.jpg补头 →:文件开头插入 FF D8 FF结果 →:能被当成 JPEG 处理Windows 上的具体做法(PowerShell):
# 读原始字节$ts = [IO.File]::ReadAllBytes("segment-000123.ts")
# JPEG 的 SOI(Start Of Image)标记:FF D8 FF$bom = [byte[]](0xFF, 0xD8, 0xFF)
# 前面拼上 BOM$jpg = New-Object byte[] ($bom.Length + $ts.Length)[Array]::Copy($bom, 0, $jpg, 0, $bom.Length)[Array]::Copy($ts, 0, $jpg, $bom.Length, $ts.Length)
[IO.File]::WriteAllBytes("segment-000123.jpg", $jpg)改名 + 改文件头,搞定。
三、.ts 到底是个什么文件
.ts 这个后缀有点误导人 —— 它不是 TypeScript。
在这个上下文里,它指的是 MPEG-2 Transport Stream(MPEG-2 传送流),也叫 M2TS。这是直播和流媒体领域的标准容器格式。
它的核心特征:固定长度包 + 同步字节
┌──────────┬──────────────────────────────────────┬──────────┐│ 0x47 │ 184 字节负载 │ 0x47 ││ 同步字节 │ (音频/视频/PES/PSI 数据) │ 同步字节 │└──────────┴──────────────────────────────────────┴──────────┘ 一个包 = 188 字节,且每个包的第一个字节都是 0x470x47(十进制 71)叫做同步字节(sync byte),它是 MPEG-TS 最显著的特征。
设计成”每 188 字节重复一个同步字节”是有明确目的的:
- 抗错误 / 快速重同步。 广播信号丢了一部分,接收端不用从头解析,只要扫到下一个
0x47就能重新对齐 —— 这是”打一个同步标记”,和硬盘扇区的同步区、CD 的同步信号是同一个思路。 - 恒定码率、可交织。 多路音频和视频可以交织在同一条流里,接收端按包头解复用。
- 不依赖文件可寻址。 广播流不能 seek,所以设计上就是”从头顺序读”。
流媒体里它在哪儿
HLS(HTTP Live Streaming)的经典做法就是把一段视频切成 10 秒左右的小块,每块一个 .ts 文件,播放器按顺序拉:
playlist.m3u8 ├── seg001.ts 0s - 10s ├── seg002.ts 10s - 20s ├── seg003.ts 20s - 30s └── ...(DASH 协议现在更多用 fMP4,但 HLS + .ts 依然是兼容性最广的方案。)
所以”播放过程中不断产生新请求”这个特征,本质上就是分片流媒体。 它跟”伪装成图片”没有必然关系 —— 分片是正常行为,伪装是额外加的一层。
四、BOM 是什么:先说狭义,再说广义
狭义:BOM = Byte Order Mark
BOM 最原始的含义是字节序标记,用来告诉程序”这个文本文件是用哪种编码写的”:
| 编码 | 文件开头的 BOM |
|---|---|
| UTF-8 | EF BB BF |
| UTF-16 LE | FF FE |
| UTF-16 BE | FE FF |
| UTF-32 LE | FF FE 00 00 |
有了它,一个程序扫一眼开头三个字节就知道后面该怎么解码字节流,而不需要靠猜。
广义:BOM = 文件魔数(magic number / 文件签名)
在文件类型识别里,这个概念被泛化了 —— 靠开头几个字节判断这是什么格式,这在技术圈也被叫作 BOM。
常见的一些:
| 类型 | 开头字节(十六进制) |
|---|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 0D 0A 1A 0A |
| GIF | 47 49 46 38(就是 ASCII 的 “GIF8”) |
25 50 44 46(“%PDF”) | |
| ZIP | 50 4B 03 04(“PK\x03\x04”) |
| MPEG-TS | 47(每 188 字节一个) |
JPEG 的 FF D8 FF 里:
FF D8是 SOI(Start Of Image) 标记- 后面的
FF是下一个标记的第一个字节。JPEG 的结构是”一串标记”,每个标记都以FF开头、FF结尾。典型的 JPEG 紧接着是 APP0/JFIF 段:
FF D8 ← SOIFF E0 00 10 4A 46 49 46 00 ← APP0 / JFIF 标识01 01 00 ← 版本、密度单位00 01 00 01 ← X/Y 密度00 00 ← 缩略图(无)... ← 后续帧数据FF D9 ← EOI (End Of Image)五、这招究竟骗过了谁 —— 一个需要诚实拆开的部分
这里有个特别容易想错的地方,我第一次也搞反了。
浏览器处理资源,看的是 HTTP 响应头
Content-Type,不是文件后缀,也不是文件头。服务端返回
Content-Type: image/jpeg,浏览器就把它当图片渲染,哪怕内容其实是 TS 流。
所以在浏览器里,改后缀 + 改文件头本身不会改变任何行为 —— 决定行为的是服务器返回的头。
那为什么这招有用?真正生效的地方是:
| 环节 | 谁在看什么 | 这招有没有用 |
|---|---|---|
| 浏览器渲染资源 | Content-Type 响应头 | ❌ 没用 |
| 站点的防下载逻辑 | URL 路径的后缀 | ✅ 有用 |
| CDN / 静态服务的类型分类 | 后缀 + 配置规则 | ✅ 有用 |
| 本地工具的文件类型识别 | 文件头魔数 | ✅ 有用 |
| 真正的图片解码器 | JPEG 段结构 | ❌ 解不出画面 |
WARNING最后一行是最重要的:加上
FF D8 FF之后,它骗过的是”识别环节”,不是”解码环节”。真正的图片解码器在读完 SOI 之后,会去找下一个合法标记(
FF C0帧头、FF E0APP0 等),结果读到的是47开头的 TS 包 —— 解析直接失败。所以它不是一个”能显示的图片”,它只是一个”被认为是图片的文件”。
那它到底解决了什么问题?通常是这两种:
① 绕开站点自己的防下载检查。 很多站点的”禁止下载”逻辑很朴素:拦截 URL 里出现 /video/、/media/、.mp4 的请求,或者对已知播放器域名做限制。你改成 .jpg,它的规则就不匹配了。
② 让文件能通过”按扩展名分类”的流程。 比如上传到某个只接受图片的图床/网盘、或者粘贴进某个只认图片的地方。
CAUTION顺便说清楚一件事:改文件头并不会让你”免费把视频存到图床上”。如果你上传的是完整的视频内容,图床不会因为前三个字节是 JPEG 就把它压缩、转码、或者长期保留 —— 多数图床会做内容检测。要真想在图床上传,那叫滥用,而且大部分服务条款明确禁止。
那 Quetta 的下载按钮为什么没弹
Quetta 的下载器是识别到了才弹按钮的(它支持 MP4 / WebM / M3U8 / HLS 这些格式)。伪装成图片的分片不在它的识别范围内,所以没有弹出。这不是 bug,是它没认出来。
顺便说一个我在用的时候观察到的版本差异:
Quetta 在 官网(quetta.net)提供的安装包 里可以下载 YouTube 视频,Google Play 商店版本不行。
同款应用、不同分发渠道、功能不同 —— 这在移动端很常见(渠道审核、功能分渠道灰度、区域策略都可能造成差异)。我没有查到官方对这个差异的明确说明,所以这里只记录我实测到的现象。官网版拿得到 Play 版拿不到的功能,Play 版用户可以考虑从官网获取。
六、Quetta 本身值在哪
抛开这段小插曲,Quetta 这个浏览器我是真在用:
| 能力 | 说明 |
|---|---|
| 内置广告/追踪拦截 | 不需要装扩展(支持页面的部分除外) |
| 视频下载器 | 支持 MP4 / WebM / M3U8 / HLS |
| 原生支持 Chrome 扩展 | 这是它最大的差异化 —— 移动端浏览器里能直接用桌面扩展 |
| 隐私优先 | 无痕、广告/追踪拦截 |
支持 Chrome 扩展这一条是杀手级。移动端浏览器普遍不支持扩展,导致很多”网页端能做、手机上做不了”的事做不了。Quetta 直接把这个口子开了 —— 我装了个广告拦截扩展,规则和电脑上完全一致。
七、总结这一个小结
- 观察到矛盾:视频在播,但 Network 里全是
.jpg,没有视频请求。 - 推断:视频被切成 MPEG-TS 分片,以”图片”的名义走 CDN —— 白嫖图片托管的带宽和缓存。
.ts是 MPEG-2 Transport Stream:188 字节定长包,每包开头一个0x47同步字节,为抗错和可复用而设计;HLS 分片流媒体的标准容器。- BOM 狭义是字节序标记(UTF-8 是
EF BB BF),广义就是文件魔数 —— 靠开头几字节认类型。JPEG 的 SOI 是FF D8 FF。 - 这招骗的是”识别”不是”解码”:浏览器看
Content-Type,图片解码器看段结构;真正被绕过的是站点自己的防下载规则。 - Quetta 值在原生支持 Chrome 扩展,官网版和 Play 版功能有差异。
整件事最值得记的一点,其实跟下载无关:
遇到”看到的现象”和”知道的事实”矛盾时,F12 是最短的路。 屏幕上的一切都是别人写好的结果,而 Network 面板里的是真实发生的请求。
这次我只是觉得”没弹出下载按钮有点奇怪”,多按了一次 F12。