2654 字
13 分钟
F12 抓包发现「视频伪装成图片」,以及 .ts 文件和 BOM 到底是什么

用 Quetta 逛某个站的时候,遇到一件怪事。

页面上没有弹出下载按钮。 但视频确实在播。

于是我做了所有技术人都会做的那件事:F12 → Network。

然后我看到了一个很有意思的东西。


一、Network 面板里滚动的全是「图片」#

flowchart TD A["打开页面播放视频"] --> B["F12 → Network 面板"] B --> C{"观察请求"} C --> D["大量 .jpg 请求
随播放进度不断新增"] 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):

Terminal window
# 读原始字节
$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 字节,且每个包的第一个字节都是 0x47

0x47(十进制 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-8EF BB BF
UTF-16 LEFF FE
UTF-16 BEFE FF
UTF-32 LEFF FE 00 00

有了它,一个程序扫一眼开头三个字节就知道后面该怎么解码字节流,而不需要靠猜。

广义:BOM = 文件魔数(magic number / 文件签名)#

在文件类型识别里,这个概念被泛化了 —— 靠开头几个字节判断这是什么格式,这在技术圈也被叫作 BOM。

常见的一些:

类型开头字节(十六进制)
JPEGFF D8 FF
PNG89 50 4E 47 0D 0A 1A 0A
GIF47 49 46 38(就是 ASCII 的 “GIF8”)
PDF25 50 44 46(“%PDF”)
ZIP50 4B 03 04(“PK\x03\x04”)
MPEG-TS47(每 188 字节一个)

JPEG 的 FF D8 FF 里:

  • FF D8 是 SOI(Start Of Image) 标记
  • 后面的 FF 是下一个标记的第一个字节。JPEG 的结构是”一串标记”,每个标记都以 FF 开头、FF 结尾。典型的 JPEG 紧接着是 APP0/JFIF 段:
FF D8 ← SOI
FF 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 E0 APP0 等),结果读到的是 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 直接把这个口子开了 —— 我装了个广告拦截扩展,规则和电脑上完全一致。


七、总结这一个小结#

  1. 观察到矛盾:视频在播,但 Network 里全是 .jpg,没有视频请求。
  2. 推断:视频被切成 MPEG-TS 分片,以”图片”的名义走 CDN —— 白嫖图片托管的带宽和缓存。
  3. .ts 是 MPEG-2 Transport Stream:188 字节定长包,每包开头一个 0x47 同步字节,为抗错和可复用而设计;HLS 分片流媒体的标准容器。
  4. BOM 狭义是字节序标记(UTF-8 是 EF BB BF),广义就是文件魔数 —— 靠开头几字节认类型。JPEG 的 SOI 是 FF D8 FF。
  5. 这招骗的是”识别”不是”解码”:浏览器看 Content-Type,图片解码器看段结构;真正被绕过的是站点自己的防下载规则。
  6. Quetta 值在原生支持 Chrome 扩展,官网版和 Play 版功能有差异。

整件事最值得记的一点,其实跟下载无关:

遇到”看到的现象”和”知道的事实”矛盾时,F12 是最短的路。 屏幕上的一切都是别人写好的结果,而 Network 面板里的是真实发生的请求。

这次我只是觉得”没弹出下载按钮有点奇怪”,多按了一次 F12。

F12 抓包发现「视频伪装成图片」,以及 .ts 文件和 BOM 到底是什么
https://ycxdev.com/posts/quetta-f12-network-ts-and-bom/
作者
YCX
发布于
2026-10-14
许可协议
CC BY-NC-SA 4.0