beauticode-dsh 1.0.5 → 1.0.12
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/README.zh-CN.md +2 -2
- package/agent.mjs +69 -52
- package/cli.js +58 -23
- package/client.js +102 -22
- package/console.js +184 -36
- package/control-client.mjs +6 -4
- package/index.mjs +22 -8
- package/package.json +1 -1
- package/themes/internal-beyond/bg-canvas.png +0 -0
- package/ui-host.mjs +432 -42
- package/vendor/adapter-dsh/bridge.js +2 -4
- package/vendor/adapter-dsh/session.js +60 -23
- package/vendor/adapter-dsh/tray-handoff.js +13 -11
- package/vendor/core/apply-transaction.js +87 -45
- package/vendor/core/background-store.js +211 -351
- package/vendor/core/error-message.js +0 -8
- package/vendor/core/file-lock.js +13 -34
- package/vendor/core/index.js +1 -2
- package/vendor/core/media-server.js +22 -40
- package/vendor/core/media-source.js +36 -0
- package/vendor/core/media-validation.js +26 -19
- package/vendor/core/process-liveness.js +0 -205
- package/vendor/core/skin-pack.js +0 -64
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
该 Cordis 插件向 DSH Web 注入 beautiCode 浏览器客户端,并提供本机鉴权接口。支持图片、MP4、播放位置、静音、摸鱼模式以及清除背景。
|
|
4
4
|
|
|
5
|
-
装好插件并运行 `dsh web`
|
|
5
|
+
装好插件并运行 `dsh web` 后,侧栏「设置」上方会出现「背景」:在 Windows 上点击图片或视频会打开系统文件选择器,选好后在页面内命名,背景立即应用并进入已保存主题;也可以清除、开关声音、切换或删除主题,以及打开「皮肤中心」安装已审核的社区皮肤。本地导入只在 Node 端保存绝对路径,并通过带 Range 支持的回环媒体服务读取原文件,不复制用户选择的图片或整段视频主文件。Windows 强制使用本地引用;原生选择器不可用时会明确报错,不会静默上传或复制媒体。非 Windows 环境保留兼容上传,并明确提示该模式会保存托管副本。网页控制台没有摸鱼;浅色/深色跟 DSH 自己的外观走。下次打开会恢复上次背景。设置 `BEAUTICODE_SKIN_CENTER` 或填写 `skin-center.json` 后,皮肤中心才会显示远程目录。
|
|
6
6
|
|
|
7
7
|
插件也会注册对话工具(`beauticode_*`)和斜杠命令(`/bg`、`/bg-theme`、`/bg-clear`)。不需要托盘;托盘若已在跑则复用它。
|
|
8
8
|
|
|
@@ -40,7 +40,7 @@ npx @deepseek-ai/dsh web
|
|
|
40
40
|
|
|
41
41
|
手动接入时:
|
|
42
42
|
|
|
43
|
-
1. 修改 `cordis.patch.
|
|
43
|
+
1. 修改 `cordis.patch.yml` 中的插件接入项;优先使用上面的安装命令自动完成接线。
|
|
44
44
|
2. 确保 DSH 和 beautiCode 使用相同的 `BEAUTICODE_DATA_ROOT`。
|
|
45
45
|
3. 自己运行 `dsh web`(或带 `--patch`)。
|
|
46
46
|
4. 打开页面后用侧栏「背景」,或输入 `/bg <本机绝对路径>`,或直接跟 AI 说把某个视频/图片设为背景。
|
package/agent.mjs
CHANGED
|
@@ -61,15 +61,20 @@ async function chineseError(error) {
|
|
|
61
61
|
return raw || "操作失败。";
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
async function unwrapApplyResult(result, fallbackMode, message) {
|
|
65
|
-
if (!result || result.ok === false) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
async function unwrapApplyResult(result, fallbackMode, message) {
|
|
65
|
+
if (!result || result.ok === false) {
|
|
66
|
+
const failure = new Error(await chineseError(result?.error || "操作失败。"));
|
|
67
|
+
if (result?.sourceMode != null) failure.sourceMode = result.sourceMode;
|
|
68
|
+
if (result?.timings != null) failure.timings = result.timings;
|
|
69
|
+
throw failure;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
generation: result.generation ?? null,
|
|
74
|
+
mode: result.mode ?? fallbackMode,
|
|
75
|
+
sourceMode: result.sourceMode ?? null,
|
|
76
|
+
timings: result.timings ?? null,
|
|
77
|
+
message,
|
|
73
78
|
...(result.theme
|
|
74
79
|
? {
|
|
75
80
|
theme: {
|
|
@@ -98,7 +103,7 @@ function normalizeActionOptions(dataRootOrOptions) {
|
|
|
98
103
|
: {};
|
|
99
104
|
}
|
|
100
105
|
|
|
101
|
-
function unwrapApply(result, fallbackMode, message) {
|
|
106
|
+
function unwrapApply(result, fallbackMode, message) {
|
|
102
107
|
if (!result || result.ok === false) {
|
|
103
108
|
fail(result?.error || "操作失败。");
|
|
104
109
|
}
|
|
@@ -116,18 +121,24 @@ function unwrapApply(result, fallbackMode, message) {
|
|
|
116
121
|
},
|
|
117
122
|
}
|
|
118
123
|
: {}),
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function presentStatus(status) {
|
|
123
|
-
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function presentStatus(status) {
|
|
128
|
+
const background = status.manifest?.background ?? status.background ?? null;
|
|
129
|
+
return {
|
|
124
130
|
ok: true,
|
|
125
131
|
hostReady: status.hostReady === true || status.sessions > 0,
|
|
126
132
|
sessions: status.sessions ?? 0,
|
|
127
133
|
fish: status.fish === true,
|
|
128
134
|
muted: status.muted !== false,
|
|
129
135
|
tone: status.tone ?? "dark",
|
|
130
|
-
background
|
|
136
|
+
background,
|
|
137
|
+
sourceMode: background
|
|
138
|
+
? background.source?.kind === "local"
|
|
139
|
+
? "local"
|
|
140
|
+
: "managed"
|
|
141
|
+
: "clear",
|
|
131
142
|
themeId: typeof status.themeId === "string" && status.themeId ? status.themeId : null,
|
|
132
143
|
message: formatStatusText(status),
|
|
133
144
|
};
|
|
@@ -167,21 +178,26 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
167
178
|
}
|
|
168
179
|
const effects = effectsForPreset(options?.effects?.preset) || options?.effects || null;
|
|
169
180
|
const persistTheme = options?.persistTheme !== false;
|
|
170
|
-
const themeName =
|
|
171
|
-
String(options?.themeName ?? "").trim() ||
|
|
172
|
-
themeNameFromFilePath(inspected.path, "图片");
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
const themeName =
|
|
182
|
+
String(options?.themeName ?? "").trim() ||
|
|
183
|
+
themeNameFromFilePath(inspected.path, "图片");
|
|
184
|
+
const source = options?.source === "managed" ? "managed" : "local";
|
|
185
|
+
const input = { type: "image", imagePath: inspected.path, source };
|
|
186
|
+
if (effects) input.effects = effects;
|
|
187
|
+
const resolved = await backend();
|
|
188
|
+
if (resolved.kind === "tray") {
|
|
189
|
+
const result = await request({
|
|
190
|
+
method: "POST",
|
|
191
|
+
path: persistTheme ? "/theme/apply" : "/apply/image",
|
|
192
|
+
body: persistTheme
|
|
193
|
+
? { name: themeName, input }
|
|
194
|
+
: {
|
|
195
|
+
imagePath: inspected.path,
|
|
196
|
+
source,
|
|
197
|
+
...(effects ? { effects } : {}),
|
|
198
|
+
},
|
|
199
|
+
signal,
|
|
200
|
+
});
|
|
185
201
|
return unwrapApplyResult(
|
|
186
202
|
result,
|
|
187
203
|
"image",
|
|
@@ -189,10 +205,10 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
189
205
|
? `已将「${result.theme?.name || themeName}」设为背景。`
|
|
190
206
|
: "已将图片设为背景。",
|
|
191
207
|
);
|
|
192
|
-
}
|
|
193
|
-
const applied = persistTheme
|
|
194
|
-
? await resolved.session.
|
|
195
|
-
: await resolved.session.apply(input);
|
|
208
|
+
}
|
|
209
|
+
const applied = persistTheme
|
|
210
|
+
? await resolved.session.applyAndSaveTheme(input, themeName)
|
|
211
|
+
: await resolved.session.apply(input);
|
|
196
212
|
return unwrapApplyResult(
|
|
197
213
|
applied,
|
|
198
214
|
"image",
|
|
@@ -245,11 +261,12 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
245
261
|
fail("beauticode_apply_video 只接受 .mp4 文件。");
|
|
246
262
|
}
|
|
247
263
|
const persistTheme = input?.persistTheme !== false;
|
|
248
|
-
const themeName =
|
|
249
|
-
String(input?.themeName ?? "").trim() ||
|
|
250
|
-
themeNameFromFilePath(inspected.path, "视频");
|
|
251
|
-
const
|
|
252
|
-
const
|
|
264
|
+
const themeName =
|
|
265
|
+
String(input?.themeName ?? "").trim() ||
|
|
266
|
+
themeNameFromFilePath(inspected.path, "视频");
|
|
267
|
+
const source = input?.source === "managed" ? "managed" : "local";
|
|
268
|
+
const body = { videoPath: inspected.path, persistTheme, themeName, source };
|
|
269
|
+
const localInput = { type: "video", videoPath: inspected.path, source };
|
|
253
270
|
if (typeof input.poster === "string" && input.poster.trim()) {
|
|
254
271
|
const poster = await inspectLocalMedia(input.poster);
|
|
255
272
|
if (!poster.ok) fail(poster.error);
|
|
@@ -265,14 +282,14 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
265
282
|
body.startAt = startAt;
|
|
266
283
|
localInput.startAt = startAt;
|
|
267
284
|
}
|
|
268
|
-
const resolved = await backend();
|
|
269
|
-
if (resolved.kind === "tray") {
|
|
270
|
-
const result = await request({
|
|
271
|
-
method: "POST",
|
|
272
|
-
path: "/apply/video",
|
|
273
|
-
body,
|
|
274
|
-
signal,
|
|
275
|
-
});
|
|
285
|
+
const resolved = await backend();
|
|
286
|
+
if (resolved.kind === "tray") {
|
|
287
|
+
const result = await request({
|
|
288
|
+
method: "POST",
|
|
289
|
+
path: persistTheme ? "/theme/apply" : "/apply/video",
|
|
290
|
+
body: persistTheme ? { name: themeName, input: localInput } : body,
|
|
291
|
+
signal,
|
|
292
|
+
});
|
|
276
293
|
return unwrapApplyResult(
|
|
277
294
|
result,
|
|
278
295
|
"video",
|
|
@@ -280,10 +297,10 @@ export function createBeauticodeActions(dataRootOrOptions) {
|
|
|
280
297
|
? `已将「${result.theme?.name || themeName}」设为背景。`
|
|
281
298
|
: "已将视频设为背景。",
|
|
282
299
|
);
|
|
283
|
-
}
|
|
284
|
-
const applied = persistTheme
|
|
285
|
-
? await resolved.session.
|
|
286
|
-
: await resolved.session.apply(localInput);
|
|
300
|
+
}
|
|
301
|
+
const applied = persistTheme
|
|
302
|
+
? await resolved.session.applyAndSaveTheme(localInput, themeName)
|
|
303
|
+
: await resolved.session.apply(localInput);
|
|
287
304
|
return unwrapApplyResult(
|
|
288
305
|
applied,
|
|
289
306
|
"video",
|
package/cli.js
CHANGED
|
@@ -46,9 +46,17 @@ function defaultDataRoot() {
|
|
|
46
46
|
return path.join(os.homedir(), ".beauticode");
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function defaultDshHome() {
|
|
50
|
-
return process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
51
|
-
}
|
|
49
|
+
function defaultDshHome() {
|
|
50
|
+
return process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function defaultPluginHome(dshHome) {
|
|
54
|
+
return path.join(path.resolve(dshHome), "plugins", pluginName);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function legacyDefaultPluginHome() {
|
|
58
|
+
return path.join(defaultDataRoot(), "plugin");
|
|
59
|
+
}
|
|
52
60
|
|
|
53
61
|
function toFileUri(filePath) {
|
|
54
62
|
const full = path.resolve(filePath).replaceAll("\\", "/");
|
|
@@ -222,15 +230,34 @@ async function sameInstalledVersion(dest) {
|
|
|
222
230
|
}
|
|
223
231
|
}
|
|
224
232
|
|
|
225
|
-
async function copyPackage(dest) {
|
|
233
|
+
async function copyPackage(dest) {
|
|
226
234
|
if (await sameInstalledVersion(dest)) return false;
|
|
227
235
|
await fsp.rm(dest, { recursive: true, force: true });
|
|
228
236
|
await fsp.cp(here, dest, {
|
|
229
237
|
recursive: true,
|
|
230
238
|
filter: (source) => shouldCopy(source),
|
|
231
239
|
});
|
|
232
|
-
return true;
|
|
233
|
-
}
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function removeLegacyManagedPlugin(legacyHome, currentHome) {
|
|
244
|
+
if (!legacyHome) return false;
|
|
245
|
+
const legacy = path.resolve(legacyHome);
|
|
246
|
+
if (legacy === path.resolve(currentHome) || !fs.existsSync(legacy)) return false;
|
|
247
|
+
try {
|
|
248
|
+
const pkg = JSON.parse(await fsp.readFile(path.join(legacy, "package.json"), "utf8"));
|
|
249
|
+
if (
|
|
250
|
+
![pluginName, "@beauticode/dsh-plugin"].includes(pkg.name) ||
|
|
251
|
+
!fs.existsSync(path.join(legacy, "index.mjs"))
|
|
252
|
+
) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
await fsp.rm(legacy, { recursive: true, force: true });
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
234
261
|
|
|
235
262
|
async function ensureEngine(dest) {
|
|
236
263
|
const vendor = path.join(dest, "vendor", "adapter-dsh", "index.js");
|
|
@@ -320,7 +347,7 @@ async function removeWebPackageDep(webPackage) {
|
|
|
320
347
|
return true;
|
|
321
348
|
}
|
|
322
349
|
|
|
323
|
-
async function install(opts) {
|
|
350
|
+
async function install(opts) {
|
|
324
351
|
const dest = path.resolve(opts.pluginHome);
|
|
325
352
|
const dshHome = path.resolve(opts.dshHome);
|
|
326
353
|
const webProfile = path.join(dshHome, "profiles", "web");
|
|
@@ -340,21 +367,25 @@ async function install(opts) {
|
|
|
340
367
|
await linkPluginIntoProfile(webProfile, dest);
|
|
341
368
|
await ensureWebPackageDep(webPackage, dest);
|
|
342
369
|
await writePatch(webPatch, packageInsert());
|
|
343
|
-
if (fs.existsSync(homePatch)) {
|
|
370
|
+
if (fs.existsSync(homePatch)) {
|
|
344
371
|
const homeRaw = await fsp.readFile(homePatch, "utf8");
|
|
345
|
-
if (hasBridge(homeRaw)) await removePatch(homePatch);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
console.log(
|
|
349
|
-
console.log(
|
|
372
|
+
if (hasBridge(homeRaw)) await removePatch(homePatch);
|
|
373
|
+
}
|
|
374
|
+
const migrated = await removeLegacyManagedPlugin(opts.legacyPluginHome, dest);
|
|
375
|
+
console.log(`已写入 ${webPatch}`);
|
|
376
|
+
console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`);
|
|
377
|
+
if (migrated) console.log("已迁移 1.0.5 的旧插件目录,已保留主题数据。");
|
|
378
|
+
console.log("请自己运行:npx @deepseek-ai/dsh web");
|
|
350
379
|
return { dest, patch: webPatch };
|
|
351
380
|
}
|
|
352
381
|
|
|
353
|
-
await fsp.mkdir(dshHome, { recursive: true });
|
|
354
|
-
await writePatch(homePatch, fileUriInsert(toFileUri(indexFile)));
|
|
355
|
-
|
|
356
|
-
console.log(
|
|
357
|
-
console.log(
|
|
382
|
+
await fsp.mkdir(dshHome, { recursive: true });
|
|
383
|
+
await writePatch(homePatch, fileUriInsert(toFileUri(indexFile)));
|
|
384
|
+
const migrated = await removeLegacyManagedPlugin(opts.legacyPluginHome, dest);
|
|
385
|
+
console.log(`DSH web profile 还不存在,已写入 ${homePatch}`);
|
|
386
|
+
console.log(copied ? `插件已安装到 ${dest}` : `已复用已安装的插件 ${dest}`);
|
|
387
|
+
if (migrated) console.log("已迁移 1.0.5 的旧插件目录,已保留主题数据。");
|
|
388
|
+
console.log("请自己运行:npx @deepseek-ai/dsh web");
|
|
358
389
|
return { dest, patch: homePatch };
|
|
359
390
|
}
|
|
360
391
|
|
|
@@ -386,11 +417,15 @@ async function uninstall(opts) {
|
|
|
386
417
|
return { removed };
|
|
387
418
|
}
|
|
388
419
|
|
|
389
|
-
export async function runCli(argv = process.argv.slice(2)) {
|
|
390
|
-
const dshHome = argValue(argv, "--dsh-home") || defaultDshHome();
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
const opts = {
|
|
420
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
421
|
+
const dshHome = argValue(argv, "--dsh-home") || defaultDshHome();
|
|
422
|
+
const requestedPluginHome = argValue(argv, "--plugin-home");
|
|
423
|
+
const pluginHome = requestedPluginHome || defaultPluginHome(dshHome);
|
|
424
|
+
const opts = {
|
|
425
|
+
dshHome,
|
|
426
|
+
pluginHome,
|
|
427
|
+
legacyPluginHome: requestedPluginHome ? null : legacyDefaultPluginHome(),
|
|
428
|
+
};
|
|
394
429
|
if (argv.includes("--remove")) return uninstall(opts);
|
|
395
430
|
return install(opts);
|
|
396
431
|
}
|
package/client.js
CHANGED
|
@@ -197,25 +197,103 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
|
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
200
|
+
function describeVideoState(video, phase) {
|
|
201
|
+
const mediaErrorNames = {
|
|
202
|
+
1: "MEDIA_ERR_ABORTED",
|
|
203
|
+
2: "MEDIA_ERR_NETWORK",
|
|
204
|
+
3: "MEDIA_ERR_DECODE",
|
|
205
|
+
4: "MEDIA_ERR_SRC_NOT_SUPPORTED",
|
|
206
|
+
};
|
|
207
|
+
const code = Number(video?.error?.code) || 0;
|
|
208
|
+
const mediaError = code ? mediaErrorNames[code] || `MEDIA_ERR_${code}` : "none";
|
|
209
|
+
return `${phase};mediaError=${mediaError};readyState=${video?.readyState ?? -1};networkState=${video?.networkState ?? -1};paused=${Boolean(video?.paused)}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function waitForVideo(video, timeoutMs = 20_000) {
|
|
213
|
+
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return Promise.resolve();
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const done = () => {
|
|
216
|
+
cleanup();
|
|
217
|
+
resolve();
|
|
218
|
+
};
|
|
219
|
+
const failed = () => {
|
|
220
|
+
cleanup();
|
|
221
|
+
reject(new Error(describeVideoState(video, "MP4 加载或解码失败")));
|
|
222
|
+
};
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
cleanup();
|
|
225
|
+
reject(new Error(describeVideoState(video, "等待视频首帧超时")));
|
|
226
|
+
}, timeoutMs);
|
|
227
|
+
const cleanup = () => {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
video.removeEventListener("loadeddata", done);
|
|
230
|
+
video.removeEventListener("error", failed);
|
|
231
|
+
};
|
|
215
232
|
video.addEventListener("loadeddata", done, { once: true });
|
|
216
233
|
video.addEventListener("error", failed, { once: true });
|
|
217
|
-
});
|
|
218
|
-
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function waitForStablePlayback(video, timeoutMs = 8_000) {
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const startedAt = performance.now();
|
|
240
|
+
const initialTime = Number.isFinite(video.currentTime) ? video.currentTime : 0;
|
|
241
|
+
let frameSeen = false;
|
|
242
|
+
let settled = false;
|
|
243
|
+
let frameRequest = null;
|
|
244
|
+
|
|
245
|
+
const cleanup = () => {
|
|
246
|
+
clearInterval(timer);
|
|
247
|
+
video.removeEventListener("playing", check);
|
|
248
|
+
video.removeEventListener("timeupdate", check);
|
|
249
|
+
video.removeEventListener("error", failed);
|
|
250
|
+
if (frameRequest != null && typeof video.cancelVideoFrameCallback === "function") {
|
|
251
|
+
video.cancelVideoFrameCallback(frameRequest);
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const finish = (error) => {
|
|
255
|
+
if (settled) return;
|
|
256
|
+
settled = true;
|
|
257
|
+
cleanup();
|
|
258
|
+
if (error) reject(error);
|
|
259
|
+
else resolve();
|
|
260
|
+
};
|
|
261
|
+
const failed = () => {
|
|
262
|
+
finish(new Error(describeVideoState(video, "视频解码器报告失败")));
|
|
263
|
+
};
|
|
264
|
+
const check = () => {
|
|
265
|
+
if (settled) return;
|
|
266
|
+
if (video.error) {
|
|
267
|
+
failed();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const progressed =
|
|
271
|
+
Number.isFinite(video.currentTime) && video.currentTime >= initialTime + 0.08;
|
|
272
|
+
if (
|
|
273
|
+
video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA &&
|
|
274
|
+
!video.paused &&
|
|
275
|
+
(frameSeen || progressed)
|
|
276
|
+
) {
|
|
277
|
+
finish();
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (performance.now() - startedAt >= timeoutMs) {
|
|
281
|
+
finish(new Error(describeVideoState(video, "视频未在稳定窗口内输出首帧")));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const timer = setInterval(check, 100);
|
|
285
|
+
video.addEventListener("playing", check);
|
|
286
|
+
video.addEventListener("timeupdate", check);
|
|
287
|
+
video.addEventListener("error", failed, { once: true });
|
|
288
|
+
if (typeof video.requestVideoFrameCallback === "function") {
|
|
289
|
+
frameRequest = video.requestVideoFrameCallback(() => {
|
|
290
|
+
frameSeen = true;
|
|
291
|
+
check();
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
check();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
219
297
|
|
|
220
298
|
function seekVideo(video, value) {
|
|
221
299
|
const requested = Number(value);
|
|
@@ -299,11 +377,13 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
|
|
|
299
377
|
video.poster = payload.imageUrl;
|
|
300
378
|
video.src = payload.videoUrl;
|
|
301
379
|
node.replaceChildren(image, video);
|
|
302
|
-
await waitForVideo(video);
|
|
303
|
-
if (activePayload !== payload) return;
|
|
304
|
-
seekVideo(video, payload.startAt);
|
|
305
|
-
await playWithPreference(video);
|
|
306
|
-
|
|
380
|
+
await waitForVideo(video);
|
|
381
|
+
if (activePayload !== payload) return;
|
|
382
|
+
seekVideo(video, payload.startAt);
|
|
383
|
+
await playWithPreference(video);
|
|
384
|
+
await waitForStablePlayback(video);
|
|
385
|
+
if (activePayload !== payload) return;
|
|
386
|
+
document.documentElement.dataset.bcActive = "true";
|
|
307
387
|
document.documentElement.dataset.bcMedia = "video";
|
|
308
388
|
const visible = video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && !video.paused;
|
|
309
389
|
await acknowledgeRender(payload, visible, visible, visible ? null : "视频未开始播放");
|