@hyperframes/studio-server 0.7.60 → 0.7.61
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-4ETS2LXI.js +192 -0
- package/dist/chunk-4ETS2LXI.js.map +1 -0
- package/dist/chunk-6XMC64FJ.js +584 -0
- package/dist/chunk-6XMC64FJ.js.map +1 -0
- package/dist/chunk-LVXVG4V6.js +282 -0
- package/dist/chunk-LVXVG4V6.js.map +1 -0
- package/dist/chunk-W2SBTCO2.js +32 -0
- package/dist/chunk-W2SBTCO2.js.map +1 -0
- package/dist/chunk-X62ASOGO.js +30 -0
- package/dist/chunk-X62ASOGO.js.map +1 -0
- package/dist/chunk-XVQX2JHE.js +422 -0
- package/dist/chunk-XVQX2JHE.js.map +1 -0
- package/dist/chunk-YBR7MXIO.js +459 -0
- package/dist/chunk-YBR7MXIO.js.map +1 -0
- package/dist/chunk-ZUW4PULZ.js +66 -0
- package/dist/chunk-ZUW4PULZ.js.map +1 -0
- package/dist/helpers/finiteMutation.js +4 -24
- package/dist/helpers/finiteMutation.js.map +1 -1
- package/dist/helpers/manualEditsRenderScript.js +5 -577
- package/dist/helpers/manualEditsRenderScript.js.map +1 -1
- package/dist/helpers/mediaCodecMap.d.ts +100 -0
- package/dist/helpers/mediaCodecMap.js +25 -0
- package/dist/helpers/mediaCodecMap.js.map +1 -0
- package/dist/helpers/mediaProxyPreview.d.ts +4 -0
- package/dist/helpers/mediaProxyPreview.js +17 -0
- package/dist/helpers/mediaProxyPreview.js.map +1 -0
- package/dist/helpers/proxyTranscoder.d.ts +61 -0
- package/dist/helpers/proxyTranscoder.js +30 -0
- package/dist/helpers/proxyTranscoder.js.map +1 -0
- package/dist/helpers/screenshotClip.js +3 -22
- package/dist/helpers/screenshotClip.js.map +1 -1
- package/dist/helpers/sourceMutation.js +9 -419
- package/dist/helpers/sourceMutation.js.map +1 -1
- package/dist/helpers/studioMotionRenderScript.js +4 -186
- package/dist/helpers/studioMotionRenderScript.js.map +1 -1
- package/dist/index.d.ts +6 -202
- package/dist/index.js +172 -1373
- package/dist/index.js.map +1 -1
- package/dist/mediaProxyPreview-CeshDUjE.d.ts +259 -0
- package/package.json +15 -3
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PROXY_VARIANT_CONFIG,
|
|
3
|
+
probeMediaMetadata
|
|
4
|
+
} from "./chunk-LVXVG4V6.js";
|
|
5
|
+
|
|
6
|
+
// src/helpers/proxyTranscoder.ts
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import { createHash, randomUUID } from "crypto";
|
|
9
|
+
import {
|
|
10
|
+
existsSync as existsSync2,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
statSync as statSync2,
|
|
15
|
+
unlinkSync as unlinkSync2,
|
|
16
|
+
utimesSync
|
|
17
|
+
} from "fs";
|
|
18
|
+
import { basename, dirname, isAbsolute, join as join2, relative, sep } from "path";
|
|
19
|
+
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
20
|
+
|
|
21
|
+
// src/helpers/proxyCache.ts
|
|
22
|
+
import { existsSync, readdirSync, statSync, unlinkSync } from "fs";
|
|
23
|
+
import { extname, join } from "path";
|
|
24
|
+
var DEFAULT_MAX_BYTES = 10 * 1024 * 1024 * 1024;
|
|
25
|
+
var DEFAULT_STALE_TEMP_MS = 60 * 60 * 1e3;
|
|
26
|
+
var DEFAULT_MIN_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
27
|
+
var PROXY_EXTENSIONS = new Set(
|
|
28
|
+
Object.values(PROXY_VARIANT_CONFIG).map(({ extension }) => extension)
|
|
29
|
+
);
|
|
30
|
+
var lastSweepAt = /* @__PURE__ */ new Map();
|
|
31
|
+
function positiveEnvNumber(name, fallback) {
|
|
32
|
+
const parsed = Number(process.env[name]);
|
|
33
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
34
|
+
}
|
|
35
|
+
function proxyCacheCleanupDefaults() {
|
|
36
|
+
return {
|
|
37
|
+
maxBytes: positiveEnvNumber("HYPERFRAMES_PROXY_CACHE_MAX_BYTES", DEFAULT_MAX_BYTES),
|
|
38
|
+
maxIdleMs: positiveEnvNumber("HYPERFRAMES_PROXY_CACHE_MAX_IDLE_DAYS", 30) * 24 * 60 * 60 * 1e3,
|
|
39
|
+
staleTempMs: positiveEnvNumber("HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS", DEFAULT_STALE_TEMP_MS),
|
|
40
|
+
minSweepIntervalMs: positiveEnvNumber(
|
|
41
|
+
"HYPERFRAMES_PROXY_CACHE_SWEEP_INTERVAL_MS",
|
|
42
|
+
DEFAULT_MIN_SWEEP_INTERVAL_MS
|
|
43
|
+
)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function shouldSkipSweep(cacheDir, now, minSweepIntervalMs) {
|
|
47
|
+
const previousSweep = lastSweepAt.get(cacheDir);
|
|
48
|
+
if (previousSweep !== void 0 && now - previousSweep < minSweepIntervalMs) return true;
|
|
49
|
+
lastSweepAt.set(cacheDir, now);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
function readCacheInventory(cacheDir, protectedPaths, now, staleTempMs) {
|
|
53
|
+
const entries = [];
|
|
54
|
+
const staleTemps = [];
|
|
55
|
+
for (const dirent of readdirSync(cacheDir, { withFileTypes: true })) {
|
|
56
|
+
if (!dirent.isFile()) continue;
|
|
57
|
+
const path = join(cacheDir, dirent.name);
|
|
58
|
+
const stat = statSync(path);
|
|
59
|
+
const entry = {
|
|
60
|
+
path,
|
|
61
|
+
size: stat.size,
|
|
62
|
+
modifiedAt: stat.mtimeMs,
|
|
63
|
+
protected: protectedPaths.has(path)
|
|
64
|
+
};
|
|
65
|
+
if (dirent.name.startsWith(".tmp-")) {
|
|
66
|
+
if (now - stat.mtimeMs >= staleTempMs) staleTemps.push(entry);
|
|
67
|
+
} else if (PROXY_EXTENSIONS.has(extname(dirent.name))) {
|
|
68
|
+
entries.push(entry);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const oldestFirst = (a, b) => a.modifiedAt - b.modifiedAt || a.path.localeCompare(b.path);
|
|
72
|
+
entries.sort(oldestFirst);
|
|
73
|
+
staleTemps.sort(oldestFirst);
|
|
74
|
+
return { entries, staleTemps };
|
|
75
|
+
}
|
|
76
|
+
function evictCacheEntries(entries, staleTemps, now, maxIdleMs, maxBytes) {
|
|
77
|
+
const bytesBefore = entries.reduce((total, entry) => total + entry.size, 0);
|
|
78
|
+
let bytesAfter = bytesBefore;
|
|
79
|
+
const removed = [];
|
|
80
|
+
const remove = (entry, countsTowardBudget) => {
|
|
81
|
+
unlinkSync(entry.path);
|
|
82
|
+
removed.push(entry.path);
|
|
83
|
+
if (countsTowardBudget) bytesAfter -= entry.size;
|
|
84
|
+
};
|
|
85
|
+
for (const entry of staleTemps) remove(entry, false);
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (!entry.protected && now - entry.modifiedAt >= maxIdleMs) remove(entry, true);
|
|
88
|
+
}
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (bytesAfter <= maxBytes) break;
|
|
91
|
+
if (!entry.protected && existsSync(entry.path)) remove(entry, true);
|
|
92
|
+
}
|
|
93
|
+
return { removed, bytesBefore, bytesAfter };
|
|
94
|
+
}
|
|
95
|
+
function cleanupProxyCache(cacheDir, options = {}) {
|
|
96
|
+
const defaults = proxyCacheCleanupDefaults();
|
|
97
|
+
const now = options.now ?? Date.now();
|
|
98
|
+
const minSweepIntervalMs = options.minSweepIntervalMs ?? defaults.minSweepIntervalMs;
|
|
99
|
+
if (shouldSkipSweep(cacheDir, now, minSweepIntervalMs)) {
|
|
100
|
+
return { removed: [], bytesBefore: 0, bytesAfter: 0, skipped: true };
|
|
101
|
+
}
|
|
102
|
+
if (!existsSync(cacheDir)) {
|
|
103
|
+
return { removed: [], bytesBefore: 0, bytesAfter: 0, skipped: false };
|
|
104
|
+
}
|
|
105
|
+
const maxBytes = options.maxBytes ?? defaults.maxBytes;
|
|
106
|
+
const maxIdleMs = options.maxIdleMs ?? defaults.maxIdleMs;
|
|
107
|
+
const staleTempMs = options.staleTempMs ?? defaults.staleTempMs;
|
|
108
|
+
const protectedPaths = options.protectedPaths ?? /* @__PURE__ */ new Set();
|
|
109
|
+
const { entries, staleTemps } = readCacheInventory(cacheDir, protectedPaths, now, staleTempMs);
|
|
110
|
+
return {
|
|
111
|
+
...evictCacheEntries(entries, staleTemps, now, maxIdleMs, maxBytes),
|
|
112
|
+
skipped: false
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/helpers/proxyTranscoder.ts
|
|
117
|
+
var PROXY_PARAMS_VERSION = "v3";
|
|
118
|
+
var CACHE_DIR_NAME = ".transcode-cache";
|
|
119
|
+
function boundedEnvInteger(name, fallback, min, max) {
|
|
120
|
+
const raw = process.env[name]?.trim();
|
|
121
|
+
if (!raw) return fallback;
|
|
122
|
+
const parsed = Number(raw);
|
|
123
|
+
return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;
|
|
124
|
+
}
|
|
125
|
+
var MAX_CONCURRENT_TRANSCODES = boundedEnvInteger("HYPERFRAMES_PROXY_MAX_CONCURRENCY", 2, 1, 16);
|
|
126
|
+
var MAX_QUEUED_TRANSCODES = boundedEnvInteger("HYPERFRAMES_PROXY_MAX_QUEUE", 8, 0, 256);
|
|
127
|
+
var STDERR_TAIL_MAX_CHARS = 4e3;
|
|
128
|
+
var TRANSCODE_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
129
|
+
var FAILURE_CACHE_TTL_MS = 60 * 1e3;
|
|
130
|
+
var MAX_FAILURE_CACHE_ENTRIES = 128;
|
|
131
|
+
var DEFAULT_PROXY_WAIT_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
132
|
+
var ProxyTranscodeError = class extends Error {
|
|
133
|
+
exitCode;
|
|
134
|
+
stderrTail;
|
|
135
|
+
constructor(message, exitCode, stderrTail) {
|
|
136
|
+
super(message);
|
|
137
|
+
this.name = "ProxyTranscodeError";
|
|
138
|
+
this.exitCode = exitCode;
|
|
139
|
+
this.stderrTail = stderrTail;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
var FfmpegUnavailableError = class extends ProxyTranscodeError {
|
|
143
|
+
constructor() {
|
|
144
|
+
super("ffmpeg binary not found", null, "");
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
var FfmpegMissingFilterError = class extends ProxyTranscodeError {
|
|
148
|
+
constructor() {
|
|
149
|
+
super(
|
|
150
|
+
"HDR proxying requires ffmpeg zscale/tonemap filters (libzimg); install an ffmpeg build with libzimg support",
|
|
151
|
+
null,
|
|
152
|
+
""
|
|
153
|
+
);
|
|
154
|
+
this.name = "FfmpegMissingFilterError";
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var ProxyCapacityError = class extends ProxyTranscodeError {
|
|
158
|
+
constructor() {
|
|
159
|
+
super("media proxy queue is full; retry shortly", null, "");
|
|
160
|
+
this.name = "ProxyCapacityError";
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
var ProxySourceOutsideProjectError = class extends ProxyTranscodeError {
|
|
164
|
+
constructor() {
|
|
165
|
+
super("media proxy source must be inside the project", null, "");
|
|
166
|
+
this.name = "ProxySourceOutsideProjectError";
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var ProxyWaitTimeoutError = class extends ProxyTranscodeError {
|
|
170
|
+
constructor(timeoutMs) {
|
|
171
|
+
super(`media proxy did not become ready within ${timeoutMs}ms`, null, "");
|
|
172
|
+
this.name = "ProxyWaitTimeoutError";
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
async function waitForProxy(promise, timeoutMs = DEFAULT_PROXY_WAIT_TIMEOUT_MS) {
|
|
176
|
+
let timer;
|
|
177
|
+
try {
|
|
178
|
+
return await Promise.race([
|
|
179
|
+
promise,
|
|
180
|
+
new Promise((_resolve, reject) => {
|
|
181
|
+
timer = setTimeout(() => reject(new ProxyWaitTimeoutError(timeoutMs)), timeoutMs);
|
|
182
|
+
timer.unref();
|
|
183
|
+
})
|
|
184
|
+
]);
|
|
185
|
+
} finally {
|
|
186
|
+
if (timer) clearTimeout(timer);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function canonicalizeProxySource(projectDir, absoluteSourcePath) {
|
|
190
|
+
const canonicalProjectDir = realpathSync(projectDir);
|
|
191
|
+
const canonicalSourcePath = realpathSync(absoluteSourcePath);
|
|
192
|
+
const relPath = relative(canonicalProjectDir, canonicalSourcePath);
|
|
193
|
+
if (relPath === ".." || relPath.startsWith(`..${sep}`) || isAbsolute(relPath)) {
|
|
194
|
+
throw new ProxySourceOutsideProjectError();
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
projectDir: canonicalProjectDir,
|
|
198
|
+
sourcePath: canonicalSourcePath,
|
|
199
|
+
relativePath: relPath.normalize("NFC")
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function buildProxyCacheKey(source, variant) {
|
|
203
|
+
const stat = statSync2(source.sourcePath);
|
|
204
|
+
return createHash("sha256").update(
|
|
205
|
+
`${source.relativePath}\0${stat.mtimeMs}\0${stat.size}\0${PROXY_PARAMS_VERSION}\0${variant}`
|
|
206
|
+
).digest("hex");
|
|
207
|
+
}
|
|
208
|
+
function getCanonicalProxyCachePath(source, variant) {
|
|
209
|
+
const key = buildProxyCacheKey(source, variant);
|
|
210
|
+
return join2(
|
|
211
|
+
source.projectDir,
|
|
212
|
+
CACHE_DIR_NAME,
|
|
213
|
+
`${key}${PROXY_VARIANT_CONFIG[variant].extension}`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
function getProxyCachePath(projectDir, absoluteSourcePath, variant = "h264") {
|
|
217
|
+
return getCanonicalProxyCachePath(
|
|
218
|
+
canonicalizeProxySource(projectDir, absoluteSourcePath),
|
|
219
|
+
variant
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
var activeTranscodes = 0;
|
|
223
|
+
var waitQueue = [];
|
|
224
|
+
function acquireSlot() {
|
|
225
|
+
return new Promise((resolveSlot, reject) => {
|
|
226
|
+
const tryAcquire = () => {
|
|
227
|
+
if (activeTranscodes < MAX_CONCURRENT_TRANSCODES) {
|
|
228
|
+
activeTranscodes++;
|
|
229
|
+
resolveSlot();
|
|
230
|
+
} else {
|
|
231
|
+
if (waitQueue.length >= MAX_QUEUED_TRANSCODES) {
|
|
232
|
+
reject(new ProxyCapacityError());
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
waitQueue.push(tryAcquire);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
tryAcquire();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function releaseSlot() {
|
|
242
|
+
activeTranscodes--;
|
|
243
|
+
const next = waitQueue.shift();
|
|
244
|
+
if (next) next();
|
|
245
|
+
}
|
|
246
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
247
|
+
function maintainProxyCache(cacheDir) {
|
|
248
|
+
try {
|
|
249
|
+
cleanupProxyCache(cacheDir, { protectedPaths: new Set(inFlight.keys()) });
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.warn(
|
|
252
|
+
`[media-proxy] cache cleanup failed: ${error instanceof Error ? error.message : String(error)}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function markCacheEntryUsed(cachePath) {
|
|
257
|
+
try {
|
|
258
|
+
const now = /* @__PURE__ */ new Date();
|
|
259
|
+
utimesSync(cachePath, now, now);
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
var failedTranscodes = /* @__PURE__ */ new Map();
|
|
264
|
+
var hdrFilterCheck;
|
|
265
|
+
function ensureHdrFilters(ffmpegPath) {
|
|
266
|
+
if (hdrFilterCheck?.ffmpegPath === ffmpegPath) return hdrFilterCheck.promise;
|
|
267
|
+
const promise = new Promise((resolveCheck, rejectCheck) => {
|
|
268
|
+
const proc = spawn(ffmpegPath, ["-hide_banner", "-filters"], {
|
|
269
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
270
|
+
});
|
|
271
|
+
let stdout = "";
|
|
272
|
+
proc.stdout?.on("data", (chunk) => {
|
|
273
|
+
stdout += chunk.toString();
|
|
274
|
+
});
|
|
275
|
+
proc.on("error", () => rejectCheck(new FfmpegMissingFilterError()));
|
|
276
|
+
proc.on("close", (code) => {
|
|
277
|
+
if (code !== 0 || !/\bzscale\b/.test(stdout) || !/\btonemap\b/.test(stdout)) {
|
|
278
|
+
rejectCheck(new FfmpegMissingFilterError());
|
|
279
|
+
} else {
|
|
280
|
+
resolveCheck();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
hdrFilterCheck = { ffmpegPath, promise };
|
|
285
|
+
return promise;
|
|
286
|
+
}
|
|
287
|
+
function rememberFailure(cachePath, error) {
|
|
288
|
+
failedTranscodes.delete(cachePath);
|
|
289
|
+
failedTranscodes.set(cachePath, { error, expiresAt: Date.now() + FAILURE_CACHE_TTL_MS });
|
|
290
|
+
while (failedTranscodes.size > MAX_FAILURE_CACHE_ENTRIES) {
|
|
291
|
+
const oldest = failedTranscodes.keys().next().value;
|
|
292
|
+
if (oldest === void 0) break;
|
|
293
|
+
failedTranscodes.delete(oldest);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function clearFailedTranscodesForTest() {
|
|
297
|
+
failedTranscodes.clear();
|
|
298
|
+
}
|
|
299
|
+
async function runFfmpeg(sourcePath, outputPath, variant) {
|
|
300
|
+
const metadata = await probeMediaMetadata(sourcePath);
|
|
301
|
+
const ffmpegPath = findFfBinary("ffmpeg", { configuredMustExist: true });
|
|
302
|
+
if (!ffmpegPath) {
|
|
303
|
+
throw new FfmpegUnavailableError();
|
|
304
|
+
}
|
|
305
|
+
if (metadata.color.isHdr && variant !== "vp9") await ensureHdrFilters(ffmpegPath);
|
|
306
|
+
const evenScale = "scale=trunc(iw/2)*2:trunc(ih/2)*2";
|
|
307
|
+
const pixelFormat = variant === "vp9" ? "yuva420p" : "yuv420p";
|
|
308
|
+
const videoFilter = metadata.color.isHdr && variant !== "vp9" ? [
|
|
309
|
+
"zscale=t=linear:npl=100",
|
|
310
|
+
"tonemap=hable:desat=0",
|
|
311
|
+
"zscale=p=bt709:t=bt709:m=bt709:r=tv",
|
|
312
|
+
evenScale,
|
|
313
|
+
`format=${pixelFormat}`
|
|
314
|
+
].join(",") : [evenScale, `format=${pixelFormat}`].join(",");
|
|
315
|
+
return new Promise((resolvePromise, reject) => {
|
|
316
|
+
const commonArgs = ["-y", "-i", sourcePath, "-vf", videoFilter];
|
|
317
|
+
const h264Args = [
|
|
318
|
+
"-c:v",
|
|
319
|
+
"libx264",
|
|
320
|
+
"-profile:v",
|
|
321
|
+
"high",
|
|
322
|
+
"-pix_fmt",
|
|
323
|
+
"yuv420p",
|
|
324
|
+
"-colorspace",
|
|
325
|
+
"bt709",
|
|
326
|
+
"-color_primaries",
|
|
327
|
+
"bt709",
|
|
328
|
+
"-color_trc",
|
|
329
|
+
"bt709",
|
|
330
|
+
"-crf",
|
|
331
|
+
"18",
|
|
332
|
+
"-preset",
|
|
333
|
+
"veryfast",
|
|
334
|
+
"-c:a",
|
|
335
|
+
"aac",
|
|
336
|
+
"-movflags",
|
|
337
|
+
"+faststart"
|
|
338
|
+
];
|
|
339
|
+
const vp9Args = [
|
|
340
|
+
"-c:v",
|
|
341
|
+
"libvpx-vp9",
|
|
342
|
+
"-b:v",
|
|
343
|
+
"0",
|
|
344
|
+
"-crf",
|
|
345
|
+
"23",
|
|
346
|
+
"-deadline",
|
|
347
|
+
"good",
|
|
348
|
+
"-pix_fmt",
|
|
349
|
+
"yuva420p",
|
|
350
|
+
"-colorspace",
|
|
351
|
+
"bt709",
|
|
352
|
+
"-color_primaries",
|
|
353
|
+
"bt709",
|
|
354
|
+
"-color_trc",
|
|
355
|
+
"bt709",
|
|
356
|
+
"-row-mt",
|
|
357
|
+
"1",
|
|
358
|
+
"-cpu-used",
|
|
359
|
+
"4",
|
|
360
|
+
"-auto-alt-ref",
|
|
361
|
+
"0",
|
|
362
|
+
"-metadata:s:v:0",
|
|
363
|
+
"alpha_mode=1",
|
|
364
|
+
"-ac",
|
|
365
|
+
"2",
|
|
366
|
+
"-c:a",
|
|
367
|
+
"libopus"
|
|
368
|
+
];
|
|
369
|
+
const args = [...commonArgs, ...variant === "vp9" ? vp9Args : h264Args, outputPath];
|
|
370
|
+
const proc = spawn(ffmpegPath, args, {
|
|
371
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
372
|
+
timeout: TRANSCODE_TIMEOUT_MS,
|
|
373
|
+
killSignal: "SIGKILL"
|
|
374
|
+
});
|
|
375
|
+
let stderrTail = "";
|
|
376
|
+
proc.stderr?.on("data", (chunk) => {
|
|
377
|
+
stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_MAX_CHARS);
|
|
378
|
+
});
|
|
379
|
+
proc.on("error", (err) => {
|
|
380
|
+
reject(new ProxyTranscodeError(`failed to spawn ffmpeg: ${err.message}`, null, stderrTail));
|
|
381
|
+
});
|
|
382
|
+
proc.on("close", (code, signal) => {
|
|
383
|
+
if (code === 0) {
|
|
384
|
+
resolvePromise();
|
|
385
|
+
} else if (signal) {
|
|
386
|
+
reject(
|
|
387
|
+
new ProxyTranscodeError(
|
|
388
|
+
`ffmpeg killed by ${signal} (timeout ${TRANSCODE_TIMEOUT_MS}ms or external kill)`,
|
|
389
|
+
null,
|
|
390
|
+
stderrTail
|
|
391
|
+
)
|
|
392
|
+
);
|
|
393
|
+
} else {
|
|
394
|
+
reject(new ProxyTranscodeError(`ffmpeg exited with code ${code}`, code, stderrTail));
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
async function transcodeToCache(absoluteSourcePath, cachePath, variant) {
|
|
400
|
+
await acquireSlot();
|
|
401
|
+
try {
|
|
402
|
+
if (existsSync2(cachePath)) return cachePath;
|
|
403
|
+
const cacheDir = dirname(cachePath);
|
|
404
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
405
|
+
const tempPath = join2(cacheDir, `.tmp-${randomUUID()}-${basename(cachePath)}`);
|
|
406
|
+
try {
|
|
407
|
+
await runFfmpeg(absoluteSourcePath, tempPath, variant);
|
|
408
|
+
renameSync(tempPath, cachePath);
|
|
409
|
+
maintainProxyCache(cacheDir);
|
|
410
|
+
return cachePath;
|
|
411
|
+
} finally {
|
|
412
|
+
if (existsSync2(tempPath)) unlinkSync2(tempPath);
|
|
413
|
+
}
|
|
414
|
+
} finally {
|
|
415
|
+
releaseSlot();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async function resolveProxy(projectDir, absoluteSourcePath, variant = "h264") {
|
|
419
|
+
const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
|
|
420
|
+
const cachePath = getCanonicalProxyCachePath(source, variant);
|
|
421
|
+
if (existsSync2(cachePath)) {
|
|
422
|
+
markCacheEntryUsed(cachePath);
|
|
423
|
+
maintainProxyCache(dirname(cachePath));
|
|
424
|
+
return cachePath;
|
|
425
|
+
}
|
|
426
|
+
const rememberedFailure = failedTranscodes.get(cachePath);
|
|
427
|
+
if (rememberedFailure) {
|
|
428
|
+
if (rememberedFailure.expiresAt > Date.now()) throw rememberedFailure.error;
|
|
429
|
+
failedTranscodes.delete(cachePath);
|
|
430
|
+
}
|
|
431
|
+
const existing = inFlight.get(cachePath);
|
|
432
|
+
if (existing) return existing;
|
|
433
|
+
const promise = transcodeToCache(source.sourcePath, cachePath, variant).catch((err) => {
|
|
434
|
+
if (err instanceof ProxyTranscodeError && !(err instanceof FfmpegUnavailableError) && !(err instanceof FfmpegMissingFilterError) && !(err instanceof ProxyCapacityError) && !(err instanceof ProxySourceOutsideProjectError)) {
|
|
435
|
+
rememberFailure(cachePath, err);
|
|
436
|
+
}
|
|
437
|
+
throw err;
|
|
438
|
+
}).finally(() => {
|
|
439
|
+
inFlight.delete(cachePath);
|
|
440
|
+
});
|
|
441
|
+
inFlight.set(cachePath, promise);
|
|
442
|
+
return promise;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export {
|
|
446
|
+
PROXY_PARAMS_VERSION,
|
|
447
|
+
TRANSCODE_TIMEOUT_MS,
|
|
448
|
+
DEFAULT_PROXY_WAIT_TIMEOUT_MS,
|
|
449
|
+
ProxyTranscodeError,
|
|
450
|
+
FfmpegMissingFilterError,
|
|
451
|
+
ProxyCapacityError,
|
|
452
|
+
ProxySourceOutsideProjectError,
|
|
453
|
+
ProxyWaitTimeoutError,
|
|
454
|
+
waitForProxy,
|
|
455
|
+
getProxyCachePath,
|
|
456
|
+
clearFailedTranscodesForTest,
|
|
457
|
+
resolveProxy
|
|
458
|
+
};
|
|
459
|
+
//# sourceMappingURL=chunk-YBR7MXIO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helpers/proxyTranscoder.ts","../src/helpers/proxyCache.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport {\n existsSync,\n mkdirSync,\n realpathSync,\n renameSync,\n statSync,\n unlinkSync,\n utimesSync,\n} from \"node:fs\";\nimport { basename, dirname, isAbsolute, join, relative, sep } from \"node:path\";\nimport { findFfBinary } from \"@hyperframes/parsers/ff-binaries\";\nimport { probeMediaMetadata } from \"./mediaMetadata.js\";\nimport { cleanupProxyCache } from \"./proxyCache.js\";\nimport { PROXY_VARIANT_CONFIG, type ProxyVariant } from \"./mediaCodecMap.js\";\n\n/**\n * Transcodes browser-hostile local video sources (HEVC, ProRes, ...) into a\n * cached, seekable authoring proxy. Consumed by the preview/play/static\n * project routes (U3/U4) to serve a `?hf-proxy=` request; never used on\n * the render path (render always sees the original file).\n *\n * IMPORTANT — request-lifecycle detachment: nothing here accepts or wires an\n * AbortSignal. `resolveProxy` returns a promise shared by every concurrent\n * caller for the same cache key (in-flight dedupe below); if a route handler\n * killed the ffmpeg child on client abort (page reload, HMR), every other\n * caller waiting on that same promise would fail too, and the next request\n * would restart a transcode that may have been minutes into a long asset.\n * Callers MUST let the child run to completion regardless of request\n * cancellation and simply let the held response also abort — the cache\n * entry still lands for the next request.\n */\n\nexport const PROXY_PARAMS_VERSION = \"v3\";\n\nconst CACHE_DIR_NAME = \".transcode-cache\";\n\nfunction boundedEnvInteger(name: string, fallback: number, min: number, max: number): number {\n const raw = process.env[name]?.trim();\n if (!raw) return fallback;\n const parsed = Number(raw);\n return Number.isSafeInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback;\n}\n\n// ffmpeg is internally multithreaded, so two concurrent proxy encodes already\n// saturate a typical laptop. Operators of shared/large machines may tune the\n// bounded values without patching the package; invalid values fail safe.\nconst MAX_CONCURRENT_TRANSCODES = boundedEnvInteger(\"HYPERFRAMES_PROXY_MAX_CONCURRENCY\", 2, 1, 16);\nconst MAX_QUEUED_TRANSCODES = boundedEnvInteger(\"HYPERFRAMES_PROXY_MAX_QUEUE\", 8, 0, 256);\n\nconst STDERR_TAIL_MAX_CHARS = 4000;\nexport const TRANSCODE_TIMEOUT_MS = 15 * 60 * 1000;\nconst FAILURE_CACHE_TTL_MS = 60 * 1000;\nconst MAX_FAILURE_CACHE_ENTRIES = 128;\nexport const DEFAULT_PROXY_WAIT_TIMEOUT_MS = 2 * 60 * 1000;\n\nexport class ProxyTranscodeError extends Error {\n readonly exitCode: number | null;\n readonly stderrTail: string;\n\n constructor(message: string, exitCode: number | null, stderrTail: string) {\n super(message);\n this.name = \"ProxyTranscodeError\";\n this.exitCode = exitCode;\n this.stderrTail = stderrTail;\n }\n}\n\n/** \"ffmpeg isn't installed\" — an environment condition, not a per-source\n * failure, so it is deliberately NOT remembered by the negative cache below\n * (installing ffmpeg mid-session must recover without a server restart). */\nclass FfmpegUnavailableError extends ProxyTranscodeError {\n constructor() {\n super(\"ffmpeg binary not found\", null, \"\");\n }\n}\n\nexport class FfmpegMissingFilterError extends ProxyTranscodeError {\n constructor() {\n super(\n \"HDR proxying requires ffmpeg zscale/tonemap filters (libzimg); install an ffmpeg build with libzimg support\",\n null,\n \"\",\n );\n this.name = \"FfmpegMissingFilterError\";\n }\n}\n\nexport class ProxyCapacityError extends ProxyTranscodeError {\n constructor() {\n super(\"media proxy queue is full; retry shortly\", null, \"\");\n this.name = \"ProxyCapacityError\";\n }\n}\n\nexport class ProxySourceOutsideProjectError extends ProxyTranscodeError {\n constructor() {\n super(\"media proxy source must be inside the project\", null, \"\");\n this.name = \"ProxySourceOutsideProjectError\";\n }\n}\n\nexport class ProxyWaitTimeoutError extends ProxyTranscodeError {\n constructor(timeoutMs: number) {\n super(`media proxy did not become ready within ${timeoutMs}ms`, null, \"\");\n this.name = \"ProxyWaitTimeoutError\";\n }\n}\n\n/** Bounds one caller's wait without cancelling the shared in-flight ffmpeg\n * job. Other preview/publish callers still receive the completed cache entry. */\nexport async function waitForProxy<T>(\n promise: Promise<T>,\n timeoutMs = DEFAULT_PROXY_WAIT_TIMEOUT_MS,\n): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => reject(new ProxyWaitTimeoutError(timeoutMs)), timeoutMs);\n timer.unref();\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\n/**\n * Cache key inputs per the plan: source path relative to the project (so the\n * cache is portable across checkouts at different absolute locations), mtime\n * and file size (mtime alone can collide on same-second re-exports on\n * coarse-timestamp filesystems; size catches nearly all such cases at zero\n * cost), and a params version token so changing the ffmpeg recipe below\n * invalidates every cached proxy cleanly.\n */\ntype CanonicalProxySource = {\n projectDir: string;\n sourcePath: string;\n relativePath: string;\n};\n\nfunction canonicalizeProxySource(\n projectDir: string,\n absoluteSourcePath: string,\n): CanonicalProxySource {\n const canonicalProjectDir = realpathSync(projectDir);\n const canonicalSourcePath = realpathSync(absoluteSourcePath);\n const relPath = relative(canonicalProjectDir, canonicalSourcePath);\n if (relPath === \"..\" || relPath.startsWith(`..${sep}`) || isAbsolute(relPath)) {\n throw new ProxySourceOutsideProjectError();\n }\n return {\n projectDir: canonicalProjectDir,\n sourcePath: canonicalSourcePath,\n relativePath: relPath.normalize(\"NFC\"),\n };\n}\n\nfunction buildProxyCacheKey(source: CanonicalProxySource, variant: ProxyVariant): string {\n const stat = statSync(source.sourcePath);\n return createHash(\"sha256\")\n .update(\n `${source.relativePath}\\0${stat.mtimeMs}\\0${stat.size}\\0${PROXY_PARAMS_VERSION}\\0${variant}`,\n )\n .digest(\"hex\");\n}\n\nfunction getCanonicalProxyCachePath(source: CanonicalProxySource, variant: ProxyVariant): string {\n const key = buildProxyCacheKey(source, variant);\n return join(\n source.projectDir,\n CACHE_DIR_NAME,\n `${key}${PROXY_VARIANT_CONFIG[variant].extension}`,\n );\n}\n\n/**\n * Computes the absolute path a proxy for this source would live at, without\n * transcoding anything. Route handlers use this to check cache state (e.g.\n * for ETag/If-None-Match) before deciding whether to await a transcode.\n */\nexport function getProxyCachePath(\n projectDir: string,\n absoluteSourcePath: string,\n variant: ProxyVariant = \"h264\",\n): string {\n return getCanonicalProxyCachePath(\n canonicalizeProxySource(projectDir, absoluteSourcePath),\n variant,\n );\n}\n\n// --- global concurrency limiter -------------------------------------------\n// ponytail: a bare counter + FIFO wait queue is the whole semaphore; no\n// dependency pulled in for this. Both element-triggered and pre-warm calls\n// go through the same `resolveProxy` entry point, so both queue here.\n\nlet activeTranscodes = 0;\nconst waitQueue: Array<() => void> = [];\n\nfunction acquireSlot(): Promise<void> {\n return new Promise((resolveSlot, reject) => {\n const tryAcquire = (): void => {\n if (activeTranscodes < MAX_CONCURRENT_TRANSCODES) {\n activeTranscodes++;\n resolveSlot();\n } else {\n if (waitQueue.length >= MAX_QUEUED_TRANSCODES) {\n reject(new ProxyCapacityError());\n return;\n }\n waitQueue.push(tryAcquire);\n }\n };\n tryAcquire();\n });\n}\n\nfunction releaseSlot(): void {\n activeTranscodes--;\n const next = waitQueue.shift();\n if (next) next();\n}\n\n// --- per-key in-flight dedupe ----------------------------------------------\n\nconst inFlight = new Map<string, Promise<string>>();\n\nfunction maintainProxyCache(cacheDir: string): void {\n try {\n cleanupProxyCache(cacheDir, { protectedPaths: new Set(inFlight.keys()) });\n } catch (error) {\n // Cache maintenance must never turn a playable preview into an error.\n console.warn(\n `[media-proxy] cache cleanup failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n}\n\nfunction markCacheEntryUsed(cachePath: string): void {\n try {\n const now = new Date();\n utimesSync(cachePath, now, now);\n } catch {\n // A concurrent cleanup may have removed a stale entry after existsSync;\n // the normal miss path below will recreate it on the next request.\n }\n}\n\n// --- negative cache ---------------------------------------------------------\n// A source that failed to transcode fails again identically until the file\n// changes (the cache key embeds mtime+size, so a re-export invalidates this\n// naturally). Remembering the failure per key means repeated `?hf-proxy=`\n// requests for a broken asset rethrow instantly instead of respawning ffmpeg\n// on every retry the browser makes.\ninterface RememberedFailure {\n error: ProxyTranscodeError;\n expiresAt: number;\n}\n\nconst failedTranscodes = new Map<string, RememberedFailure>();\n\nlet hdrFilterCheck: { ffmpegPath: string; promise: Promise<void> } | undefined;\n\nfunction ensureHdrFilters(ffmpegPath: string): Promise<void> {\n if (hdrFilterCheck?.ffmpegPath === ffmpegPath) return hdrFilterCheck.promise;\n const promise = new Promise<void>((resolveCheck, rejectCheck) => {\n const proc = spawn(ffmpegPath, [\"-hide_banner\", \"-filters\"], {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n let stdout = \"\";\n proc.stdout?.on(\"data\", (chunk: Buffer) => {\n stdout += chunk.toString();\n });\n proc.on(\"error\", () => rejectCheck(new FfmpegMissingFilterError()));\n proc.on(\"close\", (code) => {\n if (code !== 0 || !/\\bzscale\\b/.test(stdout) || !/\\btonemap\\b/.test(stdout)) {\n rejectCheck(new FfmpegMissingFilterError());\n } else {\n resolveCheck();\n }\n });\n });\n hdrFilterCheck = { ffmpegPath, promise };\n return promise;\n}\n\nfunction rememberFailure(cachePath: string, error: ProxyTranscodeError): void {\n failedTranscodes.delete(cachePath);\n failedTranscodes.set(cachePath, { error, expiresAt: Date.now() + FAILURE_CACHE_TTL_MS });\n while (failedTranscodes.size > MAX_FAILURE_CACHE_ENTRIES) {\n const oldest = failedTranscodes.keys().next().value;\n if (oldest === undefined) break;\n failedTranscodes.delete(oldest);\n }\n}\n\n/** Test hook: forget remembered transcode failures (module state persists\n * across tests that don't reload the module). */\nexport function clearFailedTranscodesForTest(): void {\n failedTranscodes.clear();\n}\n\nasync function runFfmpeg(\n sourcePath: string,\n outputPath: string,\n variant: ProxyVariant,\n): Promise<void> {\n const metadata = await probeMediaMetadata(sourcePath);\n const ffmpegPath = findFfBinary(\"ffmpeg\", { configuredMustExist: true });\n if (!ffmpegPath) {\n throw new FfmpegUnavailableError();\n }\n // The HDR tonemap filters discard alpha. VP9 is the alpha-preserving proxy\n // variant, so retain its source color values instead of making it opaque.\n if (metadata.color.isHdr && variant !== \"vp9\") await ensureHdrFilters(ffmpegPath);\n const evenScale = \"scale=trunc(iw/2)*2:trunc(ih/2)*2\";\n const pixelFormat = variant === \"vp9\" ? \"yuva420p\" : \"yuv420p\";\n const videoFilter =\n metadata.color.isHdr && variant !== \"vp9\"\n ? [\n \"zscale=t=linear:npl=100\",\n \"tonemap=hable:desat=0\",\n \"zscale=p=bt709:t=bt709:m=bt709:r=tv\",\n evenScale,\n `format=${pixelFormat}`,\n ].join(\",\")\n : [evenScale, `format=${pixelFormat}`].join(\",\");\n\n return new Promise((resolvePromise, reject) => {\n const commonArgs = [\"-y\", \"-i\", sourcePath, \"-vf\", videoFilter];\n const h264Args = [\n \"-c:v\",\n \"libx264\",\n \"-profile:v\",\n \"high\",\n \"-pix_fmt\",\n \"yuv420p\",\n \"-colorspace\",\n \"bt709\",\n \"-color_primaries\",\n \"bt709\",\n \"-color_trc\",\n \"bt709\",\n \"-crf\",\n \"18\",\n \"-preset\",\n \"veryfast\",\n \"-c:a\",\n \"aac\",\n \"-movflags\",\n \"+faststart\",\n ];\n const vp9Args = [\n \"-c:v\",\n \"libvpx-vp9\",\n \"-b:v\",\n \"0\",\n \"-crf\",\n \"23\",\n \"-deadline\",\n \"good\",\n \"-pix_fmt\",\n \"yuva420p\",\n \"-colorspace\",\n \"bt709\",\n \"-color_primaries\",\n \"bt709\",\n \"-color_trc\",\n \"bt709\",\n \"-row-mt\",\n \"1\",\n \"-cpu-used\",\n \"4\",\n \"-auto-alt-ref\",\n \"0\",\n \"-metadata:s:v:0\",\n \"alpha_mode=1\",\n \"-ac\",\n \"2\",\n \"-c:a\",\n \"libopus\",\n ];\n const args = [...commonArgs, ...(variant === \"vp9\" ? vp9Args : h264Args), outputPath];\n\n // Hard ceiling so a hung ffmpeg can never permanently occupy one of the\n // global transcode slots: the child is killed and the slot released via\n // the caller's finally. Generous because long assets transcode at\n // roughly real time; a healthy encode of any authoring asset fits.\n const proc = spawn(ffmpegPath, args, {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n timeout: TRANSCODE_TIMEOUT_MS,\n killSignal: \"SIGKILL\",\n });\n let stderrTail = \"\";\n proc.stderr?.on(\"data\", (chunk: Buffer) => {\n stderrTail = (stderrTail + chunk.toString()).slice(-STDERR_TAIL_MAX_CHARS);\n });\n proc.on(\"error\", (err) => {\n reject(new ProxyTranscodeError(`failed to spawn ffmpeg: ${err.message}`, null, stderrTail));\n });\n proc.on(\"close\", (code, signal) => {\n if (code === 0) {\n resolvePromise();\n } else if (signal) {\n reject(\n new ProxyTranscodeError(\n `ffmpeg killed by ${signal} (timeout ${TRANSCODE_TIMEOUT_MS}ms or external kill)`,\n null,\n stderrTail,\n ),\n );\n } else {\n reject(new ProxyTranscodeError(`ffmpeg exited with code ${code}`, code, stderrTail));\n }\n });\n });\n}\n\nasync function transcodeToCache(\n absoluteSourcePath: string,\n cachePath: string,\n variant: ProxyVariant,\n): Promise<string> {\n await acquireSlot();\n try {\n // Another caller may have finished (or a pre-warm beat us) while queued.\n if (existsSync(cachePath)) return cachePath;\n\n const cacheDir = dirname(cachePath);\n mkdirSync(cacheDir, { recursive: true });\n const tempPath = join(cacheDir, `.tmp-${randomUUID()}-${basename(cachePath)}`);\n try {\n await runFfmpeg(absoluteSourcePath, tempPath, variant);\n renameSync(tempPath, cachePath);\n maintainProxyCache(cacheDir);\n return cachePath;\n } finally {\n // No partial files: if anything above threw, remove whatever ffmpeg\n // may have partially written under the temp name.\n if (existsSync(tempPath)) unlinkSync(tempPath);\n }\n } finally {\n releaseSlot();\n }\n}\n\n/**\n * Resolves the cached proxy variant for `absoluteSourcePath`, transcoding it at\n * most once per cache key. Concurrent calls for the same key (including a\n * pre-warm call racing an element-triggered one) share one ffmpeg child and\n * one promise; calls for different keys queue through the global concurrency\n * limiter above. Throws `ProxyTranscodeError` on failure (missing ffmpeg or a\n * nonzero exit) — callers (route handlers) decide how to surface that (502).\n */\nexport async function resolveProxy(\n projectDir: string,\n absoluteSourcePath: string,\n variant: ProxyVariant = \"h264\",\n): Promise<string> {\n const source = canonicalizeProxySource(projectDir, absoluteSourcePath);\n const cachePath = getCanonicalProxyCachePath(source, variant);\n if (existsSync(cachePath)) {\n markCacheEntryUsed(cachePath);\n maintainProxyCache(dirname(cachePath));\n return cachePath;\n }\n\n const rememberedFailure = failedTranscodes.get(cachePath);\n if (rememberedFailure) {\n if (rememberedFailure.expiresAt > Date.now()) throw rememberedFailure.error;\n failedTranscodes.delete(cachePath);\n }\n\n const existing = inFlight.get(cachePath);\n if (existing) return existing;\n\n const promise = transcodeToCache(source.sourcePath, cachePath, variant)\n .catch((err: unknown) => {\n if (\n err instanceof ProxyTranscodeError &&\n !(err instanceof FfmpegUnavailableError) &&\n !(err instanceof FfmpegMissingFilterError) &&\n !(err instanceof ProxyCapacityError) &&\n !(err instanceof ProxySourceOutsideProjectError)\n ) {\n rememberFailure(cachePath, err);\n }\n throw err;\n })\n .finally(() => {\n inFlight.delete(cachePath);\n });\n inFlight.set(cachePath, promise);\n return promise;\n}\n","import { existsSync, readdirSync, statSync, unlinkSync } from \"node:fs\";\nimport { extname, join } from \"node:path\";\nimport { PROXY_VARIANT_CONFIG } from \"./mediaCodecMap.js\";\n\nconst DEFAULT_MAX_BYTES = 10 * 1024 * 1024 * 1024;\nconst DEFAULT_STALE_TEMP_MS = 60 * 60 * 1000;\nconst DEFAULT_MIN_SWEEP_INTERVAL_MS = 5 * 60 * 1000;\nconst PROXY_EXTENSIONS: ReadonlySet<string> = new Set(\n Object.values(PROXY_VARIANT_CONFIG).map(({ extension }) => extension),\n);\n\nexport interface ProxyCacheCleanupOptions {\n maxBytes?: number;\n maxIdleMs?: number;\n staleTempMs?: number;\n minSweepIntervalMs?: number;\n protectedPaths?: ReadonlySet<string>;\n now?: number;\n}\n\nexport interface ProxyCacheCleanupResult {\n removed: string[];\n bytesBefore: number;\n bytesAfter: number;\n skipped: boolean;\n}\n\ninterface CacheEntry {\n path: string;\n size: number;\n modifiedAt: number;\n protected: boolean;\n}\n\nconst lastSweepAt = new Map<string, number>();\n\nfunction positiveEnvNumber(name: string, fallback: number): number {\n const parsed = Number(process.env[name]);\n return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;\n}\n\nfunction proxyCacheCleanupDefaults(): Required<\n Pick<ProxyCacheCleanupOptions, \"maxBytes\" | \"maxIdleMs\" | \"staleTempMs\" | \"minSweepIntervalMs\">\n> {\n return {\n maxBytes: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_MAX_BYTES\", DEFAULT_MAX_BYTES),\n maxIdleMs: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_MAX_IDLE_DAYS\", 30) * 24 * 60 * 60 * 1000,\n staleTempMs: positiveEnvNumber(\"HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS\", DEFAULT_STALE_TEMP_MS),\n minSweepIntervalMs: positiveEnvNumber(\n \"HYPERFRAMES_PROXY_CACHE_SWEEP_INTERVAL_MS\",\n DEFAULT_MIN_SWEEP_INTERVAL_MS,\n ),\n };\n}\n\nfunction shouldSkipSweep(cacheDir: string, now: number, minSweepIntervalMs: number): boolean {\n const previousSweep = lastSweepAt.get(cacheDir);\n if (previousSweep !== undefined && now - previousSweep < minSweepIntervalMs) return true;\n lastSweepAt.set(cacheDir, now);\n return false;\n}\n\nfunction readCacheInventory(\n cacheDir: string,\n protectedPaths: ReadonlySet<string>,\n now: number,\n staleTempMs: number,\n): { entries: CacheEntry[]; staleTemps: CacheEntry[] } {\n const entries: CacheEntry[] = [];\n const staleTemps: CacheEntry[] = [];\n for (const dirent of readdirSync(cacheDir, { withFileTypes: true })) {\n if (!dirent.isFile()) continue;\n const path = join(cacheDir, dirent.name);\n const stat = statSync(path);\n const entry = {\n path,\n size: stat.size,\n modifiedAt: stat.mtimeMs,\n protected: protectedPaths.has(path),\n };\n if (dirent.name.startsWith(\".tmp-\")) {\n if (now - stat.mtimeMs >= staleTempMs) staleTemps.push(entry);\n } else if (PROXY_EXTENSIONS.has(extname(dirent.name))) {\n entries.push(entry);\n }\n }\n const oldestFirst = (a: CacheEntry, b: CacheEntry): number =>\n a.modifiedAt - b.modifiedAt || a.path.localeCompare(b.path);\n entries.sort(oldestFirst);\n staleTemps.sort(oldestFirst);\n return { entries, staleTemps };\n}\n\nfunction evictCacheEntries(\n entries: CacheEntry[],\n staleTemps: CacheEntry[],\n now: number,\n maxIdleMs: number,\n maxBytes: number,\n): Omit<ProxyCacheCleanupResult, \"skipped\"> {\n const bytesBefore = entries.reduce((total, entry) => total + entry.size, 0);\n let bytesAfter = bytesBefore;\n const removed: string[] = [];\n const remove = (entry: CacheEntry, countsTowardBudget: boolean): void => {\n unlinkSync(entry.path);\n removed.push(entry.path);\n if (countsTowardBudget) bytesAfter -= entry.size;\n };\n\n for (const entry of staleTemps) remove(entry, false);\n for (const entry of entries) {\n if (!entry.protected && now - entry.modifiedAt >= maxIdleMs) remove(entry, true);\n }\n for (const entry of entries) {\n if (bytesAfter <= maxBytes) break;\n if (!entry.protected && existsSync(entry.path)) remove(entry, true);\n }\n return { removed, bytesBefore, bytesAfter };\n}\n\n/**\n * Opportunistically bounds a project's transparent-proxy cache. Cleanup is\n * synchronous because callers already perform filesystem bookkeeping on the\n * preview request path, but rate limiting keeps the directory scan off the\n * hot path. Errors intentionally bubble so callers can warn without turning\n * a cache-maintenance failure into a preview failure.\n */\nexport function cleanupProxyCache(\n cacheDir: string,\n options: ProxyCacheCleanupOptions = {},\n): ProxyCacheCleanupResult {\n const defaults = proxyCacheCleanupDefaults();\n const now = options.now ?? Date.now();\n const minSweepIntervalMs = options.minSweepIntervalMs ?? defaults.minSweepIntervalMs;\n if (shouldSkipSweep(cacheDir, now, minSweepIntervalMs)) {\n return { removed: [], bytesBefore: 0, bytesAfter: 0, skipped: true };\n }\n if (!existsSync(cacheDir)) {\n return { removed: [], bytesBefore: 0, bytesAfter: 0, skipped: false };\n }\n\n const maxBytes = options.maxBytes ?? defaults.maxBytes;\n const maxIdleMs = options.maxIdleMs ?? defaults.maxIdleMs;\n const staleTempMs = options.staleTempMs ?? defaults.staleTempMs;\n const protectedPaths = options.protectedPaths ?? new Set<string>();\n const { entries, staleTemps } = readCacheInventory(cacheDir, protectedPaths, now, staleTempMs);\n return {\n ...evictCacheEntries(entries, staleTemps, now, maxIdleMs, maxBytes),\n skipped: false,\n };\n}\n"],"mappings":"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","names":["existsSync","statSync","unlinkSync","join","statSync","join","existsSync","unlinkSync"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PROXY_PARAMS_VERSION,
|
|
3
|
+
resolveProxy
|
|
4
|
+
} from "./chunk-YBR7MXIO.js";
|
|
5
|
+
import {
|
|
6
|
+
createMediaCodecProbeCache,
|
|
7
|
+
proxyVariantFor,
|
|
8
|
+
scanProjectMediaCodecMap
|
|
9
|
+
} from "./chunk-LVXVG4V6.js";
|
|
10
|
+
|
|
11
|
+
// src/helpers/mediaProxyPreview.ts
|
|
12
|
+
import { resolve } from "path";
|
|
13
|
+
function isAutoProxyEnabled(adapter) {
|
|
14
|
+
return adapter.autoProxy !== false;
|
|
15
|
+
}
|
|
16
|
+
function resolvePreviewMediaCodecProbeCache(adapter) {
|
|
17
|
+
return adapter.mediaCodecProbeCache ?? createMediaCodecProbeCache();
|
|
18
|
+
}
|
|
19
|
+
function proxyEtagSalt(raw) {
|
|
20
|
+
if (raw === void 0) return "";
|
|
21
|
+
return `:proxy:${raw}:${PROXY_PARAMS_VERSION}`;
|
|
22
|
+
}
|
|
23
|
+
function injectScriptTagIntoHead(html, scriptTag) {
|
|
24
|
+
if (html.includes("</head>")) return html.replace("</head>", `${scriptTag}
|
|
25
|
+
</head>`);
|
|
26
|
+
return `${scriptTag}
|
|
27
|
+
${html}`;
|
|
28
|
+
}
|
|
29
|
+
async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeCache) {
|
|
30
|
+
let map;
|
|
31
|
+
try {
|
|
32
|
+
map = await scanProjectMediaCodecMap(
|
|
33
|
+
projectDir,
|
|
34
|
+
htmlSources,
|
|
35
|
+
probeCache ? { cache: probeCache } : {}
|
|
36
|
+
);
|
|
37
|
+
} catch {
|
|
38
|
+
return html;
|
|
39
|
+
}
|
|
40
|
+
if (Object.keys(map).length === 0) return html;
|
|
41
|
+
for (const [rootRelativePathname, facts] of Object.entries(map)) {
|
|
42
|
+
if (!facts.browserHostile) continue;
|
|
43
|
+
resolveProxy(
|
|
44
|
+
projectDir,
|
|
45
|
+
resolve(projectDir, rootRelativePathname.replace(/^\/+/, "")),
|
|
46
|
+
proxyVariantFor(facts)
|
|
47
|
+
).catch(() => {
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
const json = JSON.stringify(map).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
51
|
+
const tag = `<script data-hf-media-codec-map>window.__HF_MEDIA_CODEC_MAP__=${json};</script>`;
|
|
52
|
+
return injectScriptTagIntoHead(html, tag);
|
|
53
|
+
}
|
|
54
|
+
async function injectMediaCodecMap(html, adapter, projectDir, compSrcPath, probeCache) {
|
|
55
|
+
if (!isAutoProxyEnabled(adapter)) return html;
|
|
56
|
+
return injectMediaCodecMapIntoHtml(html, projectDir, [{ html, compSrcPath }], probeCache);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
isAutoProxyEnabled,
|
|
61
|
+
resolvePreviewMediaCodecProbeCache,
|
|
62
|
+
proxyEtagSalt,
|
|
63
|
+
injectMediaCodecMapIntoHtml,
|
|
64
|
+
injectMediaCodecMap
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=chunk-ZUW4PULZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helpers/mediaProxyPreview.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport type { StudioApiAdapter } from \"../types.js\";\nimport {\n createMediaCodecProbeCache,\n proxyVariantFor,\n scanProjectMediaCodecMap,\n type HtmlSourceLike,\n type MediaCodecMap,\n type MediaCodecProbeCache,\n} from \"./mediaCodecMap.js\";\nimport { resolveProxy, PROXY_PARAMS_VERSION } from \"./proxyTranscoder.js\";\n\n/**\n * Transparent-media-proxy wiring shared by `routes/preview.ts`\n * (docs/plans/2026-07-14-002-feat-transparent-media-proxies-plan.md, unit U3).\n * Split out of the route module to keep it under the repo's 600-line file cap.\n */\n\n/**\n * Preview-route-local adapter surface for the auto-proxy feature. Both\n * fields are optional so any existing `StudioApiAdapter` value remains\n * structurally assignable without editing the shared interface:\n * `autoProxy` defaults to true (on) when omitted — a later unit wires the\n * CLI `--no-proxy` flag / `hyperframes.json` setting through it;\n * `mediaCodecProbeCache` lets a host share one probe cache across\n * preview/play/static-server surfaces instead of each constructing its own.\n */\nexport type PreviewApiAdapter = StudioApiAdapter & {\n autoProxy?: boolean;\n mediaCodecProbeCache?: MediaCodecProbeCache;\n};\n\nexport function isAutoProxyEnabled(adapter: PreviewApiAdapter): boolean {\n return adapter.autoProxy !== false;\n}\n\n/** One probe cache per server instance — construct once in `registerPreviewRoutes`\n * and reuse across every request so the mtime-cache benefit in\n * `scanProjectMediaCodecMap` actually applies. A host that wants to share the\n * cache across other surfaces (play, static project server) can pass its own\n * via `adapter.mediaCodecProbeCache`. */\nexport function resolvePreviewMediaCodecProbeCache(\n adapter: PreviewApiAdapter,\n): MediaCodecProbeCache {\n return adapter.mediaCodecProbeCache ?? createMediaCodecProbeCache();\n}\n\n/**\n * ETag salt for `?hf-proxy=` asset requests, mirroring `variablesEtagSalt` in\n * preview.ts: salted by the raw param value plus the transcoder's params\n * version, so a future proxy-recipe change (which bumps `PROXY_PARAMS_VERSION`)\n * or a different proxy variant invalidates cached 304s without needing to\n * touch the proxy file itself.\n */\nexport function proxyEtagSalt(raw: string | undefined): string {\n if (raw === undefined) return \"\";\n return `:proxy:${raw}:${PROXY_PARAMS_VERSION}`;\n}\n\n// Mirrors `injectScriptTagIntoHead` in routes/preview.ts (kept local rather\n// than imported to avoid a helpers → routes dependency edge for one\n// two-line utility).\nfunction injectScriptTagIntoHead(html: string, scriptTag: string): string {\n if (html.includes(\"</head>\")) return html.replace(\"</head>\", `${scriptTag}\\n</head>`);\n return `${scriptTag}\\n${html}`;\n}\n\n/**\n * Injects `window.__HF_MEDIA_CODEC_MAP__` (the U1 codec-facts scan) into\n * served composition HTML, and fire-and-forget pre-warms `resolveProxy` for\n * every browser-hostile entry so an element's proactive swap usually hits a\n * warm cache (KTD: protects the per-origin connection budget under held\n * responses). No second concurrency limiter here — the transcoder's own\n * global bound throttles both pre-warm and element-triggered calls.\n * Pre-warm failures are swallowed; an actual `?hf-proxy=` request surfaces\n * them as a 502. Alpha-bearing entries pre-warm their VP9/WebM variant.\n *\n * The single shared implementation for every auto-proxy surface — the studio\n * preview route (via `injectMediaCodecMap` below) and the CLI's composition /\n * static project servers (via the `./media-proxy-preview` subpath export).\n * Empty maps leave HTML untouched, preserving the normal no-hostile-media\n * preview path. On-demand proxy requests enforce the same eligibility gate.\n */\nexport async function injectMediaCodecMapIntoHtml(\n html: string,\n projectDir: string,\n htmlSources: HtmlSourceLike[],\n probeCache?: MediaCodecProbeCache,\n): Promise<string> {\n let map: MediaCodecMap;\n try {\n map = await scanProjectMediaCodecMap(\n projectDir,\n htmlSources,\n probeCache ? { cache: probeCache } : {},\n );\n } catch {\n // Best-effort: a scan failure must never block serving the page.\n return html;\n }\n if (Object.keys(map).length === 0) return html;\n for (const [rootRelativePathname, facts] of Object.entries(map)) {\n if (!facts.browserHostile) continue;\n resolveProxy(\n projectDir,\n resolve(projectDir, rootRelativePathname.replace(/^\\/+/, \"\")),\n proxyVariantFor(facts),\n ).catch(() => {\n // Swallowed: the pre-warm is best-effort. A real `?hf-proxy=` request\n // for this asset re-attempts the transcode and reports failure (502).\n });\n }\n // <-escape prevents a src path containing \"</script>\" from breaking out of\n // the injected tag, mirroring injectPreviewVariables in routes/preview.ts.\n const json = JSON.stringify(map)\n .replace(/</g, \"\\\\u003c\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n const tag = `<script data-hf-media-codec-map>window.__HF_MEDIA_CODEC_MAP__=${json};</script>`;\n return injectScriptTagIntoHead(html, tag);\n}\n\n/**\n * Adapter-aware wrapper used by the studio preview routes: skipped entirely\n * (no scan, no injection) when auto-proxy is off for this adapter.\n */\nexport async function injectMediaCodecMap(\n html: string,\n adapter: PreviewApiAdapter,\n projectDir: string,\n compSrcPath: string,\n probeCache: MediaCodecProbeCache,\n): Promise<string> {\n if (!isAutoProxyEnabled(adapter)) return html;\n return injectMediaCodecMapIntoHtml(html, projectDir, [{ html, compSrcPath }], probeCache);\n}\n"],"mappings":";;;;;;;;;;;AAAA,SAAS,eAAe;AAgCjB,SAAS,mBAAmB,SAAqC;AACtE,SAAO,QAAQ,cAAc;AAC/B;AAOO,SAAS,mCACd,SACsB;AACtB,SAAO,QAAQ,wBAAwB,2BAA2B;AACpE;AASO,SAAS,cAAc,KAAiC;AAC7D,MAAI,QAAQ,OAAW,QAAO;AAC9B,SAAO,UAAU,GAAG,IAAI,oBAAoB;AAC9C;AAKA,SAAS,wBAAwB,MAAc,WAA2B;AACxE,MAAI,KAAK,SAAS,SAAS,EAAG,QAAO,KAAK,QAAQ,WAAW,GAAG,SAAS;AAAA,QAAW;AACpF,SAAO,GAAG,SAAS;AAAA,EAAK,IAAI;AAC9B;AAkBA,eAAsB,4BACpB,MACA,YACA,aACA,YACiB;AACjB,MAAI;AACJ,MAAI;AACF,UAAM,MAAM;AAAA,MACV;AAAA,MACA;AAAA,MACA,aAAa,EAAE,OAAO,WAAW,IAAI,CAAC;AAAA,IACxC;AAAA,EACF,QAAQ;AAEN,WAAO;AAAA,EACT;AACA,MAAI,OAAO,KAAK,GAAG,EAAE,WAAW,EAAG,QAAO;AAC1C,aAAW,CAAC,sBAAsB,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC/D,QAAI,CAAC,MAAM,eAAgB;AAC3B;AAAA,MACE;AAAA,MACA,QAAQ,YAAY,qBAAqB,QAAQ,QAAQ,EAAE,CAAC;AAAA,MAC5D,gBAAgB,KAAK;AAAA,IACvB,EAAE,MAAM,MAAM;AAAA,IAGd,CAAC;AAAA,EACH;AAGA,QAAM,OAAO,KAAK,UAAU,GAAG,EAC5B,QAAQ,MAAM,SAAS,EACvB,QAAQ,WAAW,SAAS,EAC5B,QAAQ,WAAW,SAAS;AAC/B,QAAM,MAAM,iEAAiE,IAAI;AACjF,SAAO,wBAAwB,MAAM,GAAG;AAC1C;AAMA,eAAsB,oBACpB,MACA,SACA,YACA,aACA,YACiB;AACjB,MAAI,CAAC,mBAAmB,OAAO,EAAG,QAAO;AACzC,SAAO,4BAA4B,MAAM,YAAY,CAAC,EAAE,MAAM,YAAY,CAAC,GAAG,UAAU;AAC1F;","names":[]}
|
|
@@ -1,27 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
if (typeof value === "number") {
|
|
7
|
-
return Number.isFinite(value) ? [] : [{ path, reason: "non-finite-number" }];
|
|
8
|
-
}
|
|
9
|
-
if (!value || typeof value !== "object") return [];
|
|
10
|
-
if (Array.isArray(value)) {
|
|
11
|
-
return value.flatMap(
|
|
12
|
-
(item, index) => findUnsafeMutationValues(item, `${path}[${index}]`, options)
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
return Object.entries(value).flatMap(
|
|
16
|
-
([key, item]) => findUnsafeMutationValues(item, `${path}.${key}`, options)
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
var DOM_PATCH_NULL_VALUE_PATH = /^body\.operations\[\d+\]\.value$/;
|
|
20
|
-
function findUnsafeDomPatchValues(value) {
|
|
21
|
-
return findUnsafeMutationValues(value, "body", {
|
|
22
|
-
allowNullPath: (path) => DOM_PATCH_NULL_VALUE_PATH.test(path)
|
|
23
|
-
});
|
|
24
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
findUnsafeDomPatchValues,
|
|
3
|
+
findUnsafeMutationValues
|
|
4
|
+
} from "../chunk-X62ASOGO.js";
|
|
25
5
|
export {
|
|
26
6
|
findUnsafeDomPatchValues,
|
|
27
7
|
findUnsafeMutationValues
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|