@hyperframes/producer 0.1.12 → 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/public-server.js
CHANGED
|
@@ -102275,8 +102275,8 @@ ${right2.raw}`)
|
|
|
102275
102275
|
if (!parentClosePattern.test(between)) {
|
|
102276
102276
|
pushFinding({
|
|
102277
102277
|
code: "video_nested_in_timed_element",
|
|
102278
|
-
severity: "
|
|
102279
|
-
message: `<video> with data-start
|
|
102278
|
+
severity: "error",
|
|
102279
|
+
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.`,
|
|
102280
102280
|
elementId: readAttr(tag.raw, "id") || void 0,
|
|
102281
102281
|
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).",
|
|
102282
102282
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -102338,6 +102338,41 @@ ${right2.raw}`)
|
|
|
102338
102338
|
});
|
|
102339
102339
|
}
|
|
102340
102340
|
}
|
|
102341
|
+
for (const tag of tags) {
|
|
102342
|
+
if (tag.name !== "video" && tag.name !== "audio") continue;
|
|
102343
|
+
const hasDataStart = readAttr(tag.raw, "data-start");
|
|
102344
|
+
const hasId = readAttr(tag.raw, "id");
|
|
102345
|
+
const hasSrc = readAttr(tag.raw, "src");
|
|
102346
|
+
if (hasDataStart && !hasId) {
|
|
102347
|
+
pushFinding({
|
|
102348
|
+
code: "media_missing_id",
|
|
102349
|
+
severity: "error",
|
|
102350
|
+
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.`,
|
|
102351
|
+
fixHint: `Add a unique id attribute: <${tag.name} id="my-${tag.name}" ...>`,
|
|
102352
|
+
snippet: truncateSnippet(tag.raw)
|
|
102353
|
+
});
|
|
102354
|
+
}
|
|
102355
|
+
if (hasDataStart && hasId && !hasSrc) {
|
|
102356
|
+
pushFinding({
|
|
102357
|
+
code: "media_missing_src",
|
|
102358
|
+
severity: "error",
|
|
102359
|
+
message: `<${tag.name} id="${hasId}"> has data-start but no src attribute. The renderer cannot load this media.`,
|
|
102360
|
+
elementId: hasId,
|
|
102361
|
+
fixHint: `Add a src attribute to the <${tag.name}> element directly. If using <source> children, the renderer still requires src on the parent element.`,
|
|
102362
|
+
snippet: truncateSnippet(tag.raw)
|
|
102363
|
+
});
|
|
102364
|
+
}
|
|
102365
|
+
if (readAttr(tag.raw, "preload") === "none") {
|
|
102366
|
+
pushFinding({
|
|
102367
|
+
code: "media_preload_none",
|
|
102368
|
+
severity: "warning",
|
|
102369
|
+
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.`,
|
|
102370
|
+
elementId: hasId || void 0,
|
|
102371
|
+
fixHint: `Remove preload="none" or change to preload="auto". The framework manages media loading.`,
|
|
102372
|
+
snippet: truncateSnippet(tag.raw)
|
|
102373
|
+
});
|
|
102374
|
+
}
|
|
102375
|
+
}
|
|
102341
102376
|
for (const tag of tags) {
|
|
102342
102377
|
if (tag.name === "audio" || tag.name === "script" || tag.name === "style") continue;
|
|
102343
102378
|
if (!readAttr(tag.raw, "data-start")) continue;
|
|
@@ -102453,6 +102488,23 @@ ${right2.raw}`)
|
|
|
102453
102488
|
}
|
|
102454
102489
|
}
|
|
102455
102490
|
}
|
|
102491
|
+
{
|
|
102492
|
+
const externalScriptRe = /<script\b[^>]*\bsrc=["'](https?:\/\/[^"']+)["'][^>]*>/gi;
|
|
102493
|
+
let match2;
|
|
102494
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
102495
|
+
while ((match2 = externalScriptRe.exec(source2)) !== null) {
|
|
102496
|
+
const src = match2[1] ?? "";
|
|
102497
|
+
if (seen2.has(src)) continue;
|
|
102498
|
+
seen2.add(src);
|
|
102499
|
+
pushFinding({
|
|
102500
|
+
code: "external_script_dependency",
|
|
102501
|
+
severity: "info",
|
|
102502
|
+
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.`,
|
|
102503
|
+
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.",
|
|
102504
|
+
snippet: truncateSnippet(match2[0] ?? "")
|
|
102505
|
+
});
|
|
102506
|
+
}
|
|
102507
|
+
}
|
|
102456
102508
|
const errorCount = findings.filter((finding) => finding.severity === "error").length;
|
|
102457
102509
|
const warningCount = findings.length - errorCount;
|
|
102458
102510
|
return {
|
|
@@ -103894,6 +103946,42 @@ import { join as join7 } from "path";
|
|
|
103894
103946
|
|
|
103895
103947
|
// ../engine/src/utils/ffprobe.ts
|
|
103896
103948
|
import { spawn as spawn7 } from "child_process";
|
|
103949
|
+
function runFfprobe(args) {
|
|
103950
|
+
return new Promise((resolve12, reject) => {
|
|
103951
|
+
const proc = spawn7("ffprobe", args);
|
|
103952
|
+
let stdout = "";
|
|
103953
|
+
let stderr = "";
|
|
103954
|
+
proc.stdout.on("data", (data) => {
|
|
103955
|
+
stdout += data.toString();
|
|
103956
|
+
});
|
|
103957
|
+
proc.stderr.on("data", (data) => {
|
|
103958
|
+
stderr += data.toString();
|
|
103959
|
+
});
|
|
103960
|
+
proc.on("close", (code) => {
|
|
103961
|
+
if (code !== 0) {
|
|
103962
|
+
reject(new Error(`[FFmpeg] ffprobe exited with code ${code}: ${stderr}`));
|
|
103963
|
+
} else {
|
|
103964
|
+
resolve12(stdout);
|
|
103965
|
+
}
|
|
103966
|
+
});
|
|
103967
|
+
proc.on("error", (err) => {
|
|
103968
|
+
if (err.code === "ENOENT") {
|
|
103969
|
+
reject(new Error("[FFmpeg] ffprobe not found. Please install FFmpeg."));
|
|
103970
|
+
} else {
|
|
103971
|
+
reject(err);
|
|
103972
|
+
}
|
|
103973
|
+
});
|
|
103974
|
+
});
|
|
103975
|
+
}
|
|
103976
|
+
function parseProbeJson(stdout) {
|
|
103977
|
+
try {
|
|
103978
|
+
return JSON.parse(stdout);
|
|
103979
|
+
} catch (e) {
|
|
103980
|
+
throw new Error(
|
|
103981
|
+
`[FFmpeg] Failed to parse ffprobe output: ${e instanceof Error ? e.message : e}`
|
|
103982
|
+
);
|
|
103983
|
+
}
|
|
103984
|
+
}
|
|
103897
103985
|
var videoMetadataCache = /* @__PURE__ */ new Map();
|
|
103898
103986
|
var audioMetadataCache = /* @__PURE__ */ new Map();
|
|
103899
103987
|
function parseFrameRate(frameRateStr) {
|
|
@@ -103908,11 +103996,9 @@ function parseFrameRate(frameRateStr) {
|
|
|
103908
103996
|
}
|
|
103909
103997
|
async function extractVideoMetadata(filePath) {
|
|
103910
103998
|
const cached = videoMetadataCache.get(filePath);
|
|
103911
|
-
if (cached)
|
|
103912
|
-
|
|
103913
|
-
|
|
103914
|
-
const probePromise = new Promise((resolve12, reject) => {
|
|
103915
|
-
const args = [
|
|
103999
|
+
if (cached) return cached;
|
|
104000
|
+
const probePromise = (async () => {
|
|
104001
|
+
const stdout = await runFfprobe([
|
|
103916
104002
|
"-v",
|
|
103917
104003
|
"quiet",
|
|
103918
104004
|
"-print_format",
|
|
@@ -103920,56 +104006,24 @@ async function extractVideoMetadata(filePath) {
|
|
|
103920
104006
|
"-show_format",
|
|
103921
104007
|
"-show_streams",
|
|
103922
104008
|
filePath
|
|
103923
|
-
];
|
|
103924
|
-
const
|
|
103925
|
-
|
|
103926
|
-
|
|
103927
|
-
|
|
103928
|
-
|
|
103929
|
-
|
|
103930
|
-
|
|
103931
|
-
|
|
103932
|
-
|
|
103933
|
-
|
|
103934
|
-
|
|
103935
|
-
|
|
103936
|
-
|
|
103937
|
-
|
|
103938
|
-
|
|
103939
|
-
|
|
103940
|
-
|
|
103941
|
-
if (!videoStream) {
|
|
103942
|
-
reject(new Error("[FFmpeg] No video stream found"));
|
|
103943
|
-
return;
|
|
103944
|
-
}
|
|
103945
|
-
const hasAudio = output2.streams.some((s) => s.codec_type === "audio");
|
|
103946
|
-
const fps = parseFrameRate(videoStream.avg_frame_rate) || parseFrameRate(videoStream.r_frame_rate);
|
|
103947
|
-
const durationSeconds = output2.format.duration ? parseFloat(output2.format.duration) : 0;
|
|
103948
|
-
const metadata = {
|
|
103949
|
-
durationSeconds,
|
|
103950
|
-
width: videoStream.width || 0,
|
|
103951
|
-
height: videoStream.height || 0,
|
|
103952
|
-
fps,
|
|
103953
|
-
videoCodec: videoStream.codec_name || "unknown",
|
|
103954
|
-
hasAudio
|
|
103955
|
-
};
|
|
103956
|
-
resolve12(metadata);
|
|
103957
|
-
} catch (parseError) {
|
|
103958
|
-
reject(
|
|
103959
|
-
new Error(
|
|
103960
|
-
`[FFmpeg] Failed to parse ffprobe output: ${parseError instanceof Error ? parseError.message : parseError}`
|
|
103961
|
-
)
|
|
103962
|
-
);
|
|
103963
|
-
}
|
|
103964
|
-
});
|
|
103965
|
-
ffprobe.on("error", (err) => {
|
|
103966
|
-
if (err.code === "ENOENT") {
|
|
103967
|
-
reject(new Error("[FFmpeg] ffprobe not found. Please install FFmpeg."));
|
|
103968
|
-
} else {
|
|
103969
|
-
reject(err);
|
|
103970
|
-
}
|
|
103971
|
-
});
|
|
103972
|
-
});
|
|
104009
|
+
]);
|
|
104010
|
+
const output2 = parseProbeJson(stdout);
|
|
104011
|
+
const videoStream = output2.streams.find((s) => s.codec_type === "video");
|
|
104012
|
+
if (!videoStream) throw new Error("[FFmpeg] No video stream found");
|
|
104013
|
+
const rFps = parseFrameRate(videoStream.r_frame_rate);
|
|
104014
|
+
const avgFps = parseFrameRate(videoStream.avg_frame_rate);
|
|
104015
|
+
const fps = avgFps || rFps;
|
|
104016
|
+
const isVFR = rFps > 0 && avgFps > 0 && Math.abs(rFps - avgFps) / Math.max(rFps, avgFps) > 0.1;
|
|
104017
|
+
return {
|
|
104018
|
+
durationSeconds: output2.format.duration ? parseFloat(output2.format.duration) : 0,
|
|
104019
|
+
width: videoStream.width || 0,
|
|
104020
|
+
height: videoStream.height || 0,
|
|
104021
|
+
fps,
|
|
104022
|
+
videoCodec: videoStream.codec_name || "unknown",
|
|
104023
|
+
hasAudio: output2.streams.some((s) => s.codec_type === "audio"),
|
|
104024
|
+
isVFR
|
|
104025
|
+
};
|
|
104026
|
+
})();
|
|
103973
104027
|
videoMetadataCache.set(filePath, probePromise);
|
|
103974
104028
|
probePromise.catch(() => {
|
|
103975
104029
|
if (videoMetadataCache.get(filePath) === probePromise) {
|
|
@@ -103980,11 +104034,9 @@ async function extractVideoMetadata(filePath) {
|
|
|
103980
104034
|
}
|
|
103981
104035
|
async function extractAudioMetadata(filePath) {
|
|
103982
104036
|
const cached = audioMetadataCache.get(filePath);
|
|
103983
|
-
if (cached)
|
|
103984
|
-
|
|
103985
|
-
|
|
103986
|
-
const probePromise = new Promise((resolve12, reject) => {
|
|
103987
|
-
const args = [
|
|
104037
|
+
if (cached) return cached;
|
|
104038
|
+
const probePromise = (async () => {
|
|
104039
|
+
const stdout = await runFfprobe([
|
|
103988
104040
|
"-v",
|
|
103989
104041
|
"quiet",
|
|
103990
104042
|
"-print_format",
|
|
@@ -103992,53 +104044,19 @@ async function extractAudioMetadata(filePath) {
|
|
|
103992
104044
|
"-show_format",
|
|
103993
104045
|
"-show_streams",
|
|
103994
104046
|
filePath
|
|
103995
|
-
];
|
|
103996
|
-
const
|
|
103997
|
-
|
|
103998
|
-
|
|
103999
|
-
|
|
104000
|
-
|
|
104001
|
-
|
|
104002
|
-
|
|
104003
|
-
|
|
104004
|
-
|
|
104005
|
-
|
|
104006
|
-
|
|
104007
|
-
|
|
104008
|
-
return;
|
|
104009
|
-
}
|
|
104010
|
-
try {
|
|
104011
|
-
const output2 = JSON.parse(stdout);
|
|
104012
|
-
const audioStream = output2.streams.find((s) => s.codec_type === "audio");
|
|
104013
|
-
if (!audioStream) {
|
|
104014
|
-
reject(new Error("[FFmpeg] No audio stream found"));
|
|
104015
|
-
return;
|
|
104016
|
-
}
|
|
104017
|
-
const durationSeconds = output2.format.duration ? parseFloat(output2.format.duration) : 0;
|
|
104018
|
-
const metadata = {
|
|
104019
|
-
durationSeconds,
|
|
104020
|
-
sampleRate: audioStream.sample_rate ? parseInt(audioStream.sample_rate) : 44100,
|
|
104021
|
-
channels: audioStream.channels || 2,
|
|
104022
|
-
audioCodec: audioStream.codec_name || "unknown",
|
|
104023
|
-
bitrate: output2.format.bit_rate ? parseInt(output2.format.bit_rate) : void 0
|
|
104024
|
-
};
|
|
104025
|
-
resolve12(metadata);
|
|
104026
|
-
} catch (parseError) {
|
|
104027
|
-
reject(
|
|
104028
|
-
new Error(
|
|
104029
|
-
`[FFmpeg] Failed to parse ffprobe output: ${parseError instanceof Error ? parseError.message : parseError}`
|
|
104030
|
-
)
|
|
104031
|
-
);
|
|
104032
|
-
}
|
|
104033
|
-
});
|
|
104034
|
-
ffprobe.on("error", (err) => {
|
|
104035
|
-
if (err.code === "ENOENT") {
|
|
104036
|
-
reject(new Error("[FFmpeg] ffprobe not found. Please install FFmpeg."));
|
|
104037
|
-
} else {
|
|
104038
|
-
reject(err);
|
|
104039
|
-
}
|
|
104040
|
-
});
|
|
104041
|
-
});
|
|
104047
|
+
]);
|
|
104048
|
+
const output2 = parseProbeJson(stdout);
|
|
104049
|
+
const audioStream = output2.streams.find((s) => s.codec_type === "audio");
|
|
104050
|
+
if (!audioStream) throw new Error("[FFmpeg] No audio stream found");
|
|
104051
|
+
const durationSeconds = output2.format.duration ? parseFloat(output2.format.duration) : 0;
|
|
104052
|
+
return {
|
|
104053
|
+
durationSeconds,
|
|
104054
|
+
sampleRate: audioStream.sample_rate ? parseInt(audioStream.sample_rate) : 44100,
|
|
104055
|
+
channels: audioStream.channels || 2,
|
|
104056
|
+
audioCodec: audioStream.codec_name || "unknown",
|
|
104057
|
+
bitrate: output2.format.bit_rate ? parseInt(output2.format.bit_rate) : void 0
|
|
104058
|
+
};
|
|
104059
|
+
})();
|
|
104042
104060
|
audioMetadataCache.set(filePath, probePromise);
|
|
104043
104061
|
probePromise.catch(() => {
|
|
104044
104062
|
if (audioMetadataCache.get(filePath) === probePromise) {
|
|
@@ -104047,6 +104065,57 @@ async function extractAudioMetadata(filePath) {
|
|
|
104047
104065
|
});
|
|
104048
104066
|
return probePromise;
|
|
104049
104067
|
}
|
|
104068
|
+
var keyframeCache = /* @__PURE__ */ new Map();
|
|
104069
|
+
async function analyzeKeyframeIntervals(filePath) {
|
|
104070
|
+
const cached = keyframeCache.get(filePath);
|
|
104071
|
+
if (cached) return cached;
|
|
104072
|
+
const promise = analyzeKeyframeIntervalsUncached(filePath);
|
|
104073
|
+
keyframeCache.set(filePath, promise);
|
|
104074
|
+
promise.catch(() => {
|
|
104075
|
+
if (keyframeCache.get(filePath) === promise) {
|
|
104076
|
+
keyframeCache.delete(filePath);
|
|
104077
|
+
}
|
|
104078
|
+
});
|
|
104079
|
+
return promise;
|
|
104080
|
+
}
|
|
104081
|
+
async function analyzeKeyframeIntervalsUncached(filePath) {
|
|
104082
|
+
const stdout = await runFfprobe([
|
|
104083
|
+
"-v",
|
|
104084
|
+
"quiet",
|
|
104085
|
+
"-select_streams",
|
|
104086
|
+
"v:0",
|
|
104087
|
+
"-skip_frame",
|
|
104088
|
+
"nokey",
|
|
104089
|
+
"-show_entries",
|
|
104090
|
+
"frame=pts_time",
|
|
104091
|
+
"-of",
|
|
104092
|
+
"csv=p=0",
|
|
104093
|
+
filePath
|
|
104094
|
+
]);
|
|
104095
|
+
const timestamps = stdout.split("\n").map((line) => parseFloat(line.trim())).filter((t) => Number.isFinite(t));
|
|
104096
|
+
if (timestamps.length < 2) {
|
|
104097
|
+
return {
|
|
104098
|
+
avgIntervalSeconds: 0,
|
|
104099
|
+
maxIntervalSeconds: 0,
|
|
104100
|
+
keyframeCount: timestamps.length,
|
|
104101
|
+
isProblematic: false
|
|
104102
|
+
};
|
|
104103
|
+
}
|
|
104104
|
+
let maxInterval = 0;
|
|
104105
|
+
let totalInterval = 0;
|
|
104106
|
+
for (let i = 1; i < timestamps.length; i++) {
|
|
104107
|
+
const interval = (timestamps[i] ?? 0) - (timestamps[i - 1] ?? 0);
|
|
104108
|
+
totalInterval += interval;
|
|
104109
|
+
if (interval > maxInterval) maxInterval = interval;
|
|
104110
|
+
}
|
|
104111
|
+
const avgInterval = totalInterval / (timestamps.length - 1);
|
|
104112
|
+
return {
|
|
104113
|
+
avgIntervalSeconds: Math.round(avgInterval * 100) / 100,
|
|
104114
|
+
maxIntervalSeconds: Math.round(maxInterval * 100) / 100,
|
|
104115
|
+
keyframeCount: timestamps.length,
|
|
104116
|
+
isProblematic: maxInterval > 2
|
|
104117
|
+
};
|
|
104118
|
+
}
|
|
104050
104119
|
|
|
104051
104120
|
// ../engine/src/utils/urlDownloader.ts
|
|
104052
104121
|
import { createWriteStream as createWriteStream2, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
|
|
@@ -104118,20 +104187,38 @@ function isHttpUrl(path12) {
|
|
|
104118
104187
|
function parseVideoElements(html) {
|
|
104119
104188
|
const videos = [];
|
|
104120
104189
|
const { document: document2 } = parseHTML(html);
|
|
104121
|
-
const videoEls =
|
|
104190
|
+
const videoEls = Array.from(
|
|
104191
|
+
/* @__PURE__ */ new Set([
|
|
104192
|
+
...Array.from(document2.querySelectorAll("video[id][src]")),
|
|
104193
|
+
...Array.from(document2.querySelectorAll("video[src][data-start]"))
|
|
104194
|
+
])
|
|
104195
|
+
);
|
|
104196
|
+
videoEls.forEach((el, i) => {
|
|
104197
|
+
if (!el.id) el.id = `hf-video-${i}`;
|
|
104198
|
+
});
|
|
104122
104199
|
for (const el of videoEls) {
|
|
104123
104200
|
const id = el.getAttribute("id");
|
|
104124
104201
|
const src = el.getAttribute("src");
|
|
104125
104202
|
if (!id || !src) continue;
|
|
104126
104203
|
const startAttr = el.getAttribute("data-start");
|
|
104127
104204
|
const endAttr = el.getAttribute("data-end");
|
|
104205
|
+
const durationAttr = el.getAttribute("data-duration");
|
|
104128
104206
|
const mediaStartAttr = el.getAttribute("data-media-start");
|
|
104129
104207
|
const hasAudioAttr = el.getAttribute("data-has-audio");
|
|
104208
|
+
const start = startAttr ? parseFloat(startAttr) : 0;
|
|
104209
|
+
let end = 0;
|
|
104210
|
+
if (endAttr) {
|
|
104211
|
+
end = parseFloat(endAttr);
|
|
104212
|
+
} else if (durationAttr) {
|
|
104213
|
+
end = start + parseFloat(durationAttr);
|
|
104214
|
+
} else {
|
|
104215
|
+
end = Infinity;
|
|
104216
|
+
}
|
|
104130
104217
|
videos.push({
|
|
104131
104218
|
id,
|
|
104132
104219
|
src,
|
|
104133
|
-
start
|
|
104134
|
-
end
|
|
104220
|
+
start,
|
|
104221
|
+
end,
|
|
104135
104222
|
mediaStart: mediaStartAttr ? parseFloat(mediaStartAttr) : 0,
|
|
104136
104223
|
hasAudio: hasAudioAttr === "true"
|
|
104137
104224
|
});
|
|
@@ -104241,7 +104328,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config2)
|
|
|
104241
104328
|
return { error: { videoId: video.id, error: `Video file not found: ${videoPath}` } };
|
|
104242
104329
|
}
|
|
104243
104330
|
let videoDuration = video.end - video.start;
|
|
104244
|
-
if (videoDuration <= 0) {
|
|
104331
|
+
if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
|
|
104245
104332
|
const metadata = await extractVideoMetadata(videoPath);
|
|
104246
104333
|
const sourceDuration = metadata.durationSeconds - video.mediaStart;
|
|
104247
104334
|
videoDuration = sourceDuration > 0 ? sourceDuration : metadata.durationSeconds;
|
|
@@ -105611,6 +105698,7 @@ async function compileHtmlFile(html, baseDir, downloadDir) {
|
|
|
105611
105698
|
if (clampList.length > 0) {
|
|
105612
105699
|
compiledHtml = clampDurations(compiledHtml, clampList);
|
|
105613
105700
|
}
|
|
105701
|
+
compiledHtml = compiledHtml.replace(/(<video\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
|
|
105614
105702
|
return { html: compiledHtml, unresolvedCompositions };
|
|
105615
105703
|
}
|
|
105616
105704
|
async function parseSubCompositions(html, projectDir, downloadDir, parentOffset = 0, parentEnd = Infinity, visited = /* @__PURE__ */ new Set()) {
|
|
@@ -105963,13 +106051,51 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
|
|
|
105963
106051
|
} = await parseSubCompositions(compiledHtml, projectDir, downloadDir);
|
|
105964
106052
|
const fullHtml = ensureFullDocument(compiledHtml);
|
|
105965
106053
|
const inlinedHtml = inlineSubCompositions(fullHtml, subCompositions, projectDir);
|
|
106054
|
+
const sanitizedHtml = inlinedHtml.replace(
|
|
106055
|
+
/(<(?:video|audio)\b[^>]*?)\s+preload\s*=\s*["']none["']/gi,
|
|
106056
|
+
"$1"
|
|
106057
|
+
);
|
|
105966
106058
|
const html = injectDeterministicFontFaces(
|
|
105967
|
-
coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(
|
|
106059
|
+
coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
|
|
105968
106060
|
);
|
|
105969
106061
|
const mainVideos = parseVideoElements(html);
|
|
105970
106062
|
const mainAudios = parseAudioElements(html);
|
|
105971
106063
|
const videos = dedupeElementsById([...subVideos, ...mainVideos]);
|
|
105972
106064
|
const audios = dedupeElementsById([...subAudios, ...mainAudios]);
|
|
106065
|
+
for (const video of videos) {
|
|
106066
|
+
if (isHttpUrl(video.src)) continue;
|
|
106067
|
+
const videoPath = resolve7(projectDir, video.src);
|
|
106068
|
+
const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
|
|
106069
|
+
Promise.all([analyzeKeyframeIntervals(videoPath), extractVideoMetadata(videoPath)]).then(([analysis, metadata]) => {
|
|
106070
|
+
if (analysis.isProblematic) {
|
|
106071
|
+
console.warn(
|
|
106072
|
+
`[Compiler] WARNING: Video "${video.id}" has sparse keyframes (max interval: ${analysis.maxIntervalSeconds}s). This causes seek failures and frame freezing. Re-encode with: ${reencode}`
|
|
106073
|
+
);
|
|
106074
|
+
}
|
|
106075
|
+
if (metadata.isVFR) {
|
|
106076
|
+
console.warn(
|
|
106077
|
+
`[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}`
|
|
106078
|
+
);
|
|
106079
|
+
}
|
|
106080
|
+
}).catch(() => {
|
|
106081
|
+
});
|
|
106082
|
+
}
|
|
106083
|
+
const autoIdVideos = videos.filter((v) => v.id.startsWith("hf-video-"));
|
|
106084
|
+
let htmlWithIds = html;
|
|
106085
|
+
if (autoIdVideos.length > 0) {
|
|
106086
|
+
const { document: idDoc } = parseHTML(html);
|
|
106087
|
+
let changed = false;
|
|
106088
|
+
for (const v of autoIdVideos) {
|
|
106089
|
+
const el = idDoc.querySelector(`video[src="${v.src}"]:not([id])`);
|
|
106090
|
+
if (el) {
|
|
106091
|
+
el.id = v.id;
|
|
106092
|
+
changed = true;
|
|
106093
|
+
}
|
|
106094
|
+
}
|
|
106095
|
+
if (changed) {
|
|
106096
|
+
htmlWithIds = idDoc.documentElement?.outerHTML ?? html;
|
|
106097
|
+
}
|
|
106098
|
+
}
|
|
105973
106099
|
const { document: document2 } = parseHTML(html);
|
|
105974
106100
|
const rootEl = document2.querySelector("[data-composition-id]");
|
|
105975
106101
|
const width = rootEl ? parseInt(rootEl.getAttribute("data-width") || "1080", 10) : 1080;
|
|
@@ -105978,7 +106104,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
|
|
|
105978
106104
|
rootEl.getAttribute("data-duration") || rootEl.getAttribute("data-composition-duration") || "0"
|
|
105979
106105
|
) : 0;
|
|
105980
106106
|
return {
|
|
105981
|
-
html,
|
|
106107
|
+
html: htmlWithIds,
|
|
105982
106108
|
subCompositions,
|
|
105983
106109
|
videos,
|
|
105984
106110
|
audios,
|
|
@@ -106884,6 +107010,13 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
|
|
|
106884
107010
|
}
|
|
106885
107011
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
106886
107012
|
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
107013
|
+
const isTimeoutError = errorMessage.includes("Waiting failed") || errorMessage.includes("timeout exceeded") || errorMessage.includes("Navigation timeout");
|
|
107014
|
+
const wasParallel = job.config.workers !== 1;
|
|
107015
|
+
if (isTimeoutError && wasParallel) {
|
|
107016
|
+
log.warn(
|
|
107017
|
+
`Parallel capture timed out with ${job.config.workers ?? "auto"} workers. Video-heavy compositions often need sequential capture. Retry with --workers 1`
|
|
107018
|
+
);
|
|
107019
|
+
}
|
|
106887
107020
|
job.error = errorMessage;
|
|
106888
107021
|
updateJobStatus(job, "failed", `Failed: ${errorMessage}`, job.progress, onProgress);
|
|
106889
107022
|
const elapsed = Date.now() - pipelineStart;
|