@hyperframes/studio-server 0.8.208 → 0.8.210

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.
@@ -204,14 +204,15 @@ function claim(file) {
204
204
  const draft = `${file}-${process.pid}-${randomUUID2()}.tmp`;
205
205
  writeFileSync(draft, String(process.pid));
206
206
  try {
207
- linkSync(draft, file);
208
- recordStart(file);
209
- return true;
210
- } catch (error) {
211
- const code = error.code ?? "";
212
- if (code === "EEXIST") return false;
213
- if (!NO_HARD_LINKS.has(code)) throw error;
214
- return createExclusively(file);
207
+ try {
208
+ linkSync(draft, file);
209
+ } catch (error) {
210
+ const code = error.code ?? "";
211
+ if (code === "EEXIST") return false;
212
+ if (!NO_HARD_LINKS.has(code)) throw error;
213
+ return createExclusively(file);
214
+ }
215
+ return recordOwnership(file);
215
216
  } finally {
216
217
  rmSync(draft, { force: true });
217
218
  }
@@ -219,12 +220,20 @@ function claim(file) {
219
220
  function createExclusively(file) {
220
221
  try {
221
222
  writeFileSync(file, String(process.pid), { flag: "wx" });
222
- recordStart(file);
223
- return true;
224
223
  } catch (error) {
225
224
  if (error.code !== "EEXIST") throw error;
226
225
  return false;
227
226
  }
227
+ return recordOwnership(file);
228
+ }
229
+ function recordOwnership(file) {
230
+ try {
231
+ recordStart(file);
232
+ return true;
233
+ } catch (error) {
234
+ releaseOwn(file);
235
+ throw error;
236
+ }
228
237
  }
229
238
  function releaseOwn(file) {
230
239
  if (ownerOf(file) !== process.pid) return;
@@ -524,11 +533,12 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
524
533
 
525
534
  // src/history/blobStore.ts
526
535
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
527
- import { constants, createReadStream, createWriteStream, renameSync } from "fs";
528
- import { copyFile, mkdir, readdir, readFile, rename, rm, stat } from "fs/promises";
536
+ import { constants, createReadStream, createWriteStream } from "fs";
537
+ import { copyFile, mkdir, readdir, readFile, rm, stat } from "fs/promises";
529
538
  import { dirname, join as join2 } from "path";
530
539
  import { pipeline } from "stream/promises";
531
540
  import { atomicTempPath } from "@hyperframes/core/atomic-file";
541
+ import { renameWithRetry, renameWithRetryAsync } from "@hyperframes/core/rename-retry";
532
542
  var BLOB_HASH = /^[0-9a-f]{64}$/;
533
543
  function blobPath(dir, hash) {
534
544
  if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
@@ -554,7 +564,7 @@ async function cloneOrCopy(from, to, beforeReplace) {
554
564
  try {
555
565
  await copyFile(from, temp, constants.COPYFILE_FICLONE);
556
566
  beforeReplace?.();
557
- renameSync(temp, to);
567
+ renameWithRetry(temp, to);
558
568
  } catch (error) {
559
569
  await rm(temp, { force: true });
560
570
  throw error;
@@ -610,7 +620,7 @@ async function openBlobStore(dir) {
610
620
  }
611
621
  await mkdir(dirname(blobPath(dir, hash)), { recursive: true });
612
622
  const size = (await stat(temp)).size;
613
- await rename(temp, blobPath(dir, hash));
623
+ await renameWithRetryAsync(temp, blobPath(dir, hash));
614
624
  sizes.set(hash, size);
615
625
  total += size;
616
626
  return hash;
@@ -634,7 +644,7 @@ async function openBlobStore(dir) {
634
644
  }
635
645
  const to = blobPath(asideDir, hash);
636
646
  await mkdir(dirname(to), { recursive: true });
637
- const moved = remove && await rename(absPath, to).then(
647
+ const moved = remove && await renameWithRetryAsync(absPath, to).then(
638
648
  () => true,
639
649
  () => false
640
650
  );
@@ -644,7 +654,7 @@ async function openBlobStore(dir) {
644
654
  await rm(temp, { force: true });
645
655
  throw error;
646
656
  });
647
- await rename(temp, to);
657
+ await renameWithRetryAsync(temp, to);
648
658
  }
649
659
  asideSizes.set(hash, (await stat(to)).size);
650
660
  holds.set(hash, /* @__PURE__ */ new Set([path]));
@@ -699,7 +709,7 @@ function uncopiedOnlyHashes(log, sizeOf) {
699
709
 
700
710
  // src/history/pruneHistories.ts
701
711
  import { randomUUID as randomUUID5 } from "crypto";
702
- import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
712
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync, statSync as statSync3, writeFileSync as writeFileSync3 } from "fs";
703
713
  import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
704
714
  import { tmpdir } from "os";
705
715
  import { dirname as dirname2, join as join4 } from "path";
@@ -863,7 +873,7 @@ function recheckAndMoveAway(historyRoot, id, { now, dryRun, tempDir }) {
863
873
  if (projectDir === null) return null;
864
874
  if (dryRun) return { projectDir, trash: null };
865
875
  const trash = join4(historyRoot, `${PRUNED_PREFIX}${id}-${randomUUID5()}`);
866
- renameSync2(home, trash);
876
+ renameSync(home, trash);
867
877
  return { projectDir, trash };
868
878
  }
869
879
  function abandonedProject(home, id, now, tempDir) {
@@ -922,7 +932,7 @@ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs }
922
932
 
923
933
  // src/history/videoState.ts
924
934
  import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
925
- import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync8, renameSync as renameSync3, statSync as statSync4 } from "fs";
935
+ import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync8, renameSync as renameSync2, statSync as statSync4 } from "fs";
926
936
  import { isAbsolute as isAbsolute4, join as join7, resolve as resolve6 } from "path";
927
937
  import { replaceFileAtomically as replaceFileAtomically4 } from "@hyperframes/core/atomic-file";
928
938
 
@@ -1323,7 +1333,7 @@ async function updateState(dir, update) {
1323
1333
  state = readState(dir);
1324
1334
  } catch (error) {
1325
1335
  if (!(error instanceof SyntaxError)) throw error;
1326
- renameSync3(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
1336
+ renameSync2(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
1327
1337
  state = emptyState();
1328
1338
  }
1329
1339
  update(state);
@@ -1,3 +1,6 @@
1
+ import {
2
+ probeAudioTrack
3
+ } from "./chunk-X73QTST2.js";
1
4
  import {
2
5
  resolveWithinProject
3
6
  } from "./chunk-QF7MFJJU.js";
@@ -318,16 +321,29 @@ function updateAudioVolume(html, id, volume) {
318
321
  }
319
322
  return html.slice(0, insertion) + ` data-volume="${value}"` + html.slice(insertion);
320
323
  }
321
- function loudnessMeasureArgs(file, tag) {
324
+ function loudnessMeasureArgs(file, tag, stream) {
322
325
  const args = ["-hide_banner", "-nostats"];
323
326
  if (tag.mediaStart > 0) args.push("-ss", String(tag.mediaStart));
324
327
  const sourceSpan = tag.duration === null ? null : tag.duration * (tag.playbackRate ?? 1);
325
328
  if (sourceSpan !== null && sourceSpan > 0) args.push("-t", String(sourceSpan));
326
- args.push("-i", file, "-map", "0:a:0", "-vn", "-af", "ebur128=peak=true", "-f", "null", "-");
329
+ args.push(
330
+ "-i",
331
+ file,
332
+ "-map",
333
+ `0:a:${stream}`,
334
+ "-vn",
335
+ "-af",
336
+ "ebur128=peak=true",
337
+ "-f",
338
+ "null",
339
+ "-"
340
+ );
327
341
  return args;
328
342
  }
329
343
  async function measureAudio(ffmpegPath, file, tag) {
330
- const result = await execFileAsync(ffmpegPath, loudnessMeasureArgs(file, tag), {
344
+ const track = await probeAudioTrack(file);
345
+ if (!track) throw new Error("no audio stream to measure");
346
+ const result = await execFileAsync(ffmpegPath, loudnessMeasureArgs(file, tag, track.stream), {
331
347
  encoding: "utf8",
332
348
  maxBuffer: 16 * 1024 * 1024,
333
349
  timeout: FFMPEG_TIMEOUT_MS,
@@ -1,3 +1,6 @@
1
+ import {
2
+ probeMediaMetadata
3
+ } from "./chunk-X73QTST2.js";
1
4
  import {
2
5
  realpath
3
6
  } from "./chunk-QF7MFJJU.js";
@@ -6,6 +9,34 @@ import {
6
9
  import { scanHtmlOpeningTags, decodeAuthoredAttribute } from "@hyperframes/parsers";
7
10
  import { statSync } from "fs";
8
11
  import { sep } from "path";
12
+ import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
13
+
14
+ // src/helpers/hdrFilters.ts
15
+ import { spawn } from "child_process";
16
+ var checks = /* @__PURE__ */ new Map();
17
+ function ffmpegHasHdrFilters(ffmpegPath) {
18
+ const known = checks.get(ffmpegPath);
19
+ if (known) return known;
20
+ const check = new Promise((resolveCheck) => {
21
+ const proc = spawn(ffmpegPath, ["-hide_banner", "-filters"], {
22
+ stdio: ["ignore", "pipe", "pipe"],
23
+ windowsHide: true
24
+ });
25
+ let stdout = "";
26
+ proc.stdout?.on("data", (chunk) => {
27
+ stdout += chunk.toString();
28
+ });
29
+ proc.on("error", () => resolveCheck(false));
30
+ proc.on(
31
+ "close",
32
+ (code) => resolveCheck(code === 0 && /\bzscale\b/.test(stdout) && /\btonemap\b/.test(stdout))
33
+ );
34
+ });
35
+ checks.set(ffmpegPath, check);
36
+ return check;
37
+ }
38
+
39
+ // src/helpers/mediaCodecMap.ts
9
40
  import { rewriteAssetPath } from "@hyperframes/parsers/asset-paths";
10
41
  import {
11
42
  cleanAssetUrl,
@@ -16,176 +47,6 @@ import {
16
47
  import {
17
48
  isHeavyForPreview
18
49
  } from "@hyperframes/core";
19
-
20
- // src/helpers/mediaMetadata.ts
21
- import { execFile } from "child_process";
22
- import { extname } from "path";
23
- import {
24
- firstFrameColourArgs,
25
- parseFirstFrameColour
26
- } from "@hyperframes/core";
27
- import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
28
- import { videoStreamHasAlpha } from "@hyperframes/parsers/media-alpha";
29
- var execFileRunner = (command, args, options) => new Promise((resolvePromise) => {
30
- execFile(
31
- command,
32
- args,
33
- { timeout: options?.timeout, maxBuffer: options?.maxBuffer, windowsHide: true },
34
- (error, stdout, stderr) => {
35
- if (error && error.code === "ENOENT") {
36
- resolvePromise({ status: null, stdout: "", stderr: "", error });
37
- return;
38
- }
39
- if (error) {
40
- const status = typeof error.code === "number" ? error.code : 1;
41
- resolvePromise({ status, stdout: stdout ?? "", stderr: stderr ?? "" });
42
- return;
43
- }
44
- resolvePromise({ status: 0, stdout: stdout ?? "", stderr: stderr ?? "" });
45
- }
46
- );
47
- });
48
- var VIDEO_EXT = /* @__PURE__ */ new Set([
49
- ".mp4",
50
- ".mov",
51
- ".webm",
52
- ".mkv",
53
- ".avi",
54
- ".m4v",
55
- ".mxf",
56
- ".mts",
57
- ".m2ts",
58
- ".ts"
59
- ]);
60
- var IMAGE_EXT = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
61
- var AUDIO_EXT = /* @__PURE__ */ new Set([".mp3", ".wav", ".ogg", ".m4a", ".aac", ".flac"]);
62
- function lower(value) {
63
- return value?.toLowerCase() ?? "";
64
- }
65
- function inferKindFromPath(path) {
66
- const ext = extname(path).toLowerCase();
67
- if (VIDEO_EXT.has(ext)) return "video";
68
- if (IMAGE_EXT.has(ext)) return "image";
69
- if (AUDIO_EXT.has(ext)) return "audio";
70
- return "unknown";
71
- }
72
- function colorLabel(input) {
73
- if (input.isHdr) {
74
- if (input.hdrTransfer === "pq") return "HDR PQ";
75
- if (input.hdrTransfer === "hlg") return "HDR HLG";
76
- return "HDR";
77
- }
78
- if (input.colorPrimaries.includes("bt709") || input.colorSpace.includes("bt709") || input.colorTransfer.includes("bt709")) {
79
- return "SDR Rec.709";
80
- }
81
- return "SDR/unknown";
82
- }
83
- function classifyMediaColor(stream) {
84
- const colorPrimaries = lower(stream?.color_primaries);
85
- const colorSpace = lower(stream?.color_space);
86
- const colorTransfer = lower(stream?.color_transfer);
87
- const isHdr = colorPrimaries.includes("bt2020") || colorSpace.includes("bt2020") || colorTransfer === "smpte2084" || colorTransfer === "arib-std-b67";
88
- const hdrTransfer = isHdr ? colorTransfer === "smpte2084" ? "pq" : colorTransfer === "arib-std-b67" ? "hlg" : "unknown" : null;
89
- return {
90
- dynamicRange: stream ? isHdr ? "hdr" : "sdr" : "unknown",
91
- hdrTransfer,
92
- label: stream ? colorLabel({ isHdr, hdrTransfer, colorPrimaries, colorSpace, colorTransfer }) : "Unknown",
93
- isHdr,
94
- codecName: stream?.codec_name,
95
- profile: stream?.profile,
96
- pixelFormat: stream?.pix_fmt,
97
- colorSpace: stream?.color_space,
98
- colorTransfer: stream?.color_transfer,
99
- colorPrimaries: stream?.color_primaries,
100
- bitsPerRawSample: stream?.bits_per_raw_sample
101
- };
102
- }
103
- async function probeFirstFrameColour(filePath, runner = execFileRunner) {
104
- const ffmpegPath = findFfBinary("ffmpeg", { configuredMustExist: true }) ?? (runner === execFileRunner ? void 0 : "ffmpeg");
105
- if (!ffmpegPath) return {};
106
- const result = await runner(ffmpegPath, firstFrameColourArgs(filePath), {
107
- timeout: 15e3,
108
- maxBuffer: 1024 * 1024
109
- });
110
- return result.status === 0 ? parseFirstFrameColour(String(result.stderr)) : {};
111
- }
112
- function bitDepthOf(stream) {
113
- const raw = Number(stream.bits_per_raw_sample);
114
- if (Number.isFinite(raw) && raw > 0) return raw;
115
- const suffixed = /(\d{1,3})(?:le|be)$/.exec(stream.pix_fmt ?? "");
116
- return suffixed ? Number(suffixed[1]) : 8;
117
- }
118
- function sourceFactsOf(stream, formatBitrate) {
119
- if (!stream?.width || !stream.height) return void 0;
120
- const bitrate = Number(stream.bit_rate ?? formatBitrate);
121
- return {
122
- width: stream.width,
123
- height: stream.height,
124
- bitDepth: bitDepthOf(stream),
125
- bitrate: Number.isFinite(bitrate) ? bitrate : 0
126
- };
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
- }
152
- async function probeMediaMetadata(filePath, runner = execFileRunner) {
153
- const kind = inferKindFromPath(filePath);
154
- if (kind === "audio" || kind === "unknown") {
155
- return { kind, color: classifyMediaColor(null) };
156
- }
157
- const ffprobePath = findFfBinary("ffprobe", { configuredMustExist: true }) ?? (runner === execFileRunner ? void 0 : "ffprobe");
158
- if (!ffprobePath) {
159
- return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
160
- }
161
- const result = await runner(
162
- ffprobePath,
163
- [
164
- "-v",
165
- "error",
166
- "-show_entries",
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",
168
- "-of",
169
- "json",
170
- "--",
171
- filePath
172
- ],
173
- { timeout: 15e3, maxBuffer: 1024 * 1024 }
174
- );
175
- if (result.error?.code === "ENOENT") {
176
- return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
177
- }
178
- if (result.status !== 0) {
179
- return { kind, color: classifyMediaColor(null), probeError: "ffprobe failed" };
180
- }
181
- try {
182
- return metadataFromProbe(kind, JSON.parse(String(result.stdout || "{}")));
183
- } catch {
184
- return { kind, color: classifyMediaColor(null), probeError: "ffprobe returned invalid json" };
185
- }
186
- }
187
-
188
- // src/helpers/mediaCodecMap.ts
189
50
  var BROWSER_HOSTILE_CODECS = {
190
51
  // HEVC decode is platform-bound, not absent: macOS Chrome answers
191
52
  // canPlayType with "probably" and keeps the source, while Chrome on
@@ -258,12 +119,13 @@ function decideMediaProxyEligibility(facts, lightBox) {
258
119
  }
259
120
  return { eligible: false, reason: "browser_safe_codec" };
260
121
  }
261
- function codecFactsFor(codecName, hasAlpha, source) {
122
+ function codecFactsFor(codecName, hasAlpha, source, toneMapsToSdr = false) {
262
123
  const hostile = hostileCodecEntry(codecName);
263
124
  return {
264
125
  codecName,
265
- browserHostile: hostile !== void 0,
266
- representativeMime: hostile?.representativeMime ?? null,
126
+ browserHostile: hostile !== void 0 || toneMapsToSdr,
127
+ // An HDR clip is never 'playable' for preview, whatever its codec: it takes the SDR copy the export uses.
128
+ representativeMime: toneMapsToSdr ? null : hostile?.representativeMime ?? null,
267
129
  hasAlpha,
268
130
  ...source && { source }
269
131
  };
@@ -274,7 +136,22 @@ async function probeCodecFacts(filePath, runner) {
274
136
  if (metadata.kind !== "video") return null;
275
137
  const codecName = metadata.color.codecName;
276
138
  if (!codecName) return null;
277
- return codecFactsFor(codecName, metadata.hasAlpha === true, metadata.source);
139
+ const hasAlpha = metadata.hasAlpha === true;
140
+ const toneMaps = !hasAlpha && await toneMapsHdr(metadata.color.hdrTransfer);
141
+ return codecFactsFor(codecName, hasAlpha, metadata.source, toneMaps);
142
+ }
143
+ var warnedNoHdrFilters = false;
144
+ async function toneMapsHdr(hdrTransfer) {
145
+ if (hdrTransfer !== "pq" && hdrTransfer !== "hlg") return false;
146
+ const ffmpegPath = findFfBinary("ffmpeg");
147
+ if (ffmpegPath && await ffmpegHasHdrFilters(ffmpegPath)) return true;
148
+ if (!warnedNoHdrFilters) {
149
+ warnedNoHdrFilters = true;
150
+ console.warn(
151
+ `[hyperframes] A video in this project is HDR, but this ffmpeg has no zscale/tonemap (libzimg), so the preview plays it as the browser shows it and will not match an SDR export. Install an ffmpeg with libzimg.`
152
+ );
153
+ }
154
+ return false;
278
155
  }
279
156
  function createMediaCodecProbeCache() {
280
157
  return /* @__PURE__ */ new Map();
@@ -368,8 +245,7 @@ async function scanProjectMediaCodecMap(projectDir, htmlSources, options = {}) {
368
245
  }
369
246
 
370
247
  export {
371
- probeFirstFrameColour,
372
- probeMediaMetadata,
248
+ ffmpegHasHdrFilters,
373
249
  BROWSER_HOSTILE_CODECS,
374
250
  shouldPrewarmProxy,
375
251
  mediaProxyDemand,
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  PROXY_VARIANT_CONFIG,
3
+ ffmpegHasHdrFilters
4
+ } from "./chunk-FUTDRYQK.js";
5
+ import {
3
6
  probeFirstFrameColour,
4
7
  probeMediaMetadata
5
- } from "./chunk-JKPLDWAB.js";
8
+ } from "./chunk-X73QTST2.js";
6
9
  import {
7
10
  mkdirWithinProject,
8
11
  realProjectRoot,
@@ -404,9 +407,8 @@ var ProxyDroppedError = class extends ProxyTranscodeError {
404
407
  var activeTranscodes = 0;
405
408
  var waitQueue = [];
406
409
  function enqueue(job) {
407
- const firstNormal = job.priority ? waitQueue.findIndex((queued) => !queued.priority) : -1;
408
- if (firstNormal === -1) waitQueue.push(job);
409
- else waitQueue.splice(firstNormal, 0, job);
410
+ if (job.priority) waitQueue.unshift(job);
411
+ else waitQueue.push(job);
410
412
  }
411
413
  function dequeue(job) {
412
414
  const index = waitQueue.indexOf(job);
@@ -442,7 +444,6 @@ function releaseSlot() {
442
444
  waitQueue.shift()?.queued?.start();
443
445
  }
444
446
  function prioritize(job) {
445
- if (job.priority) return;
446
447
  job.priority = true;
447
448
  if (!job.queued) return;
448
449
  dequeue(job);
@@ -479,29 +480,8 @@ function markCacheEntryUsed(cachePath) {
479
480
  }
480
481
  }
481
482
  var failedTranscodes = /* @__PURE__ */ new Map();
482
- var hdrFilterCheck;
483
- function ensureHdrFilters(ffmpegPath) {
484
- if (hdrFilterCheck?.ffmpegPath === ffmpegPath) return hdrFilterCheck.promise;
485
- const promise = new Promise((resolveCheck, rejectCheck) => {
486
- const proc = spawn(ffmpegPath, ["-hide_banner", "-filters"], {
487
- stdio: ["ignore", "pipe", "pipe"],
488
- windowsHide: true
489
- });
490
- let stdout = "";
491
- proc.stdout?.on("data", (chunk) => {
492
- stdout += chunk.toString();
493
- });
494
- proc.on("error", () => rejectCheck(new FfmpegMissingFilterError()));
495
- proc.on("close", (code) => {
496
- if (code !== 0 || !/\bzscale\b/.test(stdout) || !/\btonemap\b/.test(stdout)) {
497
- rejectCheck(new FfmpegMissingFilterError());
498
- } else {
499
- resolveCheck();
500
- }
501
- });
502
- });
503
- hdrFilterCheck = { ffmpegPath, promise };
504
- return promise;
483
+ async function ensureHdrFilters(ffmpegPath) {
484
+ if (!await ffmpegHasHdrFilters(ffmpegPath)) throw new FfmpegMissingFilterError();
505
485
  }
506
486
  function rememberFailure(cachePath, error) {
507
487
  const ttlMs = error instanceof FfmpegUnavailableError || error instanceof FfmpegMissingFilterError ? ENVIRONMENT_FAILURE_TTL_MS : FAILURE_CACHE_TTL_MS;
@@ -761,6 +741,10 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
761
741
  const videoFilter = await buildVideoFilter(ffmpegPath, sourcePath, metadata, keepsAlpha, box);
762
742
  const alphaDecoder = keepsAlpha ? alphaDecoderFor(metadata.color.codecName) : null;
763
743
  const decoderArgs = alphaDecoder ? ["-c:v", alphaDecoder] : [];
744
+ const streamArgs = [
745
+ ...["-map", "0:V:0"],
746
+ ...metadata.audioStream === void 0 ? [] : ["-map", `0:a:${metadata.audioStream}`]
747
+ ];
764
748
  const totalSeconds = metadata.durationSeconds;
765
749
  const gate = keepsAlpha ? ALPHA_PIECE_GATE : UNGATED;
766
750
  const vp8Threads = (pieces) => [
@@ -778,6 +762,7 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
778
762
  ...decoderArgs,
779
763
  "-i",
780
764
  sourcePath,
765
+ ...streamArgs,
781
766
  ...range ? ["-t", String(range.lengthSeconds)] : [],
782
767
  "-vf",
783
768
  videoFilter,
@@ -0,0 +1,192 @@
1
+ // src/helpers/mediaMetadata.ts
2
+ import { execFile } from "child_process";
3
+ import { extname } from "path";
4
+ import {
5
+ firstFrameColourArgs,
6
+ parseFirstFrameColour
7
+ } from "@hyperframes/core";
8
+ import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
9
+ import { videoStreamHasAlpha } from "@hyperframes/parsers/media-alpha";
10
+ import { decodableAudioStream } from "@hyperframes/parsers/media-streams";
11
+ var execFileRunner = (command, args, options) => new Promise((resolvePromise) => {
12
+ execFile(
13
+ command,
14
+ args,
15
+ { timeout: options?.timeout, maxBuffer: options?.maxBuffer, windowsHide: true },
16
+ (error, stdout, stderr) => {
17
+ if (error && error.code === "ENOENT") {
18
+ resolvePromise({ status: null, stdout: "", stderr: "", error });
19
+ return;
20
+ }
21
+ if (error) {
22
+ const status = typeof error.code === "number" ? error.code : 1;
23
+ resolvePromise({ status, stdout: stdout ?? "", stderr: stderr ?? "" });
24
+ return;
25
+ }
26
+ resolvePromise({ status: 0, stdout: stdout ?? "", stderr: stderr ?? "" });
27
+ }
28
+ );
29
+ });
30
+ var VIDEO_EXT = /* @__PURE__ */ new Set([
31
+ ".mp4",
32
+ ".mov",
33
+ ".webm",
34
+ ".mkv",
35
+ ".avi",
36
+ ".m4v",
37
+ ".mxf",
38
+ ".mts",
39
+ ".m2ts",
40
+ ".ts"
41
+ ]);
42
+ var IMAGE_EXT = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
43
+ var AUDIO_EXT = /* @__PURE__ */ new Set([".mp3", ".wav", ".ogg", ".m4a", ".aac", ".flac"]);
44
+ function lower(value) {
45
+ return value?.toLowerCase() ?? "";
46
+ }
47
+ function inferKindFromPath(path) {
48
+ const ext = extname(path).toLowerCase();
49
+ if (VIDEO_EXT.has(ext)) return "video";
50
+ if (IMAGE_EXT.has(ext)) return "image";
51
+ if (AUDIO_EXT.has(ext)) return "audio";
52
+ return "unknown";
53
+ }
54
+ function colorLabel(input) {
55
+ if (input.isHdr) {
56
+ if (input.hdrTransfer === "pq") return "HDR PQ";
57
+ if (input.hdrTransfer === "hlg") return "HDR HLG";
58
+ return "HDR";
59
+ }
60
+ if (input.colorPrimaries.includes("bt709") || input.colorSpace.includes("bt709") || input.colorTransfer.includes("bt709")) {
61
+ return "SDR Rec.709";
62
+ }
63
+ return "SDR/unknown";
64
+ }
65
+ function classifyMediaColor(stream) {
66
+ const colorPrimaries = lower(stream?.color_primaries);
67
+ const colorSpace = lower(stream?.color_space);
68
+ const colorTransfer = lower(stream?.color_transfer);
69
+ const isHdr = colorPrimaries.includes("bt2020") || colorSpace.includes("bt2020") || colorTransfer === "smpte2084" || colorTransfer === "arib-std-b67";
70
+ const hdrTransfer = isHdr ? colorTransfer === "smpte2084" ? "pq" : colorTransfer === "arib-std-b67" ? "hlg" : "unknown" : null;
71
+ return {
72
+ dynamicRange: stream ? isHdr ? "hdr" : "sdr" : "unknown",
73
+ hdrTransfer,
74
+ label: stream ? colorLabel({ isHdr, hdrTransfer, colorPrimaries, colorSpace, colorTransfer }) : "Unknown",
75
+ isHdr,
76
+ codecName: stream?.codec_name,
77
+ profile: stream?.profile,
78
+ pixelFormat: stream?.pix_fmt,
79
+ colorSpace: stream?.color_space,
80
+ colorTransfer: stream?.color_transfer,
81
+ colorPrimaries: stream?.color_primaries,
82
+ bitsPerRawSample: stream?.bits_per_raw_sample
83
+ };
84
+ }
85
+ async function probeFirstFrameColour(filePath, runner = execFileRunner) {
86
+ const ffmpegPath = findFfBinary("ffmpeg", { configuredMustExist: true }) ?? (runner === execFileRunner ? void 0 : "ffmpeg");
87
+ if (!ffmpegPath) return {};
88
+ const result = await runner(ffmpegPath, firstFrameColourArgs(filePath), {
89
+ timeout: 15e3,
90
+ maxBuffer: 1024 * 1024
91
+ });
92
+ return result.status === 0 ? parseFirstFrameColour(String(result.stderr)) : {};
93
+ }
94
+ function bitDepthOf(stream) {
95
+ const raw = Number(stream.bits_per_raw_sample);
96
+ if (Number.isFinite(raw) && raw > 0) return raw;
97
+ const suffixed = /(\d{1,3})(?:le|be)$/.exec(stream.pix_fmt ?? "");
98
+ return suffixed ? Number(suffixed[1]) : 8;
99
+ }
100
+ function sourceFactsOf(stream, formatBitrate) {
101
+ if (!stream?.width || !stream.height) return void 0;
102
+ const bitrate = Number(stream.bit_rate ?? formatBitrate);
103
+ return {
104
+ width: stream.width,
105
+ height: stream.height,
106
+ bitDepth: bitDepthOf(stream),
107
+ bitrate: Number.isFinite(bitrate) ? bitrate : 0
108
+ };
109
+ }
110
+ var STREAM_SUMMARY_MAX = 12;
111
+ function summarizeStream(stream) {
112
+ const flags = Object.entries(stream.disposition ?? {}).filter(([, on]) => on === 1).map(([name]) => name);
113
+ return `${stream.codec_type ?? "?"}:${stream.codec_name ?? "?"}[${flags.join(",")}]`;
114
+ }
115
+ function audioOf(streams) {
116
+ const audioStream = decodableAudioStream(streams);
117
+ return audioStream === null ? { hasAudio: false } : { hasAudio: true, audioStream };
118
+ }
119
+ function metadataFromProbe(kind, parsed) {
120
+ const stream = parsed.streams?.find((item) => {
121
+ if (kind === "image") return item.codec_type === "video";
122
+ return item.codec_type === kind && item.disposition?.attached_pic !== 1;
123
+ });
124
+ const metadata = {
125
+ kind,
126
+ color: classifyMediaColor(stream),
127
+ streams: (parsed.streams ?? []).slice(0, STREAM_SUMMARY_MAX).map(summarizeStream)
128
+ };
129
+ if (kind === "video") {
130
+ Object.assign(metadata, audioOf(parsed.streams ?? []));
131
+ metadata.hasAlpha = stream !== void 0 && videoStreamHasAlpha(stream);
132
+ const duration = Number(parsed.format?.duration);
133
+ if (Number.isFinite(duration) && duration > 0) metadata.durationSeconds = duration;
134
+ metadata.source = sourceFactsOf(stream, parsed.format?.bit_rate);
135
+ }
136
+ return metadata;
137
+ }
138
+ async function probeAudioTrack(filePath, runner = execFileRunner) {
139
+ const result = await runner(
140
+ findFfBinary("ffprobe") ?? "ffprobe",
141
+ [
142
+ ...["-v", "error", "-select_streams", "a"],
143
+ ...["-show_entries", "stream=codec_type,codec_name,channels", "-of", "json", "--", filePath]
144
+ ],
145
+ { timeout: 3e4 }
146
+ );
147
+ if (result.status !== 0) return null;
148
+ const streams = JSON.parse(String(result.stdout || "{}")).streams ?? [];
149
+ const stream = decodableAudioStream(streams);
150
+ return stream === null ? null : { stream, channels: streams[stream]?.channels ?? 0 };
151
+ }
152
+ async function probeMediaMetadata(filePath, runner = execFileRunner) {
153
+ const kind = inferKindFromPath(filePath);
154
+ if (kind === "audio" || kind === "unknown") {
155
+ return { kind, color: classifyMediaColor(null) };
156
+ }
157
+ const ffprobePath = findFfBinary("ffprobe", { configuredMustExist: true }) ?? (runner === execFileRunner ? void 0 : "ffprobe");
158
+ if (!ffprobePath) {
159
+ return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
160
+ }
161
+ const result = await runner(
162
+ ffprobePath,
163
+ [
164
+ "-v",
165
+ "error",
166
+ "-show_entries",
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",
168
+ "-of",
169
+ "json",
170
+ "--",
171
+ filePath
172
+ ],
173
+ { timeout: 15e3, maxBuffer: 1024 * 1024 }
174
+ );
175
+ if (result.error?.code === "ENOENT") {
176
+ return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
177
+ }
178
+ if (result.status !== 0) {
179
+ return { kind, color: classifyMediaColor(null), probeError: "ffprobe failed" };
180
+ }
181
+ try {
182
+ return metadataFromProbe(kind, JSON.parse(String(result.stdout || "{}")));
183
+ } catch {
184
+ return { kind, color: classifyMediaColor(null), probeError: "ffprobe returned invalid json" };
185
+ }
186
+ }
187
+
188
+ export {
189
+ probeFirstFrameColour,
190
+ probeAudioTrack,
191
+ probeMediaMetadata
192
+ };
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  PROXY_PARAMS_VERSION,
3
3
  resolveProxy
4
- } from "./chunk-MNJZQCBL.js";
4
+ } from "./chunk-KPCW4DQM.js";
5
5
  import {
6
6
  createMediaCodecProbeCache,
7
7
  proxyVariantFor,
8
8
  recordProxyPrewarm,
9
9
  scanProjectMedia,
10
10
  shouldPrewarmProxy
11
- } from "./chunk-JKPLDWAB.js";
11
+ } from "./chunk-FUTDRYQK.js";
12
12
 
13
13
  // src/helpers/mediaProxyPreview.ts
14
14
  import { resolve } from "path";
@@ -57,7 +57,7 @@ declare function updateAudioVolume(html: string, id: string, volume: number): st
57
57
  * -61.8 LUFS: output-side `-t 4` reported -33.8 LUFS, input-side reports -61.8,
58
58
  * the same as physically cutting the file first.
59
59
  */
60
- declare function loudnessMeasureArgs(file: string, tag: MeasuredWindow): string[];
60
+ declare function loudnessMeasureArgs(file: string, tag: MeasuredWindow, stream: number): string[];
61
61
  type MeasuredWindow = Pick<AudioTag, "mediaStart" | "duration"> & Partial<Pick<AudioTag, "playbackRate">>;
62
62
  declare function measureAudio(ffmpegPath: string, file: string, tag: MeasuredWindow): Promise<LoudnessMeasurement>;
63
63
  declare function requiredFfmpeg(): string;
@@ -11,7 +11,8 @@ import {
11
11
  requiredFfmpeg,
12
12
  resolveLocalAudioPath,
13
13
  updateAudioVolume
14
- } from "../chunk-QZWVJKLI.js";
14
+ } from "../chunk-EGNSA3MU.js";
15
+ import "../chunk-X73QTST2.js";
15
16
  import "../chunk-QF7MFJJU.js";
16
17
  import "../chunk-SVQFE44C.js";
17
18
  import "../chunk-L3HJVTQE.js";
@@ -14,7 +14,8 @@ import {
14
14
  scanProjectMedia,
15
15
  scanProjectMediaCodecMap,
16
16
  shouldPrewarmProxy
17
- } from "../chunk-JKPLDWAB.js";
17
+ } from "../chunk-FUTDRYQK.js";
18
+ import "../chunk-X73QTST2.js";
18
19
  import "../chunk-QF7MFJJU.js";
19
20
  import "../chunk-SVQFE44C.js";
20
21
  import "../chunk-L3HJVTQE.js";
@@ -4,9 +4,10 @@ import {
4
4
  isAutoProxyEnabled,
5
5
  proxyEtagSalt,
6
6
  resolvePreviewMediaCodecProbeCache
7
- } from "../chunk-OAHY2RQP.js";
8
- import "../chunk-MNJZQCBL.js";
9
- import "../chunk-JKPLDWAB.js";
7
+ } from "../chunk-YOCRTHW3.js";
8
+ import "../chunk-KPCW4DQM.js";
9
+ import "../chunk-FUTDRYQK.js";
10
+ import "../chunk-X73QTST2.js";
10
11
  import "../chunk-QF7MFJJU.js";
11
12
  import "../chunk-SVQFE44C.js";
12
13
  import "../chunk-L3HJVTQE.js";
@@ -20,8 +20,9 @@ import {
20
20
  retryProxyCopy,
21
21
  sourceFolderKind,
22
22
  waitForProxy
23
- } from "../chunk-MNJZQCBL.js";
24
- import "../chunk-JKPLDWAB.js";
23
+ } from "../chunk-KPCW4DQM.js";
24
+ import "../chunk-FUTDRYQK.js";
25
+ import "../chunk-X73QTST2.js";
25
26
  import "../chunk-QF7MFJJU.js";
26
27
  import "../chunk-SVQFE44C.js";
27
28
  import "../chunk-L3HJVTQE.js";
@@ -22,7 +22,7 @@ import {
22
22
  recordRenderSuccess,
23
23
  registerRenderOutput,
24
24
  retainSnapshotOutput
25
- } from "../chunk-WAMZ2Z5O.js";
25
+ } from "../chunk-BWZMWYB2.js";
26
26
  import "../chunk-QF7MFJJU.js";
27
27
  import "../chunk-SVQFE44C.js";
28
28
  import "../chunk-L3HJVTQE.js";
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ import {
62
62
  resolveProjectSignature,
63
63
  retainSnapshotOutput,
64
64
  settledFileTag
65
- } from "./chunk-WAMZ2Z5O.js";
65
+ } from "./chunk-BWZMWYB2.js";
66
66
  import {
67
67
  clearElementScreenshotIsolation,
68
68
  getElementScreenshotClip
@@ -76,13 +76,13 @@ import {
76
76
  requestedProjectPath,
77
77
  requiredFfmpeg,
78
78
  resolveLocalAudioPath
79
- } from "./chunk-QZWVJKLI.js";
79
+ } from "./chunk-EGNSA3MU.js";
80
80
  import {
81
81
  injectMediaCodecMap,
82
82
  isAutoProxyEnabled,
83
83
  proxyEtagSalt,
84
84
  resolvePreviewMediaCodecProbeCache
85
- } from "./chunk-OAHY2RQP.js";
85
+ } from "./chunk-YOCRTHW3.js";
86
86
  import {
87
87
  CACHE_DIR_NAME,
88
88
  PROXY_PENDING_RETRY_AFTER_SECONDS,
@@ -95,16 +95,19 @@ import {
95
95
  resolveProxy,
96
96
  retryProxyCopy,
97
97
  waitForProxy
98
- } from "./chunk-MNJZQCBL.js";
98
+ } from "./chunk-KPCW4DQM.js";
99
99
  import {
100
100
  PROXY_VARIANT_CONFIG,
101
101
  decideMediaProxyEligibility,
102
102
  isProxyVariantRequest,
103
103
  probeAssetCodec,
104
- probeMediaMetadata,
105
104
  recordProxyRequest,
106
105
  resolveProxyVariantRequest
107
- } from "./chunk-JKPLDWAB.js";
106
+ } from "./chunk-FUTDRYQK.js";
107
+ import {
108
+ probeAudioTrack,
109
+ probeMediaMetadata
110
+ } from "./chunk-X73QTST2.js";
108
111
  import {
109
112
  folderGone,
110
113
  isInHiddenOrVendorDir,
@@ -315,6 +318,33 @@ function insertGsapCoreBeforeHeadPlugin(html, coreTag) {
315
318
  ${html.slice(at)}`;
316
319
  }
317
320
 
321
+ // src/helpers/rootPaths.ts
322
+ import { mapStartTagsAndStyles } from "@hyperframes/core/compiler/html-document";
323
+ var ROOT_PATH = /^\/(?!\/)/;
324
+ var PATH_ATTR = /(\s(?:src|href|poster|xlink:href)\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/gi;
325
+ var CSS_ROOT_URL = /(\burl\(\s*)(["']?)(\/(?!\/)[^"')\s]*)\2/gi;
326
+ var projectRelative = (path, base) => ROOT_PATH.test(path) && path !== "/" && !path.startsWith(base) ? path.slice(1) : path;
327
+ var underBase = (path, base) => {
328
+ const relative5 = projectRelative(path, base);
329
+ return relative5 === path ? path : base + relative5;
330
+ };
331
+ function withRootPathsRelative(html, base) {
332
+ const css = (text2) => text2.replace(
333
+ CSS_ROOT_URL,
334
+ (_m, open2, quote, path) => `${open2}${quote}${projectRelative(path, base)}${quote}`
335
+ );
336
+ const attrs = (tag) => tag.replace(PATH_ATTR, (whole, name, dq, sq, bare) => {
337
+ const path = (dq ?? sq ?? bare ?? "").trim();
338
+ const moved = projectRelative(path, base);
339
+ if (moved === path) return whole;
340
+ return dq !== void 0 ? `${name}"${moved}"` : sq !== void 0 ? `${name}'${moved}'` : `${name}${moved}`;
341
+ });
342
+ return mapStartTagsAndStyles(html, {
343
+ tag: (tag) => /^<(?:a|base)[\s/>]/i.test(tag) ? tag : css(attrs(tag)),
344
+ style: css
345
+ });
346
+ }
347
+
318
348
  // src/helpers/subComposition.ts
319
349
  function rewriteRelativePaths(root, compPath, projectDir) {
320
350
  const assetExists = (path) => existsSync(join2(projectDir, path));
@@ -463,6 +493,10 @@ ${headContent}`;
463
493
  }
464
494
  if (compHeadContent) headContent += `
465
495
  ${compHeadContent}`;
496
+ if (baseHref) {
497
+ headContent = withRootPathsRelative(headContent, baseHref);
498
+ rewrittenContent = withRootPathsRelative(rewrittenContent, baseHref);
499
+ }
466
500
  headContent = stripEmbeddedRuntimeScripts(headContent);
467
501
  headContent += `
468
502
  <script ${RUNTIME_BOOTSTRAP_ATTR}="1" src="${runtimeUrl}"></script>`;
@@ -836,13 +870,17 @@ function computePeaks(floats, count) {
836
870
  const maxPeak = Math.max(...peaks, 1e-3);
837
871
  return peaks.map((p) => p / maxPeak);
838
872
  }
839
- function decodeAudioPeaks(audioPath) {
873
+ async function decodeAudioPeaks(audioPath) {
874
+ const track = await probeAudioTrack(audioPath);
875
+ if (!track) throw new Error("no audio stream");
840
876
  return new Promise((resolvePromise, reject) => {
841
877
  const proc = spawn(
842
878
  findFfBinary("ffmpeg") ?? "ffmpeg",
843
879
  [
844
880
  "-i",
845
881
  audioPath,
882
+ "-map",
883
+ `0:a:${track.stream}`,
846
884
  "-af",
847
885
  "atrim=start_sample=1152",
848
886
  "-f",
@@ -917,6 +955,7 @@ import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeF
917
955
  import { tmpdir } from "os";
918
956
  import { basename, join as join4 } from "path";
919
957
  import { FFPROBE_PATH_ENV, findFfBinary as findFfBinary2 } from "@hyperframes/parsers/ff-binaries";
958
+ import { decodableAudioStream } from "@hyperframes/parsers/media-streams";
920
959
  var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
921
960
  var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac|flac)$/i;
922
961
  var defaultRunner = spawnSync;
@@ -931,7 +970,7 @@ function validateUploadedMedia(filePath, runner = defaultRunner) {
931
970
  if (!ffprobe) return { ok: true, unchecked: FFPROBE_MISSING };
932
971
  const result = runner(
933
972
  ffprobe,
934
- ["-v", "error", "-show_entries", "stream=codec_type", "-of", "json", "--", filePath],
973
+ ["-v", "error", "-show_entries", "stream=codec_type,codec_name", "-of", "json", "--", filePath],
935
974
  { windowsHide: true }
936
975
  );
937
976
  if (result.error && process.env[FFPROBE_PATH_ENV]?.trim()) {
@@ -953,7 +992,7 @@ function checkStreams(stdout, isVideo, isAudio) {
953
992
  const parsed = JSON.parse(stdout);
954
993
  const streams = parsed.streams ?? [];
955
994
  const hasVideo = streams.some((stream) => stream.codec_type === "video");
956
- const hasAudio = streams.some((stream) => stream.codec_type === "audio");
995
+ const hasAudio = decodableAudioStream(streams) !== null;
957
996
  if (isVideo && !hasVideo) {
958
997
  return { ok: false, reason: "no supported video stream found" };
959
998
  }
@@ -3891,6 +3930,7 @@ import {
3891
3930
  import {
3892
3931
  PREVIEW_PROXY_BOX_PARAM,
3893
3932
  PREVIEW_PROXY_LIGHT_PARAM,
3933
+ PREVIEW_PROXY_PRIORITY_HEADER,
3894
3934
  PREVIEW_PROXY_PROGRESS_HEADER,
3895
3935
  PREVIEW_PROXY_RETRY_PARAM,
3896
3936
  parsePreviewProxyBox
@@ -3907,7 +3947,6 @@ import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
3907
3947
 
3908
3948
  // src/helpers/rootImportMap.ts
3909
3949
  import { findStartTags as findStartTags2, injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
3910
- var ROOT_PATH = /^\/(?!\/)/;
3911
3950
  var IMPORT_MAP_TYPE = /\btype\s*=\s*["']?importmap["']?(?=[\s/>])/i;
3912
3951
  function findImportMap(html) {
3913
3952
  for (const at of findStartTags2(html, "script")) {
@@ -3934,7 +3973,7 @@ function withRootImportMap(html, base) {
3934
3973
  return html;
3935
3974
  }
3936
3975
  if (!map || typeof map !== "object") return html;
3937
- const under = (path) => ROOT_PATH.test(path) && !path.startsWith(base) ? base + path.slice(1) : path;
3976
+ const under = (path) => underBase(path, base);
3938
3977
  const moved = (entries = {}) => Object.fromEntries(
3939
3978
  Object.entries(entries).map(([key, target]) => [key, under(String(target))])
3940
3979
  );
@@ -4352,7 +4391,7 @@ var PREVIEW_BUNDLE_OPTIONS = {
4352
4391
  var previewBaseHref = (projectId) => `/api/projects/${encodeURIComponent(projectId)}/preview/`;
4353
4392
  var withPreviewBase = (html, projectId) => {
4354
4393
  const base = previewBaseHref(projectId);
4355
- const mapped = withRootImportMap(html, base);
4394
+ const mapped = withRootImportMap(withRootPathsRelative(html, base), base);
4356
4395
  return hasBaseElement(mapped) ? mapped : injectTagsAtHeadStart2(mapped, `<base href="${base}">`);
4357
4396
  };
4358
4397
  function resolvePreviewGsapBaseUrl(adapter) {
@@ -4397,10 +4436,9 @@ function registerPreviewRoutes(api, adapter) {
4397
4436
  bundled = ensureHfIds2(diskMain.html);
4398
4437
  mainCompositionPath = diskMain.compositionPath;
4399
4438
  }
4400
- bundled = withPreviewRuntime(bundled, adapter.runtimeUrl);
4439
+ bundled = withPreviewRuntime(withPreviewBase(bundled, project.id), adapter.runtimeUrl);
4401
4440
  recordPreviewReferences(project.dir, bundled);
4402
4441
  recordPreviewBuilt(project.dir);
4403
- bundled = withPreviewBase(bundled, project.id);
4404
4442
  bundled = injectStudioPreviewAugmentations(
4405
4443
  ensureHfIds2(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
4406
4444
  adapter,
@@ -4425,9 +4463,9 @@ function registerPreviewRoutes(api, adapter) {
4425
4463
  } catch {
4426
4464
  const fallback = resolveProjectMainHtml(project.dir, project.id);
4427
4465
  if (fallback) {
4428
- const fallbackHtml = withPreviewBase(
4429
- withPreviewRuntime(ensureHfIds2(fallback.html), adapter.runtimeUrl),
4430
- project.id
4466
+ const fallbackHtml = withPreviewRuntime(
4467
+ withPreviewBase(ensureHfIds2(fallback.html), project.id),
4468
+ adapter.runtimeUrl
4431
4469
  );
4432
4470
  let fallbackAugmented = injectStudioPreviewAugmentations(
4433
4471
  await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
@@ -4595,7 +4633,11 @@ ${etag}`;
4595
4633
  if (c.req.query(PREVIEW_PROXY_RETRY_PARAM) !== void 0) {
4596
4634
  retryProxyCopy(project.dir, file, proxyVariant, proxyBox);
4597
4635
  }
4598
- servedPath = await waitForProxy(resolveProxy(project.dir, file, proxyVariant, proxyBox), 0);
4636
+ const priority = c.req.header(PREVIEW_PROXY_PRIORITY_HEADER) === "1";
4637
+ servedPath = await waitForProxy(
4638
+ resolveProxy(project.dir, file, proxyVariant, proxyBox, { priority }),
4639
+ 0
4640
+ );
4599
4641
  } catch (err) {
4600
4642
  if (err instanceof ProxyWaitTimeoutError) {
4601
4643
  const progress = proxyCopyProgress(project.dir, file, proxyVariant, proxyBox);
@@ -5873,12 +5915,10 @@ import { join as join14, resolve as resolve6 } from "path";
5873
5915
  import { isWithinProjectRoot as isWithinProjectRoot3 } from "@hyperframes/parsers/asset-resolution";
5874
5916
 
5875
5917
  // src/helpers/peakMap.ts
5876
- import { execFile as execFile2, spawn as spawn2 } from "child_process";
5877
- import { promisify } from "util";
5918
+ import { spawn as spawn2 } from "child_process";
5878
5919
  import { findFfBinary as findFfBinary4 } from "@hyperframes/parsers/ff-binaries";
5879
5920
  var PEAK_BIN_SECONDS = 0.05;
5880
5921
  var PEAK_SAMPLE_RATE = 48e3;
5881
- var execFileAsync = promisify(execFile2);
5882
5922
  var PEAK_MAP_CACHE_VERSION = "peaks-v2";
5883
5923
  function createPeakAccumulator(sampleRate = PEAK_SAMPLE_RATE, binSeconds = PEAK_BIN_SECONDS, channels = 2) {
5884
5924
  return {
@@ -5912,29 +5952,10 @@ function accumulatePeaks(acc, chunk) {
5912
5952
  function finishPeaks(acc) {
5913
5953
  return acc.bins.map((peak) => Number(peak.toFixed(4)));
5914
5954
  }
5915
- async function probeChannels(mediaPath) {
5916
- const { stdout } = await execFileAsync(
5917
- findFfBinary4("ffprobe") ?? "ffprobe",
5918
- [
5919
- "-v",
5920
- "error",
5921
- "-select_streams",
5922
- "a:0",
5923
- "-show_entries",
5924
- "stream=channels",
5925
- "-of",
5926
- "csv=p=0",
5927
- "--",
5928
- mediaPath
5929
- ],
5930
- { encoding: "utf8", timeout: 3e4, windowsHide: true }
5931
- );
5932
- const channels = Number.parseInt(stdout.trim(), 10);
5933
- if (!Number.isFinite(channels) || channels < 1) throw new Error("no audio stream");
5934
- return channels;
5935
- }
5936
5955
  async function decodePeakMap(mediaPath) {
5937
- const channels = await probeChannels(mediaPath);
5956
+ const track = await probeAudioTrack(mediaPath);
5957
+ if (!track || track.channels < 1) throw new Error("no audio stream");
5958
+ const { stream, channels } = track;
5938
5959
  return new Promise((resolvePromise, reject) => {
5939
5960
  const proc = spawn2(
5940
5961
  findFfBinary4("ffmpeg") ?? "ffmpeg",
@@ -5944,7 +5965,7 @@ async function decodePeakMap(mediaPath) {
5944
5965
  "-i",
5945
5966
  mediaPath,
5946
5967
  "-map",
5947
- "0:a:0",
5968
+ `0:a:${stream}`,
5948
5969
  "-vn",
5949
5970
  "-ar",
5950
5971
  String(PEAK_SAMPLE_RATE),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.8.208",
3
+ "version": "0.8.210",
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.208",
88
- "@hyperframes/parsers": "0.8.208"
87
+ "@hyperframes/core": "0.8.210",
88
+ "@hyperframes/parsers": "0.8.210"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/node": "^25.0.10",