@hyperframes/producer 0.8.197 → 0.8.199
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/README.md +1 -1
- package/dist/distributed.js +232 -150
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +59 -59
- package/dist/index.js +748 -431
- package/dist/public-server.js +495 -268
- package/dist/renderRequest.d.ts +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/services/distributed/chunkWeights.d.ts +16 -0
- package/dist/services/distributed/plan.d.ts +7 -0
- package/dist/services/hdrCompositor.d.ts +5 -5
- package/dist/services/hdrFrameWindows.d.ts +23 -0
- package/dist/services/render/renderEtaGuard.d.ts +35 -0
- package/dist/services/render/stages/captureHdrResources.d.ts +5 -17
- package/dist/services/renderOrchestrator.d.ts +5 -5
- package/package.json +7 -7
package/dist/public-server.js
CHANGED
|
@@ -4,19 +4,20 @@
|
|
|
4
4
|
import {
|
|
5
5
|
existsSync as existsSync19,
|
|
6
6
|
mkdirSync as mkdirSync10,
|
|
7
|
-
statSync as
|
|
7
|
+
statSync as statSync9,
|
|
8
8
|
mkdtempSync as mkdtempSync4,
|
|
9
9
|
writeFileSync as writeFileSync6,
|
|
10
|
-
rmSync as
|
|
10
|
+
rmSync as rmSync12,
|
|
11
11
|
createReadStream as createReadStream3
|
|
12
12
|
} from "node:fs";
|
|
13
|
-
import { resolve as resolve9, dirname as dirname12, join as
|
|
13
|
+
import { resolve as resolve9, dirname as dirname12, join as join30 } from "node:path";
|
|
14
14
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
15
15
|
import { parseArgs } from "node:util";
|
|
16
16
|
import crypto from "node:crypto";
|
|
17
17
|
import { Hono as Hono2 } from "hono";
|
|
18
18
|
import { streamSSE } from "hono/streaming";
|
|
19
19
|
import { serve as serve2 } from "@hono/node-server";
|
|
20
|
+
import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engine";
|
|
20
21
|
|
|
21
22
|
// src/services/renderOrchestrator.ts
|
|
22
23
|
import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
|
|
@@ -25,8 +26,8 @@ import {
|
|
|
25
26
|
mkdirSync as mkdirSync9,
|
|
26
27
|
readFileSync as readFileSync8,
|
|
27
28
|
readdirSync as readdirSync5,
|
|
28
|
-
rmSync as
|
|
29
|
-
statSync as
|
|
29
|
+
rmSync as rmSync11,
|
|
30
|
+
statSync as statSync7,
|
|
30
31
|
writeFileSync as writeFileSync5,
|
|
31
32
|
copyFileSync as copyFileSync4,
|
|
32
33
|
appendFileSync
|
|
@@ -75,7 +76,7 @@ import {
|
|
|
75
76
|
augmentProtocolTimeoutError,
|
|
76
77
|
augmentPageNavigationTimeoutError
|
|
77
78
|
} from "@hyperframes/engine";
|
|
78
|
-
import { join as
|
|
79
|
+
import { join as join26, dirname as dirname10, resolve as resolve7 } from "path";
|
|
79
80
|
import { totalmem as totalmem2 } from "node:os";
|
|
80
81
|
import { randomUUID } from "crypto";
|
|
81
82
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -1909,8 +1910,8 @@ function replanAfterFailure(plan, failure) {
|
|
|
1909
1910
|
}
|
|
1910
1911
|
|
|
1911
1912
|
// src/services/render/stages/captureSegmentedStage.ts
|
|
1912
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as
|
|
1913
|
-
import { join as
|
|
1913
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync6, statSync as statSync3 } from "node:fs";
|
|
1914
|
+
import { join as join10 } from "node:path";
|
|
1914
1915
|
import {
|
|
1915
1916
|
captureFrameToBuffer as captureFrameToBuffer2,
|
|
1916
1917
|
closeCaptureSession as closeCaptureSession2,
|
|
@@ -2294,7 +2295,7 @@ import {
|
|
|
2294
2295
|
} from "@hyperframes/engine";
|
|
2295
2296
|
|
|
2296
2297
|
// src/services/hdrCompositor.ts
|
|
2297
|
-
import { readSync as readSync2
|
|
2298
|
+
import { readSync as readSync2 } from "fs";
|
|
2298
2299
|
import { join as join6 } from "path";
|
|
2299
2300
|
import {
|
|
2300
2301
|
captureAlphaPng,
|
|
@@ -2347,10 +2348,10 @@ function cropRgb48le(src, srcW, srcH, cropX, cropY, cropW, cropH) {
|
|
|
2347
2348
|
}
|
|
2348
2349
|
function closeHdrVideoFrameSource(source, log) {
|
|
2349
2350
|
try {
|
|
2350
|
-
|
|
2351
|
+
source.windows.close();
|
|
2351
2352
|
} catch (err) {
|
|
2352
|
-
log?.warn("Failed to close HDR raw frame
|
|
2353
|
-
|
|
2353
|
+
log?.warn("Failed to close HDR raw frame windows", {
|
|
2354
|
+
dir: source.dir,
|
|
2354
2355
|
error: err instanceof Error ? err.message : String(err)
|
|
2355
2356
|
});
|
|
2356
2357
|
}
|
|
@@ -2360,7 +2361,7 @@ function resolveHdrVideoFrameIndex(time, startTime, fps, frameCount, loop = fals
|
|
|
2360
2361
|
if (frameIndex < 0 || frameCount < 1) return null;
|
|
2361
2362
|
return loop ? frameIndex % frameCount : Math.min(frameIndex, frameCount - 1);
|
|
2362
2363
|
}
|
|
2363
|
-
function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
|
|
2364
|
+
async function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
|
|
2364
2365
|
const frameSource = hdrVideoFrameSources.get(el.id);
|
|
2365
2366
|
const startTime = hdrStartTimes.get(el.id);
|
|
2366
2367
|
if (!frameSource || startTime === void 0 || el.opacity <= 0) {
|
|
@@ -2376,13 +2377,32 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStart
|
|
|
2376
2377
|
if (effectiveIndex === null) return;
|
|
2377
2378
|
const mediaTime = (frameSource.mediaStart ?? 0) + effectiveIndex / fps;
|
|
2378
2379
|
if (isVideoHiddenBeforeStreamStart(frameSource.videoStreamLeadSeconds, mediaTime)) return;
|
|
2379
|
-
|
|
2380
|
+
if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
|
|
2381
|
+
const lease = await frameSource.windows.lease(effectiveIndex);
|
|
2382
|
+
if (!lease) return;
|
|
2383
|
+
try {
|
|
2384
|
+
blitScratchFrameWithoutYielding(
|
|
2385
|
+
canvas,
|
|
2386
|
+
el,
|
|
2387
|
+
frameSource,
|
|
2388
|
+
lease,
|
|
2389
|
+
width,
|
|
2390
|
+
height,
|
|
2391
|
+
log,
|
|
2392
|
+
sourceTransfer,
|
|
2393
|
+
targetTransfer,
|
|
2394
|
+
hdrPerf
|
|
2395
|
+
);
|
|
2396
|
+
} finally {
|
|
2397
|
+
lease.release();
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
function blitScratchFrameWithoutYielding(canvas, el, frameSource, lease, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
|
|
2380
2401
|
try {
|
|
2381
|
-
if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
|
|
2382
2402
|
const bytesRead = timeHdrPhase(
|
|
2383
2403
|
hdrPerf,
|
|
2384
2404
|
"hdrVideoReadDecodeMs",
|
|
2385
|
-
() => readSync2(
|
|
2405
|
+
() => readSync2(lease.fd, frameSource.scratch, 0, frameSource.frameSize, lease.offset)
|
|
2386
2406
|
);
|
|
2387
2407
|
if (bytesRead !== frameSource.frameSize) return;
|
|
2388
2408
|
const hdrRgb = frameSource.scratch;
|
|
@@ -2611,7 +2631,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
|
|
|
2611
2631
|
hdrPerf
|
|
2612
2632
|
);
|
|
2613
2633
|
} else {
|
|
2614
|
-
blitHdrVideoLayer(
|
|
2634
|
+
await blitHdrVideoLayer(
|
|
2615
2635
|
canvas,
|
|
2616
2636
|
layer.element,
|
|
2617
2637
|
time,
|
|
@@ -2655,7 +2675,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
|
|
|
2655
2675
|
startTime,
|
|
2656
2676
|
localTime: localTime.toFixed(3),
|
|
2657
2677
|
hdrFrameNum: frameNum,
|
|
2658
|
-
|
|
2678
|
+
dir: frameSource?.dir ?? null,
|
|
2659
2679
|
frameCount: frameSource?.frameCount ?? null
|
|
2660
2680
|
});
|
|
2661
2681
|
}
|
|
@@ -2751,18 +2771,8 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
|
|
|
2751
2771
|
}
|
|
2752
2772
|
|
|
2753
2773
|
// src/services/render/stages/captureHdrResources.ts
|
|
2754
|
-
import {
|
|
2755
|
-
|
|
2756
|
-
constants as constants2,
|
|
2757
|
-
fstatSync as fstatSync3,
|
|
2758
|
-
mkdirSync as mkdirSync2,
|
|
2759
|
-
mkdtempSync as mkdtempSync2,
|
|
2760
|
-
openSync as openSync3,
|
|
2761
|
-
readFileSync as readFileSync3,
|
|
2762
|
-
rmSync as rmSync4,
|
|
2763
|
-
statfsSync
|
|
2764
|
-
} from "node:fs";
|
|
2765
|
-
import { join as join7 } from "node:path";
|
|
2774
|
+
import { mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, rmSync as rmSync5, statSync as statSync2, statfsSync } from "node:fs";
|
|
2775
|
+
import { join as join8 } from "node:path";
|
|
2766
2776
|
import {
|
|
2767
2777
|
decodePngToRgb48le,
|
|
2768
2778
|
extractMediaMetadata as extractMediaMetadata2,
|
|
@@ -2776,7 +2786,90 @@ import {
|
|
|
2776
2786
|
runFfmpeg
|
|
2777
2787
|
} from "@hyperframes/engine";
|
|
2778
2788
|
import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
2779
|
-
|
|
2789
|
+
|
|
2790
|
+
// src/services/hdrFrameWindows.ts
|
|
2791
|
+
import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
|
|
2792
|
+
import { join as join7 } from "node:path";
|
|
2793
|
+
function createHdrFrameWindows(opts) {
|
|
2794
|
+
const { dir, frameSize, windowFrames, maxResident, extract } = opts;
|
|
2795
|
+
let totalFrames = opts.totalFrames;
|
|
2796
|
+
const windows = /* @__PURE__ */ new Map();
|
|
2797
|
+
const drop = (index, w) => {
|
|
2798
|
+
windows.delete(index);
|
|
2799
|
+
if (w.fd >= 0) closeSync3(w.fd);
|
|
2800
|
+
rmSync4(w.path, { force: true });
|
|
2801
|
+
};
|
|
2802
|
+
const open = (index) => {
|
|
2803
|
+
const known = windows.get(index);
|
|
2804
|
+
if (known) {
|
|
2805
|
+
windows.delete(index);
|
|
2806
|
+
windows.set(index, known);
|
|
2807
|
+
return known;
|
|
2808
|
+
}
|
|
2809
|
+
for (const [i, w2] of windows) {
|
|
2810
|
+
if (windows.size < maxResident) break;
|
|
2811
|
+
if (w2.pins === 0) drop(i, w2);
|
|
2812
|
+
}
|
|
2813
|
+
const firstFrame = index * windowFrames;
|
|
2814
|
+
const w = {
|
|
2815
|
+
firstFrame,
|
|
2816
|
+
path: join7(dir, `window-${index}.rgb48le`),
|
|
2817
|
+
frames: 0,
|
|
2818
|
+
fd: -1,
|
|
2819
|
+
pins: 0,
|
|
2820
|
+
ready: Promise.resolve()
|
|
2821
|
+
};
|
|
2822
|
+
w.ready = extract({
|
|
2823
|
+
firstFrame,
|
|
2824
|
+
frames: Math.min(windowFrames, totalFrames - firstFrame),
|
|
2825
|
+
path: w.path
|
|
2826
|
+
}).then((produced) => {
|
|
2827
|
+
if (produced < 1) return;
|
|
2828
|
+
w.fd = openSync3(w.path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
2829
|
+
w.frames = produced;
|
|
2830
|
+
});
|
|
2831
|
+
windows.set(index, w);
|
|
2832
|
+
return w;
|
|
2833
|
+
};
|
|
2834
|
+
return {
|
|
2835
|
+
get totalFrames() {
|
|
2836
|
+
return totalFrames;
|
|
2837
|
+
},
|
|
2838
|
+
async lease(frameIndex) {
|
|
2839
|
+
for (; ; ) {
|
|
2840
|
+
if (totalFrames < 1) return null;
|
|
2841
|
+
const index = Math.min(Math.max(0, frameIndex), totalFrames - 1);
|
|
2842
|
+
const key = Math.floor(index / windowFrames);
|
|
2843
|
+
const w = open(key);
|
|
2844
|
+
w.pins += 1;
|
|
2845
|
+
try {
|
|
2846
|
+
await w.ready;
|
|
2847
|
+
} catch (error) {
|
|
2848
|
+
w.pins -= 1;
|
|
2849
|
+
windows.delete(key);
|
|
2850
|
+
throw error;
|
|
2851
|
+
}
|
|
2852
|
+
const local = index - w.firstFrame;
|
|
2853
|
+
if (local < w.frames) {
|
|
2854
|
+
return {
|
|
2855
|
+
fd: w.fd,
|
|
2856
|
+
offset: local * frameSize,
|
|
2857
|
+
release: () => {
|
|
2858
|
+
w.pins -= 1;
|
|
2859
|
+
}
|
|
2860
|
+
};
|
|
2861
|
+
}
|
|
2862
|
+
w.pins -= 1;
|
|
2863
|
+
totalFrames = Math.min(totalFrames, w.firstFrame + w.frames);
|
|
2864
|
+
}
|
|
2865
|
+
},
|
|
2866
|
+
close() {
|
|
2867
|
+
for (const [i, w] of [...windows]) drop(i, w);
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// src/services/render/stages/captureHdrResources.ts
|
|
2780
2873
|
function tempDirSafePrefix(id) {
|
|
2781
2874
|
const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
|
|
2782
2875
|
return safe || "video";
|
|
@@ -2821,10 +2914,16 @@ async function probeHdrExtractionDims(args) {
|
|
|
2821
2914
|
const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
|
|
2822
2915
|
for (const el of stacking) {
|
|
2823
2916
|
if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !prep.hdrExtractionDims.has(el.id)) {
|
|
2824
|
-
prep.hdrExtractionDims.set(el.id, {
|
|
2917
|
+
prep.hdrExtractionDims.set(el.id, {
|
|
2918
|
+
width: el.layoutWidth,
|
|
2919
|
+
height: el.layoutHeight
|
|
2920
|
+
});
|
|
2825
2921
|
}
|
|
2826
2922
|
if (el.isHdr && nativeHdrImageIds.has(el.id) && !prep.hdrImageFitInfo.has(el.id)) {
|
|
2827
|
-
prep.hdrImageFitInfo.set(el.id, {
|
|
2923
|
+
prep.hdrImageFitInfo.set(el.id, {
|
|
2924
|
+
fit: el.objectFit,
|
|
2925
|
+
position: el.objectPosition
|
|
2926
|
+
});
|
|
2828
2927
|
}
|
|
2829
2928
|
}
|
|
2830
2929
|
}
|
|
@@ -2843,23 +2942,21 @@ async function probeHdrExtractionDims(args) {
|
|
|
2843
2942
|
const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
|
|
2844
2943
|
for (const el of retryStacking) {
|
|
2845
2944
|
if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
|
|
2846
|
-
prep.hdrExtractionDims.set(el.id, {
|
|
2945
|
+
prep.hdrExtractionDims.set(el.id, {
|
|
2946
|
+
width: el.layoutWidth,
|
|
2947
|
+
height: el.layoutHeight
|
|
2948
|
+
});
|
|
2847
2949
|
if (!prep.hdrImageFitInfo.has(el.id)) {
|
|
2848
|
-
prep.hdrImageFitInfo.set(el.id, {
|
|
2950
|
+
prep.hdrImageFitInfo.set(el.id, {
|
|
2951
|
+
fit: el.objectFit,
|
|
2952
|
+
position: el.objectPosition
|
|
2953
|
+
});
|
|
2849
2954
|
}
|
|
2850
2955
|
break;
|
|
2851
2956
|
}
|
|
2852
2957
|
}
|
|
2853
2958
|
}
|
|
2854
2959
|
}
|
|
2855
|
-
function estimateHdrExtractionBytes(videos, fps) {
|
|
2856
|
-
let total = 0;
|
|
2857
|
-
for (const v of videos) {
|
|
2858
|
-
const frames = Math.ceil(Math.max(0, v.durationSeconds) * fps);
|
|
2859
|
-
total += frames * v.width * v.height * 6;
|
|
2860
|
-
}
|
|
2861
|
-
return total;
|
|
2862
|
-
}
|
|
2863
2960
|
var HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
|
|
2864
2961
|
var HDR_EXTRACTION_CGROUP_BUDGET_FRACTION = 0.5;
|
|
2865
2962
|
var HDR_EXTRACTION_WARN_BYTES = 1e10;
|
|
@@ -2923,24 +3020,22 @@ function resolveHdrExtractionWindow(video, compositionDuration, metadata) {
|
|
|
2923
3020
|
}
|
|
2924
3021
|
return window2;
|
|
2925
3022
|
}
|
|
2926
|
-
function cleanupHdrFrameDirectory(frameDir,
|
|
3023
|
+
function cleanupHdrFrameDirectory(frameDir, log) {
|
|
2927
3024
|
if (process.env.KEEP_TEMP === "1") return;
|
|
2928
3025
|
try {
|
|
2929
|
-
|
|
3026
|
+
rmSync5(frameDir, { recursive: true, force: true });
|
|
2930
3027
|
} catch (err) {
|
|
2931
3028
|
log?.warn("Failed to clean up HDR raw frame directory", {
|
|
2932
3029
|
frameDir,
|
|
2933
|
-
rawPath,
|
|
2934
3030
|
error: err instanceof Error ? err.message : String(err)
|
|
2935
3031
|
});
|
|
2936
3032
|
}
|
|
2937
3033
|
}
|
|
2938
3034
|
function cleanupHdrVideoFrameSource(source, log) {
|
|
2939
3035
|
closeHdrVideoFrameSource(source, log);
|
|
2940
|
-
cleanupHdrFrameDirectory(source.dir,
|
|
3036
|
+
cleanupHdrFrameDirectory(source.dir, log);
|
|
2941
3037
|
}
|
|
2942
|
-
function assertHdrExtractionDiskHeadroom(framesDir,
|
|
2943
|
-
const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
|
|
3038
|
+
function assertHdrExtractionDiskHeadroom(framesDir, estimatedBytes, log) {
|
|
2944
3039
|
const configuredBudget = resolveHdrExtractionBudgetBytes(
|
|
2945
3040
|
process.env[HDR_EXTRACTION_MAX_BYTES_ENV]
|
|
2946
3041
|
);
|
|
@@ -2974,14 +3069,29 @@ function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
|
|
|
2974
3069
|
budgetBytes: resolveHdrExtractionActiveBudgetBytes(configuredBudget, freeBytes)
|
|
2975
3070
|
};
|
|
2976
3071
|
}
|
|
3072
|
+
var DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
|
|
3073
|
+
function hdrWindowFrames(frameSize, windowBytes) {
|
|
3074
|
+
return Math.max(8, Math.floor(windowBytes / frameSize));
|
|
3075
|
+
}
|
|
3076
|
+
async function probeFirstTick(srcPath, mediaStart, fps, runFfmpegImpl, abortSignal) {
|
|
3077
|
+
const args = ["-ss", String(mediaStart), "-i", srcPath, "-frames:v", "1"];
|
|
3078
|
+
const result = await runFfmpegImpl([...args, ...["-vf", "showinfo", "-f", "null", "-"]], {
|
|
3079
|
+
signal: abortSignal
|
|
3080
|
+
});
|
|
3081
|
+
const match = /pts_time:(-?[\d.]+)/.exec(result.stderr);
|
|
3082
|
+
return match ? Math.max(0, Math.floor(Number(match[1]) * fps + 0.5)) : 0;
|
|
3083
|
+
}
|
|
2977
3084
|
async function extractHdrVideoFrames(args) {
|
|
2978
3085
|
const { job, log, framesDir, composition, prep, width, height, abortSignal, hdrDiagnostics } = args;
|
|
3086
|
+
const maxResident = Math.max(2, args.maxResidentWindows ?? 2);
|
|
3087
|
+
const windowBytes = args.windowBytes ?? DEFAULT_HDR_WINDOW_BYTES;
|
|
2979
3088
|
const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
|
|
2980
3089
|
const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
|
|
2981
3090
|
const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
|
|
3091
|
+
const fps = fpsToNumber4(job.config.fps);
|
|
2982
3092
|
const out = /* @__PURE__ */ new Map();
|
|
2983
3093
|
mkdirSync2(framesDir, { recursive: true });
|
|
2984
|
-
|
|
3094
|
+
let scratchBytes = 0;
|
|
2985
3095
|
const extractionWindows = /* @__PURE__ */ new Map();
|
|
2986
3096
|
for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
|
|
2987
3097
|
const video = composition.videos.find((v) => v.id === videoId);
|
|
@@ -3002,16 +3112,13 @@ async function extractHdrVideoFrames(args) {
|
|
|
3002
3112
|
videoStreamLeadSeconds: metadata.videoStreamLeadSeconds
|
|
3003
3113
|
});
|
|
3004
3114
|
prep.hdrVideoStartTimes.set(videoId, window2.compositionStart);
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
height: dims.height
|
|
3009
|
-
});
|
|
3115
|
+
const frameSize = dims.width * dims.height * 6;
|
|
3116
|
+
const frames = window2.finalFrameOnly ? 1 : Math.ceil(window2.durationSeconds * fps);
|
|
3117
|
+
scratchBytes += Math.min(frames, maxResident * hdrWindowFrames(frameSize, windowBytes)) * frameSize;
|
|
3010
3118
|
}
|
|
3011
3119
|
const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
|
|
3012
3120
|
framesDir,
|
|
3013
|
-
|
|
3014
|
-
fpsToNumber4(job.config.fps),
|
|
3121
|
+
scratchBytes,
|
|
3015
3122
|
log
|
|
3016
3123
|
);
|
|
3017
3124
|
const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
|
|
@@ -3023,79 +3130,79 @@ async function extractHdrVideoFrames(args) {
|
|
|
3023
3130
|
if (!video || !planned) continue;
|
|
3024
3131
|
const { window: window2 } = planned;
|
|
3025
3132
|
mkdirSync2(framesDir, { recursive: true });
|
|
3026
|
-
const frameDir = mkdtempSync2(
|
|
3133
|
+
const frameDir = mkdtempSync2(join8(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
|
|
3027
3134
|
createdFrameDirs.add(frameDir);
|
|
3028
3135
|
const dims = prep.hdrExtractionDims.get(videoId) ?? { width, height };
|
|
3029
|
-
const rawPath = join7(frameDir, "frames.rgb48le");
|
|
3030
|
-
const extractionMediaStart = window2.extractionMediaStart ?? window2.mediaStart;
|
|
3031
|
-
const extractionStart = String(extractionMediaStart);
|
|
3032
|
-
const ffmpegArgs = [];
|
|
3033
|
-
if (window2.finalFrameOnly) {
|
|
3034
|
-
ffmpegArgs.push("-i", srcPath, "-ss", extractionStart, "-frames:v", "1");
|
|
3035
|
-
} else {
|
|
3036
|
-
ffmpegArgs.push(
|
|
3037
|
-
"-ss",
|
|
3038
|
-
extractionStart,
|
|
3039
|
-
"-i",
|
|
3040
|
-
srcPath,
|
|
3041
|
-
"-t",
|
|
3042
|
-
String(window2.durationSeconds)
|
|
3043
|
-
);
|
|
3044
|
-
}
|
|
3045
|
-
if (!window2.finalFrameOnly) {
|
|
3046
|
-
ffmpegArgs.push("-r", fpsToFfmpegArg(job.config.fps));
|
|
3047
|
-
}
|
|
3048
|
-
ffmpegArgs.push(
|
|
3049
|
-
"-vf",
|
|
3050
|
-
`scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`,
|
|
3051
|
-
"-pix_fmt",
|
|
3052
|
-
"rgb48le",
|
|
3053
|
-
"-f",
|
|
3054
|
-
"rawvideo",
|
|
3055
|
-
"-y",
|
|
3056
|
-
rawPath
|
|
3057
|
-
);
|
|
3058
|
-
const result = await runFfmpegImpl(ffmpegArgs, { signal: abortSignal });
|
|
3059
|
-
if (!result.success) {
|
|
3060
|
-
hdrDiagnostics.videoExtractionFailures += 1;
|
|
3061
|
-
log.error("HDR frame pre-extraction failed; aborting render", {
|
|
3062
|
-
videoId,
|
|
3063
|
-
srcPath,
|
|
3064
|
-
stderr: result.stderr.slice(-400)
|
|
3065
|
-
});
|
|
3066
|
-
throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
|
|
3067
|
-
error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
|
|
3068
|
-
failureReason: result.failureReason
|
|
3069
|
-
});
|
|
3070
|
-
}
|
|
3071
3136
|
const frameSize = dims.width * dims.height * 6;
|
|
3072
|
-
const
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3137
|
+
const extractionMediaStart = window2.extractionMediaStart ?? window2.mediaStart;
|
|
3138
|
+
const scale = `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`;
|
|
3139
|
+
const firstTick = window2.finalFrameOnly ? 0 : await probeFirstTick(srcPath, extractionMediaStart, fps, runFfmpegImpl, abortSignal);
|
|
3140
|
+
const totalFrames = window2.finalFrameOnly ? 1 : Math.max(1, Math.ceil(window2.durationSeconds * fps - firstTick - 1e-9)) + Number(video.loop);
|
|
3141
|
+
const windowFrames = Math.min(totalFrames, hdrWindowFrames(frameSize, windowBytes));
|
|
3142
|
+
const windows = createHdrFrameWindows({
|
|
3143
|
+
dir: frameDir,
|
|
3144
|
+
frameSize,
|
|
3145
|
+
totalFrames,
|
|
3146
|
+
windowFrames,
|
|
3147
|
+
maxResident,
|
|
3148
|
+
extract: async (spec) => {
|
|
3149
|
+
const ffmpegArgs = window2.finalFrameOnly ? (
|
|
3150
|
+
// Decode before seeking for the one-frame path. Input-side seeking can
|
|
3151
|
+
// return zero frames for valid unindexed/negative-base transports.
|
|
3152
|
+
["-i", srcPath, "-ss", String(extractionMediaStart), "-frames:v", "1", "-vf", scale]
|
|
3153
|
+
) : hdrWindowFfmpegArgs({
|
|
3154
|
+
srcPath,
|
|
3155
|
+
mediaStart: extractionMediaStart,
|
|
3156
|
+
fps,
|
|
3157
|
+
fpsArg: fpsToFfmpegArg(job.config.fps),
|
|
3158
|
+
firstTick,
|
|
3159
|
+
firstFrame: spec.firstFrame,
|
|
3160
|
+
frames: spec.frames,
|
|
3161
|
+
scale
|
|
3162
|
+
});
|
|
3163
|
+
const result = await runFfmpegImpl(
|
|
3164
|
+
[...ffmpegArgs, "-pix_fmt", "rgb48le", "-f", "rawvideo", "-y", spec.path],
|
|
3165
|
+
{ signal: abortSignal }
|
|
3080
3166
|
);
|
|
3167
|
+
if (!result.success) {
|
|
3168
|
+
hdrDiagnostics.videoExtractionFailures += 1;
|
|
3169
|
+
log.error("HDR frame extraction failed; aborting render", {
|
|
3170
|
+
videoId,
|
|
3171
|
+
srcPath,
|
|
3172
|
+
firstFrame: spec.firstFrame,
|
|
3173
|
+
stderr: result.stderr.slice(-400)
|
|
3174
|
+
});
|
|
3175
|
+
throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
|
|
3176
|
+
error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
|
|
3177
|
+
failureReason: result.failureReason
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
return Math.min(Math.floor(statSync2(spec.path).size / frameSize), spec.frames);
|
|
3081
3181
|
}
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
frameCount,
|
|
3090
|
-
scratch: Buffer.allocUnsafe(frameSize),
|
|
3091
|
-
loop: video.loop,
|
|
3092
|
-
mediaStart: extractionMediaStart,
|
|
3093
|
-
videoStreamLeadSeconds: planned.videoStreamLeadSeconds
|
|
3094
|
-
});
|
|
3095
|
-
handedOff = true;
|
|
3096
|
-
} finally {
|
|
3097
|
-
if (!handedOff) closeSync4(fd);
|
|
3182
|
+
});
|
|
3183
|
+
const first = await windows.lease(0);
|
|
3184
|
+
if (!first) {
|
|
3185
|
+
hdrDiagnostics.videoExtractionFailures += 1;
|
|
3186
|
+
throw new Error(
|
|
3187
|
+
`HDR frame extraction produced no frames for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
|
|
3188
|
+
);
|
|
3098
3189
|
}
|
|
3190
|
+
first.release();
|
|
3191
|
+
if (video.loop) (await windows.lease(totalFrames - 1))?.release();
|
|
3192
|
+
out.set(videoId, {
|
|
3193
|
+
dir: frameDir,
|
|
3194
|
+
windows,
|
|
3195
|
+
width: dims.width,
|
|
3196
|
+
height: dims.height,
|
|
3197
|
+
frameSize,
|
|
3198
|
+
get frameCount() {
|
|
3199
|
+
return windows.totalFrames;
|
|
3200
|
+
},
|
|
3201
|
+
scratch: Buffer.allocUnsafe(frameSize),
|
|
3202
|
+
loop: video.loop,
|
|
3203
|
+
mediaStart: extractionMediaStart,
|
|
3204
|
+
videoStreamLeadSeconds: planned.videoStreamLeadSeconds
|
|
3205
|
+
});
|
|
3099
3206
|
}
|
|
3100
3207
|
return { sources: out, estimatedBytes, releaseReservation };
|
|
3101
3208
|
} catch (error) {
|
|
@@ -3104,12 +3211,23 @@ async function extractHdrVideoFrames(args) {
|
|
|
3104
3211
|
createdFrameDirs.delete(source.dir);
|
|
3105
3212
|
}
|
|
3106
3213
|
for (const frameDir of createdFrameDirs) {
|
|
3107
|
-
cleanupHdrFrameDirectory(frameDir,
|
|
3214
|
+
cleanupHdrFrameDirectory(frameDir, log);
|
|
3108
3215
|
}
|
|
3109
3216
|
releaseReservation();
|
|
3110
3217
|
throw error;
|
|
3111
3218
|
}
|
|
3112
3219
|
}
|
|
3220
|
+
function hdrWindowFfmpegArgs(a) {
|
|
3221
|
+
const unit = Math.ceil(a.fps);
|
|
3222
|
+
const seekFrame = a.firstFrame < unit ? 0 : (Math.floor(a.firstFrame / unit) - 1) * unit;
|
|
3223
|
+
const trim = a.firstFrame - seekFrame;
|
|
3224
|
+
const filters = [`fps=${a.fpsArg}:start_time=${(a.firstTick / a.fps).toFixed(6)}`, a.scale];
|
|
3225
|
+
if (trim > 0) filters.push(`trim=start_frame=${trim}`);
|
|
3226
|
+
return [
|
|
3227
|
+
...["-ss", (a.mediaStart + seekFrame / a.fps).toFixed(6), "-i", a.srcPath],
|
|
3228
|
+
...["-vf", filters.join(","), "-frames:v", String(a.frames)]
|
|
3229
|
+
];
|
|
3230
|
+
}
|
|
3113
3231
|
function decodeHdrImageBuffers(args) {
|
|
3114
3232
|
const { log, hdrImageSrcPaths, prep, hdrDiagnostics } = args;
|
|
3115
3233
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -3129,7 +3247,11 @@ function decodeHdrImageBuffers(args) {
|
|
|
3129
3247
|
fit,
|
|
3130
3248
|
fitInfo?.position
|
|
3131
3249
|
);
|
|
3132
|
-
out.set(imageId, {
|
|
3250
|
+
out.set(imageId, {
|
|
3251
|
+
data: resampled,
|
|
3252
|
+
width: layout.width,
|
|
3253
|
+
height: layout.height
|
|
3254
|
+
});
|
|
3133
3255
|
} else {
|
|
3134
3256
|
out.set(imageId, {
|
|
3135
3257
|
data: Buffer.from(decoded.data),
|
|
@@ -3241,7 +3363,7 @@ async function captureSceneIntoBuffer(a) {
|
|
|
3241
3363
|
hdrPerf
|
|
3242
3364
|
);
|
|
3243
3365
|
} else {
|
|
3244
|
-
blitHdrVideoLayer(
|
|
3366
|
+
await blitHdrVideoLayer(
|
|
3245
3367
|
sceneBuf,
|
|
3246
3368
|
el,
|
|
3247
3369
|
time,
|
|
@@ -3443,7 +3565,7 @@ function isTargetLossError(error) {
|
|
|
3443
3565
|
// src/services/render/stages/captureStreamingStage.ts
|
|
3444
3566
|
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
3445
3567
|
import { tmpdir } from "node:os";
|
|
3446
|
-
import { join as
|
|
3568
|
+
import { join as join9 } from "node:path";
|
|
3447
3569
|
import {
|
|
3448
3570
|
DrawElementVerificationError,
|
|
3449
3571
|
captureFrameToBuffer,
|
|
@@ -3605,11 +3727,11 @@ function createDrainFrameGuard(args) {
|
|
|
3605
3727
|
return buf;
|
|
3606
3728
|
}
|
|
3607
3729
|
if (db < verifyMinDb) {
|
|
3608
|
-
const dumpDir = await mkdtemp(
|
|
3730
|
+
const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
|
|
3609
3731
|
if (dumpDir) {
|
|
3610
3732
|
await Promise.all([
|
|
3611
|
-
writeFile(
|
|
3612
|
-
writeFile(
|
|
3733
|
+
writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
|
|
3734
|
+
writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
|
|
3613
3735
|
]).catch(() => {
|
|
3614
3736
|
});
|
|
3615
3737
|
}
|
|
@@ -4058,7 +4180,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4058
4180
|
|
|
4059
4181
|
// src/services/render/stages/captureSegmentedStage.ts
|
|
4060
4182
|
function segmentOutputPath(segmentDir, index) {
|
|
4061
|
-
return
|
|
4183
|
+
return join10(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
|
|
4062
4184
|
}
|
|
4063
4185
|
function segmentEncoderOptions(base, segment) {
|
|
4064
4186
|
return {
|
|
@@ -4134,7 +4256,7 @@ async function captureSegment(ctx, segment) {
|
|
|
4134
4256
|
path: segmentPath,
|
|
4135
4257
|
// Recorded so resume can prove the file on disk is the one that
|
|
4136
4258
|
// finished; absent only in unit tests, whose fake encoder writes none.
|
|
4137
|
-
bytes: existsSync6(segmentPath) ?
|
|
4259
|
+
bytes: existsSync6(segmentPath) ? statSync3(segmentPath).size : 0
|
|
4138
4260
|
});
|
|
4139
4261
|
return encodeResult.durationMs;
|
|
4140
4262
|
} finally {
|
|
@@ -4152,7 +4274,7 @@ var DEFAULT_DEPS = {
|
|
|
4152
4274
|
// is invisible to a test that swaps the engine module after this file has
|
|
4153
4275
|
// been evaluated (bun updates live bindings, not copies).
|
|
4154
4276
|
closeSession: (session) => closeCaptureSession2(session),
|
|
4155
|
-
removeFile: (path) =>
|
|
4277
|
+
removeFile: (path) => rmSync6(path, { force: true })
|
|
4156
4278
|
};
|
|
4157
4279
|
function defaultSessionFactory(input) {
|
|
4158
4280
|
let probeSession = input.probeSession;
|
|
@@ -4319,7 +4441,7 @@ async function runCaptureSegmentedStage(input) {
|
|
|
4319
4441
|
const { log, dedupPerfs, assertNotAborted } = input;
|
|
4320
4442
|
const deps = { ...DEFAULT_DEPS, ...input.deps };
|
|
4321
4443
|
const segments = planSegments(input.totalFrames, input.segmentFrames);
|
|
4322
|
-
const segmentDir = input.segmentDir ??
|
|
4444
|
+
const segmentDir = input.segmentDir ?? join10(input.workDir, "segments");
|
|
4323
4445
|
mkdirSync3(segmentDir, { recursive: true });
|
|
4324
4446
|
const skip = input.completedSegments ?? /* @__PURE__ */ new Set();
|
|
4325
4447
|
const pending = segments.filter((s) => !skip.has(s.index));
|
|
@@ -4390,10 +4512,83 @@ async function runCaptureSegmentedStage(input) {
|
|
|
4390
4512
|
};
|
|
4391
4513
|
}
|
|
4392
4514
|
|
|
4515
|
+
// src/services/render/renderEtaGuard.ts
|
|
4516
|
+
var RENDER_TOO_SLOW = "RENDER_TOO_SLOW";
|
|
4517
|
+
var ETA_MARGIN = 1.2;
|
|
4518
|
+
var MIN_SAMPLE_MS = 6e4;
|
|
4519
|
+
var MIN_SAMPLE_FRAMES = 3;
|
|
4520
|
+
var POLL_MS = 5e3;
|
|
4521
|
+
var RenderTooSlowError = class extends Error {
|
|
4522
|
+
code = RENDER_TOO_SLOW;
|
|
4523
|
+
// The producer server reads these off the error for the wire envelope.
|
|
4524
|
+
// fallow-ignore-next-line unused-class-member
|
|
4525
|
+
owner = "user";
|
|
4526
|
+
// fallow-ignore-next-line unused-class-member
|
|
4527
|
+
retryable = false;
|
|
4528
|
+
publicMetadata;
|
|
4529
|
+
constructor(stage, input) {
|
|
4530
|
+
const { framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs } = input;
|
|
4531
|
+
super(
|
|
4532
|
+
`render too slow: stage ${stage}, ${framesCompleted}/${totalFrames} frames, ${Math.round(msPerFrame)} ms/frame, ETA ${etaMs} ms exceeds maxRenderMs ${maxRenderMs}`
|
|
4533
|
+
);
|
|
4534
|
+
this.name = "RenderTooSlowError";
|
|
4535
|
+
this.publicMetadata = { stage, framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs };
|
|
4536
|
+
}
|
|
4537
|
+
};
|
|
4538
|
+
function checkRenderEta(stage, input) {
|
|
4539
|
+
const { maxRenderMs, elapsedMs, framesCompleted, totalFrames, msPerFrame } = input;
|
|
4540
|
+
if (!maxRenderMs || maxRenderMs <= 0 || !(msPerFrame > 0)) return void 0;
|
|
4541
|
+
const etaMs = Math.round(elapsedMs + (totalFrames - framesCompleted) * msPerFrame);
|
|
4542
|
+
if (etaMs <= maxRenderMs * ETA_MARGIN) return void 0;
|
|
4543
|
+
return new RenderTooSlowError(stage, { ...input, maxRenderMs, etaMs });
|
|
4544
|
+
}
|
|
4545
|
+
function createRateSampler() {
|
|
4546
|
+
let base;
|
|
4547
|
+
let lastFrames = 0;
|
|
4548
|
+
return {
|
|
4549
|
+
observe(done, at) {
|
|
4550
|
+
if (done === 0 || done < lastFrames) base = void 0;
|
|
4551
|
+
lastFrames = done;
|
|
4552
|
+
if (done === 0) return void 0;
|
|
4553
|
+
base ??= { at, frames: done };
|
|
4554
|
+
const sampleMs = at - base.at;
|
|
4555
|
+
const sampleFrames = done - base.frames;
|
|
4556
|
+
if (sampleMs < MIN_SAMPLE_MS || sampleFrames < MIN_SAMPLE_FRAMES) return void 0;
|
|
4557
|
+
return sampleMs / sampleFrames;
|
|
4558
|
+
}
|
|
4559
|
+
};
|
|
4560
|
+
}
|
|
4561
|
+
function startRenderEtaGuard(options) {
|
|
4562
|
+
const { maxRenderMs, job, totalFrames, pipelineStartMs, onTooSlow, now = Date.now } = options;
|
|
4563
|
+
if (!maxRenderMs || maxRenderMs <= 0) return () => {
|
|
4564
|
+
};
|
|
4565
|
+
const sampler = createRateSampler();
|
|
4566
|
+
const stop = () => clearInterval(timer);
|
|
4567
|
+
const timer = setInterval(() => {
|
|
4568
|
+
const done = job.framesRendered ?? 0;
|
|
4569
|
+
if (done >= totalFrames) return stop();
|
|
4570
|
+
const at = now();
|
|
4571
|
+
const msPerFrame = sampler.observe(done, at);
|
|
4572
|
+
if (msPerFrame === void 0) return;
|
|
4573
|
+
const error = checkRenderEta("capture", {
|
|
4574
|
+
maxRenderMs,
|
|
4575
|
+
elapsedMs: at - pipelineStartMs,
|
|
4576
|
+
framesCompleted: done,
|
|
4577
|
+
totalFrames,
|
|
4578
|
+
msPerFrame
|
|
4579
|
+
});
|
|
4580
|
+
if (!error) return;
|
|
4581
|
+
stop();
|
|
4582
|
+
onTooSlow(error);
|
|
4583
|
+
}, options.pollMs ?? POLL_MS);
|
|
4584
|
+
timer.unref?.();
|
|
4585
|
+
return stop;
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4393
4588
|
// src/services/render/segmentManifest.ts
|
|
4394
4589
|
import { createHash as createHash2 } from "node:crypto";
|
|
4395
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as
|
|
4396
|
-
import { join as
|
|
4590
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4591
|
+
import { join as join11 } from "node:path";
|
|
4397
4592
|
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
4398
4593
|
var MANIFEST_FILENAME = "segments.json";
|
|
4399
4594
|
function computeSegmentPlanHash(input) {
|
|
@@ -4419,7 +4614,7 @@ function computeSegmentPlanHash(input) {
|
|
|
4419
4614
|
return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
|
|
4420
4615
|
}
|
|
4421
4616
|
function segmentDirFor(projectRendersDir, planHash) {
|
|
4422
|
-
return
|
|
4617
|
+
return join11(projectRendersDir, ".hf-segments", planHash);
|
|
4423
4618
|
}
|
|
4424
4619
|
function hasNumber(value, key) {
|
|
4425
4620
|
return key in value && typeof Reflect.get(value, key) === "number";
|
|
@@ -4437,7 +4632,7 @@ function isManifest(value) {
|
|
|
4437
4632
|
return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
|
|
4438
4633
|
}
|
|
4439
4634
|
function readSegmentManifest(segmentDir) {
|
|
4440
|
-
const path =
|
|
4635
|
+
const path = join11(segmentDir, MANIFEST_FILENAME);
|
|
4441
4636
|
if (!existsSync7(path)) return null;
|
|
4442
4637
|
try {
|
|
4443
4638
|
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
@@ -4448,9 +4643,9 @@ function readSegmentManifest(segmentDir) {
|
|
|
4448
4643
|
}
|
|
4449
4644
|
function writeSegmentManifest(segmentDir, manifest) {
|
|
4450
4645
|
mkdirSync4(segmentDir, { recursive: true });
|
|
4451
|
-
const tmp =
|
|
4646
|
+
const tmp = join11(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
|
|
4452
4647
|
writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
|
|
4453
|
-
renameWithRetry2(tmp,
|
|
4648
|
+
renameWithRetry2(tmp, join11(segmentDir, MANIFEST_FILENAME));
|
|
4454
4649
|
}
|
|
4455
4650
|
async function probeSegmentFrameCount(path, probe) {
|
|
4456
4651
|
try {
|
|
@@ -4468,7 +4663,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
|
|
|
4468
4663
|
if (manifest.planHash !== expectedHash) return ok;
|
|
4469
4664
|
for (const entry of manifest.completed) {
|
|
4470
4665
|
if (!existsSync7(entry.path)) continue;
|
|
4471
|
-
const size =
|
|
4666
|
+
const size = statSync4(entry.path).size;
|
|
4472
4667
|
if (size <= 0 || size !== entry.bytes) continue;
|
|
4473
4668
|
const frames = await probeFrames(entry.path);
|
|
4474
4669
|
if (frames !== entry.endFrame - entry.startFrame) continue;
|
|
@@ -4562,7 +4757,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
|
4562
4757
|
}
|
|
4563
4758
|
|
|
4564
4759
|
// src/services/render/captureCost.ts
|
|
4565
|
-
import { join as
|
|
4760
|
+
import { join as join12 } from "node:path";
|
|
4566
4761
|
import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
|
|
4567
4762
|
import {
|
|
4568
4763
|
calculateOptimalWorkers,
|
|
@@ -4807,7 +5002,7 @@ async function runCaptureCalibration(input) {
|
|
|
4807
5002
|
});
|
|
4808
5003
|
let calibration;
|
|
4809
5004
|
try {
|
|
4810
|
-
calibration = await runOneCalibration(
|
|
5005
|
+
calibration = await runOneCalibration(join12(workDir, "capture-calibration"), calibrationCfg);
|
|
4811
5006
|
} catch (error) {
|
|
4812
5007
|
const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
|
|
4813
5008
|
if (!shouldFallback) {
|
|
@@ -4840,7 +5035,7 @@ async function runCaptureCalibration(input) {
|
|
|
4840
5035
|
const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
|
|
4841
5036
|
try {
|
|
4842
5037
|
calibration = await runOneCalibration(
|
|
4843
|
-
|
|
5038
|
+
join12(workDir, "capture-calibration-screenshot"),
|
|
4844
5039
|
screenshotCfg
|
|
4845
5040
|
);
|
|
4846
5041
|
} catch (fallbackError) {
|
|
@@ -5272,14 +5467,14 @@ function countAuthoredTimedClips(html) {
|
|
|
5272
5467
|
}
|
|
5273
5468
|
|
|
5274
5469
|
// src/services/render/stages/compileStage.ts
|
|
5275
|
-
import { join as
|
|
5470
|
+
import { join as join15 } from "node:path";
|
|
5276
5471
|
import { suggestMatchingPreset } from "@hyperframes/core";
|
|
5277
5472
|
|
|
5278
5473
|
// src/services/htmlCompiler.ts
|
|
5279
5474
|
import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
|
|
5280
5475
|
import { createHash as createHash5 } from "node:crypto";
|
|
5281
5476
|
import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync6 } from "fs";
|
|
5282
|
-
import { join as
|
|
5477
|
+
import { join as join14, dirname as dirname6, resolve as resolve6, basename as basename4, relative as relative2 } from "path";
|
|
5283
5478
|
import { parseHTML as parseHTML4 } from "linkedom";
|
|
5284
5479
|
import {
|
|
5285
5480
|
COMPOSITION_SOURCE_URL,
|
|
@@ -5506,8 +5701,8 @@ import {
|
|
|
5506
5701
|
|
|
5507
5702
|
// src/services/animatedGifPrep.ts
|
|
5508
5703
|
import { createHash as createHash4 } from "node:crypto";
|
|
5509
|
-
import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as
|
|
5510
|
-
import { dirname as dirname5, isAbsolute as isAbsolute2, join as
|
|
5704
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync7, statSync as statSync5 } from "node:fs";
|
|
5705
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join13, resolve as resolve5 } from "node:path";
|
|
5511
5706
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
5512
5707
|
import { parseAnimatedGifMetadata } from "@hyperframes/core";
|
|
5513
5708
|
import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
|
|
@@ -5556,7 +5751,7 @@ function resolveGifSourcePath(src, options) {
|
|
|
5556
5751
|
}
|
|
5557
5752
|
function isUsableFile(path) {
|
|
5558
5753
|
try {
|
|
5559
|
-
const stat =
|
|
5754
|
+
const stat = statSync5(path);
|
|
5560
5755
|
return stat.isFile() && stat.size > 0;
|
|
5561
5756
|
} catch {
|
|
5562
5757
|
return false;
|
|
@@ -5691,10 +5886,10 @@ async function ensurePreparedWebm(input) {
|
|
|
5691
5886
|
if (!isUsableFile(input.cachePath)) {
|
|
5692
5887
|
renameWithRetry3(tmpPath, input.cachePath);
|
|
5693
5888
|
} else {
|
|
5694
|
-
|
|
5889
|
+
rmSync7(tmpPath, { force: true });
|
|
5695
5890
|
}
|
|
5696
5891
|
} catch (error) {
|
|
5697
|
-
|
|
5892
|
+
rmSync7(tmpPath, { force: true });
|
|
5698
5893
|
throw error;
|
|
5699
5894
|
}
|
|
5700
5895
|
}
|
|
@@ -5750,7 +5945,7 @@ function replaceImageWithVideo(input) {
|
|
|
5750
5945
|
return video;
|
|
5751
5946
|
}
|
|
5752
5947
|
async function prepareAnimatedGifInputs(html, options) {
|
|
5753
|
-
const outputDir = options.outputDir ??
|
|
5948
|
+
const outputDir = options.outputDir ?? join13(options.downloadDir, PREPARED_GIF_SUBDIR);
|
|
5754
5949
|
const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
|
|
5755
5950
|
const cacheDir = options.cacheDir ?? outputDir;
|
|
5756
5951
|
const { document: document2 } = parseHTML3(html);
|
|
@@ -5772,8 +5967,8 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
5772
5967
|
const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
|
|
5773
5968
|
const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
|
|
5774
5969
|
const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
|
|
5775
|
-
const cachePath =
|
|
5776
|
-
const outputPath =
|
|
5970
|
+
const cachePath = join13(cacheDir, filename);
|
|
5971
|
+
const outputPath = join13(outputDir, filename);
|
|
5777
5972
|
const outputSrc = `${outputSrcPrefix}/${filename}`;
|
|
5778
5973
|
await ensurePreparedWebm({
|
|
5779
5974
|
sourcePath,
|
|
@@ -6187,7 +6382,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6187
6382
|
}
|
|
6188
6383
|
} else {
|
|
6189
6384
|
filePath = decodedUrlPath(src);
|
|
6190
|
-
if (!filePath.startsWith("/")) filePath =
|
|
6385
|
+
if (!filePath.startsWith("/")) filePath = join14(baseDir, filePath);
|
|
6191
6386
|
filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
|
|
6192
6387
|
}
|
|
6193
6388
|
if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
|
|
@@ -6807,7 +7002,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
|
|
|
6807
7002
|
return downloadAndRewriteUrls(
|
|
6808
7003
|
urlSet,
|
|
6809
7004
|
html,
|
|
6810
|
-
|
|
7005
|
+
join14(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
6811
7006
|
"Remote media download failed",
|
|
6812
7007
|
"Localized remote media source(s)"
|
|
6813
7008
|
);
|
|
@@ -6822,7 +7017,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
|
|
|
6822
7017
|
return downloadAndRewriteUrls(
|
|
6823
7018
|
urlSet,
|
|
6824
7019
|
html,
|
|
6825
|
-
|
|
7020
|
+
join14(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
6826
7021
|
"Remote image download failed",
|
|
6827
7022
|
"Localized remote image source(s)"
|
|
6828
7023
|
);
|
|
@@ -6838,7 +7033,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
|
|
|
6838
7033
|
return downloadAndRewriteUrls(
|
|
6839
7034
|
urlSet,
|
|
6840
7035
|
html,
|
|
6841
|
-
|
|
7036
|
+
join14(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
6842
7037
|
"Remote background-image download failed",
|
|
6843
7038
|
"Localized remote background-image(s)",
|
|
6844
7039
|
// Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
|
|
@@ -6973,7 +7168,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
|
|
|
6973
7168
|
return downloadAndRewriteUrls(
|
|
6974
7169
|
urlSet,
|
|
6975
7170
|
processed,
|
|
6976
|
-
|
|
7171
|
+
join14(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
6977
7172
|
"Remote font download failed",
|
|
6978
7173
|
"Localized remote font face(s)",
|
|
6979
7174
|
(h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
|
|
@@ -7640,7 +7835,7 @@ async function runCompileStage(input) {
|
|
|
7640
7835
|
abortSignal
|
|
7641
7836
|
} = input;
|
|
7642
7837
|
const compileStart = Date.now();
|
|
7643
|
-
const compiled = await compileForRender(projectDir, htmlPath,
|
|
7838
|
+
const compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"), {
|
|
7644
7839
|
log,
|
|
7645
7840
|
failClosedFontFetch: failClosedFontFetch === true,
|
|
7646
7841
|
allowSystemFontCapture,
|
|
@@ -7648,7 +7843,7 @@ async function runCompileStage(input) {
|
|
|
7648
7843
|
variables: input.variables,
|
|
7649
7844
|
urlByAbsentLocalPath: input.urlByAbsentLocalPath,
|
|
7650
7845
|
pageMotionBlur: input.pageMotionBlur,
|
|
7651
|
-
animatedGifCacheDir: cfg.extractCacheDir ?
|
|
7846
|
+
animatedGifCacheDir: cfg.extractCacheDir ? join15(cfg.extractCacheDir, "animated-gif") : void 0,
|
|
7652
7847
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
|
|
7653
7848
|
});
|
|
7654
7849
|
assertNotAborted();
|
|
@@ -7711,7 +7906,7 @@ async function runCompileStage(input) {
|
|
|
7711
7906
|
};
|
|
7712
7907
|
await preflightCompositionAssetMediaTypes({
|
|
7713
7908
|
projectDir,
|
|
7714
|
-
compiledDir:
|
|
7909
|
+
compiledDir: join15(workDir, "compiled"),
|
|
7715
7910
|
composition,
|
|
7716
7911
|
signal: abortSignal
|
|
7717
7912
|
});
|
|
@@ -7756,7 +7951,7 @@ async function runCompileStage(input) {
|
|
|
7756
7951
|
}
|
|
7757
7952
|
|
|
7758
7953
|
// src/services/render/stages/probeStage.ts
|
|
7759
|
-
import { join as
|
|
7954
|
+
import { join as join16 } from "node:path";
|
|
7760
7955
|
import { parseHTML as parseHTML5 } from "linkedom";
|
|
7761
7956
|
import {
|
|
7762
7957
|
closeCaptureSession as closeCaptureSession4,
|
|
@@ -7948,7 +8143,7 @@ async function runProbeStage(input) {
|
|
|
7948
8143
|
});
|
|
7949
8144
|
fileServer = await createFileServer({
|
|
7950
8145
|
projectDir,
|
|
7951
|
-
compiledDir:
|
|
8146
|
+
compiledDir: join16(workDir, "compiled"),
|
|
7952
8147
|
port: 0,
|
|
7953
8148
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
7954
8149
|
fps: job.config.fps
|
|
@@ -7976,7 +8171,7 @@ async function runProbeStage(input) {
|
|
|
7976
8171
|
log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
|
|
7977
8172
|
probeSession = await createCaptureSession4(
|
|
7978
8173
|
fileServer.url,
|
|
7979
|
-
|
|
8174
|
+
join16(workDir, "probe"),
|
|
7980
8175
|
captureOpts,
|
|
7981
8176
|
null,
|
|
7982
8177
|
probeCfg
|
|
@@ -8061,7 +8256,7 @@ async function runProbeStage(input) {
|
|
|
8061
8256
|
});
|
|
8062
8257
|
probeSession = await createCaptureSession4(
|
|
8063
8258
|
fileServer.url,
|
|
8064
|
-
|
|
8259
|
+
join16(workDir, "probe-screenshot"),
|
|
8065
8260
|
captureOpts,
|
|
8066
8261
|
null,
|
|
8067
8262
|
{ ...probeCfg, forceScreenshot: true }
|
|
@@ -8097,7 +8292,7 @@ async function runProbeStage(input) {
|
|
|
8097
8292
|
compiled,
|
|
8098
8293
|
resolutions,
|
|
8099
8294
|
projectDir,
|
|
8100
|
-
|
|
8295
|
+
join16(workDir, "downloads")
|
|
8101
8296
|
);
|
|
8102
8297
|
assertNotAborted();
|
|
8103
8298
|
composition.videos = compiled.videos;
|
|
@@ -8288,7 +8483,7 @@ async function runProbeStage(input) {
|
|
|
8288
8483
|
}
|
|
8289
8484
|
await preflightCompositionAssetMediaTypes({
|
|
8290
8485
|
projectDir,
|
|
8291
|
-
compiledDir:
|
|
8486
|
+
compiledDir: join16(workDir, "compiled"),
|
|
8292
8487
|
composition,
|
|
8293
8488
|
signal: abortSignal
|
|
8294
8489
|
});
|
|
@@ -8422,7 +8617,7 @@ function validateRenderDuration(input) {
|
|
|
8422
8617
|
|
|
8423
8618
|
// src/services/render/stages/extractVideosStage.ts
|
|
8424
8619
|
import { existsSync as existsSync11 } from "node:fs";
|
|
8425
|
-
import { join as
|
|
8620
|
+
import { join as join17 } from "node:path";
|
|
8426
8621
|
import {
|
|
8427
8622
|
classifyVideoExtractionError,
|
|
8428
8623
|
createFrameLookupTable,
|
|
@@ -8737,14 +8932,14 @@ async function runExtractVideosStage(input) {
|
|
|
8737
8932
|
// because short boundary counts can differ by one frame.
|
|
8738
8933
|
{
|
|
8739
8934
|
fps: job.config.fps,
|
|
8740
|
-
outputDir:
|
|
8935
|
+
outputDir: join17(compiledDir, "__hyperframes_video_frames"),
|
|
8741
8936
|
format: job.config.videoFrameFormat ?? "auto",
|
|
8742
8937
|
toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
|
|
8743
8938
|
timelineEnd: composition.duration,
|
|
8744
8939
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
8745
8940
|
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
|
8746
8941
|
deferRangeExtraction,
|
|
8747
|
-
contentKeyedDirs: [
|
|
8942
|
+
contentKeyedDirs: [join17(compiledDir, REMOTE_MEDIA_SUBDIR)],
|
|
8748
8943
|
urlByAbsentLocalPath
|
|
8749
8944
|
},
|
|
8750
8945
|
abortSignal,
|
|
@@ -8811,8 +9006,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
8811
9006
|
}
|
|
8812
9007
|
|
|
8813
9008
|
// src/services/render/stages/audioStage.ts
|
|
8814
|
-
import { join as
|
|
8815
|
-
import { rmSync as
|
|
9009
|
+
import { join as join19 } from "node:path";
|
|
9010
|
+
import { rmSync as rmSync9 } from "node:fs";
|
|
8816
9011
|
import {
|
|
8817
9012
|
MIXED_AUDIO_FILENAME,
|
|
8818
9013
|
audioFailureDiagnostics,
|
|
@@ -8821,8 +9016,8 @@ import {
|
|
|
8821
9016
|
|
|
8822
9017
|
// src/services/render/audioPadTrim.ts
|
|
8823
9018
|
import { spawn } from "node:child_process";
|
|
8824
|
-
import { mkdtempSync as mkdtempSync3, rmSync as
|
|
8825
|
-
import { dirname as dirname7, join as
|
|
9019
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
|
|
9020
|
+
import { dirname as dirname7, join as join18 } from "node:path";
|
|
8826
9021
|
import {
|
|
8827
9022
|
DEFAULT_CONFIG,
|
|
8828
9023
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
@@ -9025,7 +9220,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
9025
9220
|
error: `audioPadTrim: failed to materialize ${plan.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
9026
9221
|
};
|
|
9027
9222
|
} finally {
|
|
9028
|
-
for (const path of plan.cleanupPaths)
|
|
9223
|
+
for (const path of plan.cleanupPaths) rmSync8(path, { force: true });
|
|
9029
9224
|
}
|
|
9030
9225
|
let loweredDb = 0;
|
|
9031
9226
|
if (probeTruePeak) {
|
|
@@ -9071,8 +9266,8 @@ async function constrainAacTruePeak(input) {
|
|
|
9071
9266
|
async function enforceAacTruePeak(input) {
|
|
9072
9267
|
let scratchDir;
|
|
9073
9268
|
try {
|
|
9074
|
-
scratchDir = mkdtempSync3(
|
|
9075
|
-
const correctedPath =
|
|
9269
|
+
scratchDir = mkdtempSync3(join18(dirname7(input.audioPath), ".true-peak-"));
|
|
9270
|
+
const correctedPath = join18(scratchDir, "audio.m4a");
|
|
9076
9271
|
const measurements = [];
|
|
9077
9272
|
let attenuationDb = 0;
|
|
9078
9273
|
let measuredPath = input.audioPath;
|
|
@@ -9123,7 +9318,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9123
9318
|
)}`
|
|
9124
9319
|
};
|
|
9125
9320
|
} finally {
|
|
9126
|
-
if (scratchDir)
|
|
9321
|
+
if (scratchDir) rmSync8(scratchDir, { recursive: true, force: true });
|
|
9127
9322
|
}
|
|
9128
9323
|
}
|
|
9129
9324
|
function failResult(outputPath, target, source, error) {
|
|
@@ -9270,7 +9465,7 @@ function assertAudioNotInterrupted(failures) {
|
|
|
9270
9465
|
}
|
|
9271
9466
|
async function runAudioStage(input) {
|
|
9272
9467
|
const stageStart = Date.now();
|
|
9273
|
-
const pcmSourcePath =
|
|
9468
|
+
const pcmSourcePath = join19(input.workDir, "audio-source.wav");
|
|
9274
9469
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
9275
9470
|
try {
|
|
9276
9471
|
const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
|
|
@@ -9290,7 +9485,7 @@ async function runAudioStage(input) {
|
|
|
9290
9485
|
}
|
|
9291
9486
|
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
9292
9487
|
} finally {
|
|
9293
|
-
|
|
9488
|
+
rmSync9(pcmSourcePath, { force: true });
|
|
9294
9489
|
}
|
|
9295
9490
|
}
|
|
9296
9491
|
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
@@ -9308,7 +9503,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9308
9503
|
assertNotAborted,
|
|
9309
9504
|
log
|
|
9310
9505
|
} = input;
|
|
9311
|
-
const audioOutputPath =
|
|
9506
|
+
const audioOutputPath = join19(workDir, MIXED_AUDIO_FILENAME);
|
|
9312
9507
|
let hasAudio = false;
|
|
9313
9508
|
let audioError;
|
|
9314
9509
|
let audioFailures;
|
|
@@ -9319,7 +9514,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9319
9514
|
audioResult = await processCompositionAudio(
|
|
9320
9515
|
audios,
|
|
9321
9516
|
projectDir,
|
|
9322
|
-
|
|
9517
|
+
join19(workDir, "audio-work"),
|
|
9323
9518
|
audioOutputPath,
|
|
9324
9519
|
duration,
|
|
9325
9520
|
abortSignal,
|
|
@@ -9388,7 +9583,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9388
9583
|
|
|
9389
9584
|
// src/services/render/stages/captureStage.ts
|
|
9390
9585
|
import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
|
|
9391
|
-
import { join as
|
|
9586
|
+
import { join as join20 } from "node:path";
|
|
9392
9587
|
import {
|
|
9393
9588
|
classifyCaptureFailure as classifyCaptureFailure2,
|
|
9394
9589
|
frameFileExtension,
|
|
@@ -9461,13 +9656,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
|
|
|
9461
9656
|
if (!existsSync12(workDir)) return total;
|
|
9462
9657
|
for (const attempt of readdirSync3(workDir)) {
|
|
9463
9658
|
if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
|
|
9464
|
-
total += sampleDirectoryBytes(
|
|
9659
|
+
total += sampleDirectoryBytes(join20(workDir, attempt));
|
|
9465
9660
|
continue;
|
|
9466
9661
|
}
|
|
9467
9662
|
if (!/^capture-attempt-\d+$/.test(attempt)) continue;
|
|
9468
|
-
const attemptDir =
|
|
9663
|
+
const attemptDir = join20(workDir, attempt);
|
|
9469
9664
|
for (const name of readdirSync3(attemptDir)) {
|
|
9470
|
-
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(
|
|
9665
|
+
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join20(attemptDir, name));
|
|
9471
9666
|
}
|
|
9472
9667
|
}
|
|
9473
9668
|
return total;
|
|
@@ -9754,7 +9949,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
|
|
|
9754
9949
|
|
|
9755
9950
|
// src/services/render/stages/captureHdrStage.ts
|
|
9756
9951
|
import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
|
|
9757
|
-
import { join as
|
|
9952
|
+
import { join as join24 } from "node:path";
|
|
9758
9953
|
import {
|
|
9759
9954
|
closeCaptureSession as closeCaptureSession7,
|
|
9760
9955
|
cloneCaptureWarnings as cloneCaptureWarnings2,
|
|
@@ -9826,7 +10021,7 @@ function createHdrImageTransferCache(options = {}) {
|
|
|
9826
10021
|
}
|
|
9827
10022
|
|
|
9828
10023
|
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
9829
|
-
import { join as
|
|
10024
|
+
import { join as join21 } from "node:path";
|
|
9830
10025
|
import {
|
|
9831
10026
|
TRANSITIONS,
|
|
9832
10027
|
crossfade
|
|
@@ -9951,7 +10146,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
9951
10146
|
);
|
|
9952
10147
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
9953
10148
|
writeFileExclusiveSync(
|
|
9954
|
-
|
|
10149
|
+
join21(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
9955
10150
|
normalCanvas
|
|
9956
10151
|
);
|
|
9957
10152
|
}
|
|
@@ -9983,7 +10178,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
9983
10178
|
}
|
|
9984
10179
|
|
|
9985
10180
|
// src/services/render/stages/captureHdrHybridLoop.ts
|
|
9986
|
-
import { join as
|
|
10181
|
+
import { join as join23 } from "node:path";
|
|
9987
10182
|
import {
|
|
9988
10183
|
TRANSITIONS as TRANSITIONS2,
|
|
9989
10184
|
closeCaptureSession as closeCaptureSession6,
|
|
@@ -9997,7 +10192,7 @@ import {
|
|
|
9997
10192
|
// src/services/shaderTransitionWorkerPool.ts
|
|
9998
10193
|
import { Worker } from "node:worker_threads";
|
|
9999
10194
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
10000
|
-
import { dirname as dirname8, join as
|
|
10195
|
+
import { dirname as dirname8, join as join22 } from "node:path";
|
|
10001
10196
|
import { createRequire } from "node:module";
|
|
10002
10197
|
import { existsSync as existsSync13 } from "node:fs";
|
|
10003
10198
|
import { cpus as cpus2 } from "node:os";
|
|
@@ -10011,9 +10206,9 @@ function resolveWorkerEntry(explicit) {
|
|
|
10011
10206
|
return { path: override, isTs };
|
|
10012
10207
|
}
|
|
10013
10208
|
const moduleDir = dirname8(fileURLToPath3(import.meta.url));
|
|
10014
|
-
const jsPath =
|
|
10209
|
+
const jsPath = join22(moduleDir, "shaderTransitionWorker.js");
|
|
10015
10210
|
if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
|
|
10016
|
-
const tsPath =
|
|
10211
|
+
const tsPath = join22(moduleDir, "shaderTransitionWorker.ts");
|
|
10017
10212
|
return { path: tsPath, isTs: true };
|
|
10018
10213
|
}
|
|
10019
10214
|
function buildExecArgv(entryIsTs) {
|
|
@@ -10381,7 +10576,7 @@ async function runHybridLayeredFrameLoop(input) {
|
|
|
10381
10576
|
);
|
|
10382
10577
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
10383
10578
|
writeFileExclusiveSync(
|
|
10384
|
-
|
|
10579
|
+
join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
10385
10580
|
canvas
|
|
10386
10581
|
);
|
|
10387
10582
|
}
|
|
@@ -10550,7 +10745,8 @@ async function runCaptureHdrStage(input) {
|
|
|
10550
10745
|
width,
|
|
10551
10746
|
height,
|
|
10552
10747
|
abortSignal,
|
|
10553
|
-
hdrDiagnostics
|
|
10748
|
+
hdrDiagnostics,
|
|
10749
|
+
maxResidentWindows: Math.max(1, workerCount) + 1
|
|
10554
10750
|
});
|
|
10555
10751
|
releaseHdrExtractionReservation = extracted.releaseReservation;
|
|
10556
10752
|
for (const [id, source] of extracted.sources) hdrVideoFrameSources.set(id, source);
|
|
@@ -10568,7 +10764,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10568
10764
|
if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
|
|
10569
10765
|
}
|
|
10570
10766
|
const debugDumpEnabled = process.env.KEEP_TEMP === "1";
|
|
10571
|
-
const debugDumpDir = debugDumpEnabled ?
|
|
10767
|
+
const debugDumpDir = debugDumpEnabled ? join24(framesDir, "debug-composite") : null;
|
|
10572
10768
|
if (debugDumpDir && !existsSync14(debugDumpDir)) {
|
|
10573
10769
|
mkdirSync7(debugDumpDir, { recursive: true });
|
|
10574
10770
|
}
|
|
@@ -10728,12 +10924,12 @@ import {
|
|
|
10728
10924
|
mkdirSync as mkdirSync8,
|
|
10729
10925
|
readdirSync as readdirSync4,
|
|
10730
10926
|
readFileSync as readFileSync7,
|
|
10731
|
-
rmSync as
|
|
10732
|
-
statSync as
|
|
10927
|
+
rmSync as rmSync10,
|
|
10928
|
+
statSync as statSync6,
|
|
10733
10929
|
writeFileSync as writeFileSync4
|
|
10734
10930
|
} from "node:fs";
|
|
10735
10931
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
10736
|
-
import { dirname as dirname9, join as
|
|
10932
|
+
import { dirname as dirname9, join as join25 } from "node:path";
|
|
10737
10933
|
import {
|
|
10738
10934
|
encodeFramesChunkedConcat,
|
|
10739
10935
|
encodeFramesFromDir,
|
|
@@ -10858,7 +11054,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
10858
11054
|
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
10859
11055
|
if (failure) return failure;
|
|
10860
11056
|
}
|
|
10861
|
-
const fileSize = existsSync15(outputPath) ?
|
|
11057
|
+
const fileSize = existsSync15(outputPath) ? statSync6(outputPath).size : 0;
|
|
10862
11058
|
return {
|
|
10863
11059
|
success: true,
|
|
10864
11060
|
outputPath,
|
|
@@ -10867,7 +11063,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
10867
11063
|
fileSize
|
|
10868
11064
|
};
|
|
10869
11065
|
} finally {
|
|
10870
|
-
|
|
11066
|
+
rmSync10(input.palettePath, { force: true });
|
|
10871
11067
|
}
|
|
10872
11068
|
}
|
|
10873
11069
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -10942,7 +11138,7 @@ function encodeWhileCapturing(input, totalFrames) {
|
|
|
10942
11138
|
await new Promise((resolve10) => wake = resolve10);
|
|
10943
11139
|
}
|
|
10944
11140
|
if (signal.aborted) return;
|
|
10945
|
-
const mergedPath =
|
|
11141
|
+
const mergedPath = join25(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
10946
11142
|
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
10947
11143
|
landed.delete(next);
|
|
10948
11144
|
if (!await stream.writeFrame(frame)) {
|
|
@@ -11025,11 +11221,11 @@ async function runEncodeStage(input) {
|
|
|
11025
11221
|
);
|
|
11026
11222
|
}
|
|
11027
11223
|
captured.forEach((name, i) => {
|
|
11028
|
-
const dst =
|
|
11029
|
-
copyFileSync3(
|
|
11224
|
+
const dst = join25(outputPath, formatExportFrameName(i, "png"));
|
|
11225
|
+
copyFileSync3(join25(framesDir, name), dst);
|
|
11030
11226
|
});
|
|
11031
11227
|
if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
|
|
11032
|
-
copyFileSync3(audioOutputPath,
|
|
11228
|
+
copyFileSync3(audioOutputPath, join25(outputPath, MIXED_AUDIO_FILENAME2));
|
|
11033
11229
|
log.info(
|
|
11034
11230
|
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
11035
11231
|
);
|
|
@@ -11049,7 +11245,7 @@ async function runEncodeStage(input) {
|
|
|
11049
11245
|
const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
|
|
11050
11246
|
fps: job.config.fps,
|
|
11051
11247
|
loop,
|
|
11052
|
-
palettePath:
|
|
11248
|
+
palettePath: join25(dirname9(videoOnlyPath), "gif-palette.png"),
|
|
11053
11249
|
preserveAlpha: needsAlpha,
|
|
11054
11250
|
signal: abortSignal,
|
|
11055
11251
|
timeout: engineCfg.ffmpegEncodeTimeout
|
|
@@ -11262,9 +11458,9 @@ function sampleDirectoryBytes(dir) {
|
|
|
11262
11458
|
continue;
|
|
11263
11459
|
}
|
|
11264
11460
|
for (const name of entries) {
|
|
11265
|
-
const full =
|
|
11461
|
+
const full = join26(current, name);
|
|
11266
11462
|
try {
|
|
11267
|
-
const st =
|
|
11463
|
+
const st = statSync7(full);
|
|
11268
11464
|
if (st.isDirectory()) {
|
|
11269
11465
|
stack.push(full);
|
|
11270
11466
|
} else if (st.isFile()) {
|
|
@@ -11411,8 +11607,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11411
11607
|
const ranges = [];
|
|
11412
11608
|
let rangeStart = null;
|
|
11413
11609
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
11414
|
-
const framePath =
|
|
11415
|
-
const missing = !existsSync16(framePath) ||
|
|
11610
|
+
const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
11611
|
+
const missing = !existsSync16(framePath) || statSync7(framePath).size <= 8;
|
|
11416
11612
|
if (missing && rangeStart === null) {
|
|
11417
11613
|
rangeStart = frameIndex;
|
|
11418
11614
|
} else if (!missing && rangeStart !== null) {
|
|
@@ -11434,7 +11630,7 @@ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, ran
|
|
|
11434
11630
|
workerId,
|
|
11435
11631
|
startFrame: rangeStart + range.startFrame,
|
|
11436
11632
|
endFrame: rangeStart + range.endFrame,
|
|
11437
|
-
outputDir:
|
|
11633
|
+
outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
|
|
11438
11634
|
outputFrameOffset: rangeStart
|
|
11439
11635
|
}));
|
|
11440
11636
|
batches.push(batch);
|
|
@@ -11462,8 +11658,8 @@ function getNextRetryWorkerCount(currentWorkers) {
|
|
|
11462
11658
|
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
11463
11659
|
}
|
|
11464
11660
|
function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
|
|
11465
|
-
if (platform2 === "win32" || platform2 === "darwin") return
|
|
11466
|
-
return
|
|
11661
|
+
if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
|
|
11662
|
+
return join26(dirname10(outputPath), `work-${jobId}-`);
|
|
11467
11663
|
}
|
|
11468
11664
|
var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
|
|
11469
11665
|
function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
|
|
@@ -11495,7 +11691,7 @@ The composition is too large for the available memory. To reduce memory pressure
|
|
|
11495
11691
|
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
11496
11692
|
let captured = 0;
|
|
11497
11693
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
11498
|
-
const framePath =
|
|
11694
|
+
const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
11499
11695
|
if (existsSync16(framePath)) captured++;
|
|
11500
11696
|
}
|
|
11501
11697
|
return captured;
|
|
@@ -11521,7 +11717,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11521
11717
|
reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
|
|
11522
11718
|
});
|
|
11523
11719
|
pendingTransientRetry = false;
|
|
11524
|
-
const attemptWorkDir =
|
|
11720
|
+
const attemptWorkDir = join26(options.workDir, `capture-attempt-${attempt}`);
|
|
11525
11721
|
const batches = missingRanges ? buildMissingFrameRetryBatches(
|
|
11526
11722
|
missingRanges,
|
|
11527
11723
|
currentWorkers,
|
|
@@ -11978,7 +12174,7 @@ function resolveDeShortBand(args) {
|
|
|
11978
12174
|
return args.bandOpen ? "applied" : "skipped_elements";
|
|
11979
12175
|
}
|
|
11980
12176
|
function shouldPreferSingleWorkerDrawElement(args) {
|
|
11981
|
-
return args.workerCount > 1 &&
|
|
12177
|
+
return args.workerCount > 1 && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
|
|
11982
12178
|
}
|
|
11983
12179
|
function isProbeDeGated(session) {
|
|
11984
12180
|
return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
|
|
@@ -12114,17 +12310,17 @@ function deVerifyFallbackTelemetry(err) {
|
|
|
12114
12310
|
}
|
|
12115
12311
|
function resolveRenderDebugDir() {
|
|
12116
12312
|
const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve7(process.env.PRODUCER_RENDERS_DIR, "..") : resolve7(dirname10(fileURLToPath4(import.meta.url)), "../..");
|
|
12117
|
-
return
|
|
12313
|
+
return join26(producerRoot, ".debug");
|
|
12118
12314
|
}
|
|
12119
12315
|
async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
|
|
12120
12316
|
validateHlsRenderConfig(job.config);
|
|
12121
12317
|
const debugDir = resolveRenderDebugDir();
|
|
12122
12318
|
const outputDir = dirname10(outputPath);
|
|
12123
12319
|
if (!existsSync16(outputDir)) mkdirSync9(outputDir, { recursive: true });
|
|
12124
|
-
const workDir = job.config.debug ?
|
|
12320
|
+
const workDir = job.config.debug ? join26(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
|
|
12125
12321
|
const pipelineStart = Date.now();
|
|
12126
12322
|
const baseLog = job.config.logger ?? defaultLogger;
|
|
12127
|
-
const logPath = job.config.debug ?
|
|
12323
|
+
const logPath = job.config.debug ? join26(workDir, "render.log") : null;
|
|
12128
12324
|
const execution = new RenderExecutionContext({
|
|
12129
12325
|
request: { renderJobId: job.id, projectDir, outputPath },
|
|
12130
12326
|
logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
|
|
@@ -12138,7 +12334,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
12138
12334
|
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
12139
12335
|
return;
|
|
12140
12336
|
}
|
|
12141
|
-
|
|
12337
|
+
rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
12142
12338
|
});
|
|
12143
12339
|
try {
|
|
12144
12340
|
await executeRenderPipeline({
|
|
@@ -12169,7 +12365,9 @@ async function executeRenderPipeline(input) {
|
|
|
12169
12365
|
const log = execution.logger;
|
|
12170
12366
|
const eventPublisher = execution.events;
|
|
12171
12367
|
const onProgress = execution.onProgress;
|
|
12172
|
-
const
|
|
12368
|
+
const tooSlow = new AbortController();
|
|
12369
|
+
let tooSlowError;
|
|
12370
|
+
const executionSignal = job.config.maxRenderMs ? AbortSignal.any([tooSlow.signal, ...execution.signal ? [execution.signal] : []]) : execution.signal;
|
|
12173
12371
|
let fileServer = null;
|
|
12174
12372
|
let probeSession = null;
|
|
12175
12373
|
let lastBrowserConsole = [];
|
|
@@ -12182,7 +12380,7 @@ async function executeRenderPipeline(input) {
|
|
|
12182
12380
|
imageDecodeFailures: 0
|
|
12183
12381
|
};
|
|
12184
12382
|
let hdrPerf;
|
|
12185
|
-
const perfOutputPath =
|
|
12383
|
+
const perfOutputPath = join26(workDir, "perf-summary.json");
|
|
12186
12384
|
const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
|
|
12187
12385
|
const observability = new RenderObservabilityRecorder({
|
|
12188
12386
|
pipelineStartMs: pipelineStart,
|
|
@@ -12284,15 +12482,15 @@ async function executeRenderPipeline(input) {
|
|
|
12284
12482
|
requestedWorkers: job.config.workers ?? "auto"
|
|
12285
12483
|
});
|
|
12286
12484
|
const entryFile = job.config.entryFile || "index.html";
|
|
12287
|
-
let htmlPath =
|
|
12485
|
+
let htmlPath = join26(projectDir, entryFile);
|
|
12288
12486
|
if (!existsSync16(htmlPath)) {
|
|
12289
12487
|
throw new Error(`Entry file not found: ${htmlPath}`);
|
|
12290
12488
|
}
|
|
12291
12489
|
assertNotAborted();
|
|
12292
12490
|
const rawEntry = readFileSync8(htmlPath, "utf-8");
|
|
12293
12491
|
if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
|
|
12294
|
-
const wrapperPath =
|
|
12295
|
-
const projectIndexPath =
|
|
12492
|
+
const wrapperPath = join26(workDir, "standalone-entry.html");
|
|
12493
|
+
const projectIndexPath = join26(projectDir, "index.html");
|
|
12296
12494
|
if (!existsSync16(projectIndexPath)) {
|
|
12297
12495
|
throw new Error(
|
|
12298
12496
|
`Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
|
|
@@ -12455,7 +12653,7 @@ async function executeRenderPipeline(input) {
|
|
|
12455
12653
|
updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
|
|
12456
12654
|
code: "extract_video"
|
|
12457
12655
|
});
|
|
12458
|
-
const compiledDir =
|
|
12656
|
+
const compiledDir = join26(workDir, "compiled");
|
|
12459
12657
|
const extractResult = await observeRenderStage(
|
|
12460
12658
|
observability,
|
|
12461
12659
|
"video_extract",
|
|
@@ -12589,7 +12787,7 @@ async function executeRenderPipeline(input) {
|
|
|
12589
12787
|
try {
|
|
12590
12788
|
fileServer = await createFileServer({
|
|
12591
12789
|
projectDir,
|
|
12592
|
-
compiledDir:
|
|
12790
|
+
compiledDir: join26(workDir, "compiled"),
|
|
12593
12791
|
port: 0,
|
|
12594
12792
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
12595
12793
|
fps: job.config.fps
|
|
@@ -12607,7 +12805,7 @@ async function executeRenderPipeline(input) {
|
|
|
12607
12805
|
if (!activeFileServer) {
|
|
12608
12806
|
throw new Error("File server failed to initialize before frame capture");
|
|
12609
12807
|
}
|
|
12610
|
-
const framesDir =
|
|
12808
|
+
const framesDir = join26(workDir, "captured-frames");
|
|
12611
12809
|
if (!existsSync16(framesDir)) mkdirSync9(framesDir, { recursive: true });
|
|
12612
12810
|
updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
|
|
12613
12811
|
code: "start_browsers"
|
|
@@ -12711,7 +12909,6 @@ async function executeRenderPipeline(input) {
|
|
|
12711
12909
|
const WOULD_RESOLVE_MULTI_WORKER = 2;
|
|
12712
12910
|
const deInversionArgs = {
|
|
12713
12911
|
workerCount: WOULD_RESOLVE_MULTI_WORKER,
|
|
12714
|
-
requestedWorkers: job.config.workers,
|
|
12715
12912
|
useDrawElement: cfg.useDrawElement,
|
|
12716
12913
|
deCompileGate,
|
|
12717
12914
|
forceScreenshot: captureForceScreenshot,
|
|
@@ -12917,7 +13114,7 @@ async function executeRenderPipeline(input) {
|
|
|
12917
13114
|
} else if (deInversionEligible && workerCount > 1) {
|
|
12918
13115
|
deWorkerInversion = "inverted";
|
|
12919
13116
|
log.info(
|
|
12920
|
-
`[Render] Fast capture: single-worker drawElement streaming preferred over ${workerCount}-worker screenshot capture (${totalFrames} frames >= ${deInversionEffectiveMinFrames}; verified path, measured faster at every worker count). Set HF_DE_SINGLE_MIN_FRAMES=0
|
|
13117
|
+
`[Render] Fast capture: single-worker drawElement streaming preferred over ${workerCount}-worker screenshot capture (${totalFrames} frames >= ${deInversionEffectiveMinFrames}; verified path, measured faster at every worker count). Set HF_DE_SINGLE_MIN_FRAMES=0 to override.`
|
|
12921
13118
|
);
|
|
12922
13119
|
workerCount = 1;
|
|
12923
13120
|
}
|
|
@@ -12961,6 +13158,27 @@ async function executeRenderPipeline(input) {
|
|
|
12961
13158
|
deWorkerInversion: deWorkerInversion ?? "none",
|
|
12962
13159
|
deParallelRouter: deParallelRouter ?? "none"
|
|
12963
13160
|
});
|
|
13161
|
+
const calibratedTooSlow = checkRenderEta("capture_calibration", {
|
|
13162
|
+
maxRenderMs: job.config.maxRenderMs,
|
|
13163
|
+
elapsedMs: Date.now() - pipelineStart,
|
|
13164
|
+
framesCompleted: 0,
|
|
13165
|
+
totalFrames,
|
|
13166
|
+
msPerFrame: (captureCalibration?.estimate.p95Ms ?? 0) / workerCount
|
|
13167
|
+
});
|
|
13168
|
+
if (calibratedTooSlow) throw calibratedTooSlow;
|
|
13169
|
+
execution.defer(
|
|
13170
|
+
"stop render ETA guard",
|
|
13171
|
+
startRenderEtaGuard({
|
|
13172
|
+
maxRenderMs: job.config.maxRenderMs,
|
|
13173
|
+
job,
|
|
13174
|
+
totalFrames,
|
|
13175
|
+
pipelineStartMs: pipelineStart,
|
|
13176
|
+
onTooSlow: (error) => {
|
|
13177
|
+
tooSlowError = error;
|
|
13178
|
+
tooSlow.abort(error);
|
|
13179
|
+
}
|
|
13180
|
+
})
|
|
13181
|
+
);
|
|
12964
13182
|
let useStreamingEncode = shouldUseStreamingEncode(
|
|
12965
13183
|
cfg,
|
|
12966
13184
|
outputFormat,
|
|
@@ -13054,7 +13272,7 @@ async function executeRenderPipeline(input) {
|
|
|
13054
13272
|
hls: ".mp4"
|
|
13055
13273
|
};
|
|
13056
13274
|
const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
|
|
13057
|
-
const videoOnlyPath =
|
|
13275
|
+
const videoOnlyPath = join26(workDir, `video-only${videoExt}`);
|
|
13058
13276
|
const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
|
|
13059
13277
|
if (usePageSideCompositingForTransitions) {
|
|
13060
13278
|
activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
|
|
@@ -13300,7 +13518,7 @@ async function executeRenderPipeline(input) {
|
|
|
13300
13518
|
pageMotionBlur: job.config.pageMotionBlur,
|
|
13301
13519
|
captureQuality: captureOptions.quality
|
|
13302
13520
|
});
|
|
13303
|
-
const segmentDir = segmentDirFor(
|
|
13521
|
+
const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
|
|
13304
13522
|
let completedSegments = /* @__PURE__ */ new Set();
|
|
13305
13523
|
if (job.config.resumeSegments === true) {
|
|
13306
13524
|
const existing = readSegmentManifest(segmentDir);
|
|
@@ -13315,10 +13533,10 @@ async function executeRenderPipeline(input) {
|
|
|
13315
13533
|
});
|
|
13316
13534
|
}
|
|
13317
13535
|
if (completedSegments.size === 0) {
|
|
13318
|
-
|
|
13536
|
+
rmSync11(segmentDir, { recursive: true, force: true });
|
|
13319
13537
|
}
|
|
13320
13538
|
} else {
|
|
13321
|
-
|
|
13539
|
+
rmSync11(segmentDir, { recursive: true, force: true });
|
|
13322
13540
|
}
|
|
13323
13541
|
const segmentManifest = readSegmentManifest(segmentDir) ?? {
|
|
13324
13542
|
version: 1,
|
|
@@ -13399,7 +13617,7 @@ async function executeRenderPipeline(input) {
|
|
|
13399
13617
|
);
|
|
13400
13618
|
updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
|
|
13401
13619
|
if (job.config.keepSegments !== true) {
|
|
13402
|
-
|
|
13620
|
+
rmSync11(segmentDir, { recursive: true, force: true });
|
|
13403
13621
|
}
|
|
13404
13622
|
} else {
|
|
13405
13623
|
capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
|
|
@@ -13668,7 +13886,7 @@ async function executeRenderPipeline(input) {
|
|
|
13668
13886
|
"capture_disk",
|
|
13669
13887
|
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
13670
13888
|
);
|
|
13671
|
-
|
|
13889
|
+
rmSync11(framesDir, { recursive: true, force: true });
|
|
13672
13890
|
mkdirSync9(framesDir, { recursive: true });
|
|
13673
13891
|
resetCaptureAttemptProgress(job);
|
|
13674
13892
|
dedupPerfs.length = 0;
|
|
@@ -13855,7 +14073,7 @@ async function executeRenderPipeline(input) {
|
|
|
13855
14073
|
}
|
|
13856
14074
|
if (job.config.debug) {
|
|
13857
14075
|
if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
|
|
13858
|
-
const debugOutput =
|
|
14076
|
+
const debugOutput = join26(workDir, `output${videoExt}`);
|
|
13859
14077
|
copyFileSync4(stagedOutputPath, debugOutput);
|
|
13860
14078
|
}
|
|
13861
14079
|
}
|
|
@@ -13866,8 +14084,9 @@ async function executeRenderPipeline(input) {
|
|
|
13866
14084
|
job.outputPath = outputPath;
|
|
13867
14085
|
updateJobStatus(job, "complete", "Render complete", 100, onProgress);
|
|
13868
14086
|
await eventPublisher.flush();
|
|
13869
|
-
} catch (
|
|
13870
|
-
|
|
14087
|
+
} catch (caught) {
|
|
14088
|
+
const error = tooSlowError ?? caught;
|
|
14089
|
+
if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
|
|
13871
14090
|
job.error = error instanceof Error ? error.message : "render_cancelled";
|
|
13872
14091
|
updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
|
|
13873
14092
|
await eventPublisher.flush();
|
|
@@ -13961,13 +14180,13 @@ async function executeRenderPipeline(input) {
|
|
|
13961
14180
|
import {
|
|
13962
14181
|
existsSync as existsSync17,
|
|
13963
14182
|
readFileSync as readFileSync9,
|
|
13964
|
-
statSync as
|
|
14183
|
+
statSync as statSync8,
|
|
13965
14184
|
openSync as openSync4,
|
|
13966
|
-
fstatSync as
|
|
13967
|
-
closeSync as
|
|
14185
|
+
fstatSync as fstatSync3,
|
|
14186
|
+
closeSync as closeSync4,
|
|
13968
14187
|
constants as constants3
|
|
13969
14188
|
} from "node:fs";
|
|
13970
|
-
import { resolve as resolve8, join as
|
|
14189
|
+
import { resolve as resolve8, join as join27, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
|
|
13971
14190
|
import { lintHyperframeHtml } from "@hyperframes/lint";
|
|
13972
14191
|
function isStringRecord(value) {
|
|
13973
14192
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -14001,19 +14220,19 @@ function readEntryHtml(filePath) {
|
|
|
14001
14220
|
} catch (error) {
|
|
14002
14221
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
14003
14222
|
return null;
|
|
14004
|
-
if (!
|
|
14223
|
+
if (!statSync8(filePath, { throwIfNoEntry: false })?.isFile()) return null;
|
|
14005
14224
|
throw error;
|
|
14006
14225
|
}
|
|
14007
14226
|
try {
|
|
14008
|
-
if (!
|
|
14227
|
+
if (!fstatSync3(fd).isFile()) return null;
|
|
14009
14228
|
return readFileSync9(fd, "utf-8");
|
|
14010
14229
|
} finally {
|
|
14011
|
-
|
|
14230
|
+
closeSync4(fd);
|
|
14012
14231
|
}
|
|
14013
14232
|
}
|
|
14014
14233
|
function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
14015
14234
|
const absProjectDir = resolve8(projectDir);
|
|
14016
|
-
if (!existsSync17(absProjectDir) || !
|
|
14235
|
+
if (!existsSync17(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
|
|
14017
14236
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
14018
14237
|
}
|
|
14019
14238
|
const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
|
|
@@ -14035,7 +14254,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
|
14035
14254
|
}
|
|
14036
14255
|
}
|
|
14037
14256
|
return {
|
|
14038
|
-
error: `No HTML entry file found in project directory: ${
|
|
14257
|
+
error: `No HTML entry file found in project directory: ${join27(absProjectDir, preferredEntryFile || "index.html")}`
|
|
14039
14258
|
};
|
|
14040
14259
|
}
|
|
14041
14260
|
function prepareHyperframeLintBody(body) {
|
|
@@ -14081,7 +14300,7 @@ async function runHyperframeLint(prepared) {
|
|
|
14081
14300
|
// src/services/healthWorker.ts
|
|
14082
14301
|
import { Worker as Worker2 } from "node:worker_threads";
|
|
14083
14302
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
14084
|
-
import { dirname as dirname11, join as
|
|
14303
|
+
import { dirname as dirname11, join as join28 } from "node:path";
|
|
14085
14304
|
import { existsSync as existsSync18 } from "node:fs";
|
|
14086
14305
|
var DEFAULT_HEALTH_PORT = 9848;
|
|
14087
14306
|
async function startHealthWorker(options = {}) {
|
|
@@ -14157,7 +14376,7 @@ function defaultLogger2() {
|
|
|
14157
14376
|
}
|
|
14158
14377
|
function resolveWorkerEntry2() {
|
|
14159
14378
|
const here = dirname11(fileURLToPath5(import.meta.url));
|
|
14160
|
-
const candidates = [
|
|
14379
|
+
const candidates = [join28(here, "healthWorkerThread.js"), join28(here, "healthWorkerThread.ts")];
|
|
14161
14380
|
for (const candidate of candidates) {
|
|
14162
14381
|
if (existsSync18(candidate)) return candidate;
|
|
14163
14382
|
}
|
|
@@ -14193,7 +14412,7 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
|
14193
14412
|
|
|
14194
14413
|
// src/services/distributed/plan.ts
|
|
14195
14414
|
import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
|
|
14196
|
-
import { extname as extname4, isAbsolute as isAbsolute4, join as
|
|
14415
|
+
import { extname as extname4, isAbsolute as isAbsolute4, join as join29, relative as relative4, sep as sep3 } from "node:path";
|
|
14197
14416
|
import {
|
|
14198
14417
|
fpsToNumber as fpsToNumber10,
|
|
14199
14418
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
@@ -14233,6 +14452,9 @@ import {
|
|
|
14233
14452
|
var PLAN_AUDIO_RELATIVE_PATH = MIXED_AUDIO_FILENAME3;
|
|
14234
14453
|
var execFile = promisify2(execFileCallback);
|
|
14235
14454
|
|
|
14455
|
+
// src/services/distributed/chunkWeights.ts
|
|
14456
|
+
import { resolveFrameFormat } from "@hyperframes/engine";
|
|
14457
|
+
|
|
14236
14458
|
// src/services/distributed/planProtocol.ts
|
|
14237
14459
|
var PLAN_SCHEMA_VERSION = 1;
|
|
14238
14460
|
var PLAN_ARTIFACT_LAYOUT = "plan-dir-v1";
|
|
@@ -14294,9 +14516,9 @@ var PLAN_ROOT_CATEGORY = {
|
|
|
14294
14516
|
var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
14295
14517
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
14296
14518
|
"plan.json",
|
|
14297
|
-
|
|
14298
|
-
|
|
14299
|
-
|
|
14519
|
+
join29("meta", "composition.json"),
|
|
14520
|
+
join29("meta", "encoder.json"),
|
|
14521
|
+
join29("meta", "chunks.json")
|
|
14300
14522
|
];
|
|
14301
14523
|
|
|
14302
14524
|
// src/services/distributed/renderConfigValidation.ts
|
|
@@ -14437,6 +14659,7 @@ function assertRequestOptionScalars(options) {
|
|
|
14437
14659
|
assertOptionalInteger(options, "gifLoop");
|
|
14438
14660
|
assertOptionalInteger(options, "hlsSegmentSeconds", 1);
|
|
14439
14661
|
assertOptionalInteger(options, "workers", 1);
|
|
14662
|
+
assertOptionalInteger(options, "maxRenderMs");
|
|
14440
14663
|
assertOptionalInteger(options, "crf");
|
|
14441
14664
|
for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
|
|
14442
14665
|
assertOptionalBoolean(options, field);
|
|
@@ -14529,7 +14752,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
14529
14752
|
"INVALID_VIDEO_METADATA",
|
|
14530
14753
|
"VIDEO_SOURCE_UNRENDERABLE",
|
|
14531
14754
|
"VIDEO_EXTRACTION_FAILED",
|
|
14532
|
-
"ENCODER_INTERRUPTED"
|
|
14755
|
+
"ENCODER_INTERRUPTED",
|
|
14756
|
+
"RENDER_TOO_SLOW"
|
|
14533
14757
|
]);
|
|
14534
14758
|
function extractSafeRenderErrorCode(error) {
|
|
14535
14759
|
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
|
@@ -14588,6 +14812,7 @@ function parseRenderOptions(body) {
|
|
|
14588
14812
|
const fps = parseServerFps(body.fps);
|
|
14589
14813
|
const quality = parseServerQuality(body.quality);
|
|
14590
14814
|
const workers = typeof body.workers === "number" ? body.workers : void 0;
|
|
14815
|
+
const maxRenderMs = typeof body.maxRenderMs === "number" ? body.maxRenderMs : void 0;
|
|
14591
14816
|
const useGpu = body.gpu === true;
|
|
14592
14817
|
const debug = body.debug === true;
|
|
14593
14818
|
const strictness = body.bestEffort === false ? "strict" : "best-effort";
|
|
@@ -14603,6 +14828,7 @@ function parseRenderOptions(body) {
|
|
|
14603
14828
|
fps,
|
|
14604
14829
|
quality,
|
|
14605
14830
|
workers,
|
|
14831
|
+
maxRenderMs,
|
|
14606
14832
|
crf,
|
|
14607
14833
|
videoBitrate,
|
|
14608
14834
|
useGpu,
|
|
@@ -14636,6 +14862,7 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
14636
14862
|
quality: input.quality,
|
|
14637
14863
|
format: input.format ?? "mp4",
|
|
14638
14864
|
workers: input.workers,
|
|
14865
|
+
maxRenderMs: input.maxRenderMs,
|
|
14639
14866
|
crf: input.crf,
|
|
14640
14867
|
videoBitrate: input.videoBitrate,
|
|
14641
14868
|
useGpu: input.useGpu,
|
|
@@ -14662,6 +14889,9 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
|
|
|
14662
14889
|
function validateRenderOverrides(body) {
|
|
14663
14890
|
const encoder = parseEncoderOptions(body, parseServerFormat(body.format) ?? "mp4");
|
|
14664
14891
|
if (!encoder.ok) return encoder.error;
|
|
14892
|
+
if (body.maxRenderMs !== void 0 && (typeof body.maxRenderMs !== "number" || !Number.isInteger(body.maxRenderMs) || body.maxRenderMs < 0)) {
|
|
14893
|
+
return "maxRenderMs must be a non-negative integer number of milliseconds";
|
|
14894
|
+
}
|
|
14665
14895
|
if (body.variables !== void 0 && !isPlainObject2(body.variables)) {
|
|
14666
14896
|
return 'variables must be a JSON object keyed by variable id (e.g. {"title":"Hello"})';
|
|
14667
14897
|
}
|
|
@@ -14689,11 +14919,12 @@ function validateOutputResolutionOverride(body) {
|
|
|
14689
14919
|
}
|
|
14690
14920
|
return void 0;
|
|
14691
14921
|
}
|
|
14922
|
+
var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
|
|
14692
14923
|
function prepareProjectDirectory(projectDir, options) {
|
|
14693
14924
|
const candidate = nonEmptyString(projectDir);
|
|
14694
14925
|
if (!candidate) return null;
|
|
14695
14926
|
const absProjectDir = resolve9(candidate);
|
|
14696
|
-
if (!existsSync19(absProjectDir) || !
|
|
14927
|
+
if (!existsSync19(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
|
|
14697
14928
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
14698
14929
|
}
|
|
14699
14930
|
const entry = options.entryFile || "index.html";
|
|
@@ -14709,11 +14940,7 @@ async function resolveInlineRenderHtml(body) {
|
|
|
14709
14940
|
if (!previewUrl)
|
|
14710
14941
|
return { error: "Missing render source: provide projectDir, previewUrl, or html" };
|
|
14711
14942
|
try {
|
|
14712
|
-
|
|
14713
|
-
if (!response.ok) {
|
|
14714
|
-
return { error: `Failed to fetch previewUrl: ${response.status} ${response.statusText}` };
|
|
14715
|
-
}
|
|
14716
|
-
return { html: await response.text() };
|
|
14943
|
+
return { html: await fetchPublicHttpsText2(previewUrl, { maxBytes: MAX_PREVIEW_HTML_BYTES }) };
|
|
14717
14944
|
} catch (error) {
|
|
14718
14945
|
return {
|
|
14719
14946
|
error: `Failed to fetch previewUrl: ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -14722,8 +14949,8 @@ async function resolveInlineRenderHtml(body) {
|
|
|
14722
14949
|
}
|
|
14723
14950
|
function materializeInlineProject(html, options) {
|
|
14724
14951
|
const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
|
|
14725
|
-
const tempProjectDir = mkdtempSync4(
|
|
14726
|
-
writeFileSync6(
|
|
14952
|
+
const tempProjectDir = mkdtempSync4(join30(tempRoot, "producer-project-"));
|
|
14953
|
+
writeFileSync6(join30(tempProjectDir, "index.html"), html, "utf-8");
|
|
14727
14954
|
return {
|
|
14728
14955
|
prepared: {
|
|
14729
14956
|
input: { projectDir: tempProjectDir, ...options },
|
|
@@ -14784,7 +15011,7 @@ function createArtifactStore(ttlMs) {
|
|
|
14784
15011
|
function cleanupTempDir(dir, log) {
|
|
14785
15012
|
if (!dir) return;
|
|
14786
15013
|
try {
|
|
14787
|
-
|
|
15014
|
+
rmSync12(dir, { recursive: true, force: true });
|
|
14788
15015
|
} catch (error) {
|
|
14789
15016
|
log.warn("Failed to cleanup temp project dir", {
|
|
14790
15017
|
cleanupProjectDir: dir,
|
|
@@ -14793,7 +15020,7 @@ function cleanupTempDir(dir, log) {
|
|
|
14793
15020
|
}
|
|
14794
15021
|
}
|
|
14795
15022
|
function outputFileSize(path) {
|
|
14796
|
-
return existsSync19(path) ?
|
|
15023
|
+
return existsSync19(path) ? statSync9(path).size : 0;
|
|
14797
15024
|
}
|
|
14798
15025
|
function createBlockingProgressReporter(log, requestId) {
|
|
14799
15026
|
let lastLoggedPct = -10;
|
|
@@ -15093,7 +15320,7 @@ function createRenderHandlers(options = {}) {
|
|
|
15093
15320
|
store.delete(token);
|
|
15094
15321
|
return c.json({ success: false, error: "Output artifact file missing" }, 404);
|
|
15095
15322
|
}
|
|
15096
|
-
const stats =
|
|
15323
|
+
const stats = statSync9(artifact.path);
|
|
15097
15324
|
return new Response(createReadStream3(artifact.path), {
|
|
15098
15325
|
headers: {
|
|
15099
15326
|
"content-type": "video/mp4",
|