['2024078201', '600001859', 'fhd'], //CCTV-1高清 'cctv2' => ['2024075401', '600001800', 'fhd'], //CCTV-2高清 'cctv3' => ['2024068501', '600001801', 'fhd'], //CCTV-3高清 'cctv4' => ['2029797101', '600001814', 'fhd'], //CCTV-4高清 'cctv5' => ['2024078401', '600001818', 'fhd'], //CCTV-5高清 'cctv5p' => ['2024078001', '600001817', 'fhd'], //CCTV-5+高清 'cctv6' => ['2013693901', '600108442', 'fhd'], //CCTV-6高清 'cctv7' => ['2024072001', '600004092', 'fhd'], //CCTV-7高清 'cctv8' => ['2029793001', '600001803', 'fhd'], //CCTV-8高清 'cctv9' => ['2024078601', '600004078', 'fhd'], //CCTV-9高清 'cctv10' => ['2024078701', '600001805', 'fhd'], //CCTV-10高清 'cctv11' => ['2027248701', '600001806', 'fhd'], //CCTV-11高清 'cctv12' => ['2027248801', '600001807', 'fhd'], //CCTV-12高清 'cctv13' => ['2029797201', '600001811', 'fhd'], //CCTV-13高清 'cctv14' => ['2027248901', '600001809', 'fhd'], //CCTV-14高清 'cctv15' => ['2027249001', '600001815', 'fhd'], //CCTV-15高清 'cctv16' => ['2027249101', '600098637', 'fhd'], //CCTV-16高清 'cctv164k' => ['2027249301', '600099502', 'fhd'], //CCTV-16(4K) 'cctv17' => ['2027249401', '600001810', 'fhd'], //CCTV-17高清 'cctv4k' => ['2029810301', '600002264', 'fhd'], //CCTV-4K 'cctv8k' => ['2026774101', '600156816', 'fhd'], //CCTV-8K 'cgtn' => ['2024181701', '600014550', 'fhd'], //CGTN 'cgtnfy' => ['2024181801', '600084704', 'fhd'], //CGTN法语频道 'cgtney' => ['2024181901', '600084758', 'fhd'], //CGTN俄语频道 'cgtnalby' => ['2024182001', '600084782', 'fhd'], //CGTN阿拉伯语频道 'cgtnxby' => ['2024182101', '600084744', 'fhd'], //CGTN西班牙语频道 'cgtnwyjl' => ['2024182301', '600084781', 'fhd'], //CGTN外语纪录频道 'cctvfyjc' => ['2025637103', '600099658', 'shd'], //CCTV风云剧场频道 'cctvdyjc' => ['2026874203', '600099655', 'shd'], //CCTV第一剧场频道 'cctvhjjc' => ['2026874303', '600099620', 'shd'], //CCTV怀旧剧场频道 'cctvsjdl' => ['2026874403', '600099637', 'shd'], //CCTV世界地理频道 'cctvfyyy' => ['2026874503', '600099660', 'shd'], //CCTV风云音乐频道 'cctvbqkj' => ['2026874603', '600099649', 'shd'], //CCTV兵器科技频道 'cctvfyzq' => ['2026966203', '600099636', 'shd'], //CCTV风云足球频道 'cctvgeqwq' => ['2026874703', '600099659', 'shd'], //CCTV高尔夫·网球频道 'cctvnxss' => ['2026874803', '600099650', 'shd'], //CCTV女性时尚频道 'cctvyswhjp' => ['2026874903', '600099653', 'shd'], //CCTV央视文化精品频道 'cctvystq' => ['2026875003', '600099652', 'shd'], //CCTV央视台球频道 'cctvdszn' => ['2026875103', '600099656', 'shd'], //CCTV电视指南频道 'cctvwsjk' => ['2025637003', '600099651', 'shd'], //CCTV卫生健康频道 'bjws' => ['2024052703', '600002309', 'fhd'], //北京卫视 'jsws' => ['2024171103', '600002521', 'fhd'], //江苏卫视 'dfws' => ['2024054503', '600002483', 'fhd'], //东方卫视 'zjws' => ['2024054703', '600002520', 'fhd'], //浙江卫视 'hnws' => ['2024054803', '600002475', 'fhd'], //湖南卫视 'hbws' => ['2024171203', '600002508', 'fhd'], //湖北卫视 'gdws' => ['2024060903', '600002485', 'fhd'], //广东卫视 'gxws' => ['2024060703', '600002509', 'fhd'], //广西卫视 'hljws' => ['2029797003', '600002498', 'fhd'], //黑龙江卫视 'hnws2' => ['2024055603', '600002506', 'fhd'], //海南卫视 'cqws' => ['2024061103', '600002531', 'fhd'], //重庆卫视 'szws' => ['2024061303', '600002481', 'fhd'], //深圳卫视 'scws' => ['2024061403', '600002516', 'fhd'], //四川卫视 'henanws' => ['2029797303', '600002525', 'fhd'], //河南卫视 'fjdnhz' => ['2024061503', '600002484', 'fhd'], //福建东南卫视 'gzhws' => ['2024061603', '600002490', 'fhd'], //贵州卫视 'jxws' => ['2024061703', '600002503', 'fhd'], //江西卫视 'lnws' => ['2024171303', '600002505', 'fhd'], //辽宁卫视 'ahws' => ['2024171403', '600002532', 'fhd'], //安徽卫视 'hbws2' => ['2024171503', '600002493', 'fhd'], //河北卫视 'sdws' => ['2029787903', '600002513', 'fhd'], //山东卫视 'tjws' => ['2019927003', '600152137', 'fhd'], //天津卫视 'jlws' => ['2025561503', '600190405', 'fhd'], //吉林卫视 'shanxiws' => ['2029795103', '600190400', 'fhd'], //陕西卫视 'nxws' => ['2025608503', '600190737', 'fhd'], //宁夏卫视 'nmgws' => ['2025561203', '600190401', 'fhd'], //内蒙古卫视 'ynws' => ['2025561303', '600190402', 'fhd'], //云南卫视 'shanxiws2' => ['2025560803', '600190407', 'fhd'], //山西卫视 'qhws' => ['2025559103', '600190406', 'fhd'], //青海卫视 'xzws' => ['2025558003', '600190403', 'fhd'], //西藏卫视 'cetv1' => ['2022823801', '600171827', 'fhd'], //中国教育电视台1频道 'gxpd' => ['2029360403', '600213139', 'fhd'], //国学频道 'xjws' => ['2019927403', '600152138', 'fhd'] //新疆卫视 ]; class CKeyManager { // 常量定义 const DELTA = 0x9e3779b9; const ROUNDS = 16; const LOG_ROUNDS = 4; const SALT_LEN = 2; const ZERO_LEN = 7; const TEA_CKEY = '59b2f7cf725ef43c34fdd7c123411ed3'; const GUARD_TEA_KEY = '110DBEC10C23E7D2E56A1CAD6914EF1B'; private $xorKey = [0x84, 0x2E, 0xED, 0x08, 0xF0, 0x66, 0xE6, 0xEA, 0x48, 0xB4, 0xCA, 0xA9, 0x91, 0xED, 0x6F, 0xF3]; private $guardXorKey = [0xB3, 0xC9, 0x53, 0xA0, 0x69, 0x13, 0xAD, 0x4D]; private $standardAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; private $customAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-='; // 当前使用的GUID private $guid = ''; /** * 构造函数 */ public function __construct() { error_reporting(E_ALL); ini_set('display_errors', 1); date_default_timezone_set('Asia/Shanghai'); // 初始化时生成一个随机GUID $this->generateGuid(); } /** * 生成随机GUID */ private function generateGuid() { $this->guid = sprintf('%08s%04s%04s%04s%12s', dechex(mt_rand(0, 0xffffffff)), dechex(mt_rand(0, 0xffff)), dechex(mt_rand(0, 0xffff)), dechex(mt_rand(0, 0xffff)), dechex(mt_rand(0, 0xffffffffffff)) ); // 确保GUID是32位十六进制字符串(不带连字符) if (strlen($this->guid) !== 32) { $this->guid = str_pad($this->guid, 32, '0', STR_PAD_LEFT); } return $this->guid; } /** * 获取当前GUID */ public function getGuid() { return $this->guid; } /** * 设置自定义GUID */ public function setGuid($guid) { $this->guid = $guid; } /** * 重置GUID(生成新的随机GUID) */ public function resetGuid() { return $this->generateGuid(); } // ================== 辅助函数 =================== /** * 生成随机十六进制字符串 */ private function randomHexStr($length) { $hex = ''; for ($i = 0; $i < $length; $i++) { $hex .= dechex(mt_rand(0, 15)); } return strtoupper($hex); } /** * 生成spvcode */ private function spvcode($defn) { $height = 1080; if (preg_match('/(4k|8k|hdr)/i', $defn)) { $height = 2160; } $frame_rates = [30, 60, 90, 120]; $h264_parts = []; $h265_parts = []; foreach ($frame_rates as $fps) { $h264_parts[] = "{$fps}:{$height}"; $h265_parts[] = "{$fps}:{$height}"; } $h264_str = implode(',', $h264_parts); $h265_str = implode(',', $h265_parts); $spvcode_raw = "H({$h264_str}|{$h264_str});2({$h265_str}|{$h265_str})"; return base64_encode($spvcode_raw); } /** * 计算签名 */ private function calcSignature($buffer) { $signature = 0; foreach ($buffer as $byte) { $signature = (0x83 * $signature + ($byte & 0xFF)) & 0x7FFFFFFF; } return $signature; } /** * 自定义Base64解码 */ private function customDecode($text) { if (empty($text)) return ''; $text = rtrim($text, '='); if (strlen($text) % 4 != 0) { $text .= str_repeat('=', 4 - (strlen($text) % 4)); } $translationTable = []; $len = min(strlen($this->customAlphabet), strlen($this->standardAlphabet)); for ($i = 0; $i < $len; $i++) { $translationTable[$this->customAlphabet[$i]] = $this->standardAlphabet[$i]; } $translatedStr = strtr($text, $translationTable); return base64_decode($translatedStr); } /** * 自定义Base64编码 */ private function customEncode($text) { $encoded = base64_encode($text); $translationTable = []; $len = min(strlen($this->standardAlphabet), strlen($this->customAlphabet)); for ($i = 0; $i < $len; $i++) { $translationTable[$this->standardAlphabet[$i]] = $this->customAlphabet[$i]; } return rtrim(strtr($encoded, $translationTable), '='); } /** * XOR加密/解密 */ private function xorArray($byteArray) { $retArray = []; $len = count($byteArray); for ($i = 0; $i < $len; $i++) { $retArray[] = $byteArray[$i] ^ $this->xorKey[$i & 0xF]; } return $retArray; } // ================== TEA加解密函数 =================== /** * TEA ECB模式加密 */ private function teaEncryptECB($pInBuf, $pKey) { if (strlen($pInBuf) < 8) { $pInBuf = str_pad($pInBuf, 8, "\0"); } $unpacked = unpack('N2', $pInBuf); $y = $unpacked[1]; $z = $unpacked[2]; $k = [ unpack('N', substr($pKey, 0, 4))[1], unpack('N', substr($pKey, 4, 4))[1], unpack('N', substr($pKey, 8, 4))[1], unpack('N', substr($pKey, 12, 4))[1] ]; $sum = 0; for ($i = 0; $i < self::ROUNDS; $i++) { $sum = ($sum + self::DELTA) & 0xFFFFFFFF; $y = ($y + ((($z << 4) + $k[0]) ^ ($z + $sum) ^ (($z >> 5) + $k[1]))) & 0xFFFFFFFF; $z = ($z + ((($y << 4) + $k[2]) ^ ($y + $sum) ^ (($y >> 5) + $k[3]))) & 0xFFFFFFFF; } return pack('N2', $y, $z); } /** * TEA ECB模式解密 */ private function teaDecryptECB($pInBuf, $pKey) { $unpacked = unpack('N2', $pInBuf); $y = $unpacked[1]; $z = $unpacked[2]; $k = [ unpack('N', substr($pKey, 0, 4))[1], unpack('N', substr($pKey, 4, 4))[1], unpack('N', substr($pKey, 8, 4))[1], unpack('N', substr($pKey, 12, 4))[1] ]; $sum = (self::DELTA << self::LOG_ROUNDS) & 0xFFFFFFFF; for ($i = 0; $i < self::ROUNDS; $i++) { $z = ($z - ((($y << 4) + $k[2]) ^ ($y + $sum) ^ (($y >> 5) + $k[3]))) & 0xFFFFFFFF; $y = ($y - ((($z << 4) + $k[0]) ^ ($z + $sum) ^ (($z >> 5) + $k[1]))) & 0xFFFFFFFF; $sum = ($sum - self::DELTA) & 0xFFFFFFFF; } return pack('N2', $y, $z); } // ================== CBC模式加解密 =================== /** * CBC模式加密 */ private function oiSymmetryEncrypt2($pInBuf, $nInBufLen, $pKey) { // 计算填充长度 $nPadSaltBodyZeroLen = $nInBufLen + 1 + self::SALT_LEN + self::ZERO_LEN; $nPadlen = $nPadSaltBodyZeroLen % 8; if ($nPadlen) { $nPadlen = 8 - $nPadlen; } $pOutBuf = ''; // 第一块数据 $src_buf = array_fill(0, 8, 0); $src_buf[0] = (mt_rand(0, 255) & 0xF8) | $nPadlen; $src_i = 1; // 填充 while ($nPadlen) { $src_buf[$src_i] = mt_rand(0, 255); $src_i++; $nPadlen--; } $iv_plain = array_fill(0, 8, 0); $iv_crypt = $iv_plain; // 处理Salt $i = 0; while ($i < self::SALT_LEN) { if ($src_i < 8) { $src_buf[$src_i] = mt_rand(0, 255); $src_i++; $i++; } if ($src_i == 8) { // 异或前一块密文 for ($j = 0; $j < 8; $j++) { $src_buf[$j] ^= $iv_crypt[$j]; } $temp_out = $this->teaEncryptECB(pack('C*', ...$src_buf), $pKey); $temp_bytes = array_values(unpack('C*', $temp_out)); // 异或前一块明文 for ($j = 0; $j < 8; $j++) { $temp_bytes[$j] ^= $iv_plain[$j]; } $iv_plain = $src_buf; $iv_crypt = $temp_bytes; $pOutBuf .= pack('C*', ...$temp_bytes); $src_i = 0; } } // 处理主体数据 $pInBufIndex = 0; while ($nInBufLen) { if ($src_i < 8) { $src_buf[$src_i] = ord($pInBuf[$pInBufIndex]); $pInBufIndex++; $src_i++; $nInBufLen--; } if ($src_i == 8) { // 异或前一块密文 for ($j = 0; $j < 8; $j++) { $src_buf[$j] ^= $iv_crypt[$j]; } $temp_out = $this->teaEncryptECB(pack('C*', ...$src_buf), $pKey); $temp_bytes = array_values(unpack('C*', $temp_out)); // 异或前一块明文 for ($j = 0; $j < 8; $j++) { $temp_bytes[$j] ^= $iv_plain[$j]; } $iv_plain = $src_buf; $iv_crypt = $temp_bytes; $pOutBuf .= pack('C*', ...$temp_bytes); $src_i = 0; } } // 处理Zero填充 $i = 0; while ($i < self::ZERO_LEN) { if ($src_i < 8) { $src_buf[$src_i] = 0; $src_i++; $i++; } if ($src_i == 8) { // 异或前一块密文 for ($j = 0; $j < 8; $j++) { $src_buf[$j] ^= $iv_crypt[$j]; } $temp_out = $this->teaEncryptECB(pack('C*', ...$src_buf), $pKey); $temp_bytes = array_values(unpack('C*', $temp_out)); // 异或前一块明文 for ($j = 0; $j < 8; $j++) { $temp_bytes[$j] ^= $iv_plain[$j]; } $iv_plain = $src_buf; $iv_crypt = $temp_bytes; $pOutBuf .= pack('C*', ...$temp_bytes); $src_i = 0; } } // 处理最后一组 if ($src_i > 0) { // 填充剩余字节 for ($j = $src_i; $j < 8; $j++) { $src_buf[$j] = 0; } // 异或前一块密文 for ($j = 0; $j < 8; $j++) { $src_buf[$j] ^= $iv_crypt[$j]; } $temp_out = $this->teaEncryptECB(pack('C*', ...$src_buf), $pKey); $temp_bytes = array_values(unpack('C*', $temp_out)); // 异或前一块明文 for ($j = 0; $j < 8; $j++) { $temp_bytes[$j] ^= $iv_plain[$j]; } $pOutBuf .= pack('C*', ...$temp_bytes); } return $pOutBuf; } /** * CBC模式解密 */ private function oiSymmetryDecrypt2($pInBuf, $nInBufLen, $pKey) { if (($nInBufLen % 8) != 0 || $nInBufLen < 16) { return false; } // 解密第一个块 $dest_buf_str = $this->teaDecryptECB(substr($pInBuf, 0, 8), $pKey); $dest_buf = array_values(unpack('C*', $dest_buf_str)); $nPadLen = $dest_buf[0] & 0x07; // 计算明文长度 $i = $nInBufLen - 1; $i = $i - $nPadLen - self::SALT_LEN - self::ZERO_LEN; if ($i < 0) { return false; } $pOutBufLen = $i; // 初始化IV $iv_pre_crypt = array_fill(0, 8, 0); $iv_cur_crypt = array_values(unpack('C*', substr($pInBuf, 0, 8))); $pInBufOffset = 8; $dest_i = 1; // 跳过Padding $dest_i += $nPadLen; // 跳过Salt $salt_count = 1; while ($salt_count <= self::SALT_LEN) { if ($dest_i < 8) { $dest_i++; $salt_count++; } elseif ($dest_i == 8) { $iv_pre_crypt = $iv_cur_crypt; $iv_cur_crypt = array_values(unpack('C*', substr($pInBuf, $pInBufOffset, 8))); for ($j = 0; $j < 8; $j++) { if ($pInBufOffset + $j >= $nInBufLen) { return false; } $dest_buf[$j] ^= $iv_cur_crypt[$j]; } $temp_buf = $this->teaDecryptECB(pack('C*', ...$dest_buf), $pKey); $dest_buf = array_values(unpack('C*', $temp_buf)); $pInBufOffset += 8; $dest_i = 0; } } // 还原明文 $nPlainLen = $pOutBufLen; $plain_bytes = []; while ($nPlainLen > 0) { if ($dest_i < 8) { $plain_bytes[] = $dest_buf[$dest_i] ^ $iv_pre_crypt[$dest_i]; $dest_i++; $nPlainLen--; } elseif ($dest_i == 8) { $iv_pre_crypt = $iv_cur_crypt; $iv_cur_crypt = array_values(unpack('C*', substr($pInBuf, $pInBufOffset, 8))); for ($j = 0; $j < 8; $j++) { if ($pInBufOffset + $j >= $nInBufLen) { return false; } $dest_buf[$j] ^= $iv_cur_crypt[$j]; } $temp_buf = $this->teaDecryptECB(pack('C*', ...$dest_buf), $pKey); $dest_buf = array_values(unpack('C*', $temp_buf)); $pInBufOffset += 8; $dest_i = 0; } } return pack('C*', ...$plain_bytes); } /** * 按 vsCKey::task_encGuard 生成 ck_guard_time。 */ private function generateCkGuardTime($timestamp, $guid, $guardData = '-1', $packageName = 'null', $processName = 'null') { $body = pack('N', $timestamp); foreach ([ $this->guardLastFive($guid), $this->guardLastFive($packageName), $this->guardLastFive($processName), $guardData ] as $part) { $body .= pack('n', strlen($part)) . $part; } $plain = pack('n', strlen($body)) . $body; $checksum = $this->calcSignature(array_values(unpack('C*', $plain))); $encrypted = $this->oiSymmetryEncrypt2($plain, strlen($plain), hex2bin(self::GUARD_TEA_KEY)); $encrypted .= pack('N', $checksum); $bytes = array_values(unpack('C*', $encrypted)); $len = count($bytes); for ($i = 0; $i < $len; $i++) { $bytes[$i] ^= $this->guardXorKey[$i & 7]; } return strtoupper(bin2hex(pack('C*', ...$bytes))); } private function guardLastFive($value) { $value = (string)$value; return strlen($value) >= 5 ? substr($value, -5) : ''; } // ================== 公开的加解密方法 =================== /** * 加密数据生成cKey * @param string $data 要加密的数据 * @return string 生成的cKey */ public function encryptDataToCKey($data) { $teaCkey = hex2bin(self::TEA_CKEY); // 计算数据长度 $data_len = strlen($data); // 计算校验和 $data_array = array_values(unpack('C*', $data)); $checksum = $this->calcSignature($data_array); // TEA加密 $encrypted = $this->oiSymmetryEncrypt2($data, $data_len, $teaCkey); // 添加校验和 $encrypted .= pack('N', $checksum); // XOR加密 $encrypted_array = array_values(unpack('C*', $encrypted)); $xor_array = $this->xorArray($encrypted_array); $xor_encrypted = pack('C*', ...$xor_array); // Base64编码 $base64_encoded = $this->customEncode($xor_encrypted); return "--01" . $base64_encoded; } /** * 解密cKey获取数据 * @param string $ckey 要解密的cKey * @return array|false 解密后的数据和校验和,或false表示失败 */ public function decryptCKeyToData($ckey) { $teaCkey = hex2bin(self::TEA_CKEY); // 移除前缀 $ckey_without_prefix = substr($ckey, 4); // 自定义Base64解码 $base64_decoded = $this->customDecode($ckey_without_prefix); if (!$base64_decoded) { return false; } // XOR解密 $xor_array = array_values(unpack('C*', $base64_decoded)); $xor_decrypted_array = $this->xorArray($xor_array); $xor_decrypted = pack('C*', ...$xor_decrypted_array); // 分离数据和校验和 $data_len = strlen($xor_decrypted) - 4; $encrypted_data = substr($xor_decrypted, 0, $data_len); $checksum_bytes = substr($xor_decrypted, $data_len); $checksum = unpack('N', $checksum_bytes)[1]; // TEA解密 $decrypted = $this->oiSymmetryDecrypt2($encrypted_data, $data_len, $teaCkey); return [ 'data' => $decrypted, 'checksum' => $checksum ]; } // ================== 数据包构建方法 =================== /** * 构建数据包 * @param array $params 参数数组 * @return string 构建的数据包 */ public function buildPacket($params) { $data = ''; // 1. 头部 (12字节) - 固定值 $data .= hex2bin('0000004200000004000004d2'); // 2. Platform (4字节) $data .= pack('N', $params['Platform']); // 3. Signature (4字节) - 先置0,后面计算 $data .= pack('N', 0); // 4. Timestamp (4字节) $data .= pack('N', $params['Timestamp']); // 5. Sdtfrom (长度+字符串) $sdtfrom = $params['Sdtfrom']; $data .= pack('n', strlen($sdtfrom)) . $sdtfrom; // 6. randFlag (长度+字符串) - 使用传入的值 $randFlag = $params['randFlag']; $data .= pack('n', strlen($randFlag)) . $randFlag; // 7. appVer (长度+字符串) $appVer = $params['appVer']; $data .= pack('n', strlen($appVer)) . $appVer; // 8. vid (长度+字符串) $vid = $params['vid']; $data .= pack('n', strlen($vid)) . $vid; // 9. guid (长度+字符串) $guid = $params['guid']; $data .= pack('n', strlen($guid)) . $guid; // 10. part1 (4字节) $data .= pack('N', 1); // 11. isDlna (4字节) - 根据原始样本是0 $data .= pack('N', 1); // 12. uid (长度+字符串) $uid = "2622783A"; $data .= pack('n', strlen($uid)) . $uid; // 13. bundleID (长度+字符串) $bundleID = "nil"; $data .= pack('n', strlen($bundleID)) . $bundleID; // 14. uuid4 (长度+字符串) $uuid4 = $params['uuid4']; $data .= pack('n', strlen($uuid4)) . $uuid4; // 15. bundleID1 (长度+字符串) - 重复bundleID $data .= pack('n', strlen($bundleID)) . $bundleID; // 16. ckeyVersion (长度+字符串) $ckeyVersion = "v0.1.000"; $data .= pack('n', strlen($ckeyVersion)) . $ckeyVersion; // 17. packageName (长度+字符串) $packageName = "com.cctv.yangshipin.app.iphone"; $data .= pack('n', strlen($packageName)) . $packageName; // 18. platform_str (长度+字符串) $platform_str = "4330403"; $data .= pack('n', strlen($platform_str)) . $platform_str; // 19. ex_json_bus (长度+字符串) $ex_json_bus = "ex_json_bus"; $data .= pack('n', strlen($ex_json_bus)) . $ex_json_bus; // 20. ex_json_vs (长度+字符串) $ex_json_vs = "ex_json_vs"; $data .= pack('n', strlen($ex_json_vs)) . $ex_json_vs; // 21. ck_guard_time (长度+字符串) - 88个字符 $ck_guard_time = $params['ck_guard_time']; $data .= pack('n', strlen($ck_guard_time)) . $ck_guard_time; // 验证主体长度 $body_length = strlen($data); // 添加长度头 $buffer = pack('n', $body_length) . $data; // 计算签名 $buffer_array = array_values(unpack('C*', $buffer)); $signature = $this->calcSignature($buffer_array); // 更新签名(位置:跳过长度头2字节+头部12字节+Platform4字节=18字节处) $buffer = substr($buffer, 0, 18) . pack('N', $signature) . substr($buffer, 22); return $buffer; } /** * 生成完整的cKey * @param string $cnlid 频道ID * @param int|null $timestamp 时间戳 * @return array 包含cKey、参数和数据包的数组 */ public function generateCKey($cnlid, $timestamp = null) { if ($timestamp === null) { $timestamp = time(); } // 生成所有随机值 $randFlag = base64_encode(random_bytes(18)); $uuid4 = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); $ck_guard_time = $this->generateCkGuardTime($timestamp, $this->guid); $randFlag='_zj1A5Gh6QYcxWjIUGos2w=='; $params = [ 'Platform' => 4330403, 'Timestamp' => $timestamp, 'Sdtfrom' => 'dcgh', 'vid' => $cnlid, 'guid' => $this->guid, // 使用当前GUID 'appVer' => 'V8.22.1035.3031', 'randFlag' => $randFlag, 'uuid4' => '57eab0c4-2c58-44c6-8ae9-dd2757525dc5', 'ck_guard_time' => $ck_guard_time ]; $buffer = $this->buildPacket($params); $ckey = $this->encryptDataToCKey($buffer); return [ 'ckey' => $ckey, 'params' => $params, 'buffer' => $buffer ]; } // ================== 网络请求方法(优化版本) =================== /** * 发起直播或回看请求(优化版) * @param string $cnlid 频道ID * @param string $livepid 直播ID * @param string $defn 清晰度 * @param string|null $playseek 回看时间(格式:YYYYMMDDHHMMSS-YYYYMMDDHHMMSS) * @return array 请求结果 */ public function makeLiveRequest($cnlid, $livepid = '600001859', $defn = 'fhd', $playseek = null) { // 每次请求生成新的随机GUID $this->generateGuid(); // 只生成一次cKey(直播和回看使用相同的cKey参数) $ckeyResult = $this->generateCKey($cnlid); $ckey = $ckeyResult['ckey']; $params = $ckeyResult['params']; // 生成flowid $flowid = sprintf('%s_%d', sprintf('%04X%04X-%04X-%04X-%04X-%04X%04X%04X', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ), 4330403 ); // 提前判断模式并设置参数 $isPlayback = !empty($playseek); $playbackTimestamp = null; // 处理回看时间 if ($isPlayback) { try { $parts = explode('-', $playseek); $startTimeStr = $parts[0]; $dateTime = DateTime::createFromFormat('YmdHis', $startTimeStr, new DateTimeZone('Asia/Shanghai')); if ($dateTime === false) { throw new Exception("回看时间格式错误: " . $startTimeStr); } $playbackTimestamp = $dateTime->getTimestamp(); } catch (Exception $e) { return [ 'success' => false, 'error' => '回看时间处理失败: ' . $e->getMessage(), 'playseek' => $playseek ]; } } // 构建基础请求参数(根据模式设置不同参数) $spvcode = $this->spvcode($defn); $request_params = [ "atime" => "120", "livepid" => $livepid, "cnlid" => $cnlid, "appVer" => "V8.22.1035.3031", "app_version" => "300090", "caplv" => "1", "cmd" => "2", "defn" => $defn, "device" => "iPhone", "encryptVer" => "4.2", "getpreviewinfo" => "0", "hevclv" => "33", "lang" => "zh-Hans_JP", "livequeue" => "0", "logintype" => "1", "nettype" => "1", "newnettype" => "1", "newplatform" => "4330403", "platform" => "4330403", "sdtfrom" => "v3021", "spacode" => "23", "spaudio" => "1", "spdemuxer" => "6", "spdrm" => "2", "spdynamicrange" => "7", "spflv" => "1", "spflvaudio" => "1", "sphdrfps" => "60", "sphttps" => "0", "spvcode" => "MSgzMDoyMTYwLDYwOjIxNjB8MzA6MjE2MCw2MDoyMTYwKTsyKDMwOjIxNjAsNjA6MjE2MHwzMDoyMTYwLDYwOjIxNjAp", "spvideo" => "4", "stream" => "1", "system" => "1", "sysver" => "ios18.2.1", "uhd_flag" => "4", "cKey" => $ckey, "guid" => $this->guid, "fntick" => $params['Timestamp'], "flowid" => $flowid, ]; // 根据模式设置不同的参数 if ($isPlayback) { // 回看模式:第一次尝试 - 添加playbacktime参数 $request_params['playbacktime'] = $playbackTimestamp; // 发送第一次请求 $response = $this->sendHttpRequest($request_params); if ($response['success'] && isset($response['response']['playurl'])) { return $response; } else { // 第二次尝试 - 不添加playbacktime参数 unset($request_params['playbacktime']); // 发送第二次请求 $response = $this->sendHttpRequest($request_params); if ($response['success'] && isset($response['response']['playurl'])) { // 手动处理URL:修改域名并添加starttime参数 $playurl = $response['response']['playurl']; $playurl = $this->processPlaybackUrl($playurl, $playbackTimestamp); $response['response']['playurl'] = $playurl; return $response; } else { return [ 'success' => false, 'error' => '无法获取回看地址', 'playseek' => $playseek, 'response' => $response['response'] ?? null ]; } } } else { // 直播模式:设置playbacktime为0 $request_params['playbacktime'] = "0"; // 发送请求 return $this->sendHttpRequest($request_params); } } /** * 处理回看URL * @param string $playurl 原始播放URL * @param int $playbackTimestamp 回看时间戳 * @return string 处理后的URL */ private function processPlaybackUrl($playurl, $playbackTimestamp) { // 修改域名 $urlParts = explode('/', $playurl); if (count($urlParts) >= 3) { $urlParts[2] = 'tlivecloud-playback-cdn.ysp.cctv.cn/tcloud.cctv.com'; $playurl = implode('/', $urlParts); // 添加starttime参数 if (strpos($playurl, '?') !== false) { $playurl .= '&starttime=' . $playbackTimestamp; } else { $playurl .= '?starttime=' . $playbackTimestamp; } } return $playurl; } /** * 发送HTTP请求 * @param array $params 请求参数 * @return array 请求结果 */ private function sendHttpRequest($params) { $url = "https://bkliveinfo.ysp.cctv.cn"; $query_string = http_build_query($params); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url . '?' . $query_string); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'User-Agent: qqlive', 'Connection: Keep-Alive', 'Accept: application/json' ]); curl_setopt($ch, CURLOPT_TIMEOUT, 15); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); $response = curl_exec($ch); $error = curl_error($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($error) { return [ 'success' => false, 'error' => 'cURL错误: ' . $error, 'http_code' => $http_code ]; } $data = json_decode($response, true); if ($data) { if (isset($data['iretcode'])) { $result = [ 'success' => $data['iretcode'] == 0, 'iretcode' => $data['iretcode'], 'http_code' => $http_code, 'response' => $data ]; if ($data['iretcode'] == 0) { $result['playurl'] = $data['playurl'] ?? null; } else { $result['error'] = $data['errinfo'] ?? '未知错误'; } return $result; } } return [ 'success' => false, 'error' => '无效的JSON响应', 'http_code' => $http_code, 'raw_response' => substr($response, 0, 500) ]; } /** * 简化版获取播放地址(支持回看) * @param string $cnlid 频道ID * @param string $livepid 直播ID(可选,默认600001859) * @param string $defn 清晰度(可选,默认fhd) * @param string|null $playseek 回看时间(可选,null表示直播) * @return string|null 播放地址或null */ public function getPlayUrl($cnlid, $livepid = '600001859', $defn = 'fhd', $playseek = null) { $result = $this->makeLiveRequest($cnlid, $livepid, $defn, $playseek); if ($result['success'] && isset($result['playurl'])) { return $result['playurl']; } return null; } // ================== 数据包解析方法 =================== /** * 解析数据包 * @param string $data 数据包二进制数据 * @return array 解析后的字段数组 */ public function parsePacket($data) { if (strlen($data) < 2) { return false; } $pos = 0; $result = []; // 1. 长度头 (2字节) $result['packet_len'] = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; // 2. 固定头部 (12字节) $result['header'] = substr($data, $pos, 12); $pos += 12; // 3. Platform (4字节) $result['platform'] = unpack('N', substr($data, $pos, 4))[1]; $pos += 4; // 4. Signature (4字节) $result['signature'] = unpack('N', substr($data, $pos, 4))[1]; $pos += 4; // 5. Timestamp (4字节) $result['timestamp'] = unpack('N', substr($data, $pos, 4))[1]; $pos += 4; // 6. Sdtfrom (长度+字符串) $sdtfrom_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['sdtfrom'] = substr($data, $pos, $sdtfrom_len); $pos += $sdtfrom_len; // 7. randFlag (长度+字符串) $randFlag_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['randFlag'] = substr($data, $pos, $randFlag_len); $pos += $randFlag_len; // 8. appVer (长度+字符串) $appVer_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['appVer'] = substr($data, $pos, $appVer_len); $pos += $appVer_len; // 9. vid (长度+字符串) $vid_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['vid'] = substr($data, $pos, $vid_len); $pos += $vid_len; // 10. guid (长度+字符串) $guid_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['guid'] = substr($data, $pos, $guid_len); $pos += $guid_len; // 11. part1 (4字节) $result['part1'] = unpack('N', substr($data, $pos, 4))[1]; $pos += 4; // 12. isDlna (4字节) $result['isDlna'] = unpack('N', substr($data, $pos, 4))[1]; $pos += 4; // 13. uid (长度+字符串) $uid_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['uid'] = substr($data, $pos, $uid_len); $pos += $uid_len; // 14. bundleID (长度+字符串) $bundleID_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['bundleID'] = substr($data, $pos, $bundleID_len); $pos += $bundleID_len; // 15. uuid4 (长度+字符串) $uuid4_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['uuid4'] = substr($data, $pos, $uuid4_len); $pos += $uuid4_len; // 16. bundleID1 (长度+字符串) $bundleID1_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['bundleID1'] = substr($data, $pos, $bundleID1_len); $pos += $bundleID1_len; // 17. ckeyVersion (长度+字符串) $ckeyVersion_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['ckeyVersion'] = substr($data, $pos, $ckeyVersion_len); $pos += $ckeyVersion_len; // 18. packageName (长度+字符串) $packageName_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['packageName'] = substr($data, $pos, $packageName_len); $pos += $packageName_len; // 19. platform_str (长度+字符串) $platform_str_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['platform_str'] = substr($data, $pos, $platform_str_len); $pos += $platform_str_len; // 20. ex_json_bus (长度+字符串) $ex_json_bus_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['ex_json_bus'] = substr($data, $pos, $ex_json_bus_len); $pos += $ex_json_bus_len; // 21. ex_json_vs (长度+字符串) $ex_json_vs_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['ex_json_vs'] = substr($data, $pos, $ex_json_vs_len); $pos += $ex_json_vs_len; // 22. ck_guard_time (长度+字符串) $ck_guard_time_len = unpack('n', substr($data, $pos, 2))[1]; $pos += 2; $result['ck_guard_time'] = substr($data, $pos, $ck_guard_time_len); $pos += $ck_guard_time_len; $result['total_size'] = strlen($data); $result['parsed_size'] = $pos; $result['remaining'] = substr($data, $pos); return $result; } /** * 验证cKey * @param string $ckey cKey字符串 * @return bool 是否验证通过 */ public function verifyCKey($ckey) { $decrypt_result = $this->decryptCKeyToData($ckey); if (!$decrypt_result) { return false; } $calculated_checksum = $this->calcSignature(array_values(unpack('C*', $decrypt_result['data']))); return $decrypt_result['checksum'] == $calculated_checksum; } /** * 解析回看时间字符串 * @param string $playseek 回看时间字符串(格式:YYYYMMDDHHMMSS-YYYYMMDDHHMMSS) * @return array 包含开始时间和结束时间的数组 */ public function parsePlayseek($playseek) { $parts = explode('-', $playseek); if (count($parts) !== 2) { throw new Exception("回看时间格式错误,应为: YYYYMMDDHHMMSS-YYYYMMDDHHMMSS"); } $startTimeStr = $parts[0]; $endTimeStr = $parts[1]; $startTime = DateTime::createFromFormat('YmdHis', $startTimeStr, new DateTimeZone('Asia/Shanghai')); $endTime = DateTime::createFromFormat('YmdHis', $endTimeStr, new DateTimeZone('Asia/Shanghai')); if ($startTime === false || $endTime === false) { throw new Exception("回看时间解析失败"); } return [ 'start_time' => $startTime, 'end_time' => $endTime, 'start_timestamp' => $startTime->getTimestamp(), 'end_timestamp' => $endTime->getTimestamp(), 'start_str' => $startTime->format('Y-m-d H:i:s'), 'end_str' => $endTime->format('Y-m-d H:i:s'), 'duration' => $endTime->getTimestamp() - $startTime->getTimestamp() ]; } /** * 生成回看时间字符串 * @param string $startDateTime 开始时间(格式:Y-m-d H:i:s) * @param string $endDateTime 结束时间(格式:Y-m-d H:i:s) * @return string 回看时间字符串 */ public function generatePlayseek($startDateTime, $endDateTime) { $startTime = DateTime::createFromFormat('Y-m-d H:i:s', $startDateTime, new DateTimeZone('Asia/Shanghai')); $endTime = DateTime::createFromFormat('Y-m-d H:i:s', $endDateTime, new DateTimeZone('Asia/Shanghai')); if ($startTime === false || $endTime === false) { throw new Exception("时间格式错误,应为: Y-m-d H:i:s"); } return $startTime->format('YmdHis') . '-' . $endTime->format('YmdHis'); } } $ckeyManager = new CKeyManager(); $playseek = $_GET['playseek'] ?? null; // 缓存配置(仅用于直播) $cookieKey = 'playurl_cache'; $cacheTimeoutLive = 80; // 直播缓存超时 4分钟 $cookieExpire = time() + 3600; // Cookie本身有效期1小时 // 读取缓存 $cacheJson = $_COOKIE[$cookieKey] ?? '{}'; $cache = json_decode($cacheJson, true) ?: []; $now = time(); $isLive = ($playseek === null || $playseek === ''); // 无playseek参数视为直播 $playUrl = null; $m3u8Content = false; $maxAttempts = 2; for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { $needRefresh = true; // 默认需要刷新 // 仅在第一次尝试且为直播时检查缓存 if ($attempt == 1 && $isLive) { if (isset($cache[$id]) && is_array($cache[$id])) { $entry = $cache[$id]; if (($now - $entry['time']) <= $cacheTimeoutLive) { $needRefresh = false; $playUrl = $entry['url']; } } } // 点播模式或直播缓存失效/不存在时,needRefresh保持true if ($needRefresh) { $playUrl = $ckeyManager->getPlayUrl($n[$id][0], $n[$id][1], $n[$id][2], $playseek); if (!$playUrl) { die("获取播放地址失败\n"); } // 只有直播才更新缓存 if ($isLive) { $cache[$id] = [ 'url' => $playUrl, 'time' => $now ]; setcookie($cookieKey, json_encode($cache), $cookieExpire, '/'); } else { // 点播模式直接跳转(不输出M3U8) header("Location: " . $playUrl); exit(); } } // 获取 M3U8 内容(仅直播模式会走到这里,点播已在上面跳转) $m3u8Content = @file_get_contents($playUrl); if ($m3u8Content === false) { $m3u8Content = false; // 明确设为false,进入后续重试判断 } if ($m3u8Content !== false) { break; // 成功获取,跳出循环 } // 获取失败:如果是第一次尝试且使用了缓存地址,则清除缓存 if ($attempt == 1 && $isLive && !$needRefresh) { unset($cache[$id]); setcookie($cookieKey, json_encode($cache), $cookieExpire, '/'); } // 继续下一次尝试 } if ($m3u8Content === false) { die("无法获取 M3U8 内容,请稍后重试\n"); } // 补全 TS 路径(仅直播) $baseUrl = substr($playUrl, 0, strrpos($playUrl, '/') + 1); header('Content-Type: application/vnd.apple.mpegurl'); print_r(preg_replace("/(.*?.ts)/i", $baseUrl."$1",$m3u8Content)); exit();