dsh-folia 2.5.9 → 2.6.1
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/boot/index.js +19 -1
- package/lib/client.js +12 -1
- package/lib/routes/boot.js +68 -5
- package/package.json +1 -1
package/lib/boot/index.js
CHANGED
|
@@ -53,6 +53,9 @@ export function readBoot() {
|
|
|
53
53
|
// 最长停留:安全上限,不是「正常退出点」。默认 20 秒,足够一集 15 秒的片子播完。
|
|
54
54
|
maxHoldMs: Number.isFinite(Number(raw.maxHoldMs)) ? Math.min(60000, Math.max(3000, Number(raw.maxHoldMs))) : 20000,
|
|
55
55
|
sound: raw.sound === true,
|
|
56
|
+
// 音量 0~1。带声音自动播放会被浏览器策略拦,所以 `sound` 默认关;
|
|
57
|
+
// 打开后脚本先试带声音,被拦就退回静音重试一次(画面优先,绝不黑屏)。
|
|
58
|
+
volume: Number.isFinite(Number(raw.volume)) ? Math.min(1, Math.max(0, Number(raw.volume))) : 1,
|
|
56
59
|
};
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -144,8 +147,22 @@ export const BOOT_SCREEN_SOURCE = String.raw`
|
|
|
144
147
|
video.addEventListener("error", teardown, { once: true });
|
|
145
148
|
root.appendChild(video);
|
|
146
149
|
(doc.body || doc.documentElement).appendChild(root);
|
|
150
|
+
// 声音:只有用户显式开启才带声;否则静音(否则 Chromium 直接拒绝自动播放)。
|
|
151
|
+
try {
|
|
152
|
+
if (cfg.sound) { video.muted = false; video.volume = (typeof cfg.volume === "number" ? cfg.volume : 1); }
|
|
153
|
+
else { video.muted = true; }
|
|
154
|
+
} catch (e) {}
|
|
147
155
|
var p = video.play();
|
|
148
|
-
if (p && typeof p.catch === "function")
|
|
156
|
+
if (p && typeof p.catch === "function") {
|
|
157
|
+
p.catch(function () {
|
|
158
|
+
// 带声音被自动播放策略拦住了:退回静音再试一次 —— 画面先出来最重要。
|
|
159
|
+
try {
|
|
160
|
+
video.muted = true;
|
|
161
|
+
var p2 = video.play();
|
|
162
|
+
if (p2 && typeof p2.catch === "function") p2.catch(function () { teardown(); });
|
|
163
|
+
} catch (e) { teardown(); }
|
|
164
|
+
});
|
|
165
|
+
}
|
|
149
166
|
} catch (e) {
|
|
150
167
|
teardown();
|
|
151
168
|
return;
|
|
@@ -213,6 +230,7 @@ export function installBootInjection(ctx, note) {
|
|
|
213
230
|
maxHoldMs: boot.maxHoldMs,
|
|
214
231
|
enterMode: boot.enterMode,
|
|
215
232
|
sound: boot.sound,
|
|
233
|
+
volume: boot.volume,
|
|
216
234
|
};
|
|
217
235
|
table.push({ kind: 'script', placement: 'head', text: 'globalThis.__FOLIA_BOOT__=' + JSON.stringify(cfg) });
|
|
218
236
|
table.push({ kind: 'script', placement: 'head', text: BOOT_SCREEN_SOURCE });
|
package/lib/client.js
CHANGED
|
@@ -17070,7 +17070,7 @@ window.__ModuleLoader__.load({
|
|
|
17070
17070
|
));
|
|
17071
17071
|
|
|
17072
17072
|
children.push(React.createElement("div", { key: "hint", className: "we-picker__hint", style: { margin: "2px 0 6px", opacity: 0.75 } },
|
|
17073
|
-
weT("
|
|
17073
|
+
weT("下载一条并「使用」后,每次启动都会先播它;默认播完才淡出进入界面。点击或按 Esc 可立即跳过。「带声音」若被浏览器的自动播放策略拦下,会自动退回静音播放。")));
|
|
17074
17074
|
|
|
17075
17075
|
if (state && state.error && !clips.length) {
|
|
17076
17076
|
children.push(React.createElement("div", { key: "err", className: "we-picker__hint", style: { color: "#ff9a9a" } },
|
|
@@ -17108,6 +17108,17 @@ window.__ModuleLoader__.load({
|
|
|
17108
17108
|
),
|
|
17109
17109
|
),
|
|
17110
17110
|
opt(weT("淡出时长"), "fadeMs", [[400, "0.4 秒"], [600, "0.6 秒"], [900, "0.9 秒"], [1200, "1.2 秒"], [1600, "1.6 秒"]]),
|
|
17111
|
+
// 声音:默认关(带声音的自动播放会被浏览器策略拦)。打开后若仍被拦,
|
|
17112
|
+
// 脚本会自动退回静音播放 —— 画面优先,不会黑屏。
|
|
17113
|
+
React.createElement("label", { style: { display: "flex", gap: "6px", alignItems: "center", fontSize: "12px" } },
|
|
17114
|
+
React.createElement("input", {
|
|
17115
|
+
type: "checkbox",
|
|
17116
|
+
checked: Boolean(state.sound),
|
|
17117
|
+
disabled: Boolean(busy),
|
|
17118
|
+
onChange: (e) => act("settings", { sound: e.target.checked }, ""),
|
|
17119
|
+
}),
|
|
17120
|
+
weT("带声音"),
|
|
17121
|
+
),
|
|
17111
17122
|
opt(weT("最短显示"), "minShowMs", [[0, "不限制"], [600, "0.6 秒"], [1200, "1.2 秒"], [2000, "2 秒"]]),
|
|
17112
17123
|
opt(weT("最长停留"), "maxHoldMs", [[10000, "10 秒"], [20000, "20 秒(默认)"], [30000, "30 秒"], [60000, "60 秒"]]),
|
|
17113
17124
|
));
|
package/lib/routes/boot.js
CHANGED
|
@@ -54,6 +54,64 @@ function readBodyText(req, maxBytes) {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* 并行分块下载。
|
|
59
|
+
*
|
|
60
|
+
* 为什么要它:单连接下载 8~50 MB 的片子,在跨境线路上经常只有 200~400 KB/s,
|
|
61
|
+
* 一条连接就是那么慢;而站点(R2 前面的代理)**支持 Range**(实测 206 +
|
|
62
|
+
* accept-ranges: bytes)。把文件切成 N 段并发拉,能把单连接的带宽上限绕过去。
|
|
63
|
+
*
|
|
64
|
+
* 策略:先探一段 `bytes=0-0` 读 `content-range` 拿总长;能并行就并行,不能就退回
|
|
65
|
+
* 单次 GET。任何一段失败都退回单次 GET(宁可慢,不可错)。
|
|
66
|
+
*/
|
|
67
|
+
async function fetchClipBytes(url, chunks = 4) {
|
|
68
|
+
const single = async () => {
|
|
69
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
70
|
+
if (!r.ok) throw new Error('http-' + r.status);
|
|
71
|
+
return Buffer.from(await r.arrayBuffer());
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// 探测:只要第 0 字节,顺便从 content-range 里读出总长
|
|
75
|
+
let total = 0;
|
|
76
|
+
try {
|
|
77
|
+
const probe = await fetch(url, { headers: { range: 'bytes=0-0' }, signal: AbortSignal.timeout(15000) });
|
|
78
|
+
if (probe.status === 206) {
|
|
79
|
+
const cr = String(probe.headers.get('content-range') || '');
|
|
80
|
+
const m = /\/(\d+)$/.exec(cr);
|
|
81
|
+
if (m) total = Number(m[1]);
|
|
82
|
+
// 这一字节不用留
|
|
83
|
+
await probe.arrayBuffer().catch(() => {});
|
|
84
|
+
} else {
|
|
85
|
+
await probe.arrayBuffer().catch(() => {});
|
|
86
|
+
}
|
|
87
|
+
} catch { total = 0; }
|
|
88
|
+
|
|
89
|
+
if (!total || total < 2 * 1024 * 1024) return single(); // 小文件并行没意义
|
|
90
|
+
|
|
91
|
+
const n = Math.max(2, Math.min(chunks, 6));
|
|
92
|
+
const part = Math.ceil(total / n);
|
|
93
|
+
try {
|
|
94
|
+
const bufs = await Promise.all(Array.from({ length: n }, (_, i) => {
|
|
95
|
+
const start = i * part;
|
|
96
|
+
const end = Math.min(total - 1, start + part - 1);
|
|
97
|
+
return fetch(url, {
|
|
98
|
+
headers: { range: 'bytes=' + start + '-' + end },
|
|
99
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
100
|
+
}).then(async (r) => {
|
|
101
|
+
if (r.status !== 206) throw new Error('range-' + r.status);
|
|
102
|
+
const b = Buffer.from(await r.arrayBuffer());
|
|
103
|
+
if (b.length !== end - start + 1) throw new Error('short-chunk');
|
|
104
|
+
return b;
|
|
105
|
+
});
|
|
106
|
+
}));
|
|
107
|
+
const joined = Buffer.concat(bufs);
|
|
108
|
+
if (joined.length !== total) return single(); // 长度对不上:别赌,重来
|
|
109
|
+
return joined;
|
|
110
|
+
} catch {
|
|
111
|
+
return single();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
57
115
|
/** 读请求体里的 `{ id }`。 */
|
|
58
116
|
async function readIdBody(req) {
|
|
59
117
|
try {
|
|
@@ -163,6 +221,8 @@ export function registerBootRoutes(webServer, c) {
|
|
|
163
221
|
enabled: boot.enabled,
|
|
164
222
|
clipId: boot.clipId,
|
|
165
223
|
enterMode: boot.enterMode,
|
|
224
|
+
sound: boot.sound,
|
|
225
|
+
volume: boot.volume,
|
|
166
226
|
fadeMs: boot.fadeMs,
|
|
167
227
|
minShowMs: boot.minShowMs,
|
|
168
228
|
maxHoldMs: boot.maxHoldMs,
|
|
@@ -186,12 +246,13 @@ export function registerBootRoutes(webServer, c) {
|
|
|
186
246
|
const dest = clipFile(id);
|
|
187
247
|
if (!dest) { sendJson(res, 400, { ok: false, error: 'bad-id' }); return; }
|
|
188
248
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const ct =
|
|
192
|
-
const buf = Buffer.from(await r.arrayBuffer());
|
|
249
|
+
// 分块并发下载:跨境单连接常常只有几百 KB/s,4 路并发能明显缩短等待。
|
|
250
|
+
const buf = await fetchClipBytes(item.url, 4);
|
|
251
|
+
const ct = 'video/mp4'; // 并行分段之后没有单一响应头可读,类型下面用 ftyp 验
|
|
193
252
|
// 站点把未知路径回落到 HTML —— 这类"伪 200"绝不能当成视频存下来(否则每次开机黑屏)。
|
|
194
|
-
|
|
253
|
+
// 只认文件头:`ftyp` 在偏移 4。比 content-type 更可靠(站点回退页是 HTML,
|
|
254
|
+
// 而并行分段之后我们也拿不到单一响应头)。
|
|
255
|
+
if (buf.length < 1024 || buf.toString('latin1', 4, 8) !== 'ftyp') {
|
|
195
256
|
sendJson(res, 422, { ok: false, error: 'not-a-video', detail: ct || 'no content-type' });
|
|
196
257
|
return;
|
|
197
258
|
}
|
|
@@ -253,6 +314,8 @@ export function registerBootRoutes(webServer, c) {
|
|
|
253
314
|
} catch { /* 坏请求 = 不改 */ }
|
|
254
315
|
const boot = writeBoot({
|
|
255
316
|
...(patch.enterMode !== undefined ? { enterMode: String(patch.enterMode) } : {}),
|
|
317
|
+
...(patch.sound !== undefined ? { sound: Boolean(patch.sound) } : {}),
|
|
318
|
+
...(patch.volume !== undefined ? { volume: Number(patch.volume) } : {}),
|
|
256
319
|
...(patch.fadeMs !== undefined ? { fadeMs: Number(patch.fadeMs) } : {}),
|
|
257
320
|
...(patch.minShowMs !== undefined ? { minShowMs: Number(patch.minShowMs) } : {}),
|
|
258
321
|
...(patch.maxHoldMs !== undefined ? { maxHoldMs: Number(patch.maxHoldMs) } : {}),
|