mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(cli): propagate live capture budget
This commit is contained in:
@@ -20,6 +20,14 @@ export interface DiscoveredLottie {
|
||||
frameRate?: number;
|
||||
}
|
||||
|
||||
interface RemainingBudget {
|
||||
remainingMs?: () => number;
|
||||
}
|
||||
|
||||
function liveRemainingMs(budget: RemainingBudget, fallbackMs: number): number {
|
||||
return budget.remainingMs?.() ?? fallbackMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and save discovered Lottie animations to disk.
|
||||
*
|
||||
@@ -30,11 +38,13 @@ export interface DiscoveredLottie {
|
||||
export async function saveLottieAnimations(
|
||||
discoveredLotties: DiscoveredLottie[],
|
||||
lottieDir: string,
|
||||
budget: RemainingBudget = {},
|
||||
): Promise<number> {
|
||||
let savedCount = 0;
|
||||
const savedHashes = new Set<string>(); // Deduplicate by content
|
||||
|
||||
for (let li = 0; li < discoveredLotties.length && li < 10; li++) {
|
||||
if (liveRemainingMs(budget, 10_000) <= 0) break;
|
||||
const lottieItem = discoveredLotties[li]!;
|
||||
try {
|
||||
let jsonData: string | undefined;
|
||||
@@ -43,9 +53,11 @@ export async function saveLottieAnimations(
|
||||
// Already have the JSON data from network interception
|
||||
jsonData = JSON.stringify(lottieItem.data);
|
||||
} else if (lottieItem.url) {
|
||||
const requestTimeoutMs = Math.min(10_000, liveRemainingMs(budget, 10_000));
|
||||
if (requestTimeoutMs <= 0) break;
|
||||
// SSRF guard — safeFetch re-checks the denylist on every redirect hop
|
||||
const res = await safeFetch(lottieItem.url, {
|
||||
signal: AbortSignal.timeout(10000),
|
||||
signal: AbortSignal.timeout(requestTimeoutMs),
|
||||
headers: { "User-Agent": "HyperFrames/1.0" },
|
||||
});
|
||||
if (!res || !res.ok) continue;
|
||||
@@ -117,6 +129,7 @@ export async function renderLottiePreviews(
|
||||
chromeBrowser: Browser,
|
||||
lottieDir: string,
|
||||
outputDir: string,
|
||||
budget: RemainingBudget = {},
|
||||
): Promise<void> {
|
||||
const manifest: Array<{
|
||||
file: string;
|
||||
@@ -133,6 +146,7 @@ export async function renderLottiePreviews(
|
||||
|
||||
for (const file of readdirSync(lottieDir)) {
|
||||
if (!file.endsWith(".json")) continue;
|
||||
if (liveRemainingMs(budget, 1) <= 0) break;
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(join(lottieDir, file), "utf-8"));
|
||||
const fr = raw.fr || 30;
|
||||
@@ -146,7 +160,9 @@ export async function renderLottiePreviews(
|
||||
|
||||
let previewPage;
|
||||
try {
|
||||
if (liveRemainingMs(budget, 1) <= 0) break;
|
||||
previewPage = await chromeBrowser.newPage();
|
||||
if (liveRemainingMs(budget, 1) <= 0) break;
|
||||
await previewPage.setViewport({ width: 400, height: 400 });
|
||||
const animData = JSON.parse(readFileSync(join(lottieDir, file), "utf-8"));
|
||||
const midFrame = Math.floor(((raw.op || 0) - (raw.ip || 0)) * 0.3);
|
||||
@@ -180,11 +196,13 @@ export async function renderLottiePreviews(
|
||||
await previewPage
|
||||
.waitForFunction(() => (window as any).__READY === true, { timeout: 5000 })
|
||||
.catch(() => {});
|
||||
await previewPage.screenshot({
|
||||
path: join(previewDir, previewName),
|
||||
type: "png",
|
||||
omitBackground: true,
|
||||
});
|
||||
if (liveRemainingMs(budget, 1) > 0) {
|
||||
await previewPage.screenshot({
|
||||
path: join(previewDir, previewName),
|
||||
type: "png",
|
||||
omitBackground: true,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/* preview rendering failed — non-critical */
|
||||
} finally {
|
||||
@@ -362,11 +380,12 @@ async function sampleVideoDom(
|
||||
page: Page,
|
||||
budgetMs: number,
|
||||
netSet: Set<string>,
|
||||
budget: RemainingBudget,
|
||||
): Promise<VideoDescriptor[]> {
|
||||
const seen = new Map<string, VideoDescriptor>();
|
||||
const start = Date.now();
|
||||
let stale = 0;
|
||||
while (Date.now() - start < budgetMs && stale < 3) {
|
||||
while (Date.now() - start < budgetMs && stale < 3 && liveRemainingMs(budget, 1) > 0) {
|
||||
let grew = false;
|
||||
const netBefore = netSet.size;
|
||||
for (const d of await scanVideoDom(page)) {
|
||||
@@ -377,7 +396,12 @@ async function sampleVideoDom(
|
||||
}
|
||||
if (netSet.size > netBefore) grew = true;
|
||||
stale = grew ? 0 : stale + 1;
|
||||
await new Promise((r) => setTimeout(r, 2000));
|
||||
const waitMs = Math.min(
|
||||
2000,
|
||||
Math.max(0, budgetMs - (Date.now() - start)),
|
||||
Math.max(0, liveRemainingMs(budget, 2000)),
|
||||
);
|
||||
if (waitMs > 0) await new Promise((r) => setTimeout(r, waitMs));
|
||||
}
|
||||
return [...seen.values()];
|
||||
}
|
||||
@@ -407,7 +431,12 @@ export async function captureVideoManifest(
|
||||
page: Page,
|
||||
outputDir: string,
|
||||
progress: (stage: string, detail?: string) => void,
|
||||
opts?: { networkVideoUrls?: Set<string>; sampleMs?: number; downloadBudgetMs?: number },
|
||||
opts?: {
|
||||
networkVideoUrls?: Set<string>;
|
||||
sampleMs?: number;
|
||||
downloadBudgetMs?: number;
|
||||
remainingMs?: () => number;
|
||||
},
|
||||
): Promise<void> {
|
||||
const netSet = opts?.networkVideoUrls ?? new Set<string>();
|
||||
const sampleMs = opts?.sampleMs ?? 0;
|
||||
@@ -416,7 +445,9 @@ export async function captureVideoManifest(
|
||||
// DOM scan, optionally sampled over time (Layer 2) when videos are present.
|
||||
const initial = await scanVideoDom(page);
|
||||
const domVideos =
|
||||
initial.length > 0 && sampleMs > 0 ? await sampleVideoDom(page, sampleMs, netSet) : initial;
|
||||
initial.length > 0 && sampleMs > 0
|
||||
? await sampleVideoDom(page, sampleMs, netSet, opts ?? {})
|
||||
: initial;
|
||||
|
||||
// Merge DOM (rich) + network-only (thin, Layer 1), deduped by download
|
||||
// filename so a clip seen in both lands once. netSet is read here — AFTER
|
||||
@@ -472,7 +503,11 @@ export async function captureVideoManifest(
|
||||
|
||||
const dlStart = Date.now();
|
||||
for (let vi = 0; vi < merged.length && vi < 20; vi++) {
|
||||
if (remainingVideoDownloadTimeoutMs(dlStart, downloadBudgetMs) <= 0) break;
|
||||
const iterationRemainingMs = liveRemainingMs(
|
||||
opts ?? {},
|
||||
remainingVideoDownloadTimeoutMs(dlStart, downloadBudgetMs),
|
||||
);
|
||||
if (iterationRemainingMs <= 0) break;
|
||||
const v = merged[vi]!;
|
||||
let preview: string | undefined;
|
||||
|
||||
@@ -496,16 +531,18 @@ export async function captureVideoManifest(
|
||||
}, v.filename)) as { x: number; y: number; width: number; height: number } | null;
|
||||
if (rect && rect.width >= 10) {
|
||||
await new Promise((r) => setTimeout(r, 200)); // let decoder settle
|
||||
await page.screenshot({
|
||||
path: join(previewDir, previewName),
|
||||
clip: {
|
||||
x: Math.max(0, rect.x),
|
||||
y: Math.max(0, rect.y),
|
||||
width: Math.min(rect.width, 1920),
|
||||
height: Math.min(rect.height, 1080),
|
||||
},
|
||||
});
|
||||
preview = `assets/videos/previews/${previewName}`;
|
||||
if (liveRemainingMs(opts ?? {}, 1) > 0) {
|
||||
await page.screenshot({
|
||||
path: join(previewDir, previewName),
|
||||
clip: {
|
||||
x: Math.max(0, rect.x),
|
||||
y: Math.max(0, rect.y),
|
||||
width: Math.min(rect.width, 1920),
|
||||
height: Math.min(rect.height, 1080),
|
||||
},
|
||||
});
|
||||
preview = `assets/videos/previews/${previewName}`;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* preview failed — non-critical */
|
||||
@@ -516,7 +553,10 @@ export async function captureVideoManifest(
|
||||
// direct file. Cumulative budget caps total download time so a throttled
|
||||
// host or many large clips can't stall capture — over budget, keep the
|
||||
// preview (if any) and stop fetching bodies.
|
||||
const downloadTimeoutMs = remainingVideoDownloadTimeoutMs(dlStart, downloadBudgetMs);
|
||||
const downloadTimeoutMs = Math.min(
|
||||
remainingVideoDownloadTimeoutMs(dlStart, downloadBudgetMs),
|
||||
liveRemainingMs(opts ?? {}, VIDEO_DOWNLOAD_TIMEOUT_MS),
|
||||
);
|
||||
const savedPath =
|
||||
downloadTimeoutMs > 0
|
||||
? await downloadVideoBody(v.src, v.filename, videoManifestDir, downloadTimeoutMs)
|
||||
|
||||
Reference in New Issue
Block a user