@hyperframes/studio-server 0.8.201 → 0.8.203

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.
@@ -125,6 +125,30 @@ function sourceFactsOf(stream, formatBitrate) {
125
125
  bitrate: Number.isFinite(bitrate) ? bitrate : 0
126
126
  };
127
127
  }
128
+ var STREAM_SUMMARY_MAX = 12;
129
+ function summarizeStream(stream) {
130
+ const flags = Object.entries(stream.disposition ?? {}).filter(([, on]) => on === 1).map(([name]) => name);
131
+ return `${stream.codec_type ?? "?"}:${stream.codec_name ?? "?"}[${flags.join(",")}]`;
132
+ }
133
+ function metadataFromProbe(kind, parsed) {
134
+ const stream = parsed.streams?.find((item) => {
135
+ if (kind === "image") return item.codec_type === "video";
136
+ return item.codec_type === kind && item.disposition?.attached_pic !== 1;
137
+ });
138
+ const metadata = {
139
+ kind,
140
+ color: classifyMediaColor(stream),
141
+ streams: (parsed.streams ?? []).slice(0, STREAM_SUMMARY_MAX).map(summarizeStream)
142
+ };
143
+ if (kind === "video") {
144
+ metadata.hasAudio = (parsed.streams ?? []).some((item) => item.codec_type === "audio");
145
+ metadata.hasAlpha = stream !== void 0 && videoStreamHasAlpha(stream);
146
+ const duration = Number(parsed.format?.duration);
147
+ if (Number.isFinite(duration) && duration > 0) metadata.durationSeconds = duration;
148
+ metadata.source = sourceFactsOf(stream, parsed.format?.bit_rate);
149
+ }
150
+ return metadata;
151
+ }
128
152
  async function probeMediaMetadata(filePath, runner = execFileRunner) {
129
153
  const kind = inferKindFromPath(filePath);
130
154
  if (kind === "audio" || kind === "unknown") {
@@ -140,7 +164,7 @@ async function probeMediaMetadata(filePath, runner = execFileRunner) {
140
164
  "-v",
141
165
  "error",
142
166
  "-show_entries",
143
- "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample,width,height,bit_rate:stream_disposition=attached_pic:stream_tags=alpha_mode:format=duration,bit_rate",
167
+ "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample,width,height,bit_rate:stream_disposition:stream_tags=alpha_mode:format=duration,bit_rate",
144
168
  "-of",
145
169
  "json",
146
170
  "--",
@@ -155,20 +179,7 @@ async function probeMediaMetadata(filePath, runner = execFileRunner) {
155
179
  return { kind, color: classifyMediaColor(null), probeError: "ffprobe failed" };
156
180
  }
157
181
  try {
158
- const parsed = JSON.parse(String(result.stdout || "{}"));
159
- const stream = parsed.streams?.find((item) => {
160
- if (kind === "image") return item.codec_type === "video";
161
- return item.codec_type === kind && item.disposition?.attached_pic !== 1;
162
- });
163
- const metadata = { kind, color: classifyMediaColor(stream) };
164
- if (kind === "video") {
165
- metadata.hasAudio = (parsed.streams ?? []).some((item) => item.codec_type === "audio");
166
- metadata.hasAlpha = stream !== void 0 && videoStreamHasAlpha(stream);
167
- const duration = Number(parsed.format?.duration);
168
- if (Number.isFinite(duration) && duration > 0) metadata.durationSeconds = duration;
169
- metadata.source = sourceFactsOf(stream, parsed.format?.bit_rate);
170
- }
171
- return metadata;
182
+ return metadataFromProbe(kind, JSON.parse(String(result.stdout || "{}")));
172
183
  } catch {
173
184
  return { kind, color: classifyMediaColor(null), probeError: "ffprobe returned invalid json" };
174
185
  }
@@ -348,7 +359,7 @@ async function scanProjectMedia(projectDir, htmlSources, options = {}) {
348
359
  const scan = { codecMap: {}, sourceFacts: {} };
349
360
  for (const [pathname, facts] of await probeProjectAssets(projectDir, htmlSources, options)) {
350
361
  if (facts?.browserHostile) scan.codecMap[pathname] = facts;
351
- if (facts?.source) scan.sourceFacts[pathname] = facts.source;
362
+ if (facts?.source) scan.sourceFacts[pathname] = { ...facts.source, codec: facts.codecName };
352
363
  }
353
364
  return scan;
354
365
  }
@@ -2,7 +2,7 @@ import {
2
2
  PROXY_VARIANT_CONFIG,
3
3
  probeFirstFrameColour,
4
4
  probeMediaMetadata
5
- } from "./chunk-C2RFW2FX.js";
5
+ } from "./chunk-JKPLDWAB.js";
6
6
  import {
7
7
  mkdirWithinProject,
8
8
  realProjectRoot,
@@ -11,23 +11,19 @@ import {
11
11
 
12
12
  // src/helpers/proxyTranscoder.ts
13
13
  import { spawn } from "child_process";
14
- import { createHash, randomUUID } from "crypto";
14
+ import { randomUUID } from "crypto";
15
15
  import {
16
- existsSync as existsSync2,
16
+ existsSync as existsSync4,
17
17
  mkdtempSync,
18
18
  renameSync,
19
- rmSync,
20
- statSync as statSync2,
19
+ rmSync as rmSync2,
21
20
  unlinkSync as unlinkSync2,
22
21
  utimesSync,
23
- writeFileSync
22
+ writeFileSync as writeFileSync2
24
23
  } from "fs";
25
24
  import { availableParallelism, tmpdir, totalmem } from "os";
26
- import { basename, dirname, extname as extname2, isAbsolute, join as join2, relative, resolve, sep } from "path";
27
- import {
28
- formatPreviewProxyBox,
29
- hdrToSdrToneMapFilter
30
- } from "@hyperframes/core";
25
+ import { basename as basename3, dirname as dirname3, extname as extname3, isAbsolute, join as join4, relative, resolve, sep } from "path";
26
+ import { hdrToSdrToneMapFilter } from "@hyperframes/core";
31
27
  import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
32
28
  import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
33
29
 
@@ -143,6 +139,148 @@ function cleanupProxyCache(cacheDir, options = {}) {
143
139
  };
144
140
  }
145
141
 
142
+ // src/helpers/proxyCacheKey.ts
143
+ import { createHash } from "crypto";
144
+ import {
145
+ closeSync,
146
+ createReadStream,
147
+ existsSync as existsSync2,
148
+ fstatSync,
149
+ openSync,
150
+ readFileSync,
151
+ statSync as statSync2,
152
+ writeFileSync
153
+ } from "fs";
154
+ import { basename, dirname, extname as extname2, join as join2 } from "path";
155
+ import { formatPreviewProxyBox } from "@hyperframes/core";
156
+ var SMALL_FILE_BYTES = 16 * 1024 * 1024;
157
+ var COPY_NAME = /^[0-9a-f]{64}\.(?:mp4|webm)$/;
158
+ function digest(...parts) {
159
+ return createHash("sha256").update(parts.join("\0")).digest("hex");
160
+ }
161
+ function boxPart(box) {
162
+ return box ? formatPreviewProxyBox(box) : "";
163
+ }
164
+ function fastCopyKey(sourcePath, version, variant, box) {
165
+ const stat = statSync2(sourcePath);
166
+ return digest(
167
+ sourcePath,
168
+ String(stat.mtimeMs),
169
+ String(stat.size),
170
+ version,
171
+ variant,
172
+ boxPart(box)
173
+ );
174
+ }
175
+ async function contentCopyKey(sourcePath, version, variant, box, signal) {
176
+ const hash = createHash("sha256");
177
+ const fd = openSync(sourcePath, "r");
178
+ try {
179
+ if (fstatSync(fd).size <= SMALL_FILE_BYTES) hash.update(readFileSync(fd));
180
+ else
181
+ for await (const chunk of createReadStream(sourcePath, { fd, autoClose: false, signal }))
182
+ hash.update(chunk);
183
+ } finally {
184
+ closeSync(fd);
185
+ }
186
+ return digest(hash.digest("hex"), version, variant, boxPart(box));
187
+ }
188
+ function sourceSizeBytes(sourcePath) {
189
+ try {
190
+ const fd = openSync(sourcePath, "r");
191
+ try {
192
+ return fstatSync(fd).size;
193
+ } finally {
194
+ closeSync(fd);
195
+ }
196
+ } catch {
197
+ return void 0;
198
+ }
199
+ }
200
+ function fastIndexPath(cachePath) {
201
+ return join2(dirname(cachePath), `.fast-${basename(cachePath, extname2(cachePath))}`);
202
+ }
203
+ function contentIndexPath(cacheDir, contentKey) {
204
+ return join2(cacheDir, `.content-${contentKey}`);
205
+ }
206
+ function readCopyIndex(indexPath) {
207
+ try {
208
+ const name = readFileSync(indexPath, "utf8").trim();
209
+ const target = join2(dirname(indexPath), name);
210
+ return COPY_NAME.test(name) && existsSync2(target) ? target : null;
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+ function writeCopyIndex(indexPath, copyPath) {
216
+ writeFileSync(indexPath, basename(copyPath));
217
+ }
218
+
219
+ // src/helpers/proxyLock.ts
220
+ import {
221
+ closeSync as closeSync2,
222
+ existsSync as existsSync3,
223
+ openSync as openSync2,
224
+ readFileSync as readFileSync2,
225
+ rmSync,
226
+ statSync as statSync3,
227
+ writeSync
228
+ } from "fs";
229
+ import { basename as basename2, dirname as dirname2, join as join3 } from "path";
230
+ var POLL_MS = 500;
231
+ var UNREADABLE_LOCK_MS = 1e4;
232
+ function lockPathFor(cachePath) {
233
+ return join3(dirname2(cachePath), `.lock-${basename2(cachePath)}`);
234
+ }
235
+ function ownerIsAlive(lockPath) {
236
+ try {
237
+ const pid = Number(readFileSync2(lockPath, "utf8"));
238
+ if (!Number.isInteger(pid) || pid <= 0) {
239
+ return Date.now() - statSync3(lockPath).mtimeMs < UNREADABLE_LOCK_MS;
240
+ }
241
+ try {
242
+ process.kill(pid, 0);
243
+ return true;
244
+ } catch (err) {
245
+ return err.code === "EPERM";
246
+ }
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+ function pause(ms, signal) {
252
+ return new Promise((resolve2, reject) => {
253
+ const timer = setTimeout(() => {
254
+ signal.removeEventListener("abort", onAbort);
255
+ resolve2();
256
+ }, ms);
257
+ const onAbort = () => {
258
+ clearTimeout(timer);
259
+ reject(signal.reason);
260
+ };
261
+ signal.addEventListener("abort", onAbort, { once: true });
262
+ });
263
+ }
264
+ async function acquireCopyLock(cachePath, signal) {
265
+ const lockPath = lockPathFor(cachePath);
266
+ for (; ; ) {
267
+ signal.throwIfAborted();
268
+ if (existsSync3(cachePath)) return null;
269
+ try {
270
+ const fd = openSync2(lockPath, "wx");
271
+ writeSync(fd, String(process.pid));
272
+ closeSync2(fd);
273
+ if (!existsSync3(cachePath)) return () => rmSync(lockPath, { force: true });
274
+ rmSync(lockPath, { force: true });
275
+ return null;
276
+ } catch (err) {
277
+ if (err.code !== "EEXIST") throw err;
278
+ }
279
+ if (ownerIsAlive(lockPath)) await pause(POLL_MS, signal);
280
+ else rmSync(lockPath, { force: true });
281
+ }
282
+ }
283
+
146
284
  // src/helpers/proxyTranscoder.ts
147
285
  var PROXY_PARAMS_VERSION = "v7";
148
286
  var CACHE_DIR_NAME = ".transcode-cache";
@@ -235,33 +373,16 @@ function canonicalizeProxySource(projectDir, absoluteSourcePath) {
235
373
  if (requestedRelativePath === ".." || requestedRelativePath.startsWith(`..${sep}`) || isAbsolute(requestedRelativePath)) {
236
374
  throw new ProxySourceOutsideProjectError();
237
375
  }
238
- const canonicalProjectDir = realProjectRoot(projectDir);
239
- const canonicalSourcePath = realpath(absoluteSourcePath);
240
- const canonicalRelativePath = relative(canonicalProjectDir, canonicalSourcePath);
241
- const sourceIsInsideCanonicalProject = canonicalRelativePath !== ".." && !canonicalRelativePath.startsWith(`..${sep}`) && !isAbsolute(canonicalRelativePath);
242
- return {
243
- projectDir: canonicalProjectDir,
244
- sourcePath: canonicalSourcePath,
245
- relativePath: requestedRelativePath.normalize("NFC"),
246
- // An external target needs a stable identity in addition to its project-local
247
- // symlink path, otherwise retargeting the link can reuse an unrelated proxy.
248
- cacheIdentity: (sourceIsInsideCanonicalProject ? canonicalRelativePath : canonicalSourcePath).normalize("NFC")
249
- };
250
- }
251
- function buildProxyCacheKey(source, variant, box) {
252
- const stat = statSync2(source.sourcePath);
253
- const size = box ? `\0${formatPreviewProxyBox(box)}` : "";
254
- return createHash("sha256").update(
255
- `${source.relativePath}\0${source.cacheIdentity}\0${stat.mtimeMs}\0${stat.size}\0${PROXY_PARAMS_VERSION}\0${variant}${size}`
256
- ).digest("hex");
376
+ return { projectDir: realProjectRoot(projectDir), sourcePath: realpath(absoluteSourcePath) };
257
377
  }
258
378
  function getCanonicalProxyCachePath(source, variant, box) {
259
- const key = buildProxyCacheKey(source, variant, box);
260
- return join2(
379
+ const key = fastCopyKey(source.sourcePath, PROXY_PARAMS_VERSION, variant, box);
380
+ const made = join4(
261
381
  source.projectDir,
262
382
  CACHE_DIR_NAME,
263
383
  `${key}${PROXY_VARIANT_CONFIG[variant].extension}`
264
384
  );
385
+ return existsSync4(made) ? made : readCopyIndex(fastIndexPath(made)) ?? made;
265
386
  }
266
387
  function getProxyCachePath(projectDir, absoluteSourcePath, variant = "h264", box) {
267
388
  return getCanonicalProxyCachePath(
@@ -406,11 +527,53 @@ function proxyScaleFilter(box) {
406
527
  return `scale=${width}:${height}`;
407
528
  }
408
529
  var STDERR_ERROR_LINE = /error|invalid|denied|no space|not found|unknown/i;
409
- function stderrReason(stderrTail) {
410
- const lines = stderrTail.split("\n").map((line2) => line2.trim()).filter(Boolean);
411
- const errorLines = lines.filter((line2) => STDERR_ERROR_LINE.test(line2));
412
- const line = errorLines.at(-1) ?? lines.at(-1);
413
- return line ? `: ${line.replace(/[.!]+$/, "")}` : "";
530
+ var OPEN_OUTPUT_LINE = /^Error opening output file /;
531
+ var ABSOLUTE_PATH = /(?:[A-Za-z]:)?(?:[\\/][^\\/\s'"]+){2,}/g;
532
+ var REASON_LINES = 3;
533
+ function stderrReason(stderrTail, knownPaths) {
534
+ const lines = stderrTail.split("\n").map((line) => line.trim()).filter(Boolean);
535
+ const openedAt = lines.map((candidate) => OPEN_OUTPUT_LINE.test(candidate)).lastIndexOf(true);
536
+ const causes = lines.filter(
537
+ (line, index) => !OPEN_OUTPUT_LINE.test(line) && (STDERR_ERROR_LINE.test(line) || index === openedAt - 1)
538
+ );
539
+ const shown = (causes.length ? causes : lines.slice(-1)).slice(-REASON_LINES);
540
+ const byLength = knownPaths.filter(isAbsolute).sort((a, b) => b.length - a.length);
541
+ const reason = shown.map(
542
+ (line) => byLength.reduce((text, path) => text.split(path).join("<path>"), line).replace(ABSOLUTE_PATH, "<path>").replace(/[.!]+$/, "")
543
+ );
544
+ return reason.length ? `: ${reason.join("; ")}` : "";
545
+ }
546
+ var CLOUD_FOLDER = /\/(?:Library\/CloudStorage|Dropbox|OneDrive|Google Drive)(?:[ \-/]|$)/i;
547
+ function sourceFolderKind(sourcePath) {
548
+ const path = sourcePath.replace(/\\/g, "/");
549
+ if (path.includes("/Library/Mobile Documents/")) return "icloud";
550
+ if (CLOUD_FOLDER.test(path)) return "cloud-storage";
551
+ return path.startsWith("/Volumes/") ? "external-volume" : "local";
552
+ }
553
+ function describeSource(sourcePath, metadata) {
554
+ const { codecName, pixelFormat, colorTransfer } = metadata.color;
555
+ const megabytes = sourceSizeBytes(sourcePath);
556
+ return [
557
+ metadata.kind,
558
+ codecName,
559
+ pixelFormat,
560
+ colorTransfer,
561
+ metadata.hasAudio ? "with audio" : "no audio",
562
+ metadata.streams?.length ? `streams ${metadata.streams.join(" ")}` : void 0,
563
+ metadata.durationSeconds ? `${Math.round(metadata.durationSeconds)}s` : void 0,
564
+ megabytes === void 0 ? "missing" : `${Math.round(megabytes / 1e6)} MB`,
565
+ sourceFolderKind(sourcePath)
566
+ ].filter(Boolean).join(", ");
567
+ }
568
+ function withSource(err, describe) {
569
+ if (err instanceof ProxyTranscodeError && err.name === "ProxyTranscodeError") {
570
+ return new ProxyTranscodeError(
571
+ `${err.message} (source: ${describe()})`,
572
+ err.exitCode,
573
+ err.stderrTail
574
+ );
575
+ }
576
+ return err;
414
577
  }
415
578
  function chunkRanges(durationSeconds) {
416
579
  if (!durationSeconds || durationSeconds <= PROXY_CHUNK_SECONDS * 1.5) return null;
@@ -456,9 +619,9 @@ function createPieceGate(size) {
456
619
  }
457
620
  async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, signal, gate, onDone = () => {
458
621
  }) {
459
- const extension = extname2(tempPath);
622
+ const extension = extname3(tempPath);
460
623
  const pieces = ranges.map((_, index) => `${chunkPrefix}${index}${extension}`);
461
- const pending = ranges.flatMap((_, index) => existsSync2(pieces[index]) ? [] : [index]);
624
+ const pending = ranges.flatMap((_, index) => existsSync4(pieces[index]) ? [] : [index]);
462
625
  let doneSeconds = 0;
463
626
  for (const [index, range] of ranges.entries()) {
464
627
  if (!pending.includes(index)) doneSeconds += range.lengthSeconds;
@@ -477,7 +640,7 @@ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, sig
477
640
  const index = pending[cursor++];
478
641
  const range = ranges[index];
479
642
  const piece = pieces[index];
480
- const pieceTemp = join2(dirname(piece), `.tmp-${randomUUID()}-${basename(piece)}`);
643
+ const pieceTemp = join4(dirname3(piece), `.tmp-${randomUUID()}-${basename3(piece)}`);
481
644
  try {
482
645
  await encode(pieceTemp, range, pieceSignal, (encodedSeconds) => {
483
646
  inFlight2.set(index, encodedSeconds);
@@ -488,7 +651,7 @@ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, sig
488
651
  doneSeconds += range.lengthSeconds;
489
652
  onDone(doneSeconds);
490
653
  } finally {
491
- if (existsSync2(pieceTemp)) unlinkSync2(pieceTemp);
654
+ if (existsSync4(pieceTemp)) unlinkSync2(pieceTemp);
492
655
  }
493
656
  } finally {
494
657
  gate.release();
@@ -505,19 +668,19 @@ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, sig
505
668
  )
506
669
  );
507
670
  if (failure !== void 0) throw failure;
508
- const listDir = mkdtempSync(join2(tmpdir(), "hf-proxy-join-"));
671
+ const listDir = mkdtempSync(join4(tmpdir(), "hf-proxy-join-"));
509
672
  try {
510
- const listPath = join2(listDir, "pieces.txt");
511
- writeFileSync(
673
+ const listPath = join4(listDir, "pieces.txt");
674
+ writeFileSync2(
512
675
  listPath,
513
676
  pieces.map((piece) => `file '${piece.replace(/\\/g, "/").replace(/'/g, "'\\''")}'
514
677
  `).join("")
515
678
  );
516
679
  await concat(listPath, tempPath);
517
680
  } finally {
518
- rmSync(listDir, { recursive: true, force: true });
681
+ rmSync2(listDir, { recursive: true, force: true });
519
682
  }
520
- for (const piece of pieces) if (existsSync2(piece)) unlinkSync2(piece);
683
+ for (const piece of pieces) if (existsSync4(piece)) unlinkSync2(piece);
521
684
  }
522
685
  var KEYFRAME_EVERY_SECOND_ARGS = ["-force_key_frames", "expr:gte(t,n_forced*1)"];
523
686
  var H264_ARGS = [
@@ -627,24 +790,28 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
627
790
  onEncoded
628
791
  );
629
792
  const ranges = chunkRanges(totalSeconds);
630
- if (!ranges) {
631
- return encode(
793
+ try {
794
+ if (!ranges) {
795
+ return await encode(
796
+ outputPath,
797
+ void 0,
798
+ signal,
799
+ (encodedSeconds) => onProgress({ doneSeconds: encodedSeconds, totalSeconds })
800
+ );
801
+ }
802
+ await encodeInChunks(
803
+ ranges,
804
+ chunkPrefix,
632
805
  outputPath,
633
- void 0,
806
+ encode,
807
+ (listPath, target) => spawnFfmpeg(ffmpegPath, concatArgs(listPath, target, variant), CONCAT_TIMEOUT_MS, signal),
634
808
  signal,
635
- (encodedSeconds) => onProgress({ doneSeconds: encodedSeconds, totalSeconds })
809
+ gate,
810
+ (doneSeconds) => onProgress({ doneSeconds, totalSeconds })
636
811
  );
812
+ } catch (err) {
813
+ throw withSource(err, () => describeSource(sourcePath, metadata));
637
814
  }
638
- await encodeInChunks(
639
- ranges,
640
- chunkPrefix,
641
- outputPath,
642
- encode,
643
- (listPath, target) => spawnFfmpeg(ffmpegPath, concatArgs(listPath, target, variant), CONCAT_TIMEOUT_MS, signal),
644
- signal,
645
- gate,
646
- (doneSeconds) => onProgress({ doneSeconds, totalSeconds })
647
- );
648
815
  }
649
816
  function concatArgs(listPath, target, variant) {
650
817
  return [
@@ -725,7 +892,7 @@ function spawnFfmpeg(ffmpegPath, args, stallMs, signal, onProgress) {
725
892
  } else {
726
893
  reject(
727
894
  new ProxyTranscodeError(
728
- `ffmpeg exited with code ${code}${stderrReason(stderrTail)}`,
895
+ `ffmpeg exited with code ${code}${stderrReason(stderrTail, args)}`,
729
896
  code,
730
897
  stderrTail
731
898
  )
@@ -737,14 +904,31 @@ function spawnFfmpeg(ffmpegPath, args, stallMs, signal, onProgress) {
737
904
  async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath, variant, box) {
738
905
  await acquireSlot(job);
739
906
  try {
740
- if (existsSync2(cachePath)) return cachePath;
741
- const cacheDir = dirname(cachePath);
907
+ if (existsSync4(cachePath)) return cachePath;
908
+ const cacheDir = dirname3(cachePath);
742
909
  mkdirWithinProject(projectDir, cacheDir);
743
- const tempPath = join2(cacheDir, `.tmp-${randomUUID()}-${basename(cachePath)}`);
910
+ const contentIndex = contentIndexPath(
911
+ cacheDir,
912
+ await contentCopyKey(
913
+ absoluteSourcePath,
914
+ PROXY_PARAMS_VERSION,
915
+ variant,
916
+ box,
917
+ job.abandoned.signal
918
+ )
919
+ );
920
+ const sameContent = readCopyIndex(contentIndex);
921
+ if (sameContent) {
922
+ writeCopyIndex(fastIndexPath(cachePath), sameContent);
923
+ return sameContent;
924
+ }
925
+ const release = await acquireCopyLock(cachePath, job.abandoned.signal);
926
+ if (!release) return cachePath;
927
+ const tempPath = join4(cacheDir, `.tmp-${randomUUID()}-${basename3(cachePath)}`);
744
928
  try {
745
- const chunkPrefix = join2(
929
+ const chunkPrefix = join4(
746
930
  cacheDir,
747
- `${CHUNK_PREFIX}${basename(cachePath, extname2(cachePath))}-`
931
+ `${CHUNK_PREFIX}${basename3(cachePath, extname3(cachePath))}-`
748
932
  );
749
933
  await runFfmpeg(
750
934
  absoluteSourcePath,
@@ -756,19 +940,30 @@ async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath,
756
940
  (progress) => reportProgress(job, progress)
757
941
  );
758
942
  renameSync(tempPath, cachePath);
943
+ writeCopyIndex(contentIndex, cachePath);
759
944
  maintainProxyCache(cacheDir);
760
945
  return cachePath;
761
946
  } finally {
762
- if (existsSync2(tempPath)) unlinkSync2(tempPath);
947
+ if (existsSync4(tempPath)) unlinkSync2(tempPath);
948
+ release();
763
949
  }
764
950
  } finally {
765
951
  releaseSlot();
766
952
  }
767
953
  }
954
+ function proxyCopyProgress(projectDir, absoluteSourcePath, variant, box) {
955
+ const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
956
+ const progress = inFlight.get(getCanonicalProxyCachePath(source, variant, box))?.job.lastProgress;
957
+ return progress?.totalSeconds ? Math.min(1, progress.doneSeconds / progress.totalSeconds) : void 0;
958
+ }
959
+ function retryProxyCopy(projectDir, absoluteSourcePath, variant, box) {
960
+ const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
961
+ failedTranscodes.delete(getCanonicalProxyCachePath(source, variant, box));
962
+ }
768
963
  var proxyActivity = 0;
769
964
  function proxyActivityMark(projectDir) {
770
- if (!existsSync2(projectDir)) return String(proxyActivity);
771
- const cacheDir = join2(realpath(projectDir), CACHE_DIR_NAME) + sep;
965
+ if (!existsSync4(projectDir)) return String(proxyActivity);
966
+ const cacheDir = join4(realpath(projectDir), CACHE_DIR_NAME) + sep;
772
967
  for (const cachePath of inFlight.keys()) if (cachePath.startsWith(cacheDir)) return null;
773
968
  return String(proxyActivity);
774
969
  }
@@ -776,9 +971,9 @@ async function resolveProxy(projectDir, absoluteSourcePath, variant = "h264", bo
776
971
  options.signal?.throwIfAborted();
777
972
  const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
778
973
  const cachePath = getCanonicalProxyCachePath(source, variant, box);
779
- if (existsSync2(cachePath)) {
974
+ if (existsSync4(cachePath)) {
780
975
  markCacheEntryUsed(cachePath);
781
- maintainProxyCache(dirname(cachePath));
976
+ maintainProxyCache(dirname3(cachePath));
782
977
  return cachePath;
783
978
  }
784
979
  const rememberedFailure = failedTranscodes.get(cachePath);
@@ -835,7 +1030,7 @@ async function resolveProxies(projectDir, sources, onSettled = () => {
835
1030
  const first = sources[0];
836
1031
  if (first) {
837
1032
  maintainProxyCache(
838
- dirname(getProxyCachePath(projectDir, first.sourcePath, first.variant)),
1033
+ dirname3(getProxyCachePath(projectDir, first.sourcePath, first.variant)),
839
1034
  Math.min(
840
1035
  Number(process.env.HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS) || UNTOUCHED_TEMP_IS_DEAD_MS,
841
1036
  UNTOUCHED_TEMP_IS_DEAD_MS
@@ -891,6 +1086,9 @@ export {
891
1086
  waitForProxy,
892
1087
  getProxyCachePath,
893
1088
  clearFailedTranscodesForTest,
1089
+ sourceFolderKind,
1090
+ proxyCopyProgress,
1091
+ retryProxyCopy,
894
1092
  proxyActivityMark,
895
1093
  resolveProxy,
896
1094
  resolveProxies
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  PROXY_PARAMS_VERSION,
3
3
  resolveProxy
4
- } from "./chunk-5UEDEOD2.js";
4
+ } from "./chunk-MNJZQCBL.js";
5
5
  import {
6
6
  createMediaCodecProbeCache,
7
7
  proxyVariantFor,
8
8
  recordProxyPrewarm,
9
9
  scanProjectMedia,
10
10
  shouldPrewarmProxy
11
- } from "./chunk-C2RFW2FX.js";
11
+ } from "./chunk-JKPLDWAB.js";
12
12
 
13
13
  // src/helpers/mediaProxyPreview.ts
14
14
  import { resolve } from "path";
@@ -14,7 +14,7 @@ import {
14
14
  scanProjectMedia,
15
15
  scanProjectMediaCodecMap,
16
16
  shouldPrewarmProxy
17
- } from "../chunk-C2RFW2FX.js";
17
+ } from "../chunk-JKPLDWAB.js";
18
18
  import "../chunk-QF7MFJJU.js";
19
19
  import "../chunk-SVQFE44C.js";
20
20
  import "../chunk-L3HJVTQE.js";
@@ -4,9 +4,9 @@ import {
4
4
  isAutoProxyEnabled,
5
5
  proxyEtagSalt,
6
6
  resolvePreviewMediaCodecProbeCache
7
- } from "../chunk-HZCA2O76.js";
8
- import "../chunk-5UEDEOD2.js";
9
- import "../chunk-C2RFW2FX.js";
7
+ } from "../chunk-OAHY2RQP.js";
8
+ import "../chunk-MNJZQCBL.js";
9
+ import "../chunk-JKPLDWAB.js";
10
10
  import "../chunk-QF7MFJJU.js";
11
11
  import "../chunk-SVQFE44C.js";
12
12
  import "../chunk-L3HJVTQE.js";
@@ -59,6 +59,11 @@ interface ProxyProgress {
59
59
  /** Test hook: forget remembered transcode failures (module state persists
60
60
  * across tests that don't reload the module). */
61
61
  declare function clearFailedTranscodesForTest(): void;
62
+ type SourceFolderKind = "icloud" | "cloud-storage" | "external-volume" | "local";
63
+ /** Where a source lives, in the terms that decide whether its file can be read and written normally. */
64
+ declare function sourceFolderKind(sourcePath: string): SourceFolderKind;
65
+ declare function proxyCopyProgress(projectDir: string, absoluteSourcePath: string, variant: ProxyVariant, box?: PreviewProxyBox): number | undefined;
66
+ declare function retryProxyCopy(projectDir: string, absoluteSourcePath: string, variant: ProxyVariant, box?: PreviewProxyBox): void;
62
67
  /** Null while this project has a copy in progress; otherwise a mark that moves when any copy finishes or is refused. */
63
68
  declare function proxyActivityMark(projectDir: string): string | null;
64
69
  /**
@@ -89,4 +94,4 @@ interface ResolveProxyOptions {
89
94
  onProgress?: (progress: ProxyProgress) => void;
90
95
  }
91
96
 
92
- export { CACHE_DIR_NAME, DEFAULT_PROXY_WAIT_TIMEOUT_MS, FfmpegMissingFilterError, FfmpegUnavailableError, PROXY_CHUNK_SECONDS, PROXY_PARAMS_VERSION, PROXY_PENDING_RETRY_AFTER_SECONDS, ProxyCapacityError, type ProxyProgress, ProxySourceOutsideProjectError, ProxyTranscodeError, ProxyWaitTimeoutError, type ResolveProxyOptions, TRANSCODE_STALL_MS, clearFailedTranscodesForTest, getProxyCachePath, proxyActivityMark, resolveProxies, resolveProxy, waitForProxy };
97
+ export { CACHE_DIR_NAME, DEFAULT_PROXY_WAIT_TIMEOUT_MS, FfmpegMissingFilterError, FfmpegUnavailableError, PROXY_CHUNK_SECONDS, PROXY_PARAMS_VERSION, PROXY_PENDING_RETRY_AFTER_SECONDS, ProxyCapacityError, type ProxyProgress, ProxySourceOutsideProjectError, ProxyTranscodeError, ProxyWaitTimeoutError, type ResolveProxyOptions, type SourceFolderKind, TRANSCODE_STALL_MS, clearFailedTranscodesForTest, getProxyCachePath, proxyActivityMark, proxyCopyProgress, resolveProxies, resolveProxy, retryProxyCopy, sourceFolderKind, waitForProxy };
@@ -14,11 +14,14 @@ import {
14
14
  clearFailedTranscodesForTest,
15
15
  getProxyCachePath,
16
16
  proxyActivityMark,
17
+ proxyCopyProgress,
17
18
  resolveProxies,
18
19
  resolveProxy,
20
+ retryProxyCopy,
21
+ sourceFolderKind,
19
22
  waitForProxy
20
- } from "../chunk-5UEDEOD2.js";
21
- import "../chunk-C2RFW2FX.js";
23
+ } from "../chunk-MNJZQCBL.js";
24
+ import "../chunk-JKPLDWAB.js";
22
25
  import "../chunk-QF7MFJJU.js";
23
26
  import "../chunk-SVQFE44C.js";
24
27
  import "../chunk-L3HJVTQE.js";
@@ -38,7 +41,10 @@ export {
38
41
  clearFailedTranscodesForTest,
39
42
  getProxyCachePath,
40
43
  proxyActivityMark,
44
+ proxyCopyProgress,
41
45
  resolveProxies,
42
46
  resolveProxy,
47
+ retryProxyCopy,
48
+ sourceFolderKind,
43
49
  waitForProxy
44
50
  };
package/dist/index.js CHANGED
@@ -82,7 +82,7 @@ import {
82
82
  isAutoProxyEnabled,
83
83
  proxyEtagSalt,
84
84
  resolvePreviewMediaCodecProbeCache
85
- } from "./chunk-HZCA2O76.js";
85
+ } from "./chunk-OAHY2RQP.js";
86
86
  import {
87
87
  CACHE_DIR_NAME,
88
88
  PROXY_PENDING_RETRY_AFTER_SECONDS,
@@ -91,9 +91,11 @@ import {
91
91
  ProxyWaitTimeoutError,
92
92
  cleanupProxyCache,
93
93
  proxyActivityMark,
94
+ proxyCopyProgress,
94
95
  resolveProxy,
96
+ retryProxyCopy,
95
97
  waitForProxy
96
- } from "./chunk-5UEDEOD2.js";
98
+ } from "./chunk-MNJZQCBL.js";
97
99
  import {
98
100
  PROXY_VARIANT_CONFIG,
99
101
  decideMediaProxyEligibility,
@@ -102,7 +104,7 @@ import {
102
104
  probeMediaMetadata,
103
105
  recordProxyRequest,
104
106
  resolveProxyVariantRequest
105
- } from "./chunk-C2RFW2FX.js";
107
+ } from "./chunk-JKPLDWAB.js";
106
108
  import {
107
109
  folderGone,
108
110
  isInHiddenOrVendorDir,
@@ -3888,6 +3890,8 @@ import {
3888
3890
  import {
3889
3891
  PREVIEW_PROXY_BOX_PARAM,
3890
3892
  PREVIEW_PROXY_LIGHT_PARAM,
3893
+ PREVIEW_PROXY_PROGRESS_HEADER,
3894
+ PREVIEW_PROXY_RETRY_PARAM,
3891
3895
  parsePreviewProxyBox
3892
3896
  } from "@hyperframes/core";
3893
3897
  import {
@@ -4539,12 +4543,17 @@ ${etag}`;
4539
4543
  let servedContentType = contentType;
4540
4544
  if (proxyVariant !== void 0) {
4541
4545
  try {
4546
+ if (c.req.query(PREVIEW_PROXY_RETRY_PARAM) !== void 0) {
4547
+ retryProxyCopy(project.dir, file, proxyVariant, proxyBox);
4548
+ }
4542
4549
  servedPath = await waitForProxy(resolveProxy(project.dir, file, proxyVariant, proxyBox), 0);
4543
4550
  } catch (err) {
4544
4551
  if (err instanceof ProxyWaitTimeoutError) {
4552
+ const progress = proxyCopyProgress(project.dir, file, proxyVariant, proxyBox);
4545
4553
  return c.text("media proxy is being made", 202, {
4546
- "Retry-After": String(PROXY_PENDING_RETRY_AFTER_SECONDS),
4547
- "Cache-Control": "no-store"
4554
+ "Retry-After": progress === void 0 ? String(PROXY_PENDING_RETRY_AFTER_SECONDS) : "1",
4555
+ "Cache-Control": "no-store",
4556
+ ...progress !== void 0 && { [PREVIEW_PROXY_PROGRESS_HEADER]: progress.toFixed(3) }
4548
4557
  });
4549
4558
  }
4550
4559
  if (err instanceof ProxyCapacityError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.8.201",
3
+ "version": "0.8.203",
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.201",
88
- "@hyperframes/parsers": "0.8.201"
87
+ "@hyperframes/core": "0.8.203",
88
+ "@hyperframes/parsers": "0.8.203"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/node": "^25.0.10",