diff --git a/src/config_server.cpp b/src/config_server.cpp index 6987fc1..a1f08bb 100644 --- a/src/config_server.cpp +++ b/src/config_server.cpp @@ -324,8 +324,13 @@ const char* index_html() .warn { color:#9a3412; } .ok { color:#166534; } .muted { color:#667785; } + .help { margin-top:4px; font-size:12px; line-height:1.35; color:#667785; } + .status-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px; margin-bottom:12px; } + .status-box { border:1px solid #e1e7ed; border-radius:6px; padding:10px; background:#fbfcfd; } + .status-box strong { display:block; margin-bottom:4px; color:#243746; } + .url-cell { max-width:360px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } #toast { min-height:24px; margin-left:6px; } - @media (max-width: 860px) { .grid, .grid.two { grid-template-columns:1fr; } table { display:block; overflow-x:auto; } } + @media (max-width: 860px) { .grid, .grid.two, .status-grid { grid-template-columns:1fr; } table { display:block; overflow-x:auto; } } @@ -419,6 +424,35 @@ async function api(path, options) { function setToast(text, cls='muted') { const t=$('toast'); t.className=cls; t.textContent=text; } +function helpFor(id, text) { + const el = $(id); + if (!el || !el.parentElement || el.parentElement.querySelector('.help')) return; + const div = document.createElement('div'); + div.className = 'help'; + div.textContent = text; + el.parentElement.appendChild(div); +} + +function installHelp() { + helpFor('srs_root', 'SRS安装根目录,安装脚本会把 srs/bin、conf、html 放在这里。'); + helpFor('record_config', 'video服务读取这个SRS录像配置来解析录像目录、HTTP端口和切片时长。'); + helpFor('public_interface', '用于生成对外播放地址的网卡名,比如 enP2p33s0;取不到IP会回退到127.0.0.1。'); + helpFor('stream_app', 'RTMP路径里的app名,默认 camera,对应 rtmp://host/app/stream。'); + helpFor('live_enabled', '控制是否向 live SRS 推实时流,关闭后不会生成实时播放分支。'); + helpFor('record_enabled', '控制是否向 record SRS 推流并由SRS写录像。'); + helpFor('record_path', 'SRS DVR保存mp4切片的根目录,目标设备上要确保可写。'); + helpFor('dvr_duration', '每个录像文件的切片长度,单位秒。'); + helpFor('retention_days', '录像保留天数,超过后由video服务扫描清理。'); + helpFor('usage_threshold', '磁盘使用率阈值,0.9表示90%,超过后按最老小时清理。'); + helpFor('live_rtmp_port', 'video服务把实时流推到这个RTMP端口。'); + helpFor('live_http_api_port', 'SRS WHEP/WebRTC播放接口端口,页面和MQTT返回的实时播放URL使用它。'); + helpFor('live_http_server_port', 'SRS自带静态页面和HTTP-FLV调试入口。'); + helpFor('live_rtc_port', 'WebRTC UDP媒体端口,需要网络放通。'); + helpFor('record_rtmp_port', 'video服务把录像流推到这个RTMP端口。'); + helpFor('record_http_api_port', 'record实例的SRS HTTP API端口。'); + helpFor('record_http_server_port', '录像文件HTTP访问端口,回放URL会走这个HTTP server。'); +} + function fillConfig() { const m = config.mqtt_server || {}; const s = config.srs || {}; @@ -497,12 +531,42 @@ async function loadConfig() { config = await api('/api/config'); fillConfig(); } async function loadStatus() { const st = await api('/api/status'); const disk = st.disk || {}; + $('status').innerHTML = renderStatus(st, disk); + return; $('status').innerHTML = `
对外IP:${st.public_ip || 'unknown'}
录像磁盘:${disk.ok ? `${disk.used_percent.toFixed(1)}% used` : `${disk.error || 'unavailable'}`}
服务:${(st.services||[]).map(s => `${s.name}: ${s.active}`).join(' | ')}
通道:${(st.channels||[]).map(c => `${c.name} ${c.device_exists?'':'(无设备)'} ${c.running?'运行':'停止'}`).join(',')}
`; } + +function escapeHtml(v) { + return String(v ?? '').replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch])); +} + +function renderStatus(st, disk) { + const services = (st.services || []).map(s => `${escapeHtml(s.name)}: ${escapeHtml(s.active)}`).join('
'); + const diskText = disk.ok ? `${Number(disk.used_percent || 0).toFixed(1)}% used
${escapeHtml(disk.path)}
` : `${escapeHtml(disk.error || 'unavailable')}`; + const channelRows = (st.channels || []).map(c => { + const running = c.running ? '运行' : '停止'; + const enabled = c.enabled ? '启用' : '禁用'; + const exists = c.device_exists ? '存在' : '不存在'; + const reason = c.reason || (c.enabled ? '' : '通道已禁用'); + const url = c.url ? `${escapeHtml(c.url)}` : ''; + return `${escapeHtml(c.name)}${escapeHtml(c.device)}${enabled}${exists}${running}${escapeHtml(reason)}${url}`; + }).join(''); + return ` +
+
对外IP${escapeHtml(st.public_ip || 'unknown')}
+
录像磁盘${diskText}
+
服务${services || ''}
+
+ + + ${channelRows || ''} +
通道设备配置设备节点推流状态原因播放地址
无通道
`; +} + async function saveConfig() { try { collectConfig(); @@ -510,7 +574,7 @@ async function saveConfig() { setToast(res.message || '已保存,重启服务后生效', res.ok ? 'ok' : 'warn'); } catch (e) { setToast(e.message, 'warn'); } } -async function loadAll() { try { await loadConfig(); await loadStatus(); setToast('已刷新'); } catch(e) { setToast(e.message, 'warn'); } } +async function loadAll() { try { await loadConfig(); installHelp(); await loadStatus(); setToast('已刷新'); } catch(e) { setToast(e.message, 'warn'); } } loadAll();