mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix: speed up video frame injection renders (#596)
This commit is contained in:
@@ -74,6 +74,47 @@ export interface CaptureSession {
|
||||
// Circular buffer for browser console messages dumped on render failure diagnostics.
|
||||
// Complex compositions produce 100+ messages; 50 was too small to capture relevant errors.
|
||||
const BROWSER_CONSOLE_BUFFER_SIZE = 200;
|
||||
const CAPTURE_SESSION_CLOSE_TIMEOUT_MS = 5_000;
|
||||
|
||||
async function waitForCloseWithTimeout(promise: Promise<unknown>): Promise<boolean> {
|
||||
let timedOut = false;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
await Promise.race([
|
||||
promise.then(
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
),
|
||||
new Promise<void>((resolve) => {
|
||||
timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
resolve();
|
||||
}, CAPTURE_SESSION_CLOSE_TIMEOUT_MS);
|
||||
}),
|
||||
]);
|
||||
if (timer) clearTimeout(timer);
|
||||
return !timedOut;
|
||||
}
|
||||
|
||||
function forceKillBrowserProcess(browser: Browser): void {
|
||||
const browserProcess = (
|
||||
browser as unknown as {
|
||||
process?: () => { kill: (signal?: NodeJS.Signals) => boolean; killed?: boolean } | null;
|
||||
}
|
||||
).process?.();
|
||||
|
||||
if (browserProcess && !browserProcess.killed) {
|
||||
try {
|
||||
browserProcess.kill("SIGKILL");
|
||||
} catch {
|
||||
// Best-effort cleanup after Puppeteer close has already timed out.
|
||||
}
|
||||
}
|
||||
try {
|
||||
browser.disconnect();
|
||||
} catch {
|
||||
// Best-effort cleanup after Puppeteer close has already timed out.
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCaptureSession(
|
||||
serverUrl: string,
|
||||
@@ -674,11 +715,21 @@ export async function closeCaptureSession(session: CaptureSession): Promise<void
|
||||
// but browserReleased=false → second call no-ops on page and retries browser.
|
||||
// This matches the orchestrator's intent for HDR cleanup.
|
||||
if (!session.pageReleased && session.page) {
|
||||
await session.page.close().catch(() => {});
|
||||
const pageClosed = await waitForCloseWithTimeout(session.page.close());
|
||||
if (!pageClosed) {
|
||||
console.warn("[FrameCapture] Timed out closing page; forcing browser process shutdown");
|
||||
forceKillBrowserProcess(session.browser);
|
||||
}
|
||||
session.pageReleased = true;
|
||||
}
|
||||
if (!session.browserReleased && session.browser) {
|
||||
await releaseBrowser(session.browser, session.config);
|
||||
const browserClosed = await waitForCloseWithTimeout(
|
||||
releaseBrowser(session.browser, session.config),
|
||||
);
|
||||
if (!browserClosed) {
|
||||
console.warn("[FrameCapture] Timed out closing browser; forcing browser process shutdown");
|
||||
forceKillBrowserProcess(session.browser);
|
||||
}
|
||||
session.browserReleased = true;
|
||||
}
|
||||
session.isInitialized = false;
|
||||
|
||||
@@ -446,13 +446,15 @@ export async function injectVideoFramesBatch(
|
||||
}
|
||||
}
|
||||
img.decoding = "sync";
|
||||
img.src = item.dataUri;
|
||||
pendingDecodes.push(
|
||||
img
|
||||
.decode()
|
||||
.catch(() => undefined)
|
||||
.then(() => undefined),
|
||||
);
|
||||
if (img.getAttribute("src") !== item.dataUri) {
|
||||
img.src = item.dataUri;
|
||||
pendingDecodes.push(
|
||||
img
|
||||
.decode()
|
||||
.catch(() => undefined)
|
||||
.then(() => undefined),
|
||||
);
|
||||
}
|
||||
img.style.opacity = String(computedOpacity);
|
||||
img.style.visibility = "visible";
|
||||
// Hide the native <video> with visibility only — never clobber inline
|
||||
|
||||
@@ -14,7 +14,16 @@ import { injectVideoFramesBatch, syncVideoFrameVisibility } from "./screenshotSe
|
||||
import { type BeforeCaptureHook } from "./frameCapture.js";
|
||||
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
|
||||
|
||||
function createFrameDataUriCache(cacheLimit: number) {
|
||||
export interface VideoFrameInjectorOptions extends Partial<
|
||||
Pick<EngineConfig, "frameDataUriCacheLimit">
|
||||
> {
|
||||
frameSrcResolver?: (framePath: string) => string | null;
|
||||
}
|
||||
|
||||
function createFrameSourceCache(
|
||||
cacheLimit: number,
|
||||
frameSrcResolver?: (framePath: string) => string | null,
|
||||
) {
|
||||
const cache = new Map<string, string>();
|
||||
const inFlight = new Map<string, Promise<string>>();
|
||||
|
||||
@@ -33,6 +42,9 @@ function createFrameDataUriCache(cacheLimit: number) {
|
||||
}
|
||||
|
||||
async function get(framePath: string): Promise<string> {
|
||||
const servedSrc = frameSrcResolver?.(framePath);
|
||||
if (servedSrc) return servedSrc;
|
||||
|
||||
const cached = cache.get(framePath);
|
||||
if (cached) {
|
||||
remember(framePath, cached);
|
||||
@@ -67,7 +79,7 @@ function createFrameDataUriCache(cacheLimit: number) {
|
||||
*/
|
||||
export function createVideoFrameInjector(
|
||||
frameLookup: FrameLookupTable | null,
|
||||
config?: Partial<Pick<EngineConfig, "frameDataUriCacheLimit">>,
|
||||
config?: VideoFrameInjectorOptions,
|
||||
): BeforeCaptureHook | null {
|
||||
if (!frameLookup) return null;
|
||||
|
||||
@@ -75,7 +87,7 @@ export function createVideoFrameInjector(
|
||||
32,
|
||||
config?.frameDataUriCacheLimit ?? DEFAULT_CONFIG.frameDataUriCacheLimit,
|
||||
);
|
||||
const frameCache = createFrameDataUriCache(cacheLimit);
|
||||
const frameCache = createFrameSourceCache(cacheLimit, config?.frameSrcResolver);
|
||||
const lastInjectedFrameByVideo = new Map<string, number>();
|
||||
|
||||
return async (page: Page, time: number) => {
|
||||
|
||||
Reference in New Issue
Block a user