@hyperframes/engine 0.8.9 → 0.8.11
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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/frameCapture.d.ts +72 -31
- package/dist/services/frameCapture.d.ts.map +1 -1
- package/dist/services/frameCapture.js +226 -117
- package/dist/services/frameCapture.js.map +1 -1
- package/dist/types.d.ts +12 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
|
-
import { quantizeTimeToFrame, fpsToNumber } from "@hyperframes/core";
|
|
12
|
+
import { quantizeTimeToFrame, fpsToNumber, resolveAuthoredTimingWindow, } from "@hyperframes/core";
|
|
13
13
|
// ── Extracted modules ───────────────────────────────────────────────────────
|
|
14
14
|
import { acquireBrowser, releaseBrowser, forceReleaseBrowser, buildChromeArgs, resolveBrowserGpuMode, resolveHeadlessShellPath, } from "./browserManager.js";
|
|
15
15
|
import { beginFrameCapture, ensureRenderFrameSiblings, getCdpSession, pageContentExceedsCaptureHeight, pageScreenshotCapture, initTransparentBackground, shouldDefaultCaptureBeyondViewport, } from "./screenshotService.js";
|
|
@@ -1904,18 +1904,15 @@ async function prepareFrameForCapture(session, frameIndex, time) {
|
|
|
1904
1904
|
* cut changes content with no tween; treat those frames as animated so the post-cut
|
|
1905
1905
|
* frame is captured fresh and later static frames reuse the correct scene.
|
|
1906
1906
|
*/
|
|
1907
|
-
|
|
1908
|
-
const schedule = await page.evaluate(() => Array.from(document.querySelectorAll("[data-start]")).map((el) => ({
|
|
1909
|
-
start: parseFloat(el.dataset.start || ""),
|
|
1910
|
-
dur: parseFloat(el.dataset.duration || ""),
|
|
1911
|
-
})));
|
|
1907
|
+
export function computeAuthoredClipBoundaryFrames(schedule, fps) {
|
|
1912
1908
|
const frames = new Set();
|
|
1913
|
-
for (const
|
|
1914
|
-
|
|
1909
|
+
for (const rawTiming of schedule) {
|
|
1910
|
+
const timing = resolveAuthoredTimingWindow(rawTiming);
|
|
1911
|
+
if (!timing)
|
|
1915
1912
|
continue;
|
|
1916
|
-
const edges = [Math.round(start * fps)];
|
|
1917
|
-
if (
|
|
1918
|
-
edges.push(Math.round(
|
|
1913
|
+
const edges = [Math.round(timing.start * fps)];
|
|
1914
|
+
if (timing.end != null)
|
|
1915
|
+
edges.push(Math.round(timing.end * fps));
|
|
1919
1916
|
for (const e of edges) {
|
|
1920
1917
|
for (const f of [e - 1, e, e + 1]) {
|
|
1921
1918
|
if (f >= 0)
|
|
@@ -1925,6 +1922,16 @@ async function computeClipBoundaryFrames(page, fps) {
|
|
|
1925
1922
|
}
|
|
1926
1923
|
return frames;
|
|
1927
1924
|
}
|
|
1925
|
+
async function computeClipBoundaryFrames(page, fps) {
|
|
1926
|
+
const schedule = await page.evaluate(() => Array.from(document.querySelectorAll("[data-start]")).map((el) => ({
|
|
1927
|
+
start: el.getAttribute("data-start"),
|
|
1928
|
+
duration: el.getAttribute("data-duration"),
|
|
1929
|
+
authoredDuration: el.getAttribute("data-hf-authored-duration"),
|
|
1930
|
+
end: el.getAttribute("data-end"),
|
|
1931
|
+
authoredEnd: el.getAttribute("data-hf-authored-end"),
|
|
1932
|
+
})));
|
|
1933
|
+
return computeAuthoredClipBoundaryFrames(schedule, fps);
|
|
1934
|
+
}
|
|
1928
1935
|
// Static dedup is an optional optimization. Building frame-index Sets scales with the
|
|
1929
1936
|
// composition's declared duration, so malformed/sentinel durations must fail closed before
|
|
1930
1937
|
// allocating them. Normal capture and the producer's typed duration validation still proceed.
|
|
@@ -2108,64 +2115,135 @@ const STATIC_VERIFY_REFERENCE_STRIDE = 24;
|
|
|
2108
2115
|
// optimization before the real render starts. Exhaustion fails closed: dedup is
|
|
2109
2116
|
// disabled and normal capture proceeds.
|
|
2110
2117
|
const STATIC_VERIFY_MAX_MS = 15_000;
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2118
|
+
// Two captures (anchor + comparison) are the minimum proof for a run. Four hundred
|
|
2119
|
+
// therefore permits 200 fully verified runs while bounding pathological schedules.
|
|
2120
|
+
const STATIC_VERIFY_MIN_SCREENSHOT_CAP = 400;
|
|
2121
|
+
// Preserve the legacy tuning headroom: raising the composition-wide sample floor may
|
|
2122
|
+
// raise the cap proportionally, but never changes the fixed wall deadline.
|
|
2123
|
+
const STATIC_VERIFY_SAMPLE_CAP_MULTIPLIER = 8;
|
|
2124
|
+
// Keep sample-driven work within the 400-screenshot base cap: 50 comparisons × 8
|
|
2125
|
+
// legacy headroom. Mandatory <=24-gap points may still raise the independent cap.
|
|
2126
|
+
const STATIC_VERIFY_MAX_GLOBAL_SAMPLE_FLOOR = STATIC_VERIFY_MIN_SCREENSHOT_CAP / STATIC_VERIFY_SAMPLE_CAP_MULTIPLIER;
|
|
2127
|
+
function contiguousStaticRuns(frames) {
|
|
2128
|
+
const runs = [];
|
|
2129
|
+
for (const frame of frames) {
|
|
2130
|
+
const last = runs.at(-1);
|
|
2131
|
+
if (last && frame === last.b + 1)
|
|
2132
|
+
last.b = frame;
|
|
2133
|
+
else
|
|
2134
|
+
runs.push({ a: frame, b: frame });
|
|
2135
|
+
}
|
|
2136
|
+
return runs;
|
|
2137
|
+
}
|
|
2138
|
+
function mandatoryRunComparisons(anchor, end) {
|
|
2139
|
+
const points = new Set();
|
|
2140
|
+
for (let frame = anchor + STATIC_VERIFY_REFERENCE_STRIDE; frame < end; frame += STATIC_VERIFY_REFERENCE_STRIDE) {
|
|
2141
|
+
points.add(frame);
|
|
2142
|
+
}
|
|
2143
|
+
points.add(end);
|
|
2144
|
+
return [...points].sort((left, right) => left - right);
|
|
2145
|
+
}
|
|
2146
|
+
/** Pure composition-wide planner. Every retained run is profitable and has gaps <=24 frames. */
|
|
2147
|
+
export function planStaticVerification(staticFrames, sampleFloor) {
|
|
2148
|
+
const frames = [...staticFrames].sort((left, right) => left - right);
|
|
2149
|
+
const allRuns = contiguousStaticRuns(frames).map(({ a, b }) => {
|
|
2150
|
+
const comparisons = mandatoryRunComparisons(a - 1, b);
|
|
2151
|
+
const frameCount = b - a + 1;
|
|
2152
|
+
return {
|
|
2153
|
+
a,
|
|
2154
|
+
b,
|
|
2155
|
+
anchor: a - 1,
|
|
2156
|
+
comparisons,
|
|
2157
|
+
frameCount,
|
|
2158
|
+
netSavings: frameCount - comparisons.length - 1,
|
|
2159
|
+
};
|
|
2160
|
+
});
|
|
2161
|
+
const runs = allRuns.filter((run) => run.anchor >= 0 && run.netSavings > 0);
|
|
2162
|
+
const skippedRuns = allRuns
|
|
2163
|
+
.filter((run) => run.anchor < 0 || run.netSavings <= 0)
|
|
2164
|
+
.map((run) => ({ a: run.a, b: run.b, reason: "unprofitable" }));
|
|
2165
|
+
const normalizedSampleFloor = Number.isFinite(sampleFloor)
|
|
2166
|
+
? Math.max(1, Math.floor(sampleFloor))
|
|
2167
|
+
: 1;
|
|
2168
|
+
const effectiveSampleFloor = Math.min(normalizedSampleFloor, STATIC_VERIFY_MAX_GLOBAL_SAMPLE_FLOOR);
|
|
2169
|
+
const comparisonCount = () => runs.reduce((sum, run) => sum + run.comparisons.length, 0);
|
|
2170
|
+
while (comparisonCount() < effectiveSampleFloor) {
|
|
2171
|
+
const candidates = runs
|
|
2172
|
+
.filter((run) => run.frameCount > run.comparisons.length + 2)
|
|
2173
|
+
.flatMap((run) => {
|
|
2174
|
+
const points = [run.anchor, ...run.comparisons];
|
|
2175
|
+
return points.slice(1).map((right, index) => ({
|
|
2176
|
+
run,
|
|
2177
|
+
left: points[index],
|
|
2178
|
+
right,
|
|
2179
|
+
width: right - points[index],
|
|
2180
|
+
}));
|
|
2181
|
+
})
|
|
2182
|
+
.filter((gap) => gap.width > 1)
|
|
2183
|
+
.sort((left, right) => right.width - left.width || left.run.a - right.run.a || left.left - right.left);
|
|
2184
|
+
const selected = candidates[0];
|
|
2185
|
+
if (!selected)
|
|
2186
|
+
break;
|
|
2187
|
+
selected.run.comparisons.push(Math.floor((selected.left + selected.right) / 2));
|
|
2188
|
+
selected.run.comparisons.sort((left, right) => left - right);
|
|
2189
|
+
selected.run.netSavings = selected.run.frameCount - selected.run.comparisons.length - 1;
|
|
2190
|
+
}
|
|
2191
|
+
runs.sort((left, right) => right.netSavings - left.netSavings || left.a - right.a);
|
|
2192
|
+
const plannedComparisons = comparisonCount();
|
|
2193
|
+
return {
|
|
2194
|
+
runs,
|
|
2195
|
+
skippedRuns,
|
|
2196
|
+
effectiveSampleFloor,
|
|
2197
|
+
predictedFrames: frames.length,
|
|
2198
|
+
verifiedCandidateFrames: runs.reduce((sum, run) => sum + run.frameCount, 0),
|
|
2199
|
+
plannedAnchors: runs.length,
|
|
2200
|
+
plannedComparisons,
|
|
2201
|
+
plannedScreenshots: runs.length + plannedComparisons,
|
|
2202
|
+
};
|
|
2143
2203
|
}
|
|
2144
2204
|
/**
|
|
2145
2205
|
* Empirically verify the predicted-static set before trusting it. Group static frames
|
|
2146
2206
|
* into runs; each run [a..b] reuses anchor a-1. CRITICAL: compare against the ANCHOR,
|
|
2147
2207
|
* not the predecessor — a slow drift with sub-quantization per-frame deltas is byte-
|
|
2148
2208
|
* identical frame-to-frame yet drifts far from the anchor by the run's end (the real
|
|
2149
|
-
* frozen error). Capture each run's anchor once, compare
|
|
2150
|
-
*
|
|
2151
|
-
*
|
|
2209
|
+
* frozen error). Capture each run's anchor once, compare its end plus deterministic
|
|
2210
|
+
* composition-wide points that preserve a 24-frame maximum gap; any mismatch ⇒ the
|
|
2211
|
+
* run isn't truly static ⇒ disable dedup whole-comp. Capture-mode-
|
|
2212
|
+
* independent (seeks + screenshots in normal DOM). Budget exhaustion may retain only
|
|
2213
|
+
* runs whose anchor and every planned comparison completed successfully.
|
|
2152
2214
|
*/
|
|
2153
|
-
export async function verifyStaticFramesSafe(session, page, staticFrames, fps, sampleCount) {
|
|
2154
|
-
const
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
const
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
const
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2215
|
+
export async function verifyStaticFramesSafe(session, page, staticFrames, fps, sampleCount, dependencies = {}) {
|
|
2216
|
+
const plan = planStaticVerification(staticFrames, sampleCount);
|
|
2217
|
+
const now = dependencies.now ?? Date.now;
|
|
2218
|
+
const capture = dependencies.capture ?? pageScreenshotCapture;
|
|
2219
|
+
const startedAt = now();
|
|
2220
|
+
const deadline = startedAt + STATIC_VERIFY_MAX_MS;
|
|
2221
|
+
const verifiedFrames = new Set();
|
|
2222
|
+
const stats = {
|
|
2223
|
+
plannedRuns: plan.runs.length,
|
|
2224
|
+
completedRuns: 0,
|
|
2225
|
+
plannedAnchors: plan.plannedAnchors,
|
|
2226
|
+
completedAnchors: 0,
|
|
2227
|
+
plannedComparisons: plan.plannedComparisons,
|
|
2228
|
+
completedComparisons: 0,
|
|
2229
|
+
seeks: 0,
|
|
2230
|
+
screenshots: 0,
|
|
2231
|
+
byteComparisons: 0,
|
|
2232
|
+
elapsedMs: 0,
|
|
2233
|
+
predictedFrames: plan.predictedFrames,
|
|
2234
|
+
verifiedFrames: 0,
|
|
2235
|
+
unverifiedFrames: plan.predictedFrames,
|
|
2236
|
+
};
|
|
2237
|
+
const finish = (outcome, badFrame) => ({
|
|
2238
|
+
outcome,
|
|
2239
|
+
verifiedFrames,
|
|
2240
|
+
...(badFrame == null ? {} : { badFrame }),
|
|
2241
|
+
stats,
|
|
2242
|
+
});
|
|
2243
|
+
if (plan.runs.length === 0)
|
|
2244
|
+
return finish("unprofitable");
|
|
2168
2245
|
const seekToFrame = async (frameIdx) => {
|
|
2246
|
+
stats.seeks++;
|
|
2169
2247
|
const t = quantizeTimeToFrame(frameIdx / fps, fps);
|
|
2170
2248
|
await page.evaluate((tt) => {
|
|
2171
2249
|
const hf = window.__hf;
|
|
@@ -2173,52 +2251,55 @@ export async function verifyStaticFramesSafe(session, page, staticFrames, fps, s
|
|
|
2173
2251
|
hf.seek(tt, { suppressEvents: true });
|
|
2174
2252
|
}, t);
|
|
2175
2253
|
};
|
|
2254
|
+
const hardCap = Math.max(STATIC_VERIFY_MIN_SCREENSHOT_CAP, plan.effectiveSampleFloor * STATIC_VERIFY_SAMPLE_CAP_MULTIPLIER, Math.ceil(plan.predictedFrames / STATIC_VERIFY_REFERENCE_STRIDE) * 3 + plan.runs.length);
|
|
2176
2255
|
const seekCapture = async (frameIdx) => {
|
|
2256
|
+
if (now() >= deadline)
|
|
2257
|
+
return "time_budget";
|
|
2258
|
+
if (stats.screenshots >= hardCap)
|
|
2259
|
+
return "count_budget";
|
|
2177
2260
|
await seekToFrame(frameIdx);
|
|
2178
|
-
|
|
2261
|
+
stats.screenshots++;
|
|
2262
|
+
return capture(page, session.options);
|
|
2179
2263
|
};
|
|
2180
|
-
// Verify
|
|
2181
|
-
//
|
|
2182
|
-
//
|
|
2183
|
-
// — against the anchor the run actually reuses.
|
|
2184
|
-
//
|
|
2185
|
-
// hardCap bounds pathological cases and hitting it DISABLES dedup (conservative:
|
|
2186
|
-
// never trust an unverified set). It must scale with the new density model:
|
|
2187
|
-
// each run now costs roughly span/STATIC_VERIFY_REFERENCE_STRIDE + 1 checks (plus
|
|
2188
|
-
// one anchor), not the ~8 the old flat point cap cost — sizing the budget only off
|
|
2189
|
-
// sampleCount (which no longer drives density for long runs) would make a
|
|
2190
|
-
// genuinely-static long composition spuriously disarm under the new, more
|
|
2191
|
-
// thorough checking. `frames.length` approximates total interior checks; a 3x
|
|
2192
|
-
// margin absorbs per-run anchor overhead and the 3-point floor on short runs.
|
|
2193
|
-
const hardCap = Math.max(sampleCount * 8, 400, Math.ceil(frames.length / STATIC_VERIFY_REFERENCE_STRIDE) * 3 + runs.length);
|
|
2264
|
+
// Verify profitable runs in deterministic savings order. A run is added to the armed
|
|
2265
|
+
// set only after its anchor and every planner-selected comparison match. Budget exits
|
|
2266
|
+
// retain completed runs; mismatch or infrastructure failure clears all verified frames.
|
|
2194
2267
|
try {
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
return { badFrame: a, budgetExhausted: true };
|
|
2202
|
-
const anchorBuf = await seekCapture(anchor);
|
|
2203
|
-
spent++;
|
|
2204
|
-
for (const f of computeStaticVerificationPoints(a, b, sampleCount)) {
|
|
2205
|
-
if (Date.now() >= deadline)
|
|
2206
|
-
return { badFrame: f, budgetExhausted: true };
|
|
2268
|
+
for (const run of plan.runs) {
|
|
2269
|
+
const anchorBuf = await seekCapture(run.anchor);
|
|
2270
|
+
if (typeof anchorBuf === "string")
|
|
2271
|
+
return finish(anchorBuf, run.a);
|
|
2272
|
+
stats.completedAnchors++;
|
|
2273
|
+
for (const f of run.comparisons) {
|
|
2207
2274
|
const cur = await seekCapture(f);
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2275
|
+
if (typeof cur === "string")
|
|
2276
|
+
return finish(cur, f);
|
|
2277
|
+
stats.completedComparisons++;
|
|
2278
|
+
stats.byteComparisons++;
|
|
2279
|
+
if (!anchorBuf.equals(cur)) {
|
|
2280
|
+
verifiedFrames.clear();
|
|
2281
|
+
stats.verifiedFrames = 0;
|
|
2282
|
+
stats.unverifiedFrames = plan.predictedFrames;
|
|
2283
|
+
return finish("mismatch", f);
|
|
2284
|
+
}
|
|
2211
2285
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2286
|
+
stats.completedRuns++;
|
|
2287
|
+
for (let frame = run.a; frame <= run.b; frame++)
|
|
2288
|
+
verifiedFrames.add(frame);
|
|
2289
|
+
stats.verifiedFrames = verifiedFrames.size;
|
|
2290
|
+
stats.unverifiedFrames = plan.predictedFrames - verifiedFrames.size;
|
|
2217
2291
|
}
|
|
2218
|
-
return
|
|
2292
|
+
return finish("verified");
|
|
2293
|
+
}
|
|
2294
|
+
catch {
|
|
2295
|
+
verifiedFrames.clear();
|
|
2296
|
+
stats.verifiedFrames = 0;
|
|
2297
|
+
stats.unverifiedFrames = plan.predictedFrames;
|
|
2298
|
+
return finish("infrastructure");
|
|
2219
2299
|
}
|
|
2220
2300
|
finally {
|
|
2221
2301
|
await seekToFrame(0).catch(() => { });
|
|
2302
|
+
stats.elapsedMs = Math.max(0, now() - startedAt);
|
|
2222
2303
|
}
|
|
2223
2304
|
}
|
|
2224
2305
|
/**
|
|
@@ -2283,24 +2364,36 @@ async function armStaticDedup(session, page, logInitPhase) {
|
|
|
2283
2364
|
}
|
|
2284
2365
|
const rawSamples = Number(process.env.HF_STATIC_DEDUP_SAMPLES ?? "24");
|
|
2285
2366
|
const samples = Number.isFinite(rawSamples) && rawSamples >= 1 ? rawSamples : 24;
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
? "verification_budget"
|
|
2292
|
-
: "verification_failed";
|
|
2293
|
-
logInitPhase(verdict.budgetExhausted
|
|
2294
|
-
? `static-frame dedup: disabled (verification budget exhausted before frame ${verdict.badFrame}; ` +
|
|
2295
|
-
`too much predicted-static material to fully verify — this is the safe fallback, not an error)`
|
|
2296
|
-
: `static-frame dedup: disabled (verification failed — content drifts from anchor at ` +
|
|
2297
|
-
`predicted-static frame ${verdict.badFrame})`);
|
|
2367
|
+
session.staticDedupPredictedCount = stats.staticFrameSet.size;
|
|
2368
|
+
if (process.env.HF_STATIC_DEDUP_VERIFY === "false") {
|
|
2369
|
+
session.staticFrames = stats.staticFrameSet;
|
|
2370
|
+
logInitPhase(`static-frame dedup: ${stats.staticFrameSet.size}/${stats.totalFrames} frame(s) reusable ` +
|
|
2371
|
+
`(verification explicitly disabled)`);
|
|
2298
2372
|
return;
|
|
2299
2373
|
}
|
|
2300
|
-
|
|
2301
|
-
session.
|
|
2302
|
-
|
|
2303
|
-
|
|
2374
|
+
const verdict = await verifyStaticFramesSafe(session, page, stats.staticFrameSet, fps, samples);
|
|
2375
|
+
session.staticDedupVerification = verdict;
|
|
2376
|
+
if (verdict.outcome === "mismatch" || verdict.outcome === "infrastructure") {
|
|
2377
|
+
session.staticDedupSkipReason = "verification_failed";
|
|
2378
|
+
logInitPhase(verdict.outcome === "mismatch"
|
|
2379
|
+
? `static-frame dedup: disabled (verification mismatch at predicted-static frame ${verdict.badFrame})`
|
|
2380
|
+
: "static-frame dedup: disabled (verification infrastructure failure)");
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
if (verdict.outcome === "unprofitable") {
|
|
2384
|
+
session.staticDedupSkipReason = "unprofitable";
|
|
2385
|
+
logInitPhase("static-frame dedup: disabled (verification cost cannot save captures)");
|
|
2386
|
+
return;
|
|
2387
|
+
}
|
|
2388
|
+
if (verdict.verifiedFrames.size === 0) {
|
|
2389
|
+
session.staticDedupSkipReason = "verification_budget";
|
|
2390
|
+
logInitPhase(`static-frame dedup: disabled (${verdict.outcome} before frame ${verdict.badFrame}; no run fully verified)`);
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
session.staticFrames = verdict.verifiedFrames;
|
|
2394
|
+
logInitPhase(`static-frame dedup: ${verdict.verifiedFrames.size}/${stats.staticFrameSet.size} predicted frame(s) reusable ` +
|
|
2395
|
+
`(outcome=${verdict.outcome}, runs=${verdict.stats.completedRuns}/${verdict.stats.plannedRuns}, ` +
|
|
2396
|
+
`screenshots=${verdict.stats.screenshots}, seeks=${verdict.stats.seeks}, elapsedMs=${verdict.stats.elapsedMs})`);
|
|
2304
2397
|
}
|
|
2305
2398
|
/**
|
|
2306
2399
|
* Walk window.__timelines and collect frame intervals where GSAP tweens animate
|
|
@@ -2405,8 +2498,11 @@ async function captureFrameCore(session, frameIndex, time) {
|
|
|
2405
2498
|
// Use the SAME floor+epsilon idiom as quantizeTimeToFrame so the dedup lookup agrees
|
|
2406
2499
|
// with the frame the seek actually lands on, even if `time` ever isn't exactly i/fps.
|
|
2407
2500
|
const absFrameIndex = Math.floor(time * fpsToNumber(options.fps) + 1e-9);
|
|
2408
|
-
if (session.staticFrames?.has(absFrameIndex) &&
|
|
2501
|
+
if (session.staticFrames?.has(absFrameIndex) &&
|
|
2502
|
+
session.lastFrameBuffer &&
|
|
2503
|
+
session.lastFrameAbsoluteIndex === absFrameIndex - 1) {
|
|
2409
2504
|
session.staticDedupCount = (session.staticDedupCount ?? 0) + 1;
|
|
2505
|
+
session.lastFrameAbsoluteIndex = absFrameIndex;
|
|
2410
2506
|
return {
|
|
2411
2507
|
buffer: session.lastFrameBuffer,
|
|
2412
2508
|
quantizedTime: quantizeTimeToFrame(time, fpsToNumber(options.fps)),
|
|
@@ -2516,8 +2612,10 @@ async function captureFrameCore(session, frameIndex, time) {
|
|
|
2516
2612
|
session.capturePerf.totalMs += captureTimeMs;
|
|
2517
2613
|
session.capturePerf.frameMs.push(captureTimeMs);
|
|
2518
2614
|
// Retain this freshly-captured buffer so the following static frames can reuse it.
|
|
2519
|
-
if (session.staticFrames)
|
|
2615
|
+
if (session.staticFrames) {
|
|
2520
2616
|
session.lastFrameBuffer = screenshotBuffer;
|
|
2617
|
+
session.lastFrameAbsoluteIndex = absFrameIndex;
|
|
2618
|
+
}
|
|
2521
2619
|
return { buffer: screenshotBuffer, quantizedTime, captureTimeMs };
|
|
2522
2620
|
}
|
|
2523
2621
|
catch (captureError) {
|
|
@@ -2785,6 +2883,7 @@ export async function discardWarmupCapture(session, frameIndex = 0, time = 0, in
|
|
|
2785
2883
|
const noDamageBefore = session.beginFrameNoDamageCount;
|
|
2786
2884
|
const dedupCountBefore = session.staticDedupCount;
|
|
2787
2885
|
const lastFrameBufferBefore = session.lastFrameBuffer;
|
|
2886
|
+
const lastFrameAbsoluteIndexBefore = session.lastFrameAbsoluteIndex;
|
|
2788
2887
|
try {
|
|
2789
2888
|
await innerCapture(session, frameIndex, time);
|
|
2790
2889
|
}
|
|
@@ -2797,6 +2896,7 @@ export async function discardWarmupCapture(session, frameIndex = 0, time = 0, in
|
|
|
2797
2896
|
session.beginFrameNoDamageCount = noDamageBefore;
|
|
2798
2897
|
session.staticDedupCount = dedupCountBefore;
|
|
2799
2898
|
session.lastFrameBuffer = lastFrameBufferBefore;
|
|
2899
|
+
session.lastFrameAbsoluteIndex = lastFrameAbsoluteIndexBefore;
|
|
2800
2900
|
}
|
|
2801
2901
|
}
|
|
2802
2902
|
export async function closeCaptureSession(session) {
|
|
@@ -2879,6 +2979,7 @@ export function prepareCaptureSessionForReuse(session, outputDir, onBeforeCaptur
|
|
|
2879
2979
|
// intact: it's keyed in absolute frames and stays valid for a same-composition reuse;
|
|
2880
2980
|
// lastFrameBuffer must be re-seeded by this render's first fresh capture.
|
|
2881
2981
|
session.lastFrameBuffer = undefined;
|
|
2982
|
+
session.lastFrameAbsoluteIndex = undefined;
|
|
2882
2983
|
session.staticDedupCount = 0;
|
|
2883
2984
|
}
|
|
2884
2985
|
export async function getCompositionDuration(session) {
|
|
@@ -3054,9 +3155,17 @@ export function getCapturePerfSummary(session) {
|
|
|
3054
3155
|
warnings: cloneCaptureWarnings(session.warnings),
|
|
3055
3156
|
staticDedupReused: session.staticDedupCount ?? 0,
|
|
3056
3157
|
staticDedupEnabled: session.staticDedupEnabled ?? false,
|
|
3057
|
-
// armed ⟺ a non-empty static set survived verification
|
|
3158
|
+
// armed ⟺ a non-empty static set survived verification.
|
|
3058
3159
|
staticDedupArmed: (session.staticFrames?.size ?? 0) > 0,
|
|
3059
|
-
staticDedupPredicted: session.
|
|
3160
|
+
staticDedupPredicted: session.staticDedupPredictedCount ?? 0,
|
|
3161
|
+
staticDedupVerified: session.staticFrames?.size ?? 0,
|
|
3162
|
+
staticDedupVerificationOutcome: session.staticDedupVerification?.outcome,
|
|
3163
|
+
staticDedupVerificationPlannedRuns: session.staticDedupVerification?.stats.plannedRuns,
|
|
3164
|
+
staticDedupVerificationCompletedRuns: session.staticDedupVerification?.stats.completedRuns,
|
|
3165
|
+
staticDedupVerificationScreenshots: session.staticDedupVerification?.stats.screenshots,
|
|
3166
|
+
staticDedupVerificationSeeks: session.staticDedupVerification?.stats.seeks,
|
|
3167
|
+
staticDedupVerificationComparisons: session.staticDedupVerification?.stats.byteComparisons,
|
|
3168
|
+
staticDedupVerificationElapsedMs: session.staticDedupVerification?.stats.elapsedMs,
|
|
3060
3169
|
staticDedupSkipReason: session.staticDedupSkipReason,
|
|
3061
3170
|
beginFrameNoDamage: session.beginFrameNoDamageCount,
|
|
3062
3171
|
beginFrameHasDamage: session.beginFrameHasDamageCount,
|