Parcourir la source

手机端支持后台运行检测(适配Android WebView)

- 三种检测来源:
  1) Android原生桥: onAndroidLifecycle/onNativeBackground/onNativeForeground
  2) Page Visibility API(visibilitychange/pagehide/pageshow)
  3) 心跳兜底: 定时器冻结超3秒后恢复判定经过后台
- 后台时暂停视频拉流(省流量/服务器CPU), 回前台自动恢复
- 切到报警页也暂停视频拉流
- 页面顶部显示"后台运行中"状态
- H5就绪时回调AndroidBridge.onWebReady
dev il y a 6 jours
Parent
commit
c782c091b3
1 fichiers modifiés avec 76 ajouts et 2 suppressions
  1. 76 2
      web/mobile.html

+ 76 - 2
web/mobile.html

@@ -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) {