@hyperframes/studio-server 0.8.207 → 0.8.209

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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,
@@ -479,29 +482,8 @@ function markCacheEntryUsed(cachePath) {
479
482
  }
480
483
  }
481
484
  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;
485
+ async function ensureHdrFilters(ffmpegPath) {
486
+ if (!await ffmpegHasHdrFilters(ffmpegPath)) throw new FfmpegMissingFilterError();
505
487
  }
506
488
  function rememberFailure(cachePath, error) {
507
489
  const ttlMs = error instanceof FfmpegUnavailableError || error instanceof FfmpegMissingFilterError ? ENVIRONMENT_FAILURE_TTL_MS : FAILURE_CACHE_TTL_MS;
@@ -761,6 +743,10 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
761
743
  const videoFilter = await buildVideoFilter(ffmpegPath, sourcePath, metadata, keepsAlpha, box);
762
744
  const alphaDecoder = keepsAlpha ? alphaDecoderFor(metadata.color.codecName) : null;
763
745
  const decoderArgs = alphaDecoder ? ["-c:v", alphaDecoder] : [];
746
+ const streamArgs = [
747
+ ...["-map", "0:V:0"],
748
+ ...metadata.audioStream === void 0 ? [] : ["-map", `0:a:${metadata.audioStream}`]
749
+ ];
764
750
  const totalSeconds = metadata.durationSeconds;
765
751
  const gate = keepsAlpha ? ALPHA_PIECE_GATE : UNGATED;
766
752
  const vp8Threads = (pieces) => [
@@ -778,6 +764,7 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
778
764
  ...decoderArgs,
779
765
  "-i",
780
766
  sourcePath,
767
+ ...streamArgs,
781
768
  ...range ? ["-t", String(range.lengthSeconds)] : [],
782
769
  "-vf",
783
770
  videoFilter,
@@ -20,7 +20,7 @@ import {
20
20
  } from "fs";
21
21
  import { homedir } from "os";
22
22
  import { dirname as dirname5, isAbsolute as isAbsolute5, join as join8, relative as relative5, resolve as resolve7, sep as sep5 } from "path";
23
- import { replaceFileAtomically as replaceFileAtomically5 } from "@hyperframes/core/atomic-file";
23
+ import { replaceFileAtomically as replaceFileAtomically6 } from "@hyperframes/core/atomic-file";
24
24
 
25
25
  // src/helpers/fileVersion.ts
26
26
  import { createHash, randomUUID } from "crypto";
@@ -103,22 +103,15 @@ var consumeFileWriteReceipt = identifyFileWrite;
103
103
  // src/helpers/renderOutputs.ts
104
104
  import { readFileSync as readFileSync2 } from "fs";
105
105
  import { isAbsolute, relative, resolve, sep } from "path";
106
- import { replaceFileAtomically } from "@hyperframes/core/atomic-file";
106
+ import { replaceFileAtomically as replaceFileAtomically2 } from "@hyperframes/core/atomic-file";
107
107
 
108
108
  // src/history/ownerLock.ts
109
109
  import { spawnSync } from "child_process";
110
110
  import { randomUUID as randomUUID2 } from "crypto";
111
- import {
112
- linkSync,
113
- mkdirSync,
114
- readFileSync,
115
- renameSync,
116
- rmSync,
117
- statSync,
118
- writeFileSync
119
- } from "fs";
111
+ import { linkSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
120
112
  import { uptime } from "os";
121
113
  import { join } from "path";
114
+ import { replaceFileAtomically } from "@hyperframes/core/atomic-file";
122
115
  var HistoryBusyError = class extends Error {
123
116
  constructor(pid, url) {
124
117
  super(`This project's history is open in another process (pid ${pid}).`);
@@ -211,14 +204,15 @@ function claim(file) {
211
204
  const draft = `${file}-${process.pid}-${randomUUID2()}.tmp`;
212
205
  writeFileSync(draft, String(process.pid));
213
206
  try {
214
- linkSync(draft, file);
215
- recordStart(file);
216
- return true;
217
- } catch (error) {
218
- const code = error.code ?? "";
219
- if (code === "EEXIST") return false;
220
- if (!NO_HARD_LINKS.has(code)) throw error;
221
- 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);
222
216
  } finally {
223
217
  rmSync(draft, { force: true });
224
218
  }
@@ -226,12 +220,20 @@ function claim(file) {
226
220
  function createExclusively(file) {
227
221
  try {
228
222
  writeFileSync(file, String(process.pid), { flag: "wx" });
229
- recordStart(file);
230
- return true;
231
223
  } catch (error) {
232
224
  if (error.code !== "EEXIST") throw error;
233
225
  return false;
234
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
+ }
235
237
  }
236
238
  function releaseOwn(file) {
237
239
  if (ownerOf(file) !== process.pid) return;
@@ -258,9 +260,7 @@ function evictDeadOwner(file) {
258
260
  }
259
261
  function advertise(urlFile, url) {
260
262
  if (!url) return rmSync(urlFile, { force: true });
261
- const draft = `${urlFile}-${process.pid}-${randomUUID2()}.tmp`;
262
- writeFileSync(draft, JSON.stringify({ pid: process.pid, url }));
263
- renameSync(draft, urlFile);
263
+ replaceFileAtomically(urlFile, JSON.stringify({ pid: process.pid, url }));
264
264
  }
265
265
  function servedBy(file, urlFile, pid) {
266
266
  try {
@@ -294,7 +294,12 @@ async function takeHistoryOwnership(home, waitMs, url) {
294
294
  if (next === "claimed") break;
295
295
  if (next === "wait") await new Promise((settle) => setTimeout(settle, 50));
296
296
  }
297
- advertise(urlFile, url);
297
+ try {
298
+ advertise(urlFile, url);
299
+ } catch (error) {
300
+ releaseOwn(file);
301
+ throw error;
302
+ }
298
303
  let held = true;
299
304
  return () => {
300
305
  if (held && servedBy(file, urlFile, process.pid)) rmSync(urlFile, { force: true });
@@ -344,7 +349,7 @@ async function registerRenderOutput(projectDir, output) {
344
349
  const release = await takeHistoryOwnership(lock, 5e3);
345
350
  try {
346
351
  const paths = [.../* @__PURE__ */ new Set([...readRenderOutputs(projectDir), path])].sort();
347
- replaceFileAtomically(registry, JSON.stringify({ version: 1, paths }) + "\n", 420);
352
+ replaceFileAtomically2(registry, JSON.stringify({ version: 1, paths }) + "\n", 420);
348
353
  } finally {
349
354
  release();
350
355
  }
@@ -528,11 +533,12 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
528
533
 
529
534
  // src/history/blobStore.ts
530
535
  import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
531
- import { constants, createReadStream, createWriteStream, renameSync as renameSync2 } from "fs";
532
- 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";
533
538
  import { dirname, join as join2 } from "path";
534
539
  import { pipeline } from "stream/promises";
535
540
  import { atomicTempPath } from "@hyperframes/core/atomic-file";
541
+ import { renameWithRetry, renameWithRetryAsync } from "@hyperframes/core/rename-retry";
536
542
  var BLOB_HASH = /^[0-9a-f]{64}$/;
537
543
  function blobPath(dir, hash) {
538
544
  if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
@@ -558,7 +564,7 @@ async function cloneOrCopy(from, to, beforeReplace) {
558
564
  try {
559
565
  await copyFile(from, temp, constants.COPYFILE_FICLONE);
560
566
  beforeReplace?.();
561
- renameSync2(temp, to);
567
+ renameWithRetry(temp, to);
562
568
  } catch (error) {
563
569
  await rm(temp, { force: true });
564
570
  throw error;
@@ -614,7 +620,7 @@ async function openBlobStore(dir) {
614
620
  }
615
621
  await mkdir(dirname(blobPath(dir, hash)), { recursive: true });
616
622
  const size = (await stat(temp)).size;
617
- await rename(temp, blobPath(dir, hash));
623
+ await renameWithRetryAsync(temp, blobPath(dir, hash));
618
624
  sizes.set(hash, size);
619
625
  total += size;
620
626
  return hash;
@@ -638,7 +644,7 @@ async function openBlobStore(dir) {
638
644
  }
639
645
  const to = blobPath(asideDir, hash);
640
646
  await mkdir(dirname(to), { recursive: true });
641
- const moved = remove && await rename(absPath, to).then(
647
+ const moved = remove && await renameWithRetryAsync(absPath, to).then(
642
648
  () => true,
643
649
  () => false
644
650
  );
@@ -648,7 +654,7 @@ async function openBlobStore(dir) {
648
654
  await rm(temp, { force: true });
649
655
  throw error;
650
656
  });
651
- await rename(temp, to);
657
+ await renameWithRetryAsync(temp, to);
652
658
  }
653
659
  asideSizes.set(hash, (await stat(to)).size);
654
660
  holds.set(hash, /* @__PURE__ */ new Set([path]));
@@ -703,7 +709,7 @@ function uncopiedOnlyHashes(log, sizeOf) {
703
709
 
704
710
  // src/history/pruneHistories.ts
705
711
  import { randomUUID as randomUUID5 } from "crypto";
706
- import { readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync as renameSync3, 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";
707
713
  import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
708
714
  import { tmpdir } from "os";
709
715
  import { dirname as dirname2, join as join4 } from "path";
@@ -712,7 +718,7 @@ import { dirname as dirname2, join as join4 } from "path";
712
718
  import { randomUUID as randomUUID4 } from "crypto";
713
719
  import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
714
720
  import { basename, join as join3, resolve as resolve3 } from "path";
715
- import { replaceFileAtomically as replaceFileAtomically2 } from "@hyperframes/core/atomic-file";
721
+ import { replaceFileAtomically as replaceFileAtomically3 } from "@hyperframes/core/atomic-file";
716
722
  var ID_PATH = join3(".hyperframes", "history-id");
717
723
  var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
718
724
  var isHistoryId = (name) => ID_SHAPE.test(name);
@@ -775,7 +781,7 @@ var HistoryIdError = class extends Error {
775
781
  function recordProject(historyDir, dir, folder) {
776
782
  const record = { dir, ino: folder.ino, born: folder.birthtimeMs, dev: folder.dev };
777
783
  mkdirSync2(historyDir, { recursive: true });
778
- replaceFileAtomically2(join3(historyDir, "project.json"), JSON.stringify(record), 420);
784
+ replaceFileAtomically3(join3(historyDir, "project.json"), JSON.stringify(record), 420);
779
785
  }
780
786
 
781
787
  // src/history/pruneHistories.ts
@@ -867,7 +873,7 @@ function recheckAndMoveAway(historyRoot, id, { now, dryRun, tempDir }) {
867
873
  if (projectDir === null) return null;
868
874
  if (dryRun) return { projectDir, trash: null };
869
875
  const trash = join4(historyRoot, `${PRUNED_PREFIX}${id}-${randomUUID5()}`);
870
- renameSync3(home, trash);
876
+ renameSync(home, trash);
871
877
  return { projectDir, trash };
872
878
  }
873
879
  function abandonedProject(home, id, now, tempDir) {
@@ -926,9 +932,9 @@ function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs }
926
932
 
927
933
  // src/history/videoState.ts
928
934
  import { createHash as createHash5, randomUUID as randomUUID6 } from "crypto";
929
- import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync8, renameSync as renameSync4, statSync as statSync4 } from "fs";
935
+ import { createReadStream as createReadStream2, existsSync as existsSync2, readFileSync as readFileSync8, renameSync as renameSync2, statSync as statSync4 } from "fs";
930
936
  import { isAbsolute as isAbsolute4, join as join7, resolve as resolve6 } from "path";
931
- import { replaceFileAtomically as replaceFileAtomically3 } from "@hyperframes/core/atomic-file";
937
+ import { replaceFileAtomically as replaceFileAtomically4 } from "@hyperframes/core/atomic-file";
932
938
 
933
939
  // src/history/artifactCleanup.ts
934
940
  import { createHash as createHash4 } from "crypto";
@@ -1327,11 +1333,11 @@ async function updateState(dir, update) {
1327
1333
  state = readState(dir);
1328
1334
  } catch (error) {
1329
1335
  if (!(error instanceof SyntaxError)) throw error;
1330
- renameSync4(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
1336
+ renameSync2(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
1331
1337
  state = emptyState();
1332
1338
  }
1333
1339
  update(state);
1334
- replaceFileAtomically3(statePath(dir), JSON.stringify(state, null, 2) + "\n", 420);
1340
+ replaceFileAtomically4(statePath(dir), JSON.stringify(state, null, 2) + "\n", 420);
1335
1341
  } finally {
1336
1342
  release();
1337
1343
  }
@@ -1534,7 +1540,7 @@ async function finishSnapshotRun(projectDir, path) {
1534
1540
  // src/history/historyLog.ts
1535
1541
  import { closeSync as closeSync3, constants as constants4, mkdirSync as mkdirSync4, openSync as openSync3, readFileSync as readFileSync9, writeSync as writeSync2 } from "fs";
1536
1542
  import { dirname as dirname4 } from "path";
1537
- import { replaceFileAtomically as replaceFileAtomically4 } from "@hyperframes/core/atomic-file";
1543
+ import { replaceFileAtomically as replaceFileAtomically5 } from "@hyperframes/core/atomic-file";
1538
1544
  var START = "start";
1539
1545
  function parseRecord(line) {
1540
1546
  try {
@@ -1596,7 +1602,7 @@ function writeLog(file, log) {
1596
1602
  ...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
1597
1603
  ];
1598
1604
  mkdirSync4(dirname4(file), { recursive: true });
1599
- replaceFileAtomically4(file, records2.map((r) => `${JSON.stringify(r)}
1605
+ replaceFileAtomically5(file, records2.map((r) => `${JSON.stringify(r)}
1600
1606
  `).join(""), 420);
1601
1607
  }
1602
1608
  function applyEntry(manifest2, entry, side) {
@@ -1966,7 +1972,7 @@ var Engine = class {
1966
1972
  const file = join8(this.home, "adopting.json");
1967
1973
  if (!this.adopting.size) return rmSync3(file, { force: true });
1968
1974
  mkdirSync5(this.home, { recursive: true });
1969
- replaceFileAtomically5(file, JSON.stringify([...this.adopting.keys()]), 420);
1975
+ replaceFileAtomically6(file, JSON.stringify([...this.adopting.keys()]), 420);
1970
1976
  }
1971
1977
  async adopt(file, sweptAt) {
1972
1978
  try {
@@ -2018,7 +2024,7 @@ var Engine = class {
2018
2024
  saveStatCache() {
2019
2025
  const files = JSON.stringify(Object.fromEntries(this.tracked));
2020
2026
  mkdirSync5(this.home, { recursive: true });
2021
- replaceFileAtomically5(join8(this.home, "stat.json"), files, 420);
2027
+ replaceFileAtomically6(join8(this.home, "stat.json"), files, 420);
2022
2028
  }
2023
2029
  /**
2024
2030
  * Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  PROXY_PARAMS_VERSION,
3
3
  resolveProxy
4
- } from "./chunk-MNJZQCBL.js";
4
+ } from "./chunk-2B5SR4RJ.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";
@@ -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,
@@ -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
+ };
@@ -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-C3KTZ7G3.js";
8
+ import "../chunk-2B5SR4RJ.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-2B5SR4RJ.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-OSZE5WT3.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-OSZE5WT3.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-C3KTZ7G3.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-2B5SR4RJ.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
  }
@@ -3902,8 +3941,50 @@ import {
3902
3941
  normalizeGsapBaseUrl,
3903
3942
  normalizeGsapLocalRoot
3904
3943
  } from "@hyperframes/core/gsap-cdn";
3905
- import { findStartTags as findStartTags3, injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
3944
+ import { findStartTags as findStartTags4, injectTagsAtHeadStart as injectTagsAtHeadStart2 } from "@hyperframes/core/compiler/html-document";
3906
3945
  import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
3946
+
3947
+ // src/helpers/rootImportMap.ts
3948
+ import { findStartTags as findStartTags2, injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
3949
+ var IMPORT_MAP_TYPE = /\btype\s*=\s*["']?importmap["']?(?=[\s/>])/i;
3950
+ function findImportMap(html) {
3951
+ for (const at of findStartTags2(html, "script")) {
3952
+ const open2 = /^<script\b[^>]*>/i.exec(html.slice(at));
3953
+ if (!open2 || !IMPORT_MAP_TYPE.test(open2[0])) continue;
3954
+ const start = at + open2[0].length;
3955
+ const end = html.toLowerCase().indexOf("</script", start);
3956
+ if (end !== -1) return { start, end, json: html.slice(start, end) };
3957
+ }
3958
+ return null;
3959
+ }
3960
+ function withRootImportMap(html, base) {
3961
+ const ours = { "/": base, [base]: base };
3962
+ const own = findImportMap(html);
3963
+ if (!own)
3964
+ return injectTagsAtHeadStart(
3965
+ html,
3966
+ `<script type="importmap">${JSON.stringify({ imports: ours })}</script>`
3967
+ );
3968
+ let map;
3969
+ try {
3970
+ map = JSON.parse(own.json);
3971
+ } catch {
3972
+ return html;
3973
+ }
3974
+ if (!map || typeof map !== "object") return html;
3975
+ const under = (path) => underBase(path, base);
3976
+ const moved = (entries = {}) => Object.fromEntries(
3977
+ Object.entries(entries).map(([key, target]) => [key, under(String(target))])
3978
+ );
3979
+ const merged = { ...map, imports: { ...ours, ...moved(map.imports) } };
3980
+ if (map.scopes)
3981
+ merged.scopes = Object.fromEntries(
3982
+ Object.entries(map.scopes).map(([scope, entries]) => [under(scope), moved(entries)])
3983
+ );
3984
+ return html.slice(0, own.start) + JSON.stringify(merged) + html.slice(own.end);
3985
+ }
3986
+
3987
+ // src/routes/preview.ts
3907
3988
  import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
3908
3989
 
3909
3990
  // src/helpers/previewReads.ts
@@ -4037,7 +4118,7 @@ function injectPreviewVariables(html, values) {
4037
4118
  }
4038
4119
 
4039
4120
  // src/helpers/lazyPreviewImages.ts
4040
- import { findStartTags as findStartTags2 } from "@hyperframes/core/compiler/html-document";
4121
+ import { findStartTags as findStartTags3 } from "@hyperframes/core/compiler/html-document";
4041
4122
  import { parseHTML as parseHTML6 } from "linkedom";
4042
4123
  import { STUDIO_PREVIEW_LAZY_ATTR } from "@hyperframes/core/studio-preview-mark";
4043
4124
  function startsAfterZero(el) {
@@ -4050,7 +4131,7 @@ var hasLoading = (img) => Array.from(img.attributes).some((attr) => attr.name.to
4050
4131
  function lazyPreviewImages(html) {
4051
4132
  if (!/<!doctype|<html[\s>]/i.test(html)) return html;
4052
4133
  const images = [...parseHTML6(html).document.querySelectorAll("img")];
4053
- const tags = findStartTags2(html, "img");
4134
+ const tags = findStartTags3(html, "img");
4054
4135
  if (tags.length !== images.length) return html;
4055
4136
  const parts = [];
4056
4137
  let from = 0;
@@ -4136,7 +4217,7 @@ function injectMotionPathPluginIfNeeded(html, baseUrl) {
4136
4217
  }
4137
4218
  function findGsapCoreScript(html) {
4138
4219
  const scripts = [...parseHTML7(html).document.querySelectorAll("script")];
4139
- const starts = findStartTags3(html, "script");
4220
+ const starts = findStartTags4(html, "script");
4140
4221
  if (starts.length !== scripts.length) return null;
4141
4222
  const at = scripts.findIndex(
4142
4223
  (script) => isGsapCoreUrl(lowerCaseAttributes(script).get("src") ?? "")
@@ -4242,16 +4323,16 @@ var PREVIEW_CAPTURE_PARAM = "hf-capture";
4242
4323
  var EARLY_ERRORS_SCRIPT = `<script>(function(){var seen=window.${STUDIO_PREVIEW_ERRORS}=[];
4243
4324
  addEventListener("error",function(e){seen.push(e.message||String(e))});})();</script>`;
4244
4325
  function injectStudioPreviewAugmentations(html, adapter, projectDir, activeCompositionPath, options) {
4245
- const marked = options.capture ? injectTagsAtHeadStart(
4326
+ const marked = options.capture ? injectTagsAtHeadStart2(
4246
4327
  html,
4247
4328
  `<meta name="${STUDIO_PREVIEW_MARK_META}" content="${STUDIO_PREVIEW_CAPTURE_MODE}">`
4248
- ) : injectTagsAtHeadStart(
4329
+ ) : injectTagsAtHeadStart2(
4249
4330
  lazyPreviewImages(html),
4250
4331
  `<meta name="${STUDIO_PREVIEW_MARK_META}">${EARLY_ERRORS_SCRIPT}`
4251
4332
  );
4252
4333
  const withFallback = injectGsapCdnFallback(
4253
4334
  injectProjectSignature(
4254
- injectTagsAtHeadStart(
4335
+ injectTagsAtHeadStart2(
4255
4336
  marked,
4256
4337
  `<meta name="${GSAP_BASE_URL_META}" content="${escapeGsapUrl(options.gsapBaseUrl)}">`
4257
4338
  ),
@@ -4307,7 +4388,11 @@ var PREVIEW_BUNDLE_OPTIONS = {
4307
4388
  sceneParts: true
4308
4389
  };
4309
4390
  var previewBaseHref = (projectId) => `/api/projects/${encodeURIComponent(projectId)}/preview/`;
4310
- var withPreviewBase = (html, projectId) => hasBaseElement(html) ? html : injectTagsAtHeadStart(html, `<base href="${previewBaseHref(projectId)}">`);
4391
+ var withPreviewBase = (html, projectId) => {
4392
+ const base = previewBaseHref(projectId);
4393
+ const mapped = withRootImportMap(withRootPathsRelative(html, base), base);
4394
+ return hasBaseElement(mapped) ? mapped : injectTagsAtHeadStart2(mapped, `<base href="${base}">`);
4395
+ };
4311
4396
  function resolvePreviewGsapBaseUrl(adapter) {
4312
4397
  const configured = process.env.HYPERFRAMES_GSAP_BASE_URL;
4313
4398
  if (configured?.trim() || !adapter.gsapLocalRoot) {
@@ -4350,10 +4435,9 @@ function registerPreviewRoutes(api, adapter) {
4350
4435
  bundled = ensureHfIds2(diskMain.html);
4351
4436
  mainCompositionPath = diskMain.compositionPath;
4352
4437
  }
4353
- bundled = withPreviewRuntime(bundled, adapter.runtimeUrl);
4438
+ bundled = withPreviewRuntime(withPreviewBase(bundled, project.id), adapter.runtimeUrl);
4354
4439
  recordPreviewReferences(project.dir, bundled);
4355
4440
  recordPreviewBuilt(project.dir);
4356
- bundled = withPreviewBase(bundled, project.id);
4357
4441
  bundled = injectStudioPreviewAugmentations(
4358
4442
  ensureHfIds2(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
4359
4443
  adapter,
@@ -4378,9 +4462,9 @@ function registerPreviewRoutes(api, adapter) {
4378
4462
  } catch {
4379
4463
  const fallback = resolveProjectMainHtml(project.dir, project.id);
4380
4464
  if (fallback) {
4381
- const fallbackHtml = withPreviewBase(
4382
- withPreviewRuntime(ensureHfIds2(fallback.html), adapter.runtimeUrl),
4383
- project.id
4465
+ const fallbackHtml = withPreviewRuntime(
4466
+ withPreviewBase(ensureHfIds2(fallback.html), project.id),
4467
+ adapter.runtimeUrl
4384
4468
  );
4385
4469
  let fallbackAugmented = injectStudioPreviewAugmentations(
4386
4470
  await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
@@ -4482,6 +4566,7 @@ ${etag}`;
4482
4566
  gsapBaseUrl
4483
4567
  );
4484
4568
  if (!html) return c.text("not found", 404);
4569
+ html = withRootImportMap(html, baseHref);
4485
4570
  recordPreviewReferences(project.dir, html);
4486
4571
  html = ensureHfIds2(await transformPreviewHtml(html, adapter, project, compPath));
4487
4572
  html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath, {
@@ -5825,12 +5910,10 @@ import { join as join14, resolve as resolve6 } from "path";
5825
5910
  import { isWithinProjectRoot as isWithinProjectRoot3 } from "@hyperframes/parsers/asset-resolution";
5826
5911
 
5827
5912
  // src/helpers/peakMap.ts
5828
- import { execFile as execFile2, spawn as spawn2 } from "child_process";
5829
- import { promisify } from "util";
5913
+ import { spawn as spawn2 } from "child_process";
5830
5914
  import { findFfBinary as findFfBinary4 } from "@hyperframes/parsers/ff-binaries";
5831
5915
  var PEAK_BIN_SECONDS = 0.05;
5832
5916
  var PEAK_SAMPLE_RATE = 48e3;
5833
- var execFileAsync = promisify(execFile2);
5834
5917
  var PEAK_MAP_CACHE_VERSION = "peaks-v2";
5835
5918
  function createPeakAccumulator(sampleRate = PEAK_SAMPLE_RATE, binSeconds = PEAK_BIN_SECONDS, channels = 2) {
5836
5919
  return {
@@ -5864,29 +5947,10 @@ function accumulatePeaks(acc, chunk) {
5864
5947
  function finishPeaks(acc) {
5865
5948
  return acc.bins.map((peak) => Number(peak.toFixed(4)));
5866
5949
  }
5867
- async function probeChannels(mediaPath) {
5868
- const { stdout } = await execFileAsync(
5869
- findFfBinary4("ffprobe") ?? "ffprobe",
5870
- [
5871
- "-v",
5872
- "error",
5873
- "-select_streams",
5874
- "a:0",
5875
- "-show_entries",
5876
- "stream=channels",
5877
- "-of",
5878
- "csv=p=0",
5879
- "--",
5880
- mediaPath
5881
- ],
5882
- { encoding: "utf8", timeout: 3e4, windowsHide: true }
5883
- );
5884
- const channels = Number.parseInt(stdout.trim(), 10);
5885
- if (!Number.isFinite(channels) || channels < 1) throw new Error("no audio stream");
5886
- return channels;
5887
- }
5888
5950
  async function decodePeakMap(mediaPath) {
5889
- const channels = await probeChannels(mediaPath);
5951
+ const track = await probeAudioTrack(mediaPath);
5952
+ if (!track || track.channels < 1) throw new Error("no audio stream");
5953
+ const { stream, channels } = track;
5890
5954
  return new Promise((resolvePromise, reject) => {
5891
5955
  const proc = spawn2(
5892
5956
  findFfBinary4("ffmpeg") ?? "ffmpeg",
@@ -5896,7 +5960,7 @@ async function decodePeakMap(mediaPath) {
5896
5960
  "-i",
5897
5961
  mediaPath,
5898
5962
  "-map",
5899
- "0:a:0",
5963
+ `0:a:${stream}`,
5900
5964
  "-vn",
5901
5965
  "-ar",
5902
5966
  String(PEAK_SAMPLE_RATE),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.8.207",
3
+ "version": "0.8.209",
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.207",
88
- "@hyperframes/parsers": "0.8.207"
87
+ "@hyperframes/core": "0.8.209",
88
+ "@hyperframes/parsers": "0.8.209"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/node": "^25.0.10",