本文從 ActionScript 反編譯角度,深度還原優酷播放器核心邏輯:getPlayList API 回應結構、Seed 混淆演算法、Key1/Key2 異或解密、分段 K 值處理與最終 URL 組裝。適合需自行維護解析器的開發者參考。
$baseUrl = 'https://v.youku.com/player/getPlayList/VideoIDS/';
$videoId = $_GET['vid'] ?? 'XMjY0ODE1MDA0'; // 測試用 ID
$ch = curl_init($baseUrl . $videoId);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true, // 需要 Header 解析 Set-Cookie 等
CURLOPT_REFERER => 'https://v.youku.com/v_show/id_' . $videoId,
CURLOPT_USERAGENT => $_SERVER['HTTP_USER_AGENT'],
]);
$response = curl_exec($ch);
curl_close($ch);
// 回應為 JSON 夾雜在 HTML/JS 中,需正則提取
preg_match('~"seed"\s*:\s*(\d+)~i', $response, $mSeed);
preg_match('~"(flv|mp4|hd2)"\s*:\s*"([^"]+)"~i', $response, $mEncoded);
preg_match('~"key1"\s*:\s*"([^"]+)"~i', $response, $mKey1);
preg_match('~"key2"\s*:\s*"([^"]+)"~i', $response, $mKey2);
$seed = (int)$mSeed[1];
$format = $mEncoded[1]; // flv / mp4 / hd2
$encrypted = $mEncoded[2]; // 加密的 fileid 字串
$key1 = $mKey1[1];
$key2 = $mKey2[1];
class Decoder
{
private int $seed;
private string $mixed = '';
public function __construct(int $seed)
{
$this->seed = $seed;
}
// LCG 產生下一隨機比例 [0,1)
private function nextRatio(): float
{
$this->seed = (($this->seed * 211) + 30031) % 65536;
return $this->seed / 65536;
}
// 生成 64 字元混淆字串 (cg_hun)
private function buildMixed(): void
{
$source = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ/\:._-1234567890';
$this->mixed = '';
for ($i = 0; $i < strlen($source); $i++) {
$idx = (int)($this->nextRatio() * strlen($source));
$this->mixed .= $source[$idx];
$source = substr_replace($source, '', $idx, 1);
}
}
// 解密 fileid:按 * 分割索引,從 mixed 取字元
public function decodeFileId(string $encrypted): string
{
$this->buildMixed();
$parts = explode('*', $encrypted);
array_pop($parts); // 移除尾部空值
$result = '';
foreach ($parts as $idx) {
$result .= $this->mixed[(int)$idx] ?? '';
}
return $result;
}
// 解密 Key:key1 (hex) 異或 0xA55AA5A5,拼接 key2
public function decodeKey(string $key1, string $key2): string
{
$val = hexdec($key1);
$val ^= 0xA55AA5A5; // -1520786011 (有號整數)
return $key2 . dechex($val);
}
}
$decoder = new Decoder($seed);
$fileId = $decoder->decodeFileId($encrypted);
$key = $decoder->decodeKey($key1, $key2);
// 構造 SID:時間戳 + 隨機 + 固定後綴
$sid = time() . mt_rand(10, 99) . '1000' . mt_rand(30, 80) . '00';
// 畫質分段代碼:00=第一段, 01=第二段... 0f=第十五段
$segCode = '00';
// 最終 URL 結構
$url = "https://f.youku.com/player/getFlvPath/sid/{$sid}_{$segCode}/st/{$format}/fileid/{$fileId}?K={$key}";
echo $url;
新版 API 回應中 segs.{format} 為陣列,每段擁有獨立 k 值:
"segs": {
"mp4": [
{"no":"0","size":"18367795","seconds":"421","k":"281ff2875db680bb261c02ce"},
{"no":"1","size":"19045091","seconds":"421","k":"45398cdd4aa44968261c02ce"}
]
}
正確做法:遍歷每一段,使用該段的 k 組裝 URL,下載後用 ffmpeg 合併:
foreach ($data->data[0]->segs->$format as $seg) {
$segUrl = "https://f.youku.com/player/getFlvPath/sid/{$sid}_{$seg->no}/st/{$format}/fileid/{$fileId}?K={$seg->k}";
// 下載該段...
}
// 合併:ffmpeg -f concat -safe 0 -i list.txt -c copy output.mp4
| AS3 類/方法 | 功能 | PHP 對應 |
|---|---|---|
Decoder.__construct(seed) | 初始化 LCG 種子 | __construct($seed) |
ran() | 產生下一隨機比例 | nextRatio() |
cg_hun() | 生成混淆字串 | buildMixed() |
decode(fileid) | 解密 fileid | decodeFileId() |
decode_key(key1,key2) | 異或解密 Key | decodeKey() |
本文僅供技術研究與教學用途。繞過平台授權機制下載受版權保護內容,可能違反《中華人民共和國著作權法》第 47 條及《信息網絡傳播權保護條例》。商業專案請務必取得正規授權或使用平台提供的開放 API。