dsh-mpkg-wallpaper 3.16.3 → 3.16.4

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/client.js CHANGED
@@ -6876,9 +6876,11 @@ function mpwThumbFail(el, why) {
6876
6876
  /** ①(新) scene 内嵌视频探测(scene-video 快路径):探测 URL 按 key 路由。
6877
6877
  * 返回 { has, mime };网络错误/未找到视为无视频(回退静态帧)。 */
6878
6878
  function sceneVideoCheckUrl(key) {
6879
- if (key && key.indexOf("library|") === 0) return HOST_URL + "/library-scene-video-check?ltoken=" + encodeURIComponent(key.slice(8));
6879
+ // ①(2026-10-04) `has=1`:只要"有没有"(宿主走只读目录表+前缀的 verdict,不再为问一句提取整段视频);
6880
+ // 不带该参数时宿主保持旧行为(含 mime/size 与探测即落盘缓存)——旧路径判据因此逐位不变。
6881
+ if (key && key.indexOf("library|") === 0) return HOST_URL + "/library-scene-video-check?has=1&ltoken=" + encodeURIComponent(key.slice(8));
6880
6882
  const name = key && key.indexOf("custom|") === 0 ? key.slice(7) : "";
6881
- return HOST_URL + "/custom-scene-video-check?folder=" + encodeURIComponent(name);
6883
+ return HOST_URL + "/custom-scene-video-check?has=1&folder=" + encodeURIComponent(name);
6882
6884
  }
6883
6885
  function checkSceneVideo(key) {
6884
6886
  // ①(修正) 探测加 4s 超时:host 端首查需同步读包/落盘(可达 1-3s),
package/lib/index.js CHANGED
@@ -16,7 +16,7 @@ import { execFileSync, spawn, spawnSync } from 'node:child_process';
16
16
  // ①(2026-09-21 音频格式补全) 后三个导出是**唯一的**扩展名→MIME 表与嗅探入口,见 docs/AUDIO-TRACK-SPEC.md §1.2/§3:
17
17
  // 路由侧原先各自内联一份扩展名三元链 + 一份魔数嗅探,已与渲染器支持的后缀漂移(.oga/.m4a/.aac 缺失、ftyp 偏移写错)。
18
18
  // ⚠ 这条 import 必须**保持单行**:tools/bundle-equivalence-test.mjs 的具名导入解析器按行读(注释进括号会解析失败)。
19
- import { extractSceneMainImage, extractSceneMainImageFromDir, extractSceneManifest, extractSceneLayer, extractTexVideoMp4, collectSceneVideoFiles, parsePkg, readPkgEntry, scanSceneAudio, scanSceneVideo, clearSceneVideoScanCache, suffixAudioMime, audioMimeFor, AUDIO_SUFFIX_MIME, mpwParseWeJson, mpwWeJsonSwallowed } from './pkg-extract.js';
19
+ import { extractSceneMainImage, extractSceneMainImageFromDir, extractSceneManifest, extractSceneLayer, extractTexVideoMp4, collectSceneVideoFiles, parsePkg, readPkgEntry, scanSceneAudio, scanSceneVideo, clearSceneVideoScanCache, probeSceneVideoVerdict, suffixAudioMime, audioMimeFor, AUDIO_SUFFIX_MIME, mpwParseWeJson, mpwWeJsonSwallowed } from './pkg-extract.js';
20
20
  // ①(新 2026-09-16 I 项) 网页(web)类壁纸:内容优先的类型判定 + WE API shim 注入 + 跨源策略
21
21
  // (MIT,本仓库自写;API 名单参考 oneincase/webwallgl,未复制其代码,见 ../docs/COPYING-RULES.md §4)
22
22
  // ①(WP-1 2026-09-19) 追加:帧内存储 facade 的持久化路由(/web-store)、宿主媒体音频控制
@@ -4229,6 +4229,31 @@ function apply(ctx) {
4229
4229
  const probeSceneVideo = (req, res, dir) => {
4230
4230
  const t0 = Date.now();
4231
4231
  try {
4232
+ /* ①(2026-10-04) **只判不取**(**仅当调用方显式要 `?has=1` 时**):客户端"卡片/清单阶段"
4233
+ * 只问"这张场景有没有内嵌视频"(`lib/client.js` 只读 `d.has`),而 `ensureSceneVideo` 是
4234
+ * "判定 + 提取"一体的(会把整段内嵌视频读进内存并落盘;实测 792MB 的容器 = 791.6MB 读盘)。
4235
+ * `?has=1` 时先走只读目录表 + 候选前缀的 `probeSceneVideoVerdict`(实测 64KB/容器):能判定
4236
+ * 就直接答 `{ok,has}`,答不了(unknown/多独立视频/松散目录)回退提取路径。
4237
+ * ⚠ **不带该参数时行为与改前逐位相同**(含 `{has,mime,size}` 响应形状与"探测即落盘缓存"
4238
+ * 的既有契约)—— 既有判据 `tools/scene-video-test.mjs` 钉的正是那条默认路径。 */
4239
+ const wantHasOnly = (() => {
4240
+ try {
4241
+ const q = String((req && req.url) || '');
4242
+ return /[?&]has=1(?:&|$)/.test(q);
4243
+ } catch { return false; }
4244
+ })();
4245
+ try {
4246
+ const verdict = wantHasOnly ? probeSceneVideoVerdict(dir) : null;
4247
+ if (verdict && verdict.decided) {
4248
+ const ms0 = Date.now() - t0;
4249
+ if (ms0 > 500) {
4250
+ try { console.log('[dsh-mpkg-wallpaper][perf] scene 视频探测(verdict) ' + ms0 + 'ms:', dir); } catch {}
4251
+ }
4252
+ if (!verdict.has) { json(res, 200, { ok: true, has: false, verdict: true }); return; }
4253
+ json(res, 200, { ok: true, has: true, verdict: true });
4254
+ return;
4255
+ }
4256
+ } catch { /* verdict 出意外 → 回退提取路径(与改前同一答案) */ }
4232
4257
  const v = ensureSceneVideo(dir);
4233
4258
  const ms = Date.now() - t0;
4234
4259
  if (ms > 500) {
@@ -427,6 +427,35 @@ function readPkgEntry(data, entry) {
427
427
  return out;
428
428
  }
429
429
 
430
+ /**
431
+ * ①(2026-10-04) **从"已读出的存储字节"还原一条目**:把"是否 LZ4 链"的判定交给**唯一口径**
432
+ * `probeCompressedEntry`(与 `parsePkg` 同一函数),而不是依赖调用方传进来的 `flags`。
433
+ *
434
+ * 为什么需要:`readPkgIndexFromFile` 只读目录表,条目内容不在已读头里 ⇒ 头解析出来的条目
435
+ * `flags` 恒为 0(`partial:true`)。索引先行的路径据此把**压缩条目**当 raw 用 ⇒ 视频/TEX
436
+ * 在"压缩存储"时会静默探测失败(legacy 整包口径却能成功)—— 这正是"两条路径判定分叉"的形态。
437
+ * 这里用全量存储字节做判定(信息最全),成功即解压;`flags & 1` 的快路保留(整包口径)。
438
+ */
439
+ function decodeStoredEntry(stored, entry) {
440
+ const raw = stored instanceof Uint8Array ? stored : new Uint8Array(stored)
441
+ let original = null
442
+ if ((entry.flags & 1) === 0) {
443
+ // 头解析出来的条目 `size` 是**存储长度**(partial),不能拿它当原始长度:链头 u64 才是真值
444
+ try { original = probeCompressedEntry(raw, 0, raw.length) } catch { original = null }
445
+ if (original === null) return raw
446
+ }
447
+ if (original === null) {
448
+ // 整包口径(flags&1):`entry.size` 已是 probe 出来的原始长度;仍以链头为准做一致性兜底
449
+ try {
450
+ const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength)
451
+ const head = Number(view.getBigUint64(0, true))
452
+ original = head > 0 && head <= 2147483647 ? head : entry.size
453
+ } catch { original = entry.size }
454
+ }
455
+ const w = { path: entry.path, offset: 0, compressedSize: raw.length, size: original, flags: 1 }
456
+ return readPkgEntry(raw, w)
457
+ }
458
+
430
459
  /* ═══════════════════════════════════════════════════════════════════════════
431
460
  * ①(2026-09-15 用户第 1 条反馈「扫描音频的速度能否快些」) 惰性音频索引
432
461
  *
@@ -2446,11 +2475,9 @@ function scanSceneVideo(dirOrPath, opts = {}) {
2446
2475
  const got = readSync(fd, stored, 0, stored.length, Math.max(0, e.offset));
2447
2476
  bytesRead += got; entriesRead++;
2448
2477
  const buf = got === stored.length ? stored : stored.subarray(0, got);
2449
- if (e.flags & 1) {
2450
- const w = { path: e.path, offset: 0, compressedSize: buf.length, size: e.size, flags: 1 };
2451
- return readPkgEntry(buf, w);
2452
- }
2453
- return new Uint8Array(buf);
2478
+ // ①(2026-10-04) 走 decodeStoredEntry:头解析的条目 flags 恒为 0(partial),
2479
+ // 压缩条目必须靠 probeCompressedEntry 判(全量口径),否则会被当 raw 用 ⇒ 静默漏判。
2480
+ return decodeStoredEntry(buf, e);
2454
2481
  } catch { return null; }
2455
2482
  };
2456
2483
  // ① 独立视频条目:只读它们(旧路径读了全部 TEX 也会丢弃这些结果)
@@ -2510,7 +2537,90 @@ function scanSceneVideo(dirOrPath, opts = {}) {
2510
2537
  return { video, source, cacheHit: false, indexEntries, entriesRead, bytesRead, tableBytes: source === 'pkg' ? undefined : 0, ms };
2511
2538
  }
2512
2539
 
2513
- export { extractSceneMainImage, extractSceneMainImageFromDir, extractSceneManifest, extractSceneLayer, extractTexVideoMp4, collectSceneVideoFiles, parsePkg, readPkgEntry, parseTex, PKG_MAGIC_RE };
2540
+ /* ═══════════════════════════════════════════════════════════════════════════
2541
+ * ①(2026-10-04) 「只判不取」的 scene 内嵌视频 verdict
2542
+ *
2543
+ * 动机(本机实测):`ensureSceneVideo` 是"判定 + 提取"一体的 —— 探一次就把整段内嵌视频读进
2544
+ * 内存并落盘(实测样例里 712 MB 的视频 = 712 MB 读盘),而客户端在**列表/卡片阶段**只问
2545
+ * `{has, mime}`(`lib/client.js` 的 `sceneVideoCheckUrl` 只读 `d.has`)。本函数只做判定:
2546
+ * **只读目录表 + 每条候选的前缀**(本机实测判定成本 ≈ 60 KB/包),绝不读视频载荷。
2547
+ *
2548
+ * 判定口径与 `scanSceneVideo` / legacy(`findSceneVideoInPkg`)**同源**:
2549
+ * · 有独立视频条目 ⇒ 恰好 1 条时可判定(取它);≥2 条时"谁最大"要看解压后长度 ⇒ 交回慢路径;
2550
+ * · 否则看 TEX:逐条前缀判定(`probeTexVideoDecision`)——
2551
+ * ≥2 条判为 video ⇒ `has:false`(与 legacy "多 TEX 视频 ⇒ null" 一致);
2552
+ * exactly 1 条 video 且无 unknown ⇒ `has:true`;0 条 video 且无 unknown ⇒ `has:false`;
2553
+ * 出现 unknown(前缀不足/异常布局/压缩解释歧义)⇒ `decided:false`,调用方回退 `ensureSceneVideo`。
2554
+ * · **松散目录**(`scene.json` + 平铺 `.tex`)一律 `decided:false`:那种场景没有大载荷,不值得为它加一条分支。
2555
+ *
2556
+ * 返回 `{ has, ref, tex, decided, source, reason, indexEntries, bytesRead, ms }`。
2557
+ * ⚠ 本函数**不写任何缓存**(判定结果由调用方决定是否复用;提取物缓存仍归 `ensureSceneVideo`)。
2558
+ * ═══════════════════════════════════════════════════════════════════════════ */
2559
+ function probeSceneVideoVerdict(dirOrPath, opts = {}) {
2560
+ const t0 = process.hrtime.bigint()
2561
+ const done = (o) => Object.assign({ has: null, ref: null, tex: null, decided: false, source: 'pkg', reason: null, indexEntries: 0, bytesRead: 0, texPeeked: 0, ms: Number(process.hrtime.bigint() - t0) / 1e6 }, o)
2562
+ let target = dirOrPath
2563
+ try {
2564
+ const st0 = statSync(dirOrPath)
2565
+ if (st0.isDirectory()) {
2566
+ const pkgPath = join(dirOrPath, 'scene.pkg')
2567
+ if (!existsSync(pkgPath)) return done({ source: 'dir', reason: 'loose-dir' })
2568
+ target = pkgPath
2569
+ }
2570
+ } catch (e) { return done({ source: 'unknown', reason: 'stat-failed' }) }
2571
+ let idx = null
2572
+ try { idx = readPkgIndexFromFile(target, opts) } catch (e) { return done({ reason: 'index-unreadable' }) }
2573
+ const idxEntries = idx.entries
2574
+ const sizeOf = (e) => (e.compressedSize == null ? e.size : e.compressedSize)
2575
+ const base = { indexEntries: idxEntries.length, bytesRead: idx.headBytes, texPeeked: 0 }
2576
+ const standalone = idxEntries.filter((e) => SCENE_VIDEO_EXT_RE.test(String(e.path || '').toLowerCase()) && sizeOf(e) > 0)
2577
+ if (standalone.length === 1) return done(Object.assign({ has: true, ref: standalone[0].path, tex: false, decided: true }, base))
2578
+ if (standalone.length > 1) return done(Object.assign({ reason: 'multi-standalone' }, base))
2579
+ const texEntries = idxEntries.filter((e) => String(e.path || '').toLowerCase().endsWith('.tex'))
2580
+ let bytesRead = idx.headBytes
2581
+ let texPeeked = 0
2582
+ const fd = openSync(target, 'r')
2583
+ try {
2584
+ let positives = 0, unknown = 0, firstPositive = null
2585
+ for (const e of texEntries) {
2586
+ const want = Math.min(sizeOf(e), TEX_PEEK_BYTES)
2587
+ if (want <= 0) { unknown++; continue }
2588
+ let head = null
2589
+ try {
2590
+ const buf = Buffer.allocUnsafe(want)
2591
+ const got = readSync(fd, buf, 0, want, Math.max(0, e.offset))
2592
+ bytesRead += got
2593
+ if (got > 0) texPeeked++
2594
+ head = got === want ? buf : buf.subarray(0, got)
2595
+ } catch { head = null }
2596
+ const decide = (bytes) => (bytes && bytes.length)
2597
+ ? probeTexVideoDecision((off, len) => (off + len > bytes.length ? null : bytes.subarray(off, off + len)), bytes, e.path)
2598
+ : 'unknown'
2599
+ let decision = decide(head)
2600
+ // 头解析的条目 `flags` 未知:可能是 LZ4 链条目 ⇒ 再按"压缩解释"试一次(成功且判出结论才采用)
2601
+ if (decision === 'unknown') {
2602
+ try {
2603
+ const d = readPkgEntryHead(head, { path: e.path, offset: 0, compressedSize: head.length, size: e.size, flags: 1 }, TEX_PEEK_BYTES)
2604
+ const d2 = decide(d)
2605
+ if (d2 !== 'unknown') decision = d2
2606
+ } catch { /* 不是压缩条目(或前缀不足)→ 保持 unknown */ }
2607
+ }
2608
+ if (decision === 'none') continue
2609
+ if (decision === 'video') {
2610
+ positives++
2611
+ if (!firstPositive) firstPositive = e.path
2612
+ if (positives >= 2) break
2613
+ } else unknown++
2614
+ }
2615
+ const out = { indexEntries: idxEntries.length, bytesRead, texPeeked }
2616
+ if (positives >= 2) return done(Object.assign({ has: false, decided: true }, out))
2617
+ if (positives === 1 && unknown === 0) return done(Object.assign({ has: true, ref: firstPositive, tex: true, decided: true }, out))
2618
+ if (positives === 0 && unknown === 0) return done(Object.assign({ has: false, decided: true }, out))
2619
+ return done(Object.assign({ reason: 'unknown-tex-decision', positives, unknown }, out))
2620
+ } finally { closeSync(fd) }
2621
+ }
2622
+
2623
+ export { extractSceneMainImage, extractSceneMainImageFromDir, extractSceneManifest, extractSceneLayer, extractTexVideoMp4, collectSceneVideoFiles, parsePkg, readPkgEntry, parseTex, PKG_MAGIC_RE, probeSceneVideoVerdict, decodeStoredEntry };
2514
2624
  // ①(2026-09-15 音频扫描提速 / 2026-09-16 洁净室重写) 惰性音频索引:只读目录表 + 仅候选头 ≤16 字节。
2515
2625
  // 名字与结构依据 docs/AUDIO-TRACK-SPEC.md(后缀判定 / 容器判定 / 枚举核心里外三层)。
2516
2626
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-mpkg-wallpaper",
3
- "version": "3.16.3",
3
+ "version": "3.16.4",
4
4
  "description": "DSH Web 壁纸引擎 Wallpaper Engine mpkg 背景插件:浏览器内直接解析 .mpkg(preview.gif 动态背景/内嵌 mp4 视频/多时段切换),整屏统一虚化/对话框/弹层/遮罩独立虚化、镜头缩放平移、时钟、冲突检测。",
5
5
  "private": false,
6
6
  "type": "module",