@hyperframes/studio-server 0.8.186 → 0.8.189

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-25IT6FMR.js";
4
+ } from "./chunk-N2HH4SGD.js";
5
5
  import {
6
6
  createMediaCodecProbeCache,
7
7
  proxyVariantFor,
8
8
  recordProxyPrewarm,
9
9
  scanProjectMediaCodecMap,
10
10
  shouldPrewarmProxy
11
- } from "./chunk-OVOXGASQ.js";
11
+ } from "./chunk-XMNHDWQI.js";
12
12
 
13
13
  // src/helpers/mediaProxyPreview.ts
14
14
  import { resolve } from "path";
@@ -24,7 +24,7 @@ function proxyEtagSalt(raw, box) {
24
24
  if (raw === void 0) return "";
25
25
  return `:proxy:${raw}:${PROXY_PARAMS_VERSION}${box ? `:${formatPreviewProxyBox(box)}` : ""}`;
26
26
  }
27
- async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeCache, prewarm = true, signal) {
27
+ async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeCache, prewarm = true, signal, box) {
28
28
  let map;
29
29
  try {
30
30
  map = await scanProjectMediaCodecMap(
@@ -43,7 +43,7 @@ async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeC
43
43
  projectDir,
44
44
  resolve(projectDir, rootRelativePathname.replace(/^\/+/, "")),
45
45
  proxyVariantFor(facts),
46
- void 0,
46
+ box,
47
47
  { signal }
48
48
  ).catch(() => {
49
49
  });
@@ -2,7 +2,7 @@ import {
2
2
  PROXY_VARIANT_CONFIG,
3
3
  probeFirstFrameColour,
4
4
  probeMediaMetadata
5
- } from "./chunk-OVOXGASQ.js";
5
+ } from "./chunk-XMNHDWQI.js";
6
6
  import {
7
7
  mkdirWithinProject,
8
8
  realProjectRoot,
@@ -12,8 +12,18 @@ import {
12
12
  // src/helpers/proxyTranscoder.ts
13
13
  import { spawn } from "child_process";
14
14
  import { createHash, randomUUID } from "crypto";
15
- import { existsSync as existsSync2, renameSync, statSync as statSync2, unlinkSync as unlinkSync2, utimesSync } from "fs";
16
- import { basename, dirname, isAbsolute, join as join2, relative, resolve, sep } from "path";
15
+ import {
16
+ existsSync as existsSync2,
17
+ mkdtempSync,
18
+ renameSync,
19
+ rmSync,
20
+ statSync as statSync2,
21
+ unlinkSync as unlinkSync2,
22
+ utimesSync,
23
+ writeFileSync
24
+ } from "fs";
25
+ import { tmpdir } from "os";
26
+ import { basename, dirname, extname as extname2, isAbsolute, join as join2, relative, resolve, sep } from "path";
17
27
  import {
18
28
  formatPreviewProxyBox,
19
29
  hdrToSdrToneMapFilter
@@ -31,7 +41,9 @@ var PROXY_EXTENSIONS = new Set(
31
41
  Object.values(PROXY_VARIANT_CONFIG).map(({ extension }) => extension)
32
42
  );
33
43
  var TRANSCODER_PROXY_NAME = /^[0-9a-f]{64}\.\w+$/;
34
- var TRANSCODER_TEMP_NAME = /^\.tmp-[0-9a-f-]{36}-[0-9a-f]{64}\.\w+$/;
44
+ var TRANSCODER_TEMP_NAME = /^\.tmp-[0-9a-f-]{36}-(?:\.part-[0-9a-f]{64}-\d+|[0-9a-f]{64})\.\w+$/;
45
+ var TRANSCODER_PIECE_NAME = /^\.part-([0-9a-f]{64})-\d+(\.\w+)$/;
46
+ var STALE_PIECE_MS = 60 * 60 * 1e3;
35
47
  var lastSweepAt = /* @__PURE__ */ new Map();
36
48
  function positiveEnvNumber(name, fallback) {
37
49
  const parsed = Number(process.env[name]);
@@ -54,6 +66,15 @@ function shouldSkipSweep(cacheDir, now, minSweepIntervalMs) {
54
66
  lastSweepAt.set(cacheDir, now);
55
67
  return false;
56
68
  }
69
+ function isLeftoverName(name) {
70
+ return TRANSCODER_TEMP_NAME.test(name) || TRANSCODER_PIECE_NAME.test(name);
71
+ }
72
+ function leftoverIsDead(cacheDir, name, modifiedAt, protectedPaths, now, staleTempMs) {
73
+ const piece = TRANSCODER_PIECE_NAME.exec(name);
74
+ if (!piece) return now - modifiedAt >= staleTempMs;
75
+ const copyIsRunning = protectedPaths.has(join(cacheDir, `${piece[1]}${piece[2]}`));
76
+ return !copyIsRunning && now - modifiedAt >= STALE_PIECE_MS;
77
+ }
57
78
  function readCacheInventory(cacheDir, protectedPaths, now, staleTempMs) {
58
79
  const entries = [];
59
80
  const staleTemps = [];
@@ -67,8 +88,10 @@ function readCacheInventory(cacheDir, protectedPaths, now, staleTempMs) {
67
88
  modifiedAt: stat.mtimeMs,
68
89
  protected: protectedPaths.has(path)
69
90
  };
70
- if (TRANSCODER_TEMP_NAME.test(dirent.name)) {
71
- if (now - stat.mtimeMs >= staleTempMs) staleTemps.push(entry);
91
+ if (isLeftoverName(dirent.name)) {
92
+ if (leftoverIsDead(cacheDir, dirent.name, stat.mtimeMs, protectedPaths, now, staleTempMs)) {
93
+ staleTemps.push(entry);
94
+ }
72
95
  } else if (TRANSCODER_PROXY_NAME.test(dirent.name) && PROXY_EXTENSIONS.has(extname(dirent.name))) {
73
96
  entries.push(entry);
74
97
  }
@@ -133,11 +156,15 @@ var MAX_CONCURRENT_TRANSCODES = boundedEnvInteger("HYPERFRAMES_PROXY_MAX_CONCURR
133
156
  var MAX_QUEUED_TRANSCODES = boundedEnvInteger("HYPERFRAMES_PROXY_MAX_QUEUE", 8, 0, 256);
134
157
  var STDERR_TAIL_MAX_CHARS = 4e3;
135
158
  var TRANSCODE_TIMEOUT_MS = 15 * 60 * 1e3;
159
+ var PROXY_CHUNK_SECONDS = 60;
160
+ var CHUNK_PREFIX = ".part-";
161
+ var CONCAT_TIMEOUT_MS = 2 * 60 * 1e3;
136
162
  var FAILURE_CACHE_TTL_MS = 60 * 1e3;
137
163
  var PROXY_PENDING_RETRY_AFTER_SECONDS = 2;
138
164
  var ENVIRONMENT_FAILURE_TTL_MS = 5 * PROXY_PENDING_RETRY_AFTER_SECONDS * 1e3;
139
165
  var MAX_FAILURE_CACHE_ENTRIES = 128;
140
166
  var DEFAULT_PROXY_WAIT_TIMEOUT_MS = 2 * 60 * 1e3;
167
+ var UNTOUCHED_TEMP_IS_DEAD_MS = 10 * 60 * 1e3;
141
168
  var ProxyTranscodeError = class extends Error {
142
169
  exitCode;
143
170
  stderrTail;
@@ -305,9 +332,9 @@ var inFlight = /* @__PURE__ */ new Map();
305
332
  function forget(job) {
306
333
  if (inFlight.get(job.cachePath)?.job === job) inFlight.delete(job.cachePath);
307
334
  }
308
- function maintainProxyCache(cacheDir) {
335
+ function maintainProxyCache(cacheDir, staleTempMs) {
309
336
  try {
310
- cleanupProxyCache(cacheDir, { protectedPaths: new Set(inFlight.keys()) });
337
+ cleanupProxyCache(cacheDir, { protectedPaths: new Set(inFlight.keys()), staleTempMs });
311
338
  } catch (error) {
312
339
  console.warn(
313
340
  `[media-proxy] cache cleanup failed: ${error instanceof Error ? error.message : String(error)}`
@@ -372,83 +399,173 @@ function stderrReason(stderrTail) {
372
399
  const line = errorLines.at(-1) ?? lines.at(-1);
373
400
  return line ? `: ${line.replace(/[.!]+$/, "")}` : "";
374
401
  }
375
- async function runFfmpeg(sourcePath, outputPath, variant, box, signal) {
376
- const metadata = await probeMediaMetadata(sourcePath);
377
- const ffmpegPath = findFfBinary("ffmpeg", { configuredMustExist: true });
378
- if (!ffmpegPath) {
379
- throw new FfmpegUnavailableError();
402
+ function chunkRanges(durationSeconds) {
403
+ if (!durationSeconds || durationSeconds <= PROXY_CHUNK_SECONDS * 1.5) return null;
404
+ const count = Math.floor(durationSeconds / PROXY_CHUNK_SECONDS);
405
+ return Array.from({ length: count }, (_, index) => ({
406
+ startSeconds: index * PROXY_CHUNK_SECONDS,
407
+ lengthSeconds: index === count - 1 ? durationSeconds - index * PROXY_CHUNK_SECONDS : PROXY_CHUNK_SECONDS
408
+ }));
409
+ }
410
+ function ffmpegBudgetError(chunksKept, chunkCount) {
411
+ return new ProxyTranscodeError(
412
+ `ffmpeg ran out of its ${TRANSCODE_TIMEOUT_MS}ms budget with ${chunksKept}/${chunkCount} pieces done; the finished pieces are kept and the next attempt continues from them`,
413
+ null,
414
+ ""
415
+ );
416
+ }
417
+ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat) {
418
+ const extension = extname2(tempPath);
419
+ const deadline = Date.now() + TRANSCODE_TIMEOUT_MS;
420
+ const pieces = ranges.map((_, index) => `${chunkPrefix}${index}${extension}`);
421
+ for (const [index, range] of ranges.entries()) {
422
+ const piece = pieces[index];
423
+ if (existsSync2(piece)) continue;
424
+ const timeoutMs = deadline - Date.now();
425
+ if (timeoutMs <= 0) throw ffmpegBudgetError(index, ranges.length);
426
+ const pieceTemp = join2(dirname(piece), `.tmp-${randomUUID()}-${basename(piece)}`);
427
+ try {
428
+ await encode(pieceTemp, timeoutMs, range);
429
+ renameSync(pieceTemp, piece);
430
+ } finally {
431
+ if (existsSync2(pieceTemp)) unlinkSync2(pieceTemp);
432
+ }
380
433
  }
381
- const keepsAlpha = variant === "vp8";
434
+ const listDir = mkdtempSync(join2(tmpdir(), "hf-proxy-join-"));
435
+ try {
436
+ const listPath = join2(listDir, "pieces.txt");
437
+ writeFileSync(
438
+ listPath,
439
+ pieces.map((piece) => `file '${piece.replace(/\\/g, "/").replace(/'/g, "'\\''")}'
440
+ `).join("")
441
+ );
442
+ await concat(listPath, tempPath);
443
+ } finally {
444
+ rmSync(listDir, { recursive: true, force: true });
445
+ }
446
+ for (const piece of pieces) if (existsSync2(piece)) unlinkSync2(piece);
447
+ }
448
+ var H264_ARGS = [
449
+ "-c:v",
450
+ "libx264",
451
+ "-profile:v",
452
+ "high",
453
+ "-pix_fmt",
454
+ "yuv420p",
455
+ "-colorspace",
456
+ "bt709",
457
+ "-color_primaries",
458
+ "bt709",
459
+ "-color_trc",
460
+ "bt709",
461
+ "-crf",
462
+ "18",
463
+ "-preset",
464
+ "veryfast",
465
+ "-c:a",
466
+ "aac",
467
+ "-movflags",
468
+ "+faststart"
469
+ ];
470
+ var VP8_ARGS = [
471
+ "-c:v",
472
+ "libvpx",
473
+ "-b:v",
474
+ "0",
475
+ "-crf",
476
+ "23",
477
+ "-deadline",
478
+ "good",
479
+ "-pix_fmt",
480
+ "yuva420p",
481
+ "-colorspace",
482
+ "bt709",
483
+ "-color_primaries",
484
+ "bt709",
485
+ "-color_trc",
486
+ "bt709",
487
+ "-cpu-used",
488
+ "4",
489
+ "-auto-alt-ref",
490
+ "0",
491
+ "-metadata:s:v:0",
492
+ "alpha_mode=1",
493
+ "-ac",
494
+ "2",
495
+ "-c:a",
496
+ "libopus"
497
+ ];
498
+ async function buildVideoFilter(ffmpegPath, sourcePath, metadata, keepsAlpha, box) {
382
499
  const { hdrTransfer } = metadata.color;
383
500
  const toneMap = (hdrTransfer === "pq" || hdrTransfer === "hlg") && !keepsAlpha;
384
501
  if (toneMap) await ensureHdrFilters(ffmpegPath);
385
502
  const firstFrame = toneMap ? await probeFirstFrameColour(sourcePath) : {};
386
503
  const evenScale = proxyScaleFilter(box);
387
504
  const pixelFormat = keepsAlpha ? "yuva420p" : "yuv420p";
388
- const videoFilter = toneMap ? [
505
+ return toneMap ? [
389
506
  hdrToSdrToneMapFilter(metadata.color, firstFrame),
390
507
  `${evenScale}:out_color_matrix=bt709:out_range=tv`,
391
508
  `format=${pixelFormat}`
392
509
  ].join(",") : [evenScale, `format=${pixelFormat}`].join(",");
510
+ }
511
+ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, signal) {
512
+ const metadata = await probeMediaMetadata(sourcePath);
513
+ const ffmpegPath = findFfBinary("ffmpeg", { configuredMustExist: true });
514
+ if (!ffmpegPath) {
515
+ throw new FfmpegUnavailableError();
516
+ }
517
+ const keepsAlpha = variant === "vp8";
518
+ const videoFilter = await buildVideoFilter(ffmpegPath, sourcePath, metadata, keepsAlpha, box);
519
+ const alphaDecoder = keepsAlpha ? alphaDecoderFor(metadata.color.codecName) : null;
520
+ const decoderArgs = alphaDecoder ? ["-c:v", alphaDecoder] : [];
521
+ const encode = (target, timeoutMs, range) => spawnFfmpeg(
522
+ ffmpegPath,
523
+ [
524
+ "-y",
525
+ ...range ? ["-ss", String(range.startSeconds)] : [],
526
+ ...decoderArgs,
527
+ "-i",
528
+ sourcePath,
529
+ ...range ? ["-t", String(range.lengthSeconds)] : [],
530
+ "-vf",
531
+ videoFilter,
532
+ ...variant === "vp8" ? VP8_ARGS : H264_ARGS,
533
+ target
534
+ ],
535
+ timeoutMs,
536
+ signal
537
+ );
538
+ const ranges = chunkRanges(metadata.durationSeconds);
539
+ if (!ranges) return encode(outputPath, TRANSCODE_TIMEOUT_MS);
540
+ await encodeInChunks(
541
+ ranges,
542
+ chunkPrefix,
543
+ outputPath,
544
+ encode,
545
+ (listPath, target) => spawnFfmpeg(ffmpegPath, concatArgs(listPath, target, variant), CONCAT_TIMEOUT_MS, signal)
546
+ );
547
+ }
548
+ function concatArgs(listPath, target, variant) {
549
+ return [
550
+ "-y",
551
+ "-f",
552
+ "concat",
553
+ "-safe",
554
+ "0",
555
+ "-i",
556
+ listPath,
557
+ "-c",
558
+ "copy",
559
+ ...variant === "h264" ? ["-movflags", "+faststart"] : [],
560
+ target
561
+ ];
562
+ }
563
+ function spawnFfmpeg(ffmpegPath, args, timeoutMs, signal) {
393
564
  return new Promise((resolvePromise, reject) => {
394
- const alphaDecoder = keepsAlpha ? alphaDecoderFor(metadata.color.codecName) : null;
395
- const decoderArgs = alphaDecoder ? ["-c:v", alphaDecoder] : [];
396
- const commonArgs = ["-y", ...decoderArgs, "-i", sourcePath, "-vf", videoFilter];
397
- const h264Args = [
398
- "-c:v",
399
- "libx264",
400
- "-profile:v",
401
- "high",
402
- "-pix_fmt",
403
- "yuv420p",
404
- "-colorspace",
405
- "bt709",
406
- "-color_primaries",
407
- "bt709",
408
- "-color_trc",
409
- "bt709",
410
- "-crf",
411
- "18",
412
- "-preset",
413
- "veryfast",
414
- "-c:a",
415
- "aac",
416
- "-movflags",
417
- "+faststart"
418
- ];
419
- const vp8Args = [
420
- "-c:v",
421
- "libvpx",
422
- "-b:v",
423
- "0",
424
- "-crf",
425
- "23",
426
- "-deadline",
427
- "good",
428
- "-pix_fmt",
429
- "yuva420p",
430
- "-colorspace",
431
- "bt709",
432
- "-color_primaries",
433
- "bt709",
434
- "-color_trc",
435
- "bt709",
436
- "-cpu-used",
437
- "4",
438
- "-auto-alt-ref",
439
- "0",
440
- "-metadata:s:v:0",
441
- "alpha_mode=1",
442
- "-ac",
443
- "2",
444
- "-c:a",
445
- "libopus"
446
- ];
447
- const args = [...commonArgs, ...variant === "vp8" ? vp8Args : h264Args, outputPath];
448
565
  signal.throwIfAborted();
449
566
  const proc = spawn(ffmpegPath, args, {
450
567
  stdio: ["ignore", "ignore", "pipe"],
451
- timeout: TRANSCODE_TIMEOUT_MS,
568
+ timeout: timeoutMs,
452
569
  killSignal: "SIGKILL",
453
570
  signal,
454
571
  windowsHide: true
@@ -467,7 +584,7 @@ async function runFfmpeg(sourcePath, outputPath, variant, box, signal) {
467
584
  } else if (signal2) {
468
585
  reject(
469
586
  new ProxyTranscodeError(
470
- `ffmpeg killed by ${signal2} (timeout ${TRANSCODE_TIMEOUT_MS}ms or external kill)`,
587
+ `ffmpeg killed by ${signal2} (timeout ${timeoutMs}ms or external kill)`,
471
588
  null,
472
589
  stderrTail
473
590
  )
@@ -492,7 +609,18 @@ async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath,
492
609
  mkdirWithinProject(projectDir, cacheDir);
493
610
  const tempPath = join2(cacheDir, `.tmp-${randomUUID()}-${basename(cachePath)}`);
494
611
  try {
495
- await runFfmpeg(absoluteSourcePath, tempPath, variant, box, job.abandoned.signal);
612
+ const chunkPrefix = join2(
613
+ cacheDir,
614
+ `${CHUNK_PREFIX}${basename(cachePath, extname2(cachePath))}-`
615
+ );
616
+ await runFfmpeg(
617
+ absoluteSourcePath,
618
+ tempPath,
619
+ chunkPrefix,
620
+ variant,
621
+ box,
622
+ job.abandoned.signal
623
+ );
496
624
  renameSync(tempPath, cachePath);
497
625
  maintainProxyCache(cacheDir);
498
626
  return cachePath;
@@ -560,13 +688,23 @@ async function resolveProxy(projectDir, absoluteSourcePath, variant = "h264", bo
560
688
  }
561
689
  async function resolveProxies(projectDir, sources, onSettled = () => {
562
690
  }) {
691
+ const first = sources[0];
692
+ if (first) {
693
+ maintainProxyCache(
694
+ dirname(getProxyCachePath(projectDir, first.sourcePath, first.variant)),
695
+ Math.min(
696
+ Number(process.env.HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS) || UNTOUCHED_TEMP_IS_DEAD_MS,
697
+ UNTOUCHED_TEMP_IS_DEAD_MS
698
+ )
699
+ );
700
+ }
563
701
  const results = [];
564
702
  let next = 0;
565
703
  const worker = async () => {
566
704
  for (let index = next++; index < sources.length; index = next++) {
567
- const { sourcePath, variant } = sources[index];
705
+ const { sourcePath, variant, box } = sources[index];
568
706
  results[index] = await waitForProxy(
569
- resolveProxy(projectDir, sourcePath, variant),
707
+ resolveProxy(projectDir, sourcePath, variant, box),
570
708
  TRANSCODE_TIMEOUT_MS
571
709
  ).then(
572
710
  (value) => ({ status: "fulfilled", value }),
@@ -121,7 +121,7 @@ async function probeMediaMetadata(filePath, runner = execFileRunner) {
121
121
  "-v",
122
122
  "error",
123
123
  "-show_entries",
124
- "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample:stream_disposition=attached_pic:stream_tags=alpha_mode",
124
+ "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample:stream_disposition=attached_pic:stream_tags=alpha_mode:format=duration",
125
125
  "-of",
126
126
  "json",
127
127
  "--",
@@ -145,6 +145,8 @@ async function probeMediaMetadata(filePath, runner = execFileRunner) {
145
145
  if (kind === "video") {
146
146
  metadata.hasAudio = (parsed.streams ?? []).some((item) => item.codec_type === "audio");
147
147
  metadata.hasAlpha = stream !== void 0 && videoStreamHasAlpha(stream);
148
+ const duration = Number(parsed.format?.duration);
149
+ if (Number.isFinite(duration) && duration > 0) metadata.durationSeconds = duration;
148
150
  }
149
151
  return metadata;
150
152
  } catch {
@@ -11,7 +11,7 @@ import { mkdir as mkdir2, readdir as readdir3, rm as rm3, rmdir, writeFile } fro
11
11
  import {
12
12
  existsSync as existsSync3,
13
13
  lstatSync as lstatSync4,
14
- statSync as statSync4,
14
+ statSync as statSync5,
15
15
  mkdirSync as mkdirSync5,
16
16
  readdirSync as readdirSync4,
17
17
  readFileSync as readFileSync10,
@@ -107,7 +107,15 @@ import { replaceFileAtomically } from "@hyperframes/core/atomic-file";
107
107
 
108
108
  // src/history/ownerLock.ts
109
109
  import { randomUUID as randomUUID2 } from "crypto";
110
- import { linkSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
110
+ import {
111
+ linkSync,
112
+ mkdirSync,
113
+ readFileSync,
114
+ renameSync,
115
+ rmSync,
116
+ statSync,
117
+ writeFileSync
118
+ } from "fs";
111
119
  import { join } from "path";
112
120
  var HistoryBusyError = class extends Error {
113
121
  constructor(pid, url) {
@@ -120,6 +128,8 @@ var HistoryBusyError = class extends Error {
120
128
  url;
121
129
  };
122
130
  var WINDOWS_BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
131
+ var NO_HARD_LINKS = /* @__PURE__ */ new Set(["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EPERM"]);
132
+ var TAKING_MS = 2e3;
123
133
  function busyOnWindows(error) {
124
134
  return process.platform === "win32" && WINDOWS_BUSY.has(error.code ?? "");
125
135
  }
@@ -141,6 +151,17 @@ function ownerOf(file) {
141
151
  throw error;
142
152
  }
143
153
  }
154
+ function beingTaken(file) {
155
+ try {
156
+ const { size, mtimeMs } = statSync(file);
157
+ return size === 0 && Date.now() - mtimeMs < TAKING_MS;
158
+ } catch {
159
+ return false;
160
+ }
161
+ }
162
+ function deadHolder(file, pid) {
163
+ return !alive(pid) && !beingTaken(file);
164
+ }
144
165
  function claim(file) {
145
166
  const draft = `${file}-${process.pid}-${randomUUID2()}.tmp`;
146
167
  writeFileSync(draft, String(process.pid));
@@ -148,12 +169,23 @@ function claim(file) {
148
169
  linkSync(draft, file);
149
170
  return true;
150
171
  } catch (error) {
151
- if (error.code !== "EEXIST") throw error;
152
- return false;
172
+ const code = error.code ?? "";
173
+ if (code === "EEXIST") return false;
174
+ if (!NO_HARD_LINKS.has(code)) throw error;
175
+ return createExclusively(file);
153
176
  } finally {
154
177
  rmSync(draft, { force: true });
155
178
  }
156
179
  }
180
+ function createExclusively(file) {
181
+ try {
182
+ writeFileSync(file, String(process.pid), { flag: "wx" });
183
+ return true;
184
+ } catch (error) {
185
+ if (error.code !== "EEXIST") throw error;
186
+ return false;
187
+ }
188
+ }
157
189
  function releaseOwn(file) {
158
190
  if (ownerOf(file) === process.pid) rmSync(file, { force: true });
159
191
  }
@@ -161,12 +193,12 @@ function evictDeadOwner(file) {
161
193
  const evictor = `${file}.evict`;
162
194
  if (!claim(evictor)) {
163
195
  const holder = ownerOf(evictor);
164
- if (holder !== null && !alive(holder)) rmSync(evictor, { force: true });
196
+ if (holder !== null && deadHolder(evictor, holder)) rmSync(evictor, { force: true });
165
197
  return false;
166
198
  }
167
199
  try {
168
200
  const pid = ownerOf(file);
169
- if (pid !== null && !alive(pid)) rmSync(file, { force: true });
201
+ if (pid !== null && deadHolder(file, pid)) rmSync(file, { force: true });
170
202
  return true;
171
203
  } finally {
172
204
  releaseOwn(evictor);
@@ -191,7 +223,7 @@ function attemptClaim(file, urlFile, deadline) {
191
223
  if (claim(file)) return "claimed";
192
224
  const pid = ownerOf(file);
193
225
  if (pid === null) return "retry";
194
- if (!alive(pid) && evictDeadOwner(file)) return "retry";
226
+ if (deadHolder(file, pid) && evictDeadOwner(file)) return "retry";
195
227
  if (Date.now() >= deadline) throw new HistoryBusyError(pid, servedBy(file, urlFile, pid));
196
228
  return "wait";
197
229
  }
@@ -526,14 +558,14 @@ async function openBlobStore(dir) {
526
558
 
527
559
  // src/history/pruneHistories.ts
528
560
  import { randomUUID as randomUUID5 } from "crypto";
529
- import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync3, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
561
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync3, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
530
562
  import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
531
563
  import { tmpdir } from "os";
532
564
  import { dirname as dirname2, join as join4 } from "path";
533
565
 
534
566
  // src/history/historyId.ts
535
567
  import { randomUUID as randomUUID4 } from "crypto";
536
- import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2 } from "fs";
568
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
537
569
  import { basename, join as join3, resolve as resolve3 } from "path";
538
570
  import { replaceFileAtomically as replaceFileAtomically2 } from "@hyperframes/core/atomic-file";
539
571
  var ID_PATH = join3(".hyperframes", "history-id");
@@ -566,7 +598,7 @@ function readRecord(historyDir) {
566
598
  function isCopyOf0878Folder(recordedDir, id, folder) {
567
599
  let there;
568
600
  try {
569
- there = statSync(recordedDir, { throwIfNoEntry: false });
601
+ there = statSync2(recordedDir, { throwIfNoEntry: false });
570
602
  } catch {
571
603
  return false;
572
604
  }
@@ -574,7 +606,7 @@ function isCopyOf0878Folder(recordedDir, id, folder) {
574
606
  }
575
607
  function projectHistoryId(projectDir, historyRoot) {
576
608
  const dir = resolve3(projectDir);
577
- const folder = statSync(dir);
609
+ const folder = statSync2(dir);
578
610
  let id = readId(dir);
579
611
  if (!id && existsSync(join3(dir, ID_PATH)))
580
612
  throw new HistoryIdError(
@@ -701,24 +733,24 @@ function abandonedProject(home, id, now, tempDir) {
701
733
  return projectGone(dir, id, record?.dev, tempDir) ? dir : null;
702
734
  }
703
735
  function projectGone(dir, id, dev, tempDir) {
704
- if (!statSync2(join4(dir, ID_PATH), { throwIfNoEntry: false }))
736
+ if (!statSync3(join4(dir, ID_PATH), { throwIfNoEntry: false }))
705
737
  return diskStillHere(dir, dev, tempDir);
706
738
  const there = readId(dir);
707
739
  return there !== null && there !== id;
708
740
  }
709
741
  function diskStillHere(dir, dev, tempDir) {
710
742
  let at = dir;
711
- let stat2 = statSync2(at, { throwIfNoEntry: false });
743
+ let stat2 = statSync3(at, { throwIfNoEntry: false });
712
744
  while (!stat2) {
713
745
  if (dirname2(at) === at) return false;
714
746
  at = dirname2(at);
715
- stat2 = statSync2(at, { throwIfNoEntry: false });
747
+ stat2 = statSync3(at, { throwIfNoEntry: false });
716
748
  }
717
749
  if (typeof dev === "number") return stat2.dev === dev;
718
750
  return at === dir || isSafePath(tempDir, at);
719
751
  }
720
752
  function lastUsed(home) {
721
- const mtime = (file) => statSync2(join4(home, file), { throwIfNoEntry: false })?.mtimeMs ?? 0;
753
+ const mtime = (file) => statSync3(join4(home, file), { throwIfNoEntry: false })?.mtimeMs ?? 0;
722
754
  return Math.max(mtime("project.json"), mtime("log.jsonl"));
723
755
  }
724
756
  async function bytesIn(dir) {
@@ -749,7 +781,7 @@ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs }
749
781
 
750
782
  // src/history/videoState.ts
751
783
  import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
752
- import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync8, renameSync as renameSync4, statSync as statSync3 } from "fs";
784
+ import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync8, renameSync as renameSync4, statSync as statSync4 } from "fs";
753
785
  import { isAbsolute as isAbsolute4, join as join7, resolve as resolve6 } from "path";
754
786
  import { replaceFileAtomically as replaceFileAtomically3 } from "@hyperframes/core/atomic-file";
755
787
 
@@ -1320,7 +1352,7 @@ function retainRevision(retained, revision) {
1320
1352
  return [revision, ...retained.filter((id) => id !== revision)].slice(0, 2);
1321
1353
  }
1322
1354
  async function recordRenderSuccess(projectDir, output, status = "completed", capture) {
1323
- const outputFile = statSync3(output);
1355
+ const outputFile = statSync4(output);
1324
1356
  if (!outputFile.isFile() && !outputFile.isDirectory() || outputFile.isFile() && outputFile.size === 0)
1325
1357
  throw new Error("Render output is missing or empty.");
1326
1358
  const after = capture ? await beginArtifactCapture(projectDir) : null;
@@ -1556,8 +1588,8 @@ function fileLeftIn(dir, at, deleted) {
1556
1588
  return void 0;
1557
1589
  }
1558
1590
  function ignoresCase(dir) {
1559
- const self = statSync4(join8(dir, ID_PATH), { throwIfNoEntry: false });
1560
- const other = statSync4(join8(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
1591
+ const self = statSync5(join8(dir, ID_PATH), { throwIfNoEntry: false });
1592
+ const other = statSync5(join8(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
1561
1593
  return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
1562
1594
  }
1563
1595
  var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
@@ -1597,7 +1629,7 @@ var Engine = class {
1597
1629
  this.dir = resolve7(options.projectDir);
1598
1630
  this.home = join8(options.historyRoot, projectId);
1599
1631
  this.foldsCase = ignoresCase(this.dir);
1600
- const folder = statSync4(this.dir, { throwIfNoEntry: false });
1632
+ const folder = statSync5(this.dir, { throwIfNoEntry: false });
1601
1633
  if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
1602
1634
  throw this.replaced();
1603
1635
  this.folder = { dev: folder.dev, ino: folder.ino, birthtimeMs: folder.birthtimeMs };
@@ -1662,7 +1694,7 @@ var Engine = class {
1662
1694
  }
1663
1695
  /** Another project if the folder or its id changed, or the id is gone: a recreated folder may reuse the inode. */
1664
1696
  whereFolder() {
1665
- const now = statSync4(this.dir, { throwIfNoEntry: false });
1697
+ const now = statSync5(this.dir, { throwIfNoEntry: false });
1666
1698
  if (!now) return "gone";
1667
1699
  const same = now.dev === this.folder.dev && sameFolder(now, this.folder);
1668
1700
  return same && readId(this.dir) === this.projectId ? "here" : "replaced";
@@ -2467,7 +2499,7 @@ async function openProjectHistory(options) {
2467
2499
  function readProjectHistory(options) {
2468
2500
  const dir = resolve7(options.projectDir);
2469
2501
  const id = readId(dir);
2470
- const home = id && isRecordedFolder(join8(options.historyRoot, id), statSync4(dir)) ? join8(options.historyRoot, id) : null;
2502
+ const home = id && isRecordedFolder(join8(options.historyRoot, id), statSync5(dir)) ? join8(options.historyRoot, id) : null;
2471
2503
  const read = home && readLog(logFileIn(home), () => {
2472
2504
  });
2473
2505
  const log = withoutHiddenPaths(read || { baseline: /* @__PURE__ */ new Map(), entries: [], pins: /* @__PURE__ */ new Set() });
@@ -13,7 +13,7 @@ import {
13
13
  resolveProxyVariantRequest,
14
14
  scanProjectMediaCodecMap,
15
15
  shouldPrewarmProxy
16
- } from "../chunk-OVOXGASQ.js";
16
+ } from "../chunk-XMNHDWQI.js";
17
17
  import "../chunk-QF7MFJJU.js";
18
18
  import "../chunk-SVQFE44C.js";
19
19
  import "../chunk-L3HJVTQE.js";
@@ -1,5 +1,5 @@
1
1
  import '@hyperframes/core';
2
- export { a as PreviewApiAdapter, P as PreviewDocumentStore, i as injectMediaCodecMap, f as injectMediaCodecMapIntoHtml, g as isAutoProxyEnabled, p as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-B5j7l9r4.js';
2
+ export { a as PreviewApiAdapter, P as PreviewDocumentStore, i as injectMediaCodecMap, f as injectMediaCodecMapIntoHtml, g as isAutoProxyEnabled, p as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-CAiGJucG.js';
3
3
  import './mediaCodecMap.js';
4
4
  import 'sharp';
5
5
  import '@hyperframes/parsers';
@@ -4,9 +4,9 @@ import {
4
4
  isAutoProxyEnabled,
5
5
  proxyEtagSalt,
6
6
  resolvePreviewMediaCodecProbeCache
7
- } from "../chunk-T5CGF7VZ.js";
8
- import "../chunk-25IT6FMR.js";
9
- import "../chunk-OVOXGASQ.js";
7
+ } from "../chunk-HTHV7ZM6.js";
8
+ import "../chunk-N2HH4SGD.js";
9
+ import "../chunk-XMNHDWQI.js";
10
10
  import "../chunk-QF7MFJJU.js";
11
11
  import "../chunk-SVQFE44C.js";
12
12
  import "../chunk-L3HJVTQE.js";
@@ -71,6 +71,7 @@ declare function resolveProxy(projectDir: string, absoluteSourcePath: string, va
71
71
  declare function resolveProxies(projectDir: string, sources: ReadonlyArray<{
72
72
  sourcePath: string;
73
73
  variant: ProxyVariant;
74
+ box?: PreviewProxyBox;
74
75
  }>, onSettled?: () => void): Promise<PromiseSettledResult<string>[]>;
75
76
  interface ResolveProxyOptions {
76
77
  priority?: boolean;
@@ -16,8 +16,8 @@ import {
16
16
  resolveProxies,
17
17
  resolveProxy,
18
18
  waitForProxy
19
- } from "../chunk-25IT6FMR.js";
20
- import "../chunk-OVOXGASQ.js";
19
+ } from "../chunk-N2HH4SGD.js";
20
+ import "../chunk-XMNHDWQI.js";
21
21
  import "../chunk-QF7MFJJU.js";
22
22
  import "../chunk-SVQFE44C.js";
23
23
  import "../chunk-L3HJVTQE.js";
@@ -20,7 +20,7 @@ import {
20
20
  recordRenderSuccess,
21
21
  registerRenderOutput,
22
22
  retainSnapshotOutput
23
- } from "../chunk-XM2TJIF5.js";
23
+ } from "../chunk-YIMSBQIO.js";
24
24
  import "../chunk-QF7MFJJU.js";
25
25
  import "../chunk-SVQFE44C.js";
26
26
  import "../chunk-L3HJVTQE.js";
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Hono } from 'hono';
2
- import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-B5j7l9r4.js';
3
- export { L as LintResult, a as PreviewApiAdapter, R as RenderJobState, b as ResolvedProject, c as StudioSelectionResponse, d as StudioSelectionSnapshot, e as StudioSelectionTextField } from './mediaProxyPreview-B5j7l9r4.js';
2
+ import { S as StudioApiAdapter, P as PreviewDocumentStore, M as MediaProcessingJobState } from './mediaProxyPreview-CAiGJucG.js';
3
+ export { L as LintResult, a as PreviewApiAdapter, R as RenderJobState, b as ResolvedProject, c as StudioSelectionResponse, d as StudioSelectionSnapshot, e as StudioSelectionTextField } from './mediaProxyPreview-CAiGJucG.js';
4
4
  export { C as ClosedWindow, D as DEFAULT_HISTORY_ROOT, S as HISTORY_START, H as HistoryClosedError, a as HistoryEntry, b as HistoryEntrySide, c as HistoryFileChange, d as HistoryListItem, e as HistoryResult, f as HistoryTarget, g as HistoryWho, h as HistoryWindow, M as MAX_WINDOW_IDLE_MS, P as ProjectHistory, i as ProjectHistoryOptions, j as ProjectHistoryView, U as UNDO_MODES, k as UndoMode, o as openProjectHistory, r as readProjectHistory } from './projectHistory-ByC9sa3X.js';
5
5
  export { ArtifactCapture, ArtifactInput, ArtifactRecord, HistoryBusyError, HistoryIdError, ProjectHistoryRecord, PrunedHistory, RenderRecord, VideoState, artifactDirectory, beginArtifactCapture, finishSnapshotRun, forgetRecordedArtifacts, historyCache, listProjectHistories, pruneGoneProjectHistories, recordArtifact, recordArtifacts, recordRenderSuccess, registerRenderOutput, retainSnapshotOutput } from './history/index.js';
6
6
  export { CACHE_DIR_NAME as PROXY_CACHE_DIR_NAME } from './helpers/proxyTranscoder.js';
package/dist/index.js CHANGED
@@ -60,7 +60,7 @@ import {
60
60
  resolveProjectSignature,
61
61
  retainSnapshotOutput,
62
62
  settledFileTag
63
- } from "./chunk-XM2TJIF5.js";
63
+ } from "./chunk-YIMSBQIO.js";
64
64
  import {
65
65
  clearElementScreenshotIsolation,
66
66
  getElementScreenshotClip
@@ -80,7 +80,7 @@ import {
80
80
  isAutoProxyEnabled,
81
81
  proxyEtagSalt,
82
82
  resolvePreviewMediaCodecProbeCache
83
- } from "./chunk-T5CGF7VZ.js";
83
+ } from "./chunk-HTHV7ZM6.js";
84
84
  import {
85
85
  CACHE_DIR_NAME,
86
86
  PROXY_PENDING_RETRY_AFTER_SECONDS,
@@ -91,7 +91,7 @@ import {
91
91
  proxyActivityMark,
92
92
  resolveProxy,
93
93
  waitForProxy
94
- } from "./chunk-25IT6FMR.js";
94
+ } from "./chunk-N2HH4SGD.js";
95
95
  import {
96
96
  PROXY_VARIANT_CONFIG,
97
97
  decideMediaProxyEligibility,
@@ -100,7 +100,7 @@ import {
100
100
  probeMediaMetadata,
101
101
  recordProxyRequest,
102
102
  resolveProxyVariantRequest
103
- } from "./chunk-OVOXGASQ.js";
103
+ } from "./chunk-XMNHDWQI.js";
104
104
  import {
105
105
  folderGone,
106
106
  isInHiddenOrVendorDir,
@@ -4643,6 +4643,11 @@ function registerRenderRoutes(api, adapter) {
4643
4643
  }
4644
4644
  const encoder = parseEncoderOptions(body, format);
4645
4645
  if (!encoder.ok) return c.json({ error: encoder.error }, 400);
4646
+ const requestedPageMotionBlur = body.pageMotionBlur;
4647
+ if (requestedPageMotionBlur !== void 0 && requestedPageMotionBlur !== "off") {
4648
+ return c.json({ error: "pageMotionBlur must be off when supplied" }, 400);
4649
+ }
4650
+ const pageMotionBlur = requestedPageMotionBlur === void 0 && body.previewMotionBlur === false ? "off" : requestedPageMotionBlur;
4646
4651
  const now = /* @__PURE__ */ new Date();
4647
4652
  const jobId = `${project.id}_${formatRenderOutputTimestamp(now)}`;
4648
4653
  const rendersDir = adapter.rendersDir(project);
@@ -4663,7 +4668,7 @@ function registerRenderRoutes(api, adapter) {
4663
4668
  useGpu: body.gpu === true && format === "mp4",
4664
4669
  distinctId: typeof body.telemetryDistinctId === "string" ? body.telemetryDistinctId : void 0,
4665
4670
  telemetryOptOut: body.telemetryOptOut === true,
4666
- ...body.previewMotionBlur === false ? { pageMotionBlur: "off" } : {}
4671
+ ...pageMotionBlur === void 0 ? {} : { pageMotionBlur }
4667
4672
  });
4668
4673
  jobState.createdAt = Date.now();
4669
4674
  renderJobs.set(jobId, jobState);
@@ -302,7 +302,7 @@ declare function proxyEtagSalt(raw: string | undefined, box?: PreviewProxyBox):
302
302
  */
303
303
  declare function injectMediaCodecMapIntoHtml(html: string, projectDir: string, htmlSources: HtmlSourceLike[], probeCache?: MediaCodecProbeCache, prewarm?: boolean,
304
304
  /** Aborting it stops the pre-warms nobody else waits for. */
305
- signal?: AbortSignal): Promise<string>;
305
+ signal?: AbortSignal, box?: PreviewProxyBox): Promise<string>;
306
306
  /**
307
307
  * Adapter-aware wrapper used by the studio preview routes: skipped entirely
308
308
  * (no scan, no injection) when auto-proxy is off for this adapter.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.8.186",
3
+ "version": "0.8.189",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -84,8 +84,8 @@
84
84
  "linkedom": "^0.18.12",
85
85
  "postcss": "^8.5.8",
86
86
  "postcss-selector-parser": "^7.1.2",
87
- "@hyperframes/core": "0.8.186",
88
- "@hyperframes/parsers": "0.8.186"
87
+ "@hyperframes/parsers": "0.8.189",
88
+ "@hyperframes/core": "0.8.189"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/node": "^25.0.10",