@hyperframes/engine 0.6.119 → 0.6.120

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.
Files changed (73) hide show
  1. package/package.json +24 -7
  2. package/scripts/generate-lut-reference.py +0 -168
  3. package/scripts/test-fitTextFontSize-browser.ts +0 -135
  4. package/src/cdp-headless-experimental.d.ts +0 -54
  5. package/src/config.test.ts +0 -213
  6. package/src/config.ts +0 -417
  7. package/src/index.ts +0 -273
  8. package/src/services/audioMixer.test.ts +0 -326
  9. package/src/services/audioMixer.ts +0 -604
  10. package/src/services/audioMixer.types.ts +0 -35
  11. package/src/services/audioVolumeEnvelope.test.ts +0 -176
  12. package/src/services/audioVolumeEnvelope.ts +0 -138
  13. package/src/services/browserManager.test.ts +0 -330
  14. package/src/services/browserManager.ts +0 -670
  15. package/src/services/chunkEncoder.test.ts +0 -1415
  16. package/src/services/chunkEncoder.ts +0 -831
  17. package/src/services/chunkEncoder.types.ts +0 -60
  18. package/src/services/extractionCache.test.ts +0 -199
  19. package/src/services/extractionCache.ts +0 -216
  20. package/src/services/fileServer.ts +0 -110
  21. package/src/services/frameCapture-discardWarmup.test.ts +0 -183
  22. package/src/services/frameCapture-namePolyfill.test.ts +0 -78
  23. package/src/services/frameCapture-pollImagesReady.test.ts +0 -153
  24. package/src/services/frameCapture-staticDedupIndex.test.ts +0 -76
  25. package/src/services/frameCapture-warmupTicks.test.ts +0 -174
  26. package/src/services/frameCapture.test.ts +0 -192
  27. package/src/services/frameCapture.ts +0 -1934
  28. package/src/services/hdrCapture.test.ts +0 -159
  29. package/src/services/hdrCapture.ts +0 -315
  30. package/src/services/parallelCoordinator.test.ts +0 -139
  31. package/src/services/parallelCoordinator.ts +0 -437
  32. package/src/services/screenshotService.test.ts +0 -510
  33. package/src/services/screenshotService.ts +0 -615
  34. package/src/services/streamingEncoder.test.ts +0 -832
  35. package/src/services/streamingEncoder.ts +0 -594
  36. package/src/services/systemMemory.test.ts +0 -324
  37. package/src/services/systemMemory.ts +0 -180
  38. package/src/services/videoFrameExtractor.test.ts +0 -1062
  39. package/src/services/videoFrameExtractor.ts +0 -1139
  40. package/src/services/videoFrameInjector.test.ts +0 -300
  41. package/src/services/videoFrameInjector.ts +0 -687
  42. package/src/services/vp9Options.ts +0 -13
  43. package/src/types.ts +0 -191
  44. package/src/utils/alphaBlit.test.ts +0 -1349
  45. package/src/utils/alphaBlit.ts +0 -1015
  46. package/src/utils/assertSwiftShader.test.ts +0 -130
  47. package/src/utils/assertSwiftShader.ts +0 -126
  48. package/src/utils/ffmpegBinaries.test.ts +0 -43
  49. package/src/utils/ffmpegBinaries.ts +0 -63
  50. package/src/utils/ffprobe.test.ts +0 -342
  51. package/src/utils/ffprobe.ts +0 -457
  52. package/src/utils/gpuEncoder.test.ts +0 -140
  53. package/src/utils/gpuEncoder.ts +0 -268
  54. package/src/utils/hdr.test.ts +0 -191
  55. package/src/utils/hdr.ts +0 -137
  56. package/src/utils/hdrCompositing.test.ts +0 -130
  57. package/src/utils/htmlTemplate.test.ts +0 -42
  58. package/src/utils/htmlTemplate.ts +0 -42
  59. package/src/utils/layerCompositor.test.ts +0 -150
  60. package/src/utils/layerCompositor.ts +0 -58
  61. package/src/utils/parityContract.ts +0 -1
  62. package/src/utils/processTracker.test.ts +0 -74
  63. package/src/utils/processTracker.ts +0 -41
  64. package/src/utils/readWebGlVendorInfoFromCanvas.ts +0 -52
  65. package/src/utils/runFfmpeg.test.ts +0 -102
  66. package/src/utils/runFfmpeg.ts +0 -136
  67. package/src/utils/shaderTransitions.test.ts +0 -738
  68. package/src/utils/shaderTransitions.ts +0 -1130
  69. package/src/utils/uint16-alignment-audit.test.ts +0 -125
  70. package/src/utils/urlDownloader.test.ts +0 -65
  71. package/src/utils/urlDownloader.ts +0 -143
  72. package/tsconfig.json +0 -19
  73. package/vitest.config.ts +0 -7
@@ -1,60 +0,0 @@
1
- import type { HdrTransfer } from "../utils/hdr.js";
2
- import type { Fps } from "@hyperframes/core";
3
-
4
- export interface EncoderOptions {
5
- /** Frame rate as an exact rational; see `Fps` in @hyperframes/core. */
6
- fps: Fps;
7
- width: number;
8
- height: number;
9
- codec?: "h264" | "h265" | "vp9" | "prores";
10
- preset?: string;
11
- quality?: number;
12
- bitrate?: string;
13
- pixelFormat?: string;
14
- /** libvpx-vp9 -cpu-used value. Defaults to the engine VP9 setting. */
15
- vp9CpuUsed?: number;
16
- useGpu?: boolean;
17
- hdr?: { transfer: HdrTransfer };
18
- /**
19
- * When `true`, force closed-GOP encoding with a keyframe at every
20
- * `gopSize` boundary so the resulting chunk file can be losslessly
21
- * concatenated (`ffmpeg -f concat -c copy`) with sibling chunks.
22
- *
23
- * Default `false`: GOP placement is left to libx264/libx265 defaults
24
- * (open-GOP, scenecut-driven keyframes), preserving the in-process
25
- * renderer's byte-identical output.
26
- *
27
- * Honored by the SW libx264 / libx265 / libvpx-vp9 paths. GPU encoders
28
- * and ProRes ignore the flag — GPU concat-copy is a separate story and
29
- * ProRes is intra-only (every frame is already a keyframe, so no
30
- * closed-GOP forcing is needed).
31
- *
32
- * For libvpx-vp9, closed-GOP also forces `-auto-alt-ref 0` so the
33
- * boundary frame between chunks remains independently decodable —
34
- * libvpx-vp9's default alt-ref frames can land anywhere in the GOP
35
- * for compression and break concat-copy seams.
36
- */
37
- lockGopForChunkConcat?: boolean;
38
- /**
39
- * Required when `lockGopForChunkConcat` is `true`. Number of frames per
40
- * GOP — set to `chunkSize` so every chunk starts on an IDR keyframe and
41
- * concat-copy boundaries land on independently-decodable frames.
42
- */
43
- gopSize?: number;
44
- }
45
-
46
- export interface EncodeResult {
47
- success: boolean;
48
- outputPath: string;
49
- durationMs: number;
50
- framesEncoded: number;
51
- fileSize: number;
52
- error?: string;
53
- }
54
-
55
- export interface MuxResult {
56
- success: boolean;
57
- outputPath: string;
58
- durationMs: number;
59
- error?: string;
60
- }
@@ -1,199 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
-
6
- import {
7
- COMPLETE_SENTINEL,
8
- FRAME_FILENAME_PREFIX,
9
- SCHEMA_PREFIX,
10
- cacheEntryDirName,
11
- computeCacheKey,
12
- ensureCacheEntryDir,
13
- lookupCacheEntry,
14
- markCacheEntryComplete,
15
- readKeyStat,
16
- type CacheKeyInput,
17
- } from "./extractionCache.js";
18
-
19
- const keyFor = (videoPath: string, overrides: Partial<CacheKeyInput> = {}): CacheKeyInput => {
20
- const stat = readKeyStat(videoPath);
21
- if (!stat) throw new Error(`keyFor fixture missing on disk: ${videoPath}`);
22
- return {
23
- videoPath,
24
- mtimeMs: stat.mtimeMs,
25
- size: stat.size,
26
- mediaStart: 0,
27
- duration: 3,
28
- fps: 30,
29
- format: "jpg",
30
- ...overrides,
31
- };
32
- };
33
-
34
- describe("extractionCache constants", () => {
35
- it("exposes the v2 schema prefix", () => {
36
- expect(SCHEMA_PREFIX).toBe("hfcache-v2-");
37
- });
38
-
39
- it("exposes the frame filename prefix shared with the extractor", () => {
40
- expect(FRAME_FILENAME_PREFIX).toBe("frame_");
41
- });
42
-
43
- it("uses a dotfile sentinel so ls-without-A hides it", () => {
44
- expect(COMPLETE_SENTINEL.startsWith(".")).toBe(true);
45
- });
46
- });
47
-
48
- describe("computeCacheKey", () => {
49
- let tmpRoot: string;
50
- let sourceFile: string;
51
-
52
- beforeEach(() => {
53
- tmpRoot = mkdtempSync(join(tmpdir(), "hf-extract-cache-test-"));
54
- sourceFile = join(tmpRoot, "clip.mp4");
55
- writeFileSync(sourceFile, "fake-video-bytes", "utf-8");
56
- });
57
-
58
- afterEach(() => {
59
- if (existsSync(tmpRoot)) rmSync(tmpRoot, { recursive: true, force: true });
60
- });
61
-
62
- const base = (videoPath: string): CacheKeyInput => keyFor(videoPath);
63
-
64
- it("returns the same key for identical inputs", () => {
65
- const a = computeCacheKey(base(sourceFile));
66
- const b = computeCacheKey(base(sourceFile));
67
- expect(a).toBe(b);
68
- });
69
-
70
- it("produces a 64-char hex SHA-256 digest", () => {
71
- const key = computeCacheKey(base(sourceFile));
72
- expect(key).toMatch(/^[0-9a-f]{64}$/);
73
- });
74
-
75
- it("changes when path changes (moved files re-extract)", () => {
76
- const other = join(tmpRoot, "other.mp4");
77
- writeFileSync(other, "fake-video-bytes", "utf-8");
78
- const a = computeCacheKey(base(sourceFile));
79
- const b = computeCacheKey(base(other));
80
- expect(a).not.toBe(b);
81
- });
82
-
83
- it("changes when mediaStart changes", () => {
84
- const a = computeCacheKey(base(sourceFile));
85
- const b = computeCacheKey({ ...base(sourceFile), mediaStart: 1 });
86
- expect(a).not.toBe(b);
87
- });
88
-
89
- it("changes when duration changes", () => {
90
- const a = computeCacheKey(base(sourceFile));
91
- const b = computeCacheKey({ ...base(sourceFile), duration: 5 });
92
- expect(a).not.toBe(b);
93
- });
94
-
95
- it("changes when fps changes (different frame count invalidates key)", () => {
96
- const a = computeCacheKey(base(sourceFile));
97
- const b = computeCacheKey({ ...base(sourceFile), fps: 60 });
98
- expect(a).not.toBe(b);
99
- });
100
-
101
- it("changes when format changes", () => {
102
- const a = computeCacheKey(base(sourceFile));
103
- const b = computeCacheKey({ ...base(sourceFile), format: "png" });
104
- expect(a).not.toBe(b);
105
- });
106
-
107
- it("normalizes non-finite duration so Infinity doesn't produce unstable keys", () => {
108
- const a = computeCacheKey({ ...base(sourceFile), duration: Infinity });
109
- const b = computeCacheKey({ ...base(sourceFile), duration: Infinity });
110
- expect(a).toBe(b);
111
- });
112
-
113
- it("changes when file content changes (mtime+size bump)", () => {
114
- const before = computeCacheKey(base(sourceFile));
115
- // Force an mtime change by waiting 5ms then overwriting with different bytes.
116
- // 5ms is well above the Linux mtime resolution (typically nanoseconds) and
117
- // below any Windows cache coherency window. Using a longer sleep pads against
118
- // coarse filesystem mtime granularity without slowing the suite.
119
- const start = Date.now();
120
- while (Date.now() - start < 5) {
121
- /* spin */
122
- }
123
- writeFileSync(sourceFile, "different-bytes-longer-than-before", "utf-8");
124
- const after = computeCacheKey(base(sourceFile));
125
- expect(after).not.toBe(before);
126
- });
127
-
128
- it("readKeyStat returns null for a missing source (callers skip the cache)", () => {
129
- // Previously readKeyStat returned a `{mtimeMs: 0, size: 0}` sentinel for
130
- // missing files; two unrelated missing paths then shared the same cache
131
- // key tuple and polluted the cache. The contract now returns null so
132
- // callers can explicitly skip the cache path and let the extractor
133
- // surface the real file-not-found error.
134
- const missing = join(tmpRoot, "does-not-exist.mp4");
135
- expect(readKeyStat(missing)).toBeNull();
136
- });
137
- });
138
-
139
- describe("cacheEntryDirName", () => {
140
- it("prefixes with the schema and truncates to 16 hex chars", () => {
141
- const full = "a".repeat(64);
142
- expect(cacheEntryDirName(full)).toBe(`${SCHEMA_PREFIX}${"a".repeat(16)}`);
143
- });
144
- });
145
-
146
- describe("lookupCacheEntry / markCacheEntryComplete", () => {
147
- let tmpRoot: string;
148
- let sourceFile: string;
149
-
150
- beforeEach(() => {
151
- tmpRoot = mkdtempSync(join(tmpdir(), "hf-extract-cache-test-"));
152
- sourceFile = join(tmpRoot, "clip.mp4");
153
- writeFileSync(sourceFile, "fake-video-bytes", "utf-8");
154
- });
155
-
156
- afterEach(() => {
157
- if (existsSync(tmpRoot)) rmSync(tmpRoot, { recursive: true, force: true });
158
- });
159
-
160
- const base = (videoPath: string): CacheKeyInput => keyFor(videoPath);
161
-
162
- it("misses on an empty cache root", () => {
163
- const lookup = lookupCacheEntry(tmpRoot, base(sourceFile));
164
- expect(lookup.hit).toBe(false);
165
- expect(lookup.entry.dir.startsWith(tmpRoot)).toBe(true);
166
- });
167
-
168
- it("hits after ensureCacheEntryDir + markCacheEntryComplete", () => {
169
- const first = lookupCacheEntry(tmpRoot, base(sourceFile));
170
- ensureCacheEntryDir(first.entry);
171
- markCacheEntryComplete(first.entry);
172
-
173
- const second = lookupCacheEntry(tmpRoot, base(sourceFile));
174
- expect(second.hit).toBe(true);
175
- expect(second.entry.dir).toBe(first.entry.dir);
176
- });
177
-
178
- it("treats an in-progress dir without the sentinel as a miss", () => {
179
- const lookup = lookupCacheEntry(tmpRoot, base(sourceFile));
180
- ensureCacheEntryDir(lookup.entry);
181
- // Simulate abandoned extraction — frames written but sentinel never marked.
182
- writeFileSync(join(lookup.entry.dir, "frame_00001.jpg"), "x", "utf-8");
183
- const again = lookupCacheEntry(tmpRoot, base(sourceFile));
184
- expect(again.hit).toBe(false);
185
- });
186
-
187
- it("places entries under the cache root, not the source parent", () => {
188
- const subroot = join(tmpRoot, "cache-root");
189
- mkdirSync(subroot, { recursive: true });
190
- const lookup = lookupCacheEntry(subroot, base(sourceFile));
191
- expect(lookup.entry.dir.startsWith(subroot)).toBe(true);
192
- });
193
-
194
- it("uses the same directory for identical inputs across lookups", () => {
195
- const a = lookupCacheEntry(tmpRoot, base(sourceFile));
196
- const b = lookupCacheEntry(tmpRoot, base(sourceFile));
197
- expect(a.entry.dir).toBe(b.entry.dir);
198
- });
199
- });
@@ -1,216 +0,0 @@
1
- /**
2
- * Content-Addressed Extraction Cache
3
- *
4
- * Video frame extraction is the single most expensive phase of a render
5
- * after capture. Repeat renders of the same composition (preview → final,
6
- * studio iteration) re-extract identical frames from the same source file,
7
- * burning ffmpeg time that adds no value. This module keys extracted frame
8
- * bundles on the (path, mtime, size, mediaStart, duration, fps, format)
9
- * tuple so re-renders resolve to a pre-extracted directory instead of
10
- * re-invoking ffmpeg.
11
- *
12
- * ### Scheme
13
- *
14
- * - The key is the SHA-256 of a stable JSON encoding of the tuple above.
15
- * - Cache entries live under `<rootDir>/<SCHEMA_PREFIX><key[0..16]>/` so
16
- * `ls` output and tracing logs stay short. Truncation to 16 hex chars
17
- * leaves 64 bits of entropy — collision risk at cache scale is negligible.
18
- * - A completed entry is marked by writing the `.hf-complete` sentinel file
19
- * after all frames are on disk. A dir without the sentinel is treated as
20
- * absent (stale/abandoned) and re-extracted into a fresh key (the old dir
21
- * is left for external gc — the cache owns keys, not deletion policy).
22
- *
23
- * ### Versioning
24
- *
25
- * `SCHEMA_PREFIX` bumps when the cache-contents invariant changes (e.g.
26
- * extraction format, frame layout). Old entries under the previous prefix
27
- * become inert and can be gc'd by the caller.
28
- */
29
-
30
- import { createHash } from "node:crypto";
31
- import { mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
32
- import { existsSync } from "node:fs";
33
- import { join } from "node:path";
34
- import type { VideoMetadata } from "../utils/ffprobe.js";
35
-
36
- /** Filename prefix for extracted frames. Shared with the extractor. */
37
- export const FRAME_FILENAME_PREFIX = "frame_";
38
-
39
- /** Sentinel filename written after a cache entry is fully populated. */
40
- export const COMPLETE_SENTINEL = ".hf-complete";
41
-
42
- /** Current schema version. Bump when cache-entry layout changes. */
43
- export const SCHEMA_PREFIX = "hfcache-v2-";
44
-
45
- /** Truncated hex chars of SHA-256 used for the entry directory name. */
46
- const KEY_HEX_CHARS = 16;
47
-
48
- export type CacheFrameFormat = "jpg" | "png";
49
-
50
- export interface CacheKeyInput {
51
- /** Absolute path to the source video file. Part of the key so moved files
52
- * re-extract rather than match by (size, mtime) alone. */
53
- videoPath: string;
54
- /** Source file modification time in ms (floored). Invalidates the key on edit. */
55
- mtimeMs: number;
56
- /** Source file size in bytes. Invalidates the key on content change. */
57
- size: number;
58
- /** Seconds into source the composition starts reading (video.mediaStart). */
59
- mediaStart: number;
60
- /** Seconds of source the composition uses. Infinity is normalized to -1
61
- * so callers that pass an unresolved "natural duration" still produce a
62
- * stable key across invocations. */
63
- duration: number;
64
- /** Target output frames-per-second. */
65
- fps: number;
66
- /** Output image format. */
67
- format: CacheFrameFormat;
68
- }
69
-
70
- export interface CacheEntry {
71
- /** Absolute path to the cache entry directory. */
72
- dir: string;
73
- /** Full 64-char SHA-256 hex digest (parent of the truncated key). */
74
- keyHash: string;
75
- }
76
-
77
- export interface CacheLookup {
78
- /** Cache entry information — always returned even on a miss so the caller
79
- * can extract directly into `dir` then call `markCacheEntryComplete`. */
80
- entry: CacheEntry;
81
- /** True when the entry exists AND carries the completion sentinel. */
82
- hit: boolean;
83
- }
84
-
85
- /**
86
- * Read `(mtimeMs, size)` for a path. Returns `null` if the file is missing —
87
- * callers should skip the cache path for that entry so the extractor surfaces
88
- * the real file-not-found error. Returning a zero-stat sentinel would let two
89
- * missing files share the same `(0, 0)` tuple and pollute the cache with an
90
- * orphaned entry.
91
- */
92
- export function readKeyStat(videoPath: string): { mtimeMs: number; size: number } | null {
93
- try {
94
- const stat = statSync(videoPath);
95
- return { mtimeMs: Math.floor(stat.mtimeMs), size: stat.size };
96
- } catch {
97
- return null;
98
- }
99
- }
100
-
101
- function canonicalKeyBlob(input: CacheKeyInput): string {
102
- const durationForKey = Number.isFinite(input.duration) ? input.duration : -1;
103
- return JSON.stringify({
104
- p: input.videoPath,
105
- m: input.mtimeMs,
106
- s: input.size,
107
- ms: input.mediaStart,
108
- d: durationForKey,
109
- f: input.fps,
110
- fmt: input.format,
111
- });
112
- }
113
-
114
- /**
115
- * Compute the SHA-256 hex digest for a cache key input.
116
- */
117
- export function computeCacheKey(input: CacheKeyInput): string {
118
- return createHash("sha256").update(canonicalKeyBlob(input)).digest("hex");
119
- }
120
-
121
- /**
122
- * Derive the truncated cache-entry directory name from a full key hash.
123
- * Exposed so tests and the entry dir resolver share one truncation rule.
124
- */
125
- export function cacheEntryDirName(keyHash: string): string {
126
- return SCHEMA_PREFIX + keyHash.slice(0, KEY_HEX_CHARS);
127
- }
128
-
129
- /**
130
- * Look up a cache entry by key input. Returns the resolved entry path plus a
131
- * `hit` flag. On miss, callers should extract frames into `entry.dir`
132
- * (after calling `ensureCacheEntryDir`) and then call `markCacheEntryComplete`
133
- * once the extraction succeeds.
134
- */
135
- export function lookupCacheEntry(rootDir: string, input: CacheKeyInput): CacheLookup {
136
- const keyHash = computeCacheKey(input);
137
- const dir = join(rootDir, cacheEntryDirName(keyHash));
138
- const complete = existsSync(join(dir, COMPLETE_SENTINEL));
139
- return { entry: { dir, keyHash }, hit: complete };
140
- }
141
-
142
- /**
143
- * Ensure a cache entry's directory exists so the extractor can write into it.
144
- * Idempotent: `mkdirSync({recursive:true})` is a no-op when the dir exists.
145
- */
146
- export function ensureCacheEntryDir(entry: CacheEntry): void {
147
- mkdirSync(entry.dir, { recursive: true });
148
- }
149
-
150
- /**
151
- * Write the completion sentinel so subsequent lookups treat this entry as a
152
- * hit. Must be called only after every frame has been written.
153
- *
154
- * Concurrency: lookup→populate→mark is non-atomic. Two concurrent renders of
155
- * the same key may both miss, both extract into the same dir, and the later
156
- * writer's frames win. The result is correct (identical inputs yield identical
157
- * frames) but wasteful. Acceptable for a single-process render pipeline;
158
- * anyone running concurrent renders against a shared cache root should front
159
- * it with an external lock.
160
- */
161
- export function markCacheEntryComplete(entry: CacheEntry): void {
162
- writeFileSync(join(entry.dir, COMPLETE_SENTINEL), "", "utf-8");
163
- }
164
-
165
- /**
166
- * Rebuild the in-memory frame index for a cached entry. Called on cache hits
167
- * so the extractor's caller receives the same `ExtractedFrames` shape it
168
- * would get from a fresh extraction — without re-running ffmpeg or ffprobe.
169
- *
170
- * The `metadata` argument is the `VideoMetadata` probed in the extractor's
171
- * Phase 2 (pre-preflight). Passing it here avoids an extra ffprobe on the
172
- * hit path.
173
- */
174
- export interface RehydrateOptions {
175
- videoId: string;
176
- srcPath: string;
177
- fps: number;
178
- format: CacheFrameFormat;
179
- metadata: VideoMetadata;
180
- }
181
-
182
- export interface RehydratedFrames {
183
- videoId: string;
184
- srcPath: string;
185
- outputDir: string;
186
- framePattern: string;
187
- fps: number;
188
- totalFrames: number;
189
- metadata: VideoMetadata;
190
- framePaths: Map<number, string>;
191
- }
192
-
193
- export function rehydrateCacheEntry(
194
- entry: CacheEntry,
195
- options: RehydrateOptions,
196
- ): RehydratedFrames {
197
- const framePattern = `${FRAME_FILENAME_PREFIX}%05d.${options.format}`;
198
- const framePaths = new Map<number, string>();
199
- const suffix = `.${options.format}`;
200
- const files = readdirSync(entry.dir)
201
- .filter((f) => f.startsWith(FRAME_FILENAME_PREFIX) && f.endsWith(suffix))
202
- .sort();
203
- files.forEach((file, idx) => {
204
- framePaths.set(idx, join(entry.dir, file));
205
- });
206
- return {
207
- videoId: options.videoId,
208
- srcPath: options.srcPath,
209
- outputDir: entry.dir,
210
- framePattern,
211
- fps: options.fps,
212
- totalFrames: framePaths.size,
213
- metadata: options.metadata,
214
- framePaths,
215
- };
216
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * File Server
3
- *
4
- * Lightweight HTTP server that serves a project directory to headless Chrome.
5
- * Optionally injects scripts into index.html on-the-fly (e.g. runtime, bridge).
6
- * Framework-agnostic — the caller decides what scripts to inject.
7
- */
8
-
9
- import { Hono } from "hono";
10
- import { serve } from "@hono/node-server";
11
- import { readFileSync, existsSync, statSync } from "node:fs";
12
- import { join, extname } from "node:path";
13
- import { injectScriptsIntoHtml } from "@hyperframes/core/compiler";
14
-
15
- const MIME_TYPES: Record<string, string> = {
16
- ".html": "text/html; charset=utf-8",
17
- ".css": "text/css; charset=utf-8",
18
- ".js": "application/javascript; charset=utf-8",
19
- ".json": "application/json; charset=utf-8",
20
- ".png": "image/png",
21
- ".jpg": "image/jpeg",
22
- ".jpeg": "image/jpeg",
23
- ".gif": "image/gif",
24
- ".svg": "image/svg+xml",
25
- ".webp": "image/webp",
26
- ".mp4": "video/mp4",
27
- ".webm": "video/webm",
28
- ".mp3": "audio/mpeg",
29
- ".wav": "audio/wav",
30
- ".ogg": "audio/ogg",
31
- ".aac": "audio/aac",
32
- ".woff": "font/woff",
33
- ".woff2": "font/woff2",
34
- ".ttf": "font/ttf",
35
- ".otf": "font/otf",
36
- };
37
-
38
- export interface FileServerOptions {
39
- projectDir: string;
40
- compiledDir?: string;
41
- port?: number;
42
- /** Scripts injected into <head> of index.html. Default: none. */
43
- headScripts?: string[];
44
- /** Scripts injected before </body> of index.html. Default: none. */
45
- bodyScripts?: string[];
46
- /** Strip embedded runtime scripts from HTML before injection. Default: true. */
47
- stripEmbeddedRuntime?: boolean;
48
- }
49
-
50
- export interface FileServerHandle {
51
- url: string;
52
- port: number;
53
- close: () => void;
54
- }
55
-
56
- export function createFileServer(options: FileServerOptions): Promise<FileServerHandle> {
57
- const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
58
-
59
- const headScripts = options.headScripts ?? [];
60
- const bodyScripts = options.bodyScripts ?? [];
61
-
62
- const app = new Hono();
63
-
64
- app.get("/*", (c) => {
65
- let requestPath = c.req.path;
66
- if (requestPath === "/") requestPath = "/index.html";
67
-
68
- // Remove leading slash
69
- const relativePath = requestPath.replace(/^\//, "");
70
- const compiledPath = compiledDir ? join(compiledDir, relativePath) : null;
71
- const hasCompiledFile = Boolean(
72
- compiledPath && existsSync(compiledPath) && statSync(compiledPath).isFile(),
73
- );
74
- const filePath = hasCompiledFile ? (compiledPath as string) : join(projectDir, relativePath);
75
-
76
- if (!existsSync(filePath) || !statSync(filePath).isFile()) {
77
- return c.text("Not found", 404);
78
- }
79
-
80
- const ext = extname(filePath).toLowerCase();
81
- const contentType = MIME_TYPES[ext] || "application/octet-stream";
82
-
83
- if (ext === ".html") {
84
- const rawHtml = readFileSync(filePath, "utf-8");
85
- const html =
86
- relativePath === "index.html"
87
- ? injectScriptsIntoHtml(rawHtml, headScripts, bodyScripts, stripEmbeddedRuntime)
88
- : rawHtml;
89
- return c.text(html, 200, { "Content-Type": contentType });
90
- }
91
-
92
- const content = readFileSync(filePath);
93
- return new Response(content, {
94
- status: 200,
95
- headers: { "Content-Type": contentType },
96
- });
97
- });
98
-
99
- return new Promise((resolve) => {
100
- const server = serve({ fetch: app.fetch, port }, (info) => {
101
- const actualPort = info.port;
102
- const url = `http://localhost:${actualPort}`;
103
- resolve({
104
- url,
105
- port: actualPort,
106
- close: () => server.close(),
107
- });
108
- });
109
- });
110
- }