@hyperframes/producer 0.4.6 → 0.4.7
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/index.js +1084 -286
- package/dist/index.js.map +4 -4
- package/dist/public-server.js +1084 -286
- package/dist/public-server.js.map +4 -4
- package/dist/services/fileServer.d.ts +17 -2
- package/dist/services/fileServer.d.ts.map +1 -1
- package/dist/services/renderOrchestrator.d.ts +2 -11
- package/dist/services/renderOrchestrator.d.ts.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -89421,6 +89421,7 @@ import {
|
|
|
89421
89421
|
mkdirSync as mkdirSync10,
|
|
89422
89422
|
rmSync as rmSync3,
|
|
89423
89423
|
readFileSync as readFileSync9,
|
|
89424
|
+
readdirSync as readdirSync6,
|
|
89424
89425
|
writeFileSync as writeFileSync4,
|
|
89425
89426
|
copyFileSync as copyFileSync2,
|
|
89426
89427
|
appendFileSync
|
|
@@ -98668,6 +98669,8 @@ var DEFAULT_CONFIG = {
|
|
|
98668
98669
|
ffmpegEncodeTimeout: 6e5,
|
|
98669
98670
|
ffmpegProcessTimeout: 3e5,
|
|
98670
98671
|
ffmpegStreamingTimeout: 6e5,
|
|
98672
|
+
hdr: false,
|
|
98673
|
+
hdrAutoDetect: true,
|
|
98671
98674
|
audioGain: 1.35,
|
|
98672
98675
|
frameDataUriCacheLimit: 256,
|
|
98673
98676
|
playerReadyTimeout: 45e3,
|
|
@@ -98724,6 +98727,12 @@ function resolveConfig(overrides) {
|
|
|
98724
98727
|
"FFMPEG_STREAMING_TIMEOUT_MS",
|
|
98725
98728
|
DEFAULT_CONFIG.ffmpegStreamingTimeout
|
|
98726
98729
|
),
|
|
98730
|
+
hdr: (() => {
|
|
98731
|
+
const raw2 = env2("PRODUCER_HDR_TRANSFER");
|
|
98732
|
+
if (raw2 === "hlg" || raw2 === "pq") return { transfer: raw2 };
|
|
98733
|
+
return void 0;
|
|
98734
|
+
})(),
|
|
98735
|
+
hdrAutoDetect: envBool("PRODUCER_HDR_AUTO_DETECT", DEFAULT_CONFIG.hdrAutoDetect),
|
|
98727
98736
|
audioGain: envNum("PRODUCER_AUDIO_GAIN", DEFAULT_CONFIG.audioGain),
|
|
98728
98737
|
frameDataUriCacheLimit: Math.max(
|
|
98729
98738
|
32,
|
|
@@ -98920,7 +98929,8 @@ function buildChromeArgs(options, config2) {
|
|
|
98920
98929
|
"--font-render-hinting=none",
|
|
98921
98930
|
"--force-color-profile=srgb",
|
|
98922
98931
|
`--window-size=${options.width},${options.height}`,
|
|
98923
|
-
//
|
|
98932
|
+
// Prevent Chrome from throttling background tabs/timers — critical when the
|
|
98933
|
+
// page is offscreen during headless capture
|
|
98924
98934
|
"--disable-background-timer-throttling",
|
|
98925
98935
|
"--disable-backgrounding-occluded-windows",
|
|
98926
98936
|
"--disable-renderer-backgrounding",
|
|
@@ -100907,6 +100917,24 @@ async function pageScreenshotCapture(page, options) {
|
|
|
100907
100917
|
});
|
|
100908
100918
|
return Buffer.from(result.data, "base64");
|
|
100909
100919
|
}
|
|
100920
|
+
async function initTransparentBackground(page) {
|
|
100921
|
+
const client = await getCdpSession(page);
|
|
100922
|
+
await client.send("Emulation.setDefaultBackgroundColorOverride", {
|
|
100923
|
+
color: { r: 0, g: 0, b: 0, a: 0 }
|
|
100924
|
+
});
|
|
100925
|
+
}
|
|
100926
|
+
async function captureAlphaPng(page, width, height) {
|
|
100927
|
+
const client = await getCdpSession(page);
|
|
100928
|
+
const result = await client.send("Page.captureScreenshot", {
|
|
100929
|
+
format: "png",
|
|
100930
|
+
fromSurface: true,
|
|
100931
|
+
captureBeyondViewport: false,
|
|
100932
|
+
optimizeForSpeed: false,
|
|
100933
|
+
// must be false to preserve alpha
|
|
100934
|
+
clip: { x: 0, y: 0, width, height, scale: 1 }
|
|
100935
|
+
});
|
|
100936
|
+
return Buffer.from(result.data, "base64");
|
|
100937
|
+
}
|
|
100910
100938
|
async function injectVideoFramesBatch(page, updates) {
|
|
100911
100939
|
if (updates.length === 0) return;
|
|
100912
100940
|
await page.evaluate(
|
|
@@ -100928,16 +100956,7 @@ async function injectVideoFramesBatch(page, updates) {
|
|
|
100928
100956
|
video.parentNode?.insertBefore(img, video.nextSibling);
|
|
100929
100957
|
}
|
|
100930
100958
|
if (!img) continue;
|
|
100931
|
-
|
|
100932
|
-
img.style.position = computedStyle.position;
|
|
100933
|
-
img.style.width = computedStyle.width;
|
|
100934
|
-
img.style.height = computedStyle.height;
|
|
100935
|
-
img.style.top = computedStyle.top;
|
|
100936
|
-
img.style.left = computedStyle.left;
|
|
100937
|
-
img.style.right = computedStyle.right;
|
|
100938
|
-
img.style.bottom = computedStyle.bottom;
|
|
100939
|
-
img.style.inset = computedStyle.inset;
|
|
100940
|
-
} else {
|
|
100959
|
+
{
|
|
100941
100960
|
const videoRect = video.getBoundingClientRect();
|
|
100942
100961
|
const offsetLeft = Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0;
|
|
100943
100962
|
const offsetTop = Number.isFinite(video.offsetTop) ? video.offsetTop : 0;
|
|
@@ -100988,14 +101007,22 @@ async function syncVideoFrameVisibility(page, activeVideoIds) {
|
|
|
100988
101007
|
const active = new Set(ids);
|
|
100989
101008
|
const videos = Array.from(document.querySelectorAll("video[data-start]"));
|
|
100990
101009
|
for (const video of videos) {
|
|
100991
|
-
if (active.has(video.id)) continue;
|
|
100992
|
-
video.style.removeProperty("display");
|
|
100993
|
-
video.style.setProperty("visibility", "hidden", "important");
|
|
100994
|
-
video.style.setProperty("opacity", "0", "important");
|
|
100995
|
-
video.style.setProperty("pointer-events", "none", "important");
|
|
100996
101010
|
const img = video.nextElementSibling;
|
|
100997
|
-
|
|
100998
|
-
|
|
101011
|
+
const hasImg = img && img.classList.contains("__render_frame__");
|
|
101012
|
+
if (active.has(video.id)) {
|
|
101013
|
+
video.style.setProperty("visibility", "hidden", "important");
|
|
101014
|
+
video.style.setProperty("pointer-events", "none", "important");
|
|
101015
|
+
if (hasImg) {
|
|
101016
|
+
img.style.visibility = "visible";
|
|
101017
|
+
}
|
|
101018
|
+
} else {
|
|
101019
|
+
video.style.removeProperty("display");
|
|
101020
|
+
video.style.setProperty("visibility", "hidden", "important");
|
|
101021
|
+
video.style.setProperty("opacity", "0", "important");
|
|
101022
|
+
video.style.setProperty("pointer-events", "none", "important");
|
|
101023
|
+
if (hasImg) {
|
|
101024
|
+
img.style.visibility = "hidden";
|
|
101025
|
+
}
|
|
100999
101026
|
}
|
|
101000
101027
|
}
|
|
101001
101028
|
}, activeVideoIds);
|
|
@@ -101452,7 +101479,7 @@ var ENCODER_PRESETS = {
|
|
|
101452
101479
|
standard: { preset: "medium", quality: 18, codec: "h264" },
|
|
101453
101480
|
high: { preset: "slow", quality: 15, codec: "h264" }
|
|
101454
101481
|
};
|
|
101455
|
-
function getEncoderPreset(quality, format3 = "mp4") {
|
|
101482
|
+
function getEncoderPreset(quality, format3 = "mp4", hdr) {
|
|
101456
101483
|
const base = ENCODER_PRESETS[quality];
|
|
101457
101484
|
if (format3 === "webm") {
|
|
101458
101485
|
return {
|
|
@@ -101470,6 +101497,15 @@ function getEncoderPreset(quality, format3 = "mp4") {
|
|
|
101470
101497
|
pixelFormat: "yuva444p10le"
|
|
101471
101498
|
};
|
|
101472
101499
|
}
|
|
101500
|
+
if (hdr) {
|
|
101501
|
+
return {
|
|
101502
|
+
preset: base.preset === "ultrafast" ? "fast" : base.preset,
|
|
101503
|
+
quality: base.quality,
|
|
101504
|
+
codec: "h265",
|
|
101505
|
+
pixelFormat: "yuv420p10le",
|
|
101506
|
+
hdr
|
|
101507
|
+
};
|
|
101508
|
+
}
|
|
101473
101509
|
return { ...base, pixelFormat: "yuv420p" };
|
|
101474
101510
|
}
|
|
101475
101511
|
function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
|
|
@@ -101527,6 +101563,9 @@ function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
|
|
|
101527
101563
|
args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
|
|
101528
101564
|
}
|
|
101529
101565
|
}
|
|
101566
|
+
if (codec === "h265") {
|
|
101567
|
+
args.push("-tag:v", "hvc1");
|
|
101568
|
+
}
|
|
101530
101569
|
} else if (codec === "vp9") {
|
|
101531
101570
|
args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
|
|
101532
101571
|
args.push("-deadline", preset === "ultrafast" ? "realtime" : "good");
|
|
@@ -101833,31 +101872,79 @@ async function applyFaststart(inputPath, outputPath, signal, config2) {
|
|
|
101833
101872
|
import { spawn as spawn6 } from "child_process";
|
|
101834
101873
|
import { existsSync as existsSync6, mkdirSync as mkdirSync3, statSync as statSync4 } from "fs";
|
|
101835
101874
|
import { dirname as dirname6 } from "path";
|
|
101875
|
+
|
|
101876
|
+
// ../engine/src/utils/hdr.ts
|
|
101877
|
+
function isHdrColorSpace(cs) {
|
|
101878
|
+
if (!cs) return false;
|
|
101879
|
+
return cs.colorPrimaries.includes("bt2020") || cs.colorSpace.includes("bt2020") || cs.colorTransfer === "smpte2084" || cs.colorTransfer === "arib-std-b67";
|
|
101880
|
+
}
|
|
101881
|
+
var DEFAULT_HDR10_MASTERING = {
|
|
101882
|
+
masterDisplay: "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
|
|
101883
|
+
maxCll: "1000,400"
|
|
101884
|
+
};
|
|
101885
|
+
function getHdrEncoderColorParams(transfer, mastering = DEFAULT_HDR10_MASTERING) {
|
|
101886
|
+
const colorTrc = transfer === "pq" ? "smpte2084" : "arib-std-b67";
|
|
101887
|
+
const tagging = `colorprim=bt2020:transfer=${colorTrc}:colormatrix=bt2020nc`;
|
|
101888
|
+
const metadata = `master-display=${mastering.masterDisplay}:max-cll=${mastering.maxCll}`;
|
|
101889
|
+
return {
|
|
101890
|
+
colorPrimaries: "bt2020",
|
|
101891
|
+
colorTrc,
|
|
101892
|
+
colorspace: "bt2020nc",
|
|
101893
|
+
pixelFormat: "yuv420p10le",
|
|
101894
|
+
x265ColorParams: `${tagging}:${metadata}`,
|
|
101895
|
+
mastering
|
|
101896
|
+
};
|
|
101897
|
+
}
|
|
101898
|
+
function analyzeCompositionHdr(colorSpaces) {
|
|
101899
|
+
let hasPq = false;
|
|
101900
|
+
let hasHdr = false;
|
|
101901
|
+
for (const cs of colorSpaces) {
|
|
101902
|
+
if (!isHdrColorSpace(cs)) continue;
|
|
101903
|
+
hasHdr = true;
|
|
101904
|
+
if (cs?.colorTransfer === "smpte2084") hasPq = true;
|
|
101905
|
+
}
|
|
101906
|
+
if (!hasHdr) return { hasHdr: false, dominantTransfer: null };
|
|
101907
|
+
const dominantTransfer = hasPq ? "pq" : "hlg";
|
|
101908
|
+
return { hasHdr: true, dominantTransfer };
|
|
101909
|
+
}
|
|
101910
|
+
|
|
101911
|
+
// ../engine/src/services/streamingEncoder.ts
|
|
101836
101912
|
function createFrameReorderBuffer(startFrame, endFrame) {
|
|
101837
|
-
let
|
|
101838
|
-
|
|
101839
|
-
const
|
|
101840
|
-
|
|
101841
|
-
|
|
101842
|
-
|
|
101843
|
-
|
|
101844
|
-
|
|
101913
|
+
let cursor = startFrame;
|
|
101914
|
+
const pending = /* @__PURE__ */ new Map();
|
|
101915
|
+
const enqueueAt = (frame, resolve13) => {
|
|
101916
|
+
const list = pending.get(frame);
|
|
101917
|
+
if (list === void 0) {
|
|
101918
|
+
pending.set(frame, [resolve13]);
|
|
101919
|
+
} else {
|
|
101920
|
+
list.push(resolve13);
|
|
101845
101921
|
}
|
|
101846
101922
|
};
|
|
101847
|
-
|
|
101848
|
-
|
|
101849
|
-
|
|
101850
|
-
|
|
101851
|
-
|
|
101852
|
-
|
|
101853
|
-
|
|
101854
|
-
|
|
101855
|
-
|
|
101856
|
-
|
|
101857
|
-
|
|
101858
|
-
|
|
101859
|
-
|
|
101923
|
+
const flushAt = (frame) => {
|
|
101924
|
+
const list = pending.get(frame);
|
|
101925
|
+
if (list === void 0) return;
|
|
101926
|
+
pending.delete(frame);
|
|
101927
|
+
for (const resolve13 of list) resolve13();
|
|
101928
|
+
};
|
|
101929
|
+
const waitForFrame = (frame) => new Promise((resolve13) => {
|
|
101930
|
+
if (frame === cursor) {
|
|
101931
|
+
resolve13();
|
|
101932
|
+
return;
|
|
101933
|
+
}
|
|
101934
|
+
enqueueAt(frame, resolve13);
|
|
101935
|
+
});
|
|
101936
|
+
const advanceTo = (frame) => {
|
|
101937
|
+
cursor = frame;
|
|
101938
|
+
flushAt(frame);
|
|
101860
101939
|
};
|
|
101940
|
+
const waitForAllDone = () => new Promise((resolve13) => {
|
|
101941
|
+
if (cursor >= endFrame) {
|
|
101942
|
+
resolve13();
|
|
101943
|
+
return;
|
|
101944
|
+
}
|
|
101945
|
+
enqueueAt(endFrame, resolve13);
|
|
101946
|
+
});
|
|
101947
|
+
return { waitForFrame, advanceTo, waitForAllDone };
|
|
101861
101948
|
}
|
|
101862
101949
|
function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
|
|
101863
101950
|
const {
|
|
@@ -101870,19 +101957,36 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
|
|
|
101870
101957
|
useGpu = false,
|
|
101871
101958
|
imageFormat = "jpeg"
|
|
101872
101959
|
} = options;
|
|
101873
|
-
const
|
|
101874
|
-
|
|
101875
|
-
|
|
101876
|
-
"
|
|
101877
|
-
|
|
101878
|
-
|
|
101879
|
-
|
|
101880
|
-
|
|
101881
|
-
|
|
101882
|
-
|
|
101883
|
-
|
|
101884
|
-
|
|
101885
|
-
|
|
101960
|
+
const args = [];
|
|
101961
|
+
if (options.rawInputFormat) {
|
|
101962
|
+
const hdrTransfer = options.hdr?.transfer;
|
|
101963
|
+
const inputColorTrc = hdrTransfer === "pq" ? "smpte2084" : hdrTransfer === "hlg" ? "arib-std-b67" : void 0;
|
|
101964
|
+
args.push(
|
|
101965
|
+
"-f",
|
|
101966
|
+
"rawvideo",
|
|
101967
|
+
"-pix_fmt",
|
|
101968
|
+
options.rawInputFormat,
|
|
101969
|
+
"-s",
|
|
101970
|
+
`${options.width}x${options.height}`,
|
|
101971
|
+
"-framerate",
|
|
101972
|
+
String(fps)
|
|
101973
|
+
);
|
|
101974
|
+
if (inputColorTrc) {
|
|
101975
|
+
args.push(
|
|
101976
|
+
"-color_primaries",
|
|
101977
|
+
"bt2020",
|
|
101978
|
+
"-color_trc",
|
|
101979
|
+
inputColorTrc,
|
|
101980
|
+
"-colorspace",
|
|
101981
|
+
"bt2020nc"
|
|
101982
|
+
);
|
|
101983
|
+
}
|
|
101984
|
+
args.push("-i", "-");
|
|
101985
|
+
} else {
|
|
101986
|
+
const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
|
|
101987
|
+
args.push("-f", "image2pipe", "-vcodec", inputCodec, "-framerate", String(fps), "-i", "-");
|
|
101988
|
+
}
|
|
101989
|
+
args.push("-r", String(fps));
|
|
101886
101990
|
const shouldUseGpu = useGpu && gpuEncoder !== null;
|
|
101887
101991
|
if (codec === "h264" || codec === "h265") {
|
|
101888
101992
|
if (shouldUseGpu) {
|
|
@@ -101920,12 +102024,15 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
|
|
|
101920
102024
|
if (bitrate) args.push("-b:v", bitrate);
|
|
101921
102025
|
else args.push("-crf", String(quality));
|
|
101922
102026
|
const xParamsFlag = codec === "h264" ? "-x264-params" : "-x265-params";
|
|
101923
|
-
const colorParams = "colorprim=bt709:transfer=bt709:colormatrix=bt709";
|
|
102027
|
+
const colorParams = options.rawInputFormat && options.hdr ? getHdrEncoderColorParams(options.hdr.transfer).x265ColorParams : "colorprim=bt709:transfer=bt709:colormatrix=bt709";
|
|
101924
102028
|
if (preset === "ultrafast") {
|
|
101925
102029
|
args.push(xParamsFlag, `aq-mode=3:${colorParams}`);
|
|
101926
102030
|
} else {
|
|
101927
102031
|
args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
|
|
101928
102032
|
}
|
|
102033
|
+
if (codec === "h265") {
|
|
102034
|
+
args.push("-tag:v", "hvc1");
|
|
102035
|
+
}
|
|
101929
102036
|
}
|
|
101930
102037
|
} else if (codec === "vp9") {
|
|
101931
102038
|
args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
|
|
@@ -101941,17 +102048,31 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
|
|
|
101941
102048
|
return [...args, "-y", outputPath];
|
|
101942
102049
|
}
|
|
101943
102050
|
if (codec === "h264" || codec === "h265") {
|
|
101944
|
-
|
|
101945
|
-
|
|
101946
|
-
|
|
101947
|
-
|
|
101948
|
-
|
|
101949
|
-
|
|
101950
|
-
|
|
101951
|
-
|
|
101952
|
-
|
|
101953
|
-
|
|
101954
|
-
|
|
102051
|
+
if (options.rawInputFormat && options.hdr) {
|
|
102052
|
+
args.push(
|
|
102053
|
+
"-colorspace:v",
|
|
102054
|
+
"bt2020nc",
|
|
102055
|
+
"-color_primaries:v",
|
|
102056
|
+
"bt2020",
|
|
102057
|
+
"-color_trc:v",
|
|
102058
|
+
options.hdr.transfer === "pq" ? "smpte2084" : "arib-std-b67",
|
|
102059
|
+
"-color_range",
|
|
102060
|
+
"tv"
|
|
102061
|
+
);
|
|
102062
|
+
} else {
|
|
102063
|
+
args.push(
|
|
102064
|
+
"-colorspace:v",
|
|
102065
|
+
"bt709",
|
|
102066
|
+
"-color_primaries:v",
|
|
102067
|
+
"bt709",
|
|
102068
|
+
"-color_trc:v",
|
|
102069
|
+
"bt709",
|
|
102070
|
+
"-color_range",
|
|
102071
|
+
"tv"
|
|
102072
|
+
);
|
|
102073
|
+
}
|
|
102074
|
+
if (options.rawInputFormat) {
|
|
102075
|
+
} else if (gpuEncoder === "vaapi") {
|
|
101955
102076
|
const vfIdx = args.indexOf("-vf");
|
|
101956
102077
|
if (vfIdx !== -1) {
|
|
101957
102078
|
args[vfIdx + 1] = `scale=in_range=pc:out_range=tv,${args[vfIdx + 1]}`;
|
|
@@ -102021,14 +102142,16 @@ Process error: ${err.message}`;
|
|
|
102021
102142
|
if (exitStatus !== "running" || !ffmpeg.stdin || ffmpeg.stdin.destroyed) {
|
|
102022
102143
|
return false;
|
|
102023
102144
|
}
|
|
102024
|
-
|
|
102145
|
+
const copy = Buffer.from(buffer);
|
|
102146
|
+
return ffmpeg.stdin.write(copy);
|
|
102025
102147
|
},
|
|
102026
102148
|
close: async () => {
|
|
102027
102149
|
clearTimeout(timer2);
|
|
102028
102150
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
102029
|
-
|
|
102151
|
+
const stdin = ffmpeg.stdin;
|
|
102152
|
+
if (stdin && !stdin.destroyed) {
|
|
102030
102153
|
await new Promise((resolve13) => {
|
|
102031
|
-
|
|
102154
|
+
stdin.end(() => resolve13());
|
|
102032
102155
|
});
|
|
102033
102156
|
}
|
|
102034
102157
|
await exitPromise;
|
|
@@ -102132,6 +102255,10 @@ async function extractVideoMetadata(filePath) {
|
|
|
102132
102255
|
const avgFps = parseFrameRate(videoStream.avg_frame_rate);
|
|
102133
102256
|
const fps = avgFps || rFps;
|
|
102134
102257
|
const isVFR = rFps > 0 && avgFps > 0 && Math.abs(rFps - avgFps) / Math.max(rFps, avgFps) > 0.1;
|
|
102258
|
+
const colorTransfer = videoStream.color_transfer || "";
|
|
102259
|
+
const colorPrimaries = videoStream.color_primaries || "";
|
|
102260
|
+
const colorSpaceVal = videoStream.color_space || "";
|
|
102261
|
+
const hasColorInfo = !!(colorTransfer || colorPrimaries || colorSpaceVal);
|
|
102135
102262
|
return {
|
|
102136
102263
|
durationSeconds: output2.format.duration ? parseFloat(output2.format.duration) : 0,
|
|
102137
102264
|
width: videoStream.width || 0,
|
|
@@ -102139,7 +102266,8 @@ async function extractVideoMetadata(filePath) {
|
|
|
102139
102266
|
fps,
|
|
102140
102267
|
videoCodec: videoStream.codec_name || "unknown",
|
|
102141
102268
|
hasAudio: output2.streams.some((s) => s.codec_type === "audio"),
|
|
102142
|
-
isVFR
|
|
102269
|
+
isVFR,
|
|
102270
|
+
colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
|
|
102143
102271
|
};
|
|
102144
102272
|
})();
|
|
102145
102273
|
videoMetadataCache.set(filePath, probePromise);
|
|
@@ -102347,18 +102475,20 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
|
|
|
102347
102475
|
const metadata = await extractVideoMetadata(videoPath);
|
|
102348
102476
|
const framePattern = `frame_%05d.${format3}`;
|
|
102349
102477
|
const outputPattern = join8(videoOutputDir, framePattern);
|
|
102350
|
-
const
|
|
102351
|
-
|
|
102352
|
-
|
|
102353
|
-
|
|
102354
|
-
|
|
102355
|
-
|
|
102356
|
-
|
|
102357
|
-
|
|
102358
|
-
|
|
102359
|
-
"
|
|
102360
|
-
|
|
102361
|
-
|
|
102478
|
+
const isHdr = isHdrColorSpace(metadata.colorSpace);
|
|
102479
|
+
const isMacOS = process.platform === "darwin";
|
|
102480
|
+
const args = [];
|
|
102481
|
+
if (isHdr && isMacOS) {
|
|
102482
|
+
args.push("-hwaccel", "videotoolbox");
|
|
102483
|
+
}
|
|
102484
|
+
args.push("-ss", String(startTime), "-i", videoPath, "-t", String(duration));
|
|
102485
|
+
const vfFilters = [];
|
|
102486
|
+
if (isHdr && isMacOS) {
|
|
102487
|
+
vfFilters.push("format=nv12");
|
|
102488
|
+
}
|
|
102489
|
+
vfFilters.push(`fps=${fps}`);
|
|
102490
|
+
args.push("-vf", vfFilters.join(","));
|
|
102491
|
+
args.push("-q:v", format3 === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0");
|
|
102362
102492
|
if (format3 === "png") args.push("-compression_level", "6");
|
|
102363
102493
|
args.push("-y", outputPattern);
|
|
102364
102494
|
return new Promise((resolve13, reject) => {
|
|
@@ -102418,30 +102548,100 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
|
|
|
102418
102548
|
});
|
|
102419
102549
|
});
|
|
102420
102550
|
}
|
|
102551
|
+
async function convertSdrToHdr(inputPath, outputPath, signal, config2) {
|
|
102552
|
+
const timeout2 = config2?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
102553
|
+
const args = [
|
|
102554
|
+
"-i",
|
|
102555
|
+
inputPath,
|
|
102556
|
+
"-vf",
|
|
102557
|
+
"colorspace=all=bt2020:iall=bt709:range=tv",
|
|
102558
|
+
"-color_primaries",
|
|
102559
|
+
"bt2020",
|
|
102560
|
+
"-color_trc",
|
|
102561
|
+
"arib-std-b67",
|
|
102562
|
+
"-colorspace",
|
|
102563
|
+
"bt2020nc",
|
|
102564
|
+
"-c:v",
|
|
102565
|
+
"libx264",
|
|
102566
|
+
"-preset",
|
|
102567
|
+
"fast",
|
|
102568
|
+
"-crf",
|
|
102569
|
+
"16",
|
|
102570
|
+
"-c:a",
|
|
102571
|
+
"copy",
|
|
102572
|
+
"-y",
|
|
102573
|
+
outputPath
|
|
102574
|
+
];
|
|
102575
|
+
const result = await runFfmpeg(args, { signal, timeout: timeout2 });
|
|
102576
|
+
if (!result.success) {
|
|
102577
|
+
throw new Error(
|
|
102578
|
+
`SDR\u2192HDR conversion failed (exit ${result.exitCode}): ${result.stderr.slice(-300)}`
|
|
102579
|
+
);
|
|
102580
|
+
}
|
|
102581
|
+
}
|
|
102421
102582
|
async function extractAllVideoFrames(videos, baseDir, options, signal, config2, compiledDir) {
|
|
102422
102583
|
const startTime = Date.now();
|
|
102423
102584
|
const extracted = [];
|
|
102424
102585
|
const errors = [];
|
|
102425
102586
|
let totalFramesExtracted = 0;
|
|
102587
|
+
const resolvedVideos = [];
|
|
102588
|
+
for (const video of videos) {
|
|
102589
|
+
if (signal?.aborted) break;
|
|
102590
|
+
try {
|
|
102591
|
+
let videoPath = video.src;
|
|
102592
|
+
if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
|
|
102593
|
+
const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
|
|
102594
|
+
videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
|
|
102595
|
+
}
|
|
102596
|
+
if (isHttpUrl(videoPath)) {
|
|
102597
|
+
const downloadDir = join8(options.outputDir, "_downloads");
|
|
102598
|
+
mkdirSync5(downloadDir, { recursive: true });
|
|
102599
|
+
videoPath = await downloadToTemp(videoPath, downloadDir);
|
|
102600
|
+
}
|
|
102601
|
+
if (!existsSync8(videoPath)) {
|
|
102602
|
+
errors.push({ videoId: video.id, error: `Video file not found: ${videoPath}` });
|
|
102603
|
+
continue;
|
|
102604
|
+
}
|
|
102605
|
+
resolvedVideos.push({ video, videoPath });
|
|
102606
|
+
} catch (err) {
|
|
102607
|
+
errors.push({ videoId: video.id, error: err instanceof Error ? err.message : String(err) });
|
|
102608
|
+
}
|
|
102609
|
+
}
|
|
102610
|
+
const videoColorSpaces = await Promise.all(
|
|
102611
|
+
resolvedVideos.map(async ({ videoPath }) => {
|
|
102612
|
+
const metadata = await extractVideoMetadata(videoPath);
|
|
102613
|
+
return metadata.colorSpace;
|
|
102614
|
+
})
|
|
102615
|
+
);
|
|
102616
|
+
const hasAnyHdr = videoColorSpaces.some(isHdrColorSpace);
|
|
102617
|
+
if (hasAnyHdr) {
|
|
102618
|
+
const convertDir = join8(options.outputDir, "_hdr_normalized");
|
|
102619
|
+
mkdirSync5(convertDir, { recursive: true });
|
|
102620
|
+
for (let i = 0; i < resolvedVideos.length; i++) {
|
|
102621
|
+
if (signal?.aborted) break;
|
|
102622
|
+
const cs = videoColorSpaces[i] ?? null;
|
|
102623
|
+
if (!isHdrColorSpace(cs)) {
|
|
102624
|
+
const entry = resolvedVideos[i];
|
|
102625
|
+
if (!entry) continue;
|
|
102626
|
+
const convertedPath = join8(convertDir, `${entry.video.id}_hdr.mp4`);
|
|
102627
|
+
try {
|
|
102628
|
+
await convertSdrToHdr(entry.videoPath, convertedPath, signal, config2);
|
|
102629
|
+
entry.videoPath = convertedPath;
|
|
102630
|
+
} catch (err) {
|
|
102631
|
+
errors.push({
|
|
102632
|
+
videoId: entry.video.id,
|
|
102633
|
+
error: `SDR\u2192HDR conversion failed: ${err instanceof Error ? err.message : String(err)}`
|
|
102634
|
+
});
|
|
102635
|
+
}
|
|
102636
|
+
}
|
|
102637
|
+
}
|
|
102638
|
+
}
|
|
102426
102639
|
const results = await Promise.all(
|
|
102427
|
-
|
|
102640
|
+
resolvedVideos.map(async ({ video, videoPath }) => {
|
|
102428
102641
|
if (signal?.aborted) {
|
|
102429
102642
|
throw new Error("Video frame extraction cancelled");
|
|
102430
102643
|
}
|
|
102431
102644
|
try {
|
|
102432
|
-
let videoPath = video.src;
|
|
102433
|
-
if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
|
|
102434
|
-
const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
|
|
102435
|
-
videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
|
|
102436
|
-
}
|
|
102437
|
-
if (isHttpUrl(videoPath)) {
|
|
102438
|
-
const downloadDir = join8(options.outputDir, "_downloads");
|
|
102439
|
-
mkdirSync5(downloadDir, { recursive: true });
|
|
102440
|
-
videoPath = await downloadToTemp(videoPath, downloadDir);
|
|
102441
|
-
}
|
|
102442
|
-
if (!existsSync8(videoPath)) {
|
|
102443
|
-
return { error: { videoId: video.id, error: `Video file not found: ${videoPath}` } };
|
|
102444
|
-
}
|
|
102445
102645
|
let videoDuration = video.end - video.start;
|
|
102446
102646
|
if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
|
|
102447
102647
|
const metadata = await extractVideoMetadata(videoPath);
|
|
@@ -102676,6 +102876,74 @@ function createVideoFrameInjector(frameLookup, config2) {
|
|
|
102676
102876
|
}
|
|
102677
102877
|
};
|
|
102678
102878
|
}
|
|
102879
|
+
async function hideVideoElements(page, videoIds) {
|
|
102880
|
+
if (videoIds.length === 0) return;
|
|
102881
|
+
await page.evaluate((ids) => {
|
|
102882
|
+
for (const id of ids) {
|
|
102883
|
+
const el = document.getElementById(id);
|
|
102884
|
+
if (el) {
|
|
102885
|
+
el.style.setProperty("visibility", "hidden", "important");
|
|
102886
|
+
el.style.setProperty("opacity", "0", "important");
|
|
102887
|
+
const img = document.getElementById(`__render_frame_${id}__`);
|
|
102888
|
+
if (img) img.style.setProperty("visibility", "hidden", "important");
|
|
102889
|
+
}
|
|
102890
|
+
}
|
|
102891
|
+
}, videoIds);
|
|
102892
|
+
}
|
|
102893
|
+
async function showVideoElements(page, videoIds) {
|
|
102894
|
+
if (videoIds.length === 0) return;
|
|
102895
|
+
await page.evaluate((ids) => {
|
|
102896
|
+
for (const id of ids) {
|
|
102897
|
+
const el = document.getElementById(id);
|
|
102898
|
+
if (el) {
|
|
102899
|
+
el.style.removeProperty("visibility");
|
|
102900
|
+
el.style.removeProperty("opacity");
|
|
102901
|
+
const img = document.getElementById(`__render_frame_${id}__`);
|
|
102902
|
+
if (img) img.style.removeProperty("visibility");
|
|
102903
|
+
}
|
|
102904
|
+
}
|
|
102905
|
+
}, videoIds);
|
|
102906
|
+
}
|
|
102907
|
+
async function queryElementStacking(page, nativeHdrVideoIds) {
|
|
102908
|
+
const hdrIds = Array.from(nativeHdrVideoIds);
|
|
102909
|
+
return page.evaluate((hdrIdList) => {
|
|
102910
|
+
const hdrSet = new Set(hdrIdList);
|
|
102911
|
+
const elements = document.querySelectorAll("[data-start]");
|
|
102912
|
+
const results = [];
|
|
102913
|
+
function getEffectiveZIndex(node) {
|
|
102914
|
+
let current = node;
|
|
102915
|
+
while (current) {
|
|
102916
|
+
const cs = window.getComputedStyle(current);
|
|
102917
|
+
const pos = cs.position;
|
|
102918
|
+
const z = parseInt(cs.zIndex);
|
|
102919
|
+
if (!Number.isNaN(z) && pos !== "static") return z;
|
|
102920
|
+
current = current.parentElement;
|
|
102921
|
+
}
|
|
102922
|
+
return 0;
|
|
102923
|
+
}
|
|
102924
|
+
for (const el of elements) {
|
|
102925
|
+
const id = el.id;
|
|
102926
|
+
if (!id) continue;
|
|
102927
|
+
const rect = el.getBoundingClientRect();
|
|
102928
|
+
const style = window.getComputedStyle(el);
|
|
102929
|
+
const zIndex = getEffectiveZIndex(el);
|
|
102930
|
+
const opacity = parseFloat(style.opacity) || 1;
|
|
102931
|
+
const visible = style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
|
|
102932
|
+
results.push({
|
|
102933
|
+
id,
|
|
102934
|
+
zIndex,
|
|
102935
|
+
x: Math.round(rect.x),
|
|
102936
|
+
y: Math.round(rect.y),
|
|
102937
|
+
width: Math.round(rect.width),
|
|
102938
|
+
height: Math.round(rect.height),
|
|
102939
|
+
opacity,
|
|
102940
|
+
visible,
|
|
102941
|
+
isHdr: hdrSet.has(id)
|
|
102942
|
+
});
|
|
102943
|
+
}
|
|
102944
|
+
return results;
|
|
102945
|
+
}, hdrIds);
|
|
102946
|
+
}
|
|
102679
102947
|
|
|
102680
102948
|
// ../engine/src/services/audioMixer.ts
|
|
102681
102949
|
import { existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
|
|
@@ -105786,6 +106054,292 @@ var serve = (options, listeningListener) => {
|
|
|
105786
106054
|
return server;
|
|
105787
106055
|
};
|
|
105788
106056
|
|
|
106057
|
+
// ../engine/src/utils/alphaBlit.ts
|
|
106058
|
+
import { inflateSync } from "zlib";
|
|
106059
|
+
function paeth(a, b, c) {
|
|
106060
|
+
const p = a + b - c;
|
|
106061
|
+
const pa = Math.abs(p - a);
|
|
106062
|
+
const pb = Math.abs(p - b);
|
|
106063
|
+
const pc = Math.abs(p - c);
|
|
106064
|
+
if (pa <= pb && pa <= pc) return a;
|
|
106065
|
+
if (pb <= pc) return b;
|
|
106066
|
+
return c;
|
|
106067
|
+
}
|
|
106068
|
+
function decodePngRaw(buf, caller) {
|
|
106069
|
+
if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71 || buf[4] !== 13 || buf[5] !== 10 || buf[6] !== 26 || buf[7] !== 10) {
|
|
106070
|
+
throw new Error(`${caller}: not a PNG file`);
|
|
106071
|
+
}
|
|
106072
|
+
let pos = 8;
|
|
106073
|
+
let width = 0;
|
|
106074
|
+
let height = 0;
|
|
106075
|
+
let bitDepth = 0;
|
|
106076
|
+
let colorType = 0;
|
|
106077
|
+
let interlace = 0;
|
|
106078
|
+
let sawIhdr = false;
|
|
106079
|
+
const idatChunks = [];
|
|
106080
|
+
while (pos + 12 <= buf.length) {
|
|
106081
|
+
const chunkLen = buf.readUInt32BE(pos);
|
|
106082
|
+
const chunkType = buf.toString("ascii", pos + 4, pos + 8);
|
|
106083
|
+
const chunkData = buf.subarray(pos + 8, pos + 8 + chunkLen);
|
|
106084
|
+
if (chunkType === "IHDR") {
|
|
106085
|
+
width = chunkData.readUInt32BE(0);
|
|
106086
|
+
height = chunkData.readUInt32BE(4);
|
|
106087
|
+
bitDepth = chunkData[8] ?? 0;
|
|
106088
|
+
colorType = chunkData[9] ?? 0;
|
|
106089
|
+
interlace = chunkData[12] ?? 0;
|
|
106090
|
+
sawIhdr = true;
|
|
106091
|
+
} else if (chunkType === "IDAT") {
|
|
106092
|
+
idatChunks.push(Buffer.from(chunkData));
|
|
106093
|
+
} else if (chunkType === "IEND") {
|
|
106094
|
+
break;
|
|
106095
|
+
}
|
|
106096
|
+
pos += 12 + chunkLen;
|
|
106097
|
+
}
|
|
106098
|
+
if (!sawIhdr) {
|
|
106099
|
+
throw new Error(`${caller}: PNG missing IHDR chunk`);
|
|
106100
|
+
}
|
|
106101
|
+
if (colorType !== 2 && colorType !== 6) {
|
|
106102
|
+
throw new Error(`${caller}: unsupported color type ${colorType} (expected 2=RGB or 6=RGBA)`);
|
|
106103
|
+
}
|
|
106104
|
+
if (interlace !== 0) {
|
|
106105
|
+
throw new Error(
|
|
106106
|
+
`${caller}: Adam7-interlaced PNGs are not supported (interlace method ${interlace})`
|
|
106107
|
+
);
|
|
106108
|
+
}
|
|
106109
|
+
const channels = colorType === 6 ? 4 : 3;
|
|
106110
|
+
const bpp = channels * (bitDepth / 8);
|
|
106111
|
+
const stride = width * bpp;
|
|
106112
|
+
const compressed = Buffer.concat(idatChunks);
|
|
106113
|
+
const decompressed = inflateSync(compressed);
|
|
106114
|
+
const rawPixels = Buffer.allocUnsafe(height * stride);
|
|
106115
|
+
const prevRow = new Uint8Array(stride);
|
|
106116
|
+
const currRow = new Uint8Array(stride);
|
|
106117
|
+
let srcPos = 0;
|
|
106118
|
+
for (let y = 0; y < height; y++) {
|
|
106119
|
+
const filterType = decompressed[srcPos++] ?? 0;
|
|
106120
|
+
const rawRow = decompressed.subarray(srcPos, srcPos + stride);
|
|
106121
|
+
srcPos += stride;
|
|
106122
|
+
switch (filterType) {
|
|
106123
|
+
case 0:
|
|
106124
|
+
currRow.set(rawRow);
|
|
106125
|
+
break;
|
|
106126
|
+
case 1:
|
|
106127
|
+
for (let x = 0; x < stride; x++) {
|
|
106128
|
+
currRow[x] = (rawRow[x] ?? 0) + (x >= bpp ? currRow[x - bpp] ?? 0 : 0) & 255;
|
|
106129
|
+
}
|
|
106130
|
+
break;
|
|
106131
|
+
case 2:
|
|
106132
|
+
for (let x = 0; x < stride; x++) {
|
|
106133
|
+
currRow[x] = (rawRow[x] ?? 0) + (prevRow[x] ?? 0) & 255;
|
|
106134
|
+
}
|
|
106135
|
+
break;
|
|
106136
|
+
case 3:
|
|
106137
|
+
for (let x = 0; x < stride; x++) {
|
|
106138
|
+
const left2 = x >= bpp ? currRow[x - bpp] ?? 0 : 0;
|
|
106139
|
+
const up = prevRow[x] ?? 0;
|
|
106140
|
+
currRow[x] = (rawRow[x] ?? 0) + Math.floor((left2 + up) / 2) & 255;
|
|
106141
|
+
}
|
|
106142
|
+
break;
|
|
106143
|
+
case 4:
|
|
106144
|
+
for (let x = 0; x < stride; x++) {
|
|
106145
|
+
const left2 = x >= bpp ? currRow[x - bpp] ?? 0 : 0;
|
|
106146
|
+
const up = prevRow[x] ?? 0;
|
|
106147
|
+
const upLeft = x >= bpp ? prevRow[x - bpp] ?? 0 : 0;
|
|
106148
|
+
currRow[x] = (rawRow[x] ?? 0) + paeth(left2, up, upLeft) & 255;
|
|
106149
|
+
}
|
|
106150
|
+
break;
|
|
106151
|
+
default:
|
|
106152
|
+
throw new Error(`${caller}: unknown filter type ${filterType} at row ${y}`);
|
|
106153
|
+
}
|
|
106154
|
+
rawPixels.set(currRow, y * stride);
|
|
106155
|
+
prevRow.set(currRow);
|
|
106156
|
+
}
|
|
106157
|
+
return { width, height, bitDepth, colorType, rawPixels };
|
|
106158
|
+
}
|
|
106159
|
+
function decodePng(buf) {
|
|
106160
|
+
const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePng");
|
|
106161
|
+
if (bitDepth !== 8) {
|
|
106162
|
+
throw new Error(`decodePng: unsupported bit depth ${bitDepth} (expected 8)`);
|
|
106163
|
+
}
|
|
106164
|
+
const output2 = new Uint8Array(width * height * 4);
|
|
106165
|
+
if (colorType === 6) {
|
|
106166
|
+
output2.set(rawPixels);
|
|
106167
|
+
} else {
|
|
106168
|
+
for (let i = 0; i < width * height; i++) {
|
|
106169
|
+
output2[i * 4 + 0] = rawPixels[i * 3 + 0] ?? 0;
|
|
106170
|
+
output2[i * 4 + 1] = rawPixels[i * 3 + 1] ?? 0;
|
|
106171
|
+
output2[i * 4 + 2] = rawPixels[i * 3 + 2] ?? 0;
|
|
106172
|
+
output2[i * 4 + 3] = 255;
|
|
106173
|
+
}
|
|
106174
|
+
}
|
|
106175
|
+
return { width, height, data: output2 };
|
|
106176
|
+
}
|
|
106177
|
+
function decodePngToRgb48le(buf) {
|
|
106178
|
+
const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePngToRgb48le");
|
|
106179
|
+
if (bitDepth !== 16) {
|
|
106180
|
+
throw new Error(`decodePngToRgb48le: unsupported bit depth ${bitDepth} (expected 16)`);
|
|
106181
|
+
}
|
|
106182
|
+
const bpp = colorType === 6 ? 8 : 6;
|
|
106183
|
+
const output2 = Buffer.allocUnsafe(width * height * 6);
|
|
106184
|
+
for (let y = 0; y < height; y++) {
|
|
106185
|
+
const dstBase = y * width * 6;
|
|
106186
|
+
const srcRowBase = y * width * bpp;
|
|
106187
|
+
for (let x = 0; x < width; x++) {
|
|
106188
|
+
const srcBase = srcRowBase + x * bpp;
|
|
106189
|
+
output2[dstBase + x * 6 + 0] = rawPixels[srcBase + 1] ?? 0;
|
|
106190
|
+
output2[dstBase + x * 6 + 1] = rawPixels[srcBase + 0] ?? 0;
|
|
106191
|
+
output2[dstBase + x * 6 + 2] = rawPixels[srcBase + 3] ?? 0;
|
|
106192
|
+
output2[dstBase + x * 6 + 3] = rawPixels[srcBase + 2] ?? 0;
|
|
106193
|
+
output2[dstBase + x * 6 + 4] = rawPixels[srcBase + 5] ?? 0;
|
|
106194
|
+
output2[dstBase + x * 6 + 5] = rawPixels[srcBase + 4] ?? 0;
|
|
106195
|
+
}
|
|
106196
|
+
}
|
|
106197
|
+
return { width, height, data: output2 };
|
|
106198
|
+
}
|
|
106199
|
+
function buildSrgbToHdrLut(transfer) {
|
|
106200
|
+
const lut = new Uint16Array(256);
|
|
106201
|
+
const hlgA = 0.17883277;
|
|
106202
|
+
const hlgB = 1 - 4 * hlgA;
|
|
106203
|
+
const hlgC = 0.5 - hlgA * Math.log(4 * hlgA);
|
|
106204
|
+
const pqM1 = 0.1593017578125;
|
|
106205
|
+
const pqM2 = 78.84375;
|
|
106206
|
+
const pqC1 = 0.8359375;
|
|
106207
|
+
const pqC2 = 18.8515625;
|
|
106208
|
+
const pqC3 = 18.6875;
|
|
106209
|
+
const pqMaxNits = 1e4;
|
|
106210
|
+
const sdrNits = 203;
|
|
106211
|
+
for (let i = 0; i < 256; i++) {
|
|
106212
|
+
const v = i / 255;
|
|
106213
|
+
const linear = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
106214
|
+
let signal;
|
|
106215
|
+
if (transfer === "hlg") {
|
|
106216
|
+
signal = linear <= 1 / 12 ? Math.sqrt(3 * linear) : hlgA * Math.log(12 * linear - hlgB) + hlgC;
|
|
106217
|
+
} else {
|
|
106218
|
+
const Lp = Math.max(0, linear * sdrNits / pqMaxNits);
|
|
106219
|
+
const Lm1 = Math.pow(Lp, pqM1);
|
|
106220
|
+
signal = Math.pow((pqC1 + pqC2 * Lm1) / (1 + pqC3 * Lm1), pqM2);
|
|
106221
|
+
}
|
|
106222
|
+
lut[i] = Math.min(65535, Math.round(signal * 65535));
|
|
106223
|
+
}
|
|
106224
|
+
return lut;
|
|
106225
|
+
}
|
|
106226
|
+
var SRGB_TO_HLG = buildSrgbToHdrLut("hlg");
|
|
106227
|
+
var SRGB_TO_PQ = buildSrgbToHdrLut("pq");
|
|
106228
|
+
function getSrgbToHdrLut(transfer) {
|
|
106229
|
+
return transfer === "pq" ? SRGB_TO_PQ : SRGB_TO_HLG;
|
|
106230
|
+
}
|
|
106231
|
+
function blitRgba8OverRgb48le(domRgba, canvas, width, height, transfer = "hlg") {
|
|
106232
|
+
const pixelCount = width * height;
|
|
106233
|
+
const lut = getSrgbToHdrLut(transfer);
|
|
106234
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
106235
|
+
const da = domRgba[i * 4 + 3] ?? 0;
|
|
106236
|
+
if (da === 0) {
|
|
106237
|
+
continue;
|
|
106238
|
+
} else if (da === 255) {
|
|
106239
|
+
const r16 = lut[domRgba[i * 4 + 0] ?? 0] ?? 0;
|
|
106240
|
+
const g16 = lut[domRgba[i * 4 + 1] ?? 0] ?? 0;
|
|
106241
|
+
const b16 = lut[domRgba[i * 4 + 2] ?? 0] ?? 0;
|
|
106242
|
+
canvas.writeUInt16LE(r16, i * 6);
|
|
106243
|
+
canvas.writeUInt16LE(g16, i * 6 + 2);
|
|
106244
|
+
canvas.writeUInt16LE(b16, i * 6 + 4);
|
|
106245
|
+
} else {
|
|
106246
|
+
const alpha = da / 255;
|
|
106247
|
+
const invAlpha = 1 - alpha;
|
|
106248
|
+
const hdrR = (canvas[i * 6 + 0] ?? 0) | (canvas[i * 6 + 1] ?? 0) << 8;
|
|
106249
|
+
const hdrG = (canvas[i * 6 + 2] ?? 0) | (canvas[i * 6 + 3] ?? 0) << 8;
|
|
106250
|
+
const hdrB = (canvas[i * 6 + 4] ?? 0) | (canvas[i * 6 + 5] ?? 0) << 8;
|
|
106251
|
+
const domR = lut[domRgba[i * 4 + 0] ?? 0] ?? 0;
|
|
106252
|
+
const domG = lut[domRgba[i * 4 + 1] ?? 0] ?? 0;
|
|
106253
|
+
const domB = lut[domRgba[i * 4 + 2] ?? 0] ?? 0;
|
|
106254
|
+
canvas.writeUInt16LE(Math.round(domR * alpha + hdrR * invAlpha), i * 6);
|
|
106255
|
+
canvas.writeUInt16LE(Math.round(domG * alpha + hdrG * invAlpha), i * 6 + 2);
|
|
106256
|
+
canvas.writeUInt16LE(Math.round(domB * alpha + hdrB * invAlpha), i * 6 + 4);
|
|
106257
|
+
}
|
|
106258
|
+
}
|
|
106259
|
+
}
|
|
106260
|
+
function cornerAlpha(px, py, cx, cy, r) {
|
|
106261
|
+
const dx = px - cx;
|
|
106262
|
+
const dy = py - cy;
|
|
106263
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
106264
|
+
if (dist > r + 0.5) return 0;
|
|
106265
|
+
if (dist > r - 0.5) return r + 0.5 - dist;
|
|
106266
|
+
return 1;
|
|
106267
|
+
}
|
|
106268
|
+
function roundedRectAlpha(px, py, w, h, radii) {
|
|
106269
|
+
const [tl, tr, br, bl] = radii;
|
|
106270
|
+
if (px < tl && py < tl) return cornerAlpha(px, py, tl, tl, tl);
|
|
106271
|
+
if (px >= w - tr && py < tr) return cornerAlpha(px, py, w - tr, tr, tr);
|
|
106272
|
+
if (px >= w - br && py >= h - br) return cornerAlpha(px, py, w - br, h - br, br);
|
|
106273
|
+
if (px < bl && py >= h - bl) return cornerAlpha(px, py, bl, h - bl, bl);
|
|
106274
|
+
return 1;
|
|
106275
|
+
}
|
|
106276
|
+
function blitRgb48leRegion(canvas, source2, dx, dy, sw, sh, canvasWidth, canvasHeight, opacity, borderRadius) {
|
|
106277
|
+
if (sw <= 0 || sh <= 0) return;
|
|
106278
|
+
const op = opacity ?? 1;
|
|
106279
|
+
const x0 = Math.max(0, dx);
|
|
106280
|
+
const y0 = Math.max(0, dy);
|
|
106281
|
+
const x1 = Math.min(canvasWidth, dx + sw);
|
|
106282
|
+
const y1 = Math.min(canvasHeight, dy + sh);
|
|
106283
|
+
if (x0 >= x1 || y0 >= y1) return;
|
|
106284
|
+
const clippedW = x1 - x0;
|
|
106285
|
+
const srcOffsetX = x0 - dx;
|
|
106286
|
+
const srcOffsetY = y0 - dy;
|
|
106287
|
+
const hasMask = borderRadius !== void 0;
|
|
106288
|
+
if (op >= 0.999 && !hasMask) {
|
|
106289
|
+
for (let y = 0; y < y1 - y0; y++) {
|
|
106290
|
+
const srcRowOff = ((srcOffsetY + y) * sw + srcOffsetX) * 6;
|
|
106291
|
+
const dstRowOff = ((y0 + y) * canvasWidth + x0) * 6;
|
|
106292
|
+
source2.copy(canvas, dstRowOff, srcRowOff, srcRowOff + clippedW * 6);
|
|
106293
|
+
}
|
|
106294
|
+
} else {
|
|
106295
|
+
for (let y = 0; y < y1 - y0; y++) {
|
|
106296
|
+
for (let x = 0; x < clippedW; x++) {
|
|
106297
|
+
let effectiveOp = op;
|
|
106298
|
+
if (hasMask) {
|
|
106299
|
+
const ma = roundedRectAlpha(srcOffsetX + x, srcOffsetY + y, sw, sh, borderRadius);
|
|
106300
|
+
if (ma <= 0) continue;
|
|
106301
|
+
effectiveOp *= ma;
|
|
106302
|
+
}
|
|
106303
|
+
const srcOff = ((srcOffsetY + y) * sw + srcOffsetX + x) * 6;
|
|
106304
|
+
const dstOff = ((y0 + y) * canvasWidth + x0 + x) * 6;
|
|
106305
|
+
if (effectiveOp >= 0.999) {
|
|
106306
|
+
source2.copy(canvas, dstOff, srcOff, srcOff + 6);
|
|
106307
|
+
} else {
|
|
106308
|
+
const invEff = 1 - effectiveOp;
|
|
106309
|
+
const sr = source2.readUInt16LE(srcOff);
|
|
106310
|
+
const sg = source2.readUInt16LE(srcOff + 2);
|
|
106311
|
+
const sb = source2.readUInt16LE(srcOff + 4);
|
|
106312
|
+
const dr = canvas.readUInt16LE(dstOff);
|
|
106313
|
+
const dg = canvas.readUInt16LE(dstOff + 2);
|
|
106314
|
+
const db = canvas.readUInt16LE(dstOff + 4);
|
|
106315
|
+
canvas.writeUInt16LE(Math.round(sr * effectiveOp + dr * invEff), dstOff);
|
|
106316
|
+
canvas.writeUInt16LE(Math.round(sg * effectiveOp + dg * invEff), dstOff + 2);
|
|
106317
|
+
canvas.writeUInt16LE(Math.round(sb * effectiveOp + db * invEff), dstOff + 4);
|
|
106318
|
+
}
|
|
106319
|
+
}
|
|
106320
|
+
}
|
|
106321
|
+
}
|
|
106322
|
+
}
|
|
106323
|
+
|
|
106324
|
+
// ../engine/src/utils/layerCompositor.ts
|
|
106325
|
+
function groupIntoLayers(elements) {
|
|
106326
|
+
const sorted = [...elements].sort((a, b) => a.zIndex - b.zIndex);
|
|
106327
|
+
const layers = [];
|
|
106328
|
+
for (const el of sorted) {
|
|
106329
|
+
if (el.isHdr) {
|
|
106330
|
+
layers.push({ type: "hdr", element: el });
|
|
106331
|
+
} else {
|
|
106332
|
+
const last2 = layers[layers.length - 1];
|
|
106333
|
+
if (last2 && last2.type === "dom") {
|
|
106334
|
+
last2.elementIds.push(el.id);
|
|
106335
|
+
} else {
|
|
106336
|
+
layers.push({ type: "dom", elementIds: [el.id] });
|
|
106337
|
+
}
|
|
106338
|
+
}
|
|
106339
|
+
}
|
|
106340
|
+
return layers;
|
|
106341
|
+
}
|
|
106342
|
+
|
|
105789
106343
|
// src/services/renderOrchestrator.ts
|
|
105790
106344
|
import { join as join15, dirname as dirname10, resolve as resolve10 } from "path";
|
|
105791
106345
|
import { randomUUID } from "crypto";
|
|
@@ -106106,6 +106660,10 @@ var RENDER_MODE_SCRIPT = `(function() {
|
|
|
106106
106660
|
}
|
|
106107
106661
|
waitForPlayer();
|
|
106108
106662
|
})();`;
|
|
106663
|
+
var HF_EARLY_STUB = `(function() {
|
|
106664
|
+
if (typeof window === "undefined") return;
|
|
106665
|
+
if (!window.__hf) window.__hf = {};
|
|
106666
|
+
})();`;
|
|
106109
106667
|
var HF_BRIDGE_SCRIPT = `(function() {
|
|
106110
106668
|
var __realSetInterval =
|
|
106111
106669
|
window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetInterval === "function"
|
|
@@ -106151,20 +106709,24 @@ var HF_BRIDGE_SCRIPT = `(function() {
|
|
|
106151
106709
|
if (!p || typeof p.renderSeek !== "function" || typeof p.getDuration !== "function") {
|
|
106152
106710
|
return false;
|
|
106153
106711
|
}
|
|
106154
|
-
window.__hf
|
|
106155
|
-
|
|
106712
|
+
var hf = window.__hf || {};
|
|
106713
|
+
Object.defineProperty(hf, "duration", {
|
|
106714
|
+
configurable: true,
|
|
106715
|
+
enumerable: true,
|
|
106716
|
+
get: function() {
|
|
106156
106717
|
var d = p.getDuration();
|
|
106157
106718
|
return d > 0 ? d : getDeclaredDuration();
|
|
106158
106719
|
},
|
|
106159
|
-
|
|
106160
|
-
|
|
106161
|
-
|
|
106162
|
-
|
|
106163
|
-
|
|
106164
|
-
|
|
106165
|
-
|
|
106166
|
-
|
|
106720
|
+
});
|
|
106721
|
+
hf.seek = function(t) {
|
|
106722
|
+
p.renderSeek(t);
|
|
106723
|
+
var nextTimeMs = (Math.max(0, Number(t) || 0)) * 1000;
|
|
106724
|
+
if (window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.seekToTime === "function") {
|
|
106725
|
+
window.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
|
|
106726
|
+
}
|
|
106727
|
+
seekSameOriginChildFrames(window, nextTimeMs);
|
|
106167
106728
|
};
|
|
106729
|
+
window.__hf = hf;
|
|
106168
106730
|
return true;
|
|
106169
106731
|
}
|
|
106170
106732
|
if (bridge()) return;
|
|
@@ -106246,7 +106808,7 @@ ${headTags}`);
|
|
|
106246
106808
|
}
|
|
106247
106809
|
function createFileServer2(options) {
|
|
106248
106810
|
const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
|
|
106249
|
-
const preHeadScripts = options.preHeadScripts ?? [];
|
|
106811
|
+
const preHeadScripts = [HF_EARLY_STUB, ...options.preHeadScripts ?? []];
|
|
106250
106812
|
const headScripts = options.headScripts ?? [getVerifiedHyperframeRuntimeSource()];
|
|
106251
106813
|
const bodyScripts = options.bodyScripts ?? [RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT];
|
|
106252
106814
|
const app = new Hono2();
|
|
@@ -107506,6 +108068,24 @@ async function safeCleanup(label, fn, log = defaultLogger) {
|
|
|
107506
108068
|
});
|
|
107507
108069
|
}
|
|
107508
108070
|
}
|
|
108071
|
+
var frameDirMaxIndexCache = /* @__PURE__ */ new Map();
|
|
108072
|
+
var FRAME_FILENAME_RE = /^frame_(\d+)\.png$/;
|
|
108073
|
+
function getMaxFrameIndex(frameDir) {
|
|
108074
|
+
const cached = frameDirMaxIndexCache.get(frameDir);
|
|
108075
|
+
if (cached !== void 0) return cached;
|
|
108076
|
+
let max = 0;
|
|
108077
|
+
try {
|
|
108078
|
+
for (const name of readdirSync6(frameDir)) {
|
|
108079
|
+
const m = FRAME_FILENAME_RE.exec(name);
|
|
108080
|
+
if (!m) continue;
|
|
108081
|
+
const n = Number(m[1]);
|
|
108082
|
+
if (Number.isFinite(n) && n > max) max = n;
|
|
108083
|
+
}
|
|
108084
|
+
} catch {
|
|
108085
|
+
}
|
|
108086
|
+
frameDirMaxIndexCache.set(frameDir, max);
|
|
108087
|
+
return max;
|
|
108088
|
+
}
|
|
107509
108089
|
var RenderCancelledError = class extends Error {
|
|
107510
108090
|
reason;
|
|
107511
108091
|
constructor(message = "render_cancelled", reason = "aborted") {
|
|
@@ -107905,7 +108485,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
107905
108485
|
}
|
|
107906
108486
|
}
|
|
107907
108487
|
}
|
|
107908
|
-
} catch {
|
|
108488
|
+
} catch (err) {
|
|
108489
|
+
log.warn("Failed to gather browser diagnostics for zero-duration composition", {
|
|
108490
|
+
error: err instanceof Error ? err.message : String(err)
|
|
108491
|
+
});
|
|
107909
108492
|
diagnostics.push("(Could not gather browser diagnostics \u2014 page may have crashed)");
|
|
107910
108493
|
}
|
|
107911
108494
|
const hint = diagnostics.length > 0 ? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ") : "\n\nCheck that GSAP timelines are registered on window.__timelines.";
|
|
@@ -107929,8 +108512,26 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
107929
108512
|
updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
|
|
107930
108513
|
let frameLookup = null;
|
|
107931
108514
|
const compiledDir = join15(workDir, "compiled");
|
|
108515
|
+
let extractionResult = null;
|
|
108516
|
+
const nativeHdrVideoIds = /* @__PURE__ */ new Set();
|
|
107932
108517
|
if (composition.videos.length > 0) {
|
|
107933
|
-
|
|
108518
|
+
await Promise.all(
|
|
108519
|
+
composition.videos.map(async (v) => {
|
|
108520
|
+
let videoPath = v.src;
|
|
108521
|
+
if (!videoPath.startsWith("/")) {
|
|
108522
|
+
const fromCompiled = existsSync15(join15(compiledDir, videoPath)) ? join15(compiledDir, videoPath) : join15(projectDir, videoPath);
|
|
108523
|
+
videoPath = fromCompiled;
|
|
108524
|
+
}
|
|
108525
|
+
if (!existsSync15(videoPath)) return;
|
|
108526
|
+
const meta = await extractVideoMetadata(videoPath);
|
|
108527
|
+
if (isHdrColorSpace(meta.colorSpace)) {
|
|
108528
|
+
nativeHdrVideoIds.add(v.id);
|
|
108529
|
+
}
|
|
108530
|
+
})
|
|
108531
|
+
);
|
|
108532
|
+
}
|
|
108533
|
+
if (composition.videos.length > 0) {
|
|
108534
|
+
extractionResult = await extractAllVideoFrames(
|
|
107934
108535
|
composition.videos,
|
|
107935
108536
|
projectDir,
|
|
107936
108537
|
{ fps: job.config.fps, outputDir: join15(workDir, "video-frames") },
|
|
@@ -107965,6 +108566,23 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
107965
108566
|
} else {
|
|
107966
108567
|
perfStages.videoExtractMs = Date.now() - stage2Start;
|
|
107967
108568
|
}
|
|
108569
|
+
let effectiveHdr;
|
|
108570
|
+
if (frameLookup) {
|
|
108571
|
+
const colorSpaces = (extractionResult?.extracted ?? []).map((ext) => ext.metadata.colorSpace);
|
|
108572
|
+
const info = analyzeCompositionHdr(colorSpaces);
|
|
108573
|
+
if (info.hasHdr && info.dominantTransfer) {
|
|
108574
|
+
effectiveHdr = { transfer: info.dominantTransfer };
|
|
108575
|
+
}
|
|
108576
|
+
}
|
|
108577
|
+
if (effectiveHdr && outputFormat !== "mp4") {
|
|
108578
|
+
log.info(`[Render] HDR source detected but format is ${outputFormat} \u2014 using SDR`);
|
|
108579
|
+
effectiveHdr = void 0;
|
|
108580
|
+
}
|
|
108581
|
+
if (effectiveHdr) {
|
|
108582
|
+
log.info(
|
|
108583
|
+
`[Render] HDR source detected \u2014 output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
|
|
108584
|
+
);
|
|
108585
|
+
}
|
|
107968
108586
|
const stage3Start = Date.now();
|
|
107969
108587
|
updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
|
|
107970
108588
|
const audioOutputPath = join15(workDir, "audio.aac");
|
|
@@ -108010,218 +108628,398 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
108010
108628
|
const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
|
|
108011
108629
|
const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
|
|
108012
108630
|
const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
|
|
108013
|
-
const
|
|
108014
|
-
const
|
|
108015
|
-
const
|
|
108016
|
-
const baseEncoderOpts = {
|
|
108017
|
-
fps: job.config.fps,
|
|
108018
|
-
width,
|
|
108019
|
-
height,
|
|
108020
|
-
codec: preset.codec,
|
|
108021
|
-
preset: preset.preset,
|
|
108022
|
-
quality: effectiveQuality,
|
|
108023
|
-
bitrate: effectiveBitrate,
|
|
108024
|
-
pixelFormat: preset.pixelFormat,
|
|
108025
|
-
useGpu: job.config.useGpu
|
|
108026
|
-
};
|
|
108631
|
+
const hasHdrVideo = effectiveHdr && composition.videos.length > 0 && frameLookup;
|
|
108632
|
+
const encoderHdr = hasHdrVideo ? effectiveHdr : void 0;
|
|
108633
|
+
const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr);
|
|
108027
108634
|
job.framesRendered = 0;
|
|
108028
|
-
|
|
108029
|
-
|
|
108030
|
-
|
|
108635
|
+
if (hasHdrVideo) {
|
|
108636
|
+
log.info("[Render] HDR layered composite: z-ordered DOM + native HLG video layers");
|
|
108637
|
+
const hdrVideoIds = composition.videos.filter((v) => nativeHdrVideoIds.has(v.id)).map((v) => v.id);
|
|
108638
|
+
const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
|
|
108639
|
+
for (const v of composition.videos) {
|
|
108640
|
+
if (!hdrVideoIds.includes(v.id)) continue;
|
|
108641
|
+
let srcPath = v.src;
|
|
108642
|
+
if (!srcPath.startsWith("/")) {
|
|
108643
|
+
const fromCompiled = join15(compiledDir, srcPath);
|
|
108644
|
+
srcPath = existsSync15(fromCompiled) ? fromCompiled : join15(projectDir, srcPath);
|
|
108645
|
+
}
|
|
108646
|
+
hdrVideoSrcPaths.set(v.id, srcPath);
|
|
108647
|
+
}
|
|
108648
|
+
const domSession = await createCaptureSession(
|
|
108649
|
+
fileServer.url,
|
|
108650
|
+
framesDir,
|
|
108651
|
+
captureOptions,
|
|
108652
|
+
createVideoFrameInjector(frameLookup),
|
|
108653
|
+
cfg
|
|
108654
|
+
);
|
|
108655
|
+
await initializeSession(domSession);
|
|
108656
|
+
assertNotAborted();
|
|
108657
|
+
lastBrowserConsole = domSession.browserConsoleBuffer;
|
|
108658
|
+
await initTransparentBackground(domSession.page);
|
|
108659
|
+
const hdrEncoder = await spawnStreamingEncoder(
|
|
108031
108660
|
videoOnlyPath,
|
|
108032
108661
|
{
|
|
108033
|
-
|
|
108034
|
-
|
|
108662
|
+
fps: job.config.fps,
|
|
108663
|
+
width,
|
|
108664
|
+
height,
|
|
108665
|
+
codec: preset.codec,
|
|
108666
|
+
preset: preset.preset,
|
|
108667
|
+
quality: preset.quality,
|
|
108668
|
+
pixelFormat: preset.pixelFormat,
|
|
108669
|
+
hdr: preset.hdr,
|
|
108670
|
+
rawInputFormat: "rgb48le"
|
|
108035
108671
|
},
|
|
108036
|
-
abortSignal
|
|
108672
|
+
abortSignal,
|
|
108673
|
+
{ ffmpegStreamingTimeout: 36e5 }
|
|
108037
108674
|
);
|
|
108038
108675
|
assertNotAborted();
|
|
108039
|
-
|
|
108040
|
-
|
|
108041
|
-
const
|
|
108042
|
-
|
|
108043
|
-
|
|
108044
|
-
const
|
|
108045
|
-
|
|
108046
|
-
|
|
108047
|
-
|
|
108048
|
-
|
|
108049
|
-
|
|
108050
|
-
|
|
108051
|
-
|
|
108052
|
-
|
|
108053
|
-
|
|
108054
|
-
|
|
108055
|
-
|
|
108056
|
-
|
|
108057
|
-
|
|
108058
|
-
|
|
108059
|
-
|
|
108060
|
-
|
|
108061
|
-
|
|
108062
|
-
|
|
108063
|
-
|
|
108064
|
-
|
|
108065
|
-
|
|
108066
|
-
|
|
108067
|
-
|
|
108676
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
108677
|
+
const hdrFrameDirs = /* @__PURE__ */ new Map();
|
|
108678
|
+
for (const [videoId, srcPath] of hdrVideoSrcPaths) {
|
|
108679
|
+
const video = composition.videos.find((v) => v.id === videoId);
|
|
108680
|
+
if (!video) continue;
|
|
108681
|
+
const frameDir = join15(framesDir, `hdr_${videoId}`);
|
|
108682
|
+
mkdirSync10(frameDir, { recursive: true });
|
|
108683
|
+
const duration = video.end - video.start;
|
|
108684
|
+
try {
|
|
108685
|
+
execSync2(
|
|
108686
|
+
`ffmpeg -ss ${video.mediaStart} -i "${srcPath}" -t ${duration} -r ${job.config.fps} -vf "scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height}" -pix_fmt rgb48le -c:v png "${join15(frameDir, "frame_%04d.png")}"`,
|
|
108687
|
+
{ maxBuffer: 1024 * 1024, stdio: ["pipe", "pipe", "pipe"] }
|
|
108688
|
+
);
|
|
108689
|
+
} catch (err) {
|
|
108690
|
+
log.warn("HDR frame pre-extraction failed; loop will fill with black", {
|
|
108691
|
+
videoId,
|
|
108692
|
+
srcPath,
|
|
108693
|
+
error: err instanceof Error ? err.message : String(err)
|
|
108694
|
+
});
|
|
108695
|
+
}
|
|
108696
|
+
hdrFrameDirs.set(videoId, frameDir);
|
|
108697
|
+
}
|
|
108698
|
+
assertNotAborted();
|
|
108699
|
+
try {
|
|
108700
|
+
const beforeCaptureHook = domSession.onBeforeCapture;
|
|
108701
|
+
for (let i = 0; i < job.totalFrames; i++) {
|
|
108702
|
+
assertNotAborted();
|
|
108703
|
+
const time = i / job.config.fps;
|
|
108704
|
+
await domSession.page.evaluate((t) => {
|
|
108705
|
+
if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
|
|
108706
|
+
}, time);
|
|
108707
|
+
if (beforeCaptureHook) {
|
|
108708
|
+
await beforeCaptureHook(domSession.page, time);
|
|
108709
|
+
}
|
|
108710
|
+
const stackingInfo = await queryElementStacking(domSession.page, nativeHdrVideoIds);
|
|
108711
|
+
const layers = groupIntoLayers(stackingInfo);
|
|
108712
|
+
if (i % 30 === 0) {
|
|
108713
|
+
const hdrEl = stackingInfo.find((e) => e.isHdr);
|
|
108714
|
+
const hdrInLayers = layers.some((l) => l.type === "hdr");
|
|
108715
|
+
log.debug("[Render] HDR layer composite frame", {
|
|
108716
|
+
frame: i,
|
|
108717
|
+
time: time.toFixed(2),
|
|
108718
|
+
hdrElement: hdrEl ? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width } : null,
|
|
108719
|
+
hdrLayerPresent: hdrInLayers,
|
|
108720
|
+
layerCount: layers.length
|
|
108721
|
+
});
|
|
108722
|
+
}
|
|
108723
|
+
const canvas = Buffer.alloc(width * height * 6);
|
|
108724
|
+
for (const layer of layers) {
|
|
108725
|
+
if (layer.type === "hdr") {
|
|
108726
|
+
const el = layer.element;
|
|
108727
|
+
const frameDir = hdrFrameDirs.get(el.id);
|
|
108728
|
+
const video = composition.videos.find((v) => v.id === el.id);
|
|
108729
|
+
if (!frameDir || !video) continue;
|
|
108730
|
+
const videoFrameIndex = Math.round((time - video.start) * job.config.fps) + 1;
|
|
108731
|
+
const maxIndex = getMaxFrameIndex(frameDir);
|
|
108732
|
+
const inBounds = videoFrameIndex >= 1 && (maxIndex === 0 || videoFrameIndex <= maxIndex);
|
|
108733
|
+
const framePath = inBounds ? join15(frameDir, `frame_${String(videoFrameIndex).padStart(4, "0")}.png`) : null;
|
|
108734
|
+
if (framePath !== null && existsSync15(framePath)) {
|
|
108735
|
+
try {
|
|
108736
|
+
const hdrRgb = decodePngToRgb48le(readFileSync9(framePath)).data;
|
|
108737
|
+
blitRgb48leRegion(
|
|
108738
|
+
canvas,
|
|
108739
|
+
hdrRgb,
|
|
108740
|
+
el.x,
|
|
108741
|
+
el.y,
|
|
108742
|
+
el.width,
|
|
108743
|
+
el.height,
|
|
108744
|
+
width,
|
|
108745
|
+
height,
|
|
108746
|
+
el.opacity < 0.999 ? el.opacity : void 0
|
|
108747
|
+
);
|
|
108748
|
+
} catch (err) {
|
|
108749
|
+
log.warn("HDR layer decode/blit failed; skipping layer for frame", {
|
|
108750
|
+
frameIndex: i,
|
|
108751
|
+
videoId: el.id,
|
|
108752
|
+
framePath,
|
|
108753
|
+
error: err instanceof Error ? err.message : String(err)
|
|
108754
|
+
});
|
|
108755
|
+
}
|
|
108756
|
+
}
|
|
108757
|
+
} else {
|
|
108758
|
+
const allElementIds = stackingInfo.map((e) => e.id);
|
|
108759
|
+
const layerIds = new Set(layer.elementIds);
|
|
108760
|
+
const hideIds = allElementIds.filter(
|
|
108761
|
+
(id) => !layerIds.has(id) || nativeHdrVideoIds.has(id)
|
|
108068
108762
|
);
|
|
108763
|
+
await hideVideoElements(domSession.page, hideIds);
|
|
108764
|
+
const domPng = await captureAlphaPng(domSession.page, width, height);
|
|
108765
|
+
await showVideoElements(domSession.page, hideIds);
|
|
108766
|
+
await domSession.page.evaluate((t) => {
|
|
108767
|
+
if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
|
|
108768
|
+
}, time);
|
|
108769
|
+
try {
|
|
108770
|
+
const { data: domRgba } = decodePng(domPng);
|
|
108771
|
+
const hdrTransfer = effectiveHdr ? effectiveHdr.transfer : "hlg";
|
|
108772
|
+
blitRgba8OverRgb48le(domRgba, canvas, width, height, hdrTransfer);
|
|
108773
|
+
} catch (err) {
|
|
108774
|
+
log.warn("DOM layer decode/blit failed; skipping overlay for frame", {
|
|
108775
|
+
frameIndex: i,
|
|
108776
|
+
layerIds: layer.elementIds,
|
|
108777
|
+
error: err instanceof Error ? err.message : String(err)
|
|
108778
|
+
});
|
|
108779
|
+
}
|
|
108069
108780
|
}
|
|
108070
|
-
},
|
|
108071
|
-
onFrameBuffer,
|
|
108072
|
-
cfg
|
|
108073
|
-
);
|
|
108074
|
-
if (probeSession) {
|
|
108075
|
-
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
108076
|
-
await closeCaptureSession(probeSession);
|
|
108077
|
-
probeSession = null;
|
|
108078
|
-
}
|
|
108079
|
-
} else {
|
|
108080
|
-
const videoInjector = createVideoFrameInjector(frameLookup);
|
|
108081
|
-
const session = probeSession ?? await createCaptureSession(
|
|
108082
|
-
fileServer.url,
|
|
108083
|
-
framesDir,
|
|
108084
|
-
captureOptions,
|
|
108085
|
-
videoInjector,
|
|
108086
|
-
cfg
|
|
108087
|
-
);
|
|
108088
|
-
if (probeSession) {
|
|
108089
|
-
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
108090
|
-
probeSession = null;
|
|
108091
|
-
}
|
|
108092
|
-
try {
|
|
108093
|
-
if (!session.isInitialized) {
|
|
108094
|
-
await initializeSession(session);
|
|
108095
108781
|
}
|
|
108096
|
-
|
|
108097
|
-
|
|
108098
|
-
|
|
108099
|
-
assertNotAborted();
|
|
108100
|
-
const time = i / job.config.fps;
|
|
108101
|
-
const { buffer } = await captureFrameToBuffer(session, i, time);
|
|
108102
|
-
await reorderBuffer.waitForFrame(i);
|
|
108103
|
-
currentEncoder.writeFrame(buffer);
|
|
108104
|
-
reorderBuffer.advanceTo(i + 1);
|
|
108105
|
-
job.framesRendered = i + 1;
|
|
108782
|
+
hdrEncoder.writeFrame(canvas);
|
|
108783
|
+
job.framesRendered = i + 1;
|
|
108784
|
+
if ((i + 1) % 10 === 0 || i + 1 === job.totalFrames) {
|
|
108106
108785
|
const frameProgress = (i + 1) / job.totalFrames;
|
|
108107
|
-
const progress = 25 + frameProgress * 55;
|
|
108108
108786
|
updateJobStatus(
|
|
108109
108787
|
job,
|
|
108110
108788
|
"rendering",
|
|
108111
|
-
`
|
|
108112
|
-
Math.round(
|
|
108789
|
+
`HDR composite frame ${i + 1}/${job.totalFrames}`,
|
|
108790
|
+
Math.round(25 + frameProgress * 55),
|
|
108113
108791
|
onProgress
|
|
108114
108792
|
);
|
|
108115
108793
|
}
|
|
108116
|
-
} finally {
|
|
108117
|
-
lastBrowserConsole = session.browserConsoleBuffer;
|
|
108118
|
-
await closeCaptureSession(session);
|
|
108119
108794
|
}
|
|
108795
|
+
} finally {
|
|
108796
|
+
lastBrowserConsole = domSession.browserConsoleBuffer;
|
|
108797
|
+
await closeCaptureSession(domSession);
|
|
108120
108798
|
}
|
|
108121
|
-
const
|
|
108799
|
+
const hdrEncodeResult = await hdrEncoder.close();
|
|
108122
108800
|
assertNotAborted();
|
|
108123
|
-
if (!
|
|
108124
|
-
throw new Error(`
|
|
108801
|
+
if (!hdrEncodeResult.success) {
|
|
108802
|
+
throw new Error(`HDR encode failed: ${hdrEncodeResult.error}`);
|
|
108125
108803
|
}
|
|
108126
108804
|
perfStages.captureMs = Date.now() - stage4Start;
|
|
108127
|
-
perfStages.encodeMs =
|
|
108805
|
+
perfStages.encodeMs = hdrEncodeResult.durationMs;
|
|
108128
108806
|
} else {
|
|
108129
|
-
|
|
108130
|
-
|
|
108131
|
-
await
|
|
108132
|
-
|
|
108133
|
-
|
|
108134
|
-
|
|
108135
|
-
|
|
108136
|
-
|
|
108137
|
-
|
|
108138
|
-
|
|
108139
|
-
|
|
108140
|
-
|
|
108141
|
-
|
|
108142
|
-
|
|
108807
|
+
let streamingEncoder = null;
|
|
108808
|
+
if (enableStreamingEncode) {
|
|
108809
|
+
streamingEncoder = await spawnStreamingEncoder(
|
|
108810
|
+
videoOnlyPath,
|
|
108811
|
+
{
|
|
108812
|
+
fps: job.config.fps,
|
|
108813
|
+
width,
|
|
108814
|
+
height,
|
|
108815
|
+
codec: preset.codec,
|
|
108816
|
+
preset: preset.preset,
|
|
108817
|
+
quality: preset.quality,
|
|
108818
|
+
pixelFormat: preset.pixelFormat,
|
|
108819
|
+
useGpu: job.config.useGpu,
|
|
108820
|
+
imageFormat: captureOptions.format || "jpeg",
|
|
108821
|
+
hdr: preset.hdr
|
|
108822
|
+
},
|
|
108823
|
+
abortSignal
|
|
108824
|
+
);
|
|
108825
|
+
assertNotAborted();
|
|
108826
|
+
}
|
|
108827
|
+
if (enableStreamingEncode && streamingEncoder) {
|
|
108828
|
+
const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
|
|
108829
|
+
const currentEncoder = streamingEncoder;
|
|
108830
|
+
if (workerCount > 1) {
|
|
108831
|
+
const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
|
|
108832
|
+
const onFrameBuffer = async (frameIndex, buffer) => {
|
|
108833
|
+
await reorderBuffer.waitForFrame(frameIndex);
|
|
108834
|
+
currentEncoder.writeFrame(buffer);
|
|
108835
|
+
reorderBuffer.advanceTo(frameIndex + 1);
|
|
108836
|
+
};
|
|
108837
|
+
await executeParallelCapture(
|
|
108838
|
+
fileServer.url,
|
|
108839
|
+
workDir,
|
|
108840
|
+
tasks,
|
|
108841
|
+
captureOptions,
|
|
108842
|
+
() => createVideoFrameInjector(frameLookup),
|
|
108843
|
+
abortSignal,
|
|
108844
|
+
(progress) => {
|
|
108845
|
+
job.framesRendered = progress.capturedFrames;
|
|
108846
|
+
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
108847
|
+
const progressPct = 25 + frameProgress * 55;
|
|
108848
|
+
if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
|
|
108849
|
+
updateJobStatus(
|
|
108850
|
+
job,
|
|
108851
|
+
"rendering",
|
|
108852
|
+
`Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
|
|
108853
|
+
Math.round(progressPct),
|
|
108854
|
+
onProgress
|
|
108855
|
+
);
|
|
108856
|
+
}
|
|
108857
|
+
},
|
|
108858
|
+
onFrameBuffer,
|
|
108859
|
+
cfg
|
|
108860
|
+
);
|
|
108861
|
+
if (probeSession) {
|
|
108862
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
108863
|
+
await closeCaptureSession(probeSession);
|
|
108864
|
+
probeSession = null;
|
|
108865
|
+
}
|
|
108866
|
+
} else {
|
|
108867
|
+
const videoInjector = createVideoFrameInjector(frameLookup);
|
|
108868
|
+
const session = probeSession ?? await createCaptureSession(
|
|
108869
|
+
fileServer.url,
|
|
108870
|
+
framesDir,
|
|
108871
|
+
captureOptions,
|
|
108872
|
+
videoInjector,
|
|
108873
|
+
cfg
|
|
108874
|
+
);
|
|
108875
|
+
if (probeSession) {
|
|
108876
|
+
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
108877
|
+
probeSession = null;
|
|
108878
|
+
}
|
|
108879
|
+
try {
|
|
108880
|
+
if (!session.isInitialized) {
|
|
108881
|
+
await initializeSession(session);
|
|
108882
|
+
}
|
|
108883
|
+
assertNotAborted();
|
|
108884
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
108885
|
+
for (let i = 0; i < job.totalFrames; i++) {
|
|
108886
|
+
assertNotAborted();
|
|
108887
|
+
const time = i / job.config.fps;
|
|
108888
|
+
const { buffer } = await captureFrameToBuffer(session, i, time);
|
|
108889
|
+
await reorderBuffer.waitForFrame(i);
|
|
108890
|
+
currentEncoder.writeFrame(buffer);
|
|
108891
|
+
reorderBuffer.advanceTo(i + 1);
|
|
108892
|
+
job.framesRendered = i + 1;
|
|
108893
|
+
const frameProgress = (i + 1) / job.totalFrames;
|
|
108894
|
+
const progress = 25 + frameProgress * 55;
|
|
108143
108895
|
updateJobStatus(
|
|
108144
108896
|
job,
|
|
108145
108897
|
"rendering",
|
|
108146
|
-
`
|
|
108147
|
-
Math.round(
|
|
108898
|
+
`Streaming frame ${i + 1}/${job.totalFrames}`,
|
|
108899
|
+
Math.round(progress),
|
|
108148
108900
|
onProgress
|
|
108149
108901
|
);
|
|
108150
108902
|
}
|
|
108151
|
-
}
|
|
108152
|
-
|
|
108153
|
-
|
|
108154
|
-
|
|
108155
|
-
await mergeWorkerFrames(workDir, tasks, framesDir);
|
|
108156
|
-
if (probeSession) {
|
|
108157
|
-
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
108158
|
-
await closeCaptureSession(probeSession);
|
|
108159
|
-
probeSession = null;
|
|
108903
|
+
} finally {
|
|
108904
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
108905
|
+
await closeCaptureSession(session);
|
|
108906
|
+
}
|
|
108160
108907
|
}
|
|
108161
|
-
|
|
108162
|
-
|
|
108163
|
-
|
|
108164
|
-
|
|
108165
|
-
framesDir,
|
|
108166
|
-
captureOptions,
|
|
108167
|
-
videoInjector,
|
|
108168
|
-
cfg
|
|
108169
|
-
);
|
|
108170
|
-
if (probeSession) {
|
|
108171
|
-
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
108172
|
-
probeSession = null;
|
|
108908
|
+
const encodeResult = await currentEncoder.close();
|
|
108909
|
+
assertNotAborted();
|
|
108910
|
+
if (!encodeResult.success) {
|
|
108911
|
+
throw new Error(`Streaming encode failed: ${encodeResult.error}`);
|
|
108173
108912
|
}
|
|
108174
|
-
|
|
108175
|
-
|
|
108176
|
-
|
|
108913
|
+
perfStages.captureMs = Date.now() - stage4Start;
|
|
108914
|
+
perfStages.encodeMs = encodeResult.durationMs;
|
|
108915
|
+
} else {
|
|
108916
|
+
if (workerCount > 1) {
|
|
108917
|
+
const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
|
|
108918
|
+
await executeParallelCapture(
|
|
108919
|
+
fileServer.url,
|
|
108920
|
+
workDir,
|
|
108921
|
+
tasks,
|
|
108922
|
+
captureOptions,
|
|
108923
|
+
() => createVideoFrameInjector(frameLookup),
|
|
108924
|
+
abortSignal,
|
|
108925
|
+
(progress) => {
|
|
108926
|
+
job.framesRendered = progress.capturedFrames;
|
|
108927
|
+
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
108928
|
+
const progressPct = 25 + frameProgress * 45;
|
|
108929
|
+
if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
|
|
108930
|
+
updateJobStatus(
|
|
108931
|
+
job,
|
|
108932
|
+
"rendering",
|
|
108933
|
+
`Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
|
|
108934
|
+
Math.round(progressPct),
|
|
108935
|
+
onProgress
|
|
108936
|
+
);
|
|
108937
|
+
}
|
|
108938
|
+
},
|
|
108939
|
+
void 0,
|
|
108940
|
+
cfg
|
|
108941
|
+
);
|
|
108942
|
+
await mergeWorkerFrames(workDir, tasks, framesDir);
|
|
108943
|
+
if (probeSession) {
|
|
108944
|
+
lastBrowserConsole = probeSession.browserConsoleBuffer;
|
|
108945
|
+
await closeCaptureSession(probeSession);
|
|
108946
|
+
probeSession = null;
|
|
108177
108947
|
}
|
|
108178
|
-
|
|
108179
|
-
|
|
108180
|
-
|
|
108181
|
-
|
|
108182
|
-
|
|
108183
|
-
|
|
108184
|
-
|
|
108185
|
-
|
|
108186
|
-
|
|
108187
|
-
|
|
108188
|
-
|
|
108189
|
-
|
|
108190
|
-
`Capturing frame ${i + 1}/${job.totalFrames}`,
|
|
108191
|
-
Math.round(progress),
|
|
108192
|
-
onProgress
|
|
108193
|
-
);
|
|
108948
|
+
} else {
|
|
108949
|
+
const videoInjector = createVideoFrameInjector(frameLookup);
|
|
108950
|
+
const session = probeSession ?? await createCaptureSession(
|
|
108951
|
+
fileServer.url,
|
|
108952
|
+
framesDir,
|
|
108953
|
+
captureOptions,
|
|
108954
|
+
videoInjector,
|
|
108955
|
+
cfg
|
|
108956
|
+
);
|
|
108957
|
+
if (probeSession) {
|
|
108958
|
+
prepareCaptureSessionForReuse(session, framesDir, videoInjector);
|
|
108959
|
+
probeSession = null;
|
|
108194
108960
|
}
|
|
108195
|
-
|
|
108196
|
-
|
|
108197
|
-
|
|
108961
|
+
try {
|
|
108962
|
+
if (!session.isInitialized) {
|
|
108963
|
+
await initializeSession(session);
|
|
108964
|
+
}
|
|
108965
|
+
assertNotAborted();
|
|
108966
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
108967
|
+
for (let i = 0; i < job.totalFrames; i++) {
|
|
108968
|
+
assertNotAborted();
|
|
108969
|
+
const time = i / job.config.fps;
|
|
108970
|
+
await captureFrame(session, i, time);
|
|
108971
|
+
job.framesRendered = i + 1;
|
|
108972
|
+
const frameProgress = (i + 1) / job.totalFrames;
|
|
108973
|
+
const progress = 25 + frameProgress * 45;
|
|
108974
|
+
updateJobStatus(
|
|
108975
|
+
job,
|
|
108976
|
+
"rendering",
|
|
108977
|
+
`Capturing frame ${i + 1}/${job.totalFrames}`,
|
|
108978
|
+
Math.round(progress),
|
|
108979
|
+
onProgress
|
|
108980
|
+
);
|
|
108981
|
+
}
|
|
108982
|
+
} finally {
|
|
108983
|
+
lastBrowserConsole = session.browserConsoleBuffer;
|
|
108984
|
+
await closeCaptureSession(session);
|
|
108985
|
+
}
|
|
108986
|
+
}
|
|
108987
|
+
perfStages.captureMs = Date.now() - stage4Start;
|
|
108988
|
+
const stage5Start = Date.now();
|
|
108989
|
+
updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
|
|
108990
|
+
const frameExt = needsAlpha ? "png" : "jpg";
|
|
108991
|
+
const framePattern = `frame_%06d.${frameExt}`;
|
|
108992
|
+
const encoderOpts = {
|
|
108993
|
+
fps: job.config.fps,
|
|
108994
|
+
width,
|
|
108995
|
+
height,
|
|
108996
|
+
codec: preset.codec,
|
|
108997
|
+
preset: preset.preset,
|
|
108998
|
+
quality: preset.quality,
|
|
108999
|
+
pixelFormat: preset.pixelFormat,
|
|
109000
|
+
useGpu: job.config.useGpu,
|
|
109001
|
+
hdr: preset.hdr
|
|
109002
|
+
};
|
|
109003
|
+
const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
|
|
109004
|
+
framesDir,
|
|
109005
|
+
framePattern,
|
|
109006
|
+
videoOnlyPath,
|
|
109007
|
+
encoderOpts,
|
|
109008
|
+
chunkedEncodeSize,
|
|
109009
|
+
abortSignal
|
|
109010
|
+
) : await encodeFramesFromDir(
|
|
109011
|
+
framesDir,
|
|
109012
|
+
framePattern,
|
|
109013
|
+
videoOnlyPath,
|
|
109014
|
+
encoderOpts,
|
|
109015
|
+
abortSignal
|
|
109016
|
+
);
|
|
109017
|
+
assertNotAborted();
|
|
109018
|
+
if (!encodeResult.success) {
|
|
109019
|
+
throw new Error(`Encoding failed: ${encodeResult.error}`);
|
|
108198
109020
|
}
|
|
109021
|
+
perfStages.encodeMs = Date.now() - stage5Start;
|
|
108199
109022
|
}
|
|
108200
|
-
perfStages.captureMs = Date.now() - stage4Start;
|
|
108201
|
-
const stage5Start = Date.now();
|
|
108202
|
-
updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
|
|
108203
|
-
const frameExt = needsAlpha ? "png" : "jpg";
|
|
108204
|
-
const framePattern = `frame_%06d.${frameExt}`;
|
|
108205
|
-
const encoderOpts = baseEncoderOpts;
|
|
108206
|
-
const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
|
|
108207
|
-
framesDir,
|
|
108208
|
-
framePattern,
|
|
108209
|
-
videoOnlyPath,
|
|
108210
|
-
encoderOpts,
|
|
108211
|
-
chunkedEncodeSize,
|
|
108212
|
-
abortSignal
|
|
108213
|
-
) : await encodeFramesFromDir(
|
|
108214
|
-
framesDir,
|
|
108215
|
-
framePattern,
|
|
108216
|
-
videoOnlyPath,
|
|
108217
|
-
encoderOpts,
|
|
108218
|
-
abortSignal
|
|
108219
|
-
);
|
|
108220
|
-
assertNotAborted();
|
|
108221
|
-
if (!encodeResult.success) {
|
|
108222
|
-
throw new Error(`Encoding failed: ${encodeResult.error}`);
|
|
108223
|
-
}
|
|
108224
|
-
perfStages.encodeMs = Date.now() - stage5Start;
|
|
108225
109023
|
}
|
|
108226
109024
|
if (probeSession !== null) {
|
|
108227
109025
|
const remainingProbeSession = probeSession;
|