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.
package/webwallgl.mjs CHANGED
@@ -47,10 +47,24 @@ function clear(rt) {
47
47
  rt.raf = void 0;
48
48
  for (const p of rt.videoPairs ?? []) p.destroy();
49
49
  rt.videoPairs = void 0;
50
- if (rt.sceneCleanup) rt.sceneCleanup();
50
+ if (rt.sceneCleanup) {
51
+ try {
52
+ rt.sceneCleanup();
53
+ } catch {
54
+ }
55
+ }
51
56
  rt.sceneCleanup = void 0;
52
57
  rt.sceneCtl = void 0;
53
58
  rt.pointerCtl = void 0;
59
+ rt.mediaCtl = void 0;
60
+ const ds = rt.wallpaperDisposers ?? [];
61
+ rt.wallpaperDisposers = [];
62
+ for (const d of ds) {
63
+ try {
64
+ d();
65
+ } catch {
66
+ }
67
+ }
54
68
  if (rt.renderer) {
55
69
  rt.renderer.dispose?.();
56
70
  rt.renderer = void 0;
@@ -92,10 +106,18 @@ function clear(rt) {
92
106
  rt.canvas = void 0;
93
107
  rt.ctx = void 0;
94
108
  rt.info = void 0;
109
+ clearSceneDebugGlobals();
95
110
  resetFrameMeter(rt);
96
111
  }
97
112
  function destroyRuntime(rt) {
98
- clear(rt);
113
+ try {
114
+ clear(rt);
115
+ } catch {
116
+ }
117
+ if (rt.peekRaf !== void 0) {
118
+ cancelAnimationFrame(rt.peekRaf);
119
+ rt.peekRaf = void 0;
120
+ }
99
121
  for (const off of rt.disposers.splice(0)) {
100
122
  try {
101
123
  off();
@@ -151,6 +173,32 @@ function applyCoverAlignToDom(rt) {
151
173
  const obj = rt.video ?? rt.img;
152
174
  if (obj && obj.isConnected) obj.style.objectPosition = pos;
153
175
  }
176
+ const SCENE_DEBUG_GLOBALS = [
177
+ "__scene",
178
+ "__sceneLayers",
179
+ "__textures",
180
+ "__objScripts",
181
+ "__textWidgets",
182
+ "__mediaHooks",
183
+ "__mediaControl",
184
+ "__mediaStats",
185
+ "__mediaSet",
186
+ "__system",
187
+ "__liveSystem",
188
+ "__audioStats",
189
+ "__audioMute",
190
+ "__particleStats",
191
+ "__particleToggle",
192
+ "__pointerStats",
193
+ "__compositeStats",
194
+ "__compositeEnable"
195
+ ];
196
+ function clearSceneDebugGlobals() {
197
+ const w = window;
198
+ for (const k of SCENE_DEBUG_GLOBALS) {
199
+ if (k in w) delete w[k];
200
+ }
201
+ }
154
202
  function syncCanvasSize(rt, canvas, cfg) {
155
203
  const dpr = effectiveDpr(rt, cfg);
156
204
  const w = Math.max(1, Math.round((canvas.clientWidth || window.innerWidth || 1) * dpr));
@@ -3539,6 +3587,11 @@ function rewriteXrayFragScale(fragGlsl) {
3539
3587
  const from = usesVarying ? /unprojectedUVs\s*\*=\s*v_PointerScale\s*\*/ : /unprojectedUVs\s*\*=\s*g_PointerScale\s*\*/;
3540
3588
  return out.replace(from, "unprojectedUVs *= " + XRAY_FRAG_UV_SCALE + " *");
3541
3589
  }
3590
+ const XRAY_SIZE_FALLBACK = 1;
3591
+ function constantFallback(uniformName, declaredDefault) {
3592
+ if (uniformName === "g_PointerScale") return XRAY_SIZE_FALLBACK;
3593
+ return declaredDefault;
3594
+ }
3542
3595
  function createRenderer(canvas, opts = {}) {
3543
3596
  const gl = canvas.getContext("webgl2", { premultipliedAlpha: false, antialias: false, alpha: false, preserveDrawingBuffer: true });
3544
3597
  if (!gl) throw new Error("当前浏览器不支持 WebGL2");
@@ -3895,7 +3948,8 @@ function createRenderer(canvas, opts = {}) {
3895
3948
  }
3896
3949
  for (const [matKey, entry] of Object.entries(matMeta || {})) {
3897
3950
  if (!constants || !(matKey in constants)) {
3898
- if (entry.default !== void 0) setConstant(uni, entry.uniform, entry.default);
3951
+ const dflt = constantFallback(entry.uniform, entry.default);
3952
+ if (dflt !== void 0) setConstant(uni, entry.uniform, dflt);
3899
3953
  }
3900
3954
  }
3901
3955
  }
@@ -5114,9 +5168,11 @@ const rndMod = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProper
5114
5168
  __proto__: null,
5115
5169
  ALIGN,
5116
5170
  XRAY_IDLE_SCREEN_UV,
5171
+ XRAY_SIZE_FALLBACK,
5117
5172
  applyColorBlendCPU,
5118
5173
  collectGroupDescendantIds,
5119
5174
  colorBlendPlan,
5175
+ constantFallback,
5120
5176
  createRenderer,
5121
5177
  effectFboSize,
5122
5178
  layerWantsPreserveBackdrop,
@@ -11340,19 +11396,84 @@ async function decodeGifFrames(src) {
11340
11396
  dec.close?.();
11341
11397
  return { width, height, frames };
11342
11398
  }
11399
+ function attachVideoSpectrum(rt, cfg, v) {
11400
+ if (rt.audioBridge) return;
11401
+ const AC = window.AudioContext || window.webkitAudioContext;
11402
+ if (!AC) return;
11403
+ let ctx;
11404
+ let analyser;
11405
+ try {
11406
+ ctx = new AC();
11407
+ const srcNode = ctx.createMediaElementSource(v);
11408
+ analyser = ctx.createAnalyser();
11409
+ analyser.fftSize = 256;
11410
+ analyser.smoothingTimeConstant = 0.75;
11411
+ srcNode.connect(analyser);
11412
+ analyser.connect(ctx.destination);
11413
+ } catch (e) {
11414
+ reportDiag(rt, cfg, `media 频谱接管跳过: ${e?.message ?? e}`);
11415
+ return;
11416
+ }
11417
+ const bins = new Uint8Array(analyser.frequencyBinCount);
11418
+ const left = new Float32Array(64);
11419
+ const right = new Float32Array(64);
11420
+ const bridge = () => {
11421
+ if (ctx.state !== "running") return null;
11422
+ analyser.getByteFrequencyData(bins);
11423
+ const n = Math.min(64, bins.length);
11424
+ for (let i = 0; i < n; i++) {
11425
+ const x = bins[i] / 255;
11426
+ left[i] = x;
11427
+ right[i] = x;
11428
+ }
11429
+ for (let i = n; i < 64; i++) {
11430
+ left[i] = 0;
11431
+ right[i] = 0;
11432
+ }
11433
+ return { left, right };
11434
+ };
11435
+ rt.audioBridge = bridge;
11436
+ if (ctx.state === "suspended") {
11437
+ const kick = () => {
11438
+ void ctx.resume().catch(() => {
11439
+ });
11440
+ window.removeEventListener("pointerdown", kick);
11441
+ window.removeEventListener("keydown", kick);
11442
+ };
11443
+ window.addEventListener("pointerdown", kick, { once: true });
11444
+ window.addEventListener("keydown", kick, { once: true });
11445
+ (rt.wallpaperDisposers ??= []).push(() => {
11446
+ window.removeEventListener("pointerdown", kick);
11447
+ window.removeEventListener("keydown", kick);
11448
+ });
11449
+ }
11450
+ (rt.wallpaperDisposers ??= []).push(() => {
11451
+ if (rt.audioBridge === bridge) rt.audioBridge = null;
11452
+ void ctx.close().catch(() => {
11453
+ });
11454
+ });
11455
+ }
11343
11456
  function mountMedia(rt, cfg) {
11344
11457
  clear(rt);
11345
- if (!cfg.src) {
11458
+ const failHard = (why) => {
11459
+ reportDiag(rt, cfg, `media ${cfg.type} 失败: ${why}`);
11460
+ rt.onError?.(new Error(`媒体壁纸(${cfg.type})${why}`));
11346
11461
  rt.fallbackPage?.();
11462
+ };
11463
+ if (!cfg.src) {
11464
+ failHard("缺少资源 URL(cfg.src 为空)");
11347
11465
  return;
11348
11466
  }
11349
11467
  const isVideo = cfg.type === "video";
11350
11468
  const isGif = cfg.type === "gif";
11351
- const c = document.createElement("canvas");
11469
+ const embedded = cfg.canvas instanceof HTMLCanvasElement;
11470
+ const c = embedded ? cfg.canvas : document.createElement("canvas");
11352
11471
  const dpr = effectiveDpr(rt, cfg);
11353
- c.width = Math.max(1, Math.round(innerWidth * dpr));
11354
- c.height = Math.max(1, Math.round(innerHeight * dpr));
11355
- c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
11472
+ const vw = c.clientWidth || window.innerWidth || 1;
11473
+ const vh = c.clientHeight || window.innerHeight || 1;
11474
+ c.width = Math.max(1, Math.round(vw * dpr));
11475
+ c.height = Math.max(1, Math.round(vh * dpr));
11476
+ if (!embedded) c.style.cssText = "position:absolute;inset:0;width:100%;height:100%;";
11356
11477
  const gl2 = c.getContext("webgl2", {
11357
11478
  premultipliedAlpha: false,
11358
11479
  antialias: false,
@@ -11360,13 +11481,32 @@ function mountMedia(rt, cfg) {
11360
11481
  preserveDrawingBuffer: true
11361
11482
  });
11362
11483
  if (!gl2) {
11363
- reportDiag(rt, cfg, `media ${cfg.type}: WEBGL2_UNAVAILABLE,回退 DOM 渲染`);
11484
+ reportDiag(rt, cfg, `media ${cfg.type}: WEBGL2_UNAVAILABLE`);
11485
+ if (embedded) {
11486
+ rt.onError?.(new Error("WEBGL2_UNAVAILABLE"));
11487
+ return;
11488
+ }
11364
11489
  if (isVideo) mountVideoDom(rt, cfg);
11365
11490
  else mountGifDom(rt, cfg);
11366
11491
  return;
11367
11492
  }
11368
- rt.wrap?.appendChild(c);
11493
+ if (!embedded) rt.wrap?.appendChild(c);
11369
11494
  rt.canvas = c;
11495
+ if (rt.onSceneInfo) {
11496
+ const hook = rt.onSceneInfo;
11497
+ rt.onSceneInfo = void 0;
11498
+ try {
11499
+ hook({
11500
+ width: vw,
11501
+ height: vh,
11502
+ layerCount: 0,
11503
+ hasModels: false,
11504
+ hasParticles: false,
11505
+ hasText: false
11506
+ });
11507
+ } catch {
11508
+ }
11509
+ }
11370
11510
  let disposed = false;
11371
11511
  rt.sceneCleanup = () => {
11372
11512
  disposed = true;
@@ -11378,6 +11518,21 @@ function mountMedia(rt, cfg) {
11378
11518
  let pauseImpl;
11379
11519
  let resumeImpl;
11380
11520
  let videoWasPlaying = false;
11521
+ rt.sceneAudio = {
11522
+ setVolume(v) {
11523
+ const vid = rt.video;
11524
+ if (!vid) return;
11525
+ const vol = Math.max(0, Math.min(1, Number(v) || 0));
11526
+ vid.volume = vol;
11527
+ vid.muted = vol <= 0;
11528
+ if (vol > 0 && vid.paused && !rt.paused) {
11529
+ void vid.play().catch((e) => {
11530
+ reportDiag(rt, cfg, `media 取消静音后自动播放被拒绝: ${e?.message ?? e}`);
11531
+ });
11532
+ }
11533
+ },
11534
+ audios: []
11535
+ };
11381
11536
  rt.sceneCtl = {
11382
11537
  pause() {
11383
11538
  pauseImpl?.();
@@ -11391,8 +11546,7 @@ function mountMedia(rt, cfg) {
11391
11546
  const fail = (why) => {
11392
11547
  if (disposed) return;
11393
11548
  disposed = true;
11394
- reportDiag(rt, cfg, `media ${cfg.type} 失败: ${why}`);
11395
- rt.fallbackPage?.();
11549
+ failHard(why);
11396
11550
  };
11397
11551
  void (async () => {
11398
11552
  try {
@@ -11440,6 +11594,7 @@ function mountMedia(rt, cfg) {
11440
11594
  });
11441
11595
  if (!rt.paused) void v.play().catch(() => {
11442
11596
  });
11597
+ attachVideoSpectrum(rt, cfg, v);
11443
11598
  reportDiag(rt, cfg, `media video ${mediaW}x${mediaH} → scene 渲染`);
11444
11599
  } else {
11445
11600
  let decoded = false;
@@ -11539,6 +11694,14 @@ function mountMedia(rt, cfg) {
11539
11694
  peek.x,
11540
11695
  peek.y
11541
11696
  ).then(() => {
11697
+ if (rt.onFirstFrame) {
11698
+ const first = rt.onFirstFrame;
11699
+ rt.onFirstFrame = void 0;
11700
+ try {
11701
+ first();
11702
+ } catch {
11703
+ }
11704
+ }
11542
11705
  if (disposed || rt.paused) return;
11543
11706
  rt.raf = requestAnimationFrame(renderLoop);
11544
11707
  }).catch((e) => fail(String(e.message || e).slice(0, 200)));
@@ -11621,6 +11784,48 @@ function mountGifDom(rt, cfg) {
11621
11784
  rt.img = img;
11622
11785
  }
11623
11786
  const PKG_PATHS = ["scene.pkg", "scenes/scene.pkg", "gifscene.pkg"];
11787
+ const EXT_TYPES = {
11788
+ mp4: "video",
11789
+ webm: "video",
11790
+ mov: "video",
11791
+ m4v: "video",
11792
+ ogv: "video",
11793
+ gif: "gif",
11794
+ png: "image",
11795
+ jpg: "image",
11796
+ jpeg: "image",
11797
+ webp: "image",
11798
+ avif: "image",
11799
+ bmp: "image"
11800
+ };
11801
+ function typeFromMime(mime) {
11802
+ const m = mime.toLowerCase().split(";")[0].trim();
11803
+ if (m === "image/gif") return "gif";
11804
+ if (m.startsWith("video/")) return "video";
11805
+ if (m.startsWith("image/")) return "image";
11806
+ return null;
11807
+ }
11808
+ function sniffMediaType(url) {
11809
+ if (typeof url !== "string" || !url) return null;
11810
+ let path = url;
11811
+ const hash = path.indexOf("#");
11812
+ if (hash >= 0) path = path.slice(0, hash);
11813
+ const q = path.indexOf("?");
11814
+ if (q >= 0) path = path.slice(0, q);
11815
+ const seg = path.split("/").pop() || "";
11816
+ const dot = seg.lastIndexOf(".");
11817
+ if (dot < 0) return null;
11818
+ return EXT_TYPES[seg.slice(dot + 1).toLowerCase()] ?? null;
11819
+ }
11820
+ async function sniffMediaTypeByHead(url, init, signal) {
11821
+ try {
11822
+ const r = await fetch(url, { ...init, method: "HEAD", signal });
11823
+ if (!r.ok) return null;
11824
+ return typeFromMime(r.headers.get("content-type") || "");
11825
+ } catch {
11826
+ return null;
11827
+ }
11828
+ }
11624
11829
  function httpSource(baseUrl, init) {
11625
11830
  const base = baseUrl.replace(/\/+$/, "");
11626
11831
  return {
@@ -11675,6 +11880,29 @@ function httpSource(baseUrl, init) {
11675
11880
  if (signal?.aborted) throw new Error("aborted");
11676
11881
  }
11677
11882
  return { url: `${base}/${file}` };
11883
+ },
11884
+ /**
11885
+ * 媒体壁纸(video/gif/image)的资源地址:`{base}/{project.file}`。
11886
+ *
11887
+ * 与 webEntry 的区别是**没有默认文件名可兜底**:网页壁纸缺 file 时
11888
+ * index.html 是行业惯例,媒体壁纸的文件名(scene.mp4 / xxx.gif)完全由作者定,
11889
+ * 猜一个只会 404。拿不到 file 就返回 null,让 mount 报「无法解析媒体 URL」,
11890
+ * 而不是发一个必然失败的请求、再把那个 404 当成根因写进错误里。
11891
+ */
11892
+ async mediaEntry(signal) {
11893
+ let file = "";
11894
+ try {
11895
+ const r = await fetch(`${base}/project.json`, { ...init, signal });
11896
+ if (r.ok) {
11897
+ const project = await r.json();
11898
+ if (project && typeof project.file === "string" && project.file.trim()) {
11899
+ file = project.file.trim().replace(/^\/+/, "");
11900
+ }
11901
+ }
11902
+ } catch {
11903
+ if (signal?.aborted) throw new Error("aborted");
11904
+ }
11905
+ return file ? { url: `${base}/${file}` } : null;
11678
11906
  }
11679
11907
  };
11680
11908
  }
@@ -11694,6 +11922,43 @@ function bytesSource(pkg2, project, key) {
11694
11922
  project: async () => project ?? null
11695
11923
  };
11696
11924
  }
11925
+ function mediaSource(urlOrFile, options) {
11926
+ const isBlob = typeof urlOrFile !== "string";
11927
+ const named = urlOrFile;
11928
+ let type = options?.type ?? (isBlob ? typeFromMime(urlOrFile.type || "") : null) ?? sniffMediaType(isBlob ? String(named.name ?? "") : urlOrFile);
11929
+ let headTried = false;
11930
+ let objectUrl = null;
11931
+ const url = () => {
11932
+ if (!isBlob) return urlOrFile;
11933
+ if (!objectUrl) objectUrl = URL.createObjectURL(urlOrFile);
11934
+ return objectUrl;
11935
+ };
11936
+ const key = options?.key ?? (isBlob ? typeof named.name === "string" ? `media:${named.name}:${named.size}:${named.lastModified ?? 0}` : void 0 : `media:${urlOrFile}`);
11937
+ return {
11938
+ key,
11939
+ async scenePkg() {
11940
+ throw new Error("mediaSource 是纯媒体来源,没有 scene.pkg(请改用 httpSource/fileSource/bytesSource)");
11941
+ },
11942
+ // type 为 null 时也如实返回:resolveMountConfig 会再按 mediaEntry 的 URL
11943
+ // 嗅探一次(含 HEAD 兜底),仍认不出才落回 scene 并报错
11944
+ async project() {
11945
+ return type ? { type } : null;
11946
+ },
11947
+ async mediaEntry(signal) {
11948
+ if (!type && !isBlob && !headTried) {
11949
+ headTried = true;
11950
+ type = await sniffMediaTypeByHead(urlOrFile, void 0, signal);
11951
+ }
11952
+ return { url: url(), type: type ?? void 0 };
11953
+ },
11954
+ dispose() {
11955
+ if (objectUrl) {
11956
+ URL.revokeObjectURL(objectUrl);
11957
+ objectUrl = null;
11958
+ }
11959
+ }
11960
+ };
11961
+ }
11697
11962
  const LOOP_PREROLL_SEC = 0.5;
11698
11963
  const LOOP_HOLD_SEC = 0.04;
11699
11964
  const LOOP_SWAP_EPS = 0.08;
@@ -12974,9 +13239,10 @@ function mountScene(rt, cfg) {
12974
13239
  const audioDriverRef = {
12975
13240
  current: null
12976
13241
  };
12977
- let mediaDriver = simMedia;
13242
+ let liveMediaOverride = null;
13243
+ const currentMediaDriver = () => liveMediaOverride ?? rt.mediaSource ?? simMedia;
12978
13244
  let windowDriver = simWindow;
12979
- const audioSim = { enabled: supportsAudioProcessing };
13245
+ const audioSim = { enabled: supportsAudioProcessing && !rt.audioDisabled };
12980
13246
  const zero = (n) => new Float32Array(n);
12981
13247
  const SILENT_AUDIO = {
12982
13248
  left16: zero(16),
@@ -13075,7 +13341,7 @@ function mountScene(rt, cfg) {
13075
13341
  const shortcuts = system.createShortcutHandler((name) => {
13076
13342
  reportDiag(rt, cfg, `openUserShortcut: ${name}`);
13077
13343
  });
13078
- const mediaSim = { enabled: true, override: null };
13344
+ const mediaSim = { enabled: !rt.mediaDisabled, override: null };
13079
13345
  const mediaHooks = [];
13080
13346
  let lastMediaSnap = null;
13081
13347
  liveHold.lastSnap = {
@@ -13084,7 +13350,7 @@ function mountScene(rt, cfg) {
13084
13350
  if (lastMediaSnap) lastMediaSnap.hasThumbnail = v;
13085
13351
  }
13086
13352
  };
13087
- const mediaSnapshot = () => mediaDriver.snapshot;
13353
+ const mediaSnapshot = () => currentMediaDriver().snapshot;
13088
13354
  const registerMediaHook = (sb) => {
13089
13355
  if (!sb || !sb.hasMediaHook || mediaHooks.includes(sb)) return;
13090
13356
  mediaHooks.push(sb);
@@ -13129,36 +13395,30 @@ function mountScene(rt, cfg) {
13129
13395
  }
13130
13396
  lastMediaSnap = media.cloneMediaSnapshot(mediaSnapshot());
13131
13397
  };
13398
+ const callDriver = (name) => {
13399
+ const drv = currentMediaDriver();
13400
+ const fn = drv?.[name];
13401
+ if (typeof fn === "function") {
13402
+ try {
13403
+ fn.call(drv);
13404
+ } catch (e) {
13405
+ reportDiag(rt, cfg, `media ${name} 失败: ${e?.message}`);
13406
+ }
13407
+ }
13408
+ dispatchMediaNow();
13409
+ return mediaSnapshot();
13410
+ };
13132
13411
  const mediaControl = {
13133
13412
  get snapshot() {
13134
13413
  return mediaSnapshot();
13135
13414
  },
13136
- skipNext: () => {
13137
- mediaDriver.skipNext();
13138
- dispatchMediaNow();
13139
- return mediaSnapshot();
13140
- },
13141
- skipPrevious: () => {
13142
- mediaDriver.skipPrevious();
13143
- dispatchMediaNow();
13144
- return mediaSnapshot();
13145
- },
13146
- play: () => {
13147
- mediaDriver.play();
13148
- dispatchMediaNow();
13149
- return mediaSnapshot();
13150
- },
13151
- pause: () => {
13152
- mediaDriver.pause();
13153
- dispatchMediaNow();
13154
- return mediaSnapshot();
13155
- },
13156
- playPause: () => {
13157
- mediaDriver.playPause();
13158
- dispatchMediaNow();
13159
- return mediaSnapshot();
13160
- }
13415
+ skipNext: () => callDriver("skipNext"),
13416
+ skipPrevious: () => callDriver("skipPrevious"),
13417
+ play: () => callDriver("play"),
13418
+ pause: () => callDriver("pause"),
13419
+ playPause: () => callDriver("playPause")
13161
13420
  };
13421
+ rt.mediaCtl = mediaControl;
13162
13422
  window.__mediaControl = mediaControl;
13163
13423
  window.__system = {
13164
13424
  media: mediaControl,
@@ -13279,6 +13539,18 @@ function mountScene(rt, cfg) {
13279
13539
  }
13280
13540
  }
13281
13541
  if (cfg.liveSystem) {
13542
+ const liveSlot = {
13543
+ handle: null,
13544
+ dead: false
13545
+ };
13546
+ (rt.wallpaperDisposers ??= []).push(() => {
13547
+ liveSlot.dead = true;
13548
+ try {
13549
+ liveSlot.handle?.dispose();
13550
+ } catch {
13551
+ }
13552
+ liveSlot.handle = null;
13553
+ });
13282
13554
  try {
13283
13555
  const uploadLiveArtwork = async (info) => {
13284
13556
  try {
@@ -13319,7 +13591,7 @@ function mountScene(rt, cfg) {
13319
13591
  generated: true
13320
13592
  });
13321
13593
  }
13322
- const snap = mediaDriver.snapshot;
13594
+ const snap = currentMediaDriver().snapshot;
13323
13595
  if (palette) {
13324
13596
  snap.primaryColor = media.mediaVec3(...palette.primary);
13325
13597
  snap.secondaryColor = media.mediaVec3(...palette.secondary);
@@ -13344,39 +13616,48 @@ function mountScene(rt, cfg) {
13344
13616
  void uploadLiveArtwork(info);
13345
13617
  }
13346
13618
  });
13347
- mediaDriver = live.media;
13348
- windowDriver = live.windowTitle;
13349
- liveHold.mediaDriver = live.media;
13350
- if (live.status().audio === "mic") audioDriverRef.current = live.audio;
13351
- if (mediaDriver.snapshot.hasMedia) {
13352
- for (const { name, event } of media.diffMediaEvents(null, mediaDriver.snapshot)) {
13353
- for (const sb of mediaHooks) {
13354
- try {
13355
- sb.callMedia(name, event);
13356
- } catch {
13619
+ if (liveSlot.dead) {
13620
+ try {
13621
+ live.dispose();
13622
+ } catch {
13623
+ }
13624
+ live = null;
13625
+ } else {
13626
+ liveSlot.handle = live;
13627
+ liveMediaOverride = live.media;
13628
+ windowDriver = live.windowTitle;
13629
+ liveHold.mediaDriver = live.media;
13630
+ if (live.status().audio === "mic") audioDriverRef.current = live.audio;
13631
+ if (currentMediaDriver().snapshot.hasMedia) {
13632
+ for (const { name, event } of media.diffMediaEvents(null, currentMediaDriver().snapshot)) {
13633
+ for (const sb of mediaHooks) {
13634
+ try {
13635
+ sb.callMedia(name, event);
13636
+ } catch {
13637
+ }
13357
13638
  }
13358
13639
  }
13640
+ lastMediaSnap = media.cloneMediaSnapshot(currentMediaDriver().snapshot);
13359
13641
  }
13360
- lastMediaSnap = media.cloneMediaSnapshot(mediaDriver.snapshot);
13642
+ const st = live.status();
13643
+ reportDiag(
13644
+ rt,
13645
+ cfg,
13646
+ `liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
13647
+ );
13648
+ reportDiag(
13649
+ rt,
13650
+ cfg,
13651
+ `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
13652
+ );
13653
+ window.__system = {
13654
+ media: mediaControl,
13655
+ windowTitle: windowDriver.snapshot,
13656
+ shortcuts: shortcuts.last,
13657
+ live: () => live.status()
13658
+ };
13659
+ window.__liveSystem = () => live.status();
13361
13660
  }
13362
- const st = live.status();
13363
- reportDiag(
13364
- rt,
13365
- cfg,
13366
- `liveSystem: audio=${st.audio} media=${st.media} window=${st.window}` + (st.title ? ` title="${st.title}"` : "") + (st.hasArtwork ? " artwork=1" : "")
13367
- );
13368
- reportDiag(
13369
- rt,
13370
- cfg,
13371
- `audio: ${audioDriverRef.current ? "live mic" : "simulated"} stream, supportsaudioprocessing=${supportsAudioProcessing}`
13372
- );
13373
- window.__system = {
13374
- media: mediaControl,
13375
- windowTitle: windowDriver.snapshot,
13376
- shortcuts: shortcuts.last,
13377
- live: () => live.status()
13378
- };
13379
- window.__liveSystem = () => live.status();
13380
13661
  } catch (e) {
13381
13662
  reportDiag(rt, cfg, `liveSystem: 启动失败,回退模拟源 (${e instanceof Error ? e.message : e})`);
13382
13663
  live = null;
@@ -14708,17 +14989,15 @@ function mountScene(rt, cfg) {
14708
14989
  if (now - lastRender >= interval) {
14709
14990
  lastRender = now;
14710
14991
  markFrame(rt, now);
14711
- if (rt.onFirstFrame) {
14712
- const first = rt.onFirstFrame;
14713
- rt.onFirstFrame = void 0;
14714
- first();
14715
- }
14716
14992
  syncCanvasSize(rt, c, rt.cfg);
14717
14993
  const t = (now - start - pauseAccum) / 1e3;
14718
14994
  inputView.update(pointerSrc.state);
14719
14995
  if (mediaSim.enabled) {
14720
14996
  if (live?.media) live.media.pump();
14721
- else simMedia.update(t);
14997
+ else {
14998
+ const drv = currentMediaDriver();
14999
+ if (typeof drv?.update === "function") drv.update(t);
15000
+ }
14722
15001
  const snap = mediaSnapshot();
14723
15002
  const evts = media.diffMediaEvents(lastMediaSnap, snap);
14724
15003
  if (evts.length) {
@@ -14824,6 +15103,14 @@ function mountScene(rt, cfg) {
14824
15103
  }
14825
15104
  const peek = rt.coverAlign;
14826
15105
  void renderer.render(scene, textures, c.width, c.height, t, normalizeFit(rt.cfg.fit), peek.x, peek.y).then(() => {
15106
+ if (rt.onFirstFrame) {
15107
+ const first = rt.onFirstFrame;
15108
+ rt.onFirstFrame = void 0;
15109
+ try {
15110
+ first();
15111
+ } catch {
15112
+ }
15113
+ }
14827
15114
  if (disposed || rt.paused) return;
14828
15115
  try {
14829
15116
  dispatchCursor();
@@ -15144,6 +15431,44 @@ function defaultAudioDriver() {
15144
15431
  }
15145
15432
  };
15146
15433
  }
15434
+ function bridgeAudioDriver(rt) {
15435
+ const left = new Float32Array(64);
15436
+ const right = new Float32Array(64);
15437
+ return {
15438
+ snapshot() {
15439
+ const src = rt.audioBridge?.();
15440
+ const sl = src?.left;
15441
+ const sr = src?.right;
15442
+ const n = sl && sr ? Math.min(64, sl.length, sr.length) : 0;
15443
+ for (let i = 0; i < n; i++) {
15444
+ left[i] = Math.max(0, Math.min(1, Number(sl[i]) || 0));
15445
+ right[i] = Math.max(0, Math.min(1, Number(sr[i]) || 0));
15446
+ }
15447
+ for (let i = n; i < 64; i++) {
15448
+ left[i] = 0;
15449
+ right[i] = 0;
15450
+ }
15451
+ return { left, right };
15452
+ }
15453
+ };
15454
+ }
15455
+ function liveAudioDriver(handle) {
15456
+ const left = new Float32Array(64);
15457
+ const right = new Float32Array(64);
15458
+ return {
15459
+ tick() {
15460
+ handle.audio.pump();
15461
+ },
15462
+ snapshot() {
15463
+ const s = handle.audio.snapshot;
15464
+ for (let i = 0; i < 64; i++) {
15465
+ left[i] = Math.max(0, Math.min(1, Number(s.left64[i]) || 0));
15466
+ right[i] = Math.max(0, Math.min(1, Number(s.right64[i]) || 0));
15467
+ }
15468
+ return { left, right };
15469
+ }
15470
+ };
15471
+ }
15147
15472
  function resolveContainer(rt, cfg) {
15148
15473
  if (rt.wrap) return rt.wrap;
15149
15474
  const el = cfg.canvas;
@@ -15458,8 +15783,10 @@ function pushMediaDiff(rt, prev, snap) {
15458
15783
  }
15459
15784
  return media.cloneMediaSnapshot(snap);
15460
15785
  }
15461
- function startAudioPump(rt, driver, frameClock) {
15786
+ function startAudioPump(rt, driver, frameClock, liveHold = { driver: null }) {
15462
15787
  if (!driver) return;
15788
+ const bridged = bridgeAudioDriver(rt);
15789
+ const pick = () => rt.audioBridge ? bridged : liveHold.driver ?? driver;
15463
15790
  let raf = 0;
15464
15791
  let lastPush = 0;
15465
15792
  const tick = (now) => {
@@ -15471,8 +15798,9 @@ function startAudioPump(rt, driver, frameClock) {
15471
15798
  if (now - lastPush < interval * 0.85) return;
15472
15799
  lastPush = now;
15473
15800
  try {
15474
- driver.tick?.(now);
15475
- const snap = driver.snapshot();
15801
+ const cur = pick();
15802
+ cur.tick?.(now);
15803
+ const snap = cur.snapshot();
15476
15804
  const arr = packWebAudioArrayInto(pumpBuffer, snap.left, snap.right);
15477
15805
  weShimCall(rt, (w) => w.__wePushAudio?.(arr));
15478
15806
  if (frameClock && now - frameClock.last > 200) markFrame(rt, now);
@@ -15491,6 +15819,7 @@ function startAudioPump(rt, driver, frameClock) {
15491
15819
  }
15492
15820
  function startMediaPump(rt, driver) {
15493
15821
  if (!driver) return;
15822
+ const pick = () => rt.mediaSource ?? driver;
15494
15823
  let raf = 0;
15495
15824
  let lastMedia = null;
15496
15825
  let lastTick = 0;
@@ -15500,8 +15829,9 @@ function startMediaPump(rt, driver) {
15500
15829
  if (now - lastTick < 200) return;
15501
15830
  lastTick = now;
15502
15831
  try {
15503
- driver.update(now / 1e3);
15504
- lastMedia = pushMediaDiff(rt, lastMedia, driver.snapshot);
15832
+ const cur = pick();
15833
+ cur.update?.(now / 1e3);
15834
+ lastMedia = pushMediaDiff(rt, lastMedia, cur.snapshot);
15505
15835
  } catch {
15506
15836
  }
15507
15837
  };
@@ -15596,18 +15926,65 @@ function mountWeb(rt, cfg) {
15596
15926
  }
15597
15927
  installWebCtl(rt);
15598
15928
  const cfgExt = cfg;
15599
- const audioDriver = cfgExt._webAudio === null ? null : cfgExt._webAudio ?? defaultAudioDriver();
15600
- const mediaDriver = cfgExt._webMedia === null ? null : cfgExt._webMedia ?? defaultMediaDriver();
15929
+ const audioDriver = cfgExt._webAudio === null || rt.audioDisabled ? null : cfgExt._webAudio ?? defaultAudioDriver();
15930
+ const mediaDriver = cfgExt._webMedia === null || rt.mediaDisabled ? null : cfgExt._webMedia ?? defaultMediaDriver();
15601
15931
  const finishBare = (why) => {
15602
15932
  reportDiag(rt, cfg, `网页壁纸 shim 注入失败(${why}),退回裸 iframe`);
15603
15933
  attachIframe(rt, cfg, container, entry, { injected: false });
15604
15934
  startAudioPump(rt, null);
15605
15935
  startMediaPump(rt, null);
15936
+ let beat = 0;
15937
+ const tick = (now) => {
15938
+ if (!rt.iframe) return;
15939
+ beat = requestAnimationFrame(tick);
15940
+ if (rt.paused) return;
15941
+ markFrame(rt, now);
15942
+ };
15943
+ beat = requestAnimationFrame(tick);
15944
+ (rt.wallpaperDisposers ??= []).push(() => cancelAnimationFrame(beat));
15606
15945
  };
15607
15946
  const frameClock = { last: 0 };
15947
+ const liveHold = { driver: null };
15608
15948
  const startPumps = () => {
15609
- startAudioPump(rt, audioDriver, frameClock);
15949
+ startAudioPump(rt, audioDriver, frameClock, liveHold);
15610
15950
  startMediaPump(rt, mediaDriver);
15951
+ if (cfg.liveSystem && audioDriver) {
15952
+ const liveSlot = {
15953
+ handle: null,
15954
+ dead: false
15955
+ };
15956
+ (rt.wallpaperDisposers ??= []).push(() => {
15957
+ liveSlot.dead = true;
15958
+ liveHold.driver = null;
15959
+ try {
15960
+ liveSlot.handle?.dispose();
15961
+ } catch {
15962
+ }
15963
+ liveSlot.handle = null;
15964
+ });
15965
+ void (async () => {
15966
+ try {
15967
+ const live = await startLiveSystem({ origin: location.origin });
15968
+ if (liveSlot.dead) {
15969
+ try {
15970
+ live.dispose();
15971
+ } catch {
15972
+ }
15973
+ return;
15974
+ }
15975
+ liveSlot.handle = live;
15976
+ const st = live.status();
15977
+ if (st.audio === "mic") {
15978
+ liveHold.driver = liveAudioDriver(live);
15979
+ reportDiag(rt, cfg, "liveSystem: 网页壁纸音频改用麦克风");
15980
+ } else {
15981
+ reportDiag(rt, cfg, `liveSystem: 麦克风不可用(${st.audio}),网页壁纸沿用模拟源`);
15982
+ }
15983
+ } catch (e) {
15984
+ reportDiag(rt, cfg, `liveSystem: 启动失败,网页壁纸沿用模拟源 (${e?.message ?? e})`);
15985
+ }
15986
+ })();
15987
+ }
15611
15988
  };
15612
15989
  void (async () => {
15613
15990
  const defaults = await fetchProjectWire(entry);
@@ -15674,6 +16051,99 @@ function mountWallpaper(rt, cfg) {
15674
16051
  rt.onUnhandledType?.(cfg);
15675
16052
  }
15676
16053
  }
16054
+ class Color {
16055
+ x;
16056
+ y;
16057
+ z;
16058
+ constructor(x, y, z) {
16059
+ this.x = Number(x) || 0;
16060
+ this.y = Number(y) || 0;
16061
+ this.z = Number(z) || 0;
16062
+ }
16063
+ add(o) {
16064
+ return new Color(this.x + o.x, this.y + o.y, this.z + o.z);
16065
+ }
16066
+ subtract(o) {
16067
+ return new Color(this.x - o.x, this.y - o.y, this.z - o.z);
16068
+ }
16069
+ multiply(k) {
16070
+ if (typeof k === "number") return new Color(this.x * k, this.y * k, this.z * k);
16071
+ return new Color(this.x * k.x, this.y * k.y, this.z * k.z);
16072
+ }
16073
+ toString() {
16074
+ return `${this.x} ${this.y} ${this.z}`;
16075
+ }
16076
+ }
16077
+ function mediaColor(r, g, b) {
16078
+ if (r instanceof Color) return r;
16079
+ if (Array.isArray(r)) return new Color(r[0] ?? 0, r[1] ?? 0, r[2] ?? 0);
16080
+ if (typeof r === "object" && r !== null) {
16081
+ const o = r;
16082
+ return new Color(o.x ?? 0, o.y ?? 0, o.z ?? 0);
16083
+ }
16084
+ return new Color(r, g ?? 0, b ?? 0);
16085
+ }
16086
+ const DEF_PRIMARY = [0.35, 0.38, 0.45];
16087
+ const DEF_SECONDARY = [0.12, 0.13, 0.17];
16088
+ const DEF_TERTIARY = [0.72, 0.76, 0.84];
16089
+ const DEF_TEXT = [0.95, 0.96, 0.98];
16090
+ function locateLyric(lyrics, position) {
16091
+ if (!Array.isArray(lyrics) || lyrics.length === 0) return { line: "", index: -1 };
16092
+ let idx = -1;
16093
+ for (let i = 0; i < lyrics.length; i++) {
16094
+ const at = Number(lyrics[i]?.[0]);
16095
+ if (Number.isFinite(at) && at <= position) idx = i;
16096
+ else break;
16097
+ }
16098
+ return { line: idx >= 0 ? String(lyrics[idx][1] ?? "") : "", index: idx };
16099
+ }
16100
+ function buildSnapshot(init) {
16101
+ const position = Number(init.position) || 0;
16102
+ const lyrics = Array.isArray(init.lyrics) ? init.lyrics : [];
16103
+ const { line, index } = locateLyric(lyrics, position);
16104
+ const state = init.state !== void 0 ? init.state : init.playing === false ? 2 : init.playing ? 1 : 0;
16105
+ return {
16106
+ hasMedia: init.hasMedia ?? (init.title != null || init.artist != null || !!init.playing),
16107
+ state,
16108
+ title: String(init.title ?? ""),
16109
+ artist: String(init.artist ?? ""),
16110
+ album: String(init.album ?? ""),
16111
+ albumArtist: String(init.albumArtist ?? init.artist ?? ""),
16112
+ position,
16113
+ duration: Number(init.duration) || 0,
16114
+ hasThumbnail: init.hasThumbnail ?? false,
16115
+ primaryColor: mediaColor(init.primaryColor ?? DEF_PRIMARY),
16116
+ secondaryColor: mediaColor(init.secondaryColor ?? DEF_SECONDARY),
16117
+ tertiaryColor: mediaColor(init.tertiaryColor ?? DEF_TERTIARY),
16118
+ textColor: mediaColor(init.textColor ?? DEF_TEXT),
16119
+ highContrastColor: mediaColor(init.highContrastColor ?? init.textColor ?? DEF_TEXT),
16120
+ trackIndex: Number(init.trackIndex) || 0,
16121
+ lyrics,
16122
+ lyricLine: line,
16123
+ lyricIndex: index
16124
+ };
16125
+ }
16126
+ function createMediaSource(init = {}, controls = {}) {
16127
+ let cur = { ...init };
16128
+ let snap = buildSnapshot(cur);
16129
+ return {
16130
+ get snapshot() {
16131
+ return snap;
16132
+ },
16133
+ set(patch) {
16134
+ cur = { ...cur, ...patch };
16135
+ snap = buildSnapshot(cur);
16136
+ },
16137
+ // 宿主的快照由外部事件驱动,不需要按帧自行推进;留空实现满足接口即可
16138
+ update() {
16139
+ },
16140
+ skipNext: controls.skipNext,
16141
+ skipPrevious: controls.skipPrevious,
16142
+ play: controls.play,
16143
+ pause: controls.pause,
16144
+ playPause: controls.playPause
16145
+ };
16146
+ }
15677
16147
  function normalizeFitOption(fit) {
15678
16148
  if (fit === "fit") return "contain";
15679
16149
  if (fit === "fill") return "cover";
@@ -15683,6 +16153,13 @@ function isWebProject(project) {
15683
16153
  const t = project?.type;
15684
16154
  return typeof t === "string" && t.toLowerCase() === "web";
15685
16155
  }
16156
+ const MEDIA_TYPES = /* @__PURE__ */ new Set(["video", "gif", "image"]);
16157
+ function mediaProjectType(project) {
16158
+ const t = project?.type;
16159
+ if (typeof t !== "string") return null;
16160
+ const lower = t.toLowerCase();
16161
+ return MEDIA_TYPES.has(lower) ? lower : null;
16162
+ }
15686
16163
  function ensureSceneCanvas(el) {
15687
16164
  if (el instanceof HTMLCanvasElement) return el;
15688
16165
  const existing = el.querySelector(":scope > canvas[data-webwallgl]");
@@ -15725,6 +16202,47 @@ async function resolveMountConfig(el, o) {
15725
16202
  if (!url) throw new Error("网页壁纸:无法解析入口 URL(需要 Source.webEntry 或 httpSource)");
15726
16203
  return { ...base, type: "web", src: url, source: o.source };
15727
16204
  }
16205
+ const mediaType = mediaProjectType(project);
16206
+ if (mediaType) {
16207
+ let url;
16208
+ let entryType;
16209
+ try {
16210
+ const entry = await o.source.mediaEntry?.();
16211
+ url = entry?.url;
16212
+ entryType = entry?.type;
16213
+ } catch {
16214
+ url = void 0;
16215
+ }
16216
+ if (!url && o.source.key) {
16217
+ const file = project && typeof project.file === "string" ? String(project.file).trim().replace(/^\/+/, "") : "";
16218
+ if (file) url = `${o.source.key.replace(/\/+$/, "")}/${file}`;
16219
+ }
16220
+ if (!url) {
16221
+ throw new Error(
16222
+ `媒体壁纸(${mediaType}):无法解析资源 URL(需要 Source.mediaEntry 或 project.file + httpSource)`
16223
+ );
16224
+ }
16225
+ const canvas2 = ensureSceneCanvas(el);
16226
+ const finalType = MEDIA_TYPES.has(String(entryType).toLowerCase()) ? String(entryType).toLowerCase() : mediaType;
16227
+ return { ...base, type: finalType, src: url, canvas: canvas2, source: o.source };
16228
+ }
16229
+ const declaredType = typeof project?.type === "string" ? String(project.type).trim() : "";
16230
+ if (!declaredType && typeof o.source.mediaEntry === "function") {
16231
+ let url;
16232
+ let entryType;
16233
+ try {
16234
+ const entry = await o.source.mediaEntry();
16235
+ url = entry?.url;
16236
+ entryType = entry?.type;
16237
+ } catch {
16238
+ url = void 0;
16239
+ }
16240
+ const sniffed = (MEDIA_TYPES.has(String(entryType).toLowerCase()) ? String(entryType).toLowerCase() : null) ?? (url ? sniffMediaType(url) : null);
16241
+ if (url && sniffed) {
16242
+ const canvas2 = ensureSceneCanvas(el);
16243
+ return { ...base, type: sniffed, src: url, canvas: canvas2, source: o.source };
16244
+ }
16245
+ }
15728
16246
  const canvas = ensureSceneCanvas(el);
15729
16247
  return { ...base, type: "scene", canvas, source: o.source };
15730
16248
  }
@@ -15764,6 +16282,26 @@ function createScene(el, options) {
15764
16282
  rt.onSceneInfo = (info) => {
15765
16283
  rt.info = info;
15766
16284
  };
16285
+ if ("audio" in o) applyAudio(o.audio ?? null);
16286
+ if ("media" in o) {
16287
+ rt.mediaSource = o.media ?? null;
16288
+ rt.mediaDisabled = o.media === null;
16289
+ }
16290
+ };
16291
+ const applyAudio = (src) => {
16292
+ rt.audioDisabled = src === null;
16293
+ if (!src) {
16294
+ rt.audioBridge = null;
16295
+ return;
16296
+ }
16297
+ rt.audioBridge = () => {
16298
+ try {
16299
+ const s = src.snapshot();
16300
+ return s && s.left && s.right ? s : null;
16301
+ } catch {
16302
+ return null;
16303
+ }
16304
+ };
15767
16305
  };
15768
16306
  const armFirstFrame = () => {
15769
16307
  return new Promise((resolve) => {
@@ -15820,6 +16358,7 @@ function createScene(el, options) {
15820
16358
  setFit(fit) {
15821
16359
  rt.cfg.fit = fit;
15822
16360
  resetCoverAlign(rt);
16361
+ rt.webRelayout?.();
15823
16362
  },
15824
16363
  setFps(fps) {
15825
16364
  rt.cfg.sceneFps = fps;
@@ -15843,11 +16382,81 @@ function createScene(el, options) {
15843
16382
  getProperties() {
15844
16383
  return { ...rt.liveUserProps ?? {} };
15845
16384
  },
16385
+ // 宿主频谱源。只存引用,实际每帧拉取在 scene-mount 的渲染循环里。
16386
+ // 与 __wp.setAudioBridge 同纪律:换场景不清空,装一次对之后所有场景生效
16387
+ // (wireOptions 也只在选项里出现 audio 时才覆盖,见那里的说明)。
16388
+ setAudio(src) {
16389
+ currentOptions = { ...currentOptions, audio: src };
16390
+ applyAudio(src);
16391
+ rt.audioDisabled = false;
16392
+ },
16393
+ // 系统媒体源。与 setAudio 同纪律:只存引用、换场景不清空,
16394
+ // scene 与 web 两条装配路径读同一个 rt.mediaSource。
16395
+ // 注意语义与 MountOptions.media:null 不同:这里的 null 按文档是
16396
+ // 「回落内置模拟源」,不是禁用(禁用只在挂载选项里表达)。
16397
+ setMedia(src) {
16398
+ currentOptions = { ...currentOptions, media: src };
16399
+ rt.mediaSource = src ?? null;
16400
+ rt.mediaDisabled = false;
16401
+ },
16402
+ // 媒体控制面。装配后由 mountScene 写入 rt.mediaCtl;未装配(或媒体/网页
16403
+ // 壁纸尚无控制面)时给一个惰性替身,读快照得空、控制方法静默无效 ——
16404
+ // 让调用方能无条件 `wp.media.playPause()` 而不必先判空。
16405
+ get media() {
16406
+ const ctl = rt.mediaCtl;
16407
+ if (ctl) return ctl;
16408
+ const empty = {
16409
+ hasMedia: false,
16410
+ state: 0,
16411
+ title: "",
16412
+ artist: "",
16413
+ album: "",
16414
+ albumArtist: "",
16415
+ position: 0,
16416
+ duration: 0,
16417
+ hasThumbnail: false,
16418
+ primaryColor: mediaColor(0, 0, 0),
16419
+ secondaryColor: mediaColor(0, 0, 0),
16420
+ tertiaryColor: mediaColor(0, 0, 0),
16421
+ textColor: mediaColor(1, 1, 1),
16422
+ highContrastColor: mediaColor(1, 1, 1),
16423
+ trackIndex: 0,
16424
+ lyrics: [],
16425
+ lyricLine: "",
16426
+ lyricIndex: -1
16427
+ };
16428
+ const noop = () => empty;
16429
+ return {
16430
+ get snapshot() {
16431
+ return empty;
16432
+ },
16433
+ skipNext: noop,
16434
+ skipPrevious: noop,
16435
+ play: noop,
16436
+ pause: noop,
16437
+ playPause: noop
16438
+ };
16439
+ },
16440
+ // 外部指针注入。pointerCtl 由 mountScene / mountWeb 各自装配时设置,
16441
+ // 媒体壁纸不设 —— 那时这里静默无效,与整页渲染器的 __wp.pushPointer 一致。
16442
+ pushPointer(u, v, buttons) {
16443
+ rt.pointerCtl?.push({ u, v, buttons });
16444
+ },
16445
+ pointerLeave() {
16446
+ rt.pointerCtl?.leave();
16447
+ },
15846
16448
  async load(source) {
16449
+ const prev = currentOptions.source;
16450
+ if (prev && prev !== source) {
16451
+ try {
16452
+ prev.dispose?.();
16453
+ } catch {
16454
+ }
16455
+ }
15847
16456
  currentOptions = { ...currentOptions, source };
15848
16457
  wireOptions(currentOptions);
15849
16458
  const cfg = await resolveMountConfig(boundEl, currentOptions);
15850
- if (cfg.type === "scene" && cfg.canvas instanceof HTMLCanvasElement) {
16459
+ if (cfg.type !== "web" && cfg.canvas instanceof HTMLCanvasElement) {
15851
16460
  boundEl = cfg.canvas;
15852
16461
  }
15853
16462
  rt.cfg = cfg;
@@ -15874,6 +16483,10 @@ function createScene(el, options) {
15874
16483
  },
15875
16484
  destroy() {
15876
16485
  destroyRuntime(rt);
16486
+ try {
16487
+ currentOptions.source?.dispose?.();
16488
+ } catch {
16489
+ }
15877
16490
  rt.onDiagnostic = void 0;
15878
16491
  rt.onError = void 0;
15879
16492
  rt.onFirstFrame = void 0;
@@ -15901,11 +16514,11 @@ function createScene(el, options) {
15901
16514
  currentOptions = o;
15902
16515
  wireOptions(o);
15903
16516
  const cfg = await resolveMountConfig(el, o);
15904
- if (cfg.type === "scene" && cfg.canvas instanceof HTMLCanvasElement) {
16517
+ if (cfg.type !== "web" && cfg.canvas instanceof HTMLCanvasElement) {
15905
16518
  boundEl = cfg.canvas;
15906
16519
  }
15907
16520
  rt.cfg = cfg;
15908
- rt.paused = o.autoplay === false;
16521
+ rt.paused = false;
15909
16522
  rt.info = void 0;
15910
16523
  resetCoverAlign(rt);
15911
16524
  if (o.properties && Object.keys(o.properties).length) {
@@ -15919,6 +16532,7 @@ function createScene(el, options) {
15919
16532
  } finally {
15920
16533
  failure.off();
15921
16534
  }
16535
+ if (o.autoplay === false) instance.pause();
15922
16536
  if ((o.volume ?? 0) > 0) instance.setVolume(o.volume);
15923
16537
  if (o.properties && Object.keys(o.properties).length) instance.setProperties(o.properties);
15924
16538
  };
@@ -15933,9 +16547,13 @@ async function mount(el, options) {
15933
16547
  }
15934
16548
  export {
15935
16549
  bytesSource,
16550
+ createMediaSource,
15936
16551
  createScene,
15937
16552
  fileSource,
15938
16553
  httpSource,
15939
- mount
16554
+ mediaColor,
16555
+ mediaSource,
16556
+ mount,
16557
+ sniffMediaType
15940
16558
  };
15941
16559
  //# sourceMappingURL=webwallgl.mjs.map