hyperframes 0.6.13 → 0.6.15

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.
package/dist/cli.js CHANGED
@@ -54,7 +54,7 @@ var VERSION;
54
54
  var init_version = __esm({
55
55
  "src/version.ts"() {
56
56
  "use strict";
57
- VERSION = true ? "0.6.13" : "0.0.0-dev";
57
+ VERSION = true ? "0.6.15" : "0.0.0-dev";
58
58
  }
59
59
  });
60
60
 
@@ -11321,8 +11321,8 @@ function flushSync() {
11321
11321
  eventQueue = [];
11322
11322
  const payload = JSON.stringify({ api_key: POSTHOG_API_KEY, batch });
11323
11323
  try {
11324
- const { spawn: spawn16 } = __require("child_process");
11325
- const child = spawn16(
11324
+ const { spawn: spawn17 } = __require("child_process");
11325
+ const child = spawn17(
11326
11326
  process.execPath,
11327
11327
  [
11328
11328
  "-e",
@@ -12235,6 +12235,37 @@ var init_skills = __esm({
12235
12235
  }
12236
12236
  });
12237
12237
 
12238
+ // src/utils/openBrowser.ts
12239
+ import { spawn as spawn2 } from "child_process";
12240
+ function buildBrowserArgs(url, options) {
12241
+ const args = [];
12242
+ if (options.userDataDir) {
12243
+ args.push(`--user-data-dir=${options.userDataDir}`);
12244
+ }
12245
+ args.push(url);
12246
+ return args;
12247
+ }
12248
+ function openBrowser(url, options = {}) {
12249
+ if (options.browserPath) {
12250
+ const args = buildBrowserArgs(url, options);
12251
+ const child = spawn2(options.browserPath, args, {
12252
+ detached: true,
12253
+ stdio: "ignore"
12254
+ });
12255
+ child.on("error", () => {
12256
+ });
12257
+ child.unref();
12258
+ return;
12259
+ }
12260
+ import("open").then((mod) => mod.default(url)).catch(() => {
12261
+ });
12262
+ }
12263
+ var init_openBrowser = __esm({
12264
+ "src/utils/openBrowser.ts"() {
12265
+ "use strict";
12266
+ }
12267
+ });
12268
+
12238
12269
  // ../core/src/lint/index.ts
12239
12270
  var lint_exports = {};
12240
12271
  __export(lint_exports, {
@@ -13055,7 +13086,7 @@ var init_mime = __esm({
13055
13086
  });
13056
13087
 
13057
13088
  // ../core/src/studio-api/helpers/waveform.ts
13058
- import { spawn as spawn2 } from "child_process";
13089
+ import { spawn as spawn3 } from "child_process";
13059
13090
  import { existsSync as existsSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5 } from "fs";
13060
13091
  import { join as join11 } from "path";
13061
13092
  function buildWaveformCacheKey(assetPath) {
@@ -13079,7 +13110,7 @@ function computePeaks(floats, count) {
13079
13110
  }
13080
13111
  function decodeAudioPeaks(audioPath) {
13081
13112
  return new Promise((resolve43, reject) => {
13082
- const proc = spawn2(
13113
+ const proc = spawn3(
13083
13114
  "ffmpeg",
13084
13115
  [
13085
13116
  "-i",
@@ -27898,7 +27929,10 @@ function registerThumbnailRoutes(api, adapter2) {
27898
27929
  selectorIndex
27899
27930
  });
27900
27931
  if (!buffer) {
27901
- return c3.json({ error: "Thumbnail generation returned null" }, 500);
27932
+ return c3.json(
27933
+ { error: "Thumbnail generation failed \u2014 Chrome browser may not be available" },
27934
+ 500
27935
+ );
27902
27936
  }
27903
27937
  if (!existsSync14(cacheDir)) mkdirSync8(cacheDir, { recursive: true });
27904
27938
  writeFileSync9(cachePath2, buffer);
@@ -29854,10 +29888,10 @@ var init_frameCapture = __esm({
29854
29888
  });
29855
29889
 
29856
29890
  // ../engine/src/utils/gpuEncoder.ts
29857
- import { spawn as spawn3 } from "child_process";
29891
+ import { spawn as spawn4 } from "child_process";
29858
29892
  async function detectGpuEncoder() {
29859
29893
  return new Promise((resolve43) => {
29860
- const ffmpeg = spawn3("ffmpeg", ["-encoders"], {
29894
+ const ffmpeg = spawn4("ffmpeg", ["-encoders"], {
29861
29895
  stdio: ["pipe", "pipe", "pipe"]
29862
29896
  });
29863
29897
  let stdout2 = "";
@@ -29977,7 +30011,7 @@ var init_hdr = __esm({
29977
30011
  });
29978
30012
 
29979
30013
  // ../engine/src/utils/runFfmpeg.ts
29980
- import { spawn as spawn4 } from "child_process";
30014
+ import { spawn as spawn5 } from "child_process";
29981
30015
  function formatFfmpegError(exitCode, stderr, tailLines = DEFAULT_STDERR_TAIL_LINES) {
29982
30016
  const tail = (stderr ?? "").split(/\r?\n/).filter((line) => line.length > 0).slice(-tailLines).join("\n");
29983
30017
  if (exitCode === null) {
@@ -29993,7 +30027,7 @@ async function runFfmpeg(args, opts) {
29993
30027
  const timeout = opts?.timeout ?? DEFAULT_TIMEOUT;
29994
30028
  const onStderr = opts?.onStderr;
29995
30029
  return new Promise((resolve43) => {
29996
- const ffmpeg = spawn4("ffmpeg", args);
30030
+ const ffmpeg = spawn5("ffmpeg", args);
29997
30031
  let stderr = "";
29998
30032
  const onAbort = () => {
29999
30033
  ffmpeg.kill("SIGTERM");
@@ -30047,7 +30081,7 @@ var init_runFfmpeg = __esm({
30047
30081
  });
30048
30082
 
30049
30083
  // ../engine/src/services/chunkEncoder.ts
30050
- import { spawn as spawn5 } from "child_process";
30084
+ import { spawn as spawn6 } from "child_process";
30051
30085
  import { copyFileSync, existsSync as existsSync20, mkdirSync as mkdirSync11, readdirSync as readdirSync10, statSync as statSync6, writeFileSync as writeFileSync12 } from "fs";
30052
30086
  import { join as join24, dirname as dirname7 } from "path";
30053
30087
  function getEncoderPreset(quality, format = "mp4", hdr) {
@@ -30260,7 +30294,7 @@ async function encodeFramesFromDir(framesDir, framePattern, outputPath, options,
30260
30294
  const inputArgs = ["-framerate", fpsToFfmpegArg(options.fps), "-i", inputPath];
30261
30295
  const args = buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder);
30262
30296
  return new Promise((resolve43) => {
30263
- const ffmpeg = spawn5("ffmpeg", args);
30297
+ const ffmpeg = spawn6("ffmpeg", args);
30264
30298
  let stderr = "";
30265
30299
  const onAbort = () => {
30266
30300
  ffmpeg.kill("SIGTERM");
@@ -30370,7 +30404,7 @@ async function encodeFramesChunkedConcat(framesDir, framePattern, outputPath, op
30370
30404
  if (options.useGpu) gpuEncoder = await getCachedGpuEncoder();
30371
30405
  const args = buildEncoderArgs(options, inputArgs, chunkPath, gpuEncoder);
30372
30406
  const chunkResult = await new Promise((resolve43) => {
30373
- const ffmpeg = spawn5("ffmpeg", args);
30407
+ const ffmpeg = spawn6("ffmpeg", args);
30374
30408
  let stderr = "";
30375
30409
  ffmpeg.stderr.on("data", (d2) => {
30376
30410
  stderr += d2.toString();
@@ -30411,7 +30445,7 @@ async function encodeFramesChunkedConcat(framesDir, framePattern, outputPath, op
30411
30445
  outputPath
30412
30446
  ];
30413
30447
  const concatResult = await new Promise((resolve43) => {
30414
- const ffmpeg = spawn5("ffmpeg", concatArgs);
30448
+ const ffmpeg = spawn6("ffmpeg", concatArgs);
30415
30449
  let stderr = "";
30416
30450
  ffmpeg.stderr.on("data", (d2) => {
30417
30451
  stderr += d2.toString();
@@ -30517,7 +30551,7 @@ var init_chunkEncoder = __esm({
30517
30551
  });
30518
30552
 
30519
30553
  // ../engine/src/services/streamingEncoder.ts
30520
- import { spawn as spawn6 } from "child_process";
30554
+ import { spawn as spawn7 } from "child_process";
30521
30555
  import { existsSync as existsSync21, mkdirSync as mkdirSync12, statSync as statSync7 } from "fs";
30522
30556
  import { dirname as dirname8 } from "path";
30523
30557
  function createFrameReorderBuffer(startFrame, endFrame) {
@@ -30727,7 +30761,7 @@ async function spawnStreamingEncoder(outputPath, options, signal, config) {
30727
30761
  }
30728
30762
  const args = buildStreamingArgs(options, outputPath, gpuEncoder);
30729
30763
  const startTime = Date.now();
30730
- const ffmpeg = spawn6("ffmpeg", args, {
30764
+ const ffmpeg = spawn7("ffmpeg", args, {
30731
30765
  stdio: ["pipe", "pipe", "pipe"]
30732
30766
  });
30733
30767
  let exitStatus = "running";
@@ -30766,21 +30800,31 @@ Process error: ${err.message}`;
30766
30800
  }
30767
30801
  }
30768
30802
  const streamingTimeout = config?.ffmpegStreamingTimeout ?? DEFAULT_CONFIG2.ffmpegStreamingTimeout;
30769
- const timer = setTimeout(() => {
30770
- if (exitStatus === "running") {
30771
- ffmpeg.kill("SIGTERM");
30772
- }
30773
- }, streamingTimeout);
30803
+ let timer = null;
30804
+ const resetTimer = () => {
30805
+ if (timer) clearTimeout(timer);
30806
+ timer = setTimeout(() => {
30807
+ if (exitStatus === "running") {
30808
+ ffmpeg.kill("SIGTERM");
30809
+ }
30810
+ }, streamingTimeout);
30811
+ };
30812
+ resetTimer();
30774
30813
  const encoder = {
30775
30814
  writeFrame: (buffer) => {
30776
30815
  if (exitStatus !== "running" || !ffmpeg.stdin || ffmpeg.stdin.destroyed) {
30777
30816
  return false;
30778
30817
  }
30779
30818
  const copy = Buffer.from(buffer);
30780
- return ffmpeg.stdin.write(copy);
30819
+ const accepted = ffmpeg.stdin.write(copy);
30820
+ if (accepted) resetTimer();
30821
+ return accepted;
30781
30822
  },
30782
30823
  close: async () => {
30783
- clearTimeout(timer);
30824
+ if (timer) {
30825
+ clearTimeout(timer);
30826
+ timer = null;
30827
+ }
30784
30828
  if (signal) signal.removeEventListener("abort", onAbort);
30785
30829
  const stdin = ffmpeg.stdin;
30786
30830
  if (stdin && !stdin.destroyed) {
@@ -30825,12 +30869,12 @@ var init_streamingEncoder = __esm({
30825
30869
  });
30826
30870
 
30827
30871
  // ../engine/src/utils/ffprobe.ts
30828
- import { spawn as spawn7 } from "child_process";
30872
+ import { spawn as spawn8 } from "child_process";
30829
30873
  import { readFileSync as readFileSync18 } from "fs";
30830
30874
  import { extname as extname5 } from "path";
30831
30875
  function runFfprobe(args) {
30832
30876
  return new Promise((resolve43, reject) => {
30833
- const proc = spawn7("ffprobe", args);
30877
+ const proc = spawn8("ffprobe", args);
30834
30878
  let stdout2 = "";
30835
30879
  let stderr = "";
30836
30880
  proc.stdout.on("data", (data) => {
@@ -32534,7 +32578,7 @@ var init_extractionCache = __esm({
32534
32578
  });
32535
32579
 
32536
32580
  // ../engine/src/services/videoFrameExtractor.ts
32537
- import { spawn as spawn8 } from "child_process";
32581
+ import { spawn as spawn9 } from "child_process";
32538
32582
  import { existsSync as existsSync25, mkdirSync as mkdirSync15, readdirSync as readdirSync12, rmSync as rmSync5 } from "fs";
32539
32583
  import { isAbsolute as isAbsolute4, join as join28, posix as posix2, resolve as resolve15, sep as sep4 } from "path";
32540
32584
  function parseVideoElements(html) {
@@ -32632,7 +32676,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
32632
32676
  if (format === "png") args.push("-compression_level", "6");
32633
32677
  args.push("-y", outputPattern);
32634
32678
  return new Promise((resolve43, reject) => {
32635
- const ffmpeg = spawn8("ffmpeg", args);
32679
+ const ffmpeg = spawn9("ffmpeg", args);
32636
32680
  let stderr = "";
32637
32681
  const onAbort = () => {
32638
32682
  ffmpeg.kill("SIGTERM");
@@ -33968,18 +34012,19 @@ function calculateOptimalWorkers(totalFrames, requested, config) {
33968
34012
  }
33969
34013
  return finalWorkers;
33970
34014
  }
33971
- function distributeFrames(totalFrames, workerCount, workDir) {
34015
+ function distributeFrames(totalFrames, workerCount, workDir, rangeStart = 0) {
33972
34016
  const tasks = [];
33973
34017
  const framesPerWorker = Math.ceil(totalFrames / workerCount);
33974
34018
  for (let i2 = 0; i2 < workerCount; i2++) {
33975
- const startFrame = i2 * framesPerWorker;
33976
- const endFrame = Math.min((i2 + 1) * framesPerWorker, totalFrames);
33977
- if (startFrame >= totalFrames) break;
34019
+ const startFrame = rangeStart + i2 * framesPerWorker;
34020
+ const endFrame = Math.min(rangeStart + (i2 + 1) * framesPerWorker, rangeStart + totalFrames);
34021
+ if (startFrame >= rangeStart + totalFrames) break;
33978
34022
  tasks.push({
33979
34023
  workerId: i2,
33980
34024
  startFrame,
33981
34025
  endFrame,
33982
- outputDir: join30(workDir, `worker-${i2}`)
34026
+ outputDir: join30(workDir, `worker-${i2}`),
34027
+ outputFrameOffset: rangeStart
33983
34028
  });
33984
34029
  }
33985
34030
  return tasks;
@@ -33999,16 +34044,18 @@ async function executeWorkerTask(task, serverUrl, captureOptions, createBeforeCa
33999
34044
  config
34000
34045
  );
34001
34046
  await initializeSession(session);
34047
+ const outputOffset = task.outputFrameOffset ?? 0;
34002
34048
  for (let i2 = task.startFrame; i2 < task.endFrame; i2++) {
34003
34049
  if (signal?.aborted) {
34004
34050
  throw new Error("Parallel worker cancelled");
34005
34051
  }
34006
34052
  const time = i2 * captureOptions.fps.den / captureOptions.fps.num;
34053
+ const fileFrameIdx = i2 - outputOffset;
34007
34054
  if (onFrameBuffer) {
34008
- const { buffer } = await captureFrameToBuffer(session, i2, time);
34055
+ const { buffer } = await captureFrameToBuffer(session, fileFrameIdx, time);
34009
34056
  await onFrameBuffer(i2, buffer);
34010
34057
  } else {
34011
- await captureFrame(session, i2, time);
34058
+ await captureFrame(session, fileFrameIdx, time);
34012
34059
  }
34013
34060
  framesCaptured++;
34014
34061
  if (onFrameCaptured) onFrameCaptured(task.workerId, i2);
@@ -39029,17 +39076,18 @@ async function runCaptureStage(input) {
39029
39076
  let { workerCount, probeSession } = input;
39030
39077
  let lastBrowserConsole = [];
39031
39078
  const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
39032
- if (frameRange !== void 0 && workerCount > 1) {
39033
- throw new Error(
39034
- `[captureStage] frameRange capture requires workerCount === 1 (received workerCount=${workerCount}). Distributed chunk workers fan out at the activity layer; reduce workerCount to 1 when passing frameRange.`
39035
- );
39036
- }
39037
39079
  if (frameRange !== void 0) {
39038
39080
  if (!Number.isFinite(frameRange.startFrame) || !Number.isFinite(frameRange.endFrame) || frameRange.startFrame < 0 || frameRange.endFrame <= frameRange.startFrame) {
39039
39081
  throw new Error(
39040
39082
  `[captureStage] invalid frameRange: ${JSON.stringify(frameRange)}. Expected non-negative startFrame strictly less than endFrame.`
39041
39083
  );
39042
39084
  }
39085
+ const rangeFrames = frameRange.endFrame - frameRange.startFrame;
39086
+ if (rangeFrames !== totalFrames) {
39087
+ throw new Error(
39088
+ `[captureStage] frameRange size (${rangeFrames}) must equal totalFrames (${totalFrames}). Received frameRange=${JSON.stringify(frameRange)}.`
39089
+ );
39090
+ }
39043
39091
  }
39044
39092
  if (workerCount > 1) {
39045
39093
  const attempts = await executeDiskCaptureWithAdaptiveRetry({
@@ -39053,6 +39101,7 @@ async function runCaptureStage(input) {
39053
39101
  captureOptions: buildCaptureOptions(),
39054
39102
  createBeforeCaptureHook: createRenderVideoFrameInjector,
39055
39103
  abortSignal,
39104
+ frameRangeStart: frameRange?.startFrame,
39056
39105
  onProgress: (progress) => {
39057
39106
  job.framesRendered = progress.capturedFrames;
39058
39107
  const frameProgress = progress.capturedFrames / progress.totalFrames;
@@ -40942,16 +40991,17 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
40942
40991
  }
40943
40992
  return ranges;
40944
40993
  }
40945
- function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt) {
40994
+ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0) {
40946
40995
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
40947
40996
  const batches = [];
40948
40997
  for (let i2 = 0; i2 < ranges.length; i2 += workersPerBatch) {
40949
40998
  const batchIndex = batches.length;
40950
40999
  const batch = ranges.slice(i2, i2 + workersPerBatch).map((range, workerId) => ({
40951
41000
  workerId,
40952
- startFrame: range.startFrame,
40953
- endFrame: range.endFrame,
40954
- outputDir: join49(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`)
41001
+ startFrame: rangeStart + range.startFrame,
41002
+ endFrame: rangeStart + range.endFrame,
41003
+ outputDir: join49(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
41004
+ outputFrameOffset: rangeStart
40955
41005
  }));
40956
41006
  batches.push(batch);
40957
41007
  }
@@ -40982,6 +41032,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
40982
41032
  let currentWorkers = options.initialWorkerCount;
40983
41033
  let missingRanges = null;
40984
41034
  let attempt = 0;
41035
+ const rangeStart = options.frameRangeStart ?? 0;
40985
41036
  while (true) {
40986
41037
  const frameCount = missingRanges ? countFrameRanges(missingRanges) : options.totalFrames;
40987
41038
  attempts.push({
@@ -40991,7 +41042,13 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
40991
41042
  reason: attempt === 0 ? "initial" : "retry"
40992
41043
  });
40993
41044
  const attemptWorkDir = join49(options.workDir, `capture-attempt-${attempt}`);
40994
- const batches = missingRanges ? buildMissingFrameRetryBatches(missingRanges, currentWorkers, attemptWorkDir, attempt) : [distributeFrames(options.totalFrames, currentWorkers, attemptWorkDir)];
41045
+ const batches = missingRanges ? buildMissingFrameRetryBatches(
41046
+ missingRanges,
41047
+ currentWorkers,
41048
+ attemptWorkDir,
41049
+ attempt,
41050
+ rangeStart
41051
+ ) : [distributeFrames(options.totalFrames, currentWorkers, attemptWorkDir, rangeStart)];
40995
41052
  try {
40996
41053
  for (const tasks of batches) {
40997
41054
  const capturedBeforeBatch = countCapturedFrames(
@@ -43423,6 +43480,14 @@ async function plan(projectDir, config, planDir) {
43423
43480
  height,
43424
43481
  format: config.format
43425
43482
  };
43483
+ try {
43484
+ rmSync11(workDir, { recursive: true, force: true });
43485
+ } catch (err) {
43486
+ log2.warn("[plan] failed to remove temp work dir", {
43487
+ workDir,
43488
+ error: err instanceof Error ? err.message : String(err)
43489
+ });
43490
+ }
43426
43491
  const freezeResult = await freezePlan({
43427
43492
  planDir,
43428
43493
  composition: compositionJson,
@@ -43436,14 +43501,6 @@ async function plan(projectDir, config, planDir) {
43436
43501
  hasAudio: audioResult.hasAudio
43437
43502
  });
43438
43503
  const planHash = freezeResult.planHash;
43439
- try {
43440
- rmSync11(workDir, { recursive: true, force: true });
43441
- } catch (err) {
43442
- log2.warn("[plan] failed to remove temp work dir", {
43443
- workDir,
43444
- error: err instanceof Error ? err.message : String(err)
43445
- });
43446
- }
43447
43504
  const sizeLimitBytes = config.planDirSizeLimitBytes ?? PLAN_DIR_SIZE_LIMIT_BYTES;
43448
43505
  const planDirBytes = measurePlanDirBytes(planDir);
43449
43506
  if (planDirBytes > sizeLimitBytes) {
@@ -43731,6 +43788,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
43731
43788
  session = await createCaptureSession(fileServer.url, framesDir, captureOptions, null, cfg);
43732
43789
  await assertSwiftShader(session.page, readWebGlVendorInfoFromCanvas);
43733
43790
  await initializeSession(session);
43791
+ const chunkWorkerCount = calculateOptimalWorkers(framesInChunk, void 0, cfg);
43734
43792
  await runCaptureStage({
43735
43793
  fileServer,
43736
43794
  workDir,
@@ -43740,11 +43798,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
43740
43798
  cfg,
43741
43799
  forceScreenshot: encoder.forceScreenshot,
43742
43800
  log: log2,
43743
- workerCount: 1,
43744
- // Pass the pre-warmed session through as `probeSession` so captureStage
43745
- // reuses it via `prepareCaptureSessionForReuse` instead of spinning up
43746
- // a fresh browser. The stage closes the session in its `finally`,
43747
- // so we MUST clear our own reference here to avoid a double-close.
43801
+ workerCount: chunkWorkerCount,
43802
+ // The parallel branch closes this session and spins up its own
43803
+ // worker sessions, wasting the ~3-5s of pre-warmed setup. Worth a
43804
+ // follow-up to skip pre-warmup when the resolved workerCount > 1.
43748
43805
  probeSession: session,
43749
43806
  needsAlpha: plan2.dimensions.format !== "mp4",
43750
43807
  captureAttempts: [],
@@ -43882,7 +43939,7 @@ var init_renderChunk = __esm({
43882
43939
  });
43883
43940
 
43884
43941
  // ../producer/src/services/render/audioPadTrim.ts
43885
- import { spawn as spawn9 } from "child_process";
43942
+ import { spawn as spawn10 } from "child_process";
43886
43943
  function buildPadTrimAudioArgs(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
43887
43944
  const delta = targetDurationSeconds - sourceDurationSeconds;
43888
43945
  const targetSec = formatSeconds(targetDurationSeconds);
@@ -44048,7 +44105,7 @@ async function defaultRunFfmpeg(args) {
44048
44105
  }
44049
44106
  function runFfprobeJson(args) {
44050
44107
  return new Promise((resolve43, reject) => {
44051
- const proc = spawn9("ffprobe", args);
44108
+ const proc = spawn10("ffprobe", args);
44052
44109
  let stdout2 = "";
44053
44110
  let stderr = "";
44054
44111
  proc.stdout.on("data", (data) => {
@@ -44406,7 +44463,10 @@ async function getThumbnailBrowser() {
44406
44463
  }
44407
44464
  } catch {
44408
44465
  }
44409
- const acquired = await acquireBrowser2(buildChromeArgs2({ width: 1920, height: 1080 }));
44466
+ const acquired = await acquireBrowser2(
44467
+ buildChromeArgs2({ width: 1920, height: 1080, captureMode: "screenshot" }),
44468
+ { forceScreenshot: true }
44469
+ );
44410
44470
  _thumbnailBrowser = acquired.browser;
44411
44471
  _thumbnailBrowser.on("disconnected", () => {
44412
44472
  _thumbnailBrowser = null;
@@ -44423,7 +44483,11 @@ async function getThumbnailBrowser() {
44423
44483
  process.once("SIGTERM", () => void onExit());
44424
44484
  process.once("SIGINT", () => void onExit());
44425
44485
  return _thumbnailBrowser;
44426
- } catch {
44486
+ } catch (err) {
44487
+ console.warn(
44488
+ "[Studio] Failed to launch thumbnail browser:",
44489
+ err instanceof Error ? err.message : err
44490
+ );
44427
44491
  _thumbnailBrowserInitializing = null;
44428
44492
  return null;
44429
44493
  }
@@ -44530,26 +44594,37 @@ function createStudioServer(options) {
44530
44594
  },
44531
44595
  async generateThumbnail(opts) {
44532
44596
  const browser = await getThumbnailBrowser();
44533
- if (!browser) return null;
44597
+ if (!browser) {
44598
+ console.warn("[Studio] Thumbnail: no browser available \u2014 Chrome may not be installed");
44599
+ return null;
44600
+ }
44534
44601
  let page = null;
44535
44602
  try {
44536
44603
  page = await browser.newPage();
44537
44604
  await page.setViewport({ width: opts.width || 1920, height: opts.height || 1080 });
44538
44605
  await page.goto(opts.previewUrl, { waitUntil: "domcontentloaded", timeout: 1e4 });
44539
- await page.waitForFunction(() => !!window.__timelines || !!window.__playerReady, {
44540
- timeout: 5e3
44541
- }).catch(() => {
44606
+ await page.waitForFunction(
44607
+ () => {
44608
+ const w3 = window;
44609
+ return !!(w3.__timelines && Object.keys(w3.__timelines).length > 0);
44610
+ },
44611
+ { timeout: 5e3 }
44612
+ ).catch(() => {
44542
44613
  });
44543
44614
  await page.evaluate((t2) => {
44544
- const win = window;
44545
- if (win.__player?.seek) win.__player.seek(t2);
44546
- else if (win.__timeline?.seek) {
44547
- win.__timeline.pause();
44548
- win.__timeline.seek(t2);
44615
+ const w3 = window;
44616
+ if (typeof w3.__player?.seek === "function") {
44617
+ w3.__player.seek(t2);
44618
+ } else if (w3.__timelines) {
44619
+ for (const tl of Object.values(w3.__timelines)) {
44620
+ tl?.pause?.(t2);
44621
+ }
44622
+ w3.gsap?.ticker?.tick?.();
44549
44623
  }
44550
44624
  }, opts.seekTime);
44551
44625
  const manifestContent = readStudioManualEditManifestContent(opts.project.dir);
44552
44626
  await applyStudioManualEditsToThumbnailPage(page, manifestContent, opts.compPath);
44627
+ await page.evaluate(() => document.fonts?.ready);
44553
44628
  await new Promise((r2) => setTimeout(r2, 200));
44554
44629
  await reapplyStudioManualEditsToThumbnailPage(page);
44555
44630
  let clip;
@@ -44567,7 +44642,11 @@ function createStudioServer(options) {
44567
44642
  }
44568
44643
  );
44569
44644
  return screenshot;
44570
- } catch {
44645
+ } catch (err) {
44646
+ console.warn(
44647
+ "[Studio] Thumbnail generation failed:",
44648
+ err instanceof Error ? err.message : err
44649
+ );
44571
44650
  return null;
44572
44651
  } finally {
44573
44652
  await page?.close().catch(() => {
@@ -44714,7 +44793,7 @@ __export(preview_exports, {
44714
44793
  default: () => preview_default,
44715
44794
  examples: () => examples
44716
44795
  });
44717
- import { spawn as spawn10 } from "child_process";
44796
+ import { spawn as spawn11 } from "child_process";
44718
44797
  import { existsSync as existsSync48, lstatSync as lstatSync2, symlinkSync as symlinkSync2, unlinkSync as unlinkSync5, readlinkSync, mkdirSync as mkdirSync30 } from "fs";
44719
44798
  import { resolve as resolve25, dirname as dirname18, basename as basename6, join as join58 } from "path";
44720
44799
  import { fileURLToPath as fileURLToPath6 } from "url";
@@ -44749,7 +44828,7 @@ async function runDevMode(dir, options) {
44749
44828
  const s2 = ft();
44750
44829
  s2.start("Starting studio...");
44751
44830
  const studioPkgDir = join58(repoRoot, "packages", "studio");
44752
- const child = spawn10("bun", ["run", "dev"], {
44831
+ const child = spawn11("bun", ["run", "dev"], {
44753
44832
  cwd: studioPkgDir,
44754
44833
  stdio: ["ignore", "pipe", "pipe"]
44755
44834
  });
@@ -44768,7 +44847,9 @@ async function runDevMode(dir, options) {
44768
44847
  console.log();
44769
44848
  if (!options?.noOpen) {
44770
44849
  const urlToOpen = `${frontendUrl}#project/${pName}`;
44771
- import("open").then((mod) => mod.default(urlToOpen)).catch(() => {
44850
+ openBrowser(urlToOpen, {
44851
+ browserPath: options?.browserPath,
44852
+ userDataDir: options?.userDataDir
44772
44853
  });
44773
44854
  }
44774
44855
  child.stdout?.removeListener("data", handleOutput);
@@ -44824,7 +44905,7 @@ async function runLocalStudioMode(dir, options) {
44824
44905
  ge(c2.bold("hyperframes preview") + c2.dim(" (local studio)"));
44825
44906
  const s2 = ft();
44826
44907
  s2.start("Starting studio...");
44827
- const child = spawn10("npx", ["vite"], {
44908
+ const child = spawn11("npx", ["vite"], {
44828
44909
  cwd: studioPkgPath,
44829
44910
  stdio: ["ignore", "pipe", "pipe"]
44830
44911
  });
@@ -44843,7 +44924,9 @@ async function runLocalStudioMode(dir, options) {
44843
44924
  console.log(` ${c2.dim("Press Ctrl+C to stop")}`);
44844
44925
  console.log();
44845
44926
  if (!options?.noOpen) {
44846
- import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {
44927
+ openBrowser(`${url}#project/${pName}`, {
44928
+ browserPath: options?.browserPath,
44929
+ userDataDir: options?.userDataDir
44847
44930
  });
44848
44931
  }
44849
44932
  }
@@ -44910,7 +44993,9 @@ async function runEmbeddedMode(dir, startPort, options) {
44910
44993
  );
44911
44994
  console.log();
44912
44995
  if (!options?.noOpen) {
44913
- import("open").then((mod) => mod.default(`${url2}#project/${pName}`)).catch(() => {
44996
+ openBrowser(`${url2}#project/${pName}`, {
44997
+ browserPath: options?.browserPath,
44998
+ userDataDir: options?.userDataDir
44914
44999
  });
44915
45000
  }
44916
45001
  return;
@@ -44931,7 +45016,9 @@ async function runEmbeddedMode(dir, startPort, options) {
44931
45016
  console.log(` ${c2.dim("Press Ctrl+C to stop")}`);
44932
45017
  console.log();
44933
45018
  if (!options?.noOpen) {
44934
- import("open").then((mod) => mod.default(`${url}#project/${pName}`)).catch(() => {
45019
+ openBrowser(`${url}#project/${pName}`, {
45020
+ browserPath: options?.browserPath,
45021
+ userDataDir: options?.userDataDir
44935
45022
  });
44936
45023
  }
44937
45024
  let rl;
@@ -44967,6 +45054,7 @@ var init_preview2 = __esm({
44967
45054
  init_dist5();
44968
45055
  init_colors();
44969
45056
  init_env();
45057
+ init_openBrowser();
44970
45058
  init_lintProject();
44971
45059
  init_lintFormat();
44972
45060
  init_portUtils();
@@ -44976,6 +45064,7 @@ var init_preview2 = __esm({
44976
45064
  ["Use a custom port", "hyperframes preview --port 8080"],
44977
45065
  ["Force a new server even if one is already running", "hyperframes preview --force-new"],
44978
45066
  ["Start without opening the browser", "hyperframes preview --no-open"],
45067
+ ["Open with a specific browser", "hyperframes preview --browser-path /usr/bin/chromium"],
44979
45068
  ["List all active preview servers", "hyperframes preview --list"],
44980
45069
  ["Kill all active preview servers", "hyperframes preview --kill-all"]
44981
45070
  ];
@@ -45003,6 +45092,14 @@ var init_preview2 = __esm({
45003
45092
  type: "boolean",
45004
45093
  default: true,
45005
45094
  description: "Open browser automatically"
45095
+ },
45096
+ "browser-path": {
45097
+ type: "string",
45098
+ description: "Path to the browser executable to open"
45099
+ },
45100
+ "user-data-dir": {
45101
+ type: "string",
45102
+ description: "Chromium-compatible user data directory (requires --browser-path)"
45006
45103
  }
45007
45104
  },
45008
45105
  async run({ args }) {
@@ -45053,15 +45150,28 @@ var init_preview2 = __esm({
45053
45150
  console.log();
45054
45151
  }
45055
45152
  }
45153
+ if (args["user-data-dir"] && !args["browser-path"]) {
45154
+ R2.error("--user-data-dir requires --browser-path");
45155
+ process.exitCode = 1;
45156
+ return;
45157
+ }
45056
45158
  const noOpen = !args.open;
45159
+ const browserPath = args["browser-path"];
45160
+ const userDataDir = args["user-data-dir"];
45057
45161
  if (isDevMode()) {
45058
- return runDevMode(dir, { projectName, noOpen });
45162
+ return runDevMode(dir, { projectName, noOpen, browserPath, userDataDir });
45059
45163
  }
45060
45164
  if (hasLocalStudio(dir)) {
45061
- return runLocalStudioMode(dir, { projectName, noOpen });
45165
+ return runLocalStudioMode(dir, { projectName, noOpen, browserPath, userDataDir });
45062
45166
  }
45063
45167
  const forceNew = !!args["force-new"];
45064
- return runEmbeddedMode(dir, startPort, { projectName, forceNew, noOpen });
45168
+ return runEmbeddedMode(dir, startPort, {
45169
+ projectName,
45170
+ forceNew,
45171
+ noOpen,
45172
+ browserPath,
45173
+ userDataDir
45174
+ });
45065
45175
  }
45066
45176
  });
45067
45177
  }
@@ -45086,7 +45196,7 @@ import {
45086
45196
  } from "fs";
45087
45197
  import { resolve as resolve26, basename as basename7, join as join59, dirname as dirname19 } from "path";
45088
45198
  import { fileURLToPath as fileURLToPath7 } from "url";
45089
- import { execFileSync as execFileSync5, spawn as spawn11 } from "child_process";
45199
+ import { execFileSync as execFileSync5, spawn as spawn12 } from "child_process";
45090
45200
  function probeVideo(filePath) {
45091
45201
  try {
45092
45202
  const raw = execFileSync5(
@@ -45128,7 +45238,7 @@ function isWebCompatible(codec) {
45128
45238
  }
45129
45239
  function transcodeToMp4(inputPath, outputPath) {
45130
45240
  return new Promise((resolvePromise) => {
45131
- const child = spawn11(
45241
+ const child = spawn12(
45132
45242
  "ffmpeg",
45133
45243
  [
45134
45244
  "-i",
@@ -46439,11 +46549,13 @@ var init_play = __esm({
46439
46549
  init_dist5();
46440
46550
  init_colors();
46441
46551
  init_project();
46552
+ init_openBrowser();
46442
46553
  examples5 = [
46443
46554
  ["Play the current project", "hyperframes play"],
46444
46555
  ["Play a specific project directory", "hyperframes play ./my-video"],
46445
46556
  ["Use a custom port", "hyperframes play --port 8080"],
46446
- ["Start without opening the browser", "hyperframes play --no-open"]
46557
+ ["Start without opening the browser", "hyperframes play --no-open"],
46558
+ ["Open with a specific browser", "hyperframes play --browser-path /usr/bin/chromium"]
46447
46559
  ];
46448
46560
  play_default = defineCommand({
46449
46561
  meta: { name: "play", description: "Play a composition in a lightweight browser player" },
@@ -46454,11 +46566,24 @@ var init_play = __esm({
46454
46566
  type: "boolean",
46455
46567
  default: true,
46456
46568
  description: "Open browser automatically"
46569
+ },
46570
+ "browser-path": {
46571
+ type: "string",
46572
+ description: "Path to the browser executable to open"
46573
+ },
46574
+ "user-data-dir": {
46575
+ type: "string",
46576
+ description: "Chromium-compatible user data directory (requires --browser-path)"
46457
46577
  }
46458
46578
  },
46459
46579
  async run({ args }) {
46460
46580
  const project = resolveProject(args.dir);
46461
46581
  const startPort = parseInt(args.port ?? "3003", 10);
46582
+ if (args["user-data-dir"] && !args["browser-path"]) {
46583
+ R2.error("--user-data-dir requires --browser-path");
46584
+ process.exitCode = 1;
46585
+ return;
46586
+ }
46462
46587
  const runtimePath = resolveRuntimePath2();
46463
46588
  if (!runtimePath) {
46464
46589
  R2.error("HyperFrames runtime not found. Run `bun run build` first.");
@@ -46559,7 +46684,9 @@ var init_play = __esm({
46559
46684
  console.log(` ${c2.dim("Press Ctrl+C to stop")}`);
46560
46685
  console.log();
46561
46686
  if (args.open) {
46562
- import("open").then((mod) => mod.default(url)).catch(() => {
46687
+ void openBrowser(url, {
46688
+ browserPath: args["browser-path"],
46689
+ userDataDir: args["user-data-dir"]
46563
46690
  });
46564
46691
  }
46565
46692
  return new Promise(() => {
@@ -47046,7 +47173,7 @@ __export(render_exports, {
47046
47173
  import { mkdirSync as mkdirSync32, readdirSync as readdirSync23, readFileSync as readFileSync36, statSync as statSync20, writeFileSync as writeFileSync26, rmSync as rmSync14 } from "fs";
47047
47174
  import { cpus as cpus4, freemem as freemem3, tmpdir as tmpdir4 } from "os";
47048
47175
  import { resolve as resolve32, dirname as dirname21, join as join62, basename as basename11 } from "path";
47049
- import { execFileSync as execFileSync6, spawn as spawn12 } from "child_process";
47176
+ import { execFileSync as execFileSync6, spawn as spawn13 } from "child_process";
47050
47177
  function formatFpsParseError(input, reason) {
47051
47178
  switch (reason) {
47052
47179
  case "empty":
@@ -47266,7 +47393,7 @@ async function renderDocker(projectDir, outputPath, options) {
47266
47393
  }
47267
47394
  try {
47268
47395
  await new Promise((resolvePromise, reject) => {
47269
- const child = spawn12("docker", dockerArgs, {
47396
+ const child = spawn13("docker", dockerArgs, {
47270
47397
  // When quiet, still show stderr so container errors surface
47271
47398
  stdio: options.quiet ? ["pipe", "pipe", "inherit"] : "inherit"
47272
47399
  });
@@ -49471,7 +49598,7 @@ __export(pipeline_exports, {
49471
49598
  resolveRenderTargets: () => resolveRenderTargets,
49472
49599
  waitForExit: () => waitForExit
49473
49600
  });
49474
- import { spawn as spawn13 } from "child_process";
49601
+ import { spawn as spawn14 } from "child_process";
49475
49602
  import { extname as extname10 } from "path";
49476
49603
  function inferOutputFormat(outputPath) {
49477
49604
  const ext = extname10(outputPath).toLowerCase();
@@ -49648,7 +49775,7 @@ async function render2(options) {
49648
49775
  }
49649
49776
  }
49650
49777
  function spawnFfmpeg(args, label2, stdio) {
49651
- const proc = spawn13("ffmpeg", args, { stdio });
49778
+ const proc = spawn14("ffmpeg", args, { stdio });
49652
49779
  let stderrBuf = "";
49653
49780
  proc.stderr?.on("data", (d2) => {
49654
49781
  stderrBuf += d2.toString();
@@ -51486,7 +51613,7 @@ __export(snapshot_exports, {
51486
51613
  default: () => snapshot_default,
51487
51614
  examples: () => examples22
51488
51615
  });
51489
- import { spawn as spawn14 } from "child_process";
51616
+ import { spawn as spawn15 } from "child_process";
51490
51617
  import { existsSync as existsSync66, mkdtempSync as mkdtempSync3, readFileSync as readFileSync44, mkdirSync as mkdirSync36, rmSync as rmSync15 } from "fs";
51491
51618
  import { tmpdir as tmpdir5 } from "os";
51492
51619
  import { resolve as resolve40, join as join72, relative as relative9, isAbsolute as isAbsolute9 } from "path";
@@ -51512,7 +51639,7 @@ async function extractVideoFrameToBuffer(videoPath, timeSeconds, useVp9AlphaDeco
51512
51639
  "-y",
51513
51640
  outPath
51514
51641
  );
51515
- const ff = spawn14("ffmpeg", args);
51642
+ const ff = spawn15("ffmpeg", args);
51516
51643
  let stderr = "";
51517
51644
  let timedOut = false;
51518
51645
  const timer = setTimeout(() => {
@@ -51629,19 +51756,14 @@ async function captureSnapshots(projectDir, opts) {
51629
51756
  for (let i2 = 0; i2 < positions.length; i2++) {
51630
51757
  const time = positions[i2];
51631
51758
  await page.evaluate((t2) => {
51632
- const win = window;
51633
- if (win.__player?.seek) {
51634
- win.__player.seek(t2);
51635
- } else {
51636
- const tls = win.__timelines;
51637
- if (tls) {
51638
- for (const key2 in tls) {
51639
- if (tls[key2]?.seek) {
51640
- tls[key2].pause();
51641
- tls[key2].seek(t2);
51642
- }
51643
- }
51759
+ const w3 = window;
51760
+ if (typeof w3.__player?.seek === "function") {
51761
+ w3.__player.seek(t2);
51762
+ } else if (w3.__timelines) {
51763
+ for (const tl of Object.values(w3.__timelines)) {
51764
+ tl?.pause?.(t2);
51644
51765
  }
51766
+ w3.gsap?.ticker?.tick?.();
51645
51767
  }
51646
51768
  }, time);
51647
51769
  await page.evaluate(
@@ -94925,7 +95047,7 @@ __export(autoUpdate_exports, {
94925
95047
  reportCompletedUpdate: () => reportCompletedUpdate,
94926
95048
  scheduleBackgroundInstall: () => scheduleBackgroundInstall
94927
95049
  });
94928
- import { spawn as spawn15 } from "child_process";
95050
+ import { spawn as spawn16 } from "child_process";
94929
95051
  import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync41, openSync as openSync2 } from "fs";
94930
95052
  import { homedir as homedir12 } from "os";
94931
95053
  import { join as join80 } from "path";
@@ -94976,7 +95098,7 @@ function launchDetachedInstall(installCommand, version) {
94976
95098
  });
94977
95099
  `;
94978
95100
  const out = openSync2(LOG_FILE, "a", 384);
94979
- const child = spawn15(process.execPath, ["-e", nodeScript], {
95101
+ const child = spawn16(process.execPath, ["-e", nodeScript], {
94980
95102
  detached: true,
94981
95103
  stdio: ["ignore", out, out],
94982
95104
  windowsHide: true,