dsh-folia 4.0.2 → 4.0.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/routes/boot.js +34 -5
- package/lib/routes/inventory.js +3 -1
- package/package.json +1 -1
package/lib/routes/boot.js
CHANGED
|
@@ -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
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
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
|
-
|
|
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/lib/routes/inventory.js
CHANGED
|
@@ -258,7 +258,9 @@ function foliaItemFromCatalog(it, base) {
|
|
|
258
258
|
// 预览优先级:**站点自己给的 preview 最高**(省掉宿主这一跳,也最省流量);
|
|
259
259
|
// 站点没给才退回宿主缩略图代理。只有预览走小图,`media` 永远是原图 ——
|
|
260
260
|
// 应用壁纸用的是 `media`,所以画质不受影响。
|
|
261
|
-
|
|
261
|
+
// 站点后台已经自动生成缩略图(thumbUrl,webp 小图),优先用它 ——
|
|
262
|
+
// 比宿主自己 ffmpeg 生成更快(少一跳),也比你手传更省事。
|
|
263
|
+
const sitePreview = foliaAbs(base, it && (it.thumbUrl || it.preview || it.poster)) || null;
|
|
262
264
|
const thumb = sitePreview ? null : (type === 'image' ? '/folia/site-thumb?u=' + encodeURIComponent(media) : null);
|
|
263
265
|
const extHit = String(media).match(/\.([a-z0-9]{2,5})(?:[?#]|$)/i);
|
|
264
266
|
const num = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Number(v) : undefined);
|