Netflix 视频抓取细节

详细记录下用 chrome 抓取视频的教程

准备工作

widevine-l3-decryptor

安装插件 widevine-l3-decryptor,github 地址

1
git clone https://github.com/p00f/widevine-l3-decryptor

chrome 插件打开开发者模式选择“加载已解压的扩展程序”,定位到项目地址即可。

tampermonkey 和 Netflix - subtitle downloader

安装插件 tampermonkey chrome 商店地址,并安装用户脚本 Netflix - subtitle downloader greasyfork 地址

改写脚本370行左右,向 console 输出 manifest 信息

1
2
3
4
5
6
7
8
9
10
11
12
// hijack JSON.parse and JSON.stringify functions
((parse, stringify) => {
JSON.parse = function (text) {
const data = parse(text);
if (data && data.result && data.result.timedtexttracks && data.result.movieId) {
window.dispatchEvent(new CustomEvent('netflix_sub_downloader_data', {detail: data.result}));
console.log('manifest:')
console.log(data)
console.log(stringify(data))
}
return data;
};

netflix-1080p

安装插件 netflix-1080p,github 地址

chrome 插件打开开发者模式选择“加载已解压的扩展程序”,定位到项目地址/src目录即可。

这是我正在改写的一个版本,其中 cadmium-playercore 是根据最新的官方版本改写的,主要是为了让 chrome 也能获取 1080p 视频。其他的功能还不能正常使用。

地址的获取

Netflix 的视频、音频、和字幕是分开的,只有视频流是采用了 DRM 加密的,音频和字幕未加密。所有的相关信息都在一个 manifest 中可以找到,这个 manifest 可以通过前文提到的魔改后的 Netflix - subtitle downloader 脚本输出到控制台。脚本会以两种形式输出,一种是直接在控制台里面输出 object,另一种是输出 JSON 字符串,方便 copy 到其他文本编辑器查看。推荐使用 vscode 来查看。

视频地址的解析

视频信息在 manifest 的 result.video_tracks.streams 属性下。Netflix 会准备很多视频流,用来根据网络情况实现码率的自动调整。我们抓取的时候只需要抓取码率最高的一条即可,一般是最后一条。

但是,用官方原版的网页播放,在试用 chrome 的时候它最高只会推 720p 的流,所以我们需要用到 netflix-1080p 这个插件。这个插件的原理就是手动改写 cadmium-playercore-xxx.xxx.xxx.js 文件里面的 profiles。格式化之后大约在 112510 行左右

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
viewableId: n,
// profiles: g,
profiles: [
"playready-h264mpl30-dash",
"playready-h264mpl31-dash",
"playready-h264mpl40-dash",

"playready-h264hpl30-dash",
"playready-h264hpl31-dash",
"playready-h264hpl40-dash",

// "ddplus-5.1hq-dash",
// "ddplus-Atmos-dash",
"heaac-2-dash",
"heaac-2hq-dash",
"heaac-5.1-dash",

"simplesdh",
"nflx-cmisc",
"BIF240",
"BIF320"
],

其中 playready-h264mpl40-dashplayready-h264hpl40-dash 是 1080p 对应的 profile。

在 20298 行左右有比较全的 profile 可选信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
k.vM = "playready-h264mpl30-dash";
k.YF = "playready-h264mpl31-dash";
k.mV = "playready-h264mpl40-dash";
k.C6a = "playready-h264hpl22-dash";
k.jV = "playready-h264hpl30-dash";
k.kV = "playready-h264hpl31-dash";
k.lV = "playready-h264hpl40-dash";
k.o8 = "hevc-main-L30-dash-cenc";
k.p8 = "hevc-main-L31-dash-cenc";
k.q8 = "hevc-main-L40-dash-cenc";
k.r8 = "hevc-main-L41-dash-cenc";
k.s8 = "hevc-main-L50-dash-cenc";
k.t8 = "hevc-main-L51-dash-cenc";
k.k8 = "hevc-main10-L30-dash-cenc";
k.l8 = "hevc-main10-L31-dash-cenc";
k.m8 = "hevc-main10-L40-dash-cenc";
k.n8 = "hevc-main10-L41-dash-cenc";
k.$F = "hevc-main10-L50-dash-cenc";
k.aG = "hevc-main10-L51-dash-cenc";
k.tV = "hevc-main10-L30-dash-cenc-prk";
k.vV = "hevc-main10-L31-dash-cenc-prk";
k.xV = "hevc-main10-L40-dash-cenc-prk";
k.zV = "hevc-main10-L41-dash-cenc-prk";
k.uV = "hevc-main10-L30-dash-cenc-prk-do";
k.wV = "hevc-main10-L31-dash-cenc-prk-do";
k.yV = "hevc-main10-L40-dash-cenc-prk-do";
k.AV = "hevc-main10-L41-dash-cenc-prk-do";
k.BV = "hevc-main10-L50-dash-cenc-prk-do";
k.CV = "hevc-main10-L51-dash-cenc-prk-do";
k.MXb = "hevc-main-L30-L31-dash-cenc-tl";
k.NXb = "hevc-main-L31-L40-dash-cenc-tl";
k.OXb = "hevc-main-L40-L41-dash-cenc-tl";
k.PXb = "hevc-main-L50-L51-dash-cenc-tl";
k.IXb = "hevc-main10-L30-L31-dash-cenc-tl";
k.JXb = "hevc-main10-L31-L40-dash-cenc-tl";
k.KXb = "hevc-main10-L40-L41-dash-cenc-tl";
k.LXb = "hevc-main10-L50-L51-dash-cenc-tl";
k.mM = "hevc-dv5-main10-L30-dash-cenc-prk";
k.nM = "hevc-dv5-main10-L31-dash-cenc-prk";
k.OF = "hevc-dv5-main10-L40-dash-cenc-prk";
k.oM = "hevc-dv5-main10-L41-dash-cenc-prk";
k.pM = "hevc-dv5-main10-L50-dash-cenc-prk";
k.UU = "hevc-dv5-main10-L51-dash-cenc-prk";
k.d8 = "hevc-hdr-main10-L30-dash-cenc";
k.e8 = "hevc-hdr-main10-L31-dash-cenc";
k.f8 = "hevc-hdr-main10-L40-dash-cenc";
k.g8 = "hevc-hdr-main10-L41-dash-cenc";
k.h8 = "hevc-hdr-main10-L50-dash-cenc";
k.i8 = "hevc-hdr-main10-L51-dash-cenc";
k.yM = "hevc-hdr-main10-L30-dash-cenc-prk";
k.zM = "hevc-hdr-main10-L31-dash-cenc-prk";
k.ZF = "hevc-hdr-main10-L40-dash-cenc-prk";
k.AM = "hevc-hdr-main10-L41-dash-cenc-prk";
k.oV = "hevc-hdr-main10-L50-dash-cenc-prk";
k.pV = "hevc-hdr-main10-L51-dash-cenc-prk";
k.xDa = "vp9-profile0-L21-dash-cenc";
k.oN = "vp9-profile0-L30-dash-cenc";
k.pN = "vp9-profile0-L31-dash-cenc";
k.qN = "vp9-profile0-L40-dash-cenc";
k.sDa = "vp9-profile2-L30-dash-cenc-prk";
k.tDa = "vp9-profile2-L31-dash-cenc-prk";
k.uDa = "vp9-profile2-L40-dash-cenc-prk";
k.vDa = "vp9-profile2-L50-dash-cenc-prk";
k.wDa = "vp9-profile2-L51-dash-cenc-prk";
k.Nsa = "av1-main-L20-dash-cbcs-prk";
k.Osa = "av1-main-L21-dash-cbcs-prk";
k.Psa = "av1-main-L30-dash-cbcs-prk";
k.Qsa = "av1-main-L31-dash-cbcs-prk";
k.Rsa = "av1-main-L40-dash-cbcs-prk";
k.Ssa = "av1-main-L41-dash-cbcs-prk";
k.Tsa = "av1-main-L50-dash-cbcs-prk";
k.Usa = "av1-main-L51-dash-cbcs-prk";

但不是每一种都能通过这种方式在 chrome 上获取, 大部分随意组合的 profile 都会报错。

解密密匙的获取

安装了 widevine-l3-decryptor 之后,只要是 chrome 上在放 Netflix 的视频,控制台就会输出

1
WidevineDecryptor: Found key: 709487bfa80678a4fd32fd26cbf42b60 (KID=00000000056167750000000000000000)

由于 Netflix 准备了很多码率和分辨率的视频,大部分时候它们不一定公用同一个密匙,这个时候会输出很多条。不用一个个的试,manifest 里面已经包含了对应的 KID 信息

1
2
3
4
5
6
7
"trackType": "PRIMARY",
"content_profile": "playready-h264mpl40-dash",
"bitrate": 6490,
"peakBitrate": 0,
"dimensionsCount": 2,
"dimensionsLabel": "2D",
"drmHeaderId": "0000000005e4d1810000000000000000",

你只需要把 KID 和 drmHeaderId 对上号即可。

视频流的下载

每一个视频流都有个 urls 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"urls": [
{
"url": "https://ipv4-c035-hkg001-ix.1.oca.nflxvideo.net/?o=xxx",
"cdn_id": 63365
},
{
"url": "https://ipv4-c033-hkg001-ix.1.oca.nflxvideo.net/?o=xxx",
"cdn_id": 63363
},
{
"url": "https://ipv4-c104-sea001-ix.1.oca.nflxvideo.net/?o=xxx",
"cdn_id": 18327
}
]

包含三个下载地址,这个三个地址下的东西是一样的,只是对应不同的 cdn 而已,任选一个即可。可以直接用 chrome 下载。下载完成后重命名下载的文件, <任意名称>.mp4 即可。

视频流解密

我用的 bento4 套件,命令如下

1
mp4decrypt --key 2:cac5936c9fcb86756c0a4f2eeb1b5642 sweet.home.s01e01.mp4 sweet.home.s01e01.d.mp4

其中 --key <id>:<key>,的 id 是视频流在 mp4 文件中的 id,可以用 mediainfo 查看。一般不是 1 就是 2。key就是 widevine-l3-decryptor 输出的解密密匙。后面接 <加密视频文件名>.mp4 <解密视频文件名>.mp4

音频的获取

Netflix 提供了 AAC 2.0 / 5.0 和 DDP 2.0 / 5.0 / Atmos(部分) 的音频文件,而且一般还准备了多国语言。对应的 profile 有

1
2
3
4
5
6
7
8
9
10
11
12
c.qV = "heaac-2-dash";
c.I6a = "heaac-5.1-dash";
c.rV = "heaac-2hq-dash";
c.WW = "xheaac-dash";
c.SU = "ddplus-2.0-dash";
c.i7 = "ddplus-5.1-dash";
c.TU = "ddplus-atmos-dash";
c.j8 = "playready-heaac-2-dash";
c.K6a = "heaac-2-dash-enc";
c.P4a = "ddplus-2.0-dash-enc";
c.Q4a = "ddplus-5.1-dash-enc";
c.J6a = "playready-heaac-2-dash-enc";

AAC 的音频 chrome 能抓地址且能正常播放, DDP 的音频 chrome 也能抓,但会导致播放报错。一般我们是抓 DDP 5.1 640 kb/s 码率的那一条音频。如果你只想在移动设备上播放,可以只抓 AAC 2.0 heaac-2hq-dash

下载方式和视频流一样,文件名直接改 .mp4 后缀即可。

Atmos 音频的探测与获取 (need help)

我现在有两个问题不能很好解决

  • 部分视频有 Atmos 音频,如何探测? 我知道可以用Windows store 的 app 看,我希望直接在 chrome 上就能探测。
  • DDP5.1 和 DDP Atmos 的抓取我是通过改 profile 实现抓取的,但是在抓取 DDP 音频之后 chrome 会报错,影响播放,每次该来该去很麻烦,有没有一劳永逸的解决方案?

字幕的获取

字幕直接用 Netflix - subtitle downloader 能很方便的抓取 webvtt 字幕,然后用 subtitle edit 转成 srt 字幕即可。

另外你如果希望抓图片字幕,在 manifest 里面找 nflx-cmisc 格式的下载,文件名改 .zip,然后想办法转蓝光格式的图片字幕或者 vobsub,本人暂未研究。

封装

用 mkvtoolnix,一般是封装最高码率的 1080p 视频,640 k 码率的 DDP 5.1 (有 Atmos 封 Atmos),全部的 srt 字幕。具体规范以后讨论。