webwallgl 1.1.0 → 1.3.5

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.
@@ -51,10 +51,24 @@
51
51
  rt.raf = void 0;
52
52
  for (const p of rt.videoPairs ?? []) p.destroy();
53
53
  rt.videoPairs = void 0;
54
- if (rt.sceneCleanup) rt.sceneCleanup();
54
+ if (rt.sceneCleanup) {
55
+ try {
56
+ rt.sceneCleanup();
57
+ } catch {
58
+ }
59
+ }
55
60
  rt.sceneCleanup = void 0;
56
61
  rt.sceneCtl = void 0;
57
62
  rt.pointerCtl = void 0;
63
+ rt.mediaCtl = void 0;
64
+ const ds = rt.wallpaperDisposers ?? [];
65
+ rt.wallpaperDisposers = [];
66
+ for (const d of ds) {
67
+ try {
68
+ d();
69
+ } catch {
70
+ }
71
+ }
58
72
  if (rt.renderer) {
59
73
  rt.renderer.dispose?.();
60
74
  rt.renderer = void 0;
@@ -96,10 +110,18 @@
96
110
  rt.canvas = void 0;
97
111
  rt.ctx = void 0;
98
112
  rt.info = void 0;
113
+ clearSceneDebugGlobals();
99
114
  resetFrameMeter(rt);
100
115
  }
101
116
  function destroyRuntime(rt) {
102
- clear(rt);
117
+ try {
118
+ clear(rt);
119
+ } catch {
120
+ }
121
+ if (rt.peekRaf !== void 0) {
122
+ cancelAnimationFrame(rt.peekRaf);
123
+ rt.peekRaf = void 0;
124
+ }
103
125
  for (const off of rt.disposers.splice(0)) {
104
126
  try {
105
127
  off();
@@ -155,6 +177,32 @@
155
177
  const obj = rt.video ?? rt.img;
156
178
  if (obj && obj.isConnected) obj.style.objectPosition = pos;
157
179
  }
180
+ const SCENE_DEBUG_GLOBALS = [
181
+ "__scene",
182
+ "__sceneLayers",
183
+ "__textures",
184
+ "__objScripts",
185
+ "__textWidgets",
186
+ "__mediaHooks",
187
+ "__mediaControl",
188
+ "__mediaStats",
189
+ "__mediaSet",
190
+ "__system",
191
+ "__liveSystem",
192
+ "__audioStats",
193
+ "__audioMute",
194
+ "__particleStats",
195
+ "__particleToggle",
196
+ "__pointerStats",
197
+ "__compositeStats",
198
+ "__compositeEnable"
199
+ ];
200
+ function clearSceneDebugGlobals() {
201
+ const w = window;
202
+ for (const k of SCENE_DEBUG_GLOBALS) {
203
+ if (k in w) delete w[k];
204
+ }
205
+ }
158
206
  function syncCanvasSize(rt, canvas, cfg) {
159
207
  const dpr = effectiveDpr(rt, cfg);
160
208
  const w = Math.max(1, Math.round((canvas.clientWidth || window.innerWidth || 1) * dpr));
@@ -3543,6 +3591,11 @@ void main() {
3543
3591
  const from = usesVarying ? /unprojectedUVs\s*\*=\s*v_PointerScale\s*\*/ : /unprojectedUVs\s*\*=\s*g_PointerScale\s*\*/;
3544
3592
  return out.replace(from, "unprojectedUVs *= " + XRAY_FRAG_UV_SCALE + " *");
3545
3593
  }
3594
+ const XRAY_SIZE_FALLBACK = 1;
3595
+ function constantFallback(uniformName, declaredDefault) {
3596
+ if (uniformName === "g_PointerScale") return XRAY_SIZE_FALLBACK;
3597
+ return declaredDefault;
3598
+ }
3546
3599
  function createRenderer(canvas, opts = {}) {
3547
3600
  const gl = canvas.getContext("webgl2", { premultipliedAlpha: false, antialias: false, alpha: false, preserveDrawingBuffer: true });
3548
3601
  if (!gl) throw new Error("当前浏览器不支持 WebGL2");
@@ -3899,7 +3952,8 @@ void main() {
3899
3952
  }
3900
3953
  for (const [matKey, entry] of Object.entries(matMeta || {})) {
3901
3954
  if (!constants || !(matKey in constants)) {
3902
- if (entry.default !== void 0) setConstant(uni, entry.uniform, entry.default);
3955
+ const dflt = constantFallback(entry.uniform, entry.default);
3956
+ if (dflt !== void 0) setConstant(uni, entry.uniform, dflt);
3903
3957
  }
3904
3958
  }
3905
3959
  }
@@ -5118,9 +5172,11 @@ void main() {
5118
5172
  __proto__: null,
5119
5173
  ALIGN,
5120
5174
  XRAY_IDLE_SCREEN_UV,
5175
+ XRAY_SIZE_FALLBACK,
5121
5176
  applyColorBlendCPU,
5122
5177
  collectGroupDescendantIds,
5123
5178
  colorBlendPlan,
5179
+ constantFallback,
5124
5180
  createRenderer,
5125
5181
  effectFboSize,
5126
5182
  layerWantsPreserveBackdrop,
@@ -11344,19 +11400,84 @@ void main() {
11344
11400
  dec.close?.();
11345
11401
  return { width, height, frames };
11346
11402
  }
11403
+ function attachVideoSpectrum(rt, cfg, v) {
11404
+ if (rt.audioBridge) return;
11405
+ const AC = window.AudioContext || window.webkitAudioContext;
11406
+ if (!AC) return;
11407
+ let ctx;
11408
+ let analyser;
11409
+ try {
11410
+ ctx = new AC();
11411
+ const srcNode = ctx.createMediaElementSource(v);
11412
+ analyser = ctx.createAnalyser();
11413
+ analyser.fftSize = 256;
11414
+ analyser.smoothingTimeConstant = 0.75;
11415
+ srcNode.connect(analyser);
11416
+ analyser.connect(ctx.destination);
11417
+ } catch (e) {
11418
+ reportDiag(rt, cfg, `media 频谱接管跳过: ${e?.message ?? e}`);
11419
+ return;
11420
+ }
11421
+ const bins = new Uint8Array(analyser.frequencyBinCount);
11422
+ const left = new Float32Array(64);
11423
+ const right = new Float32Array(64);
11424
+ const bridge = () => {
11425
+ if (ctx.state !== "running") return null;
11426
+ analyser.getByteFrequencyData(bins);
11427
+ const n = Math.min(64, bins.length);
11428
+ for (let i = 0; i < n; i++) {
11429
+ const x = bins[i] / 255;
11430
+ left[i] = x;
11431
+ right[i] = x;
11432
+ }
11433
+ for (let i = n; i < 64; i++) {
11434
+ left[i] = 0;
11435
+ right[i] = 0;
11436
+ }
11437
+ return { left, right };
11438
+ };
11439
+ rt.audioBridge = bridge;
11440
+ if (ctx.state === "suspended") {
11441
+ const kick = () => {
11442
+ void ctx.resume().catch(() => {
11443
+ });
11444
+ window.removeEventListener("pointerdown", kick);
11445
+ window.removeEventListener("keydown", kick);
11446
+ };
11447
+ window.addEventListener("pointerdown", kick, { once: true });
11448
+ window.addEventListener("keydown", kick, { once: true });
11449
+ (rt.wallpaperDisposers ??= []).push(() => {
11450
+ window.removeEventListener("pointerdown", kick);
11451
+ window.removeEventListener("keydown", kick);
11452
+ });
11453
+ }
11454
+ (rt.wallpaperDisposers ??= []).push(() => {
11455
+ if (rt.audioBridge === bridge) rt.audioBridge = null;
11456
+ void ctx.close().catch(() => {
11457
+ });
11458
+ });
11459
+ }
11347
11460
  function mountMedia(rt, cfg) {
11348
11461
  clear(rt);
11349
- if (!cfg.src) {
11462
+ const failHard = (why) => {
11463
+ reportDiag(rt, cfg, `media ${cfg.type} 失败: ${why}`);
11464
+ rt.onError?.(new Error(`媒体壁纸(${cfg.type})${why}`));
11350
11465
  rt.fallbackPage?.();
11466
+ };
11467
+ if (!cfg.src) {
11468
+ failHard("缺少资源 URL(cfg.src 为空)");
11351
11469
  return;
11352
11470
  }
11353
11471
  const isVideo = cfg.type === "video";
11354
11472
  const isGif = cfg.type === "gif";
11355
- const c = document.createElement("canvas");
11473
+ const embedded = cfg.canvas instanceof HTMLCanvasElement;
11474
+ const c = embedded ? cfg.canvas : document.createElement("canvas");
11356
11475
  const dpr = effectiveDpr(rt, cfg);
11357
- c.width = Math.max(1, Math.round(innerWidth * dpr));
11358
- c.height = Math.max(1, Math.round(innerHeight * dpr));
11359
- c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
11476
+ const vw = c.clientWidth || window.innerWidth || 1;
11477
+ const vh = c.clientHeight || window.innerHeight || 1;
11478
+ c.width = Math.max(1, Math.round(vw * dpr));
11479
+ c.height = Math.max(1, Math.round(vh * dpr));
11480
+ if (!embedded) c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
11360
11481
  const gl2 = c.getContext("webgl2", {
11361
11482
  premultipliedAlpha: false,
11362
11483
  antialias: false,
@@ -11364,13 +11485,32 @@ void main() {
11364
11485
  preserveDrawingBuffer: true
11365
11486
  });
11366
11487
  if (!gl2) {
11367
- reportDiag(rt, cfg, `media ${cfg.type}: WEBGL2_UNAVAILABLE,回退 DOM 渲染`);
11488
+ reportDiag(rt, cfg, `media ${cfg.type}: WEBGL2_UNAVAILABLE`);
11489
+ if (embedded) {
11490
+ rt.onError?.(new Error("WEBGL2_UNAVAILABLE"));
11491
+ return;
11492
+ }
11368
11493
  if (isVideo) mountVideoDom(rt, cfg);
11369
11494
  else mountGifDom(rt, cfg);
11370
11495
  return;
11371
11496
  }
11372
- rt.wrap?.appendChild(c);
11497
+ if (!embedded) rt.wrap?.appendChild(c);
11373
11498
  rt.canvas = c;
11499
+ if (rt.onSceneInfo) {
11500
+ const hook = rt.onSceneInfo;
11501
+ rt.onSceneInfo = void 0;
11502
+ try {
11503
+ hook({
11504
+ width: vw,
11505
+ height: vh,
11506
+ layerCount: 0,
11507
+ hasModels: false,
11508
+ hasParticles: false,
11509
+ hasText: false
11510
+ });
11511
+ } catch {
11512
+ }
11513
+ }
11374
11514
  let disposed = false;
11375
11515
  rt.sceneCleanup = () => {
11376
11516
  disposed = true;
@@ -11382,6 +11522,21 @@ void main() {
11382
11522
  let pauseImpl;
11383
11523
  let resumeImpl;
11384
11524
  let videoWasPlaying = false;
11525
+ rt.sceneAudio = {
11526
+ setVolume(v) {
11527
+ const vid = rt.video;
11528
+ if (!vid) return;
11529
+ const vol = Math.max(0, Math.min(1, Number(v) || 0));
11530
+ vid.volume = vol;
11531
+ vid.muted = vol <= 0;
11532
+ if (vol > 0 && vid.paused && !rt.paused) {
11533
+ void vid.play().catch((e) => {
11534
+ reportDiag(rt, cfg, `media 取消静音后自动播放被拒绝: ${e?.message ?? e}`);
11535
+ });
11536
+ }
11537
+ },
11538
+ audios: []
11539
+ };
11385
11540
  rt.sceneCtl = {
11386
11541
  pause() {
11387
11542
  pauseImpl?.();
@@ -11395,8 +11550,7 @@ void main() {
11395
11550
  const fail = (why) => {
11396
11551
  if (disposed) return;
11397
11552
  disposed = true;
11398
- reportDiag(rt, cfg, `media ${cfg.type} 失败: ${why}`);
11399
- rt.fallbackPage?.();
11553
+ failHard(why);
11400
11554
  };
11401
11555
  void (async () => {
11402
11556
  try {
@@ -11444,6 +11598,7 @@ void main() {
11444
11598
  });
11445
11599
  if (!rt.paused) void v.play().catch(() => {
11446
11600
  });
11601
+ attachVideoSpectrum(rt, cfg, v);
11447
11602
  reportDiag(rt, cfg, `media video ${mediaW}x${mediaH} → scene 渲染`);
11448
11603
  } else {
11449
11604
  let decoded = false;
@@ -11543,6 +11698,14 @@ void main() {
11543
11698
  peek.x,
11544
11699
  peek.y
11545
11700
  ).then(() => {
11701
+ if (rt.onFirstFrame) {
11702
+ const first = rt.onFirstFrame;
11703
+ rt.onFirstFrame = void 0;
11704
+ try {
11705
+ first();
11706
+ } catch {
11707
+ }
11708
+ }
11546
11709
  if (disposed || rt.paused) return;
11547
11710
  rt.raf = requestAnimationFrame(renderLoop);
11548
11711
  }).catch((e) => fail(String(e.message || e).slice(0, 200)));
@@ -11625,6 +11788,48 @@ void main() {
11625
11788
  rt.img = img;
11626
11789
  }
11627
11790
  const PKG_PATHS = ["scene.pkg", "scenes/scene.pkg", "gifscene.pkg"];
11791
+ const EXT_TYPES = {
11792
+ mp4: "video",
11793
+ webm: "video",
11794
+ mov: "video",
11795
+ m4v: "video",
11796
+ ogv: "video",
11797
+ gif: "gif",
11798
+ png: "image",
11799
+ jpg: "image",
11800
+ jpeg: "image",
11801
+ webp: "image",
11802
+ avif: "image",
11803
+ bmp: "image"
11804
+ };
11805
+ function typeFromMime(mime) {
11806
+ const m = mime.toLowerCase().split(";")[0].trim();
11807
+ if (m === "image/gif") return "gif";
11808
+ if (m.startsWith("video/")) return "video";
11809
+ if (m.startsWith("image/")) return "image";
11810
+ return null;
11811
+ }
11812
+ function sniffMediaType(url) {
11813
+ if (typeof url !== "string" || !url) return null;
11814
+ let path = url;
11815
+ const hash = path.indexOf("#");
11816
+ if (hash >= 0) path = path.slice(0, hash);
11817
+ const q = path.indexOf("?");
11818
+ if (q >= 0) path = path.slice(0, q);
11819
+ const seg = path.split("/").pop() || "";
11820
+ const dot = seg.lastIndexOf(".");
11821
+ if (dot < 0) return null;
11822
+ return EXT_TYPES[seg.slice(dot + 1).toLowerCase()] ?? null;
11823
+ }
11824
+ async function sniffMediaTypeByHead(url, init, signal) {
11825
+ try {
11826
+ const r = await fetch(url, { ...init, method: "HEAD", signal });
11827
+ if (!r.ok) return null;
11828
+ return typeFromMime(r.headers.get("content-type") || "");
11829
+ } catch {
11830
+ return null;
11831
+ }
11832
+ }
11628
11833
  function httpSource(baseUrl, init) {
11629
11834
  const base = baseUrl.replace(/\/+$/, "");
11630
11835
  return {
@@ -11679,6 +11884,29 @@ void main() {
11679
11884
  if (signal?.aborted) throw new Error("aborted");
11680
11885
  }
11681
11886
  return { url: `${base}/${file}` };
11887
+ },
11888
+ /**
11889
+ * 媒体壁纸(video/gif/image)的资源地址:`{base}/{project.file}`。
11890
+ *
11891
+ * 与 webEntry 的区别是**没有默认文件名可兜底**:网页壁纸缺 file 时
11892
+ * index.html 是行业惯例,媒体壁纸的文件名(scene.mp4 / xxx.gif)完全由作者定,
11893
+ * 猜一个只会 404。拿不到 file 就返回 null,让 mount 报「无法解析媒体 URL」,
11894
+ * 而不是发一个必然失败的请求、再把那个 404 当成根因写进错误里。
11895
+ */
11896
+ async mediaEntry(signal) {
11897
+ let file = "";
11898
+ try {
11899
+ const r = await fetch(`${base}/project.json`, { ...init, signal });
11900
+ if (r.ok) {
11901
+ const project = await r.json();
11902
+ if (project && typeof project.file === "string" && project.file.trim()) {
11903
+ file = project.file.trim().replace(/^\/+/, "");
11904
+ }
11905
+ }
11906
+ } catch {
11907
+ if (signal?.aborted) throw new Error("aborted");
11908
+ }
11909
+ return file ? { url: `${base}/${file}` } : null;
11682
11910
  }
11683
11911
  };
11684
11912
  }
@@ -11698,6 +11926,43 @@ void main() {
11698
11926
  project: async () => project ?? null
11699
11927
  };
11700
11928
  }
11929
+ function mediaSource(urlOrFile, options) {
11930
+ const isBlob = typeof urlOrFile !== "string";
11931
+ const named = urlOrFile;
11932
+ let type = options?.type ?? (isBlob ? typeFromMime(urlOrFile.type || "") : null) ?? sniffMediaType(isBlob ? String(named.name ?? "") : urlOrFile);
11933
+ let headTried = false;
11934
+ let objectUrl = null;
11935
+ const url = () => {
11936
+ if (!isBlob) return urlOrFile;
11937
+ if (!objectUrl) objectUrl = URL.createObjectURL(urlOrFile);
11938
+ return objectUrl;
11939
+ };
11940
+ const key = options?.key ?? (isBlob ? typeof named.name === "string" ? `media:${named.name}:${named.size}:${named.lastModified ?? 0}` : void 0 : `media:${urlOrFile}`);
11941
+ return {
11942
+ key,
11943
+ async scenePkg() {
11944
+ throw new Error("mediaSource 是纯媒体来源,没有 scene.pkg(请改用 httpSource/fileSource/bytesSource)");
11945
+ },
11946
+ // type 为 null 时也如实返回:resolveMountConfig 会再按 mediaEntry 的 URL
11947
+ // 嗅探一次(含 HEAD 兜底),仍认不出才落回 scene 并报错
11948
+ async project() {
11949
+ return type ? { type } : null;
11950
+ },
11951
+ async mediaEntry(signal) {
11952
+ if (!type && !isBlob && !headTried) {
11953
+ headTried = true;
11954
+ type = await sniffMediaTypeByHead(urlOrFile, void 0, signal);
11955
+ }
11956
+ return { url: url(), type: type ?? void 0 };
11957
+ },
11958
+ dispose() {
11959
+ if (objectUrl) {
11960
+ URL.revokeObjectURL(objectUrl);
11961
+ objectUrl = null;
11962
+ }
11963
+ }
11964
+ };
11965
+ }
11701
11966
  const LOOP_PREROLL_SEC = 0.5;
11702
11967
  const LOOP_HOLD_SEC = 0.04;
11703
11968
  const LOOP_SWAP_EPS = 0.08;
@@ -12978,9 +13243,10 @@ vec3 DecompressNormal(vec4 tex) {
12978
13243
  const audioDriverRef = {
12979
13244
  current: null
12980
13245
  };
12981
- let mediaDriver = simMedia;
13246
+ let liveMediaOverride = null;
13247
+ const currentMediaDriver = () => liveMediaOverride ?? rt.mediaSource ?? simMedia;
12982
13248
  let windowDriver = simWindow;
12983
- const audioSim = { enabled: supportsAudioProcessing };
13249
+ const audioSim = { enabled: supportsAudioProcessing && !rt.audioDisabled };
12984
13250
  const zero = (n) => new Float32Array(n);
12985
13251
  const SILENT_AUDIO = {
12986
13252
  left16: zero(16),
@@ -13079,7 +13345,7 @@ vec3 DecompressNormal(vec4 tex) {
13079
13345
  const shortcuts = system.createShortcutHandler((name) => {
13080
13346
  reportDiag(rt, cfg, `openUserShortcut: ${name}`);
13081
13347
  });
13082
- const mediaSim = { enabled: true, override: null };
13348
+ const mediaSim = { enabled: !rt.mediaDisabled, override: null };
13083
13349
  const mediaHooks = [];
13084
13350
  let lastMediaSnap = null;
13085
13351
  liveHold.lastSnap = {
@@ -13088,7 +13354,7 @@ vec3 DecompressNormal(vec4 tex) {
13088
13354
  if (lastMediaSnap) lastMediaSnap.hasThumbnail = v;
13089
13355
  }
13090
13356
  };
13091
- const mediaSnapshot = () => mediaDriver.snapshot;
13357
+ const mediaSnapshot = () => currentMediaDriver().snapshot;
13092
13358
  const registerMediaHook = (sb) => {
13093
13359
  if (!sb || !sb.hasMediaHook || mediaHooks.includes(sb)) return;
13094
13360
  mediaHooks.push(sb);
@@ -13133,36 +13399,30 @@ vec3 DecompressNormal(vec4 tex) {
13133
13399
  }
13134
13400
  lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
13135
13401
  };
13402
+ const callDriver = (name) => {
13403
+ const drv = currentMediaDriver();
13404
+ const fn = drv?.[name];
13405
+ if (typeof fn === "function") {
13406
+ try {
13407
+ fn.call(drv);
13408
+ } catch (e) {
13409
+ reportDiag(rt, cfg, `media ${name} 失败: ${e?.message}`);
13410
+ }
13411
+ }
13412
+ dispatchMediaNow();
13413
+ return mediaSnapshot();
13414
+ };
13136
13415
  const mediaControl = {
13137
13416
  get snapshot() {
13138
13417
  return mediaSnapshot();
13139
13418
  },
13140
- skipNext: () => {
13141
- mediaDriver.skipNext();
13142
- dispatchMediaNow();
13143
- return mediaSnapshot();
13144
- },
13145
- skipPrevious: () => {
13146
- mediaDriver.skipPrevious();
13147
- dispatchMediaNow();
13148
- return mediaSnapshot();
13149
- },
13150
- play: () => {
13151
- mediaDriver.play();
13152
- dispatchMediaNow();
13153
- return mediaSnapshot();
13154
- },
13155
- pause: () => {
13156
- mediaDriver.pause();
13157
- dispatchMediaNow();
13158
- return mediaSnapshot();
13159
- },
13160
- playPause: () => {
13161
- mediaDriver.playPause();
13162
- dispatchMediaNow();
13163
- return mediaSnapshot();
13164
- }
13419
+ skipNext: () => callDriver("skipNext"),
13420
+ skipPrevious: () => callDriver("skipPrevious"),
13421
+ play: () => callDriver("play"),
13422
+ pause: () => callDriver("pause"),
13423
+ playPause: () => callDriver("playPause")
13165
13424
  };
13425
+ rt.mediaCtl = mediaControl;
13166
13426
  window.__mediaControl = mediaControl;
13167
13427
  window.__system = {
13168
13428
  media: mediaControl,
@@ -13283,6 +13543,18 @@ vec3 DecompressNormal(vec4 tex) {
13283
13543
  }
13284
13544
  }
13285
13545
  if (cfg.liveSystem) {
13546
+ const liveSlot = {
13547
+ handle: null,
13548
+ dead: false
13549
+ };
13550
+ (rt.wallpaperDisposers ??= []).push(() => {
13551
+ liveSlot.dead = true;
13552
+ try {
13553
+ liveSlot.handle?.dispose();
13554
+ } catch {
13555
+ }
13556
+ liveSlot.handle = null;
13557
+ });
13286
13558
  try {
13287
13559
  const uploadLiveArtwork = async (info) => {
13288
13560
  try {
@@ -13323,7 +13595,7 @@ vec3 DecompressNormal(vec4 tex) {
13323
13595
  generated: true
13324
13596
  });
13325
13597
  }
13326
- const snap = mediaDriver.snapshot;
13598
+ const snap = currentMediaDriver().snapshot;
13327
13599
  if (palette) {
13328
13600
  snap.primaryColor = media.mediaVec3(...palette.primary);
13329
13601
  snap.secondaryColor = media.mediaVec3(...palette.secondary);
@@ -13348,39 +13620,48 @@ vec3 DecompressNormal(vec4 tex) {
13348
13620
  void uploadLiveArtwork(info);
13349
13621
  }
13350
13622
  });
13351
- mediaDriver = live.media;
13352
- windowDriver = live.windowTitle;
13353
- liveHold.mediaDriver = live.media;
13354
- if (live.status().audio === "mic") audioDriverRef.current = live.audio;
13355
- if (mediaDriver.snapshot.hasMedia) {
13356
- for (const { name, event } of media.diffMediaEvents(null, mediaDriver.snapshot)) {
13357
- for (const sb of mediaHooks) {
13358
- try {
13359
- sb.callMedia(name, event);
13360
- } catch {
13623
+ if (liveSlot.dead) {
13624
+ try {
13625
+ live.dispose();
13626
+ } catch {
13627
+ }
13628
+ live = null;
13629
+ } else {
13630
+ liveSlot.handle = live;
13631
+ liveMediaOverride = live.media;
13632
+ windowDriver = live.windowTitle;
13633
+ liveHold.mediaDriver = live.media;
13634
+ if (live.status().audio === "mic") audioDriverRef.current = live.audio;
13635
+ if (currentMediaDriver().snapshot.hasMedia) {
13636
+ for (const { name, event } of media.diffMediaEvents(null, currentMediaDriver().snapshot)) {
13637
+ for (const sb of mediaHooks) {
13638
+ try {
13639
+ sb.callMedia(name, event);
13640
+ } catch {
13641
+ }
13361
13642
  }
13362
13643
  }
13644
+ lastMediaSnap = media.cloneMediaSnapshot(currentMediaDriver().snapshot);
13363
13645
  }
13364
- lastMediaSnap = media.cloneMediaSnapshot(mediaDriver.snapshot);
13646
+ const st = live.status();
13647
+ reportDiag(
13648
+ rt,
13649
+ cfg,
13650
+ `liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
13651
+ );
13652
+ reportDiag(
13653
+ rt,
13654
+ cfg,
13655
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
13656
+ );
13657
+ window.__system = {
13658
+ media: mediaControl,
13659
+ windowTitle: windowDriver.snapshot,
13660
+ shortcuts: shortcuts.last,
13661
+ live: () => live.status()
13662
+ };
13663
+ window.__liveSystem = () => live.status();
13365
13664
  }
13366
- const st = live.status();
13367
- reportDiag(
13368
- rt,
13369
- cfg,
13370
- `liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
13371
- );
13372
- reportDiag(
13373
- rt,
13374
- cfg,
13375
- `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
13376
- );
13377
- window.__system = {
13378
- media: mediaControl,
13379
- windowTitle: windowDriver.snapshot,
13380
- shortcuts: shortcuts.last,
13381
- live: () => live.status()
13382
- };
13383
- window.__liveSystem = () => live.status();
13384
13665
  } catch (e) {
13385
13666
  reportDiag(rt, cfg, `liveSystem: 启动失败,回退模拟源 (${e instanceof Error ? e.message : e})`);
13386
13667
  live = null;
@@ -14712,17 +14993,15 @@ vec3 DecompressNormal(vec4 tex) {
14712
14993
  if (now - lastRender >= interval) {
14713
14994
  lastRender = now;
14714
14995
  markFrame(rt, now);
14715
- if (rt.onFirstFrame) {
14716
- const first = rt.onFirstFrame;
14717
- rt.onFirstFrame = void 0;
14718
- first();
14719
- }
14720
14996
  syncCanvasSize(rt, c, rt.cfg);
14721
14997
  const t = (now - start - pauseAccum) / 1e3;
14722
14998
  inputView.update(pointerSrc.state);
14723
14999
  if (mediaSim.enabled) {
14724
15000
  if (live?.media) live.media.pump();
14725
- else simMedia.update(t);
15001
+ else {
15002
+ const drv = currentMediaDriver();
15003
+ if (typeof drv?.update === "function") drv.update(t);
15004
+ }
14726
15005
  const snap = mediaSnapshot();
14727
15006
  const evts = media.diffMediaEvents(lastMediaSnap, snap);
14728
15007
  if (evts.length) {
@@ -14828,6 +15107,14 @@ vec3 DecompressNormal(vec4 tex) {
14828
15107
  }
14829
15108
  const peek = rt.coverAlign;
14830
15109
  void renderer.render(scene, textures, c.width, c.height, t, normalizeFit(rt.cfg.fit), peek.x, peek.y).then(() => {
15110
+ if (rt.onFirstFrame) {
15111
+ const first = rt.onFirstFrame;
15112
+ rt.onFirstFrame = void 0;
15113
+ try {
15114
+ first();
15115
+ } catch {
15116
+ }
15117
+ }
14831
15118
  if (disposed || rt.paused) return;
14832
15119
  try {
14833
15120
  dispatchCursor();
@@ -15148,6 +15435,44 @@ ${escapeScriptClose(opts.seedScript)}
15148
15435
  }
15149
15436
  };
15150
15437
  }
15438
+ function bridgeAudioDriver(rt) {
15439
+ const left = new Float32Array(64);
15440
+ const right = new Float32Array(64);
15441
+ return {
15442
+ snapshot() {
15443
+ const src = rt.audioBridge?.();
15444
+ const sl = src?.left;
15445
+ const sr = src?.right;
15446
+ const n = sl && sr ? Math.min(64, sl.length, sr.length) : 0;
15447
+ for (let i = 0; i < n; i++) {
15448
+ left[i] = Math.max(0, Math.min(1, Number(sl[i]) || 0));
15449
+ right[i] = Math.max(0, Math.min(1, Number(sr[i]) || 0));
15450
+ }
15451
+ for (let i = n; i < 64; i++) {
15452
+ left[i] = 0;
15453
+ right[i] = 0;
15454
+ }
15455
+ return { left, right };
15456
+ }
15457
+ };
15458
+ }
15459
+ function liveAudioDriver(handle) {
15460
+ const left = new Float32Array(64);
15461
+ const right = new Float32Array(64);
15462
+ return {
15463
+ tick() {
15464
+ handle.audio.pump();
15465
+ },
15466
+ snapshot() {
15467
+ const s = handle.audio.snapshot;
15468
+ for (let i = 0; i < 64; i++) {
15469
+ left[i] = Math.max(0, Math.min(1, Number(s.left64[i]) || 0));
15470
+ right[i] = Math.max(0, Math.min(1, Number(s.right64[i]) || 0));
15471
+ }
15472
+ return { left, right };
15473
+ }
15474
+ };
15475
+ }
15151
15476
  function resolveContainer(rt, cfg) {
15152
15477
  if (rt.wrap) return rt.wrap;
15153
15478
  const el = cfg.canvas;
@@ -15462,8 +15787,10 @@ ${escapeScriptClose(opts.seedScript)}
15462
15787
  }
15463
15788
  return media.cloneMediaSnapshot(snap);
15464
15789
  }
15465
- function startAudioPump(rt, driver, frameClock) {
15790
+ function startAudioPump(rt, driver, frameClock, liveHold = { driver: null }) {
15466
15791
  if (!driver) return;
15792
+ const bridged = bridgeAudioDriver(rt);
15793
+ const pick = () => rt.audioBridge ? bridged : liveHold.driver ?? driver;
15467
15794
  let raf = 0;
15468
15795
  let lastPush = 0;
15469
15796
  const tick = (now) => {
@@ -15475,8 +15802,9 @@ ${escapeScriptClose(opts.seedScript)}
15475
15802
  if (now - lastPush < interval * 0.85) return;
15476
15803
  lastPush = now;
15477
15804
  try {
15478
- driver.tick?.(now);
15479
- const snap = driver.snapshot();
15805
+ const cur = pick();
15806
+ cur.tick?.(now);
15807
+ const snap = cur.snapshot();
15480
15808
  const arr = packWebAudioArrayInto(pumpBuffer, snap.left, snap.right);
15481
15809
  weShimCall(rt, (w) => w.__wePushAudio?.(arr));
15482
15810
  if (frameClock && now - frameClock.last > 200) markFrame(rt, now);
@@ -15495,6 +15823,7 @@ ${escapeScriptClose(opts.seedScript)}
15495
15823
  }
15496
15824
  function startMediaPump(rt, driver) {
15497
15825
  if (!driver) return;
15826
+ const pick = () => rt.mediaSource ?? driver;
15498
15827
  let raf = 0;
15499
15828
  let lastMedia = null;
15500
15829
  let lastTick = 0;
@@ -15504,8 +15833,9 @@ ${escapeScriptClose(opts.seedScript)}
15504
15833
  if (now - lastTick < 200) return;
15505
15834
  lastTick = now;
15506
15835
  try {
15507
- driver.update(now / 1e3);
15508
- lastMedia = pushMediaDiff(rt, lastMedia, driver.snapshot);
15836
+ const cur = pick();
15837
+ cur.update?.(now / 1e3);
15838
+ lastMedia = pushMediaDiff(rt, lastMedia, cur.snapshot);
15509
15839
  } catch {
15510
15840
  }
15511
15841
  };
@@ -15600,18 +15930,65 @@ ${escapeScriptClose(opts.seedScript)}
15600
15930
  }
15601
15931
  installWebCtl(rt);
15602
15932
  const cfgExt = cfg;
15603
- const audioDriver = cfgExt._webAudio === null ? null : cfgExt._webAudio ?? defaultAudioDriver();
15604
- const mediaDriver = cfgExt._webMedia === null ? null : cfgExt._webMedia ?? defaultMediaDriver();
15933
+ const audioDriver = cfgExt._webAudio === null || rt.audioDisabled ? null : cfgExt._webAudio ?? defaultAudioDriver();
15934
+ const mediaDriver = cfgExt._webMedia === null || rt.mediaDisabled ? null : cfgExt._webMedia ?? defaultMediaDriver();
15605
15935
  const finishBare = (why) => {
15606
15936
  reportDiag(rt, cfg, `网页壁纸 shim 注入失败(${why}),退回裸 iframe`);
15607
15937
  attachIframe(rt, cfg, container, entry, { injected: false });
15608
15938
  startAudioPump(rt, null);
15609
15939
  startMediaPump(rt, null);
15940
+ let beat = 0;
15941
+ const tick = (now) => {
15942
+ if (!rt.iframe) return;
15943
+ beat = requestAnimationFrame(tick);
15944
+ if (rt.paused) return;
15945
+ markFrame(rt, now);
15946
+ };
15947
+ beat = requestAnimationFrame(tick);
15948
+ (rt.wallpaperDisposers ??= []).push(() => cancelAnimationFrame(beat));
15610
15949
  };
15611
15950
  const frameClock = { last: 0 };
15951
+ const liveHold = { driver: null };
15612
15952
  const startPumps = () => {
15613
- startAudioPump(rt, audioDriver, frameClock);
15953
+ startAudioPump(rt, audioDriver, frameClock, liveHold);
15614
15954
  startMediaPump(rt, mediaDriver);
15955
+ if (cfg.liveSystem && audioDriver) {
15956
+ const liveSlot = {
15957
+ handle: null,
15958
+ dead: false
15959
+ };
15960
+ (rt.wallpaperDisposers ??= []).push(() => {
15961
+ liveSlot.dead = true;
15962
+ liveHold.driver = null;
15963
+ try {
15964
+ liveSlot.handle?.dispose();
15965
+ } catch {
15966
+ }
15967
+ liveSlot.handle = null;
15968
+ });
15969
+ void (async () => {
15970
+ try {
15971
+ const live = await startLiveSystem({ origin: location.origin });
15972
+ if (liveSlot.dead) {
15973
+ try {
15974
+ live.dispose();
15975
+ } catch {
15976
+ }
15977
+ return;
15978
+ }
15979
+ liveSlot.handle = live;
15980
+ const st = live.status();
15981
+ if (st.audio === "mic") {
15982
+ liveHold.driver = liveAudioDriver(live);
15983
+ reportDiag(rt, cfg, "liveSystem: 网页壁纸音频改用麦克风");
15984
+ } else {
15985
+ reportDiag(rt, cfg, `liveSystem: 麦克风不可用(${st.audio}),网页壁纸沿用模拟源`);
15986
+ }
15987
+ } catch (e) {
15988
+ reportDiag(rt, cfg, `liveSystem: 启动失败,网页壁纸沿用模拟源 (${e?.message ?? e})`);
15989
+ }
15990
+ })();
15991
+ }
15615
15992
  };
15616
15993
  void (async () => {
15617
15994
  const defaults = await fetchProjectWire(entry);
@@ -15678,6 +16055,99 @@ ${escapeScriptClose(opts.seedScript)}
15678
16055
  rt.onUnhandledType?.(cfg);
15679
16056
  }
15680
16057
  }
16058
+ class Color {
16059
+ x;
16060
+ y;
16061
+ z;
16062
+ constructor(x, y, z) {
16063
+ this.x = Number(x) || 0;
16064
+ this.y = Number(y) || 0;
16065
+ this.z = Number(z) || 0;
16066
+ }
16067
+ add(o) {
16068
+ return new Color(this.x + o.x, this.y + o.y, this.z + o.z);
16069
+ }
16070
+ subtract(o) {
16071
+ return new Color(this.x - o.x, this.y - o.y, this.z - o.z);
16072
+ }
16073
+ multiply(k) {
16074
+ if (typeof k === "number") return new Color(this.x * k, this.y * k, this.z * k);
16075
+ return new Color(this.x * k.x, this.y * k.y, this.z * k.z);
16076
+ }
16077
+ toString() {
16078
+ return `${this.x} ${this.y} ${this.z}`;
16079
+ }
16080
+ }
16081
+ function mediaColor(r, g, b) {
16082
+ if (r instanceof Color) return r;
16083
+ if (Array.isArray(r)) return new Color(r[0] ?? 0, r[1] ?? 0, r[2] ?? 0);
16084
+ if (typeof r === "object" && r !== null) {
16085
+ const o = r;
16086
+ return new Color(o.x ?? 0, o.y ?? 0, o.z ?? 0);
16087
+ }
16088
+ return new Color(r, g ?? 0, b ?? 0);
16089
+ }
16090
+ const DEF_PRIMARY = [0.35, 0.38, 0.45];
16091
+ const DEF_SECONDARY = [0.12, 0.13, 0.17];
16092
+ const DEF_TERTIARY = [0.72, 0.76, 0.84];
16093
+ const DEF_TEXT = [0.95, 0.96, 0.98];
16094
+ function locateLyric(lyrics, position) {
16095
+ if (!Array.isArray(lyrics) || lyrics.length === 0) return { line: "", index: -1 };
16096
+ let idx = -1;
16097
+ for (let i = 0; i < lyrics.length; i++) {
16098
+ const at = Number(lyrics[i]?.[0]);
16099
+ if (Number.isFinite(at) && at <= position) idx = i;
16100
+ else break;
16101
+ }
16102
+ return { line: idx >= 0 ? String(lyrics[idx][1] ?? "") : "", index: idx };
16103
+ }
16104
+ function buildSnapshot(init) {
16105
+ const position = Number(init.position) || 0;
16106
+ const lyrics = Array.isArray(init.lyrics) ? init.lyrics : [];
16107
+ const { line, index } = locateLyric(lyrics, position);
16108
+ const state = init.state !== void 0 ? init.state : init.playing === false ? 2 : init.playing ? 1 : 0;
16109
+ return {
16110
+ hasMedia: init.hasMedia ?? (init.title != null || init.artist != null || !!init.playing),
16111
+ state,
16112
+ title: String(init.title ?? ""),
16113
+ artist: String(init.artist ?? ""),
16114
+ album: String(init.album ?? ""),
16115
+ albumArtist: String(init.albumArtist ?? init.artist ?? ""),
16116
+ position,
16117
+ duration: Number(init.duration) || 0,
16118
+ hasThumbnail: init.hasThumbnail ?? false,
16119
+ primaryColor: mediaColor(init.primaryColor ?? DEF_PRIMARY),
16120
+ secondaryColor: mediaColor(init.secondaryColor ?? DEF_SECONDARY),
16121
+ tertiaryColor: mediaColor(init.tertiaryColor ?? DEF_TERTIARY),
16122
+ textColor: mediaColor(init.textColor ?? DEF_TEXT),
16123
+ highContrastColor: mediaColor(init.highContrastColor ?? init.textColor ?? DEF_TEXT),
16124
+ trackIndex: Number(init.trackIndex) || 0,
16125
+ lyrics,
16126
+ lyricLine: line,
16127
+ lyricIndex: index
16128
+ };
16129
+ }
16130
+ function createMediaSource(init = {}, controls = {}) {
16131
+ let cur = { ...init };
16132
+ let snap = buildSnapshot(cur);
16133
+ return {
16134
+ get snapshot() {
16135
+ return snap;
16136
+ },
16137
+ set(patch) {
16138
+ cur = { ...cur, ...patch };
16139
+ snap = buildSnapshot(cur);
16140
+ },
16141
+ // 宿主的快照由外部事件驱动,不需要按帧自行推进;留空实现满足接口即可
16142
+ update() {
16143
+ },
16144
+ skipNext: controls.skipNext,
16145
+ skipPrevious: controls.skipPrevious,
16146
+ play: controls.play,
16147
+ pause: controls.pause,
16148
+ playPause: controls.playPause
16149
+ };
16150
+ }
15681
16151
  function normalizeFitOption(fit) {
15682
16152
  if (fit === "fit") return "contain";
15683
16153
  if (fit === "fill") return "cover";
@@ -15687,6 +16157,13 @@ ${escapeScriptClose(opts.seedScript)}
15687
16157
  const t = project?.type;
15688
16158
  return typeof t === "string" && t.toLowerCase() === "web";
15689
16159
  }
16160
+ const MEDIA_TYPES = /* @__PURE__ */ new Set(["video", "gif", "image"]);
16161
+ function mediaProjectType(project) {
16162
+ const t = project?.type;
16163
+ if (typeof t !== "string") return null;
16164
+ const lower = t.toLowerCase();
16165
+ return MEDIA_TYPES.has(lower) ? lower : null;
16166
+ }
15690
16167
  function ensureSceneCanvas(el) {
15691
16168
  if (el instanceof HTMLCanvasElement) return el;
15692
16169
  const existing = el.querySelector(":scope > canvas[data-webwallgl]");
@@ -15729,6 +16206,47 @@ ${escapeScriptClose(opts.seedScript)}
15729
16206
  if (!url) throw new Error("网页壁纸:无法解析入口 URL(需要 Source.webEntry 或 httpSource)");
15730
16207
  return { ...base, type: "web", src: url, source: o.source };
15731
16208
  }
16209
+ const mediaType = mediaProjectType(project);
16210
+ if (mediaType) {
16211
+ let url;
16212
+ let entryType;
16213
+ try {
16214
+ const entry = await o.source.mediaEntry?.();
16215
+ url = entry?.url;
16216
+ entryType = entry?.type;
16217
+ } catch {
16218
+ url = void 0;
16219
+ }
16220
+ if (!url && o.source.key) {
16221
+ const file = project && typeof project.file === "string" ? String(project.file).trim().replace(/^\/+/, "") : "";
16222
+ if (file) url = `${o.source.key.replace(/\/+$/, "")}/${file}`;
16223
+ }
16224
+ if (!url) {
16225
+ throw new Error(
16226
+ `媒体壁纸(${mediaType}):无法解析资源 URL(需要 Source.mediaEntry 或 project.file + httpSource)`
16227
+ );
16228
+ }
16229
+ const canvas2 = ensureSceneCanvas(el);
16230
+ const finalType = MEDIA_TYPES.has(String(entryType).toLowerCase()) ? String(entryType).toLowerCase() : mediaType;
16231
+ return { ...base, type: finalType, src: url, canvas: canvas2, source: o.source };
16232
+ }
16233
+ const declaredType = typeof project?.type === "string" ? String(project.type).trim() : "";
16234
+ if (!declaredType && typeof o.source.mediaEntry === "function") {
16235
+ let url;
16236
+ let entryType;
16237
+ try {
16238
+ const entry = await o.source.mediaEntry();
16239
+ url = entry?.url;
16240
+ entryType = entry?.type;
16241
+ } catch {
16242
+ url = void 0;
16243
+ }
16244
+ const sniffed = (MEDIA_TYPES.has(String(entryType).toLowerCase()) ? String(entryType).toLowerCase() : null) ?? (url ? sniffMediaType(url) : null);
16245
+ if (url && sniffed) {
16246
+ const canvas2 = ensureSceneCanvas(el);
16247
+ return { ...base, type: sniffed, src: url, canvas: canvas2, source: o.source };
16248
+ }
16249
+ }
15732
16250
  const canvas = ensureSceneCanvas(el);
15733
16251
  return { ...base, type: "scene", canvas, source: o.source };
15734
16252
  }
@@ -15768,6 +16286,26 @@ ${escapeScriptClose(opts.seedScript)}
15768
16286
  rt.onSceneInfo = (info) => {
15769
16287
  rt.info = info;
15770
16288
  };
16289
+ if ("audio" in o) applyAudio(o.audio ?? null);
16290
+ if ("media" in o) {
16291
+ rt.mediaSource = o.media ?? null;
16292
+ rt.mediaDisabled = o.media === null;
16293
+ }
16294
+ };
16295
+ const applyAudio = (src) => {
16296
+ rt.audioDisabled = src === null;
16297
+ if (!src) {
16298
+ rt.audioBridge = null;
16299
+ return;
16300
+ }
16301
+ rt.audioBridge = () => {
16302
+ try {
16303
+ const s = src.snapshot();
16304
+ return s && s.left && s.right ? s : null;
16305
+ } catch {
16306
+ return null;
16307
+ }
16308
+ };
15771
16309
  };
15772
16310
  const armFirstFrame = () => {
15773
16311
  return new Promise((resolve) => {
@@ -15824,6 +16362,7 @@ ${escapeScriptClose(opts.seedScript)}
15824
16362
  setFit(fit) {
15825
16363
  rt.cfg.fit = fit;
15826
16364
  resetCoverAlign(rt);
16365
+ rt.webRelayout?.();
15827
16366
  },
15828
16367
  setFps(fps) {
15829
16368
  rt.cfg.sceneFps = fps;
@@ -15847,11 +16386,81 @@ ${escapeScriptClose(opts.seedScript)}
15847
16386
  getProperties() {
15848
16387
  return { ...rt.liveUserProps ?? {} };
15849
16388
  },
16389
+ // 宿主频谱源。只存引用,实际每帧拉取在 scene-mount 的渲染循环里。
16390
+ // 与 __wp.setAudioBridge 同纪律:换场景不清空,装一次对之后所有场景生效
16391
+ // (wireOptions 也只在选项里出现 audio 时才覆盖,见那里的说明)。
16392
+ setAudio(src) {
16393
+ currentOptions = { ...currentOptions, audio: src };
16394
+ applyAudio(src);
16395
+ rt.audioDisabled = false;
16396
+ },
16397
+ // 系统媒体源。与 setAudio 同纪律:只存引用、换场景不清空,
16398
+ // scene 与 web 两条装配路径读同一个 rt.mediaSource。
16399
+ // 注意语义与 MountOptions.media:null 不同:这里的 null 按文档是
16400
+ // 「回落内置模拟源」,不是禁用(禁用只在挂载选项里表达)。
16401
+ setMedia(src) {
16402
+ currentOptions = { ...currentOptions, media: src };
16403
+ rt.mediaSource = src ?? null;
16404
+ rt.mediaDisabled = false;
16405
+ },
16406
+ // 媒体控制面。装配后由 mountScene 写入 rt.mediaCtl;未装配(或媒体/网页
16407
+ // 壁纸尚无控制面)时给一个惰性替身,读快照得空、控制方法静默无效 ——
16408
+ // 让调用方能无条件 `wp.media.playPause()` 而不必先判空。
16409
+ get media() {
16410
+ const ctl = rt.mediaCtl;
16411
+ if (ctl) return ctl;
16412
+ const empty = {
16413
+ hasMedia: false,
16414
+ state: 0,
16415
+ title: "",
16416
+ artist: "",
16417
+ album: "",
16418
+ albumArtist: "",
16419
+ position: 0,
16420
+ duration: 0,
16421
+ hasThumbnail: false,
16422
+ primaryColor: mediaColor(0, 0, 0),
16423
+ secondaryColor: mediaColor(0, 0, 0),
16424
+ tertiaryColor: mediaColor(0, 0, 0),
16425
+ textColor: mediaColor(1, 1, 1),
16426
+ highContrastColor: mediaColor(1, 1, 1),
16427
+ trackIndex: 0,
16428
+ lyrics: [],
16429
+ lyricLine: "",
16430
+ lyricIndex: -1
16431
+ };
16432
+ const noop = () => empty;
16433
+ return {
16434
+ get snapshot() {
16435
+ return empty;
16436
+ },
16437
+ skipNext: noop,
16438
+ skipPrevious: noop,
16439
+ play: noop,
16440
+ pause: noop,
16441
+ playPause: noop
16442
+ };
16443
+ },
16444
+ // 外部指针注入。pointerCtl 由 mountScene / mountWeb 各自装配时设置,
16445
+ // 媒体壁纸不设 —— 那时这里静默无效,与整页渲染器的 __wp.pushPointer 一致。
16446
+ pushPointer(u, v, buttons) {
16447
+ rt.pointerCtl?.push({ u, v, buttons });
16448
+ },
16449
+ pointerLeave() {
16450
+ rt.pointerCtl?.leave();
16451
+ },
15850
16452
  async load(source) {
16453
+ const prev = currentOptions.source;
16454
+ if (prev && prev !== source) {
16455
+ try {
16456
+ prev.dispose?.();
16457
+ } catch {
16458
+ }
16459
+ }
15851
16460
  currentOptions = { ...currentOptions, source };
15852
16461
  wireOptions(currentOptions);
15853
16462
  const cfg = await resolveMountConfig(boundEl, currentOptions);
15854
- if (cfg.type === "scene" && cfg.canvas instanceof HTMLCanvasElement) {
16463
+ if (cfg.type !== "web" && cfg.canvas instanceof HTMLCanvasElement) {
15855
16464
  boundEl = cfg.canvas;
15856
16465
  }
15857
16466
  rt.cfg = cfg;
@@ -15878,6 +16487,10 @@ ${escapeScriptClose(opts.seedScript)}
15878
16487
  },
15879
16488
  destroy() {
15880
16489
  destroyRuntime(rt);
16490
+ try {
16491
+ currentOptions.source?.dispose?.();
16492
+ } catch {
16493
+ }
15881
16494
  rt.onDiagnostic = void 0;
15882
16495
  rt.onError = void 0;
15883
16496
  rt.onFirstFrame = void 0;
@@ -15905,11 +16518,11 @@ ${escapeScriptClose(opts.seedScript)}
15905
16518
  currentOptions = o;
15906
16519
  wireOptions(o);
15907
16520
  const cfg = await resolveMountConfig(el, o);
15908
- if (cfg.type === "scene" && cfg.canvas instanceof HTMLCanvasElement) {
16521
+ if (cfg.type !== "web" && cfg.canvas instanceof HTMLCanvasElement) {
15909
16522
  boundEl = cfg.canvas;
15910
16523
  }
15911
16524
  rt.cfg = cfg;
15912
- rt.paused = o.autoplay === false;
16525
+ rt.paused = false;
15913
16526
  rt.info = void 0;
15914
16527
  resetCoverAlign(rt);
15915
16528
  if (o.properties && Object.keys(o.properties).length) {
@@ -15923,6 +16536,7 @@ ${escapeScriptClose(opts.seedScript)}
15923
16536
  } finally {
15924
16537
  failure.off();
15925
16538
  }
16539
+ if (o.autoplay === false) instance.pause();
15926
16540
  if ((o.volume ?? 0) > 0) instance.setVolume(o.volume);
15927
16541
  if (o.properties && Object.keys(o.properties).length) instance.setProperties(o.properties);
15928
16542
  };
@@ -15936,10 +16550,14 @@ ${escapeScriptClose(opts.seedScript)}
15936
16550
  return instance;
15937
16551
  }
15938
16552
  exports2.bytesSource = bytesSource;
16553
+ exports2.createMediaSource = createMediaSource;
15939
16554
  exports2.createScene = createScene;
15940
16555
  exports2.fileSource = fileSource;
15941
16556
  exports2.httpSource = httpSource;
16557
+ exports2.mediaColor = mediaColor;
16558
+ exports2.mediaSource = mediaSource;
15942
16559
  exports2.mount = mount;
16560
+ exports2.sniffMediaType = sniffMediaType;
15943
16561
  Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" });
15944
16562
  });
15945
16563
  //# sourceMappingURL=webwallgl.global.js.map