<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>统一播放器</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https:; script-src 'self' https://g.alicdn.com 'unsafe-eval' 'unsafe-inline' blob:; style-src 'self' https://g.alicdn.com 'unsafe-inline'; frame-src https:; media-src https: blob:; img-src 'self' data: https:; connect-src https:; worker-src blob:; object-src 'none'; base-uri 'self'; form-action 'self';">
<link rel="stylesheet" href="https://g.alicdn.com/apsara-media-box/imp-web-player/2.25.1/skins/default/aliplayer-min.css" />
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
}
#player-container {
width: 100%;
height: 100%;
position: relative;
}
.loading, .error {
color: #fff;
text-align: center;
padding: 40px 20px;
}
.audio-player {
height: 120px !important;
display: flex;
align-items: center;
justify-content: center;
}
/* 移动端控制栏隐藏样式 */
.prism-player.mobile-hide-controls .prism-controlbar {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.prism-player .prism-controlbar {
transition: opacity 0.3s ease;
}
</style>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
</head>
<body>
<div id="player-container"></div>

<script src="https://g.alicdn.com/apsara-media-box/imp-web-player/2.25.1/aliplayer-min.js"></script>
<script>
// 常量定义(集中管理,所有可配置项均在此处统一维护)
const CONSTS = {
// 核心开关配置
WHITELIST_ENABLE: false, // 白名单验证开关:true=开启域名验证(仅允许白名单内域名播放),false=关闭验证(所有HTTPS地址均可播放)

// 域名白名单配置
ALLOWED_DOMAINS: ['hd.ijycnd.com', 'dsm.pcwnas.cn', 'your-trusted-domain.com'], // 可信域名列表:仅当WHITELIST_ENABLE=true时生效,多个域名用英文逗号分隔,格式为'域名.com'

// 媒体格式配置
MEDIA_EXTS: ['m3u8', 'mp4', 'mp3', 'flv'], // 支持的媒体文件扩展名列表,用于解析和识别媒体类型
MEDIA_REGEX: /\.(m3u8|mp4|mp3|flv)($|\?)/i, // 媒体文件后缀正则表达式:用于快速判断URL是否为直接可播放的媒体文件

// 超时/延迟配置
FETCH_TIMEOUT: 10000, // 地址解析请求超时时间(毫秒):防止请求长时间无响应,10000=10秒
CONTROL_HIDE_DELAY: 3000, // 移动端控制栏自动隐藏延迟(毫秒):播放后多久隐藏控制栏,3000=3秒

// 播放器尺寸配置
AUDIO_HEIGHT: '120px', // 音频文件播放时的播放器高度:区别于视频的全屏显示
FULL_HEIGHT: '100%', // 视频文件播放时的播放器高度:占满整个容器

// 请求头配置
USER_AGENT: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version 16.0 Mobile/15E148 Safari/604.1" // 解析地址时使用的User-Agent:模拟iPhone移动端请求,提高兼容性
};

// 全局状态
const state = {
player: null, // 播放器实例对象
hideTimer: null, // 移动端控制栏隐藏定时器
container: document.getElementById("player-container") // 播放器容器DOM节点(缓存避免重复查询)
};

// ===================== 工具函数 =====================
/**
* 判断是否为移动端
*/
const isMobile = () => /Android|iPhone|iPad|iPod|Windows Phone/i.test(navigator.userAgent);

/**
* 验证URL合法性(仅HTTPS)
* @param {string} url - 待验证的URL
* @returns {boolean} 是否合法
*/
const isValidUrl = (url) => {
try {
const parsed = new URL(url);
return parsed.protocol === 'https:';
} catch (e) {
return false;
}
};

/**
* 验证是否为可信域名(受白名单开关控制)
* @param {string} url - 待验证的URL
* @returns {boolean} 是否可信
*/
const isTrustedDomain = (url) => {
// 白名单开关关闭时,直接返回true(跳过验证)
if (!CONSTS.WHITELIST_ENABLE) return true;

try {
const parsed = new URL(url);
return CONSTS.ALLOWED_DOMAINS.includes(parsed.hostname);
} catch (e) {
return false;
}
};

/**
* 判断文件类型
* @param {string} url - 文件URL
* @param {string} ext - 扩展名(mp3/flv等)
* @returns {boolean} 是否为指定类型
*/
const isFileType = (url, ext) => new RegExp(`\\.${ext}($|\\?)`, 'i').test(url);

/**
* 显示状态提示
* @param {string} type - loading/error
* @param {string} text - 提示文本
*/
const showStatus = (type, text) => {
state.container.innerHTML = `<div class="${type}">${text}</div>`;
};

// ===================== 核心逻辑 =====================
/**
* 解析媒体地址
* @param {string} pageUrl - 页面URL
* @returns {string|null} 解析后的媒体地址
*/
const parseMediaUrl = async (pageUrl) => {
// 白名单开关控制:关闭时仅验证URL格式,不验证域名
if (!isValidUrl(pageUrl) || (CONSTS.WHITELIST_ENABLE && !isTrustedDomain(pageUrl))) return null;

try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CONSTS.FETCH_TIMEOUT);

const res = await fetch(pageUrl, {
headers: {
"User-Agent": CONSTS.USER_AGENT,
"Referer": window.location.origin
},
signal: controller.signal,
mode: 'cors'
});

clearTimeout(timeoutId);
if (!res.ok) return null;

const html = await res.text();
// 遍历所有媒体类型
for (const ext of CONSTS.MEDIA_EXTS) {
const match = html.match(new RegExp(`https?:\/\/[^\\s"']+\\.${ext}[^\\s"']*`, 'i'));
if (match && isValidUrl(match[0])) return match[0];
}

return null;
} catch (e) {
return null;
}
};

/**
* 移动端控制栏管理
*/
const MobileControls = {
/**
* 初始化控制逻辑
* @param {string} url - 播放地址
*/
init(url) {
if (isFileType(url, 'mp3') || !isMobile()) return;

const playerEl = document.querySelector('.prism-player');
const controlBar = document.querySelector('.prism-controlbar');
if (!playerEl || !controlBar) return;

this.clearTimer();

// 播放器事件绑定(统一处理)
const events = {
play: () => this.hideControls(playerEl),
pause: () => this.showControls(playerEl, true),
fullscreenchange: () => this.showControls(playerEl)
};
Object.entries(events).forEach(([event, handler]) => {
state.player.on(event, handler);
});

// 点击/触摸事件
playerEl.addEventListener('click', (e) => this.handleClick(e, playerEl));
playerEl.addEventListener('touchstart', () => this.showControls(playerEl));

// 控制栏元素事件
controlBar.querySelectorAll('.prism-progress-bar, .prism-play-btn, .prism-volume, .prism-fullscreen-btn')
.forEach(el => {
el.addEventListener('click', () => this.showControls(playerEl));
el.addEventListener('touchstart', () => this.showControls(playerEl));
});
},

/**
* 隐藏控制栏
* @param {HTMLElement} playerEl - 播放器元素
*/
hideControls(playerEl) {
this.clearTimer();
state.hideTimer = setTimeout(() => {
playerEl.classList.add('mobile-hide-controls');
}, CONSTS.CONTROL_HIDE_DELAY);
},

/**
* 显示控制栏
* @param {HTMLElement} playerEl - 播放器元素
* @param {boolean} keepShow - 是否保持显示
*/
showControls(playerEl, keepShow = false) {
this.clearTimer();
playerEl.classList.remove('mobile-hide-controls');
if (!keepShow) this.hideControls(playerEl);
},

/**
* 清除定时器
*/
clearTimer() {
if (state.hideTimer) {
clearTimeout(state.hideTimer);
state.hideTimer = null;
}
},

/**
* 点击事件处理
* @param {Event} e - 事件对象
* @param {HTMLElement} playerEl - 播放器元素
*/
handleClick(e, playerEl) {
if (!e.target.closest('.prism-controlbar')) {
const isHidden = playerEl.classList.contains('mobile-hide-controls');
isHidden ? this.showControls(playerEl) : state.player[state.player.paused ? 'play' : 'pause']();
}
}
};

/**
* 绑定全屏方向控制
*/
const bindFullScreen = () => {
const handleFullScreenChange = () => {
const isFullScreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement);
if (!screen.orientation || isFileType(state.player?.getOption?.("source"), 'mp3')) return;

try {
screen.orientation.lock(isFullScreen ? "landscape" : "portrait").catch(() => {});
} catch (e) {}
};

// 统一绑定全屏事件
['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange'].forEach(event => {
document.addEventListener(event, handleFullScreenChange);
});
};

/**
* 创建播放器实例
* @param {string} url - 媒体地址
*/
const createPlayer = (url) => {
if (!isValidUrl(url)) {
showStatus('error', "非法的播放地址");
return;
}

// 清空容器
state.container.innerHTML = "";
const isAudio = isFileType(url, 'mp3');
const isFlv = isFileType(url, 'flv');
const mobile = isMobile();

// 播放器基础配置(精简冗余配置)
const playerConfig = {
id: "player-container",
source: url,
width: "100%",
height: isAudio ? CONSTS.AUDIO_HEIGHT : CONSTS.FULL_HEIGHT,
autoplay: false,
playsinline: true,
preload: "metadata",
controlBarVisibility: isAudio ? "always" : (mobile ? "always" : "hover"),
useH5Prism: true,
isLive: false,
language: "zh-cn",
timeout: CONSTS.FETCH_TIMEOUT,
enableStashBuffer: true,
ignoreManifestErrors: true,
disableStatsReport: true,
useHlsPluginForHls: true
};

// FLV格式专属配置
if (isFlv) {
playerConfig.useFlvPluginForFlv = true;
playerConfig.flvOptions = {
enableStashBuffer: true,
stashInitialSize: 128
};
}

// 创建播放器
state.player = new Aliplayer(playerConfig);

// 绑定全屏事件
bindFullScreen();

// 播放器就绪后初始化
state.player.on('ready', () => {
MobileControls.init(url);

// 自动播放触发逻辑(合并条件)
if (isAudio || mobile || isFlv) {
state.container.addEventListener('click', () => {
state.player.paused && state.player.play().catch(err => console.log("播放失败:", err));
}, { once: true });
}
});

// 错误处理(精简逻辑)
state.player.on("error", () => {
const originalUrl = window.parent.now;
// 白名单开关控制错误处理时的域名验证
if (isValidUrl(originalUrl) && isTrustedDomain(originalUrl)) {
loadIframe(originalUrl);
} else {
showStatus('error', CONSTS.WHITELIST_ENABLE ? "播放失败且地址不可信" : "播放失败");
}
});
};

/**
* 加载iframe播放
* @param {string} url - 播放地址
*/
const loadIframe = (url) => {
// 白名单开关控制iframe加载时的域名验证
if (!isValidUrl(url) || (CONSTS.WHITELIST_ENABLE && !isTrustedDomain(url))) {
showStatus('error', CONSTS.WHITELIST_ENABLE ? "不支持的播放地址" : "非法的播放地址(仅支持HTTPS)");
return;
}

state.container.innerHTML = `
<iframe
src="${url}"
width="100%"
height="100%"
frameborder="0"
allow="fullscreen; autoplay; media-playback"
sandbox="allow-scripts allow-same-origin allowfullscreen"
referrerpolicy="strict-origin-when-cross-origin">
</iframe>
`;
};

/**
* 初始化入口
*/
const init = async () => {
const playUrl = window.parent.now || "";

// 参数校验(白名单开关控制域名验证)
if (!playUrl) return showStatus('error', "播放地址为空");
if (!isValidUrl(playUrl)) return showStatus('error', "非法的播放地址(仅支持HTTPS)");
if (CONSTS.WHITELIST_ENABLE && !isTrustedDomain(playUrl)) return showStatus('error', "不可信的播放域名");

// 直接播放媒体文件
if (CONSTS.MEDIA_REGEX.test(playUrl)) {
createPlayer(playUrl);
return;
}

// 解析媒体地址
showStatus('loading', "正在解析视频地址...");
const realUrl = await parseMediaUrl(playUrl);
realUrl ? createPlayer(realUrl) : loadIframe(playUrl);
};

/**
* 页面清理(精简冗余逻辑)
*/
const cleanup = () => {
try {
MobileControls.clearTimer();
if (state.player) state.player.dispose();
if (screen.orientation) screen.orientation.unlock();
} catch (e) {}
};

// 事件绑定(精简写法)
window.addEventListener("load", init);
window.addEventListener("beforeunload", cleanup);
</script>
</body>
</html>