@hyperframes/studio-server 0.8.208 → 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.
- package/dist/{chunk-MNJZQCBL.js → chunk-2B5SR4RJ.js} +11 -24
- package/dist/{chunk-WAMZ2Z5O.js → chunk-BWZMWYB2.js} +30 -20
- package/dist/{chunk-OAHY2RQP.js → chunk-C3KTZ7G3.js} +2 -2
- package/dist/{chunk-QZWVJKLI.js → chunk-EGNSA3MU.js} +19 -3
- package/dist/{chunk-JKPLDWAB.js → chunk-FUTDRYQK.js} +52 -176
- package/dist/chunk-X73QTST2.js +192 -0
- package/dist/helpers/loudness.d.ts +1 -1
- package/dist/helpers/loudness.js +2 -1
- package/dist/helpers/mediaCodecMap.js +2 -1
- package/dist/helpers/mediaProxyPreview.js +4 -3
- package/dist/helpers/proxyTranscoder.js +3 -2
- package/dist/history/index.js +1 -1
- package/dist/index.js +59 -43
- package/package.json +3 -3
|
@@ -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-
|
|
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
|
-
|
|
483
|
-
|
|
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,
|
|
@@ -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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
528
|
-
import { copyFile, mkdir, readdir, readFile,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1336
|
+
renameSync2(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
|
|
1327
1337
|
state = emptyState();
|
|
1328
1338
|
}
|
|
1329
1339
|
update(state);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROXY_PARAMS_VERSION,
|
|
3
3
|
resolveProxy
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/helpers/loudness.js
CHANGED
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
requiredFfmpeg,
|
|
12
12
|
resolveLocalAudioPath,
|
|
13
13
|
updateAudioVolume
|
|
14
|
-
} from "../chunk-
|
|
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-
|
|
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-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
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-
|
|
24
|
-
import "../chunk-
|
|
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";
|
package/dist/history/index.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
resolveProjectSignature,
|
|
63
63
|
retainSnapshotOutput,
|
|
64
64
|
settledFileTag
|
|
65
|
-
} from "./chunk-
|
|
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-
|
|
79
|
+
} from "./chunk-EGNSA3MU.js";
|
|
80
80
|
import {
|
|
81
81
|
injectMediaCodecMap,
|
|
82
82
|
isAutoProxyEnabled,
|
|
83
83
|
proxyEtagSalt,
|
|
84
84
|
resolvePreviewMediaCodecProbeCache
|
|
85
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
995
|
+
const hasAudio = decodableAudioStream(streams) !== null;
|
|
957
996
|
if (isVideo && !hasVideo) {
|
|
958
997
|
return { ok: false, reason: "no supported video stream found" };
|
|
959
998
|
}
|
|
@@ -3907,7 +3946,6 @@ import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
|
3907
3946
|
|
|
3908
3947
|
// src/helpers/rootImportMap.ts
|
|
3909
3948
|
import { findStartTags as findStartTags2, injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
|
|
3910
|
-
var ROOT_PATH = /^\/(?!\/)/;
|
|
3911
3949
|
var IMPORT_MAP_TYPE = /\btype\s*=\s*["']?importmap["']?(?=[\s/>])/i;
|
|
3912
3950
|
function findImportMap(html) {
|
|
3913
3951
|
for (const at of findStartTags2(html, "script")) {
|
|
@@ -3934,7 +3972,7 @@ function withRootImportMap(html, base) {
|
|
|
3934
3972
|
return html;
|
|
3935
3973
|
}
|
|
3936
3974
|
if (!map || typeof map !== "object") return html;
|
|
3937
|
-
const under = (path) =>
|
|
3975
|
+
const under = (path) => underBase(path, base);
|
|
3938
3976
|
const moved = (entries = {}) => Object.fromEntries(
|
|
3939
3977
|
Object.entries(entries).map(([key, target]) => [key, under(String(target))])
|
|
3940
3978
|
);
|
|
@@ -4352,7 +4390,7 @@ var PREVIEW_BUNDLE_OPTIONS = {
|
|
|
4352
4390
|
var previewBaseHref = (projectId) => `/api/projects/${encodeURIComponent(projectId)}/preview/`;
|
|
4353
4391
|
var withPreviewBase = (html, projectId) => {
|
|
4354
4392
|
const base = previewBaseHref(projectId);
|
|
4355
|
-
const mapped = withRootImportMap(html, base);
|
|
4393
|
+
const mapped = withRootImportMap(withRootPathsRelative(html, base), base);
|
|
4356
4394
|
return hasBaseElement(mapped) ? mapped : injectTagsAtHeadStart2(mapped, `<base href="${base}">`);
|
|
4357
4395
|
};
|
|
4358
4396
|
function resolvePreviewGsapBaseUrl(adapter) {
|
|
@@ -4397,10 +4435,9 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
4397
4435
|
bundled = ensureHfIds2(diskMain.html);
|
|
4398
4436
|
mainCompositionPath = diskMain.compositionPath;
|
|
4399
4437
|
}
|
|
4400
|
-
bundled = withPreviewRuntime(bundled, adapter.runtimeUrl);
|
|
4438
|
+
bundled = withPreviewRuntime(withPreviewBase(bundled, project.id), adapter.runtimeUrl);
|
|
4401
4439
|
recordPreviewReferences(project.dir, bundled);
|
|
4402
4440
|
recordPreviewBuilt(project.dir);
|
|
4403
|
-
bundled = withPreviewBase(bundled, project.id);
|
|
4404
4441
|
bundled = injectStudioPreviewAugmentations(
|
|
4405
4442
|
ensureHfIds2(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
|
|
4406
4443
|
adapter,
|
|
@@ -4425,9 +4462,9 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
4425
4462
|
} catch {
|
|
4426
4463
|
const fallback = resolveProjectMainHtml(project.dir, project.id);
|
|
4427
4464
|
if (fallback) {
|
|
4428
|
-
const fallbackHtml =
|
|
4429
|
-
|
|
4430
|
-
|
|
4465
|
+
const fallbackHtml = withPreviewRuntime(
|
|
4466
|
+
withPreviewBase(ensureHfIds2(fallback.html), project.id),
|
|
4467
|
+
adapter.runtimeUrl
|
|
4431
4468
|
);
|
|
4432
4469
|
let fallbackAugmented = injectStudioPreviewAugmentations(
|
|
4433
4470
|
await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
|
|
@@ -5873,12 +5910,10 @@ import { join as join14, resolve as resolve6 } from "path";
|
|
|
5873
5910
|
import { isWithinProjectRoot as isWithinProjectRoot3 } from "@hyperframes/parsers/asset-resolution";
|
|
5874
5911
|
|
|
5875
5912
|
// src/helpers/peakMap.ts
|
|
5876
|
-
import {
|
|
5877
|
-
import { promisify } from "util";
|
|
5913
|
+
import { spawn as spawn2 } from "child_process";
|
|
5878
5914
|
import { findFfBinary as findFfBinary4 } from "@hyperframes/parsers/ff-binaries";
|
|
5879
5915
|
var PEAK_BIN_SECONDS = 0.05;
|
|
5880
5916
|
var PEAK_SAMPLE_RATE = 48e3;
|
|
5881
|
-
var execFileAsync = promisify(execFile2);
|
|
5882
5917
|
var PEAK_MAP_CACHE_VERSION = "peaks-v2";
|
|
5883
5918
|
function createPeakAccumulator(sampleRate = PEAK_SAMPLE_RATE, binSeconds = PEAK_BIN_SECONDS, channels = 2) {
|
|
5884
5919
|
return {
|
|
@@ -5912,29 +5947,10 @@ function accumulatePeaks(acc, chunk) {
|
|
|
5912
5947
|
function finishPeaks(acc) {
|
|
5913
5948
|
return acc.bins.map((peak) => Number(peak.toFixed(4)));
|
|
5914
5949
|
}
|
|
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
5950
|
async function decodePeakMap(mediaPath) {
|
|
5937
|
-
const
|
|
5951
|
+
const track = await probeAudioTrack(mediaPath);
|
|
5952
|
+
if (!track || track.channels < 1) throw new Error("no audio stream");
|
|
5953
|
+
const { stream, channels } = track;
|
|
5938
5954
|
return new Promise((resolvePromise, reject) => {
|
|
5939
5955
|
const proc = spawn2(
|
|
5940
5956
|
findFfBinary4("ffmpeg") ?? "ffmpeg",
|
|
@@ -5944,7 +5960,7 @@ async function decodePeakMap(mediaPath) {
|
|
|
5944
5960
|
"-i",
|
|
5945
5961
|
mediaPath,
|
|
5946
5962
|
"-map",
|
|
5947
|
-
|
|
5963
|
+
`0:a:${stream}`,
|
|
5948
5964
|
"-vn",
|
|
5949
5965
|
"-ar",
|
|
5950
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.
|
|
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.
|
|
88
|
-
"@hyperframes/parsers": "0.8.
|
|
87
|
+
"@hyperframes/core": "0.8.209",
|
|
88
|
+
"@hyperframes/parsers": "0.8.209"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@types/node": "^25.0.10",
|