热搜: 活动 交友 discuz
查看: 6|回复: 1

[html音画] 一行代码,让音乐播放器拥有频谱特效

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式

一行代码,让音乐播放器拥有频谱特效

在你的 HTML5 音乐播放器里,想要加上酷炫的实时频谱可视化效果?传统方案需要引入 CSS、创建容器、加载多个 JS 文件、手动对接播放器接口…… 现在,我们只需 一行 script 标签,就能完成所有工作。

核心用法

在页面 <head> 或 <body> 任意位置插入:

<script src="https://bbs.cnzv.cc/v/waveform/spectrum.js" data-effect="1"></script>

就这一行。没有 ``,没有额外 `

`,没有任何插件初始化代码。删除这一行,频谱完全消失,播放不受任何影响。

背后做了什么

频谱.png

spectrum.js 作为唯一入口,自动完成四件事:

  1. 自动定位音频:实时查找页面中的 <audio> 元素,即使播放器用 innerHTML 重建 DOM 也能找到
  2. 自动注入样式:动态创建 <link> 标签,加载同目录的 waveform.css
  3. 自动挂载容器:创建 <div id="spectrum"> 并自动挂载到音频父容器
  4. 自动加载插件:动态 <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?播放频谱
跨域外链必须✅❌ 没写则空白
同源本地不需要✅✅
代理后不需要✅✅

关闭频谱的三种方式

  1. 删掉那一行 script — 页面干净无痕
  2. script 加 data-off="1" — 
    <script src="https://bbs.cnzv.cc/v/waveform/spectrum.js" data-off="1"></script>

  3. 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;

世上无难事,实在不行就摇铃
 楼主| 发表于 半小时前 | 显示全部楼层
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
  6. <title>草原起风了 · 音乐播放器</title>
  7. <style>
  8.   * { margin: 0; padding: 0; box-sizing: border-box; }
  9.   html, body { height: 100%; background: #000; overflow: hidden; }
  10.   body {
  11.     display: flex;
  12.     justify-content: center;
  13.     align-items: center;
  14.     font-family: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
  15.   }

  16.   /* 舞台容器:自适应视口,最大保持原尺寸比例 */
  17.   #papa {
  18.     position: relative;
  19.     width: min(1600px, 100vw);
  20.     height: min(850px, 100vh);
  21.     background: url('https://cnup.cnzv.cc/view.php/ZjE2MmU.jpg') no-repeat center / cover;
  22.     box-shadow: 0 0 0 1px gray;
  23.     overflow: hidden;
  24.     z-index: 1;
  25.   }

  26.   /* 背景视频:screen 混合只保留亮部,营造光影叠加效果 */
  27.   #papa > video {
  28.     position: absolute;
  29.     inset: 0;
  30.     width: 100%;
  31.     height: calc(100% + 200px);
  32.     object-fit: cover;
  33.     mix-blend-mode: screen;
  34.     pointer-events: none;
  35.   }

  36.   /* 自动播放被拦截时的点击提示层 */
  37.   #hint {
  38.     position: absolute;
  39.     inset: 0;
  40.     z-index: 5;
  41.     display: grid;
  42.     place-items: center;
  43.     color: #fff;
  44.     font-size: 1.2em;
  45.     letter-spacing: 2px;
  46.     background: rgba(0, 0, 0, .35);
  47.     cursor: pointer;
  48.     transition: opacity .4s ease;
  49.   }
  50.   #hint.hidden { opacity: 0; pointer-events: none; }
  51. </style>
  52.   <!-- ===== 频谱外挂:一行接入 =====
  53.        data-effect 指定默认第几种(1~18);CSS、频谱层、插件全部由 spectrum.js 自动载入。
  54.        放 head 或 body 都可以;删掉这一行(或加 ?nospec)即完全关闭,播放不受影响。 -->
  55.   <script src="https://bbs.cnzv.cc/v/waveform/spectrum.js" data-effect="1"></script>

  56. </head>
  57. <body>
  58.   <div id="papa">
  59.     <!-- 跨域音源(该站返回 Access-Control-Allow-Origin: *)。
  60.          关键:必须以 CORS 模式加载,<audio> 上要显式写 crossorigin="anonymous",
  61.          且必须写在标签里(资源开始加载后 JS 再补属性已经来不及)。
  62.          否则浏览器用 no-cors 取回的音频会被标记为「非 CORS 同源」:能正常出声,
  63.          但 captureStream 只给静音轨、MediaElementSource 也会静音 → 频谱全 0(一片空白)。
  64.          用 anonymous 而不是 use-credentials:源站 ACAO 是通配符 *,凭据模式会被浏览器拒绝。
  65.          CORS 头若失效,下方脚本会自动去掉 crossorigin 重载,保证播放不受影响。
  66.          换成本地 play.m4a 则同源,不需要 crossorigin,也不需要代理。 -->
  67.     <audio id="aud" src="https://imgimg.qqdsw8.cn/view.php/9b8781a7fd14b6bbce3102a2b84afbd4.mp3"
  68.            crossorigin="anonymous" autoplay loop preload="auto"></audio>
  69.     <!-- 本地同源备用(无需 crossorigin / 代理):
  70.     <audio id="aud" src="play.m4a" autoplay loop preload="auto"></audio> -->
  71.     <video src="https://img2.tukuppt.com/video_show/7165162/00/20/20/5f2102739a334.mp4" autoplay loop muted playsinline></video>
  72.     <div id="hint">点击任意位置播放音乐 ♪</div>
  73.   </div>



  74. <script>
  75. (function () {
  76.   'use strict';

  77.   var geci = `[00:02.08]三天三夜(雷鬼版)
  78. [00:08.36]LRC编辑:醉美水芙蓉
  79. [00:26.19]一点都不会累
  80. [00:29.40]我已经跳了三天三夜
  81. [00:33.06]我现在的心情喝汽水也会醉Oh
  82. [00:39.09]完全都不会疲倦
  83. [00:42.36]我还要再跳三天三夜
  84. [00:46.02]我现在的心情轻得好像可以飞
  85. [00:54.06]OKOK 加入我的行列
  86. [00:55.92]No K No K 白天跳到黑夜
  87. [00:59.22]Always Always 快乐不会吃亏
  88. [01:02.40]No Way No Way 谁也别想拒绝
  89. [01:08.67]三天三夜的三更半夜 跳舞不要停歇
  90. [01:15.15]三天三夜的三更半夜 飘浮只靠音乐
  91. [01:21.69]三天三夜的三更半夜 全身只剩汗水
  92. [01:28.11]三天三夜的三更半夜Ba Ba Ba
  93. [01:43.83]完全都没有极限
  94. [01:47.13]我已经High 了三天三夜
  95. [01:50.73]我现在的心情就在出轨的边缘yeah
  96. [01:56.79]根本不可能喝醉
  97. [02:00.06]我还要再High 三天三夜
  98. [02:03.69]我眼里的世界就像是一部卡通影片
  99. [02:11.79]OKOK 加入我的行列
  100. [02:13.71]No K No K 白天跳到黑夜
  101. [02:16.92]Always Always 快乐不会吃亏
  102. [02:20.16]No Way No Way 谁也别想拒绝
  103. [02:26.58]三天三夜的三更半夜 跳舞不要停歇
  104. [02:33.03]三天三夜的三更半夜 飘浮只靠音乐
  105. [02:39.51]三天三夜的三更半夜 全身只剩汗水
  106. [02:45.93]三天三夜的三更半夜Ba Ba Ba
  107. [03:21.72]OKOK 加入我的行列
  108. [03:23.64]No K No K 白天跳到黑夜
  109. [03:26.91]Always Always 快乐不会吃亏
  110. [03:30.18]No Way No Way 谁也别想拒绝
  111. [03:34.32]三天三夜的三更半夜 跳舞不要停歇
  112. [03:39.90]三天三夜的三更半夜 飘浮只靠音乐
  113. [03:46.41]三天三夜的三更半夜 全身只剩汗水
  114. [03:52.89]三天三夜的三更半夜Ba Ba Ba
  115. [04:02.18]- 谢谢欣赏 -`;

  116.   // 加载歌词播放器组件
  117.   var sf = document.createElement('script');
  118.   sf.charset = 'utf-8';
  119.   sf.src = 'https://bbs.cnzv.cc/v/waveform/line_yslrc.js';
  120.   document.body.appendChild(sf);
  121.   sf.onload = function () {
  122.     HCPlayer({
  123.       papa: '#papa',
  124.       geci: geci,
  125.       skip: 0.5,
  126.       average: 0,
  127.       // 歌词按「距底 + 水平居中」定位:#papa 高度自适应视口,
  128.       // 写死 top:710px 在矮视口(如开着 DevTools)会被 overflow:hidden 整行裁掉。
  129.       lrc_css: 'top: auto; bottom: 100px; left: 50%; transform: translateX(-50%); --bg: linear-gradient(#FFFFFF,#06F406); color: silver;font: bold 3.0em sans-serif',
  130.       // 控制条:固定宽 470px,水平居中,距底 30px
  131.       player_css: 'width: 470px; left: 50%; transform: translateX(-50%); bottom: 30px; color: white; --prog: white; --track: gray; --bg: url("https://cnup.cnzv.cc/view.php/ZjE2MmU.jpg") no-repeat center/cover',
  132.       fs_css: '--bg: transparent; --color: snow;'
  133.     });

  134.     // 兜底:强制控制条水平居中、距底 30px(覆盖默认 left:auto 导致的左对齐)
  135.     var mp = document.getElementById('mplayer');
  136.     if (mp) {
  137.       mp.style.left = '50%';
  138.       mp.style.transform = 'translateX(-50%)';
  139.       mp.style.bottom = '30px';
  140.       mp.style.zIndex = '10';   // 压在频谱层(2)之上,保证控制条可点
  141.     }
  142.   };

  143.   // —— 自动播放兜底 ——
  144.   // 现代浏览器会拦截带声音的自动播放,这里在首次交互时补播
  145.   // 注意:<audio> 会被 HCPlayer 用 innerHTML+= 重建,播放/监听都要针对「重建后的实体」
  146.   function getAud() { return document.getElementById('aud'); }
  147.   function getHint() { return document.getElementById('hint'); }  // 同样是重建后的实体

  148.   function startPlay() {
  149.     var el = getAud();
  150.     if (!el) return;
  151.     var p = el.play();
  152.     if (p && p.catch) {
  153.       p.then(hideHint).catch(showHint);
  154.     }
  155.   }
  156.   function showHint() { var h = getHint(); if (h) h.classList.remove('hidden'); }
  157.   function hideHint() { var h = getHint(); if (h) h.classList.add('hidden'); }

  158.   // 用事件委托监听播放(元素重建后依然有效)
  159.   document.addEventListener('play', function (e) {
  160.     if (e.target && e.target.id === 'aud') hideHint();
  161.   }, true);

  162.   // —— CORS 兜底:保证「加频谱 / 换音源」永远不会把播放弄没 ——
  163.   // 带 crossorigin 的音频,一旦源站 CORS 头失效就会直接加载失败(不只是频谱空白)。
  164.   // 此时去掉 crossorigin 重新加载,退回普通跨域播放,代价仅是频谱不显示。
  165.   // 注意:媒体元素的 error 事件不冒泡,必须在捕获阶段做委托。
  166.   var corsFallback = 0;
  167.   document.addEventListener('error', function (e) {
  168.     var a = e.target;
  169.     if (!a || a.id !== 'aud' || !a.crossOrigin || corsFallback >= 2) return;
  170.     corsFallback++;
  171.     a.removeAttribute('crossorigin');
  172.     a.load();
  173.     var p = a.play();
  174.     if (p && p.catch) p.catch(function () {});
  175.   }, true);

  176.   // 先尝试自动播放,失败则等待用户点击
  177.   startPlay();
  178.   document.addEventListener('click', function once() {
  179.     startPlay();
  180.     document.removeEventListener('click', once);
  181.   }, { once: true });
  182. })();
  183. </script>
  184. </body>
  185. </html>
复制代码

世上无难事,实在不行就摇铃
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|小黑屋|纳兰音画网 ( 蜀ICP备2021011087号-1 )

GMT+8, 2026-11-7 23:55 , Processed in 0.063937 second(s), 33 queries .

Powered by Discuz! X5.0

© 2001-2020 Comsenz Inc.

快速回复 返回顶部 返回列表