beauticode-dsh 1.0.19 → 1.0.20

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/client.js +126 -16
  2. package/package.json +1 -1
package/client.js CHANGED
@@ -19,6 +19,7 @@
19
19
  const VIDEO_PROBE_TIMEOUT_MS = 2_000;
20
20
  const DSH_STRUCTURE_TIMEOUT_MS = CLIENT_APPLY_DEADLINE_MS;
21
21
  const FRAME_FALLBACK_MS = 120;
22
+ const VIDEO_FIRST_FRAME_PROGRESS_SEC = 0.03;
22
23
  const VIDEO_STABLE_FRAMES = 3;
23
24
  const VIDEO_STABLE_PROGRESS_SEC = 0.18;
24
25
  const CROSSFADE_MS = 180;
@@ -82,8 +83,11 @@ html[data-bc-fish="true"] #beauticode-bg-stage::after{background:transparent!imp
82
83
  #beauticode-bg-stage[data-bc-transitioning="true"] .beauticode-media-slot[data-bc-role="current"]{opacity:0}
83
84
  #beauticode-bg-stage[data-bc-transitioning="true"] .beauticode-media-slot[data-bc-role="candidate"]{opacity:1}
84
85
  #beauticode-bg-stage .beauticode-media-slot img,#beauticode-bg-stage .beauticode-media-slot video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;pointer-events:none;transition:opacity 120ms ease}
85
- #beauticode-bg-stage .beauticode-media-slot img{z-index:0;opacity:1}
86
- #beauticode-bg-stage .beauticode-media-slot video{z-index:1;opacity:.001}
86
+ #beauticode-bg-stage .beauticode-media-slot img{z-index:2;opacity:1}
87
+ /* Keep cold candidate video paintable. Chromium may defer decoding media that
88
+ is nearly transparent, which deadlocks the first-frame gate. The poster
89
+ covers it until data-bc-video-ready is committed. */
90
+ #beauticode-bg-stage .beauticode-media-slot video{z-index:1;opacity:1}
87
91
  #beauticode-bg-stage .beauticode-media-slot[data-bc-video-ready="true"] img{opacity:0}
88
92
  #beauticode-bg-stage .beauticode-media-slot[data-bc-video-ready="true"] video{opacity:1}
89
93
  @media (prefers-reduced-motion:reduce){#beauticode-bg-stage .beauticode-media-slot,#beauticode-bg-stage .beauticode-media-slot img,#beauticode-bg-stage .beauticode-media-slot video{transition:none!important}}
@@ -641,6 +645,89 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
641
645
  });
642
646
  }
643
647
 
648
+ function waitForPresentedFrame(video, signal, timeoutMs) {
649
+ return new Promise((resolve, reject) => {
650
+ const startedAt = performance.now();
651
+ const initialTime = Number.isFinite(video.currentTime) ? video.currentTime : 0;
652
+ let frameSeen = false;
653
+ let playingSeen =
654
+ !video.paused && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA;
655
+ let settled = false;
656
+ let frameRequest = null;
657
+ const hasFrameCallback = typeof video.requestVideoFrameCallback === "function";
658
+
659
+ const cleanup = () => {
660
+ clearInterval(timer);
661
+ video.removeEventListener("playing", onPlaying);
662
+ video.removeEventListener("timeupdate", check);
663
+ video.removeEventListener("loadeddata", check);
664
+ video.removeEventListener("canplay", check);
665
+ video.removeEventListener("error", failed);
666
+ video.removeEventListener("abort", aborted);
667
+ signal?.removeEventListener?.("abort", aborted);
668
+ if (frameRequest != null && typeof video.cancelVideoFrameCallback === "function") {
669
+ video.cancelVideoFrameCallback(frameRequest);
670
+ }
671
+ };
672
+ const finish = (error) => {
673
+ if (settled) return;
674
+ settled = true;
675
+ cleanup();
676
+ if (error) reject(error);
677
+ else resolve();
678
+ };
679
+ const failed = () => {
680
+ finish(new Error(describeVideoState(video, "视频解码器报告失败")));
681
+ };
682
+ const aborted = () => {
683
+ finish(signal?.aborted ? abortError() : new Error(describeVideoState(video, "视频播放已中止")));
684
+ };
685
+ const check = () => {
686
+ if (settled) return;
687
+ if (video.error) {
688
+ failed();
689
+ return;
690
+ }
691
+ if (!video.paused && video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) {
692
+ playingSeen = true;
693
+ }
694
+ const current = Number.isFinite(video.currentTime) ? video.currentTime : initialTime;
695
+ const progressed = current >= initialTime + VIDEO_FIRST_FRAME_PROGRESS_SEC;
696
+ if (
697
+ video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA &&
698
+ !video.paused &&
699
+ playingSeen &&
700
+ (frameSeen || progressed)
701
+ ) {
702
+ finish();
703
+ return;
704
+ }
705
+ if (performance.now() - startedAt >= timeoutMs) {
706
+ finish(new Error(describeVideoState(video, "视频未在首帧窗口内完成呈现")));
707
+ }
708
+ };
709
+ const onPlaying = () => {
710
+ playingSeen = true;
711
+ check();
712
+ };
713
+ const onFrame = () => {
714
+ frameSeen = true;
715
+ check();
716
+ };
717
+ const timer = setInterval(check, 80);
718
+ video.addEventListener("playing", onPlaying);
719
+ video.addEventListener("timeupdate", check);
720
+ video.addEventListener("loadeddata", check);
721
+ video.addEventListener("canplay", check);
722
+ video.addEventListener("error", failed, { once: true });
723
+ video.addEventListener("abort", aborted, { once: true });
724
+ signal?.addEventListener?.("abort", aborted, { once: true });
725
+ if (hasFrameCallback) frameRequest = video.requestVideoFrameCallback(onFrame);
726
+ if (signal?.aborted) aborted();
727
+ check();
728
+ });
729
+ }
730
+
644
731
  function waitForStablePlayback(video, signal, timeoutMs) {
645
732
  return new Promise((resolve, reject) => {
646
733
  const startedAt = performance.now();
@@ -899,8 +986,8 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
899
986
  if (!slot || slot !== mountedCurrentSlot() || slot.dataset.bcMedia !== payload.media) {
900
987
  return false;
901
988
  }
902
- if (slot.dataset.bcImageUrl !== payload.imageUrl) return false;
903
989
  if (payload.media !== "video") {
990
+ if (slot.dataset.bcImageUrl !== payload.imageUrl) return false;
904
991
  const image = slot.querySelector?.("img");
905
992
  return Boolean(
906
993
  image?.isConnected === true &&
@@ -921,11 +1008,10 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
921
1008
  ) {
922
1009
  return false;
923
1010
  }
924
- const requestedStartAt = Number(payload.startAt);
925
- const normalizedStartAt =
926
- Number.isFinite(requestedStartAt) && requestedStartAt >= 0 ? requestedStartAt : 0;
927
- const appliedStartAt = Number(slot.dataset.bcStartAt);
928
- return Number.isFinite(appliedStartAt) && Math.abs(appliedStartAt - normalizedStartAt) < 0.25;
1011
+ // A local re-import can keep the exact same video handle while producing a
1012
+ // new poster token and resetting startAt. Reuse the live decoder; the
1013
+ // caller updates the poster metadata and seeks in place when needed.
1014
+ return true;
929
1015
  }
930
1016
 
931
1017
  function updateCommittedDom(payload) {
@@ -1115,7 +1201,23 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
1115
1201
  let reusable = slotMatchesPayload(currentSlot, payload);
1116
1202
  if (reusable && payload.media === "video") {
1117
1203
  try {
1118
- await waitForStablePlayback(activeVideo(), signal, Math.min(750, remaining()));
1204
+ const reusableVideo = activeVideo();
1205
+ const requestedStartAt = Number(payload.startAt);
1206
+ const normalizedStartAt =
1207
+ Number.isFinite(requestedStartAt) && requestedStartAt >= 0 ? requestedStartAt : 0;
1208
+ const appliedStartAt = Number(currentSlot.dataset.bcStartAt);
1209
+ if (
1210
+ !Number.isFinite(appliedStartAt) ||
1211
+ Math.abs(appliedStartAt - normalizedStartAt) >= 0.25
1212
+ ) {
1213
+ const nextStartAt = seekVideo(reusableVideo, normalizedStartAt);
1214
+ currentSlot.dataset.bcStartAt = String(nextStartAt);
1215
+ await waitForSeek(reusableVideo, signal, remaining());
1216
+ await waitForVideo(reusableVideo, signal, remaining());
1217
+ await waitForPresentedFrame(reusableVideo, signal, remaining());
1218
+ } else {
1219
+ await waitForStablePlayback(reusableVideo, signal, Math.min(750, remaining()));
1220
+ }
1119
1221
  } catch (error) {
1120
1222
  if (isAbortError(error) || signal?.aborted) throw error;
1121
1223
  reusable = false;
@@ -1124,6 +1226,8 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
1124
1226
  if (reusable) {
1125
1227
  committedPayload = payload;
1126
1228
  currentSlot.dataset.bcGeneration = String(payload.generation);
1229
+ currentSlot.dataset.bcImageUrl = payload.imageUrl;
1230
+ if (payload.videoUrl) currentSlot.dataset.bcVideoUrl = payload.videoUrl;
1127
1231
  updateCommittedDom(payload);
1128
1232
  syncGallery(payload);
1129
1233
  await acknowledgeRender(payload, true, true);
@@ -1150,12 +1254,6 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
1150
1254
  if (payload.media !== "video" || typeof payload.videoUrl !== "string") {
1151
1255
  throw new Error("视频载荷无效");
1152
1256
  }
1153
- await probeVideoSource(
1154
- payload.videoUrl,
1155
- signal,
1156
- Math.min(VIDEO_PROBE_TIMEOUT_MS, remaining()),
1157
- );
1158
- throwIfAborted(signal);
1159
1257
  const video = document.createElement("video");
1160
1258
  video.autoplay = true;
1161
1259
  video.loop = true;
@@ -1172,11 +1270,20 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
1172
1270
  candidateVideo = video;
1173
1271
  video.src = payload.videoUrl;
1174
1272
  video.load();
1273
+ // Start the real media request before the diagnostic Range probe. During
1274
+ // a cold Chromium/decoder start, serial probing used part of the same
1275
+ // eight-second budget without advancing the frame that users can see.
1276
+ const sourceProbe = probeVideoSource(
1277
+ payload.videoUrl,
1278
+ signal,
1279
+ Math.min(VIDEO_PROBE_TIMEOUT_MS, remaining()),
1280
+ );
1175
1281
  const imageReady = loadImage(candidate, payload.imageUrl, signal, remaining());
1176
1282
  // play() is deliberately started alongside media readiness. Chromium is
1177
1283
  // allowed to ignore preload=auto for hidden/background media; waiting for
1178
1284
  // canplay before play() creates a circular stall at readyState=0.
1179
1285
  await Promise.all([
1286
+ sourceProbe,
1180
1287
  imageReady,
1181
1288
  waitForVideo(video, signal, remaining()),
1182
1289
  startCandidatePlayback(video, signal, remaining()),
@@ -1186,7 +1293,10 @@ html[data-bc-fish="true"] #root{opacity:0!important;visibility:hidden!important;
1186
1293
  candidate.dataset.bcStartAt = String(appliedStartAt);
1187
1294
  await waitForSeek(video, signal, remaining());
1188
1295
  await waitForVideo(video, signal, remaining());
1189
- await waitForStablePlayback(video, signal, remaining());
1296
+ // Transaction success means one frame was actually presented. Longer
1297
+ // three-frame stability remains useful for reusing an existing slot, but
1298
+ // must not turn a healthy cold decoder into a false first-frame failure.
1299
+ await waitForPresentedFrame(video, signal, remaining());
1190
1300
  throwIfAborted(signal);
1191
1301
  candidate.dataset.bcVideoReady = "true";
1192
1302
  await commitCandidate(payload, candidate, signal, remaining);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beauticode-dsh",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "Cordis plugin: image/video backgrounds for DeepSeek Harness web.",
5
5
  "type": "module",
6
6
  "main": "./index.mjs",