safesight/control/internal/web/ui/templates/monitor.html
tian b57fef13d4 refactor(control): 视频监控页数据与运行看板统一(服务端渲染)
问题: monitor 页前端 fetch /api/monitor/channels 拿残缺数据
(仅通道名),再打补丁关联视频源名——数据源不统一。
修复: 提取 consolePageData() 共用,monitor 页服务端渲染
ConsoleDevices.Channels(含 SourceName/Display),显示
'设备名 · 视频源名',与看板完全同源。
撤销 channelMetaMap 补丁(不再需要);monitor JS 直接用
data-channel 构造 HLS URL。
2026-08-02 14:10:08 +08:00

111 lines
4.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{define "monitor"}}
<div class="card">
<div class="section-title">
<div>
<h2 class="title-with-icon">{{icon "devices"}}<span>视频监控</span></h2>
<div class="form-hint">所有在线设备的实时画面</div>
</div>
<div class="actions compact">
<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">
{{$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 esc(s) { return (s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"); }
// 视频源状态覆盖层(监控页看的是 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';
}
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);
});
// 拉取各设备视频源状态(通道名=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);
});
})
.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;
}
});
}
initMonitor();
</script>
{{end}}