@higgsfield/video-editor-mcp 1.0.3 → 1.0.4
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.html +244 -138
- package/dist/server.mjs +634 -262
- package/node_modules/@higgsfield/higgsedit/dist/capture-page.js +113 -39
- package/node_modules/@higgsfield/higgsedit/dist/chunk-4D6AXU4B.mjs +45 -27
- package/node_modules/@higgsfield/higgsedit/dist/chunk-C6SASQHE.mjs +130 -176
- package/node_modules/@higgsfield/higgsedit/dist/export-settings.mjs +75 -0
- package/node_modules/@higgsfield/higgsedit/dist/media-upload.mjs +381 -0
- package/node_modules/@higgsfield/higgsedit/dist/proxy.mjs +170 -0
- package/package.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/plugin.json +1 -1
|
@@ -36814,9 +36814,17 @@ function invert(m) {
|
|
|
36814
36814
|
return [m[3] * id, -m[1] * id, -m[2] * id, m[0] * id, (m[2] * m[5] - m[3] * m[4]) * id, (m[1] * m[4] - m[0] * m[5]) * id];
|
|
36815
36815
|
}
|
|
36816
36816
|
var sceneFps = (scene) => scene?.get(SceneConfig)?.fps ?? DEFAULT_FPS;
|
|
36817
|
+
// Capture selection scopes composition and pixels, but root timing stays in the
|
|
36818
|
+
// ordinary editor's stage clock, independent of the selected scene's own fps.
|
|
36819
|
+
var captureSceneIds = new WeakMap();
|
|
36820
|
+
function stageFps(world) {
|
|
36821
|
+
const stage = findStage(world);
|
|
36822
|
+
return sceneFps(stage ? childrenOf(stage).find((e) => e.get(Node).tag === "scene") : undefined);
|
|
36823
|
+
}
|
|
36817
36824
|
function rootScenes(world) {
|
|
36818
36825
|
const stage = findStage(world);
|
|
36819
|
-
|
|
36826
|
+
const sceneId = captureSceneIds.get(world);
|
|
36827
|
+
return stage ? childrenOf(stage).filter((e) => e.get(Node).tag === "scene" && (sceneId === undefined || e.get(Node).key === sceneId || e.get(Node).id === sceneId)) : [];
|
|
36820
36828
|
}
|
|
36821
36829
|
function span(world, entity, fps) {
|
|
36822
36830
|
const synced = syncedPlacement(world, entity, fps, SYNC_CLOCK);
|
|
@@ -36855,7 +36863,7 @@ function span(world, entity, fps) {
|
|
|
36855
36863
|
}
|
|
36856
36864
|
function fpsUnder(world, parent) {
|
|
36857
36865
|
for (let e = parent; e; e = parentOf(e)) if (e.get(Node).tag === "scene") return sceneFps(e);
|
|
36858
|
-
return
|
|
36866
|
+
return stageFps(world);
|
|
36859
36867
|
}
|
|
36860
36868
|
var SYNC_CLOCK = { span, fpsUnder, speed: mediaSpeed };
|
|
36861
36869
|
function sourceInOf(world, entity, fps) {
|
|
@@ -36869,7 +36877,7 @@ function localTime(world, entity, time) {
|
|
|
36869
36877
|
const chain = [];
|
|
36870
36878
|
for (let e = entity; e && e.get(Node).tag !== "stage"; e = parentOf(e)) chain.unshift(e);
|
|
36871
36879
|
let clock = time;
|
|
36872
|
-
let fps =
|
|
36880
|
+
let fps = stageFps(world);
|
|
36873
36881
|
for (const e of chain) {
|
|
36874
36882
|
const start = span(world, e, fps).start;
|
|
36875
36883
|
const tag2 = e.get(Node).tag;
|
|
@@ -36885,7 +36893,8 @@ function composition(world) {
|
|
|
36885
36893
|
const config = scene?.get(SceneConfig);
|
|
36886
36894
|
const fps = sceneFps(scene);
|
|
36887
36895
|
let duration = 0;
|
|
36888
|
-
|
|
36896
|
+
const rootFps = stageFps(world);
|
|
36897
|
+
for (const s of scenes) duration = Math.max(duration, span(world, s, rootFps).end);
|
|
36889
36898
|
return {
|
|
36890
36899
|
scene,
|
|
36891
36900
|
width: config?.width ?? DEFAULT_SCENE.width,
|
|
@@ -37040,11 +37049,14 @@ function textPivot(entity, text) {
|
|
|
37040
37049
|
const fallback = text.x0 === 0 ? 0 : align === "center" ? 0.5 : 1;
|
|
37041
37050
|
return [text.x0 + num3(resolved(entity, "anchorX"), fallback) * text.width, num3(resolved(entity, "anchorY"), 0) * text.height];
|
|
37042
37051
|
}
|
|
37043
|
-
function evaluate(world, time) {
|
|
37052
|
+
function evaluate(world, time, onlyScene) {
|
|
37044
37053
|
const comp = composition(world);
|
|
37054
|
+
const sceneConfig = onlyScene?.get(SceneConfig);
|
|
37055
|
+
const sourceFps = stageFps(world);
|
|
37056
|
+
const sourceEnd = onlyScene ? span(world, onlyScene, sourceFps).end : comp.duration;
|
|
37045
37057
|
const stage = findStage(world);
|
|
37046
37058
|
if (!stage) return comp;
|
|
37047
|
-
const root = { time, length:
|
|
37059
|
+
const root = { time, length: sourceEnd, fps: sourceFps, matrix: [1, 0, 0, 1, 0, 0], opacity: 1, width: sceneConfig?.width ?? comp.width, height: sceneConfig?.height ?? comp.height, baseWidth: sceneConfig?.width ?? comp.width, baseHeight: sceneConfig?.height ?? comp.height, active: true };
|
|
37048
37060
|
const timeWalk = (entity, frame, held = false) => {
|
|
37049
37061
|
const next = timeSystem(world, entity, frame, held);
|
|
37050
37062
|
const crossings = entity.get(Crossings);
|
|
@@ -37054,7 +37066,10 @@ function evaluate(world, time) {
|
|
|
37054
37066
|
return;
|
|
37055
37067
|
}
|
|
37056
37068
|
const crossing = new Set(crossings?.active.flatMap((c) => [c.left, c.right]));
|
|
37057
|
-
for (const child of childrenOf(entity))
|
|
37069
|
+
for (const child of childrenOf(entity)) {
|
|
37070
|
+
if (entity === stage && onlyScene && child !== onlyScene) deactivate(child);
|
|
37071
|
+
else timeWalk(child, next, crossing.has(child));
|
|
37072
|
+
}
|
|
37058
37073
|
};
|
|
37059
37074
|
timeWalk(stage, root);
|
|
37060
37075
|
motionSystem(world);
|
|
@@ -37070,9 +37085,11 @@ function evaluate(world, time) {
|
|
|
37070
37085
|
transformWalk(stage, root);
|
|
37071
37086
|
return comp;
|
|
37072
37087
|
}
|
|
37073
|
-
function clips(world) {
|
|
37088
|
+
function clips(world, onlyScene) {
|
|
37074
37089
|
const out = [];
|
|
37075
37090
|
const comp = composition(world);
|
|
37091
|
+
const rootFps = stageFps(world);
|
|
37092
|
+
if (onlyScene) comp.duration = span(world, onlyScene, rootFps).end;
|
|
37076
37093
|
const visit = (entity, origin, fps, depth3, scene) => {
|
|
37077
37094
|
const node = entity.get(Node);
|
|
37078
37095
|
if (node.tag === "textRange" || node.tag === "shaderPaint") return;
|
|
@@ -37086,15 +37103,15 @@ function clips(world) {
|
|
|
37086
37103
|
visit(child, clock, isScene ? sceneFps(entity) : fps, depth3 + 1, isScene ? node.key : scene);
|
|
37087
37104
|
}
|
|
37088
37105
|
};
|
|
37089
|
-
for (const scene of rootScenes(world)) visit(scene, 0,
|
|
37106
|
+
for (const scene of onlyScene ? [onlyScene] : rootScenes(world)) visit(scene, 0, rootFps, 0, void 0);
|
|
37090
37107
|
return out;
|
|
37091
37108
|
}
|
|
37092
37109
|
function hiddenInTree(entity) {
|
|
37093
37110
|
for (let e = entity; e; e = parentOf(e)) if (e.get(Visibility)?.hidden === true) return true;
|
|
37094
37111
|
return false;
|
|
37095
37112
|
}
|
|
37096
|
-
function audioClips(world) {
|
|
37097
|
-
const spans = new Map(clips(world).map((c) => [c.key, c]));
|
|
37113
|
+
function audioClips(world, onlyScene) {
|
|
37114
|
+
const spans = new Map(clips(world, onlyScene).map((c) => [c.key, c]));
|
|
37098
37115
|
const fps = composition(world).fps;
|
|
37099
37116
|
const out = [];
|
|
37100
37117
|
for (const e of world.query(Media, Node)) {
|
|
@@ -37108,7 +37125,7 @@ function audioClips(world) {
|
|
|
37108
37125
|
src: media.src,
|
|
37109
37126
|
start: clip.start,
|
|
37110
37127
|
end: clip.end,
|
|
37111
|
-
sourceIn: sourceInOf(world, e, fps),
|
|
37128
|
+
sourceIn: sourceInOf(world, e, onlyScene ? fpsUnder(world, e) : fps),
|
|
37112
37129
|
volume: media.volume ?? 1,
|
|
37113
37130
|
fadeIn: Math.max(0, media.fadeIn ?? 0),
|
|
37114
37131
|
fadeOut: Math.max(0, media.fadeOut ?? 0),
|
|
@@ -38585,7 +38602,7 @@ function drawFrame(ctx, world, comp, media, opts) {
|
|
|
38585
38602
|
ctx.clearRect(0, 0, opts.origin ? ctx.canvas.width : comp.width * s, opts.origin ? ctx.canvas.height : comp.height * s);
|
|
38586
38603
|
const stage = findStage(world);
|
|
38587
38604
|
if (!stage) return;
|
|
38588
|
-
childrenOf(stage).forEach((scene) => drawEntity(ctx, scene, media, opts, true));
|
|
38605
|
+
childrenOf(stage).filter((scene) => !opts.scene || scene === opts.scene).forEach((scene) => drawEntity(ctx, scene, media, opts, true));
|
|
38589
38606
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
38590
38607
|
ctx.globalAlpha = 1;
|
|
38591
38608
|
}
|
|
@@ -40621,6 +40638,17 @@ function mountComponent(component, world, mode = "live", options = {}) {
|
|
|
40621
40638
|
var HOLD_TIMEOUT_MS = 3e4;
|
|
40622
40639
|
var relativeMedia = (src) => src.replace(/^\.\//, "");
|
|
40623
40640
|
var fontsInUse = (world) => [...usedFonts(world), ...htmlFonts(world)];
|
|
40641
|
+
function captureMediaEntities(world, includes) {
|
|
40642
|
+
const needed = new Set(world.query(Media).filter(includes));
|
|
40643
|
+
const targets = new Map(syncLinks(world).filter((link) => !link.problem && link.target).map((link) => [link.entity, link.target]));
|
|
40644
|
+
// Only load timing dependencies of the selected scene, not every sibling scene.
|
|
40645
|
+
// Set iteration includes additions; cycles are bounded by the finite entity set.
|
|
40646
|
+
for (const entity of needed) {
|
|
40647
|
+
const target = targets.get(entity);
|
|
40648
|
+
if (target) needed.add(target);
|
|
40649
|
+
}
|
|
40650
|
+
return needed;
|
|
40651
|
+
}
|
|
40624
40652
|
var Session = class {
|
|
40625
40653
|
world = createProjectWorld();
|
|
40626
40654
|
mount;
|
|
@@ -40632,6 +40660,18 @@ var Session = class {
|
|
|
40632
40660
|
mediaUrl;
|
|
40633
40661
|
options;
|
|
40634
40662
|
comp;
|
|
40663
|
+
captureScene;
|
|
40664
|
+
captureIncludes(entity) {
|
|
40665
|
+
if (!this.captureScene) return true;
|
|
40666
|
+
for (let e = entity; e; e = parentOf(e)) if (e === this.captureScene) return true;
|
|
40667
|
+
return false;
|
|
40668
|
+
}
|
|
40669
|
+
captureFonts() {
|
|
40670
|
+
return fontsInUse({ query: (...traits) => this.world.query(...traits).filter((entity) => this.captureIncludes(entity)) });
|
|
40671
|
+
}
|
|
40672
|
+
evaluateCapture(time) {
|
|
40673
|
+
return evaluate(this.world, time, this.captureScene);
|
|
40674
|
+
}
|
|
40635
40675
|
/** The `@inspect` consts the last load evaluated, with the values it used. */
|
|
40636
40676
|
inspect = [];
|
|
40637
40677
|
constructor(options) {
|
|
@@ -40662,19 +40702,24 @@ var Session = class {
|
|
|
40662
40702
|
const component = evaluateProject(modules, { ...externals, ...this.packages(packages, externals) });
|
|
40663
40703
|
const t1 = performance.now();
|
|
40664
40704
|
const world = createProjectWorld();
|
|
40705
|
+
if (this.options.sceneId !== undefined) captureSceneIds.set(world, this.options.sceneId);
|
|
40665
40706
|
const mount = mountComponent(component, world, this.options.mode === "capture" ? "capture" : "live", { resolveUrl: this.mediaUrl });
|
|
40666
40707
|
const t2 = performance.now();
|
|
40667
40708
|
this.mount?.dispose();
|
|
40668
40709
|
this.world.destroy();
|
|
40669
40710
|
this.world = world;
|
|
40670
40711
|
this.mount = mount;
|
|
40712
|
+
if (this.options.sceneId !== undefined) {
|
|
40713
|
+
this.captureScene = rootScenes(world).find((scene) => scene.get(Node).key === this.options.sceneId || scene.get(Node).id === this.options.sceneId);
|
|
40714
|
+
if (!this.captureScene) throw new Error(`unknown scene: ${this.options.sceneId}`);
|
|
40715
|
+
}
|
|
40671
40716
|
this.inspect = inspect.entries;
|
|
40672
40717
|
const faces = await fontsLoaded;
|
|
40673
40718
|
this.projectFamilies = new Set(faces.flatMap((f) => f ? [f.family.replace(/^["']|["']$/g, "").toLowerCase()] : []));
|
|
40674
40719
|
this.googleFonts.retryFailed();
|
|
40675
|
-
await this.googleFonts.load(
|
|
40720
|
+
await this.googleFonts.load(this.captureFonts(), this.projectFamilies);
|
|
40676
40721
|
await this.resolveMedia(this.options.mode === "capture");
|
|
40677
|
-
this.comp =
|
|
40722
|
+
this.comp = this.evaluateCapture(0);
|
|
40678
40723
|
return {
|
|
40679
40724
|
composition: { width: this.comp.width, height: this.comp.height, fps: this.comp.fps, duration: this.comp.duration, frames: this.comp.frames },
|
|
40680
40725
|
clips: clips(world),
|
|
@@ -40686,28 +40731,29 @@ var Session = class {
|
|
|
40686
40731
|
/** Opens every video and audio source to learn its duration; waits for them when `wait` (capture), else updates later. */
|
|
40687
40732
|
async resolveMedia(wait2) {
|
|
40688
40733
|
const durations = this.world.get(MediaInfo).durations;
|
|
40689
|
-
const
|
|
40734
|
+
const needed = captureMediaEntities(this.world, (entity) => this.captureIncludes(entity));
|
|
40735
|
+
const work = [...needed].filter((e) => (e.get(Node).tag === "video" || e.get(Node).tag === "audio") && e.get(Media).src).map((e) => {
|
|
40690
40736
|
const src = e.get(Media).src;
|
|
40691
40737
|
const handle = e.get(Node).tag === "audio" ? this.media.audio(src) : this.media.video(src);
|
|
40692
40738
|
return handle.ready.then((facts) => {
|
|
40693
40739
|
durations.set(src, facts.duration);
|
|
40694
40740
|
});
|
|
40695
40741
|
});
|
|
40696
|
-
for (const e of this.world.query(Captions)) {
|
|
40742
|
+
for (const e of this.world.query(Captions).filter((entity) => this.captureIncludes(entity))) {
|
|
40697
40743
|
const src = e.get(Captions).src;
|
|
40698
40744
|
if (src) work.push(this.media.transcript(src).ready.then((t) => void durations.set(src, transcriptLength(t))));
|
|
40699
40745
|
}
|
|
40700
|
-
work.push(this.resolveSyncs());
|
|
40746
|
+
work.push(this.resolveSyncs(needed));
|
|
40701
40747
|
if (wait2) await Promise.all(work);
|
|
40702
40748
|
else await Promise.race([Promise.allSettled(work), new Promise((r) => setTimeout(r, 0))]);
|
|
40703
40749
|
}
|
|
40704
40750
|
/** Asks the server to measure every `syncTo` pair (it caches them) so the runtime can place those elements. */
|
|
40705
|
-
async resolveSyncs() {
|
|
40751
|
+
async resolveSyncs(needed) {
|
|
40706
40752
|
const world = this.world;
|
|
40707
40753
|
const offsets = world.get(MediaInfo).offsets;
|
|
40708
40754
|
const url2 = this.options.syncUrl ?? `${new URL(this.options.mediaBase, globalThis.location?.href).origin}/__higgsedit/sync`;
|
|
40709
40755
|
await Promise.all(
|
|
40710
|
-
syncLinks(world).map(async (link) => {
|
|
40756
|
+
syncLinks(world).filter((link) => needed.has(link.entity)).map(async (link) => {
|
|
40711
40757
|
if (link.problem || !link.src || !link.targetSrc) {
|
|
40712
40758
|
console.warn(`syncTo on ${link.key}: ${link.problem ?? "nothing to align"}; it keeps its default placement`);
|
|
40713
40759
|
return;
|
|
@@ -40739,22 +40785,21 @@ var Session = class {
|
|
|
40739
40785
|
}
|
|
40740
40786
|
/** Writes the tick for frame `f` (capture mode: a pure function of `f`). */
|
|
40741
40787
|
tick(frame, opts = {}) {
|
|
40742
|
-
const fps = this.
|
|
40788
|
+
const fps = stageFps(this.world);
|
|
40743
40789
|
const time = opts.time ?? frame / fps;
|
|
40744
40790
|
batch(() => this.mount?.tick({ time, frame, delta: opts.delta ?? 1 / fps, playing: opts.playing ?? true }));
|
|
40745
|
-
this.comp =
|
|
40791
|
+
this.comp = this.evaluateCapture(time);
|
|
40746
40792
|
return this.comp;
|
|
40747
40793
|
}
|
|
40748
40794
|
/**
|
|
40749
40795
|
* Frame barrier: holds registered since the tick, exact media for active elements, and
|
|
40750
40796
|
* fonts; re-checked until no new work appears.
|
|
40751
40797
|
*/
|
|
40752
|
-
async settle(frame) {
|
|
40798
|
+
async settle(frame, time = frame / stageFps(this.world)) {
|
|
40753
40799
|
const document2 = this.mount?.document;
|
|
40754
|
-
const fps = this.comp?.fps ?? 30;
|
|
40755
40800
|
for (let round = 0; round < 50; round++) {
|
|
40756
40801
|
const holds = document2 ? [...document2.holds] : [];
|
|
40757
|
-
const work = Promise.all([...holds, mediaReady(this.world, this.media), this.googleFonts.load(
|
|
40802
|
+
const work = Promise.all([...holds, mediaReady(this.world, this.media), this.googleFonts.load(this.captureFonts(), this.projectFamilies), globalThis.document?.fonts?.ready]);
|
|
40758
40803
|
let timer;
|
|
40759
40804
|
const timeout = new Promise((_, reject) => {
|
|
40760
40805
|
timer = setTimeout(() => reject(new Error(`frame ${frame}: hold() did not settle within ${HOLD_TIMEOUT_MS / 1e3} s`)), HOLD_TIMEOUT_MS);
|
|
@@ -40764,7 +40809,7 @@ var Session = class {
|
|
|
40764
40809
|
} finally {
|
|
40765
40810
|
clearTimeout(timer);
|
|
40766
40811
|
}
|
|
40767
|
-
this.comp =
|
|
40812
|
+
this.comp = this.evaluateCapture(time);
|
|
40768
40813
|
if (!document2 || document2.holds.size === 0) {
|
|
40769
40814
|
await mediaReady(this.world, this.media);
|
|
40770
40815
|
return;
|
|
@@ -40779,7 +40824,7 @@ var Session = class {
|
|
|
40779
40824
|
await this.settle(frame);
|
|
40780
40825
|
drawFrame(ctx, this.world, this.comp, this.media, { mode: "capture", scale });
|
|
40781
40826
|
} catch (error) {
|
|
40782
|
-
throw withFrame(error, frame, this.
|
|
40827
|
+
throw withFrame(error, frame, stageFps(this.world));
|
|
40783
40828
|
}
|
|
40784
40829
|
}
|
|
40785
40830
|
/** Live mode: draw whatever media is ready now. */
|
|
@@ -40802,8 +40847,23 @@ var session;
|
|
|
40802
40847
|
var canvas;
|
|
40803
40848
|
var depth2 = 8;
|
|
40804
40849
|
var encoder;
|
|
40850
|
+
var outputComposition;
|
|
40851
|
+
var outputOrigin = 0;
|
|
40852
|
+
async function renderOutputFrame(ctx, frame) {
|
|
40853
|
+
if (!outputComposition) return session.renderFrame(ctx, frame);
|
|
40854
|
+
const time = outputOrigin + frame / outputComposition.fps;
|
|
40855
|
+
const authoredFrame = Math.round(time * stageFps(session.world));
|
|
40856
|
+
session.tick(authoredFrame, { time });
|
|
40857
|
+
await session.settle(authoredFrame, time);
|
|
40858
|
+
const source = session.captureScene?.get(SceneConfig) ?? session.comp;
|
|
40859
|
+
const sourceWidth = source.width ?? session.comp.width;
|
|
40860
|
+
const sourceHeight = source.height ?? session.comp.height;
|
|
40861
|
+
const scale = Math.min(outputComposition.width / sourceWidth, outputComposition.height / sourceHeight);
|
|
40862
|
+
const origin = { x: -(outputComposition.width / scale - sourceWidth) / 2, y: -(outputComposition.height / scale - sourceHeight) / 2 };
|
|
40863
|
+
drawFrame(ctx, session.world, session.comp, session.media, { mode: "capture", scale, origin, scene: session.captureScene });
|
|
40864
|
+
}
|
|
40805
40865
|
function context() {
|
|
40806
|
-
const comp = session.comp;
|
|
40866
|
+
const comp = outputComposition ?? session.comp;
|
|
40807
40867
|
if (!canvas || canvas.width !== comp.width || canvas.height !== comp.height) {
|
|
40808
40868
|
canvas = new OffscreenCanvas(comp.width, comp.height);
|
|
40809
40869
|
}
|
|
@@ -40817,22 +40877,36 @@ function read(ctx) {
|
|
|
40817
40877
|
return image.data;
|
|
40818
40878
|
}
|
|
40819
40879
|
var api = {
|
|
40820
|
-
async load({ modules, fonts, packages, mediaBase, depth: d, hostFrames }) {
|
|
40880
|
+
async load({ modules, fonts, packages, mediaBase, depth: d, hostFrames, spec, sceneId }) {
|
|
40821
40881
|
depth2 = d;
|
|
40822
40882
|
session = new Session({
|
|
40823
40883
|
mediaBase,
|
|
40824
40884
|
mode: "capture",
|
|
40885
|
+
sceneId,
|
|
40825
40886
|
...hostFrames ? { makeVideo: (src) => new HostFrameVideo(src, hostFrames) } : {}
|
|
40826
40887
|
});
|
|
40827
|
-
|
|
40888
|
+
const info = await session.load(modules, fonts, void 0, packages);
|
|
40889
|
+
outputComposition = undefined;
|
|
40890
|
+
outputOrigin = 0;
|
|
40891
|
+
if (spec || sceneId !== undefined) {
|
|
40892
|
+
const scene = session.captureScene;
|
|
40893
|
+
const timing = scene ? span(session.world, scene, stageFps(session.world)) : { start: 0, end: info.composition.duration };
|
|
40894
|
+
const config = scene?.get(SceneConfig);
|
|
40895
|
+
outputOrigin = timing.start;
|
|
40896
|
+
const fps = spec?.video.fps ?? config?.fps ?? info.composition.fps;
|
|
40897
|
+
const duration = timing.end - timing.start;
|
|
40898
|
+
outputComposition = { width: spec?.video.width ?? config?.width ?? info.composition.width, height: spec?.video.height ?? config?.height ?? info.composition.height, fps, duration, frames: Math.max(1, Math.round(duration * fps)) };
|
|
40899
|
+
return { ...info, composition: outputComposition, sourceFps: config?.fps ?? info.composition.fps };
|
|
40900
|
+
}
|
|
40901
|
+
return info;
|
|
40828
40902
|
},
|
|
40829
40903
|
async png(frame) {
|
|
40830
40904
|
const ctx = context();
|
|
40831
|
-
await
|
|
40905
|
+
await renderOutputFrame(ctx, frame);
|
|
40832
40906
|
return toBase64(await canvas.convertToBlob({ type: "image/png" }));
|
|
40833
40907
|
},
|
|
40834
40908
|
async sheet(frames, { columns, width }) {
|
|
40835
|
-
const comp = session.comp;
|
|
40909
|
+
const comp = outputComposition ?? session.comp;
|
|
40836
40910
|
const thumbW = width;
|
|
40837
40911
|
const thumbH = Math.round(width * comp.height / comp.width);
|
|
40838
40912
|
const label = 28;
|
|
@@ -40843,7 +40917,7 @@ var api = {
|
|
|
40843
40917
|
out.fillRect(0, 0, sheet.width, sheet.height);
|
|
40844
40918
|
for (const [i, frame] of frames.entries()) {
|
|
40845
40919
|
const ctx = context();
|
|
40846
|
-
await
|
|
40920
|
+
await renderOutputFrame(ctx, frame);
|
|
40847
40921
|
const x = i % columns * thumbW;
|
|
40848
40922
|
const y = Math.floor(i / columns) * (thumbH + label);
|
|
40849
40923
|
out.drawImage(canvas, x, y, thumbW, thumbH);
|
|
@@ -40854,12 +40928,12 @@ var api = {
|
|
|
40854
40928
|
}
|
|
40855
40929
|
return toBase64(await sheet.convertToBlob({ type: "image/png" }));
|
|
40856
40930
|
},
|
|
40857
|
-
audio: () => audioClips(session.world),
|
|
40931
|
+
audio: () => audioClips(session.world, session.captureScene).map((clip) => ({ ...clip, start: clip.start - outputOrigin, end: clip.end - outputOrigin })),
|
|
40858
40932
|
fontMisses: () => session.fontMisses(),
|
|
40859
40933
|
async raw(frame, url2) {
|
|
40860
40934
|
const ctx = context();
|
|
40861
40935
|
const t0 = performance.now();
|
|
40862
|
-
await
|
|
40936
|
+
await renderOutputFrame(ctx, frame);
|
|
40863
40937
|
const t1 = performance.now();
|
|
40864
40938
|
const data = read(ctx);
|
|
40865
40939
|
const t2 = performance.now();
|
|
@@ -40869,17 +40943,17 @@ var api = {
|
|
|
40869
40943
|
},
|
|
40870
40944
|
async digest(frame) {
|
|
40871
40945
|
const ctx = context();
|
|
40872
|
-
await
|
|
40946
|
+
await renderOutputFrame(ctx, frame);
|
|
40873
40947
|
const data = read(ctx);
|
|
40874
40948
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
40875
40949
|
return [...new Uint8Array(hash)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
40876
40950
|
},
|
|
40877
40951
|
async encodeCheck({ codec, bitrate }) {
|
|
40878
|
-
const comp = session.comp;
|
|
40952
|
+
const comp = outputComposition ?? session.comp;
|
|
40879
40953
|
return await encodeProblem({ depth: depth2, codec, width: comp.width, height: comp.height, fps: comp.fps, bitrate }) ?? null;
|
|
40880
40954
|
},
|
|
40881
40955
|
async encodeStart({ url: url2, codec, quantizer, bitrate }) {
|
|
40882
|
-
const comp = session.comp;
|
|
40956
|
+
const comp = outputComposition ?? session.comp;
|
|
40883
40957
|
context();
|
|
40884
40958
|
encoder = await createWebCodecsEncoder(canvas, {
|
|
40885
40959
|
depth: depth2,
|
|
@@ -40898,7 +40972,7 @@ var api = {
|
|
|
40898
40972
|
async encodeFrame(frame, index) {
|
|
40899
40973
|
const ctx = context();
|
|
40900
40974
|
const t0 = performance.now();
|
|
40901
|
-
await
|
|
40975
|
+
await renderOutputFrame(ctx, frame);
|
|
40902
40976
|
const t1 = performance.now();
|
|
40903
40977
|
await encoder.add(index);
|
|
40904
40978
|
return { renderMs: t1 - t0, encodeMs: performance.now() - t1 };
|
|
@@ -152,7 +152,7 @@ var FontLoadError = class extends Error {
|
|
|
152
152
|
super(`${fonts}; the frame would draw in a fallback font.`);
|
|
153
153
|
}
|
|
154
154
|
};
|
|
155
|
-
async function openCapture(dir, depth, allowFontFallback = false) {
|
|
155
|
+
async function openCapture(dir, depth, allowFontFallback = false, options = {}) {
|
|
156
156
|
const decoders = /* @__PURE__ */ new Map();
|
|
157
157
|
const sinks = /* @__PURE__ */ new Map();
|
|
158
158
|
let sinkId = 0;
|
|
@@ -203,20 +203,30 @@ async function openCapture(dir, depth, allowFontFallback = false) {
|
|
|
203
203
|
}
|
|
204
204
|
];
|
|
205
205
|
const server = await startServer(dir, handlers);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
206
|
+
let browser;
|
|
207
|
+
let page;
|
|
208
|
+
let info;
|
|
209
|
+
try {
|
|
210
|
+
browser = await launch();
|
|
211
|
+
page = await capturePage(browser, server.origin);
|
|
212
|
+
const modules = compileProject(readSources(dir));
|
|
213
|
+
const fonts = fontFiles(dir);
|
|
214
|
+
const packages = (await bundlePackages(dir)).code;
|
|
215
|
+
info = await page.evaluate(
|
|
216
|
+
({ modules, fonts, packages, origin, depth, options }) => globalThis.higgsedit.load({ modules, fonts, packages, mediaBase: `${origin}/project`, depth, ...options, ...depth === 10 ? { hostFrames: `${origin}/__higgsedit/media` } : {} }),
|
|
217
|
+
{ modules, fonts, packages, origin: server.origin, depth, options }
|
|
218
|
+
);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
for (const decoder of decoders.values()) decoder.close();
|
|
221
|
+
try { await browser?.close(); } finally { await server.close(); }
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
215
224
|
return {
|
|
216
225
|
page,
|
|
217
226
|
browser,
|
|
218
227
|
server,
|
|
219
228
|
composition: info.composition,
|
|
229
|
+
sourceFps: info.sourceFps ?? info.composition.fps,
|
|
220
230
|
decoders,
|
|
221
231
|
sink(onBody) {
|
|
222
232
|
const id = String(++sinkId);
|
|
@@ -231,8 +241,7 @@ async function openCapture(dir, depth, allowFontFallback = false) {
|
|
|
231
241
|
},
|
|
232
242
|
async close() {
|
|
233
243
|
for (const d of decoders.values()) d.close();
|
|
234
|
-
await browser.close();
|
|
235
|
-
await server.close();
|
|
244
|
+
try { await browser.close(); } finally { await server.close(); }
|
|
236
245
|
}
|
|
237
246
|
};
|
|
238
247
|
}
|
|
@@ -251,15 +260,16 @@ var api = (page) => ({
|
|
|
251
260
|
});
|
|
252
261
|
|
|
253
262
|
// src/render.ts
|
|
254
|
-
var toFrame = (value, fps, fallback) => {
|
|
263
|
+
var toFrame = (value, fps, fallback, sourceFps = fps) => {
|
|
255
264
|
if (value === void 0) return fallback;
|
|
256
|
-
const seconds = parseTime(/^-?\d+(\.\d+)?$/.test(value) ? Number(value) : value,
|
|
265
|
+
const seconds = parseTime(/^-?\d+(\.\d+)?$/.test(value) ? Number(value) : value, sourceFps);
|
|
257
266
|
if (seconds === void 0) throw new Error(`cannot read time "${value}"`);
|
|
258
267
|
return Math.round(seconds * fps);
|
|
259
268
|
};
|
|
260
269
|
var FFMPEG_CODECS = { x264: "libx264", x265: "libx265", videotoolbox: "hevc_videotoolbox" };
|
|
261
270
|
function videoArgs(opts, encoder) {
|
|
262
271
|
const tags = ["-colorspace", "bt709", "-color_primaries", "bt709", "-color_trc", "bt709", "-color_range", "tv"];
|
|
272
|
+
const quality = opts.spec?.video.bitrate !== undefined ? ["-b:v", String(opts.spec.video.bitrate)] : ["-crf", String(opts.crf ?? 18)];
|
|
263
273
|
if (encoder === "videotoolbox") {
|
|
264
274
|
return ["-c:v", "hevc_videotoolbox", "-profile:v", opts.depth === 10 ? "main10" : "main", "-q:v", "70", "-tag:v", "hvc1", ...tags];
|
|
265
275
|
}
|
|
@@ -269,8 +279,7 @@ function videoArgs(opts, encoder) {
|
|
|
269
279
|
"libx265",
|
|
270
280
|
"-preset",
|
|
271
281
|
opts.preset ?? "fast",
|
|
272
|
-
|
|
273
|
-
String(opts.crf ?? 18),
|
|
282
|
+
...quality,
|
|
274
283
|
"-profile:v",
|
|
275
284
|
opts.depth === 10 ? "main10" : "main",
|
|
276
285
|
"-tag:v",
|
|
@@ -280,7 +289,7 @@ function videoArgs(opts, encoder) {
|
|
|
280
289
|
...tags
|
|
281
290
|
];
|
|
282
291
|
}
|
|
283
|
-
return ["-c:v", "libx264", "-preset", opts.preset ?? "medium",
|
|
292
|
+
return ["-c:v", "libx264", "-preset", opts.preset ?? "medium", ...quality, ...tags];
|
|
284
293
|
}
|
|
285
294
|
function gainFilter(clip, from) {
|
|
286
295
|
const len = clip.end - clip.start;
|
|
@@ -309,7 +318,7 @@ function rateFilter(clip) {
|
|
|
309
318
|
async function render(dir, opts) {
|
|
310
319
|
const log = opts.log ?? (() => {
|
|
311
320
|
});
|
|
312
|
-
const session = await openCapture(dir, opts.depth, opts.allowFontFallback);
|
|
321
|
+
const session = await openCapture(dir, opts.depth, opts.allowFontFallback, { spec: opts.spec, sceneId: opts.sceneId });
|
|
313
322
|
try {
|
|
314
323
|
return await renderWith(session, dir, opts, log);
|
|
315
324
|
} finally {
|
|
@@ -338,6 +347,10 @@ async function audioMix(session, dir, span) {
|
|
|
338
347
|
return { audio, inputs, filters };
|
|
339
348
|
}
|
|
340
349
|
var AUDIO_CODEC = ["-c:a", "aac", "-b:a", "192k"];
|
|
350
|
+
function audioArgs(opts) {
|
|
351
|
+
const audio = opts.spec?.audio;
|
|
352
|
+
return audio ? ["-c:a", "aac", "-b:a", String(audio.bitrate ?? 192000), "-ar", String(audio.sampleRate), "-ac", String(audio.channels)] : AUDIO_CODEC;
|
|
353
|
+
}
|
|
341
354
|
function cancelled() {
|
|
342
355
|
const error = new Error("render cancelled");
|
|
343
356
|
error.name = "RenderCancelledError";
|
|
@@ -352,14 +365,14 @@ function progress(opts, log, started, done, count) {
|
|
|
352
365
|
}
|
|
353
366
|
async function renderWith(session, dir, opts, log) {
|
|
354
367
|
const { width, height, fps, frames: total } = session.composition;
|
|
355
|
-
const first = toFrame(opts.from, fps, 0);
|
|
356
|
-
const last = Math.min(total, toFrame(opts.to, fps, total));
|
|
368
|
+
const first = Math.max(0, toFrame(opts.from, fps, 0, session.sourceFps));
|
|
369
|
+
const last = Math.min(total, toFrame(opts.to, fps, total, session.sourceFps));
|
|
357
370
|
if (last <= first) throw new Error(`nothing to render between frame ${first} and ${last}`);
|
|
358
371
|
const span = { first, last, count: last - first, t0: first / fps, duration: (last - first) / fps };
|
|
359
372
|
const encoder = await pickEncoder(session, opts, log);
|
|
360
373
|
const out = resolve(opts.out);
|
|
361
374
|
mkdirSync(dirname(out), { recursive: true });
|
|
362
|
-
const mix = await audioMix(session, dir, span);
|
|
375
|
+
const mix = opts.spec?.audio === null ? { audio: [], inputs: [], filters: [] } : await audioMix(session, dir, span);
|
|
363
376
|
const started = performance.now();
|
|
364
377
|
const { codec, breakdown } = await (encoder === "webcodecs" ? encodeInPage(session, opts, out, span, mix, log, started) : encodeWithFfmpeg(session, opts, encoder, out, span, mix, log, started)).catch((error) => {
|
|
365
378
|
rmSync(out, { force: true });
|
|
@@ -388,11 +401,13 @@ async function pickEncoder(session, opts, log) {
|
|
|
388
401
|
const encoder = opts.encoder ?? (opts.depth === 10 ? "x265" : "webcodecs");
|
|
389
402
|
if (encoder !== "webcodecs") return encoder;
|
|
390
403
|
const { width, height, fps } = session.composition;
|
|
391
|
-
const
|
|
404
|
+
const codec = opts.spec?.video.codec ?? opts.webcodecsCodec ?? "avc";
|
|
405
|
+
const problem = await api(session.page).encodeCheck({ codec, bitrate: opts.spec?.video.bitrate ?? fallbackBitrate(width, height, fps) });
|
|
392
406
|
if (!problem) return encoder;
|
|
393
407
|
if (!opts.encoder) {
|
|
394
|
-
|
|
395
|
-
|
|
408
|
+
const fallback = codec === "hevc" ? "x265" : "x264";
|
|
409
|
+
log(`${problem}; encoding with ${fallback}`);
|
|
410
|
+
return fallback;
|
|
396
411
|
}
|
|
397
412
|
throw new Error(`${problem}; ${opts.depth === 10 ? "render --depth 10 without --encoder webcodecs (ffmpeg x265)" : "render without --encoder webcodecs"}`);
|
|
398
413
|
}
|
|
@@ -401,7 +416,7 @@ async function encodeWithFfmpeg(session, opts, codec, out, span, mix, log, start
|
|
|
401
416
|
const pixFmt = opts.depth === 10 ? "rgbaf16le" : "rgba";
|
|
402
417
|
const outFmt = opts.depth === 10 ? "yuv420p10le" : "yuv420p";
|
|
403
418
|
const filters = [`[0:v]scale=out_color_matrix=bt709:out_range=limited:flags=accurate_rnd+full_chroma_int,format=${outFmt},setparams=color_primaries=bt709:color_trc=bt709:colorspace=bt709:range=tv[v]`, ...mix.filters];
|
|
404
|
-
const maps = ["-map", "[v]", ...mix.audio.length ? ["-map", "[a]", ...
|
|
419
|
+
const maps = ["-map", "[v]", ...mix.audio.length ? ["-map", "[a]", ...audioArgs(opts)] : []];
|
|
405
420
|
const encoder = ffmpeg(
|
|
406
421
|
[
|
|
407
422
|
"-y",
|
|
@@ -476,7 +491,7 @@ async function encodeInPage(session, opts, out, span, mix, log, started) {
|
|
|
476
491
|
const sink = session.sink(async (position, body) => {
|
|
477
492
|
await file.write(body, 0, body.length, position);
|
|
478
493
|
});
|
|
479
|
-
await page.encodeStart({ url: sink, codec: opts.webcodecsCodec ?? "avc", quantizer: opts.crf ?? 18, bitrate: fallbackBitrate(width, height, fps) });
|
|
494
|
+
await page.encodeStart({ url: sink, codec: opts.spec?.video.codec ?? opts.webcodecsCodec ?? "avc", ...opts.spec?.video.bitrate === undefined ? { quantizer: opts.crf ?? 18 } : {}, bitrate: opts.spec?.video.bitrate ?? fallbackBitrate(width, height, fps) });
|
|
480
495
|
for (let f = span.first; f < span.last; f++) {
|
|
481
496
|
if (opts.signal?.aborted) {
|
|
482
497
|
await page.encodeCancel();
|
|
@@ -489,13 +504,16 @@ async function encodeInPage(session, opts, out, span, mix, log, started) {
|
|
|
489
504
|
progress(opts, log, started, f - span.first + 1, span.count);
|
|
490
505
|
}
|
|
491
506
|
({ codec } = await page.encodeEnd());
|
|
507
|
+
} catch (error) {
|
|
508
|
+
await page.encodeCancel().catch(() => {});
|
|
509
|
+
throw error;
|
|
492
510
|
} finally {
|
|
493
511
|
await file.close();
|
|
494
512
|
}
|
|
495
513
|
const hevc = !codec.startsWith("avc1");
|
|
496
514
|
const retag = `${hevc ? "hevc" : "h264"}_metadata=colour_primaries=1`;
|
|
497
515
|
const tag = hevc ? ["-tag:v", "hvc1"] : [];
|
|
498
|
-
const audio = mix.audio.length ? ["-filter_complex", mix.filters.join(";"), "-map", "[a]", ...
|
|
516
|
+
const audio = mix.audio.length ? ["-filter_complex", mix.filters.join(";"), "-map", "[a]", ...audioArgs(opts)] : [];
|
|
499
517
|
const muxer = ffmpeg(["-y", "-i", video, ...mix.inputs, "-map", "0:v", "-c:v", "copy", "-bsf:v", retag, ...tag, "-color_primaries", "bt709", ...audio, "-t", span.duration.toFixed(6), "-movflags", "+faststart", out]);
|
|
500
518
|
let stderr = "";
|
|
501
519
|
muxer.stderr.on("data", (d) => stderr += d.toString());
|