|
|
https://5kds.com/thread-3449-1-1.html
- <?php
- date_default_timezone_set('Asia/Shanghai');
- $id = isset($_GET['id']) ? $_GET['id'] : 'cctv1';
- $n = [
- 'cctv1' => ['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';
-
- private $xorKey = [0x84, 0x2E, 0xED, 0x08, 0xF0, 0x66, 0xE6, 0xEA, 0x48, 0xB4, 0xCA, 0xA9, 0x91, 0xED, 0x6F, 0xF3];
- 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);
- }
-
- // ================== 公开的加解密方法 ===================
-
- /**
- * 加密数据生成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->randomHexStr(88); // 88个字符
- $ck_guard_time = '1907CEBB43DD91205C0AA24CAA050DCE0EA64FEA1AB8F3D20C45B08B35952308456EE297396350DAA26DDC14';
- $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();
复制代码
|
|