@hyperframes/studio-server 0.8.202 → 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,
@@ -185,6 +185,18 @@ async function contentCopyKey(sourcePath, version, variant, box, signal) {
185
185
  }
186
186
  return digest(hash.digest("hex"), version, variant, boxPart(box));
187
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
+ }
188
200
  function fastIndexPath(cachePath) {
189
201
  return join2(dirname(cachePath), `.fast-${basename(cachePath, extname2(cachePath))}`);
190
202
  }
@@ -515,11 +527,53 @@ function proxyScaleFilter(box) {
515
527
  return `scale=${width}:${height}`;
516
528
  }
517
529
  var STDERR_ERROR_LINE = /error|invalid|denied|no space|not found|unknown/i;
518
- function stderrReason(stderrTail) {
519
- const lines = stderrTail.split("\n").map((line2) => line2.trim()).filter(Boolean);
520
- const errorLines = lines.filter((line2) => STDERR_ERROR_LINE.test(line2));
521
- const line = errorLines.at(-1) ?? lines.at(-1);
522
- 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;
523
577
  }
524
578
  function chunkRanges(durationSeconds) {
525
579
  if (!durationSeconds || durationSeconds <= PROXY_CHUNK_SECONDS * 1.5) return null;
@@ -736,24 +790,28 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
736
790
  onEncoded
737
791
  );
738
792
  const ranges = chunkRanges(totalSeconds);
739
- if (!ranges) {
740
- 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,
741
805
  outputPath,
742
- void 0,
806
+ encode,
807
+ (listPath, target) => spawnFfmpeg(ffmpegPath, concatArgs(listPath, target, variant), CONCAT_TIMEOUT_MS, signal),
743
808
  signal,
744
- (encodedSeconds) => onProgress({ doneSeconds: encodedSeconds, totalSeconds })
809
+ gate,
810
+ (doneSeconds) => onProgress({ doneSeconds, totalSeconds })
745
811
  );
812
+ } catch (err) {
813
+ throw withSource(err, () => describeSource(sourcePath, metadata));
746
814
  }
747
- await encodeInChunks(
748
- ranges,
749
- chunkPrefix,
750
- outputPath,
751
- encode,
752
- (listPath, target) => spawnFfmpeg(ffmpegPath, concatArgs(listPath, target, variant), CONCAT_TIMEOUT_MS, signal),
753
- signal,
754
- gate,
755
- (doneSeconds) => onProgress({ doneSeconds, totalSeconds })
756
- );
757
815
  }
758
816
  function concatArgs(listPath, target, variant) {
759
817
  return [
@@ -834,7 +892,7 @@ function spawnFfmpeg(ffmpegPath, args, stallMs, signal, onProgress) {
834
892
  } else {
835
893
  reject(
836
894
  new ProxyTranscodeError(
837
- `ffmpeg exited with code ${code}${stderrReason(stderrTail)}`,
895
+ `ffmpeg exited with code ${code}${stderrReason(stderrTail, args)}`,
838
896
  code,
839
897
  stderrTail
840
898
  )
@@ -893,6 +951,15 @@ async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath,
893
951
  releaseSlot();
894
952
  }
895
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
+ }
896
963
  var proxyActivity = 0;
897
964
  function proxyActivityMark(projectDir) {
898
965
  if (!existsSync4(projectDir)) return String(proxyActivity);
@@ -1019,6 +1086,9 @@ export {
1019
1086
  waitForProxy,
1020
1087
  getProxyCachePath,
1021
1088
  clearFailedTranscodesForTest,
1089
+ sourceFolderKind,
1090
+ proxyCopyProgress,
1091
+ retryProxyCopy,
1022
1092
  proxyActivityMark,
1023
1093
  resolveProxy,
1024
1094
  resolveProxies
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  PROXY_PARAMS_VERSION,
3
3
  resolveProxy
4
- } from "./chunk-RX6VALW3.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-2MJKVMZU.js";
8
- import "../chunk-RX6VALW3.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-RX6VALW3.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-2MJKVMZU.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-RX6VALW3.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.202",
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.202",
88
- "@hyperframes/parsers": "0.8.202"
87
+ "@hyperframes/core": "0.8.203",
88
+ "@hyperframes/parsers": "0.8.203"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/node": "^25.0.10",