浏览器能解码哪些图像和视频格式,会泄露引擎、版本乃至 GPU。解析 Accept、canPlayType 与 decodingInfo 三种读取方式。
各家浏览器能解码哪些图像和视频格式,至今仍不统一。JPEG XL 就是最明显的例子:Safari 从 Safari 17 起就能解码它,而 Chromium 和 Firefox 在是否正式支持上反复摇摆。只要两个引擎对“能解码什么”的回答不一样,“你支持哪些格式?”这个问题的答案就能反过来识别提问的浏览器。本文讨论的正是这一指纹面:图像与媒体的解码能力。它可以通过三种方式读取,其中一种能深入到你的硬件。
要点速览
- 格式支持在三个层面被读取: HTTP
Accept请求头(无需 JavaScript)、canPlayType()与isTypeSupported()这类能力查询,以及MediaCapabilities.decodingInfo()——它额外返回smooth和powerEfficient。 - 单独使用时信号较粗。 格式支持与浏览器家族和版本高度相关,基本只是把 User-Agent 又说了一遍,别指望靠它锁定某个人。
- 它有两种真正的用途: 一是与声称的 UA 做矛盾校验,二是提供 UA 里没有的硬件解码信息。
powerEfficient是最有意思的字段。 它表明某个编解码器是否由硬件解码,反映的是 GPU、驱动和操作系统——无需 WebGL,也无需权限弹窗。- 它的衰减曲线与其他信号不同。 格式支持随浏览器、系统和驱动更新而变化,与 canvas 或字体信号的变化节奏并不相同。
读取方式一:Accept 请求头
第一种读取发生在任何脚本运行之前。请求页面或图片时携带的 Accept 头会列出浏览器接受的图像格式。较新的 Chromium 会发送类似 image/avif,image/webp,image/apng,image/svg+xml,image/* 的内容,其他引擎发送的列表则更短或顺序不同。这就是服务器在第一个请求上看到的东西,纯 HTTP 指纹识别一文介绍了整组请求头如何在无 JavaScript 的情况下被读取。格式列表能快速暴露引擎和大致版本,而你为屏蔽脚本安装的任何工具都碰不到它。
它随时间变化的原因很简单:浏览器发布一个解码器,就会加入相应的标记。MDN 的图像文件类型参考列出了各种格式及其 MIME 类型,Can I Use 的 AVIF和 JPEG XL 页面则展示了支持情况有多不均衡。AVIF 如今几乎已普及;JPEG XL 仍然分裂,正因为各家尚未统一,它才成了一个有区分度的信号。
读取方式二:JavaScript 能力查询
脚本可以直接询问,而不必从请求头推断。HTMLMediaElement.canPlayType() 接收 MIME 类型和可选的编解码器字符串,返回 ""、"maybe" 或 "probably"。MediaSource.isTypeSupported() 对流媒体管线做同样的事,图像则可以通过加载每种格式的极小 data URI 样本、看能否解码来探测。
const probes = [
'video/mp4; codecs="avc1.42E01E"',
'video/mp4; codecs="hev1.1.6.L93.B0"',
'video/webm; codecs="vp9"',
'video/mp4; codecs="av01.0.05M.08"',
'audio/ogg; codecs="opus"',
];
const v = document.createElement('video');
const vector = probes.map((p) => v.canPlayType(p) || 'no').join('|');
得到的是一个短字符串,其组合随引擎、版本和操作系统而异,因为 HEVC 等专有编解码器往往取决于平台的媒体框架,而不只是浏览器本身。注意 "maybe" 与 "probably" 的区别本身就是不同实现之间的差异,而不只是“行”与“不行”。
读取方式三:MediaCapabilities 与硬件信息
第三种读取才是本文值得单独成篇的原因。navigator.mediaCapabilities.decodingInfo() 接收完整配置——编解码器、分辨率、码率、帧率——并返回三个布尔值:supported、smooth 和 powerEfficient。
const info = await navigator.mediaCapabilities.decodingInfo({
type: 'file',
video: {
contentType: 'video/mp4; codecs="hev1.1.6.L93.B0"',
width: 3840, height: 2160, bitrate: 20_000_000, framerate: 60,
},
});
// { supported: true, smooth: true, powerEfficient: true }
powerEfficient 实际上反映的是能否硬件解码。GPU 能用硬件解码 4K HEVC 或 AV1 的机器,与只能回退到软件解码的机器,给出的答案不同,而答案取决于 GPU 代际、驱动和操作系统媒体栈。对一组编解码器、尺寸和帧率的组合逐一探测,就能得到一个带有硬件特征的小向量,这是 User-Agent 不携带的信息。它相当于 WebGPU 指纹识别中 GPU 信号在媒体 API 里的对应物,也能用于指纹与硬件不匹配一文所说的交叉校验,而且既不需要权限,也不需要 canvas。
实话实说:熵有多少
单独看并不多。两个人在同一操作系统上运行同一版本的 Chrome,Accept 列表和 canPlayType 向量几乎一致,所以这一指纹面的软件部分基本只是重述浏览器家族和版本。直说吧:格式支持单独拿出来,并不是一个强标识符。
它在两种情况下变得有用:
- 矛盾校验。 声称是 Safari 却无法解码 HEVC,或声称是新版 Chrome 却缺少 AV1,都说不通。检测系统会把它与无头浏览器检测(缺失编解码器是经典破绽)配合使用,也用于更广泛的一致性检查。
- 硬件信息。 在一组探测上得到的
powerEfficient和smooth,能把浏览器版本相同但 GPU 不同的机器区分开,UA 做不到这一点。
有一个相邻的话题需要区分:WebRTC 指纹识别读取的是浏览器为实时通话声明的编解码器,而本文讨论的是对存储媒体、流媒体和图像的解码能力。
稳定性:不同的衰减曲线
canvas 和字体信号会在你安装字体或更新图形栈时改变。格式支持则在浏览器发布或移除解码器、操作系统更新媒体框架、或驱动更新开启或关闭硬件解码时变化。因此这个信号呈阶跃式变化,与更新周期绑定。对想维持稳定标识的追踪方来说,这好坏参半:两次更新之间很可靠,更新后却会突然改变。对防御追踪的一方来说,更新确实会改变这部分特征,但由于大量用户几乎同时更新,你多半只是换进了另一群人里,而不会因此变得显眼。
你能做什么
这里没有开关。格式支持是功能性特性,伪造它会让视频和图片出问题。切实可行的做法:
- 使用主流且保持更新的浏览器,让你的答案与一大群人一致,而不是一种罕见组合。
- 如有条件,优先使用能限制媒体能力 API 暴露信息的抗指纹模式;它们会牺牲一部分功能,换来更小的暴露面。
- 对只伪造 UA 而不匹配解码特征的工具保持警惕——这种不匹配恰恰是这类检测最擅长抓的。
想看看你的浏览器实际报告了什么、与其他信号是否一致,可以运行指纹检测。
常见问题
编解码器支持是强指纹吗? 不是。它信号较粗,与浏览器版本紧密相关,基本只是重述 User-Agent。它的价值在于矛盾校验和硬件解码信息。
屏蔽 JavaScript 能阻止它吗?
只能阻止 JavaScript 那几种读取。Accept 头无论如何都会在第一个请求上发送。
为什么 powerEfficient 比 supported 更有揭示性?
supported 跟随浏览器构建,而 powerEfficient 跟随你的 GPU、驱动和操作系统。


