@hyperframes/studio-server 0.8.201 → 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.
|
@@ -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,136 @@ 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
273
|
var PROXY_PARAMS_VERSION = "v7";
|
|
148
274
|
var CACHE_DIR_NAME = ".transcode-cache";
|
|
@@ -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(
|
|
@@ -456,9 +565,9 @@ function createPieceGate(size) {
|
|
|
456
565
|
}
|
|
457
566
|
async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, signal, gate, onDone = () => {
|
|
458
567
|
}) {
|
|
459
|
-
const extension =
|
|
568
|
+
const extension = extname3(tempPath);
|
|
460
569
|
const pieces = ranges.map((_, index) => `${chunkPrefix}${index}${extension}`);
|
|
461
|
-
const pending = ranges.flatMap((_, index) =>
|
|
570
|
+
const pending = ranges.flatMap((_, index) => existsSync4(pieces[index]) ? [] : [index]);
|
|
462
571
|
let doneSeconds = 0;
|
|
463
572
|
for (const [index, range] of ranges.entries()) {
|
|
464
573
|
if (!pending.includes(index)) doneSeconds += range.lengthSeconds;
|
|
@@ -477,7 +586,7 @@ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, sig
|
|
|
477
586
|
const index = pending[cursor++];
|
|
478
587
|
const range = ranges[index];
|
|
479
588
|
const piece = pieces[index];
|
|
480
|
-
const pieceTemp =
|
|
589
|
+
const pieceTemp = join4(dirname3(piece), `.tmp-${randomUUID()}-${basename3(piece)}`);
|
|
481
590
|
try {
|
|
482
591
|
await encode(pieceTemp, range, pieceSignal, (encodedSeconds) => {
|
|
483
592
|
inFlight2.set(index, encodedSeconds);
|
|
@@ -488,7 +597,7 @@ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, sig
|
|
|
488
597
|
doneSeconds += range.lengthSeconds;
|
|
489
598
|
onDone(doneSeconds);
|
|
490
599
|
} finally {
|
|
491
|
-
if (
|
|
600
|
+
if (existsSync4(pieceTemp)) unlinkSync2(pieceTemp);
|
|
492
601
|
}
|
|
493
602
|
} finally {
|
|
494
603
|
gate.release();
|
|
@@ -505,19 +614,19 @@ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, sig
|
|
|
505
614
|
)
|
|
506
615
|
);
|
|
507
616
|
if (failure !== void 0) throw failure;
|
|
508
|
-
const listDir = mkdtempSync(
|
|
617
|
+
const listDir = mkdtempSync(join4(tmpdir(), "hf-proxy-join-"));
|
|
509
618
|
try {
|
|
510
|
-
const listPath =
|
|
511
|
-
|
|
619
|
+
const listPath = join4(listDir, "pieces.txt");
|
|
620
|
+
writeFileSync2(
|
|
512
621
|
listPath,
|
|
513
622
|
pieces.map((piece) => `file '${piece.replace(/\\/g, "/").replace(/'/g, "'\\''")}'
|
|
514
623
|
`).join("")
|
|
515
624
|
);
|
|
516
625
|
await concat(listPath, tempPath);
|
|
517
626
|
} finally {
|
|
518
|
-
|
|
627
|
+
rmSync2(listDir, { recursive: true, force: true });
|
|
519
628
|
}
|
|
520
|
-
for (const piece of pieces) if (
|
|
629
|
+
for (const piece of pieces) if (existsSync4(piece)) unlinkSync2(piece);
|
|
521
630
|
}
|
|
522
631
|
var KEYFRAME_EVERY_SECOND_ARGS = ["-force_key_frames", "expr:gte(t,n_forced*1)"];
|
|
523
632
|
var H264_ARGS = [
|
|
@@ -737,14 +846,31 @@ function spawnFfmpeg(ffmpegPath, args, stallMs, signal, onProgress) {
|
|
|
737
846
|
async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath, variant, box) {
|
|
738
847
|
await acquireSlot(job);
|
|
739
848
|
try {
|
|
740
|
-
if (
|
|
741
|
-
const cacheDir =
|
|
849
|
+
if (existsSync4(cachePath)) return cachePath;
|
|
850
|
+
const cacheDir = dirname3(cachePath);
|
|
742
851
|
mkdirWithinProject(projectDir, cacheDir);
|
|
743
|
-
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)}`);
|
|
744
870
|
try {
|
|
745
|
-
const chunkPrefix =
|
|
871
|
+
const chunkPrefix = join4(
|
|
746
872
|
cacheDir,
|
|
747
|
-
`${CHUNK_PREFIX}${
|
|
873
|
+
`${CHUNK_PREFIX}${basename3(cachePath, extname3(cachePath))}-`
|
|
748
874
|
);
|
|
749
875
|
await runFfmpeg(
|
|
750
876
|
absoluteSourcePath,
|
|
@@ -756,10 +882,12 @@ async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath,
|
|
|
756
882
|
(progress) => reportProgress(job, progress)
|
|
757
883
|
);
|
|
758
884
|
renameSync(tempPath, cachePath);
|
|
885
|
+
writeCopyIndex(contentIndex, cachePath);
|
|
759
886
|
maintainProxyCache(cacheDir);
|
|
760
887
|
return cachePath;
|
|
761
888
|
} finally {
|
|
762
|
-
if (
|
|
889
|
+
if (existsSync4(tempPath)) unlinkSync2(tempPath);
|
|
890
|
+
release();
|
|
763
891
|
}
|
|
764
892
|
} finally {
|
|
765
893
|
releaseSlot();
|
|
@@ -767,8 +895,8 @@ async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath,
|
|
|
767
895
|
}
|
|
768
896
|
var proxyActivity = 0;
|
|
769
897
|
function proxyActivityMark(projectDir) {
|
|
770
|
-
if (!
|
|
771
|
-
const cacheDir =
|
|
898
|
+
if (!existsSync4(projectDir)) return String(proxyActivity);
|
|
899
|
+
const cacheDir = join4(realpath(projectDir), CACHE_DIR_NAME) + sep;
|
|
772
900
|
for (const cachePath of inFlight.keys()) if (cachePath.startsWith(cacheDir)) return null;
|
|
773
901
|
return String(proxyActivity);
|
|
774
902
|
}
|
|
@@ -776,9 +904,9 @@ async function resolveProxy(projectDir, absoluteSourcePath, variant = "h264", bo
|
|
|
776
904
|
options.signal?.throwIfAborted();
|
|
777
905
|
const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
|
|
778
906
|
const cachePath = getCanonicalProxyCachePath(source, variant, box);
|
|
779
|
-
if (
|
|
907
|
+
if (existsSync4(cachePath)) {
|
|
780
908
|
markCacheEntryUsed(cachePath);
|
|
781
|
-
maintainProxyCache(
|
|
909
|
+
maintainProxyCache(dirname3(cachePath));
|
|
782
910
|
return cachePath;
|
|
783
911
|
}
|
|
784
912
|
const rememberedFailure = failedTranscodes.get(cachePath);
|
|
@@ -835,7 +963,7 @@ async function resolveProxies(projectDir, sources, onSettled = () => {
|
|
|
835
963
|
const first = sources[0];
|
|
836
964
|
if (first) {
|
|
837
965
|
maintainProxyCache(
|
|
838
|
-
|
|
966
|
+
dirname3(getProxyCachePath(projectDir, first.sourcePath, first.variant)),
|
|
839
967
|
Math.min(
|
|
840
968
|
Number(process.env.HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS) || UNTOUCHED_TEMP_IS_DEAD_MS,
|
|
841
969
|
UNTOUCHED_TEMP_IS_DEAD_MS
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
isAutoProxyEnabled,
|
|
5
5
|
proxyEtagSalt,
|
|
6
6
|
resolvePreviewMediaCodecProbeCache
|
|
7
|
-
} from "../chunk-
|
|
8
|
-
import "../chunk-
|
|
7
|
+
} from "../chunk-2MJKVMZU.js";
|
|
8
|
+
import "../chunk-RX6VALW3.js";
|
|
9
9
|
import "../chunk-C2RFW2FX.js";
|
|
10
10
|
import "../chunk-QF7MFJJU.js";
|
|
11
11
|
import "../chunk-SVQFE44C.js";
|
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-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,
|
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",
|