@hyperframes/studio-server 0.8.200 → 0.8.202
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-ZCCJNXS7.js → chunk-2MJKVMZU.js} +14 -12
- package/dist/{chunk-XMNHDWQI.js → chunk-C2RFW2FX.js} +45 -15
- package/dist/{chunk-VKQTZKGA.js → chunk-OSZE5WT3.js} +56 -4
- package/dist/{chunk-URMIRXBS.js → chunk-RX6VALW3.js} +282 -73
- package/dist/helpers/mediaCodecMap.d.ts +19 -9
- package/dist/helpers/mediaCodecMap.js +3 -1
- package/dist/helpers/mediaProxyPreview.js +3 -3
- package/dist/helpers/proxyTranscoder.d.ts +1 -1
- package/dist/helpers/proxyTranscoder.js +2 -2
- package/dist/history/index.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +45 -13
- package/package.json +3 -3
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROXY_PARAMS_VERSION,
|
|
3
3
|
resolveProxy
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-RX6VALW3.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-C2RFW2FX.js";
|
|
12
12
|
|
|
13
13
|
// src/helpers/mediaProxyPreview.ts
|
|
14
14
|
import { resolve } from "path";
|
|
@@ -25,17 +25,15 @@ function proxyEtagSalt(raw, box) {
|
|
|
25
25
|
return `:proxy:${raw}:${PROXY_PARAMS_VERSION}${box ? `:${formatPreviewProxyBox(box)}` : ""}`;
|
|
26
26
|
}
|
|
27
27
|
async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeCache, prewarm = true, signal, box) {
|
|
28
|
-
let
|
|
28
|
+
let scan;
|
|
29
29
|
try {
|
|
30
|
-
|
|
31
|
-
projectDir,
|
|
32
|
-
htmlSources,
|
|
33
|
-
probeCache ? { cache: probeCache } : {}
|
|
34
|
-
);
|
|
30
|
+
scan = await scanProjectMedia(projectDir, htmlSources, probeCache ? { cache: probeCache } : {});
|
|
35
31
|
} catch {
|
|
36
32
|
return html;
|
|
37
33
|
}
|
|
38
|
-
|
|
34
|
+
const { codecMap: map, sourceFacts } = scan;
|
|
35
|
+
const hasMap = Object.keys(map).length > 0;
|
|
36
|
+
if (!hasMap && Object.keys(sourceFacts).length === 0) return html;
|
|
39
37
|
for (const [rootRelativePathname, facts] of Object.entries(map)) {
|
|
40
38
|
if (!prewarm || !shouldPrewarmProxy(facts)) continue;
|
|
41
39
|
recordProxyPrewarm();
|
|
@@ -48,8 +46,12 @@ async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeC
|
|
|
48
46
|
).catch(() => {
|
|
49
47
|
});
|
|
50
48
|
}
|
|
51
|
-
const
|
|
52
|
-
const
|
|
49
|
+
const safeJson = (value) => JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
50
|
+
const assignments = [
|
|
51
|
+
...hasMap ? [`window.__HF_MEDIA_CODEC_MAP__=${safeJson(map)};`] : [],
|
|
52
|
+
`window.__HF_MEDIA_SOURCE_FACTS__=${safeJson(sourceFacts)};`
|
|
53
|
+
];
|
|
54
|
+
const tag = `<script data-hf-media-codec-map>${assignments.join("")}</script>`;
|
|
53
55
|
return insertBeforeCloseTag(html, "head", `${tag}
|
|
54
56
|
`) ?? `${tag}
|
|
55
57
|
${html}`;
|
|
@@ -13,6 +13,9 @@ import {
|
|
|
13
13
|
isUnresolvedAssetPlaceholder,
|
|
14
14
|
resolveExistingLocalAsset as resolveAsset
|
|
15
15
|
} from "@hyperframes/parsers/asset-resolution";
|
|
16
|
+
import {
|
|
17
|
+
isHeavyForPreview
|
|
18
|
+
} from "@hyperframes/core";
|
|
16
19
|
|
|
17
20
|
// src/helpers/mediaMetadata.ts
|
|
18
21
|
import { execFile } from "child_process";
|
|
@@ -106,6 +109,22 @@ async function probeFirstFrameColour(filePath, runner = execFileRunner) {
|
|
|
106
109
|
});
|
|
107
110
|
return result.status === 0 ? parseFirstFrameColour(String(result.stderr)) : {};
|
|
108
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
|
+
}
|
|
109
128
|
async function probeMediaMetadata(filePath, runner = execFileRunner) {
|
|
110
129
|
const kind = inferKindFromPath(filePath);
|
|
111
130
|
if (kind === "audio" || kind === "unknown") {
|
|
@@ -121,7 +140,7 @@ async function probeMediaMetadata(filePath, runner = execFileRunner) {
|
|
|
121
140
|
"-v",
|
|
122
141
|
"error",
|
|
123
142
|
"-show_entries",
|
|
124
|
-
"stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample:stream_disposition=attached_pic:stream_tags=alpha_mode:format=duration",
|
|
143
|
+
"stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample,width,height,bit_rate:stream_disposition=attached_pic:stream_tags=alpha_mode:format=duration,bit_rate",
|
|
125
144
|
"-of",
|
|
126
145
|
"json",
|
|
127
146
|
"--",
|
|
@@ -147,6 +166,7 @@ async function probeMediaMetadata(filePath, runner = execFileRunner) {
|
|
|
147
166
|
metadata.hasAlpha = stream !== void 0 && videoStreamHasAlpha(stream);
|
|
148
167
|
const duration = Number(parsed.format?.duration);
|
|
149
168
|
if (Number.isFinite(duration) && duration > 0) metadata.durationSeconds = duration;
|
|
169
|
+
metadata.source = sourceFactsOf(stream, parsed.format?.bit_rate);
|
|
150
170
|
}
|
|
151
171
|
return metadata;
|
|
152
172
|
} catch {
|
|
@@ -219,18 +239,22 @@ function resolveProxyVariantRequest(request, facts) {
|
|
|
219
239
|
const expected = proxyVariantFor(facts);
|
|
220
240
|
return request === "auto" || request === expected ? expected : null;
|
|
221
241
|
}
|
|
222
|
-
function decideMediaProxyEligibility(facts) {
|
|
242
|
+
function decideMediaProxyEligibility(facts, lightBox) {
|
|
223
243
|
if (!facts) return { eligible: false, reason: "unknown_codec" };
|
|
224
|
-
if (
|
|
225
|
-
|
|
244
|
+
if (facts.browserHostile) return { eligible: true };
|
|
245
|
+
if (lightBox && facts.source && isHeavyForPreview(facts.source, lightBox)) {
|
|
246
|
+
return { eligible: true };
|
|
247
|
+
}
|
|
248
|
+
return { eligible: false, reason: "browser_safe_codec" };
|
|
226
249
|
}
|
|
227
|
-
function codecFactsFor(codecName, hasAlpha) {
|
|
250
|
+
function codecFactsFor(codecName, hasAlpha, source) {
|
|
228
251
|
const hostile = hostileCodecEntry(codecName);
|
|
229
252
|
return {
|
|
230
253
|
codecName,
|
|
231
254
|
browserHostile: hostile !== void 0,
|
|
232
255
|
representativeMime: hostile?.representativeMime ?? null,
|
|
233
|
-
hasAlpha
|
|
256
|
+
hasAlpha,
|
|
257
|
+
...source && { source }
|
|
234
258
|
};
|
|
235
259
|
}
|
|
236
260
|
async function probeCodecFacts(filePath, runner) {
|
|
@@ -239,7 +263,7 @@ async function probeCodecFacts(filePath, runner) {
|
|
|
239
263
|
if (metadata.kind !== "video") return null;
|
|
240
264
|
const codecName = metadata.color.codecName;
|
|
241
265
|
if (!codecName) return null;
|
|
242
|
-
return codecFactsFor(codecName, metadata.hasAlpha === true);
|
|
266
|
+
return codecFactsFor(codecName, metadata.hasAlpha === true, metadata.source);
|
|
243
267
|
}
|
|
244
268
|
function createMediaCodecProbeCache() {
|
|
245
269
|
return /* @__PURE__ */ new Map();
|
|
@@ -301,9 +325,8 @@ function collectLocalVideoAssets(projectDir, htmlSources) {
|
|
|
301
325
|
return candidates;
|
|
302
326
|
}
|
|
303
327
|
var PROBE_CONCURRENCY = 8;
|
|
304
|
-
async function
|
|
328
|
+
async function probeProjectAssets(projectDir, htmlSources, options) {
|
|
305
329
|
const candidates = collectLocalVideoAssets(projectDir, htmlSources);
|
|
306
|
-
if (candidates.size === 0) return {};
|
|
307
330
|
const cache = options.cache ?? defaultProbeCache;
|
|
308
331
|
const entries = [...candidates.entries()];
|
|
309
332
|
const facts = new Array(entries.length).fill(null);
|
|
@@ -319,12 +342,18 @@ async function scanProjectMediaCodecMap(projectDir, htmlSources, options = {}) {
|
|
|
319
342
|
}
|
|
320
343
|
})
|
|
321
344
|
);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
345
|
+
return entries.map(([, pathname], index) => [pathname, facts[index] ?? null]);
|
|
346
|
+
}
|
|
347
|
+
async function scanProjectMedia(projectDir, htmlSources, options = {}) {
|
|
348
|
+
const scan = { codecMap: {}, sourceFacts: {} };
|
|
349
|
+
for (const [pathname, facts] of await probeProjectAssets(projectDir, htmlSources, options)) {
|
|
350
|
+
if (facts?.browserHostile) scan.codecMap[pathname] = facts;
|
|
351
|
+
if (facts?.source) scan.sourceFacts[pathname] = facts.source;
|
|
352
|
+
}
|
|
353
|
+
return scan;
|
|
354
|
+
}
|
|
355
|
+
async function scanProjectMediaCodecMap(projectDir, htmlSources, options = {}) {
|
|
356
|
+
return (await scanProjectMedia(projectDir, htmlSources, options)).codecMap;
|
|
328
357
|
}
|
|
329
358
|
|
|
330
359
|
export {
|
|
@@ -343,5 +372,6 @@ export {
|
|
|
343
372
|
decideMediaProxyEligibility,
|
|
344
373
|
createMediaCodecProbeCache,
|
|
345
374
|
probeAssetCodec,
|
|
375
|
+
scanProjectMedia,
|
|
346
376
|
scanProjectMediaCodecMap
|
|
347
377
|
};
|
|
@@ -106,6 +106,7 @@ import { isAbsolute, relative, resolve, sep } from "path";
|
|
|
106
106
|
import { replaceFileAtomically } from "@hyperframes/core/atomic-file";
|
|
107
107
|
|
|
108
108
|
// src/history/ownerLock.ts
|
|
109
|
+
import { spawnSync } from "child_process";
|
|
109
110
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
110
111
|
import {
|
|
111
112
|
linkSync,
|
|
@@ -116,6 +117,7 @@ import {
|
|
|
116
117
|
statSync,
|
|
117
118
|
writeFileSync
|
|
118
119
|
} from "fs";
|
|
120
|
+
import { uptime } from "os";
|
|
119
121
|
import { join } from "path";
|
|
120
122
|
var HistoryBusyError = class extends Error {
|
|
121
123
|
constructor(pid, url) {
|
|
@@ -130,6 +132,7 @@ var HistoryBusyError = class extends Error {
|
|
|
130
132
|
var WINDOWS_BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
131
133
|
var NO_HARD_LINKS = /* @__PURE__ */ new Set(["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EPERM"]);
|
|
132
134
|
var TAKING_MS = 2e3;
|
|
135
|
+
var BOOT_SLACK_MS = 6e4;
|
|
133
136
|
function busyOnWindows(error) {
|
|
134
137
|
return process.platform === "win32" && WINDOWS_BUSY.has(error.code ?? "");
|
|
135
138
|
}
|
|
@@ -142,6 +145,48 @@ function alive(pid) {
|
|
|
142
145
|
return error.code === "EPERM";
|
|
143
146
|
}
|
|
144
147
|
}
|
|
148
|
+
function processStart(pid) {
|
|
149
|
+
try {
|
|
150
|
+
if (process.platform === "linux") {
|
|
151
|
+
const stat4 = readFileSync(`/proc/${pid}/stat`, "utf-8");
|
|
152
|
+
return stat4.slice(stat4.lastIndexOf(")") + 2).split(" ")[19] ?? null;
|
|
153
|
+
}
|
|
154
|
+
if (process.platform === "win32") return null;
|
|
155
|
+
const ps = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
156
|
+
encoding: "utf-8",
|
|
157
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
158
|
+
});
|
|
159
|
+
return ps.status === 0 ? ps.stdout.trim() || null : null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function recordedStart(file, pid) {
|
|
165
|
+
try {
|
|
166
|
+
const [owner, ...start] = readFileSync(`${file}.start`, "utf-8").split("\n");
|
|
167
|
+
return Number(owner) === pid && start.length > 0 ? start.join("\n") : null;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function recordStart(file) {
|
|
173
|
+
const start = processStart(process.pid);
|
|
174
|
+
if (start) writeFileSync(`${file}.start`, `${process.pid}
|
|
175
|
+
${start}`);
|
|
176
|
+
}
|
|
177
|
+
function holdsLock(file, pid) {
|
|
178
|
+
if (!alive(pid)) return false;
|
|
179
|
+
const recorded = recordedStart(file, pid);
|
|
180
|
+
if (recorded !== null) {
|
|
181
|
+
const now = processStart(pid);
|
|
182
|
+
return now === null || now === recorded;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return statSync(file).mtimeMs > Date.now() - uptime() * 1e3 - BOOT_SLACK_MS;
|
|
186
|
+
} catch {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
145
190
|
function ownerOf(file) {
|
|
146
191
|
try {
|
|
147
192
|
const text = readFileSync(file, "utf-8");
|
|
@@ -160,13 +205,14 @@ function beingTaken(file) {
|
|
|
160
205
|
}
|
|
161
206
|
}
|
|
162
207
|
function deadHolder(file, pid) {
|
|
163
|
-
return !
|
|
208
|
+
return !holdsLock(file, pid) && !beingTaken(file);
|
|
164
209
|
}
|
|
165
210
|
function claim(file) {
|
|
166
211
|
const draft = `${file}-${process.pid}-${randomUUID2()}.tmp`;
|
|
167
212
|
writeFileSync(draft, String(process.pid));
|
|
168
213
|
try {
|
|
169
214
|
linkSync(draft, file);
|
|
215
|
+
recordStart(file);
|
|
170
216
|
return true;
|
|
171
217
|
} catch (error) {
|
|
172
218
|
const code = error.code ?? "";
|
|
@@ -180,6 +226,7 @@ function claim(file) {
|
|
|
180
226
|
function createExclusively(file) {
|
|
181
227
|
try {
|
|
182
228
|
writeFileSync(file, String(process.pid), { flag: "wx" });
|
|
229
|
+
recordStart(file);
|
|
183
230
|
return true;
|
|
184
231
|
} catch (error) {
|
|
185
232
|
if (error.code !== "EEXIST") throw error;
|
|
@@ -187,7 +234,9 @@ function createExclusively(file) {
|
|
|
187
234
|
}
|
|
188
235
|
}
|
|
189
236
|
function releaseOwn(file) {
|
|
190
|
-
if (ownerOf(file)
|
|
237
|
+
if (ownerOf(file) !== process.pid) return;
|
|
238
|
+
rmSync(file, { force: true });
|
|
239
|
+
rmSync(`${file}.start`, { force: true });
|
|
191
240
|
}
|
|
192
241
|
function evictDeadOwner(file) {
|
|
193
242
|
const evictor = `${file}.evict`;
|
|
@@ -198,7 +247,10 @@ function evictDeadOwner(file) {
|
|
|
198
247
|
}
|
|
199
248
|
try {
|
|
200
249
|
const pid = ownerOf(file);
|
|
201
|
-
if (pid !== null && deadHolder(file, pid))
|
|
250
|
+
if (pid !== null && deadHolder(file, pid)) {
|
|
251
|
+
rmSync(file, { force: true });
|
|
252
|
+
rmSync(`${file}.start`, { force: true });
|
|
253
|
+
}
|
|
202
254
|
return true;
|
|
203
255
|
} finally {
|
|
204
256
|
releaseOwn(evictor);
|
|
@@ -213,7 +265,7 @@ function advertise(urlFile, url) {
|
|
|
213
265
|
function servedBy(file, urlFile, pid) {
|
|
214
266
|
try {
|
|
215
267
|
const said = JSON.parse(readFileSync(urlFile, "utf-8"));
|
|
216
|
-
const live = said.pid === pid &&
|
|
268
|
+
const live = said.pid === pid && holdsLock(file, pid) && ownerOf(file) === pid;
|
|
217
269
|
return live && typeof said.url === "string" ? said.url : void 0;
|
|
218
270
|
} catch {
|
|
219
271
|
return void 0;
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
PROXY_VARIANT_CONFIG,
|
|
3
3
|
probeFirstFrameColour,
|
|
4
4
|
probeMediaMetadata
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-C2RFW2FX.js";
|
|
6
6
|
import {
|
|
7
7
|
mkdirWithinProject,
|
|
8
8
|
realProjectRoot,
|
|
@@ -11,23 +11,19 @@ import {
|
|
|
11
11
|
|
|
12
12
|
// src/helpers/proxyTranscoder.ts
|
|
13
13
|
import { spawn } from "child_process";
|
|
14
|
-
import {
|
|
14
|
+
import { randomUUID } from "crypto";
|
|
15
15
|
import {
|
|
16
|
-
existsSync as
|
|
16
|
+
existsSync as existsSync4,
|
|
17
17
|
mkdtempSync,
|
|
18
18
|
renameSync,
|
|
19
|
-
rmSync,
|
|
20
|
-
statSync as statSync2,
|
|
19
|
+
rmSync as rmSync2,
|
|
21
20
|
unlinkSync as unlinkSync2,
|
|
22
21
|
utimesSync,
|
|
23
|
-
writeFileSync
|
|
22
|
+
writeFileSync as writeFileSync2
|
|
24
23
|
} from "fs";
|
|
25
|
-
import { availableParallelism, tmpdir } from "os";
|
|
26
|
-
import { basename, dirname, extname as
|
|
27
|
-
import {
|
|
28
|
-
formatPreviewProxyBox,
|
|
29
|
-
hdrToSdrToneMapFilter
|
|
30
|
-
} from "@hyperframes/core";
|
|
24
|
+
import { availableParallelism, tmpdir, totalmem } from "os";
|
|
25
|
+
import { basename as basename3, dirname as dirname3, extname as extname3, isAbsolute, join as join4, relative, resolve, sep } from "path";
|
|
26
|
+
import { hdrToSdrToneMapFilter } from "@hyperframes/core";
|
|
31
27
|
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
32
28
|
import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
|
|
33
29
|
|
|
@@ -143,8 +139,138 @@ function cleanupProxyCache(cacheDir, options = {}) {
|
|
|
143
139
|
};
|
|
144
140
|
}
|
|
145
141
|
|
|
142
|
+
// src/helpers/proxyCacheKey.ts
|
|
143
|
+
import { createHash } from "crypto";
|
|
144
|
+
import {
|
|
145
|
+
closeSync,
|
|
146
|
+
createReadStream,
|
|
147
|
+
existsSync as existsSync2,
|
|
148
|
+
fstatSync,
|
|
149
|
+
openSync,
|
|
150
|
+
readFileSync,
|
|
151
|
+
statSync as statSync2,
|
|
152
|
+
writeFileSync
|
|
153
|
+
} from "fs";
|
|
154
|
+
import { basename, dirname, extname as extname2, join as join2 } from "path";
|
|
155
|
+
import { formatPreviewProxyBox } from "@hyperframes/core";
|
|
156
|
+
var SMALL_FILE_BYTES = 16 * 1024 * 1024;
|
|
157
|
+
var COPY_NAME = /^[0-9a-f]{64}\.(?:mp4|webm)$/;
|
|
158
|
+
function digest(...parts) {
|
|
159
|
+
return createHash("sha256").update(parts.join("\0")).digest("hex");
|
|
160
|
+
}
|
|
161
|
+
function boxPart(box) {
|
|
162
|
+
return box ? formatPreviewProxyBox(box) : "";
|
|
163
|
+
}
|
|
164
|
+
function fastCopyKey(sourcePath, version, variant, box) {
|
|
165
|
+
const stat = statSync2(sourcePath);
|
|
166
|
+
return digest(
|
|
167
|
+
sourcePath,
|
|
168
|
+
String(stat.mtimeMs),
|
|
169
|
+
String(stat.size),
|
|
170
|
+
version,
|
|
171
|
+
variant,
|
|
172
|
+
boxPart(box)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
async function contentCopyKey(sourcePath, version, variant, box, signal) {
|
|
176
|
+
const hash = createHash("sha256");
|
|
177
|
+
const fd = openSync(sourcePath, "r");
|
|
178
|
+
try {
|
|
179
|
+
if (fstatSync(fd).size <= SMALL_FILE_BYTES) hash.update(readFileSync(fd));
|
|
180
|
+
else
|
|
181
|
+
for await (const chunk of createReadStream(sourcePath, { fd, autoClose: false, signal }))
|
|
182
|
+
hash.update(chunk);
|
|
183
|
+
} finally {
|
|
184
|
+
closeSync(fd);
|
|
185
|
+
}
|
|
186
|
+
return digest(hash.digest("hex"), version, variant, boxPart(box));
|
|
187
|
+
}
|
|
188
|
+
function fastIndexPath(cachePath) {
|
|
189
|
+
return join2(dirname(cachePath), `.fast-${basename(cachePath, extname2(cachePath))}`);
|
|
190
|
+
}
|
|
191
|
+
function contentIndexPath(cacheDir, contentKey) {
|
|
192
|
+
return join2(cacheDir, `.content-${contentKey}`);
|
|
193
|
+
}
|
|
194
|
+
function readCopyIndex(indexPath) {
|
|
195
|
+
try {
|
|
196
|
+
const name = readFileSync(indexPath, "utf8").trim();
|
|
197
|
+
const target = join2(dirname(indexPath), name);
|
|
198
|
+
return COPY_NAME.test(name) && existsSync2(target) ? target : null;
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function writeCopyIndex(indexPath, copyPath) {
|
|
204
|
+
writeFileSync(indexPath, basename(copyPath));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/helpers/proxyLock.ts
|
|
208
|
+
import {
|
|
209
|
+
closeSync as closeSync2,
|
|
210
|
+
existsSync as existsSync3,
|
|
211
|
+
openSync as openSync2,
|
|
212
|
+
readFileSync as readFileSync2,
|
|
213
|
+
rmSync,
|
|
214
|
+
statSync as statSync3,
|
|
215
|
+
writeSync
|
|
216
|
+
} from "fs";
|
|
217
|
+
import { basename as basename2, dirname as dirname2, join as join3 } from "path";
|
|
218
|
+
var POLL_MS = 500;
|
|
219
|
+
var UNREADABLE_LOCK_MS = 1e4;
|
|
220
|
+
function lockPathFor(cachePath) {
|
|
221
|
+
return join3(dirname2(cachePath), `.lock-${basename2(cachePath)}`);
|
|
222
|
+
}
|
|
223
|
+
function ownerIsAlive(lockPath) {
|
|
224
|
+
try {
|
|
225
|
+
const pid = Number(readFileSync2(lockPath, "utf8"));
|
|
226
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
227
|
+
return Date.now() - statSync3(lockPath).mtimeMs < UNREADABLE_LOCK_MS;
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
process.kill(pid, 0);
|
|
231
|
+
return true;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return err.code === "EPERM";
|
|
234
|
+
}
|
|
235
|
+
} catch {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function pause(ms, signal) {
|
|
240
|
+
return new Promise((resolve2, reject) => {
|
|
241
|
+
const timer = setTimeout(() => {
|
|
242
|
+
signal.removeEventListener("abort", onAbort);
|
|
243
|
+
resolve2();
|
|
244
|
+
}, ms);
|
|
245
|
+
const onAbort = () => {
|
|
246
|
+
clearTimeout(timer);
|
|
247
|
+
reject(signal.reason);
|
|
248
|
+
};
|
|
249
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
async function acquireCopyLock(cachePath, signal) {
|
|
253
|
+
const lockPath = lockPathFor(cachePath);
|
|
254
|
+
for (; ; ) {
|
|
255
|
+
signal.throwIfAborted();
|
|
256
|
+
if (existsSync3(cachePath)) return null;
|
|
257
|
+
try {
|
|
258
|
+
const fd = openSync2(lockPath, "wx");
|
|
259
|
+
writeSync(fd, String(process.pid));
|
|
260
|
+
closeSync2(fd);
|
|
261
|
+
if (!existsSync3(cachePath)) return () => rmSync(lockPath, { force: true });
|
|
262
|
+
rmSync(lockPath, { force: true });
|
|
263
|
+
return null;
|
|
264
|
+
} catch (err) {
|
|
265
|
+
if (err.code !== "EEXIST") throw err;
|
|
266
|
+
}
|
|
267
|
+
if (ownerIsAlive(lockPath)) await pause(POLL_MS, signal);
|
|
268
|
+
else rmSync(lockPath, { force: true });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
146
272
|
// src/helpers/proxyTranscoder.ts
|
|
147
|
-
var PROXY_PARAMS_VERSION = "
|
|
273
|
+
var PROXY_PARAMS_VERSION = "v7";
|
|
148
274
|
var CACHE_DIR_NAME = ".transcode-cache";
|
|
149
275
|
function boundedEnvInteger(name, fallback, min, max) {
|
|
150
276
|
const raw = process.env[name]?.trim();
|
|
@@ -235,33 +361,16 @@ function canonicalizeProxySource(projectDir, absoluteSourcePath) {
|
|
|
235
361
|
if (requestedRelativePath === ".." || requestedRelativePath.startsWith(`..${sep}`) || isAbsolute(requestedRelativePath)) {
|
|
236
362
|
throw new ProxySourceOutsideProjectError();
|
|
237
363
|
}
|
|
238
|
-
|
|
239
|
-
const canonicalSourcePath = realpath(absoluteSourcePath);
|
|
240
|
-
const canonicalRelativePath = relative(canonicalProjectDir, canonicalSourcePath);
|
|
241
|
-
const sourceIsInsideCanonicalProject = canonicalRelativePath !== ".." && !canonicalRelativePath.startsWith(`..${sep}`) && !isAbsolute(canonicalRelativePath);
|
|
242
|
-
return {
|
|
243
|
-
projectDir: canonicalProjectDir,
|
|
244
|
-
sourcePath: canonicalSourcePath,
|
|
245
|
-
relativePath: requestedRelativePath.normalize("NFC"),
|
|
246
|
-
// An external target needs a stable identity in addition to its project-local
|
|
247
|
-
// symlink path, otherwise retargeting the link can reuse an unrelated proxy.
|
|
248
|
-
cacheIdentity: (sourceIsInsideCanonicalProject ? canonicalRelativePath : canonicalSourcePath).normalize("NFC")
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
function buildProxyCacheKey(source, variant, box) {
|
|
252
|
-
const stat = statSync2(source.sourcePath);
|
|
253
|
-
const size = box ? `\0${formatPreviewProxyBox(box)}` : "";
|
|
254
|
-
return createHash("sha256").update(
|
|
255
|
-
`${source.relativePath}\0${source.cacheIdentity}\0${stat.mtimeMs}\0${stat.size}\0${PROXY_PARAMS_VERSION}\0${variant}${size}`
|
|
256
|
-
).digest("hex");
|
|
364
|
+
return { projectDir: realProjectRoot(projectDir), sourcePath: realpath(absoluteSourcePath) };
|
|
257
365
|
}
|
|
258
366
|
function getCanonicalProxyCachePath(source, variant, box) {
|
|
259
|
-
const key =
|
|
260
|
-
|
|
367
|
+
const key = fastCopyKey(source.sourcePath, PROXY_PARAMS_VERSION, variant, box);
|
|
368
|
+
const made = join4(
|
|
261
369
|
source.projectDir,
|
|
262
370
|
CACHE_DIR_NAME,
|
|
263
371
|
`${key}${PROXY_VARIANT_CONFIG[variant].extension}`
|
|
264
372
|
);
|
|
373
|
+
return existsSync4(made) ? made : readCopyIndex(fastIndexPath(made)) ?? made;
|
|
265
374
|
}
|
|
266
375
|
function getProxyCachePath(projectDir, absoluteSourcePath, variant = "h264", box) {
|
|
267
376
|
return getCanonicalProxyCachePath(
|
|
@@ -420,42 +529,106 @@ function chunkRanges(durationSeconds) {
|
|
|
420
529
|
lengthSeconds: index === count - 1 ? durationSeconds - index * PROXY_CHUNK_SECONDS : PROXY_CHUNK_SECONDS
|
|
421
530
|
}));
|
|
422
531
|
}
|
|
423
|
-
|
|
532
|
+
var UNGATED = { size: 1, acquire: async () => {
|
|
533
|
+
}, release: () => {
|
|
534
|
+
} };
|
|
535
|
+
function createPieceGate(size) {
|
|
536
|
+
let free = size;
|
|
537
|
+
const waiters = [];
|
|
538
|
+
return {
|
|
539
|
+
size,
|
|
540
|
+
acquire: (signal) => {
|
|
541
|
+
signal.throwIfAborted();
|
|
542
|
+
if (free > 0) {
|
|
543
|
+
free--;
|
|
544
|
+
return Promise.resolve();
|
|
545
|
+
}
|
|
546
|
+
return new Promise((resolve2, reject) => {
|
|
547
|
+
const grant = () => {
|
|
548
|
+
signal.removeEventListener("abort", onAbort);
|
|
549
|
+
resolve2();
|
|
550
|
+
};
|
|
551
|
+
const onAbort = () => {
|
|
552
|
+
waiters.splice(waiters.indexOf(grant), 1);
|
|
553
|
+
reject(signal.reason);
|
|
554
|
+
};
|
|
555
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
556
|
+
waiters.push(grant);
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
release: () => {
|
|
560
|
+
const next = waiters.shift();
|
|
561
|
+
if (next) next();
|
|
562
|
+
else free++;
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, signal, gate, onDone = () => {
|
|
424
567
|
}) {
|
|
425
|
-
const extension =
|
|
568
|
+
const extension = extname3(tempPath);
|
|
426
569
|
const pieces = ranges.map((_, index) => `${chunkPrefix}${index}${extension}`);
|
|
570
|
+
const pending = ranges.flatMap((_, index) => existsSync4(pieces[index]) ? [] : [index]);
|
|
427
571
|
let doneSeconds = 0;
|
|
428
572
|
for (const [index, range] of ranges.entries()) {
|
|
429
|
-
if (
|
|
573
|
+
if (!pending.includes(index)) doneSeconds += range.lengthSeconds;
|
|
430
574
|
}
|
|
431
575
|
if (doneSeconds) onDone(doneSeconds);
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
576
|
+
const inFlight2 = /* @__PURE__ */ new Map();
|
|
577
|
+
const stop = new AbortController();
|
|
578
|
+
const pieceSignal = AbortSignal.any([signal, stop.signal]);
|
|
579
|
+
let failure;
|
|
580
|
+
let cursor = 0;
|
|
581
|
+
const work = async () => {
|
|
582
|
+
for (; ; ) {
|
|
583
|
+
await gate.acquire(pieceSignal);
|
|
584
|
+
try {
|
|
585
|
+
if (cursor >= pending.length) return;
|
|
586
|
+
const index = pending[cursor++];
|
|
587
|
+
const range = ranges[index];
|
|
588
|
+
const piece = pieces[index];
|
|
589
|
+
const pieceTemp = join4(dirname3(piece), `.tmp-${randomUUID()}-${basename3(piece)}`);
|
|
590
|
+
try {
|
|
591
|
+
await encode(pieceTemp, range, pieceSignal, (encodedSeconds) => {
|
|
592
|
+
inFlight2.set(index, encodedSeconds);
|
|
593
|
+
onDone(doneSeconds + [...inFlight2.values()].reduce((sum, seconds) => sum + seconds, 0));
|
|
594
|
+
});
|
|
595
|
+
renameSync(pieceTemp, piece);
|
|
596
|
+
inFlight2.delete(index);
|
|
597
|
+
doneSeconds += range.lengthSeconds;
|
|
598
|
+
onDone(doneSeconds);
|
|
599
|
+
} finally {
|
|
600
|
+
if (existsSync4(pieceTemp)) unlinkSync2(pieceTemp);
|
|
601
|
+
}
|
|
602
|
+
} finally {
|
|
603
|
+
gate.release();
|
|
604
|
+
}
|
|
443
605
|
}
|
|
444
|
-
}
|
|
445
|
-
|
|
606
|
+
};
|
|
607
|
+
await Promise.all(
|
|
608
|
+
Array.from(
|
|
609
|
+
{ length: Math.min(gate.size, pending.length) },
|
|
610
|
+
() => work().catch((error) => {
|
|
611
|
+
failure ??= error;
|
|
612
|
+
stop.abort();
|
|
613
|
+
})
|
|
614
|
+
)
|
|
615
|
+
);
|
|
616
|
+
if (failure !== void 0) throw failure;
|
|
617
|
+
const listDir = mkdtempSync(join4(tmpdir(), "hf-proxy-join-"));
|
|
446
618
|
try {
|
|
447
|
-
const listPath =
|
|
448
|
-
|
|
619
|
+
const listPath = join4(listDir, "pieces.txt");
|
|
620
|
+
writeFileSync2(
|
|
449
621
|
listPath,
|
|
450
622
|
pieces.map((piece) => `file '${piece.replace(/\\/g, "/").replace(/'/g, "'\\''")}'
|
|
451
623
|
`).join("")
|
|
452
624
|
);
|
|
453
625
|
await concat(listPath, tempPath);
|
|
454
626
|
} finally {
|
|
455
|
-
|
|
627
|
+
rmSync2(listDir, { recursive: true, force: true });
|
|
456
628
|
}
|
|
457
|
-
for (const piece of pieces) if (
|
|
629
|
+
for (const piece of pieces) if (existsSync4(piece)) unlinkSync2(piece);
|
|
458
630
|
}
|
|
631
|
+
var KEYFRAME_EVERY_SECOND_ARGS = ["-force_key_frames", "expr:gte(t,n_forced*1)"];
|
|
459
632
|
var H264_ARGS = [
|
|
460
633
|
"-c:v",
|
|
461
634
|
"libx264",
|
|
@@ -478,7 +651,11 @@ var H264_ARGS = [
|
|
|
478
651
|
"-movflags",
|
|
479
652
|
"+faststart"
|
|
480
653
|
];
|
|
481
|
-
var
|
|
654
|
+
var CORES = availableParallelism();
|
|
655
|
+
var PIECE_MEMORY_BYTES = 3 * 1024 ** 3;
|
|
656
|
+
var ALPHA_PIECE_GATE = createPieceGate(
|
|
657
|
+
Math.max(1, Math.min(4, CORES - 1, Math.floor(totalmem() / PIECE_MEMORY_BYTES)))
|
|
658
|
+
);
|
|
482
659
|
var VP8_ARGS = [
|
|
483
660
|
"-c:v",
|
|
484
661
|
"libvpx",
|
|
@@ -498,8 +675,6 @@ var VP8_ARGS = [
|
|
|
498
675
|
"bt709",
|
|
499
676
|
"-cpu-used",
|
|
500
677
|
"4",
|
|
501
|
-
"-threads",
|
|
502
|
-
VP8_THREADS,
|
|
503
678
|
"-auto-alt-ref",
|
|
504
679
|
"0",
|
|
505
680
|
"-metadata:s:v:0",
|
|
@@ -533,7 +708,12 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
|
|
|
533
708
|
const alphaDecoder = keepsAlpha ? alphaDecoderFor(metadata.color.codecName) : null;
|
|
534
709
|
const decoderArgs = alphaDecoder ? ["-c:v", alphaDecoder] : [];
|
|
535
710
|
const totalSeconds = metadata.durationSeconds;
|
|
536
|
-
const
|
|
711
|
+
const gate = keepsAlpha ? ALPHA_PIECE_GATE : UNGATED;
|
|
712
|
+
const vp8Threads = (pieces) => [
|
|
713
|
+
"-threads",
|
|
714
|
+
String(Math.min(4, Math.floor(CORES / pieces)))
|
|
715
|
+
];
|
|
716
|
+
const encode = (target, range, pieceSignal, onEncoded) => spawnFfmpeg(
|
|
537
717
|
ffmpegPath,
|
|
538
718
|
[
|
|
539
719
|
"-y",
|
|
@@ -547,21 +727,31 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
|
|
|
547
727
|
...range ? ["-t", String(range.lengthSeconds)] : [],
|
|
548
728
|
"-vf",
|
|
549
729
|
videoFilter,
|
|
550
|
-
...
|
|
730
|
+
...KEYFRAME_EVERY_SECOND_ARGS,
|
|
731
|
+
...keepsAlpha ? [...VP8_ARGS, ...vp8Threads(range ? gate.size : 1)] : H264_ARGS,
|
|
551
732
|
target
|
|
552
733
|
],
|
|
553
734
|
TRANSCODE_STALL_MS,
|
|
554
|
-
|
|
555
|
-
|
|
735
|
+
pieceSignal,
|
|
736
|
+
onEncoded
|
|
556
737
|
);
|
|
557
738
|
const ranges = chunkRanges(totalSeconds);
|
|
558
|
-
if (!ranges)
|
|
739
|
+
if (!ranges) {
|
|
740
|
+
return encode(
|
|
741
|
+
outputPath,
|
|
742
|
+
void 0,
|
|
743
|
+
signal,
|
|
744
|
+
(encodedSeconds) => onProgress({ doneSeconds: encodedSeconds, totalSeconds })
|
|
745
|
+
);
|
|
746
|
+
}
|
|
559
747
|
await encodeInChunks(
|
|
560
748
|
ranges,
|
|
561
749
|
chunkPrefix,
|
|
562
750
|
outputPath,
|
|
563
751
|
encode,
|
|
564
752
|
(listPath, target) => spawnFfmpeg(ffmpegPath, concatArgs(listPath, target, variant), CONCAT_TIMEOUT_MS, signal),
|
|
753
|
+
signal,
|
|
754
|
+
gate,
|
|
565
755
|
(doneSeconds) => onProgress({ doneSeconds, totalSeconds })
|
|
566
756
|
);
|
|
567
757
|
}
|
|
@@ -656,14 +846,31 @@ function spawnFfmpeg(ffmpegPath, args, stallMs, signal, onProgress) {
|
|
|
656
846
|
async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath, variant, box) {
|
|
657
847
|
await acquireSlot(job);
|
|
658
848
|
try {
|
|
659
|
-
if (
|
|
660
|
-
const cacheDir =
|
|
849
|
+
if (existsSync4(cachePath)) return cachePath;
|
|
850
|
+
const cacheDir = dirname3(cachePath);
|
|
661
851
|
mkdirWithinProject(projectDir, cacheDir);
|
|
662
|
-
const
|
|
852
|
+
const contentIndex = contentIndexPath(
|
|
853
|
+
cacheDir,
|
|
854
|
+
await contentCopyKey(
|
|
855
|
+
absoluteSourcePath,
|
|
856
|
+
PROXY_PARAMS_VERSION,
|
|
857
|
+
variant,
|
|
858
|
+
box,
|
|
859
|
+
job.abandoned.signal
|
|
860
|
+
)
|
|
861
|
+
);
|
|
862
|
+
const sameContent = readCopyIndex(contentIndex);
|
|
863
|
+
if (sameContent) {
|
|
864
|
+
writeCopyIndex(fastIndexPath(cachePath), sameContent);
|
|
865
|
+
return sameContent;
|
|
866
|
+
}
|
|
867
|
+
const release = await acquireCopyLock(cachePath, job.abandoned.signal);
|
|
868
|
+
if (!release) return cachePath;
|
|
869
|
+
const tempPath = join4(cacheDir, `.tmp-${randomUUID()}-${basename3(cachePath)}`);
|
|
663
870
|
try {
|
|
664
|
-
const chunkPrefix =
|
|
871
|
+
const chunkPrefix = join4(
|
|
665
872
|
cacheDir,
|
|
666
|
-
`${CHUNK_PREFIX}${
|
|
873
|
+
`${CHUNK_PREFIX}${basename3(cachePath, extname3(cachePath))}-`
|
|
667
874
|
);
|
|
668
875
|
await runFfmpeg(
|
|
669
876
|
absoluteSourcePath,
|
|
@@ -675,10 +882,12 @@ async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath,
|
|
|
675
882
|
(progress) => reportProgress(job, progress)
|
|
676
883
|
);
|
|
677
884
|
renameSync(tempPath, cachePath);
|
|
885
|
+
writeCopyIndex(contentIndex, cachePath);
|
|
678
886
|
maintainProxyCache(cacheDir);
|
|
679
887
|
return cachePath;
|
|
680
888
|
} finally {
|
|
681
|
-
if (
|
|
889
|
+
if (existsSync4(tempPath)) unlinkSync2(tempPath);
|
|
890
|
+
release();
|
|
682
891
|
}
|
|
683
892
|
} finally {
|
|
684
893
|
releaseSlot();
|
|
@@ -686,8 +895,8 @@ async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath,
|
|
|
686
895
|
}
|
|
687
896
|
var proxyActivity = 0;
|
|
688
897
|
function proxyActivityMark(projectDir) {
|
|
689
|
-
if (!
|
|
690
|
-
const cacheDir =
|
|
898
|
+
if (!existsSync4(projectDir)) return String(proxyActivity);
|
|
899
|
+
const cacheDir = join4(realpath(projectDir), CACHE_DIR_NAME) + sep;
|
|
691
900
|
for (const cachePath of inFlight.keys()) if (cachePath.startsWith(cacheDir)) return null;
|
|
692
901
|
return String(proxyActivity);
|
|
693
902
|
}
|
|
@@ -695,9 +904,9 @@ async function resolveProxy(projectDir, absoluteSourcePath, variant = "h264", bo
|
|
|
695
904
|
options.signal?.throwIfAborted();
|
|
696
905
|
const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
|
|
697
906
|
const cachePath = getCanonicalProxyCachePath(source, variant, box);
|
|
698
|
-
if (
|
|
907
|
+
if (existsSync4(cachePath)) {
|
|
699
908
|
markCacheEntryUsed(cachePath);
|
|
700
|
-
maintainProxyCache(
|
|
909
|
+
maintainProxyCache(dirname3(cachePath));
|
|
701
910
|
return cachePath;
|
|
702
911
|
}
|
|
703
912
|
const rememberedFailure = failedTranscodes.get(cachePath);
|
|
@@ -754,7 +963,7 @@ async function resolveProxies(projectDir, sources, onSettled = () => {
|
|
|
754
963
|
const first = sources[0];
|
|
755
964
|
if (first) {
|
|
756
965
|
maintainProxyCache(
|
|
757
|
-
|
|
966
|
+
dirname3(getProxyCachePath(projectDir, first.sourcePath, first.variant)),
|
|
758
967
|
Math.min(
|
|
759
968
|
Number(process.env.HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS) || UNTOUCHED_TEMP_IS_DEAD_MS,
|
|
760
969
|
UNTOUCHED_TEMP_IS_DEAD_MS
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PreviewSourceFacts, PreviewProxyBox } from '@hyperframes/core';
|
|
2
|
+
|
|
1
3
|
interface FfprobeRunResult {
|
|
2
4
|
status: number | null;
|
|
3
5
|
stdout: string | Buffer;
|
|
@@ -31,6 +33,8 @@ interface AssetCodecFacts {
|
|
|
31
33
|
/** Source carries an alpha channel (`MediaMetadata.hasAlpha`). Alpha sources use a
|
|
32
34
|
* VP8/WebM proxy so their transparency is preserved across Chromium builds. */
|
|
33
35
|
hasAlpha: boolean;
|
|
36
|
+
/** Size, bit depth and bitrate, for deciding whether a playable file deserves a lighter copy. */
|
|
37
|
+
source?: PreviewSourceFacts;
|
|
34
38
|
}
|
|
35
39
|
/** Server-root-relative URL pathname -> that asset's codec facts. */
|
|
36
40
|
type MediaCodecMap = Record<string, AssetCodecFacts>;
|
|
@@ -96,8 +100,11 @@ type MediaProxyEligibility = {
|
|
|
96
100
|
eligible: false;
|
|
97
101
|
reason: MediaProxyIneligibilityReason;
|
|
98
102
|
};
|
|
99
|
-
/**
|
|
100
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Single policy gate shared by proactive scans and on-demand proxy routes. A file Chrome can play is
|
|
105
|
+
* eligible only for a lighter copy (`lightBox`, the box that request asks for) and only when it is heavy there.
|
|
106
|
+
*/
|
|
107
|
+
declare function decideMediaProxyEligibility(facts: AssetCodecFacts | null, lightBox?: PreviewProxyBox): MediaProxyEligibility;
|
|
101
108
|
interface CachedAssetProbe {
|
|
102
109
|
/** The file a symlinked path pointed at, so retargeting the link is a miss. */
|
|
103
110
|
target: string;
|
|
@@ -126,12 +133,15 @@ interface ScanProjectMediaCodecMapOptions {
|
|
|
126
133
|
cache?: MediaCodecProbeCache;
|
|
127
134
|
runner?: FfprobeRunner;
|
|
128
135
|
}
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
/** What a scan of a project's local videos finds: the hostile ones for the proxy map, and every readable one's size. */
|
|
137
|
+
interface ProjectMediaScan {
|
|
138
|
+
codecMap: MediaCodecMap;
|
|
139
|
+
/** Root-relative URL pathname -> size, bit depth and bitrate. */
|
|
140
|
+
sourceFacts: Record<string, PreviewSourceFacts>;
|
|
141
|
+
}
|
|
142
|
+
/** Scans composition HTML for local `<video src>` references; a video that can't be probed is omitted. */
|
|
143
|
+
declare function scanProjectMedia(projectDir: string, htmlSources: HtmlSourceLike[], options?: ScanProjectMediaCodecMapOptions): Promise<ProjectMediaScan>;
|
|
144
|
+
/** The injection map alone: root-relative URL pathname -> codec facts of the hostile videos. */
|
|
135
145
|
declare function scanProjectMediaCodecMap(projectDir: string, htmlSources: HtmlSourceLike[], options?: ScanProjectMediaCodecMapOptions): Promise<MediaCodecMap>;
|
|
136
146
|
|
|
137
|
-
export { type AssetCodecFacts, BROWSER_HOSTILE_CODECS, type BrowserHostileCodec, type HtmlSourceLike, type MediaCodecMap, type MediaCodecProbeCache, type MediaProxyEligibility, type MediaProxyIneligibilityReason, PROXY_VARIANT_CONFIG, type ProxyVariant, type ProxyVariantRequest, type ScanProjectMediaCodecMapOptions, createMediaCodecProbeCache, decideMediaProxyEligibility, isProxyVariant, isProxyVariantRequest, mediaProxyDemand, probeAssetCodec, proxyVariantFor, recordProxyPrewarm, recordProxyRequest, resolveProxyVariantRequest, scanProjectMediaCodecMap, shouldPrewarmProxy };
|
|
147
|
+
export { type AssetCodecFacts, BROWSER_HOSTILE_CODECS, type BrowserHostileCodec, type HtmlSourceLike, type MediaCodecMap, type MediaCodecProbeCache, type MediaProxyEligibility, type MediaProxyIneligibilityReason, PROXY_VARIANT_CONFIG, type ProjectMediaScan, type ProxyVariant, type ProxyVariantRequest, type ScanProjectMediaCodecMapOptions, createMediaCodecProbeCache, decideMediaProxyEligibility, isProxyVariant, isProxyVariantRequest, mediaProxyDemand, probeAssetCodec, proxyVariantFor, recordProxyPrewarm, recordProxyRequest, resolveProxyVariantRequest, scanProjectMedia, scanProjectMediaCodecMap, shouldPrewarmProxy };
|
|
@@ -11,9 +11,10 @@ import {
|
|
|
11
11
|
recordProxyPrewarm,
|
|
12
12
|
recordProxyRequest,
|
|
13
13
|
resolveProxyVariantRequest,
|
|
14
|
+
scanProjectMedia,
|
|
14
15
|
scanProjectMediaCodecMap,
|
|
15
16
|
shouldPrewarmProxy
|
|
16
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-C2RFW2FX.js";
|
|
17
18
|
import "../chunk-QF7MFJJU.js";
|
|
18
19
|
import "../chunk-SVQFE44C.js";
|
|
19
20
|
import "../chunk-L3HJVTQE.js";
|
|
@@ -30,6 +31,7 @@ export {
|
|
|
30
31
|
recordProxyPrewarm,
|
|
31
32
|
recordProxyRequest,
|
|
32
33
|
resolveProxyVariantRequest,
|
|
34
|
+
scanProjectMedia,
|
|
33
35
|
scanProjectMediaCodecMap,
|
|
34
36
|
shouldPrewarmProxy
|
|
35
37
|
};
|
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
isAutoProxyEnabled,
|
|
5
5
|
proxyEtagSalt,
|
|
6
6
|
resolvePreviewMediaCodecProbeCache
|
|
7
|
-
} from "../chunk-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
7
|
+
} from "../chunk-2MJKVMZU.js";
|
|
8
|
+
import "../chunk-RX6VALW3.js";
|
|
9
|
+
import "../chunk-C2RFW2FX.js";
|
|
10
10
|
import "../chunk-QF7MFJJU.js";
|
|
11
11
|
import "../chunk-SVQFE44C.js";
|
|
12
12
|
import "../chunk-L3HJVTQE.js";
|
|
@@ -11,7 +11,7 @@ import { ProxyVariant } from './mediaCodecMap.js';
|
|
|
11
11
|
* caller's `signal` detaches that caller; once no caller is left, the copy is
|
|
12
12
|
* dropped from the queue or its ffmpeg is killed.
|
|
13
13
|
*/
|
|
14
|
-
declare const PROXY_PARAMS_VERSION = "
|
|
14
|
+
declare const PROXY_PARAMS_VERSION = "v7";
|
|
15
15
|
declare const CACHE_DIR_NAME = ".transcode-cache";
|
|
16
16
|
/** A copy that makes no progress this long is ended: a hung encode, not a slow one. */
|
|
17
17
|
declare const TRANSCODE_STALL_MS: number;
|
|
@@ -17,8 +17,8 @@ import {
|
|
|
17
17
|
resolveProxies,
|
|
18
18
|
resolveProxy,
|
|
19
19
|
waitForProxy
|
|
20
|
-
} from "../chunk-
|
|
21
|
-
import "../chunk-
|
|
20
|
+
} from "../chunk-RX6VALW3.js";
|
|
21
|
+
import "../chunk-C2RFW2FX.js";
|
|
22
22
|
import "../chunk-QF7MFJJU.js";
|
|
23
23
|
import "../chunk-SVQFE44C.js";
|
|
24
24
|
import "../chunk-L3HJVTQE.js";
|
package/dist/history/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ interface DuplicateElementResult {
|
|
|
93
93
|
matched: boolean;
|
|
94
94
|
newId: string | null;
|
|
95
95
|
}
|
|
96
|
-
declare function duplicateElementInHtml(source: string, target: SourceMutationTarget, newId: string, at: number): DuplicateElementResult;
|
|
96
|
+
declare function duplicateElementInHtml(source: string, target: SourceMutationTarget, newId: string, at: number, fallbackDuration?: number): DuplicateElementResult;
|
|
97
97
|
|
|
98
98
|
interface FileMutationInput {
|
|
99
99
|
sourceFile: string;
|
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-OSZE5WT3.js";
|
|
66
66
|
import {
|
|
67
67
|
clearElementScreenshotIsolation,
|
|
68
68
|
getElementScreenshotClip
|
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
isAutoProxyEnabled,
|
|
83
83
|
proxyEtagSalt,
|
|
84
84
|
resolvePreviewMediaCodecProbeCache
|
|
85
|
-
} from "./chunk-
|
|
85
|
+
} from "./chunk-2MJKVMZU.js";
|
|
86
86
|
import {
|
|
87
87
|
CACHE_DIR_NAME,
|
|
88
88
|
PROXY_PENDING_RETRY_AFTER_SECONDS,
|
|
@@ -93,7 +93,7 @@ import {
|
|
|
93
93
|
proxyActivityMark,
|
|
94
94
|
resolveProxy,
|
|
95
95
|
waitForProxy
|
|
96
|
-
} from "./chunk-
|
|
96
|
+
} from "./chunk-RX6VALW3.js";
|
|
97
97
|
import {
|
|
98
98
|
PROXY_VARIANT_CONFIG,
|
|
99
99
|
decideMediaProxyEligibility,
|
|
@@ -102,7 +102,7 @@ import {
|
|
|
102
102
|
probeMediaMetadata,
|
|
103
103
|
recordProxyRequest,
|
|
104
104
|
resolveProxyVariantRequest
|
|
105
|
-
} from "./chunk-
|
|
105
|
+
} from "./chunk-C2RFW2FX.js";
|
|
106
106
|
import {
|
|
107
107
|
folderGone,
|
|
108
108
|
isInHiddenOrVendorDir,
|
|
@@ -3885,7 +3885,11 @@ import {
|
|
|
3885
3885
|
STUDIO_PREVIEW_MARK_META,
|
|
3886
3886
|
STUDIO_PREVIEW_ERRORS
|
|
3887
3887
|
} from "@hyperframes/core/studio-preview-mark";
|
|
3888
|
-
import {
|
|
3888
|
+
import {
|
|
3889
|
+
PREVIEW_PROXY_BOX_PARAM,
|
|
3890
|
+
PREVIEW_PROXY_LIGHT_PARAM,
|
|
3891
|
+
parsePreviewProxyBox
|
|
3892
|
+
} from "@hyperframes/core";
|
|
3889
3893
|
import {
|
|
3890
3894
|
GSAP_BASE_URL_META,
|
|
3891
3895
|
gsapCdnDist as gsapCdnDist2,
|
|
@@ -4502,7 +4506,8 @@ ${etag}`;
|
|
|
4502
4506
|
const proxyParam = c.req.query("hf-proxy");
|
|
4503
4507
|
const boxParam = c.req.query(PREVIEW_PROXY_BOX_PARAM);
|
|
4504
4508
|
const proxyBox = boxParam === void 0 ? void 0 : parsePreviewProxyBox(boxParam);
|
|
4505
|
-
|
|
4509
|
+
const lightRequested = c.req.query(PREVIEW_PROXY_LIGHT_PARAM) !== void 0;
|
|
4510
|
+
if (proxyBox === null || proxyBox && proxyParam === void 0 || lightRequested && !proxyBox) {
|
|
4506
4511
|
return c.text("not found", 404);
|
|
4507
4512
|
}
|
|
4508
4513
|
let proxyVariant;
|
|
@@ -4511,7 +4516,7 @@ ${etag}`;
|
|
|
4511
4516
|
return c.text("not found", 404);
|
|
4512
4517
|
}
|
|
4513
4518
|
const facts = await probeAssetCodec(file, void 0, mediaCodecProbeCache);
|
|
4514
|
-
const eligibility = decideMediaProxyEligibility(facts);
|
|
4519
|
+
const eligibility = decideMediaProxyEligibility(facts, lightRequested ? proxyBox : void 0);
|
|
4515
4520
|
if (!eligibility.eligible) {
|
|
4516
4521
|
return c.text(`media proxy unavailable: ${eligibility.reason}`, 422);
|
|
4517
4522
|
}
|
|
@@ -6648,9 +6653,29 @@ function registerHistoryRoutes(api, adapter) {
|
|
|
6648
6653
|
);
|
|
6649
6654
|
}
|
|
6650
6655
|
|
|
6656
|
+
// src/helpers/previewErrorPage.ts
|
|
6657
|
+
import { STUDIO_PREVIEW_ERRORS as STUDIO_PREVIEW_ERRORS2 } from "@hyperframes/core/studio-preview-mark";
|
|
6658
|
+
function previewErrorPage(status) {
|
|
6659
|
+
const message = `The preview page could not be loaded (HTTP ${status}).`;
|
|
6660
|
+
return `<!doctype html><meta charset="utf-8"><script>window.${STUDIO_PREVIEW_ERRORS2}=[${JSON.stringify(message)}]</script><p>${message}</p>`;
|
|
6661
|
+
}
|
|
6662
|
+
|
|
6651
6663
|
// src/createStudioApi.ts
|
|
6664
|
+
async function answerRefusedFrameWithPage(c, next) {
|
|
6665
|
+
await next();
|
|
6666
|
+
if (c.req.header("sec-fetch-dest") !== "iframe" || c.res.status < 400) return;
|
|
6667
|
+
const headers = new Headers(c.res.headers);
|
|
6668
|
+
headers.set("content-type", "text/html; charset=utf-8");
|
|
6669
|
+
headers.delete("content-length");
|
|
6670
|
+
c.res = new Response(previewErrorPage(c.res.status), {
|
|
6671
|
+
status: c.res.status,
|
|
6672
|
+
headers
|
|
6673
|
+
});
|
|
6674
|
+
}
|
|
6652
6675
|
function createStudioApi(adapter) {
|
|
6653
6676
|
const api = new Hono();
|
|
6677
|
+
api.use("/projects/:id/preview/*", answerRefusedFrameWithPage);
|
|
6678
|
+
api.use("/projects/:id/preview", answerRefusedFrameWithPage);
|
|
6654
6679
|
api.use(async function answerProjectDirMissingAfterErrorHandlers(c, next) {
|
|
6655
6680
|
const hostHeaders = new Headers(c.res.headers);
|
|
6656
6681
|
await next();
|
|
@@ -6701,13 +6726,14 @@ function createStudioApi(adapter) {
|
|
|
6701
6726
|
// src/helpers/duplicateElement.ts
|
|
6702
6727
|
import { sameInstant } from "@hyperframes/core/clip-facts";
|
|
6703
6728
|
import { ensureHfIds as ensureHfIds4 } from "@hyperframes/parsers/hf-ids";
|
|
6704
|
-
function duplicateElementInHtml(source, target, newId, at) {
|
|
6729
|
+
function duplicateElementInHtml(source, target, newId, at, fallbackDuration = 0) {
|
|
6705
6730
|
const { document, wrappedFragment } = parseSourceDocument(source);
|
|
6706
6731
|
const element = findTargetElement(document, target);
|
|
6707
6732
|
if (!element || !element.parentElement) return { html: source, matched: false, newId: null };
|
|
6708
|
-
const duration = numericAttribute(element, "data-duration");
|
|
6733
|
+
const duration = element.hasAttribute("data-duration") ? numericAttribute(element, "data-duration") : fallbackDuration;
|
|
6734
|
+
const start = numericAttribute(element, "data-start");
|
|
6709
6735
|
const track = numericAttribute(element, "data-track-index") ?? 0;
|
|
6710
|
-
if (duration === null ||
|
|
6736
|
+
if (duration === null || start === null) {
|
|
6711
6737
|
return { html: source, matched: false, newId: null };
|
|
6712
6738
|
}
|
|
6713
6739
|
const uniqueId = nextUniqueId(document, newId);
|
|
@@ -6717,13 +6743,13 @@ function duplicateElementInHtml(source, target, newId, at) {
|
|
|
6717
6743
|
clone.setAttribute("id", uniqueId);
|
|
6718
6744
|
dedupeClonedCompositionId(document, clone);
|
|
6719
6745
|
stripStableIds(clone);
|
|
6720
|
-
clone
|
|
6746
|
+
moveElementStart(clone, start, at);
|
|
6721
6747
|
element.parentElement.insertBefore(clone, element.nextSibling);
|
|
6722
6748
|
const html = wrappedFragment ? document.body.innerHTML || "" : document.toString();
|
|
6723
6749
|
return { html: ensureHfIds4(html), matched: true, newId: uniqueId };
|
|
6724
6750
|
}
|
|
6725
6751
|
function rippleElements(document, element, at, duration, track) {
|
|
6726
|
-
for (const candidate of Array.from(document.querySelectorAll("[data-start]
|
|
6752
|
+
for (const candidate of Array.from(document.querySelectorAll("[data-start]"))) {
|
|
6727
6753
|
if (candidate === element || candidate.getAttribute("data-track-index") !== String(track)) {
|
|
6728
6754
|
continue;
|
|
6729
6755
|
}
|
|
@@ -6731,9 +6757,15 @@ function rippleElements(document, element, at, duration, track) {
|
|
|
6731
6757
|
if (start === null) continue;
|
|
6732
6758
|
const metInsertionPoint = sameInstant(start, at);
|
|
6733
6759
|
if (start < at && !metInsertionPoint) continue;
|
|
6734
|
-
candidate
|
|
6760
|
+
moveElementStart(candidate, start, (metInsertionPoint ? at : start) + duration);
|
|
6735
6761
|
}
|
|
6736
6762
|
}
|
|
6763
|
+
function moveElementStart(element, start, nextStart) {
|
|
6764
|
+
element.setAttribute("data-start", String(nextStart));
|
|
6765
|
+
if (!element.hasAttribute("data-end")) return;
|
|
6766
|
+
const end = numericAttribute(element, "data-end");
|
|
6767
|
+
if (end !== null) element.setAttribute("data-end", String(end + (nextStart - start)));
|
|
6768
|
+
}
|
|
6737
6769
|
function stripStableIds(element) {
|
|
6738
6770
|
element.removeAttribute("data-hf-id");
|
|
6739
6771
|
for (const child of Array.from(element.querySelectorAll("[data-hf-id]"))) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio-server",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.202",
|
|
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.202",
|
|
88
|
+
"@hyperframes/parsers": "0.8.202"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@types/node": "^25.0.10",
|