dsh-mpkg-wallpaper 3.13.8 → 3.13.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/client.js +335 -21
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -400,6 +400,7 @@ Start here when something is wrong: one click sends state back to the local host
|
|
|
400
400
|
| `?mpwstore=0\|mem` | Disable the in-frame storage facade / keep it in memory without persisting |
|
|
401
401
|
| `?mpwpersist=legacy` | Settings persistence back to the old behaviour |
|
|
402
402
|
| `?bgwrapfix=legacy`, `?hdrfrostwatch=off` | Wallpaper-layer visibility / frost watchdog |
|
|
403
|
+
| `?hiddengate=legacy` | **Whole hidden-gate family back to the old behaviour** (no more blocking our autoplay/retries while the page is hidden, frozen or background-reloaded; for A/B and emergencies. Default = never start playback while hidden, stop every sound surface we own when hidden, resume per policy on return; see [`docs/WALLPAPER-LIFECYCLE.md`](docs/WALLPAPER-LIFECYCLE.md) §6.4) |
|
|
403
404
|
|
|
404
405
|
### Renderer-side diagnostics flags (pure Scene wallpapers)
|
|
405
406
|
|
package/README.md
CHANGED
|
@@ -401,6 +401,7 @@ node tools/build-bundle.mjs
|
|
|
401
401
|
| `?mpwstore=0\|mem` | 关帧内存储 facade / 只留内存不落盘 |
|
|
402
402
|
| `?mpwpersist=legacy` | 设置持久化回旧行为 |
|
|
403
403
|
| `?bgwrapfix=legacy`、`?hdrfrostwatch=off` | 壁纸层可见性 / 磨砂看门狗 |
|
|
404
|
+
| `?hiddengate=legacy` | **隐藏闸门整族回旧行为**(后台挂载/冻结/切页时不再拦我们的起播与重试;A/B 与线上救急用。默认档=页面隐藏时一律不起播、隐藏时停住所有我们的出声面、回到可见按当时策略恢复;详见 [`docs/WALLPAPER-LIFECYCLE.md`](docs/WALLPAPER-LIFECYCLE.md) §6.4) |
|
|
404
405
|
|
|
405
406
|
### 渲染器侧诊断开关(纯 Scene 壁纸)
|
|
406
407
|
|
package/lib/client.js
CHANGED
|
@@ -147,7 +147,13 @@ function mpwTrace(ev, data) {
|
|
|
147
147
|
count("retries");
|
|
148
148
|
try { console.warn("[dsh-mpkg-wallpaper] 🔁 " + tag + " rs=0 无元数据 → 第 " + tries + "/" + RETRY_MAX + " 次补 load()(上游 Firefox Bug " + BUG + ";依据渲染器仓探针 tests/mpkg-videobase-recovery-probe.mjs:只补一次 load() 即恢复)"); } catch (e) {}
|
|
149
149
|
try { el.load() } catch (e) {}
|
|
150
|
-
|
|
150
|
+
/* H(2026-09-25) hidden 期间**只补 load()、不起播**(补 load 是为了解上游 Bug 2056444 的
|
|
151
|
+
元数据挂起;起播要等可见 —— 见 mpwHiddenBootGate 那一节)。
|
|
152
|
+
写法用 `typeof … === "function"` 是**故意的**:本块被 tools/blob-media-retry-test.mjs
|
|
153
|
+
按 BEGIN/END 标记**整段切出来独立求值**(那条判据 S1-4 明确要求"不依赖闭包里的其它名字")
|
|
154
|
+
⇒ 生产里它是闭包函数、照常过闸;切出来跑时它是 undefined、行为与修前逐字一致。
|
|
155
|
+
"这一发真的接上了闸门"由 tools/hidden-gate-test.mjs 的源码锚点单独钉住(删掉即红)。 */
|
|
156
|
+
if (!(typeof mpwPlayBlockedBy === "function" && mpwPlayBlockedBy("blob-retry"))) { try { const p = el.play(); if (p && typeof p.catch === "function") p.catch(() => {}) } catch (e) {} }
|
|
151
157
|
timer = setTimeout(tick, RETRY_GAP_MS);
|
|
152
158
|
} catch (e) {}
|
|
153
159
|
};
|
|
@@ -819,7 +825,9 @@ function sectionSigNow() {
|
|
|
819
825
|
img.src = desired;
|
|
820
826
|
try { img.load && img.load() } catch {}
|
|
821
827
|
}
|
|
822
|
-
|
|
828
|
+
/* H(2026-09-25) 这条 120ms 延迟重放是**后台重载**那条路上最容易漏的一发:它不查 hidden,
|
|
829
|
+
而 `video.load()` 之后的 `play()` 正是"挂载期起播"的形态(hidden 时不 play)。 */
|
|
830
|
+
if (video) { try { video.load(); if (!mpwPlayBlockedBy("auto-refresh")) { const p = video.play(); if (p && p.catch) p.catch(() => {}) } } catch {} }
|
|
823
831
|
try { applyFromStorage(); } catch (e) { mpwErr("applyFromStorage(autoRefresh 延迟重放)", e); }
|
|
824
832
|
} catch (e) {}
|
|
825
833
|
}, 120);
|
|
@@ -3517,7 +3525,9 @@ function mpwThumbFail(el, why) {
|
|
|
3517
3525
|
// 无条件 play(),把用户手动暂停解开(用户实测"暂停后开启效果又解开暂停")。
|
|
3518
3526
|
// ①(NP-4) 门控**在 npPrimePlay 里**(它自己判 wallUserPaused/powPaused),
|
|
3519
3527
|
// 并且把"静音/电平落点"挪到**起播成功之后**(顺序根因见 npPrimePlay 注释)。
|
|
3520
|
-
|
|
3528
|
+
// H(2026-09-25) 再加一道**挂载期**闸门(在 npPrimePlay 之前):挂载完成时页面隐藏 ⇒
|
|
3529
|
+
// 一律不起播,只记一条台账,等第一次可见时按当时策略恢复(见 mpwHiddenBootGate)。
|
|
3530
|
+
if (!mpwHiddenBootGate("showVideoEl")) { try { npPrimePlay(video); } catch {} }
|
|
3521
3531
|
/* ②(2026-09-20 审计第 2 条) 视频档同样验活:源不可用 ⇒ 撤 src + 记状态 + 层隐藏
|
|
3522
3532
|
(否则是一块黑屏,用户分不清"视频坏了"和"壁纸就是这个颜色")。 */
|
|
3523
3533
|
try {
|
|
@@ -4514,6 +4524,11 @@ function mpwThumbFail(el, why) {
|
|
|
4514
4524
|
// ①(修正) frame.muted 逻辑曾写反(!(mute) → 默认反而出声);现在正向。
|
|
4515
4525
|
applyWebMute(frame);
|
|
4516
4526
|
frame.style.display = "";
|
|
4527
|
+
/* H(2026-09-25) **挂载期隐藏 ⇒ 帧一律先 park**:web/场景壁纸的作者脚本在 load 之后会
|
|
4528
|
+
自己起播(`?audio=1` 的场景音轨、web 壁纸的 BGM),而这条路上我们**没有** play()
|
|
4529
|
+
可拦(声音是帧内作者发起的)⇒ 唯一能做的就是挂载后立刻把这一页收起来。
|
|
4530
|
+
`applyWebMute` 里已按 hidden 下发 park(含渲染器图级通道 + shim 通道),这里补记台账。 */
|
|
4531
|
+
try { if (String(frame.getAttribute("src") || "") && mpwHiddenBootGate("showWebEl")) pauseWebFrame(); } catch (e) {}
|
|
4517
4532
|
// ⑳(新) web 壁纸倍速:iframe 内 <video> 的 playbackRate(同源可访问;
|
|
4518
4533
|
// 部分 web 壁纸用 canvas/WebGL 渲染无 <video>,静默跳过)
|
|
4519
4534
|
try { applyWebSpeed(frame); } catch {}
|
|
@@ -4631,6 +4646,29 @@ function mpwThumbFail(el, why) {
|
|
|
4631
4646
|
/* ①(2026-09-25 任务 ㉑ 追加) 帧内音频归属/暂停状态也要跟着走:`npFrameSoundBlocked()` 为真
|
|
4632
4647
|
(卡片暂停/我们真的在放音)时,把这一页**收起来**(停源),否则只是压增益。 */
|
|
4633
4648
|
try { if (npFrameSoundBlocked()) sendRendererAudioPolicy(frame, true, true, "np-owns"); } catch {}
|
|
4649
|
+
/* H(2026-09-25) **隐藏/冻结 ⇒ 帧一律 park**:这是"web 帧 / 场景帧(`?audio=1`)在后台
|
|
4650
|
+
挂载时仍起播"那条路的唯一落点(跨源帧读不到 contentDocument、`frame.muted` 对 iframe
|
|
4651
|
+
本来就不是标准属性、shim 只覆盖 shim 帧)。park 会走渲染器侧 pause + ctx.suspend。
|
|
4652
|
+
只在**真的挂了帧**(有 src)时下发,避免对空帧刷 shim 失败台账。 */
|
|
4653
|
+
try {
|
|
4654
|
+
if (frame && String(frame.getAttribute("src") || "") && mpwHiddenAudioBlock()) {
|
|
4655
|
+
if (sendRendererAudioPolicy(frame, true, true, "hidden-park")) {
|
|
4656
|
+
mpwHiddenLedgerPush("frame-park", { src: String(frame.getAttribute("src") || "").slice(0, 60) });
|
|
4657
|
+
}
|
|
4658
|
+
}
|
|
4659
|
+
} catch (e) {}
|
|
4660
|
+
/* H(2026-09-25 ③) **如实标注"这个帧我们够不着"**:不透明源(sandbox 无 allow-same-origin)
|
|
4661
|
+
且不是 shim 帧 ⇒ `contentDocument` 为 null、shim 通道不存在、`frame.muted` 又是空写 ⇒
|
|
4662
|
+
我们对它**一个字节都控制不了**。这里只记账(绝不假装"已经静音了"),并写进
|
|
4663
|
+
`window.__mpwUnreachableFrames` 供真机归因(owner=frame、无可用通道)。 */
|
|
4664
|
+
try {
|
|
4665
|
+
if (frame && String(frame.getAttribute("src") || "") && !frame.contentDocument && !mpwWebShimUrl(frame.getAttribute("src"))) {
|
|
4666
|
+
const rec = { at: Date.now(), src: String(frame.getAttribute("src") || "").replace(/token=[^&]*/, "token=…").slice(0, 90), why: "opaque-origin/no-shim → 我们的静音与暂停都到不了这个帧" };
|
|
4667
|
+
const a = window.__mpwUnreachableFrames || (window.__mpwUnreachableFrames = []);
|
|
4668
|
+
a.push(rec); if (a.length > 8) a.shift();
|
|
4669
|
+
if (!mpwHiddenLedgerSeen("frame-mute-unreachable")) mpwHiddenLedgerPush("frame-mute-unreachable", { src: rec.src });
|
|
4670
|
+
}
|
|
4671
|
+
} catch (e) {}
|
|
4634
4672
|
// ①(2026-09-16 I 项) shim 通道:帧内静音由 shim 执行(不透明源读不到帧内元素)
|
|
4635
4673
|
if (frame && mpwWebShimUrl(frame.getAttribute("src"))) {
|
|
4636
4674
|
const want = { muted: mute, speed: npPolicySpeed(), volume: npWantVolume() };
|
|
@@ -4795,13 +4833,157 @@ function mpwThumbFail(el, why) {
|
|
|
4795
4833
|
return s.powPauseHidden !== void 0 ? !!s.powPauseHidden : DEFAULT_POW_PAUSE_HIDDEN;
|
|
4796
4834
|
} catch (e) { return DEFAULT_POW_PAUSE_HIDDEN }
|
|
4797
4835
|
}
|
|
4798
|
-
|
|
4836
|
+
/* ══════════════════════════════════════════════════════════════════════════════════
|
|
4837
|
+
H(2026-09-25 真机修复)「**后台挂载期起播**」—— C 那一轮的三个缺口
|
|
4838
|
+
──────────────────────────────────────────────────────────────────────────────────
|
|
4839
|
+
现场(用户新报,与 C 那次**不同**):
|
|
4840
|
+
「后台只有 Termux + Via 在跑;在哔哩哔哩 App 里看视频时**突然冒出声音**,与 B 站音频
|
|
4841
|
+
叠加(B 站没被暂停);回到 Via 发现 DSH 页面**需要重新加载**(= 页面此前被 Android
|
|
4842
|
+
冻结/丢弃/静默重载过)」。
|
|
4843
|
+
三个现场特征指向**同一条链**:Android 把后台标签冻结→丢弃→**在后台重新加载**,
|
|
4844
|
+
于是我们的插件在 `document.hidden === true` 的状态下**走了一遍完整挂载** ——
|
|
4845
|
+
· ① `visibilitychange` **可能根本不派发**(页面一挂载就已经是 hidden)⇒ 只挂在事件上的
|
|
4846
|
+
`powPauseHidden` 那一路(`setupPowerSave` 里的 `updatePowerPause()`)没跑,
|
|
4847
|
+
`powPaused` 仍是 **false**;
|
|
4848
|
+
· ② 各路挂载/重试路径的 `play()` 里,只有 `npPrimePlay` / `resumeWallpaperVideo` 查了
|
|
4849
|
+
`mpwHiddenAudioBlock()`;**其余 14 处只查 `!(wallUserPaused || powPaused)`,一处都
|
|
4850
|
+
没查 `document.hidden`**(C 那轮只收口了"补起播"三条,见下表);
|
|
4851
|
+
· ③ `powPaused` 是**快照**:C 的判据是"上一轮省电跑过",冻结/丢弃后台重载这条路上它
|
|
4852
|
+
从来没被置真 ⇒ C 的闸门在**这一条路径上等于没有**。
|
|
4853
|
+
修法(与 C 同一个判据 `mpwHiddenAudioBlock()`,只是把落点补全 + 补启动期状态机):
|
|
4854
|
+
① 启动期:挂载完成时 `document.hidden === true` ⇒ **一律不起播**(视频/BGM/web 帧/
|
|
4855
|
+
场景帧/`npPrimePlay` 全部),只记**一条**台账;等到第一次
|
|
4856
|
+
`visibilitychange → visible`(或用户手势)再**按当时策略**恢复。
|
|
4857
|
+
两种状态**必须可区分**(现场排障就靠这个):
|
|
4858
|
+
· `never-started`(`mpwHiddenBootBlocked`):从没起播过 ⇒ 回到可见时**可以**起播;
|
|
4859
|
+
· `paused-by-hidden`(`mpwHiddenPausedByUs`):隐藏前在播、被我们停的 ⇒ 回来按
|
|
4860
|
+
原状态续播(`powHidWallWasPlaying` 那条既有语义,不动)。
|
|
4861
|
+
② 隐藏/冻结/丢弃:`visibilitychange`(hidden) / `pagehide` / **`freeze`**(新增;不存在就
|
|
4862
|
+
静默跳过)时把**所有我们的出声面**停住:`video.pause()`、web 帧下发 `park`、
|
|
4863
|
+
场景帧下发 `park`(`sendRendererAudioPolicy(frame,true,true,…)`)。
|
|
4864
|
+
`AudioContext.suspend()`:**我们自己的面里没有 AudioContext**(唯一的 WebAudio 是
|
|
4865
|
+
帧内作者的,跨源只能靠 `park` 让渲染器自己 suspend;总线的既有决定是"压增益而不是
|
|
4866
|
+
suspend",理由见 `MPW-AUDIO-BUS` 的注释)⇒ 不碰别人的 ctx,只**如实记账**。
|
|
4867
|
+
③ 只对**我们自己的面**负责:第三方(鲸鱼挂件等)一个字节都不碰,但审计里如实标 owner,
|
|
4868
|
+
且"hidden 期间起播"这条**无论 owner 是谁都记一条**(含 owner/src/栈,见 `mpwAuditPush`)。
|
|
4869
|
+
语义与开关(两条逃生门,都登记在文档里):
|
|
4870
|
+
· 复用既有设置项 `powPauseHidden`:显式关掉 ⇒ 本闸门一并失效(= "切页也出声是我选的");
|
|
4871
|
+
· 新增 `?hiddengate=legacy`(URL,刷新即生效):**整族**回到没有 hidden 闸门的旧行为,
|
|
4872
|
+
A/B 对照与线上救急用(见 README「插件侧 URL 逃生口」表 + docs/WALLPAPER-LIFECYCLE.md)。
|
|
4873
|
+
判据(离线、无浏览器):`node tools/hidden-gate-test.mjs`(含变异自证:删掉任一新增门控 ⇒ 必红)。
|
|
4874
|
+
══════════════════════════════════════════════════════════════════════════════════ */
|
|
4875
|
+
function mpwHiddenGateLegacy() {
|
|
4876
|
+
try { return new URLSearchParams(location.search).get("hiddengate") === "legacy" } catch (e) { return false }
|
|
4877
|
+
}
|
|
4878
|
+
let mpwHiddenFrozen = false; // 见过 freeze(Android 冻结)⇒ 视为隐藏,直到 resume/可见
|
|
4879
|
+
let mpwHiddenBootBlocked = false; // 挂载期因隐藏被挡住(**从没起播过**)
|
|
4880
|
+
let mpwHiddenPausedByUs = false; // 隐藏时我们按下的暂停(**被隐藏暂停过**)
|
|
4881
|
+
let mpwHiddenBootLogged = false; // 启动期那条台账只记一次("只记一条")
|
|
4882
|
+
let mpwHiddenLedger = []; // 有界台账(≤64,真机可读:window.__mpwHiddenLedger)
|
|
4883
|
+
/** 台账:**只记账、零行为影响**。真机上"这次到底谁起的 / 为什么没起"就靠它。
|
|
4884
|
+
* **合并规则**(必须):有些调用点是 800ms/2s 周期驱动的(`applyWebMute` / `npApplyFrameMute`)
|
|
4885
|
+
* ⇒ 同一 `(kind, where, src)` 在 2s 内只留第一条,否则 64 条的环会被"同一件事"刷满,
|
|
4886
|
+
* 真正要看的那几条(`boot-hidden-no-autoplay` / `pause` / `hidden-play`)会被挤出去。 */
|
|
4887
|
+
function mpwHiddenLedgerPush(kind, extra) {
|
|
4888
|
+
try {
|
|
4889
|
+
const x = extra || {};
|
|
4890
|
+
const last = mpwHiddenLedger[mpwHiddenLedger.length - 1];
|
|
4891
|
+
if (last && last.kind === String(kind || "") && last.where === x.where && last.src === x.src
|
|
4892
|
+
&& (Date.now() - Number(last.t || 0)) < 2000) return last;
|
|
4893
|
+
const rec = Object.assign({ t: Date.now(), kind: String(kind || ""),
|
|
4894
|
+
hidden: (() => { try { return document.hidden === true } catch (e) { return null } })(),
|
|
4895
|
+
vis: (() => { try { return String(document.visibilityState || "") } catch (e) { return "" } })(),
|
|
4896
|
+
pow: !!powPaused, block: null, bootBlocked: !!mpwHiddenBootBlocked, pausedByUs: !!mpwHiddenPausedByUs,
|
|
4897
|
+
frozen: !!mpwHiddenFrozen, legacy: mpwHiddenGateLegacy(), gateOn: mpwHiddenPauseOn(),
|
|
4898
|
+
}, extra || {});
|
|
4899
|
+
try { rec.block = mpwHiddenAudioBlock() } catch (e) {}
|
|
4900
|
+
mpwHiddenLedger.push(rec);
|
|
4901
|
+
if (mpwHiddenLedger.length > 64) mpwHiddenLedger.splice(0, mpwHiddenLedger.length - 64);
|
|
4902
|
+
try { window.__mpwHiddenLedger = mpwHiddenLedger } catch (e) {}
|
|
4903
|
+
return rec;
|
|
4904
|
+
} catch (e) { return null }
|
|
4905
|
+
}
|
|
4906
|
+
/** 台账去重读法:同类事件只记第一条(周期函数每 800ms 调一次,不刷屏)。 */
|
|
4907
|
+
function mpwHiddenLedgerSeen(kind) {
|
|
4908
|
+
try { for (let i = 0; i < mpwHiddenLedger.length; i++) if (mpwHiddenLedger[i] && mpwHiddenLedger[i].kind === kind) return true; } catch (e) {}
|
|
4909
|
+
return false;
|
|
4910
|
+
}
|
|
4911
|
+
/** `document.hidden` 的**实时**读法(不依赖任何事件):冻结/丢弃/后台重载三条路都要它。 */
|
|
4912
|
+
function mpwHiddenLive() {
|
|
4799
4913
|
try {
|
|
4800
|
-
if (
|
|
4914
|
+
if (mpwHiddenFrozen) return true;
|
|
4801
4915
|
if (powHiddenNow) return true;
|
|
4802
4916
|
return !!(typeof document !== "undefined" && document && document.hidden === true);
|
|
4803
4917
|
} catch (e) { return false }
|
|
4804
4918
|
}
|
|
4919
|
+
function mpwHiddenAudioBlock() {
|
|
4920
|
+
try {
|
|
4921
|
+
/* 逃生门:`?hiddengate=legacy` ⇒ 整族闸门失效(含 C 那一轮的三条),回到旧行为。 */
|
|
4922
|
+
if (mpwHiddenGateLegacy()) return false;
|
|
4923
|
+
if (!mpwHiddenPauseOn()) return false;
|
|
4924
|
+
return mpwHiddenLive();
|
|
4925
|
+
} catch (e) { return false }
|
|
4926
|
+
}
|
|
4927
|
+
/** H-③ 所有"**我们自己的**起播入口"共用的一道闸:用户暂停 / 省电暂停 / hidden。
|
|
4928
|
+
* 返回值 = true 表示"**拦住了**"(调用方必须不 play)。`where` 进台账,用于归因。 */
|
|
4929
|
+
function mpwPlayBlockedBy(where) {
|
|
4930
|
+
try {
|
|
4931
|
+
if (wallUserPaused || powPaused) return true;
|
|
4932
|
+
if (mpwHiddenAudioBlock()) { mpwHiddenLedgerPush("play-blocked", { why: "hidden", where: String(where || "") }); return true }
|
|
4933
|
+
return false;
|
|
4934
|
+
} catch (e) { return false }
|
|
4935
|
+
}
|
|
4936
|
+
/** C 那轮 7 处重试路径的既有写法 `!(wallUserPaused || powPaused)` 的**等价加强版**
|
|
4937
|
+
* (多出 hidden 一条;语义不变,见上面 H-②的根因)。 */
|
|
4938
|
+
function mpwMediaPlayBlocked() { return mpwPlayBlockedBy("media-retry") }
|
|
4939
|
+
/** H-① 挂载期闸门:`document.hidden === true` 时**一律不起播**,只记一条台账。
|
|
4940
|
+
* @returns {boolean} true = 拦住了(调用方不许 play / 不许 prime)。 */
|
|
4941
|
+
function mpwHiddenBootGate(where) {
|
|
4942
|
+
try {
|
|
4943
|
+
if (!mpwHiddenAudioBlock()) return false;
|
|
4944
|
+
/* "从没起播过"与"被隐藏暂停过"必须区分:挂载期这条只置 bootBlocked,绝不置 pausedByUs。 */
|
|
4945
|
+
mpwHiddenBootBlocked = true;
|
|
4946
|
+
if (!mpwHiddenBootLogged) {
|
|
4947
|
+
mpwHiddenBootLogged = true;
|
|
4948
|
+
mpwHiddenLedgerPush("boot-hidden-no-autoplay", { where: String(where || ""), state: "never-started" });
|
|
4949
|
+
try { console.warn("[dsh-mpkg-wallpaper] 挂载时页面处于隐藏状态 ⇒ **一律不起播**(视频/BGM/帧全部),等第一次可见时按当时策略恢复;台账 window.__mpwHiddenLedger(" + where + ")"); } catch (e) {}
|
|
4950
|
+
} else {
|
|
4951
|
+
mpwHiddenLedgerPush("mount-blocked", { where: String(where || "") });
|
|
4952
|
+
}
|
|
4953
|
+
return true;
|
|
4954
|
+
} catch (e) { return false }
|
|
4955
|
+
}
|
|
4956
|
+
/** H-① 恢复:**从没起播过**那一条路(`paused-by-hidden` 走既有的 resumeWallpaperVideo)。
|
|
4957
|
+
* 只在"策略允许"时动手:用户暂停 / 卡片暂停 / 浏览器拒绝过 ⇒ 一个字节都不碰。 */
|
|
4958
|
+
function mpwHiddenBootResume(where) {
|
|
4959
|
+
try {
|
|
4960
|
+
if (!mpwHiddenBootBlocked) return false;
|
|
4961
|
+
if (mpwHiddenAudioBlock()) return false; // 还隐藏着 ⇒ 不恢复
|
|
4962
|
+
mpwHiddenBootBlocked = false;
|
|
4963
|
+
const s = readSection();
|
|
4964
|
+
let did = [];
|
|
4965
|
+
/* 壁纸视频:闸门与 npPrimePlay 一模一样(它内部还会再判一次,这里只是留痕) */
|
|
4966
|
+
try {
|
|
4967
|
+
const vid = npActiveVideo(s) || bgElements().video;
|
|
4968
|
+
if (vid && vid.paused && vid.src && !npPlayBlocked && !wallUserPaused && !npCardPaused && !npUserPausedOf(vid)) {
|
|
4969
|
+
npPrimePlay(vid); did.push("video");
|
|
4970
|
+
}
|
|
4971
|
+
} catch (e) {}
|
|
4972
|
+
/* 我们自己的 <audio>(曲目档):只有"清单在 + 用户没按暂停"才续播 */
|
|
4973
|
+
try {
|
|
4974
|
+
const list = npTrackList(s);
|
|
4975
|
+
if (npAudio && npAudio.paused && list && list.tracks && list.tracks.length && !npCardPaused) {
|
|
4976
|
+
const a = String(npAudio.getAttribute("src") || "");
|
|
4977
|
+
if (!a) npLoadTrack(true); else { const p = npAudio.play(); if (p && p.catch) p.catch(() => {}); }
|
|
4978
|
+
did.push("audio");
|
|
4979
|
+
}
|
|
4980
|
+
} catch (e) {}
|
|
4981
|
+
/* 帧:只撤销**我们**按下的暂停(webFramePausedByUs 记账,作者自己停着的不碰) */
|
|
4982
|
+
try { const f = bgElements().frame; if (f && f.style.display !== "none") { resumeWebFrame(); did.push("frame") } } catch (e) {}
|
|
4983
|
+
mpwHiddenLedgerPush("boot-visible-resume", { where: String(where || ""), did: did.join("+") || "none", state: "never-started->running" });
|
|
4984
|
+
return true;
|
|
4985
|
+
} catch (e) { return false }
|
|
4986
|
+
}
|
|
4805
4987
|
/* ══════════════════════════════════════════════════════════════════════════════════
|
|
4806
4988
|
C3(2026-09-20 真机修复)「**换档即断开旧音源**」:一条真凶级漏洞
|
|
4807
4989
|
──────────────────────────────────────────────────────────────────────────────────
|
|
@@ -5028,6 +5210,16 @@ function mpwThumbFail(el, why) {
|
|
|
5028
5210
|
* (否则重试那一下会被"状态校准"当场撤销,等于没重试)。 */
|
|
5029
5211
|
function npPlayRetryMuted(video, err) {
|
|
5030
5212
|
npOpTrace("prime-reject", null, String(err && err.name || "Error"));
|
|
5213
|
+
/* H(2026-09-25) hidden 期间**连 muted 重试也不做**(先判、再记"被拒"状态:否则会把一次
|
|
5214
|
+
根本没发生的重试记成 `retryMuted:true`)。为什么这条也要挡:
|
|
5215
|
+
① 它对"隐藏期 no-play"这条契约是噪声;② 真机审计 09-23 那条
|
|
5216
|
+
`hidden:true kind=play muted=true paused=true` 正是这类"被节流的 promise 回调在被隐藏
|
|
5217
|
+
之后才落地"的形态 —— 元素当时还没起播(paused=true 就是证据)。 */
|
|
5218
|
+
if (mpwPlayBlockedBy("prime-retry-muted")) {
|
|
5219
|
+
try { npMarkPlayBlocked({ name: String(err && err.name || "Error"), message: String(err && err.message || "").slice(0, 200), at: Date.now(), retryMuted: false, hiddenSkipped: true }); } catch (e) {}
|
|
5220
|
+
npOpTrace("prime-retry", null, "skipped:hidden");
|
|
5221
|
+
return false;
|
|
5222
|
+
}
|
|
5031
5223
|
npMarkPlayBlocked({
|
|
5032
5224
|
name: String(err && err.name || "Error"),
|
|
5033
5225
|
message: String(err && err.message || "").slice(0, 200), at: Date.now(), retryMuted: true,
|
|
@@ -5071,6 +5263,9 @@ function mpwThumbFail(el, why) {
|
|
|
5071
5263
|
npGestureArmed = false;
|
|
5072
5264
|
if (!npPlayBlocked && !npSoundBlocked) return;
|
|
5073
5265
|
if (mpwHiddenAudioBlock()) { npOpTrace("gesture-retry", null, "gated:hidden"); return; } // C: 隐藏时不补起播
|
|
5266
|
+
/* H(2026-09-25 ①) 用户手势 = 挂载期被挡那条路的**第二个恢复口**(第一个是第一次可见;
|
|
5267
|
+
见 mpwHiddenBootGate/mpwHiddenBootResume)。 */
|
|
5268
|
+
try { mpwHiddenBootResume("gesture"); } catch (e) {}
|
|
5074
5269
|
/* ①(NP-4) 用户真的交互了 ⇒ 声音限制解除:清掉"被挡住"状态,取消静音并续播。 */
|
|
5075
5270
|
npSoundBlocked = false; npSoundReason = null;
|
|
5076
5271
|
npGestureUnlock("gesture"); // C4:这一次交互同时解除"必须 muted"(此后按设置)
|
|
@@ -6042,6 +6237,9 @@ function mpwThumbFail(el, why) {
|
|
|
6042
6237
|
disarmSceneWatchdog(true); // ①(批次15 B1) 切到 canvas 合成壁纸:解除场景看门狗并清兜底视觉
|
|
6043
6238
|
try { if (window.__mpwLnReset) window.__mpwLnReset(); } catch {} // ①(批次15 B3) 方向键守卫复位
|
|
6044
6239
|
try { if (video) video.pause(); } catch {}
|
|
6240
|
+
/* H(2026-09-25) 挂载期隐藏 ⇒ 记一条"从没起播过"的台账(video 已 pause;场景合成的
|
|
6241
|
+
音轨由渲染器帧那条 park 通道负责,见 applyWebMute / showWebEl)。 */
|
|
6242
|
+
try { mpwHiddenBootGate("showSceneEl"); } catch (e) {}
|
|
6045
6243
|
wrap.classList.remove("mpw-img", "mpw-video", "mpw-web");
|
|
6046
6244
|
wrap.classList.add("mpw-scene");
|
|
6047
6245
|
if (img) img.style.display = "none";
|
|
@@ -6925,7 +7123,7 @@ function mpwThumbFail(el, why) {
|
|
|
6925
7123
|
vid2.removeAttribute("src");
|
|
6926
7124
|
vid2.src = hostUrl;
|
|
6927
7125
|
vid2.load();
|
|
6928
|
-
try { if (!(
|
|
7126
|
+
try { if (!mpwMediaPlayBlocked()) { const pp = vid2.play(); if (pp && pp.catch) pp.catch(() => {}); } } catch {}
|
|
6929
7127
|
} catch {}
|
|
6930
7128
|
}).catch(() => {});
|
|
6931
7129
|
}
|
|
@@ -6981,7 +7179,7 @@ function mpwThumbFail(el, why) {
|
|
|
6981
7179
|
vid.removeAttribute("src");
|
|
6982
7180
|
vid.src = hostUrl;
|
|
6983
7181
|
vid.load();
|
|
6984
|
-
try { if (!(
|
|
7182
|
+
try { if (!mpwMediaPlayBlocked()) { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } } catch {}
|
|
6985
7183
|
} catch {}
|
|
6986
7184
|
return;
|
|
6987
7185
|
}
|
|
@@ -6994,7 +7192,7 @@ function mpwThumbFail(el, why) {
|
|
|
6994
7192
|
vid.removeAttribute("src");
|
|
6995
7193
|
vid.src = resolveHostUrl(frameImg);
|
|
6996
7194
|
vid.load();
|
|
6997
|
-
try { if (!(
|
|
7195
|
+
try { if (!mpwMediaPlayBlocked()) { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } } catch {}
|
|
6998
7196
|
} catch {}
|
|
6999
7197
|
return;
|
|
7000
7198
|
}
|
|
@@ -7022,7 +7220,7 @@ function mpwThumbFail(el, why) {
|
|
|
7022
7220
|
vid.load();
|
|
7023
7221
|
// ①(修正) 暂停门控:自动转码重试也尊重暂停,否则用户暂停后
|
|
7024
7222
|
// 编码报错自动切转码会重播(绕过 showVideoEl 的门控)。
|
|
7025
|
-
try { if (!(
|
|
7223
|
+
try { if (!mpwMediaPlayBlocked()) { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } } catch {}
|
|
7026
7224
|
} catch {}
|
|
7027
7225
|
};
|
|
7028
7226
|
// 旧行为(回退开关)
|
|
@@ -7042,7 +7240,7 @@ function mpwThumbFail(el, why) {
|
|
|
7042
7240
|
vid.removeAttribute("src");
|
|
7043
7241
|
vid.src = cur;
|
|
7044
7242
|
vid.load();
|
|
7045
|
-
try { if (!(
|
|
7243
|
+
try { if (!mpwMediaPlayBlocked()) { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } } catch {}
|
|
7046
7244
|
} catch {}
|
|
7047
7245
|
};
|
|
7048
7246
|
try {
|
|
@@ -7075,7 +7273,7 @@ function mpwThumbFail(el, why) {
|
|
|
7075
7273
|
vid.src = cur;
|
|
7076
7274
|
vid.load();
|
|
7077
7275
|
// ①(修正) 暂停门控:404 重试不重播(同上,尊重暂停状态)
|
|
7078
|
-
try { if (!(
|
|
7276
|
+
try { if (!mpwMediaPlayBlocked()) { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } } catch {}
|
|
7079
7277
|
}, 800 * vid.__mpwHostTries);
|
|
7080
7278
|
});
|
|
7081
7279
|
}
|
|
@@ -7103,7 +7301,7 @@ function mpwThumbFail(el, why) {
|
|
|
7103
7301
|
vid.removeAttribute("src");
|
|
7104
7302
|
vid.src = hostUrl;
|
|
7105
7303
|
vid.load();
|
|
7106
|
-
try { if (!(
|
|
7304
|
+
try { if (!mpwMediaPlayBlocked()) { const pp = vid.play(); if (pp && pp.catch) pp.catch(() => {}); } } catch {}
|
|
7107
7305
|
// ②(fix) 转码完成自动切换:回退后轮询 host 转码进度,
|
|
7108
7306
|
// done → 重新 apply(此时 /transcode 命中缓存,秒开转码产物)。
|
|
7109
7307
|
// 只轮询到转码完成或超时(上限 3 分钟,避免无限轮询)。
|
|
@@ -8890,7 +9088,7 @@ function mpwThumbFail(el, why) {
|
|
|
8890
9088
|
on("ended", () => { try {
|
|
8891
9089
|
const list = npTrackList(readSection());
|
|
8892
9090
|
if (list && list.tracks.length > 1) npStepTrack(1, true);
|
|
8893
|
-
else if (npAudio) { npAudio.currentTime = 0; const p = npAudio.play(); if (p && p.catch) p.catch(() => {}); }
|
|
9091
|
+
else if (npAudio && !mpwPlayBlockedBy("audio-loop")) { npAudio.currentTime = 0; const p = npAudio.play(); if (p && p.catch) p.catch(() => {}); }
|
|
8894
9092
|
} catch (e) {} });
|
|
8895
9093
|
on("error", () => { try { npAudioError() } catch (e) {} });
|
|
8896
9094
|
(document.body || document.documentElement).appendChild(npAudio);
|
|
@@ -8961,6 +9159,11 @@ function mpwThumbFail(el, why) {
|
|
|
8961
9159
|
if (!npWantPlay) return;
|
|
8962
9160
|
if (!(video.currentTime < 1.5)) return; /* 放起来之后才停(播完/切源)⇒ 不关我们的事 */
|
|
8963
9161
|
if (npSoundBlocked) return;
|
|
9162
|
+
/* H(2026-09-25) hidden 期间连这一发 muted 补播也不做:它虽然不出声,但会让
|
|
9163
|
+
"隐藏期间有 play()" 这条审计噪声盖住真凶(且解码白白耗电)。
|
|
9164
|
+
**必须在置 npSoundBlocked 之前判**:否则"被策略停住"这条状态会在隐藏期被
|
|
9165
|
+
单方面点亮,回到可见时没人再装手势重试(`npArmGestureRetry` 在下面那一行)。 */
|
|
9166
|
+
if (mpwPlayBlockedBy("pause-replay")) { npOpTrace("audio-blocked", null, "skipped:hidden"); return; }
|
|
8964
9167
|
npSoundBlocked = true;
|
|
8965
9168
|
npSoundReason = { at: Date.now(), currentTime: +video.currentTime.toFixed(2), muted: video.muted, hint: "没有用户手势时浏览器不允许有声播放(Firefox 会直接把它停住)" };
|
|
8966
9169
|
try { window.__mpwNpSoundBlocked = npSoundReason; } catch (e) {}
|
|
@@ -9045,7 +9248,8 @@ function mpwThumbFail(el, why) {
|
|
|
9045
9248
|
≤32MiB 的 Blob 钉在 registry 里,自动连播 N 首 = 32MiB × N 单调增长。 */
|
|
9046
9249
|
npAudio.src = obj;
|
|
9047
9250
|
npBlobUrlSet(obj);
|
|
9048
|
-
|
|
9251
|
+
/* H(2026-09-25) hidden 时只装源、不起播(闸门见 mpwHiddenBootGate)。 */
|
|
9252
|
+
if (!mpwPlayBlockedBy("audio-blob-retry")) { const p = npAudio.play(); if (p && p.catch) p.catch(() => {}); }
|
|
9049
9253
|
/* ①(2026-09-23 上游 Firefox Bug 2056444) 同一族的静默挂起(blob 音轨 rs=0 / 无 error、
|
|
9050
9254
|
play() 的 Promise 永不 settle;`decodeAudioData` 亦然)⇒ 同一个有界重试兜底。 */
|
|
9051
9255
|
mpwBlobMediaRetry(npAudio, "音轨");
|
|
@@ -9078,7 +9282,12 @@ function mpwThumbFail(el, why) {
|
|
|
9078
9282
|
const st = readSection();
|
|
9079
9283
|
a.muted = !!(st.mute !== void 0 ? !!st.mute : true);
|
|
9080
9284
|
if (npVolumeSet()) a.volume = npWantVolume(); /* ①(NP-4)① 用户设过电平才写(默认不碰) */
|
|
9081
|
-
if (play) {
|
|
9285
|
+
if (play) {
|
|
9286
|
+
/* H(2026-09-25) 起播前过闸:hidden 期间**只装源不播**。
|
|
9287
|
+
刻意**不**置"挂载期被挡"状态(那是 `showVideoEl/showWebEl/showSceneEl` 三个挂载点
|
|
9288
|
+
的职责)⇒ 换曲/next 这类被挡的起播不会在回到可见时自作主张补播。 */
|
|
9289
|
+
if (!mpwPlayBlockedBy("npLoadTrack")) { const p = a.play(); if (p && p.catch) p.catch(() => {}); }
|
|
9290
|
+
}
|
|
9082
9291
|
} catch (e) { mpwErr("npLoadTrack", e) }
|
|
9083
9292
|
npApplyFrameMute();
|
|
9084
9293
|
npSyncAudio();
|
|
@@ -9174,7 +9383,13 @@ function mpwThumbFail(el, why) {
|
|
|
9174
9383
|
const a = npEnsureAudio();
|
|
9175
9384
|
/* 还没装源(清单刚到/换过壁纸)⇒ 先装当前曲目再放 */
|
|
9176
9385
|
if (a && !String(a.getAttribute("src") || "")) { npLoadTrack(true); did = "audio.load+play"; }
|
|
9177
|
-
else if (a) {
|
|
9386
|
+
else if (a) {
|
|
9387
|
+
/* H(2026-09-25) 卡片/系统媒体键的播放也要过 hidden 闸:真机上"页面隐藏时
|
|
9388
|
+
被程序化调用 transport('play')"是后台重放那条链的一环(用户意图只在
|
|
9389
|
+
可见时可能发生 —— 隐藏页面上按不到卡片)。同样**不**置"挂载期被挡"。 */
|
|
9390
|
+
if (mpwPlayBlockedBy("transport:play")) { did = "audio.play(blocked-hidden)"; npOpTrace("transport-hidden", op, did); }
|
|
9391
|
+
else { const p = a.play(); if (p && p.catch) p.catch(() => {}); did = "audio.play"; }
|
|
9392
|
+
}
|
|
9178
9393
|
} else if (npAudio) { npCardPaused = true; npPersistPaused(true); npAudio.pause(); did = "audio.pause"; }
|
|
9179
9394
|
} else if (!vid) {
|
|
9180
9395
|
/* 清单还没到:拉一次,到位后由 applyNowPlaying 把键的状态刷新(不假装按过了) */
|
|
@@ -9483,7 +9698,7 @@ function mpwThumbFail(el, why) {
|
|
|
9483
9698
|
else { npSetUserPaused(vid2, true); npWantPlay = false; try { vid2.pause() } catch (e) {} }
|
|
9484
9699
|
npOpTrace("link-align", act, "video");
|
|
9485
9700
|
} else if (act === "pause") { try { if (npAudio) npAudio.pause() } catch (e) {} npOpTrace("link-align", act, "audio") }
|
|
9486
|
-
else { try { if (npAudio && !npCardPaused) { const p = npAudio.play(); if (p && p.catch) p.catch(() => {}) } } catch (e) {} npOpTrace("link-align", act, "audio") }
|
|
9701
|
+
else { try { if (npAudio && !npCardPaused && !mpwPlayBlockedBy("link-align")) { const p = npAudio.play(); if (p && p.catch) p.catch(() => {}) } } catch (e) {} npOpTrace("link-align", act, "audio") }
|
|
9487
9702
|
try { npApplyFrameMute() } catch (e) {}
|
|
9488
9703
|
}
|
|
9489
9704
|
npLinkPrev = linkNow2;
|
|
@@ -9570,6 +9785,10 @@ function mpwThumbFail(el, why) {
|
|
|
9570
9785
|
const { frame } = bgElements();
|
|
9571
9786
|
if (!frame) return;
|
|
9572
9787
|
if (!webFramePausedByUs) return; // ⑤ 我们没停过 ⇒ 一个字节都不碰
|
|
9788
|
+
/* H(2026-09-25) **hidden 期间一个字节都不 play()**:本函数会被 `npApplyFrameMute`
|
|
9789
|
+
在"设置变化/延迟重放"里叫到,那条路上页面可能已经是隐藏的(C 那轮漏的就是这类)。
|
|
9790
|
+
早退**不**清 `webFramePausedByUs` ⇒ 回到可见时 `resumeWallpaperVideo` 会再叫一次。 */
|
|
9791
|
+
if (mpwHiddenAudioBlock()) { mpwHiddenLedgerPush("frame-resume-blocked", { why: "hidden" }); return; }
|
|
9573
9792
|
webFramePausedByUs = false;
|
|
9574
9793
|
/* ①(2026-09-25 任务 ㉑ 追加) 撤销我们自己按下的"收起来"(跨源帧那条)。 */
|
|
9575
9794
|
try { sendRendererAudioPolicy(frame, !!readSection().mute || npFrameSoundBlocked(), false, "resume-frame") } catch (e) {}
|
|
@@ -9584,6 +9803,18 @@ function mpwThumbFail(el, why) {
|
|
|
9584
9803
|
function pauseWallpaperVideo() {
|
|
9585
9804
|
try {
|
|
9586
9805
|
const { video, frame } = bgElements();
|
|
9806
|
+
/* H(2026-09-25) 状态区分:`paused-by-hidden` = "隐藏前真的在播 ⇒ 我们把它停住的"
|
|
9807
|
+
(回到可见时按原状态续播);与"从没起播过"(`mpwHiddenBootBlocked`)是两回事。
|
|
9808
|
+
**只有"真的停住了一个正在播的东西"才算**:
|
|
9809
|
+
① 用户手动暂停壁纸那条路(`pauseWallpaperVideo` 也被 `toggleWallPause` 调)不算;
|
|
9810
|
+
② **后台重载挂载**那条路也不算 —— 那时元素**从没起播过**(`never-started`),
|
|
9811
|
+
该由 `mpwHiddenBootBlocked` 表达。否则两种状态同时为真,
|
|
9812
|
+
"谁被隐藏暂停过 / 谁从没起播过"就答不准了(真浏览器档 B5 抓到的就是这个)。
|
|
9813
|
+
判据用下面两行马上要算的"暂停前在不在播"(video / 我们自己的 audio)。 */
|
|
9814
|
+
try {
|
|
9815
|
+
const wasPlaying = !!(video && !video.paused && video.src) || !!(npAudio && !npAudio.paused);
|
|
9816
|
+
if (mpwHiddenAudioBlock() && wasPlaying) mpwHiddenPausedByUs = true;
|
|
9817
|
+
} catch (e) {}
|
|
9587
9818
|
/* C(2026-09-20) 记下"暂停之前壁纸在不在播":恢复时**按原状态**续播
|
|
9588
9819
|
(原本就暂停的绝不自作主张起播 —— 用户要的是"切页静音",不是"回来自己开始放")。 */
|
|
9589
9820
|
try { powHidWallWasPlaying = !!(video && !video.paused && video.src) } catch (e) {}
|
|
@@ -9617,17 +9848,21 @@ function mpwThumbFail(el, why) {
|
|
|
9617
9848
|
try { const p = video.play(); if (p && p.catch) p.catch(() => {}); } catch {}
|
|
9618
9849
|
}
|
|
9619
9850
|
// ⑤ 只撤销**我们**按下的暂停(webFramePausedByUs 记账;作者自己停着的媒体不碰)
|
|
9620
|
-
|
|
9851
|
+
/* H(2026-09-25) 隐藏期间**一次都不许 play()**:这条恢复路径也会被"设置变化/延迟重放"
|
|
9852
|
+
在后台叫到(C 那轮只挡住了 video 那一行,帧与我们自己的 audio 两行漏着)。 */
|
|
9853
|
+
if (frame && frame.style.display !== "none" && !mpwHiddenAudioBlock()) resumeWebFrame();
|
|
9621
9854
|
/* ⑤ 静音回落到设置/卡片口径(不能把"卡片暂停"那条压住的静音也一起放开) */
|
|
9622
9855
|
try { npApplyFrameMute(); } catch (e) {}
|
|
9623
9856
|
/* ⑤ 隐藏前在放才续播;隐藏前就是暂停的绝不自作主张播放 */
|
|
9624
9857
|
try {
|
|
9625
|
-
if (powHidNpWasPlaying && npAudio && npAudio.paused && !npCardPaused) {
|
|
9858
|
+
if (powHidNpWasPlaying && npAudio && npAudio.paused && !npCardPaused && !mpwHiddenAudioBlock()) {
|
|
9626
9859
|
const p = npAudio.play(); if (p && p.catch) p.catch(() => {});
|
|
9627
9860
|
}
|
|
9628
9861
|
powHidNpWasPlaying = false;
|
|
9629
9862
|
} catch (e) {}
|
|
9630
9863
|
powHidWallWasPlaying = false;
|
|
9864
|
+
mpwHiddenPausedByUs = false;
|
|
9865
|
+
mpwHiddenLedgerPush("visible-resume", { force: force === true });
|
|
9631
9866
|
} catch {}
|
|
9632
9867
|
}
|
|
9633
9868
|
/** 按三档状态决定暂停/恢复(任一档触发即暂停,全恢复才继续)。 */
|
|
@@ -9639,9 +9874,14 @@ function mpwThumbFail(el, why) {
|
|
|
9639
9874
|
const battOn = s.powPauseBattery !== void 0 ? !!s.powPauseBattery : false;
|
|
9640
9875
|
let hiddenNow = powHiddenNow;
|
|
9641
9876
|
try { if (!hiddenNow && typeof document !== "undefined" && document && document.hidden === true) hiddenNow = true } catch (e) {}
|
|
9877
|
+
if (mpwHiddenFrozen) hiddenNow = true; // H: freeze(Android 冻结)与 hidden 同一待遇
|
|
9642
9878
|
const shouldPause = (hiddenOn && hiddenNow) || (blurOn && !powFocusedNow) || (battOn && powBattery && powBattery.charging === false);
|
|
9643
|
-
if (shouldPause && !powPaused) { powPaused = true; pauseWallpaperVideo(); }
|
|
9879
|
+
if (shouldPause && !powPaused) { powPaused = true; mpwHiddenLedgerPush("pause", { by: "power" }); pauseWallpaperVideo(); }
|
|
9644
9880
|
else if (!shouldPause && powPaused) { powPaused = false; resumeWallpaperVideo(); }
|
|
9881
|
+
/* H(2026-09-25 ①) **挂载期被挡**那条独立状态:挂载完成时就是隐藏的 ⇒ 从没起播过;
|
|
9882
|
+
回到可见(且策略允许)时由它负责把那一次"补上"。与上面"按原状态续播"互不干扰:
|
|
9883
|
+
`resumeWallpaperVideo` 只在 `powHidWallWasPlaying` 为真时动手,而这条置的是 false。 */
|
|
9884
|
+
if (!shouldPause) { try { mpwHiddenBootResume("updatePowerPause"); } catch (e) {} }
|
|
9645
9885
|
} catch {}
|
|
9646
9886
|
}
|
|
9647
9887
|
/** 注册省电监听(一次性)。
|
|
@@ -9658,6 +9898,8 @@ function mpwThumbFail(el, why) {
|
|
|
9658
9898
|
try {
|
|
9659
9899
|
document.addEventListener("visibilitychange", () => {
|
|
9660
9900
|
powHiddenNow = document.hidden === true;
|
|
9901
|
+
if (!powHiddenNow) mpwHiddenFrozen = false; // H: 可见 ⇒ 冻结状态一并解除
|
|
9902
|
+
mpwHiddenLedgerPush("visibilitychange", { state: (() => { try { return String(document.visibilityState || "") } catch (e) { return "" } })() });
|
|
9661
9903
|
updatePowerPause();
|
|
9662
9904
|
});
|
|
9663
9905
|
} catch {}
|
|
@@ -9667,9 +9909,31 @@ function mpwThumbFail(el, why) {
|
|
|
9667
9909
|
} catch {}
|
|
9668
9910
|
// ⑤ pagehide/pageshow:bfcache / 切标签在部分浏览器不派发 visibilitychange
|
|
9669
9911
|
try {
|
|
9670
|
-
window.addEventListener("pagehide", () => { powHiddenNow = true; updatePowerPause(); });
|
|
9912
|
+
window.addEventListener("pagehide", () => { powHiddenNow = true; mpwHiddenLedgerPush("pagehide"); updatePowerPause(); });
|
|
9671
9913
|
window.addEventListener("pageshow", () => {
|
|
9672
9914
|
try { powHiddenNow = document.hidden === true } catch (e) { powHiddenNow = false }
|
|
9915
|
+
mpwHiddenLedgerPush("pageshow");
|
|
9916
|
+
updatePowerPause();
|
|
9917
|
+
});
|
|
9918
|
+
} catch {}
|
|
9919
|
+
/* H(2026-09-25 ②) `freeze` / `resume`:页面生命周期 API(Android 冻结后台标签时派发)。
|
|
9920
|
+
为什么必须单独接:冻结**不一定**派发 `visibilitychange`(页面可能"一挂载就已经是 hidden"),
|
|
9921
|
+
而冻结期间我们的解码/音频仍可能被系统放行 ⇒ 把 freeze 当成 hidden 是最保守也最正确的读法。
|
|
9922
|
+
`document.addEventListener('freeze')` 在不支持的浏览器上等于挂了个永不触发的监听(无害,
|
|
9923
|
+
不抛错)⇒ 不做能力探测、也不写能力位(免得把"支持"说成"不支持")。 */
|
|
9924
|
+
try {
|
|
9925
|
+
document.addEventListener("freeze", () => {
|
|
9926
|
+
mpwHiddenFrozen = true;
|
|
9927
|
+
mpwHiddenLedgerPush("freeze");
|
|
9928
|
+
/* 走**同一条状态机**(而不是单独 pause 一下):`updatePowerPause` 里
|
|
9929
|
+
`mpwHiddenFrozen ⇒ hiddenNow` ⇒ `powPaused=true` + `pauseWallpaperVideo()` 全落点;
|
|
9930
|
+
对应地 `resume`/可见时它会走 `resumeWallpaperVideo()` 按**原状态**续播
|
|
9931
|
+
(单独 pause 会让画面永久冻在冻结那一刻 —— 恢复没人管)。 */
|
|
9932
|
+
updatePowerPause();
|
|
9933
|
+
});
|
|
9934
|
+
document.addEventListener("resume", () => {
|
|
9935
|
+
mpwHiddenFrozen = false;
|
|
9936
|
+
mpwHiddenLedgerPush("resume");
|
|
9673
9937
|
updatePowerPause();
|
|
9674
9938
|
});
|
|
9675
9939
|
} catch {}
|
|
@@ -10383,6 +10647,11 @@ function mpwThumbFail(el, why) {
|
|
|
10383
10647
|
下一次就能直接从上报里读到来源。开关 `?npaudit=0` 完全不装(零开销);缺省装。
|
|
10384
10648
|
══════════════════════════════════════════════════════════════════════════════════ */
|
|
10385
10649
|
const MPW_AUDIT_MAX = 200;
|
|
10650
|
+
/* H(2026-09-25 ③) 「hidden 期间起播」**无论 owner 是谁都记一条**的独立台账(≤16 条):
|
|
10651
|
+
为什么不能只靠上面那个 200 条环形缓冲 —— 它**刷新即清空**,而本条现场恰恰是"页面在后台
|
|
10652
|
+
被丢弃/重载",等用户回到 Via 时缓冲里往往什么都没有了。这份台账挂在 `window` 上并与
|
|
10653
|
+
`mpwHiddenLedger` 一起进 `/diag` 信标,owner/src/栈三样齐全(第三方也照记,只是不控制它)。 */
|
|
10654
|
+
const MPW_HIDDEN_PLAY_MAX = 16;
|
|
10386
10655
|
let mpwAuditInstalled = false;
|
|
10387
10656
|
let mpwAuditLastBeacon = 0;
|
|
10388
10657
|
function mpwAuditOn() {
|
|
@@ -10477,6 +10746,18 @@ function mpwThumbFail(el, why) {
|
|
|
10477
10746
|
|| !!(rec.el && rec.el.connected === false && rec.paused === false)
|
|
10478
10747
|
|| (kind === "play" && rec.hidden === true)
|
|
10479
10748
|
|| ((kind === "unmute" || kind === "volume") && rec.hidden === true && rec.muted === false);
|
|
10749
|
+
/* H(2026-09-25 ③) hidden 期间那**两类**转变(起播 / 取消静音)另存一份**独立台账**:
|
|
10750
|
+
① owner 是谁都记(我们自己、壁纸帧、别的插件、跨源帧的 foreign 条目)—— 只记录、不控制;
|
|
10751
|
+
② 刷新丢不掉的那些行会跟着 /diag 信标一起落盘(见下面 beacon 的 hiddenPlays 字段)。 */
|
|
10752
|
+
if (rec.hidden === true && (kind === "play" || kind === "unmute" || kind === "volume" || kind === "webaudio-start" || kind === "audioctx-resume")) {
|
|
10753
|
+
try {
|
|
10754
|
+
const hp = window.__mpwHiddenPlays || (window.__mpwHiddenPlays = []);
|
|
10755
|
+
hp.push({ t: rec.t, kind: rec.kind, who: rec.who, win: rec.win, el: rec.el, muted: rec.muted, volume: rec.volume, paused: rec.paused, npMute: rec.np && rec.np.mute });
|
|
10756
|
+
if (hp.length > MPW_HIDDEN_PLAY_MAX) hp.splice(0, hp.length - MPW_HIDDEN_PLAY_MAX);
|
|
10757
|
+
if (store) store.hiddenPlays = hp;
|
|
10758
|
+
mpwHiddenLedgerPush("hidden-play", { owner: (rec.el && rec.el.owner) || ((rec.win && rec.win.foreign) ? "foreign-frame" : "unknown"), playKind: rec.kind, src: (rec.el && rec.el.src) || (rec.win && rec.win.url) || "", audible: !!audible });
|
|
10759
|
+
} catch (e) {}
|
|
10760
|
+
}
|
|
10480
10761
|
if (suspicious) {
|
|
10481
10762
|
if (store) { try { store.suspicious = (store.suspicious || 0) + 1 } catch (e) {} }
|
|
10482
10763
|
if (Date.now() - mpwAuditLastBeacon > 5000) {
|
|
@@ -10488,6 +10769,11 @@ function mpwThumbFail(el, why) {
|
|
|
10488
10769
|
: (audible ? "audible-playback" : (rec.el && rec.el.connected === false ? "detached-playing" : "hidden-transition"))),
|
|
10489
10770
|
record: rec, window: list.slice(-12), count: list.length,
|
|
10490
10771
|
visibility: (() => { try { return document.visibilityState } catch (e) { return null } })(),
|
|
10772
|
+
/* H(2026-09-25 ③) 两条"刷新也丢不掉"的台账一起进信标:hidden 期间的起播
|
|
10773
|
+
(含 owner/src/栈)与我们自己的隐藏闸门台账。真机下次复现时,**即使页面
|
|
10774
|
+
在后台被丢弃/重载**,落盘的那一份里也有这两样。 */
|
|
10775
|
+
hiddenPlays: (() => { try { return (window.__mpwHiddenPlays || []).slice(-8) } catch (e) { return null } })(),
|
|
10776
|
+
hiddenLedger: (() => { try { return mpwHiddenLedger.slice(-12) } catch (e) { return null } })(),
|
|
10491
10777
|
});
|
|
10492
10778
|
} catch (e) {}
|
|
10493
10779
|
try { console.warn("[dsh-mpkg-wallpaper][audio-audit] 可疑出声:", JSON.stringify(rec).slice(0, 400)); } catch (e) {}
|
|
@@ -11864,6 +12150,34 @@ function installAudioBus(win, opts = {}) {
|
|
|
11864
12150
|
npSource: () => { try { return { kind: npSourceKind(readSection()), id: npSourceId(readSection()), prev: npSrcIdPrev, audioPaused: npAudio ? !!npAudio.paused : null, audioSrc: npAudio ? String(npAudio.getAttribute('src') || '').slice(0, 60) : null } } catch (e) { return null } },
|
|
11865
12151
|
hiddenAudioBlock: () => { try { return mpwHiddenAudioBlock() } catch (e) { return null } },
|
|
11866
12152
|
hiddenPauseOn: () => { try { return mpwHiddenPauseOn() } catch (e) { return null } },
|
|
12153
|
+
/* ══ H(2026-09-25) 隐藏闸门的钩子(tools/hidden-gate-test.mjs 用;生产不引用)══
|
|
12154
|
+
暴露的都是**真实现**:状态机(gate/bootBlocked/pausedByUs/frozen)、台账、以及三个
|
|
12155
|
+
真挂载点(showVideoEl/showWebEl/showSceneEl)+ 两条真恢复路径(pause/resume)。 */
|
|
12156
|
+
hiddenState: () => {
|
|
12157
|
+
try {
|
|
12158
|
+
return {
|
|
12159
|
+
legacy: mpwHiddenGateLegacy(), on: mpwHiddenPauseOn(), live: mpwHiddenLive(),
|
|
12160
|
+
block: mpwHiddenAudioBlock(), frozen: !!mpwHiddenFrozen,
|
|
12161
|
+
bootBlocked: !!mpwHiddenBootBlocked, pausedByUs: !!mpwHiddenPausedByUs,
|
|
12162
|
+
bootLogged: !!mpwHiddenBootLogged, gateOn: !!mpwHiddenPauseOn(),
|
|
12163
|
+
};
|
|
12164
|
+
} catch (e) { return { err: String(e && e.message || e) } }
|
|
12165
|
+
},
|
|
12166
|
+
hiddenLedger: () => { try { return mpwHiddenLedger.slice() } catch (e) { return [] } },
|
|
12167
|
+
hiddenLedgerClear: () => { try { mpwHiddenLedger.length = 0; mpwHiddenBootLogged = false; return true } catch (e) { return false } },
|
|
12168
|
+
hiddenPlays: () => { try { return (window.__mpwHiddenPlays || []).slice() } catch (e) { return [] } },
|
|
12169
|
+
/* 挂载点:走的是**生产函数本身**(含 boot 闸门、applyWebMute、pauseWebFrame 那几段) */
|
|
12170
|
+
showVideo: (url) => { try { showVideoEl(url || readSection().image || "host:?token=t&index=0"); return true } catch (e) { return String(e && e.message || e) } },
|
|
12171
|
+
showWeb: (url) => { try { showWebEl(url || readSection().webUrl || ""); return true } catch (e) { return String(e && e.message || e) } },
|
|
12172
|
+
/* 恢复/暂停两条真路径 + 挂载期那条独立恢复口 */
|
|
12173
|
+
pauseWallpaperVideo: () => { try { pauseWallpaperVideo(); return true } catch (e) { return String(e && e.message || e) } },
|
|
12174
|
+
resumeWallpaperVideo: (force) => { try { resumeWallpaperVideo(force === true); return true } catch (e) { return String(e && e.message || e) } },
|
|
12175
|
+
bootGate: (where) => { try { return mpwHiddenBootGate(where || "test") } catch (e) { return String(e && e.message || e) } },
|
|
12176
|
+
bootResume: (where) => { try { return mpwHiddenBootResume(where || "test") } catch (e) { return String(e && e.message || e) } },
|
|
12177
|
+
/* 帧策略的可判定读数(渲染器 park / shim policy 都落在这本台账上) */
|
|
12178
|
+
rendererPolicy: () => { try { return (window.__mpwRendererAudioPush || { n: 0, last: null, log: [] }) } catch (e) { return null } },
|
|
12179
|
+
rendererPolicyClear: () => { try { window.__mpwRendererAudioPush = { n: 0, last: null, log: [] }; return true } catch (e) { return false } },
|
|
12180
|
+
unreachableFrames: () => { try { return (window.__mpwUnreachableFrames || []).slice() } catch (e) { return [] } },
|
|
11867
12181
|
ops: () => { try { return (window.__mpwNpOps || []).slice(-12) } catch (e) { return [] } },
|
|
11868
12182
|
// ⑤ 可见性/省电
|
|
11869
12183
|
defaultPowPauseHidden: DEFAULT_POW_PAUSE_HIDDEN,
|
package/package.json
CHANGED