Files
tvbox_collect/jaychouqq/yingshi/php/yspios.php
T
2026-06-29 03:41:14 +00:00

1343 lines
46 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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';
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();