@pixerate/editor 0.123.0 → 0.124.1
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/agent/index.cjs +2 -2
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.js +3 -3
- package/dist/animation/index.d.cts +2 -1
- package/dist/animation/index.d.ts +2 -1
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +11 -733
- package/dist/animation-editor/index.d.ts +11 -733
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/{bezier-C-W9LFyD.d.cts → bezier-CBzKKDjl.d.cts} +2 -66
- package/dist/{bezier-DQZE5dxA.d.ts → bezier-CWv6LQal.d.ts} +2 -66
- package/dist/{brushstrokeRender-M4S65DHT.cjs → brushstrokeRender-EELTV7U4.cjs} +8 -8
- package/dist/{brushstrokeRender-M4S65DHT.cjs.map → brushstrokeRender-EELTV7U4.cjs.map} +1 -1
- package/dist/{brushstrokeRender-CDDRSV2W.js → brushstrokeRender-LRV7GXGD.js} +3 -3
- package/dist/canvas/index.cjs +6 -6
- package/dist/canvas/index.d.cts +10 -6
- package/dist/canvas/index.d.ts +10 -6
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-SMJGE7HM.cjs → chunk-4QQZV5Y5.cjs} +512 -39
- package/dist/chunk-4QQZV5Y5.cjs.map +1 -0
- package/dist/{chunk-TVXFHE4T.js → chunk-64PYNEHJ.js} +45 -12
- package/dist/chunk-64PYNEHJ.js.map +1 -0
- package/dist/{chunk-2IW3BWRT.cjs → chunk-7LQO6OHH.cjs} +2 -2
- package/dist/{chunk-2IW3BWRT.cjs.map → chunk-7LQO6OHH.cjs.map} +1 -1
- package/dist/chunk-CN4PJHUG.cjs +234 -0
- package/dist/chunk-CN4PJHUG.cjs.map +1 -0
- package/dist/chunk-FHYDQOPC.cjs +215 -0
- package/dist/chunk-FHYDQOPC.cjs.map +1 -0
- package/dist/{chunk-KMBC4HNV.js → chunk-J67OLQUQ.js} +2 -2
- package/dist/chunk-JRXJTPES.js +215 -0
- package/dist/chunk-JRXJTPES.js.map +1 -0
- package/dist/{chunk-IPFRWILU.js → chunk-ORHMVUR6.js} +488 -15
- package/dist/chunk-ORHMVUR6.js.map +1 -0
- package/dist/{chunk-6CJRDJCU.cjs → chunk-PEHYWUVV.cjs} +176 -111
- package/dist/chunk-PEHYWUVV.cjs.map +1 -0
- package/dist/chunk-QEG3GRN2.js +234 -0
- package/dist/chunk-QEG3GRN2.js.map +1 -0
- package/dist/{chunk-6ZBR4J62.js → chunk-WATAQ2Z2.js} +79 -14
- package/dist/chunk-WATAQ2Z2.js.map +1 -0
- package/dist/{chunk-RLRUAPBJ.cjs → chunk-Z6OUCS2K.cjs} +47 -14
- package/dist/chunk-Z6OUCS2K.cjs.map +1 -0
- package/dist/effects/index.cjs +605 -0
- package/dist/effects/index.cjs.map +1 -0
- package/dist/effects/index.d.cts +163 -0
- package/dist/effects/index.d.ts +163 -0
- package/dist/effects/index.js +605 -0
- package/dist/effects/index.js.map +1 -0
- package/dist/evaluate-2S3vSPLy.d.cts +55 -0
- package/dist/evaluate-CfmT6xA3.d.ts +55 -0
- package/dist/{export-DfjzHAJI.d.ts → export-BGtXgMJ3.d.ts} +536 -128
- package/dist/{export-Dv0-ZVrV.d.cts → export-BdfY_lH0.d.ts} +392 -5
- package/dist/{export-Dv0-ZVrV.d.ts → export-CbpjwxaI.d.cts} +392 -5
- package/dist/{export-lU5qSpHE.d.cts → export-WmYoWmLM.d.cts} +536 -128
- package/dist/flair-CxB3fOcU.d.ts +739 -0
- package/dist/flair-D7BxyBwL.d.cts +739 -0
- package/dist/gradient-KDfCvhuj.d.cts +66 -0
- package/dist/gradient-KDfCvhuj.d.ts +66 -0
- package/dist/helpers-777mC6dc.d.cts +253 -0
- package/dist/helpers-DX2_7L5t.d.ts +253 -0
- package/dist/image-editor/index.cjs +1296 -805
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +39 -182
- package/dist/image-editor/index.d.ts +39 -182
- package/dist/image-editor/index.js +497 -6
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{opentype-p8Xxf-Wh.d.ts → opentype-14PDI5D2.d.ts} +1 -1
- package/dist/{opentype-BV7wWatY.d.cts → opentype-D80AVmMu.d.cts} +1 -1
- package/dist/paint/index.cjs +2 -2
- package/dist/paint/index.d.cts +32 -10
- package/dist/paint/index.d.ts +32 -10
- package/dist/paint/index.js +1 -1
- package/dist/paint-editor/index.cjs +10 -8
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +11 -5
- package/dist/paint-editor/index.d.ts +11 -5
- package/dist/paint-editor/index.js +4 -2
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/parameters/index.d.cts +4 -184
- package/dist/parameters/index.d.ts +4 -184
- package/dist/{pathfinder-IxcyhO00.d.cts → pathfinder-C0UoFypx.d.cts} +1 -1
- package/dist/{pathfinder-StVChJOT.d.ts → pathfinder-DjISxkUv.d.ts} +1 -1
- package/dist/{player-DrcP2xqj.d.cts → player-CBtEoU2x.d.cts} +2 -54
- package/dist/{player-DV4Jujdq.d.ts → player-cGLrngL_.d.ts} +2 -54
- package/dist/{svg-BtND4TJu.d.cts → svg-BDrjRnL1.d.cts} +3 -2
- package/dist/{svg-BfVXG7hL.d.ts → svg-C-vZ3q50.d.ts} +3 -2
- package/dist/tools/index.cjs +2 -2
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools-C7JtLYzo.d.ts +126 -0
- package/dist/tools-DHKGHiSK.d.cts +126 -0
- package/dist/{types-fuXEme9j.d.ts → types-BcX7sCsn.d.ts} +2 -2
- package/dist/{types-DJF3Ghn5.d.cts → types-CznabG7m.d.cts} +2 -2
- package/dist/types-D3-J3TFf.d.ts +185 -0
- package/dist/types-DMcf_z4J.d.cts +185 -0
- package/dist/vector/index.d.cts +10 -10
- package/dist/vector/index.d.ts +10 -10
- package/dist/video-editor/index.cjs +749 -277
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +265 -138
- package/dist/video-editor/index.d.ts +265 -138
- package/dist/video-editor/index.js +490 -18
- package/dist/video-editor/index.js.map +1 -1
- package/dist/{warp-BjmAgQwq.d.cts → warp-CH9MM435.d.cts} +1 -1
- package/dist/{warp-Ca5fedzS.d.ts → warp-k_PdtXOG.d.ts} +1 -1
- package/package.json +6 -1
- package/skills/pixerate-image-editor/SKILL.md +7 -0
- package/skills/pixerate-image-editor/references/provider-effects.md +35 -0
- package/skills/pixerate-video-editor/SKILL.md +39 -1
- package/dist/chunk-6CJRDJCU.cjs.map +0 -1
- package/dist/chunk-6ZBR4J62.js.map +0 -1
- package/dist/chunk-IPFRWILU.js.map +0 -1
- package/dist/chunk-RLRUAPBJ.cjs.map +0 -1
- package/dist/chunk-SMJGE7HM.cjs.map +0 -1
- package/dist/chunk-TVXFHE4T.js.map +0 -1
- /package/dist/{brushstrokeRender-CDDRSV2W.js.map → brushstrokeRender-LRV7GXGD.js.map} +0 -0
- /package/dist/{chunk-KMBC4HNV.js.map → chunk-J67OLQUQ.js.map} +0 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
VIDEO_CLIP_ANIMATION_EFFECTS,
|
|
5
5
|
VIDEO_CLIP_TYPES,
|
|
6
6
|
VIDEO_EASE_NAMES,
|
|
7
|
+
VIDEO_EFFECT_PLACES,
|
|
7
8
|
VIDEO_FADE_CURVES,
|
|
8
9
|
VIDEO_FITS,
|
|
9
10
|
VIDEO_GRADE_CONTROLS,
|
|
@@ -17,12 +18,15 @@ import {
|
|
|
17
18
|
VIDEO_TRANSFORM_PROPERTIES,
|
|
18
19
|
VIDEO_TRANSITION_TYPES,
|
|
19
20
|
VIDEO_WIPE_DIRECTIONS,
|
|
21
|
+
VideoEffectsEngine,
|
|
20
22
|
VideoLutCache,
|
|
21
23
|
allVideoClipIds,
|
|
22
24
|
allVideoTracks,
|
|
23
25
|
applyVideoGrade,
|
|
24
26
|
applyVideoLut,
|
|
25
27
|
applyVideoLutToPixels,
|
|
28
|
+
checkVideoEffectPlace,
|
|
29
|
+
checkVideoEffectValues,
|
|
26
30
|
checkVideoTimeRemap,
|
|
27
31
|
drawVideoFrame,
|
|
28
32
|
ffmpegFilterValue,
|
|
@@ -35,13 +39,16 @@ import {
|
|
|
35
39
|
formatVideoTimecodeFrames,
|
|
36
40
|
isVideoDropFrameRate,
|
|
37
41
|
isVideoKeyframes,
|
|
42
|
+
mixEffectPixels,
|
|
38
43
|
nextFreeVideoSlot,
|
|
39
44
|
normalizeVideoGrade,
|
|
40
45
|
normalizeVideoGradeLut,
|
|
46
|
+
overEffectPixels,
|
|
41
47
|
parseVideoCubeLut,
|
|
42
48
|
parseVideoLoudnessMeasurement,
|
|
43
49
|
placeVideoRect,
|
|
44
50
|
removeVideoKey,
|
|
51
|
+
renderVideoFrameExact,
|
|
45
52
|
retimeVideoAnimatable,
|
|
46
53
|
setVideoKey,
|
|
47
54
|
snapToVideoFrame,
|
|
@@ -59,6 +66,7 @@ import {
|
|
|
59
66
|
videoClipAnimationProperties,
|
|
60
67
|
videoClipAnimationSpans,
|
|
61
68
|
videoClipBefore,
|
|
69
|
+
videoClipEffectUses,
|
|
62
70
|
videoClipEnd,
|
|
63
71
|
videoClipHandles,
|
|
64
72
|
videoClipOverhang,
|
|
@@ -71,7 +79,11 @@ import {
|
|
|
71
79
|
videoContactSheetSvg,
|
|
72
80
|
videoEaseBezier,
|
|
73
81
|
videoEaseProgress,
|
|
82
|
+
videoEffectFrameTime,
|
|
83
|
+
videoEffectLayerKey,
|
|
84
|
+
videoEffectValuesAt,
|
|
74
85
|
videoFadeCurveGain,
|
|
86
|
+
videoFfmpegUnsupportedEffects,
|
|
75
87
|
videoFrameAt,
|
|
76
88
|
videoFrameDuration,
|
|
77
89
|
videoFrameLeafLayers,
|
|
@@ -93,12 +105,15 @@ import {
|
|
|
93
105
|
videoKeyframeBreakpoints,
|
|
94
106
|
videoKeyframeFfmpegExpr,
|
|
95
107
|
videoKeysOf,
|
|
108
|
+
videoLayersUseEffects,
|
|
109
|
+
videoLayersWithoutTransitionEffects,
|
|
96
110
|
videoLutFromFunction,
|
|
97
111
|
videoLutSvgApproximation,
|
|
98
112
|
videoLutSvgPrimitives,
|
|
99
113
|
videoLutToCube,
|
|
100
114
|
videoOverlap,
|
|
101
115
|
videoProjectDuration,
|
|
116
|
+
videoProjectEffects,
|
|
102
117
|
videoProjectSvg,
|
|
103
118
|
videoSequenceChildren,
|
|
104
119
|
videoSequenceDuration,
|
|
@@ -119,7 +134,15 @@ import {
|
|
|
119
134
|
videoTransitionEase,
|
|
120
135
|
videoValueAt,
|
|
121
136
|
videoWipeRect
|
|
122
|
-
} from "../chunk-
|
|
137
|
+
} from "../chunk-ORHMVUR6.js";
|
|
138
|
+
import {
|
|
139
|
+
createEffectToolProvider
|
|
140
|
+
} from "../chunk-JRXJTPES.js";
|
|
141
|
+
import {
|
|
142
|
+
EffectsSlot,
|
|
143
|
+
isEffectError
|
|
144
|
+
} from "../chunk-QEG3GRN2.js";
|
|
145
|
+
import "../chunk-4FX5ACVG.js";
|
|
123
146
|
import {
|
|
124
147
|
namedWithSuggestion
|
|
125
148
|
} from "../chunk-INZJALPY.js";
|
|
@@ -239,13 +262,52 @@ function parseVideoGradeLut(v, path) {
|
|
|
239
262
|
function parseVideoTransition(v, path) {
|
|
240
263
|
if (v === void 0) return void 0;
|
|
241
264
|
if (!isObject(v)) return fail(path, "must be an object { type, duration }");
|
|
265
|
+
const type = oneOf(v.type, VIDEO_TRANSITION_TYPES, `${path}.type`, false);
|
|
266
|
+
if (type === "effect" && v.effect === void 0) fail(`${path}.effect`, 'is required for an effect transition: { effect: "<effect id>", values }');
|
|
267
|
+
if (type !== "effect" && v.effect !== void 0) fail(`${path}.effect`, 'is only for transitions of type "effect"');
|
|
242
268
|
return compact({
|
|
243
|
-
type
|
|
269
|
+
type,
|
|
244
270
|
duration: num(v.duration, `${path}.duration`, { positive: true }),
|
|
245
271
|
color: str(v.color, `${path}.color`, true),
|
|
246
272
|
direction: oneOf(v.direction, VIDEO_WIPE_DIRECTIONS, `${path}.direction`),
|
|
247
|
-
audioCurve: oneOf(v.audioCurve, VIDEO_FADE_CURVES, `${path}.audioCurve`)
|
|
273
|
+
audioCurve: oneOf(v.audioCurve, VIDEO_FADE_CURVES, `${path}.audioCurve`),
|
|
274
|
+
effect: v.effect === void 0 ? void 0 : parseVideoEffectRef(v.effect, `${path}.effect`, false)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
var isEffectValue = (x) => typeof x === "string" || typeof x === "boolean" || typeof x === "number" && Number.isFinite(x) || Array.isArray(x) && x.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
278
|
+
function parseVideoEffectValues(v, path, keyframes = true) {
|
|
279
|
+
if (v === void 0) return void 0;
|
|
280
|
+
if (!isObject(v)) return fail(path, "must be an object of control values from describe_effect");
|
|
281
|
+
const out = {};
|
|
282
|
+
for (const [k, x] of Object.entries(v)) {
|
|
283
|
+
if (isEffectValue(x)) out[k] = Array.isArray(x) ? [...x] : x;
|
|
284
|
+
else if (keyframes && isObject(x)) out[k] = parseVideoKeyframes(x, `${path}.${k}`);
|
|
285
|
+
else fail(`${path}.${k}`, keyframes ? "must be a number, string, true / false, a list of numbers, or keyframes { keys }" : "must be a number, string, true / false or a list of numbers");
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
function parseVideoEffectRef(v, path, keyframes = true) {
|
|
290
|
+
if (!isObject(v)) return fail(path, "must be an object { effect, values }");
|
|
291
|
+
const unknown = Object.keys(v).find((k) => !["effect", "provider", "name", "values"].includes(k));
|
|
292
|
+
if (unknown) fail(`${path}.${unknown}`, "is not an effect field; use effect, provider, name, values");
|
|
293
|
+
return compact({ effect: str(v.effect, `${path}.effect`), provider: str(v.provider, `${path}.provider`, true), name: str(v.name, `${path}.name`, true), values: parseVideoEffectValues(v.values, `${path}.values`, keyframes) });
|
|
294
|
+
}
|
|
295
|
+
function parseVideoClipEffects(v, path) {
|
|
296
|
+
if (v === void 0) return void 0;
|
|
297
|
+
if (!Array.isArray(v)) return fail(path, "must be a list of effects { id, effect, values }");
|
|
298
|
+
const ids = /* @__PURE__ */ new Set();
|
|
299
|
+
const out = v.map((e, i) => {
|
|
300
|
+
const at = `${path}[${i}]`;
|
|
301
|
+
if (!isObject(e)) return fail(at, "must be an object { id, effect, values }");
|
|
302
|
+
const unknown = Object.keys(e).find((k) => !["id", "effect", "provider", "name", "values", "enabled", "opacity"].includes(k));
|
|
303
|
+
if (unknown) fail(`${at}.${unknown}`, "is not a clip effect field; use id, effect, provider, name, values, enabled, opacity");
|
|
304
|
+
const id = str(e.id, `${at}.id`);
|
|
305
|
+
if (ids.has(id)) fail(`${at}.id`, `repeats "${id}"; each effect on a clip needs its own id`);
|
|
306
|
+
ids.add(id);
|
|
307
|
+
const { effect, provider, name, values } = parseVideoEffectRef({ effect: e.effect, provider: e.provider, name: e.name, values: e.values }, at);
|
|
308
|
+
return compact({ id, effect, provider, name, values, enabled: bool(e.enabled, `${at}.enabled`), opacity: parseVideoAnimatable(e.opacity, `${at}.opacity`, { optional: true, min: 0, max: 1 }) });
|
|
248
309
|
});
|
|
310
|
+
return out.length ? out : void 0;
|
|
249
311
|
}
|
|
250
312
|
function parseVideoTranscriptWords(v, path) {
|
|
251
313
|
if (v === void 0) return void 0;
|
|
@@ -316,7 +378,8 @@ function parseVideoClip(v, path = "clip") {
|
|
|
316
378
|
animateOut: parseVideoClipAnimation(v.animateOut, `${path}.animateOut`),
|
|
317
379
|
fadeIn: num(v.fadeIn, `${path}.fadeIn`, { optional: true, min: 0 }),
|
|
318
380
|
fadeOut: num(v.fadeOut, `${path}.fadeOut`, { optional: true, min: 0 }),
|
|
319
|
-
transition: parseVideoTransition(v.transition, `${path}.transition`)
|
|
381
|
+
transition: parseVideoTransition(v.transition, `${path}.transition`),
|
|
382
|
+
effects: parseVideoClipEffects(v.effects, `${path}.effects`)
|
|
320
383
|
};
|
|
321
384
|
if (type === "media")
|
|
322
385
|
return compact({
|
|
@@ -356,6 +419,10 @@ function parseVideoClip(v, path = "clip") {
|
|
|
356
419
|
fontWeight: num(v.fontWeight, `${path}.fontWeight`, { optional: true, min: 100, max: 1e3 }),
|
|
357
420
|
background: str(v.background, `${path}.background`, true)
|
|
358
421
|
});
|
|
422
|
+
if (type === "effect") {
|
|
423
|
+
const ref = parseVideoEffectRef({ effect: v.effect, provider: v.provider, name: v.name === void 0 ? void 0 : v.name, values: v.values }, path);
|
|
424
|
+
return compact({ type, ...base, ...ref, name: base.name });
|
|
425
|
+
}
|
|
359
426
|
return compact({ type, ...base, color: str(v.color, `${path}.color`) });
|
|
360
427
|
}
|
|
361
428
|
function parseVideoTrack(v, path = "track") {
|
|
@@ -653,7 +720,7 @@ function retimeClipKeys(clip, fn) {
|
|
|
653
720
|
if (isVideoKeyframes(v)) a.set(fn(v));
|
|
654
721
|
}
|
|
655
722
|
}
|
|
656
|
-
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "timeRemap", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
723
|
+
var OPTIONAL_CLIP_FIELDS = ["name", "effects", "values", "provider", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "timeRemap", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
657
724
|
var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
|
|
658
725
|
var Editor = class {
|
|
659
726
|
constructor(p) {
|
|
@@ -1099,7 +1166,7 @@ var Editor = class {
|
|
|
1099
1166
|
const { track, clip } = this.clip(e.clip);
|
|
1100
1167
|
this.unlocked(track);
|
|
1101
1168
|
if (clip.type === "sequence") refuse(`Clip "${clip.id}" is a compound clip, which plays its sequence at normal speed; ramp the clips inside the sequence instead.`);
|
|
1102
|
-
if (clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to ramp; only video and audio clips change speed.`);
|
|
1169
|
+
if (clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : clip.type === "effect" ? "effect clip" : "colour"}, so it has no source to ramp; only video and audio clips change speed.`);
|
|
1103
1170
|
const asset = this.asset(clip.asset);
|
|
1104
1171
|
if (asset.kind === "image") refuse(`Clip "${clip.id}" is a still ("${asset.id}"), so it has no motion to ramp.`);
|
|
1105
1172
|
const media = clip;
|
|
@@ -1156,7 +1223,7 @@ var Editor = class {
|
|
|
1156
1223
|
case "slip_clip": {
|
|
1157
1224
|
const { track, clip } = this.clip(e.clip);
|
|
1158
1225
|
this.unlocked(track);
|
|
1159
|
-
if (clip.type !== "media" && clip.type !== "sequence") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to slip; only video, audio and compound clips slip.`);
|
|
1226
|
+
if (clip.type !== "media" && clip.type !== "sequence") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : clip.type === "effect" ? "effect clip" : "colour"}, so it has no source to slip; only video, audio and compound clips slip.`);
|
|
1160
1227
|
const source = clip.type === "sequence" ? `sequence "${clip.sequence}"` : `"${this.asset(clip.asset).id}"`;
|
|
1161
1228
|
if (clip.type === "media" && this.asset(clip.asset).kind === "image") refuse(`Clip "${clip.id}" is a still ("${clip.asset}"), so it has no source to slip; only video and audio clips slip.`);
|
|
1162
1229
|
const handles = videoClipHandles(clip, videoAssetMap(this.p), videoSequenceMap(this.p));
|
|
@@ -2451,6 +2518,9 @@ var VideoPreviewEngine = class {
|
|
|
2451
2518
|
this.frameCache = options.frameCache !== void 0 ? options.frameCache : source.frameCache ?? null;
|
|
2452
2519
|
this.unsubscribeFrames = this.frameCache ? this.frameCache.subscribe(() => this.redraw()) : () => {
|
|
2453
2520
|
};
|
|
2521
|
+
this.effects = options.effects !== void 0 ? options.effects : source.effectsEngine ?? null;
|
|
2522
|
+
this.unsubscribeEffects = this.effects ? this.effects.subscribe(() => this.redraw()) : () => {
|
|
2523
|
+
};
|
|
2454
2524
|
const pixels = options.pixels !== void 0 ? options.pixels : source.previewPixels;
|
|
2455
2525
|
this.detachPixels = pixels ? pixels.attach(() => this.readPixels()) : () => {
|
|
2456
2526
|
};
|
|
@@ -2480,6 +2550,8 @@ var VideoPreviewEngine = class {
|
|
|
2480
2550
|
fps = 30;
|
|
2481
2551
|
unsubscribeLuts;
|
|
2482
2552
|
scratchCanvases = /* @__PURE__ */ new Map();
|
|
2553
|
+
effects;
|
|
2554
|
+
unsubscribeEffects;
|
|
2483
2555
|
/** The canvas's pixels and the frame they show, or `null` when its context cannot be read. */
|
|
2484
2556
|
readPixels() {
|
|
2485
2557
|
const ctx = this.ctx;
|
|
@@ -2568,14 +2640,22 @@ var VideoPreviewEngine = class {
|
|
|
2568
2640
|
}
|
|
2569
2641
|
for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
|
|
2570
2642
|
for (const entry of [...this.media.values()]) if (!active.has(entry.clip)) this.release(entry);
|
|
2571
|
-
this.draw(frame);
|
|
2643
|
+
this.draw(this.drawable(project, frame));
|
|
2644
|
+
}
|
|
2645
|
+
/** With effects, the frame is drawn at the start of its frame, so effect pictures are cached once per frame. */
|
|
2646
|
+
drawable(project, frame) {
|
|
2647
|
+
if (!this.effects?.available || !videoLayersUseEffects(frame.layers)) return frame;
|
|
2648
|
+
const at = videoEffectFrameTime(frame.time, project.fps);
|
|
2649
|
+
return at === frame.time ? frame : { ...videoFrameAt(project, at), time: frame.time };
|
|
2572
2650
|
}
|
|
2573
2651
|
redraw() {
|
|
2574
2652
|
if (this.scheduled || this.destroyed) return;
|
|
2575
2653
|
this.scheduled = true;
|
|
2576
2654
|
queueMicrotask(() => {
|
|
2577
2655
|
this.scheduled = false;
|
|
2578
|
-
if (
|
|
2656
|
+
if (this.destroyed) return;
|
|
2657
|
+
const { project, time } = this.source.getState();
|
|
2658
|
+
this.draw(this.drawable(project, videoFrameAt(project, time)));
|
|
2579
2659
|
});
|
|
2580
2660
|
}
|
|
2581
2661
|
sourceFor = (layer) => {
|
|
@@ -2613,11 +2693,20 @@ var VideoPreviewEngine = class {
|
|
|
2613
2693
|
if (this.canvas.height !== h) this.canvas.height = h;
|
|
2614
2694
|
this.complete = true;
|
|
2615
2695
|
const sourceFor = (layer) => {
|
|
2696
|
+
if (layer.type === "effect") return null;
|
|
2616
2697
|
const source = this.sourceFor(layer);
|
|
2617
2698
|
if (!source) this.complete = false;
|
|
2618
2699
|
return source;
|
|
2619
2700
|
};
|
|
2620
|
-
drawVideoFrame(this.ctx, frame, sourceFor, resolution, {
|
|
2701
|
+
drawVideoFrame(this.ctx, frame, sourceFor, resolution, {
|
|
2702
|
+
defineSvgFilter: this.defineSvgFilter,
|
|
2703
|
+
lutFor: this.lutFor,
|
|
2704
|
+
scratch: this.scratch,
|
|
2705
|
+
effects: this.effects,
|
|
2706
|
+
onEffectWaiting: () => {
|
|
2707
|
+
this.complete = false;
|
|
2708
|
+
}
|
|
2709
|
+
});
|
|
2621
2710
|
this.drawn = { time: frame.time, complete: this.complete };
|
|
2622
2711
|
this.options.onFrame?.(frame);
|
|
2623
2712
|
this.pixelsTarget?.drawn(frame.time, this.complete);
|
|
@@ -2668,6 +2757,7 @@ var VideoPreviewEngine = class {
|
|
|
2668
2757
|
this.detachPixels();
|
|
2669
2758
|
this.unsubscribeLuts();
|
|
2670
2759
|
this.unsubscribeFrames();
|
|
2760
|
+
this.unsubscribeEffects();
|
|
2671
2761
|
this.filterHost?.remove();
|
|
2672
2762
|
this.filterHost = null;
|
|
2673
2763
|
this.filters.clear();
|
|
@@ -2689,6 +2779,10 @@ var VideoEditorController = class {
|
|
|
2689
2779
|
luts;
|
|
2690
2780
|
/** The host's frame cache, if it gave one (the preview fills and reads it). */
|
|
2691
2781
|
frameCache;
|
|
2782
|
+
/** The effects provider slot (see `setEffectsProvider`). */
|
|
2783
|
+
effects;
|
|
2784
|
+
/** Rendered effect pictures for the preview and frame-exact exports. */
|
|
2785
|
+
effectsEngine;
|
|
2692
2786
|
project;
|
|
2693
2787
|
issues;
|
|
2694
2788
|
selection = [];
|
|
@@ -2706,6 +2800,9 @@ var VideoEditorController = class {
|
|
|
2706
2800
|
this.audioPeaks = options.audioPeaks instanceof VideoAudioPeaksCache ? options.audioPeaks : new VideoAudioPeaksCache(options.audioPeaks);
|
|
2707
2801
|
this.luts = options.luts instanceof VideoLutCache ? options.luts : new VideoLutCache(options.luts);
|
|
2708
2802
|
this.frameCache = options.frameCache ?? null;
|
|
2803
|
+
this.effects = new EffectsSlot();
|
|
2804
|
+
if (options.effects) this.effects.set(options.effects);
|
|
2805
|
+
this.effectsEngine = new VideoEffectsEngine({ slot: this.effects });
|
|
2709
2806
|
this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
|
|
2710
2807
|
this.issues = checkVideoProject(this.project);
|
|
2711
2808
|
this.readOnly = options.readOnly ?? false;
|
|
@@ -2721,12 +2818,14 @@ var VideoEditorController = class {
|
|
|
2721
2818
|
this.player.subscribe(() => this.notify()),
|
|
2722
2819
|
this.history.subscribe(() => {
|
|
2723
2820
|
if (!this.applying) this.notify();
|
|
2724
|
-
})
|
|
2821
|
+
}),
|
|
2822
|
+
this.effects.subscribe(() => this.notify())
|
|
2725
2823
|
);
|
|
2726
2824
|
}
|
|
2727
2825
|
/** Pauses and stops listening to the player and history. Subscribing again reconnects (React StrictMode). */
|
|
2728
2826
|
destroy() {
|
|
2729
2827
|
this.player.pause();
|
|
2828
|
+
this.effectsEngine.reset();
|
|
2730
2829
|
this.unsubscribers.splice(0).forEach((u) => u());
|
|
2731
2830
|
}
|
|
2732
2831
|
getState = () => this.state;
|
|
@@ -2760,6 +2859,7 @@ var VideoEditorController = class {
|
|
|
2760
2859
|
undoLabel: h.undoName,
|
|
2761
2860
|
redoLabel: h.redoName,
|
|
2762
2861
|
previewScale: this.previewScale,
|
|
2862
|
+
effectsProvider: this.effects.provider,
|
|
2763
2863
|
revision: this.revision
|
|
2764
2864
|
};
|
|
2765
2865
|
}
|
|
@@ -2789,7 +2889,7 @@ var VideoEditorController = class {
|
|
|
2789
2889
|
this.notify();
|
|
2790
2890
|
this.onChange?.(project);
|
|
2791
2891
|
}
|
|
2792
|
-
commit(label, next) {
|
|
2892
|
+
commit(label, next, merge) {
|
|
2793
2893
|
const before = this.project;
|
|
2794
2894
|
this.error = null;
|
|
2795
2895
|
if (next === before) {
|
|
@@ -2803,7 +2903,8 @@ var VideoEditorController = class {
|
|
|
2803
2903
|
undo: () => this.setProject(before),
|
|
2804
2904
|
redo: () => this.setProject(next)
|
|
2805
2905
|
};
|
|
2806
|
-
this.history.
|
|
2906
|
+
if (merge) this.history.executeMerged(command, { key: merge });
|
|
2907
|
+
else this.history.execute(command);
|
|
2807
2908
|
}
|
|
2808
2909
|
refuse(message) {
|
|
2809
2910
|
this.error = message;
|
|
@@ -2816,14 +2917,15 @@ var VideoEditorController = class {
|
|
|
2816
2917
|
}
|
|
2817
2918
|
/**
|
|
2818
2919
|
* Applies edits as one undo step, all or nothing (see `applyVideoEdits`). With `dryRun` it
|
|
2819
|
-
* reports what would happen and changes nothing.
|
|
2920
|
+
* reports what would happen and changes nothing. Edits with the same `merge` key in a row
|
|
2921
|
+
* (a slider drag) fold into one undo step.
|
|
2820
2922
|
*/
|
|
2821
2923
|
apply(edits, label = "Edit video", options = {}) {
|
|
2822
2924
|
const reason = this.readOnlyReason();
|
|
2823
2925
|
if (reason) return this.refuse(reason);
|
|
2824
2926
|
const result = applyVideoEdits(this.project, edits);
|
|
2825
2927
|
if (!result.ok) return options.dryRun ? result : this.refuse(result.error);
|
|
2826
|
-
if (!options.dryRun) this.commit(label, result.project);
|
|
2928
|
+
if (!options.dryRun) this.commit(label, result.project, options.merge);
|
|
2827
2929
|
return result;
|
|
2828
2930
|
}
|
|
2829
2931
|
/** Opens a project (JSON or object). `clearHistory` drops the undo stack (opening a file); otherwise it is one undo step. */
|
|
@@ -2923,6 +3025,181 @@ var VideoEditorController = class {
|
|
|
2923
3025
|
if (!this.selection.length) return this.refuse("Select clips to delete first.");
|
|
2924
3026
|
return this.apply({ op: "delete_clips", clips: [...this.selection], ripple: options.ripple }, options.ripple ? "Ripple delete" : "Delete clips");
|
|
2925
3027
|
}
|
|
3028
|
+
// -----------------------------------------------------------------------------------------
|
|
3029
|
+
// Effects
|
|
3030
|
+
// -----------------------------------------------------------------------------------------
|
|
3031
|
+
/** The registered effects provider, or `null`. */
|
|
3032
|
+
get effectsProvider() {
|
|
3033
|
+
return this.effects.provider;
|
|
3034
|
+
}
|
|
3035
|
+
/** Registers (or with `null` removes) the effects provider. Not an edit: no undo step. */
|
|
3036
|
+
setEffectsProvider(provider) {
|
|
3037
|
+
this.effects.set(provider);
|
|
3038
|
+
}
|
|
3039
|
+
/** Registers `provider` and returns a function that removes it again (if it is still the one set). */
|
|
3040
|
+
useEffects(provider) {
|
|
3041
|
+
return this.effects.use(provider);
|
|
3042
|
+
}
|
|
3043
|
+
/** The provider's description of an effect, checked for `place` (rejects without a provider or for an unknown id). */
|
|
3044
|
+
async describeEffect(effect, place) {
|
|
3045
|
+
const d = await this.effects.require().describe(effect);
|
|
3046
|
+
if (place) checkVideoEffectPlace(d, place);
|
|
3047
|
+
return d;
|
|
3048
|
+
}
|
|
3049
|
+
refused(message) {
|
|
3050
|
+
return this.refuse(message);
|
|
3051
|
+
}
|
|
3052
|
+
async effectEdit(run, merge) {
|
|
3053
|
+
const reason = this.readOnlyReason();
|
|
3054
|
+
if (reason) return this.refused(reason);
|
|
3055
|
+
let plan;
|
|
3056
|
+
try {
|
|
3057
|
+
plan = await run();
|
|
3058
|
+
} catch (error) {
|
|
3059
|
+
if (isEffectError(error) || error instanceof Error) return this.refused(error.message);
|
|
3060
|
+
throw error;
|
|
3061
|
+
}
|
|
3062
|
+
const result = this.apply(plan.edit, plan.label, { merge });
|
|
3063
|
+
if (!result.ok) return result;
|
|
3064
|
+
return { ...result, clip: plan.clip, ...plan.effect ? { effect: plan.effect() } : {} };
|
|
3065
|
+
}
|
|
3066
|
+
clipOrThrow(id) {
|
|
3067
|
+
const at = findVideoClip(this.project, id);
|
|
3068
|
+
if (!at) throw new Error(`There is no clip "${id}"; list_applied_effects or get_video_project lists the clips.`);
|
|
3069
|
+
return at.clip;
|
|
3070
|
+
}
|
|
3071
|
+
/**
|
|
3072
|
+
* Adds a filter or overlay effect to a clip's effect stack (at `index`, default the end: applied
|
|
3073
|
+
* last), with `values` over the effect's defaults (numbers may be keyframes). One undo step.
|
|
3074
|
+
* Returns the effect with its new id.
|
|
3075
|
+
*/
|
|
3076
|
+
applyClipEffect(clip, effect, options = {}) {
|
|
3077
|
+
return this.effectEdit(async () => {
|
|
3078
|
+
const current = this.clipOrThrow(clip);
|
|
3079
|
+
const d = await this.describeEffect(effect, "clip");
|
|
3080
|
+
const values = checkVideoEffectValues(d, options.values, { keyframes: true });
|
|
3081
|
+
const stack = [...current.effects ?? []];
|
|
3082
|
+
const taken = new Set(stack.map((e) => e.id));
|
|
3083
|
+
let n = 1;
|
|
3084
|
+
while (taken.has(`fx${n}`)) n++;
|
|
3085
|
+
const added = { id: `fx${n}`, effect: d.id, provider: this.effects.require().id, name: d.name, values };
|
|
3086
|
+
if (options.enabled === false) added.enabled = false;
|
|
3087
|
+
if (options.opacity !== void 0) added.opacity = options.opacity;
|
|
3088
|
+
const index = options.index ?? stack.length;
|
|
3089
|
+
if (!(Number.isInteger(index) && index >= 0 && index <= stack.length)) throw new Error(`"index" must be a whole number from 0 to ${stack.length}.`);
|
|
3090
|
+
stack.splice(index, 0, added);
|
|
3091
|
+
return { edit: { op: "update_clip", clip, set: { effects: stack } }, label: `Add effect ${d.name}`, clip, effect: () => added };
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
/**
|
|
3095
|
+
* Changes an effect on a clip: `values` (merged over its current ones; numbers may be
|
|
3096
|
+
* keyframes, `null` resets a control to its default), `enabled`, `opacity` or its place in the
|
|
3097
|
+
* stack (`index`). Values are checked against the provider's controls. One undo step.
|
|
3098
|
+
*/
|
|
3099
|
+
updateClipEffect(clip, instance, changes, options = {}) {
|
|
3100
|
+
return this.effectEdit(async () => {
|
|
3101
|
+
const current = this.clipOrThrow(clip);
|
|
3102
|
+
const stack = [...current.effects ?? []];
|
|
3103
|
+
const i = stack.findIndex((e) => e.id === instance);
|
|
3104
|
+
if (i < 0) throw new Error(`Clip "${clip}" has no effect "${instance}"; its effects are ${stack.map((e) => `"${e.id}" (${e.name ?? e.effect})`).join(", ") || "none"}.`);
|
|
3105
|
+
const was = stack[i];
|
|
3106
|
+
const next = { ...was };
|
|
3107
|
+
if (changes.values) {
|
|
3108
|
+
const d = await this.describeEffect(was.effect);
|
|
3109
|
+
const reset = Object.keys(changes.values).filter((k) => changes.values[k] === null);
|
|
3110
|
+
const given = Object.fromEntries(Object.entries(changes.values).filter(([, v]) => v !== null));
|
|
3111
|
+
const base = { ...was.values ?? {} };
|
|
3112
|
+
for (const k of reset) {
|
|
3113
|
+
if (!(k in d.controls.properties)) throw new Error(`"${k}" is not a control of "${d.id}"; its controls are ${Object.keys(d.controls.properties).join(", ")}.`);
|
|
3114
|
+
base[k] = d.defaults[k];
|
|
3115
|
+
}
|
|
3116
|
+
next.values = checkVideoEffectValues(d, given, { base, keyframes: true });
|
|
3117
|
+
}
|
|
3118
|
+
if (changes.enabled !== void 0) {
|
|
3119
|
+
if (changes.enabled) delete next.enabled;
|
|
3120
|
+
else next.enabled = false;
|
|
3121
|
+
}
|
|
3122
|
+
if (changes.opacity !== void 0) next.opacity = changes.opacity;
|
|
3123
|
+
stack.splice(i, 1);
|
|
3124
|
+
const index = changes.index ?? i;
|
|
3125
|
+
if (!(Number.isInteger(index) && index >= 0 && index <= stack.length)) throw new Error(`"index" must be a whole number from 0 to ${stack.length}.`);
|
|
3126
|
+
stack.splice(index, 0, next);
|
|
3127
|
+
return { edit: { op: "update_clip", clip, set: { effects: stack } }, label: `Change effect ${was.name ?? was.effect}`, clip, effect: () => next };
|
|
3128
|
+
}, options.merge);
|
|
3129
|
+
}
|
|
3130
|
+
/** Removes an effect from a clip's stack (works without a provider, so placeholders can be cleared). One undo step. */
|
|
3131
|
+
removeClipEffect(clip, instance) {
|
|
3132
|
+
const reason = this.readOnlyReason();
|
|
3133
|
+
if (reason) return this.refused(reason);
|
|
3134
|
+
const at = findVideoClip(this.project, clip);
|
|
3135
|
+
if (!at) return this.refused(`There is no clip "${clip}".`);
|
|
3136
|
+
const stack = at.clip.effects ?? [];
|
|
3137
|
+
const was = stack.find((e) => e.id === instance);
|
|
3138
|
+
if (!was) return this.refused(`Clip "${clip}" has no effect "${instance}"; its effects are ${stack.map((e) => `"${e.id}"`).join(", ") || "none"}.`);
|
|
3139
|
+
const rest = stack.filter((e) => e !== was);
|
|
3140
|
+
const result = rest.length ? this.apply({ op: "update_clip", clip, set: { effects: rest } }, `Remove effect ${was.name ?? was.effect}`) : this.apply({ op: "update_clip", clip, remove: ["effects"] }, `Remove effect ${was.name ?? was.effect}`);
|
|
3141
|
+
return result.ok ? { ...result, clip, effect: was } : result;
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* Sets a provider transition into `clip` from the clip that ends where it starts (centred on the
|
|
3145
|
+
* cut, `duration` ms, default the effect's own length or 1000). One undo step.
|
|
3146
|
+
*/
|
|
3147
|
+
setEffectTransition(clip, effect, options = {}) {
|
|
3148
|
+
return this.effectEdit(async () => {
|
|
3149
|
+
this.clipOrThrow(clip);
|
|
3150
|
+
const d = await this.describeEffect(effect, "transition");
|
|
3151
|
+
const values = checkVideoEffectValues(d, options.values);
|
|
3152
|
+
const duration = options.duration ?? d.duration ?? 1e3;
|
|
3153
|
+
return { edit: { op: "set_transition", clip, transition: { type: "effect", duration, effect: { effect: d.id, provider: this.effects.require().id, name: d.name, values } } }, label: `Transition ${d.name}`, clip };
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Adds an effect clip: a generator, title or overlay effect drawn over the whole frame for
|
|
3158
|
+
* `duration` ms (default the effect's own length, else 5 s) at `start` (default the end of the
|
|
3159
|
+
* track). One undo step. Returns the new clip's id.
|
|
3160
|
+
*/
|
|
3161
|
+
addEffectClip(track, effect, options = {}) {
|
|
3162
|
+
return this.effectEdit(async () => {
|
|
3163
|
+
const d = await this.describeEffect(effect, "generator");
|
|
3164
|
+
const values = checkVideoEffectValues(d, options.values, { keyframes: true });
|
|
3165
|
+
const duration = options.duration ?? d.duration ?? 5e3;
|
|
3166
|
+
const label = `Add ${d.name}`;
|
|
3167
|
+
const clipId = options.id ?? "";
|
|
3168
|
+
return {
|
|
3169
|
+
edit: { op: "add_clip", track, mode: options.mode, ripple: options.ripple, clip: { type: "effect", ...options.id ? { id: options.id } : {}, effect: d.id, provider: this.effects.require().id, name: options.name ?? d.name, values, start: options.start, duration } },
|
|
3170
|
+
label,
|
|
3171
|
+
clip: clipId
|
|
3172
|
+
};
|
|
3173
|
+
}).then((r) => r.ok && !r.clip ? { ...r, clip: r.outcomes[0]?.clip ?? "" } : r);
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* Changes the values of an effect clip's picture (`target` its id) or of the effect transition
|
|
3177
|
+
* into a clip, merged over the current ones and checked against the provider's controls.
|
|
3178
|
+
* Transition values cannot be keyframed. One undo step.
|
|
3179
|
+
*/
|
|
3180
|
+
updateEffectValues(clip, part, values, options = {}) {
|
|
3181
|
+
return this.effectEdit(async () => {
|
|
3182
|
+
const current = this.clipOrThrow(clip);
|
|
3183
|
+
if (part === "generator") {
|
|
3184
|
+
if (current.type !== "effect") throw new Error(`Clip "${clip}" is not an effect clip.`);
|
|
3185
|
+
const d2 = await this.describeEffect(current.effect);
|
|
3186
|
+
return { edit: { op: "update_clip", clip, set: { values: checkVideoEffectValues(d2, values, { base: current.values ?? {}, keyframes: true }) } }, label: `Change ${current.name ?? d2.name}`, clip };
|
|
3187
|
+
}
|
|
3188
|
+
const t = current.transition;
|
|
3189
|
+
if (t?.type !== "effect" || !t.effect) throw new Error(`Clip "${clip}" has no effect transition.`);
|
|
3190
|
+
const d = await this.describeEffect(t.effect.effect);
|
|
3191
|
+
const merged = checkVideoEffectValues(d, values, { base: t.effect.values ?? {} });
|
|
3192
|
+
return { edit: { op: "set_transition", clip, transition: { ...t, effect: { ...t.effect, values: merged } } }, label: `Change transition ${d.name}`, clip };
|
|
3193
|
+
}, options.merge);
|
|
3194
|
+
}
|
|
3195
|
+
/** Keys for one numeric control of a clip effect (clip-relative ms), or `null` to stop animating it (keeping its value at the clip's start). */
|
|
3196
|
+
setClipEffectKeyframes(clip, instance, control, keys) {
|
|
3197
|
+
const current = findVideoClip(this.project, clip)?.clip;
|
|
3198
|
+
const was = current?.effects?.find((e) => e.id === instance);
|
|
3199
|
+
const value = was?.values?.[control];
|
|
3200
|
+
const plain = keys === null ? isVideoKeyframes(value) ? value.keys[0]?.v : value : { keys: [...keys].sort((a, b) => a.t - b.t) };
|
|
3201
|
+
return this.updateClipEffect(clip, instance, { values: { [control]: plain ?? null } });
|
|
3202
|
+
}
|
|
2926
3203
|
/** What plays at `time` (default: the playhead). */
|
|
2927
3204
|
frame(time = this.player.currentTime) {
|
|
2928
3205
|
return videoFrameAt(this.project, time);
|
|
@@ -2983,7 +3260,8 @@ var VIDEO_TRANSITION_LABELS = {
|
|
|
2983
3260
|
wipe: "Wipe",
|
|
2984
3261
|
push: "Push",
|
|
2985
3262
|
slide: "Slide",
|
|
2986
|
-
iris: "Iris"
|
|
3263
|
+
iris: "Iris",
|
|
3264
|
+
effect: "Effect"
|
|
2987
3265
|
};
|
|
2988
3266
|
var videoTransitionHasDirection = (type) => type === "wipe" || type === "push" || type === "slide";
|
|
2989
3267
|
var VIDEO_BLEND_MODE_LABELS = {
|
|
@@ -3012,6 +3290,7 @@ function videoClipTint(clip, asset) {
|
|
|
3012
3290
|
if (clip.type === "text") return "#a855f7";
|
|
3013
3291
|
if (clip.type === "sequence") return "#0d9488";
|
|
3014
3292
|
if (clip.type === "color") return clip.color;
|
|
3293
|
+
if (clip.type === "effect") return "#db2777";
|
|
3015
3294
|
if (!asset) return "#ef4444";
|
|
3016
3295
|
return asset.kind === "audio" ? "#10b981" : asset.kind === "image" ? "#f59e0b" : "#3b82f6";
|
|
3017
3296
|
}
|
|
@@ -3020,6 +3299,7 @@ function videoClipLabel(clip, asset, sequence) {
|
|
|
3020
3299
|
if (clip.type === "text") return clip.text.split("\n")[0];
|
|
3021
3300
|
if (clip.type === "color") return clip.color;
|
|
3022
3301
|
if (clip.type === "sequence") return `Compound: ${sequence?.name ?? clip.sequence}`;
|
|
3302
|
+
if (clip.type === "effect") return `Effect: ${clip.effect}`;
|
|
3023
3303
|
const base = asset?.name ?? asset?.src.replace(/^.*[\\/]/, "") ?? clip.asset;
|
|
3024
3304
|
const speed = videoClipSpeed(clip);
|
|
3025
3305
|
return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
|
|
@@ -3226,7 +3506,7 @@ var TRANSFORM = object(
|
|
|
3226
3506
|
);
|
|
3227
3507
|
var TRANSITION = object(
|
|
3228
3508
|
{
|
|
3229
|
-
type: { type: "string", enum:
|
|
3509
|
+
type: { type: "string", enum: VIDEO_TRANSITION_TYPES.filter((t) => t !== "effect"), description: "crossfade (this clip fades up over the one before), dip (down into a colour and up out of it), wipe (an edge travels across revealing this clip), push (this clip slides in and pushes the one before out), slide (this clip slides in over the one before) or iris (a circle opens from the centre). For a transition effect from the effects provider use set_video_effect_transition." },
|
|
3230
3510
|
duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms, centred on the cut. Every type but dip needs half of it in media past the cut on both sides (handles); a dip needs none." },
|
|
3231
3511
|
color: COLOR("For dip: the colour passed through (default black)"),
|
|
3232
3512
|
direction: { type: "string", enum: [...VIDEO_WIPE_DIRECTIONS], description: "For wipe, push and slide: the way the edge or picture travels (default right, coming in from the left)." },
|
|
@@ -4007,7 +4287,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
4007
4287
|
if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
|
|
4008
4288
|
const { clip } = at;
|
|
4009
4289
|
if (clip.type === "sequence") throw new Error(`Clip "${id}" is a compound clip, which plays its sequence at normal speed; change the speed of the clips inside the sequence instead.`);
|
|
4010
|
-
if (clip.type !== "media") throw new Error(`Clip "${id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to change the speed of; use a video or audio clip.`);
|
|
4290
|
+
if (clip.type !== "media") throw new Error(`Clip "${id}" is a ${clip.type === "text" ? "title" : clip.type === "effect" ? "effect clip" : "colour"}, so it has no source to change the speed of; use a video or audio clip.`);
|
|
4011
4291
|
if (project().assets.find((a) => a.id === clip.asset)?.kind === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
|
|
4012
4292
|
return clip;
|
|
4013
4293
|
};
|
|
@@ -4355,6 +4635,178 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
4355
4635
|
}
|
|
4356
4636
|
};
|
|
4357
4637
|
}
|
|
4638
|
+
|
|
4639
|
+
// src/video-editor/effect-tools.ts
|
|
4640
|
+
function videoClipAppliedEffects(clip) {
|
|
4641
|
+
const out = [];
|
|
4642
|
+
const ref = (r) => ({ effect: r.effect, ...r.name ? { name: r.name } : {}, ...r.provider ? { provider: r.provider } : {} });
|
|
4643
|
+
if (clip.type === "effect") out.push({ id: "generator", ...ref(clip), role: "effect clip", enabled: true, values: clip.values ?? {} });
|
|
4644
|
+
if (clip.transition?.type === "effect" && clip.transition.effect) out.push({ id: "transition", ...ref(clip.transition.effect), role: "transition", enabled: true, values: { ...clip.transition.effect.values ?? {} }, duration: clip.transition.duration });
|
|
4645
|
+
(clip.effects ?? []).forEach((e, index) => out.push({ id: e.id, ...ref(e), role: "clip effect", enabled: e.enabled !== false, ...e.opacity !== void 0 ? { opacity: e.opacity } : {}, values: e.values ?? {}, index }));
|
|
4646
|
+
return out;
|
|
4647
|
+
}
|
|
4648
|
+
var MS2 = (what) => ({ type: "number", minimum: 0, description: `${what}, in milliseconds.` });
|
|
4649
|
+
var KEYS2 = {
|
|
4650
|
+
type: "array",
|
|
4651
|
+
minItems: 1,
|
|
4652
|
+
items: {
|
|
4653
|
+
type: "object",
|
|
4654
|
+
properties: {
|
|
4655
|
+
t: { type: "number", description: "Clip-relative time in ms (0 = the clip's start)." },
|
|
4656
|
+
v: { type: "number", description: "The control's value at t, within its range from describe_effect." },
|
|
4657
|
+
ease: { anyOf: [{ type: "string", enum: [...VIDEO_EASE_NAMES] }, { type: "array", items: { type: "number" }, minItems: 4, maxItems: 4 }], description: "How the value moves to the next key: linear (default), hold, ease-in, ease-out, ease-in-out, or CSS cubic-bezier [x1, y1, x2, y2]." }
|
|
4658
|
+
},
|
|
4659
|
+
required: ["t", "v"],
|
|
4660
|
+
additionalProperties: false
|
|
4661
|
+
},
|
|
4662
|
+
description: "Keys in any order (sorted for you), one per time."
|
|
4663
|
+
};
|
|
4664
|
+
function createVideoEffectsToolProvider(controller, options = {}) {
|
|
4665
|
+
const readOnlyReason = () => {
|
|
4666
|
+
const ro = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
|
|
4667
|
+
if (ro) return typeof ro === "string" ? ro : "The video project is read-only.";
|
|
4668
|
+
return controller.readOnlyReason();
|
|
4669
|
+
};
|
|
4670
|
+
const clipOf = (id) => {
|
|
4671
|
+
const at = findVideoClip(controller.getProject(), id);
|
|
4672
|
+
if (!at) {
|
|
4673
|
+
const ids = controller.getProject().tracks.flatMap((t) => t.clips.map((c) => c.id));
|
|
4674
|
+
throw new Error(`There is no clip "${id}"; the clips are ${ids.map((c) => `"${c}"`).join(", ") || "none"} (get_video_project lists them).`);
|
|
4675
|
+
}
|
|
4676
|
+
return at.clip;
|
|
4677
|
+
};
|
|
4678
|
+
const result = (r, target, instance, extra = {}) => {
|
|
4679
|
+
if (!r.ok) throw new Error(r.error);
|
|
4680
|
+
const effects = videoClipAppliedEffects(clipOf(target));
|
|
4681
|
+
const effect = effects.find((e) => e.id === (instance ?? r.effect?.id)) ?? (r.effect ? { id: r.effect.id, effect: r.effect.effect, role: "clip effect", enabled: r.effect.enabled !== false, values: r.effect.values ?? {} } : effects[0]);
|
|
4682
|
+
return { target, effect, effects, ...extra };
|
|
4683
|
+
};
|
|
4684
|
+
return createEffectToolProvider({
|
|
4685
|
+
id: options.id ?? "video-effects",
|
|
4686
|
+
namespace: options.namespace,
|
|
4687
|
+
target: { noun: "clip", id: 'A clip id from get_video_project (e.g. "c1").' },
|
|
4688
|
+
applyUses: ["filter", "overlay"],
|
|
4689
|
+
applyDetails: "Filters change the clip's picture and overlays draw over it, in stack order, on the picture as it comes from the source, before the clip's grade, placement, opacity and blend; numbers may be keyframed (clip-relative ms). Transitions go through set_video_effect_transition and generators or titles through add_video_effect_clip. The preview renders them frame by frame; the ffmpeg export then needs rendered frames (see export_video_ffmpeg).",
|
|
4690
|
+
provider: () => controller.effectsProvider,
|
|
4691
|
+
readOnlyReason,
|
|
4692
|
+
list: (target) => {
|
|
4693
|
+
const clips = target ? [clipOf(target)] : controller.getProject().tracks.flatMap((t) => t.clips);
|
|
4694
|
+
return clips.map((c) => ({ target: c.id, ...c.name ? { name: c.name } : {}, effects: videoClipAppliedEffects(c) })).filter((t) => target || t.effects.length);
|
|
4695
|
+
},
|
|
4696
|
+
apply: async (target, effect, args) => {
|
|
4697
|
+
clipOf(target);
|
|
4698
|
+
return result(await controller.applyClipEffect(target, effect, { values: args.values, index: args.index, enabled: args.enabled, opacity: args.opacity }), target);
|
|
4699
|
+
},
|
|
4700
|
+
update: async (target, instance, changes) => {
|
|
4701
|
+
clipOf(target);
|
|
4702
|
+
if (instance === "generator" || instance === "transition") {
|
|
4703
|
+
const extra = ["enabled", "opacity", "index"].filter((k) => changes[k] !== void 0);
|
|
4704
|
+
if (extra.length) throw new Error(`An ${instance === "generator" ? "effect clip" : "effect transition"} only takes values; change its ${extra.join(", ")} with update_video_clip${instance === "transition" ? " or set_video_effect_transition" : ""}.`);
|
|
4705
|
+
const values = Object.fromEntries(Object.entries(changes.values ?? {}).filter(([, v]) => v !== null));
|
|
4706
|
+
return result(await controller.updateEffectValues(target, instance, values), target, instance);
|
|
4707
|
+
}
|
|
4708
|
+
return result(await controller.updateClipEffect(target, instance, changes), target, instance);
|
|
4709
|
+
},
|
|
4710
|
+
remove: async (target, instance) => {
|
|
4711
|
+
const clip = clipOf(target);
|
|
4712
|
+
const was = videoClipAppliedEffects(clip).find((e) => e.id === instance);
|
|
4713
|
+
if (!was) throw new Error(`Clip "${target}" has no effect "${instance}"; its effects are ${videoClipAppliedEffects(clip).map((e) => `"${e.id}"`).join(", ") || "none"}.`);
|
|
4714
|
+
if (instance === "generator") throw new Error(`"${target}" is an effect clip: its effect is the clip itself. Delete the clip with delete_video_clips.`);
|
|
4715
|
+
if (instance === "transition") {
|
|
4716
|
+
const r2 = controller.apply({ op: "set_transition", clip: target, transition: null }, "Remove transition");
|
|
4717
|
+
if (!r2.ok) throw new Error(r2.error);
|
|
4718
|
+
return { target, effect: was, effects: videoClipAppliedEffects(clipOf(target)) };
|
|
4719
|
+
}
|
|
4720
|
+
const r = controller.removeClipEffect(target, instance);
|
|
4721
|
+
if (!r.ok) throw new Error(r.error);
|
|
4722
|
+
return { target, effect: was, effects: videoClipAppliedEffects(clipOf(target)) };
|
|
4723
|
+
},
|
|
4724
|
+
subscribe: (notify) => controller.subscribe(() => notify()),
|
|
4725
|
+
extraTools: (ctx) => [
|
|
4726
|
+
{
|
|
4727
|
+
name: "add_video_effect_clip",
|
|
4728
|
+
title: "Add an effect clip",
|
|
4729
|
+
description: "Adds a clip drawn by a provider generator, title or overlay effect (search_effects with use generator, title or overlay), filling the frame like a colour clip; transparent effects show the tracks beneath. It can be moved, trimmed, faded, graded and given clip effects like any clip. One undo step. Returns the new clip id and its effect. The ffmpeg export then needs rendered frames.",
|
|
4730
|
+
inputSchema: {
|
|
4731
|
+
type: "object",
|
|
4732
|
+
properties: {
|
|
4733
|
+
track: { type: "string", minLength: 1, description: 'A video track id from get_video_project (e.g. "v1"); earlier tracks draw on top.' },
|
|
4734
|
+
effect: ctx.effectIdSchema(["generator", "title", "overlay"]),
|
|
4735
|
+
start: MS2("Where it starts on the timeline (default: the end of the track)"),
|
|
4736
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms (default: the effect's own length, else 5000)." },
|
|
4737
|
+
values: ctx.valuesSchema("Control values over the effect's defaults", true),
|
|
4738
|
+
name: { type: "string", minLength: 1, description: "A label for the clip (default: the effect's name)." },
|
|
4739
|
+
mode: { type: "string", enum: [...VIDEO_PLACEMENT_MODES], description: "What to do with clips already there: refuse (default), overwrite or insert." }
|
|
4740
|
+
},
|
|
4741
|
+
required: ["track", "effect"],
|
|
4742
|
+
additionalProperties: false
|
|
4743
|
+
},
|
|
4744
|
+
annotations: { title: "Add an effect clip", ...ctx.annotations.write },
|
|
4745
|
+
handler: async (args) => {
|
|
4746
|
+
ctx.requireProvider();
|
|
4747
|
+
const r = await controller.addEffectClip(args.track, args.effect, { start: args.start, duration: args.duration, values: args.values, name: args.name, mode: args.mode });
|
|
4748
|
+
if (!r.ok) throw new Error(r.error);
|
|
4749
|
+
return { clip: r.clip, track: args.track, effects: videoClipAppliedEffects(clipOf(r.clip)) };
|
|
4750
|
+
}
|
|
4751
|
+
},
|
|
4752
|
+
{
|
|
4753
|
+
name: "set_video_effect_transition",
|
|
4754
|
+
title: "Set an effect transition",
|
|
4755
|
+
description: `Sets a provider transition effect (search_effects with use transition) into a clip from the clip that ends where it starts on the same track, centred on the cut; like a crossfade it needs half its length in media past the cut on both sides. Replaces any transition there. One undo step. Returns the clip's effects. Remove it with remove_effect (effect "transition") or set_video_transition null.`,
|
|
4756
|
+
inputSchema: {
|
|
4757
|
+
type: "object",
|
|
4758
|
+
properties: {
|
|
4759
|
+
clip: { type: "string", minLength: 1, description: "The incoming clip (the one after the cut), from get_video_project." },
|
|
4760
|
+
effect: ctx.effectIdSchema(["transition"]),
|
|
4761
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "Length in ms, centred on the cut (default: the effect's own length, else 1000)." },
|
|
4762
|
+
values: ctx.valuesSchema("Control values over the effect's defaults (plain values; transitions cannot be keyframed)")
|
|
4763
|
+
},
|
|
4764
|
+
required: ["clip", "effect"],
|
|
4765
|
+
additionalProperties: false
|
|
4766
|
+
},
|
|
4767
|
+
annotations: { title: "Set an effect transition", ...ctx.annotations.writeIdempotent },
|
|
4768
|
+
handler: async (args) => {
|
|
4769
|
+
ctx.requireProvider();
|
|
4770
|
+
const r = await controller.setEffectTransition(args.clip, args.effect, { duration: args.duration, values: args.values });
|
|
4771
|
+
if (!r.ok) throw new Error(r.error);
|
|
4772
|
+
return { clip: args.clip, transition: clipOf(args.clip).transition, effects: videoClipAppliedEffects(clipOf(args.clip)) };
|
|
4773
|
+
}
|
|
4774
|
+
},
|
|
4775
|
+
{
|
|
4776
|
+
name: "set_video_effect_keyframes",
|
|
4777
|
+
title: "Keyframe an effect control",
|
|
4778
|
+
description: "Animates one number control of a clip effect or an effect clip with keys at clip-relative times (ms), or with keys null stops animating it, keeping its value at the clip's start. Values are checked against the control's range from describe_effect. One undo step. Returns the effect.",
|
|
4779
|
+
inputSchema: {
|
|
4780
|
+
type: "object",
|
|
4781
|
+
properties: {
|
|
4782
|
+
clip: { type: "string", minLength: 1, description: "A clip id from list_applied_effects." },
|
|
4783
|
+
effect: { type: "string", minLength: 1, description: `The effect's id on the clip ("fx1", or "generator" for an effect clip's own effect).` },
|
|
4784
|
+
control: { type: "string", minLength: 1, description: "A number control name from describe_effect." },
|
|
4785
|
+
keys: { anyOf: [KEYS2, { type: "null" }], description: "The keys, or null to stop animating the control." }
|
|
4786
|
+
},
|
|
4787
|
+
required: ["clip", "effect", "control", "keys"],
|
|
4788
|
+
additionalProperties: false
|
|
4789
|
+
},
|
|
4790
|
+
annotations: { title: "Keyframe an effect control", ...ctx.annotations.writeIdempotent },
|
|
4791
|
+
handler: async (args) => {
|
|
4792
|
+
ctx.requireProvider();
|
|
4793
|
+
const keys = args.keys;
|
|
4794
|
+
let r;
|
|
4795
|
+
if (args.effect === "generator") {
|
|
4796
|
+
const clip = clipOf(args.clip);
|
|
4797
|
+
if (clip.type !== "effect") throw new Error(`Clip "${args.clip}" is not an effect clip; give the id of one of its effects (list_applied_effects).`);
|
|
4798
|
+
const was = clip.values?.[args.control];
|
|
4799
|
+
const plain = keys === null ? isVideoKeyframes(was) ? was.keys[0]?.v : was : { keys: [...keys].sort((a, b) => a.t - b.t) };
|
|
4800
|
+
if (plain === void 0) throw new Error(`"${args.control}" has no value to keep; give keys instead.`);
|
|
4801
|
+
r = await controller.updateEffectValues(args.clip, "generator", { [args.control]: plain });
|
|
4802
|
+
} else r = await controller.setClipEffectKeyframes(args.clip, args.effect, args.control, keys);
|
|
4803
|
+
if (!r.ok) throw new Error(r.error);
|
|
4804
|
+
return { clip: args.clip, effect: videoClipAppliedEffects(clipOf(args.clip)).find((e) => e.id === args.effect) };
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
]
|
|
4808
|
+
});
|
|
4809
|
+
}
|
|
4358
4810
|
export {
|
|
4359
4811
|
VIDEO_BLEND_MODES,
|
|
4360
4812
|
VIDEO_BLEND_MODE_LABELS,
|
|
@@ -4367,6 +4819,7 @@ export {
|
|
|
4367
4819
|
VIDEO_DEFAULT_TITLE_DURATION,
|
|
4368
4820
|
VIDEO_EASE_NAMES,
|
|
4369
4821
|
VIDEO_EDIT_OPS,
|
|
4822
|
+
VIDEO_EFFECT_PLACES,
|
|
4370
4823
|
VIDEO_FADE_CURVES,
|
|
4371
4824
|
VIDEO_FADE_CURVE_LABELS,
|
|
4372
4825
|
VIDEO_FITS,
|
|
@@ -4390,6 +4843,7 @@ export {
|
|
|
4390
4843
|
VIDEO_WIPE_DIRECTIONS,
|
|
4391
4844
|
VideoAudioPeaksCache,
|
|
4392
4845
|
VideoEditorController,
|
|
4846
|
+
VideoEffectsEngine,
|
|
4393
4847
|
VideoFrameCache,
|
|
4394
4848
|
VideoLutCache,
|
|
4395
4849
|
VideoPreviewEngine,
|
|
@@ -4401,9 +4855,12 @@ export {
|
|
|
4401
4855
|
applyVideoLut,
|
|
4402
4856
|
applyVideoLutToPixels,
|
|
4403
4857
|
buildVideoAudioPeaks,
|
|
4858
|
+
checkVideoEffectPlace,
|
|
4859
|
+
checkVideoEffectValues,
|
|
4404
4860
|
checkVideoProject,
|
|
4405
4861
|
checkVideoTimeRemap,
|
|
4406
4862
|
createVideoEditorToolProvider,
|
|
4863
|
+
createVideoEffectsToolProvider,
|
|
4407
4864
|
createVideoElementDecoder,
|
|
4408
4865
|
createVideoProject,
|
|
4409
4866
|
decodeVideoAudioPeaks,
|
|
@@ -4425,16 +4882,21 @@ export {
|
|
|
4425
4882
|
isVideoDropFrameRate,
|
|
4426
4883
|
isVideoKeyframes,
|
|
4427
4884
|
mergeVideoRanges,
|
|
4885
|
+
mixEffectPixels,
|
|
4428
4886
|
nextFreeVideoSlot,
|
|
4429
4887
|
normalizeVideoGrade,
|
|
4430
4888
|
normalizeVideoGradeLut,
|
|
4431
4889
|
normalizeVideoWord,
|
|
4890
|
+
overEffectPixels,
|
|
4432
4891
|
parseVideoAnimatable,
|
|
4433
4892
|
parseVideoAsset,
|
|
4434
4893
|
parseVideoAssetProxies,
|
|
4435
4894
|
parseVideoClip,
|
|
4436
4895
|
parseVideoClipAnimation,
|
|
4896
|
+
parseVideoClipEffects,
|
|
4437
4897
|
parseVideoCubeLut,
|
|
4898
|
+
parseVideoEffectRef,
|
|
4899
|
+
parseVideoEffectValues,
|
|
4438
4900
|
parseVideoGrade,
|
|
4439
4901
|
parseVideoGradeLut,
|
|
4440
4902
|
parseVideoKeyframes,
|
|
@@ -4455,6 +4917,7 @@ export {
|
|
|
4455
4917
|
probeVideoMedia,
|
|
4456
4918
|
readVideoScopesPanel,
|
|
4457
4919
|
removeVideoKey,
|
|
4920
|
+
renderVideoFrameExact,
|
|
4458
4921
|
resolveVideoPreviewScale,
|
|
4459
4922
|
retimeVideoAnimatable,
|
|
4460
4923
|
setVideoKey,
|
|
@@ -4482,7 +4945,9 @@ export {
|
|
|
4482
4945
|
videoClipAnimated,
|
|
4483
4946
|
videoClipAnimationProperties,
|
|
4484
4947
|
videoClipAnimationSpans,
|
|
4948
|
+
videoClipAppliedEffects,
|
|
4485
4949
|
videoClipBefore,
|
|
4950
|
+
videoClipEffectUses,
|
|
4486
4951
|
videoClipEnd,
|
|
4487
4952
|
videoClipHandles,
|
|
4488
4953
|
videoClipKeyframes,
|
|
@@ -4503,9 +4968,13 @@ export {
|
|
|
4503
4968
|
videoDropTracks,
|
|
4504
4969
|
videoEaseBezier,
|
|
4505
4970
|
videoEaseProgress,
|
|
4971
|
+
videoEffectFrameTime,
|
|
4972
|
+
videoEffectLayerKey,
|
|
4973
|
+
videoEffectValuesAt,
|
|
4506
4974
|
videoExposureNotes,
|
|
4507
4975
|
videoExposureStats,
|
|
4508
4976
|
videoFadeCurveGain,
|
|
4977
|
+
videoFfmpegUnsupportedEffects,
|
|
4509
4978
|
videoFrameAt,
|
|
4510
4979
|
videoFrameCacheKey,
|
|
4511
4980
|
videoFrameDuration,
|
|
@@ -4535,6 +5004,8 @@ export {
|
|
|
4535
5004
|
videoKeyframeToggleEdit,
|
|
4536
5005
|
videoKeyframeValueEdit,
|
|
4537
5006
|
videoKeysOf,
|
|
5007
|
+
videoLayersUseEffects,
|
|
5008
|
+
videoLayersWithoutTransitionEffects,
|
|
4538
5009
|
videoLuma,
|
|
4539
5010
|
videoLumaWaveform,
|
|
4540
5011
|
videoLutEdits,
|
|
@@ -4546,6 +5017,7 @@ export {
|
|
|
4546
5017
|
videoPeakDb,
|
|
4547
5018
|
videoPreviewSrc,
|
|
4548
5019
|
videoProjectDuration,
|
|
5020
|
+
videoProjectEffects,
|
|
4549
5021
|
videoProjectHasProxies,
|
|
4550
5022
|
videoProjectSvg,
|
|
4551
5023
|
videoProxyFfmpeg,
|