Files
tvbox_collect/jaychouqq/yingshi/php/ppv.php
T
2026-07-13 15:46:17 +00:00

1183 lines
34 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
/*
首发地址:https://t.me/iptvofficalgroup
转发或转化本代码,务必注明出处,否则生个孩子没屁眼
使用方法:
M3U列表: /ppv.php?m3u=1
TXT列表: /ppv.php?txt=1
*/
const PPV_STREAMS = 'https://api.ppv.st/api/streams';
const EMBED_FETCH = 'https://embedindia.st/fetch';
const EMBED_ORIGIN = 'https://embedindia.st';
const BROWSER_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
const SEC_CH_UA = '"Google Chrome";v="148", "Chromium";v="148", "Not)A;Brand";v="24"';
const SEC_CH_UA_PLATFORM = '"Windows"';
const DEFAULT_ID = 'rally-tv';
const ROOM_TTL = 600;
const SOURCE_TTL = 300;
const TOKEN_MARGIN = 90;
const PLAYLIST_TTL = 600;
function starts_with(string $s, string $prefix): bool
{
return strncmp($s, $prefix, strlen($prefix)) === 0;
}
function ends_with(string $s, string $suffix): bool
{
$n = strlen($suffix);
return $n === 0 || substr($s, -$n) === $suffix;
}
function has_text(string $s, string $needle): bool
{
return strpos($s, $needle) !== false;
}
function path_slug(string $slug): string
{
$parts = [];
foreach (explode('/', trim($slug, '/')) as $part) {
if ($part !== '') {
$parts[] = rawurlencode($part);
}
}
return implode('/', $parts);
}
function embed_url(string $slug): string
{
return EMBED_ORIGIN . '/embed/' . path_slug($slug);
}
function normalize_embed_slug(string $slug): string
{
$slug = trim($slug);
if ($slug === '') {
return '';
}
if (preg_match('~^[a-z][a-z0-9+.-]*://~i', $slug)) {
$path = parse_url($slug, PHP_URL_PATH);
if (!is_string($path)) {
return '';
}
$slug = $path;
} else {
$cut = strcspn($slug, '?#');
$slug = substr($slug, 0, $cut);
}
$slug = trim(urldecode($slug), '/');
if (starts_with($slug, 'embed/')) {
$slug = substr($slug, 6);
}
return trim($slug, '/');
}
function input_slug(string $id): ?string
{
$id = trim($id);
if ($id === '') {
return null;
}
$path = parse_url($id, PHP_URL_PATH);
if (is_string($path) && preg_match('~/embed/(.+)$~', $path, $m)) {
return normalize_embed_slug($m[1]);
}
if (strpos($id, '/') !== false && !preg_match('~^[a-z][a-z0-9+.-]*://~i', $id)) {
return normalize_embed_slug($id);
}
return null;
}
function stream_slug(array $item): string
{
$slug = normalize_embed_slug((string)($item['uri_name'] ?? ''));
if ($slug !== '') {
return $slug;
}
$iframe = input_slug((string)($item['iframe'] ?? ''));
return is_string($iframe) ? $iframe : '';
}
function obj(array $pairs): stdClass
{
$o = new stdClass();
foreach ($pairs as $k => $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());
}