文章目录
  1. 1. 获取优酷的视频链接
    1. 1.1. 获取sid和token
    2. 1.2. 获取oip
    3. 1.3. 获取fileid
    4. 1.4. 获取K和ts
    5. 1.5. 获取st
    6. 1.6. 获取ep
    7. 1.7. 构造视频链接
  2. 2. 播放分块视频

使用网页看视频是非常不爽的,尤其是长视频,因为使用网页看视频就意味着你无法在不开多个浏览器窗口的条件下边看视频边浏览网页,而且最最重要的是有广告啊。如果获取到视频的地址就可以直接使用播放器播放了。

获取优酷的视频链接

对于优酷播放页面为

1
http://v.youku.com/v_show/id_XNzYwODQ3MTg0.html

的视频对应的mp4的真实地址为(这个视频被优酷切分成四段)

1
2
3
4
http://k.youku.com/player/getFlvPath/sid/3409483123616127f4226_00/st/mp4/fileid/030008040053F98A94E2631468DEFED15CC475-F07D-7762-1FA8-EB95B046B8B8?K=414595e722b677a42411ec29&hd=1&myp=0&ts=377&ypp=0&ctype=12&ev=1&token=7168&oip=2130706433&ep=cSaUE02FVc8C5iXejj8bYHrmdnUJXP4J9h%2bFidJmALshS562kU%2fYw%2biySPxDEv8RBldwZZ%2fwrqOUbkcRYYZDrhwQ2EmuO%2frhiIHr5dghzZVyZh0wAMWlvFSZQjb5
http://k.youku.com/player/getFlvPath/sid/3409483123616127f4226_00/st/mp4/fileid/030008040153F98A94E2631468DEFED15CC475-F07D-7762-1FA8-EB95B046B8B8?K=d4371bb9a461be672829d1b5&hd=1&myp=0&ts=401&ypp=0&ctype=12&ev=1&token=7168&oip=2130706433&ep=cSaUE02FVc8C5iXejj8bYHrmdnUJXP4J9h%2bFidJmALohS562kU%2fYw%2biySPxDEv8RBldwZZ%2fwrqOUbkcRYYZDrhwQ2EmuO%2frhiIHr5dghzZVyZh0wAMWlvFSZQjb5
http://k.youku.com/player/getFlvPath/sid/3409483123616127f4226_00/st/mp4/fileid/030008040253F98A94E2631468DEFED15CC475-F07D-7762-1FA8-EB95B046B8B8?K=f0197761ab939438261ddeef&hd=1&myp=0&ts=333&ypp=0&ctype=12&ev=1&token=7168&oip=2130706433&ep=cSaUE02FVc8C5iXejj8bYHrmdnUJXP4J9h%2bFidJmALkhS562kU%2fYw%2biySPxDEv8RBldwZZ%2fwrqOUbkcRYYZDrhwQ2EmuO%2frhiIHr5dghzZVyZh0wAMWlvFSZQjb5
http://k.youku.com/player/getFlvPath/sid/3409483123616127f4226_00/st/mp4/fileid/030008040353F98A94E2631468DEFED15CC475-F07D-7762-1FA8-EB95B046B8B8?K=9ee3d46ac50a40262411ec29&hd=1&myp=0&ts=311&ypp=0&ctype=12&ev=1&token=7168&oip=2130706433&ep=cSaUE02FVc8C5iXejj8bYHrmdnUJXP4J9h%2bFidJmALghS562kU%2fYw%2biySPxDEv8RBldwZZ%2fwrqOUbkcRYYZDrhwQ2EmuO%2frhiIHr5dghzZVyZh0wAMWlvFSZQjb5

要构造出视频地址需要知道的信息有

  • sid
  • st(文件类型如:mp4)
  • fileid(文件ID如:030008040353F98A94E2631468DEFED15CC475-F07D-7762-1FA8-EB95B046B8B8)
  • K
  • hd(总是1)
  • myp(总是0)
  • ts(时长,单位为秒)
  • ypp(总是0)
  • ctype(总是12)
  • ev(总是1)
  • token
  • oip(请求者的IP地址)
  • ep

要获取这些需要向下面这两个地址请求数据

1
2
http://v.youku.com/player/getPlayList/VideoIDS/{VideoId}
http://v.youku.com/player/getPlayList/VideoIDS/{VideoId}/Pf/4/ctype/12/ev/1

其中的{VideoId}可以从播放页的地址获取,比如文章开头的播放页面对应的VideoId是XNzYwODQ3MTg0
请求成功时,这两个地址会返回JSON格式的数据。将地址1得到的JSON对象称为data_obj而将地址2得到的称为data2_obj。
这两个地址返回的数据大部分相同。并且需要关心的信息只有data_obj.data[0]和data2_obj.data[0]。之所以要两个地址都请求是因为地址2返回的信息含有data2_obj.data[0].ip和data2_obj.data[0].ep而地址1没有data_obj.data[0].ip和data_obj.data[0].ep。
演示代码中JSON解析使用jsonxx,base64编解码使用我自己写的这个

获取sid和token

获取sid和token需要通过data2_obj.data[0].ep来获取,假设ep的值为NAXQRwkWJbrY0vbA8+JxVdbwuxE71wrKXhc=,这是一个经过base64编码的字符串。首先要对其进行base64解码,解码后的数据不是可读的字符串,需要调用youku_f函数对解码后的数据进行处理。处理后可以得到字符串3409483123616127f4226_7168,下划线前面字符串为sid,下划线之后的为token。

1
2
3
4
5
6
7
8
9
10
const jsonxx::String& ep_str = data2_obj.get<jsonxx::Array>("data").get<jsonxx::Object>(0).get<jsonxx::String>("ep");
std::string ep_str_decode;
base64_decode(ep_str.begin(), ep_str.end(), std::back_inserter(ep_str_decode));
std::string sid_and_token = youku_f("becaf9be", ep_str_decode);
std::size_t underline_pos = sid_and_token.find('_');
if (underline_pos == std::string::npos) {
return {};
}
std::string sid(sid_and_token.begin(), sid_and_token.begin() + underline_pos);
std::string token(sid_and_token.begin() + underline_pos + 1, sid_and_token.end());

其中youku_f函数的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
std::string youku_f(const std::string& str1, const std::string& str2)
{
std::array<std::size_t, 256> table;
std::iota(table.begin(), table.end(), 0);
std::size_t t = 0;
for (std::size_t i = 0; i < table.size(); ++i) {
t = ((t + table[i]) + str1[i % str1.size()]) % table.size();
std::swap(table[i], table[t]);
}
std::string result;
for (std::size_t i = 0, j = 0, k = 0; i < str2.size(); ++i) {
k = (k + 1) % table.size();
j = (j + table[k]) % table.size();
std::swap(table[j], table[k]);
result.push_back(str2[i] ^ table[(table[j] + table[k]) % table.size()]);
}
return result;
}

获取oip

data2_obj.data[0].ip就是oip,他表示请求者的IP(比如IP地址为127.0.0.1,其十六进制为0x7F000001,那么oip就是2130706433),优酷的服务器似乎不会检查这个参数,可以用任意的随机数代替。

获取fileid

获取fileid需要先获得data_obj.data[0].seed这是一个随机种子,假设seed为2368,然后构造一个长度为68的表。
{abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/\:._-1234567890}
使用随机种子2368将此表打乱得到
{f.EuXV6Cr2xiB\OQpDgSJo/zsm9Nq-RGP0hW5lHLAK8Z:kFwbMUva_d71YnI4etTycj3}
然后通过data_obj.data[0].streamtype获取支持的流类型,streamtype是一个数组,通过他可以知道有多少种视频流格式,本例中streamtype为[“flv”,”mp4″,”hd2″,”hd3″]。以mp4为例(其他类型相同),获取data_obj.data[0].streamfileids.mp4,假设其为

1
33*67*33*33*33*42*33*60*33*33*36*67*46*26*42*40*26*60*2*9*6*67*56*60*6*42*17*2*46*2*17*56*36*7*7*60*55*36*29*46*33*55*17*29*55*55*6*9*29*56*46*40*42*29*2*12*26*36*12*33*60*6*12*42*12*42*

可以看出这是一个通过字符*分隔的数值表,数值都落在区间[0,68)中。使用这些数值索引打乱后的表。

1
2
3
4
5
6
33 -> 0
67 -> 3
33 -> 0

12 -> B
42 -> 8

可以得到fileid为030008040353F98A94E2631468DEFED15CC475-F07D-7762-1FA8-EB95B046B8B8

1
2
3
const auto& stream_file_ids_obj = data_obj.get<jsonxx::Array>("data").get<jsonxx::Object>(0).get<jsonxx::Object>("streamfileids");
auto seed = data_obj.get<jsonxx::Array>("data").get<jsonxx::Object>(0).get<jsonxx::Number>("seed");
std::string file_id = get_file_id(stream_file_ids_obj.get<jsonxx::String>(stream_type), static_cast<unsigned int>(seed));

其中get_file_id函数的定义为

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
std::string get_file_id(const std::string& file_ids, unsigned int seed)
{
std::vector<char> table = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '/', '\\', ':', '.', '_', '-', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0' };
std::size_t table_len = table.size();
std::vector<char> shuffled_table;
for (std::size_t i = 0; i < table_len; ++i) {
seed = (seed * 211 + 30031) % 65536;
std::size_t index = seed * table.size() / 65536;
char ch = table[index];
shuffled_table.push_back(ch);
table.erase(table.begin() + index);
}
std::vector<std::size_t> id_vec;
std::string id_str;
for (const auto& ch : file_ids) {
if (std::isdigit(ch)) {
id_str.push_back(ch);
}
else if (ch == '*') {
if (!id_str.empty()) {
id_vec.push_back(std::stoul(id_str));
id_str.clear();
}
}
else {
throw std::invalid_argument("invalid argument: file_id.");
}
}
if (!id_str.empty()) {
if (!id_str.empty()) {
id_vec.push_back(std::stoul(id_str));
id_str.clear();
}
}
std::string result;
for (std::size_t index : id_vec) {
result.push_back(shuffled_table.at(index));
}
return result;
}

获取K和ts

data_obj.data[0].segs.mp4是一个数组,数组中的一个对象对应一个视频文件分块。在本例中它是这样子的。

data2_obj.data[0].segs.mp4[n].K有可能为-1,所以应总是从data_obj中获取。

1
2
3
4
5
6
"mp4":[
{"no":"0","size":"26458308","seconds":377,"k":"414595e722b677a42411ec29","k2":"1679ea53afb1b5f3a"},
{"no":"1","size":"26348880","seconds":401,"k":"d4371bb9a461be672829d1b5","k2":"134f0b17cac09f4aa"},
{"no":"2","size":"25437389","seconds":333,"k":"f0197761ab939438261ddeef","k2":"1cd3ed919b8f40d9b"},
{"no":"3","size":"19944147","seconds":311,"k":"9ee3d46ac50a40262411ec29","k2":"1ed1d51c208cd7db2"}
]

其中K就是k,而ts就是seconds。
对于前面的获取到的fileid需根据no来进行相应的修改,这4个分块文件的fileid分别对应
030008040053F98A94E2631468DEFED15CC475-F07D-7762-1FA8-EB95B046B8B8
030008040153F98A94E2631468DEFED15CC475-F07D-7762-1FA8-EB95B046B8B8
030008040253F98A94E2631468DEFED15CC475-F07D-7762-1FA8-EB95B046B8B8
030008040353F98A94E2631468DEFED15CC475-F07D-7762-1FA8-EB95B046B8B8
由第8位和第9位组成的两位十六进制数00~FF与这里的no对应。

获取st

st是文件类型,与streamtype是多对一的映射关系。目前已知的对应关系有

1
2
3
4
5
6
7
8
std::map<std::string, std::string> stream_type_to_file_type = {
{ "flv", "flv" },
{ "mp4", "mp4" },
{ "hd2", "flv" },
{ "3gphd", "mp4" },
{ "3gp", "flv" },
{ "hd3", "flv" }
};

获取ep

视频地址中的ep参数并不是data2_obj.data[0].ep的值。获得ep的方法为使用下划线连接sid、fileid和token。之后使用youku_f函数对字符串进行处理,再使用base64对ep进行编码,由于ep是要作为url的query value来使用的,所以还需要对base64编码过的ep进行Percent Encode(也叫UrlEncode),但是考虑到base64编码的特殊性(仅需要对+/=这3个符号进行编码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
std::string ep = youku_f("bf7e5f01", sid + '_' + file_id + '_' + token);
std::string ep_base64;
base64_encode(ep.begin(), ep.end(), std::back_inserter(ep_base64));
std::string ep_percent_encode;
// base64编码过的字符串 percent encode仅需处理+/=
for (auto ch : ep_base64) {
if (ch == '+') {
ep_percent_encode += "%2b";
}
else if (ch == '/') {
ep_percent_encode += "%2f";
}
else if (ch == '='){
ep_percent_encode += "%3d";
}
else {
ep_percent_encode.push_back(ch);
}
}

构造视频链接

在得到所有参数后就可以构造视频链接了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::string url("http://k.youku.com/player/getFlvPath/sid/");
url += sid;
url += "_00/st/";
auto file_type_iter = stream_type_to_file_type.find(stream_type);
if (file_type_iter == stream_type_to_file_type.end()) {
// 错误处理
}
url += file_type_iter->second;
url += "/fileid/";
url += file_id;
url += "?K=";
url += k;
url += "&hd=1&myp=0&ts=";
url += seconds;
url += "&ypp=0&ctype=12&ev=1&token=";
url += token;
url += "&oip=";
url += ip;
url += "&ep=";
url += ep_percent_encode;

播放分块视频

视频文件被分成了多块了就需要有一种机制来引导播放器连续的播放这些文件。m3u8就是一种现成的方案,m3u8文件格式很简单,很容易通过程序自动生成。
在Windows下我知道的支持m3u8的播放器有

  1. VLC media player(跨平台的开源播放器)
    VLC是一款很强大的播放器但是个人感觉还是有这些不足:
  • TS文件类型的m3u8不显示时间
  • 没有类似迅雷影音迷你模式的功能
  1. 迅雷影音
    迅雷影音从5.1.8开始支持m3u8,但试了下还是存在一些问题:
  • 每次启动后播放过一次m3u8后再播放任意的m3u8文件会失败,需要重启才能再播
  • 非TS文件类型的m3u8不支持连播
文章目录
  1. 1. 获取优酷的视频链接
    1. 1.1. 获取sid和token
    2. 1.2. 获取oip
    3. 1.3. 获取fileid
    4. 1.4. 获取K和ts
    5. 1.5. 获取st
    6. 1.6. 获取ep
    7. 1.7. 构造视频链接
  2. 2. 播放分块视频