@hyperframes/studio-server 0.8.80 → 0.8.81

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.
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  PROXY_PARAMS_VERSION,
3
3
  resolveProxy
4
- } from "./chunk-56JREE33.js";
4
+ } from "./chunk-G6IIFKXY.js";
5
5
  import {
6
6
  createMediaCodecProbeCache,
7
7
  proxyVariantFor,
8
8
  recordProxyPrewarm,
9
9
  scanProjectMediaCodecMap,
10
10
  shouldPrewarmProxy
11
- } from "./chunk-KOB22VPG.js";
11
+ } from "./chunk-TJQ76IEA.js";
12
12
 
13
13
  // src/helpers/mediaProxyPreview.ts
14
14
  import { resolve } from "path";
@@ -63,4 +63,4 @@ export {
63
63
  injectMediaCodecMapIntoHtml,
64
64
  injectMediaCodecMap
65
65
  };
66
- //# sourceMappingURL=chunk-YGANSFUU.js.map
66
+ //# sourceMappingURL=chunk-FTDTO3QP.js.map
@@ -1,20 +1,13 @@
1
1
  import {
2
2
  PROXY_VARIANT_CONFIG,
3
- probeMediaMetadata
4
- } from "./chunk-KOB22VPG.js";
3
+ probeMediaMetadata,
4
+ realpath
5
+ } from "./chunk-TJQ76IEA.js";
5
6
 
6
7
  // src/helpers/proxyTranscoder.ts
7
8
  import { spawn } from "child_process";
8
9
  import { createHash, randomUUID } from "crypto";
9
- import {
10
- existsSync as existsSync2,
11
- mkdirSync,
12
- realpathSync,
13
- renameSync,
14
- statSync as statSync2,
15
- unlinkSync as unlinkSync2,
16
- utimesSync
17
- } from "fs";
10
+ import { existsSync as existsSync2, mkdirSync, renameSync, statSync as statSync2, unlinkSync as unlinkSync2, utimesSync } from "fs";
18
11
  import { basename, dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
19
12
  import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
20
13
 
@@ -193,8 +186,8 @@ function canonicalizeProxySource(projectDir, absoluteSourcePath) {
193
186
  if (requestedRelativePath === ".." || requestedRelativePath.startsWith(`..${sep}`) || isAbsolute(requestedRelativePath)) {
194
187
  throw new ProxySourceOutsideProjectError();
195
188
  }
196
- const canonicalProjectDir = realpathSync(projectDir);
197
- const canonicalSourcePath = realpathSync(absoluteSourcePath);
189
+ const canonicalProjectDir = realpath(projectDir);
190
+ const canonicalSourcePath = realpath(absoluteSourcePath);
198
191
  const canonicalRelativePath = relative(canonicalProjectDir, canonicalSourcePath);
199
192
  const sourceIsInsideCanonicalProject = canonicalRelativePath !== ".." && !canonicalRelativePath.startsWith(`..${sep}`) && !isAbsolute(canonicalRelativePath);
200
193
  return {
@@ -463,4 +456,4 @@ export {
463
456
  clearFailedTranscodesForTest,
464
457
  resolveProxy
465
458
  };
466
- //# sourceMappingURL=chunk-56JREE33.js.map
459
+ //# sourceMappingURL=chunk-G6IIFKXY.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/helpers/proxyTranscoder.ts","../src/helpers/proxyCache.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport {\n existsSync,\n mkdirSync,\n realpathSync,\n renameSync,\n statSync,\n unlinkSync,\n utimesSync,\n} from \"node:fs\";\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport { findFfBinary } from \"@hyperframes/parsers/ff-binaries\";\nimport { probeMediaMetadata } from \"./mediaMetadata.js\";\nimport { cleanupProxyCache } from \"./proxyCache.js\";\nimport { PROXY_VARIANT_CONFIG, type ProxyVariant } from \"./mediaCodecMap.js\";\n\n/**\n * Transcodes browser-hostile local video sources (HEVC, ProRes, ...) into a\n * cached, seekable authoring proxy. Consumed by the preview/play/static\n * project routes (U3/U4) to serve a `?hf-proxy=` request; never used on\n * the render path (render always sees the original file).\n *\n * IMPORTANT — request-lifecycle detachment: nothing here accepts or wires an\n * AbortSignal. `resolveProxy` returns a promise shared by every concurrent\n * caller for the same cache key (in-flight dedupe below); if a route handler\n * killed the ffmpeg child on client abort (page reload, HMR), every other\n * caller waiting on that same promise would fail too, and the next request\n * would restart a transcode that may have been minutes into a long asset.\n * Callers MUST let the child run to completion regardless of request\n * cancellation and simply let the held response also abort — the cache\n * entry still lands for the next request.\n */\n\nexport const PROXY_PARAMS_VERSION = \"v4\";\n\nconst CACHE_DIR_NAME = \".transcode-cache\";\n\nfunction boundedEnvInteger(name: string, fallback: number, min: number, max: number): number {\n const raw = process.env[name]?.trim();\n if (!raw) return fallback;\n const parsed = Number(raw);\n return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;\n}\n\n// ffmpeg is internally multithreaded, so two concurrent proxy encodes already\n// saturate a typical laptop. Operators of shared/large machines may tune the\n// bounded values without patching the package; invalid values fail safe.\nconst MAX_CONCURRENT_TRANSCODES = boundedEnvInteger(\"HYPERFRAMES_PROXY_MAX_CONCURRENCY\", 2, 1, 16);\nconst MAX_QUEUED_TRANSCODES = boundedEnvInteger(\"HYPERFRAMES_PROXY_MAX_QUEUE\", 8, 0, 256);\n\nconst STDERR_TAIL_MAX_CHARS = 4000;\nexport const TRANSCODE_TIMEOUT_MS = 15 * 60 * 1000;\nconst FAILURE_CACHE_TTL_MS = 60 * 1000;\nconst MAX_FAILURE_CACHE_ENTRIES = 128;\nexport const DEFAULT_PROXY_WAIT_TIMEOUT_MS = 2 * 60 * 1000;\n\nexport class ProxyTranscodeError extends Error {\n readonly exitCode: number | null;\n readonly stderrTail: string;\n\n constructor(message: string, exitCode: number | null, stderrTail: string) {\n super(message);\n this.name = \"ProxyTranscodeError\";\n this.exitCode = exitCode;\n this.stderrTail = stderrTail;\n }\n}\n\n/** \"ffmpeg isn't installed\" — an environment condition, not a per-source\n * failure, so it is deliberately NOT remembered by the negative cache below\n * (installing ffmpeg mid-session must recover without a server restart). */\nclass FfmpegUnavailableError extends ProxyTranscodeError {\n constructor() {\n super(\"ffmpeg binary not found\", null, \"\");\n }\n}\n\nexport class FfmpegMissingFilterError extends ProxyTranscodeError {\n constructor() {\n super(\n \"HDR proxying requires ffmpeg zscale/tonemap filters (libzimg); install an ffmpeg build with libzimg support\",\n null,\n \"\",\n );\n this.name = \"FfmpegMissingFilterError\";\n }\n}\n\nexport class ProxyCapacityError extends ProxyTranscodeError {\n constructor() {\n super(\"media proxy queue is full; retry shortly\", null, \"\");\n this.name = \"ProxyCapacityError\";\n }\n}\n\nexport class ProxySourceOutsideProjectError extends ProxyTranscodeError {\n constructor() {\n super(\"media proxy source must be addressed through the project\", null, \"\");\n this.name = \"ProxySourceOutsideProjectError\";\n }\n}\n\nexport class ProxyWaitTimeoutError extends ProxyTranscodeError {\n constructor(timeoutMs: number) {\n super(`media proxy did not become ready within ${timeoutMs}ms`, null, \"\");\n this.name = \"ProxyWaitTimeoutError\";\n }\n}\n\n/** Bounds one caller's wait without cancelling the shared in-flight ffmpeg\n * job. Other preview/publish callers still receive the completed cache entry. */\nexport async function waitForProxy<T>(\n promise: Promise<T>,\n timeoutMs = DEFAULT_PROXY_WAIT_TIMEOUT_MS,\n): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => reject(new ProxyWaitTimeoutError(timeoutMs)), timeoutMs);\n timer.unref();\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n/**\n * Cache key inputs per the plan: project-relative source path (portable across\n * checkouts), canonical source identity (so retargeted external symlinks do\n * not reuse a proxy), mtime and file size (mtime alone can collide on\n * same-second re-exports on coarse-timestamp filesystems; size catches nearly\n * all such cases at zero cost), and a params version token so changing the\n * ffmpeg recipe below invalidates every cached proxy cleanly.\n */\ntype CanonicalProxySource = {\n projectDir: string;\n sourcePath: string;\n relativePath: string;\n cacheIdentity: string;\n};\n\nfunction canonicalizeProxySource(\n projectDir: string,\n absoluteSourcePath: string,\n): CanonicalProxySource {\n const requestedProjectDir = resolve(projectDir);\n const requestedSourcePath = resolve(absoluteSourcePath);\n const requestedRelativePath = relative(requestedProjectDir, requestedSourcePath);\n if (\n requestedRelativePath === \"..\" ||\n requestedRelativePath.startsWith(`..${sep}`) ||\n isAbsolute(requestedRelativePath)\n ) {\n throw new ProxySourceOutsideProjectError();\n }\n\n const canonicalProjectDir = realpathSync(projectDir);\n const canonicalSourcePath = realpathSync(absoluteSourcePath);\n const canonicalRelativePath = relative(canonicalProjectDir, canonicalSourcePath);\n const sourceIsInsideCanonicalProject =\n canonicalRelativePath !== \"..\" &&\n !canonicalRelativePath.startsWith(`..${sep}`) &&\n !isAbsolute(canonicalRelativePath);\n return {\n projectDir: canonicalProjectDir,\n sourcePath: canonicalSourcePath,\n relativePath: requestedRelativePath.normalize(\"NFC\"),\n // An external target needs a stable identity in addition to its project-local\n // symlink path, otherwise retargeting the link can reuse an unrelated proxy.\n cacheIdentity: (sourceIsInsideCanonicalProject\n ? canonicalRelativePath\n : canonicalSourcePath\n ).normalize(\"NFC\"),\n };\n}\n\nfunction buildProxyCacheKey(source: CanonicalProxySource, variant: ProxyVariant): string {\n const stat = statSync(source.sourcePath);\n return createHash(\"sha256\")\n .update(\n `${source.relativePath}\\0${source.cacheIdentity}\\0${stat.mtimeMs}\\0${stat.size}\\0${PROXY_PARAMS_VERSION}\\0${variant}`,\n )\n .digest(\"hex\");\n}\n\nfunction getCanonicalProxyCachePath(source: CanonicalProxySource, variant: ProxyVariant): string {\n const key = buildProxyCacheKey(source, variant);\n return join(\n source.projectDir,\n CACHE_DIR_NAME,\n `${key}${PROXY_VARIANT_CONFIG[variant].extension}`,\n );\n}\n\n/**\n * Computes the absolute path a proxy for this source would live at, without\n * transcoding anything. Route handlers use this to check cache state (e.g.\n * for ETag/If-None-Match) before deciding whether to await a transcode.\n */\nexport function getProxyCachePath(\n projectDir: string,\n absoluteSourcePath: string,\n variant: ProxyVariant = \"h264\",\n): string {\n return getCanonicalProxyCachePath(\n canonicalizeProxySource(projectDir, absoluteSourcePath),\n variant,\n );\n}\n\n// --- global concurrency limiter -------------------------------------------\n// ponytail: a bare counter + FIFO wait queue is the whole semaphore; no\n// dependency pulled in for this. Both element-triggered and pre-warm calls\n// go through the same `resolveProxy` entry point, so both queue here.\n\nlet activeTranscodes = 0;\nconst waitQueue: Array<() => void> = [];\n\nfunction acquireSlot(): Promise<void> {\n return new Promise((resolveSlot, reject) => {\n const tryAcquire = (): void => {\n if (activeTranscodes < MAX_CONCURRENT_TRANSCODES) {\n activeTranscodes++;\n resolveSlot();\n } else {\n if (waitQueue.length >= MAX_QUEUED_TRANSCODES) {\n reject(new ProxyCapacityError());\n return;\n }\n waitQueue.push(tryAcquire);\n }\n };\n tryAcquire();\n });\n}\n\nfunction releaseSlot(): void {\n activeTranscodes--;\n const next = waitQueue.shift();\n if (next) next();\n}\n\n// --- per-key in-flight dedupe ----------------------------------------------\n\nconst inFlight = new Map<string, Promise<string>>();\n\nfunction maintainProxyCache(cacheDir: string): void {\n try {\n cleanupProxyCache(cacheDir, { protectedPaths: new Set(inFlight.keys()) });\n } catch (error) {\n // Cache maintenance must never turn a playable preview into an error.\n console.warn(\n `[media-proxy] cache cleanup failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n}\n\nfunction markCacheEntryUsed(cachePath: string): void {\n try {\n const now = new Date();\n utimesSync(cachePath, now, now);\n } catch {\n // A concurrent cleanup may have removed a stale entry after existsSync;\n // the normal miss path below will recreate it on the next request.\n }\n}\n\n// --- negative cache ---------------------------------------------------------\n// A source that failed to transcode fails again identically until the file\n// changes (the cache key embeds mtime+size, so a re-export invalidates this\n// naturally). Remembering the failure per key means repeated `?hf-proxy=`\n// requests for a broken asset rethrow instantly instead of respawning ffmpeg\n// on every retry the browser makes.\ninterface RememberedFailure {\n error: ProxyTranscodeError;\n expiresAt: number;\n}\n\nconst failedTranscodes = new Map<string, RememberedFailure>();\n\nlet hdrFilterCheck: { ffmpegPath: string; promise: Promise<void> } | undefined;\n\nfunction ensureHdrFilters(ffmpegPath: string): Promise<void> {\n if (hdrFilterCheck?.ffmpegPath === ffmpegPath) return hdrFilterCheck.promise;\n const promise = new Promise<void>((resolveCheck, rejectCheck) => {\n const proc = spawn(ffmpegPath, [\"-hide_banner\", \"-filters\"], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n windowsHide: true,\n });\n let stdout = \"\";\n proc.stdout?.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n proc.on(\"error\", () => rejectCheck(new FfmpegMissingFilterError()));\n proc.on(\"close\", (code) => {\n if (code !== 0 || !/\\bzscale\\b/.test(stdout) || !/\\btonemap\\b/.test(stdout)) {\n rejectCheck(new FfmpegMissingFilterError());\n } else {\n resolveCheck();\n }\n });\n });\n hdrFilterCheck = { ffmpegPath, promise };\n return promise;\n}\n\nfunction rememberFailure(cachePath: string, error: ProxyTranscodeError): void {\n failedTranscodes.delete(cachePath);\n failedTranscodes.set(cachePath, { error, expiresAt: Date.now() + FAILURE_CACHE_TTL_MS });\n while (failedTranscodes.size > MAX_FAILURE_CACHE_ENTRIES) {\n const oldest = failedTranscodes.keys().next().value;\n if (oldest === undefined) break;\n failedTranscodes.delete(oldest);\n }\n}\n\n/** Test hook: forget remembered transcode failures (module state persists\n * across tests that don't reload the module). */\nexport function clearFailedTranscodesForTest(): void {\n failedTranscodes.clear();\n}\n\nasync function runFfmpeg(\n sourcePath: string,\n outputPath: string,\n variant: ProxyVariant,\n): Promise<void> {\n const metadata = await probeMediaMetadata(sourcePath);\n const ffmpegPath = findFfBinary(\"ffmpeg\", { configuredMustExist: true });\n if (!ffmpegPath) {\n throw new FfmpegUnavailableError();\n }\n // The HDR tonemap filters discard alpha. VP8 is the alpha-preserving proxy\n // variant, so retain its source color values instead of making it opaque.\n if (metadata.color.isHdr && variant !== \"vp8\") await ensureHdrFilters(ffmpegPath);\n const evenScale = \"scale=trunc(iw/2)*2:trunc(ih/2)*2\";\n const pixelFormat = variant === \"vp8\" ? \"yuva420p\" : \"yuv420p\";\n const videoFilter =\n metadata.color.isHdr && variant !== \"vp8\"\n ? [\n \"zscale=t=linear:npl=100\",\n \"tonemap=hable:desat=0\",\n \"zscale=p=bt709:t=bt709:m=bt709:r=tv\",\n evenScale,\n `format=${pixelFormat}`,\n ].join(\",\")\n : [evenScale, `format=${pixelFormat}`].join(\",\");\n\n return new Promise((resolvePromise, reject) => {\n const commonArgs = [\"-y\", \"-i\", sourcePath, \"-vf\", videoFilter];\n const h264Args = [\n \"-c:v\",\n \"libx264\",\n \"-profile:v\",\n \"high\",\n \"-pix_fmt\",\n \"yuv420p\",\n \"-colorspace\",\n \"bt709\",\n \"-color_primaries\",\n \"bt709\",\n \"-color_trc\",\n \"bt709\",\n \"-crf\",\n \"18\",\n \"-preset\",\n \"veryfast\",\n \"-c:a\",\n \"aac\",\n \"-movflags\",\n \"+faststart\",\n ];\n const vp8Args = [\n \"-c:v\",\n \"libvpx\",\n \"-b:v\",\n \"0\",\n \"-crf\",\n \"23\",\n \"-deadline\",\n \"good\",\n \"-pix_fmt\",\n \"yuva420p\",\n \"-colorspace\",\n \"bt709\",\n \"-color_primaries\",\n \"bt709\",\n \"-color_trc\",\n \"bt709\",\n \"-cpu-used\",\n \"4\",\n \"-auto-alt-ref\",\n \"0\",\n \"-metadata:s:v:0\",\n \"alpha_mode=1\",\n \"-ac\",\n \"2\",\n \"-c:a\",\n \"libopus\",\n ];\n const args = [...commonArgs, ...(variant === \"vp8\" ? vp8Args : h264Args), outputPath];\n\n // Hard ceiling so a hung ffmpeg can never permanently occupy one of the\n // global transcode slots: the child is killed and the slot released via\n // the caller's finally. Generous because long assets transcode at\n // roughly real time; a healthy encode of any authoring asset fits.\n const proc = spawn(ffmpegPath, args, {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n timeout: TRANSCODE_TIMEOUT_MS,\n killSignal: \"SIGKILL\",\n windowsHide: true,\n });\n let stderrTail = \"\";\n proc.stderr?.on(\"data\", (chunk: Buffer) => {\n stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_MAX_CHARS);\n });\n proc.on(\"error\", (err) => {\n reject(new ProxyTranscodeError(`failed to spawn ffmpeg: ${err.message}`, null, stderrTail));\n });\n proc.on(\"close\", (code, signal) => {\n if (code === 0) {\n resolvePromise();\n } else if (signal) {\n reject(\n new ProxyTranscodeError(\n `ffmpeg killed by ${signal} (timeout ${TRANSCODE_TIMEOUT_MS}ms or external kill)`,\n null,\n stderrTail,\n ),\n );\n } else {\n reject(new ProxyTranscodeError(`ffmpeg exited with code ${code}`, code, stderrTail));\n }\n });\n });\n}\n\nasync function transcodeToCache(\n absoluteSourcePath: string,\n cachePath: string,\n variant: ProxyVariant,\n): Promise<string> {\n await acquireSlot();\n try {\n // Another caller may have finished (or a pre-warm beat us) while queued.\n if (existsSync(cachePath)) return cachePath;\n\n const cacheDir = dirname(cachePath);\n mkdirSync(cacheDir, { recursive: true });\n const tempPath = join(cacheDir, `.tmp-${randomUUID()}-${basename(cachePath)}`);\n try {\n await runFfmpeg(absoluteSourcePath, tempPath, variant);\n renameSync(tempPath, cachePath);\n maintainProxyCache(cacheDir);\n return cachePath;\n } finally {\n // No partial files: if anything above threw, remove whatever ffmpeg\n // may have partially written under the temp name.\n if (existsSync(tempPath)) unlinkSync(tempPath);\n }\n } finally {\n releaseSlot();\n }\n}\n\n/**\n * Resolves the cached proxy variant for `absoluteSourcePath`, transcoding it at\n * most once per cache key. Concurrent calls for the same key (including a\n * pre-warm call racing an element-triggered one) share one ffmpeg child and\n * one promise; calls for different keys queue through the global concurrency\n * limiter above. Throws `ProxyTranscodeError` on failure (missing ffmpeg or a\n * nonzero exit) — callers (route handlers) decide how to surface that (502).\n */\nexport async function resolveProxy(\n projectDir: string,\n absoluteSourcePath: string,\n variant: ProxyVariant = \"h264\",\n): Promise<string> {\n const source = canonicalizeProxySource(projectDir, absoluteSourcePath);\n const cachePath = getCanonicalProxyCachePath(source, variant);\n if (existsSync(cachePath)) {\n markCacheEntryUsed(cachePath);\n maintainProxyCache(dirname(cachePath));\n return cachePath;\n }\n\n const rememberedFailure = failedTranscodes.get(cachePath);\n if (rememberedFailure) {\n if (rememberedFailure.expiresAt > Date.now()) throw rememberedFailure.error;\n failedTranscodes.delete(cachePath);\n }\n\n const existing = inFlight.get(cachePath);\n if (existing) return existing;\n\n const promise = transcodeToCache(source.sourcePath, cachePath, variant)\n .catch((err: unknown) => {\n if (\n err instanceof ProxyTranscodeError &&\n !(err instanceof FfmpegUnavailableError) &&\n !(err instanceof FfmpegMissingFilterError) &&\n !(err instanceof ProxyCapacityError) &&\n !(err instanceof ProxySourceOutsideProjectError)\n ) {\n rememberFailure(cachePath, err);\n }\n throw err;\n })\n .finally(() => {\n inFlight.delete(cachePath);\n });\n inFlight.set(cachePath, promise);\n return promise;\n}\n","import { existsSync, readdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { extname, join } from \"node:path\";\nimport { PROXY_VARIANT_CONFIG } from \"./mediaCodecMap.js\";\n\nconst DEFAULT_MAX_BYTES = 10 * 1024 * 1024 * 1024;\nconst DEFAULT_STALE_TEMP_MS = 60 * 60 * 1000;\nconst DEFAULT_MIN_SWEEP_INTERVAL_MS = 5 * 60 * 1000;\nconst PROXY_EXTENSIONS: ReadonlySet<string> = new Set(\n Object.values(PROXY_VARIANT_CONFIG).map(({ extension }) => extension),\n);\n\nexport interface ProxyCacheCleanupOptions {\n maxBytes?: number;\n maxIdleMs?: number;\n staleTempMs?: number;\n minSweepIntervalMs?: number;\n protectedPaths?: ReadonlySet<string>;\n now?: number;\n}\n\nexport interface ProxyCacheCleanupResult {\n removed: string[];\n bytesBefore: number;\n bytesAfter: number;\n skipped: boolean;\n}\n\ninterface CacheEntry {\n path: string;\n size: number;\n modifiedAt: number;\n protected: boolean;\n}\n\nconst lastSweepAt = new Map<string, number>();\n\nfunction positiveEnvNumber(name: string, fallback: number): number {\n const parsed = Number(process.env[name]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;\n}\n\nfunction proxyCacheCleanupDefaults(): Required<\n Pick<ProxyCacheCleanupOptions, \"maxBytes\" | \"maxIdleMs\" | \"staleTempMs\" | \"minSweepIntervalMs\">\n> {\n return {\n maxBytes: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_MAX_BYTES\", DEFAULT_MAX_BYTES),\n maxIdleMs: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_MAX_IDLE_DAYS\", 30) * 24 * 60 * 60 * 1000,\n staleTempMs: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS\", DEFAULT_STALE_TEMP_MS),\n minSweepIntervalMs: positiveEnvNumber(\n \"HYPERFRAMES_PROXY_CACHE_SWEEP_INTERVAL_MS\",\n DEFAULT_MIN_SWEEP_INTERVAL_MS,\n ),\n };\n}\n\nfunction shouldSkipSweep(cacheDir: string, now: number, minSweepIntervalMs: number): boolean {\n const previousSweep = lastSweepAt.get(cacheDir);\n if (previousSweep !== undefined && now - previousSweep < minSweepIntervalMs) return true;\n lastSweepAt.set(cacheDir, now);\n return false;\n}\n\nfunction readCacheInventory(\n cacheDir: string,\n protectedPaths: ReadonlySet<string>,\n now: number,\n staleTempMs: number,\n): { entries: CacheEntry[]; staleTemps: CacheEntry[] } {\n const entries: CacheEntry[] = [];\n const staleTemps: CacheEntry[] = [];\n for (const dirent of readdirSync(cacheDir, { withFileTypes: true })) {\n if (!dirent.isFile()) continue;\n const path = join(cacheDir, dirent.name);\n const stat = statSync(path);\n const entry = {\n path,\n size: stat.size,\n modifiedAt: stat.mtimeMs,\n protected: protectedPaths.has(path),\n };\n if (dirent.name.startsWith(\".tmp-\")) {\n if (now - stat.mtimeMs >= staleTempMs) staleTemps.push(entry);\n } else if (PROXY_EXTENSIONS.has(extname(dirent.name))) {\n entries.push(entry);\n }\n }\n const oldestFirst = (a: CacheEntry, b: CacheEntry): number =>\n a.modifiedAt - b.modifiedAt || a.path.localeCompare(b.path);\n entries.sort(oldestFirst);\n staleTemps.sort(oldestFirst);\n return { entries, staleTemps };\n}\n\nfunction evictCacheEntries(\n entries: CacheEntry[],\n staleTemps: CacheEntry[],\n now: number,\n maxIdleMs: number,\n maxBytes: number,\n): Omit<ProxyCacheCleanupResult, \"skipped\"> {\n const bytesBefore = entries.reduce((total, entry) => total + entry.size, 0);\n let bytesAfter = bytesBefore;\n const removed: string[] = [];\n const remove = (entry: CacheEntry, countsTowardBudget: boolean): void => {\n unlinkSync(entry.path);\n removed.push(entry.path);\n if (countsTowardBudget) bytesAfter -= entry.size;\n };\n\n for (const entry of staleTemps) remove(entry, false);\n for (const entry of entries) {\n if (!entry.protected && now - entry.modifiedAt >= maxIdleMs) remove(entry, true);\n }\n for (const entry of entries) {\n if (bytesAfter <= maxBytes) break;\n if (!entry.protected && existsSync(entry.path)) remove(entry, true);\n }\n return { removed, bytesBefore, bytesAfter };\n}\n\n/**\n * Opportunistically bounds a project's transparent-proxy cache. Cleanup is\n * synchronous because callers already perform filesystem bookkeeping on the\n * preview request path, but rate limiting keeps the directory scan off the\n * hot path. Errors intentionally bubble so callers can warn without turning\n * a cache-maintenance failure into a preview failure.\n */\nexport function cleanupProxyCache(\n cacheDir: string,\n options: ProxyCacheCleanupOptions = {},\n): ProxyCacheCleanupResult {\n const defaults = proxyCacheCleanupDefaults();\n const now = options.now ?? Date.now();\n const minSweepIntervalMs = options.minSweepIntervalMs ?? defaults.minSweepIntervalMs;\n if (shouldSkipSweep(cacheDir, now, minSweepIntervalMs)) {\n return { removed: [], bytesBefore: 0, bytesAfter: 0, skipped: true };\n }\n if (!existsSync(cacheDir)) {\n return { removed: [], bytesBefore: 0, bytesAfter: 0, skipped: false };\n }\n\n const maxBytes = options.maxBytes ?? defaults.maxBytes;\n const maxIdleMs = options.maxIdleMs ?? defaults.maxIdleMs;\n const staleTempMs = options.staleTempMs ?? defaults.staleTempMs;\n const protectedPaths = options.protectedPaths ?? new Set<string>();\n const { entries, staleTemps } = readCacheInventory(cacheDir, protectedPaths, now, staleTempMs);\n return {\n ...evictCacheEntries(entries, staleTemps, now, maxIdleMs, maxBytes),\n skipped: false,\n };\n}\n"],"mappings":"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","names":["existsSync","statSync","unlinkSync","join","statSync","join","existsSync","unlinkSync"]}
1
+ {"version":3,"sources":["../src/helpers/proxyTranscoder.ts","../src/helpers/proxyCache.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, renameSync, statSync, unlinkSync, utimesSync } from \"node:fs\";\nimport { basename, dirname, isAbsolute, join, relative, resolve, sep } from \"node:path\";\nimport { findFfBinary } from \"@hyperframes/parsers/ff-binaries\";\nimport { probeMediaMetadata } from \"./mediaMetadata.js\";\nimport { cleanupProxyCache } from \"./proxyCache.js\";\nimport { PROXY_VARIANT_CONFIG, type ProxyVariant } from \"./mediaCodecMap.js\";\nimport { realpath } from \"./safePath.js\";\n\n/**\n * Transcodes browser-hostile local video sources (HEVC, ProRes, ...) into a\n * cached, seekable authoring proxy. Consumed by the preview/play/static\n * project routes (U3/U4) to serve a `?hf-proxy=` request; never used on\n * the render path (render always sees the original file).\n *\n * IMPORTANT — request-lifecycle detachment: nothing here accepts or wires an\n * AbortSignal. `resolveProxy` returns a promise shared by every concurrent\n * caller for the same cache key (in-flight dedupe below); if a route handler\n * killed the ffmpeg child on client abort (page reload, HMR), every other\n * caller waiting on that same promise would fail too, and the next request\n * would restart a transcode that may have been minutes into a long asset.\n * Callers MUST let the child run to completion regardless of request\n * cancellation and simply let the held response also abort — the cache\n * entry still lands for the next request.\n */\n\nexport const PROXY_PARAMS_VERSION = \"v4\";\n\nconst CACHE_DIR_NAME = \".transcode-cache\";\n\nfunction boundedEnvInteger(name: string, fallback: number, min: number, max: number): number {\n const raw = process.env[name]?.trim();\n if (!raw) return fallback;\n const parsed = Number(raw);\n return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;\n}\n\n// ffmpeg is internally multithreaded, so two concurrent proxy encodes already\n// saturate a typical laptop. Operators of shared/large machines may tune the\n// bounded values without patching the package; invalid values fail safe.\nconst MAX_CONCURRENT_TRANSCODES = boundedEnvInteger(\"HYPERFRAMES_PROXY_MAX_CONCURRENCY\", 2, 1, 16);\nconst MAX_QUEUED_TRANSCODES = boundedEnvInteger(\"HYPERFRAMES_PROXY_MAX_QUEUE\", 8, 0, 256);\n\nconst STDERR_TAIL_MAX_CHARS = 4000;\nexport const TRANSCODE_TIMEOUT_MS = 15 * 60 * 1000;\nconst FAILURE_CACHE_TTL_MS = 60 * 1000;\nconst MAX_FAILURE_CACHE_ENTRIES = 128;\nexport const DEFAULT_PROXY_WAIT_TIMEOUT_MS = 2 * 60 * 1000;\n\nexport class ProxyTranscodeError extends Error {\n readonly exitCode: number | null;\n readonly stderrTail: string;\n\n constructor(message: string, exitCode: number | null, stderrTail: string) {\n super(message);\n this.name = \"ProxyTranscodeError\";\n this.exitCode = exitCode;\n this.stderrTail = stderrTail;\n }\n}\n\n/** \"ffmpeg isn't installed\" — an environment condition, not a per-source\n * failure, so it is deliberately NOT remembered by the negative cache below\n * (installing ffmpeg mid-session must recover without a server restart). */\nclass FfmpegUnavailableError extends ProxyTranscodeError {\n constructor() {\n super(\"ffmpeg binary not found\", null, \"\");\n }\n}\n\nexport class FfmpegMissingFilterError extends ProxyTranscodeError {\n constructor() {\n super(\n \"HDR proxying requires ffmpeg zscale/tonemap filters (libzimg); install an ffmpeg build with libzimg support\",\n null,\n \"\",\n );\n this.name = \"FfmpegMissingFilterError\";\n }\n}\n\nexport class ProxyCapacityError extends ProxyTranscodeError {\n constructor() {\n super(\"media proxy queue is full; retry shortly\", null, \"\");\n this.name = \"ProxyCapacityError\";\n }\n}\n\nexport class ProxySourceOutsideProjectError extends ProxyTranscodeError {\n constructor() {\n super(\"media proxy source must be addressed through the project\", null, \"\");\n this.name = \"ProxySourceOutsideProjectError\";\n }\n}\n\nexport class ProxyWaitTimeoutError extends ProxyTranscodeError {\n constructor(timeoutMs: number) {\n super(`media proxy did not become ready within ${timeoutMs}ms`, null, \"\");\n this.name = \"ProxyWaitTimeoutError\";\n }\n}\n\n/** Bounds one caller's wait without cancelling the shared in-flight ffmpeg\n * job. Other preview/publish callers still receive the completed cache entry. */\nexport async function waitForProxy<T>(\n promise: Promise<T>,\n timeoutMs = DEFAULT_PROXY_WAIT_TIMEOUT_MS,\n): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => reject(new ProxyWaitTimeoutError(timeoutMs)), timeoutMs);\n timer.unref();\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n/**\n * Cache key inputs per the plan: project-relative source path (portable across\n * checkouts), canonical source identity (so retargeted external symlinks do\n * not reuse a proxy), mtime and file size (mtime alone can collide on\n * same-second re-exports on coarse-timestamp filesystems; size catches nearly\n * all such cases at zero cost), and a params version token so changing the\n * ffmpeg recipe below invalidates every cached proxy cleanly.\n */\ntype CanonicalProxySource = {\n projectDir: string;\n sourcePath: string;\n relativePath: string;\n cacheIdentity: string;\n};\n\nfunction canonicalizeProxySource(\n projectDir: string,\n absoluteSourcePath: string,\n): CanonicalProxySource {\n const requestedProjectDir = resolve(projectDir);\n const requestedSourcePath = resolve(absoluteSourcePath);\n const requestedRelativePath = relative(requestedProjectDir, requestedSourcePath);\n if (\n requestedRelativePath === \"..\" ||\n requestedRelativePath.startsWith(`..${sep}`) ||\n isAbsolute(requestedRelativePath)\n ) {\n throw new ProxySourceOutsideProjectError();\n }\n\n const canonicalProjectDir = realpath(projectDir);\n const canonicalSourcePath = realpath(absoluteSourcePath);\n const canonicalRelativePath = relative(canonicalProjectDir, canonicalSourcePath);\n const sourceIsInsideCanonicalProject =\n canonicalRelativePath !== \"..\" &&\n !canonicalRelativePath.startsWith(`..${sep}`) &&\n !isAbsolute(canonicalRelativePath);\n return {\n projectDir: canonicalProjectDir,\n sourcePath: canonicalSourcePath,\n relativePath: requestedRelativePath.normalize(\"NFC\"),\n // An external target needs a stable identity in addition to its project-local\n // symlink path, otherwise retargeting the link can reuse an unrelated proxy.\n cacheIdentity: (sourceIsInsideCanonicalProject\n ? canonicalRelativePath\n : canonicalSourcePath\n ).normalize(\"NFC\"),\n };\n}\n\nfunction buildProxyCacheKey(source: CanonicalProxySource, variant: ProxyVariant): string {\n const stat = statSync(source.sourcePath);\n return createHash(\"sha256\")\n .update(\n `${source.relativePath}\\0${source.cacheIdentity}\\0${stat.mtimeMs}\\0${stat.size}\\0${PROXY_PARAMS_VERSION}\\0${variant}`,\n )\n .digest(\"hex\");\n}\n\nfunction getCanonicalProxyCachePath(source: CanonicalProxySource, variant: ProxyVariant): string {\n const key = buildProxyCacheKey(source, variant);\n return join(\n source.projectDir,\n CACHE_DIR_NAME,\n `${key}${PROXY_VARIANT_CONFIG[variant].extension}`,\n );\n}\n\n/**\n * Computes the absolute path a proxy for this source would live at, without\n * transcoding anything. Route handlers use this to check cache state (e.g.\n * for ETag/If-None-Match) before deciding whether to await a transcode.\n */\nexport function getProxyCachePath(\n projectDir: string,\n absoluteSourcePath: string,\n variant: ProxyVariant = \"h264\",\n): string {\n return getCanonicalProxyCachePath(\n canonicalizeProxySource(projectDir, absoluteSourcePath),\n variant,\n );\n}\n\n// --- global concurrency limiter -------------------------------------------\n// ponytail: a bare counter + FIFO wait queue is the whole semaphore; no\n// dependency pulled in for this. Both element-triggered and pre-warm calls\n// go through the same `resolveProxy` entry point, so both queue here.\n\nlet activeTranscodes = 0;\nconst waitQueue: Array<() => void> = [];\n\nfunction acquireSlot(): Promise<void> {\n return new Promise((resolveSlot, reject) => {\n const tryAcquire = (): void => {\n if (activeTranscodes < MAX_CONCURRENT_TRANSCODES) {\n activeTranscodes++;\n resolveSlot();\n } else {\n if (waitQueue.length >= MAX_QUEUED_TRANSCODES) {\n reject(new ProxyCapacityError());\n return;\n }\n waitQueue.push(tryAcquire);\n }\n };\n tryAcquire();\n });\n}\n\nfunction releaseSlot(): void {\n activeTranscodes--;\n const next = waitQueue.shift();\n if (next) next();\n}\n\n// --- per-key in-flight dedupe ----------------------------------------------\n\nconst inFlight = new Map<string, Promise<string>>();\n\nfunction maintainProxyCache(cacheDir: string): void {\n try {\n cleanupProxyCache(cacheDir, { protectedPaths: new Set(inFlight.keys()) });\n } catch (error) {\n // Cache maintenance must never turn a playable preview into an error.\n console.warn(\n `[media-proxy] cache cleanup failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n}\n\nfunction markCacheEntryUsed(cachePath: string): void {\n try {\n const now = new Date();\n utimesSync(cachePath, now, now);\n } catch {\n // A concurrent cleanup may have removed a stale entry after existsSync;\n // the normal miss path below will recreate it on the next request.\n }\n}\n\n// --- negative cache ---------------------------------------------------------\n// A source that failed to transcode fails again identically until the file\n// changes (the cache key embeds mtime+size, so a re-export invalidates this\n// naturally). Remembering the failure per key means repeated `?hf-proxy=`\n// requests for a broken asset rethrow instantly instead of respawning ffmpeg\n// on every retry the browser makes.\ninterface RememberedFailure {\n error: ProxyTranscodeError;\n expiresAt: number;\n}\n\nconst failedTranscodes = new Map<string, RememberedFailure>();\n\nlet hdrFilterCheck: { ffmpegPath: string; promise: Promise<void> } | undefined;\n\nfunction ensureHdrFilters(ffmpegPath: string): Promise<void> {\n if (hdrFilterCheck?.ffmpegPath === ffmpegPath) return hdrFilterCheck.promise;\n const promise = new Promise<void>((resolveCheck, rejectCheck) => {\n const proc = spawn(ffmpegPath, [\"-hide_banner\", \"-filters\"], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n windowsHide: true,\n });\n let stdout = \"\";\n proc.stdout?.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n proc.on(\"error\", () => rejectCheck(new FfmpegMissingFilterError()));\n proc.on(\"close\", (code) => {\n if (code !== 0 || !/\\bzscale\\b/.test(stdout) || !/\\btonemap\\b/.test(stdout)) {\n rejectCheck(new FfmpegMissingFilterError());\n } else {\n resolveCheck();\n }\n });\n });\n hdrFilterCheck = { ffmpegPath, promise };\n return promise;\n}\n\nfunction rememberFailure(cachePath: string, error: ProxyTranscodeError): void {\n failedTranscodes.delete(cachePath);\n failedTranscodes.set(cachePath, { error, expiresAt: Date.now() + FAILURE_CACHE_TTL_MS });\n while (failedTranscodes.size > MAX_FAILURE_CACHE_ENTRIES) {\n const oldest = failedTranscodes.keys().next().value;\n if (oldest === undefined) break;\n failedTranscodes.delete(oldest);\n }\n}\n\n/** Test hook: forget remembered transcode failures (module state persists\n * across tests that don't reload the module). */\nexport function clearFailedTranscodesForTest(): void {\n failedTranscodes.clear();\n}\n\nasync function runFfmpeg(\n sourcePath: string,\n outputPath: string,\n variant: ProxyVariant,\n): Promise<void> {\n const metadata = await probeMediaMetadata(sourcePath);\n const ffmpegPath = findFfBinary(\"ffmpeg\", { configuredMustExist: true });\n if (!ffmpegPath) {\n throw new FfmpegUnavailableError();\n }\n // The HDR tonemap filters discard alpha. VP8 is the alpha-preserving proxy\n // variant, so retain its source color values instead of making it opaque.\n if (metadata.color.isHdr && variant !== \"vp8\") await ensureHdrFilters(ffmpegPath);\n const evenScale = \"scale=trunc(iw/2)*2:trunc(ih/2)*2\";\n const pixelFormat = variant === \"vp8\" ? \"yuva420p\" : \"yuv420p\";\n const videoFilter =\n metadata.color.isHdr && variant !== \"vp8\"\n ? [\n \"zscale=t=linear:npl=100\",\n \"tonemap=hable:desat=0\",\n \"zscale=p=bt709:t=bt709:m=bt709:r=tv\",\n evenScale,\n `format=${pixelFormat}`,\n ].join(\",\")\n : [evenScale, `format=${pixelFormat}`].join(\",\");\n\n return new Promise((resolvePromise, reject) => {\n const commonArgs = [\"-y\", \"-i\", sourcePath, \"-vf\", videoFilter];\n const h264Args = [\n \"-c:v\",\n \"libx264\",\n \"-profile:v\",\n \"high\",\n \"-pix_fmt\",\n \"yuv420p\",\n \"-colorspace\",\n \"bt709\",\n \"-color_primaries\",\n \"bt709\",\n \"-color_trc\",\n \"bt709\",\n \"-crf\",\n \"18\",\n \"-preset\",\n \"veryfast\",\n \"-c:a\",\n \"aac\",\n \"-movflags\",\n \"+faststart\",\n ];\n const vp8Args = [\n \"-c:v\",\n \"libvpx\",\n \"-b:v\",\n \"0\",\n \"-crf\",\n \"23\",\n \"-deadline\",\n \"good\",\n \"-pix_fmt\",\n \"yuva420p\",\n \"-colorspace\",\n \"bt709\",\n \"-color_primaries\",\n \"bt709\",\n \"-color_trc\",\n \"bt709\",\n \"-cpu-used\",\n \"4\",\n \"-auto-alt-ref\",\n \"0\",\n \"-metadata:s:v:0\",\n \"alpha_mode=1\",\n \"-ac\",\n \"2\",\n \"-c:a\",\n \"libopus\",\n ];\n const args = [...commonArgs, ...(variant === \"vp8\" ? vp8Args : h264Args), outputPath];\n\n // Hard ceiling so a hung ffmpeg can never permanently occupy one of the\n // global transcode slots: the child is killed and the slot released via\n // the caller's finally. Generous because long assets transcode at\n // roughly real time; a healthy encode of any authoring asset fits.\n const proc = spawn(ffmpegPath, args, {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n timeout: TRANSCODE_TIMEOUT_MS,\n killSignal: \"SIGKILL\",\n windowsHide: true,\n });\n let stderrTail = \"\";\n proc.stderr?.on(\"data\", (chunk: Buffer) => {\n stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_MAX_CHARS);\n });\n proc.on(\"error\", (err) => {\n reject(new ProxyTranscodeError(`failed to spawn ffmpeg: ${err.message}`, null, stderrTail));\n });\n proc.on(\"close\", (code, signal) => {\n if (code === 0) {\n resolvePromise();\n } else if (signal) {\n reject(\n new ProxyTranscodeError(\n `ffmpeg killed by ${signal} (timeout ${TRANSCODE_TIMEOUT_MS}ms or external kill)`,\n null,\n stderrTail,\n ),\n );\n } else {\n reject(new ProxyTranscodeError(`ffmpeg exited with code ${code}`, code, stderrTail));\n }\n });\n });\n}\n\nasync function transcodeToCache(\n absoluteSourcePath: string,\n cachePath: string,\n variant: ProxyVariant,\n): Promise<string> {\n await acquireSlot();\n try {\n // Another caller may have finished (or a pre-warm beat us) while queued.\n if (existsSync(cachePath)) return cachePath;\n\n const cacheDir = dirname(cachePath);\n mkdirSync(cacheDir, { recursive: true });\n const tempPath = join(cacheDir, `.tmp-${randomUUID()}-${basename(cachePath)}`);\n try {\n await runFfmpeg(absoluteSourcePath, tempPath, variant);\n renameSync(tempPath, cachePath);\n maintainProxyCache(cacheDir);\n return cachePath;\n } finally {\n // No partial files: if anything above threw, remove whatever ffmpeg\n // may have partially written under the temp name.\n if (existsSync(tempPath)) unlinkSync(tempPath);\n }\n } finally {\n releaseSlot();\n }\n}\n\n/**\n * Resolves the cached proxy variant for `absoluteSourcePath`, transcoding it at\n * most once per cache key. Concurrent calls for the same key (including a\n * pre-warm call racing an element-triggered one) share one ffmpeg child and\n * one promise; calls for different keys queue through the global concurrency\n * limiter above. Throws `ProxyTranscodeError` on failure (missing ffmpeg or a\n * nonzero exit) — callers (route handlers) decide how to surface that (502).\n */\nexport async function resolveProxy(\n projectDir: string,\n absoluteSourcePath: string,\n variant: ProxyVariant = \"h264\",\n): Promise<string> {\n const source = canonicalizeProxySource(projectDir, absoluteSourcePath);\n const cachePath = getCanonicalProxyCachePath(source, variant);\n if (existsSync(cachePath)) {\n markCacheEntryUsed(cachePath);\n maintainProxyCache(dirname(cachePath));\n return cachePath;\n }\n\n const rememberedFailure = failedTranscodes.get(cachePath);\n if (rememberedFailure) {\n if (rememberedFailure.expiresAt > Date.now()) throw rememberedFailure.error;\n failedTranscodes.delete(cachePath);\n }\n\n const existing = inFlight.get(cachePath);\n if (existing) return existing;\n\n const promise = transcodeToCache(source.sourcePath, cachePath, variant)\n .catch((err: unknown) => {\n if (\n err instanceof ProxyTranscodeError &&\n !(err instanceof FfmpegUnavailableError) &&\n !(err instanceof FfmpegMissingFilterError) &&\n !(err instanceof ProxyCapacityError) &&\n !(err instanceof ProxySourceOutsideProjectError)\n ) {\n rememberFailure(cachePath, err);\n }\n throw err;\n })\n .finally(() => {\n inFlight.delete(cachePath);\n });\n inFlight.set(cachePath, promise);\n return promise;\n}\n","import { existsSync, readdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { extname, join } from \"node:path\";\nimport { PROXY_VARIANT_CONFIG } from \"./mediaCodecMap.js\";\n\nconst DEFAULT_MAX_BYTES = 10 * 1024 * 1024 * 1024;\nconst DEFAULT_STALE_TEMP_MS = 60 * 60 * 1000;\nconst DEFAULT_MIN_SWEEP_INTERVAL_MS = 5 * 60 * 1000;\nconst PROXY_EXTENSIONS: ReadonlySet<string> = new Set(\n Object.values(PROXY_VARIANT_CONFIG).map(({ extension }) => extension),\n);\n\nexport interface ProxyCacheCleanupOptions {\n maxBytes?: number;\n maxIdleMs?: number;\n staleTempMs?: number;\n minSweepIntervalMs?: number;\n protectedPaths?: ReadonlySet<string>;\n now?: number;\n}\n\nexport interface ProxyCacheCleanupResult {\n removed: string[];\n bytesBefore: number;\n bytesAfter: number;\n skipped: boolean;\n}\n\ninterface CacheEntry {\n path: string;\n size: number;\n modifiedAt: number;\n protected: boolean;\n}\n\nconst lastSweepAt = new Map<string, number>();\n\nfunction positiveEnvNumber(name: string, fallback: number): number {\n const parsed = Number(process.env[name]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;\n}\n\nfunction proxyCacheCleanupDefaults(): Required<\n Pick<ProxyCacheCleanupOptions, \"maxBytes\" | \"maxIdleMs\" | \"staleTempMs\" | \"minSweepIntervalMs\">\n> {\n return {\n maxBytes: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_MAX_BYTES\", DEFAULT_MAX_BYTES),\n maxIdleMs: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_MAX_IDLE_DAYS\", 30) * 24 * 60 * 60 * 1000,\n staleTempMs: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS\", DEFAULT_STALE_TEMP_MS),\n minSweepIntervalMs: positiveEnvNumber(\n \"HYPERFRAMES_PROXY_CACHE_SWEEP_INTERVAL_MS\",\n DEFAULT_MIN_SWEEP_INTERVAL_MS,\n ),\n };\n}\n\nfunction shouldSkipSweep(cacheDir: string, now: number, minSweepIntervalMs: number): boolean {\n const previousSweep = lastSweepAt.get(cacheDir);\n if (previousSweep !== undefined && now - previousSweep < minSweepIntervalMs) return true;\n lastSweepAt.set(cacheDir, now);\n return false;\n}\n\nfunction readCacheInventory(\n cacheDir: string,\n protectedPaths: ReadonlySet<string>,\n now: number,\n staleTempMs: number,\n): { entries: CacheEntry[]; staleTemps: CacheEntry[] } {\n const entries: CacheEntry[] = [];\n const staleTemps: CacheEntry[] = [];\n for (const dirent of readdirSync(cacheDir, { withFileTypes: true })) {\n if (!dirent.isFile()) continue;\n const path = join(cacheDir, dirent.name);\n const stat = statSync(path);\n const entry = {\n path,\n size: stat.size,\n modifiedAt: stat.mtimeMs,\n protected: protectedPaths.has(path),\n };\n if (dirent.name.startsWith(\".tmp-\")) {\n if (now - stat.mtimeMs >= staleTempMs) staleTemps.push(entry);\n } else if (PROXY_EXTENSIONS.has(extname(dirent.name))) {\n entries.push(entry);\n }\n }\n const oldestFirst = (a: CacheEntry, b: CacheEntry): number =>\n a.modifiedAt - b.modifiedAt || a.path.localeCompare(b.path);\n entries.sort(oldestFirst);\n staleTemps.sort(oldestFirst);\n return { entries, staleTemps };\n}\n\nfunction evictCacheEntries(\n entries: CacheEntry[],\n staleTemps: CacheEntry[],\n now: number,\n maxIdleMs: number,\n maxBytes: number,\n): Omit<ProxyCacheCleanupResult, \"skipped\"> {\n const bytesBefore = entries.reduce((total, entry) => total + entry.size, 0);\n let bytesAfter = bytesBefore;\n const removed: string[] = [];\n const remove = (entry: CacheEntry, countsTowardBudget: boolean): void => {\n unlinkSync(entry.path);\n removed.push(entry.path);\n if (countsTowardBudget) bytesAfter -= entry.size;\n };\n\n for (const entry of staleTemps) remove(entry, false);\n for (const entry of entries) {\n if (!entry.protected && now - entry.modifiedAt >= maxIdleMs) remove(entry, true);\n }\n for (const entry of entries) {\n if (bytesAfter <= maxBytes) break;\n if (!entry.protected && existsSync(entry.path)) remove(entry, true);\n }\n return { removed, bytesBefore, bytesAfter };\n}\n\n/**\n * Opportunistically bounds a project's transparent-proxy cache. Cleanup is\n * synchronous because callers already perform filesystem bookkeeping on the\n * preview request path, but rate limiting keeps the directory scan off the\n * hot path. Errors intentionally bubble so callers can warn without turning\n * a cache-maintenance failure into a preview failure.\n */\nexport function cleanupProxyCache(\n cacheDir: string,\n options: ProxyCacheCleanupOptions = {},\n): ProxyCacheCleanupResult {\n const defaults = proxyCacheCleanupDefaults();\n const now = options.now ?? Date.now();\n const minSweepIntervalMs = options.minSweepIntervalMs ?? defaults.minSweepIntervalMs;\n if (shouldSkipSweep(cacheDir, now, minSweepIntervalMs)) {\n return { removed: [], bytesBefore: 0, bytesAfter: 0, skipped: true };\n }\n if (!existsSync(cacheDir)) {\n return { removed: [], bytesBefore: 0, bytesAfter: 0, skipped: false };\n }\n\n const maxBytes = options.maxBytes ?? defaults.maxBytes;\n const maxIdleMs = options.maxIdleMs ?? defaults.maxIdleMs;\n const staleTempMs = options.staleTempMs ?? defaults.staleTempMs;\n const protectedPaths = options.protectedPaths ?? new Set<string>();\n const { entries, staleTemps } = readCacheInventory(cacheDir, protectedPaths, now, staleTempMs);\n return {\n ...evictCacheEntries(entries, staleTemps, now, maxIdleMs, maxBytes),\n skipped: false,\n };\n}\n"],"mappings":"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","names":["existsSync","statSync","unlinkSync","join","statSync","join","existsSync","unlinkSync"]}
@@ -1,6 +1,65 @@
1
1
  // src/helpers/mediaCodecMap.ts
2
- import { existsSync, realpathSync, statSync } from "fs";
3
- import { relative, resolve, sep } from "path";
2
+ import { existsSync, statSync } from "fs";
3
+
4
+ // src/helpers/safePath.ts
5
+ import { basename, dirname, isAbsolute, join, relative, sep } from "path";
6
+ import { readdirSync } from "fs";
7
+ import { realpath, resolveWithinProject } from "@hyperframes/core";
8
+ import { isSafePath, realpath as realpath2, resolveWithinProject as resolveWithinProject2 } from "@hyperframes/core";
9
+ function realFilePath(filePath) {
10
+ try {
11
+ return realpath(filePath);
12
+ } catch {
13
+ const dir = dirname(filePath);
14
+ return dir === filePath ? filePath : join(realFilePath(dir), basename(filePath));
15
+ }
16
+ }
17
+ function pinWithinProject(base, relativePath) {
18
+ const checked = resolveWithinProject(base, relativePath);
19
+ if (!checked) return null;
20
+ const inside = relative(realFilePath(base), realFilePath(checked));
21
+ const escapes = inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
22
+ return escapes ? null : join(base, inside);
23
+ }
24
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
25
+ ".thumbnails",
26
+ ".transcode-cache",
27
+ ".waveform-cache",
28
+ "node_modules",
29
+ ".git"
30
+ ]);
31
+ function shouldIgnoreDir(rel) {
32
+ return rel === ".hyperframes/backup";
33
+ }
34
+ function isInHiddenOrVendorDir(relPath) {
35
+ const segments = relPath.split("/");
36
+ return segments.slice(0, -1).some((seg) => seg.startsWith(".") || seg === "node_modules");
37
+ }
38
+ var UNREADABLE_DIR_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "ENOENT", "ENOTDIR"]);
39
+ function walkDir(dir, prefix = "") {
40
+ let entries;
41
+ try {
42
+ entries = readdirSync(dir, { withFileTypes: true });
43
+ } catch (err) {
44
+ const code = err.code;
45
+ if (!prefix || !code || !UNREADABLE_DIR_CODES.has(code)) throw err;
46
+ return [];
47
+ }
48
+ const files = [];
49
+ for (const entry of entries) {
50
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
51
+ if (IGNORE_DIRS.has(entry.name) || shouldIgnoreDir(rel)) continue;
52
+ if (entry.isDirectory()) {
53
+ files.push(...walkDir(join(dir, entry.name), rel));
54
+ } else {
55
+ files.push(rel);
56
+ }
57
+ }
58
+ return files;
59
+ }
60
+
61
+ // src/helpers/mediaCodecMap.ts
62
+ import { relative as relative2, resolve, sep as sep2 } from "path";
4
63
  import { rewriteAssetPath } from "@hyperframes/parsers/asset-paths";
5
64
  import {
6
65
  cleanAssetUrl,
@@ -241,7 +300,7 @@ async function probeAssetCodec(filePath, runner, cache = defaultProbeCache) {
241
300
  let target;
242
301
  try {
243
302
  stat = statSync(filePath);
244
- target = realpathSync(filePath);
303
+ target = realpath2(filePath);
245
304
  } catch {
246
305
  return null;
247
306
  }
@@ -262,7 +321,7 @@ function resolveExistingLocalAsset(projectDir, url) {
262
321
  (candidate) => existsSync(candidate)
263
322
  );
264
323
  if (!resolvedPath) return null;
265
- const rootRelative = relative(projectRoot, resolvedPath).split(sep).join("/");
324
+ const rootRelative = relative2(projectRoot, resolvedPath).split(sep2).join("/");
266
325
  return { resolvedPath, rootRelativePathname: `/${rootRelative}` };
267
326
  }
268
327
  function collectLocalVideoAssets(projectDir, htmlSources) {
@@ -312,6 +371,13 @@ async function scanProjectMediaCodecMap(projectDir, htmlSources, options = {}) {
312
371
  }
313
372
 
314
373
  export {
374
+ realFilePath,
375
+ pinWithinProject,
376
+ isInHiddenOrVendorDir,
377
+ walkDir,
378
+ isSafePath,
379
+ realpath2 as realpath,
380
+ resolveWithinProject2 as resolveWithinProject,
315
381
  probeMediaMetadata,
316
382
  BROWSER_HOSTILE_CODECS,
317
383
  shouldPrewarmProxy,
@@ -328,4 +394,4 @@ export {
328
394
  probeAssetCodec,
329
395
  scanProjectMediaCodecMap
330
396
  };
331
- //# sourceMappingURL=chunk-KOB22VPG.js.map
397
+ //# sourceMappingURL=chunk-TJQ76IEA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helpers/mediaCodecMap.ts","../src/helpers/safePath.ts","../src/helpers/mediaMetadata.ts"],"sourcesContent":["import { existsSync, statSync } from \"node:fs\";\nimport { realpath } from \"./safePath.js\";\nimport { relative, resolve, sep } from \"node:path\";\nimport { rewriteAssetPath } from \"@hyperframes/parsers/asset-paths\";\nimport {\n cleanAssetUrl,\n isRemoteOrInlineUrl,\n isUnresolvedAssetPlaceholder,\n maskNonScannableRanges,\n resolveLocalAssetCandidates,\n} from \"@hyperframes/parsers/asset-resolution\";\nimport { pixelFormatHasAlpha, probeMediaMetadata, type FfprobeRunner } from \"./mediaMetadata.js\";\n\n/**\n * One reusable answer to \"what codec is this asset, and is it browser-hostile?\",\n * built on top of `mediaMetadata.ts`'s ffprobe-backed prober so studio-server\n * probes each asset once instead of running a second prober.\n */\n\nexport interface AssetCodecFacts {\n codecName: string;\n browserHostile: boolean;\n /** Coarse `canPlayType()` input; `null` when not applicable (safe codec) or\n * when no representative mime exists (ProRes: browsers never decode it, so\n * the runtime always proxies rather than probing `canPlayType`). */\n representativeMime: string | null;\n /** Source carries an alpha channel (ffprobe pix_fmt). Alpha sources use a\n * VP8/WebM proxy so their transparency is preserved across Chromium builds. */\n hasAlpha: boolean;\n}\n\n/** Server-root-relative URL pathname -> that asset's codec facts. */\nexport type MediaCodecMap = Record<string, AssetCodecFacts>;\n\nexport interface BrowserHostileCodec {\n /** Coarse `canPlayType()` input; `null` when no representative mime exists\n * (ProRes: browsers never decode it, so the runtime always proxies rather\n * than probing `canPlayType`). */\n representativeMime: string | null;\n /**\n * Whether the server may transcode before any browser asks. True only where\n * no cross-platform decode exists, so some client is sure to need the\n * substitute; false where the first `?hf-proxy=` request can do it lazily.\n */\n prewarm: boolean;\n}\n\n/**\n * Browser-hostile codec table v1. One exported constant so extending it is a\n * one-line change. `ffprobe` cannot emit exact RFC 6381 codec strings, so\n * these `representativeMime` values are deliberately coarse (a false\n * positive costs one proxy transcode, never correctness; a false negative is\n * rescued by the runtime's reactive zero-videoWidth swap).\n */\nexport const BROWSER_HOSTILE_CODECS: Record<string, BrowserHostileCodec> = {\n // HEVC decode is platform-bound, not absent: macOS Chrome answers\n // canPlayType with \"probably\" and keeps the source, while Chrome on\n // Windows/Linux and Firefox everywhere need the substitute.\n hevc: { representativeMime: 'video/mp4; codecs=\"hvc1.1.6.L120.B0\"', prewarm: true },\n prores: { representativeMime: null, prewarm: true },\n // Every mainstream engine decodes AV1 and VP9, so canPlayType keeps the\n // original and only the rare browser that cannot pays for a transcode.\n av1: { representativeMime: 'video/mp4; codecs=\"av01.0.08M.08\"', prewarm: false },\n vp9: { representativeMime: 'video/webm; codecs=\"vp09.00.10.08\"', prewarm: false },\n};\n\n/** `Object.hasOwn` rather than a bare index: a codec named `constructor` or\n * `toString` would otherwise resolve against `Object.prototype`. */\nfunction hostileCodecEntry(codecName: string): BrowserHostileCodec | undefined {\n return Object.hasOwn(BROWSER_HOSTILE_CODECS, codecName)\n ? BROWSER_HOSTILE_CODECS[codecName]\n : undefined;\n}\n\n/** The pre-warm gate: true only for codecs with no cross-platform browser\n * decode, so some client will ask. See `BrowserHostileCodec.prewarm`. */\nexport function shouldPrewarmProxy(facts: AssetCodecFacts): boolean {\n return hostileCodecEntry(facts.codecName)?.prewarm === true;\n}\n\n// Per process, and deliberately in one unit — a call to `resolveProxy` — so\n// the pair reads as a ratio. Summarised at exit on stderr, in the same\n// `[hyperframes:<area>] {json}` shape as `writeUrlDownloadTelemetry`.\nconst proxyDemand = { prewarmsRequested: 0, proxyRequests: 0 };\n\n/** Snapshot of this process's pre-warm demand counters. */\nexport function mediaProxyDemand(): { prewarmsRequested: number; proxyRequests: number } {\n return { ...proxyDemand };\n}\n\nfunction writeDemandLine(event: \"prewarm_requested\" | \"summary\"): void {\n try {\n process.stderr.write(\n `[hyperframes:media-proxy] ${JSON.stringify({ event, ...proxyDemand })}\\n`,\n );\n } catch {\n // Observability must never change proxy correctness.\n }\n}\n\n/** Mirrors `isGpuProbeDebugEnabled` in packages/engine/src/utils/gpuEncoder.ts. */\nfunction isMediaProxyDebugEnabled(): boolean {\n const value = process.env.HYPERFRAMES_DEBUG_MEDIA_PROXY;\n return value === \"1\" || value === \"true\";\n}\n\n// Registered on the first pre-warm rather than at import, so a process that\n// never pre-warms adds no handler and prints nothing. Sync-only, mirroring the\n// `process.on(\"exit\")` shutdown hooks in packages/cli/src/cli.ts:331 and\n// packages/cli/src/commands/preview.ts:1329.\nlet summaryHookInstalled = false;\n\n/**\n * One proxy asked for before any browser wanted it. \"Requested\", not\n * \"started\": a warm cache makes `resolveProxy` a no-op and this counter cannot\n * see that, so it is an upper bound on transcodes, not a measure of CPU. The\n * number it does answer exactly is the one that decides policy — a nonzero\n * count beside `proxyRequests: 0` means nothing ever redeemed the pre-warm.\n *\n * The per-asset line is debug-only: a composition with fifty hostile clips\n * would otherwise print fifty JSON lines into a clack-formatted terminal on\n * every re-render. The exit summary carries the same numbers unconditionally.\n */\nexport function recordProxyPrewarm(): void {\n proxyDemand.prewarmsRequested++;\n if (!summaryHookInstalled) {\n summaryHookInstalled = true;\n process.once(\"exit\", () => writeDemandLine(\"summary\"));\n }\n if (isMediaProxyDebugEnabled()) writeDemandLine(\"prewarm_requested\");\n}\n\n/** One proxy resolved for a browser that asked, counted on the path that calls\n * `resolveProxy` so it shares a unit with `prewarmsRequested`. A 304 does not\n * count; an unconditional Range refill still does, so read it as zero versus\n * nonzero. Never logged per event, only in the exit summary. */\nexport function recordProxyRequest(): void {\n proxyDemand.proxyRequests++;\n}\n\nexport type ProxyVariant = \"h264\" | \"vp8\";\nexport type ProxyVariantRequest = ProxyVariant | \"auto\";\n\nexport const PROXY_VARIANT_CONFIG: Record<\n ProxyVariant,\n { extension: \".mp4\" | \".webm\"; contentType: \"video/mp4\" | \"video/webm\" }\n> = {\n h264: { extension: \".mp4\", contentType: \"video/mp4\" },\n vp8: { extension: \".webm\", contentType: \"video/webm\" },\n};\n\nexport function isProxyVariant(value: string): value is ProxyVariant {\n return Object.hasOwn(PROXY_VARIANT_CONFIG, value);\n}\n\nexport function isProxyVariantRequest(value: string): value is ProxyVariantRequest {\n return value === \"auto\" || isProxyVariant(value);\n}\n\nexport function proxyVariantFor(facts: AssetCodecFacts): ProxyVariant {\n return facts.hasAlpha ? \"vp8\" : \"h264\";\n}\n\nexport function resolveProxyVariantRequest(\n request: ProxyVariantRequest,\n facts: AssetCodecFacts,\n): ProxyVariant | null {\n const expected = proxyVariantFor(facts);\n return request === \"auto\" || request === expected ? expected : null;\n}\n\nexport type MediaProxyIneligibilityReason = \"browser_safe_codec\" | \"unknown_codec\";\n\nexport type MediaProxyEligibility =\n | { eligible: true }\n | { eligible: false; reason: MediaProxyIneligibilityReason };\n\n/** Single policy gate shared by proactive scans and on-demand proxy routes. */\nexport function decideMediaProxyEligibility(facts: AssetCodecFacts | null): MediaProxyEligibility {\n if (!facts) return { eligible: false, reason: \"unknown_codec\" };\n if (!facts.browserHostile) return { eligible: false, reason: \"browser_safe_codec\" };\n return { eligible: true };\n}\n\nfunction codecFactsFor(codecName: string, hasAlpha: boolean): AssetCodecFacts {\n const hostile = hostileCodecEntry(codecName);\n return {\n codecName,\n browserHostile: hostile !== undefined,\n representativeMime: hostile?.representativeMime ?? null,\n hasAlpha,\n };\n}\n\n/** `undefined` when ffprobe itself failed: no answer about the file, so nothing to cache. */\nasync function probeCodecFacts(\n filePath: string,\n runner?: FfprobeRunner,\n): Promise<AssetCodecFacts | null | undefined> {\n const metadata = runner\n ? await probeMediaMetadata(filePath, runner)\n : await probeMediaMetadata(filePath);\n if (metadata.probeError) return undefined;\n if (metadata.kind !== \"video\") return null;\n const codecName = metadata.color.codecName;\n if (!codecName) return null;\n return codecFactsFor(codecName, pixelFormatHasAlpha(metadata.color.pixelFormat));\n}\n\ninterface CachedAssetProbe {\n /** The file a symlinked path pointed at, so retargeting the link is a miss. */\n target: string;\n mtimeMs: number;\n size: number;\n facts: AssetCodecFacts | null;\n}\n\n/** Per (path, mtime) probe cache. Construct one per project/server lifetime\n * and reuse it across scans; a fresh instance defeats the caching benefit. */\nexport type MediaCodecProbeCache = Map<string, CachedAssetProbe>;\n\nexport function createMediaCodecProbeCache(): MediaCodecProbeCache {\n return new Map();\n}\n\n// Shared by every route and scan in this process that does not pass its own cache.\nconst defaultProbeCache: MediaCodecProbeCache = new Map();\nconst MAX_PROBE_CACHE_ENTRIES = 512;\n\nfunction rememberProbeResult(\n cache: MediaCodecProbeCache,\n filePath: string,\n result: CachedAssetProbe,\n): void {\n if (!cache.has(filePath) && cache.size >= MAX_PROBE_CACHE_ENTRIES) {\n const oldest = cache.keys().next().value;\n if (oldest) cache.delete(oldest);\n }\n // Refresh insertion order so frequently used assets remain resident.\n cache.delete(filePath);\n cache.set(filePath, result);\n}\n\n/**\n * Codec facts for one video, cached per path until its target, mtime or size changes; a hit skips\n * `runner`. Never throws: no video stream or a failed probe gives `null`, and a failed probe is not cached.\n */\nexport async function probeAssetCodec(\n filePath: string,\n runner?: FfprobeRunner,\n cache: MediaCodecProbeCache = defaultProbeCache,\n): Promise<AssetCodecFacts | null> {\n let stat: ReturnType<typeof statSync>;\n let target: string;\n try {\n stat = statSync(filePath);\n target = realpath(filePath);\n } catch {\n return null;\n }\n const cached = cache.get(filePath);\n if (\n cached &&\n cached.target === target &&\n cached.mtimeMs === stat.mtimeMs &&\n cached.size === stat.size\n ) {\n rememberProbeResult(cache, filePath, cached);\n return cached.facts;\n }\n const facts = await probeCodecFacts(filePath, runner);\n if (facts === undefined) return null;\n rememberProbeResult(cache, filePath, { target, mtimeMs: stat.mtimeMs, size: stat.size, facts });\n return facts;\n}\n\n/** Structurally compatible with `packages/lint/src/hevcPreviewLint.ts`'s\n * (unexported) `HtmlSourceLike`. */\nexport interface HtmlSourceLike {\n html: string;\n compSrcPath?: string;\n}\n\n// --- <video src> collection: shared primitives live in\n// @hyperframes/parsers/asset-resolution; the <video>-specific regex and the\n// pinned key derivation stay here.\nconst VIDEO_SRC_RE = /<video\\b[^>]*\\bsrc\\s*=\\s*[\"']([^\"']+)[\"'][^>]*>/gi;\n\n/**\n * Resolve a `<video src>` reference to an existing local file.\n *\n * `rootRelativePathname` is the map key format PINNED by this plan's Key\n * Technical Decisions: project-root-relative URL pathname, percent-decoded,\n * query-string-stripped, forward-slash separated, leading-slash prefixed\n * (e.g. \"/assets/videos/clip.mp4\"). This must match what the runtime derives\n * via `new URL(el.currentSrc || el.src, document.baseURI).pathname`, because\n * server-side scanning resolves filesystem paths while the DOM sees served\n * URLs — a documented prior source of this exact class of bug.\n */\nfunction resolveExistingLocalAsset(\n projectDir: string,\n url: string,\n): { resolvedPath: string; rootRelativePathname: string } | null {\n const projectRoot = resolve(projectDir);\n const resolvedPath = resolveLocalAssetCandidates(projectRoot, url).find((candidate) =>\n existsSync(candidate),\n );\n if (!resolvedPath) return null;\n const rootRelative = relative(projectRoot, resolvedPath).split(sep).join(\"/\");\n return { resolvedPath, rootRelativePathname: `/${rootRelative}` };\n}\n\n/**\n * Collects local `<video src>` references, resolved to their absolute path\n * and deduped by that path, keyed by the pinned root-relative URL pathname.\n */\n// fallow-ignore-next-line complexity\nfunction collectLocalVideoAssets(\n projectDir: string,\n htmlSources: HtmlSourceLike[],\n): Map<string, string> {\n const candidates = new Map<string, string>();\n\n for (const { html, compSrcPath } of htmlSources) {\n const scannable = maskNonScannableRanges(html);\n const re = new RegExp(VIDEO_SRC_RE.source, VIDEO_SRC_RE.flags);\n let match: RegExpExecArray | null;\n while ((match = re.exec(scannable)) !== null) {\n const rawSrc = match[1] ?? \"\";\n // Placeholder check runs on the RAW value: cleanAssetUrl() splits on ?/# and would chop inside a ${...} token.\n if (isUnresolvedAssetPlaceholder(rawSrc)) continue;\n const src = cleanAssetUrl(rawSrc);\n if (!src || isRemoteOrInlineUrl(src)) continue;\n const rootRelativeSrc = compSrcPath ? rewriteAssetPath(compSrcPath, src) : src;\n const resolved = resolveExistingLocalAsset(projectDir, rootRelativeSrc);\n if (!resolved) continue;\n candidates.set(resolved.resolvedPath, resolved.rootRelativePathname);\n }\n }\n\n return candidates;\n}\n\n// Bounds concurrent ffprobe child processes for projects referencing many\n// videos, mirroring `PROBE_CONCURRENCY` in `hevcPreviewLint.ts`.\nconst PROBE_CONCURRENCY = 8;\n\nexport interface ScanProjectMediaCodecMapOptions {\n /** Persisted across calls by the caller for the mtime-cache benefit;\n * defaults to a shared module-level cache when omitted. */\n cache?: MediaCodecProbeCache;\n runner?: FfprobeRunner;\n}\n\n/**\n * Scans a project's composition HTML for local `<video src>` references and\n * returns the injection map: root-relative URL pathname -> codec facts.\n * Best-effort throughout — a video whose codec can't be determined (missing\n * ffprobe, probe error, no video stream) is simply omitted, never thrown.\n */\nexport async function scanProjectMediaCodecMap(\n projectDir: string,\n htmlSources: HtmlSourceLike[],\n options: ScanProjectMediaCodecMapOptions = {},\n): Promise<MediaCodecMap> {\n const candidates = collectLocalVideoAssets(projectDir, htmlSources);\n if (candidates.size === 0) return {};\n\n const cache = options.cache ?? defaultProbeCache;\n const entries = [...candidates.entries()]; // [resolvedPath, rootRelativePathname]\n const facts = new Array<AssetCodecFacts | null>(entries.length).fill(null);\n let nextIndex = 0;\n const workerCount = Math.min(PROBE_CONCURRENCY, entries.length);\n await Promise.all(\n Array.from({ length: workerCount }, async () => {\n while (nextIndex < entries.length) {\n const index = nextIndex++;\n const entry = entries[index];\n if (!entry) break;\n facts[index] = await probeAssetCodec(entry[0], options.runner, cache);\n }\n }),\n );\n\n const map: MediaCodecMap = {};\n entries.forEach(([, pathname], index) => {\n const entryFacts = facts[index];\n if (entryFacts?.browserHostile) map[pathname] = entryFacts;\n });\n return map;\n}\n","import { basename, dirname, isAbsolute, join, relative, sep } from \"node:path\";\nimport { readdirSync, type Dirent } from \"node:fs\";\nimport { realpath, resolveWithinProject } from \"@hyperframes/core\";\n\n// `isSafePath` lives at the package root so non-studio-api layers (compiler,\n// CLI, engine) can share it without a backwards dependency on studio-api.\n// Re-exported here for back-compat with existing `../helpers/safePath.js` imports.\nexport { isSafePath, realpath, resolveWithinProject } from \"@hyperframes/core\";\n\n/** The real path; for a path not there (yet, or any more), the nearest existing folder's real path plus the rest. */\nexport function realFilePath(filePath: string): string {\n try {\n return realpath(filePath);\n } catch {\n const dir = dirname(filePath);\n return dir === filePath ? filePath : join(realFilePath(dir), basename(filePath));\n }\n}\n\n/**\n * `resolveWithinProject` with every link inside the project resolved now, so a write to the result lands on the\n * file checked here even if a link on the way is retargeted before the write.\n */\nexport function pinWithinProject(base: string, relativePath: string): string | null {\n const checked = resolveWithinProject(base, relativePath);\n if (!checked) return null;\n const inside = relative(realFilePath(base), realFilePath(checked));\n const escapes = inside === \"..\" || inside.startsWith(`..${sep}`) || isAbsolute(inside);\n return escapes ? null : join(base, inside);\n}\n\nconst IGNORE_DIRS = new Set([\n \".thumbnails\",\n \".transcode-cache\",\n \".waveform-cache\",\n \"node_modules\",\n \".git\",\n]);\n\nfunction shouldIgnoreDir(rel: string): boolean {\n return rel === \".hyperframes/backup\";\n}\n\n/**\n * True when any directory segment of a relative path is a dot-directory or\n * node_modules. Projects that vendor tooling assets under dot-directories\n * (.hyperframes/, .cache/, …) ship example/preset HTML that must not surface\n * as project compositions or studio lint targets (#1384). The file tree is\n * deliberately not filtered — this only gates discovery.\n */\nexport function isInHiddenOrVendorDir(relPath: string): boolean {\n const segments = relPath.split(\"/\");\n return segments.slice(0, -1).some((seg) => seg.startsWith(\".\") || seg === \"node_modules\");\n}\n\nconst UNREADABLE_DIR_CODES = new Set([\"EACCES\", \"EPERM\", \"ENOENT\", \"ENOTDIR\"]);\n\n/** Recursively walk a directory and return relative file paths. A subfolder that is unreadable\n * or removed mid-walk is skipped; any other failure, or an unreadable `dir` itself, throws. */\nexport function walkDir(dir: string, prefix = \"\"): string[] {\n let entries: Dirent[];\n try {\n entries = readdirSync(dir, { withFileTypes: true });\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (!prefix || !code || !UNREADABLE_DIR_CODES.has(code)) throw err;\n return [];\n }\n const files: string[] = [];\n for (const entry of entries) {\n const rel = prefix ? `${prefix}/${entry.name}` : entry.name;\n if (IGNORE_DIRS.has(entry.name) || shouldIgnoreDir(rel)) continue;\n if (entry.isDirectory()) {\n files.push(...walkDir(join(dir, entry.name), rel));\n } else {\n files.push(rel);\n }\n }\n return files;\n}\n","import { execFile } from \"node:child_process\";\nimport { extname } from \"node:path\";\nimport { findFfBinary } from \"@hyperframes/parsers/ff-binaries\";\n\nexport interface FfprobeRunResult {\n status: number | null;\n stdout: string | Buffer;\n stderr: string | Buffer;\n /** Spawn-level failure (covers both `NodeJS.ErrnoException` and\n * `ExecFileException`); only `code === \"ENOENT\"` is ever inspected. */\n error?: { code?: string | number | null | undefined };\n}\n\n/** Injectable ffprobe runner. May be synchronous (tests) or async (the\n * default `execFile`-based runner below), so cold scans can run many probes\n * concurrently off the event loop. */\nexport type FfprobeRunner = (\n command: string,\n args: string[],\n options?: { timeout?: number; maxBuffer?: number },\n) => FfprobeRunResult | Promise<FfprobeRunResult>;\n\n/** Default runner: genuinely async (`execFile`), unlike the previous\n * `spawnSync`-based one — a pool of concurrent probes actually parallelizes\n * (mirrors `execFileAsync` in packages/lint/src/hevcPreviewLint.ts). */\nconst execFileRunner: FfprobeRunner = (command, args, options) =>\n new Promise<FfprobeRunResult>((resolvePromise) => {\n execFile(\n command,\n args,\n { timeout: options?.timeout, maxBuffer: options?.maxBuffer, windowsHide: true },\n (error, stdout, stderr) => {\n if (error && error.code === \"ENOENT\") {\n resolvePromise({ status: null, stdout: \"\", stderr: \"\", error });\n return;\n }\n if (error) {\n // Nonzero exit / timeout / kill: report a nonzero status; callers\n // only distinguish \"ok\" (0) from \"failed\" from \"ENOENT\".\n const status = typeof error.code === \"number\" ? error.code : 1;\n resolvePromise({ status, stdout: stdout ?? \"\", stderr: stderr ?? \"\" });\n return;\n }\n resolvePromise({ status: 0, stdout: stdout ?? \"\", stderr: stderr ?? \"\" });\n },\n );\n });\n\nexport type MediaDynamicRange = \"hdr\" | \"sdr\" | \"unknown\";\nexport type MediaHdrTransfer = \"pq\" | \"hlg\" | \"unknown\";\n\nexport interface MediaColorMetadata {\n dynamicRange: MediaDynamicRange;\n hdrTransfer: MediaHdrTransfer | null;\n label: string;\n isHdr: boolean;\n codecName?: string;\n profile?: string;\n pixelFormat?: string;\n colorSpace?: string;\n colorTransfer?: string;\n colorPrimaries?: string;\n bitsPerRawSample?: string;\n}\n\nexport interface MediaMetadata {\n kind: \"video\" | \"image\" | \"audio\" | \"unknown\";\n color: MediaColorMetadata;\n /** Video audio stream, when probed. Absent means the drop path stays muted. */\n hasAudio?: boolean;\n probeError?: string;\n}\n\ninterface FfprobeStream {\n codec_type?: string;\n codec_name?: string;\n profile?: string;\n pix_fmt?: string;\n color_space?: string;\n color_transfer?: string;\n color_primaries?: string;\n bits_per_raw_sample?: string;\n disposition?: { attached_pic?: number };\n}\n\nconst VIDEO_EXT = new Set([\n \".mp4\",\n \".mov\",\n \".webm\",\n \".mkv\",\n \".avi\",\n \".m4v\",\n \".mxf\",\n \".mts\",\n \".m2ts\",\n \".ts\",\n]);\nconst IMAGE_EXT = new Set([\".jpg\", \".jpeg\", \".png\", \".webp\", \".avif\"]);\nconst AUDIO_EXT = new Set([\".mp3\", \".wav\", \".ogg\", \".m4a\", \".aac\"]);\n\nfunction lower(value: string | undefined): string {\n return value?.toLowerCase() ?? \"\";\n}\n\nfunction inferKindFromPath(path: string): MediaMetadata[\"kind\"] {\n const ext = extname(path).toLowerCase();\n if (VIDEO_EXT.has(ext)) return \"video\";\n if (IMAGE_EXT.has(ext)) return \"image\";\n if (AUDIO_EXT.has(ext)) return \"audio\";\n return \"unknown\";\n}\n\nfunction colorLabel(input: {\n isHdr: boolean;\n hdrTransfer: MediaHdrTransfer | null;\n colorPrimaries: string;\n colorSpace: string;\n colorTransfer: string;\n}): string {\n if (input.isHdr) {\n if (input.hdrTransfer === \"pq\") return \"HDR PQ\";\n if (input.hdrTransfer === \"hlg\") return \"HDR HLG\";\n return \"HDR\";\n }\n if (\n input.colorPrimaries.includes(\"bt709\") ||\n input.colorSpace.includes(\"bt709\") ||\n input.colorTransfer.includes(\"bt709\")\n ) {\n return \"SDR Rec.709\";\n }\n return \"SDR/unknown\";\n}\n\n// Conservative alpha-bearing pix_fmt list: yuva* (yuva420p, yuva444p10le...),\n// rgba/argb/bgra/abgr (packed RGB+alpha), gbrap* (planar GBR+alpha, ProRes\n// 4444 decodes to these), ya* (gray+alpha). Prefix match keeps bit-depth /\n// endianness suffixes covered.\nconst ALPHA_PIX_FMT_RE = /^(?:yuva|rgba|argb|bgra|abgr|gbrap|ya)/;\n\n/** True when an ffprobe `pix_fmt` carries an alpha component. */\nexport function pixelFormatHasAlpha(pixFmt: string | undefined): boolean {\n return pixFmt !== undefined && ALPHA_PIX_FMT_RE.test(pixFmt.toLowerCase());\n}\n\nexport function classifyMediaColor(stream: FfprobeStream | null | undefined): MediaColorMetadata {\n const colorPrimaries = lower(stream?.color_primaries);\n const colorSpace = lower(stream?.color_space);\n const colorTransfer = lower(stream?.color_transfer);\n const isHdr =\n colorPrimaries.includes(\"bt2020\") ||\n colorSpace.includes(\"bt2020\") ||\n colorTransfer === \"smpte2084\" ||\n colorTransfer === \"arib-std-b67\";\n const hdrTransfer: MediaHdrTransfer | null = isHdr\n ? colorTransfer === \"smpte2084\"\n ? \"pq\"\n : colorTransfer === \"arib-std-b67\"\n ? \"hlg\"\n : \"unknown\"\n : null;\n\n return {\n dynamicRange: stream ? (isHdr ? \"hdr\" : \"sdr\") : \"unknown\",\n hdrTransfer,\n label: stream\n ? colorLabel({ isHdr, hdrTransfer, colorPrimaries, colorSpace, colorTransfer })\n : \"Unknown\",\n isHdr,\n codecName: stream?.codec_name,\n profile: stream?.profile,\n pixelFormat: stream?.pix_fmt,\n colorSpace: stream?.color_space,\n colorTransfer: stream?.color_transfer,\n colorPrimaries: stream?.color_primaries,\n bitsPerRawSample: stream?.bits_per_raw_sample,\n };\n}\n\nexport async function probeMediaMetadata(\n filePath: string,\n runner: FfprobeRunner = execFileRunner,\n): Promise<MediaMetadata> {\n const kind = inferKindFromPath(filePath);\n if (kind === \"audio\" || kind === \"unknown\") {\n return { kind, color: classifyMediaColor(null) };\n }\n\n // The default runner degrades a missing ffprobe to \"unavailable\" without\n // spawning; injected runners own execution and receive the normal command.\n const ffprobePath =\n findFfBinary(\"ffprobe\", { configuredMustExist: true }) ??\n (runner === execFileRunner ? undefined : \"ffprobe\");\n if (!ffprobePath) {\n return { kind, color: classifyMediaColor(null), probeError: \"ffprobe unavailable\" };\n }\n\n const result = await runner(\n ffprobePath,\n [\n \"-v\",\n \"error\",\n \"-show_entries\",\n \"stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample:stream_disposition=attached_pic\",\n \"-of\",\n \"json\",\n \"--\",\n filePath,\n ],\n { timeout: 15_000, maxBuffer: 1024 * 1024 },\n );\n\n if (result.error?.code === \"ENOENT\") {\n return { kind, color: classifyMediaColor(null), probeError: \"ffprobe unavailable\" };\n }\n if (result.status !== 0) {\n return { kind, color: classifyMediaColor(null), probeError: \"ffprobe failed\" };\n }\n\n try {\n const parsed = JSON.parse(String(result.stdout || \"{}\")) as { streams?: FfprobeStream[] };\n const stream = parsed.streams?.find((item) => {\n if (kind === \"image\") return item.codec_type === \"video\";\n return item.codec_type === kind && item.disposition?.attached_pic !== 1;\n });\n const metadata: MediaMetadata = { kind, color: classifyMediaColor(stream) };\n if (kind === \"video\") {\n metadata.hasAudio = (parsed.streams ?? []).some((item) => item.codec_type === \"audio\");\n }\n return metadata;\n } catch {\n return { kind, color: classifyMediaColor(null), probeError: \"ffprobe returned invalid json\" };\n }\n}\n"],"mappings":"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","names":["realpath","resolveWithinProject","relative","sep","realpath","relative","sep"]}
@@ -13,7 +13,7 @@ import {
13
13
  resolveProxyVariantRequest,
14
14
  scanProjectMediaCodecMap,
15
15
  shouldPrewarmProxy
16
- } from "../chunk-KOB22VPG.js";
16
+ } from "../chunk-TJQ76IEA.js";
17
17
  export {
18
18
  BROWSER_HOSTILE_CODECS,
19
19
  PROXY_VARIANT_CONFIG,
@@ -1,4 +1,4 @@
1
- export { k as PreviewApiAdapter, a as PreviewDocumentStore, s as injectMediaCodecMap, t as injectMediaCodecMapIntoHtml, u as isAutoProxyEnabled, v as proxyEtagSalt, w as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-B7ckRI-l.js';
1
+ export { k as PreviewApiAdapter, a as PreviewDocumentStore, s as injectMediaCodecMap, t as injectMediaCodecMapIntoHtml, u as isAutoProxyEnabled, v as proxyEtagSalt, w as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-BOp3Wa9v.js';
2
2
  import './mediaCodecMap.js';
3
3
  import '@hyperframes/core';
4
4
  import '@hyperframes/parsers';
@@ -4,9 +4,9 @@ import {
4
4
  isAutoProxyEnabled,
5
5
  proxyEtagSalt,
6
6
  resolvePreviewMediaCodecProbeCache
7
- } from "../chunk-YGANSFUU.js";
8
- import "../chunk-56JREE33.js";
9
- import "../chunk-KOB22VPG.js";
7
+ } from "../chunk-FTDTO3QP.js";
8
+ import "../chunk-G6IIFKXY.js";
9
+ import "../chunk-TJQ76IEA.js";
10
10
  export {
11
11
  injectMediaCodecMap,
12
12
  injectMediaCodecMapIntoHtml,
@@ -11,8 +11,8 @@ import {
11
11
  getProxyCachePath,
12
12
  resolveProxy,
13
13
  waitForProxy
14
- } from "../chunk-56JREE33.js";
15
- import "../chunk-KOB22VPG.js";
14
+ } from "../chunk-G6IIFKXY.js";
15
+ import "../chunk-TJQ76IEA.js";
16
16
  export {
17
17
  DEFAULT_PROXY_WAIT_TIMEOUT_MS,
18
18
  FfmpegMissingFilterError,
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Hono } from 'hono';
2
- import { S as StudioApiAdapter, P as ProjectHistory, a as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-B7ckRI-l.js';
3
- export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, b as HISTORY_START, H as HistoryClosedError, c as HistoryEntry, d as HistoryEntrySide, e as HistoryFileChange, f as HistoryListItem, g as HistoryResult, h as HistoryWho, i as HistoryWindow, L as LintResult, j as MAX_WINDOW_IDLE_MS, k as PreviewApiAdapter, l as ProjectHistoryOptions, R as RenderJobState, m as ResolvedProject, n as StudioSelectionResponse, o as StudioSelectionSnapshot, p as StudioSelectionTextField, U as UNDO_MODES, q as UndoMode, r as openProjectHistory } from './mediaProxyPreview-B7ckRI-l.js';
2
+ import { S as StudioApiAdapter, P as ProjectHistory, a as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-BOp3Wa9v.js';
3
+ export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, b as HISTORY_START, H as HistoryClosedError, c as HistoryEntry, d as HistoryEntrySide, e as HistoryFileChange, f as HistoryListItem, g as HistoryResult, h as HistoryWho, i as HistoryWindow, L as LintResult, j as MAX_WINDOW_IDLE_MS, k as PreviewApiAdapter, l as ProjectHistoryOptions, R as RenderJobState, m as ResolvedProject, n as StudioSelectionResponse, o as StudioSelectionSnapshot, p as StudioSelectionTextField, U as UNDO_MODES, q as UndoMode, r as openProjectHistory } from './mediaProxyPreview-BOp3Wa9v.js';
4
4
  import { SourceMutationTarget } from './helpers/sourceMutation.js';
5
5
  export { PatchOperation, dedupeClonedCompositionId, findTargetElement, parseSourceDocument, patchElementInHtml, removeElementFromHtml, splitElementInHtml } from './helpers/sourceMutation.js';
6
6
  export { ScreenshotClip, getElementScreenshotClip } from './helpers/screenshotClip.js';
@@ -59,6 +59,10 @@ declare class HistoryBusyError extends Error {
59
59
  constructor(pid: number);
60
60
  }
61
61
 
62
+ declare class HistoryIdError extends Error {
63
+ constructor(message: string);
64
+ }
65
+
62
66
  type Opened = Promise<ProjectHistory | null>;
63
67
  /** A host's histories, one per project folder, opened on first use; a new project at the path gets its own. */
64
68
  declare function historyCache(open: (projectDir: string) => Opened): {
@@ -218,4 +222,4 @@ type BackgroundRemovalRender = (options: {
218
222
  }>;
219
223
  declare function createBackgroundRemovalJob(opts: BackgroundRemovalJobOptions, render: BackgroundRemovalRender): MediaProcessingJobState;
220
224
 
221
- export { type AppliedFileMutation, type BackgroundRemovalRender, DELETED_VERSION, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, HistoryBusyError, MIME_TYPES, MediaProcessingJobState, PREVIEW_BUNDLE_OPTIONS, ProjectHistory, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsPreview, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createPreviewDocumentStore, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, historyCache, identifyFileWrite, requestSubPath, settledFileTag, stampProjectHfIds, thumbnailDeviceScaleFactor, walkDir };
225
+ export { type AppliedFileMutation, type BackgroundRemovalRender, DELETED_VERSION, type DuplicateElementResult, type FileMutationInput, type FileWriteReceipt, HistoryBusyError, HistoryIdError, MIME_TYPES, MediaProcessingJobState, PREVIEW_BUNDLE_OPTIONS, ProjectHistory, SourceMutationTarget, StudioApiAdapter, type ThumbnailOutputDimensions, affectsPreview, affectsProjectSignature, applyFileMutations, buildSubCompositionHtml, compositionsAffectedBy, consumeFileWriteReceipt, createBackgroundRemovalJob, createPreviewDocumentStore, createProjectSignature, createStudioApi, duplicateElementInHtml, fileContentVersion, getMimeType, historyCache, identifyFileWrite, requestSubPath, settledFileTag, stampProjectHfIds, thumbnailDeviceScaleFactor, walkDir };