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