音泉也算是我的老朋友了。早在高中的时候,我就尝试从他的网页和APP端下载广播,还以此为契机用黑箱式的理解探索了浏览器F12和Fiddler抓包的用法。当时的发现是,音泉的媒体传输机制相当笨蛋,当然也为我提供了便利:客户端实际上是通过固定的URL直接下载(当然也可以分片下载)完整的音频文件,也不需要任何用户验证,这意味能想办法抓包获取URL就算成功。

就算技术上再守旧,音泉大概也不会一直这样下去。果不其然,到了2020年(不记得确切时间)前后,我又想去下载广播的时候才发现,他换上了一套比较成熟的流媒体机制了,当然这也意味着自动抓取下载的难度增大。直至本文写作的时间(2022年11月)为止,下载音泉广播的策略还不需要调整。这里,我们会从抓包内容出发,简单梳理排除干扰获取所需内容的思路。

我们假定读者对HTTP请求的组成和功能有基本的了解。当然不了解也不是不行。

抓包过程

我们的目标是用尽可能低的成本获取想要的信息。在正式开始之前,我们需要有这样的意识:

只要是客户端出现的东西,就一定有他的来源。 ——我

只要网页播放器放出了音视频,我们就一定能找到媒体文件;如果我们找到的是加密过的版本,那浏览器就一定在某个时候得到了密钥;如果浏览器请求了114514个片段,那他一定事先通过请求知道了片段的数量和编号。大体上来说,我们在浏览器上看到的东西归根结底只有两个源头:

  1. 加载页面时,HTML源码提供的静态信息。
  2. 加载JS时动态获取的信息片段,可能以纯文本、JSON、多媒体、JavaScript之类的形式出现。

处理静态信息是最方便的,在网页源码上就一览无余,也不难构造HTTP请求做进一步处理。动态信息是万恶之源,但如果不想深究的话也未尝没有捷径。使用F12和Fiddler的主要目的也是处理动态信息。

这里以孤独摇滚广播为例,首先打开抓包工具确认状态正常,然后点击播放。播放之前可以清空已有的请求,因为我们并不关心那些内容。

广播主界面

不出意外,我们会在F12的网络选项里看到蜂拥而至的HTTP请求,看到差不多得了就可以停止记录分析情况。(Tips:可以将抓包记录保存成文件)

F12主界面

我们发现这里有些值得注意的地方:

  • playlist.m3u8,看起来就像是记录了所有片段的汇总表。
  • chunklist.m3u8,看起来和上一个差不多。
  • key.m3u8key,看起来是经过了加密。
  • media_{数字}.aac,看起来就是我们需要的音频。

我们需要的东西估计就在那里,点进去一个一个检查请求头和内容,祈求没有看不懂的部分便是。从playlist开始,我们发现他的URL是 “https://onsen-ma3phlsvod.sslcs.cdngc.net/onsen-ma3pvod/_definst_/202211/bocchi-radio221116m4pb-09.mp4/playlist.m3u8” ,里面出现了原始文件的名字,不错。再看看内容:

1
2
3
4
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=125585,CODECS="mp4a.40.2"
chunklist.m3u8

感觉意思不大,只是指向了下一个记录元信息的文件,此外记录了带宽和编码格式。接着看chunklist,我们发现他的URL格式也类似,这是个好消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="https://onsen-ma3phlsvod.sslcs.cdngc.net/onsen-ma3pvod/_definst_/mp4:202211/bocchi-radio221116m4pb-09.mp4/key.m3u8key"
#EXTINF:10.0,
media_0.aac
#EXTINF:10.0,
media_1.aac
#EXTINF:10.0,
media_2.aac
#EXTINF:10.0,
media_3.aac
#EXTINF:10.0,
(...)
media_392.aac
#EXTINF:9.265,
media_393.aac
#EXT-X-ENDLIST

我们关心的内容都在这个文件里得到了解答:

  • 完整的文件分成了393个片段,每段长度约10秒。
  • 每个片段都经过了加密,方式是AES-128,虽然不确定具体采用了哪种实现方式。
  • 加密密钥URI是"…/key.m3u8key"。

带着这些已知信息,我们发现密钥的Content-Length是16,和加密方式的128位对上了;媒体文件看不出任何文本内容,而媒体文件的容器标识本应有一些和ASCII兼容的部分,所以可以推测整个文件都使用AES做了一次加密。如果抓包的时候使用的是Fiddler,这时我们就可以把响应内容保存成文件验证猜想。最后的结论是,我们可以用AES-128-CBC模式解密,偏移量和密钥都是key.m3u8key的内容。

至此,信息获取的准备工作基本完成,接下来的任务就是尝试自动下载所有的aac文件,然后把它们拼接起来。在这一步,我们仍然会用到抓包的信息,只不过目的性更强一些。下面介绍两种下载方法,其中第一种比较简单,对无料广播适用,会员限定广播不确定,因为还没验证;第二种比较麻烦,但是原理上不受限制。

使用ffmpeg下载广播

这种方法只需要用到ffmpeg这一个命令行工具。

考虑到m3u8是一种通用的流媒体格式,强大的ffmpeg照理说应该能直接处理Playlist,上网搜了一下发现也确实如此,这样唯一需要顾忌的地方只有请求头了。检查一下Playlist,Chunklist和各片段的请求头,会发现它们是完全一样的。以本人使用的浏览器为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Host: onsen-ma3phlsvod.sslcs.cdngc.net
Origin: https://www.onsen.ag
Referer: https://www.onsen.ag/
sec-ch-ua: "Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36

我们发现这里没有用到Cookie,是好事。起关键作用的可能是Origin,Referer和User-Agent,不过保险起见我们还是想办法都塞给ffmpeg好了。我们以Powershell为例,写个简单的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 注意是Playlist
$url = "https://onsen-ma3phlsvod.sslcs.cdngc.net/onsen-ma3pvod/_definst_/202211/bocchi-radio221116m4pb-09.mp4/playlist.m3u8"
$url -match '[0-9]{6}/(.*?)\.mp4/.*' # 识别原始文件名
$file = $Matches.1 + '.aac' # 和片段格式保持一致
ffmpeg -headers "Connection: keep-alive`r
sec-ch-ua: \`"Google Chrome\`";v=\`"107\`", \`"Chromium\`";v=\`"107\`", \`"Not=A?Brand\`";v=\`"24\`"`r
sec-ch-ua-mobile: ?0`r
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36`r
sec-ch-ua-platform: \`"Windows\`"`r
Accept: */*`r
Origin: https://www.onsen.ag`r
Sec-Fetch-Site: cross-site`r
Sec-Fetch-Mode: cors`r
Sec-Fetch-Dest: empty`r
Referer: https://www.onsen.ag/`r
Accept-Encoding: gzip, deflate, br`r
Accept-Language: zh-CN,zh;q=0.9" -i $url $file

值得注意的是,Powershell的转义格式比较独特,常用的反斜杠都要改成反引号;而且由于ffmpeg的参数解析问题,请求头里出现的双引号都要加上反斜杠转义。至于Linux用户,想必无需我多费口舌也能自己找到具体写法。在测试时,可以加上-v trace的参数,让ffmpeg打印运行过程排查问题。

由于ffmpeg帮我们处理了所有的细节,只需等待一会就能拿到成品,相当轻松。尽管获取m3u8的过程不够自动化,但除非想要大批量下载,一般需要也不值得笔者再花时间处理自动抓取网页信息的部分。

备用方法

在糟糕的情况下,请求头可能附带一些不容易获取的信息,这意味着构造请求的代价变得难以承受,但我不过是想下载广播而已。在这种情况下,可以考虑结合Fiddler的自定义脚本,在抓取到相关请求的时候把文件保存下来,按照上面的分析方法解密、拼接。这实际上也是我最早想到的方法。

Fiddler的脚本并不基于JavaScript,但是语法大差不差,对于简单功能的实现来说没有必要区分。打开Script Editor后,我们希望在请求捕捉结束之后保存HTTP的响应内容,所以在类属性加入自定义选项,再到OnBeforeResponse静态方法里加点私货:

1
2
3
4
5
6
7
8
9
10
11
// 加到类属性处
public static RulesOption("Auto save for ONSEN")
var m_AutosaveONSEN: boolean = false;

// 加到OnBeforeResponse处
if (m_AutosaveONSEN && oSession.host.StartsWith("onsen")
&& !oSession.url.EndsWith(':443')) {
var uri = oSession.fullUrl.Split('/');
var fname = uri[uri.length - 1];
oSession.SaveResponseBody("路径" + fname);
}

文件命名规则可以自行处理。保存自定义脚本之后,我们会在选项卡看到刚才加入的部分:

自定义选项

点击勾选,开始捕捉之后就能在我们指定的路径里找到m3u8、key和aac三类文件。给aac解密的部分很好操作,略去不提。随便打开一个拼接后的aac,我们发现普通的播放器也能放出来,接下来的问题就是把aac拼接起来。

尽管这个问题看来不难,但是让我困扰了一会,因为这些片段文件似乎并不是标准格式,用常规的拼接操作会出现问题。思来想去,最后感觉不如试着把Playlist喂给ffmpeg,告诉他这是从流媒体来的,结果这样成功了:

1
ffmpeg -i playlist.txt output.aac # 其实和上面是一样的

不过在运行之前要去掉Chunklist里面提示加密方式的部分,否则他会尝试去请求密钥。可以看到,这种方式周折颇多,不过原理上来说基本上是万用的。