0:绪

在上个月,我通过爬取 youtube 视频,提取 mp3,制作了一个基于云函数的搜索音乐网站。后来,我抱着解决 ifvod 广告的想法,阅读了 ifvod 的网站源码,并且通过修改 js 代码,成功删除了 ifvod 的广告。受上述两个过程的启发,我想,通过提取 ifvod 上的视频 m3u8 链接来制作一个基于云函数的视频搜索网站。

1:什么是 m3u8

m3u8 文件是一种文本文件,其格式如下:

1
2
3
4
5
6
7
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:18
#EXTINF:10.677,
media_0.ts?dnvodendtime=1613939123&dnvodhash=LMkLRmsUktNRN4h-OrHuElRGhV71N32vV4nueFAQJXw%3D&dnvodCustomParameter=0_2003%3Ae3%3Af2f%3A5700%3A8417%3Aeeca%3A9a8e%3A1d35_1&lb=3c19337f6084516e1517490ee377c4b9&us=1&vv=4e5dcf3b2432cd03dd525e0489d72248&pub=1613766323672
#EXTINF:14.515,
media_1.ts?dnvodendtime=1613939123&dnvodhash=LMkLRmsUktNRN4h-OrHuElRGhV71N32vV4nueFAQJXw%3D&dnvodCustomParameter=0_2003%3Ae3%3Af2f%3A5700%3A8417%3Aeeca%3A9a8e%3A1d35_1&lb=3c19337f6084516e1517490ee377c4b9&us=1&vv=4e5dcf3b2432cd03dd525e0489d72248&pub=1613766323672

在这个文本中,记录了视频的源地址。。我们常见的视频格式比如 mp4,mp4 文件直接储存了视频的全部信息。可直接使用播放器播放。而 m3u 又称流媒体,这种方法,是将原来完整的视频,切割成几秒到十几秒不等的,ts 格式的视频片段,将每个片段作为一个视频内容储存在服务器,而 m3u8 文件储存的就是每个片段的 url 地址。html5 播放器通过检索 m3u8 文件的内容,逐个下载视频片段,并播放。如今有许多软件支持,根据 m3u8 文件下载或播放视频。也就是说,如果我们有了 m3u8 文件的 url,就可以在网站外下载和播放视频了。

2:构造 json 请求链接

通过分析 ifvod 网站返回的数据,可以发现。在一个请求返回的 json 数据中,包含了我们想要的视频地址,请求是

https://m8.ifvod.tv/api/video/play?cinema=1&id=xcql1mznpF0&region=DE&device=1&ispath=true&usersign=1&vv=ad4bee064a3af6c8a41cda35111966fd&pub=1613905710379

分析这个请求 url,可以发现,其中 id 是视频的 id,直接出现在视频播放的地址栏中,vv 和 pub 是签名和公钥,有了这三个数据,我们就可以根据视频的 id 来获取视频的 m3u8 地址了。
尝试请求这个可以看到返回的数据是:

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
{
"ret": 200,
"data": {
"code": 0,
"msg": "",
"info": [
{
"id": 625223,
"vl": null,
"add_date": null,
"post_Year": null,
"channel": null,
"videoType": null,
"contxt": null,
"updateweekly": null,
"commentNumber": 0,
"isVideoFavrited": false,
"taxis": 0,
"flvPathList": [
{
"isHls": false,
"result": "https://s1-a1.dnvodcdn.me/fast/423B3C813853-70244.mp4",
"validator": null,
"type": 0,
"link": "http://ppt.ifvod.tv/c/c?position%3dPI%26d%3d1169%26r%3d7",
"broker": null,
"backup": null,
"rtmp": null,
"needSign": false
},
{
"isHls": false,
"result": "https://s1-a1.dnvodcdn.me/fast/A0B67356-87371.mp4",
"validator": null,
"type": 0,
"link": "http://ppt.ifvod.tv/c/c?position%3dPI%26d%3d1230%26r%3d7",
"broker": null,
"backup": null,
"rtmp": null,
"needSign": false
},
{
"isHls": false,
"result": "https://s1-a1.dnvodcdn.me/fast/F709860823-88837.mp4",
"validator": null,
"type": 0,
"link": "http://ppt.ifvod.tv/c/c?position%3dPI%26i%3d575%26r%3d7",
"broker": null,
"backup": null,
"rtmp": null,
"needSign": false
},
{
"isHls": true,
"result": "https://hss8.dnvodcdn.me/ppot/_definst_/mp4:s8/gvod/xj-fllyssx-480p-03656717C.mp4/chunklist.m3u8?dnvodendtime=1614078950&dnvodhash=c7O8AgFsLq6N1O8ysAfNyp6SlvOLrVniVvvW2PEz8zU=&dnvodCustomParameter=0_2003%3ae3%3af2f%3a5700%3a8417%3aeeca%3a9a8e%3a1d35_1&lb=1f4a12a7923561331e23eee32ad4be8a&us=1",
"validator": null,
"type": 0,
"link": null,
"broker": null,
"backup": "",
"rtmp": "https://hss8.dnvodcdn.me/ppot/_definst_/mp4:s8/gvod/xj-fllyssx-480p-03656717C.mp4/chunklist.m3u8?dnvodendtime=1614078950&dnvodhash=c7O8AgFsLq6N1O8ysAfNyp6SlvOLrVniVvvW2PEz8zU=&dnvodCustomParameter=0_2003%3ae3%3af2f%3a5700%3a8417%3aeeca%3a9a8e%3a1d35_1&lb=1f4a12a7923561331e23eee32ad4be8a&us=1",
"needSign": false
}
],
"title": "480P",
"imgPath": null,
"unlockGold": 0,
"playRecordURL": "//counter.timegate.vip/api/Counter/PlusOne?key=AddHitToMovie&id=27212",
"favrateNumber": 0,
"filterGold": 0,
"key": "puamPfe8zf7",
"cid": null,
"commentStatus": 0,
"shareCount": 0,
"pinfenRate": 0.0,
"renqiRate": 0.0,
"customData": null,
"isGuestHasMore": false,
"link": null,
"isVIPHasMore": false,
"pauseData": [
{
"isHls": false,
"result": "https://ppt.ifvod.tv/upload/video/202010020923422328455s.jpg",
"validator": null,
"type": 1,
"link": "http://ppt.ifvod.tv/c/c?position%3dPZ%26d%3d1174%26r%3d7",
"broker": null,
"backup": null,
"rtmp": null,
"needSign": false
},
{
"isHls": false,
"result": "https://ppt.ifvod.tv/upload/video/202012140947004731731s.jpg",
"validator": null,
"type": 1,
"link": "http://ppt.ifvod.tv/c/c?position%3dPZ%26d%3d1173%26r%3d7",
"broker": null,
"backup": null,
"rtmp": null,
"needSign": false
},
{
"isHls": false,
"result": "https://ppt.ifvod.tv/upload/video/202011032259305913218s.jpg",
"validator": null,
"type": 1,
"link": "http://ppt.ifvod.tv/c/c?position%3dPZ%26d%3d2145%26r%3d7",
"broker": null,
"backup": null,
"rtmp": null,
"needSign": false
},
{
"isHls": false,
"result": "https://ppt.ifvod.tv/upload/video/202102191325542507558s.jpg",
"validator": null,
"type": 1,
"link": "http://ppt.ifvod.tv/c/c?position%3dPZ%26d%3d2373%26r%3d7",
"broker": null,
"backup": null,
"rtmp": null,
"needSign": false
},
{
"isHls": false,
"result": "https://ppt.ifvod.tv/upload/video/202102120906530682725s.jpg",
"validator": null,
"type": 1,
"link": "http://ppt.ifvod.tv/c/c?position%3dPZ%26d%3d1179%26r%3d7",
"broker": null,
"backup": null,
"rtmp": null,
"needSign": false
}
],
"startData": [],
"guestSeriesList": null,
"vipSeriesList": null,
"commentList": null,
"languageList": null,
"downloadList": null,
"videoServer": { "status": 0, "info": "", "isMp4Available": false },
"copyRightInfo": null,
"publisher": null,
"previewFormat": "",
"uniqueKey": "27212_625221",
"starImg": null,
"starID": 0,
"starBgColor": null,
"age": null
}
]
},
"msg": "",
"isSpecialArea": 0
}

其中 flvPathList 键放的存放着广告和视频的地址。
现在分析,vv 和 pub 参数。
通常,为了保证请求链接不被篡改,并且,请求连接不能长期有效,都会给请求连接添加一个签名,以保证它随时变化且不被篡改。pub 是 publickey 的简写,也就是公钥,相对应的,他还有私钥 privatekey。这两个值,通常都是由服务器指定的。也就是说,他们都是可变的。。分析 js 源码,可以发现,pubkey 是由 js 生成的当前毫秒时间戳。。私钥是通过 pubkey 经过一段运算,在 7 个私钥列表中选择的一个。这种算法是可笑的,因为通常公钥应该通过私钥来生成。私钥更不可能储存在本地。
然后是 vv 参数,vv 参数是上述请求的 url 参数加上公钥和私钥,再通过 md5 编码,返回的 32 位字符串。
我使用 python 书写了一个通过原始 url,来获取签名和添加公钥后的 url 的类。

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
import time
from urllib import parse
import hashlib
import requests

class Signurl:
def __init__(self, url: str, publicKey: int):
self.publicKey = publicKey
self.privateKey = ["version001", "vers1on001", "vers1on00i",
"bersion001", "vcrsion001", "versi0n001", "versio_001", "version0o1"]
self.oldUrl = url
self.newUrl = self._getSignUrl()

def _getSignUrl(self):
paramsStr = self._getParamsStr()
r = str(self.publicKey) + "&" + paramsStr + "&" + self._getPrivateKey()
hl = hashlib.md5()
hl.update(r.encode())
newUrl = self.oldUrl+"&vv="+hl.hexdigest()+"&pub="+str(self.publicKey)
return newUrl

def _getParamsStr(self) -> str:
paramsStr = ""
params = parse.parse_qs(parse.urlparse(self.oldUrl).query)
for key in params:
paramsStr += key + "=" + params[key][0]
paramsStr += "&"
return paramsStr[0:-1].lower()

def _getPrivateKey(self):
return self.privateKey[self.publicKey % len(self.privateKey)]

其中 publickey 可以通过以下代码生成,publicKey = int(time.time()*1000)

3:提取 m3u8 链接

在获取了,json 数据后,提取出 m3u8 的链接:

https://hss8.dnvodcdn.me/ppot/definst/mp4:s8/gvod/xj-fllyssx-480p-03656717C.mp4/chunklist.m3u8?dnvodendtime=1614078521&dnvodhash=0KnVagh-3RIHR_0fE_qVTS3X_t1ogc9iRTbuUYK5GYI=&dnvodCustomParameter=0_2003%3ae3%3af2f%3a5700%3a8417%3aeeca%3a9a8e%3a1d35_1&lb=1f4a12a7923561331e23eee32ad4be8a&us=1

直接请求这个链接是,无法获取到 m3u8 文件的,因为我们还需要通过它重新构造请求,同样是使用 2 章节所用的 Signurl 类,对此 url 签名。会增加 vv 和 pub 参数在上述 url 末尾。

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

class Video:
def __init__(self, id: str) -> None:
self.publicKey = int(time.time()*1000)
self.id = id
self.dataUrl = "https://m8.ifvod.tv/api/video/play?cinema=1&id=" + \
id+"&region=DE&device=1&usersign=1"
self.playUrl = self._getM3u8Url()

def _getM3u8Url(self) -> str:
m3u8url = self._getData()
if m3u8url:
return Signurl(m3u8url, self.publicKey).newUrl
else:
return ""

def _getData(self):
reqUrl = Signurl(self.dataUrl, self.publicKey).newUrl
rep = requests.get(reqUrl)
if rep.status_code == 200:
return rep.json()['data']['info'][0]['flvPathList'][-1]['result']
else:
return ""

print(Video("WdS12xLwXh6").playUrl)

将上述两端代码放到同一个 py 文件中执行,就可以获取 m3u8 的播放链接了。

4:结语

我尝试将此链接通过在线 m3u8 播放器进行播放,遇到的问题是,如果这个视频播放了一段时间,那么在网页播放器中,通过 m3u8 只能播放到 ifvod 网页上的位置。通过分析,我推测,ifvod 对 m3u8 链接的请求做了防盗链设置,也就是说,在其他网站上播放这个 m3u8 视频,是无法播放的,之所以出现部分片段能够播放,是因为 ifvod 播放过的片段会经过 cdn,在 cdn 上是没有防盗链的。。也就是说,想通过在线 m3u8 播放器和通过 serverless 服务搭建一个播放器的想法是不能实现的。
为此,我花了许多时间来尝试和搜索破解方法。其中,iframe 中添加 referrerpolicy,网页中添加 meta 标签并修改位 no-referrer 均无法使视频播放。
唯一的办法是使用反向代理,我在我一台服务器上,通过 nginx 反向代理,实现了这一过程。但是这个方法代价太大了,首先,serverless 是有免费额度的。而服务器需要自己掏腰包。通过代理,流量会经过服务器收费。相当不划算。没有实现的价值。
如果不通过自己的网站,比如使用 chrome 浏览器插件,Play HLS M3u8,可以直接在本地播放 m3u 视频,基于此,可以自己做一个插件,通过搜索,在本地播放,因为浏览器插件是可以修改 referer 的。

评论