@hyperframes/producer 0.1.11 → 0.1.13
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/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +3 -3
- package/dist/index.js +248 -115
- package/dist/index.js.map +3 -3
- package/dist/public-server.js +248 -115
- package/dist/public-server.js.map +3 -3
- package/dist/services/htmlCompiler.d.ts.map +1 -1
- package/dist/services/renderOrchestrator.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -99486,8 +99486,8 @@ ${right2.raw}`)
|
|
|
99486
99486
|
if (!parentClosePattern.test(between)) {
|
|
99487
99487
|
pushFinding({
|
|
99488
99488
|
code: "video_nested_in_timed_element",
|
|
99489
|
-
severity: "
|
|
99490
|
-
message: `<video> with data-start
|
|
99489
|
+
severity: "error",
|
|
99490
|
+
message: `<video> with data-start is nested inside <${parent.name}${parent.id ? ` id="${parent.id}"` : ""}> which also has data-start. The framework cannot manage playback of nested media \u2014 video will be FROZEN in renders.`,
|
|
99491
99491
|
elementId: readAttr(tag.raw, "id") || void 0,
|
|
99492
99492
|
fixHint: "Move the <video> to be a direct child of the stage, or remove data-start from the wrapper div (use it as a non-timed visual container).",
|
|
99493
99493
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -99549,6 +99549,41 @@ ${right2.raw}`)
|
|
|
99549
99549
|
});
|
|
99550
99550
|
}
|
|
99551
99551
|
}
|
|
99552
|
+
for (const tag of tags) {
|
|
99553
|
+
if (tag.name !== "video" && tag.name !== "audio") continue;
|
|
99554
|
+
const hasDataStart = readAttr(tag.raw, "data-start");
|
|
99555
|
+
const hasId = readAttr(tag.raw, "id");
|
|
99556
|
+
const hasSrc = readAttr(tag.raw, "src");
|
|
99557
|
+
if (hasDataStart && !hasId) {
|
|
99558
|
+
pushFinding({
|
|
99559
|
+
code: "media_missing_id",
|
|
99560
|
+
severity: "error",
|
|
99561
|
+
message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements \u2014 this ${tag.name === "audio" ? "audio will be SILENT" : "video will be FROZEN"} in renders.`,
|
|
99562
|
+
fixHint: `Add a unique id attribute: <${tag.name} id="my-${tag.name}" ...>`,
|
|
99563
|
+
snippet: truncateSnippet(tag.raw)
|
|
99564
|
+
});
|
|
99565
|
+
}
|
|
99566
|
+
if (hasDataStart && hasId && !hasSrc) {
|
|
99567
|
+
pushFinding({
|
|
99568
|
+
code: "media_missing_src",
|
|
99569
|
+
severity: "error",
|
|
99570
|
+
message: `<${tag.name} id="${hasId}"> has data-start but no src attribute. The renderer cannot load this media.`,
|
|
99571
|
+
elementId: hasId,
|
|
99572
|
+
fixHint: `Add a src attribute to the <${tag.name}> element directly. If using <source> children, the renderer still requires src on the parent element.`,
|
|
99573
|
+
snippet: truncateSnippet(tag.raw)
|
|
99574
|
+
});
|
|
99575
|
+
}
|
|
99576
|
+
if (readAttr(tag.raw, "preload") === "none") {
|
|
99577
|
+
pushFinding({
|
|
99578
|
+
code: "media_preload_none",
|
|
99579
|
+
severity: "warning",
|
|
99580
|
+
message: `<${tag.name}${hasId ? ` id="${hasId}"` : ""}> has preload="none" which prevents the renderer from loading this media. The compiler strips it for renders, but preview may also have issues.`,
|
|
99581
|
+
elementId: hasId || void 0,
|
|
99582
|
+
fixHint: `Remove preload="none" or change to preload="auto". The framework manages media loading.`,
|
|
99583
|
+
snippet: truncateSnippet(tag.raw)
|
|
99584
|
+
});
|
|
99585
|
+
}
|
|
99586
|
+
}
|
|
99552
99587
|
for (const tag of tags) {
|
|
99553
99588
|
if (tag.name === "audio" || tag.name === "script" || tag.name === "style") continue;
|
|
99554
99589
|
if (!readAttr(tag.raw, "data-start")) continue;
|
|
@@ -99664,6 +99699,23 @@ ${right2.raw}`)
|
|
|
99664
99699
|
}
|
|
99665
99700
|
}
|
|
99666
99701
|
}
|
|
99702
|
+
{
|
|
99703
|
+
const externalScriptRe = /<script\b[^>]*\bsrc=["'](https?:\/\/[^"']+)["'][^>]*>/gi;
|
|
99704
|
+
let match2;
|
|
99705
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
99706
|
+
while ((match2 = externalScriptRe.exec(source2)) !== null) {
|
|
99707
|
+
const src = match2[1] ?? "";
|
|
99708
|
+
if (seen2.has(src)) continue;
|
|
99709
|
+
seen2.add(src);
|
|
99710
|
+
pushFinding({
|
|
99711
|
+
code: "external_script_dependency",
|
|
99712
|
+
severity: "info",
|
|
99713
|
+
message: `This composition loads an external script from \`${src}\`. The HyperFrames bundler automatically hoists CDN scripts from sub-compositions into the parent document. In unbundled runtime mode, \`loadExternalCompositions\` re-injects them. If you're using a custom pipeline that bypasses both, you'll need to include this script manually.`,
|
|
99714
|
+
fixHint: "No action needed when using `hyperframes dev` or `hyperframes render`. If using a custom pipeline, add this script tag to your root composition or HTML page.",
|
|
99715
|
+
snippet: truncateSnippet(match2[0] ?? "")
|
|
99716
|
+
});
|
|
99717
|
+
}
|
|
99718
|
+
}
|
|
99667
99719
|
const errorCount = findings.filter((finding) => finding.severity === "error").length;
|
|
99668
99720
|
const warningCount = findings.length - errorCount;
|
|
99669
99721
|
return {
|
|
@@ -101105,6 +101157,42 @@ import { join as join7 } from "path";
|
|
|
101105
101157
|
|
|
101106
101158
|
// ../engine/src/utils/ffprobe.ts
|
|
101107
101159
|
import { spawn as spawn7 } from "child_process";
|
|
101160
|
+
function runFfprobe(args) {
|
|
101161
|
+
return new Promise((resolve12, reject) => {
|
|
101162
|
+
const proc = spawn7("ffprobe", args);
|
|
101163
|
+
let stdout = "";
|
|
101164
|
+
let stderr = "";
|
|
101165
|
+
proc.stdout.on("data", (data) => {
|
|
101166
|
+
stdout += data.toString();
|
|
101167
|
+
});
|
|
101168
|
+
proc.stderr.on("data", (data) => {
|
|
101169
|
+
stderr += data.toString();
|
|
101170
|
+
});
|
|
101171
|
+
proc.on("close", (code) => {
|
|
101172
|
+
if (code !== 0) {
|
|
101173
|
+
reject(new Error(`[FFmpeg] ffprobe exited with code ${code}: ${stderr}`));
|
|
101174
|
+
} else {
|
|
101175
|
+
resolve12(stdout);
|
|
101176
|
+
}
|
|
101177
|
+
});
|
|
101178
|
+
proc.on("error", (err) => {
|
|
101179
|
+
if (err.code === "ENOENT") {
|
|
101180
|
+
reject(new Error("[FFmpeg] ffprobe not found. Please install FFmpeg."));
|
|
101181
|
+
} else {
|
|
101182
|
+
reject(err);
|
|
101183
|
+
}
|
|
101184
|
+
});
|
|
101185
|
+
});
|
|
101186
|
+
}
|
|
101187
|
+
function parseProbeJson(stdout) {
|
|
101188
|
+
try {
|
|
101189
|
+
return JSON.parse(stdout);
|
|
101190
|
+
} catch (e) {
|
|
101191
|
+
throw new Error(
|
|
101192
|
+
`[FFmpeg] Failed to parse ffprobe output: ${e instanceof Error ? e.message : e}`
|
|
101193
|
+
);
|
|
101194
|
+
}
|
|
101195
|
+
}
|
|
101108
101196
|
var videoMetadataCache = /* @__PURE__ */ new Map();
|
|
101109
101197
|
var audioMetadataCache = /* @__PURE__ */ new Map();
|
|
101110
101198
|
function parseFrameRate(frameRateStr) {
|
|
@@ -101119,11 +101207,9 @@ function parseFrameRate(frameRateStr) {
|
|
|
101119
101207
|
}
|
|
101120
101208
|
async function extractVideoMetadata(filePath) {
|
|
101121
101209
|
const cached = videoMetadataCache.get(filePath);
|
|
101122
|
-
if (cached)
|
|
101123
|
-
|
|
101124
|
-
|
|
101125
|
-
const probePromise = new Promise((resolve12, reject) => {
|
|
101126
|
-
const args = [
|
|
101210
|
+
if (cached) return cached;
|
|
101211
|
+
const probePromise = (async () => {
|
|
101212
|
+
const stdout = await runFfprobe([
|
|
101127
101213
|
"-v",
|
|
101128
101214
|
"quiet",
|
|
101129
101215
|
"-print_format",
|
|
@@ -101131,56 +101217,24 @@ async function extractVideoMetadata(filePath) {
|
|
|
101131
101217
|
"-show_format",
|
|
101132
101218
|
"-show_streams",
|
|
101133
101219
|
filePath
|
|
101134
|
-
];
|
|
101135
|
-
const
|
|
101136
|
-
|
|
101137
|
-
|
|
101138
|
-
|
|
101139
|
-
|
|
101140
|
-
|
|
101141
|
-
|
|
101142
|
-
|
|
101143
|
-
|
|
101144
|
-
|
|
101145
|
-
|
|
101146
|
-
|
|
101147
|
-
|
|
101148
|
-
|
|
101149
|
-
|
|
101150
|
-
|
|
101151
|
-
|
|
101152
|
-
if (!videoStream) {
|
|
101153
|
-
reject(new Error("[FFmpeg] No video stream found"));
|
|
101154
|
-
return;
|
|
101155
|
-
}
|
|
101156
|
-
const hasAudio = output2.streams.some((s) => s.codec_type === "audio");
|
|
101157
|
-
const fps = parseFrameRate(videoStream.avg_frame_rate) || parseFrameRate(videoStream.r_frame_rate);
|
|
101158
|
-
const durationSeconds = output2.format.duration ? parseFloat(output2.format.duration) : 0;
|
|
101159
|
-
const metadata = {
|
|
101160
|
-
durationSeconds,
|
|
101161
|
-
width: videoStream.width || 0,
|
|
101162
|
-
height: videoStream.height || 0,
|
|
101163
|
-
fps,
|
|
101164
|
-
videoCodec: videoStream.codec_name || "unknown",
|
|
101165
|
-
hasAudio
|
|
101166
|
-
};
|
|
101167
|
-
resolve12(metadata);
|
|
101168
|
-
} catch (parseError) {
|
|
101169
|
-
reject(
|
|
101170
|
-
new Error(
|
|
101171
|
-
`[FFmpeg] Failed to parse ffprobe output: ${parseError instanceof Error ? parseError.message : parseError}`
|
|
101172
|
-
)
|
|
101173
|
-
);
|
|
101174
|
-
}
|
|
101175
|
-
});
|
|
101176
|
-
ffprobe.on("error", (err) => {
|
|
101177
|
-
if (err.code === "ENOENT") {
|
|
101178
|
-
reject(new Error("[FFmpeg] ffprobe not found. Please install FFmpeg."));
|
|
101179
|
-
} else {
|
|
101180
|
-
reject(err);
|
|
101181
|
-
}
|
|
101182
|
-
});
|
|
101183
|
-
});
|
|
101220
|
+
]);
|
|
101221
|
+
const output2 = parseProbeJson(stdout);
|
|
101222
|
+
const videoStream = output2.streams.find((s) => s.codec_type === "video");
|
|
101223
|
+
if (!videoStream) throw new Error("[FFmpeg] No video stream found");
|
|
101224
|
+
const rFps = parseFrameRate(videoStream.r_frame_rate);
|
|
101225
|
+
const avgFps = parseFrameRate(videoStream.avg_frame_rate);
|
|
101226
|
+
const fps = avgFps || rFps;
|
|
101227
|
+
const isVFR = rFps > 0 && avgFps > 0 && Math.abs(rFps - avgFps) / Math.max(rFps, avgFps) > 0.1;
|
|
101228
|
+
return {
|
|
101229
|
+
durationSeconds: output2.format.duration ? parseFloat(output2.format.duration) : 0,
|
|
101230
|
+
width: videoStream.width || 0,
|
|
101231
|
+
height: videoStream.height || 0,
|
|
101232
|
+
fps,
|
|
101233
|
+
videoCodec: videoStream.codec_name || "unknown",
|
|
101234
|
+
hasAudio: output2.streams.some((s) => s.codec_type === "audio"),
|
|
101235
|
+
isVFR
|
|
101236
|
+
};
|
|
101237
|
+
})();
|
|
101184
101238
|
videoMetadataCache.set(filePath, probePromise);
|
|
101185
101239
|
probePromise.catch(() => {
|
|
101186
101240
|
if (videoMetadataCache.get(filePath) === probePromise) {
|
|
@@ -101191,11 +101245,9 @@ async function extractVideoMetadata(filePath) {
|
|
|
101191
101245
|
}
|
|
101192
101246
|
async function extractAudioMetadata(filePath) {
|
|
101193
101247
|
const cached = audioMetadataCache.get(filePath);
|
|
101194
|
-
if (cached)
|
|
101195
|
-
|
|
101196
|
-
|
|
101197
|
-
const probePromise = new Promise((resolve12, reject) => {
|
|
101198
|
-
const args = [
|
|
101248
|
+
if (cached) return cached;
|
|
101249
|
+
const probePromise = (async () => {
|
|
101250
|
+
const stdout = await runFfprobe([
|
|
101199
101251
|
"-v",
|
|
101200
101252
|
"quiet",
|
|
101201
101253
|
"-print_format",
|
|
@@ -101203,53 +101255,19 @@ async function extractAudioMetadata(filePath) {
|
|
|
101203
101255
|
"-show_format",
|
|
101204
101256
|
"-show_streams",
|
|
101205
101257
|
filePath
|
|
101206
|
-
];
|
|
101207
|
-
const
|
|
101208
|
-
|
|
101209
|
-
|
|
101210
|
-
|
|
101211
|
-
|
|
101212
|
-
|
|
101213
|
-
|
|
101214
|
-
|
|
101215
|
-
|
|
101216
|
-
|
|
101217
|
-
|
|
101218
|
-
|
|
101219
|
-
return;
|
|
101220
|
-
}
|
|
101221
|
-
try {
|
|
101222
|
-
const output2 = JSON.parse(stdout);
|
|
101223
|
-
const audioStream = output2.streams.find((s) => s.codec_type === "audio");
|
|
101224
|
-
if (!audioStream) {
|
|
101225
|
-
reject(new Error("[FFmpeg] No audio stream found"));
|
|
101226
|
-
return;
|
|
101227
|
-
}
|
|
101228
|
-
const durationSeconds = output2.format.duration ? parseFloat(output2.format.duration) : 0;
|
|
101229
|
-
const metadata = {
|
|
101230
|
-
durationSeconds,
|
|
101231
|
-
sampleRate: audioStream.sample_rate ? parseInt(audioStream.sample_rate) : 44100,
|
|
101232
|
-
channels: audioStream.channels || 2,
|
|
101233
|
-
audioCodec: audioStream.codec_name || "unknown",
|
|
101234
|
-
bitrate: output2.format.bit_rate ? parseInt(output2.format.bit_rate) : void 0
|
|
101235
|
-
};
|
|
101236
|
-
resolve12(metadata);
|
|
101237
|
-
} catch (parseError) {
|
|
101238
|
-
reject(
|
|
101239
|
-
new Error(
|
|
101240
|
-
`[FFmpeg] Failed to parse ffprobe output: ${parseError instanceof Error ? parseError.message : parseError}`
|
|
101241
|
-
)
|
|
101242
|
-
);
|
|
101243
|
-
}
|
|
101244
|
-
});
|
|
101245
|
-
ffprobe.on("error", (err) => {
|
|
101246
|
-
if (err.code === "ENOENT") {
|
|
101247
|
-
reject(new Error("[FFmpeg] ffprobe not found. Please install FFmpeg."));
|
|
101248
|
-
} else {
|
|
101249
|
-
reject(err);
|
|
101250
|
-
}
|
|
101251
|
-
});
|
|
101252
|
-
});
|
|
101258
|
+
]);
|
|
101259
|
+
const output2 = parseProbeJson(stdout);
|
|
101260
|
+
const audioStream = output2.streams.find((s) => s.codec_type === "audio");
|
|
101261
|
+
if (!audioStream) throw new Error("[FFmpeg] No audio stream found");
|
|
101262
|
+
const durationSeconds = output2.format.duration ? parseFloat(output2.format.duration) : 0;
|
|
101263
|
+
return {
|
|
101264
|
+
durationSeconds,
|
|
101265
|
+
sampleRate: audioStream.sample_rate ? parseInt(audioStream.sample_rate) : 44100,
|
|
101266
|
+
channels: audioStream.channels || 2,
|
|
101267
|
+
audioCodec: audioStream.codec_name || "unknown",
|
|
101268
|
+
bitrate: output2.format.bit_rate ? parseInt(output2.format.bit_rate) : void 0
|
|
101269
|
+
};
|
|
101270
|
+
})();
|
|
101253
101271
|
audioMetadataCache.set(filePath, probePromise);
|
|
101254
101272
|
probePromise.catch(() => {
|
|
101255
101273
|
if (audioMetadataCache.get(filePath) === probePromise) {
|
|
@@ -101258,6 +101276,57 @@ async function extractAudioMetadata(filePath) {
|
|
|
101258
101276
|
});
|
|
101259
101277
|
return probePromise;
|
|
101260
101278
|
}
|
|
101279
|
+
var keyframeCache = /* @__PURE__ */ new Map();
|
|
101280
|
+
async function analyzeKeyframeIntervals(filePath) {
|
|
101281
|
+
const cached = keyframeCache.get(filePath);
|
|
101282
|
+
if (cached) return cached;
|
|
101283
|
+
const promise = analyzeKeyframeIntervalsUncached(filePath);
|
|
101284
|
+
keyframeCache.set(filePath, promise);
|
|
101285
|
+
promise.catch(() => {
|
|
101286
|
+
if (keyframeCache.get(filePath) === promise) {
|
|
101287
|
+
keyframeCache.delete(filePath);
|
|
101288
|
+
}
|
|
101289
|
+
});
|
|
101290
|
+
return promise;
|
|
101291
|
+
}
|
|
101292
|
+
async function analyzeKeyframeIntervalsUncached(filePath) {
|
|
101293
|
+
const stdout = await runFfprobe([
|
|
101294
|
+
"-v",
|
|
101295
|
+
"quiet",
|
|
101296
|
+
"-select_streams",
|
|
101297
|
+
"v:0",
|
|
101298
|
+
"-skip_frame",
|
|
101299
|
+
"nokey",
|
|
101300
|
+
"-show_entries",
|
|
101301
|
+
"frame=pts_time",
|
|
101302
|
+
"-of",
|
|
101303
|
+
"csv=p=0",
|
|
101304
|
+
filePath
|
|
101305
|
+
]);
|
|
101306
|
+
const timestamps = stdout.split("\n").map((line) => parseFloat(line.trim())).filter((t) => Number.isFinite(t));
|
|
101307
|
+
if (timestamps.length < 2) {
|
|
101308
|
+
return {
|
|
101309
|
+
avgIntervalSeconds: 0,
|
|
101310
|
+
maxIntervalSeconds: 0,
|
|
101311
|
+
keyframeCount: timestamps.length,
|
|
101312
|
+
isProblematic: false
|
|
101313
|
+
};
|
|
101314
|
+
}
|
|
101315
|
+
let maxInterval = 0;
|
|
101316
|
+
let totalInterval = 0;
|
|
101317
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
101318
|
+
const interval = (timestamps[i] ?? 0) - (timestamps[i - 1] ?? 0);
|
|
101319
|
+
totalInterval += interval;
|
|
101320
|
+
if (interval > maxInterval) maxInterval = interval;
|
|
101321
|
+
}
|
|
101322
|
+
const avgInterval = totalInterval / (timestamps.length - 1);
|
|
101323
|
+
return {
|
|
101324
|
+
avgIntervalSeconds: Math.round(avgInterval * 100) / 100,
|
|
101325
|
+
maxIntervalSeconds: Math.round(maxInterval * 100) / 100,
|
|
101326
|
+
keyframeCount: timestamps.length,
|
|
101327
|
+
isProblematic: maxInterval > 2
|
|
101328
|
+
};
|
|
101329
|
+
}
|
|
101261
101330
|
|
|
101262
101331
|
// ../engine/src/utils/urlDownloader.ts
|
|
101263
101332
|
import { createWriteStream as createWriteStream2, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
|
|
@@ -101329,20 +101398,38 @@ function isHttpUrl(path12) {
|
|
|
101329
101398
|
function parseVideoElements(html) {
|
|
101330
101399
|
const videos = [];
|
|
101331
101400
|
const { document: document2 } = parseHTML(html);
|
|
101332
|
-
const videoEls =
|
|
101401
|
+
const videoEls = Array.from(
|
|
101402
|
+
/* @__PURE__ */ new Set([
|
|
101403
|
+
...Array.from(document2.querySelectorAll("video[id][src]")),
|
|
101404
|
+
...Array.from(document2.querySelectorAll("video[src][data-start]"))
|
|
101405
|
+
])
|
|
101406
|
+
);
|
|
101407
|
+
videoEls.forEach((el, i) => {
|
|
101408
|
+
if (!el.id) el.id = `hf-video-${i}`;
|
|
101409
|
+
});
|
|
101333
101410
|
for (const el of videoEls) {
|
|
101334
101411
|
const id = el.getAttribute("id");
|
|
101335
101412
|
const src = el.getAttribute("src");
|
|
101336
101413
|
if (!id || !src) continue;
|
|
101337
101414
|
const startAttr = el.getAttribute("data-start");
|
|
101338
101415
|
const endAttr = el.getAttribute("data-end");
|
|
101416
|
+
const durationAttr = el.getAttribute("data-duration");
|
|
101339
101417
|
const mediaStartAttr = el.getAttribute("data-media-start");
|
|
101340
101418
|
const hasAudioAttr = el.getAttribute("data-has-audio");
|
|
101419
|
+
const start = startAttr ? parseFloat(startAttr) : 0;
|
|
101420
|
+
let end = 0;
|
|
101421
|
+
if (endAttr) {
|
|
101422
|
+
end = parseFloat(endAttr);
|
|
101423
|
+
} else if (durationAttr) {
|
|
101424
|
+
end = start + parseFloat(durationAttr);
|
|
101425
|
+
} else {
|
|
101426
|
+
end = Infinity;
|
|
101427
|
+
}
|
|
101341
101428
|
videos.push({
|
|
101342
101429
|
id,
|
|
101343
101430
|
src,
|
|
101344
|
-
start
|
|
101345
|
-
end
|
|
101431
|
+
start,
|
|
101432
|
+
end,
|
|
101346
101433
|
mediaStart: mediaStartAttr ? parseFloat(mediaStartAttr) : 0,
|
|
101347
101434
|
hasAudio: hasAudioAttr === "true"
|
|
101348
101435
|
});
|
|
@@ -101452,7 +101539,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config2)
|
|
|
101452
101539
|
return { error: { videoId: video.id, error: `Video file not found: ${videoPath}` } };
|
|
101453
101540
|
}
|
|
101454
101541
|
let videoDuration = video.end - video.start;
|
|
101455
|
-
if (videoDuration <= 0) {
|
|
101542
|
+
if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
|
|
101456
101543
|
const metadata = await extractVideoMetadata(videoPath);
|
|
101457
101544
|
const sourceDuration = metadata.durationSeconds - video.mediaStart;
|
|
101458
101545
|
videoDuration = sourceDuration > 0 ? sourceDuration : metadata.durationSeconds;
|
|
@@ -105446,6 +105533,7 @@ async function compileHtmlFile(html, baseDir, downloadDir) {
|
|
|
105446
105533
|
if (clampList.length > 0) {
|
|
105447
105534
|
compiledHtml = clampDurations(compiledHtml, clampList);
|
|
105448
105535
|
}
|
|
105536
|
+
compiledHtml = compiledHtml.replace(/(<video\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
|
|
105449
105537
|
return { html: compiledHtml, unresolvedCompositions };
|
|
105450
105538
|
}
|
|
105451
105539
|
async function parseSubCompositions(html, projectDir, downloadDir, parentOffset = 0, parentEnd = Infinity, visited = /* @__PURE__ */ new Set()) {
|
|
@@ -105798,13 +105886,51 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
|
|
|
105798
105886
|
} = await parseSubCompositions(compiledHtml, projectDir, downloadDir);
|
|
105799
105887
|
const fullHtml = ensureFullDocument(compiledHtml);
|
|
105800
105888
|
const inlinedHtml = inlineSubCompositions(fullHtml, subCompositions, projectDir);
|
|
105889
|
+
const sanitizedHtml = inlinedHtml.replace(
|
|
105890
|
+
/(<(?:video|audio)\b[^>]*?)\s+preload\s*=\s*["']none["']/gi,
|
|
105891
|
+
"$1"
|
|
105892
|
+
);
|
|
105801
105893
|
const html = injectDeterministicFontFaces(
|
|
105802
|
-
coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(
|
|
105894
|
+
coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
|
|
105803
105895
|
);
|
|
105804
105896
|
const mainVideos = parseVideoElements(html);
|
|
105805
105897
|
const mainAudios = parseAudioElements(html);
|
|
105806
105898
|
const videos = dedupeElementsById([...subVideos, ...mainVideos]);
|
|
105807
105899
|
const audios = dedupeElementsById([...subAudios, ...mainAudios]);
|
|
105900
|
+
for (const video of videos) {
|
|
105901
|
+
if (isHttpUrl(video.src)) continue;
|
|
105902
|
+
const videoPath = resolve7(projectDir, video.src);
|
|
105903
|
+
const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
|
|
105904
|
+
Promise.all([analyzeKeyframeIntervals(videoPath), extractVideoMetadata(videoPath)]).then(([analysis, metadata]) => {
|
|
105905
|
+
if (analysis.isProblematic) {
|
|
105906
|
+
console.warn(
|
|
105907
|
+
`[Compiler] WARNING: Video "${video.id}" has sparse keyframes (max interval: ${analysis.maxIntervalSeconds}s). This causes seek failures and frame freezing. Re-encode with: ${reencode}`
|
|
105908
|
+
);
|
|
105909
|
+
}
|
|
105910
|
+
if (metadata.isVFR) {
|
|
105911
|
+
console.warn(
|
|
105912
|
+
`[Compiler] WARNING: Video "${video.id}" is variable frame rate (VFR). Screen recordings and phone videos are often VFR, which causes stuttering and frame skipping in renders. Re-encode with: ${reencode}`
|
|
105913
|
+
);
|
|
105914
|
+
}
|
|
105915
|
+
}).catch(() => {
|
|
105916
|
+
});
|
|
105917
|
+
}
|
|
105918
|
+
const autoIdVideos = videos.filter((v) => v.id.startsWith("hf-video-"));
|
|
105919
|
+
let htmlWithIds = html;
|
|
105920
|
+
if (autoIdVideos.length > 0) {
|
|
105921
|
+
const { document: idDoc } = parseHTML(html);
|
|
105922
|
+
let changed = false;
|
|
105923
|
+
for (const v of autoIdVideos) {
|
|
105924
|
+
const el = idDoc.querySelector(`video[src="${v.src}"]:not([id])`);
|
|
105925
|
+
if (el) {
|
|
105926
|
+
el.id = v.id;
|
|
105927
|
+
changed = true;
|
|
105928
|
+
}
|
|
105929
|
+
}
|
|
105930
|
+
if (changed) {
|
|
105931
|
+
htmlWithIds = idDoc.documentElement?.outerHTML ?? html;
|
|
105932
|
+
}
|
|
105933
|
+
}
|
|
105808
105934
|
const { document: document2 } = parseHTML(html);
|
|
105809
105935
|
const rootEl = document2.querySelector("[data-composition-id]");
|
|
105810
105936
|
const width = rootEl ? parseInt(rootEl.getAttribute("data-width") || "1080", 10) : 1080;
|
|
@@ -105813,7 +105939,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
|
|
|
105813
105939
|
rootEl.getAttribute("data-duration") || rootEl.getAttribute("data-composition-duration") || "0"
|
|
105814
105940
|
) : 0;
|
|
105815
105941
|
return {
|
|
105816
|
-
html,
|
|
105942
|
+
html: htmlWithIds,
|
|
105817
105943
|
subCompositions,
|
|
105818
105944
|
videos,
|
|
105819
105945
|
audios,
|
|
@@ -106719,6 +106845,13 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
106719
106845
|
}
|
|
106720
106846
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
106721
106847
|
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
106848
|
+
const isTimeoutError = errorMessage.includes("Waiting failed") || errorMessage.includes("timeout exceeded") || errorMessage.includes("Navigation timeout");
|
|
106849
|
+
const wasParallel = job.config.workers !== 1;
|
|
106850
|
+
if (isTimeoutError && wasParallel) {
|
|
106851
|
+
log.warn(
|
|
106852
|
+
`Parallel capture timed out with ${job.config.workers ?? "auto"} workers. Video-heavy compositions often need sequential capture. Retry with --workers 1`
|
|
106853
|
+
);
|
|
106854
|
+
}
|
|
106722
106855
|
job.error = errorMessage;
|
|
106723
106856
|
updateJobStatus(job, "failed", `Failed: ${errorMessage}`, job.progress, onProgress);
|
|
106724
106857
|
const elapsed = Date.now() - pipelineStart;
|