@hyperframes/studio-server 0.8.201 → 0.8.203
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-C2RFW2FX.js → chunk-JKPLDWAB.js} +27 -16
- package/dist/{chunk-5UEDEOD2.js → chunk-MNJZQCBL.js} +270 -72
- package/dist/{chunk-HZCA2O76.js → chunk-OAHY2RQP.js} +2 -2
- package/dist/helpers/mediaCodecMap.js +1 -1
- package/dist/helpers/mediaProxyPreview.js +3 -3
- package/dist/helpers/proxyTranscoder.d.ts +6 -1
- package/dist/helpers/proxyTranscoder.js +8 -2
- package/dist/index.js +14 -5
- package/package.json +3 -3
|
@@ -125,6 +125,30 @@ function sourceFactsOf(stream, formatBitrate) {
|
|
|
125
125
|
bitrate: Number.isFinite(bitrate) ? bitrate : 0
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
+
var STREAM_SUMMARY_MAX = 12;
|
|
129
|
+
function summarizeStream(stream) {
|
|
130
|
+
const flags = Object.entries(stream.disposition ?? {}).filter(([, on]) => on === 1).map(([name]) => name);
|
|
131
|
+
return `${stream.codec_type ?? "?"}:${stream.codec_name ?? "?"}[${flags.join(",")}]`;
|
|
132
|
+
}
|
|
133
|
+
function metadataFromProbe(kind, parsed) {
|
|
134
|
+
const stream = parsed.streams?.find((item) => {
|
|
135
|
+
if (kind === "image") return item.codec_type === "video";
|
|
136
|
+
return item.codec_type === kind && item.disposition?.attached_pic !== 1;
|
|
137
|
+
});
|
|
138
|
+
const metadata = {
|
|
139
|
+
kind,
|
|
140
|
+
color: classifyMediaColor(stream),
|
|
141
|
+
streams: (parsed.streams ?? []).slice(0, STREAM_SUMMARY_MAX).map(summarizeStream)
|
|
142
|
+
};
|
|
143
|
+
if (kind === "video") {
|
|
144
|
+
metadata.hasAudio = (parsed.streams ?? []).some((item) => item.codec_type === "audio");
|
|
145
|
+
metadata.hasAlpha = stream !== void 0 && videoStreamHasAlpha(stream);
|
|
146
|
+
const duration = Number(parsed.format?.duration);
|
|
147
|
+
if (Number.isFinite(duration) && duration > 0) metadata.durationSeconds = duration;
|
|
148
|
+
metadata.source = sourceFactsOf(stream, parsed.format?.bit_rate);
|
|
149
|
+
}
|
|
150
|
+
return metadata;
|
|
151
|
+
}
|
|
128
152
|
async function probeMediaMetadata(filePath, runner = execFileRunner) {
|
|
129
153
|
const kind = inferKindFromPath(filePath);
|
|
130
154
|
if (kind === "audio" || kind === "unknown") {
|
|
@@ -140,7 +164,7 @@ async function probeMediaMetadata(filePath, runner = execFileRunner) {
|
|
|
140
164
|
"-v",
|
|
141
165
|
"error",
|
|
142
166
|
"-show_entries",
|
|
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
|
|
167
|
+
"stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample,width,height,bit_rate:stream_disposition:stream_tags=alpha_mode:format=duration,bit_rate",
|
|
144
168
|
"-of",
|
|
145
169
|
"json",
|
|
146
170
|
"--",
|
|
@@ -155,20 +179,7 @@ async function probeMediaMetadata(filePath, runner = execFileRunner) {
|
|
|
155
179
|
return { kind, color: classifyMediaColor(null), probeError: "ffprobe failed" };
|
|
156
180
|
}
|
|
157
181
|
try {
|
|
158
|
-
|
|
159
|
-
const stream = parsed.streams?.find((item) => {
|
|
160
|
-
if (kind === "image") return item.codec_type === "video";
|
|
161
|
-
return item.codec_type === kind && item.disposition?.attached_pic !== 1;
|
|
162
|
-
});
|
|
163
|
-
const metadata = { kind, color: classifyMediaColor(stream) };
|
|
164
|
-
if (kind === "video") {
|
|
165
|
-
metadata.hasAudio = (parsed.streams ?? []).some((item) => item.codec_type === "audio");
|
|
166
|
-
metadata.hasAlpha = stream !== void 0 && videoStreamHasAlpha(stream);
|
|
167
|
-
const duration = Number(parsed.format?.duration);
|
|
168
|
-
if (Number.isFinite(duration) && duration > 0) metadata.durationSeconds = duration;
|
|
169
|
-
metadata.source = sourceFactsOf(stream, parsed.format?.bit_rate);
|
|
170
|
-
}
|
|
171
|
-
return metadata;
|
|
182
|
+
return metadataFromProbe(kind, JSON.parse(String(result.stdout || "{}")));
|
|
172
183
|
} catch {
|
|
173
184
|
return { kind, color: classifyMediaColor(null), probeError: "ffprobe returned invalid json" };
|
|
174
185
|
}
|
|
@@ -348,7 +359,7 @@ async function scanProjectMedia(projectDir, htmlSources, options = {}) {
|
|
|
348
359
|
const scan = { codecMap: {}, sourceFacts: {} };
|
|
349
360
|
for (const [pathname, facts] of await probeProjectAssets(projectDir, htmlSources, options)) {
|
|
350
361
|
if (facts?.browserHostile) scan.codecMap[pathname] = facts;
|
|
351
|
-
if (facts?.source) scan.sourceFacts[pathname] = facts.source;
|
|
362
|
+
if (facts?.source) scan.sourceFacts[pathname] = { ...facts.source, codec: facts.codecName };
|
|
352
363
|
}
|
|
353
364
|
return scan;
|
|
354
365
|
}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
PROXY_VARIANT_CONFIG,
|
|
3
3
|
probeFirstFrameColour,
|
|
4
4
|
probeMediaMetadata
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-JKPLDWAB.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
24
|
import { availableParallelism, tmpdir, totalmem } from "os";
|
|
26
|
-
import { basename, dirname, extname as
|
|
27
|
-
import {
|
|
28
|
-
formatPreviewProxyBox,
|
|
29
|
-
hdrToSdrToneMapFilter
|
|
30
|
-
} from "@hyperframes/core";
|
|
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,6 +139,148 @@ 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 sourceSizeBytes(sourcePath) {
|
|
189
|
+
try {
|
|
190
|
+
const fd = openSync(sourcePath, "r");
|
|
191
|
+
try {
|
|
192
|
+
return fstatSync(fd).size;
|
|
193
|
+
} finally {
|
|
194
|
+
closeSync(fd);
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function fastIndexPath(cachePath) {
|
|
201
|
+
return join2(dirname(cachePath), `.fast-${basename(cachePath, extname2(cachePath))}`);
|
|
202
|
+
}
|
|
203
|
+
function contentIndexPath(cacheDir, contentKey) {
|
|
204
|
+
return join2(cacheDir, `.content-${contentKey}`);
|
|
205
|
+
}
|
|
206
|
+
function readCopyIndex(indexPath) {
|
|
207
|
+
try {
|
|
208
|
+
const name = readFileSync(indexPath, "utf8").trim();
|
|
209
|
+
const target = join2(dirname(indexPath), name);
|
|
210
|
+
return COPY_NAME.test(name) && existsSync2(target) ? target : null;
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function writeCopyIndex(indexPath, copyPath) {
|
|
216
|
+
writeFileSync(indexPath, basename(copyPath));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/helpers/proxyLock.ts
|
|
220
|
+
import {
|
|
221
|
+
closeSync as closeSync2,
|
|
222
|
+
existsSync as existsSync3,
|
|
223
|
+
openSync as openSync2,
|
|
224
|
+
readFileSync as readFileSync2,
|
|
225
|
+
rmSync,
|
|
226
|
+
statSync as statSync3,
|
|
227
|
+
writeSync
|
|
228
|
+
} from "fs";
|
|
229
|
+
import { basename as basename2, dirname as dirname2, join as join3 } from "path";
|
|
230
|
+
var POLL_MS = 500;
|
|
231
|
+
var UNREADABLE_LOCK_MS = 1e4;
|
|
232
|
+
function lockPathFor(cachePath) {
|
|
233
|
+
return join3(dirname2(cachePath), `.lock-${basename2(cachePath)}`);
|
|
234
|
+
}
|
|
235
|
+
function ownerIsAlive(lockPath) {
|
|
236
|
+
try {
|
|
237
|
+
const pid = Number(readFileSync2(lockPath, "utf8"));
|
|
238
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
239
|
+
return Date.now() - statSync3(lockPath).mtimeMs < UNREADABLE_LOCK_MS;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
process.kill(pid, 0);
|
|
243
|
+
return true;
|
|
244
|
+
} catch (err) {
|
|
245
|
+
return err.code === "EPERM";
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function pause(ms, signal) {
|
|
252
|
+
return new Promise((resolve2, reject) => {
|
|
253
|
+
const timer = setTimeout(() => {
|
|
254
|
+
signal.removeEventListener("abort", onAbort);
|
|
255
|
+
resolve2();
|
|
256
|
+
}, ms);
|
|
257
|
+
const onAbort = () => {
|
|
258
|
+
clearTimeout(timer);
|
|
259
|
+
reject(signal.reason);
|
|
260
|
+
};
|
|
261
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
async function acquireCopyLock(cachePath, signal) {
|
|
265
|
+
const lockPath = lockPathFor(cachePath);
|
|
266
|
+
for (; ; ) {
|
|
267
|
+
signal.throwIfAborted();
|
|
268
|
+
if (existsSync3(cachePath)) return null;
|
|
269
|
+
try {
|
|
270
|
+
const fd = openSync2(lockPath, "wx");
|
|
271
|
+
writeSync(fd, String(process.pid));
|
|
272
|
+
closeSync2(fd);
|
|
273
|
+
if (!existsSync3(cachePath)) return () => rmSync(lockPath, { force: true });
|
|
274
|
+
rmSync(lockPath, { force: true });
|
|
275
|
+
return null;
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (err.code !== "EEXIST") throw err;
|
|
278
|
+
}
|
|
279
|
+
if (ownerIsAlive(lockPath)) await pause(POLL_MS, signal);
|
|
280
|
+
else rmSync(lockPath, { force: true });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
146
284
|
// src/helpers/proxyTranscoder.ts
|
|
147
285
|
var PROXY_PARAMS_VERSION = "v7";
|
|
148
286
|
var CACHE_DIR_NAME = ".transcode-cache";
|
|
@@ -235,33 +373,16 @@ function canonicalizeProxySource(projectDir, absoluteSourcePath) {
|
|
|
235
373
|
if (requestedRelativePath === ".." || requestedRelativePath.startsWith(`..${sep}`) || isAbsolute(requestedRelativePath)) {
|
|
236
374
|
throw new ProxySourceOutsideProjectError();
|
|
237
375
|
}
|
|
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");
|
|
376
|
+
return { projectDir: realProjectRoot(projectDir), sourcePath: realpath(absoluteSourcePath) };
|
|
257
377
|
}
|
|
258
378
|
function getCanonicalProxyCachePath(source, variant, box) {
|
|
259
|
-
const key =
|
|
260
|
-
|
|
379
|
+
const key = fastCopyKey(source.sourcePath, PROXY_PARAMS_VERSION, variant, box);
|
|
380
|
+
const made = join4(
|
|
261
381
|
source.projectDir,
|
|
262
382
|
CACHE_DIR_NAME,
|
|
263
383
|
`${key}${PROXY_VARIANT_CONFIG[variant].extension}`
|
|
264
384
|
);
|
|
385
|
+
return existsSync4(made) ? made : readCopyIndex(fastIndexPath(made)) ?? made;
|
|
265
386
|
}
|
|
266
387
|
function getProxyCachePath(projectDir, absoluteSourcePath, variant = "h264", box) {
|
|
267
388
|
return getCanonicalProxyCachePath(
|
|
@@ -406,11 +527,53 @@ function proxyScaleFilter(box) {
|
|
|
406
527
|
return `scale=${width}:${height}`;
|
|
407
528
|
}
|
|
408
529
|
var STDERR_ERROR_LINE = /error|invalid|denied|no space|not found|unknown/i;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
530
|
+
var OPEN_OUTPUT_LINE = /^Error opening output file /;
|
|
531
|
+
var ABSOLUTE_PATH = /(?:[A-Za-z]:)?(?:[\\/][^\\/\s'"]+){2,}/g;
|
|
532
|
+
var REASON_LINES = 3;
|
|
533
|
+
function stderrReason(stderrTail, knownPaths) {
|
|
534
|
+
const lines = stderrTail.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
535
|
+
const openedAt = lines.map((candidate) => OPEN_OUTPUT_LINE.test(candidate)).lastIndexOf(true);
|
|
536
|
+
const causes = lines.filter(
|
|
537
|
+
(line, index) => !OPEN_OUTPUT_LINE.test(line) && (STDERR_ERROR_LINE.test(line) || index === openedAt - 1)
|
|
538
|
+
);
|
|
539
|
+
const shown = (causes.length ? causes : lines.slice(-1)).slice(-REASON_LINES);
|
|
540
|
+
const byLength = knownPaths.filter(isAbsolute).sort((a, b) => b.length - a.length);
|
|
541
|
+
const reason = shown.map(
|
|
542
|
+
(line) => byLength.reduce((text, path) => text.split(path).join("<path>"), line).replace(ABSOLUTE_PATH, "<path>").replace(/[.!]+$/, "")
|
|
543
|
+
);
|
|
544
|
+
return reason.length ? `: ${reason.join("; ")}` : "";
|
|
545
|
+
}
|
|
546
|
+
var CLOUD_FOLDER = /\/(?:Library\/CloudStorage|Dropbox|OneDrive|Google Drive)(?:[ \-/]|$)/i;
|
|
547
|
+
function sourceFolderKind(sourcePath) {
|
|
548
|
+
const path = sourcePath.replace(/\\/g, "/");
|
|
549
|
+
if (path.includes("/Library/Mobile Documents/")) return "icloud";
|
|
550
|
+
if (CLOUD_FOLDER.test(path)) return "cloud-storage";
|
|
551
|
+
return path.startsWith("/Volumes/") ? "external-volume" : "local";
|
|
552
|
+
}
|
|
553
|
+
function describeSource(sourcePath, metadata) {
|
|
554
|
+
const { codecName, pixelFormat, colorTransfer } = metadata.color;
|
|
555
|
+
const megabytes = sourceSizeBytes(sourcePath);
|
|
556
|
+
return [
|
|
557
|
+
metadata.kind,
|
|
558
|
+
codecName,
|
|
559
|
+
pixelFormat,
|
|
560
|
+
colorTransfer,
|
|
561
|
+
metadata.hasAudio ? "with audio" : "no audio",
|
|
562
|
+
metadata.streams?.length ? `streams ${metadata.streams.join(" ")}` : void 0,
|
|
563
|
+
metadata.durationSeconds ? `${Math.round(metadata.durationSeconds)}s` : void 0,
|
|
564
|
+
megabytes === void 0 ? "missing" : `${Math.round(megabytes / 1e6)} MB`,
|
|
565
|
+
sourceFolderKind(sourcePath)
|
|
566
|
+
].filter(Boolean).join(", ");
|
|
567
|
+
}
|
|
568
|
+
function withSource(err, describe) {
|
|
569
|
+
if (err instanceof ProxyTranscodeError && err.name === "ProxyTranscodeError") {
|
|
570
|
+
return new ProxyTranscodeError(
|
|
571
|
+
`${err.message} (source: ${describe()})`,
|
|
572
|
+
err.exitCode,
|
|
573
|
+
err.stderrTail
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
return err;
|
|
414
577
|
}
|
|
415
578
|
function chunkRanges(durationSeconds) {
|
|
416
579
|
if (!durationSeconds || durationSeconds <= PROXY_CHUNK_SECONDS * 1.5) return null;
|
|
@@ -456,9 +619,9 @@ function createPieceGate(size) {
|
|
|
456
619
|
}
|
|
457
620
|
async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, signal, gate, onDone = () => {
|
|
458
621
|
}) {
|
|
459
|
-
const extension =
|
|
622
|
+
const extension = extname3(tempPath);
|
|
460
623
|
const pieces = ranges.map((_, index) => `${chunkPrefix}${index}${extension}`);
|
|
461
|
-
const pending = ranges.flatMap((_, index) =>
|
|
624
|
+
const pending = ranges.flatMap((_, index) => existsSync4(pieces[index]) ? [] : [index]);
|
|
462
625
|
let doneSeconds = 0;
|
|
463
626
|
for (const [index, range] of ranges.entries()) {
|
|
464
627
|
if (!pending.includes(index)) doneSeconds += range.lengthSeconds;
|
|
@@ -477,7 +640,7 @@ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, sig
|
|
|
477
640
|
const index = pending[cursor++];
|
|
478
641
|
const range = ranges[index];
|
|
479
642
|
const piece = pieces[index];
|
|
480
|
-
const pieceTemp =
|
|
643
|
+
const pieceTemp = join4(dirname3(piece), `.tmp-${randomUUID()}-${basename3(piece)}`);
|
|
481
644
|
try {
|
|
482
645
|
await encode(pieceTemp, range, pieceSignal, (encodedSeconds) => {
|
|
483
646
|
inFlight2.set(index, encodedSeconds);
|
|
@@ -488,7 +651,7 @@ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, sig
|
|
|
488
651
|
doneSeconds += range.lengthSeconds;
|
|
489
652
|
onDone(doneSeconds);
|
|
490
653
|
} finally {
|
|
491
|
-
if (
|
|
654
|
+
if (existsSync4(pieceTemp)) unlinkSync2(pieceTemp);
|
|
492
655
|
}
|
|
493
656
|
} finally {
|
|
494
657
|
gate.release();
|
|
@@ -505,19 +668,19 @@ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, sig
|
|
|
505
668
|
)
|
|
506
669
|
);
|
|
507
670
|
if (failure !== void 0) throw failure;
|
|
508
|
-
const listDir = mkdtempSync(
|
|
671
|
+
const listDir = mkdtempSync(join4(tmpdir(), "hf-proxy-join-"));
|
|
509
672
|
try {
|
|
510
|
-
const listPath =
|
|
511
|
-
|
|
673
|
+
const listPath = join4(listDir, "pieces.txt");
|
|
674
|
+
writeFileSync2(
|
|
512
675
|
listPath,
|
|
513
676
|
pieces.map((piece) => `file '${piece.replace(/\\/g, "/").replace(/'/g, "'\\''")}'
|
|
514
677
|
`).join("")
|
|
515
678
|
);
|
|
516
679
|
await concat(listPath, tempPath);
|
|
517
680
|
} finally {
|
|
518
|
-
|
|
681
|
+
rmSync2(listDir, { recursive: true, force: true });
|
|
519
682
|
}
|
|
520
|
-
for (const piece of pieces) if (
|
|
683
|
+
for (const piece of pieces) if (existsSync4(piece)) unlinkSync2(piece);
|
|
521
684
|
}
|
|
522
685
|
var KEYFRAME_EVERY_SECOND_ARGS = ["-force_key_frames", "expr:gte(t,n_forced*1)"];
|
|
523
686
|
var H264_ARGS = [
|
|
@@ -627,24 +790,28 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
|
|
|
627
790
|
onEncoded
|
|
628
791
|
);
|
|
629
792
|
const ranges = chunkRanges(totalSeconds);
|
|
630
|
-
|
|
631
|
-
|
|
793
|
+
try {
|
|
794
|
+
if (!ranges) {
|
|
795
|
+
return await encode(
|
|
796
|
+
outputPath,
|
|
797
|
+
void 0,
|
|
798
|
+
signal,
|
|
799
|
+
(encodedSeconds) => onProgress({ doneSeconds: encodedSeconds, totalSeconds })
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
await encodeInChunks(
|
|
803
|
+
ranges,
|
|
804
|
+
chunkPrefix,
|
|
632
805
|
outputPath,
|
|
633
|
-
|
|
806
|
+
encode,
|
|
807
|
+
(listPath, target) => spawnFfmpeg(ffmpegPath, concatArgs(listPath, target, variant), CONCAT_TIMEOUT_MS, signal),
|
|
634
808
|
signal,
|
|
635
|
-
|
|
809
|
+
gate,
|
|
810
|
+
(doneSeconds) => onProgress({ doneSeconds, totalSeconds })
|
|
636
811
|
);
|
|
812
|
+
} catch (err) {
|
|
813
|
+
throw withSource(err, () => describeSource(sourcePath, metadata));
|
|
637
814
|
}
|
|
638
|
-
await encodeInChunks(
|
|
639
|
-
ranges,
|
|
640
|
-
chunkPrefix,
|
|
641
|
-
outputPath,
|
|
642
|
-
encode,
|
|
643
|
-
(listPath, target) => spawnFfmpeg(ffmpegPath, concatArgs(listPath, target, variant), CONCAT_TIMEOUT_MS, signal),
|
|
644
|
-
signal,
|
|
645
|
-
gate,
|
|
646
|
-
(doneSeconds) => onProgress({ doneSeconds, totalSeconds })
|
|
647
|
-
);
|
|
648
815
|
}
|
|
649
816
|
function concatArgs(listPath, target, variant) {
|
|
650
817
|
return [
|
|
@@ -725,7 +892,7 @@ function spawnFfmpeg(ffmpegPath, args, stallMs, signal, onProgress) {
|
|
|
725
892
|
} else {
|
|
726
893
|
reject(
|
|
727
894
|
new ProxyTranscodeError(
|
|
728
|
-
`ffmpeg exited with code ${code}${stderrReason(stderrTail)}`,
|
|
895
|
+
`ffmpeg exited with code ${code}${stderrReason(stderrTail, args)}`,
|
|
729
896
|
code,
|
|
730
897
|
stderrTail
|
|
731
898
|
)
|
|
@@ -737,14 +904,31 @@ function spawnFfmpeg(ffmpegPath, args, stallMs, signal, onProgress) {
|
|
|
737
904
|
async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath, variant, box) {
|
|
738
905
|
await acquireSlot(job);
|
|
739
906
|
try {
|
|
740
|
-
if (
|
|
741
|
-
const cacheDir =
|
|
907
|
+
if (existsSync4(cachePath)) return cachePath;
|
|
908
|
+
const cacheDir = dirname3(cachePath);
|
|
742
909
|
mkdirWithinProject(projectDir, cacheDir);
|
|
743
|
-
const
|
|
910
|
+
const contentIndex = contentIndexPath(
|
|
911
|
+
cacheDir,
|
|
912
|
+
await contentCopyKey(
|
|
913
|
+
absoluteSourcePath,
|
|
914
|
+
PROXY_PARAMS_VERSION,
|
|
915
|
+
variant,
|
|
916
|
+
box,
|
|
917
|
+
job.abandoned.signal
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
const sameContent = readCopyIndex(contentIndex);
|
|
921
|
+
if (sameContent) {
|
|
922
|
+
writeCopyIndex(fastIndexPath(cachePath), sameContent);
|
|
923
|
+
return sameContent;
|
|
924
|
+
}
|
|
925
|
+
const release = await acquireCopyLock(cachePath, job.abandoned.signal);
|
|
926
|
+
if (!release) return cachePath;
|
|
927
|
+
const tempPath = join4(cacheDir, `.tmp-${randomUUID()}-${basename3(cachePath)}`);
|
|
744
928
|
try {
|
|
745
|
-
const chunkPrefix =
|
|
929
|
+
const chunkPrefix = join4(
|
|
746
930
|
cacheDir,
|
|
747
|
-
`${CHUNK_PREFIX}${
|
|
931
|
+
`${CHUNK_PREFIX}${basename3(cachePath, extname3(cachePath))}-`
|
|
748
932
|
);
|
|
749
933
|
await runFfmpeg(
|
|
750
934
|
absoluteSourcePath,
|
|
@@ -756,19 +940,30 @@ async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath,
|
|
|
756
940
|
(progress) => reportProgress(job, progress)
|
|
757
941
|
);
|
|
758
942
|
renameSync(tempPath, cachePath);
|
|
943
|
+
writeCopyIndex(contentIndex, cachePath);
|
|
759
944
|
maintainProxyCache(cacheDir);
|
|
760
945
|
return cachePath;
|
|
761
946
|
} finally {
|
|
762
|
-
if (
|
|
947
|
+
if (existsSync4(tempPath)) unlinkSync2(tempPath);
|
|
948
|
+
release();
|
|
763
949
|
}
|
|
764
950
|
} finally {
|
|
765
951
|
releaseSlot();
|
|
766
952
|
}
|
|
767
953
|
}
|
|
954
|
+
function proxyCopyProgress(projectDir, absoluteSourcePath, variant, box) {
|
|
955
|
+
const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
|
|
956
|
+
const progress = inFlight.get(getCanonicalProxyCachePath(source, variant, box))?.job.lastProgress;
|
|
957
|
+
return progress?.totalSeconds ? Math.min(1, progress.doneSeconds / progress.totalSeconds) : void 0;
|
|
958
|
+
}
|
|
959
|
+
function retryProxyCopy(projectDir, absoluteSourcePath, variant, box) {
|
|
960
|
+
const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
|
|
961
|
+
failedTranscodes.delete(getCanonicalProxyCachePath(source, variant, box));
|
|
962
|
+
}
|
|
768
963
|
var proxyActivity = 0;
|
|
769
964
|
function proxyActivityMark(projectDir) {
|
|
770
|
-
if (!
|
|
771
|
-
const cacheDir =
|
|
965
|
+
if (!existsSync4(projectDir)) return String(proxyActivity);
|
|
966
|
+
const cacheDir = join4(realpath(projectDir), CACHE_DIR_NAME) + sep;
|
|
772
967
|
for (const cachePath of inFlight.keys()) if (cachePath.startsWith(cacheDir)) return null;
|
|
773
968
|
return String(proxyActivity);
|
|
774
969
|
}
|
|
@@ -776,9 +971,9 @@ async function resolveProxy(projectDir, absoluteSourcePath, variant = "h264", bo
|
|
|
776
971
|
options.signal?.throwIfAborted();
|
|
777
972
|
const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
|
|
778
973
|
const cachePath = getCanonicalProxyCachePath(source, variant, box);
|
|
779
|
-
if (
|
|
974
|
+
if (existsSync4(cachePath)) {
|
|
780
975
|
markCacheEntryUsed(cachePath);
|
|
781
|
-
maintainProxyCache(
|
|
976
|
+
maintainProxyCache(dirname3(cachePath));
|
|
782
977
|
return cachePath;
|
|
783
978
|
}
|
|
784
979
|
const rememberedFailure = failedTranscodes.get(cachePath);
|
|
@@ -835,7 +1030,7 @@ async function resolveProxies(projectDir, sources, onSettled = () => {
|
|
|
835
1030
|
const first = sources[0];
|
|
836
1031
|
if (first) {
|
|
837
1032
|
maintainProxyCache(
|
|
838
|
-
|
|
1033
|
+
dirname3(getProxyCachePath(projectDir, first.sourcePath, first.variant)),
|
|
839
1034
|
Math.min(
|
|
840
1035
|
Number(process.env.HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS) || UNTOUCHED_TEMP_IS_DEAD_MS,
|
|
841
1036
|
UNTOUCHED_TEMP_IS_DEAD_MS
|
|
@@ -891,6 +1086,9 @@ export {
|
|
|
891
1086
|
waitForProxy,
|
|
892
1087
|
getProxyCachePath,
|
|
893
1088
|
clearFailedTranscodesForTest,
|
|
1089
|
+
sourceFolderKind,
|
|
1090
|
+
proxyCopyProgress,
|
|
1091
|
+
retryProxyCopy,
|
|
894
1092
|
proxyActivityMark,
|
|
895
1093
|
resolveProxy,
|
|
896
1094
|
resolveProxies
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROXY_PARAMS_VERSION,
|
|
3
3
|
resolveProxy
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-MNJZQCBL.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-JKPLDWAB.js";
|
|
12
12
|
|
|
13
13
|
// src/helpers/mediaProxyPreview.ts
|
|
14
14
|
import { resolve } from "path";
|
|
@@ -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-OAHY2RQP.js";
|
|
8
|
+
import "../chunk-MNJZQCBL.js";
|
|
9
|
+
import "../chunk-JKPLDWAB.js";
|
|
10
10
|
import "../chunk-QF7MFJJU.js";
|
|
11
11
|
import "../chunk-SVQFE44C.js";
|
|
12
12
|
import "../chunk-L3HJVTQE.js";
|
|
@@ -59,6 +59,11 @@ interface ProxyProgress {
|
|
|
59
59
|
/** Test hook: forget remembered transcode failures (module state persists
|
|
60
60
|
* across tests that don't reload the module). */
|
|
61
61
|
declare function clearFailedTranscodesForTest(): void;
|
|
62
|
+
type SourceFolderKind = "icloud" | "cloud-storage" | "external-volume" | "local";
|
|
63
|
+
/** Where a source lives, in the terms that decide whether its file can be read and written normally. */
|
|
64
|
+
declare function sourceFolderKind(sourcePath: string): SourceFolderKind;
|
|
65
|
+
declare function proxyCopyProgress(projectDir: string, absoluteSourcePath: string, variant: ProxyVariant, box?: PreviewProxyBox): number | undefined;
|
|
66
|
+
declare function retryProxyCopy(projectDir: string, absoluteSourcePath: string, variant: ProxyVariant, box?: PreviewProxyBox): void;
|
|
62
67
|
/** Null while this project has a copy in progress; otherwise a mark that moves when any copy finishes or is refused. */
|
|
63
68
|
declare function proxyActivityMark(projectDir: string): string | null;
|
|
64
69
|
/**
|
|
@@ -89,4 +94,4 @@ interface ResolveProxyOptions {
|
|
|
89
94
|
onProgress?: (progress: ProxyProgress) => void;
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
export { CACHE_DIR_NAME, DEFAULT_PROXY_WAIT_TIMEOUT_MS, FfmpegMissingFilterError, FfmpegUnavailableError, PROXY_CHUNK_SECONDS, PROXY_PARAMS_VERSION, PROXY_PENDING_RETRY_AFTER_SECONDS, ProxyCapacityError, type ProxyProgress, ProxySourceOutsideProjectError, ProxyTranscodeError, ProxyWaitTimeoutError, type ResolveProxyOptions, TRANSCODE_STALL_MS, clearFailedTranscodesForTest, getProxyCachePath, proxyActivityMark, resolveProxies, resolveProxy, waitForProxy };
|
|
97
|
+
export { CACHE_DIR_NAME, DEFAULT_PROXY_WAIT_TIMEOUT_MS, FfmpegMissingFilterError, FfmpegUnavailableError, PROXY_CHUNK_SECONDS, PROXY_PARAMS_VERSION, PROXY_PENDING_RETRY_AFTER_SECONDS, ProxyCapacityError, type ProxyProgress, ProxySourceOutsideProjectError, ProxyTranscodeError, ProxyWaitTimeoutError, type ResolveProxyOptions, type SourceFolderKind, TRANSCODE_STALL_MS, clearFailedTranscodesForTest, getProxyCachePath, proxyActivityMark, proxyCopyProgress, resolveProxies, resolveProxy, retryProxyCopy, sourceFolderKind, waitForProxy };
|
|
@@ -14,11 +14,14 @@ import {
|
|
|
14
14
|
clearFailedTranscodesForTest,
|
|
15
15
|
getProxyCachePath,
|
|
16
16
|
proxyActivityMark,
|
|
17
|
+
proxyCopyProgress,
|
|
17
18
|
resolveProxies,
|
|
18
19
|
resolveProxy,
|
|
20
|
+
retryProxyCopy,
|
|
21
|
+
sourceFolderKind,
|
|
19
22
|
waitForProxy
|
|
20
|
-
} from "../chunk-
|
|
21
|
-
import "../chunk-
|
|
23
|
+
} from "../chunk-MNJZQCBL.js";
|
|
24
|
+
import "../chunk-JKPLDWAB.js";
|
|
22
25
|
import "../chunk-QF7MFJJU.js";
|
|
23
26
|
import "../chunk-SVQFE44C.js";
|
|
24
27
|
import "../chunk-L3HJVTQE.js";
|
|
@@ -38,7 +41,10 @@ export {
|
|
|
38
41
|
clearFailedTranscodesForTest,
|
|
39
42
|
getProxyCachePath,
|
|
40
43
|
proxyActivityMark,
|
|
44
|
+
proxyCopyProgress,
|
|
41
45
|
resolveProxies,
|
|
42
46
|
resolveProxy,
|
|
47
|
+
retryProxyCopy,
|
|
48
|
+
sourceFolderKind,
|
|
43
49
|
waitForProxy
|
|
44
50
|
};
|
package/dist/index.js
CHANGED
|
@@ -82,7 +82,7 @@ import {
|
|
|
82
82
|
isAutoProxyEnabled,
|
|
83
83
|
proxyEtagSalt,
|
|
84
84
|
resolvePreviewMediaCodecProbeCache
|
|
85
|
-
} from "./chunk-
|
|
85
|
+
} from "./chunk-OAHY2RQP.js";
|
|
86
86
|
import {
|
|
87
87
|
CACHE_DIR_NAME,
|
|
88
88
|
PROXY_PENDING_RETRY_AFTER_SECONDS,
|
|
@@ -91,9 +91,11 @@ import {
|
|
|
91
91
|
ProxyWaitTimeoutError,
|
|
92
92
|
cleanupProxyCache,
|
|
93
93
|
proxyActivityMark,
|
|
94
|
+
proxyCopyProgress,
|
|
94
95
|
resolveProxy,
|
|
96
|
+
retryProxyCopy,
|
|
95
97
|
waitForProxy
|
|
96
|
-
} from "./chunk-
|
|
98
|
+
} from "./chunk-MNJZQCBL.js";
|
|
97
99
|
import {
|
|
98
100
|
PROXY_VARIANT_CONFIG,
|
|
99
101
|
decideMediaProxyEligibility,
|
|
@@ -102,7 +104,7 @@ import {
|
|
|
102
104
|
probeMediaMetadata,
|
|
103
105
|
recordProxyRequest,
|
|
104
106
|
resolveProxyVariantRequest
|
|
105
|
-
} from "./chunk-
|
|
107
|
+
} from "./chunk-JKPLDWAB.js";
|
|
106
108
|
import {
|
|
107
109
|
folderGone,
|
|
108
110
|
isInHiddenOrVendorDir,
|
|
@@ -3888,6 +3890,8 @@ import {
|
|
|
3888
3890
|
import {
|
|
3889
3891
|
PREVIEW_PROXY_BOX_PARAM,
|
|
3890
3892
|
PREVIEW_PROXY_LIGHT_PARAM,
|
|
3893
|
+
PREVIEW_PROXY_PROGRESS_HEADER,
|
|
3894
|
+
PREVIEW_PROXY_RETRY_PARAM,
|
|
3891
3895
|
parsePreviewProxyBox
|
|
3892
3896
|
} from "@hyperframes/core";
|
|
3893
3897
|
import {
|
|
@@ -4539,12 +4543,17 @@ ${etag}`;
|
|
|
4539
4543
|
let servedContentType = contentType;
|
|
4540
4544
|
if (proxyVariant !== void 0) {
|
|
4541
4545
|
try {
|
|
4546
|
+
if (c.req.query(PREVIEW_PROXY_RETRY_PARAM) !== void 0) {
|
|
4547
|
+
retryProxyCopy(project.dir, file, proxyVariant, proxyBox);
|
|
4548
|
+
}
|
|
4542
4549
|
servedPath = await waitForProxy(resolveProxy(project.dir, file, proxyVariant, proxyBox), 0);
|
|
4543
4550
|
} catch (err) {
|
|
4544
4551
|
if (err instanceof ProxyWaitTimeoutError) {
|
|
4552
|
+
const progress = proxyCopyProgress(project.dir, file, proxyVariant, proxyBox);
|
|
4545
4553
|
return c.text("media proxy is being made", 202, {
|
|
4546
|
-
"Retry-After": String(PROXY_PENDING_RETRY_AFTER_SECONDS),
|
|
4547
|
-
"Cache-Control": "no-store"
|
|
4554
|
+
"Retry-After": progress === void 0 ? String(PROXY_PENDING_RETRY_AFTER_SECONDS) : "1",
|
|
4555
|
+
"Cache-Control": "no-store",
|
|
4556
|
+
...progress !== void 0 && { [PREVIEW_PROXY_PROGRESS_HEADER]: progress.toFixed(3) }
|
|
4548
4557
|
});
|
|
4549
4558
|
}
|
|
4550
4559
|
if (err instanceof ProxyCapacityError) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio-server",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.203",
|
|
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.203",
|
|
88
|
+
"@hyperframes/parsers": "0.8.203"
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@types/node": "^25.0.10",
|