@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.
- package/dist/{chunk-MNJZQCBL.js → chunk-2B5SR4RJ.js} +11 -24
- package/dist/{chunk-OSZE5WT3.js → chunk-BWZMWYB2.js} +50 -44
- 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 +112 -48
- 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,
|
|
@@ -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
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
532
|
-
import { copyFile, mkdir, readdir, readFile,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1336
|
+
renameSync2(statePath(dir), `${statePath(dir)}.invalid-${randomUUID6()}`);
|
|
1331
1337
|
state = emptyState();
|
|
1332
1338
|
}
|
|
1333
1339
|
update(state);
|
|
1334
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
}
|
|
@@ -3902,8 +3941,50 @@ import {
|
|
|
3902
3941
|
normalizeGsapBaseUrl,
|
|
3903
3942
|
normalizeGsapLocalRoot
|
|
3904
3943
|
} from "@hyperframes/core/gsap-cdn";
|
|
3905
|
-
import { findStartTags as
|
|
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
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
4326
|
+
const marked = options.capture ? injectTagsAtHeadStart2(
|
|
4246
4327
|
html,
|
|
4247
4328
|
`<meta name="${STUDIO_PREVIEW_MARK_META}" content="${STUDIO_PREVIEW_CAPTURE_MODE}">`
|
|
4248
|
-
) :
|
|
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
|
-
|
|
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) =>
|
|
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 =
|
|
4382
|
-
|
|
4383
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|