@hyperframes/studio-server 0.8.208 → 0.8.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-WAMZ2Z5O.js → chunk-BWZMWYB2.js} +30 -20
- package/dist/{chunk-QZWVJKLI.js → chunk-EGNSA3MU.js} +19 -3
- package/dist/{chunk-JKPLDWAB.js → chunk-FUTDRYQK.js} +52 -176
- package/dist/{chunk-MNJZQCBL.js → chunk-KPCW4DQM.js} +13 -28
- package/dist/chunk-X73QTST2.js +192 -0
- package/dist/{chunk-OAHY2RQP.js → chunk-YOCRTHW3.js} +2 -2
- 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 +65 -44
- package/package.json +3 -3
|
@@ -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,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,
|
|
@@ -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,
|
|
@@ -404,9 +407,8 @@ var ProxyDroppedError = class extends ProxyTranscodeError {
|
|
|
404
407
|
var activeTranscodes = 0;
|
|
405
408
|
var waitQueue = [];
|
|
406
409
|
function enqueue(job) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
else waitQueue.splice(firstNormal, 0, job);
|
|
410
|
+
if (job.priority) waitQueue.unshift(job);
|
|
411
|
+
else waitQueue.push(job);
|
|
410
412
|
}
|
|
411
413
|
function dequeue(job) {
|
|
412
414
|
const index = waitQueue.indexOf(job);
|
|
@@ -442,7 +444,6 @@ function releaseSlot() {
|
|
|
442
444
|
waitQueue.shift()?.queued?.start();
|
|
443
445
|
}
|
|
444
446
|
function prioritize(job) {
|
|
445
|
-
if (job.priority) return;
|
|
446
447
|
job.priority = true;
|
|
447
448
|
if (!job.queued) return;
|
|
448
449
|
dequeue(job);
|
|
@@ -479,29 +480,8 @@ function markCacheEntryUsed(cachePath) {
|
|
|
479
480
|
}
|
|
480
481
|
}
|
|
481
482
|
var failedTranscodes = /* @__PURE__ */ new Map();
|
|
482
|
-
|
|
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;
|
|
483
|
+
async function ensureHdrFilters(ffmpegPath) {
|
|
484
|
+
if (!await ffmpegHasHdrFilters(ffmpegPath)) throw new FfmpegMissingFilterError();
|
|
505
485
|
}
|
|
506
486
|
function rememberFailure(cachePath, error) {
|
|
507
487
|
const ttlMs = error instanceof FfmpegUnavailableError || error instanceof FfmpegMissingFilterError ? ENVIRONMENT_FAILURE_TTL_MS : FAILURE_CACHE_TTL_MS;
|
|
@@ -761,6 +741,10 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
|
|
|
761
741
|
const videoFilter = await buildVideoFilter(ffmpegPath, sourcePath, metadata, keepsAlpha, box);
|
|
762
742
|
const alphaDecoder = keepsAlpha ? alphaDecoderFor(metadata.color.codecName) : null;
|
|
763
743
|
const decoderArgs = alphaDecoder ? ["-c:v", alphaDecoder] : [];
|
|
744
|
+
const streamArgs = [
|
|
745
|
+
...["-map", "0:V:0"],
|
|
746
|
+
...metadata.audioStream === void 0 ? [] : ["-map", `0:a:${metadata.audioStream}`]
|
|
747
|
+
];
|
|
764
748
|
const totalSeconds = metadata.durationSeconds;
|
|
765
749
|
const gate = keepsAlpha ? ALPHA_PIECE_GATE : UNGATED;
|
|
766
750
|
const vp8Threads = (pieces) => [
|
|
@@ -778,6 +762,7 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
|
|
|
778
762
|
...decoderArgs,
|
|
779
763
|
"-i",
|
|
780
764
|
sourcePath,
|
|
765
|
+
...streamArgs,
|
|
781
766
|
...range ? ["-t", String(range.lengthSeconds)] : [],
|
|
782
767
|
"-vf",
|
|
783
768
|
videoFilter,
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// src/helpers/mediaMetadata.ts
|
|
2
|
+
import { execFile } from "child_process";
|
|
3
|
+
import { extname } from "path";
|
|
4
|
+
import {
|
|
5
|
+
firstFrameColourArgs,
|
|
6
|
+
parseFirstFrameColour
|
|
7
|
+
} from "@hyperframes/core";
|
|
8
|
+
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
9
|
+
import { videoStreamHasAlpha } from "@hyperframes/parsers/media-alpha";
|
|
10
|
+
import { decodableAudioStream } from "@hyperframes/parsers/media-streams";
|
|
11
|
+
var execFileRunner = (command, args, options) => new Promise((resolvePromise) => {
|
|
12
|
+
execFile(
|
|
13
|
+
command,
|
|
14
|
+
args,
|
|
15
|
+
{ timeout: options?.timeout, maxBuffer: options?.maxBuffer, windowsHide: true },
|
|
16
|
+
(error, stdout, stderr) => {
|
|
17
|
+
if (error && error.code === "ENOENT") {
|
|
18
|
+
resolvePromise({ status: null, stdout: "", stderr: "", error });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (error) {
|
|
22
|
+
const status = typeof error.code === "number" ? error.code : 1;
|
|
23
|
+
resolvePromise({ status, stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
resolvePromise({ status: 0, stdout: stdout ?? "", stderr: stderr ?? "" });
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
var VIDEO_EXT = /* @__PURE__ */ new Set([
|
|
31
|
+
".mp4",
|
|
32
|
+
".mov",
|
|
33
|
+
".webm",
|
|
34
|
+
".mkv",
|
|
35
|
+
".avi",
|
|
36
|
+
".m4v",
|
|
37
|
+
".mxf",
|
|
38
|
+
".mts",
|
|
39
|
+
".m2ts",
|
|
40
|
+
".ts"
|
|
41
|
+
]);
|
|
42
|
+
var IMAGE_EXT = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
|
|
43
|
+
var AUDIO_EXT = /* @__PURE__ */ new Set([".mp3", ".wav", ".ogg", ".m4a", ".aac", ".flac"]);
|
|
44
|
+
function lower(value) {
|
|
45
|
+
return value?.toLowerCase() ?? "";
|
|
46
|
+
}
|
|
47
|
+
function inferKindFromPath(path) {
|
|
48
|
+
const ext = extname(path).toLowerCase();
|
|
49
|
+
if (VIDEO_EXT.has(ext)) return "video";
|
|
50
|
+
if (IMAGE_EXT.has(ext)) return "image";
|
|
51
|
+
if (AUDIO_EXT.has(ext)) return "audio";
|
|
52
|
+
return "unknown";
|
|
53
|
+
}
|
|
54
|
+
function colorLabel(input) {
|
|
55
|
+
if (input.isHdr) {
|
|
56
|
+
if (input.hdrTransfer === "pq") return "HDR PQ";
|
|
57
|
+
if (input.hdrTransfer === "hlg") return "HDR HLG";
|
|
58
|
+
return "HDR";
|
|
59
|
+
}
|
|
60
|
+
if (input.colorPrimaries.includes("bt709") || input.colorSpace.includes("bt709") || input.colorTransfer.includes("bt709")) {
|
|
61
|
+
return "SDR Rec.709";
|
|
62
|
+
}
|
|
63
|
+
return "SDR/unknown";
|
|
64
|
+
}
|
|
65
|
+
function classifyMediaColor(stream) {
|
|
66
|
+
const colorPrimaries = lower(stream?.color_primaries);
|
|
67
|
+
const colorSpace = lower(stream?.color_space);
|
|
68
|
+
const colorTransfer = lower(stream?.color_transfer);
|
|
69
|
+
const isHdr = colorPrimaries.includes("bt2020") || colorSpace.includes("bt2020") || colorTransfer === "smpte2084" || colorTransfer === "arib-std-b67";
|
|
70
|
+
const hdrTransfer = isHdr ? colorTransfer === "smpte2084" ? "pq" : colorTransfer === "arib-std-b67" ? "hlg" : "unknown" : null;
|
|
71
|
+
return {
|
|
72
|
+
dynamicRange: stream ? isHdr ? "hdr" : "sdr" : "unknown",
|
|
73
|
+
hdrTransfer,
|
|
74
|
+
label: stream ? colorLabel({ isHdr, hdrTransfer, colorPrimaries, colorSpace, colorTransfer }) : "Unknown",
|
|
75
|
+
isHdr,
|
|
76
|
+
codecName: stream?.codec_name,
|
|
77
|
+
profile: stream?.profile,
|
|
78
|
+
pixelFormat: stream?.pix_fmt,
|
|
79
|
+
colorSpace: stream?.color_space,
|
|
80
|
+
colorTransfer: stream?.color_transfer,
|
|
81
|
+
colorPrimaries: stream?.color_primaries,
|
|
82
|
+
bitsPerRawSample: stream?.bits_per_raw_sample
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function probeFirstFrameColour(filePath, runner = execFileRunner) {
|
|
86
|
+
const ffmpegPath = findFfBinary("ffmpeg", { configuredMustExist: true }) ?? (runner === execFileRunner ? void 0 : "ffmpeg");
|
|
87
|
+
if (!ffmpegPath) return {};
|
|
88
|
+
const result = await runner(ffmpegPath, firstFrameColourArgs(filePath), {
|
|
89
|
+
timeout: 15e3,
|
|
90
|
+
maxBuffer: 1024 * 1024
|
|
91
|
+
});
|
|
92
|
+
return result.status === 0 ? parseFirstFrameColour(String(result.stderr)) : {};
|
|
93
|
+
}
|
|
94
|
+
function bitDepthOf(stream) {
|
|
95
|
+
const raw = Number(stream.bits_per_raw_sample);
|
|
96
|
+
if (Number.isFinite(raw) && raw > 0) return raw;
|
|
97
|
+
const suffixed = /(\d{1,3})(?:le|be)$/.exec(stream.pix_fmt ?? "");
|
|
98
|
+
return suffixed ? Number(suffixed[1]) : 8;
|
|
99
|
+
}
|
|
100
|
+
function sourceFactsOf(stream, formatBitrate) {
|
|
101
|
+
if (!stream?.width || !stream.height) return void 0;
|
|
102
|
+
const bitrate = Number(stream.bit_rate ?? formatBitrate);
|
|
103
|
+
return {
|
|
104
|
+
width: stream.width,
|
|
105
|
+
height: stream.height,
|
|
106
|
+
bitDepth: bitDepthOf(stream),
|
|
107
|
+
bitrate: Number.isFinite(bitrate) ? bitrate : 0
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
var STREAM_SUMMARY_MAX = 12;
|
|
111
|
+
function summarizeStream(stream) {
|
|
112
|
+
const flags = Object.entries(stream.disposition ?? {}).filter(([, on]) => on === 1).map(([name]) => name);
|
|
113
|
+
return `${stream.codec_type ?? "?"}:${stream.codec_name ?? "?"}[${flags.join(",")}]`;
|
|
114
|
+
}
|
|
115
|
+
function audioOf(streams) {
|
|
116
|
+
const audioStream = decodableAudioStream(streams);
|
|
117
|
+
return audioStream === null ? { hasAudio: false } : { hasAudio: true, audioStream };
|
|
118
|
+
}
|
|
119
|
+
function metadataFromProbe(kind, parsed) {
|
|
120
|
+
const stream = parsed.streams?.find((item) => {
|
|
121
|
+
if (kind === "image") return item.codec_type === "video";
|
|
122
|
+
return item.codec_type === kind && item.disposition?.attached_pic !== 1;
|
|
123
|
+
});
|
|
124
|
+
const metadata = {
|
|
125
|
+
kind,
|
|
126
|
+
color: classifyMediaColor(stream),
|
|
127
|
+
streams: (parsed.streams ?? []).slice(0, STREAM_SUMMARY_MAX).map(summarizeStream)
|
|
128
|
+
};
|
|
129
|
+
if (kind === "video") {
|
|
130
|
+
Object.assign(metadata, audioOf(parsed.streams ?? []));
|
|
131
|
+
metadata.hasAlpha = stream !== void 0 && videoStreamHasAlpha(stream);
|
|
132
|
+
const duration = Number(parsed.format?.duration);
|
|
133
|
+
if (Number.isFinite(duration) && duration > 0) metadata.durationSeconds = duration;
|
|
134
|
+
metadata.source = sourceFactsOf(stream, parsed.format?.bit_rate);
|
|
135
|
+
}
|
|
136
|
+
return metadata;
|
|
137
|
+
}
|
|
138
|
+
async function probeAudioTrack(filePath, runner = execFileRunner) {
|
|
139
|
+
const result = await runner(
|
|
140
|
+
findFfBinary("ffprobe") ?? "ffprobe",
|
|
141
|
+
[
|
|
142
|
+
...["-v", "error", "-select_streams", "a"],
|
|
143
|
+
...["-show_entries", "stream=codec_type,codec_name,channels", "-of", "json", "--", filePath]
|
|
144
|
+
],
|
|
145
|
+
{ timeout: 3e4 }
|
|
146
|
+
);
|
|
147
|
+
if (result.status !== 0) return null;
|
|
148
|
+
const streams = JSON.parse(String(result.stdout || "{}")).streams ?? [];
|
|
149
|
+
const stream = decodableAudioStream(streams);
|
|
150
|
+
return stream === null ? null : { stream, channels: streams[stream]?.channels ?? 0 };
|
|
151
|
+
}
|
|
152
|
+
async function probeMediaMetadata(filePath, runner = execFileRunner) {
|
|
153
|
+
const kind = inferKindFromPath(filePath);
|
|
154
|
+
if (kind === "audio" || kind === "unknown") {
|
|
155
|
+
return { kind, color: classifyMediaColor(null) };
|
|
156
|
+
}
|
|
157
|
+
const ffprobePath = findFfBinary("ffprobe", { configuredMustExist: true }) ?? (runner === execFileRunner ? void 0 : "ffprobe");
|
|
158
|
+
if (!ffprobePath) {
|
|
159
|
+
return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
|
|
160
|
+
}
|
|
161
|
+
const result = await runner(
|
|
162
|
+
ffprobePath,
|
|
163
|
+
[
|
|
164
|
+
"-v",
|
|
165
|
+
"error",
|
|
166
|
+
"-show_entries",
|
|
167
|
+
"stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample,width,height,bit_rate:stream_disposition:stream_tags=alpha_mode:format=duration,bit_rate",
|
|
168
|
+
"-of",
|
|
169
|
+
"json",
|
|
170
|
+
"--",
|
|
171
|
+
filePath
|
|
172
|
+
],
|
|
173
|
+
{ timeout: 15e3, maxBuffer: 1024 * 1024 }
|
|
174
|
+
);
|
|
175
|
+
if (result.error?.code === "ENOENT") {
|
|
176
|
+
return { kind, color: classifyMediaColor(null), probeError: "ffprobe unavailable" };
|
|
177
|
+
}
|
|
178
|
+
if (result.status !== 0) {
|
|
179
|
+
return { kind, color: classifyMediaColor(null), probeError: "ffprobe failed" };
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
return metadataFromProbe(kind, JSON.parse(String(result.stdout || "{}")));
|
|
183
|
+
} catch {
|
|
184
|
+
return { kind, color: classifyMediaColor(null), probeError: "ffprobe returned invalid json" };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export {
|
|
189
|
+
probeFirstFrameColour,
|
|
190
|
+
probeAudioTrack,
|
|
191
|
+
probeMediaMetadata
|
|
192
|
+
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROXY_PARAMS_VERSION,
|
|
3
3
|
resolveProxy
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KPCW4DQM.js";
|
|
5
5
|
import {
|
|
6
6
|
createMediaCodecProbeCache,
|
|
7
7
|
proxyVariantFor,
|
|
8
8
|
recordProxyPrewarm,
|
|
9
9
|
scanProjectMedia,
|
|
10
10
|
shouldPrewarmProxy
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FUTDRYQK.js";
|
|
12
12
|
|
|
13
13
|
// src/helpers/mediaProxyPreview.ts
|
|
14
14
|
import { resolve } from "path";
|
|
@@ -57,7 +57,7 @@ declare function updateAudioVolume(html: string, id: string, volume: number): st
|
|
|
57
57
|
* -61.8 LUFS: output-side `-t 4` reported -33.8 LUFS, input-side reports -61.8,
|
|
58
58
|
* the same as physically cutting the file first.
|
|
59
59
|
*/
|
|
60
|
-
declare function loudnessMeasureArgs(file: string, tag: MeasuredWindow): string[];
|
|
60
|
+
declare function loudnessMeasureArgs(file: string, tag: MeasuredWindow, stream: number): string[];
|
|
61
61
|
type MeasuredWindow = Pick<AudioTag, "mediaStart" | "duration"> & Partial<Pick<AudioTag, "playbackRate">>;
|
|
62
62
|
declare function measureAudio(ffmpegPath: string, file: string, tag: MeasuredWindow): Promise<LoudnessMeasurement>;
|
|
63
63
|
declare function requiredFfmpeg(): string;
|
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-YOCRTHW3.js";
|
|
8
|
+
import "../chunk-KPCW4DQM.js";
|
|
9
|
+
import "../chunk-FUTDRYQK.js";
|
|
10
|
+
import "../chunk-X73QTST2.js";
|
|
10
11
|
import "../chunk-QF7MFJJU.js";
|
|
11
12
|
import "../chunk-SVQFE44C.js";
|
|
12
13
|
import "../chunk-L3HJVTQE.js";
|
|
@@ -20,8 +20,9 @@ import {
|
|
|
20
20
|
retryProxyCopy,
|
|
21
21
|
sourceFolderKind,
|
|
22
22
|
waitForProxy
|
|
23
|
-
} from "../chunk-
|
|
24
|
-
import "../chunk-
|
|
23
|
+
} from "../chunk-KPCW4DQM.js";
|
|
24
|
+
import "../chunk-FUTDRYQK.js";
|
|
25
|
+
import "../chunk-X73QTST2.js";
|
|
25
26
|
import "../chunk-QF7MFJJU.js";
|
|
26
27
|
import "../chunk-SVQFE44C.js";
|
|
27
28
|
import "../chunk-L3HJVTQE.js";
|
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-YOCRTHW3.js";
|
|
86
86
|
import {
|
|
87
87
|
CACHE_DIR_NAME,
|
|
88
88
|
PROXY_PENDING_RETRY_AFTER_SECONDS,
|
|
@@ -95,16 +95,19 @@ import {
|
|
|
95
95
|
resolveProxy,
|
|
96
96
|
retryProxyCopy,
|
|
97
97
|
waitForProxy
|
|
98
|
-
} from "./chunk-
|
|
98
|
+
} from "./chunk-KPCW4DQM.js";
|
|
99
99
|
import {
|
|
100
100
|
PROXY_VARIANT_CONFIG,
|
|
101
101
|
decideMediaProxyEligibility,
|
|
102
102
|
isProxyVariantRequest,
|
|
103
103
|
probeAssetCodec,
|
|
104
|
-
probeMediaMetadata,
|
|
105
104
|
recordProxyRequest,
|
|
106
105
|
resolveProxyVariantRequest
|
|
107
|
-
} from "./chunk-
|
|
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
|
}
|
|
@@ -3891,6 +3930,7 @@ import {
|
|
|
3891
3930
|
import {
|
|
3892
3931
|
PREVIEW_PROXY_BOX_PARAM,
|
|
3893
3932
|
PREVIEW_PROXY_LIGHT_PARAM,
|
|
3933
|
+
PREVIEW_PROXY_PRIORITY_HEADER,
|
|
3894
3934
|
PREVIEW_PROXY_PROGRESS_HEADER,
|
|
3895
3935
|
PREVIEW_PROXY_RETRY_PARAM,
|
|
3896
3936
|
parsePreviewProxyBox
|
|
@@ -3907,7 +3947,6 @@ import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
|
3907
3947
|
|
|
3908
3948
|
// src/helpers/rootImportMap.ts
|
|
3909
3949
|
import { findStartTags as findStartTags2, injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
|
|
3910
|
-
var ROOT_PATH = /^\/(?!\/)/;
|
|
3911
3950
|
var IMPORT_MAP_TYPE = /\btype\s*=\s*["']?importmap["']?(?=[\s/>])/i;
|
|
3912
3951
|
function findImportMap(html) {
|
|
3913
3952
|
for (const at of findStartTags2(html, "script")) {
|
|
@@ -3934,7 +3973,7 @@ function withRootImportMap(html, base) {
|
|
|
3934
3973
|
return html;
|
|
3935
3974
|
}
|
|
3936
3975
|
if (!map || typeof map !== "object") return html;
|
|
3937
|
-
const under = (path) =>
|
|
3976
|
+
const under = (path) => underBase(path, base);
|
|
3938
3977
|
const moved = (entries = {}) => Object.fromEntries(
|
|
3939
3978
|
Object.entries(entries).map(([key, target]) => [key, under(String(target))])
|
|
3940
3979
|
);
|
|
@@ -4352,7 +4391,7 @@ var PREVIEW_BUNDLE_OPTIONS = {
|
|
|
4352
4391
|
var previewBaseHref = (projectId) => `/api/projects/${encodeURIComponent(projectId)}/preview/`;
|
|
4353
4392
|
var withPreviewBase = (html, projectId) => {
|
|
4354
4393
|
const base = previewBaseHref(projectId);
|
|
4355
|
-
const mapped = withRootImportMap(html, base);
|
|
4394
|
+
const mapped = withRootImportMap(withRootPathsRelative(html, base), base);
|
|
4356
4395
|
return hasBaseElement(mapped) ? mapped : injectTagsAtHeadStart2(mapped, `<base href="${base}">`);
|
|
4357
4396
|
};
|
|
4358
4397
|
function resolvePreviewGsapBaseUrl(adapter) {
|
|
@@ -4397,10 +4436,9 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
4397
4436
|
bundled = ensureHfIds2(diskMain.html);
|
|
4398
4437
|
mainCompositionPath = diskMain.compositionPath;
|
|
4399
4438
|
}
|
|
4400
|
-
bundled = withPreviewRuntime(bundled, adapter.runtimeUrl);
|
|
4439
|
+
bundled = withPreviewRuntime(withPreviewBase(bundled, project.id), adapter.runtimeUrl);
|
|
4401
4440
|
recordPreviewReferences(project.dir, bundled);
|
|
4402
4441
|
recordPreviewBuilt(project.dir);
|
|
4403
|
-
bundled = withPreviewBase(bundled, project.id);
|
|
4404
4442
|
bundled = injectStudioPreviewAugmentations(
|
|
4405
4443
|
ensureHfIds2(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
|
|
4406
4444
|
adapter,
|
|
@@ -4425,9 +4463,9 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
4425
4463
|
} catch {
|
|
4426
4464
|
const fallback = resolveProjectMainHtml(project.dir, project.id);
|
|
4427
4465
|
if (fallback) {
|
|
4428
|
-
const fallbackHtml =
|
|
4429
|
-
|
|
4430
|
-
|
|
4466
|
+
const fallbackHtml = withPreviewRuntime(
|
|
4467
|
+
withPreviewBase(ensureHfIds2(fallback.html), project.id),
|
|
4468
|
+
adapter.runtimeUrl
|
|
4431
4469
|
);
|
|
4432
4470
|
let fallbackAugmented = injectStudioPreviewAugmentations(
|
|
4433
4471
|
await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
|
|
@@ -4595,7 +4633,11 @@ ${etag}`;
|
|
|
4595
4633
|
if (c.req.query(PREVIEW_PROXY_RETRY_PARAM) !== void 0) {
|
|
4596
4634
|
retryProxyCopy(project.dir, file, proxyVariant, proxyBox);
|
|
4597
4635
|
}
|
|
4598
|
-
|
|
4636
|
+
const priority = c.req.header(PREVIEW_PROXY_PRIORITY_HEADER) === "1";
|
|
4637
|
+
servedPath = await waitForProxy(
|
|
4638
|
+
resolveProxy(project.dir, file, proxyVariant, proxyBox, { priority }),
|
|
4639
|
+
0
|
|
4640
|
+
);
|
|
4599
4641
|
} catch (err) {
|
|
4600
4642
|
if (err instanceof ProxyWaitTimeoutError) {
|
|
4601
4643
|
const progress = proxyCopyProgress(project.dir, file, proxyVariant, proxyBox);
|
|
@@ -5873,12 +5915,10 @@ import { join as join14, resolve as resolve6 } from "path";
|
|
|
5873
5915
|
import { isWithinProjectRoot as isWithinProjectRoot3 } from "@hyperframes/parsers/asset-resolution";
|
|
5874
5916
|
|
|
5875
5917
|
// src/helpers/peakMap.ts
|
|
5876
|
-
import {
|
|
5877
|
-
import { promisify } from "util";
|
|
5918
|
+
import { spawn as spawn2 } from "child_process";
|
|
5878
5919
|
import { findFfBinary as findFfBinary4 } from "@hyperframes/parsers/ff-binaries";
|
|
5879
5920
|
var PEAK_BIN_SECONDS = 0.05;
|
|
5880
5921
|
var PEAK_SAMPLE_RATE = 48e3;
|
|
5881
|
-
var execFileAsync = promisify(execFile2);
|
|
5882
5922
|
var PEAK_MAP_CACHE_VERSION = "peaks-v2";
|
|
5883
5923
|
function createPeakAccumulator(sampleRate = PEAK_SAMPLE_RATE, binSeconds = PEAK_BIN_SECONDS, channels = 2) {
|
|
5884
5924
|
return {
|
|
@@ -5912,29 +5952,10 @@ function accumulatePeaks(acc, chunk) {
|
|
|
5912
5952
|
function finishPeaks(acc) {
|
|
5913
5953
|
return acc.bins.map((peak) => Number(peak.toFixed(4)));
|
|
5914
5954
|
}
|
|
5915
|
-
async function probeChannels(mediaPath) {
|
|
5916
|
-
const { stdout } = await execFileAsync(
|
|
5917
|
-
findFfBinary4("ffprobe") ?? "ffprobe",
|
|
5918
|
-
[
|
|
5919
|
-
"-v",
|
|
5920
|
-
"error",
|
|
5921
|
-
"-select_streams",
|
|
5922
|
-
"a:0",
|
|
5923
|
-
"-show_entries",
|
|
5924
|
-
"stream=channels",
|
|
5925
|
-
"-of",
|
|
5926
|
-
"csv=p=0",
|
|
5927
|
-
"--",
|
|
5928
|
-
mediaPath
|
|
5929
|
-
],
|
|
5930
|
-
{ encoding: "utf8", timeout: 3e4, windowsHide: true }
|
|
5931
|
-
);
|
|
5932
|
-
const channels = Number.parseInt(stdout.trim(), 10);
|
|
5933
|
-
if (!Number.isFinite(channels) || channels < 1) throw new Error("no audio stream");
|
|
5934
|
-
return channels;
|
|
5935
|
-
}
|
|
5936
5955
|
async function decodePeakMap(mediaPath) {
|
|
5937
|
-
const
|
|
5956
|
+
const track = await probeAudioTrack(mediaPath);
|
|
5957
|
+
if (!track || track.channels < 1) throw new Error("no audio stream");
|
|
5958
|
+
const { stream, channels } = track;
|
|
5938
5959
|
return new Promise((resolvePromise, reject) => {
|
|
5939
5960
|
const proc = spawn2(
|
|
5940
5961
|
findFfBinary4("ffmpeg") ?? "ffmpeg",
|
|
@@ -5944,7 +5965,7 @@ async function decodePeakMap(mediaPath) {
|
|
|
5944
5965
|
"-i",
|
|
5945
5966
|
mediaPath,
|
|
5946
5967
|
"-map",
|
|
5947
|
-
|
|
5968
|
+
`0:a:${stream}`,
|
|
5948
5969
|
"-vn",
|
|
5949
5970
|
"-ar",
|
|
5950
5971
|
String(PEAK_SAMPLE_RATE),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio-server",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.210",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/heygen-com/hyperframes",
|
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
"linkedom": "^0.18.12",
|
|
85
85
|
"postcss": "^8.5.8",
|
|
86
86
|
"postcss-selector-parser": "^7.1.2",
|
|
87
|
-
"@hyperframes/core": "0.8.
|
|
88
|
-
"@hyperframes/parsers": "0.8.
|
|
87
|
+
"@hyperframes/core": "0.8.210",
|
|
88
|
+
"@hyperframes/parsers": "0.8.210"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@types/node": "^25.0.10",
|