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.
- package/client.js +126 -16
- 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:
|
|
86
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|