dsh-folia 4.0.2 → 4.0.3

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.
Files changed (2) hide show
  1. package/lib/routes/boot.js +34 -5
  2. package/package.json +1 -1
@@ -9,7 +9,7 @@
9
9
  * 播放走 `/folia/boot/clip`,**支持 Range**:注入的 `<video>` 与浏览器都靠它边下边播。
10
10
  * 这也是为什么片段必须先落盘 —— 启动路径上不能有网络。
11
11
  */
12
- import { createReadStream, existsSync, statSync, writeFileSync } from 'node:fs';
12
+ import { createReadStream, existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { sendJson } from '../json-response.js';
15
15
  import { bodyReader } from '../http-body.js';
@@ -112,6 +112,28 @@ async function fetchClipBytes(url, chunks = 4) {
112
112
  }
113
113
  }
114
114
 
115
+ /**
116
+ * 判定一段字节是不是**真的视频**,并给出它的 MIME。
117
+ *
118
+ * 为什么必须验文件头而不是 content-type:站点对未知路径会回退 index.html 并以 200 返回,
119
+ * 这种"伪视频"存成 .mp4 的结果是**每次开机黑屏**。
120
+ *
121
+ * 认两种容器(覆盖站点上实际会有的上传):
122
+ * MP4/MOV ISO BMFF —— 偏移 4..8 是 `ftyp`
123
+ * WebM EBML —— 开头 4 字节是 1A 45 DF A3
124
+ */
125
+ export function sniffVideo(buf) {
126
+ if (!buf || buf.length < 1024) return null;
127
+ if (buf.length >= 12 && buf.toString('latin1', 4, 8) === 'ftyp') {
128
+ const brand = buf.toString('latin1', 8, 12);
129
+ return { mime: 'video/mp4', ext: '.mp4', container: 'mp4', brand };
130
+ }
131
+ if (buf.length >= 4 && buf[0] === 0x1a && buf[1] === 0x45 && buf[2] === 0xdf && buf[3] === 0xa3) {
132
+ return { mime: 'video/webm', ext: '.webm', container: 'webm', brand: null };
133
+ }
134
+ return null;
135
+ }
136
+
115
137
  /** 读请求体里的 `{ id }`。 */
116
138
  async function readIdBody(req) {
117
139
  try {
@@ -269,9 +291,10 @@ export function registerBootRoutes(webServer, c) {
269
291
  const buf = await fetchClipBytes(item.url, 4);
270
292
  const ct = 'video/mp4'; // 并行分段之后没有单一响应头可读,类型下面用 ftyp 验
271
293
  // 站点把未知路径回落到 HTML —— 这类"伪 200"绝不能当成视频存下来(否则每次开机黑屏)。
272
- // 只认文件头:`ftyp` 在偏移 4。比 content-type 更可靠(站点回退页是 HTML,
273
- // 而并行分段之后我们也拿不到单一响应头)。
274
- if (buf.length < 1024 || buf.toString('latin1', 4, 8) !== 'ftyp') {
294
+ // 只认文件头(mp4 的 ftyp / webm 的 EBML)。比 content-type 可靠,
295
+ // 而且并行分段之后本来也拿不到单一响应头。
296
+ const sniff = sniffVideo(buf);
297
+ if (!sniff) {
275
298
  sendJson(res, 422, { ok: false, error: 'not-a-video', detail: ct || 'no content-type' });
276
299
  return;
277
300
  }
@@ -280,6 +303,7 @@ export function registerBootRoutes(webServer, c) {
280
303
  try {
281
304
  writeFileSync(clipMetaFile(id), JSON.stringify({
282
305
  id,
306
+ mime: sniff.mime,
283
307
  title: item.title, durationSec: item.durationSec, bytes: buf.length, sha256: item.sha256 || null,
284
308
  }, null, 2) + '\n');
285
309
  } catch { /* meta 只是锦上添花 */ }
@@ -369,7 +393,12 @@ export function registerBootRoutes(webServer, c) {
369
393
  if (!file || !existsSync(file)) { res.statusCode = 404; res.end('no clip'); return; }
370
394
  const size = statSync(file).size;
371
395
  const range = String((req.headers && req.headers.range) || '');
372
- const type = 'video/mp4';
396
+ // MIME 取自下载时记下的 meta:webm 用 video/webm,否则 video/mp4。
397
+ let type = 'video/mp4';
398
+ try {
399
+ const meta = JSON.parse(readFileSync(clipMetaFile(boot.clipId), 'utf8'));
400
+ if (meta && typeof meta.mime === 'string' && /^video\//.test(meta.mime)) type = meta.mime;
401
+ } catch { /* 没 meta 就按 mp4 */ }
373
402
  // Range:`bytes=start-end` —— Chromium 播放本地大文件时会先发一小段探测请求。
374
403
  const m = /^bytes=(\d*)-(\d*)$/.exec(range.trim());
375
404
  if (m) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-folia",
3
- "version": "4.0.2",
3
+ "version": "4.0.3",
4
4
  "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
5
  "type": "module",
6
6
  "engines": {