refactor(control): 视频监控页数据与运行看板统一(服务端渲染)
问题: monitor 页前端 fetch /api/monitor/channels 拿残缺数据 (仅通道名),再打补丁关联视频源名——数据源不统一。 修复: 提取 consolePageData() 共用,monitor 页服务端渲染 ConsoleDevices.Channels(含 SourceName/Display),显示 '设备名 · 视频源名',与看板完全同源。 撤销 channelMetaMap 补丁(不再需要);monitor JS 直接用 data-channel 构造 HLS URL。
This commit is contained in:
parent
8663f8cee4
commit
b57fef13d4
@ -1075,10 +1075,9 @@ func (u *UI) pageDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
u.render(w, r, "dashboard", data)
|
||||
}
|
||||
|
||||
func (u *UI) pageConsole(w http.ResponseWriter, r *http.Request) {
|
||||
func (u *UI) consolePageData() PageData {
|
||||
u.ensureDevicesLoaded()
|
||||
data := PageData{Title: "运行看板"}
|
||||
data.Message = strings.TrimSpace(r.URL.Query().Get("msg"))
|
||||
devices := u.registry.GetDevices()
|
||||
data.Devices = devices
|
||||
|
||||
@ -1316,6 +1315,12 @@ func (u *UI) pageConsole(w http.ResponseWriter, r *http.Request) {
|
||||
data.ConsoleVideoSources = unassigned
|
||||
data.ConsoleAllVideoSources = allSources
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
func (u *UI) pageConsole(w http.ResponseWriter, r *http.Request) {
|
||||
data := u.consolePageData()
|
||||
data.Message = strings.TrimSpace(r.URL.Query().Get("msg"))
|
||||
u.render(w, r, "console", data)
|
||||
}
|
||||
|
||||
@ -5957,8 +5962,9 @@ func (u *UI) actionDeviceFaceGalleryReload(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
func (u *UI) pageMonitor(w http.ResponseWriter, r *http.Request) {
|
||||
u.ensureDevicesLoaded()
|
||||
u.render(w, r, "monitor", PageData{Title: "视频监控", Devices: u.registry.GetDevices()})
|
||||
data := u.consolePageData()
|
||||
data.Title = "视频监控"
|
||||
u.render(w, r, "monitor", data)
|
||||
}
|
||||
|
||||
func (u *UI) apiMonitorChannels(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@ -6,134 +6,105 @@
|
||||
<div class="form-hint">所有在线设备的实时画面</div>
|
||||
</div>
|
||||
<div class="actions compact">
|
||||
<button class="btn ghost" type="button" onclick="loadAll()">刷新</button>
|
||||
<button class="btn ghost" type="button" onclick="location.reload()">刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="video-wall" style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;margin-top:16px">
|
||||
<div class="card muted" style="text-align:center;grid-column:1/-1">加载中…</div>
|
||||
{{$hasCard := false}}
|
||||
{{range $cd := .ConsoleDevices}}{{if $cd.Device.Online}}{{range $ch := $cd.Channels}}
|
||||
{{$hasCard = true}}
|
||||
<div class="card video-monitor-card" style="padding:8px;position:relative" data-device-id="{{$cd.Device.DeviceID}}" data-channel="{{$ch.Name}}">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
||||
<span style="font-size:12px;font-weight:500">{{$cd.Device.DisplayName}} · {{if $ch.SourceName}}{{$ch.SourceName}}{{else}}{{$ch.Display}}{{end}}</span>
|
||||
</div>
|
||||
<video class="video-monitor-video" controls autoplay muted style="width:100%;max-height:340px;background:#000;position:relative;z-index:1"></video>
|
||||
</div>
|
||||
{{end}}{{end}}{{end}}
|
||||
{{if not $hasCard}}
|
||||
<div class="card muted" style="text-align:center;grid-column:1/-1">暂无在线设备的视频通道</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<script src="/assets/vendor/hls.min.js"></script>
|
||||
<script>
|
||||
function loadAll() {
|
||||
var wall = document.getElementById("video-wall");
|
||||
wall.innerHTML = '<div class="card muted" style="text-align:center;grid-column:1/-1">加载中…</div>';
|
||||
function esc(s) { return (s||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""); }
|
||||
|
||||
var devices = [
|
||||
{{range .Devices}}{{if .Online}}
|
||||
{id:"{{.DeviceID}}",name:"{{.DisplayName}}"},
|
||||
{{end}}{{end}}
|
||||
];
|
||||
if (devices.length === 0) {
|
||||
wall.innerHTML = '<div class="card muted" style="text-align:center;grid-column:1/-1">暂无在线设备</div>';
|
||||
// 视频源状态覆盖层(监控页看的是 HLS 输出,标题用输出层面措辞,原因标注根因)
|
||||
function applyStatus(card, status, hlsError) {
|
||||
card._sourceStatus = status; // 记住最近状态,供 HLS 错误回调取用
|
||||
var overlay = card.querySelector('.video-status-overlay');
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'video-status-overlay';
|
||||
card.appendChild(overlay);
|
||||
}
|
||||
if (hlsError) {
|
||||
var why = (status && status.last_error) ? ('视频源异常:' + esc(status.last_error)) : 'HLS 流不可用,请检查视频源';
|
||||
overlay.innerHTML = '<div class="video-status-text offline">无视频输出</div><div class="video-status-detail">' + why + '</div>';
|
||||
overlay.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
if (!status) {
|
||||
overlay.innerHTML = '<div class="video-status-text unknown">状态未知</div>';
|
||||
overlay.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
if (status.streaming) {
|
||||
// streaming=true 表示正在接收视频帧(media 实时状态),有画面,隐藏覆盖层
|
||||
overlay.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
overlay.innerHTML = '<div class="video-status-text offline">无视频输出</div>' +
|
||||
(status.last_error ? '<div class="video-status-detail">视频源异常:' + esc(status.last_error) + '</div>' : '<div class="video-status-detail">视频源离线</div>');
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
|
||||
var promises = devices.map(function(dev) {
|
||||
return fetch('/api/monitor/channels?device_id=' + encodeURIComponent(dev.id))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) { return (data.channels||[]).map(function(ch) { ch._dev = dev.name; ch._devId = dev.id; return ch; }); })
|
||||
.catch(function() { return []; });
|
||||
function initMonitor() {
|
||||
var cards = Array.prototype.slice.call(document.querySelectorAll('.video-monitor-card'));
|
||||
if (cards.length === 0) return;
|
||||
var devIds = [];
|
||||
cards.forEach(function(card) {
|
||||
var did = card.getAttribute('data-device-id');
|
||||
if (devIds.indexOf(did) < 0) devIds.push(did);
|
||||
applyStatus(card, null, false);
|
||||
});
|
||||
|
||||
Promise.all(promises).then(function(results) {
|
||||
var all = [];
|
||||
results.forEach(function(chs) { all = all.concat(chs); });
|
||||
if (all.length === 0) {
|
||||
wall.innerHTML = '<div class="card muted" style="text-align:center;grid-column:1/-1">没有可预览的通道</div>';
|
||||
return;
|
||||
}
|
||||
var cols = all.length <= 2 ? all.length : 2;
|
||||
wall.style.gridTemplateColumns = "repeat(" + cols + ",minmax(0,1fr))";
|
||||
var html = "";
|
||||
all.forEach(function(ch, i) {
|
||||
var proxyUrl = '/hls/' + ch._devId + '/hls/' + ch.name + '/index.m3u8';
|
||||
html += '<div class="card" style="padding:8px;position:relative" data-device-id="' + ch._devId + '" data-channel="' + esc(ch.name) + '">';
|
||||
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">';
|
||||
html += '<span style="font-size:12px;font-weight:500">' + esc(ch._dev) + ' · ' + esc(ch.name) + '</span>';
|
||||
html += '</div>';
|
||||
if (ch.hls_url) {
|
||||
html += '<video id="v' + i + '" controls autoplay muted style="width:100%;max-height:340px;background:#000;position:relative;z-index:1"></video>';
|
||||
} else {
|
||||
html += '<div class="muted" style="height:200px;display:flex;align-items:center;justify-content:center">无预览地址</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
wall.innerHTML = html;
|
||||
// 视频源状态覆盖层(监控页看的是 HLS 输出,标题用输出层面措辞,原因标注根因)
|
||||
function applyStatus(card, status, hlsError) {
|
||||
card._sourceStatus = status; // 记住最近状态,供 HLS 错误回调取用
|
||||
var overlay = card.querySelector('.video-status-overlay');
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'video-status-overlay';
|
||||
card.appendChild(overlay);
|
||||
}
|
||||
if (hlsError) {
|
||||
var why = (status && status.last_error) ? ('视频源异常:' + esc(status.last_error)) : 'HLS 流不可用,请检查视频源';
|
||||
overlay.innerHTML = '<div class="video-status-text offline">无视频输出</div><div class="video-status-detail">' + why + '</div>';
|
||||
overlay.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
if (!status) {
|
||||
overlay.innerHTML = '<div class="video-status-text unknown">状态未知</div>';
|
||||
overlay.style.display = 'flex';
|
||||
return;
|
||||
}
|
||||
if (status.streaming) {
|
||||
// streaming=true 表示正在接收视频帧(media 实时状态),有画面,隐藏覆盖层
|
||||
overlay.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
overlay.innerHTML = '<div class="video-status-text offline">无视频输出</div>' +
|
||||
(status.last_error ? '<div class="video-status-detail">视频源异常:' + esc(status.last_error) + '</div>' : '<div class="video-status-detail">视频源离线</div>');
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
|
||||
// 拉取各设备视频源状态
|
||||
var statusByDev = {};
|
||||
devices.forEach(function(dev) {
|
||||
fetch('/api/monitor/video-status?device_id=' + encodeURIComponent(dev.id))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var byName = {};
|
||||
(data.sources || []).forEach(function(s) { byName[s.graph_name] = s; });
|
||||
statusByDev[dev.id] = byName;
|
||||
all.forEach(function(ch) {
|
||||
if (ch._devId !== dev.id) return;
|
||||
var card = wall.querySelector('[data-device-id="' + ch._devId + '"][data-channel="' + cssEscape(ch.name) + '"]');
|
||||
if (card) applyStatus(card, byName[ch.name], false);
|
||||
});
|
||||
})
|
||||
.catch(function() {});
|
||||
});
|
||||
|
||||
// Initialize HLS players
|
||||
all.forEach(function(ch, i) {
|
||||
if (!ch.hls_url || !ch._devId) return;
|
||||
var proxyUrl = '/hls/' + ch._devId + '/hls/' + ch.name + '/index.m3u8';
|
||||
var video = document.getElementById('v' + i);
|
||||
if (!video) return;
|
||||
if (Hls.isSupported()) {
|
||||
var hls = new Hls();
|
||||
hls.on(Hls.Events.ERROR, function(evt, data) {
|
||||
if (data.fatal) {
|
||||
var card = video.closest('.card');
|
||||
if (card) applyStatus(card, card._sourceStatus, true);
|
||||
}
|
||||
// 拉取各设备视频源状态(通道名=graph 名匹配)
|
||||
devIds.forEach(function(did) {
|
||||
fetch('/api/monitor/video-status?device_id=' + encodeURIComponent(did))
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var byName = {};
|
||||
(data.sources || []).forEach(function(s) { byName[s.graph_name] = s; });
|
||||
cards.forEach(function(card) {
|
||||
if (card.getAttribute('data-device-id') !== did) return;
|
||||
applyStatus(card, byName[card.getAttribute('data-channel')], false);
|
||||
});
|
||||
hls.loadSource(proxyUrl);
|
||||
hls.attachMedia(video);
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = ch.hls_url;
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function() {});
|
||||
});
|
||||
|
||||
// HLS 播放(通道名构造代理 URL)
|
||||
cards.forEach(function(card) {
|
||||
var did = card.getAttribute('data-device-id');
|
||||
var ch = card.getAttribute('data-channel');
|
||||
var video = card.querySelector('.video-monitor-video');
|
||||
if (!video) return;
|
||||
var proxyUrl = '/hls/' + encodeURIComponent(did) + '/hls/' + encodeURIComponent(ch) + '/index.m3u8';
|
||||
if (Hls.isSupported()) {
|
||||
var hls = new Hls();
|
||||
hls.on(Hls.Events.ERROR, function(evt, data) {
|
||||
if (data.fatal) applyStatus(card, card._sourceStatus, true);
|
||||
});
|
||||
hls.loadSource(proxyUrl);
|
||||
hls.attachMedia(video);
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = proxyUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
function cssEscape(s) { return String(s).replace(/["\\]/g, '\\$&'); }
|
||||
function esc(s) { return (s||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""); }
|
||||
loadAll();
|
||||
initMonitor();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user