176 lines
7.8 KiB
HTML
176 lines
7.8 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>HDMI 录制控制台</title>
|
||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||
<style>
|
||
:root{--bg:#1a237e;--accent:#283593;--text:#fff;--card:#fff;--card-text:#000}
|
||
@media(prefers-color-scheme:dark){:root{--card:#1e1e1e;--card-text:#fff}}
|
||
body{margin:0;font-family:system-ui,-apple-system,sans-serif;background:linear-gradient(135deg,var(--bg),#283593,#4a148c);color:var(--text);min-height:100vh;display:flex;justify-content:center;padding:20px;box-sizing:border-box}
|
||
.container{max-width:720px;width:100%;background:var(--card);color:var(--card-text);border-radius:12px;padding:24px;box-shadow:0 8px 32px rgba(0,0,0,.4)}
|
||
h1{margin:0 0 24px;font-size:1.6em;display:flex;align-items:center;gap:12px}
|
||
#statusIndicator{font-size:1.2em;font-weight:600}
|
||
h2{margin:24px 0 12px;font-size:1.4em}
|
||
input,button{width:100%;padding:12px;margin:8px 0;border-radius:8px;border:none;box-sizing:border-box;font-size:1em}
|
||
input{background:#f5f5f5;color:#000;border:1px solid #ddd}
|
||
button{background:var(--accent);color:#fff;cursor:pointer;font-weight:600;transition:.2s}
|
||
button:hover{background:#1a237e}
|
||
button.loading{opacity:.7;pointer-events:none}
|
||
button.loading::after{content:" ⏳";animation:spin 1s linear infinite}
|
||
#pm2Buttons{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:8px;margin:12px 0}
|
||
#videoPlayer{position:relative;width:100%;padding-bottom:56.25%;background:#000;border-radius:8px;overflow:hidden;margin:16px 0;display:none}
|
||
video{position:absolute;inset:0;width:100%;height:100%}
|
||
#loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.8em;pointer-events:none;background:rgba(0,0,0,.5);opacity:0;transition:opacity .3s;border-radius:8px}
|
||
#pm2Log{background:#000;color:#0f0;padding:12px;height:320px;overflow-y:auto;font-family:monospace;font-size:.9em;border-radius:8px;white-space:pre-wrap;word-break:break-all}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>进程: hdmi <span id="statusIndicator">⚪ 加载中...</span></h1>
|
||
|
||
<h2>视频播放器(M3U8 / FLV)</h2>
|
||
<input type="text" id="videoUrlInput" placeholder="请输入视频链接">
|
||
<button id="playButton">播放</button>
|
||
<div id="videoPlayer">
|
||
<video controls autoplay playsinline></video>
|
||
<div id="loading">加载中...</div>
|
||
</div>
|
||
|
||
<h2>PM2 进程控制</h2>
|
||
<div id="pm2Buttons">
|
||
<button data-action="start">启动</button>
|
||
<button data-action="stop">停止</button>
|
||
<button data-action="restart">重启</button>
|
||
<button data-action="status">手动刷新</button>
|
||
</div>
|
||
|
||
<h2>实时日志(自动每秒刷新)</h2>
|
||
<pre id="pm2Log">正在加载状态与日志...</pre>
|
||
</div>
|
||
|
||
<script>
|
||
const video = document.querySelector('video');
|
||
const player = document.getElementById('videoPlayer');
|
||
const loading = document.getElementById('loading');
|
||
const logEl = document.getElementById('pm2Log');
|
||
const statusIndicator = document.getElementById('statusIndicator');
|
||
|
||
function httpToHttps(url){return url.replace(/^http:/,"https:")}
|
||
|
||
function scrollLog(){logEl.scrollTop = logEl.scrollHeight}
|
||
|
||
function updateStatus(process_status){
|
||
let emoji = '⚪', text = '未知';
|
||
if(process_status === 'online'){ emoji = ' '; text = '运行中'; }
|
||
else if(process_status === 'stopped'){ emoji = ' '; text = '已停止'; }
|
||
else if(process_status === 'errored'){ emoji = ' '; text = '错误'; }
|
||
else if(process_status === 'launching' || process_status === 'starting'){ emoji = ' '; text = '启动中'; }
|
||
else if(process_status === 'waiting restart'){ emoji = ' '; text = '等待重启'; }
|
||
else if(process_status === 'not_found'){ emoji = '❌'; text = '未注册到 PM2'; }
|
||
statusIndicator.innerHTML = `${emoji} ${text}`;
|
||
}
|
||
|
||
async function playVideo(){
|
||
let url = document.getElementById('videoUrlInput').value.trim();
|
||
if(!url) return alert('请输入视频链接');
|
||
url = httpToHttps(url);
|
||
player.style.display = 'block';
|
||
loading.style.opacity = 1;
|
||
video.pause(); video.src = '';
|
||
|
||
if(url.endsWith('.m3u8')){
|
||
if(Hls.isSupported()){
|
||
const hls = new Hls({enableWorker:true});
|
||
hls.loadSource(url);
|
||
hls.attachMedia(video);
|
||
hls.on(Hls.Events.MANIFEST_PARSED, ()=>loading.style.opacity=0);
|
||
hls.on(Hls.Events.ERROR, (e,data)=>{alert('HLS 错误: '+data.type); loading.style.opacity=0});
|
||
}else if(video.canPlayType('application/vnd.apple.mpegurl')){
|
||
video.src = url; video.play(); loading.style.opacity=0;
|
||
}else{alert('浏览器不支持 M3U8'); return}
|
||
}else if(url.endsWith('.flv')){
|
||
if(typeof flvjs !== 'undefined' && flvjs.isSupported()){
|
||
const flvPlayer = flvjs.createPlayer({type:'flv',url,isLive:true});
|
||
flvPlayer.attachMediaElement(video);
|
||
flvPlayer.load(); flvPlayer.play();
|
||
flvPlayer.on(flvjs.Events.LOADING_COMPLETE, ()=>loading.style.opacity=0);
|
||
}else{
|
||
const script = document.createElement('script');
|
||
script.src = 'https://cdn.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.min.js';
|
||
script.onload = playVideo;
|
||
document.head.appendChild(script);
|
||
return;
|
||
}
|
||
}else{alert('仅支持 .m3u8 或 .flv 格式'); return}
|
||
}
|
||
document.getElementById('playButton').onclick = playVideo;
|
||
|
||
// PM2 控制 + 实时状态/日志
|
||
async function pm2Action(action){
|
||
const btn = document.querySelector(`[data-action="${action}"]`);
|
||
if(btn) btn.classList.add('loading');
|
||
|
||
try{
|
||
const res = await fetch(`/${action}`);
|
||
if(!res.ok) throw new Error('网络错误');
|
||
const data = await res.json();
|
||
|
||
if(action === 'status'){
|
||
updateStatus(data.process_status);
|
||
|
||
let logText = `【PM2 全状态表格】(${new Date().toLocaleTimeString()})\n${data.raw_status}\n\n`;
|
||
|
||
if(data.process_status === 'not_found'){
|
||
logText += `【提示】进程 "hdmi" 未注册到 PM2(可能被 pm2 delete 删除)\n`;
|
||
}else{
|
||
logText += `【实时输出日志】`;
|
||
if(data.recent_log.includes('不存在') || data.recent_log.includes('无日志路径')){
|
||
logText += `\n${data.recent_log}`;
|
||
}else{
|
||
logText += `(最近 ${data.recent_log.trim().split('\n').length} 行)\n${data.recent_log || '无新输出'}`;
|
||
}
|
||
|
||
logText += `\n\n【实时错误日志】`;
|
||
if(data.recent_error.includes('不存在') || data.recent_error.includes('无日志路径')){
|
||
logText += `\n${data.recent_error}`;
|
||
}else{
|
||
logText += `(最近 ${data.recent_error.trim().split('\n').length} 行)\n${data.recent_error || '无错误'}`;
|
||
}
|
||
}
|
||
|
||
logEl.textContent = logText;
|
||
scrollLog();
|
||
}else{
|
||
logEl.textContent = `操作: ${action}\n${data.output || '成功'}\n\n正在刷新状态...`;
|
||
scrollLog();
|
||
setTimeout(fetchStatus, 1500);
|
||
}
|
||
}catch(err){
|
||
logEl.textContent = `请求失败: ${err.message}`;
|
||
scrollLog();
|
||
updateStatus('unknown');
|
||
}finally{
|
||
if(btn) btn.classList.remove('loading');
|
||
}
|
||
}
|
||
|
||
async function fetchStatus(){ pm2Action('status'); }
|
||
|
||
// 按钮事件委托
|
||
document.getElementById('pm2Buttons').addEventListener('click', e=>{
|
||
const btn = e.target.closest('button');
|
||
if(!btn) return;
|
||
const action = btn.dataset.action;
|
||
if(action && action !== 'status') pm2Action(action);
|
||
});
|
||
|
||
// 自动刷新(每 1 秒)
|
||
setInterval(fetchStatus, 1000);
|
||
fetchStatus();
|
||
</script>
|
||
</body>
|
||
</html>
|