@pixerate/editor 0.85.0 → 0.86.0
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/video-editor/index.cjs +395 -74
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +71 -1
- package/dist/video-editor/index.d.ts +71 -1
- package/dist/video-editor/index.js +337 -16
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-video-editor/SKILL.md +12 -1
|
@@ -340,6 +340,105 @@ function videoGradeFfmpegFilters(grade) {
|
|
|
340
340
|
});
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
// src/video-editor/remap.ts
|
|
344
|
+
function videoClipSourceTime(clip, clipTime) {
|
|
345
|
+
const base = clip.in ?? 0;
|
|
346
|
+
return clip.timeRemap ? base + videoValueAt(clip.timeRemap, clipTime, 0) : base + clipTime * (clip.speed ?? 1);
|
|
347
|
+
}
|
|
348
|
+
function videoClipSpeedAt(clip, clipTime) {
|
|
349
|
+
if (clip.type !== "media") return 1;
|
|
350
|
+
if (!clip.timeRemap) return clip.speed ?? 1;
|
|
351
|
+
const t = Math.min(Math.max(clipTime, 0), Math.max(0, clip.duration - 1));
|
|
352
|
+
return Math.max(0, videoClipSourceTime(clip, t + 1) - videoClipSourceTime(clip, t));
|
|
353
|
+
}
|
|
354
|
+
function videoSpeedRampKeys(segments) {
|
|
355
|
+
if (!segments.length) throw new Error("Give at least one stretch { duration, speed }.");
|
|
356
|
+
const keys = [];
|
|
357
|
+
let t = 0;
|
|
358
|
+
let v = 0;
|
|
359
|
+
segments.forEach((s, i) => {
|
|
360
|
+
if (!(s.duration > 0)) throw new Error(`Stretch ${i + 1} needs a duration above 0 ms.`);
|
|
361
|
+
for (const [name, x] of [["speed", s.speed], ["toSpeed", s.toSpeed]])
|
|
362
|
+
if (x !== void 0 && !(x >= 0 && x <= 20)) throw new Error(`Stretch ${i + 1}'s ${name} must be from 0 (a freeze) to 20; got ${x}.`);
|
|
363
|
+
const a = s.speed;
|
|
364
|
+
const b = s.toSpeed ?? s.speed;
|
|
365
|
+
const avg = (a + b) / 2;
|
|
366
|
+
let ease = "linear";
|
|
367
|
+
if (a !== b && avg > 0) {
|
|
368
|
+
ease = [1 / 3, 2 * a / (3 * (a + b)), 2 / 3, 1 - 2 * b / (3 * (a + b))].map((n2) => Math.round(n2 * 1e6) / 1e6);
|
|
369
|
+
}
|
|
370
|
+
keys.push({ t, v, ...ease === "linear" ? {} : { ease } });
|
|
371
|
+
t += s.duration;
|
|
372
|
+
v += avg * s.duration;
|
|
373
|
+
});
|
|
374
|
+
keys.push({ t, v });
|
|
375
|
+
return { keys: mergeLinear(keys), duration: t, source: v };
|
|
376
|
+
}
|
|
377
|
+
function mergeLinear(keys) {
|
|
378
|
+
const out = [keys[0]];
|
|
379
|
+
for (let i = 1; i < keys.length; i++) {
|
|
380
|
+
const prev = out[out.length - 1];
|
|
381
|
+
const k = keys[i];
|
|
382
|
+
const before = out.length > 1 ? out[out.length - 2] : null;
|
|
383
|
+
if (before && !before.ease && !prev.ease) {
|
|
384
|
+
const s1 = (prev.v - before.v) / (prev.t - before.t);
|
|
385
|
+
const s2 = (k.v - prev.v) / (k.t - prev.t);
|
|
386
|
+
if (Math.abs(s1 - s2) < 1e-9) {
|
|
387
|
+
out[out.length - 1] = k;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
out.push(k);
|
|
392
|
+
}
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
function checkVideoTimeRemap(remap, path = "timeRemap") {
|
|
396
|
+
const { keys } = remap;
|
|
397
|
+
if (!keys.length) throw new Error(`${path} needs at least one key.`);
|
|
398
|
+
keys.forEach((k, i) => {
|
|
399
|
+
if (k.v < 0) throw new Error(`${path}.keys[${i}].v must be at least 0 (ms after the in point).`);
|
|
400
|
+
const next = keys[i + 1];
|
|
401
|
+
if (!next) return;
|
|
402
|
+
if (next.v < k.v - 1e-6) throw new Error(`${path}.keys[${i + 1}] goes back in the source (${next.v} ms after ${k.v} ms); a ramp only plays forwards, so make each value at least the one before.`);
|
|
403
|
+
if (!videoEaseBezier(k.ease)) return;
|
|
404
|
+
let prev = k.v;
|
|
405
|
+
for (let n2 = 1; n2 <= 32; n2++) {
|
|
406
|
+
const v = videoValueAt(remap, k.t + (next.t - k.t) * n2 / 32);
|
|
407
|
+
if (v < prev - 1e-3) throw new Error(`${path}.keys[${i}].ease overshoots, which would play backwards; use a curve that only moves forwards (or linear).`);
|
|
408
|
+
prev = v;
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function videoTimeRemapSpan(remap) {
|
|
413
|
+
return { from: remap.keys[0].v, to: remap.keys[remap.keys.length - 1].v };
|
|
414
|
+
}
|
|
415
|
+
function videoTimeRemapBreakpoints(remap, step) {
|
|
416
|
+
const { points, holds } = videoKeyframeBreakpoints(remap, step);
|
|
417
|
+
const out = [];
|
|
418
|
+
points.forEach(([t, v], i) => {
|
|
419
|
+
out.push({ t, v });
|
|
420
|
+
if (holds.has(i) && points[i + 1]) out.push({ t: points[i + 1][0], v });
|
|
421
|
+
});
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
function videoSpeedRampEdit(clip, segments, ripple = true) {
|
|
425
|
+
const ramp = videoSpeedRampKeys(segments);
|
|
426
|
+
const offset = videoClipSourceTime(clip, 0) - (clip.in ?? 0);
|
|
427
|
+
return { op: "set_time_remap", clip: clip.id, keys: ramp.keys.map((k) => ({ ...k, v: k.v + offset })), duration: ramp.duration, ripple };
|
|
428
|
+
}
|
|
429
|
+
function videoFreezeFrameEdit(clip, at, duration, ripple = true) {
|
|
430
|
+
if (at < 0 || at > clip.duration) throw new Error(`${Math.round(at)} ms is outside clip "${clip.id}" (0\u2013${clip.duration} ms of clip time).`);
|
|
431
|
+
const remap = clip.timeRemap ?? { keys: [{ t: 0, v: 0 }, { t: clip.duration, v: clip.duration * (clip.speed ?? 1) }] };
|
|
432
|
+
const [left, right] = splitVideoAnimatable(remap, at);
|
|
433
|
+
const keys = [...left.keys.slice(0, -1), { ...left.keys[left.keys.length - 1], ease: "linear" }, ...right.keys.map((k) => ({ ...k, t: k.t + at + duration }))];
|
|
434
|
+
return { op: "set_time_remap", clip: clip.id, keys, duration: clip.duration + duration, ripple };
|
|
435
|
+
}
|
|
436
|
+
var videoSlowMotionRamp = (duration) => [
|
|
437
|
+
{ duration: Math.max(1, duration / 2), speed: 1 },
|
|
438
|
+
{ duration: 500, speed: 1, toSpeed: 0.3 },
|
|
439
|
+
{ duration: 1500, speed: 0.3 }
|
|
440
|
+
];
|
|
441
|
+
|
|
343
442
|
// src/video-editor/parse.ts
|
|
344
443
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
345
444
|
var fail = (path, what) => {
|
|
@@ -408,6 +507,18 @@ function parseVideoTransform(v, path) {
|
|
|
408
507
|
rotation: parseVideoAnimatable(v.rotation, `${path}.rotation`, { optional: true })
|
|
409
508
|
});
|
|
410
509
|
}
|
|
510
|
+
function parseVideoTimeRemap(v, speed, path) {
|
|
511
|
+
if (v === void 0) return void 0;
|
|
512
|
+
const remap = parseVideoAnimatable(v, path, { optional: true, min: 0 });
|
|
513
|
+
if (typeof remap === "number" || remap === void 0) return fail(path, "must be keyframes { keys: [{ t, v, ease }] } (v: ms of source after the in point)");
|
|
514
|
+
if (speed !== void 0 && speed !== 1) fail(path, 'cannot be used with a speed; a ramp sets the speed itself (remove "speed")');
|
|
515
|
+
try {
|
|
516
|
+
checkVideoTimeRemap(remap, path);
|
|
517
|
+
} catch (e) {
|
|
518
|
+
throw new Error(e.message.replace(/\.?$/, "."));
|
|
519
|
+
}
|
|
520
|
+
return remap;
|
|
521
|
+
}
|
|
411
522
|
function parseVideoClipAnimation(v, path) {
|
|
412
523
|
if (v === void 0) return void 0;
|
|
413
524
|
if (!isObject(v)) return fail(path, "must be an object { effect, duration }");
|
|
@@ -494,6 +605,7 @@ function parseVideoClip(v, path = "clip") {
|
|
|
494
605
|
speed: num(v.speed, `${path}.speed`, { optional: true, min: 0.05, max: 20 }),
|
|
495
606
|
volume: parseVideoAnimatable(v.volume, `${path}.volume`, { optional: true, min: 0, max: 4 }),
|
|
496
607
|
muted: bool(v.muted, `${path}.muted`),
|
|
608
|
+
timeRemap: parseVideoTimeRemap(v.timeRemap, v.speed, `${path}.timeRemap`),
|
|
497
609
|
fadeInCurve: oneOf(v.fadeInCurve, VIDEO_FADE_CURVES, `${path}.fadeInCurve`),
|
|
498
610
|
fadeOutCurve: oneOf(v.fadeOutCurve, VIDEO_FADE_CURVES, `${path}.fadeOutCurve`)
|
|
499
611
|
});
|
|
@@ -589,6 +701,7 @@ var videoClipSpeed = (clip) => clip.type === "media" ? clip.speed ?? 1 : 1;
|
|
|
589
701
|
function videoClipSourceRange(clip) {
|
|
590
702
|
if (clip.type !== "media") return null;
|
|
591
703
|
const start = clip.in ?? 0;
|
|
704
|
+
if (clip.timeRemap?.keys.length) return { in: start + clip.timeRemap.keys[0].v, out: start + clip.timeRemap.keys[clip.timeRemap.keys.length - 1].v };
|
|
592
705
|
return { in: start, out: start + clip.duration * videoClipSpeed(clip) };
|
|
593
706
|
}
|
|
594
707
|
var videoFrameDuration = (fps) => 1e3 / fps;
|
|
@@ -928,6 +1041,7 @@ var VIDEO_EDIT_OPS = [
|
|
|
928
1041
|
"extract_range",
|
|
929
1042
|
"split_at",
|
|
930
1043
|
"set_keyframes",
|
|
1044
|
+
"set_time_remap",
|
|
931
1045
|
"close_gaps",
|
|
932
1046
|
"add_marker",
|
|
933
1047
|
"update_marker",
|
|
@@ -955,11 +1069,14 @@ function mergeSpans(spans) {
|
|
|
955
1069
|
}
|
|
956
1070
|
return out;
|
|
957
1071
|
}
|
|
1072
|
+
var isRamped = (clip) => clip.type === "media" && !!clip.timeRemap;
|
|
958
1073
|
function clipAnimatables(clip) {
|
|
959
1074
|
const c = clip;
|
|
960
1075
|
const out = [
|
|
961
1076
|
{ get: () => c.opacity, set: (v) => c.opacity = v },
|
|
962
|
-
{ get: () => c.volume, set: (v) => c.volume = v }
|
|
1077
|
+
{ get: () => c.volume, set: (v) => c.volume = v },
|
|
1078
|
+
// A speed ramp is keys in clip time too, so trims and splits keep it on the same frames of source.
|
|
1079
|
+
{ get: () => c.timeRemap, set: (v) => c.timeRemap = v }
|
|
963
1080
|
];
|
|
964
1081
|
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => c.transform?.[p], set: (v) => c.transform && (c.transform[p] = v) });
|
|
965
1082
|
return out;
|
|
@@ -970,7 +1087,7 @@ function retimeClipKeys(clip, fn) {
|
|
|
970
1087
|
if (isVideoKeyframes(v)) a.set(fn(v));
|
|
971
1088
|
}
|
|
972
1089
|
}
|
|
973
|
-
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
1090
|
+
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"];
|
|
974
1091
|
var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
|
|
975
1092
|
var Editor = class {
|
|
976
1093
|
constructor(p) {
|
|
@@ -1057,7 +1174,7 @@ var Editor = class {
|
|
|
1057
1174
|
}
|
|
1058
1175
|
/** Moves a clip's head `delta` later in time (start, in point and keys); the caller sets start and duration. */
|
|
1059
1176
|
trimHead(clip, delta) {
|
|
1060
|
-
this.moveIn(clip, delta);
|
|
1177
|
+
if (!isRamped(clip)) this.moveIn(clip, delta);
|
|
1061
1178
|
retimeClipKeys(clip, (v) => retimeVideoAnimatable(v, -delta));
|
|
1062
1179
|
}
|
|
1063
1180
|
/** Cuts `clip` at `at` (inside it): the left part keeps the id, the right part is returned. */
|
|
@@ -1074,7 +1191,7 @@ var Editor = class {
|
|
|
1074
1191
|
if (isVideoKeyframes(l)) leftValues[i].set(l);
|
|
1075
1192
|
if (isVideoKeyframes(rr)) r.set(rr);
|
|
1076
1193
|
});
|
|
1077
|
-
this.moveIn(right, at - clip.start);
|
|
1194
|
+
if (!isRamped(right)) this.moveIn(right, at - clip.start);
|
|
1078
1195
|
clip.duration = at - clip.start;
|
|
1079
1196
|
delete clip.fadeOut;
|
|
1080
1197
|
delete clip.animateOut;
|
|
@@ -1286,7 +1403,7 @@ var Editor = class {
|
|
|
1286
1403
|
if (e.edge === "start") {
|
|
1287
1404
|
const delta = e.to - clip.start;
|
|
1288
1405
|
if (clip.duration - delta < minimum - VIDEO_TIME_EPSILON) refuse(`Trimming the start to ${formatVideoSeconds(e.to)} would leave "${clip.id}" shorter than a frame; it ends at ${formatVideoSeconds(end)}.`);
|
|
1289
|
-
if (clip.type === "media") {
|
|
1406
|
+
if (clip.type === "media" && !isRamped(clip)) {
|
|
1290
1407
|
const sourceIn = (clip.in ?? 0) + delta * speed;
|
|
1291
1408
|
if (sourceIn < -VIDEO_TIME_EPSILON) refuse(`"${clip.id}" starts ${Math.round((clip.in ?? 0) / speed)} ms into its source, so its start can move at most that far earlier (to ${formatVideoSeconds(clip.start - (clip.in ?? 0) / speed)}).`);
|
|
1292
1409
|
clip.in = Math.max(0, sourceIn);
|
|
@@ -1314,7 +1431,7 @@ var Editor = class {
|
|
|
1314
1431
|
} else {
|
|
1315
1432
|
const duration = e.to - clip.start;
|
|
1316
1433
|
if (duration < minimum - VIDEO_TIME_EPSILON) refuse(`Trimming the end to ${formatVideoSeconds(e.to)} would leave "${clip.id}" shorter than a frame; it starts at ${formatVideoSeconds(clip.start)}.`);
|
|
1317
|
-
if (clip.type === "media") {
|
|
1434
|
+
if (clip.type === "media" && !isRamped(clip)) {
|
|
1318
1435
|
const asset = assets.get(clip.asset);
|
|
1319
1436
|
const out = (clip.in ?? 0) + duration * speed;
|
|
1320
1437
|
if (asset?.duration !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
|
|
@@ -1370,6 +1487,8 @@ var Editor = class {
|
|
|
1370
1487
|
for (const k of ["id", "type"]) if (k in set) refuse(`A clip's ${k} cannot be changed; delete it and add a new one.`);
|
|
1371
1488
|
for (const k of e.remove ?? []) if (!OPTIONAL_CLIP_FIELDS.includes(k)) refuse(`"${k}" cannot be removed; removable fields are ${OPTIONAL_CLIP_FIELDS.join(", ")}.`);
|
|
1372
1489
|
if ("speed" in set && (typeof set.speed !== "number" || !(set.speed > 0))) refuse('"speed" must be a number greater than 0 (1 = normal speed).');
|
|
1490
|
+
if ("speed" in set && isRamped(at.clip)) refuse(`Clip "${at.clip.id}" has a speed ramp, which sets its speed; change the ramp, or remove it first.`);
|
|
1491
|
+
if ("timeRemap" in set) refuse("Set a speed ramp with set_time_remap, which checks it against the media and moves later clips if asked.");
|
|
1373
1492
|
let stretch = 1;
|
|
1374
1493
|
if (at.clip.type === "media" && !("duration" in set)) {
|
|
1375
1494
|
const was = videoClipSpeed(at.clip);
|
|
@@ -1389,6 +1508,56 @@ var Editor = class {
|
|
|
1389
1508
|
at.track.clips.sort((a, b) => a.start - b.start);
|
|
1390
1509
|
return this.outcomes.push({ op: e.op, clip: clip.id, track: at.track.id });
|
|
1391
1510
|
}
|
|
1511
|
+
case "set_time_remap": {
|
|
1512
|
+
const { track, clip } = this.clip(e.clip);
|
|
1513
|
+
this.unlocked(track);
|
|
1514
|
+
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.`);
|
|
1515
|
+
const asset = this.asset(clip.asset);
|
|
1516
|
+
if (asset.kind === "image") refuse(`Clip "${clip.id}" is a still ("${asset.id}"), so it has no motion to ramp.`);
|
|
1517
|
+
const media = clip;
|
|
1518
|
+
const end = videoClipEnd(media);
|
|
1519
|
+
const duration = e.duration ?? media.duration;
|
|
1520
|
+
if (!(duration >= 1e3 / this.p.fps - VIDEO_TIME_EPSILON)) refuse(`"duration" must be at least a frame (${Math.ceil(1e3 / this.p.fps)} ms).`);
|
|
1521
|
+
if (e.keys === null) {
|
|
1522
|
+
if (!media.timeRemap) refuse(`Clip "${clip.id}" has no speed ramp to remove.`);
|
|
1523
|
+
const from = (media.in ?? 0) + media.timeRemap.keys[0].v;
|
|
1524
|
+
if (asset.duration !== void 0 && from + duration > asset.duration + VIDEO_TIME_EPSILON)
|
|
1525
|
+
refuse(`At normal speed from ${Math.round(from)} ms, "${clip.id}" would run past the end of "${asset.id}" (${asset.duration} ms); give a duration of at most ${Math.floor(asset.duration - from)} ms.`);
|
|
1526
|
+
delete media.timeRemap;
|
|
1527
|
+
media.in = from;
|
|
1528
|
+
if (media.in < 1e-9) delete media.in;
|
|
1529
|
+
} else {
|
|
1530
|
+
if (!Array.isArray(e.keys) || !e.keys.length) refuse('"keys" must be a list of at least one { t, v, ease } (or null to remove the ramp).');
|
|
1531
|
+
const remap = parseVideoTimeRemap({ keys: e.keys }, void 0, "keys");
|
|
1532
|
+
remap.keys = sortVideoKeys(remap.keys);
|
|
1533
|
+
checkVideoTimeRemap(remap, "keys");
|
|
1534
|
+
const span = videoTimeRemapSpan(remap);
|
|
1535
|
+
if (asset.duration !== void 0 && (media.in ?? 0) + span.to > asset.duration + VIDEO_TIME_EPSILON)
|
|
1536
|
+
refuse(`The ramp plays ${Math.round(span.to)} ms of source from the in point (${media.in ?? 0} ms), past the end of "${asset.id}" (${asset.duration} ms); it can use at most ${Math.floor(asset.duration - (media.in ?? 0))} ms.`);
|
|
1537
|
+
media.timeRemap = remap;
|
|
1538
|
+
delete media.speed;
|
|
1539
|
+
}
|
|
1540
|
+
const newEnd = media.start + duration;
|
|
1541
|
+
const outcome = { op: e.op, clip: clip.id, track: track.id };
|
|
1542
|
+
if (Math.abs(newEnd - end) > VIDEO_TIME_EPSILON) {
|
|
1543
|
+
if (e.ripple) {
|
|
1544
|
+
const tracks = this.rippleTracks([track]);
|
|
1545
|
+
if (newEnd < end) {
|
|
1546
|
+
media.duration = duration;
|
|
1547
|
+
this.close(tracks, newEnd, end);
|
|
1548
|
+
} else {
|
|
1549
|
+
const created = this.open(tracks, end, newEnd - end, /* @__PURE__ */ new Set([clip.id]));
|
|
1550
|
+
media.duration = duration;
|
|
1551
|
+
if (created.length) outcome.created = created;
|
|
1552
|
+
}
|
|
1553
|
+
outcome.rippled = tracks.map((t) => t.id);
|
|
1554
|
+
} else {
|
|
1555
|
+
if (newEnd > end) this.fits(track, end, newEnd - end, /* @__PURE__ */ new Set([clip.id]), `Lengthening "${clip.id}"`);
|
|
1556
|
+
media.duration = duration;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return this.outcomes.push(outcome);
|
|
1560
|
+
}
|
|
1392
1561
|
case "set_transition": {
|
|
1393
1562
|
const { track, clip } = this.clip(e.clip);
|
|
1394
1563
|
this.unlocked(track);
|
|
@@ -1671,7 +1840,7 @@ function activeOnTrack(track, time) {
|
|
|
1671
1840
|
}
|
|
1672
1841
|
return out;
|
|
1673
1842
|
}
|
|
1674
|
-
var sourceTimeAt = (clip, time) => clip.type === "media" ? (clip
|
|
1843
|
+
var sourceTimeAt = (clip, time) => clip.type === "media" ? videoClipSourceTime(clip, time - clip.start) : 0;
|
|
1675
1844
|
function videoFrameAt(project, time) {
|
|
1676
1845
|
const assets = videoAssetMap(project);
|
|
1677
1846
|
const layers = [];
|
|
@@ -1691,7 +1860,7 @@ function videoFrameAt(project, time) {
|
|
|
1691
1860
|
const audible = videoAssetHasSound(asset);
|
|
1692
1861
|
if (audible && clip.type === "media" && !clip.muted && !track.muted) {
|
|
1693
1862
|
const gain = videoValueAt(clip.volume, clipTime, 1) * (track.volume ?? 1) * fadeGain(clip, inside, true) * mix;
|
|
1694
|
-
audio.push({ clip: clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed:
|
|
1863
|
+
audio.push({ clip: clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeedAt(clip, clipTime) });
|
|
1695
1864
|
}
|
|
1696
1865
|
if (track.kind !== "video" || track.hidden) continue;
|
|
1697
1866
|
if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
|
|
@@ -2344,6 +2513,7 @@ function parseVideoLoudnessMeasurement(output) {
|
|
|
2344
2513
|
return m;
|
|
2345
2514
|
}
|
|
2346
2515
|
var sec = (ms) => String(Math.round(ms) / 1e3);
|
|
2516
|
+
var fmtMs = (v) => String(Math.round(v * 100) / 100);
|
|
2347
2517
|
var FFMPEG_BLEND = {
|
|
2348
2518
|
multiply: "multiply",
|
|
2349
2519
|
screen: "screen",
|
|
@@ -2403,6 +2573,58 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2403
2573
|
let cur = "base";
|
|
2404
2574
|
let usesDrawtext = false;
|
|
2405
2575
|
const animated = /* @__PURE__ */ new Set();
|
|
2576
|
+
const ramped = /* @__PURE__ */ new Set();
|
|
2577
|
+
const rampedPicture = (remap, lead, end) => {
|
|
2578
|
+
const pts = videoTimeRemapBreakpoints(remap, 2e3 / fps);
|
|
2579
|
+
const first = pts[0];
|
|
2580
|
+
const last = pts[pts.length - 1];
|
|
2581
|
+
const s = `(T*1000+${fmtMs(first.v)})`;
|
|
2582
|
+
let e = fmtMs(last.t - first.t);
|
|
2583
|
+
for (let i = pts.length - 2; i >= 0; i--) {
|
|
2584
|
+
const a = pts[i];
|
|
2585
|
+
const b = pts[i + 1];
|
|
2586
|
+
if (b.v - a.v < 1e-6) continue;
|
|
2587
|
+
e = `if(lt(${s},${fmtMs(b.v)}),${fmtMs(a.t - first.t)}+(${s}-${fmtMs(a.v)})*${fmtMs((b.t - a.t) / (b.v - a.v))},${e})`;
|
|
2588
|
+
}
|
|
2589
|
+
const before = lead + first.t;
|
|
2590
|
+
const after = Math.max(0, end - last.t);
|
|
2591
|
+
return ["setpts=PTS-STARTPTS", `setpts='(${e})/1000/TB'`, `fps=${fps}`, ...before > 0.5 || after > 0.5 ? [`tpad=start_mode=clone:start_duration=${sec(before)}:stop_mode=clone:stop_duration=${sec(after + 1e3 / fps)}`] : [`tpad=stop_mode=clone:stop_duration=${sec(1e3 / fps)}`], `trim=duration=${sec(lead + end)}`];
|
|
2592
|
+
};
|
|
2593
|
+
const rampedSound = (remap, input2, output, lead, end) => {
|
|
2594
|
+
const pts = videoTimeRemapBreakpoints(remap, Math.max(80, 2e3 / fps));
|
|
2595
|
+
const first = pts[0];
|
|
2596
|
+
const last = pts[pts.length - 1];
|
|
2597
|
+
const n2 = audio.length;
|
|
2598
|
+
const fmtA = "aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo";
|
|
2599
|
+
const silence = (ms) => `anullsrc=r=48000:cl=stereo,atrim=duration=${sec(ms)},${fmtA}`;
|
|
2600
|
+
const pieces = [];
|
|
2601
|
+
const out = [];
|
|
2602
|
+
const parts = [];
|
|
2603
|
+
if (lead + first.t > 0.5) parts.push(silence(lead + first.t));
|
|
2604
|
+
const played = [];
|
|
2605
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
2606
|
+
const dt = pts[i + 1].t - pts[i].t;
|
|
2607
|
+
if (dt < 0.5) continue;
|
|
2608
|
+
const speed = (pts[i + 1].v - pts[i].v) / dt;
|
|
2609
|
+
if (speed < 0.1) parts.push(silence(dt));
|
|
2610
|
+
else {
|
|
2611
|
+
played.push({ from: pts[i].v - first.v, to: pts[i + 1].v - first.v, speed });
|
|
2612
|
+
parts.push(`#${played.length - 1}`);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
if (end - last.t > 0.5) parts.push(silence(end - last.t));
|
|
2616
|
+
if (played.length) out.push(`[${input2}]asplit=${played.length}${played.map((_, k2) => `[r${n2}s${k2}]`).join("")}`);
|
|
2617
|
+
parts.forEach((p, k2) => {
|
|
2618
|
+
const label = `[r${n2}p${k2}]`;
|
|
2619
|
+
if (p.startsWith("#")) {
|
|
2620
|
+
const piece = played[Number(p.slice(1))];
|
|
2621
|
+
out.push(`[r${n2}s${p.slice(1)}]atrim=start=${sec(piece.from)}:end=${sec(piece.to)},asetpts=PTS-STARTPTS,${atempo(piece.speed).join(",") || "anull"},${fmtA}${label}`);
|
|
2622
|
+
} else out.push(`${p}${label}`);
|
|
2623
|
+
pieces.push(label);
|
|
2624
|
+
});
|
|
2625
|
+
out.push(`${pieces.join("")}concat=n=${pieces.length}:v=0:a=1${output}`);
|
|
2626
|
+
return out;
|
|
2627
|
+
};
|
|
2406
2628
|
const blended = /* @__PURE__ */ new Set();
|
|
2407
2629
|
let k = 0;
|
|
2408
2630
|
const step = 2e3 / fps;
|
|
@@ -2415,7 +2637,10 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2415
2637
|
const src = options.resolve ? options.resolve(asset) : asset.src;
|
|
2416
2638
|
const span = clip.duration + lead + tail;
|
|
2417
2639
|
if (asset.kind === "image") inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", src]);
|
|
2418
|
-
else
|
|
2640
|
+
else if (clip.timeRemap) {
|
|
2641
|
+
const { from, to } = videoTimeRemapSpan(clip.timeRemap);
|
|
2642
|
+
inputs.push(["-ss", sec((clip.in ?? 0) + from), "-t", sec(to - from + 1e3 / fps), "-i", src]);
|
|
2643
|
+
} else inputs.push(["-ss", sec((clip.in ?? 0) - lead * speed), "-t", sec(span * speed), "-i", src]);
|
|
2419
2644
|
return inputs.length - 1;
|
|
2420
2645
|
};
|
|
2421
2646
|
for (const track of [...project.tracks].reverse()) {
|
|
@@ -2437,7 +2662,13 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2437
2662
|
const ai = index ?? input(clip, asset, lead, tail);
|
|
2438
2663
|
const trackGain = track.volume ?? 1;
|
|
2439
2664
|
const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(clip.volume ?? 1) * trackGain}`;
|
|
2440
|
-
|
|
2665
|
+
let source2 = `[${ai}:a]`;
|
|
2666
|
+
if (clip.timeRemap) {
|
|
2667
|
+
source2 = `[r${audio.length}]`;
|
|
2668
|
+
graph.push(...rampedSound(clip.timeRemap, `${ai}:a`, source2, lead, clip.duration + tail));
|
|
2669
|
+
ramped.add("sound");
|
|
2670
|
+
}
|
|
2671
|
+
const chain2 = [...clip.timeRemap ? [] : ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip))], volume];
|
|
2441
2672
|
const before2 = clip.transition ? videoClipBefore(track, clip) : null;
|
|
2442
2673
|
const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
|
|
2443
2674
|
const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
|
|
@@ -2449,7 +2680,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2449
2680
|
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
|
|
2450
2681
|
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
|
|
2451
2682
|
chain2.push(`adelay=${Math.round(t0)}:all=1`);
|
|
2452
|
-
graph.push(
|
|
2683
|
+
graph.push(`${source2}${chain2.join(",")}[a${audio.length}]`);
|
|
2453
2684
|
audio.push(`[a${audio.length}]`);
|
|
2454
2685
|
}
|
|
2455
2686
|
if (measure || track.kind !== "video" || track.hidden) continue;
|
|
@@ -2499,7 +2730,8 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2499
2730
|
if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
|
|
2500
2731
|
else {
|
|
2501
2732
|
source = `[${index}:v]`;
|
|
2502
|
-
|
|
2733
|
+
if (clip.type === "media" && clip.timeRemap) chain.push(...rampedPicture(clip.timeRemap, lead, clip.duration + tail), `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
2734
|
+
else chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
2503
2735
|
}
|
|
2504
2736
|
const grade = videoGradeFfmpegFilters(clip.grade);
|
|
2505
2737
|
if (grade.length) chain.push("format=rgba", ...grade);
|
|
@@ -2583,6 +2815,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2583
2815
|
if (animated.size)
|
|
2584
2816
|
notes.push(`Keyframed ${[...animated].sort().join(", ")} ${animated.size > 1 ? "are" : "is"} rendered as straight lines between keys, with eased segments sampled every two frames${animated.has("scale") ? "; animated scale needs ffmpeg 5 or later (scale with eval=frame)" : ""}.`);
|
|
2585
2817
|
if (blended.has("soft-light")) notes.push("ffmpeg's soft-light blend uses a slightly different formula from the browser's, so the render can differ a little from the preview.");
|
|
2818
|
+
if (ramped.has("sound")) notes.push("Sound through a speed ramp plays each stretch at its own speed, so a gliding change is heard in small steps (about every 80 ms), and freezes are silent.");
|
|
2586
2819
|
if (usesDrawtext) notes.push("Titles use drawtext, which needs an ffmpeg built with libfreetype (Homebrew's is not: check `ffmpeg -filters | grep drawtext`); pass titleImage to render them as images instead.");
|
|
2587
2820
|
const filterComplex = graph.join(";");
|
|
2588
2821
|
if (measure) {
|
|
@@ -2682,13 +2915,14 @@ var VideoPreviewEngine = class {
|
|
|
2682
2915
|
const { media } = this.entry(clip, src, kind);
|
|
2683
2916
|
active.add(clip);
|
|
2684
2917
|
const target = sourceTime / 1e3;
|
|
2685
|
-
|
|
2918
|
+
const frozen = speed < 0.0625;
|
|
2919
|
+
if (!frozen && Math.abs(media.playbackRate - speed) > 1e-6) media.playbackRate = speed;
|
|
2686
2920
|
if (gain === null) media.muted = true;
|
|
2687
2921
|
else {
|
|
2688
2922
|
media.muted = gain <= 0;
|
|
2689
2923
|
media.volume = Math.min(1, Math.max(0, gain));
|
|
2690
2924
|
}
|
|
2691
|
-
if (playing) {
|
|
2925
|
+
if (playing && !frozen) {
|
|
2692
2926
|
if (media.paused || Math.abs(media.currentTime - target) > tolerance) media.currentTime = target;
|
|
2693
2927
|
if (media.paused) void Promise.resolve(media.play()).catch(() => {
|
|
2694
2928
|
});
|
|
@@ -3671,6 +3905,83 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3671
3905
|
return apply(videoRemoveRangesEdits(merged, tracks), merged.length > 1 ? `Cut ${merged.length} stretches` : "Cut stretch", (r) => ({ removed: merged.length, time: round(before - videoProjectDuration(r.project)), duration: videoProjectDuration(r.project) }));
|
|
3672
3906
|
}
|
|
3673
3907
|
};
|
|
3908
|
+
const rampTarget = (id) => {
|
|
3909
|
+
const at = findVideoClip(project(), id);
|
|
3910
|
+
if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
|
|
3911
|
+
const { clip } = at;
|
|
3912
|
+
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.`);
|
|
3913
|
+
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.`);
|
|
3914
|
+
return clip;
|
|
3915
|
+
};
|
|
3916
|
+
const rampOut = (r, id) => {
|
|
3917
|
+
const outcome2 = r.outcomes[r.outcomes.length - 1];
|
|
3918
|
+
return { clip: clipOut(r.project, id), ...outcome2.rippled ? { rippled: outcome2.rippled } : {}, duration: videoProjectDuration(r.project) };
|
|
3919
|
+
};
|
|
3920
|
+
const RIPPLE_RAMP = { type: "boolean", description: "Move later material on every sync-locked track by the change in length (default true); false refuses if the clip would overlap the next one." };
|
|
3921
|
+
const setSpeedRamp = {
|
|
3922
|
+
name: "set_video_speed_ramp",
|
|
3923
|
+
title: "Ramp the speed",
|
|
3924
|
+
description: `Plays a video or audio clip through stretches of speed, from its first frame: e.g. [{ duration: 1500, speed: 1 }, { duration: 500, speed: 1, toSpeed: 0.25 }, { duration: 2000, speed: 0.25 }] for a slow-motion ramp. "toSpeed" glides evenly to a new speed over the stretch; speed 0 is a freeze frame. The clip's length becomes the stretches' total (one undo step). Replaces any ramp or speed it had. Returns the clip (with its timeRemap keys and source range).`,
|
|
3925
|
+
inputSchema: object(
|
|
3926
|
+
{
|
|
3927
|
+
clip: CLIP_ID,
|
|
3928
|
+
segments: {
|
|
3929
|
+
type: "array",
|
|
3930
|
+
minItems: 1,
|
|
3931
|
+
maxItems: 100,
|
|
3932
|
+
description: "The stretches in order, from the clip's start.",
|
|
3933
|
+
items: object(
|
|
3934
|
+
{
|
|
3935
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "Length of the stretch on the timeline, in ms." },
|
|
3936
|
+
speed: { type: "number", minimum: 0, maximum: 20, description: "Speed at its start: 1 normal, 0.5 half speed (slow motion), 2 double, 0 a freeze frame." },
|
|
3937
|
+
toSpeed: { type: "number", minimum: 0, maximum: 20, description: "Glide evenly to this speed by the stretch's end (default: stay at speed). A ramp between two speeds." }
|
|
3938
|
+
},
|
|
3939
|
+
["duration", "speed"],
|
|
3940
|
+
"One stretch."
|
|
3941
|
+
)
|
|
3942
|
+
},
|
|
3943
|
+
ripple: RIPPLE_RAMP
|
|
3944
|
+
},
|
|
3945
|
+
["clip", "segments"]
|
|
3946
|
+
),
|
|
3947
|
+
annotations: { title: "Ramp the speed", ...WRITE_IDEMPOTENT },
|
|
3948
|
+
handler: ({ clip, segments, ripple }) => {
|
|
3949
|
+
const edit = videoSpeedRampEdit(rampTarget(clip), segments, ripple ?? true);
|
|
3950
|
+
return apply(edit, "Speed ramp", (r) => ({ ...rampOut(r, clip), source: Math.round(videoSpeedRampKeys(segments).source) }));
|
|
3951
|
+
}
|
|
3952
|
+
};
|
|
3953
|
+
const addFreeze = {
|
|
3954
|
+
name: "add_video_freeze_frame",
|
|
3955
|
+
title: "Add a freeze frame",
|
|
3956
|
+
description: "Holds the frame a video clip shows at a timeline time for a while, then plays on (one undo step): the clip gets longer by the hold, and with ripple (default) everything after it on sync-locked tracks moves along. Works on clips with or without a speed ramp. Returns the clip.",
|
|
3957
|
+
inputSchema: object(
|
|
3958
|
+
{
|
|
3959
|
+
clip: CLIP_ID,
|
|
3960
|
+
at: MS("Timeline time of the frame to hold (inside the clip)"),
|
|
3961
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "How long to hold it, in ms." },
|
|
3962
|
+
ripple: RIPPLE_RAMP
|
|
3963
|
+
},
|
|
3964
|
+
["clip", "at", "duration"]
|
|
3965
|
+
),
|
|
3966
|
+
annotations: { title: "Add a freeze frame", ...WRITE },
|
|
3967
|
+
handler: ({ clip, at, duration, ripple }) => {
|
|
3968
|
+
const c = rampTarget(clip);
|
|
3969
|
+
const t = at - c.start;
|
|
3970
|
+
if (t < 0 || t > c.duration) throw new Error(`${formatVideoSeconds(at)} is outside clip "${clip}" (${formatVideoSeconds(c.start)}\u2013${formatVideoSeconds(videoClipEnd(c))}); pick a time inside it.`);
|
|
3971
|
+
return apply(videoFreezeFrameEdit(c, t, duration, ripple ?? true), "Freeze frame", (r) => rampOut(r, clip));
|
|
3972
|
+
}
|
|
3973
|
+
};
|
|
3974
|
+
const removeRamp = {
|
|
3975
|
+
name: "remove_video_speed_ramp",
|
|
3976
|
+
title: "Remove a speed ramp",
|
|
3977
|
+
description: 'Puts a ramped clip back to normal speed from the moment its ramp started (one undo step), keeping its length unless "duration" is given. Returns the clip.',
|
|
3978
|
+
inputSchema: object({ clip: CLIP_ID, duration: { type: "number", exclusiveMinimum: 0, description: "New length in ms (default: unchanged)." }, ripple: RIPPLE_RAMP }, ["clip"]),
|
|
3979
|
+
annotations: { title: "Remove a speed ramp", ...WRITE },
|
|
3980
|
+
handler: ({ clip, duration, ripple }) => {
|
|
3981
|
+
rampTarget(clip);
|
|
3982
|
+
return apply({ op: "set_time_remap", clip, keys: null, ...duration !== void 0 ? { duration } : {}, ripple: ripple ?? true }, "Remove speed ramp", (r) => rampOut(r, clip));
|
|
3983
|
+
}
|
|
3984
|
+
};
|
|
3674
3985
|
const setTransition = {
|
|
3675
3986
|
name: "set_video_transition",
|
|
3676
3987
|
title: "Set a transition",
|
|
@@ -3781,7 +4092,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3781
4092
|
};
|
|
3782
4093
|
const EDIT_OP = {
|
|
3783
4094
|
type: "object",
|
|
3784
|
-
description: `One edit: { "op": one of ${VIDEO_EDIT_OPS.filter((o) => o !== "replace_project").join(", ")}, \u2026its fields }. Fields match the single tools: add_clip { track, clip, mode, ripple }, move_clip { clip, start, track, mode }, trim_clip { clip, edge, to, ripple }, split_clip { clip, at, id }, delete_clips { clips, ripple }, update_clip { clip, set, remove }, set_transition { clip, transition }, set_keyframes { clip, property, keys (null removes) }, slip_clip { clip, delta }, roll_edit { left, right, delta }, slide_clip { clip, delta }, lift_range { from, to, tracks }, extract_range { from, to, tracks }, split_at { at, tracks }, close_gaps { track, from }, add_asset { asset }, update_asset { asset, set, remove }, remove_asset { asset }, add_track { track: { kind, id, \u2026 }, at }, update_track { track, set, remove }, remove_track { track }, move_track { track, to }, add_marker { marker: { time, label } }, update_marker { marker, set }, remove_marker { marker }, set_settings { name, width, height, fps, background }.`,
|
|
4095
|
+
description: `One edit: { "op": one of ${VIDEO_EDIT_OPS.filter((o) => o !== "replace_project").join(", ")}, \u2026its fields }. Fields match the single tools: add_clip { track, clip, mode, ripple }, move_clip { clip, start, track, mode }, trim_clip { clip, edge, to, ripple }, split_clip { clip, at, id }, delete_clips { clips, ripple }, update_clip { clip, set, remove }, set_transition { clip, transition }, set_keyframes { clip, property, keys (null removes) }, set_time_remap { clip, keys (clip ms \u2192 source ms after the in point; null removes), duration, ripple }, slip_clip { clip, delta }, roll_edit { left, right, delta }, slide_clip { clip, delta }, lift_range { from, to, tracks }, extract_range { from, to, tracks }, split_at { at, tracks }, close_gaps { track, from }, add_asset { asset }, update_asset { asset, set, remove }, remove_asset { asset }, add_track { track: { kind, id, \u2026 }, at }, update_track { track, set, remove }, remove_track { track }, move_track { track, to }, add_marker { marker: { time, label } }, update_marker { marker, set }, remove_marker { marker }, set_settings { name, width, height, fps, background }.`,
|
|
3785
4096
|
properties: { op: { type: "string", enum: VIDEO_EDIT_OPS.filter((o) => o !== "replace_project"), description: "Which edit." } },
|
|
3786
4097
|
required: ["op"]
|
|
3787
4098
|
};
|
|
@@ -3889,7 +4200,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3889
4200
|
}
|
|
3890
4201
|
};
|
|
3891
4202
|
const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
|
|
3892
|
-
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setTranscript, removeRanges, applyGradePreset, copyGrade, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
4203
|
+
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
3893
4204
|
return {
|
|
3894
4205
|
id: options.id ?? "video",
|
|
3895
4206
|
namespace: options.namespace,
|
|
@@ -4163,6 +4474,7 @@ export {
|
|
|
4163
4474
|
applyVideoGrade,
|
|
4164
4475
|
buildVideoAudioPeaks,
|
|
4165
4476
|
checkVideoProject,
|
|
4477
|
+
checkVideoTimeRemap,
|
|
4166
4478
|
createVideoEditorToolProvider,
|
|
4167
4479
|
createVideoProject,
|
|
4168
4480
|
decodeVideoAudioPeaks,
|
|
@@ -4193,6 +4505,7 @@ export {
|
|
|
4193
4505
|
parseVideoLoudnessMeasurement,
|
|
4194
4506
|
parseVideoMarker,
|
|
4195
4507
|
parseVideoProject,
|
|
4508
|
+
parseVideoTimeRemap,
|
|
4196
4509
|
parseVideoTrack,
|
|
4197
4510
|
parseVideoTranscript,
|
|
4198
4511
|
parseVideoTranscriptWords,
|
|
@@ -4233,7 +4546,9 @@ export {
|
|
|
4233
4546
|
videoClipShowsWaveform,
|
|
4234
4547
|
videoClipSoundGain,
|
|
4235
4548
|
videoClipSourceRange,
|
|
4549
|
+
videoClipSourceTime,
|
|
4236
4550
|
videoClipSpeed,
|
|
4551
|
+
videoClipSpeedAt,
|
|
4237
4552
|
videoClipTint,
|
|
4238
4553
|
videoClipWaveform,
|
|
4239
4554
|
videoClipWaveformPath,
|
|
@@ -4248,6 +4563,7 @@ export {
|
|
|
4248
4563
|
videoFrameDuration,
|
|
4249
4564
|
videoFrameToSvg,
|
|
4250
4565
|
videoFrames,
|
|
4566
|
+
videoFreezeFrameEdit,
|
|
4251
4567
|
videoGradeCssFilter,
|
|
4252
4568
|
videoGradeFfmpegFilters,
|
|
4253
4569
|
videoGradeGainsFilterId,
|
|
@@ -4272,8 +4588,13 @@ export {
|
|
|
4272
4588
|
videoProjectSvg,
|
|
4273
4589
|
videoRemoveRangesEdits,
|
|
4274
4590
|
videoRulerTicks,
|
|
4591
|
+
videoSlowMotionRamp,
|
|
4275
4592
|
videoSnapPoints,
|
|
4593
|
+
videoSpeedRampEdit,
|
|
4594
|
+
videoSpeedRampKeys,
|
|
4276
4595
|
videoTextStyle,
|
|
4596
|
+
videoTimeRemapBreakpoints,
|
|
4597
|
+
videoTimeRemapSpan,
|
|
4277
4598
|
videoTimecodeFrames,
|
|
4278
4599
|
videoTimelineTranscript,
|
|
4279
4600
|
videoTitleFrame,
|