一行代码,让音乐播放器拥有频谱特效
在你的 HTML5 音乐播放器里,想要加上酷炫的实时频谱可视化效果?传统方案需要引入 CSS、创建容器、加载多个 JS 文件、手动对接播放器接口…… 现在,我们只需 一行 script 标签 ,就能完成所有工作。
核心用法
在页面 <head> 或 <body> 任意位置插入:
<script src="https://bbs.cnzv.cc/v/waveform/spectrum.js" data-effect="1"></script>
就这一行。没有 ` `,没有额外 `
`,没有任何插件初始化代码。
删除这一行,频谱完全消失,播放不受任何影响 。
背后做了什么
spectrum.js 作为唯一入口,自动完成四件事:
自动定位音频 :实时查找页面中的 <audio> 元素,即使播放器用 innerHTML 重建 DOM 也能找到
自动注入样式 :动态创建 <link> 标签,加载同目录的 waveform.css
自动挂载容器 :创建 <div id="spectrum"> 并自动挂载到音频父容器
自动加载插件 :动态 <script> 引入 waveform.js,完成初始化并开始绘制
为什么能一行搞定?
零耦合的外挂设计
传统播放器插件要求页面必须引入特定接口(如 LMP34),而 spectrum.js 内部 临时模拟了一套 LMP34 兼容壳 ——插件加载期间接管 window.LMP34.registerPlugin,插件载入完成后立即还原,不污染页面任何现有变量 。
自愈机制:不怕 DOM 重建
歌词播放器常常用 innerHTML += 把整个 DOM 重建一遍,这会导致频谱画布变成「幽灵节点」——看得见但不工作。spectrum.js 用了三层防护:
MutationObserver 监听容器子节点变化,自动触发重建
800ms 心跳检测 ,确认 canvas 是否还在 document.body 中
音频元素变化检测 ,换歌重建时自动重新绑定
参数大全
data-* 属性(写在 script 标签上)
参数 默认值 说明
data-effect3频谱效果编号 1~18
data-audio第一个 <audio> 指定音频选择器,如 #aud
data-containeraudio 的父元素 频谱挂载容器
data-color#06F406频谱颜色(十六进制)
data-glow1发光强度 0=关 / 1~3
data-line2频谱条粗细(像素)
data-label1显示频率标签 0/1
data-off— 设为 1 强制关闭频谱
URL 参数(优先级高于 data-*)
参数 覆盖项 示例
spectrum_iddata-effect?spectrum_id=7
wf_colordata-color?wf_color=ff6600
wf_glowdata-glow?wf_glow=0
wf_linedata-line?wf_line=3
nospec— URL 里有就强制关闭
优先级链 :URL 参数 > data-* 属性 > 代码内置默认值
crossorigin 的必要性
用跨域外链音频时,<audio> 标签必须写:
<audio src="https://xxx.mp3" crossorigin="anonymous" autoplay loop></audio>
原因:浏览器默认用 no-cors 模式取跨域音频,音频被标记为「非 CORS 同源」——能正常出声,但频谱永远一片空白 (MediaElementSource 拿到的是静音轨)。加了 crossorigin="anonymous" 后,浏览器会发带 Origin 的请求,源站返回 Access-Control-Allow-Origin: *,音频才会被标记为 CORS 同源,频谱才能读到真实 PCM 数据。
注意 :crossorigin 必须写在 HTML 标签里,资源开始加载后 JS 再补属性已经来不及了 。
音源 需要 crossorigin? 播放 频谱
跨域外链 必须 ✅ ❌ 没写则空白
同源本地 不需要 ✅ ✅
代理后 不需要 ✅ ✅
关闭频谱的三种方式
删掉那一行 script — 页面干净无痕
script 加 data-off="1" — <script src="https://bbs.cnzv.cc/v/waveform/spectrum.js" data-off="1" ></script>
URL 加 ?nospec — 调试时临时关闭
所有关闭方式都保证 播放 100% 不受影响 。
设计哲学总结
外挂模式的核心:不侵入页面、不污染作用域、可随时删除、播放零依赖 。频谱是锦上添花的特效,不是播放器的必需品。把「接入成本」降到一行代码,把「移除成本」降到删掉一行,才是真正优雅的插件设计。
重点 重点 重点 跨域音乐不是每个都支持 经过测试 这个音乐地址可以 你们可以去这个网盘 https://imgimg.qqdsw8.cn/view.php/9b8781a7fd14b6bbce3102a2b84afbd4.mp3 原因:浏览器默认用 no-cors 模式取跨域音频,音频被标记为「非 CORS 同源」—— 能正常出声,但频谱永远一片空白 也有解决方法 但是需要你们自己找服务器配置代码 将下面代码保存为 xxxphp?url=音乐地址就能解决 <?php /** * Proxy.php —— Web Audio 频谱分析资源代理网关 * * 功能:本地/远程音频代理转发、CORS 跨域、Range 分块、DNS 污染绕过、 * 自动编码转换(GBK→UTF-8)、多 IP 自动 fallback、Debug 调试面板 * * @author LMP34 * @date 2026-10-24 * @link https://www.cnlog.cc/ * @require PHP 5.4+ / curl / mbstring / allow_url_fopen=On */ error_reporting(E_ALL); ini_set('display_errors', 0); ini_set('log_errors', 0); //ini_set('error_log', __DIR__ . '/proxy_error.log'); $url = isset($_GET['url']) ? $_GET['url'] : ''; if ($url === '') { header('HTTP/1.1 400 Bad Request'); exit('no url'); } header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, HEAD, OPTIONS'); header('Access-Control-Allow-Headers: Range, Content-Type'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { header('HTTP/1.1 204 No Content'); exit; } $hasRange = isset($_SERVER['HTTP_RANGE']) && $_SERVER['HTTP_RANGE'] !== ''; $debug = isset($_GET['debug']) ? true : false; $debugLog = array(); function proxyLog($msg) { global $debugLog; $debugLog[] = $msg; // @file_put_contents(__DIR__ . '/proxy_error.log', date('Y-m-d H:i:s') . ' ' . $msg . "\n", FILE_APPEND); } $urlParts = parse_url($url); $isRemoteProxy = isset($urlParts['scheme']) && ($urlParts['scheme'] === 'http' || $urlParts['scheme'] === 'https') && isset($urlParts['host']) && $urlParts['host'] !== $_SERVER['HTTP_HOST']; // 服务端可执行扩展名:必须通过 HTTP 请求执行,不能当静态文件读取源码 $execExts = array('php', 'php5', 'php7', 'phtml', 'phar', 'cgi', 'pl', 'py', 'rb', 'asp', 'aspx', 'jsp', 'jspx'); $localIsExec = false; if (!$isRemoteProxy && isset($urlParts['path'])) { $localFile = __DIR__ . '/' . ltrim($urlParts['path'], '/'); $localExt = strtolower(pathinfo($localFile, PATHINFO_EXTENSION)); $localIsExec = in_array($localExt, $execExts, true); } if (!$isRemoteProxy && isset($urlParts['path']) && !$localIsExec) { $localFile = __DIR__ . '/' . ltrim($urlParts['path'], '/'); if (file_exists($localFile) && is_file($localFile)) { proxyLog("LOCAL FILE: " . $localFile); $ext = strtolower(pathinfo($localFile, PATHINFO_EXTENSION)); $audioExts = array('mp3'=>'audio/mpeg','wav'=>'audio/wav','ogg'=>'audio/ogg','m4a'=>'audio/mp4','aac'=>'audio/aac','flac'=>'audio/flac','wma'=>'audio/x-ms-wma'); $videoExts = array('mp4'=>'video/mp4','webm'=>'video/webm','ogv'=>'video/ogg','mov'=>'video/quicktime','m4v'=>'video/x-m4v'); $textExts = array('lrc'=>'text/plain','txt'=>'text/plain','srt'=>'text/plain','ass'=>'text/plain','vtt'=>'text/plain','xml'=>'application/xml','json'=>'application/json','css'=>'text/css','js'=>'application/javascript','html'=>'text/html','htm'=>'text/html','ini'=>'text/plain','csv'=>'text/plain','log'=>'text/plain','md'=>'text/plain','smi'=>'text/plain','krc'=>'text/plain'); $ct = 'application/octet-stream'; if (isset($audioExts[$ext])) $ct = $audioExts[$ext]; elseif (isset($videoExts[$ext])) $ct = $videoExts[$ext]; elseif (isset($textExts[$ext])) $ct = $textExts[$ext] . '; charset=utf-8'; $size = filesize($localFile); if ($hasRange) { if (preg_match('/bytes=(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) { $start = ($m[1] !== '') ? intval($m[1]) : 0; $end = ($m[2] !== '') ? intval($m[2]) : $size - 1; if ($start > $end || $start >= $size) { header('HTTP/1.1 416 Requested Range Not Satisfiable'); header('Content-Range: bytes */' . $size); exit; } header('Content-Type: ' . $ct); header('HTTP/1.1 206 Partial Content'); header('Content-Length: ' . ($end - $start + 1)); header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size); $fp = fopen($localFile, 'rb'); fseek($fp, $start); echo fread($fp, $end - $start + 1); fclose($fp); exit; } } if (isset($textExts[$ext])) { $content = file_get_contents($localFile); if ($content !== false && function_exists('mb_detect_encoding')) { $detected = mb_detect_encoding($content, array('UTF-8', 'GBK', 'GB2312', 'GB18030', 'BIG5', 'UTF-7', 'ASCII'), true); if ($detected && strtoupper($detected) !== 'UTF-8' && function_exists('mb_convert_encoding')) { proxyLog("LOCAL ENCODING: detect=$detected converting to UTF-8"); $content = mb_convert_encoding($content, 'UTF-8', $detected); } elseif (!$detected && function_exists('mb_convert_encoding')) { $content2 = @mb_convert_encoding($content, 'UTF-8', 'GBK'); if ($content2 !== false && $content2 !== $content) { proxyLog("LOCAL ENCODING: fallback GBK->UTF-8"); $content = $content2; } } } header('Content-Type: ' . $ct); header('Content-Length: ' . strlen($content)); echo $content; exit; } header('Content-Type: ' . $ct); header('Accept-Ranges: bytes'); header('Content-Length: ' . $size); readfile($localFile); exit; } } function debugDump($content, $httpCode, $errMsg) { global $debugLog, $url; header('Content-Type: text/html; charset=utf-8'); echo '<pre style="font-family:Consolas,monospace;font-size:13px;color:#eee;background:#1a1a2e;padding:20px;white-space:pre-wrap;word-break:break-all;">'; echo '<b style="color:#ffcc66;">=== LMP34 Proxy Debug ===</b>' . "\n\n"; echo '<b>INPUT URL:</b> ' . htmlspecialchars($url) . "\n\n"; echo '<b>LOG:</b>' . "\n"; foreach ($debugLog as $line) echo ' ' . htmlspecialchars($line) . "\n"; echo "\n" . '<b>HTTP CODE:</b> ' . $httpCode . "\n"; echo '<b>CURL ERROR:</b> ' . ($errMsg ? htmlspecialchars($errMsg) : '(none)') . "\n"; echo '<b>CONTENT LEN:</b> ' . strlen($content) . "\n"; if (strlen($content) > 0) { echo "\n<b>CONTENT PREVIEW (first 2000 bytes):</b>\n"; echo htmlspecialchars(substr($content, 0, 2000)); if (strlen($content) > 2000) echo "\n\n... (truncated, total " . strlen($content) . " bytes)"; } echo '</pre>'; exit; } /** * 绕过服务器 DNS 污染:hosts 硬编码优先 + 公共 DNS 兜底 * 多个 IP 自动降级尝试 */ function resolveWithPublicDns($host) { if (filter_var($host, FILTER_VALIDATE_IP)) return false; $hostsMap = array( 'www.panurl.cn' => array('104.16.36.185', '104.20.61.142'), ); if (isset($hostsMap[$host])) { $list = $hostsMap[$host]; proxyLog("DNS-resolve via hosts-map: {$host} => " . implode(', ', $list)); return $list; } // 国内 DNS 优先(8.8.8.8 在国内常被干扰超时,放最后兜底) $dnsServers = array('223.5.5.5', '119.29.29.29', '8.8.8.8'); if (function_exists('shell_exec')) { foreach ($dnsServers as $dns) { $out = @shell_exec("dig +short @{$dns} {$host} A 2>/dev/null"); if ($out) { $ips = array_filter(array_map('trim', explode("\n", $out)), function($ip) { return filter_var($ip, FILTER_VALIDATE_IP); }); if (!empty($ips)) { proxyLog("DNS-resolve via dig @{$dns}: {$host} => " . implode(', ', $ips)); return array_values($ips); } } } foreach ($dnsServers as $dns) { $out = @shell_exec("nslookup {$host} {$dns} 2>/dev/null"); if ($out && preg_match_all('/Address:\s*(\d+\.\d+\.\d+\.\d+)/', $out, $m)) { $ips = array_unique($m[1]); proxyLog("DNS-resolve via nslookup @{$dns}: {$host} => " . implode(', ', $ips)); return array_values($ips); } } } $ip = @gethostbyname($host); if ($ip && $ip !== $host && filter_var($ip, FILTER_VALIDATE_IP)) { proxyLog("DNS-resolve via gethostbyname: {$host} -> {$ip}"); return array($ip); } proxyLog("DNS-resolve FAILED for {$host}"); return false; } /** * 流式抓取远程资源:curl 边下边直接转发给客户端,首字节即达。 * 解决旧逻辑(CURLOPT_RETURNTRANSFER 整文件缓冲)导致的开播前长时间"缓冲"。 * 返回 true=已开始流式输出(此后调用方必须 exit);false=未输出任何内容,可安全回退 */ function proxy_stream_fetch($url, $defaultCt, $resolveMap) { if (!function_exists('curl_init')) return false; $rangeHeader = isset($_SERVER['HTTP_RANGE']) ? $_SERVER['HTTP_RANGE'] : ''; $blockHeaders = array(); // 当前响应头块(重定向时会有多块,只取最后一块) $forwarded = false; // 响应头是否已转发 $bodyBytes = 0; // 已输出字节数 $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_MAXREDIRS, 10); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); curl_setopt($ch, CURLOPT_REFERER, dirname($url) . '/'); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 60); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); $reqHeaders = array('Accept: */*'); if ($rangeHeader !== '') $reqHeaders[] = 'Range: ' . $rangeHeader; curl_setopt($ch, CURLOPT_HTTPHEADER, $reqHeaders); if (is_array($resolveMap) && !empty($resolveMap)) { curl_setopt($ch, CURLOPT_RESOLVE, $resolveMap); } curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $line) use (&$blockHeaders) { $len = strlen($line); $t = trim($line); if ($t === '') return $len; if (stripos($t, 'HTTP/') === 0) { $blockHeaders = array(); return $len; } // 新响应块(重定向/100-continue) if (strpos($t, ':') !== false) { list($k, $v) = explode(':', $t, 2); $blockHeaders[strtolower(trim($k))] = trim($v); } return $len; }); curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $data) use (&$forwarded, &$blockHeaders, &$bodyBytes, $defaultCt) { if (!$forwarded) { $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($code >= 400) return 0; // 错误页不走流式,中止让 buffered 兜底输出规范错误 $forwarded = true; http_response_code($code === 206 ? 206 : 200); header('Content-Type: ' . (isset($blockHeaders['content-type']) ? $blockHeaders['content-type'] : $defaultCt)); header('Accept-Ranges: bytes'); if (isset($blockHeaders['content-length'])) header('Content-Length: ' . $blockHeaders['content-length']); if (isset($blockHeaders['content-range'])) header('Content-Range: ' . $blockHeaders['content-range']); } $bodyBytes += strlen($data); echo $data; @flush(); return strlen($data); }); curl_exec($ch); $errno = curl_errno($ch); proxyLog("STREAM: errno=$errno forwarded=" . ($forwarded ? 1 : 0) . " bytes=$bodyBytes url=$url"); curl_close($ch); // 已开始输出则成功;未输出任何内容则允许调用方回退 return $forwarded; } // 公共 DNS 兜底解析改为懒加载:仅在 curl 自身解析/连接失败后才做外部 DNS 探测 function ensureResolveMap($host, $port) { global $resolvedIps, $resolveMap; if (!empty($resolveMap)) return true; if ($resolvedIps === false) $resolvedIps = resolveWithPublicDns($host); if (is_array($resolvedIps)) { foreach ($resolvedIps as $ip) $resolveMap[] = "{$host}:{$port}:{$ip}"; } return !empty($resolveMap); } proxyLog("REQUEST: " . $url . " range=" . ($hasRange ? $_SERVER['HTTP_RANGE'] : 'none')); // 相对 URL(无 scheme/host)转为当前服务器的绝对 URL,确保本地 .php 等可执行文件被正确执行 $urlParts = parse_url($url); if (empty($urlParts['scheme']) || empty($urlParts['host'])) { $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST']; $absPath = isset($urlParts['path']) ? $urlParts['path'] : ''; $absQuery = isset($urlParts['query']) ? '?' . $urlParts['query'] : ''; $url = $scheme . '://' . $host . '/' . ltrim($absPath, '/') . $absQuery; proxyLog("NORMALIZED relative URL -> " . $url); $urlParts = parse_url($url); } $targetHost = isset($urlParts['host']) ? $urlParts['host'] : ''; $targetPort = isset($urlParts['scheme']) && $urlParts['scheme'] === 'https' ? 443 : 80; // DNS 解析不再预先进行(旧逻辑每次请求都 nslookup 外部 DNS,白等 1-3 秒), // 改为懒加载:仅在 curl 直连失败时经 ensureResolveMap() 触发 $resolvedIps = false; $resolveMap = array(); $ext = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION)); $audioExts = array('mp3'=>'audio/mpeg','wav'=>'audio/wav','ogg'=>'audio/ogg','m4a'=>'audio/mp4','aac'=>'audio/aac','flac'=>'audio/flac','wma'=>'audio/x-ms-wma'); $videoExts = array('mp4'=>'video/mp4','webm'=>'video/webm','ogv'=>'video/ogg','mov'=>'video/quicktime','m4v'=>'video/x-m4v'); $textExts = array('lrc'=>'text/plain','txt'=>'text/plain','srt'=>'text/plain','ass'=>'text/plain','vtt'=>'text/plain','xml'=>'application/xml','json'=>'application/json','css'=>'text/css','js'=>'application/javascript','html'=>'text/html','htm'=>'text/html','ini'=>'text/plain','csv'=>'text/plain','log'=>'text/plain','md'=>'text/plain','smi'=>'text/plain','krc'=>'text/plain'); $defaultCt = 'application/octet-stream'; if (isset($audioExts[$ext])) $defaultCt = $audioExts[$ext]; elseif (isset($videoExts[$ext])) $defaultCt = $videoExts[$ext]; elseif (isset($textExts[$ext])) $defaultCt = $textExts[$ext] . '; charset=utf-8'; $content = false; $respHeaders = array(); $httpCode = 0; $errMsg = ''; // ============ 同主机本地 PHP 脚本:直接 include 执行,避免单线程服务器 HTTP 回环死锁 ============ $isSameHost = ($targetHost === $_SERVER['HTTP_HOST'] || $targetHost === '127.0.0.1' || $targetHost === 'localhost'); $localScriptPath = ''; if ($isSameHost && in_array($ext, $execExts, true)) { $localScriptPath = __DIR__ . '/' . ltrim(parse_url($url, PHP_URL_PATH), '/'); } if ($localScriptPath && file_exists($localScriptPath) && is_file($localScriptPath)) { proxyLog("LOCAL SCRIPT EXEC: " . $localScriptPath); // 还原 $_GET / $_SERVER,使被 include 的脚本行为与直接 HTTP 访问一致 $savedGet = $_GET; $savedServer = $_SERVER; $queryString = isset($urlParts['query']) ? $urlParts['query'] : ''; parse_str($queryString, $_GET); $_SERVER['SCRIPT_NAME'] = parse_url($url, PHP_URL_PATH); $_SERVER['SCRIPT_FILENAME'] = $localScriptPath; $_SERVER['PHP_SELF'] = parse_url($url, PHP_URL_PATH); $_SERVER['QUERY_STRING'] = $queryString; $_SERVER['REQUEST_URI'] = parse_url($url, PHP_URL_PATH) . ($queryString ? '?' . $queryString : ''); $_SERVER['REQUEST_METHOD'] = 'GET'; // 所有处理逻辑放进 shutdown 函数,确保被 include 脚本调用 exit 后仍能正确处理 $localDone = false; register_shutdown_function(function () use (&$content, &$respHeaders, &$httpCode, &$errMsg, $localScriptPath, $savedGet, $savedServer, &$localDone) { if ($localDone) return; $localDone = true; $out = ob_get_contents(); if ($out === false) $out = ''; // 收集被 include 脚本设置的响应头 $captured = array(); foreach (headers_list() as $h) { if (preg_match('/^([^:]+):\s*(.*)$/', $h, $m)) { $captured[strtolower(trim($m[1]))] = trim($m[2]); } } // 清除所有已设置的头和状态码,清空输出缓冲,由 proxy 统一输出 ob_clean(); header_remove(); http_response_code(200); // 恢复环境(防止影响后续流程) $_GET = $savedGet; $_SERVER = $savedServer; $finalContent = false; $finalHeaders = array(); if (isset($captured['location'])) { $locUrl = $captured['location']; proxyLog("LOCAL SCRIPT REDIRECT -> " . $locUrl); // 优先流式转发(边下边播,首字节即达);失败再回退整段抓取 if (proxy_stream_fetch($locUrl, 'application/octet-stream', array())) return; // 重定向目标通常是远程 CDN,直接 curl 抓取(不会死锁) if (function_exists('curl_init')) { $ch = curl_init($locUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_MAXREDIRS, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); curl_setopt($ch, CURLOPT_REFERER, 'https://music.163.com/'); curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $header) use (&$finalHeaders) { $trimmed = trim($header); if ($trimmed !== '' && strpos($trimmed, ':') !== false) { list($k, $v) = explode(':', $trimmed, 2); $finalHeaders[strtolower(trim($k))] = trim($v); } return strlen($header); }); $finalContent = curl_exec($ch); $finalCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); proxyLog("LOCAL REDIRECT FETCH: code=$finalCode len=" . ($finalContent === false ? 'false' : strlen($finalContent))); } if ($finalContent === false) { $finalContent = @file_get_contents($locUrl, false, stream_context_create(array( 'http' => array('method' => 'GET', 'header' => "User-Agent: Mozilla/5.0\r\n", 'timeout' => 30), 'ssl' => array('verify_peer' => false, 'verify_peer_name' => false) ))); } } else { // 非重定向:直接使用脚本输出 $finalContent = $out; $finalCode = 200; foreach ($captured as $k => $v) { if ($k !== 'location') $finalHeaders[$k] = $v; } } // 直接输出最终内容(被 include 脚本 exit 后,后续代码不会执行) if ($finalContent !== false && strlen($finalContent) > 0) { $ct = isset($finalHeaders['content-type']) ? $finalHeaders['content-type'] : 'application/octet-stream'; header('Content-Type: ' . $ct); header('Accept-Ranges: bytes'); header('Content-Length: ' . strlen($finalContent)); echo $finalContent; } else { header('HTTP/1.1 502 Bad Gateway'); header('Content-Type: text/plain'); echo 'proxy error: local script fetch failed'; } }); ob_start(); include $localScriptPath; // 脚本没有 exit:直接透传输出 $includedOutput = ob_get_clean(); $localDone = true; $_GET = $savedGet; $_SERVER = $savedServer; $capturedHeaders = array(); foreach (headers_list() as $h) { if (preg_match('/^([^:]+):\s*(.*)$/', $h, $m)) { $capturedHeaders[strtolower(trim($m[1]))] = trim($m[2]); } } header_remove(); http_response_code(200); if (isset($capturedHeaders['location'])) { // 非 exit 但设置了 Location(少见):跟随重定向 $locUrl = $capturedHeaders['location']; proxyLog("LOCAL SCRIPT REDIRECT(non-exit) -> " . $locUrl); // 优先流式转发(边下边播,首字节即达);失败再回退整段抓取 if (proxy_stream_fetch($locUrl, 'application/octet-stream', array())) exit; $neContent = false; $neHeaders = array(); if (function_exists('curl_init')) { $ch = curl_init($locUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_MAXREDIRS, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); curl_setopt($ch, CURLOPT_REFERER, 'https://music.163.com/'); curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $header) use (&$neHeaders) { $trimmed = trim($header); if ($trimmed !== '' && strpos($trimmed, ':') !== false) { list($k, $v) = explode(':', $trimmed, 2); $neHeaders[strtolower(trim($k))] = trim($v); } return strlen($header); }); $neContent = curl_exec($ch); curl_close($ch); } if ($neContent !== false && strlen($neContent) > 0) { $ct = isset($neHeaders['content-type']) ? $neHeaders['content-type'] : 'application/octet-stream'; header('Content-Type: ' . $ct); header('Content-Length: ' . strlen($neContent)); echo $neContent; } else { header('HTTP/1.1 502 Bad Gateway'); echo 'proxy error: redirect fetch failed'; } } else { // 直接透传脚本输出(歌词、图片、XML 等) foreach ($capturedHeaders as $k => $v) { if ($k !== 'location') header($k . ': ' . $v); } if (!isset($capturedHeaders['content-type'])) { header('Content-Type: application/octet-stream'); } header('Content-Length: ' . strlen($includedOutput)); echo $includedOutput; } exit; } // 二进制资源(音频/视频/图片):优先流式转发,边下边播,首字节即达; // 文本资源仍走 buffered 路径(需要整段内容做编码转换),debug 模式也保持 buffered $isTextual = isset($textExts[$ext]) && !$hasRange; if (!$debug && !$isTextual) { if (proxy_stream_fetch($url, $defaultCt, $resolveMap)) exit; // 直连失败:懒触发公共 DNS 兜底解析后重试一次 if ($targetHost && ensureResolveMap($targetHost, $targetPort)) { proxyLog("STREAM retry with CURLOPT_RESOLVE: " . implode(',', $resolveMap)); if (proxy_stream_fetch($url, $defaultCt, $resolveMap)) exit; } proxyLog("STREAM: unavailable, fallback to buffered"); } if ($content === false && function_exists('curl_init')) { $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_MAXREDIRS, 10); curl_setopt($ch, CURLOPT_ENCODING, 'gzip, deflate'); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); curl_setopt($ch, CURLOPT_REFERER, dirname($url) . '/'); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 60); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15); $extraHeaders = array( 'Accept: */*', 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8' ); if ($hasRange) { $extraHeaders[] = 'Range: ' . $_SERVER['HTTP_RANGE']; } curl_setopt($ch, CURLOPT_HTTPHEADER, $extraHeaders); curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($ch, $header) use (&$respHeaders) { $trimmed = trim($header); if ($trimmed !== '' && strpos($trimmed, ':') !== false) { list($k, $v) = explode(':', $trimmed, 2); $respHeaders[strtolower(trim($k))] = trim($v); } return strlen($header); }); if (!empty($resolveMap)) { curl_setopt($ch, CURLOPT_RESOLVE, $resolveMap); proxyLog("CURLOPT_RESOLVE: " . count($resolveMap) . " IP(s) set"); } $content = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $errMsg = curl_error($ch); $effectiveUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); curl_close($ch); proxyLog("CURL: code=$httpCode effective=$effectiveUrl err=$errMsg len=" . ($content === false ? 'false' : strlen($content))); } if ($content === false) { $opts = array( 'http' => array( 'method' => 'GET', 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\nReferer: " . dirname($url) . "/\r\n", 'timeout' => 60 ), 'ssl' => array('verify_peer' => false, 'verify_peer_name' => false) ); if ($hasRange) { $opts['http']['header'] .= 'Range: ' . $_SERVER['HTTP_RANGE'] . "\r\n"; } // curl buffered 也失败了才走到这里:此时才懒触发公共 DNS 兜底解析 if ($targetHost) ensureResolveMap($targetHost, $targetPort); $headerHost = ($resolvedIps && $targetHost) ? "Host: {$targetHost}\r\n" : ''; $ctx = stream_context_create($opts); if ($resolvedIps && $targetHost) { foreach ($resolvedIps as $ip) { $fgcUrl = preg_replace('/^(' . preg_quote($urlParts['scheme'], '/') . ':\/\/)' . preg_quote($targetHost, '/') . '/', '$1' . $ip, $url, 1); proxyLog("FGC try: {$fgcUrl}"); $tryCtx = stream_context_create(array( 'http' => array( 'method' => 'GET', 'header' => $headerHost . $opts['http']['header'], 'timeout' => 60 ), 'ssl' => $opts['ssl'] )); $content = @file_get_contents($fgcUrl, false, $tryCtx); if ($content !== false) { $httpCode = 200; $errMsg = ''; proxyLog("FGC success via {$ip} len=" . strlen($content)); break; } proxyLog("FGC failed via {$ip}"); } } else { $content = @file_get_contents($url, false, $ctx); if ($content !== false) { $httpCode = 200; proxyLog("FGC: success len=" . strlen($content)); } else { proxyLog("FGC: failed"); } } } if ($content === false) { proxyLog("FINAL: 502 error"); if ($debug) debugDump('', 0, $errMsg ? $errMsg : 'fetch failed'); header('HTTP/1.1 502 Bad Gateway'); header('Content-Type: text/plain'); echo 'proxy error: ' . ($errMsg ? $errMsg : 'fetch failed'); exit; } if (strlen($content) === 0) { proxyLog("FINAL: empty response"); if ($debug) debugDump('', $httpCode, 'empty response from remote'); header('HTTP/1.1 502 Bad Gateway'); header('Content-Type: text/plain'); echo 'proxy error: empty response from remote'; exit; } $contentInfo = isset($respHeaders['content-type']) ? $respHeaders['content-type'] : 'unknown'; proxyLog("CONTENT: type=$contentInfo len=" . strlen($content) . " firstbytes=" . bin2hex(substr($content, 0, 16))); if ($httpCode >= 400) { proxyLog("FINAL: remote error $httpCode"); if ($debug) debugDump($content, $httpCode, 'remote returned ' . $httpCode); header('HTTP/1.1 ' . $httpCode); header('Content-Type: text/plain'); echo 'remote error ' . $httpCode; exit; } $ct = isset($respHeaders['content-type']) ? $respHeaders['content-type'] : $defaultCt; if (isset($textExts[$ext]) && strpos($ct, 'charset') === false) { $ct = rtrim($ct, '; ') . '; charset=utf-8'; } if (isset($textExts[$ext]) && !$hasRange) { $detected = false; if (function_exists('mb_detect_encoding')) { $detected = mb_detect_encoding($content, array('UTF-8', 'GBK', 'GB2312', 'GB18030', 'BIG5', 'UTF-7', 'ASCII'), true); } if ($detected && strtoupper($detected) !== 'UTF-8' && function_exists('mb_convert_encoding')) { proxyLog("ENCODING: detect=$detected converting to UTF-8"); $content = mb_convert_encoding($content, 'UTF-8', $detected); $ct = preg_replace('/charset=[^;]+/i', 'charset=utf-8', $ct); if (strpos($ct, 'charset') === false) $ct = rtrim($ct, '; ') . '; charset=utf-8'; } elseif (!$detected && function_exists('mb_convert_encoding')) { $content2 = @mb_convert_encoding($content, 'UTF-8', 'GBK'); if ($content2 !== false && $content2 !== $content) { proxyLog("ENCODING: fallback GBK->UTF-8 (changed)"); $content = $content2; $ct = preg_replace('/charset=[^;]+/i', 'charset=utf-8', $ct); if (strpos($ct, 'charset') === false) $ct = rtrim($ct, '; ') . '; charset=utf-8'; } } } header('Content-Type: ' . $ct); header('Accept-Ranges: bytes'); $contentLength = strlen($content); header('Content-Length: ' . $contentLength); if ($hasRange && $httpCode === 206) { header('HTTP/1.1 206 Partial Content'); if (isset($respHeaders['content-range'])) { header('Content-Range: ' . $respHeaders['content-range']); } } proxyLog("FINAL: ct=$ct len=$contentLength code=$httpCode"); if ($debug) debugDump($content, $httpCode, 'success'); echo $content;