fix: address PR #596 review issues (#597)

## Summary

Fixes three issues identified in the [post-merge review](https://github.com/heygen-com/hyperframes/pull/596#pullrequestreview-4214283515) of PR #596:

- **P1 (cache bypass):** When `extractCacheDir` is set, extracted frames live outside `compiledDir`, so `createCompiledFrameSrcResolver` rejects them and every frame falls back to base64 data URIs. Fix: symlink cached frame directories into `compiledDir/__hyperframes_video_frames/` after extraction and remap `framePaths` so the served-frame fast path works.

- **P2 (pooled browser stale state):** `closeCaptureSession` force-killed the Chrome process on timeout via raw `SIGKILL` without clearing `pooledBrowser` / `pooledBrowserRefCount`, leaving other sessions with a dead browser reference. Fix: add `forceReleaseBrowser()` in `browserManager` that atomically clears pool state before killing the process.

- **P3 (reserved chars in URLs):** `createCompiledFrameSrcResolver` encodes path segments with `encodeURIComponent`, but the file server used `c.req.path` (which only applies `decodeURI`) to look up files on disk. Video IDs containing `#`, `?`, or `%` produced 404s. Fix: apply `decodeURIComponent` per path segment in the file server's catch-all route.

## Test plan

- [x] `createCompiledFrameSrcResolver` tests: symlinked cache paths resolve to served URLs; cache-external paths return null; reserved characters encode correctly
- [x] `forceReleaseBrowser` tests: kills process + disconnects; tolerates already-killed process
- [x] `createFileServer` test: `video%231/frame.jpg` serves file from `video#1/frame.jpg` on disk
- [x] Typecheck: engine + producer pass
- [x] Lint + format: 0 warnings, 0 errors
This commit is contained in:
Miguel Ángel
2026-05-02 06:30:12 +02:00
committed by GitHub
parent 4750a981dd
commit 2a897d351c
9 changed files with 301 additions and 40 deletions
+4 -23
View File
@@ -16,6 +16,7 @@ import { quantizeTimeToFrame } from "@hyperframes/core";
import {
acquireBrowser,
releaseBrowser,
forceReleaseBrowser,
buildChromeArgs,
resolveHeadlessShellPath,
type CaptureMode,
@@ -95,27 +96,6 @@ async function waitForCloseWithTimeout(promise: Promise<unknown>): Promise<boole
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,
outputDir: string,
@@ -718,7 +698,8 @@ export async function closeCaptureSession(session: CaptureSession): Promise<void
const pageClosed = await waitForCloseWithTimeout(session.page.close());
if (!pageClosed) {
console.warn("[FrameCapture] Timed out closing page; forcing browser process shutdown");
forceKillBrowserProcess(session.browser);
forceReleaseBrowser(session.browser);
session.browserReleased = true;
}
session.pageReleased = true;
}
@@ -728,7 +709,7 @@ export async function closeCaptureSession(session: CaptureSession): Promise<void
);
if (!browserClosed) {
console.warn("[FrameCapture] Timed out closing browser; forcing browser process shutdown");
forceKillBrowserProcess(session.browser);
forceReleaseBrowser(session.browser);
}
session.browserReleased = true;
}