dsh-mpkg-wallpaper 3.13.0 → 3.13.1
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/lib/audio-bus.js +6 -2
- package/lib/client.js +522 -1
- package/package.json +1 -1
package/lib/audio-bus.js
CHANGED
|
@@ -58,6 +58,10 @@ export function modeMutes(m) {
|
|
|
58
58
|
export function modeMutesAll(m) { return normalizeMode(m) === 'all'; }
|
|
59
59
|
/** 该档是否只重定向(诊断档:验证 bus 收没收全)。 */
|
|
60
60
|
export function modeRedirectOnly(m) { return normalizeMode(m) === 'redirect'; }
|
|
61
|
+
/** ①(2026-09-22 真机探针抓到的 bug) **帧内模式**:`redirect` 只对**顶层**有意义(顶层只归因、不压宿主提示音),
|
|
62
|
+
* 壁纸帧要的是"压住" ⇒ 帧内把它提升为 `1`;其余档原样下传。真机证据:修前同源帧里 `__mpwAudioBus.mode`
|
|
63
|
+
* 是 `redirect`(继承顶层)⇒ 帧内**根本没静音**。 */
|
|
64
|
+
export function frameModeFor(m) { const s = normalizeMode(m); return s === 'redirect' ? '1' : s; }
|
|
61
65
|
|
|
62
66
|
/**
|
|
63
67
|
* 在一个 window 上装总线。**幂等**(同一 win 多次调用只装一遍)。
|
|
@@ -386,7 +390,7 @@ export function installAudioBus(win, opts = {}) {
|
|
|
386
390
|
state.frames = frames.map((f, i) => {
|
|
387
391
|
let sameOrigin = false, cw = null;
|
|
388
392
|
try { cw = f.contentWindow; sameOrigin = !!(cw && cw.document) } catch (e) { sameOrigin = false }
|
|
389
|
-
if (sameOrigin && cw) { try { installAudioBus(cw, { mode: state.mode, onEvent: opts.onEvent, mediaRoots: opts.mediaRoots }) } catch (e) {} }
|
|
393
|
+
if (sameOrigin && cw) { try { installAudioBus(cw, { mode: frameModeFor(state.mode), onEvent: opts.onEvent, mediaRoots: opts.mediaRoots }) } catch (e) {} }
|
|
390
394
|
else state.uncontrollable.push({ i, src: shortSrc(f.src || f.getAttribute && f.getAttribute('src')) });
|
|
391
395
|
return { i, sameOrigin, src: shortSrc(f.src || '') };
|
|
392
396
|
});
|
|
@@ -400,7 +404,7 @@ export function installAudioBus(win, opts = {}) {
|
|
|
400
404
|
__installed: true,
|
|
401
405
|
get mode() { return state.mode },
|
|
402
406
|
get muted() { return state.muted },
|
|
403
|
-
setMode(m) { state.mode = normalizeMode(m); applyMute(); emit('mode', { mode: state.mode }); return state.mode },
|
|
407
|
+
setMode(m) { state.mode = normalizeMode(m); syncFrames(); applyMute(); emit('mode', { mode: state.mode }); return state.mode },
|
|
404
408
|
setMuted(v) {
|
|
405
409
|
state.muted = !!v;
|
|
406
410
|
syncFrames();
|
package/lib/client.js
CHANGED
|
@@ -3093,6 +3093,28 @@ function mpwThumbFail(el, why) {
|
|
|
3093
3093
|
return (typeof dm === "number" && dm > 0 && dm <= 4) || (typeof hc === "number" && hc > 0 && hc <= 4);
|
|
3094
3094
|
} catch { return false; }
|
|
3095
3095
|
}
|
|
3096
|
+
/**
|
|
3097
|
+
* ①(2026-09-23 用户第 18 条「**所有** scene 壁纸都糊,全屏也一样」) **渲染器分辨率档**。
|
|
3098
|
+
*
|
|
3099
|
+
* 根因(读数,不是观感):渲染器 `?res=` 的**缺省档是固定 1920×1080**(`DEFAULT_RES_TIER='1080p'`,
|
|
3100
|
+
* 见 we-scene-demo `core/we-scene-bundle.js` 的 `parseResTier`)——而插件此前**一个分辨率参数
|
|
3101
|
+
* 都不发**。于是窗口物理像素一旦超过 1920×1080(任何 2× DPR 屏、2560/4K 显示器),画布就被
|
|
3102
|
+
* CSS **放大**出图;全屏时画布不跟着重算(乘数固定)⇒ "全屏也一样糊"。这条与壁纸无关,
|
|
3103
|
+
* 任何场景包都吃 ⇒ 用户说的"全局链路、不是某个包"完全一致。
|
|
3104
|
+
*
|
|
3105
|
+
* 修法:场景帧显式要**活档位** `?res=dpr` = 画布 = 画布盒 × 设备 DPR,resize / DPR 变化 /
|
|
3106
|
+
* 全屏切换都会重算(渲染器 `resolveLiveCanvasSize`,算式只有一份、Node 侧有门禁);安全上限由
|
|
3107
|
+
* **渲染器自己**兜住(单边 ≤4096、总像素 ≤3840×2160 = `LIVE_CANVAS_LIMITS`)⇒ 不会因为
|
|
3108
|
+
* 一台 5× DPR 的机器把画布顶到天上去(内存纪律)。
|
|
3109
|
+
* 低内存设备(`mpwSceneLowMem()`,同时已透传 `&lowmem=1`)走 `?res=auto`:按屏幕物理像素
|
|
3110
|
+
* (`innerWidth × min(DPR,2)`)在 720p/1080p/1440p/2160p 里选一个**固定**档 —— 比 1080p 贴屏,
|
|
3111
|
+
* 又不像 `dpr` 那样在超大 CSS 盒上要满额画布。
|
|
3112
|
+
* 兼容性:旧渲染器(上游产物)不认识 `res` 的这两个取值 ⇒ 按未知参数忽略,回落它自己的缺省档
|
|
3113
|
+
* (= 与本改动前逐位相同),不会因为参数变化而失败。
|
|
3114
|
+
*/
|
|
3115
|
+
function mpwSceneResParam() {
|
|
3116
|
+
try { return mpwSceneLowMem() ? "&res=auto" : "&res=dpr"; } catch { return ""; }
|
|
3117
|
+
}
|
|
3096
3118
|
/** ①(B1) 从壁纸 URL 解析场景身份(与宿主 sceneThumbIdentity 同构:custom|folder|file / library|ltoken|file)。 */
|
|
3097
3119
|
function mpwSceneIdentityFromWebUrl(url) {
|
|
3098
3120
|
try {
|
|
@@ -3736,6 +3758,7 @@ function mpwThumbFail(el, why) {
|
|
|
3736
3758
|
bust: mpwSceneBustUrl, stripBust: mpwStripBust, identity: mpwSceneIdentityFromWebUrl,
|
|
3737
3759
|
arm: armSceneWatchdog, disarm: disarmSceneWatchdog, state: mpwSceneWdState, tick: mpwSceneWdTick,
|
|
3738
3760
|
wd: () => __mpwSceneWd, beaconUrl: mpwSceneBeaconUrl, debugKeys: MPW_SCENE_DEBUG_KEYS,
|
|
3761
|
+
resParam: mpwSceneResParam, lowMem: mpwSceneLowMem,
|
|
3739
3762
|
};
|
|
3740
3763
|
} catch {}
|
|
3741
3764
|
// 测试钩子(tools/web-wallpaper-test.mjs 用;生产不引用)
|
|
@@ -5213,7 +5236,7 @@ function mpwThumbFail(el, why) {
|
|
|
5213
5236
|
const rawUrlEff = __sbToken ? rawUrl + "&st=" + encodeURIComponent(__sbToken) : rawUrl;
|
|
5214
5237
|
const url = base + (base.indexOf("?") >= 0 ? "&" : "?")
|
|
5215
5238
|
+ "pkgurl=" + encodeURIComponent(rawUrlEff) + "&embed=1" + (sceneReport ? "" : "&noreport=1") + extBase
|
|
5216
|
-
+ (lowmem ? "&lowmem=1" : "") + debugQ
|
|
5239
|
+
+ (lowmem ? "&lowmem=1" : "") + mpwSceneResParam() + debugQ
|
|
5217
5240
|
// ①(MERGED-3 1.1) 渲染器首帧 → 宿主缩略图缓存(渲染器离线后列表仍有图)。
|
|
5218
5241
|
// demo.html 首帧/第 90 帧各 POST 一次(text/plain 简单请求免预检),失败仅日志。
|
|
5219
5242
|
// ①(批次15 B1) 首帧 POST 同时是**首帧看门狗的信标**(宿主 lastpost=1 查询)。
|
|
@@ -9750,6 +9773,503 @@ function mpwThumbFail(el, why) {
|
|
|
9750
9773
|
mpwAuditPatchWin(window);
|
|
9751
9774
|
} catch (e) {}
|
|
9752
9775
|
}
|
|
9776
|
+
// ═══ MPW-AUDIO-BUS-BEGIN ═══(由 tools/build-audio-bus.mjs 从 lib/audio-bus.js 生成;**勿手改**,改 lib/audio-bus.js 后重跑生成器)
|
|
9777
|
+
const MPW_AUDIO_BUS = (() => {
|
|
9778
|
+
// audio-bus.js —— 「跨出口的总线级静音」内核(宿主侧,纯逻辑,可在桩环境单测)
|
|
9779
|
+
//
|
|
9780
|
+
// 为什么要它(事故链):用户反复报「设置里静音了,但还是会偶发卡顿地响一声」。根因不是"静音按钮没生效",
|
|
9781
|
+
// 而是**声音出口不止一个**:我们自己的音乐控件走 `<audio>`(muted 够用),但壁纸/角色语音/可视化可能走
|
|
9782
|
+
// Web Audio——`new AudioContext()` 是**别人**建的、`source.connect(ctx.destination)` 也是**别人**连的,
|
|
9783
|
+
// 我们连"谁在响"都看不见。上几轮我们只做到了**记录**(`mpwInstallAudioAudit` 的钩子),
|
|
9784
|
+
// 记录不等于接管:只要有一个节点直连 `destination`,它就绕过了我们的静音。
|
|
9785
|
+
//
|
|
9786
|
+
// 本模块做**接管**,而且是**总线级**的(逐条对应审阅意见里点出的坑):
|
|
9787
|
+
// ① `AudioNode.prototype.connect` 原型级拦截:任何节点连 `ctx.destination` ⇒ 改接到该 ctx 的
|
|
9788
|
+
// **懒创建 masterGain**。原型级 ⇒ "比我们更早创建的 AudioContext"也能被收进来(只要它**还没**连);
|
|
9789
|
+
// ② masterGain 的创建与它到 destination 的连接**走原始引用**(否则懒创建那一下会自己拦自己 ⇒ 递归);
|
|
9790
|
+
// ③ `connect` 的**返回值原样透传**(`a.connect(b).connect(c)` 链式依赖它);
|
|
9791
|
+
// ④ 判定用 `dest === ctx.destination`(**引用比较**;`instanceof` 跨 realm 会失败);
|
|
9792
|
+
// ⑤ `OfflineAudioContext` **放行**(它也有 destination;误挂 master 会改掉离线渲染结果);
|
|
9793
|
+
// ⑥ 静音 = `master.gain.cancelScheduledValues(now) + setValueAtTime(0, now)`(**不用 `ctx.suspend()`**:
|
|
9794
|
+
// 冻结 `currentTime` 会破坏第三方的调度与可视化);analyser 若接在 master **之前**,频谱仍有数据
|
|
9795
|
+
// ⇒ 这同时修掉我们"元素被 muted ⇒ 音条恒 0"的老限制;
|
|
9796
|
+
// ⑦ 归因补齐:`new Audio()`、`createMediaElementSource`(元素 muted 只影响输入源,输出在 Web Audio)、
|
|
9797
|
+
// `createMediaStreamSource`(不吃 `track.enabled`)、`src`/`srcObject` setter(autoplay 绕过 `play()`)、
|
|
9798
|
+
// `start(when)` 的迟到分布(**负很多** = bfcache/节流恢复;**负一点点** = 正常抖动,不误杀)、
|
|
9799
|
+
// Shadow DOM 里的媒体元素、`speechSynthesis`、`RTCPeerConnection` 的 `track.enabled`;
|
|
9800
|
+
// ⑧ **跨 realm 各装一遍**(同源 iframe 的 `AudioNode.prototype` 与父页不是同一个)。
|
|
9801
|
+
//
|
|
9802
|
+
// 档位(`?mpwhardmute=` 或 `bus.setMode()`)—— 二分定位用,不是"总开关":
|
|
9803
|
+
// off 只归因,不动声音(缺省)
|
|
9804
|
+
// redirect **只重定向不静音**:还能听见 ⇒ 有出口绕过 connect(历史遗留直连 / 跨 realm);
|
|
9805
|
+
// 声音变闷变小 ⇒ 有节点被**重复挂** master(多次 connect destination)
|
|
9806
|
+
// 1 重定向 + master 归零 + 媒体压制 + speechSynthesis.cancel + WebRTC track.enabled=false
|
|
9807
|
+
// all 1 + 对跨域帧 postMessage 请求静音 + 连未归因的 ctx 也压
|
|
9808
|
+
// report 只汇报不压(产出"不可控清单")
|
|
9809
|
+
//
|
|
9810
|
+
// 诚实边界:hook 之前**已经**连上 destination 的节点无法追溯(没有任何 API 能枚举已有连接)⇒
|
|
9811
|
+
// report() 会把"能观测到出声但不在我们 bus 上"的 ctx 单列为 `bypassSuspected`,不假装收全了。
|
|
9812
|
+
|
|
9813
|
+
const BUS_MODES = ['off', 'redirect', '1', 'all', 'report'];
|
|
9814
|
+
const BUS_DEFAULT_MODE = 'off';
|
|
9815
|
+
const BUS_ATTR = 'data-mpw-audio-bus';
|
|
9816
|
+
const BUS_MSG = 'mpw:audio-bus';
|
|
9817
|
+
/** 迟到补播的判定阈值:负得比这个多 ⇒ 当"恢复后补播"(bfcache/节流),否则算正常抖动,不误杀。 */
|
|
9818
|
+
const LATE_START_SEC = 1.0;
|
|
9819
|
+
const JITTER_START_SEC = 0.05;
|
|
9820
|
+
|
|
9821
|
+
const now = (win) => { try { return win.performance && win.performance.now ? win.performance.now() : Date.now() } catch (e) { return Date.now() } };
|
|
9822
|
+
|
|
9823
|
+
/** 规范化档位(未知值 ⇒ 缺省;不抛)。 */
|
|
9824
|
+
function normalizeMode(m) {
|
|
9825
|
+
const s = String(m === undefined || m === null ? '' : m).trim().toLowerCase();
|
|
9826
|
+
return BUS_MODES.indexOf(s) >= 0 ? s : BUS_DEFAULT_MODE;
|
|
9827
|
+
}
|
|
9828
|
+
|
|
9829
|
+
/** 该档是否真的**压**声音(report/off/redirect 都不压)。 */
|
|
9830
|
+
function modeMutes(m) {
|
|
9831
|
+
const s = normalizeMode(m);
|
|
9832
|
+
return s === '1' || s === 'all';
|
|
9833
|
+
}
|
|
9834
|
+
/** 该档是否要连"未归因的 ctx"一起压。 */
|
|
9835
|
+
function modeMutesAll(m) { return normalizeMode(m) === 'all'; }
|
|
9836
|
+
/** 该档是否只重定向(诊断档:验证 bus 收没收全)。 */
|
|
9837
|
+
function modeRedirectOnly(m) { return normalizeMode(m) === 'redirect'; }
|
|
9838
|
+
/** ①(2026-09-22 真机探针抓到的 bug) **帧内模式**:`redirect` 只对**顶层**有意义(顶层只归因、不压宿主提示音),
|
|
9839
|
+
* 壁纸帧要的是"压住" ⇒ 帧内把它提升为 `1`;其余档原样下传。真机证据:修前同源帧里 `__mpwAudioBus.mode`
|
|
9840
|
+
* 是 `redirect`(继承顶层)⇒ 帧内**根本没静音**。 */
|
|
9841
|
+
function frameModeFor(m) { const s = normalizeMode(m); return s === 'redirect' ? '1' : s; }
|
|
9842
|
+
|
|
9843
|
+
/**
|
|
9844
|
+
* 在一个 window 上装总线。**幂等**(同一 win 多次调用只装一遍)。
|
|
9845
|
+
* @param {object} win 目标 window(可传 iframe 的 contentWindow ⇒ 跨 realm 各装一遍)
|
|
9846
|
+
* @param {object} opts { mode, onEvent(name, detail), mediaRoots: ()=>Element[] }
|
|
9847
|
+
* @returns {object|null} api
|
|
9848
|
+
*/
|
|
9849
|
+
function installAudioBus(win, opts = {}) {
|
|
9850
|
+
if (!win || typeof win !== 'object') return null;
|
|
9851
|
+
if (win.__mpwAudioBus && win.__mpwAudioBus.__installed) { // 幂等:同一 realm 只装一遍
|
|
9852
|
+
if (opts.mode !== undefined) win.__mpwAudioBus.setMode(opts.mode);
|
|
9853
|
+
return win.__mpwAudioBus;
|
|
9854
|
+
}
|
|
9855
|
+
const log = [];
|
|
9856
|
+
const emit = (name, detail) => {
|
|
9857
|
+
const rec = { t: now(win), name, detail: detail || null };
|
|
9858
|
+
log.push(rec);
|
|
9859
|
+
if (log.length > 300) log.shift();
|
|
9860
|
+
try { if (typeof opts.onEvent === 'function') opts.onEvent(name, rec.detail) } catch (e) { /* 上报失败不许影响音频 */ }
|
|
9861
|
+
};
|
|
9862
|
+
|
|
9863
|
+
const state = {
|
|
9864
|
+
mode: normalizeMode(opts.mode),
|
|
9865
|
+
muted: false,
|
|
9866
|
+
wasMuted: false, // 上一次是否处于"真的压着"的状态(决定恢复时要不要写回 1)
|
|
9867
|
+
contexts: [], // { id, ctx, adopted, offline, createAt, createStack, frames }
|
|
9868
|
+
masters: new WeakMap(), // ctx -> masterGain
|
|
9869
|
+
adopted: new Set(), // 被我们接管过的 ctx(**含**安装前就存在的老 ctx)——静音要遍历这一份
|
|
9870
|
+
media: new Set(), // 媒体元素(含 new Audio() 与 shadow DOM 里的)
|
|
9871
|
+
tappedMedia: new WeakSet(), // 被 createMediaElementSource 接管的元素(不能 pause:analyser 要数据)
|
|
9872
|
+
redirects: 0,
|
|
9873
|
+
bypassSuspected: [], // 能观测到出声但不在 bus 上的 ctx
|
|
9874
|
+
uncontrollable: [], // 跨域帧(拿不到 contentDocument)
|
|
9875
|
+
frames: [],
|
|
9876
|
+
};
|
|
9877
|
+
let ctxSeq = 0;
|
|
9878
|
+
let timer = 0;
|
|
9879
|
+
/* 本 realm 的**原始 connect**。必须是 `AudioNode.prototype.connect` —— 真实浏览器里
|
|
9880
|
+
`AudioContext.prototype` 上**没有** connect(我第一版写错过,那样一条都拦不到)。 */
|
|
9881
|
+
let realmOrigConnect = null;
|
|
9882
|
+
let realmNodeProto = null;
|
|
9883
|
+
|
|
9884
|
+
const protoOf = (Ctor) => { try { return Ctor && Ctor.prototype ? Ctor.prototype : null } catch (e) { return null } };
|
|
9885
|
+
const stackOf = () => { try { return String(new win.Error().stack || '').split('\n').slice(2, 5).join(' | ').slice(0, 400) } catch (e) { return '' } };
|
|
9886
|
+
|
|
9887
|
+
/* ── ① 原型级 connect 拦截(含"懒创建 masterGain") ───────────────────────────── */
|
|
9888
|
+
const patched = []; // [{ proto, origConnect }],卸载时还原
|
|
9889
|
+
const patchConnect = (proto) => {
|
|
9890
|
+
if (!proto || typeof proto.connect !== 'function' || proto.__mpwBusPatched) return false;
|
|
9891
|
+
const origConnect = proto.connect;
|
|
9892
|
+
proto.connect = function mpwBusConnect(dest) {
|
|
9893
|
+
const rest = Array.prototype.slice.call(arguments, 1);
|
|
9894
|
+
let ctx = null;
|
|
9895
|
+
try { ctx = this && this.context } catch (e) { /* 某些桩没有 context */ }
|
|
9896
|
+
// 只拦"直连 destination";用**引用比较**(instanceof 跨 realm 会失败)
|
|
9897
|
+
if (ctx && dest && dest === ctx.destination && !isOfflineCtx(ctx)) {
|
|
9898
|
+
let master = state.masters.get(ctx);
|
|
9899
|
+
if (!master) {
|
|
9900
|
+
master = createMaster(ctx); // 内部全走原始引用
|
|
9901
|
+
state.masters.set(ctx, master);
|
|
9902
|
+
if (master) {
|
|
9903
|
+
state.adopted.add(ctx);
|
|
9904
|
+
/* 比我们更早创建、我们没登记过的 ctx(历史遗留):**采用时补登记**,
|
|
9905
|
+
否则 applyMute 遍历的名单里没有它 ⇒ 它照样出声(这正是"假绿"的来源)。 */
|
|
9906
|
+
if (ctxId(ctx) < 0) state.contexts.push({ id: 'ctx' + (++ctxSeq), ctx, adopted: true, legacy: true, createAt: now(win), createStack: stackOf(), frames: 0 });
|
|
9907
|
+
}
|
|
9908
|
+
}
|
|
9909
|
+
if (this !== master) state.redirects++;
|
|
9910
|
+
emit('reroute', { ctxId: ctxId(ctx), kind: nodeKind(this), stack: stackOf() });
|
|
9911
|
+
origConnect.call(this, master, ...rest);
|
|
9912
|
+
/* ③ 真 API 语义:`connect(dest)` 返回**调用方传进来的那个目标节点**(链式 `a.connect(b).connect(c)` 依赖它)。
|
|
9913
|
+
若把 master 返回出去,第三方的 `a.connect(ctx.destination).connect(y)` 会变成 `master→y`(语义被改)。 */
|
|
9914
|
+
return dest;
|
|
9915
|
+
}
|
|
9916
|
+
return origConnect.call(this, dest, ...rest);
|
|
9917
|
+
};
|
|
9918
|
+
proto.__mpwBusPatched = true;
|
|
9919
|
+
patched.push({ proto, origConnect });
|
|
9920
|
+
if (!realmOrigConnect) { realmOrigConnect = origConnect; realmNodeProto = proto }
|
|
9921
|
+
return true;
|
|
9922
|
+
};
|
|
9923
|
+
/** ② masterGain 的创建与连接**必须走原始引用**(否则懒创建那一下会自己拦自己 ⇒ 递归)。 */
|
|
9924
|
+
const createMaster = (ctx) => {
|
|
9925
|
+
const origCreate = ctx.__origCreateGain || (ctx.createGain && ctx.createGain.bind(ctx));
|
|
9926
|
+
const master = origCreate ? origCreate() : null;
|
|
9927
|
+
if (!master) return null;
|
|
9928
|
+
try {
|
|
9929
|
+
if (master.gain) { master.gain.value = state.muted ? 0 : 1 }
|
|
9930
|
+
const origConnect = realmOrigConnect;
|
|
9931
|
+
if (origConnect) origConnect.call(master, ctx.destination); // 原始引用,绕开 hook
|
|
9932
|
+
else if (master.connect) master.connect(ctx.destination);
|
|
9933
|
+
} catch (e) { emit('master-connect-fail', { err: String(e && e.message || e) }) }
|
|
9934
|
+
return master;
|
|
9935
|
+
};
|
|
9936
|
+
const isOfflineCtx = (ctx) => {
|
|
9937
|
+
try {
|
|
9938
|
+
const OAC = win.OfflineAudioContext || win.webkitOfflineAudioContext;
|
|
9939
|
+
return !!(OAC && ctx instanceof OAC); // ⑤ 离线渲染放行
|
|
9940
|
+
} catch (e) { return false }
|
|
9941
|
+
};
|
|
9942
|
+
const ctxId = (ctx) => { const f = state.contexts.find((c) => c.ctx === ctx); return f ? f.id : -1 };
|
|
9943
|
+
const nodeKind = (n) => { try { return String((n && n.constructor && n.constructor.name) || 'AudioNode') } catch (e) { return 'AudioNode' } };
|
|
9944
|
+
|
|
9945
|
+
/* ── ② AudioContext 构造拦截:登记 + 归因(接管本身靠 connect,构造只登记) ───── */
|
|
9946
|
+
const patchCtor = (winObj, key) => {
|
|
9947
|
+
try {
|
|
9948
|
+
const Orig = winObj[key];
|
|
9949
|
+
if (typeof Orig !== 'function' || Orig.__mpwBusWrapped) return;
|
|
9950
|
+
const Wrapped = function (...args) {
|
|
9951
|
+
const ctx = new Orig(...args);
|
|
9952
|
+
try {
|
|
9953
|
+
state.contexts.push({ id: 'ctx' + (++ctxSeq), ctx, adopted: false, offline: false, createAt: now(win), createStack: stackOf(), frames: 0 });
|
|
9954
|
+
emit('ctx-created', { ctor: key, state: ctx && ctx.state });
|
|
9955
|
+
} catch (e) {}
|
|
9956
|
+
return ctx;
|
|
9957
|
+
};
|
|
9958
|
+
try { Wrapped.prototype = Orig.prototype } catch (e) {}
|
|
9959
|
+
Wrapped.__mpwBusWrapped = true;
|
|
9960
|
+
winObj[key] = Wrapped; // 构造只负责**登记/归因**;接管靠下面的 AudioNode.prototype
|
|
9961
|
+
} catch (e) {}
|
|
9962
|
+
};
|
|
9963
|
+
|
|
9964
|
+
/* ── ③ 归因钩子:MediaElement / MediaStream / src / srcObject / start(when) / speech / WebRTC ── */
|
|
9965
|
+
const patchAttribution = () => {
|
|
9966
|
+
const ACproto = protoOf(win.AudioContext || win.webkitAudioContext);
|
|
9967
|
+
if (ACproto && !ACproto.__mpwBusAttr) {
|
|
9968
|
+
ACproto.__mpwBusAttr = true;
|
|
9969
|
+
for (const [fn, kind] of [['createMediaElementSource', 'media-element'], ['createMediaStreamSource', 'media-stream'], ['createMediaStreamDestination', 'media-stream-dest']]) {
|
|
9970
|
+
try {
|
|
9971
|
+
const orig = ACproto[fn];
|
|
9972
|
+
if (typeof orig !== 'function') continue;
|
|
9973
|
+
ACproto[fn] = function (...a) {
|
|
9974
|
+
const node = orig.apply(this, a);
|
|
9975
|
+
try {
|
|
9976
|
+
if (kind === 'media-element' && a[0]) { state.tappedMedia.add(a[0]); registerMedia(a[0]); }
|
|
9977
|
+
emit('source-node', { kind, ctxId: -1, stack: stackOf() });
|
|
9978
|
+
} catch (e) {}
|
|
9979
|
+
return node;
|
|
9980
|
+
};
|
|
9981
|
+
} catch (e) {}
|
|
9982
|
+
}
|
|
9983
|
+
}
|
|
9984
|
+
// start(when):迟到分布(负很多 = bfcache/节流恢复;负一点点 = 抖动)
|
|
9985
|
+
for (const [key, kind] of [['AudioBufferSourceNode', 'buffer'], ['OscillatorNode', 'oscillator'], ['ConstantSourceNode', 'constant'], ['AudioWorkletNode', 'worklet']]) {
|
|
9986
|
+
try {
|
|
9987
|
+
const Ctor = win[key]; const np = protoOf(Ctor);
|
|
9988
|
+
if (!np || typeof np.start !== 'function' || np.__mpwBusStart) continue;
|
|
9989
|
+
const origStart = np.start;
|
|
9990
|
+
np.start = function (when) {
|
|
9991
|
+
const rest = Array.prototype.slice.call(arguments, 1);
|
|
9992
|
+
try {
|
|
9993
|
+
const ctx = this.context;
|
|
9994
|
+
const cur = ctx && ctx.currentTime;
|
|
9995
|
+
const delta = (typeof when === 'number' && typeof cur === 'number') ? (when - cur) : null;
|
|
9996
|
+
/* 只**记录**、绝不阻止/丢弃 start("误杀会掉音符"):负得多 ⇒ burst(bfcache/节流恢复后的补播);
|
|
9997
|
+
负一点点 ⇒ jitter(正常调度抖动,进分布供人判读)。未来时刻不记。 */
|
|
9998
|
+
if (delta !== null && delta < 0) {
|
|
9999
|
+
emit(delta < -LATE_START_SEC ? 'start-late-burst' : 'start-late-jitter', { kind, delta: Number(delta.toFixed(3)), mode: state.mode, muted: state.muted, stack: stackOf() });
|
|
10000
|
+
}
|
|
10001
|
+
} catch (e) {}
|
|
10002
|
+
return origStart.apply(this, arguments);
|
|
10003
|
+
};
|
|
10004
|
+
np.__mpwBusStart = true;
|
|
10005
|
+
} catch (e) {}
|
|
10006
|
+
}
|
|
10007
|
+
// new Audio() / src / srcObject(autoplay + src 赋值绕过 play())
|
|
10008
|
+
try {
|
|
10009
|
+
const OrigAudio = win.Audio;
|
|
10010
|
+
if (typeof OrigAudio === 'function' && !OrigAudio.__mpwBusAudio) {
|
|
10011
|
+
const Wrapped = function (...a) { const el = new OrigAudio(...a); try { registerMedia(el); emit('new-audio', { src: shortSrc(el && el.src) }) } catch (e) {} return el };
|
|
10012
|
+
try { Wrapped.prototype = OrigAudio.prototype } catch (e) {}
|
|
10013
|
+
Wrapped.__mpwBusAudio = true;
|
|
10014
|
+
win.Audio = Wrapped;
|
|
10015
|
+
}
|
|
10016
|
+
} catch (e) {}
|
|
10017
|
+
const HME = win.HTMLMediaElement && win.HTMLMediaElement.prototype;
|
|
10018
|
+
if (HME && !HME.__mpwBusMedia) {
|
|
10019
|
+
HME.__mpwBusMedia = true;
|
|
10020
|
+
for (const prop of ['src', 'srcObject']) {
|
|
10021
|
+
try {
|
|
10022
|
+
const d = Object.getOwnPropertyDescriptor(HME, prop);
|
|
10023
|
+
if (!d || !d.set) continue;
|
|
10024
|
+
const origSet = d.set, origGet = d.get;
|
|
10025
|
+
Object.defineProperty(HME, prop, {
|
|
10026
|
+
configurable: true, enumerable: d.enumerable,
|
|
10027
|
+
get: origGet,
|
|
10028
|
+
set(v) {
|
|
10029
|
+
try { if (state.muted && modeMutes(state.mode)) suppress(this) } catch (e) {}
|
|
10030
|
+
emit(prop === 'src' ? 'src-set' : 'srcobject-set', { src: prop === 'src' ? shortSrc(v) : '[stream]', muted: !!(this && this.muted) });
|
|
10031
|
+
return origSet.call(this, v);
|
|
10032
|
+
},
|
|
10033
|
+
});
|
|
10034
|
+
} catch (e) {}
|
|
10035
|
+
}
|
|
10036
|
+
// play():静音期间**压制**且返回 resolved Promise(不排队——排队会在取消静音瞬间多声齐发)
|
|
10037
|
+
try {
|
|
10038
|
+
const origPlay = HME.play;
|
|
10039
|
+
if (typeof origPlay === 'function' && !HME.__mpwBusPlay) {
|
|
10040
|
+
HME.__mpwBusPlay = true;
|
|
10041
|
+
HME.play = function () {
|
|
10042
|
+
try {
|
|
10043
|
+
if (state.muted && modeMutes(state.mode)) {
|
|
10044
|
+
suppress(this);
|
|
10045
|
+
emit('play-suppressed', { src: shortSrc(this && this.src), stack: stackOf() });
|
|
10046
|
+
return win.Promise ? win.Promise.resolve() : undefined; // 必须 resolved,否则调用方 await 卡住
|
|
10047
|
+
}
|
|
10048
|
+
} catch (e) {}
|
|
10049
|
+
return origPlay.apply(this, arguments);
|
|
10050
|
+
};
|
|
10051
|
+
}
|
|
10052
|
+
} catch (e) {}
|
|
10053
|
+
// volumechange:静音下有人抬 volume/muted ⇒ 立刻归零 + 记栈
|
|
10054
|
+
try {
|
|
10055
|
+
if (!HME.__mpwBusVol) {
|
|
10056
|
+
HME.__mpwBusVol = true;
|
|
10057
|
+
const origAdd = HME.addEventListener;
|
|
10058
|
+
if (typeof origAdd === 'function') {
|
|
10059
|
+
const self = win;
|
|
10060
|
+
origAdd.call(self.document || self, 'volumechange', (e) => {
|
|
10061
|
+
const el = e && e.target;
|
|
10062
|
+
if (!el || !state.muted || !modeMutes(state.mode)) return;
|
|
10063
|
+
if (!el.muted || Number(el.volume) > 0) {
|
|
10064
|
+
emit('volume-raised-while-muted', { muted: !!el.muted, volume: Number(el.volume), stack: stackOf() });
|
|
10065
|
+
suppress(el);
|
|
10066
|
+
}
|
|
10067
|
+
}, true);
|
|
10068
|
+
}
|
|
10069
|
+
}
|
|
10070
|
+
} catch (e) {}
|
|
10071
|
+
}
|
|
10072
|
+
// speechSynthesis(角色语音可能走它,完全不受 <audio>/Web Audio 控制)
|
|
10073
|
+
try {
|
|
10074
|
+
const ss = win.speechSynthesis;
|
|
10075
|
+
if (ss && typeof ss.speak === 'function' && !ss.__mpwBusSpeech) {
|
|
10076
|
+
const origSpeak = ss.speak.bind(ss);
|
|
10077
|
+
ss.__mpwBusSpeech = true;
|
|
10078
|
+
ss.speak = function (u) {
|
|
10079
|
+
if (state.muted && modeMutes(state.mode)) { try { ss.cancel() } catch (e) {} ; emit('speech-suppressed', { text: String(u && u.text || '').slice(0, 60) }); return }
|
|
10080
|
+
emit('speech', { text: String(u && u.text || '').slice(0, 60) });
|
|
10081
|
+
return origSpeak(u);
|
|
10082
|
+
};
|
|
10083
|
+
}
|
|
10084
|
+
} catch (e) {}
|
|
10085
|
+
// WebRTC:outbound 的 track.enabled 是唯一能压它的开关
|
|
10086
|
+
try {
|
|
10087
|
+
const RTC = win.RTCPeerConnection;
|
|
10088
|
+
if (typeof RTC === 'function' && !RTC.__mpwBusRtc) {
|
|
10089
|
+
const Wrapped = function (...a) {
|
|
10090
|
+
const pc = new RTC(...a);
|
|
10091
|
+
try {
|
|
10092
|
+
pc.addEventListener('track', (ev) => {
|
|
10093
|
+
const tracks = (ev && ev.streams && ev.streams[0] && ev.streams[0].getTracks) ? ev.streams[0].getTracks() : [];
|
|
10094
|
+
for (const t of tracks) { try { if (t.kind === 'audio' && state.muted && modeMutes(state.mode)) t.enabled = false } catch (e) {} }
|
|
10095
|
+
emit('rtc-track', { n: tracks.length });
|
|
10096
|
+
});
|
|
10097
|
+
} catch (e) {}
|
|
10098
|
+
return pc;
|
|
10099
|
+
};
|
|
10100
|
+
try { Wrapped.prototype = RTC.prototype } catch (e) {}
|
|
10101
|
+
Wrapped.__mpwBusRtc = true;
|
|
10102
|
+
win.RTCPeerConnection = Wrapped;
|
|
10103
|
+
}
|
|
10104
|
+
} catch (e) {}
|
|
10105
|
+
};
|
|
10106
|
+
|
|
10107
|
+
const shortSrc = (s) => String(s || '').replace(/token=[^&]*/, 'token=…').slice(0, 120);
|
|
10108
|
+
|
|
10109
|
+
const registerMedia = (el) => {
|
|
10110
|
+
if (!el || typeof el !== 'object') return false;
|
|
10111
|
+
if (state.media.has(el)) return false;
|
|
10112
|
+
state.media.add(el);
|
|
10113
|
+
return true;
|
|
10114
|
+
};
|
|
10115
|
+
/** 收集媒体元素:document + **Shadow DOM**(querySelectorAll 抓不到 shadow root 里的)+ 同源 iframe。 */
|
|
10116
|
+
const collectMedia = (doc, out) => {
|
|
10117
|
+
try {
|
|
10118
|
+
for (const el of doc.querySelectorAll('audio,video')) out.push(el);
|
|
10119
|
+
for (const host of doc.querySelectorAll('*')) { if (host.shadowRoot) collectMedia(host.shadowRoot, out) }
|
|
10120
|
+
} catch (e) {}
|
|
10121
|
+
return out;
|
|
10122
|
+
};
|
|
10123
|
+
const allMedia = () => {
|
|
10124
|
+
const out = [];
|
|
10125
|
+
try { collectMedia(win.document, out) } catch (e) {}
|
|
10126
|
+
for (const el of state.media) out.push(el);
|
|
10127
|
+
if (typeof opts.mediaRoots === 'function') { try { for (const el of opts.mediaRoots() || []) out.push(el) } catch (e) {} }
|
|
10128
|
+
return out;
|
|
10129
|
+
};
|
|
10130
|
+
/** 压一个媒体元素:**被 createMediaElementSource 接管的不要 pause**(analyser 要数据,暂停 = 频谱恒 0)。 */
|
|
10131
|
+
const suppress = (el) => {
|
|
10132
|
+
try { el.muted = true } catch (e) {}
|
|
10133
|
+
try {
|
|
10134
|
+
if (state.tappedMedia.has(el)) { /* 只总线归零,保留元素输出给 analyser */ }
|
|
10135
|
+
else { el.volume = 0; if (typeof el.pause === 'function' && !el.paused) el.pause() }
|
|
10136
|
+
} catch (e) {}
|
|
10137
|
+
};
|
|
10138
|
+
|
|
10139
|
+
/* ── ④ 总线静音:只动我们自己的 masterGain(不 suspend:冻结 currentTime 会破坏调度/可视化) ── */
|
|
10140
|
+
const applyMute = () => {
|
|
10141
|
+
const mute = state.muted && modeMutes(state.mode);
|
|
10142
|
+
/* 非静音档(off/report/redirect)**不碰 gain**:我们的 master 初值就是 1,没人会去改它;
|
|
10143
|
+
反过来"每次都写 1"会盖掉第三方自己对 master 的调整,也会让 redirect 诊断档失去意义
|
|
10144
|
+
(它要能回答"bus 到底收没收全",而不是顺手改了音量)。只有**从静音恢复**那一次必须写回 1。 */
|
|
10145
|
+
if (!mute && !state.wasMuted) return;
|
|
10146
|
+
state.wasMuted = mute;
|
|
10147
|
+
for (const ctx of state.adopted) {
|
|
10148
|
+
const master = state.masters.get(ctx);
|
|
10149
|
+
if (!master || !master.gain) continue;
|
|
10150
|
+
try {
|
|
10151
|
+
const t = ctx.currentTime;
|
|
10152
|
+
if (master.gain.cancelScheduledValues) master.gain.cancelScheduledValues(t);
|
|
10153
|
+
if (master.gain.setValueAtTime) master.gain.setValueAtTime(mute ? 0 : 1, t);
|
|
10154
|
+
else master.gain.value = mute ? 0 : 1;
|
|
10155
|
+
} catch (e) { try { master.gain.value = mute ? 0 : 1 } catch (e2) {} }
|
|
10156
|
+
}
|
|
10157
|
+
for (const el of allMedia()) { try { if (mute) suppress(el); else { el.muted = false } } catch (e) {} }
|
|
10158
|
+
if (mute) { try { win.speechSynthesis && win.speechSynthesis.cancel && win.speechSynthesis.cancel() } catch (e) {} }
|
|
10159
|
+
emit('apply-mute', { muted: state.muted, mode: state.mode, contexts: state.contexts.length, media: state.media.size });
|
|
10160
|
+
};
|
|
10161
|
+
|
|
10162
|
+
/* ── ⑤ 跨域帧:压不动就**如实报不可控**;同源帧各装一遍(跨 realm) ── */
|
|
10163
|
+
const syncFrames = () => {
|
|
10164
|
+
state.uncontrollable = [];
|
|
10165
|
+
let frames = [];
|
|
10166
|
+
try { frames = Array.prototype.slice.call(win.document.querySelectorAll('iframe')) } catch (e) {}
|
|
10167
|
+
state.frames = frames.map((f, i) => {
|
|
10168
|
+
let sameOrigin = false, cw = null;
|
|
10169
|
+
try { cw = f.contentWindow; sameOrigin = !!(cw && cw.document) } catch (e) { sameOrigin = false }
|
|
10170
|
+
if (sameOrigin && cw) { try { installAudioBus(cw, { mode: frameModeFor(state.mode), onEvent: opts.onEvent, mediaRoots: opts.mediaRoots }) } catch (e) {} }
|
|
10171
|
+
else state.uncontrollable.push({ i, src: shortSrc(f.src || f.getAttribute && f.getAttribute('src')) });
|
|
10172
|
+
return { i, sameOrigin, src: shortSrc(f.src || '') };
|
|
10173
|
+
});
|
|
10174
|
+
if (modeMutesAll(state.mode)) {
|
|
10175
|
+
for (const f of frames) { try { f.contentWindow && f.contentWindow.postMessage({ type: BUS_MSG, muted: state.muted, mode: state.mode }, '*') } catch (e) {} }
|
|
10176
|
+
}
|
|
10177
|
+
return state.frames;
|
|
10178
|
+
};
|
|
10179
|
+
|
|
10180
|
+
const api = {
|
|
10181
|
+
__installed: true,
|
|
10182
|
+
get mode() { return state.mode },
|
|
10183
|
+
get muted() { return state.muted },
|
|
10184
|
+
setMode(m) { state.mode = normalizeMode(m); syncFrames(); applyMute(); emit('mode', { mode: state.mode }); return state.mode },
|
|
10185
|
+
setMuted(v) {
|
|
10186
|
+
state.muted = !!v;
|
|
10187
|
+
syncFrames();
|
|
10188
|
+
applyMute();
|
|
10189
|
+
if (state.muted && modeMutes(state.mode)) { // "偶发" ⇒ 周期性重压(新节点/自动化都要压住)
|
|
10190
|
+
if (!timer) timer = win.setInterval(() => { try { applyMute(); syncFrames() } catch (e) {} }, 750);
|
|
10191
|
+
} else if (timer) { try { win.clearInterval(timer) } catch (e) {} ; timer = 0 }
|
|
10192
|
+
return state.muted;
|
|
10193
|
+
},
|
|
10194
|
+
installFrame(cw, o) { return installAudioBus(cw, Object.assign({ mode: state.mode, onEvent: opts.onEvent }, o || {})) },
|
|
10195
|
+
syncFrames,
|
|
10196
|
+
/** 自上报快照(探针/diag 直接读它,**不需要用户操作**)。 */
|
|
10197
|
+
report() {
|
|
10198
|
+
return {
|
|
10199
|
+
mode: state.mode, muted: state.muted, redirects: state.redirects,
|
|
10200
|
+
contexts: state.contexts.map((c) => Object.assign({}, c, { ctx: undefined, adopted: state.masters.has(c.ctx), state: safeState(c.ctx) })),
|
|
10201
|
+
masters: state.contexts.filter((c) => state.masters.has(c.ctx)).length,
|
|
10202
|
+
media: allMedia().length, registered: state.media.size, tapped: countTapped(),
|
|
10203
|
+
bypassSuspected: state.bypassSuspected.slice(0, 12),
|
|
10204
|
+
uncontrollableFrames: state.uncontrollable.slice(0, 12),
|
|
10205
|
+
frames: state.frames.slice(0, 12),
|
|
10206
|
+
log: log.slice(-40),
|
|
10207
|
+
};
|
|
10208
|
+
},
|
|
10209
|
+
log: () => log.slice(-100),
|
|
10210
|
+
dispose() {
|
|
10211
|
+
if (timer) { try { win.clearInterval(timer) } catch (e) {} ; timer = 0 }
|
|
10212
|
+
for (const p of patched) { try { p.proto.connect = p.origConnect; p.proto.__mpwBusPatched = false } catch (e) {} }
|
|
10213
|
+
try { delete win.__mpwAudioBus } catch (e) { win.__mpwAudioBus = null }
|
|
10214
|
+
},
|
|
10215
|
+
};
|
|
10216
|
+
const safeState = (ctx) => { try { return ctx.state } catch (e) { return null } };
|
|
10217
|
+
const countTapped = () => { let n = 0; for (const el of state.media) if (state.tappedMedia.has(el)) n++; return n };
|
|
10218
|
+
|
|
10219
|
+
patchCtor(win, 'AudioContext'); patchCtor(win, 'webkitAudioContext');
|
|
10220
|
+
/* ④ `AudioNode.prototype` 每 realm 各装一遍(同源 iframe 的 prototype 与父页不是同一个);
|
|
10221
|
+
用 `AudioNode` 而不是 `AudioContext` —— 后者身上没有 connect。 */
|
|
10222
|
+
if (!patchConnect(protoOf(win.AudioNode))) patchConnect(protoOf(win.AudioContext));
|
|
10223
|
+
patchAttribution();
|
|
10224
|
+
try { win.__mpwAudioBus = api } catch (e) {}
|
|
10225
|
+
try {
|
|
10226
|
+
const doc = win.document;
|
|
10227
|
+
if (doc && doc.addEventListener) {
|
|
10228
|
+
const onVis = () => { emit('visibility', { state: doc.visibilityState }); if (state.muted && modeMutes(state.mode)) applyMute() };
|
|
10229
|
+
doc.addEventListener('visibilitychange', onVis, true);
|
|
10230
|
+
win.addEventListener && win.addEventListener('pageshow', (e) => { emit('pageshow', { persisted: !!(e && e.persisted) }); if (state.muted && modeMutes(state.mode)) applyMute() }, true);
|
|
10231
|
+
win.addEventListener && win.addEventListener('message', (e) => { // 跨域帧配合时的入口
|
|
10232
|
+
try { if (e && e.data && e.data.type === BUS_MSG) api.setMuted(!!e.data.muted) } catch (err) {}
|
|
10233
|
+
}, false);
|
|
10234
|
+
}
|
|
10235
|
+
} catch (e) {}
|
|
10236
|
+
emit('installed', { mode: state.mode });
|
|
10237
|
+
return api;
|
|
10238
|
+
}
|
|
10239
|
+
|
|
10240
|
+
return { installAudioBus, normalizeMode, modeMutes, modeMutesAll, modeRedirectOnly, BUS_MODES, LATE_START_SEC }
|
|
10241
|
+
})()
|
|
10242
|
+
/** 模式来源:`?mpwhardmute=off|redirect|1|all|report`。缺省 = **顶层 `redirect`**(只重定向 + 归因,
|
|
10243
|
+
* 避免顺手把宿主自己的提示音也压掉)、**帧内 `1`**(壁纸帧是真要压的那一半)。
|
|
10244
|
+
* 显式 `1`/`all` 才会连顶层一起压;帧内不接受 `redirect`(那是诊断档,帧里要的是"压住")。 */
|
|
10245
|
+
function mpwAudioBusModeFrom(search, isFrame) {
|
|
10246
|
+
const m = /[?&]mpwhardmute=([a-z0-9]+)/i.exec(String(search == null ? "" : search))
|
|
10247
|
+
const asked = m ? String(m[1]).toLowerCase() : ""
|
|
10248
|
+
if (["off", "redirect", "1", "all", "report"].indexOf(asked) >= 0) return (isFrame && asked === "redirect") ? "1" : asked
|
|
10249
|
+
return isFrame ? "1" : "redirect"
|
|
10250
|
+
}
|
|
10251
|
+
let mpwAudioBusApi = null
|
|
10252
|
+
/** 把"静音"同步到总线上(`npApplyMute` 之外的**第二条**通路:总线压的是别人建的 AudioContext)。 */
|
|
10253
|
+
function mpwAudioBusSetMuted(v) { try { if (mpwAudioBusApi) mpwAudioBusApi.setMuted(!!v) } catch (e) {} }
|
|
10254
|
+
function mpwAudioBusSync() {
|
|
10255
|
+
try { const st = readSection() || {}; mpwAudioBusSetMuted(st.mute !== void 0 ? !!st.mute : true) } catch (e) {}
|
|
10256
|
+
}
|
|
10257
|
+
function mpwInstallAudioBus() {
|
|
10258
|
+
try {
|
|
10259
|
+
if (mpwAudioBusApi) return mpwAudioBusApi
|
|
10260
|
+
const isFrame = (() => { try { return window.top !== window.self } catch (e) { return true } })()
|
|
10261
|
+
const mode = mpwAudioBusModeFrom(String((window.location && window.location.search) || ""), isFrame)
|
|
10262
|
+
mpwAudioBusApi = MPW_AUDIO_BUS.installAudioBus(window, { mode: mode })
|
|
10263
|
+
mpwAudioBusSync()
|
|
10264
|
+
/* 跟随设置:`mute` 一变就同步。800ms 轮询(一次对象读 + 一次比较),
|
|
10265
|
+
比在每个 np 路径上插一行更不容易漏;总线自己在 visibilitychange 时还会重压一次。 */
|
|
10266
|
+
let last = null
|
|
10267
|
+
try { last = !!(readSection() || {}).mute } catch (e) { last = null }
|
|
10268
|
+
setInterval(() => { try { const st = readSection() || {}; const m = (st.mute !== void 0 ? !!st.mute : true); if (m !== last) { last = m; mpwAudioBusSetMuted(m) } } catch (e) {} }, 800)
|
|
10269
|
+
return mpwAudioBusApi
|
|
10270
|
+
} catch (e) { return null }
|
|
10271
|
+
}
|
|
10272
|
+
// ═══ MPW-AUDIO-BUS-END ═══
|
|
9753
10273
|
function mpwInstallAudioAudit() {
|
|
9754
10274
|
if (mpwAuditInstalled || !mpwAuditOn()) return false;
|
|
9755
10275
|
mpwAuditInstalled = true;
|
|
@@ -9787,6 +10307,7 @@ function mpwThumbFail(el, why) {
|
|
|
9787
10307
|
return true;
|
|
9788
10308
|
}
|
|
9789
10309
|
try { mpwInstallAudioAudit(); } catch (e) {}
|
|
10310
|
+
try { mpwInstallAudioBus(); } catch (e) {}
|
|
9790
10311
|
|
|
9791
10312
|
/* ②④⑤⑥(2026-09-20) 壁纸生命周期钩子(tools/wallpaper-lifecycle-test.mjs 用;生产不引用)。
|
|
9792
10313
|
⚠ 暴露的都是**生产实现本身**(不是副本):换档成套重写 / 挂载裁决 / 形状绑定 /
|
package/package.json
CHANGED