$v) { $o->{(string)$k} = $v; } return $o; } function indians_data(): array { return [ 'timezone' => 'Asia/Shanghai', 'timezoneOffset' => -480, 'userAgent' => BROWSER_UA, 'screenSize' => '1920x1080', 'pixelDepth' => 24, 'touch' => false, 'deviceMemory' => 8, 'platform' => 'Win32', 'touchPoints' => 0, 'hardwareConcurrency' => 8, 'intlDisplayNames' => 'supported', 'mimeTypes' => ['application/pdf', 'text/pdf'], 'plugins' => ['PDF Viewer', 'Chrome PDF Viewer', 'Chromium PDF Viewer', 'Microsoft Edge PDF Viewer', 'WebKit built-in PDF'], 'webgl' => [ 'vendor' => 'Google Inc. (Intel)', 'renderer' => 'ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)', 'params' => obj([ 3379 => 16384, 3410 => 8, 3412 => 8, 3413 => 8, 3414 => 24, 3415 => 0, 7936 => 'WebKit', 7937 => 'WebKit WebGL', 7938 => 'WebGL 1.0 (OpenGL ES 2.0 Chromium)', 33901 => obj([0 => 1, 1 => 511]), 33902 => obj([0 => 1, 1 => 1]), 34024 => 16384, 34076 => 16384, 34921 => 16, 34930 => 16, 35660 => 16, 35661 => 32, 35724 => 'WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)', 36347 => 1024, 36348 => 30, 36349 => 1024, ]), 'exts' => [ 'ANGLE_instanced_arrays', 'EXT_blend_minmax', 'EXT_clip_control', 'EXT_color_buffer_half_float', 'EXT_depth_clamp', 'EXT_disjoint_timer_query', 'EXT_float_blend', 'EXT_frag_depth', 'EXT_polygon_offset_clamp', 'EXT_shader_texture_lod', 'EXT_texture_compression_bptc', 'EXT_texture_compression_rgtc', 'EXT_texture_filter_anisotropic', 'EXT_texture_mirror_clamp_to_edge', 'EXT_sRGB', 'KHR_parallel_shader_compile', 'OES_element_index_uint', 'OES_fbo_render_mipmap', 'OES_standard_derivatives', 'OES_texture_float', 'OES_texture_float_linear', 'OES_texture_half_float', 'OES_texture_half_float_linear', 'OES_vertex_array_object', 'WEBGL_blend_func_extended', 'WEBGL_color_buffer_float', 'WEBGL_compressed_texture_s3tc', 'WEBGL_compressed_texture_s3tc_srgb', 'WEBGL_debug_renderer_info', 'WEBGL_debug_shaders', 'WEBGL_depth_texture', 'WEBGL_draw_buffers', 'WEBGL_lose_context', 'WEBGL_multi_draw', ], ], ]; } function indians_hash(): string { static $hash = null; if ($hash !== null) { return $hash; } $json = json_encode(indians_data(), JSON_UNESCAPED_SLASHES); if (!is_string($json)) { throw new RuntimeException('bad gid json'); } $hash = hash('sha256', $json); return $hash; } function embed_headers(string $slug, bool $token): array { $headers = [ 'Origin: ' . EMBED_ORIGIN, 'Referer: ' . embed_url($slug), 'Accept: */*', 'User-Agent: ' . BROWSER_UA, 'sec-ch-ua: ' . SEC_CH_UA, 'sec-ch-ua-mobile: ?0', 'sec-ch-ua-platform: ' . SEC_CH_UA_PLATFORM, ]; if ($token) { $headers[] = 'indians: ' . indians_hash(); } return $headers; } function die_soft(int $code, string $text): void { http_response_code($code); header('Content-Type: text/plain; charset=utf-8'); echo $text; exit; } function http_call(string $url, string $method = 'GET', ?string $body = null, array $headers = []): array { $ch = curl_init($url); $bag = []; curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => 8, CURLOPT_TIMEOUT => 18, CURLOPT_USERAGENT => BROWSER_UA, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_HEADERFUNCTION => static function ($ch, string $line) use (&$bag): int { $p = strpos($line, ':'); if ($p !== false) { $bag[strtolower(trim(substr($line, 0, (int)$p)))] = trim(substr($line, (int)$p + 1)); } return strlen($line); }, ]); if ($method !== 'GET') { curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? ''); } if ($headers) { curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); } $raw = curl_exec($ch); $err = curl_error($ch); $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($raw === false || $code < 200 || $code >= 300) { throw new RuntimeException($err !== '' ? $err : "http {$code}"); } return [$raw, $bag, $code]; } function stream_segment(string $url): void { @set_time_limit(0); while (ob_get_level() > 0) { @ob_end_flush(); } header('Content-Type: video/MP2T'); header('Cache-Control: public, max-age=3600'); header('X-Accel-Buffering: no'); header('Connection: close'); flush(); $buf = ''; $started = false; $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => false, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => 8, CURLOPT_TIMEOUT => 0, CURLOPT_USERAGENT => BROWSER_UA, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_HTTPHEADER => [ 'Accept: */*', 'Origin: ' . EMBED_ORIGIN, 'Referer: ' . EMBED_ORIGIN . '/', 'User-Agent: ' . BROWSER_UA, ], CURLOPT_WRITEFUNCTION => static function ($ch, string $chunk) use (&$buf, &$started): int { if ($started) { echo $chunk; flush(); return strlen($chunk); } $buf .= $chunk; $off = mpegts_offset($buf); if ($off > 0 || strlen($buf) >= 4096) { $started = true; echo substr($buf, $off); $buf = ''; flush(); } return strlen($chunk); }, ]); $ok = curl_exec($ch); $err = curl_error($ch); $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if (!$started && $buf !== '') { $off = mpegts_offset($buf); echo substr($buf, $off); flush(); } if ($ok === false || $code < 200 || $code >= 300) { throw new RuntimeException($err !== '' ? $err : "segment http {$code}"); } } function cache_dir(): string { $dir = sys_get_temp_dir() . '/ppv_fetch_cache'; if (!is_dir($dir)) { mkdir($dir, 0775, true); } return $dir; } function cache_path(string $kind, string $key): string { return cache_dir() . '/' . $kind . '-' . sha1($key) . '.json'; } function cache_get(string $kind, string $key) { $file = cache_path($kind, $key); if (!is_file($file)) { return null; } $raw = file_get_contents($file); $box = is_string($raw) ? json_decode($raw, true) : null; if (!is_array($box) || ($box['until'] ?? 0) <= time()) { @unlink($file); return null; } return $box['data'] ?? null; } function cache_set(string $kind, string $key, $data, int $until): void { if ($until <= time()) { return; } $file = cache_path($kind, $key); $tmp = $file . '.' . getmypid() . '.tmp'; file_put_contents($tmp, json_encode(['until' => $until, 'data' => $data], JSON_UNESCAPED_SLASHES), LOCK_EX); rename($tmp, $file); } function enc_varint(int $n): string { $s = ''; while (true) { $b = $n & 0x7f; $n >>= 7; if ($n === 0) { return $s . chr($b); } $s .= chr($b | 0x80); } } function get_varint(string $s, int &$i): int { $n = 0; $shift = 0; $len = strlen($s); while ($i < $len) { $b = ord($s[$i++]); $n |= ($b & 0x7f) << $shift; if ($b < 0x80) { return $n; } $shift += 7; if ($shift > 63) { throw new RuntimeException('bad varint'); } } throw new RuntimeException('short varint'); } function pb_put(int $field, string $value): string { return enc_varint(($field << 3) | 2) . enc_varint(strlen($value)) . $value; } function pb_read(string $s): array { $i = 0; $out = []; $len = strlen($s); while ($i < $len) { $tag = get_varint($s, $i); $field = $tag >> 3; $wire = $tag & 7; if ($wire !== 2) { throw new RuntimeException('bad wire'); } $n = get_varint($s, $i); if ($i + $n > $len) { throw new RuntimeException('short field'); } $out[$field] = substr($s, (int)$i, (int)$n); $i += $n; } return $out; } function shift_payload(string $s): string { $s = trim($s); $o = ''; $len = strlen($s); for ($i = 0; $i < $len; $i++) { $c = ord($s[$i]); $o .= ($c >= 33 && $c <= 126) ? chr((($c - 33 + 71) % 94) + 33) : $s[$i]; } return $o; } function u32(int $n): int { return $n & 0xffffffff; } function le32(string $s, int $i): int { return ord($s[$i]) | (ord($s[$i + 1]) << 8) | (ord($s[$i + 2]) << 16) | (ord($s[$i + 3]) << 24); } function put32(int $n): string { return pack('V', u32($n)); } function rotl32(int $n, int $bits): int { $n = u32($n); return u32(($n << $bits) | ($n >> (32 - $bits))); } function chacha_qr(array &$x, int $a, int $b, int $c, int $d): void { $x[$a] = u32($x[$a] + $x[$b]); $x[$d] = rotl32($x[$d] ^ $x[$a], 16); $x[$c] = u32($x[$c] + $x[$d]); $x[$b] = rotl32($x[$b] ^ $x[$c], 12); $x[$a] = u32($x[$a] + $x[$b]); $x[$d] = rotl32($x[$d] ^ $x[$a], 8); $x[$c] = u32($x[$c] + $x[$d]); $x[$b] = rotl32($x[$b] ^ $x[$c], 7); } function chacha_block(string $key, string $nonce, int $counter): string { $c = 'expand 32-byte k'; $s = [ le32($c, 0), le32($c, 4), le32($c, 8), le32($c, 12), le32($key, 0), le32($key, 4), le32($key, 8), le32($key, 12), le32($key, 16), le32($key, 20), le32($key, 24), le32($key, 28), u32($counter), le32($nonce, 0), le32($nonce, 4), le32($nonce, 8), ]; $x = $s; for ($i = 0; $i < 10; $i++) { chacha_qr($x, 0, 4, 8, 12); chacha_qr($x, 1, 5, 9, 13); chacha_qr($x, 2, 6, 10, 14); chacha_qr($x, 3, 7, 11, 15); chacha_qr($x, 0, 5, 10, 15); chacha_qr($x, 1, 6, 11, 12); chacha_qr($x, 2, 7, 8, 13); chacha_qr($x, 3, 4, 9, 14); } $out = ''; for ($i = 0; $i < 16; $i++) { $out .= put32($x[$i] + $s[$i]); } return $out; } function chacha_stream(string $key, string $nonce, int $len, int $counter): string { $out = ''; while (strlen($out) < $len) { $out .= chacha_block($key, $nonce, $counter); $counter = u32($counter + 1); } return substr($out, 0, $len); } function pad16(string $s): string { $n = strlen($s) % 16; return $n === 0 ? '' : str_repeat("\0", 16 - $n); } function le64(int $n): string { $lo = $n & 0xffffffff; $hi = intdiv($n, 4294967296); return pack('V2', $lo, $hi); } function poly1305_mac(string $msg, string $key): string { if (PHP_INT_SIZE < 8) { throw new RuntimeException('need 64-bit php'); } $t0 = le32($key, 0); $t1 = le32($key, 4); $t2 = le32($key, 8); $t3 = le32($key, 12); $r0 = $t0 & 0x3ffffff; $r1 = (($t0 >> 26) | ($t1 << 6)) & 0x3ffff03; $r2 = (($t1 >> 20) | ($t2 << 12)) & 0x3ffc0ff; $r3 = (($t2 >> 14) | ($t3 << 18)) & 0x3f03fff; $r4 = ($t3 >> 8) & 0x00fffff; $s1 = $r1 * 5; $s2 = $r2 * 5; $s3 = $r3 * 5; $s4 = $r4 * 5; $h0 = 0; $h1 = 0; $h2 = 0; $h3 = 0; $h4 = 0; $mask = 0x3ffffff; $len = strlen($msg); for ($pos = 0; $pos < $len; $pos += 16) { $n = min(16, $len - $pos); $block = substr($msg, $pos, $n); $hibit = 1 << 24; if ($n < 16) { $block .= "\x01" . str_repeat("\0", 15 - $n); $hibit = 0; } $t0 = le32($block, 0); $t1 = le32($block, 4); $t2 = le32($block, 8); $t3 = le32($block, 12); $h0 += $t0 & $mask; $h1 += (($t0 >> 26) | ($t1 << 6)) & $mask; $h2 += (($t1 >> 20) | ($t2 << 12)) & $mask; $h3 += (($t2 >> 14) | ($t3 << 18)) & $mask; $h4 += (($t3 >> 8) & 0x00ffffff) | $hibit; $d0 = ($h0 * $r0) + ($h1 * $s4) + ($h2 * $s3) + ($h3 * $s2) + ($h4 * $s1); $d1 = ($h0 * $r1) + ($h1 * $r0) + ($h2 * $s4) + ($h3 * $s3) + ($h4 * $s2); $d2 = ($h0 * $r2) + ($h1 * $r1) + ($h2 * $r0) + ($h3 * $s4) + ($h4 * $s3); $d3 = ($h0 * $r3) + ($h1 * $r2) + ($h2 * $r1) + ($h3 * $r0) + ($h4 * $s4); $d4 = ($h0 * $r4) + ($h1 * $r3) + ($h2 * $r2) + ($h3 * $r1) + ($h4 * $r0); $c = $d0 >> 26; $h0 = $d0 & $mask; $d1 += $c; $c = $d1 >> 26; $h1 = $d1 & $mask; $d2 += $c; $c = $d2 >> 26; $h2 = $d2 & $mask; $d3 += $c; $c = $d3 >> 26; $h3 = $d3 & $mask; $d4 += $c; $c = $d4 >> 26; $h4 = $d4 & $mask; $h0 += $c * 5; $c = $h0 >> 26; $h0 &= $mask; $h1 += $c; } $c = $h1 >> 26; $h1 &= $mask; $h2 += $c; $c = $h2 >> 26; $h2 &= $mask; $h3 += $c; $c = $h3 >> 26; $h3 &= $mask; $h4 += $c; $c = $h4 >> 26; $h4 &= $mask; $h0 += $c * 5; $c = $h0 >> 26; $h0 &= $mask; $h1 += $c; $g0 = $h0 + 5; $c = $g0 >> 26; $g0 &= $mask; $g1 = $h1 + $c; $c = $g1 >> 26; $g1 &= $mask; $g2 = $h2 + $c; $c = $g2 >> 26; $g2 &= $mask; $g3 = $h3 + $c; $c = $g3 >> 26; $g3 &= $mask; $g4 = $h4 + $c - (1 << 26); if ($g4 >= 0) { $h0 = $g0; $h1 = $g1; $h2 = $g2; $h3 = $g3; $h4 = $g4; } $f0 = (($h0 | ($h1 << 26)) & 0xffffffff) + le32($key, 16); $w0 = $f0 & 0xffffffff; $f1 = ((($h1 >> 6) | ($h2 << 20)) & 0xffffffff) + le32($key, 20) + ($f0 >> 32); $w1 = $f1 & 0xffffffff; $f2 = ((($h2 >> 12) | ($h3 << 14)) & 0xffffffff) + le32($key, 24) + ($f1 >> 32); $w2 = $f2 & 0xffffffff; $f3 = ((($h3 >> 18) | ($h4 << 8)) & 0xffffffff) + le32($key, 28) + ($f2 >> 32); $w3 = $f3 & 0xffffffff; return put32($w0) . put32($w1) . put32($w2) . put32($w3); } function aead_input(string $aad, string $ciphertext): string { return $aad . pad16($aad) . $ciphertext . pad16($ciphertext) . le64(strlen($aad)) . le64(strlen($ciphertext)); } function bxor_stream(string $a, string $b): string { $out = ''; $len = strlen($a); for ($i = 0; $i < $len; $i++) { $out .= chr(ord($a[$i]) ^ ord($b[$i])); } return $out; } function open_payload(string $payload, string $island): string { $packed = base64_decode(shift_payload($payload), true); if ($packed === false || strlen($packed) < 28) { throw new RuntimeException('bad payload'); } $nonce = substr($packed, 0, 12); $box = substr($packed, 12); $key = substr($island, 0, 32); $key = str_pad($key, 32, "\0"); $tag = substr($box, -16); $ciphertext = substr($box, 0, -16); $poly_key = chacha_stream($key, $nonce, 32, 0); $got = poly1305_mac(aead_input('', $ciphertext), $poly_key); if (!hash_equals($got, $tag)) { throw new RuntimeException('bad tag'); } return bxor_stream($ciphertext, chacha_stream($key, $nonce, strlen($ciphertext), 1)); } function room_slug(string $id): string { $direct = input_slug($id); if (is_string($direct) && $direct !== '') { return $direct; } $hit = cache_get('room', $id); if (is_string($hit) && $hit !== '') { return $hit; } [$json] = http_call(PPV_STREAMS); $data = json_decode($json, true); if (!is_array($data)) { throw new RuntimeException('bad streams json'); } $want = preg_replace('/^ppv-/', '', trim($id)); foreach (($data['streams'] ?? []) as $cat) { foreach (($cat['streams'] ?? []) as $item) { $num = isset($item['id']) ? (string)$item['id'] : ''; $slug = stream_slug($item); if ($num === $want || ('ppv-' . $num) === $id || $slug === $id) { if ($slug !== '') { cache_set('room', $id, $slug, time() + ROOM_TTL); return $slug; } } // 检查子流 foreach (($item['substreams'] ?? []) as $sub) { $subNum = isset($sub['id']) ? (string)$sub['id'] : ''; $subSlug = stream_slug($sub); if ($subNum === $want || ('ppv-' . $subNum) === $id || $subSlug === $id) { if ($subSlug !== '') { cache_set('room', $id, $subSlug, time() + ROOM_TTL); return $subSlug; } } } } } throw new RuntimeException('room not found'); } function fresh_url(string $slug): string { $body = pb_put(1, $slug); [$bin, $headers] = http_call(EMBED_FETCH, 'POST', $body, array_merge(['Content-Type: application/octet-stream'], embed_headers($slug, true))); $island = $headers['island'] ?? ''; if ($island === '') { throw new RuntimeException('no island'); } $fields = pb_read($bin); if (!isset($fields[1])) { throw new RuntimeException('no payload'); } return open_payload($fields[1], $island); } function hls_get(string $url, string $slug): string { [$text] = http_call($url, 'GET', null, embed_headers($slug, false)); if (!starts_with(ltrim($text), '#EXTM3U')) { throw new RuntimeException('not m3u8'); } return $text; } function dot_path(string $path): string { $lead = starts_with($path, '/'); $tail = ends_with($path, '/'); $stack = []; foreach (explode('/', $path) as $part) { if ($part === '' || $part === '.') { continue; } if ($part === '..') { array_pop($stack); continue; } $stack[] = $part; } $out = ($lead ? '/' : '') . implode('/', $stack); return $tail && $out !== '/' ? $out . '/' : ($out === '' ? ($lead ? '/' : '') : $out); } function url_join(string $base, string $ref): string { $ref = trim($ref); if ($ref === '' || preg_match('~^[a-z][a-z0-9+.-]*:~i', $ref)) { return $ref; } $b = parse_url($base); if (!$b || empty($b['scheme']) || empty($b['host'])) { throw new RuntimeException('bad base url'); } if (starts_with($ref, '//')) { return $b['scheme'] . ':' . $ref; } $scheme = $b['scheme']; $host = $b['host']; $port = isset($b['port']) ? ':' . $b['port'] : ''; if (starts_with($ref, '/')) { return "{$scheme}://{$host}{$port}" . dot_path($ref); } $dir = isset($b['path']) ? preg_replace('~/[^/]*$~', '/', $b['path']) : '/'; return "{$scheme}://{$host}{$port}" . dot_path($dir . $ref); } function quote_uris(string $line, string $base): string { return preg_replace_callback('/URI="([^"]+)"/', static function (array $m) use ($base): string { return 'URI="' . url_join($base, $m[1]) . '"'; }, $line); } function maybe_m3u8(string $uri): bool { $path = parse_url($uri, PHP_URL_PATH); return stripos($path !== false && $path !== null ? $path : $uri, '.m3u8') !== false; } function stream_score(string $line): int { $score = 0; if (preg_match('/BANDWIDTH=(\d+)/', $line, $m)) { $score += (int)$m[1]; } if (preg_match('/RESOLUTION=(\d+)x(\d+)/', $line, $m)) { $score += ((int)$m[1] * (int)$m[2]) * 10; } return $score; } function m3u8_refs(string $text, string $base): array { $refs = []; $pending = 0; foreach (explode("\n", str_replace(["\r\n", "\r"], "\n", $text)) as $line) { $trim = trim($line); if ($trim === '') { continue; } if (starts_with($trim, '#')) { if (starts_with($trim, '#EXT-X-STREAM-INF')) { $pending = stream_score($trim); } if (has_text($line, 'URI="')) { preg_match_all('/URI="([^"]+)"/', $line, $hits); foreach ($hits[1] ?? [] as $uri) { if (maybe_m3u8($uri)) { $refs[] = [ 'url' => url_join($base, $uri), 'rank' => has_text($trim, 'I-FRAME') ? 0 : 1, 'score' => stream_score($trim), ]; } } } continue; } if (maybe_m3u8($trim)) { $refs[] = [ 'url' => url_join($base, $trim), 'rank' => $pending > 0 ? 3 : 2, 'score' => $pending, ]; } $pending = 0; } usort($refs, static function (array $a, array $b): int { return ($b['rank'] <=> $a['rank']) ?: ($b['score'] <=> $a['score']); }); return $refs; } function final_m3u8(string $url, string $slug): array { $seen = []; for ($i = 0; $i < 8; $i++) { if (isset($seen[$url])) { throw new RuntimeException('m3u8 loop'); } $seen[$url] = true; $text = hls_get($url, $slug); $refs = m3u8_refs($text, $url); if (!$refs) { return [$text, $url]; } $url = $refs[0]['url']; } throw new RuntimeException('m3u8 too deep'); } function secure_until(string $url): ?int { $path = parse_url($url, PHP_URL_PATH); if (!is_string($path)) { return null; } $parts = explode('/', $path); $n = count($parts); for ($i = 0; $i < $n; $i++) { if ($parts[$i] === 'secure' && isset($parts[$i + 3]) && ctype_digit($parts[$i + 3])) { return ((int)$parts[$i + 3]) - TOKEN_MARGIN; } } return null; } function source_until(string $source, string $final): int { $until = time() + SOURCE_TTL; foreach ([$source, $final] as $url) { $end = secure_until($url); if ($end !== null) { $until = min($until, $end); } } return max(time() + 20, $until); } function live_m3u8(string $slug): array { $hit = cache_get('source', $slug); if (is_array($hit) && isset($hit['final']) && is_string($hit['final'])) { try { $text = hls_get($hit['final'], $slug); if (!m3u8_refs($text, $hit['final'])) { return [$text, $hit['final'], 'HIT']; } } catch (Throwable $e) { } } $source = fresh_url($slug); [$text, $final] = final_m3u8($source, $slug); cache_set('source', $slug, ['source' => $source, 'final' => $final], source_until($source, $final)); return [$text, $final, 'MISS']; } function abs_m3u8(string $text, string $base): string { $text = str_replace(["\r\n", "\r"], "\n", $text); $out = []; foreach (explode("\n", $text) as $line) { $trim = trim($line); if ($trim === '') { $out[] = $line; } elseif (starts_with($trim, '#')) { $out[] = has_text($line, 'URI="') ? quote_uris($line, $base) : $line; } else { $url = url_join($base, $trim); $out[] = segment_needs_proxy($url) ? segment_proxy_url($url) : $url; } } return rtrim(implode("\n", $out)) . "\n"; } function segment_needs_proxy(string $url): bool { $path = parse_url($url, PHP_URL_PATH); if (!is_string($path)) { return false; } $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); return $ext === ''; } function segment_proxy_url(string $url): string { return self_url() . '?seg=' . rawurlencode($url) . '&ext=.ts'; } function mpegts_offset(string $bin): int { $len = strlen($bin); $max = min(4096, $len - 188 * 5); for ($off = 0; $off <= $max; $off++) { $ok = true; for ($i = 0; $i < 5; $i++) { if (ord($bin[$off + $i * 188]) !== 0x47) { $ok = false; break; } } if ($ok) { return $off; } } return 0; } function output_segment(string $url): void { if (!preg_match('~^https?://~i', $url)) { die_soft(400, 'bad segment url'); } stream_segment($url); } // ==================== 列表功能 ==================== function self_url(): string { $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $uri = $_SERVER['REQUEST_URI'] ?? '/'; $path = parse_url($uri, PHP_URL_PATH) ?? '/'; return "{$scheme}://{$host}{$path}"; } function fetch_streams(): array { $hit = cache_get('streams_list', 'all'); if (is_array($hit)) { return $hit; } [$json] = http_call(PPV_STREAMS); $data = json_decode($json, true); if (!is_array($data) || !isset($data['streams'])) { throw new RuntimeException('bad streams json'); } cache_set('streams_list', 'all', $data['streams'], time() + 300); return $data['streams']; } // 将Unix时间戳转换为北京时间字符串 (格式: m/d H:i) function format_beijing_time(int $timestamp): string { if ($timestamp <= 0) { return ''; } $dt = new DateTime('@' . $timestamp); $dt->setTimezone(new DateTimeZone('Asia/Shanghai')); return $dt->format('n/j H:i'); } function build_channels(array $streamsData): array { $channels = []; foreach ($streamsData as $cat) { $category = $cat['category'] ?? 'Unknown'; foreach (($cat['streams'] ?? []) as $item) { $uri = stream_slug($item); if ($uri === '') continue; $startTime = format_beijing_time($item['starts_at'] ?? 0); $channels[] = [ 'id' => 'ppv-' . ($item['id'] ?? ''), 'name' => $item['name'] ?? 'Unknown', 'category' => $category, 'uri_name' => $uri, 'logo' => $item['poster'] ?? '', 'tag' => $item['tag'] ?? '', 'always_live' => ($item['always_live'] ?? 0) === 1, 'start_time' => $startTime, // 比赛开始时间 ]; // 子流 foreach (($item['substreams'] ?? []) as $sub) { $subUri = stream_slug($sub); if ($subUri === '') continue; // 子流使用主流的开始时间 $channels[] = [ 'id' => 'ppv-' . ($sub['id'] ?? ''), 'name' => ($sub['name'] ?? 'Unknown') . ' [' . ($sub['tag'] ?? 'Sub') . ']', 'category' => $category, 'uri_name' => $subUri, 'logo' => $sub['poster'] ?? ($item['poster'] ?? ''), 'tag' => $sub['tag'] ?? '', 'always_live' => ($item['always_live'] ?? 0) === 1, 'start_time' => $startTime, ]; } } } return $channels; } function output_json(array $channels): void { header('Content-Type: application/json; charset=utf-8'); $base = self_url(); $out = []; foreach ($channels as $c) { $out[] = [ 'id' => $c['id'], 'name' => $c['name'], 'category' => $c['category'], 'uri_name' => $c['uri_name'], 'logo' => $c['logo'], 'start_time' => $c['start_time'], // 包含开始时间 'play_url' => $base . '?id=' . rawurlencode($c['uri_name']), ]; } echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } function output_m3u(array $channels): void { header('Content-Type: application/vnd.apple.mpegurl; charset=utf-8'); header('Content-Disposition: attachment; filename="ppv.m3u"'); $base = self_url(); $key = $base . '|m3u'; $hit = cache_get('playlist_m3u', $key); if (is_string($hit)) { header('X-PPV-Playlist-Cache: HIT'); echo $hit; return; } $text = "#EXTM3U\n"; foreach ($channels as $c) { $logo = $c['logo'] ? ' tvg-logo="' . htmlspecialchars($c['logo']) . '"' : ''; $group = htmlspecialchars($c['category']); // 在频道名称前加上比赛开始时间 [m/d H:i] $timePrefix = $c['start_time'] ? '[' . $c['start_time'] . '] ' : ''; $name = htmlspecialchars($timePrefix . $c['name']); $text .= "#EXTINF:-1{$logo} group-title=\"{$group}\",{$name}\n"; $text .= $base . '?id=' . rawurlencode($c['uri_name']) . "\n"; } cache_set('playlist_m3u', $key, $text, time() + PLAYLIST_TTL); header('X-PPV-Playlist-Cache: MISS'); echo $text; } function output_txt(array $channels): void { header('Content-Type: text/plain; charset=utf-8'); $base = self_url(); $key = $base . '|txt'; $hit = cache_get('playlist_txt', $key); if (is_string($hit)) { header('X-PPV-Playlist-Cache: HIT'); echo $hit; return; } $currentCat = ''; $text = ''; foreach ($channels as $c) { if ($c['category'] !== $currentCat) { $currentCat = $c['category']; $text .= "\n{$currentCat},#genre#\n"; } $timePrefix = $c['start_time'] ? '[' . $c['start_time'] . '] ' : ''; $name = str_replace(',', ',', $timePrefix . $c['name']); $text .= "{$name},{$base}?id=" . rawurlencode($c['uri_name']) . "\n"; } cache_set('playlist_txt', $key, $text, time() + PLAYLIST_TTL); header('X-PPV-Playlist-Cache: MISS'); echo $text; } // ==================== 主逻辑 ==================== if (isset($_GET['seg'])) { try { output_segment(trim((string)$_GET['seg'])); exit; } catch (Throwable $e) { die_soft(502, $e->getMessage()); } } // 列表模式 if (isset($_GET['list'])) { try { $streams = fetch_streams(); $channels = build_channels($streams); output_json($channels); exit; } catch (Throwable $e) { die_soft(502, $e->getMessage()); } } if (isset($_GET['m3u'])) { try { $streams = fetch_streams(); $channels = build_channels($streams); output_m3u($channels); exit; } catch (Throwable $e) { die_soft(502, $e->getMessage()); } } if (isset($_GET['txt'])) { try { $streams = fetch_streams(); $channels = build_channels($streams); output_txt($channels); exit; } catch (Throwable $e) { die_soft(502, $e->getMessage()); } } // 播放单频道 $id = isset($_GET['id']) ? trim((string)$_GET['id']) : DEFAULT_ID; if ($id === '') { $id = DEFAULT_ID; } try { $slug = room_slug($id); [$m3u8, $base, $cache] = live_m3u8($slug); header('Content-Type: application/vnd.apple.mpegurl; charset=utf-8'); header('Cache-Control: no-store'); header('X-PPV-Cache: ' . $cache); echo abs_m3u8($m3u8, $base); } catch (Throwable $e) { die_soft(502, $e->getMessage()); }