@hyperframes/engine 0.1.1 → 0.1.3

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 (49) hide show
  1. package/README.md +78 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js.map +1 -1
  4. package/dist/index.d.ts +5 -5
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/index.js.map +1 -1
  8. package/dist/services/audioMixer.d.ts.map +1 -1
  9. package/dist/services/audioMixer.js +26 -5
  10. package/dist/services/audioMixer.js.map +1 -1
  11. package/dist/services/browserManager.d.ts.map +1 -1
  12. package/dist/services/browserManager.js.map +1 -1
  13. package/dist/services/chunkEncoder.d.ts.map +1 -1
  14. package/dist/services/chunkEncoder.js +24 -3
  15. package/dist/services/chunkEncoder.js.map +1 -1
  16. package/dist/services/fileServer.d.ts.map +1 -1
  17. package/dist/services/fileServer.js.map +1 -1
  18. package/dist/services/frameCapture.d.ts.map +1 -1
  19. package/dist/services/frameCapture.js +1 -1
  20. package/dist/services/frameCapture.js.map +1 -1
  21. package/dist/services/parallelCoordinator.d.ts.map +1 -1
  22. package/dist/services/parallelCoordinator.js.map +1 -1
  23. package/dist/services/screenshotService.d.ts.map +1 -1
  24. package/dist/services/screenshotService.js.map +1 -1
  25. package/dist/services/videoFrameExtractor.d.ts.map +1 -1
  26. package/dist/services/videoFrameExtractor.js +7 -1
  27. package/dist/services/videoFrameExtractor.js.map +1 -1
  28. package/dist/services/videoFrameInjector.d.ts.map +1 -1
  29. package/dist/services/videoFrameInjector.js +3 -1
  30. package/dist/services/videoFrameInjector.js.map +1 -1
  31. package/dist/utils/ffprobe.d.ts.map +1 -1
  32. package/dist/utils/ffprobe.js +18 -2
  33. package/dist/utils/ffprobe.js.map +1 -1
  34. package/dist/utils/urlDownloader.d.ts.map +1 -1
  35. package/dist/utils/urlDownloader.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/config.ts +32 -8
  38. package/src/index.ts +15 -17
  39. package/src/services/audioMixer.ts +33 -8
  40. package/src/services/browserManager.ts +7 -2
  41. package/src/services/chunkEncoder.ts +24 -3
  42. package/src/services/fileServer.ts +3 -1
  43. package/src/services/frameCapture.ts +46 -13
  44. package/src/services/parallelCoordinator.ts +25 -5
  45. package/src/services/screenshotService.ts +4 -1
  46. package/src/services/videoFrameExtractor.ts +19 -4
  47. package/src/services/videoFrameInjector.ts +9 -3
  48. package/src/utils/ffprobe.ts +25 -4
  49. package/src/utils/urlDownloader.ts +5 -1
@@ -135,7 +135,9 @@ export function createFileServer(options: FileServerOptions): Promise<FileServer
135
135
  // Remove leading slash
136
136
  const relativePath = requestPath.replace(/^\//, "");
137
137
  const compiledPath = compiledDir ? join(compiledDir, relativePath) : null;
138
- const hasCompiledFile = Boolean(compiledPath && existsSync(compiledPath) && statSync(compiledPath).isFile());
138
+ const hasCompiledFile = Boolean(
139
+ compiledPath && existsSync(compiledPath) && statSync(compiledPath).isFile(),
140
+ );
139
141
  const filePath = hasCompiledFile ? (compiledPath as string) : join(projectDir, relativePath);
140
142
 
141
143
  if (!existsSync(filePath) || !statSync(filePath).isFile()) {
@@ -22,7 +22,12 @@ import {
22
22
  } from "./browserManager.js";
23
23
  import { beginFrameCapture, getCdpSession, pageScreenshotCapture } from "./screenshotService.js";
24
24
  import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
25
- import type { CaptureOptions, CaptureResult, CaptureBufferResult, CapturePerfSummary } from "../types.js";
25
+ import type {
26
+ CaptureOptions,
27
+ CaptureResult,
28
+ CaptureBufferResult,
29
+ CapturePerfSummary,
30
+ } from "../types.js";
26
31
 
27
32
  export type { CaptureOptions, CaptureResult, CaptureBufferResult, CapturePerfSummary };
28
33
 
@@ -72,8 +77,12 @@ export async function createCaptureSession(
72
77
  const headlessShell = resolveHeadlessShellPath(config);
73
78
  const isLinux = process.platform === "linux";
74
79
  const forceScreenshot = config?.forceScreenshot ?? DEFAULT_CONFIG.forceScreenshot;
75
- const preMode: CaptureMode = headlessShell && isLinux && !forceScreenshot ? "beginframe" : "screenshot";
76
- const chromeArgs = buildChromeArgs({ width: options.width, height: options.height, captureMode: preMode }, config);
80
+ const preMode: CaptureMode =
81
+ headlessShell && isLinux && !forceScreenshot ? "beginframe" : "screenshot";
82
+ const chromeArgs = buildChromeArgs(
83
+ { width: options.width, height: options.height, captureMode: preMode },
84
+ config,
85
+ );
77
86
 
78
87
  const { browser, captureMode } = await acquireBrowser(chromeArgs, config);
79
88
 
@@ -81,7 +90,10 @@ export async function createCaptureSession(
81
90
  const browserVersion = await browser.version();
82
91
  const expectedMajor = config?.expectedChromiumMajor;
83
92
  if (Number.isFinite(expectedMajor)) {
84
- const actualChromiumMajor = Number.parseInt((browserVersion.match(/(\d+)\./) || [])[1] || "", 10);
93
+ const actualChromiumMajor = Number.parseInt(
94
+ (browserVersion.match(/(\d+)\./) || [])[1] || "",
95
+ 10,
96
+ );
85
97
  if (Number.isFinite(actualChromiumMajor) && actualChromiumMajor !== expectedMajor) {
86
98
  throw new Error(
87
99
  `[FrameCapture] Chromium major mismatch expected=${expectedMajor} actual=${actualChromiumMajor} raw=${browserVersion}`,
@@ -121,13 +133,14 @@ export async function createCaptureSession(
121
133
  }
122
134
 
123
135
  export async function initializeSession(session: CaptureSession): Promise<void> {
124
- const { page, serverUrl, options } = session;
136
+ const { page, serverUrl } = session;
125
137
 
126
138
  // Forward browser console to host with [Browser] prefix
127
139
  page.on("console", (msg: ConsoleMessage) => {
128
140
  const type = msg.type();
129
141
  const text = msg.text();
130
- const prefix = type === "error" ? "[Browser:ERROR]" : type === "warn" ? "[Browser:WARN]" : "[Browser]";
142
+ const prefix =
143
+ type === "error" ? "[Browser:ERROR]" : type === "warn" ? "[Browser:WARN]" : "[Browser]";
131
144
  console.log(`${prefix} ${text}`);
132
145
 
133
146
  session.browserConsoleBuffer.push(`${prefix} ${text}`);
@@ -151,7 +164,8 @@ export async function initializeSession(session: CaptureSession): Promise<void>
151
164
  // Screenshot mode: standard navigation, rAF works normally
152
165
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60000 });
153
166
 
154
- const pageReadyTimeout = session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout;
167
+ const pageReadyTimeout =
168
+ session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout;
155
169
  await page.waitForFunction(
156
170
  `!!(window.__hf && typeof window.__hf.seek === "function" && window.__hf.duration > 0)`,
157
171
  { timeout: pageReadyTimeout },
@@ -230,7 +244,8 @@ export async function initializeSession(session: CaptureSession): Promise<void>
230
244
 
231
245
  // Wait for all video elements to have loaded metadata (dimensions + duration).
232
246
  // Without this, frame 0 captures videos at their 300x150 default size.
233
- const videoDeadline = Date.now() + (session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout);
247
+ const videoDeadline =
248
+ Date.now() + (session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout);
234
249
  while (Date.now() < videoDeadline) {
235
250
  const videosReady = await page.evaluate(
236
251
  `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`,
@@ -341,14 +356,24 @@ async function captureFrameCore(
341
356
  const startTime = Date.now();
342
357
 
343
358
  try {
344
- const { quantizedTime, seekMs, beforeCaptureMs } = await prepareFrameForCapture(session, frameIndex, time);
359
+ const { quantizedTime, seekMs, beforeCaptureMs } = await prepareFrameForCapture(
360
+ session,
361
+ frameIndex,
362
+ time,
363
+ );
345
364
 
346
365
  const screenshotStart = Date.now();
347
366
  let screenshotBuffer: Buffer;
348
367
 
349
368
  if (session.captureMode === "beginframe") {
350
- const frameTimeTicks = session.beginFrameTimeTicks + frameIndex * session.beginFrameIntervalMs;
351
- const result = await beginFrameCapture(page, options, frameTimeTicks, session.beginFrameIntervalMs);
369
+ const frameTimeTicks =
370
+ session.beginFrameTimeTicks + frameIndex * session.beginFrameIntervalMs;
371
+ const result = await beginFrameCapture(
372
+ page,
373
+ options,
374
+ frameTimeTicks,
375
+ session.beginFrameIntervalMs,
376
+ );
352
377
  if (result.hasDamage) session.beginFrameHasDamageCount++;
353
378
  else session.beginFrameNoDamageCount++;
354
379
  screenshotBuffer = result.buffer;
@@ -379,9 +404,17 @@ async function captureFrameCore(
379
404
  }
380
405
  }
381
406
 
382
- export async function captureFrame(session: CaptureSession, frameIndex: number, time: number): Promise<CaptureResult> {
407
+ export async function captureFrame(
408
+ session: CaptureSession,
409
+ frameIndex: number,
410
+ time: number,
411
+ ): Promise<CaptureResult> {
383
412
  const { options, outputDir } = session;
384
- const { buffer, quantizedTime, captureTimeMs } = await captureFrameCore(session, frameIndex, time);
413
+ const { buffer, quantizedTime, captureTimeMs } = await captureFrameCore(
414
+ session,
415
+ frameIndex,
416
+ time,
417
+ );
385
418
 
386
419
  const ext = options.format === "png" ? "png" : "jpg";
387
420
  const frameName = `frame_${String(frameIndex).padStart(6, "0")}.${ext}`;
@@ -57,7 +57,12 @@ const MIN_FRAMES_PER_WORKER = 30;
57
57
  export function calculateOptimalWorkers(
58
58
  totalFrames: number,
59
59
  requested?: number,
60
- config?: Partial<Pick<EngineConfig, "concurrency" | "coresPerWorker" | "minParallelFrames" | "largeRenderThreshold">>,
60
+ config?: Partial<
61
+ Pick<
62
+ EngineConfig,
63
+ "concurrency" | "coresPerWorker" | "minParallelFrames" | "largeRenderThreshold"
64
+ >
65
+ >,
61
66
  ): number {
62
67
  // Resolve effective values: config overrides → DEFAULT_CONFIG fallback.
63
68
  const effectiveMaxWorkers = (() => {
@@ -69,7 +74,8 @@ export function calculateOptimalWorkers(
69
74
  })();
70
75
  const effectiveCoresPerWorker = config?.coresPerWorker ?? DEFAULT_CONFIG.coresPerWorker;
71
76
  const effectiveMinParallelFrames = config?.minParallelFrames ?? DEFAULT_CONFIG.minParallelFrames;
72
- const effectiveLargeRenderThreshold = config?.largeRenderThreshold ?? DEFAULT_CONFIG.largeRenderThreshold;
77
+ const effectiveLargeRenderThreshold =
78
+ config?.largeRenderThreshold ?? DEFAULT_CONFIG.largeRenderThreshold;
73
79
 
74
80
  if (requested !== undefined) {
75
81
  return Math.max(MIN_WORKERS, Math.min(effectiveMaxWorkers, requested));
@@ -107,7 +113,11 @@ export function calculateOptimalWorkers(
107
113
  return finalWorkers;
108
114
  }
109
115
 
110
- export function distributeFrames(totalFrames: number, workerCount: number, workDir: string): WorkerTask[] {
116
+ export function distributeFrames(
117
+ totalFrames: number,
118
+ workerCount: number,
119
+ workDir: string,
120
+ ): WorkerTask[] {
111
121
  const tasks: WorkerTask[] = [];
112
122
  const framesPerWorker = Math.ceil(totalFrames / workerCount);
113
123
 
@@ -146,7 +156,13 @@ async function executeWorkerTask(
146
156
  let perf: CapturePerfSummary | undefined;
147
157
 
148
158
  try {
149
- session = await createCaptureSession(serverUrl, task.outputDir, captureOptions, createBeforeCaptureHook(), config);
159
+ session = await createCaptureSession(
160
+ serverUrl,
161
+ task.outputDir,
162
+ captureOptions,
163
+ createBeforeCaptureHook(),
164
+ config,
165
+ );
150
166
  await initializeSession(session);
151
167
 
152
168
  for (let i = task.startFrame; i < task.endFrame; i++) {
@@ -248,7 +264,11 @@ export async function executeParallelCapture(
248
264
  return results;
249
265
  }
250
266
 
251
- export async function mergeWorkerFrames(workDir: string, tasks: WorkerTask[], outputDir: string): Promise<number> {
267
+ export async function mergeWorkerFrames(
268
+ workDir: string,
269
+ tasks: WorkerTask[],
270
+ outputDir: string,
271
+ ): Promise<number> {
252
272
  if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
253
273
 
254
274
  let totalFrames = 0;
@@ -203,7 +203,10 @@ export async function injectVideoFramesBatch(
203
203
  );
204
204
  }
205
205
 
206
- export async function syncVideoFrameVisibility(page: Page, activeVideoIds: string[]): Promise<void> {
206
+ export async function syncVideoFrameVisibility(
207
+ page: Page,
208
+ activeVideoIds: string[],
209
+ ): Promise<void> {
207
210
  await page.evaluate((ids: string[]) => {
208
211
  const active = new Set(ids);
209
212
  const videos = Array.from(document.querySelectorAll("video[data-start]")) as HTMLVideoElement[];
@@ -252,10 +252,20 @@ export async function extractAllVideoFrames(
252
252
  }
253
253
  }
254
254
 
255
- return { success: errors.length === 0, extracted, errors, totalFramesExtracted, durationMs: Date.now() - startTime };
255
+ return {
256
+ success: errors.length === 0,
257
+ extracted,
258
+ errors,
259
+ totalFramesExtracted,
260
+ durationMs: Date.now() - startTime,
261
+ };
256
262
  }
257
263
 
258
- export function getFrameAtTime(extracted: ExtractedFrames, globalTime: number, videoStart: number): string | null {
264
+ export function getFrameAtTime(
265
+ extracted: ExtractedFrames,
266
+ globalTime: number,
267
+ videoStart: number,
268
+ ): string | null {
259
269
  const localTime = globalTime - videoStart;
260
270
  if (localTime < 0) return null;
261
271
  const frameIndex = Math.floor(localTime * extracted.fps);
@@ -344,7 +354,9 @@ export class FrameLookupTable {
344
354
  this.lastTime = globalTime;
345
355
  }
346
356
 
347
- getActiveFramePayloads(globalTime: number): Map<string, { framePath: string; frameIndex: number }> {
357
+ getActiveFramePayloads(
358
+ globalTime: number,
359
+ ): Map<string, { framePath: string; frameIndex: number }> {
348
360
  const frames = new Map<string, { framePath: string; frameIndex: number }>();
349
361
  this.refreshActiveSet(globalTime);
350
362
  for (const videoId of this.activeVideoIds) {
@@ -381,7 +393,10 @@ export class FrameLookupTable {
381
393
  }
382
394
  }
383
395
 
384
- export function createFrameLookupTable(videos: VideoElement[], extracted: ExtractedFrames[]): FrameLookupTable {
396
+ export function createFrameLookupTable(
397
+ videos: VideoElement[],
398
+ extracted: ExtractedFrames[],
399
+ ): FrameLookupTable {
385
400
  const table = new FrameLookupTable();
386
401
  const extractedMap = new Map<string, ExtractedFrames>();
387
402
  for (const ext of extracted) extractedMap.set(ext.videoId, ext);
@@ -71,7 +71,10 @@ export function createVideoFrameInjector(
71
71
  ): BeforeCaptureHook | null {
72
72
  if (!frameLookup) return null;
73
73
 
74
- const cacheLimit = Math.max(32, config?.frameDataUriCacheLimit ?? DEFAULT_CONFIG.frameDataUriCacheLimit);
74
+ const cacheLimit = Math.max(
75
+ 32,
76
+ config?.frameDataUriCacheLimit ?? DEFAULT_CONFIG.frameDataUriCacheLimit,
77
+ );
75
78
  const frameCache = createFrameDataUriCache(cacheLimit);
76
79
  const lastInjectedFrameByVideo = new Map<string, number>();
77
80
 
@@ -81,13 +84,16 @@ export function createVideoFrameInjector(
81
84
  const updates: Array<{ videoId: string; dataUri: string; frameIndex: number }> = [];
82
85
  const activeIds = new Set<string>();
83
86
  if (activePayloads.size > 0) {
84
- const pendingReads: Array<Promise<{ videoId: string; dataUri: string; frameIndex: number }>> = [];
87
+ const pendingReads: Array<Promise<{ videoId: string; dataUri: string; frameIndex: number }>> =
88
+ [];
85
89
  for (const [videoId, payload] of activePayloads) {
86
90
  activeIds.add(videoId);
87
91
  const lastFrameIndex = lastInjectedFrameByVideo.get(videoId);
88
92
  if (lastFrameIndex === payload.frameIndex) continue;
89
93
  pendingReads.push(
90
- frameCache.get(payload.framePath).then((dataUri) => ({ videoId, dataUri, frameIndex: payload.frameIndex })),
94
+ frameCache
95
+ .get(payload.framePath)
96
+ .then((dataUri) => ({ videoId, dataUri, frameIndex: payload.frameIndex })),
91
97
  );
92
98
  }
93
99
  updates.push(...(await Promise.all(pendingReads)));
@@ -59,7 +59,15 @@ export async function extractVideoMetadata(filePath: string): Promise<VideoMetad
59
59
  }
60
60
 
61
61
  const probePromise = new Promise<VideoMetadata>((resolve, reject) => {
62
- const args = ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath];
62
+ const args = [
63
+ "-v",
64
+ "quiet",
65
+ "-print_format",
66
+ "json",
67
+ "-show_format",
68
+ "-show_streams",
69
+ filePath,
70
+ ];
63
71
 
64
72
  const ffprobe = spawn("ffprobe", args);
65
73
  let stdout = "";
@@ -87,7 +95,8 @@ export async function extractVideoMetadata(filePath: string): Promise<VideoMetad
87
95
  }
88
96
 
89
97
  const hasAudio = output.streams.some((s) => s.codec_type === "audio");
90
- const fps = parseFrameRate(videoStream.avg_frame_rate) || parseFrameRate(videoStream.r_frame_rate);
98
+ const fps =
99
+ parseFrameRate(videoStream.avg_frame_rate) || parseFrameRate(videoStream.r_frame_rate);
91
100
  const durationSeconds = output.format.duration ? parseFloat(output.format.duration) : 0;
92
101
 
93
102
  const metadata: VideoMetadata = {
@@ -100,7 +109,11 @@ export async function extractVideoMetadata(filePath: string): Promise<VideoMetad
100
109
  };
101
110
  resolve(metadata);
102
111
  } catch (parseError: unknown) {
103
- reject(new Error(`[FFmpeg] Failed to parse ffprobe output: ${parseError instanceof Error ? parseError.message : parseError}`));
112
+ reject(
113
+ new Error(
114
+ `[FFmpeg] Failed to parse ffprobe output: ${parseError instanceof Error ? parseError.message : parseError}`,
115
+ ),
116
+ );
104
117
  }
105
118
  });
106
119
 
@@ -128,7 +141,15 @@ export async function extractAudioMetadata(filePath: string): Promise<AudioMetad
128
141
  }
129
142
 
130
143
  const probePromise = new Promise<AudioMetadata>((resolve, reject) => {
131
- const args = ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath];
144
+ const args = [
145
+ "-v",
146
+ "quiet",
147
+ "-print_format",
148
+ "json",
149
+ "-show_format",
150
+ "-show_streams",
151
+ filePath,
152
+ ];
132
153
 
133
154
  const ffprobe = spawn("ffprobe", args);
134
155
  let stdout = "";
@@ -14,7 +14,11 @@ function getFilenameFromUrl(url: string): string {
14
14
  return `download_${hash}${ext}`;
15
15
  }
16
16
 
17
- export async function downloadToTemp(url: string, destDir: string, timeoutMs: number = 300000): Promise<string> {
17
+ export async function downloadToTemp(
18
+ url: string,
19
+ destDir: string,
20
+ timeoutMs: number = 300000,
21
+ ): Promise<string> {
18
22
  const cachedPath = downloadPathCache.get(url);
19
23
  if (cachedPath && existsSync(cachedPath)) {
20
24
  return cachedPath;