@hyperframes/engine 0.7.42 → 0.7.44
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/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/audioMixer.d.ts.map +1 -1
- package/dist/services/audioMixer.js +34 -35
- package/dist/services/audioMixer.js.map +1 -1
- package/dist/services/frameCapture.d.ts +17 -1
- package/dist/services/frameCapture.d.ts.map +1 -1
- package/dist/services/frameCapture.js +154 -53
- package/dist/services/frameCapture.js.map +1 -1
- package/dist/services/parallelCoordinator.d.ts +18 -2
- package/dist/services/parallelCoordinator.d.ts.map +1 -1
- package/dist/services/parallelCoordinator.js +99 -4
- package/dist/services/parallelCoordinator.js.map +1 -1
- package/dist/services/referenceResolver.d.ts +34 -0
- package/dist/services/referenceResolver.d.ts.map +1 -0
- package/dist/services/referenceResolver.js +88 -0
- package/dist/services/referenceResolver.js.map +1 -0
- package/dist/services/streamingEncoder.d.ts +8 -0
- package/dist/services/streamingEncoder.d.ts.map +1 -1
- package/dist/services/streamingEncoder.js +29 -10
- package/dist/services/streamingEncoder.js.map +1 -1
- package/dist/services/videoFrameExtractor.d.ts.map +1 -1
- package/dist/services/videoFrameExtractor.js +2 -73
- package/dist/services/videoFrameExtractor.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -270,6 +270,32 @@ async function initDrawElementOrTransparentBackground(session, page, logInitPhas
|
|
|
270
270
|
await armStaticDedup(session, page, logInitPhase);
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
|
+
// Capability gate: `canvas.drawElementImage` is an unlaunched Blink feature
|
|
274
|
+
// that only exists on recent Dev/Canary Chrome builds (~151+); it is absent
|
|
275
|
+
// from Stable and from most pinned/system Chrome installs. The
|
|
276
|
+
// `--enable-features=CanvasDrawElement` flag no-ops silently on a build that
|
|
277
|
+
// doesn't implement it, so without this probe the first drawElementImage()
|
|
278
|
+
// call throws `TypeError: ... is not a function` deep inside the capture
|
|
279
|
+
// loop and takes the whole render down instead of falling back (HF#2060).
|
|
280
|
+
// Cheap (no paint-wait) and must run before any other drawElement work.
|
|
281
|
+
// Not gated by forceDE (HF_FORCE_DRAWELEMENT, an R&D knob that bypasses the
|
|
282
|
+
// quality gates below to measure raw damage) — there's no "forced but
|
|
283
|
+
// degraded" mode for a method that doesn't exist, only a crash, so this
|
|
284
|
+
// always routes to the fallback instead.
|
|
285
|
+
const supportsDrawElement = await page.evaluate(() => {
|
|
286
|
+
const c = document.createElement("canvas");
|
|
287
|
+
const ctx = c.getContext("2d");
|
|
288
|
+
return (typeof ctx?.drawElementImage === "function");
|
|
289
|
+
});
|
|
290
|
+
if (!supportsDrawElement) {
|
|
291
|
+
session.deGateReason = "unsupported_chrome";
|
|
292
|
+
console.log(`[engine] fast capture: falling back to ${session.launchCaptureMode} capture — ` +
|
|
293
|
+
"this Chrome build does not implement canvas.drawElementImage (Dev/Canary-only " +
|
|
294
|
+
"feature, ~151+); run `hyperframes browser ensure --force` to fetch a supported " +
|
|
295
|
+
"build, or set HYPERFRAMES_BROWSER_PATH to one.");
|
|
296
|
+
await routeToFallback();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
273
299
|
// SwiftShader gate: drawElement's only advantage is skipping the GPU→CPU
|
|
274
300
|
// screenshot-readback IPC. On a software rasterizer (Docker/CI, no GPU) both
|
|
275
301
|
// paths block on identical software raster, so drawElement is parity-or-slower
|
|
@@ -600,6 +626,7 @@ export async function createCaptureSession(serverUrl, outputDir, options, onBefo
|
|
|
600
626
|
onBeforeCapture,
|
|
601
627
|
isInitialized: false,
|
|
602
628
|
browserConsoleBuffer: [],
|
|
629
|
+
scriptLoadFailures: [],
|
|
603
630
|
capturePerf: {
|
|
604
631
|
frames: 0,
|
|
605
632
|
seekMs: 0,
|
|
@@ -663,16 +690,6 @@ export function formatConsoleDiagnostic(type, text, locationUrl) {
|
|
|
663
690
|
: "[Browser]";
|
|
664
691
|
return { text: `${prefix} ${text}`, suppressHostLog: false };
|
|
665
692
|
}
|
|
666
|
-
async function pollPageExpression(page, expression, timeoutMs, intervalMs = 100) {
|
|
667
|
-
const deadline = Date.now() + timeoutMs;
|
|
668
|
-
while (Date.now() < deadline) {
|
|
669
|
-
const ready = Boolean(await page.evaluate(expression));
|
|
670
|
-
if (ready)
|
|
671
|
-
return true;
|
|
672
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
673
|
-
}
|
|
674
|
-
return Boolean(await page.evaluate(expression));
|
|
675
|
-
}
|
|
676
693
|
const HF_READY_DIAGNOSTIC_EXPR = `(function() {
|
|
677
694
|
var hf = window.__hf;
|
|
678
695
|
var player = window.__player;
|
|
@@ -773,7 +790,14 @@ async function pollHfReady(page, timeoutMs, intervalMs = 100) {
|
|
|
773
790
|
` State: __hf=${diag.hasHf}, seek=${diag.hasSeek}, player=${diag.hasPlayer}, ` +
|
|
774
791
|
`renderReady=${diag.renderReady}, duration=${diag.duration}`);
|
|
775
792
|
}
|
|
776
|
-
async function pollSubCompositionTimelines(page, timeoutMs, intervalMs = 150
|
|
793
|
+
export async function pollSubCompositionTimelines(page, timeoutMs, intervalMs = 150,
|
|
794
|
+
// Fail-fast hook: when a SCRIPT resource failed to load (404 / request
|
|
795
|
+
// failure), the timeline registration it carried can never arrive — the
|
|
796
|
+
// full-timeout wait buys nothing (measured: a 705-render spike at the 45s
|
|
797
|
+
// setup bucket in 30 days of wild local renders, ~1% of renders, each also
|
|
798
|
+
// shipping silently-broken animations). Once failures are present the poll
|
|
799
|
+
// is cut to `scriptFailureGraceMs` from its start.
|
|
800
|
+
getScriptLoadFailures, scriptFailureGraceMs = 2_000) {
|
|
777
801
|
// Hosts may opt out of the timeline wait with `data-no-timeline` —
|
|
778
802
|
// compositions driven purely by CSS animations / rAF (the render-compat
|
|
779
803
|
// contract) never register window.__timelines[id], and without the opt-out
|
|
@@ -790,7 +814,28 @@ async function pollSubCompositionTimelines(page, timeoutMs, intervalMs = 150) {
|
|
|
790
814
|
}
|
|
791
815
|
return true;
|
|
792
816
|
})()`;
|
|
793
|
-
const
|
|
817
|
+
const start = Date.now();
|
|
818
|
+
const deadline = start + timeoutMs;
|
|
819
|
+
let ready = false;
|
|
820
|
+
let scriptFailureBail = false;
|
|
821
|
+
for (;;) {
|
|
822
|
+
ready = Boolean(await page.evaluate(expression));
|
|
823
|
+
if (ready)
|
|
824
|
+
break;
|
|
825
|
+
const now = Date.now();
|
|
826
|
+
if (now >= deadline)
|
|
827
|
+
break;
|
|
828
|
+
const failures = getScriptLoadFailures?.() ?? [];
|
|
829
|
+
if (failures.length > 0 && now - start >= scriptFailureGraceMs) {
|
|
830
|
+
scriptFailureBail = true;
|
|
831
|
+
console.warn(`[FrameCapture] Sub-composition timeline wait cut short after ${now - start}ms: ` +
|
|
832
|
+
`script resource(s) failed to load (${failures.join(", ")}) — ` +
|
|
833
|
+
`the timeline registration they carry can never arrive. ` +
|
|
834
|
+
`Fix the script reference; the render proceeds without those animations.`);
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
838
|
+
}
|
|
794
839
|
// Always force a timeline rebind once sub-composition timelines are
|
|
795
840
|
// confirmed present. The previous implementation only called rebind
|
|
796
841
|
// when the timeline count grew during the poll, which missed the case
|
|
@@ -804,23 +849,32 @@ async function pollSubCompositionTimelines(page, timeoutMs, intervalMs = 150) {
|
|
|
804
849
|
window.__hfForceTimelineRebind();
|
|
805
850
|
}
|
|
806
851
|
})()`);
|
|
852
|
+
return "ready";
|
|
807
853
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
854
|
+
// Enumerate the still-unregistered composition ids regardless of bail
|
|
855
|
+
// reason — a script-failure bail used to skip this entirely, so a render
|
|
856
|
+
// with multiple sub-compositions only named the failed script URL(s), not
|
|
857
|
+
// which composition(s) it was still waiting on (review).
|
|
858
|
+
const missing = await page.evaluate(`(function() {
|
|
859
|
+
var hosts = document.querySelectorAll("[data-composition-id]");
|
|
860
|
+
var timelines = window.__timelines || {};
|
|
861
|
+
var m = [];
|
|
862
|
+
for (var i = 0; i < hosts.length; i++) {
|
|
863
|
+
if (hosts[i].hasAttribute("data-no-timeline")) continue;
|
|
864
|
+
var id = hosts[i].getAttribute("data-composition-id");
|
|
865
|
+
if (id && !timelines[id]) m.push(id);
|
|
866
|
+
}
|
|
867
|
+
return m.join(", ");
|
|
868
|
+
})()`);
|
|
869
|
+
if (scriptFailureBail) {
|
|
870
|
+
console.warn(`[FrameCapture] Composition(s) still waiting on the failed script: ${missing}.`);
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
820
873
|
console.warn(`[FrameCapture] Sub-composition timelines not registered after ${timeoutMs}ms: ${missing}. ` +
|
|
821
874
|
`Compositions that load data asynchronously (e.g. fetch) must register window.__timelines[id] after setup completes. ` +
|
|
822
875
|
`Compositions intentionally driven without GSAP timelines (CSS animations / rAF) can mark the host with data-no-timeline to skip this wait.`);
|
|
823
876
|
}
|
|
877
|
+
return scriptFailureBail ? "script_failure" : "timeout";
|
|
824
878
|
}
|
|
825
879
|
async function pollVideosReady(page, skipIds, timeoutMs, intervalMs = 100) {
|
|
826
880
|
const check = async () => {
|
|
@@ -965,6 +1019,15 @@ async function waitForOptionalTailwindReady(page, timeoutMs) {
|
|
|
965
1019
|
throw new Error(`[FrameCapture] window.__tailwindReady not resolved after ${timeoutMs}ms. Tailwind browser runtime must finish before frame capture starts.`);
|
|
966
1020
|
}
|
|
967
1021
|
}
|
|
1022
|
+
// A 4xx `response` and a `requestfailed` can both fire for the same script
|
|
1023
|
+
// (e.g. a `requestfailed` following the 4xx), and repeated <script> tags for
|
|
1024
|
+
// the same URL duplicate it further — dedupe so the fail-fast warning names
|
|
1025
|
+
// each failed URL once.
|
|
1026
|
+
function recordScriptLoadFailure(session, url) {
|
|
1027
|
+
if (!session.scriptLoadFailures.includes(url)) {
|
|
1028
|
+
session.scriptLoadFailures.push(url);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
968
1031
|
// fallow-ignore-next-line unit-size
|
|
969
1032
|
export async function initializeSession(session) {
|
|
970
1033
|
const { page, serverUrl } = session;
|
|
@@ -990,6 +1053,9 @@ export async function initializeSession(session) {
|
|
|
990
1053
|
appendBrowserDiagnostic(session, text);
|
|
991
1054
|
});
|
|
992
1055
|
page.on("requestfailed", (request) => {
|
|
1056
|
+
if (request.resourceType() === "script") {
|
|
1057
|
+
recordScriptLoadFailure(session, request.url());
|
|
1058
|
+
}
|
|
993
1059
|
appendBrowserDiagnostic(session, formatRequestFailureDiagnostic({
|
|
994
1060
|
method: request.method(),
|
|
995
1061
|
resourceType: request.resourceType(),
|
|
@@ -1002,6 +1068,9 @@ export async function initializeSession(session) {
|
|
|
1002
1068
|
if (status < 400)
|
|
1003
1069
|
return;
|
|
1004
1070
|
const request = response.request();
|
|
1071
|
+
if (request.resourceType() === "script") {
|
|
1072
|
+
recordScriptLoadFailure(session, response.url());
|
|
1073
|
+
}
|
|
1005
1074
|
appendBrowserDiagnostic(session, formatHttpErrorDiagnostic({
|
|
1006
1075
|
method: request.method(),
|
|
1007
1076
|
resourceType: request.resourceType(),
|
|
@@ -1051,8 +1120,8 @@ export async function initializeSession(session) {
|
|
|
1051
1120
|
const pageReadyTimeout = session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout;
|
|
1052
1121
|
await pollHfReady(page, pageReadyTimeout);
|
|
1053
1122
|
logInitPhase("pollHfReady complete");
|
|
1054
|
-
await pollSubCompositionTimelines(page, pageReadyTimeout);
|
|
1055
|
-
logInitPhase(
|
|
1123
|
+
session.subTimelineWaitOutcome = await pollSubCompositionTimelines(page, pageReadyTimeout, undefined, () => session.scriptLoadFailures);
|
|
1124
|
+
logInitPhase(`pollSubCompositionTimelines complete (${session.subTimelineWaitOutcome})`);
|
|
1056
1125
|
await applyVideoMetadataHints(page, session.options.videoMetadataHints);
|
|
1057
1126
|
logInitPhase("applyVideoMetadataHints complete");
|
|
1058
1127
|
// Run independent readiness checks in parallel — videos, images, fonts,
|
|
@@ -1169,8 +1238,8 @@ export async function initializeSession(session) {
|
|
|
1169
1238
|
warmupState.running = false;
|
|
1170
1239
|
throw err;
|
|
1171
1240
|
}
|
|
1172
|
-
await pollSubCompositionTimelines(page, pageReadyTimeout);
|
|
1173
|
-
logInitPhase(
|
|
1241
|
+
session.subTimelineWaitOutcome = await pollSubCompositionTimelines(page, pageReadyTimeout, undefined, () => session.scriptLoadFailures);
|
|
1242
|
+
logInitPhase(`pollSubCompositionTimelines complete (${session.subTimelineWaitOutcome})`);
|
|
1174
1243
|
await applyVideoMetadataHints(page, session.options.videoMetadataHints);
|
|
1175
1244
|
logInitPhase("applyVideoMetadataHints complete");
|
|
1176
1245
|
// Run independent readiness checks in parallel — videos, images, fonts,
|
|
@@ -1561,13 +1630,16 @@ export async function verifyStaticFramesSafe(session, page, staticFrames, fps, s
|
|
|
1561
1630
|
else
|
|
1562
1631
|
runs.push({ a: f, b: f });
|
|
1563
1632
|
}
|
|
1564
|
-
const
|
|
1633
|
+
const seekToFrame = async (frameIdx) => {
|
|
1565
1634
|
const t = quantizeTimeToFrame(frameIdx / fps, fps);
|
|
1566
1635
|
await page.evaluate((tt) => {
|
|
1567
1636
|
const hf = window.__hf;
|
|
1568
1637
|
if (hf && typeof hf.seek === "function")
|
|
1569
|
-
hf.seek(tt);
|
|
1638
|
+
hf.seek(tt, { suppressEvents: true });
|
|
1570
1639
|
}, t);
|
|
1640
|
+
};
|
|
1641
|
+
const seekCapture = async (frameIdx) => {
|
|
1642
|
+
await seekToFrame(frameIdx);
|
|
1571
1643
|
return pageScreenshotCapture(page, session.options);
|
|
1572
1644
|
};
|
|
1573
1645
|
// Verify EVERY run in order (no longest-first truncation that would leave runs armed
|
|
@@ -1584,26 +1656,31 @@ export async function verifyStaticFramesSafe(session, page, staticFrames, fps, s
|
|
|
1584
1656
|
// thorough checking. `frames.length` approximates total interior checks; a 3x
|
|
1585
1657
|
// margin absorbs per-run anchor overhead and the 3-point floor on short runs.
|
|
1586
1658
|
const hardCap = Math.max(sampleCount * 8, 400, Math.ceil(frames.length / STATIC_VERIFY_REFERENCE_STRIDE) * 3 + runs.length);
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
for (const f of computeStaticVerificationPoints(a, b, sampleCount)) {
|
|
1595
|
-
const cur = await seekCapture(f);
|
|
1659
|
+
try {
|
|
1660
|
+
let spent = 0;
|
|
1661
|
+
for (const { a, b } of runs) {
|
|
1662
|
+
const anchor = a - 1;
|
|
1663
|
+
if (anchor < 0)
|
|
1664
|
+
continue;
|
|
1665
|
+
const anchorBuf = await seekCapture(anchor);
|
|
1596
1666
|
spent++;
|
|
1597
|
-
|
|
1598
|
-
|
|
1667
|
+
for (const f of computeStaticVerificationPoints(a, b, sampleCount)) {
|
|
1668
|
+
const cur = await seekCapture(f);
|
|
1669
|
+
spent++;
|
|
1670
|
+
if (!anchorBuf.equals(cur))
|
|
1671
|
+
return { badFrame: f, budgetExhausted: false };
|
|
1672
|
+
}
|
|
1673
|
+
// Budget exhausted → can't fully verify → disarm, distinct from real drift so a
|
|
1674
|
+
// `verification_budget` spike in telemetry reads as "this composition has a lot
|
|
1675
|
+
// of static material to verify," not "compositions are non-static."
|
|
1676
|
+
if (spent > hardCap)
|
|
1677
|
+
return { badFrame: a, budgetExhausted: true };
|
|
1599
1678
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
return { badFrame: a, budgetExhausted: true };
|
|
1679
|
+
return null;
|
|
1680
|
+
}
|
|
1681
|
+
finally {
|
|
1682
|
+
await seekToFrame(0).catch(() => { });
|
|
1605
1683
|
}
|
|
1606
|
-
return null;
|
|
1607
1684
|
}
|
|
1608
1685
|
/**
|
|
1609
1686
|
* Arm static-frame dedup for this render (default-on; opt out with HF_STATIC_DEDUP=false).
|
|
@@ -1960,9 +2037,28 @@ export async function captureFrameToBufferPipelined(session, frameIndex, time) {
|
|
|
1960
2037
|
// and skip the seek + drawElement + encode entirely. Same predicate as the serial
|
|
1961
2038
|
// path; clip-cut frames are excluded from staticFrames so they always capture.
|
|
1962
2039
|
if (session.staticFrames?.has(frameIndex) && session.lastEncodeResult) {
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2040
|
+
// Reuse is valid only when EVERY frame in (lastEncodeResultFrame, i] is
|
|
2041
|
+
// predicted-static — then all of them (and the reused buffer) share the
|
|
2042
|
+
// same pixels. Sequential capture reduces to has(i) (gap = {i}); the
|
|
2043
|
+
// interleaved parallel stride makes the gap N frames wide.
|
|
2044
|
+
const lastIdx = session.lastEncodeResultFrame ?? frameIndex - 1;
|
|
2045
|
+
let gapStatic = true;
|
|
2046
|
+
for (let j = lastIdx + 1; j <= frameIndex; j++) {
|
|
2047
|
+
if (!session.staticFrames.has(j)) {
|
|
2048
|
+
gapStatic = false;
|
|
2049
|
+
break;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
if (gapStatic) {
|
|
2053
|
+
session.staticDedupCount = (session.staticDedupCount ?? 0) + 1;
|
|
2054
|
+
session.capturePerf.frames += 1;
|
|
2055
|
+
// Advance the watermark on reuse too, not just on real captures — the
|
|
2056
|
+
// gap-check above starts from lastEncodeResultFrame, so leaving it
|
|
2057
|
+
// pinned to the last REAL capture makes every consecutive reuse rescan
|
|
2058
|
+
// an ever-widening window instead of just the one new frame (review).
|
|
2059
|
+
session.lastEncodeResultFrame = frameIndex;
|
|
2060
|
+
return { encodeResult: session.lastEncodeResult, captureTimeMs: Date.now() - startTime };
|
|
2061
|
+
}
|
|
1966
2062
|
}
|
|
1967
2063
|
try {
|
|
1968
2064
|
const { quantizedTime, seekMs, beforeCaptureMs } = await prepareFrameForCapture(session, frameIndex, time);
|
|
@@ -1989,8 +2085,10 @@ export async function captureFrameToBufferPipelined(session, frameIndex, time) {
|
|
|
1989
2085
|
session.capturePerf.frameMs.push(boundaryMs);
|
|
1990
2086
|
}
|
|
1991
2087
|
const boundaryResult = Promise.resolve(buffer);
|
|
1992
|
-
if (session.staticFrames)
|
|
2088
|
+
if (session.staticFrames) {
|
|
1993
2089
|
session.lastEncodeResult = boundaryResult;
|
|
2090
|
+
session.lastEncodeResultFrame = frameIndex;
|
|
2091
|
+
}
|
|
1994
2092
|
return { encodeResult: boundaryResult, captureTimeMs: Date.now() - startTime };
|
|
1995
2093
|
}
|
|
1996
2094
|
// Worker-encode is gated to the macOS GPU path (beginFrameTimeTicks === 0,
|
|
@@ -2007,8 +2105,10 @@ export async function captureFrameToBufferPipelined(session, frameIndex, time) {
|
|
|
2007
2105
|
session.capturePerf.totalMs += captureTimeMs;
|
|
2008
2106
|
session.capturePerf.frameMs.push(captureTimeMs);
|
|
2009
2107
|
// Task B: retain this encode result so a following static frame can reuse it.
|
|
2010
|
-
if (session.staticFrames)
|
|
2108
|
+
if (session.staticFrames) {
|
|
2011
2109
|
session.lastEncodeResult = encodeResult;
|
|
2110
|
+
session.lastEncodeResultFrame = frameIndex;
|
|
2111
|
+
}
|
|
2012
2112
|
return { encodeResult, captureTimeMs };
|
|
2013
2113
|
}
|
|
2014
2114
|
catch (captureError) {
|
|
@@ -2308,7 +2408,7 @@ async function captureDeVerificationFrames(session, page, logInitPhase) {
|
|
|
2308
2408
|
await page.evaluate((tt) => {
|
|
2309
2409
|
const hf = window.__hf;
|
|
2310
2410
|
if (hf && typeof hf.seek === "function")
|
|
2311
|
-
hf.seek(tt);
|
|
2411
|
+
hf.seek(tt, { suppressEvents: true });
|
|
2312
2412
|
}, t);
|
|
2313
2413
|
};
|
|
2314
2414
|
await seekTo(quantizeTimeToFrame(0, fps));
|
|
@@ -2364,6 +2464,7 @@ export function getCapturePerfSummary(session) {
|
|
|
2364
2464
|
avgBeforeCaptureMs: Math.round(session.capturePerf.beforeCaptureMs / frames),
|
|
2365
2465
|
avgScreenshotMs: Math.round(session.capturePerf.screenshotMs / frames),
|
|
2366
2466
|
p50TotalMs: medianOf(session.capturePerf.frameMs),
|
|
2467
|
+
subTimelineWaitOutcome: session.subTimelineWaitOutcome,
|
|
2367
2468
|
staticDedupReused: session.staticDedupCount ?? 0,
|
|
2368
2469
|
staticDedupEnabled: session.staticDedupEnabled ?? false,
|
|
2369
2470
|
// armed ⟺ a non-empty static set survived verification; predicted === its size.
|