@hyperframes/producer 0.8.198 → 0.8.200
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 +176 -166
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +65 -62
- package/dist/index.js +756 -451
- package/dist/public-server.js +566 -289
- package/dist/renderRequest.d.ts +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/services/distributed/chunkWeights.d.ts +2 -0
- package/dist/services/hdrCompositor.d.ts +5 -5
- package/dist/services/hdrFrameWindows.d.ts +23 -0
- package/dist/services/htmlCompiler.d.ts +0 -2
- package/dist/services/render/audioPadTrim.d.ts +2 -0
- package/dist/services/render/renderEtaGuard.d.ts +35 -0
- package/dist/services/render/stages/captureHdrResources.d.ts +5 -17
- package/dist/services/render/stages/compileStage.d.ts +1 -1
- package/dist/services/render/stages/freezePlan.d.ts +1 -0
- package/dist/services/renderOrchestrator.d.ts +7 -6
- 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,
|
|
@@ -2114,6 +2114,7 @@ function aggregateDrawElement(perfs, de) {
|
|
|
2114
2114
|
blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
|
|
2115
2115
|
blankRecaptures: drain?.blankRecaptures ?? 0,
|
|
2116
2116
|
boundaryFrames: perfs.reduce((sum, p) => sum + (p.deBoundaryFrames ?? 0), 0),
|
|
2117
|
+
riskFrames: perfs.reduce((sum, p) => sum + (p.deRiskFrames ?? 0), 0),
|
|
2117
2118
|
ncprFallbacks: perfs.reduce((sum, p) => sum + (p.deNcprFallbacks ?? 0), 0),
|
|
2118
2119
|
frameTimeouts: perfs.reduce((sum, p) => sum + (p.deFrameTimeouts ?? 0), 0)
|
|
2119
2120
|
};
|
|
@@ -2295,7 +2296,7 @@ import {
|
|
|
2295
2296
|
} from "@hyperframes/engine";
|
|
2296
2297
|
|
|
2297
2298
|
// src/services/hdrCompositor.ts
|
|
2298
|
-
import { readSync as readSync2
|
|
2299
|
+
import { readSync as readSync2 } from "fs";
|
|
2299
2300
|
import { join as join6 } from "path";
|
|
2300
2301
|
import {
|
|
2301
2302
|
captureAlphaPng,
|
|
@@ -2348,10 +2349,10 @@ function cropRgb48le(src, srcW, srcH, cropX, cropY, cropW, cropH) {
|
|
|
2348
2349
|
}
|
|
2349
2350
|
function closeHdrVideoFrameSource(source, log) {
|
|
2350
2351
|
try {
|
|
2351
|
-
|
|
2352
|
+
source.windows.close();
|
|
2352
2353
|
} catch (err) {
|
|
2353
|
-
log?.warn("Failed to close HDR raw frame
|
|
2354
|
-
|
|
2354
|
+
log?.warn("Failed to close HDR raw frame windows", {
|
|
2355
|
+
dir: source.dir,
|
|
2355
2356
|
error: err instanceof Error ? err.message : String(err)
|
|
2356
2357
|
});
|
|
2357
2358
|
}
|
|
@@ -2361,7 +2362,7 @@ function resolveHdrVideoFrameIndex(time, startTime, fps, frameCount, loop = fals
|
|
|
2361
2362
|
if (frameIndex < 0 || frameCount < 1) return null;
|
|
2362
2363
|
return loop ? frameIndex % frameCount : Math.min(frameIndex, frameCount - 1);
|
|
2363
2364
|
}
|
|
2364
|
-
function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
|
|
2365
|
+
async function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
|
|
2365
2366
|
const frameSource = hdrVideoFrameSources.get(el.id);
|
|
2366
2367
|
const startTime = hdrStartTimes.get(el.id);
|
|
2367
2368
|
if (!frameSource || startTime === void 0 || el.opacity <= 0) {
|
|
@@ -2377,13 +2378,32 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStart
|
|
|
2377
2378
|
if (effectiveIndex === null) return;
|
|
2378
2379
|
const mediaTime = (frameSource.mediaStart ?? 0) + effectiveIndex / fps;
|
|
2379
2380
|
if (isVideoHiddenBeforeStreamStart(frameSource.videoStreamLeadSeconds, mediaTime)) return;
|
|
2380
|
-
|
|
2381
|
+
if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
|
|
2382
|
+
const lease = await frameSource.windows.lease(effectiveIndex);
|
|
2383
|
+
if (!lease) return;
|
|
2384
|
+
try {
|
|
2385
|
+
blitScratchFrameWithoutYielding(
|
|
2386
|
+
canvas,
|
|
2387
|
+
el,
|
|
2388
|
+
frameSource,
|
|
2389
|
+
lease,
|
|
2390
|
+
width,
|
|
2391
|
+
height,
|
|
2392
|
+
log,
|
|
2393
|
+
sourceTransfer,
|
|
2394
|
+
targetTransfer,
|
|
2395
|
+
hdrPerf
|
|
2396
|
+
);
|
|
2397
|
+
} finally {
|
|
2398
|
+
lease.release();
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
function blitScratchFrameWithoutYielding(canvas, el, frameSource, lease, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
|
|
2381
2402
|
try {
|
|
2382
|
-
if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
|
|
2383
2403
|
const bytesRead = timeHdrPhase(
|
|
2384
2404
|
hdrPerf,
|
|
2385
2405
|
"hdrVideoReadDecodeMs",
|
|
2386
|
-
() => readSync2(
|
|
2406
|
+
() => readSync2(lease.fd, frameSource.scratch, 0, frameSource.frameSize, lease.offset)
|
|
2387
2407
|
);
|
|
2388
2408
|
if (bytesRead !== frameSource.frameSize) return;
|
|
2389
2409
|
const hdrRgb = frameSource.scratch;
|
|
@@ -2612,7 +2632,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
|
|
|
2612
2632
|
hdrPerf
|
|
2613
2633
|
);
|
|
2614
2634
|
} else {
|
|
2615
|
-
blitHdrVideoLayer(
|
|
2635
|
+
await blitHdrVideoLayer(
|
|
2616
2636
|
canvas,
|
|
2617
2637
|
layer.element,
|
|
2618
2638
|
time,
|
|
@@ -2656,7 +2676,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
|
|
|
2656
2676
|
startTime,
|
|
2657
2677
|
localTime: localTime.toFixed(3),
|
|
2658
2678
|
hdrFrameNum: frameNum,
|
|
2659
|
-
|
|
2679
|
+
dir: frameSource?.dir ?? null,
|
|
2660
2680
|
frameCount: frameSource?.frameCount ?? null
|
|
2661
2681
|
});
|
|
2662
2682
|
}
|
|
@@ -2752,18 +2772,8 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
|
|
|
2752
2772
|
}
|
|
2753
2773
|
|
|
2754
2774
|
// 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";
|
|
2775
|
+
import { mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, rmSync as rmSync5, statSync as statSync2, statfsSync } from "node:fs";
|
|
2776
|
+
import { join as join8 } from "node:path";
|
|
2767
2777
|
import {
|
|
2768
2778
|
decodePngToRgb48le,
|
|
2769
2779
|
extractMediaMetadata as extractMediaMetadata2,
|
|
@@ -2777,7 +2787,90 @@ import {
|
|
|
2777
2787
|
runFfmpeg
|
|
2778
2788
|
} from "@hyperframes/engine";
|
|
2779
2789
|
import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
2780
|
-
|
|
2790
|
+
|
|
2791
|
+
// src/services/hdrFrameWindows.ts
|
|
2792
|
+
import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
|
|
2793
|
+
import { join as join7 } from "node:path";
|
|
2794
|
+
function createHdrFrameWindows(opts) {
|
|
2795
|
+
const { dir, frameSize, windowFrames, maxResident, extract } = opts;
|
|
2796
|
+
let totalFrames = opts.totalFrames;
|
|
2797
|
+
const windows = /* @__PURE__ */ new Map();
|
|
2798
|
+
const drop = (index, w) => {
|
|
2799
|
+
windows.delete(index);
|
|
2800
|
+
if (w.fd >= 0) closeSync3(w.fd);
|
|
2801
|
+
rmSync4(w.path, { force: true });
|
|
2802
|
+
};
|
|
2803
|
+
const open = (index) => {
|
|
2804
|
+
const known = windows.get(index);
|
|
2805
|
+
if (known) {
|
|
2806
|
+
windows.delete(index);
|
|
2807
|
+
windows.set(index, known);
|
|
2808
|
+
return known;
|
|
2809
|
+
}
|
|
2810
|
+
for (const [i, w2] of windows) {
|
|
2811
|
+
if (windows.size < maxResident) break;
|
|
2812
|
+
if (w2.pins === 0) drop(i, w2);
|
|
2813
|
+
}
|
|
2814
|
+
const firstFrame = index * windowFrames;
|
|
2815
|
+
const w = {
|
|
2816
|
+
firstFrame,
|
|
2817
|
+
path: join7(dir, `window-${index}.rgb48le`),
|
|
2818
|
+
frames: 0,
|
|
2819
|
+
fd: -1,
|
|
2820
|
+
pins: 0,
|
|
2821
|
+
ready: Promise.resolve()
|
|
2822
|
+
};
|
|
2823
|
+
w.ready = extract({
|
|
2824
|
+
firstFrame,
|
|
2825
|
+
frames: Math.min(windowFrames, totalFrames - firstFrame),
|
|
2826
|
+
path: w.path
|
|
2827
|
+
}).then((produced) => {
|
|
2828
|
+
if (produced < 1) return;
|
|
2829
|
+
w.fd = openSync3(w.path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
2830
|
+
w.frames = produced;
|
|
2831
|
+
});
|
|
2832
|
+
windows.set(index, w);
|
|
2833
|
+
return w;
|
|
2834
|
+
};
|
|
2835
|
+
return {
|
|
2836
|
+
get totalFrames() {
|
|
2837
|
+
return totalFrames;
|
|
2838
|
+
},
|
|
2839
|
+
async lease(frameIndex) {
|
|
2840
|
+
for (; ; ) {
|
|
2841
|
+
if (totalFrames < 1) return null;
|
|
2842
|
+
const index = Math.min(Math.max(0, frameIndex), totalFrames - 1);
|
|
2843
|
+
const key = Math.floor(index / windowFrames);
|
|
2844
|
+
const w = open(key);
|
|
2845
|
+
w.pins += 1;
|
|
2846
|
+
try {
|
|
2847
|
+
await w.ready;
|
|
2848
|
+
} catch (error) {
|
|
2849
|
+
w.pins -= 1;
|
|
2850
|
+
windows.delete(key);
|
|
2851
|
+
throw error;
|
|
2852
|
+
}
|
|
2853
|
+
const local = index - w.firstFrame;
|
|
2854
|
+
if (local < w.frames) {
|
|
2855
|
+
return {
|
|
2856
|
+
fd: w.fd,
|
|
2857
|
+
offset: local * frameSize,
|
|
2858
|
+
release: () => {
|
|
2859
|
+
w.pins -= 1;
|
|
2860
|
+
}
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
w.pins -= 1;
|
|
2864
|
+
totalFrames = Math.min(totalFrames, w.firstFrame + w.frames);
|
|
2865
|
+
}
|
|
2866
|
+
},
|
|
2867
|
+
close() {
|
|
2868
|
+
for (const [i, w] of [...windows]) drop(i, w);
|
|
2869
|
+
}
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
// src/services/render/stages/captureHdrResources.ts
|
|
2781
2874
|
function tempDirSafePrefix(id) {
|
|
2782
2875
|
const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
|
|
2783
2876
|
return safe || "video";
|
|
@@ -2822,10 +2915,16 @@ async function probeHdrExtractionDims(args) {
|
|
|
2822
2915
|
const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
|
|
2823
2916
|
for (const el of stacking) {
|
|
2824
2917
|
if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !prep.hdrExtractionDims.has(el.id)) {
|
|
2825
|
-
prep.hdrExtractionDims.set(el.id, {
|
|
2918
|
+
prep.hdrExtractionDims.set(el.id, {
|
|
2919
|
+
width: el.layoutWidth,
|
|
2920
|
+
height: el.layoutHeight
|
|
2921
|
+
});
|
|
2826
2922
|
}
|
|
2827
2923
|
if (el.isHdr && nativeHdrImageIds.has(el.id) && !prep.hdrImageFitInfo.has(el.id)) {
|
|
2828
|
-
prep.hdrImageFitInfo.set(el.id, {
|
|
2924
|
+
prep.hdrImageFitInfo.set(el.id, {
|
|
2925
|
+
fit: el.objectFit,
|
|
2926
|
+
position: el.objectPosition
|
|
2927
|
+
});
|
|
2829
2928
|
}
|
|
2830
2929
|
}
|
|
2831
2930
|
}
|
|
@@ -2844,23 +2943,21 @@ async function probeHdrExtractionDims(args) {
|
|
|
2844
2943
|
const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
|
|
2845
2944
|
for (const el of retryStacking) {
|
|
2846
2945
|
if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
|
|
2847
|
-
prep.hdrExtractionDims.set(el.id, {
|
|
2946
|
+
prep.hdrExtractionDims.set(el.id, {
|
|
2947
|
+
width: el.layoutWidth,
|
|
2948
|
+
height: el.layoutHeight
|
|
2949
|
+
});
|
|
2848
2950
|
if (!prep.hdrImageFitInfo.has(el.id)) {
|
|
2849
|
-
prep.hdrImageFitInfo.set(el.id, {
|
|
2951
|
+
prep.hdrImageFitInfo.set(el.id, {
|
|
2952
|
+
fit: el.objectFit,
|
|
2953
|
+
position: el.objectPosition
|
|
2954
|
+
});
|
|
2850
2955
|
}
|
|
2851
2956
|
break;
|
|
2852
2957
|
}
|
|
2853
2958
|
}
|
|
2854
2959
|
}
|
|
2855
2960
|
}
|
|
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
2961
|
var HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
|
|
2865
2962
|
var HDR_EXTRACTION_CGROUP_BUDGET_FRACTION = 0.5;
|
|
2866
2963
|
var HDR_EXTRACTION_WARN_BYTES = 1e10;
|
|
@@ -2924,24 +3021,22 @@ function resolveHdrExtractionWindow(video, compositionDuration, metadata) {
|
|
|
2924
3021
|
}
|
|
2925
3022
|
return window2;
|
|
2926
3023
|
}
|
|
2927
|
-
function cleanupHdrFrameDirectory(frameDir,
|
|
3024
|
+
function cleanupHdrFrameDirectory(frameDir, log) {
|
|
2928
3025
|
if (process.env.KEEP_TEMP === "1") return;
|
|
2929
3026
|
try {
|
|
2930
|
-
|
|
3027
|
+
rmSync5(frameDir, { recursive: true, force: true });
|
|
2931
3028
|
} catch (err) {
|
|
2932
3029
|
log?.warn("Failed to clean up HDR raw frame directory", {
|
|
2933
3030
|
frameDir,
|
|
2934
|
-
rawPath,
|
|
2935
3031
|
error: err instanceof Error ? err.message : String(err)
|
|
2936
3032
|
});
|
|
2937
3033
|
}
|
|
2938
3034
|
}
|
|
2939
3035
|
function cleanupHdrVideoFrameSource(source, log) {
|
|
2940
3036
|
closeHdrVideoFrameSource(source, log);
|
|
2941
|
-
cleanupHdrFrameDirectory(source.dir,
|
|
3037
|
+
cleanupHdrFrameDirectory(source.dir, log);
|
|
2942
3038
|
}
|
|
2943
|
-
function assertHdrExtractionDiskHeadroom(framesDir,
|
|
2944
|
-
const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
|
|
3039
|
+
function assertHdrExtractionDiskHeadroom(framesDir, estimatedBytes, log) {
|
|
2945
3040
|
const configuredBudget = resolveHdrExtractionBudgetBytes(
|
|
2946
3041
|
process.env[HDR_EXTRACTION_MAX_BYTES_ENV]
|
|
2947
3042
|
);
|
|
@@ -2975,14 +3070,29 @@ function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
|
|
|
2975
3070
|
budgetBytes: resolveHdrExtractionActiveBudgetBytes(configuredBudget, freeBytes)
|
|
2976
3071
|
};
|
|
2977
3072
|
}
|
|
3073
|
+
var DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
|
|
3074
|
+
function hdrWindowFrames(frameSize, windowBytes) {
|
|
3075
|
+
return Math.max(8, Math.floor(windowBytes / frameSize));
|
|
3076
|
+
}
|
|
3077
|
+
async function probeFirstTick(srcPath, mediaStart, fps, runFfmpegImpl, abortSignal) {
|
|
3078
|
+
const args = ["-ss", String(mediaStart), "-i", srcPath, "-frames:v", "1"];
|
|
3079
|
+
const result = await runFfmpegImpl([...args, ...["-vf", "showinfo", "-f", "null", "-"]], {
|
|
3080
|
+
signal: abortSignal
|
|
3081
|
+
});
|
|
3082
|
+
const match = /pts_time:(-?[\d.]+)/.exec(result.stderr);
|
|
3083
|
+
return match ? Math.max(0, Math.floor(Number(match[1]) * fps + 0.5)) : 0;
|
|
3084
|
+
}
|
|
2978
3085
|
async function extractHdrVideoFrames(args) {
|
|
2979
3086
|
const { job, log, framesDir, composition, prep, width, height, abortSignal, hdrDiagnostics } = args;
|
|
3087
|
+
const maxResident = Math.max(2, args.maxResidentWindows ?? 2);
|
|
3088
|
+
const windowBytes = args.windowBytes ?? DEFAULT_HDR_WINDOW_BYTES;
|
|
2980
3089
|
const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
|
|
2981
3090
|
const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
|
|
2982
3091
|
const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
|
|
3092
|
+
const fps = fpsToNumber4(job.config.fps);
|
|
2983
3093
|
const out = /* @__PURE__ */ new Map();
|
|
2984
3094
|
mkdirSync2(framesDir, { recursive: true });
|
|
2985
|
-
|
|
3095
|
+
let scratchBytes = 0;
|
|
2986
3096
|
const extractionWindows = /* @__PURE__ */ new Map();
|
|
2987
3097
|
for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
|
|
2988
3098
|
const video = composition.videos.find((v) => v.id === videoId);
|
|
@@ -3003,16 +3113,13 @@ async function extractHdrVideoFrames(args) {
|
|
|
3003
3113
|
videoStreamLeadSeconds: metadata.videoStreamLeadSeconds
|
|
3004
3114
|
});
|
|
3005
3115
|
prep.hdrVideoStartTimes.set(videoId, window2.compositionStart);
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
height: dims.height
|
|
3010
|
-
});
|
|
3116
|
+
const frameSize = dims.width * dims.height * 6;
|
|
3117
|
+
const frames = window2.finalFrameOnly ? 1 : Math.ceil(window2.durationSeconds * fps);
|
|
3118
|
+
scratchBytes += Math.min(frames, maxResident * hdrWindowFrames(frameSize, windowBytes)) * frameSize;
|
|
3011
3119
|
}
|
|
3012
3120
|
const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
|
|
3013
3121
|
framesDir,
|
|
3014
|
-
|
|
3015
|
-
fpsToNumber4(job.config.fps),
|
|
3122
|
+
scratchBytes,
|
|
3016
3123
|
log
|
|
3017
3124
|
);
|
|
3018
3125
|
const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
|
|
@@ -3024,79 +3131,79 @@ async function extractHdrVideoFrames(args) {
|
|
|
3024
3131
|
if (!video || !planned) continue;
|
|
3025
3132
|
const { window: window2 } = planned;
|
|
3026
3133
|
mkdirSync2(framesDir, { recursive: true });
|
|
3027
|
-
const frameDir = mkdtempSync2(
|
|
3134
|
+
const frameDir = mkdtempSync2(join8(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
|
|
3028
3135
|
createdFrameDirs.add(frameDir);
|
|
3029
3136
|
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
3137
|
const frameSize = dims.width * dims.height * 6;
|
|
3073
|
-
const
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3138
|
+
const extractionMediaStart = window2.extractionMediaStart ?? window2.mediaStart;
|
|
3139
|
+
const scale = `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`;
|
|
3140
|
+
const firstTick = window2.finalFrameOnly ? 0 : await probeFirstTick(srcPath, extractionMediaStart, fps, runFfmpegImpl, abortSignal);
|
|
3141
|
+
const totalFrames = window2.finalFrameOnly ? 1 : Math.max(1, Math.ceil(window2.durationSeconds * fps - firstTick - 1e-9)) + Number(video.loop);
|
|
3142
|
+
const windowFrames = Math.min(totalFrames, hdrWindowFrames(frameSize, windowBytes));
|
|
3143
|
+
const windows = createHdrFrameWindows({
|
|
3144
|
+
dir: frameDir,
|
|
3145
|
+
frameSize,
|
|
3146
|
+
totalFrames,
|
|
3147
|
+
windowFrames,
|
|
3148
|
+
maxResident,
|
|
3149
|
+
extract: async (spec) => {
|
|
3150
|
+
const ffmpegArgs = window2.finalFrameOnly ? (
|
|
3151
|
+
// Decode before seeking for the one-frame path. Input-side seeking can
|
|
3152
|
+
// return zero frames for valid unindexed/negative-base transports.
|
|
3153
|
+
["-i", srcPath, "-ss", String(extractionMediaStart), "-frames:v", "1", "-vf", scale]
|
|
3154
|
+
) : hdrWindowFfmpegArgs({
|
|
3155
|
+
srcPath,
|
|
3156
|
+
mediaStart: extractionMediaStart,
|
|
3157
|
+
fps,
|
|
3158
|
+
fpsArg: fpsToFfmpegArg(job.config.fps),
|
|
3159
|
+
firstTick,
|
|
3160
|
+
firstFrame: spec.firstFrame,
|
|
3161
|
+
frames: spec.frames,
|
|
3162
|
+
scale
|
|
3163
|
+
});
|
|
3164
|
+
const result = await runFfmpegImpl(
|
|
3165
|
+
[...ffmpegArgs, "-pix_fmt", "rgb48le", "-f", "rawvideo", "-y", spec.path],
|
|
3166
|
+
{ signal: abortSignal }
|
|
3081
3167
|
);
|
|
3168
|
+
if (!result.success) {
|
|
3169
|
+
hdrDiagnostics.videoExtractionFailures += 1;
|
|
3170
|
+
log.error("HDR frame extraction failed; aborting render", {
|
|
3171
|
+
videoId,
|
|
3172
|
+
srcPath,
|
|
3173
|
+
firstFrame: spec.firstFrame,
|
|
3174
|
+
stderr: result.stderr.slice(-400)
|
|
3175
|
+
});
|
|
3176
|
+
throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
|
|
3177
|
+
error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
|
|
3178
|
+
failureReason: result.failureReason
|
|
3179
|
+
});
|
|
3180
|
+
}
|
|
3181
|
+
return Math.min(Math.floor(statSync2(spec.path).size / frameSize), spec.frames);
|
|
3082
3182
|
}
|
|
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);
|
|
3183
|
+
});
|
|
3184
|
+
const first = await windows.lease(0);
|
|
3185
|
+
if (!first) {
|
|
3186
|
+
hdrDiagnostics.videoExtractionFailures += 1;
|
|
3187
|
+
throw new Error(
|
|
3188
|
+
`HDR frame extraction produced no frames for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
|
|
3189
|
+
);
|
|
3099
3190
|
}
|
|
3191
|
+
first.release();
|
|
3192
|
+
if (video.loop) (await windows.lease(totalFrames - 1))?.release();
|
|
3193
|
+
out.set(videoId, {
|
|
3194
|
+
dir: frameDir,
|
|
3195
|
+
windows,
|
|
3196
|
+
width: dims.width,
|
|
3197
|
+
height: dims.height,
|
|
3198
|
+
frameSize,
|
|
3199
|
+
get frameCount() {
|
|
3200
|
+
return windows.totalFrames;
|
|
3201
|
+
},
|
|
3202
|
+
scratch: Buffer.allocUnsafe(frameSize),
|
|
3203
|
+
loop: video.loop,
|
|
3204
|
+
mediaStart: extractionMediaStart,
|
|
3205
|
+
videoStreamLeadSeconds: planned.videoStreamLeadSeconds
|
|
3206
|
+
});
|
|
3100
3207
|
}
|
|
3101
3208
|
return { sources: out, estimatedBytes, releaseReservation };
|
|
3102
3209
|
} catch (error) {
|
|
@@ -3105,12 +3212,23 @@ async function extractHdrVideoFrames(args) {
|
|
|
3105
3212
|
createdFrameDirs.delete(source.dir);
|
|
3106
3213
|
}
|
|
3107
3214
|
for (const frameDir of createdFrameDirs) {
|
|
3108
|
-
cleanupHdrFrameDirectory(frameDir,
|
|
3215
|
+
cleanupHdrFrameDirectory(frameDir, log);
|
|
3109
3216
|
}
|
|
3110
3217
|
releaseReservation();
|
|
3111
3218
|
throw error;
|
|
3112
3219
|
}
|
|
3113
3220
|
}
|
|
3221
|
+
function hdrWindowFfmpegArgs(a) {
|
|
3222
|
+
const unit = Math.ceil(a.fps);
|
|
3223
|
+
const seekFrame = a.firstFrame < unit ? 0 : (Math.floor(a.firstFrame / unit) - 1) * unit;
|
|
3224
|
+
const trim = a.firstFrame - seekFrame;
|
|
3225
|
+
const filters = [`fps=${a.fpsArg}:start_time=${(a.firstTick / a.fps).toFixed(6)}`, a.scale];
|
|
3226
|
+
if (trim > 0) filters.push(`trim=start_frame=${trim}`);
|
|
3227
|
+
return [
|
|
3228
|
+
...["-ss", (a.mediaStart + seekFrame / a.fps).toFixed(6), "-i", a.srcPath],
|
|
3229
|
+
...["-vf", filters.join(","), "-frames:v", String(a.frames)]
|
|
3230
|
+
];
|
|
3231
|
+
}
|
|
3114
3232
|
function decodeHdrImageBuffers(args) {
|
|
3115
3233
|
const { log, hdrImageSrcPaths, prep, hdrDiagnostics } = args;
|
|
3116
3234
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -3130,7 +3248,11 @@ function decodeHdrImageBuffers(args) {
|
|
|
3130
3248
|
fit,
|
|
3131
3249
|
fitInfo?.position
|
|
3132
3250
|
);
|
|
3133
|
-
out.set(imageId, {
|
|
3251
|
+
out.set(imageId, {
|
|
3252
|
+
data: resampled,
|
|
3253
|
+
width: layout.width,
|
|
3254
|
+
height: layout.height
|
|
3255
|
+
});
|
|
3134
3256
|
} else {
|
|
3135
3257
|
out.set(imageId, {
|
|
3136
3258
|
data: Buffer.from(decoded.data),
|
|
@@ -3242,7 +3364,7 @@ async function captureSceneIntoBuffer(a) {
|
|
|
3242
3364
|
hdrPerf
|
|
3243
3365
|
);
|
|
3244
3366
|
} else {
|
|
3245
|
-
blitHdrVideoLayer(
|
|
3367
|
+
await blitHdrVideoLayer(
|
|
3246
3368
|
sceneBuf,
|
|
3247
3369
|
el,
|
|
3248
3370
|
time,
|
|
@@ -3444,7 +3566,7 @@ function isTargetLossError(error) {
|
|
|
3444
3566
|
// src/services/render/stages/captureStreamingStage.ts
|
|
3445
3567
|
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
3446
3568
|
import { tmpdir } from "node:os";
|
|
3447
|
-
import { join as
|
|
3569
|
+
import { join as join9 } from "node:path";
|
|
3448
3570
|
import {
|
|
3449
3571
|
DrawElementVerificationError,
|
|
3450
3572
|
captureFrameToBuffer,
|
|
@@ -3606,11 +3728,11 @@ function createDrainFrameGuard(args) {
|
|
|
3606
3728
|
return buf;
|
|
3607
3729
|
}
|
|
3608
3730
|
if (db < verifyMinDb) {
|
|
3609
|
-
const dumpDir = await mkdtemp(
|
|
3731
|
+
const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
|
|
3610
3732
|
if (dumpDir) {
|
|
3611
3733
|
await Promise.all([
|
|
3612
|
-
writeFile(
|
|
3613
|
-
writeFile(
|
|
3734
|
+
writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
|
|
3735
|
+
writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
|
|
3614
3736
|
]).catch(() => {
|
|
3615
3737
|
});
|
|
3616
3738
|
}
|
|
@@ -3629,11 +3751,21 @@ function createDrainFrameGuard(args) {
|
|
|
3629
3751
|
return buf;
|
|
3630
3752
|
};
|
|
3631
3753
|
}
|
|
3632
|
-
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal) {
|
|
3754
|
+
async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
|
|
3633
3755
|
let prev = null;
|
|
3634
3756
|
const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
|
|
3635
3757
|
const guard = createDrainFrameGuard({ log, stats, frameTime });
|
|
3636
|
-
const guardFrame = (
|
|
3758
|
+
const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
|
|
3759
|
+
const captureRisk = async (idx) => {
|
|
3760
|
+
await risk.open();
|
|
3761
|
+
lastProgressAt = Date.now();
|
|
3762
|
+
return raceAgainstStall(
|
|
3763
|
+
risk.capture(idx, frameTime(idx)),
|
|
3764
|
+
stallTimeoutMs - (Date.now() - lastProgressAt),
|
|
3765
|
+
{ session, frameIndex: idx, totalFrames, stallTimeoutMs },
|
|
3766
|
+
abortSignal
|
|
3767
|
+
);
|
|
3768
|
+
};
|
|
3637
3769
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
3638
3770
|
let lastProgressAt = Date.now();
|
|
3639
3771
|
const captureWithStallGuard = (idx, promise) => raceAgainstStall(
|
|
@@ -3645,7 +3777,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3645
3777
|
const drainPrev = async () => {
|
|
3646
3778
|
if (!prev) return;
|
|
3647
3779
|
assertNotAborted();
|
|
3648
|
-
const buf = await guardFrame(prev
|
|
3780
|
+
const buf = await guardFrame(prev, await prev.encodeResult);
|
|
3649
3781
|
await reorderBuffer.waitForFrame(prev.idx);
|
|
3650
3782
|
ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
|
|
3651
3783
|
reorderBuffer.advanceTo(prev.idx + 1);
|
|
@@ -3665,7 +3797,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3665
3797
|
const drainBatch = async (batch) => {
|
|
3666
3798
|
for (const item of batch) {
|
|
3667
3799
|
assertNotAborted();
|
|
3668
|
-
const buf = await guardFrame(item
|
|
3800
|
+
const buf = await guardFrame(item, await item.encodeResult);
|
|
3669
3801
|
await reorderBuffer.waitForFrame(item.idx);
|
|
3670
3802
|
ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
|
|
3671
3803
|
reorderBuffer.advanceTo(item.idx + 1);
|
|
@@ -3683,7 +3815,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3683
3815
|
};
|
|
3684
3816
|
if (batchN > 1 && !session.onBeforeCapture) {
|
|
3685
3817
|
const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
|
|
3686
|
-
const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f));
|
|
3818
|
+
const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
|
|
3687
3819
|
let prevBatch = [];
|
|
3688
3820
|
let i = 0;
|
|
3689
3821
|
while (i < totalFrames) {
|
|
@@ -3700,6 +3832,11 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3700
3832
|
);
|
|
3701
3833
|
await drainBatch(prevBatch);
|
|
3702
3834
|
prevBatch = results.map((r) => ({ idx: r.frameIndex, encodeResult: r.encodeResult }));
|
|
3835
|
+
} else if (risk?.frames.has(i)) {
|
|
3836
|
+
const buffer = await captureRisk(i);
|
|
3837
|
+
await drainBatch(prevBatch);
|
|
3838
|
+
prevBatch = [{ idx: i, encodeResult: Promise.resolve(buffer), trusted: true }];
|
|
3839
|
+
i++;
|
|
3703
3840
|
} else {
|
|
3704
3841
|
const { encodeResult } = await captureWithStallGuard(
|
|
3705
3842
|
i,
|
|
@@ -3715,6 +3852,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
|
|
|
3715
3852
|
}
|
|
3716
3853
|
for (let i = 0; i < totalFrames; i++) {
|
|
3717
3854
|
assertNotAborted();
|
|
3855
|
+
if (risk?.frames.has(i)) {
|
|
3856
|
+
const buffer = await captureRisk(i);
|
|
3857
|
+
await drainPrev();
|
|
3858
|
+
prev = { idx: i, encodeResult: Promise.resolve(buffer), trusted: true };
|
|
3859
|
+
continue;
|
|
3860
|
+
}
|
|
3718
3861
|
const time = frameTime(i);
|
|
3719
3862
|
const { encodeResult } = await captureWithStallGuard(
|
|
3720
3863
|
i,
|
|
@@ -3961,6 +4104,24 @@ async function runCaptureStreamingStage(input) {
|
|
|
3961
4104
|
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
3962
4105
|
probeSession = null;
|
|
3963
4106
|
}
|
|
4107
|
+
let riskSession = null;
|
|
4108
|
+
const openRiskSession = async () => {
|
|
4109
|
+
const opened = await createCaptureSession(
|
|
4110
|
+
fileServer.url,
|
|
4111
|
+
framesDir,
|
|
4112
|
+
buildCaptureOptions(),
|
|
4113
|
+
createRenderVideoFrameInjector(),
|
|
4114
|
+
{
|
|
4115
|
+
...captureCfg,
|
|
4116
|
+
useDrawElement: false,
|
|
4117
|
+
staticFrameDedup: false,
|
|
4118
|
+
enableBrowserPool: false
|
|
4119
|
+
}
|
|
4120
|
+
);
|
|
4121
|
+
riskSession = opened;
|
|
4122
|
+
await initializeSession(opened);
|
|
4123
|
+
return opened;
|
|
4124
|
+
};
|
|
3964
4125
|
try {
|
|
3965
4126
|
if (!session.isInitialized) {
|
|
3966
4127
|
await initializeSession(session);
|
|
@@ -3975,6 +4136,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
3975
4136
|
blankDeterministicAccepts: 0,
|
|
3976
4137
|
blankRecaptures: 0
|
|
3977
4138
|
};
|
|
4139
|
+
const riskFrames = session.deRiskFrames;
|
|
3978
4140
|
await runWorkerEncodePipelineLoop(
|
|
3979
4141
|
session,
|
|
3980
4142
|
totalFrames,
|
|
@@ -3985,7 +4147,16 @@ async function runCaptureStreamingStage(input) {
|
|
|
3985
4147
|
onProgress,
|
|
3986
4148
|
log,
|
|
3987
4149
|
deDrainStats,
|
|
3988
|
-
abortSignal
|
|
4150
|
+
abortSignal,
|
|
4151
|
+
riskFrames && riskFrames.size > 0 ? {
|
|
4152
|
+
frames: riskFrames,
|
|
4153
|
+
// Opened on the first risk frame, in a browser of its own: pages sharing a
|
|
4154
|
+
// browser share a compositor, which starves the drawElement page's paint events.
|
|
4155
|
+
open: async () => {
|
|
4156
|
+
riskSession ??= await openRiskSession();
|
|
4157
|
+
},
|
|
4158
|
+
capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
|
|
4159
|
+
} : void 0
|
|
3989
4160
|
);
|
|
3990
4161
|
} else {
|
|
3991
4162
|
const stallTimeoutMs = resolveCaptureStallTimeoutMs();
|
|
@@ -4023,6 +4194,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4023
4194
|
} finally {
|
|
4024
4195
|
lastBrowserConsole = session.browserConsoleBuffer;
|
|
4025
4196
|
await closeCaptureSession(session);
|
|
4197
|
+
if (riskSession) await closeCaptureSession(riskSession);
|
|
4026
4198
|
}
|
|
4027
4199
|
}
|
|
4028
4200
|
const encodeFrom = job.progress;
|
|
@@ -4059,7 +4231,7 @@ async function runCaptureStreamingStage(input) {
|
|
|
4059
4231
|
|
|
4060
4232
|
// src/services/render/stages/captureSegmentedStage.ts
|
|
4061
4233
|
function segmentOutputPath(segmentDir, index) {
|
|
4062
|
-
return
|
|
4234
|
+
return join10(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
|
|
4063
4235
|
}
|
|
4064
4236
|
function segmentEncoderOptions(base, segment) {
|
|
4065
4237
|
return {
|
|
@@ -4135,7 +4307,7 @@ async function captureSegment(ctx, segment) {
|
|
|
4135
4307
|
path: segmentPath,
|
|
4136
4308
|
// Recorded so resume can prove the file on disk is the one that
|
|
4137
4309
|
// finished; absent only in unit tests, whose fake encoder writes none.
|
|
4138
|
-
bytes: existsSync6(segmentPath) ?
|
|
4310
|
+
bytes: existsSync6(segmentPath) ? statSync3(segmentPath).size : 0
|
|
4139
4311
|
});
|
|
4140
4312
|
return encodeResult.durationMs;
|
|
4141
4313
|
} finally {
|
|
@@ -4153,7 +4325,7 @@ var DEFAULT_DEPS = {
|
|
|
4153
4325
|
// is invisible to a test that swaps the engine module after this file has
|
|
4154
4326
|
// been evaluated (bun updates live bindings, not copies).
|
|
4155
4327
|
closeSession: (session) => closeCaptureSession2(session),
|
|
4156
|
-
removeFile: (path) =>
|
|
4328
|
+
removeFile: (path) => rmSync6(path, { force: true })
|
|
4157
4329
|
};
|
|
4158
4330
|
function defaultSessionFactory(input) {
|
|
4159
4331
|
let probeSession = input.probeSession;
|
|
@@ -4182,7 +4354,7 @@ function openSegmentedSessionReuse(input, session) {
|
|
|
4182
4354
|
async function openSegmentedSession(input) {
|
|
4183
4355
|
const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
|
|
4184
4356
|
const videoInjector = input.createRenderVideoFrameInjector();
|
|
4185
|
-
const captureOptions = input.buildCaptureOptions();
|
|
4357
|
+
const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
|
|
4186
4358
|
const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
|
|
4187
4359
|
parallel: (input.workerCount ?? 1) > 1
|
|
4188
4360
|
});
|
|
@@ -4320,7 +4492,7 @@ async function runCaptureSegmentedStage(input) {
|
|
|
4320
4492
|
const { log, dedupPerfs, assertNotAborted } = input;
|
|
4321
4493
|
const deps = { ...DEFAULT_DEPS, ...input.deps };
|
|
4322
4494
|
const segments = planSegments(input.totalFrames, input.segmentFrames);
|
|
4323
|
-
const segmentDir = input.segmentDir ??
|
|
4495
|
+
const segmentDir = input.segmentDir ?? join10(input.workDir, "segments");
|
|
4324
4496
|
mkdirSync3(segmentDir, { recursive: true });
|
|
4325
4497
|
const skip = input.completedSegments ?? /* @__PURE__ */ new Set();
|
|
4326
4498
|
const pending = segments.filter((s) => !skip.has(s.index));
|
|
@@ -4391,10 +4563,83 @@ async function runCaptureSegmentedStage(input) {
|
|
|
4391
4563
|
};
|
|
4392
4564
|
}
|
|
4393
4565
|
|
|
4566
|
+
// src/services/render/renderEtaGuard.ts
|
|
4567
|
+
var RENDER_TOO_SLOW = "RENDER_TOO_SLOW";
|
|
4568
|
+
var ETA_MARGIN = 1.2;
|
|
4569
|
+
var MIN_SAMPLE_MS = 6e4;
|
|
4570
|
+
var MIN_SAMPLE_FRAMES = 3;
|
|
4571
|
+
var POLL_MS = 5e3;
|
|
4572
|
+
var RenderTooSlowError = class extends Error {
|
|
4573
|
+
code = RENDER_TOO_SLOW;
|
|
4574
|
+
// The producer server reads these off the error for the wire envelope.
|
|
4575
|
+
// fallow-ignore-next-line unused-class-member
|
|
4576
|
+
owner = "user";
|
|
4577
|
+
// fallow-ignore-next-line unused-class-member
|
|
4578
|
+
retryable = false;
|
|
4579
|
+
publicMetadata;
|
|
4580
|
+
constructor(stage, input) {
|
|
4581
|
+
const { framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs } = input;
|
|
4582
|
+
super(
|
|
4583
|
+
`render too slow: stage ${stage}, ${framesCompleted}/${totalFrames} frames, ${Math.round(msPerFrame)} ms/frame, ETA ${etaMs} ms exceeds maxRenderMs ${maxRenderMs}`
|
|
4584
|
+
);
|
|
4585
|
+
this.name = "RenderTooSlowError";
|
|
4586
|
+
this.publicMetadata = { stage, framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs };
|
|
4587
|
+
}
|
|
4588
|
+
};
|
|
4589
|
+
function checkRenderEta(stage, input) {
|
|
4590
|
+
const { maxRenderMs, elapsedMs, framesCompleted, totalFrames, msPerFrame } = input;
|
|
4591
|
+
if (!maxRenderMs || maxRenderMs <= 0 || !(msPerFrame > 0)) return void 0;
|
|
4592
|
+
const etaMs = Math.round(elapsedMs + (totalFrames - framesCompleted) * msPerFrame);
|
|
4593
|
+
if (etaMs <= maxRenderMs * ETA_MARGIN) return void 0;
|
|
4594
|
+
return new RenderTooSlowError(stage, { ...input, maxRenderMs, etaMs });
|
|
4595
|
+
}
|
|
4596
|
+
function createRateSampler() {
|
|
4597
|
+
let base;
|
|
4598
|
+
let lastFrames = 0;
|
|
4599
|
+
return {
|
|
4600
|
+
observe(done, at) {
|
|
4601
|
+
if (done === 0 || done < lastFrames) base = void 0;
|
|
4602
|
+
lastFrames = done;
|
|
4603
|
+
if (done === 0) return void 0;
|
|
4604
|
+
base ??= { at, frames: done };
|
|
4605
|
+
const sampleMs = at - base.at;
|
|
4606
|
+
const sampleFrames = done - base.frames;
|
|
4607
|
+
if (sampleMs < MIN_SAMPLE_MS || sampleFrames < MIN_SAMPLE_FRAMES) return void 0;
|
|
4608
|
+
return sampleMs / sampleFrames;
|
|
4609
|
+
}
|
|
4610
|
+
};
|
|
4611
|
+
}
|
|
4612
|
+
function startRenderEtaGuard(options) {
|
|
4613
|
+
const { maxRenderMs, job, totalFrames, pipelineStartMs, onTooSlow, now = Date.now } = options;
|
|
4614
|
+
if (!maxRenderMs || maxRenderMs <= 0) return () => {
|
|
4615
|
+
};
|
|
4616
|
+
const sampler = createRateSampler();
|
|
4617
|
+
const stop = () => clearInterval(timer);
|
|
4618
|
+
const timer = setInterval(() => {
|
|
4619
|
+
const done = job.framesRendered ?? 0;
|
|
4620
|
+
if (done >= totalFrames) return stop();
|
|
4621
|
+
const at = now();
|
|
4622
|
+
const msPerFrame = sampler.observe(done, at);
|
|
4623
|
+
if (msPerFrame === void 0) return;
|
|
4624
|
+
const error = checkRenderEta("capture", {
|
|
4625
|
+
maxRenderMs,
|
|
4626
|
+
elapsedMs: at - pipelineStartMs,
|
|
4627
|
+
framesCompleted: done,
|
|
4628
|
+
totalFrames,
|
|
4629
|
+
msPerFrame
|
|
4630
|
+
});
|
|
4631
|
+
if (!error) return;
|
|
4632
|
+
stop();
|
|
4633
|
+
onTooSlow(error);
|
|
4634
|
+
}, options.pollMs ?? POLL_MS);
|
|
4635
|
+
timer.unref?.();
|
|
4636
|
+
return stop;
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4394
4639
|
// src/services/render/segmentManifest.ts
|
|
4395
4640
|
import { createHash as createHash2 } from "node:crypto";
|
|
4396
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as
|
|
4397
|
-
import { join as
|
|
4641
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4642
|
+
import { join as join11 } from "node:path";
|
|
4398
4643
|
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
4399
4644
|
var MANIFEST_FILENAME = "segments.json";
|
|
4400
4645
|
function computeSegmentPlanHash(input) {
|
|
@@ -4420,7 +4665,7 @@ function computeSegmentPlanHash(input) {
|
|
|
4420
4665
|
return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
|
|
4421
4666
|
}
|
|
4422
4667
|
function segmentDirFor(projectRendersDir, planHash) {
|
|
4423
|
-
return
|
|
4668
|
+
return join11(projectRendersDir, ".hf-segments", planHash);
|
|
4424
4669
|
}
|
|
4425
4670
|
function hasNumber(value, key) {
|
|
4426
4671
|
return key in value && typeof Reflect.get(value, key) === "number";
|
|
@@ -4438,7 +4683,7 @@ function isManifest(value) {
|
|
|
4438
4683
|
return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
|
|
4439
4684
|
}
|
|
4440
4685
|
function readSegmentManifest(segmentDir) {
|
|
4441
|
-
const path =
|
|
4686
|
+
const path = join11(segmentDir, MANIFEST_FILENAME);
|
|
4442
4687
|
if (!existsSync7(path)) return null;
|
|
4443
4688
|
try {
|
|
4444
4689
|
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
@@ -4449,9 +4694,9 @@ function readSegmentManifest(segmentDir) {
|
|
|
4449
4694
|
}
|
|
4450
4695
|
function writeSegmentManifest(segmentDir, manifest) {
|
|
4451
4696
|
mkdirSync4(segmentDir, { recursive: true });
|
|
4452
|
-
const tmp =
|
|
4697
|
+
const tmp = join11(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
|
|
4453
4698
|
writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
|
|
4454
|
-
renameWithRetry2(tmp,
|
|
4699
|
+
renameWithRetry2(tmp, join11(segmentDir, MANIFEST_FILENAME));
|
|
4455
4700
|
}
|
|
4456
4701
|
async function probeSegmentFrameCount(path, probe) {
|
|
4457
4702
|
try {
|
|
@@ -4469,7 +4714,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
|
|
|
4469
4714
|
if (manifest.planHash !== expectedHash) return ok;
|
|
4470
4715
|
for (const entry of manifest.completed) {
|
|
4471
4716
|
if (!existsSync7(entry.path)) continue;
|
|
4472
|
-
const size =
|
|
4717
|
+
const size = statSync4(entry.path).size;
|
|
4473
4718
|
if (size <= 0 || size !== entry.bytes) continue;
|
|
4474
4719
|
const frames = await probeFrames(entry.path);
|
|
4475
4720
|
if (frames !== entry.endFrame - entry.startFrame) continue;
|
|
@@ -4563,7 +4808,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
|
|
|
4563
4808
|
}
|
|
4564
4809
|
|
|
4565
4810
|
// src/services/render/captureCost.ts
|
|
4566
|
-
import { join as
|
|
4811
|
+
import { join as join12 } from "node:path";
|
|
4567
4812
|
import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
|
|
4568
4813
|
import {
|
|
4569
4814
|
calculateOptimalWorkers,
|
|
@@ -4808,7 +5053,7 @@ async function runCaptureCalibration(input) {
|
|
|
4808
5053
|
});
|
|
4809
5054
|
let calibration;
|
|
4810
5055
|
try {
|
|
4811
|
-
calibration = await runOneCalibration(
|
|
5056
|
+
calibration = await runOneCalibration(join12(workDir, "capture-calibration"), calibrationCfg);
|
|
4812
5057
|
} catch (error) {
|
|
4813
5058
|
const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
|
|
4814
5059
|
if (!shouldFallback) {
|
|
@@ -4841,7 +5086,7 @@ async function runCaptureCalibration(input) {
|
|
|
4841
5086
|
const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
|
|
4842
5087
|
try {
|
|
4843
5088
|
calibration = await runOneCalibration(
|
|
4844
|
-
|
|
5089
|
+
join12(workDir, "capture-calibration-screenshot"),
|
|
4845
5090
|
screenshotCfg
|
|
4846
5091
|
);
|
|
4847
5092
|
} catch (fallbackError) {
|
|
@@ -5273,14 +5518,14 @@ function countAuthoredTimedClips(html) {
|
|
|
5273
5518
|
}
|
|
5274
5519
|
|
|
5275
5520
|
// src/services/render/stages/compileStage.ts
|
|
5276
|
-
import { join as
|
|
5521
|
+
import { join as join15 } from "node:path";
|
|
5277
5522
|
import { suggestMatchingPreset } from "@hyperframes/core";
|
|
5278
5523
|
|
|
5279
5524
|
// src/services/htmlCompiler.ts
|
|
5280
5525
|
import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
|
|
5281
5526
|
import { createHash as createHash5 } from "node:crypto";
|
|
5282
5527
|
import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync6 } from "fs";
|
|
5283
|
-
import { join as
|
|
5528
|
+
import { join as join14, dirname as dirname6, resolve as resolve6, basename as basename4, relative as relative2 } from "path";
|
|
5284
5529
|
import { parseHTML as parseHTML4 } from "linkedom";
|
|
5285
5530
|
import {
|
|
5286
5531
|
COMPOSITION_SOURCE_URL,
|
|
@@ -5507,8 +5752,8 @@ import {
|
|
|
5507
5752
|
|
|
5508
5753
|
// src/services/animatedGifPrep.ts
|
|
5509
5754
|
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
|
|
5755
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync7, statSync as statSync5 } from "node:fs";
|
|
5756
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, join as join13, resolve as resolve5 } from "node:path";
|
|
5512
5757
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
5513
5758
|
import { parseAnimatedGifMetadata } from "@hyperframes/core";
|
|
5514
5759
|
import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
|
|
@@ -5557,7 +5802,7 @@ function resolveGifSourcePath(src, options) {
|
|
|
5557
5802
|
}
|
|
5558
5803
|
function isUsableFile(path) {
|
|
5559
5804
|
try {
|
|
5560
|
-
const stat =
|
|
5805
|
+
const stat = statSync5(path);
|
|
5561
5806
|
return stat.isFile() && stat.size > 0;
|
|
5562
5807
|
} catch {
|
|
5563
5808
|
return false;
|
|
@@ -5692,10 +5937,10 @@ async function ensurePreparedWebm(input) {
|
|
|
5692
5937
|
if (!isUsableFile(input.cachePath)) {
|
|
5693
5938
|
renameWithRetry3(tmpPath, input.cachePath);
|
|
5694
5939
|
} else {
|
|
5695
|
-
|
|
5940
|
+
rmSync7(tmpPath, { force: true });
|
|
5696
5941
|
}
|
|
5697
5942
|
} catch (error) {
|
|
5698
|
-
|
|
5943
|
+
rmSync7(tmpPath, { force: true });
|
|
5699
5944
|
throw error;
|
|
5700
5945
|
}
|
|
5701
5946
|
}
|
|
@@ -5751,7 +5996,7 @@ function replaceImageWithVideo(input) {
|
|
|
5751
5996
|
return video;
|
|
5752
5997
|
}
|
|
5753
5998
|
async function prepareAnimatedGifInputs(html, options) {
|
|
5754
|
-
const outputDir = options.outputDir ??
|
|
5999
|
+
const outputDir = options.outputDir ?? join13(options.downloadDir, PREPARED_GIF_SUBDIR);
|
|
5755
6000
|
const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
|
|
5756
6001
|
const cacheDir = options.cacheDir ?? outputDir;
|
|
5757
6002
|
const { document: document2 } = parseHTML3(html);
|
|
@@ -5773,8 +6018,8 @@ async function prepareAnimatedGifInputs(html, options) {
|
|
|
5773
6018
|
const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
|
|
5774
6019
|
const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
|
|
5775
6020
|
const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
|
|
5776
|
-
const cachePath =
|
|
5777
|
-
const outputPath =
|
|
6021
|
+
const cachePath = join13(cacheDir, filename);
|
|
6022
|
+
const outputPath = join13(outputDir, filename);
|
|
5778
6023
|
const outputSrc = `${outputSrcPrefix}/${filename}`;
|
|
5779
6024
|
await ensurePreparedWebm({
|
|
5780
6025
|
sourcePath,
|
|
@@ -6125,10 +6370,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
|
|
|
6125
6370
|
function detectThreeDTransformUsage(html) {
|
|
6126
6371
|
return THREE_D_CONTEXT_PATTERN.test(html);
|
|
6127
6372
|
}
|
|
6128
|
-
var MIX_BLEND_MODE_PATTERN = /mix-blend-mode\s*:/i;
|
|
6129
|
-
function detectMixBlendModeUsage(html) {
|
|
6130
|
-
return MIX_BLEND_MODE_PATTERN.test(html);
|
|
6131
|
-
}
|
|
6132
6373
|
var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
|
|
6133
6374
|
function detectAncestorBackgroundImage(html) {
|
|
6134
6375
|
const { document: document2 } = parseHTML4(html);
|
|
@@ -6188,7 +6429,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
|
|
|
6188
6429
|
}
|
|
6189
6430
|
} else {
|
|
6190
6431
|
filePath = decodedUrlPath(src);
|
|
6191
|
-
if (!filePath.startsWith("/")) filePath =
|
|
6432
|
+
if (!filePath.startsWith("/")) filePath = join14(baseDir, filePath);
|
|
6192
6433
|
filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
|
|
6193
6434
|
}
|
|
6194
6435
|
if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
|
|
@@ -6808,7 +7049,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
|
|
|
6808
7049
|
return downloadAndRewriteUrls(
|
|
6809
7050
|
urlSet,
|
|
6810
7051
|
html,
|
|
6811
|
-
|
|
7052
|
+
join14(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
6812
7053
|
"Remote media download failed",
|
|
6813
7054
|
"Localized remote media source(s)"
|
|
6814
7055
|
);
|
|
@@ -6823,7 +7064,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
|
|
|
6823
7064
|
return downloadAndRewriteUrls(
|
|
6824
7065
|
urlSet,
|
|
6825
7066
|
html,
|
|
6826
|
-
|
|
7067
|
+
join14(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
6827
7068
|
"Remote image download failed",
|
|
6828
7069
|
"Localized remote image source(s)"
|
|
6829
7070
|
);
|
|
@@ -6839,7 +7080,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
|
|
|
6839
7080
|
return downloadAndRewriteUrls(
|
|
6840
7081
|
urlSet,
|
|
6841
7082
|
html,
|
|
6842
|
-
|
|
7083
|
+
join14(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
6843
7084
|
"Remote background-image download failed",
|
|
6844
7085
|
"Localized remote background-image(s)",
|
|
6845
7086
|
// Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
|
|
@@ -6974,7 +7215,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
|
|
|
6974
7215
|
return downloadAndRewriteUrls(
|
|
6975
7216
|
urlSet,
|
|
6976
7217
|
processed,
|
|
6977
|
-
|
|
7218
|
+
join14(downloadDir, REMOTE_MEDIA_SUBDIR),
|
|
6978
7219
|
"Remote font download failed",
|
|
6979
7220
|
"Localized remote font face(s)",
|
|
6980
7221
|
(h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
|
|
@@ -7141,7 +7382,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7141
7382
|
const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
|
|
7142
7383
|
const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
|
|
7143
7384
|
const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
|
|
7144
|
-
const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
|
|
7145
7385
|
const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
|
|
7146
7386
|
const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
|
|
7147
7387
|
injectTextRenderingRule(
|
|
@@ -7255,7 +7495,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7255
7495
|
renderModeHints,
|
|
7256
7496
|
hasShaderTransitions,
|
|
7257
7497
|
usesThreeDTransforms,
|
|
7258
|
-
usesMixBlendMode,
|
|
7259
7498
|
hasAncestorBackgroundImage
|
|
7260
7499
|
};
|
|
7261
7500
|
}
|
|
@@ -7641,7 +7880,7 @@ async function runCompileStage(input) {
|
|
|
7641
7880
|
abortSignal
|
|
7642
7881
|
} = input;
|
|
7643
7882
|
const compileStart = Date.now();
|
|
7644
|
-
const compiled = await compileForRender(projectDir, htmlPath,
|
|
7883
|
+
const compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"), {
|
|
7645
7884
|
log,
|
|
7646
7885
|
failClosedFontFetch: failClosedFontFetch === true,
|
|
7647
7886
|
allowSystemFontCapture,
|
|
@@ -7649,7 +7888,7 @@ async function runCompileStage(input) {
|
|
|
7649
7888
|
variables: input.variables,
|
|
7650
7889
|
urlByAbsentLocalPath: input.urlByAbsentLocalPath,
|
|
7651
7890
|
pageMotionBlur: input.pageMotionBlur,
|
|
7652
|
-
animatedGifCacheDir: cfg.extractCacheDir ?
|
|
7891
|
+
animatedGifCacheDir: cfg.extractCacheDir ? join15(cfg.extractCacheDir, "animated-gif") : void 0,
|
|
7653
7892
|
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
|
|
7654
7893
|
});
|
|
7655
7894
|
assertNotAborted();
|
|
@@ -7662,13 +7901,6 @@ async function runCompileStage(input) {
|
|
|
7662
7901
|
"[Render] Fast capture: composition uses a CSS 3D rendering context (perspective / preserve-3d / backface-visibility), so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
7663
7902
|
);
|
|
7664
7903
|
}
|
|
7665
|
-
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
|
|
7666
|
-
cfg.useDrawElement = false;
|
|
7667
|
-
deCompileGate = "mix_blend_mode";
|
|
7668
|
-
log.info(
|
|
7669
|
-
"[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
|
|
7670
|
-
);
|
|
7671
|
-
}
|
|
7672
7904
|
if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
|
|
7673
7905
|
cfg.useDrawElement = false;
|
|
7674
7906
|
deCompileGate = "ancestor_background_image";
|
|
@@ -7712,7 +7944,7 @@ async function runCompileStage(input) {
|
|
|
7712
7944
|
};
|
|
7713
7945
|
await preflightCompositionAssetMediaTypes({
|
|
7714
7946
|
projectDir,
|
|
7715
|
-
compiledDir:
|
|
7947
|
+
compiledDir: join15(workDir, "compiled"),
|
|
7716
7948
|
composition,
|
|
7717
7949
|
signal: abortSignal
|
|
7718
7950
|
});
|
|
@@ -7757,7 +7989,7 @@ async function runCompileStage(input) {
|
|
|
7757
7989
|
}
|
|
7758
7990
|
|
|
7759
7991
|
// src/services/render/stages/probeStage.ts
|
|
7760
|
-
import { join as
|
|
7992
|
+
import { join as join16 } from "node:path";
|
|
7761
7993
|
import { parseHTML as parseHTML5 } from "linkedom";
|
|
7762
7994
|
import {
|
|
7763
7995
|
closeCaptureSession as closeCaptureSession4,
|
|
@@ -7949,7 +8181,7 @@ async function runProbeStage(input) {
|
|
|
7949
8181
|
});
|
|
7950
8182
|
fileServer = await createFileServer({
|
|
7951
8183
|
projectDir,
|
|
7952
|
-
compiledDir:
|
|
8184
|
+
compiledDir: join16(workDir, "compiled"),
|
|
7953
8185
|
port: 0,
|
|
7954
8186
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
7955
8187
|
fps: job.config.fps
|
|
@@ -7977,7 +8209,7 @@ async function runProbeStage(input) {
|
|
|
7977
8209
|
log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
|
|
7978
8210
|
probeSession = await createCaptureSession4(
|
|
7979
8211
|
fileServer.url,
|
|
7980
|
-
|
|
8212
|
+
join16(workDir, "probe"),
|
|
7981
8213
|
captureOpts,
|
|
7982
8214
|
null,
|
|
7983
8215
|
probeCfg
|
|
@@ -8062,7 +8294,7 @@ async function runProbeStage(input) {
|
|
|
8062
8294
|
});
|
|
8063
8295
|
probeSession = await createCaptureSession4(
|
|
8064
8296
|
fileServer.url,
|
|
8065
|
-
|
|
8297
|
+
join16(workDir, "probe-screenshot"),
|
|
8066
8298
|
captureOpts,
|
|
8067
8299
|
null,
|
|
8068
8300
|
{ ...probeCfg, forceScreenshot: true }
|
|
@@ -8098,7 +8330,7 @@ async function runProbeStage(input) {
|
|
|
8098
8330
|
compiled,
|
|
8099
8331
|
resolutions,
|
|
8100
8332
|
projectDir,
|
|
8101
|
-
|
|
8333
|
+
join16(workDir, "downloads")
|
|
8102
8334
|
);
|
|
8103
8335
|
assertNotAborted();
|
|
8104
8336
|
composition.videos = compiled.videos;
|
|
@@ -8289,7 +8521,7 @@ async function runProbeStage(input) {
|
|
|
8289
8521
|
}
|
|
8290
8522
|
await preflightCompositionAssetMediaTypes({
|
|
8291
8523
|
projectDir,
|
|
8292
|
-
compiledDir:
|
|
8524
|
+
compiledDir: join16(workDir, "compiled"),
|
|
8293
8525
|
composition,
|
|
8294
8526
|
signal: abortSignal
|
|
8295
8527
|
});
|
|
@@ -8423,7 +8655,7 @@ function validateRenderDuration(input) {
|
|
|
8423
8655
|
|
|
8424
8656
|
// src/services/render/stages/extractVideosStage.ts
|
|
8425
8657
|
import { existsSync as existsSync11 } from "node:fs";
|
|
8426
|
-
import { join as
|
|
8658
|
+
import { join as join17 } from "node:path";
|
|
8427
8659
|
import {
|
|
8428
8660
|
classifyVideoExtractionError,
|
|
8429
8661
|
createFrameLookupTable,
|
|
@@ -8738,14 +8970,14 @@ async function runExtractVideosStage(input) {
|
|
|
8738
8970
|
// because short boundary counts can differ by one frame.
|
|
8739
8971
|
{
|
|
8740
8972
|
fps: job.config.fps,
|
|
8741
|
-
outputDir:
|
|
8973
|
+
outputDir: join17(compiledDir, "__hyperframes_video_frames"),
|
|
8742
8974
|
format: job.config.videoFrameFormat ?? "auto",
|
|
8743
8975
|
toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
|
|
8744
8976
|
timelineEnd: composition.duration,
|
|
8745
8977
|
maxTransientRetries: extractionPolicy.maxTransientRetries,
|
|
8746
8978
|
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
|
8747
8979
|
deferRangeExtraction,
|
|
8748
|
-
contentKeyedDirs: [
|
|
8980
|
+
contentKeyedDirs: [join17(compiledDir, REMOTE_MEDIA_SUBDIR)],
|
|
8749
8981
|
urlByAbsentLocalPath
|
|
8750
8982
|
},
|
|
8751
8983
|
abortSignal,
|
|
@@ -8812,8 +9044,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
8812
9044
|
}
|
|
8813
9045
|
|
|
8814
9046
|
// src/services/render/stages/audioStage.ts
|
|
8815
|
-
import { join as
|
|
8816
|
-
import { rmSync as
|
|
9047
|
+
import { join as join19 } from "node:path";
|
|
9048
|
+
import { rmSync as rmSync9 } from "node:fs";
|
|
8817
9049
|
import {
|
|
8818
9050
|
MIXED_AUDIO_FILENAME,
|
|
8819
9051
|
audioFailureDiagnostics,
|
|
@@ -8822,8 +9054,8 @@ import {
|
|
|
8822
9054
|
|
|
8823
9055
|
// src/services/render/audioPadTrim.ts
|
|
8824
9056
|
import { spawn } from "node:child_process";
|
|
8825
|
-
import { mkdtempSync as mkdtempSync3, rmSync as
|
|
8826
|
-
import { dirname as dirname7, join as
|
|
9057
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
|
|
9058
|
+
import { dirname as dirname7, join as join18 } from "node:path";
|
|
8827
9059
|
import {
|
|
8828
9060
|
DEFAULT_CONFIG,
|
|
8829
9061
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
@@ -9026,7 +9258,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
9026
9258
|
error: `audioPadTrim: failed to materialize ${plan.operation}: ${err instanceof Error ? err.message : String(err)}`
|
|
9027
9259
|
};
|
|
9028
9260
|
} finally {
|
|
9029
|
-
for (const path of plan.cleanupPaths)
|
|
9261
|
+
for (const path of plan.cleanupPaths) rmSync8(path, { force: true });
|
|
9030
9262
|
}
|
|
9031
9263
|
let loweredDb = 0;
|
|
9032
9264
|
if (probeTruePeak) {
|
|
@@ -9063,19 +9295,21 @@ async function constrainAacTruePeak(input) {
|
|
|
9063
9295
|
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
9064
9296
|
return enforceAacTruePeak({
|
|
9065
9297
|
audioPath: input.audioPath,
|
|
9298
|
+
sourcePath: input.sourcePath,
|
|
9066
9299
|
targetDurationSeconds: input.targetDurationSeconds,
|
|
9067
9300
|
signal: input.signal,
|
|
9068
|
-
probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
|
|
9069
|
-
encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
9301
|
+
probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
|
|
9302
|
+
encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
9070
9303
|
});
|
|
9071
9304
|
}
|
|
9072
9305
|
async function enforceAacTruePeak(input) {
|
|
9073
9306
|
let scratchDir;
|
|
9074
9307
|
try {
|
|
9075
|
-
scratchDir = mkdtempSync3(
|
|
9076
|
-
const correctedPath =
|
|
9308
|
+
scratchDir = mkdtempSync3(join18(dirname7(input.audioPath), ".true-peak-"));
|
|
9309
|
+
const correctedPath = join18(scratchDir, "audio.m4a");
|
|
9077
9310
|
const measurements = [];
|
|
9078
9311
|
let attenuationDb = 0;
|
|
9312
|
+
let sourcePeakDbfs;
|
|
9079
9313
|
let measuredPath = input.audioPath;
|
|
9080
9314
|
for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
|
|
9081
9315
|
const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
|
|
@@ -9092,7 +9326,11 @@ async function enforceAacTruePeak(input) {
|
|
|
9092
9326
|
}
|
|
9093
9327
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
9094
9328
|
const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
|
|
9095
|
-
|
|
9329
|
+
if (input.sourcePath && sourcePeakDbfs === void 0) {
|
|
9330
|
+
sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
|
|
9331
|
+
}
|
|
9332
|
+
const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
|
|
9333
|
+
attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
|
|
9096
9334
|
const result = await input.encode(
|
|
9097
9335
|
input.audioPath,
|
|
9098
9336
|
correctedPath,
|
|
@@ -9124,7 +9362,7 @@ async function enforceAacTruePeak(input) {
|
|
|
9124
9362
|
)}`
|
|
9125
9363
|
};
|
|
9126
9364
|
} finally {
|
|
9127
|
-
if (scratchDir)
|
|
9365
|
+
if (scratchDir) rmSync8(scratchDir, { recursive: true, force: true });
|
|
9128
9366
|
}
|
|
9129
9367
|
}
|
|
9130
9368
|
function failResult(outputPath, target, source, error) {
|
|
@@ -9271,7 +9509,7 @@ function assertAudioNotInterrupted(failures) {
|
|
|
9271
9509
|
}
|
|
9272
9510
|
async function runAudioStage(input) {
|
|
9273
9511
|
const stageStart = Date.now();
|
|
9274
|
-
const pcmSourcePath =
|
|
9512
|
+
const pcmSourcePath = join19(input.workDir, "audio-source.wav");
|
|
9275
9513
|
const duration = input.totalFrames * input.fps.den / input.fps.num;
|
|
9276
9514
|
try {
|
|
9277
9515
|
const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
|
|
@@ -9291,7 +9529,7 @@ async function runAudioStage(input) {
|
|
|
9291
9529
|
}
|
|
9292
9530
|
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
9293
9531
|
} finally {
|
|
9294
|
-
|
|
9532
|
+
rmSync9(pcmSourcePath, { force: true });
|
|
9295
9533
|
}
|
|
9296
9534
|
}
|
|
9297
9535
|
async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
@@ -9309,7 +9547,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9309
9547
|
assertNotAborted,
|
|
9310
9548
|
log
|
|
9311
9549
|
} = input;
|
|
9312
|
-
const audioOutputPath =
|
|
9550
|
+
const audioOutputPath = join19(workDir, MIXED_AUDIO_FILENAME);
|
|
9313
9551
|
let hasAudio = false;
|
|
9314
9552
|
let audioError;
|
|
9315
9553
|
let audioFailures;
|
|
@@ -9320,7 +9558,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9320
9558
|
audioResult = await processCompositionAudio(
|
|
9321
9559
|
audios,
|
|
9322
9560
|
projectDir,
|
|
9323
|
-
|
|
9561
|
+
join19(workDir, "audio-work"),
|
|
9324
9562
|
audioOutputPath,
|
|
9325
9563
|
duration,
|
|
9326
9564
|
abortSignal,
|
|
@@ -9389,7 +9627,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9389
9627
|
|
|
9390
9628
|
// src/services/render/stages/captureStage.ts
|
|
9391
9629
|
import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
|
|
9392
|
-
import { join as
|
|
9630
|
+
import { join as join20 } from "node:path";
|
|
9393
9631
|
import {
|
|
9394
9632
|
classifyCaptureFailure as classifyCaptureFailure2,
|
|
9395
9633
|
frameFileExtension,
|
|
@@ -9462,13 +9700,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
|
|
|
9462
9700
|
if (!existsSync12(workDir)) return total;
|
|
9463
9701
|
for (const attempt of readdirSync3(workDir)) {
|
|
9464
9702
|
if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
|
|
9465
|
-
total += sampleDirectoryBytes(
|
|
9703
|
+
total += sampleDirectoryBytes(join20(workDir, attempt));
|
|
9466
9704
|
continue;
|
|
9467
9705
|
}
|
|
9468
9706
|
if (!/^capture-attempt-\d+$/.test(attempt)) continue;
|
|
9469
|
-
const attemptDir =
|
|
9707
|
+
const attemptDir = join20(workDir, attempt);
|
|
9470
9708
|
for (const name of readdirSync3(attemptDir)) {
|
|
9471
|
-
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(
|
|
9709
|
+
if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join20(attemptDir, name));
|
|
9472
9710
|
}
|
|
9473
9711
|
}
|
|
9474
9712
|
return total;
|
|
@@ -9528,7 +9766,7 @@ async function runCaptureStage(input) {
|
|
|
9528
9766
|
);
|
|
9529
9767
|
}
|
|
9530
9768
|
}
|
|
9531
|
-
const captureOptions = buildCaptureOptions();
|
|
9769
|
+
const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
|
|
9532
9770
|
const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
|
|
9533
9771
|
const checkDiskProjection = createDiskCaptureProjection({
|
|
9534
9772
|
framesDir,
|
|
@@ -9755,7 +9993,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
|
|
|
9755
9993
|
|
|
9756
9994
|
// src/services/render/stages/captureHdrStage.ts
|
|
9757
9995
|
import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
|
|
9758
|
-
import { join as
|
|
9996
|
+
import { join as join24 } from "node:path";
|
|
9759
9997
|
import {
|
|
9760
9998
|
closeCaptureSession as closeCaptureSession7,
|
|
9761
9999
|
cloneCaptureWarnings as cloneCaptureWarnings2,
|
|
@@ -9827,7 +10065,7 @@ function createHdrImageTransferCache(options = {}) {
|
|
|
9827
10065
|
}
|
|
9828
10066
|
|
|
9829
10067
|
// src/services/render/stages/captureHdrSequentialLoop.ts
|
|
9830
|
-
import { join as
|
|
10068
|
+
import { join as join21 } from "node:path";
|
|
9831
10069
|
import {
|
|
9832
10070
|
TRANSITIONS,
|
|
9833
10071
|
crossfade
|
|
@@ -9952,7 +10190,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
9952
10190
|
);
|
|
9953
10191
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
9954
10192
|
writeFileExclusiveSync(
|
|
9955
|
-
|
|
10193
|
+
join21(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
9956
10194
|
normalCanvas
|
|
9957
10195
|
);
|
|
9958
10196
|
}
|
|
@@ -9984,7 +10222,7 @@ async function runSequentialLayeredFrameLoop(input) {
|
|
|
9984
10222
|
}
|
|
9985
10223
|
|
|
9986
10224
|
// src/services/render/stages/captureHdrHybridLoop.ts
|
|
9987
|
-
import { join as
|
|
10225
|
+
import { join as join23 } from "node:path";
|
|
9988
10226
|
import {
|
|
9989
10227
|
TRANSITIONS as TRANSITIONS2,
|
|
9990
10228
|
closeCaptureSession as closeCaptureSession6,
|
|
@@ -9998,7 +10236,7 @@ import {
|
|
|
9998
10236
|
// src/services/shaderTransitionWorkerPool.ts
|
|
9999
10237
|
import { Worker } from "node:worker_threads";
|
|
10000
10238
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
10001
|
-
import { dirname as dirname8, join as
|
|
10239
|
+
import { dirname as dirname8, join as join22 } from "node:path";
|
|
10002
10240
|
import { createRequire } from "node:module";
|
|
10003
10241
|
import { existsSync as existsSync13 } from "node:fs";
|
|
10004
10242
|
import { cpus as cpus2 } from "node:os";
|
|
@@ -10012,9 +10250,9 @@ function resolveWorkerEntry(explicit) {
|
|
|
10012
10250
|
return { path: override, isTs };
|
|
10013
10251
|
}
|
|
10014
10252
|
const moduleDir = dirname8(fileURLToPath3(import.meta.url));
|
|
10015
|
-
const jsPath =
|
|
10253
|
+
const jsPath = join22(moduleDir, "shaderTransitionWorker.js");
|
|
10016
10254
|
if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
|
|
10017
|
-
const tsPath =
|
|
10255
|
+
const tsPath = join22(moduleDir, "shaderTransitionWorker.ts");
|
|
10018
10256
|
return { path: tsPath, isTs: true };
|
|
10019
10257
|
}
|
|
10020
10258
|
function buildExecArgv(entryIsTs) {
|
|
@@ -10382,7 +10620,7 @@ async function runHybridLayeredFrameLoop(input) {
|
|
|
10382
10620
|
);
|
|
10383
10621
|
if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
|
|
10384
10622
|
writeFileExclusiveSync(
|
|
10385
|
-
|
|
10623
|
+
join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
|
|
10386
10624
|
canvas
|
|
10387
10625
|
);
|
|
10388
10626
|
}
|
|
@@ -10551,7 +10789,8 @@ async function runCaptureHdrStage(input) {
|
|
|
10551
10789
|
width,
|
|
10552
10790
|
height,
|
|
10553
10791
|
abortSignal,
|
|
10554
|
-
hdrDiagnostics
|
|
10792
|
+
hdrDiagnostics,
|
|
10793
|
+
maxResidentWindows: Math.max(1, workerCount) + 1
|
|
10555
10794
|
});
|
|
10556
10795
|
releaseHdrExtractionReservation = extracted.releaseReservation;
|
|
10557
10796
|
for (const [id, source] of extracted.sources) hdrVideoFrameSources.set(id, source);
|
|
@@ -10569,7 +10808,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10569
10808
|
if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
|
|
10570
10809
|
}
|
|
10571
10810
|
const debugDumpEnabled = process.env.KEEP_TEMP === "1";
|
|
10572
|
-
const debugDumpDir = debugDumpEnabled ?
|
|
10811
|
+
const debugDumpDir = debugDumpEnabled ? join24(framesDir, "debug-composite") : null;
|
|
10573
10812
|
if (debugDumpDir && !existsSync14(debugDumpDir)) {
|
|
10574
10813
|
mkdirSync7(debugDumpDir, { recursive: true });
|
|
10575
10814
|
}
|
|
@@ -10729,12 +10968,12 @@ import {
|
|
|
10729
10968
|
mkdirSync as mkdirSync8,
|
|
10730
10969
|
readdirSync as readdirSync4,
|
|
10731
10970
|
readFileSync as readFileSync7,
|
|
10732
|
-
rmSync as
|
|
10733
|
-
statSync as
|
|
10971
|
+
rmSync as rmSync10,
|
|
10972
|
+
statSync as statSync6,
|
|
10734
10973
|
writeFileSync as writeFileSync4
|
|
10735
10974
|
} from "node:fs";
|
|
10736
10975
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
10737
|
-
import { dirname as dirname9, join as
|
|
10976
|
+
import { dirname as dirname9, join as join25 } from "node:path";
|
|
10738
10977
|
import {
|
|
10739
10978
|
encodeFramesChunkedConcat,
|
|
10740
10979
|
encodeFramesFromDir,
|
|
@@ -10859,7 +11098,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
10859
11098
|
const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
|
|
10860
11099
|
if (failure) return failure;
|
|
10861
11100
|
}
|
|
10862
|
-
const fileSize = existsSync15(outputPath) ?
|
|
11101
|
+
const fileSize = existsSync15(outputPath) ? statSync6(outputPath).size : 0;
|
|
10863
11102
|
return {
|
|
10864
11103
|
success: true,
|
|
10865
11104
|
outputPath,
|
|
@@ -10868,7 +11107,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
|
|
|
10868
11107
|
fileSize
|
|
10869
11108
|
};
|
|
10870
11109
|
} finally {
|
|
10871
|
-
|
|
11110
|
+
rmSync10(input.palettePath, { force: true });
|
|
10872
11111
|
}
|
|
10873
11112
|
}
|
|
10874
11113
|
function startEncodeProgress(job, stage, onProgress) {
|
|
@@ -10943,7 +11182,7 @@ function encodeWhileCapturing(input, totalFrames) {
|
|
|
10943
11182
|
await new Promise((resolve10) => wake = resolve10);
|
|
10944
11183
|
}
|
|
10945
11184
|
if (signal.aborted) return;
|
|
10946
|
-
const mergedPath =
|
|
11185
|
+
const mergedPath = join25(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
10947
11186
|
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
10948
11187
|
landed.delete(next);
|
|
10949
11188
|
if (!await stream.writeFrame(frame)) {
|
|
@@ -11026,11 +11265,11 @@ async function runEncodeStage(input) {
|
|
|
11026
11265
|
);
|
|
11027
11266
|
}
|
|
11028
11267
|
captured.forEach((name, i) => {
|
|
11029
|
-
const dst =
|
|
11030
|
-
copyFileSync3(
|
|
11268
|
+
const dst = join25(outputPath, formatExportFrameName(i, "png"));
|
|
11269
|
+
copyFileSync3(join25(framesDir, name), dst);
|
|
11031
11270
|
});
|
|
11032
11271
|
if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
|
|
11033
|
-
copyFileSync3(audioOutputPath,
|
|
11272
|
+
copyFileSync3(audioOutputPath, join25(outputPath, MIXED_AUDIO_FILENAME2));
|
|
11034
11273
|
log.info(
|
|
11035
11274
|
`[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
|
|
11036
11275
|
);
|
|
@@ -11050,7 +11289,7 @@ async function runEncodeStage(input) {
|
|
|
11050
11289
|
const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
|
|
11051
11290
|
fps: job.config.fps,
|
|
11052
11291
|
loop,
|
|
11053
|
-
palettePath:
|
|
11292
|
+
palettePath: join25(dirname9(videoOnlyPath), "gif-palette.png"),
|
|
11054
11293
|
preserveAlpha: needsAlpha,
|
|
11055
11294
|
signal: abortSignal,
|
|
11056
11295
|
timeout: engineCfg.ffmpegEncodeTimeout
|
|
@@ -11263,9 +11502,9 @@ function sampleDirectoryBytes(dir) {
|
|
|
11263
11502
|
continue;
|
|
11264
11503
|
}
|
|
11265
11504
|
for (const name of entries) {
|
|
11266
|
-
const full =
|
|
11505
|
+
const full = join26(current, name);
|
|
11267
11506
|
try {
|
|
11268
|
-
const st =
|
|
11507
|
+
const st = statSync7(full);
|
|
11269
11508
|
if (st.isDirectory()) {
|
|
11270
11509
|
stack.push(full);
|
|
11271
11510
|
} else if (st.isFile()) {
|
|
@@ -11412,8 +11651,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
|
|
|
11412
11651
|
const ranges = [];
|
|
11413
11652
|
let rangeStart = null;
|
|
11414
11653
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
11415
|
-
const framePath =
|
|
11416
|
-
const missing = !existsSync16(framePath) ||
|
|
11654
|
+
const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
11655
|
+
const missing = !existsSync16(framePath) || statSync7(framePath).size <= 8;
|
|
11417
11656
|
if (missing && rangeStart === null) {
|
|
11418
11657
|
rangeStart = frameIndex;
|
|
11419
11658
|
} else if (!missing && rangeStart !== null) {
|
|
@@ -11435,7 +11674,7 @@ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, ran
|
|
|
11435
11674
|
workerId,
|
|
11436
11675
|
startFrame: rangeStart + range.startFrame,
|
|
11437
11676
|
endFrame: rangeStart + range.endFrame,
|
|
11438
|
-
outputDir:
|
|
11677
|
+
outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
|
|
11439
11678
|
outputFrameOffset: rangeStart
|
|
11440
11679
|
}));
|
|
11441
11680
|
batches.push(batch);
|
|
@@ -11463,8 +11702,8 @@ function getNextRetryWorkerCount(currentWorkers) {
|
|
|
11463
11702
|
return Math.max(1, Math.floor(currentWorkers / 2));
|
|
11464
11703
|
}
|
|
11465
11704
|
function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
|
|
11466
|
-
if (platform2 === "win32" || platform2 === "darwin") return
|
|
11467
|
-
return
|
|
11705
|
+
if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
|
|
11706
|
+
return join26(dirname10(outputPath), `work-${jobId}-`);
|
|
11468
11707
|
}
|
|
11469
11708
|
var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
|
|
11470
11709
|
function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
|
|
@@ -11496,7 +11735,7 @@ The composition is too large for the available memory. To reduce memory pressure
|
|
|
11496
11735
|
function countCapturedFrames(totalFrames, framesDir, frameExt) {
|
|
11497
11736
|
let captured = 0;
|
|
11498
11737
|
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
|
|
11499
|
-
const framePath =
|
|
11738
|
+
const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
|
|
11500
11739
|
if (existsSync16(framePath)) captured++;
|
|
11501
11740
|
}
|
|
11502
11741
|
return captured;
|
|
@@ -11522,7 +11761,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
11522
11761
|
reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
|
|
11523
11762
|
});
|
|
11524
11763
|
pendingTransientRetry = false;
|
|
11525
|
-
const attemptWorkDir =
|
|
11764
|
+
const attemptWorkDir = join26(options.workDir, `capture-attempt-${attempt}`);
|
|
11526
11765
|
const batches = missingRanges ? buildMissingFrameRetryBatches(
|
|
11527
11766
|
missingRanges,
|
|
11528
11767
|
currentWorkers,
|
|
@@ -11979,7 +12218,10 @@ function resolveDeShortBand(args) {
|
|
|
11979
12218
|
return args.bandOpen ? "applied" : "skipped_elements";
|
|
11980
12219
|
}
|
|
11981
12220
|
function shouldPreferSingleWorkerDrawElement(args) {
|
|
11982
|
-
return args.workerCount > 1 &&
|
|
12221
|
+
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;
|
|
12222
|
+
}
|
|
12223
|
+
function probeNeedsScreenshotFrames(session) {
|
|
12224
|
+
return (session?.deRiskFrames?.size ?? 0) > 0;
|
|
11983
12225
|
}
|
|
11984
12226
|
function isProbeDeGated(session) {
|
|
11985
12227
|
return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
|
|
@@ -12115,17 +12357,17 @@ function deVerifyFallbackTelemetry(err) {
|
|
|
12115
12357
|
}
|
|
12116
12358
|
function resolveRenderDebugDir() {
|
|
12117
12359
|
const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve7(process.env.PRODUCER_RENDERS_DIR, "..") : resolve7(dirname10(fileURLToPath4(import.meta.url)), "../..");
|
|
12118
|
-
return
|
|
12360
|
+
return join26(producerRoot, ".debug");
|
|
12119
12361
|
}
|
|
12120
12362
|
async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
|
|
12121
12363
|
validateHlsRenderConfig(job.config);
|
|
12122
12364
|
const debugDir = resolveRenderDebugDir();
|
|
12123
12365
|
const outputDir = dirname10(outputPath);
|
|
12124
12366
|
if (!existsSync16(outputDir)) mkdirSync9(outputDir, { recursive: true });
|
|
12125
|
-
const workDir = job.config.debug ?
|
|
12367
|
+
const workDir = job.config.debug ? join26(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
|
|
12126
12368
|
const pipelineStart = Date.now();
|
|
12127
12369
|
const baseLog = job.config.logger ?? defaultLogger;
|
|
12128
|
-
const logPath = job.config.debug ?
|
|
12370
|
+
const logPath = job.config.debug ? join26(workDir, "render.log") : null;
|
|
12129
12371
|
const execution = new RenderExecutionContext({
|
|
12130
12372
|
request: { renderJobId: job.id, projectDir, outputPath },
|
|
12131
12373
|
logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
|
|
@@ -12139,7 +12381,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
|
|
|
12139
12381
|
log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
|
|
12140
12382
|
return;
|
|
12141
12383
|
}
|
|
12142
|
-
|
|
12384
|
+
rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
12143
12385
|
});
|
|
12144
12386
|
try {
|
|
12145
12387
|
await executeRenderPipeline({
|
|
@@ -12170,7 +12412,9 @@ async function executeRenderPipeline(input) {
|
|
|
12170
12412
|
const log = execution.logger;
|
|
12171
12413
|
const eventPublisher = execution.events;
|
|
12172
12414
|
const onProgress = execution.onProgress;
|
|
12173
|
-
const
|
|
12415
|
+
const tooSlow = new AbortController();
|
|
12416
|
+
let tooSlowError;
|
|
12417
|
+
const executionSignal = job.config.maxRenderMs ? AbortSignal.any([tooSlow.signal, ...execution.signal ? [execution.signal] : []]) : execution.signal;
|
|
12174
12418
|
let fileServer = null;
|
|
12175
12419
|
let probeSession = null;
|
|
12176
12420
|
let lastBrowserConsole = [];
|
|
@@ -12183,7 +12427,7 @@ async function executeRenderPipeline(input) {
|
|
|
12183
12427
|
imageDecodeFailures: 0
|
|
12184
12428
|
};
|
|
12185
12429
|
let hdrPerf;
|
|
12186
|
-
const perfOutputPath =
|
|
12430
|
+
const perfOutputPath = join26(workDir, "perf-summary.json");
|
|
12187
12431
|
const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
|
|
12188
12432
|
const observability = new RenderObservabilityRecorder({
|
|
12189
12433
|
pipelineStartMs: pipelineStart,
|
|
@@ -12285,15 +12529,15 @@ async function executeRenderPipeline(input) {
|
|
|
12285
12529
|
requestedWorkers: job.config.workers ?? "auto"
|
|
12286
12530
|
});
|
|
12287
12531
|
const entryFile = job.config.entryFile || "index.html";
|
|
12288
|
-
let htmlPath =
|
|
12532
|
+
let htmlPath = join26(projectDir, entryFile);
|
|
12289
12533
|
if (!existsSync16(htmlPath)) {
|
|
12290
12534
|
throw new Error(`Entry file not found: ${htmlPath}`);
|
|
12291
12535
|
}
|
|
12292
12536
|
assertNotAborted();
|
|
12293
12537
|
const rawEntry = readFileSync8(htmlPath, "utf-8");
|
|
12294
12538
|
if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
|
|
12295
|
-
const wrapperPath =
|
|
12296
|
-
const projectIndexPath =
|
|
12539
|
+
const wrapperPath = join26(workDir, "standalone-entry.html");
|
|
12540
|
+
const projectIndexPath = join26(projectDir, "index.html");
|
|
12297
12541
|
if (!existsSync16(projectIndexPath)) {
|
|
12298
12542
|
throw new Error(
|
|
12299
12543
|
`Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
|
|
@@ -12456,7 +12700,7 @@ async function executeRenderPipeline(input) {
|
|
|
12456
12700
|
updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
|
|
12457
12701
|
code: "extract_video"
|
|
12458
12702
|
});
|
|
12459
|
-
const compiledDir =
|
|
12703
|
+
const compiledDir = join26(workDir, "compiled");
|
|
12460
12704
|
const extractResult = await observeRenderStage(
|
|
12461
12705
|
observability,
|
|
12462
12706
|
"video_extract",
|
|
@@ -12590,7 +12834,7 @@ async function executeRenderPipeline(input) {
|
|
|
12590
12834
|
try {
|
|
12591
12835
|
fileServer = await createFileServer({
|
|
12592
12836
|
projectDir,
|
|
12593
|
-
compiledDir:
|
|
12837
|
+
compiledDir: join26(workDir, "compiled"),
|
|
12594
12838
|
port: 0,
|
|
12595
12839
|
preHeadScripts: [VIRTUAL_TIME_SHIM],
|
|
12596
12840
|
fps: job.config.fps
|
|
@@ -12608,7 +12852,7 @@ async function executeRenderPipeline(input) {
|
|
|
12608
12852
|
if (!activeFileServer) {
|
|
12609
12853
|
throw new Error("File server failed to initialize before frame capture");
|
|
12610
12854
|
}
|
|
12611
|
-
const framesDir =
|
|
12855
|
+
const framesDir = join26(workDir, "captured-frames");
|
|
12612
12856
|
if (!existsSync16(framesDir)) mkdirSync9(framesDir, { recursive: true });
|
|
12613
12857
|
updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
|
|
12614
12858
|
code: "start_browsers"
|
|
@@ -12657,6 +12901,7 @@ async function executeRenderPipeline(input) {
|
|
|
12657
12901
|
// Probe-resolved duration: drawElement self-verification derives its
|
|
12658
12902
|
// sample frame indices from this so they land inside the drained range.
|
|
12659
12903
|
compositionDurationSeconds: job.duration,
|
|
12904
|
+
deFrameFallback: true,
|
|
12660
12905
|
requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
|
|
12661
12906
|
// Live route for Chrome memory (spec §5 Phase −1). The aggregate route
|
|
12662
12907
|
// through CapturePerfSummary only exists on success; a mid-capture
|
|
@@ -12712,7 +12957,6 @@ async function executeRenderPipeline(input) {
|
|
|
12712
12957
|
const WOULD_RESOLVE_MULTI_WORKER = 2;
|
|
12713
12958
|
const deInversionArgs = {
|
|
12714
12959
|
workerCount: WOULD_RESOLVE_MULTI_WORKER,
|
|
12715
|
-
requestedWorkers: job.config.workers,
|
|
12716
12960
|
useDrawElement: cfg.useDrawElement,
|
|
12717
12961
|
deCompileGate,
|
|
12718
12962
|
forceScreenshot: captureForceScreenshot,
|
|
@@ -12758,7 +13002,7 @@ async function executeRenderPipeline(input) {
|
|
|
12758
13002
|
minFrames: deParallelMinFrames,
|
|
12759
13003
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12760
13004
|
supersampling: deviceScaleFactor !== 1,
|
|
12761
|
-
probeDeGated: isProbeDeGated(probeSession),
|
|
13005
|
+
probeDeGated: isProbeDeGated(probeSession) || probeNeedsScreenshotFrames(probeSession),
|
|
12762
13006
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
|
|
12763
13007
|
routerEnabled: deParallelRouterEnabled,
|
|
12764
13008
|
// Router pins 3 workers for the streaming path; don't pin when a
|
|
@@ -12800,6 +13044,8 @@ async function executeRenderPipeline(input) {
|
|
|
12800
13044
|
deInversionEligible = false;
|
|
12801
13045
|
deParallelRouterEligible = false;
|
|
12802
13046
|
deShortBand = void 0;
|
|
13047
|
+
} else if (probeNeedsScreenshotFrames(probeSession)) {
|
|
13048
|
+
deParallelRouterEligible = false;
|
|
12803
13049
|
}
|
|
12804
13050
|
}
|
|
12805
13051
|
const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
|
|
@@ -12918,7 +13164,7 @@ async function executeRenderPipeline(input) {
|
|
|
12918
13164
|
} else if (deInversionEligible && workerCount > 1) {
|
|
12919
13165
|
deWorkerInversion = "inverted";
|
|
12920
13166
|
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
|
|
13167
|
+
`[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
13168
|
);
|
|
12923
13169
|
workerCount = 1;
|
|
12924
13170
|
}
|
|
@@ -12962,6 +13208,27 @@ async function executeRenderPipeline(input) {
|
|
|
12962
13208
|
deWorkerInversion: deWorkerInversion ?? "none",
|
|
12963
13209
|
deParallelRouter: deParallelRouter ?? "none"
|
|
12964
13210
|
});
|
|
13211
|
+
const calibratedTooSlow = checkRenderEta("capture_calibration", {
|
|
13212
|
+
maxRenderMs: job.config.maxRenderMs,
|
|
13213
|
+
elapsedMs: Date.now() - pipelineStart,
|
|
13214
|
+
framesCompleted: 0,
|
|
13215
|
+
totalFrames,
|
|
13216
|
+
msPerFrame: (captureCalibration?.estimate.p95Ms ?? 0) / workerCount
|
|
13217
|
+
});
|
|
13218
|
+
if (calibratedTooSlow) throw calibratedTooSlow;
|
|
13219
|
+
execution.defer(
|
|
13220
|
+
"stop render ETA guard",
|
|
13221
|
+
startRenderEtaGuard({
|
|
13222
|
+
maxRenderMs: job.config.maxRenderMs,
|
|
13223
|
+
job,
|
|
13224
|
+
totalFrames,
|
|
13225
|
+
pipelineStartMs: pipelineStart,
|
|
13226
|
+
onTooSlow: (error) => {
|
|
13227
|
+
tooSlowError = error;
|
|
13228
|
+
tooSlow.abort(error);
|
|
13229
|
+
}
|
|
13230
|
+
})
|
|
13231
|
+
);
|
|
12965
13232
|
let useStreamingEncode = shouldUseStreamingEncode(
|
|
12966
13233
|
cfg,
|
|
12967
13234
|
outputFormat,
|
|
@@ -12970,7 +13237,8 @@ async function executeRenderPipeline(input) {
|
|
|
12970
13237
|
deParallelStreamForced
|
|
12971
13238
|
);
|
|
12972
13239
|
const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
|
|
12973
|
-
|
|
13240
|
+
const routeDropsScreenshotFrames = cfg.useDrawElement && probeNeedsScreenshotFrames(probeSession) && (!useStreamingEncode || workerCount !== 1);
|
|
13241
|
+
if (routeDropsScreenshotFrames || shouldClampDefaultDrawElement({
|
|
12974
13242
|
useDrawElement: cfg.useDrawElement,
|
|
12975
13243
|
fastCaptureExplicitOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
|
|
12976
13244
|
useStreamingEncode,
|
|
@@ -13055,7 +13323,7 @@ async function executeRenderPipeline(input) {
|
|
|
13055
13323
|
hls: ".mp4"
|
|
13056
13324
|
};
|
|
13057
13325
|
const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
|
|
13058
|
-
const videoOnlyPath =
|
|
13326
|
+
const videoOnlyPath = join26(workDir, `video-only${videoExt}`);
|
|
13059
13327
|
const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
|
|
13060
13328
|
if (usePageSideCompositingForTransitions) {
|
|
13061
13329
|
activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
|
|
@@ -13301,7 +13569,7 @@ async function executeRenderPipeline(input) {
|
|
|
13301
13569
|
pageMotionBlur: job.config.pageMotionBlur,
|
|
13302
13570
|
captureQuality: captureOptions.quality
|
|
13303
13571
|
});
|
|
13304
|
-
const segmentDir = segmentDirFor(
|
|
13572
|
+
const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
|
|
13305
13573
|
let completedSegments = /* @__PURE__ */ new Set();
|
|
13306
13574
|
if (job.config.resumeSegments === true) {
|
|
13307
13575
|
const existing = readSegmentManifest(segmentDir);
|
|
@@ -13316,10 +13584,10 @@ async function executeRenderPipeline(input) {
|
|
|
13316
13584
|
});
|
|
13317
13585
|
}
|
|
13318
13586
|
if (completedSegments.size === 0) {
|
|
13319
|
-
|
|
13587
|
+
rmSync11(segmentDir, { recursive: true, force: true });
|
|
13320
13588
|
}
|
|
13321
13589
|
} else {
|
|
13322
|
-
|
|
13590
|
+
rmSync11(segmentDir, { recursive: true, force: true });
|
|
13323
13591
|
}
|
|
13324
13592
|
const segmentManifest = readSegmentManifest(segmentDir) ?? {
|
|
13325
13593
|
version: 1,
|
|
@@ -13400,7 +13668,7 @@ async function executeRenderPipeline(input) {
|
|
|
13400
13668
|
);
|
|
13401
13669
|
updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
|
|
13402
13670
|
if (job.config.keepSegments !== true) {
|
|
13403
|
-
|
|
13671
|
+
rmSync11(segmentDir, { recursive: true, force: true });
|
|
13404
13672
|
}
|
|
13405
13673
|
} else {
|
|
13406
13674
|
capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
|
|
@@ -13669,7 +13937,7 @@ async function executeRenderPipeline(input) {
|
|
|
13669
13937
|
"capture_disk",
|
|
13670
13938
|
"drawElement capture failed; retrying with a fresh screenshot session"
|
|
13671
13939
|
);
|
|
13672
|
-
|
|
13940
|
+
rmSync11(framesDir, { recursive: true, force: true });
|
|
13673
13941
|
mkdirSync9(framesDir, { recursive: true });
|
|
13674
13942
|
resetCaptureAttemptProgress(job);
|
|
13675
13943
|
dedupPerfs.length = 0;
|
|
@@ -13856,7 +14124,7 @@ async function executeRenderPipeline(input) {
|
|
|
13856
14124
|
}
|
|
13857
14125
|
if (job.config.debug) {
|
|
13858
14126
|
if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
|
|
13859
|
-
const debugOutput =
|
|
14127
|
+
const debugOutput = join26(workDir, `output${videoExt}`);
|
|
13860
14128
|
copyFileSync4(stagedOutputPath, debugOutput);
|
|
13861
14129
|
}
|
|
13862
14130
|
}
|
|
@@ -13867,8 +14135,9 @@ async function executeRenderPipeline(input) {
|
|
|
13867
14135
|
job.outputPath = outputPath;
|
|
13868
14136
|
updateJobStatus(job, "complete", "Render complete", 100, onProgress);
|
|
13869
14137
|
await eventPublisher.flush();
|
|
13870
|
-
} catch (
|
|
13871
|
-
|
|
14138
|
+
} catch (caught) {
|
|
14139
|
+
const error = tooSlowError ?? caught;
|
|
14140
|
+
if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
|
|
13872
14141
|
job.error = error instanceof Error ? error.message : "render_cancelled";
|
|
13873
14142
|
updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
|
|
13874
14143
|
await eventPublisher.flush();
|
|
@@ -13962,13 +14231,13 @@ async function executeRenderPipeline(input) {
|
|
|
13962
14231
|
import {
|
|
13963
14232
|
existsSync as existsSync17,
|
|
13964
14233
|
readFileSync as readFileSync9,
|
|
13965
|
-
statSync as
|
|
14234
|
+
statSync as statSync8,
|
|
13966
14235
|
openSync as openSync4,
|
|
13967
|
-
fstatSync as
|
|
13968
|
-
closeSync as
|
|
14236
|
+
fstatSync as fstatSync3,
|
|
14237
|
+
closeSync as closeSync4,
|
|
13969
14238
|
constants as constants3
|
|
13970
14239
|
} from "node:fs";
|
|
13971
|
-
import { resolve as resolve8, join as
|
|
14240
|
+
import { resolve as resolve8, join as join27, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
|
|
13972
14241
|
import { lintHyperframeHtml } from "@hyperframes/lint";
|
|
13973
14242
|
function isStringRecord(value) {
|
|
13974
14243
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -14002,19 +14271,19 @@ function readEntryHtml(filePath) {
|
|
|
14002
14271
|
} catch (error) {
|
|
14003
14272
|
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
|
|
14004
14273
|
return null;
|
|
14005
|
-
if (!
|
|
14274
|
+
if (!statSync8(filePath, { throwIfNoEntry: false })?.isFile()) return null;
|
|
14006
14275
|
throw error;
|
|
14007
14276
|
}
|
|
14008
14277
|
try {
|
|
14009
|
-
if (!
|
|
14278
|
+
if (!fstatSync3(fd).isFile()) return null;
|
|
14010
14279
|
return readFileSync9(fd, "utf-8");
|
|
14011
14280
|
} finally {
|
|
14012
|
-
|
|
14281
|
+
closeSync4(fd);
|
|
14013
14282
|
}
|
|
14014
14283
|
}
|
|
14015
14284
|
function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
14016
14285
|
const absProjectDir = resolve8(projectDir);
|
|
14017
|
-
if (!existsSync17(absProjectDir) || !
|
|
14286
|
+
if (!existsSync17(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
|
|
14018
14287
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
14019
14288
|
}
|
|
14020
14289
|
const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
|
|
@@ -14036,7 +14305,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
|
|
|
14036
14305
|
}
|
|
14037
14306
|
}
|
|
14038
14307
|
return {
|
|
14039
|
-
error: `No HTML entry file found in project directory: ${
|
|
14308
|
+
error: `No HTML entry file found in project directory: ${join27(absProjectDir, preferredEntryFile || "index.html")}`
|
|
14040
14309
|
};
|
|
14041
14310
|
}
|
|
14042
14311
|
function prepareHyperframeLintBody(body) {
|
|
@@ -14082,7 +14351,7 @@ async function runHyperframeLint(prepared) {
|
|
|
14082
14351
|
// src/services/healthWorker.ts
|
|
14083
14352
|
import { Worker as Worker2 } from "node:worker_threads";
|
|
14084
14353
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
14085
|
-
import { dirname as dirname11, join as
|
|
14354
|
+
import { dirname as dirname11, join as join28 } from "node:path";
|
|
14086
14355
|
import { existsSync as existsSync18 } from "node:fs";
|
|
14087
14356
|
var DEFAULT_HEALTH_PORT = 9848;
|
|
14088
14357
|
async function startHealthWorker(options = {}) {
|
|
@@ -14158,7 +14427,7 @@ function defaultLogger2() {
|
|
|
14158
14427
|
}
|
|
14159
14428
|
function resolveWorkerEntry2() {
|
|
14160
14429
|
const here = dirname11(fileURLToPath5(import.meta.url));
|
|
14161
|
-
const candidates = [
|
|
14430
|
+
const candidates = [join28(here, "healthWorkerThread.js"), join28(here, "healthWorkerThread.ts")];
|
|
14162
14431
|
for (const candidate of candidates) {
|
|
14163
14432
|
if (existsSync18(candidate)) return candidate;
|
|
14164
14433
|
}
|
|
@@ -14194,7 +14463,7 @@ import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
|
14194
14463
|
|
|
14195
14464
|
// src/services/distributed/plan.ts
|
|
14196
14465
|
import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
|
|
14197
|
-
import { extname as extname4, isAbsolute as isAbsolute4, join as
|
|
14466
|
+
import { extname as extname4, isAbsolute as isAbsolute4, join as join29, relative as relative4, sep as sep3 } from "node:path";
|
|
14198
14467
|
import {
|
|
14199
14468
|
fpsToNumber as fpsToNumber10,
|
|
14200
14469
|
HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
|
|
@@ -14298,9 +14567,9 @@ var PLAN_ROOT_CATEGORY = {
|
|
|
14298
14567
|
var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
14299
14568
|
var FREEZE_OWNED_PLAN_FILES = [
|
|
14300
14569
|
"plan.json",
|
|
14301
|
-
|
|
14302
|
-
|
|
14303
|
-
|
|
14570
|
+
join29("meta", "composition.json"),
|
|
14571
|
+
join29("meta", "encoder.json"),
|
|
14572
|
+
join29("meta", "chunks.json")
|
|
14304
14573
|
];
|
|
14305
14574
|
|
|
14306
14575
|
// src/services/distributed/renderConfigValidation.ts
|
|
@@ -14441,6 +14710,7 @@ function assertRequestOptionScalars(options) {
|
|
|
14441
14710
|
assertOptionalInteger(options, "gifLoop");
|
|
14442
14711
|
assertOptionalInteger(options, "hlsSegmentSeconds", 1);
|
|
14443
14712
|
assertOptionalInteger(options, "workers", 1);
|
|
14713
|
+
assertOptionalInteger(options, "maxRenderMs");
|
|
14444
14714
|
assertOptionalInteger(options, "crf");
|
|
14445
14715
|
for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
|
|
14446
14716
|
assertOptionalBoolean(options, field);
|
|
@@ -14533,7 +14803,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
14533
14803
|
"INVALID_VIDEO_METADATA",
|
|
14534
14804
|
"VIDEO_SOURCE_UNRENDERABLE",
|
|
14535
14805
|
"VIDEO_EXTRACTION_FAILED",
|
|
14536
|
-
"ENCODER_INTERRUPTED"
|
|
14806
|
+
"ENCODER_INTERRUPTED",
|
|
14807
|
+
"RENDER_TOO_SLOW"
|
|
14537
14808
|
]);
|
|
14538
14809
|
function extractSafeRenderErrorCode(error) {
|
|
14539
14810
|
if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
|
|
@@ -14592,6 +14863,7 @@ function parseRenderOptions(body) {
|
|
|
14592
14863
|
const fps = parseServerFps(body.fps);
|
|
14593
14864
|
const quality = parseServerQuality(body.quality);
|
|
14594
14865
|
const workers = typeof body.workers === "number" ? body.workers : void 0;
|
|
14866
|
+
const maxRenderMs = typeof body.maxRenderMs === "number" ? body.maxRenderMs : void 0;
|
|
14595
14867
|
const useGpu = body.gpu === true;
|
|
14596
14868
|
const debug = body.debug === true;
|
|
14597
14869
|
const strictness = body.bestEffort === false ? "strict" : "best-effort";
|
|
@@ -14607,6 +14879,7 @@ function parseRenderOptions(body) {
|
|
|
14607
14879
|
fps,
|
|
14608
14880
|
quality,
|
|
14609
14881
|
workers,
|
|
14882
|
+
maxRenderMs,
|
|
14610
14883
|
crf,
|
|
14611
14884
|
videoBitrate,
|
|
14612
14885
|
useGpu,
|
|
@@ -14640,6 +14913,7 @@ function buildRenderJobConfig(input, outputPath, log) {
|
|
|
14640
14913
|
quality: input.quality,
|
|
14641
14914
|
format: input.format ?? "mp4",
|
|
14642
14915
|
workers: input.workers,
|
|
14916
|
+
maxRenderMs: input.maxRenderMs,
|
|
14643
14917
|
crf: input.crf,
|
|
14644
14918
|
videoBitrate: input.videoBitrate,
|
|
14645
14919
|
useGpu: input.useGpu,
|
|
@@ -14666,6 +14940,9 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
|
|
|
14666
14940
|
function validateRenderOverrides(body) {
|
|
14667
14941
|
const encoder = parseEncoderOptions(body, parseServerFormat(body.format) ?? "mp4");
|
|
14668
14942
|
if (!encoder.ok) return encoder.error;
|
|
14943
|
+
if (body.maxRenderMs !== void 0 && (typeof body.maxRenderMs !== "number" || !Number.isInteger(body.maxRenderMs) || body.maxRenderMs < 0)) {
|
|
14944
|
+
return "maxRenderMs must be a non-negative integer number of milliseconds";
|
|
14945
|
+
}
|
|
14669
14946
|
if (body.variables !== void 0 && !isPlainObject2(body.variables)) {
|
|
14670
14947
|
return 'variables must be a JSON object keyed by variable id (e.g. {"title":"Hello"})';
|
|
14671
14948
|
}
|
|
@@ -14698,7 +14975,7 @@ function prepareProjectDirectory(projectDir, options) {
|
|
|
14698
14975
|
const candidate = nonEmptyString(projectDir);
|
|
14699
14976
|
if (!candidate) return null;
|
|
14700
14977
|
const absProjectDir = resolve9(candidate);
|
|
14701
|
-
if (!existsSync19(absProjectDir) || !
|
|
14978
|
+
if (!existsSync19(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
|
|
14702
14979
|
return { error: `Project directory not found: ${absProjectDir}` };
|
|
14703
14980
|
}
|
|
14704
14981
|
const entry = options.entryFile || "index.html";
|
|
@@ -14723,8 +15000,8 @@ async function resolveInlineRenderHtml(body) {
|
|
|
14723
15000
|
}
|
|
14724
15001
|
function materializeInlineProject(html, options) {
|
|
14725
15002
|
const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
|
|
14726
|
-
const tempProjectDir = mkdtempSync4(
|
|
14727
|
-
writeFileSync6(
|
|
15003
|
+
const tempProjectDir = mkdtempSync4(join30(tempRoot, "producer-project-"));
|
|
15004
|
+
writeFileSync6(join30(tempProjectDir, "index.html"), html, "utf-8");
|
|
14728
15005
|
return {
|
|
14729
15006
|
prepared: {
|
|
14730
15007
|
input: { projectDir: tempProjectDir, ...options },
|
|
@@ -14785,7 +15062,7 @@ function createArtifactStore(ttlMs) {
|
|
|
14785
15062
|
function cleanupTempDir(dir, log) {
|
|
14786
15063
|
if (!dir) return;
|
|
14787
15064
|
try {
|
|
14788
|
-
|
|
15065
|
+
rmSync12(dir, { recursive: true, force: true });
|
|
14789
15066
|
} catch (error) {
|
|
14790
15067
|
log.warn("Failed to cleanup temp project dir", {
|
|
14791
15068
|
cleanupProjectDir: dir,
|
|
@@ -14794,7 +15071,7 @@ function cleanupTempDir(dir, log) {
|
|
|
14794
15071
|
}
|
|
14795
15072
|
}
|
|
14796
15073
|
function outputFileSize(path) {
|
|
14797
|
-
return existsSync19(path) ?
|
|
15074
|
+
return existsSync19(path) ? statSync9(path).size : 0;
|
|
14798
15075
|
}
|
|
14799
15076
|
function createBlockingProgressReporter(log, requestId) {
|
|
14800
15077
|
let lastLoggedPct = -10;
|
|
@@ -15094,7 +15371,7 @@ function createRenderHandlers(options = {}) {
|
|
|
15094
15371
|
store.delete(token);
|
|
15095
15372
|
return c.json({ success: false, error: "Output artifact file missing" }, 404);
|
|
15096
15373
|
}
|
|
15097
|
-
const stats =
|
|
15374
|
+
const stats = statSync9(artifact.path);
|
|
15098
15375
|
return new Response(createReadStream3(artifact.path), {
|
|
15099
15376
|
headers: {
|
|
15100
15377
|
"content-type": "video/mp4",
|