|
|
@@ -32,6 +32,7 @@ body{font-family:system-ui,sans-serif;background:#0b1220;color:#e8e8e8;height:10
|
|
|
#videoWrap{flex:1;position:relative;background:#000;min-height:0}
|
|
|
#videoWrap img{width:100%;height:100%;object-fit:contain;display:block}
|
|
|
#videoLoader{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#888}
|
|
|
+#bgState{display:none;position:absolute;top:8px;left:50%;transform:translateX(-50%);background:rgba(233,69,96,.9);color:#fff;font-size:12px;padding:5px 12px;border-radius:4px}
|
|
|
.chSwitcher{display:flex;overflow-x:auto;padding:8px;background:#0e1426;border-top:1px solid #0f3460;flex-shrink:0}
|
|
|
.chSwitcher button{padding:8px 14px;margin-right:8px;background:#0f3460;border:none;color:#eee;border-radius:4px;font-size:13px;white-space:nowrap;cursor:pointer}
|
|
|
.chSwitcher button.active{background:#e94560}
|
|
|
@@ -87,6 +88,7 @@ body{font-family:system-ui,sans-serif;background:#0b1220;color:#e8e8e8;height:10
|
|
|
<div id="videoWrap">
|
|
|
<img id="video" alt="">
|
|
|
<div id="videoLoader">加载中...</div>
|
|
|
+ <div id="bgState">后台运行中(视频已暂停)</div>
|
|
|
</div>
|
|
|
<div class="chSwitcher" id="chSwitcher"></div>
|
|
|
</div>
|
|
|
@@ -164,6 +166,16 @@ async function init() {
|
|
|
await loadChannels();
|
|
|
renderChSwitcher();
|
|
|
switchChannel(0);
|
|
|
+ notifyNativeReady();
|
|
|
+}
|
|
|
+
|
|
|
+// 通知 Android 宿主 H5 已就绪(宿主可据此开始转发生命周期事件)
|
|
|
+function notifyNativeReady() {
|
|
|
+ try {
|
|
|
+ if (window.AndroidBridge && window.AndroidBridge.onWebReady) {
|
|
|
+ window.AndroidBridge.onWebReady();
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
}
|
|
|
|
|
|
async function loadChannels() {
|
|
|
@@ -192,7 +204,7 @@ function switchChannel(id) {
|
|
|
|
|
|
// 手机浏览器对multipart MJPEG兼容性不一致, 使用单帧JPEG轮询保证兼容
|
|
|
function startVideoPolling(id) {
|
|
|
- if (videoTimer) { clearTimeout(videoTimer); videoTimer = null; }
|
|
|
+ stopVideoPolling();
|
|
|
const v = document.getElementById('video');
|
|
|
videoUrl = '/stream/' + id;
|
|
|
videoBusy = false;
|
|
|
@@ -200,6 +212,8 @@ function startVideoPolling(id) {
|
|
|
document.getElementById('videoLoader').style.display = 'block';
|
|
|
const next = () => {
|
|
|
if (curChannel !== id) return;
|
|
|
+ // 后台运行时不拉流(省流量/CPU), 回前台由 setBackground 恢复
|
|
|
+ if (isBackground) { videoTimer = setTimeout(next, 500); return; }
|
|
|
const img = new Image();
|
|
|
videoBusy = true;
|
|
|
img.onload = () => {
|
|
|
@@ -220,12 +234,72 @@ function startVideoPolling(id) {
|
|
|
next();
|
|
|
}
|
|
|
|
|
|
+function stopVideoPolling() {
|
|
|
+ if (videoTimer) { clearTimeout(videoTimer); videoTimer = null; }
|
|
|
+ videoBusy = false;
|
|
|
+}
|
|
|
+
|
|
|
+/* ============ 后台运行检测 ============
|
|
|
+ * 三种来源:
|
|
|
+ * 1) Android WebView 原生回调 (最可靠, 宿主 onPause/onResume 时调用)
|
|
|
+ * 2) Page Visibility API (浏览器/部分WebView支持)
|
|
|
+ * 3) 心跳兜底: 定时器被系统挂起时, 恢复后检测时间跳变判定经过后台
|
|
|
+ * ======================================= */
|
|
|
+let isBackground = false;
|
|
|
+let lastHeartbeat = Date.now();
|
|
|
+
|
|
|
+function setBackground(bg) {
|
|
|
+ if (isBackground === bg) return;
|
|
|
+ isBackground = bg;
|
|
|
+ const badge = document.getElementById('bgState');
|
|
|
+ if (badge) {
|
|
|
+ badge.textContent = '后台运行中(视频已暂停)';
|
|
|
+ badge.style.display = bg ? 'block' : 'none';
|
|
|
+ }
|
|
|
+ if (bg) {
|
|
|
+ stopVideoPolling(); // 进后台: 停止拉流省流量
|
|
|
+ } else if (!document.getElementById('videoPage').classList.contains('hidden')) {
|
|
|
+ startVideoPolling(curChannel); // 回前台: 恢复
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 1) 供 Android 原生调用: webView.evaluateJavascript("onAndroidLifecycle('pause')")
|
|
|
+window.onAndroidLifecycle = function(state) {
|
|
|
+ setBackground(state === 'pause' || state === 'background' || state === 'stop');
|
|
|
+};
|
|
|
+window.onNativeBackground = function() { setBackground(true); };
|
|
|
+window.onNativeForeground = function() { setBackground(false); };
|
|
|
+
|
|
|
+// 2) 浏览器 Page Visibility API
|
|
|
+document.addEventListener('visibilitychange', () => {
|
|
|
+ setBackground(document.hidden);
|
|
|
+});
|
|
|
+window.addEventListener('blur', () => { /* 仅失焦不一定是后台, 不处理 */ });
|
|
|
+window.addEventListener('pagehide', () => setBackground(true));
|
|
|
+window.addEventListener('pageshow', () => setBackground(false));
|
|
|
+
|
|
|
+// 3) 心跳兜底: 定时器被挂起后, 间隔异常变大说明经历过后台
|
|
|
+setInterval(() => {
|
|
|
+ const now = Date.now();
|
|
|
+ const gap = now - lastHeartbeat;
|
|
|
+ lastHeartbeat = now;
|
|
|
+ if (gap > 3000 && !document.hidden) {
|
|
|
+ // 定时器被冻结超过3秒后恢复 -> 刚从后台回来
|
|
|
+ setBackground(false);
|
|
|
+ }
|
|
|
+}, 1000);
|
|
|
+
|
|
|
function switchTab(tab) {
|
|
|
document.getElementById('tabVideo').classList.toggle('active', tab==='video');
|
|
|
document.getElementById('tabAlarm').classList.toggle('active', tab==='alarm');
|
|
|
document.getElementById('videoPage').classList.toggle('hidden', tab!=='video');
|
|
|
document.getElementById('alarmPage').classList.toggle('hidden', tab!=='alarm');
|
|
|
- if (tab === 'alarm') loadAlarms(true);
|
|
|
+ if (tab === 'alarm') {
|
|
|
+ stopVideoPolling(); // 看报警时暂停视频拉流, 省流量
|
|
|
+ loadAlarms(true);
|
|
|
+ } else if (!document.hidden) {
|
|
|
+ startVideoPolling(curChannel); // 回到视频页恢复
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function poseColor(a) {
|