This commit is contained in:
Your Name
2026-01-27 08:55:32 +00:00
parent 6fbd86bf43
commit 3ccbf3d973
9 changed files with 182 additions and 78 deletions

View File

@@ -4,7 +4,6 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>多进程录制控制台</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}}
@@ -14,17 +13,17 @@ h1{margin:0 0 24px;font-size:1.6em;display:flex;align-items:center;gap:12px}
#processSelect{padding:8px;border-radius:8px;border:1px solid #ddd;font-size:1em}
#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}
input,button,textarea{width:100%;padding:12px;margin:8px 0;border-radius:8px;border:none;box-sizing:border-box;font-size:1em}
input,textarea{background:#f5f5f5;color:#000;border:1px solid #ddd;font-family:monospace}
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}
#configButtons{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px;margin:12px 0}
#configStatus{color:#0f0;margin-top:8px;font-family:monospace}
#obsAddress{background:#f0f0f0;padding:12px;border-radius:8px;font-family:monospace;font-size:1.1em;margin:8px 0;word-break:break-all}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
</head>
@@ -39,13 +38,26 @@ video{position:absolute;inset:0;width:100%;height:100%}
</select>
<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>
<!-- YouTube 配置编辑区(仅 youtube 时显示) -->
<div id="configSection" style="display:none;">
<h2>YouTube 配置编辑 (config/youtube.ini)</h2>
<textarea id="youtubeConfig" rows="20"></textarea>
<div id="configButtons">
<button id="loadConfigBtn">加载配置</button>
<button id="saveConfigBtn">保存配置</button>
</div>
<pre id="configStatus">就绪</pre>
</div>
<!-- OBS 推流地址区(仅 obs 时显示) -->
<div id="obsSection" style="display:none;">
<h2>OBS 直播推流地址</h2>
<p>请在 OBS → 设置 → 推流 → 服务选择“自定义”,服务器填写以下地址:</p>
<div id="obsAddress">srt://live.local:10080/live/obs</div>
<p>密钥Stream Key留空即可。</p>
</div>
<h2>PM2 进程控制</h2>
<div id="pm2Buttons">
<button data-action="start">启动</button>
@@ -57,70 +69,48 @@ video{position:absolute;inset:0;width:100%;height:100%}
<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');
const processSelect = document.getElementById('processSelect');
let currentProcess = 'tiktok';
let currentProcess = 'tiktok'; // 默认进程
// 各专属区域
const configSection = document.getElementById('configSection');
const obsSection = document.getElementById('obsSection');
const youtubeConfig = document.getElementById('youtubeConfig');
const loadConfigBtn = document.getElementById('loadConfigBtn');
const saveConfigBtn = document.getElementById('saveConfigBtn');
const configStatus = document.getElementById('configStatus');
// 当选择进程改变时,更新当前进程并刷新状态
processSelect.addEventListener('change', () => {
currentProcess = processSelect.value;
fetchStatus();
updateSectionsVisibility();
});
function httpToHttps(url){return url.replace(/^http:/,"https:")}
// 根据当前进程显示/隐藏对应区域
function updateSectionsVisibility() {
configSection.style.display = currentProcess === 'youtube' ? 'block' : 'none';
obsSection.style.display = currentProcess === 'obs' ? 'block' : 'none';
if (currentProcess === 'youtube') {
loadConfig();
}
}
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 = '等待重启'; }
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 控制 + 实时状态/日志(所有请求都带 ?process=xxx
async function pm2Action(action){
const btn = document.querySelector(`[data-action="${action}"]`);
if(btn) btn.classList.add('loading');
@@ -166,6 +156,44 @@ async function pm2Action(action){
async function fetchStatus(){ pm2Action('status'); }
// 配置加载
async function loadConfig(){
configStatus.textContent = '加载中...';
try{
const res = await fetch(`/get_config?process=${currentProcess}`);
if(!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
youtubeConfig.value = data.content || '';
configStatus.textContent = '加载成功';
}catch(err){
configStatus.textContent = `加载失败: ${err.message}`;
}
}
// 配置保存
async function saveConfig(){
configStatus.textContent = '保存中...';
const content = youtubeConfig.value;
try{
const res = await fetch(`/save_config?process=${currentProcess}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({content})
});
if(!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
configStatus.textContent = data.message || '保存成功';
}catch(err){
configStatus.textContent = `保存失败: ${err.message}`;
}
}
loadConfigBtn.onclick = loadConfig;
saveConfigBtn.onclick = () => {
saveConfigBtn.classList.add('loading');
saveConfig().finally(() => saveConfigBtn.classList.remove('loading'));
};
// 按钮事件委托
document.getElementById('pm2Buttons').addEventListener('click', e=>{
const btn = e.target.closest('button');
@@ -174,9 +202,12 @@ document.getElementById('pm2Buttons').addEventListener('click', e=>{
if(action && action !== 'status') pm2Action(action);
});
// 自动刷新(每 1 秒)
// 自动刷新状态与日志
setInterval(fetchStatus, 1000);
// 初始加载
fetchStatus();
updateSectionsVisibility();
</script>
</body>
</html>