@pixerate/editor 0.85.0 → 0.87.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 +879 -101
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +313 -1
- package/dist/video-editor/index.d.ts +313 -1
- package/dist/video-editor/index.js +800 -22
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-video-editor/SKILL.md +24 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4;
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5;
|
|
2
2
|
|
|
3
3
|
var _chunkWAJQKWQUcjs = require('../chunk-WAJQKWQU.cjs');
|
|
4
4
|
|
|
@@ -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 = _nullishCoalesce(clip.in, () => ( 0));
|
|
346
|
+
return clip.timeRemap ? base + videoValueAt(clip.timeRemap, clipTime, 0) : base + clipTime * (_nullishCoalesce(clip.speed, () => ( 1)));
|
|
347
|
+
}
|
|
348
|
+
function videoClipSpeedAt(clip, clipTime) {
|
|
349
|
+
if (clip.type !== "media") return 1;
|
|
350
|
+
if (!clip.timeRemap) return _nullishCoalesce(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 = _nullishCoalesce(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) - (_nullishCoalesce(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 = _nullishCoalesce(clip.timeRemap, () => ( { keys: [{ t: 0, v: 0 }, { t: clip.duration, v: clip.duration * (_nullishCoalesce(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" ? _nullishCoalesce(clip.spe
|
|
|
589
701
|
function videoClipSourceRange(clip) {
|
|
590
702
|
if (clip.type !== "media") return null;
|
|
591
703
|
const start = _nullishCoalesce(clip.in, () => ( 0));
|
|
704
|
+
if (_optionalChain([clip, 'access', _2 => _2.timeRemap, 'optionalAccess', _3 => _3.keys, 'access', _4 => _4.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;
|
|
@@ -669,13 +782,13 @@ function videoClipHandles(clip, assets) {
|
|
|
669
782
|
const speed = videoClipSpeed(clip);
|
|
670
783
|
const range = videoClipSourceRange(clip);
|
|
671
784
|
const before = range.in / speed;
|
|
672
|
-
if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess',
|
|
785
|
+
if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess', _5 => _5.kind]) === "image" ? Infinity : before, after: Infinity };
|
|
673
786
|
return { before, after: Math.max(0, (asset.duration - range.out) / speed) };
|
|
674
787
|
}
|
|
675
788
|
function videoClipOverhang(track, clip) {
|
|
676
789
|
const lead = clip.transition && clip.transition.type !== "dip" && videoClipBefore(track, clip) ? clip.transition.duration / 2 : 0;
|
|
677
790
|
const next = videoClipAfter(track, clip);
|
|
678
|
-
const tail = _optionalChain([next, 'optionalAccess',
|
|
791
|
+
const tail = _optionalChain([next, 'optionalAccess', _6 => _6.transition]) && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
|
|
679
792
|
return { lead, tail };
|
|
680
793
|
}
|
|
681
794
|
function videoSnapPoints(project, options = {}) {
|
|
@@ -759,7 +872,7 @@ function videoClipAnimated(clip, project) {
|
|
|
759
872
|
const spans = videoClipAnimationSpans(clip);
|
|
760
873
|
const out = { ...clip, transform: { ...clip.transform } };
|
|
761
874
|
for (const prop of PROPS) {
|
|
762
|
-
const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access',
|
|
875
|
+
const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _7 => _7.transform, 'optionalAccess', _8 => _8[prop]]);
|
|
763
876
|
if (isVideoKeyframes(current)) continue;
|
|
764
877
|
const base = _nullishCoalesce(current, () => ( (prop === "opacity" || prop === "scale" ? 1 : 0)));
|
|
765
878
|
const away = (spec) => {
|
|
@@ -796,7 +909,7 @@ function videoClipKeyframes(clip) {
|
|
|
796
909
|
const out = [
|
|
797
910
|
{ property: "opacity", keys: videoKeysOf(clip.opacity) },
|
|
798
911
|
{ property: "volume", keys: clip.type === "media" ? videoKeysOf(clip.volume) : [] },
|
|
799
|
-
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access',
|
|
912
|
+
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _9 => _9.transform, 'optionalAccess', _10 => _10[p]])) }))
|
|
800
913
|
];
|
|
801
914
|
return out.filter((o) => o.keys.length);
|
|
802
915
|
}
|
|
@@ -853,7 +966,7 @@ function checkVideoProject(project) {
|
|
|
853
966
|
for (const which of ["animateIn", "animateOut"]) {
|
|
854
967
|
const a = clip[which];
|
|
855
968
|
if (!a) continue;
|
|
856
|
-
const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access',
|
|
969
|
+
const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _11 => _11.transform, 'optionalAccess', _12 => _12[p]])).length);
|
|
857
970
|
if (keyed.length)
|
|
858
971
|
issues.push({ code: "animation-keyed", severity: "note", ...at, message: `Clip "${clip.id}"'s ${a.effect} ${which === "animateIn" ? "entrance" : "exit"} leaves ${keyed.join(", ")} alone, because the clip keys ${keyed.length > 1 ? "them" : "it"} itself; remove those keyframes or the animation.` });
|
|
859
972
|
}
|
|
@@ -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,13 +1069,16 @@ 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
|
-
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access',
|
|
1081
|
+
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _13 => _13.transform, 'optionalAccess', _14 => _14[p]]), set: (v) => c.transform && (c.transform[p] = v) });
|
|
965
1082
|
return out;
|
|
966
1083
|
}
|
|
967
1084
|
function retimeClipKeys(clip, fn) {
|
|
@@ -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 = class {
|
|
976
1093
|
constructor(p) {;_class.prototype.__init.call(this);
|
|
@@ -1009,7 +1126,7 @@ var Editor = (_class = class {
|
|
|
1009
1126
|
kindFits(track, clip) {
|
|
1010
1127
|
const asset = clip.type === "media" ? this.asset(clip.asset) : null;
|
|
1011
1128
|
if (track.kind === "audio" && (clip.type !== "media" || asset.kind === "image")) refuse(`Track "${track.id}" is an audio track; ${clip.type === "media" ? "images" : `${clip.type} clips`} go on a video track.`);
|
|
1012
|
-
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess',
|
|
1129
|
+
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _15 => _15.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
|
|
1013
1130
|
}
|
|
1014
1131
|
/** Moves every clip on `track` starting at or after `from` by `delta`. */
|
|
1015
1132
|
shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
|
|
@@ -1051,13 +1168,13 @@ var Editor = (_class = class {
|
|
|
1051
1168
|
}
|
|
1052
1169
|
/** Moves a media clip's in point by `delta` of timeline time (stills, titles and colours have none). */
|
|
1053
1170
|
moveIn(clip, delta) {
|
|
1054
|
-
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call',
|
|
1171
|
+
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _16 => _16(this.p), 'access', _17 => _17.get, 'call', _18 => _18(clip.asset), 'optionalAccess', _19 => _19.kind]) === "image") return;
|
|
1055
1172
|
clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * videoClipSpeed(clip));
|
|
1056
1173
|
if (clip.in < 1e-9) delete clip.in;
|
|
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 = 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 = 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 = (_nullishCoalesce(clip.in, () => ( 0))) + delta * speed;
|
|
1291
1408
|
if (sourceIn < -VIDEO_TIME_EPSILON) refuse(`"${clip.id}" starts ${Math.round((_nullishCoalesce(clip.in, () => ( 0))) / speed)} ms into its source, so its start can move at most that far earlier (to ${formatVideoSeconds(clip.start - (_nullishCoalesce(clip.in, () => ( 0))) / speed)}).`);
|
|
1292
1409
|
clip.in = Math.max(0, sourceIn);
|
|
@@ -1314,10 +1431,10 @@ var Editor = (_class = 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 = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
|
|
1320
|
-
if (_optionalChain([asset, 'optionalAccess',
|
|
1437
|
+
if (_optionalChain([asset, 'optionalAccess', _20 => _20.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
|
|
1321
1438
|
refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
|
|
1322
1439
|
}
|
|
1323
1440
|
if (e.ripple) {
|
|
@@ -1370,10 +1487,12 @@ var Editor = (_class = 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 _nullishCoalesce(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);
|
|
1376
|
-
const now = "speed" in set ? set.speed : _optionalChain([e, 'access',
|
|
1495
|
+
const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _21 => _21.remove, 'optionalAccess', _22 => _22.includes, 'call', _23 => _23("speed")]) ? 1 : was;
|
|
1377
1496
|
if (now !== was) {
|
|
1378
1497
|
set.duration = at.clip.duration * was / now;
|
|
1379
1498
|
stretch = was / now;
|
|
@@ -1389,6 +1508,56 @@ var Editor = (_class = 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 = _nullishCoalesce(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 = (_nullishCoalesce(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 && (_nullishCoalesce(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 (${_nullishCoalesce(media.in, () => ( 0))} ms), past the end of "${asset.id}" (${asset.duration} ms); it can use at most ${Math.floor(asset.duration - (_nullishCoalesce(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);
|
|
@@ -1469,7 +1638,7 @@ var Editor = (_class = class {
|
|
|
1469
1638
|
this.trimHead(after, delta);
|
|
1470
1639
|
}
|
|
1471
1640
|
track.clips.sort((a, b) => a.start - b.start);
|
|
1472
|
-
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess',
|
|
1641
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _24 => _24.id]), clip.id, _optionalChain([after, 'optionalAccess', _25 => _25.id])].filter((x) => !!x), track: track.id, delta });
|
|
1473
1642
|
}
|
|
1474
1643
|
case "lift_range":
|
|
1475
1644
|
case "extract_range": {
|
|
@@ -1500,7 +1669,7 @@ var Editor = (_class = class {
|
|
|
1500
1669
|
const limits = VIDEO_KEYFRAME_LIMITS[e.property];
|
|
1501
1670
|
const transform = VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
|
|
1502
1671
|
const c = clip;
|
|
1503
|
-
const current = transform ? _optionalChain([c, 'access',
|
|
1672
|
+
const current = transform ? _optionalChain([c, 'access', _26 => _26.transform, 'optionalAccess', _27 => _27[e.property]]) : c[e.property];
|
|
1504
1673
|
let value;
|
|
1505
1674
|
if (e.keys === null) {
|
|
1506
1675
|
const v = videoValueAt(current, 0, limits.default);
|
|
@@ -1582,7 +1751,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1582
1751
|
editor.run(edit);
|
|
1583
1752
|
} catch (error) {
|
|
1584
1753
|
const message = error.message;
|
|
1585
|
-
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess',
|
|
1754
|
+
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _28 => _28.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
|
|
1586
1755
|
}
|
|
1587
1756
|
}
|
|
1588
1757
|
let next;
|
|
@@ -1601,7 +1770,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1601
1770
|
|
|
1602
1771
|
// src/video-editor/evaluate.ts
|
|
1603
1772
|
function videoTransformAt(transform, clipTime = 0) {
|
|
1604
|
-
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess',
|
|
1773
|
+
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _29 => _29.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _30 => _30.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _31 => _31.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _32 => _32.rotation]), clipTime, 0) };
|
|
1605
1774
|
}
|
|
1606
1775
|
function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
|
|
1607
1776
|
let w = size.width;
|
|
@@ -1648,8 +1817,8 @@ function videoFadeCurveGain(curve, x) {
|
|
|
1648
1817
|
function fadeGain(clip, time, sound = false) {
|
|
1649
1818
|
let g = 1;
|
|
1650
1819
|
const shape = sound && clip.type === "media" ? clip : void 0;
|
|
1651
|
-
if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess',
|
|
1652
|
-
if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess',
|
|
1820
|
+
if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _33 => _33.fadeInCurve]), (time - clip.start) / clip.fadeIn);
|
|
1821
|
+
if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _34 => _34.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
|
|
1653
1822
|
return g;
|
|
1654
1823
|
}
|
|
1655
1824
|
function videoClipSoundGain(clip, clipTime) {
|
|
@@ -1662,7 +1831,7 @@ function activeOnTrack(track, time) {
|
|
|
1662
1831
|
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
1663
1832
|
const lead = before ? clip.transition.duration / 2 : 0;
|
|
1664
1833
|
const next = videoClipAfter(track, clip);
|
|
1665
|
-
const tail = _optionalChain([next, 'optionalAccess',
|
|
1834
|
+
const tail = _optionalChain([next, 'optionalAccess', _35 => _35.transition]) ? next.transition.duration / 2 : 0;
|
|
1666
1835
|
if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
|
|
1667
1836
|
const a = { clip };
|
|
1668
1837
|
if (lead && time < clip.start + lead) a.incoming = { progress: clamp01((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
|
|
@@ -1671,7 +1840,7 @@ function activeOnTrack(track, time) {
|
|
|
1671
1840
|
}
|
|
1672
1841
|
return out;
|
|
1673
1842
|
}
|
|
1674
|
-
var sourceTimeAt = (clip, time) => clip.type === "media" ? (
|
|
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 = [];
|
|
@@ -1679,8 +1848,8 @@ function videoFrameAt(project, time) {
|
|
|
1679
1848
|
for (let ti = project.tracks.length - 1; ti >= 0; ti--) {
|
|
1680
1849
|
const track = project.tracks[ti];
|
|
1681
1850
|
for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
|
|
1682
|
-
if (_optionalChain([incoming, 'optionalAccess',
|
|
1683
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1851
|
+
if (_optionalChain([incoming, 'optionalAccess', _36 => _36.type]) === "dip" && incoming.progress < 0.5) continue;
|
|
1852
|
+
if (_optionalChain([outgoing, 'optionalAccess', _37 => _37.type]) === "dip" && outgoing.progress >= 0.5) continue;
|
|
1684
1853
|
const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
|
|
1685
1854
|
const fade = fadeGain(clip, inside);
|
|
1686
1855
|
const level = (t, transition, into) => videoFadeCurveGain(videoTransitionAudioCurve(transition), t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress);
|
|
@@ -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) * (_nullishCoalesce(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;
|
|
@@ -1711,8 +1880,8 @@ function videoFrameAt(project, time) {
|
|
|
1711
1880
|
shift = videoTransitionEase(incoming.progress) - 1;
|
|
1712
1881
|
} else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
|
|
1713
1882
|
}
|
|
1714
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1715
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1883
|
+
if (_optionalChain([outgoing, 'optionalAccess', _38 => _38.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
|
|
1884
|
+
if (_optionalChain([outgoing, 'optionalAccess', _39 => _39.type]) === "push") {
|
|
1716
1885
|
shiftDirection = _nullishCoalesce(outgoing.next.transition.direction, () => ( "right"));
|
|
1717
1886
|
shift = videoTransitionEase(outgoing.progress);
|
|
1718
1887
|
}
|
|
@@ -1722,7 +1891,7 @@ function videoFrameAt(project, time) {
|
|
|
1722
1891
|
const across = shiftDirection === "left" || shiftDirection === "right";
|
|
1723
1892
|
const sign = shiftDirection === "left" || shiftDirection === "up" ? -1 : 1;
|
|
1724
1893
|
const rect = shift ? { ...placed, ...across ? { x: placed.x + sign * shift * project.width } : { y: placed.y + sign * shift * project.height } } : placed;
|
|
1725
|
-
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access',
|
|
1894
|
+
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access', _40 => _40.transform, 'optionalAccess', _41 => _41.rotation]), clipTime, 0) };
|
|
1726
1895
|
if (wipe) layer.wipe = wipe;
|
|
1727
1896
|
if (iris) layer.iris = iris;
|
|
1728
1897
|
if (clip.blend && clip.blend !== "normal") layer.blend = clip.blend;
|
|
@@ -1955,6 +2124,379 @@ var VideoAudioPeaksCache = (_class2 = class {
|
|
|
1955
2124
|
}
|
|
1956
2125
|
}, _class2);
|
|
1957
2126
|
|
|
2127
|
+
// src/video-editor/scopes.ts
|
|
2128
|
+
var VIDEO_SCOPE_KINDS = ["histogram", "waveform", "vectorscope"];
|
|
2129
|
+
var videoLuma = (r, g, b) => 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
2130
|
+
function videoChroma(r, g, b) {
|
|
2131
|
+
const y = videoLuma(r, g, b);
|
|
2132
|
+
return [(b - y) / 1.8556, (r - y) / 1.5748];
|
|
2133
|
+
}
|
|
2134
|
+
function eachPixel(pixels, maxPixels, visit) {
|
|
2135
|
+
const { width, height, data } = pixels;
|
|
2136
|
+
if (!(width > 0 && height > 0) || data.length < width * height * 4) throw new Error(`The pixels are not ${width} \xD7 ${height} RGBA: there are ${data.length} values, and ${width * height * 4} are needed.`);
|
|
2137
|
+
const step = Math.max(1, Math.sqrt(width * height / Math.max(1, _nullishCoalesce(maxPixels, () => ( 262144)))));
|
|
2138
|
+
let count = 0;
|
|
2139
|
+
for (let fy = step / 2; fy < height; fy += step) {
|
|
2140
|
+
const y = Math.floor(fy);
|
|
2141
|
+
for (let fx = step / 2; fx < width; fx += step) {
|
|
2142
|
+
const x = Math.floor(fx);
|
|
2143
|
+
const i = (y * width + x) * 4;
|
|
2144
|
+
visit(data[i], data[i + 1], data[i + 2], x, y);
|
|
2145
|
+
count++;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return count;
|
|
2149
|
+
}
|
|
2150
|
+
var binOf = (v, bins) => Math.min(bins - 1, Math.floor((v + 1e-6) / 256 * bins));
|
|
2151
|
+
function videoHistogram(pixels, options = {}) {
|
|
2152
|
+
const bins = Math.max(1, Math.min(256, Math.round(_nullishCoalesce(options.bins, () => ( 256)))));
|
|
2153
|
+
const out = { bins, r: Array(bins).fill(0), g: Array(bins).fill(0), b: Array(bins).fill(0), luma: Array(bins).fill(0), total: 0 };
|
|
2154
|
+
out.total = eachPixel(pixels, options.maxPixels, (r, g, b) => {
|
|
2155
|
+
out.r[binOf(r, bins)]++;
|
|
2156
|
+
out.g[binOf(g, bins)]++;
|
|
2157
|
+
out.b[binOf(b, bins)]++;
|
|
2158
|
+
out.luma[binOf(videoLuma(r, g, b), bins)]++;
|
|
2159
|
+
});
|
|
2160
|
+
return out;
|
|
2161
|
+
}
|
|
2162
|
+
function videoLumaWaveform(pixels, options = {}) {
|
|
2163
|
+
const columns = Math.max(1, Math.min(pixels.width, Math.round(_nullishCoalesce(options.columns, () => ( Math.min(pixels.width, 256))))));
|
|
2164
|
+
const levels = Math.max(2, Math.min(256, Math.round(_nullishCoalesce(options.levels, () => ( 128)))));
|
|
2165
|
+
const data = new Uint32Array(columns * levels);
|
|
2166
|
+
let peak = 0;
|
|
2167
|
+
eachPixel(pixels, options.maxPixels, (r, g, b, x) => {
|
|
2168
|
+
const c = Math.min(columns - 1, Math.floor(x / pixels.width * columns));
|
|
2169
|
+
const i = c * levels + binOf(videoLuma(r, g, b), levels);
|
|
2170
|
+
if (++data[i] > peak) peak = data[i];
|
|
2171
|
+
});
|
|
2172
|
+
return { columns, levels, data, peak };
|
|
2173
|
+
}
|
|
2174
|
+
var videoVectorscopePoint = (cb, cr, size) => [(cb + 0.5) * (size - 1), (0.5 - cr) * (size - 1)];
|
|
2175
|
+
function videoVectorscope(pixels, options = {}) {
|
|
2176
|
+
const size = Math.max(16, Math.min(512, Math.round(_nullishCoalesce(options.size, () => ( 128)))));
|
|
2177
|
+
const data = new Uint32Array(size * size);
|
|
2178
|
+
let peak = 0;
|
|
2179
|
+
eachPixel(pixels, options.maxPixels, (r, g, b) => {
|
|
2180
|
+
const [cb, cr] = videoChroma(r / 255, g / 255, b / 255);
|
|
2181
|
+
const [x, y] = videoVectorscopePoint(cb, cr, size);
|
|
2182
|
+
const i = Math.round(y) * size + Math.round(x);
|
|
2183
|
+
if (++data[i] > peak) peak = data[i];
|
|
2184
|
+
});
|
|
2185
|
+
return { size, data, peak };
|
|
2186
|
+
}
|
|
2187
|
+
var round3 = (v) => Math.round(v * 1e3) / 1e3;
|
|
2188
|
+
var hex2 = (v) => Math.round(Math.min(255, Math.max(0, v))).toString(16).padStart(2, "0");
|
|
2189
|
+
function videoExposureStats(pixels, options = {}) {
|
|
2190
|
+
const low = (_nullishCoalesce(options.shadowLevel, () => ( 0.02))) * 255;
|
|
2191
|
+
const high = (_nullishCoalesce(options.highlightLevel, () => ( 0.98))) * 255;
|
|
2192
|
+
const lumaBins = new Uint32Array(256);
|
|
2193
|
+
let sumY = 0;
|
|
2194
|
+
let sumR = 0;
|
|
2195
|
+
let sumG = 0;
|
|
2196
|
+
let sumB = 0;
|
|
2197
|
+
let sumSat = 0;
|
|
2198
|
+
let maxSat = 0;
|
|
2199
|
+
let shadows = 0;
|
|
2200
|
+
let highlights = 0;
|
|
2201
|
+
let cr = 0;
|
|
2202
|
+
let cg = 0;
|
|
2203
|
+
let cbl = 0;
|
|
2204
|
+
const total = eachPixel(pixels, options.maxPixels, (r, g, b) => {
|
|
2205
|
+
const y = videoLuma(r, g, b);
|
|
2206
|
+
lumaBins[Math.min(255, Math.round(y))]++;
|
|
2207
|
+
sumY += y;
|
|
2208
|
+
sumR += r;
|
|
2209
|
+
sumG += g;
|
|
2210
|
+
sumB += b;
|
|
2211
|
+
const [u, v] = videoChroma(r / 255, g / 255, b / 255);
|
|
2212
|
+
const sat = Math.min(1, Math.hypot(u, v) / 0.5);
|
|
2213
|
+
sumSat += sat;
|
|
2214
|
+
if (sat > maxSat) maxSat = sat;
|
|
2215
|
+
if (y <= low) shadows++;
|
|
2216
|
+
if (y >= high) highlights++;
|
|
2217
|
+
if (r >= high) cr++;
|
|
2218
|
+
if (g >= high) cg++;
|
|
2219
|
+
if (b >= high) cbl++;
|
|
2220
|
+
});
|
|
2221
|
+
const percentile = (p) => {
|
|
2222
|
+
const target = p * total;
|
|
2223
|
+
let seen = 0;
|
|
2224
|
+
for (let i = 0; i < 256; i++) {
|
|
2225
|
+
seen += lumaBins[i];
|
|
2226
|
+
if (seen >= target && seen > 0) return i / 255;
|
|
2227
|
+
}
|
|
2228
|
+
return 1;
|
|
2229
|
+
};
|
|
2230
|
+
let min = 0;
|
|
2231
|
+
while (min < 255 && !lumaBins[min]) min++;
|
|
2232
|
+
let max = 255;
|
|
2233
|
+
while (max > 0 && !lumaBins[max]) max--;
|
|
2234
|
+
return {
|
|
2235
|
+
pixels: total,
|
|
2236
|
+
luma: { mean: round3(sumY / total / 255), median: round3(percentile(0.5)), min: round3(min / 255), max: round3(max / 255), p1: round3(percentile(0.01)), p99: round3(percentile(0.99)) },
|
|
2237
|
+
clipped: { shadows: round3(shadows / total), highlights: round3(highlights / total), r: round3(cr / total), g: round3(cg / total), b: round3(cbl / total) },
|
|
2238
|
+
saturation: { mean: round3(sumSat / total), max: round3(maxSat) },
|
|
2239
|
+
average: `#${hex2(sumR / total)}${hex2(sumG / total)}${hex2(sumB / total)}`
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
function videoExposureNotes(stats) {
|
|
2243
|
+
const notes = [];
|
|
2244
|
+
const pct = (v2) => `${Math.round(v2 * 1e3) / 10}%`;
|
|
2245
|
+
if (stats.clipped.shadows >= 0.05) notes.push(`${pct(stats.clipped.shadows)} of the picture is crushed to black; raise exposure or lower contrast.`);
|
|
2246
|
+
if (stats.clipped.highlights >= 0.02) notes.push(`${pct(stats.clipped.highlights)} of the picture is blown to white; lower exposure.`);
|
|
2247
|
+
else {
|
|
2248
|
+
const channels = ["r", "g", "b"].filter((k) => stats.clipped[k] >= 0.05);
|
|
2249
|
+
if (channels.length) notes.push(`The ${channels.map((k) => ({ r: "red", g: "green", b: "blue" })[k]).join(" and ")} channel${channels.length > 1 ? "s clip" : " clips"} in ${pct(Math.max(...channels.map((k) => stats.clipped[k])))} of the picture; lower exposure or saturation.`);
|
|
2250
|
+
}
|
|
2251
|
+
if (stats.luma.mean < 0.2 && stats.clipped.shadows < 0.05) notes.push(`The picture is dark (mean luma ${stats.luma.mean}); raise exposure if it is not meant to be.`);
|
|
2252
|
+
if (stats.luma.mean > 0.75 && stats.clipped.highlights < 0.02) notes.push(`The picture is bright (mean luma ${stats.luma.mean}); lower exposure if it is not meant to be.`);
|
|
2253
|
+
if (stats.luma.p99 - stats.luma.p1 < 0.3 && stats.pixels > 1) notes.push(`The picture is flat (most of it lies between luma ${stats.luma.p1} and ${stats.luma.p99}); raise contrast.`);
|
|
2254
|
+
const [r, g, b] = [1, 3, 5].map((i) => parseInt(stats.average.slice(i, i + 2), 16) / 255);
|
|
2255
|
+
const [u, v] = videoChroma(r, g, b);
|
|
2256
|
+
if (Math.hypot(u, v) > 0.04 && stats.saturation.mean < 0.35) {
|
|
2257
|
+
const warm = v - u;
|
|
2258
|
+
notes.push(`The picture has a ${Math.abs(warm) > Math.abs(v + u) * 0.5 ? warm > 0 ? "warm (orange)" : "cool (blue)" : v > 0 ? "magenta" : "green"} cast (average ${stats.average}); adjust temperature and tint.`);
|
|
2259
|
+
}
|
|
2260
|
+
return notes;
|
|
2261
|
+
}
|
|
2262
|
+
var videoVectorscopeAngle = (cb, cr) => (Math.atan2(cr, cb) * 180 / Math.PI % 360 + 360) % 360;
|
|
2263
|
+
function summarizeVideoScopes(pixels, options = {}) {
|
|
2264
|
+
const stats = videoExposureStats(pixels, options);
|
|
2265
|
+
const bins = Math.max(2, Math.min(64, Math.round(_nullishCoalesce(options.bins, () => ( 16)))));
|
|
2266
|
+
const h = videoHistogram(pixels, { ...options, bins });
|
|
2267
|
+
const share = (a) => a.map((v) => round3(v / h.total));
|
|
2268
|
+
const columns = Math.max(1, Math.min(32, Math.round(_nullishCoalesce(options.columns, () => ( 8)))));
|
|
2269
|
+
const w = videoLumaWaveform(pixels, { ...options, columns, levels: 256 });
|
|
2270
|
+
const waveform = Array.from({ length: w.columns }, (_, c) => {
|
|
2271
|
+
const counts = w.data.subarray(c * 256, c * 256 + 256);
|
|
2272
|
+
const n2 = counts.reduce((a, b) => a + b, 0);
|
|
2273
|
+
let lo = 0;
|
|
2274
|
+
while (lo < 255 && !counts[lo]) lo++;
|
|
2275
|
+
let hi = 255;
|
|
2276
|
+
while (hi > 0 && !counts[hi]) hi--;
|
|
2277
|
+
let seen = 0;
|
|
2278
|
+
let median = 0;
|
|
2279
|
+
for (let i = 0; i < 256; i++) {
|
|
2280
|
+
seen += counts[i];
|
|
2281
|
+
if (seen >= n2 / 2 && n2) {
|
|
2282
|
+
median = i;
|
|
2283
|
+
break;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
return { start: round3(c / w.columns), end: round3((c + 1) / w.columns), min: round3(lo / 255), median: round3(median / 255), max: round3(hi / 255) };
|
|
2287
|
+
}).filter((_, c) => w.data.subarray(c * 256, c * 256 + 256).some((v) => v > 0));
|
|
2288
|
+
const chroma = [];
|
|
2289
|
+
eachPixel(pixels, Math.min(_nullishCoalesce(options.maxPixels, () => ( 65536)), 65536), (r, g, b) => {
|
|
2290
|
+
const [u, v] = videoChroma(r / 255, g / 255, b / 255);
|
|
2291
|
+
chroma.push([u, v, Math.hypot(u, v)]);
|
|
2292
|
+
});
|
|
2293
|
+
chroma.sort((a, b) => b[2] - a[2]);
|
|
2294
|
+
const top = chroma.slice(0, Math.max(1, Math.ceil(chroma.length / 10)));
|
|
2295
|
+
const mu = top.reduce((s, c) => s + c[0], 0) / top.length;
|
|
2296
|
+
const mv = top.reduce((s, c) => s + c[1], 0) / top.length;
|
|
2297
|
+
const strength = Math.min(1, top.reduce((s, c) => s + c[2], 0) / top.length / 0.5);
|
|
2298
|
+
const coherence = Math.hypot(mu, mv) / Math.max(1e-9, top.reduce((s, c) => s + c[2], 0) / top.length);
|
|
2299
|
+
return {
|
|
2300
|
+
stats,
|
|
2301
|
+
notes: videoExposureNotes(stats),
|
|
2302
|
+
histogram: { bins, luma: share(h.luma), r: share(h.r), g: share(h.g), b: share(h.b) },
|
|
2303
|
+
waveform,
|
|
2304
|
+
vectorscope: { hue: strength < 0.02 ? null : Math.round(videoVectorscopeAngle(mu, mv)), strength: round3(strength), spread: round3(1 - coherence) }
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
var VIDEO_VECTORSCOPE_TARGETS = [
|
|
2308
|
+
["R", [0.75, 0, 0]],
|
|
2309
|
+
["Mg", [0.75, 0, 0.75]],
|
|
2310
|
+
["B", [0, 0, 0.75]],
|
|
2311
|
+
["Cy", [0, 0.75, 0.75]],
|
|
2312
|
+
["G", [0, 0.75, 0]],
|
|
2313
|
+
["Yl", [0.75, 0.75, 0]]
|
|
2314
|
+
].map(([name, rgb]) => {
|
|
2315
|
+
const [cb, cr] = videoChroma(rgb[0], rgb[1], rgb[2]);
|
|
2316
|
+
return { name, rgb: [...rgb], cb, cr };
|
|
2317
|
+
});
|
|
2318
|
+
function canvas(width, height) {
|
|
2319
|
+
const data = new Uint8ClampedArray(width * height * 4);
|
|
2320
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
2321
|
+
data[i] = 12;
|
|
2322
|
+
data[i + 1] = 14;
|
|
2323
|
+
data[i + 2] = 18;
|
|
2324
|
+
data[i + 3] = 255;
|
|
2325
|
+
}
|
|
2326
|
+
return { width, height, data };
|
|
2327
|
+
}
|
|
2328
|
+
function plot(img, x, y, rgb, alpha = 1, add = false) {
|
|
2329
|
+
const xi = Math.round(x);
|
|
2330
|
+
const yi = Math.round(y);
|
|
2331
|
+
if (xi < 0 || yi < 0 || xi >= img.width || yi >= img.height) return;
|
|
2332
|
+
const i = (yi * img.width + xi) * 4;
|
|
2333
|
+
for (let k = 0; k < 3; k++) img.data[i + k] = add ? img.data[i + k] + rgb[k] * alpha : img.data[i + k] * (1 - alpha) + rgb[k] * alpha;
|
|
2334
|
+
}
|
|
2335
|
+
var GRID = [70, 76, 88];
|
|
2336
|
+
var glow = (count, peak) => count ? 0.25 + 0.75 * (Math.log1p(count) / Math.log1p(Math.max(1, peak))) : 0;
|
|
2337
|
+
function videoHistogramImage(h, width = 256, height = 128) {
|
|
2338
|
+
const img = canvas(width, height);
|
|
2339
|
+
for (const q of [0.25, 0.5, 0.75]) for (let y = 0; y < height; y++) plot(img, q * (width - 1), y, GRID);
|
|
2340
|
+
const inner = (a) => a.length > 2 ? a.slice(1, -1) : a;
|
|
2341
|
+
const peak = Math.max(1, ...inner(h.r), ...inner(h.g), ...inner(h.b), ...inner(h.luma));
|
|
2342
|
+
const height01 = (v) => Math.min(1, Math.sqrt(v / peak));
|
|
2343
|
+
for (let x = 0; x < width; x++) {
|
|
2344
|
+
const bin = Math.min(h.bins - 1, Math.floor(x / width * h.bins));
|
|
2345
|
+
const channels = [
|
|
2346
|
+
[h.r, [200, 40, 40]],
|
|
2347
|
+
[h.g, [40, 180, 40]],
|
|
2348
|
+
[h.b, [50, 80, 220]]
|
|
2349
|
+
];
|
|
2350
|
+
for (const [a, rgb] of channels) {
|
|
2351
|
+
const top = height - height01(a[bin]) * height;
|
|
2352
|
+
for (let y = Math.ceil(top); y < height; y++) plot(img, x, y, rgb, 0.8, true);
|
|
2353
|
+
}
|
|
2354
|
+
plot(img, x, height - 1 - height01(h.luma[bin]) * (height - 1), [255, 255, 255]);
|
|
2355
|
+
}
|
|
2356
|
+
return img;
|
|
2357
|
+
}
|
|
2358
|
+
function videoWaveformImage(w, width = 256, height = 128) {
|
|
2359
|
+
const img = canvas(width, height);
|
|
2360
|
+
for (const q of [0, 0.25, 0.5, 0.75, 1]) for (let x = 0; x < width; x++) plot(img, x, (1 - q) * (height - 1), GRID);
|
|
2361
|
+
for (let x = 0; x < width; x++) {
|
|
2362
|
+
const c = Math.min(w.columns - 1, Math.floor(x / width * w.columns));
|
|
2363
|
+
for (let y = 0; y < height; y++) {
|
|
2364
|
+
const level = Math.min(w.levels - 1, Math.floor((height - 1 - y) / height * w.levels));
|
|
2365
|
+
const a = glow(w.data[c * w.levels + level], w.peak);
|
|
2366
|
+
if (a) plot(img, x, y, [120, 255, 140], a);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
return img;
|
|
2370
|
+
}
|
|
2371
|
+
function videoVectorscopeImage(v, size = v.size) {
|
|
2372
|
+
const img = canvas(size, size);
|
|
2373
|
+
const c = (size - 1) / 2;
|
|
2374
|
+
for (let a = 0; a < 720; a++) {
|
|
2375
|
+
const t = a / 720 * 2 * Math.PI;
|
|
2376
|
+
plot(img, c + Math.cos(t) * c, c - Math.sin(t) * c, GRID);
|
|
2377
|
+
}
|
|
2378
|
+
const skin = 123 * Math.PI / 180;
|
|
2379
|
+
for (let r = 0; r < c; r++) plot(img, c + Math.cos(skin) * r, c - Math.sin(skin) * r, [140, 110, 90]);
|
|
2380
|
+
for (const t of VIDEO_VECTORSCOPE_TARGETS) {
|
|
2381
|
+
const [x, y] = videoVectorscopePoint(t.cb, t.cr, size);
|
|
2382
|
+
const rgb = t.rgb.map((k) => 80 + k * 200);
|
|
2383
|
+
for (let k = -3; k <= 3; k++) {
|
|
2384
|
+
plot(img, x + k, y - 3, rgb);
|
|
2385
|
+
plot(img, x + k, y + 3, rgb);
|
|
2386
|
+
plot(img, x - 3, y + k, rgb);
|
|
2387
|
+
plot(img, x + 3, y + k, rgb);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
for (let y = 0; y < size; y++)
|
|
2391
|
+
for (let x = 0; x < size; x++) {
|
|
2392
|
+
const sy = Math.min(v.size - 1, Math.floor(y / size * v.size));
|
|
2393
|
+
const sx = Math.min(v.size - 1, Math.floor(x / size * v.size));
|
|
2394
|
+
const a = glow(v.data[sy * v.size + sx], v.peak);
|
|
2395
|
+
if (!a) continue;
|
|
2396
|
+
const cb = sx / (v.size - 1) - 0.5;
|
|
2397
|
+
const cr = 0.5 - sy / (v.size - 1);
|
|
2398
|
+
const r = Math.min(1, Math.max(0, 0.6 + 1.5748 * cr));
|
|
2399
|
+
const g = Math.min(1, Math.max(0, 0.6 - 0.1873 * cb - 0.4681 * cr));
|
|
2400
|
+
const b = Math.min(1, Math.max(0, 0.6 + 1.8556 * cb));
|
|
2401
|
+
plot(img, x, y, [r * 255, g * 255, b * 255], a);
|
|
2402
|
+
}
|
|
2403
|
+
return img;
|
|
2404
|
+
}
|
|
2405
|
+
function videoScopeImage(kind, pixels, width = 256, height = 128, options = {}) {
|
|
2406
|
+
if (kind === "histogram") return videoHistogramImage(videoHistogram(pixels, { ...options, bins: Math.min(256, width) }), width, height);
|
|
2407
|
+
if (kind === "waveform") return videoWaveformImage(videoLumaWaveform(pixels, { ...options, columns: Math.min(pixels.width, width), levels: Math.min(256, height) }), width, height);
|
|
2408
|
+
const size = Math.min(width, height);
|
|
2409
|
+
return videoVectorscopeImage(videoVectorscope(pixels, { ...options, size }), size);
|
|
2410
|
+
}
|
|
2411
|
+
var VideoPreviewPixels = (_class3 = class {constructor() { _class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this); }
|
|
2412
|
+
__init6() {this.reader = null}
|
|
2413
|
+
__init7() {this.drawnAt = null}
|
|
2414
|
+
__init8() {this.listeners = /* @__PURE__ */ new Set()}
|
|
2415
|
+
/** Gives the canvas to read (the latest preview wins). Returns a function that takes it away again. */
|
|
2416
|
+
attach(reader) {
|
|
2417
|
+
this.reader = reader;
|
|
2418
|
+
this.drawnAt = null;
|
|
2419
|
+
return () => {
|
|
2420
|
+
if (this.reader !== reader) return;
|
|
2421
|
+
this.reader = null;
|
|
2422
|
+
this.drawnAt = null;
|
|
2423
|
+
this.emit();
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
/** Whether a preview is attached. */
|
|
2427
|
+
get attached() {
|
|
2428
|
+
return this.reader !== null;
|
|
2429
|
+
}
|
|
2430
|
+
/** The preview drew the frame at `time`; `complete` is false while a picture was missing or stale. */
|
|
2431
|
+
drawn(time, complete) {
|
|
2432
|
+
this.drawnAt = { time, complete };
|
|
2433
|
+
this.emit();
|
|
2434
|
+
}
|
|
2435
|
+
/** The last frame drawn, with its pixels; `null` without a preview. Throws when the canvas cannot be read. */
|
|
2436
|
+
read() {
|
|
2437
|
+
if (!this.reader) return null;
|
|
2438
|
+
try {
|
|
2439
|
+
return this.reader();
|
|
2440
|
+
} catch (e) {
|
|
2441
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
2442
|
+
throw new Error(`The preview's pixels cannot be read (${reason}). Media from another origin must be served with CORS headers (Access-Control-Allow-Origin) for its pixels to be measured.`);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
/** Called after each frame the preview draws. */
|
|
2446
|
+
subscribe(listener) {
|
|
2447
|
+
this.listeners.add(listener);
|
|
2448
|
+
return () => {
|
|
2449
|
+
this.listeners.delete(listener);
|
|
2450
|
+
};
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* The frame at `time` once it is complete: now, if the last frame drawn was it, else the next
|
|
2454
|
+
* complete drawing of it. Fails after `timeout` ms (default 5000).
|
|
2455
|
+
*/
|
|
2456
|
+
next(time, timeout = 5e3) {
|
|
2457
|
+
const matches = () => this.drawnAt && this.drawnAt.complete && Math.abs(this.drawnAt.time - time) < 0.5;
|
|
2458
|
+
return new Promise((resolve, reject) => {
|
|
2459
|
+
const done = (fn) => {
|
|
2460
|
+
clearTimeout(timer);
|
|
2461
|
+
off();
|
|
2462
|
+
fn();
|
|
2463
|
+
};
|
|
2464
|
+
const check = () => {
|
|
2465
|
+
if (!this.reader) return done(() => reject(new Error("The preview was closed before it drew the frame.")));
|
|
2466
|
+
if (!matches()) return;
|
|
2467
|
+
try {
|
|
2468
|
+
const snap = this.read();
|
|
2469
|
+
done(() => snap ? resolve(snap) : reject(new Error("The preview has no pixels to read.")));
|
|
2470
|
+
} catch (e) {
|
|
2471
|
+
done(() => reject(e));
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
const off = this.subscribe(check);
|
|
2475
|
+
const timer = setTimeout(() => done(() => reject(new Error(`The preview did not finish drawing ${Math.round(time)} ms within ${timeout} ms; its media may still be loading or may have failed to load. Try again, or check the asset sources.`))), timeout);
|
|
2476
|
+
check();
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
emit() {
|
|
2480
|
+
for (const l of [...this.listeners]) l();
|
|
2481
|
+
}
|
|
2482
|
+
}, _class3);
|
|
2483
|
+
function readVideoScopesPanel(feed, kinds = VIDEO_SCOPE_KINDS, options = {}) {
|
|
2484
|
+
let snap;
|
|
2485
|
+
try {
|
|
2486
|
+
snap = feed.read();
|
|
2487
|
+
} catch (e) {
|
|
2488
|
+
return { ok: false, message: e instanceof Error ? e.message : String(e) };
|
|
2489
|
+
}
|
|
2490
|
+
if (!snap) return { ok: false, message: feed.attached ? "The preview has not drawn a frame yet." : "Show the preview to see its scopes." };
|
|
2491
|
+
const width = _nullishCoalesce(options.width, () => ( 256));
|
|
2492
|
+
const height = _nullishCoalesce(options.height, () => ( 128));
|
|
2493
|
+
const opts = { maxPixels: _nullishCoalesce(options.maxPixels, () => ( 65536)) };
|
|
2494
|
+
const images = {};
|
|
2495
|
+
for (const kind of kinds) images[kind] = videoScopeImage(kind, snap.pixels, width, height, opts);
|
|
2496
|
+
const stats = videoExposureStats(snap.pixels, opts);
|
|
2497
|
+
return { ok: true, time: snap.time, complete: snap.complete, images, stats, notes: videoExposureNotes(stats) };
|
|
2498
|
+
}
|
|
2499
|
+
|
|
1958
2500
|
// src/video-editor/transcript.ts
|
|
1959
2501
|
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1960
2502
|
function cueTime(s) {
|
|
@@ -2020,8 +2562,8 @@ function videoTranscriptTrack(project, track) {
|
|
|
2020
2562
|
return track;
|
|
2021
2563
|
}
|
|
2022
2564
|
const assets = videoAssetMap(project);
|
|
2023
|
-
const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access',
|
|
2024
|
-
return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess',
|
|
2565
|
+
const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _42 => _42.get, 'call', _43 => _43(c.asset), 'optionalAccess', _44 => _44.transcript, 'optionalAccess', _45 => _45.length]));
|
|
2566
|
+
return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _46 => _46.id]), () => ( null));
|
|
2025
2567
|
}
|
|
2026
2568
|
function videoTimelineTranscript(project, options = {}) {
|
|
2027
2569
|
const id = videoTranscriptTrack(project, options.track);
|
|
@@ -2031,8 +2573,8 @@ function videoTimelineTranscript(project, options = {}) {
|
|
|
2031
2573
|
const out = [];
|
|
2032
2574
|
for (const clip of sortedVideoClips(track)) {
|
|
2033
2575
|
if (clip.type !== "media") continue;
|
|
2034
|
-
const words = _optionalChain([assets, 'access',
|
|
2035
|
-
if (!_optionalChain([words, 'optionalAccess',
|
|
2576
|
+
const words = _optionalChain([assets, 'access', _47 => _47.get, 'call', _48 => _48(clip.asset), 'optionalAccess', _49 => _49.transcript]);
|
|
2577
|
+
if (!_optionalChain([words, 'optionalAccess', _50 => _50.length])) continue;
|
|
2036
2578
|
const speed = videoClipSpeed(clip);
|
|
2037
2579
|
const from = _nullishCoalesce(clip.in, () => ( 0));
|
|
2038
2580
|
const to = from + clip.duration * speed;
|
|
@@ -2276,7 +2818,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
|
|
|
2276
2818
|
}
|
|
2277
2819
|
|
|
2278
2820
|
// src/video-editor/export.ts
|
|
2279
|
-
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
2821
|
+
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _51 => _51.name]), () => ( _optionalChain([asset, 'optionalAccess', _52 => _52.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
|
|
2280
2822
|
function toVideoEdl(project, options = {}) {
|
|
2281
2823
|
const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
|
|
2282
2824
|
if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
|
|
@@ -2311,7 +2853,7 @@ function toVideoEdl(project, options = {}) {
|
|
|
2311
2853
|
lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
2312
2854
|
} else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
2313
2855
|
if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
|
|
2314
|
-
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
2856
|
+
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _53 => _53.name]), () => ( _optionalChain([asset, 'optionalAccess', _54 => _54.src]))), () => ( clip.asset))}`);
|
|
2315
2857
|
if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
|
|
2316
2858
|
if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
|
|
2317
2859
|
lines.push("");
|
|
@@ -2344,6 +2886,7 @@ function parseVideoLoudnessMeasurement(output) {
|
|
|
2344
2886
|
return m;
|
|
2345
2887
|
}
|
|
2346
2888
|
var sec = (ms) => String(Math.round(ms) / 1e3);
|
|
2889
|
+
var fmtMs = (v) => String(Math.round(v * 100) / 100);
|
|
2347
2890
|
var FFMPEG_BLEND = {
|
|
2348
2891
|
multiply: "multiply",
|
|
2349
2892
|
screen: "screen",
|
|
@@ -2403,6 +2946,58 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2403
2946
|
let cur = "base";
|
|
2404
2947
|
let usesDrawtext = false;
|
|
2405
2948
|
const animated = /* @__PURE__ */ new Set();
|
|
2949
|
+
const ramped = /* @__PURE__ */ new Set();
|
|
2950
|
+
const rampedPicture = (remap, lead, end) => {
|
|
2951
|
+
const pts = videoTimeRemapBreakpoints(remap, 2e3 / fps);
|
|
2952
|
+
const first = pts[0];
|
|
2953
|
+
const last = pts[pts.length - 1];
|
|
2954
|
+
const s = `(T*1000+${fmtMs(first.v)})`;
|
|
2955
|
+
let e = fmtMs(last.t - first.t);
|
|
2956
|
+
for (let i = pts.length - 2; i >= 0; i--) {
|
|
2957
|
+
const a = pts[i];
|
|
2958
|
+
const b = pts[i + 1];
|
|
2959
|
+
if (b.v - a.v < 1e-6) continue;
|
|
2960
|
+
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})`;
|
|
2961
|
+
}
|
|
2962
|
+
const before = lead + first.t;
|
|
2963
|
+
const after = Math.max(0, end - last.t);
|
|
2964
|
+
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)}`];
|
|
2965
|
+
};
|
|
2966
|
+
const rampedSound = (remap, input2, output, lead, end) => {
|
|
2967
|
+
const pts = videoTimeRemapBreakpoints(remap, Math.max(80, 2e3 / fps));
|
|
2968
|
+
const first = pts[0];
|
|
2969
|
+
const last = pts[pts.length - 1];
|
|
2970
|
+
const n2 = audio.length;
|
|
2971
|
+
const fmtA = "aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo";
|
|
2972
|
+
const silence = (ms) => `anullsrc=r=48000:cl=stereo,atrim=duration=${sec(ms)},${fmtA}`;
|
|
2973
|
+
const pieces = [];
|
|
2974
|
+
const out = [];
|
|
2975
|
+
const parts = [];
|
|
2976
|
+
if (lead + first.t > 0.5) parts.push(silence(lead + first.t));
|
|
2977
|
+
const played = [];
|
|
2978
|
+
for (let i = 0; i + 1 < pts.length; i++) {
|
|
2979
|
+
const dt = pts[i + 1].t - pts[i].t;
|
|
2980
|
+
if (dt < 0.5) continue;
|
|
2981
|
+
const speed = (pts[i + 1].v - pts[i].v) / dt;
|
|
2982
|
+
if (speed < 0.1) parts.push(silence(dt));
|
|
2983
|
+
else {
|
|
2984
|
+
played.push({ from: pts[i].v - first.v, to: pts[i + 1].v - first.v, speed });
|
|
2985
|
+
parts.push(`#${played.length - 1}`);
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
if (end - last.t > 0.5) parts.push(silence(end - last.t));
|
|
2989
|
+
if (played.length) out.push(`[${input2}]asplit=${played.length}${played.map((_, k2) => `[r${n2}s${k2}]`).join("")}`);
|
|
2990
|
+
parts.forEach((p, k2) => {
|
|
2991
|
+
const label = `[r${n2}p${k2}]`;
|
|
2992
|
+
if (p.startsWith("#")) {
|
|
2993
|
+
const piece = played[Number(p.slice(1))];
|
|
2994
|
+
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}`);
|
|
2995
|
+
} else out.push(`${p}${label}`);
|
|
2996
|
+
pieces.push(label);
|
|
2997
|
+
});
|
|
2998
|
+
out.push(`${pieces.join("")}concat=n=${pieces.length}:v=0:a=1${output}`);
|
|
2999
|
+
return out;
|
|
3000
|
+
};
|
|
2406
3001
|
const blended = /* @__PURE__ */ new Set();
|
|
2407
3002
|
let k = 0;
|
|
2408
3003
|
const step = 2e3 / fps;
|
|
@@ -2415,7 +3010,10 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2415
3010
|
const src = options.resolve ? options.resolve(asset) : asset.src;
|
|
2416
3011
|
const span = clip.duration + lead + tail;
|
|
2417
3012
|
if (asset.kind === "image") inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", src]);
|
|
2418
|
-
else
|
|
3013
|
+
else if (clip.timeRemap) {
|
|
3014
|
+
const { from, to } = videoTimeRemapSpan(clip.timeRemap);
|
|
3015
|
+
inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) + from), "-t", sec(to - from + 1e3 / fps), "-i", src]);
|
|
3016
|
+
} else inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) - lead * speed), "-t", sec(span * speed), "-i", src]);
|
|
2419
3017
|
return inputs.length - 1;
|
|
2420
3018
|
};
|
|
2421
3019
|
for (const track of [...project.tracks].reverse()) {
|
|
@@ -2437,7 +3035,13 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2437
3035
|
const ai = _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
|
|
2438
3036
|
const trackGain = _nullishCoalesce(track.volume, () => ( 1));
|
|
2439
3037
|
const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(_nullishCoalesce(clip.volume, () => ( 1))) * trackGain}`;
|
|
2440
|
-
|
|
3038
|
+
let source2 = `[${ai}:a]`;
|
|
3039
|
+
if (clip.timeRemap) {
|
|
3040
|
+
source2 = `[r${audio.length}]`;
|
|
3041
|
+
graph.push(...rampedSound(clip.timeRemap, `${ai}:a`, source2, lead, clip.duration + tail));
|
|
3042
|
+
ramped.add("sound");
|
|
3043
|
+
}
|
|
3044
|
+
const chain2 = [...clip.timeRemap ? [] : ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip))], volume];
|
|
2441
3045
|
const before2 = clip.transition ? videoClipBefore(track, clip) : null;
|
|
2442
3046
|
const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
|
|
2443
3047
|
const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
|
|
@@ -2445,11 +3049,11 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2445
3049
|
if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
|
|
2446
3050
|
if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
|
|
2447
3051
|
if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
|
|
2448
|
-
if (_optionalChain([next2, 'optionalAccess',
|
|
3052
|
+
if (_optionalChain([next2, 'optionalAccess', _55 => _55.transition, 'access', _56 => _56.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
|
|
2449
3053
|
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
|
|
2450
3054
|
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
|
|
2451
3055
|
chain2.push(`adelay=${Math.round(t0)}:all=1`);
|
|
2452
|
-
graph.push(
|
|
3056
|
+
graph.push(`${source2}${chain2.join(",")}[a${audio.length}]`);
|
|
2453
3057
|
audio.push(`[a${audio.length}]`);
|
|
2454
3058
|
}
|
|
2455
3059
|
if (measure || track.kind !== "video" || track.hidden) continue;
|
|
@@ -2458,7 +3062,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2458
3062
|
const onTimeline = `(t*1000-${Math.round(clip.start)})`;
|
|
2459
3063
|
const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
|
|
2460
3064
|
const tf = clip.transform;
|
|
2461
|
-
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
3065
|
+
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _57 => _57[p]])));
|
|
2462
3066
|
if (clip.type === "text" && options.titleImage) {
|
|
2463
3067
|
inputs.push(["-loop", "1", "-framerate", String(fps), "-t", sec(span), "-i", options.titleImage(moving ? { ...clip, transform: void 0 } : clip, { width: project.width, height: project.height })]);
|
|
2464
3068
|
index = inputs.length - 1;
|
|
@@ -2467,7 +3071,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2467
3071
|
const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
|
|
2468
3072
|
const alpha = [op, clip.fadeIn ? `min(1,(t-${sec(clip.start)})/${sec(clip.fadeIn)})` : "", clip.fadeOut ? `min(1,(${sec(end)}-t)/${sec(clip.fadeOut)})` : ""].filter(Boolean).join("*");
|
|
2469
3073
|
const coord = (axis) => {
|
|
2470
|
-
const v = _optionalChain([tf, 'optionalAccess',
|
|
3074
|
+
const v = _optionalChain([tf, 'optionalAccess', _58 => _58[axis]]);
|
|
2471
3075
|
const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
|
|
2472
3076
|
return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
|
|
2473
3077
|
};
|
|
@@ -2475,7 +3079,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2475
3079
|
const opts = [
|
|
2476
3080
|
`text=${ffmpegFilterValue(clip.text)}`,
|
|
2477
3081
|
options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
|
|
2478
|
-
isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
3082
|
+
isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _59 => _59.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _60 => _60.scale]), () => ( 1))))}`,
|
|
2479
3083
|
`fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
|
|
2480
3084
|
coord("x"),
|
|
2481
3085
|
coord("y"),
|
|
@@ -2483,7 +3087,7 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2483
3087
|
...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
|
|
2484
3088
|
`enable='between(t,${sec(clip.start)},${sec(end)})'`
|
|
2485
3089
|
];
|
|
2486
|
-
if (_optionalChain([tf, 'optionalAccess',
|
|
3090
|
+
if (_optionalChain([tf, 'optionalAccess', _61 => _61.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
|
|
2487
3091
|
if (videoGradeFfmpegFilters(clip.grade).length) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
|
|
2488
3092
|
if (clip.blend && clip.blend !== "normal") notes.push(`Title "${clip.id}" is drawn without its blend mode (drawtext draws straight onto the picture); pass titleImage to blend titles.`);
|
|
2489
3093
|
graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
|
|
@@ -2499,7 +3103,8 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2499
3103
|
if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
|
|
2500
3104
|
else {
|
|
2501
3105
|
source = `[${index}:v]`;
|
|
2502
|
-
|
|
3106
|
+
if (clip.type === "media" && clip.timeRemap) chain.push(...rampedPicture(clip.timeRemap, lead, clip.duration + tail), `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
3107
|
+
else chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
2503
3108
|
}
|
|
2504
3109
|
const grade = videoGradeFfmpegFilters(clip.grade);
|
|
2505
3110
|
if (grade.length) chain.push("format=rgba", ...grade);
|
|
@@ -2520,10 +3125,10 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2520
3125
|
const r = videoIrisRadius({ width: W, height: H }, 1);
|
|
2521
3126
|
chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(lte(hypot(X+${Math.round(rect.x)}-${W / 2},Y+${Math.round(rect.y)}-${H / 2}),${Math.round(r)}*min(1,T/${sec(2 * lead)})),alpha(X,Y),0)'`);
|
|
2522
3127
|
}
|
|
2523
|
-
const rotation = _optionalChain([placed, 'optionalAccess',
|
|
3128
|
+
const rotation = _optionalChain([placed, 'optionalAccess', _62 => _62.rotation]);
|
|
2524
3129
|
if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
|
|
2525
3130
|
else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
|
|
2526
|
-
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess',
|
|
3131
|
+
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _63 => _63.scale]))) {
|
|
2527
3132
|
const s = expr(placed.scale, onChain("t"), 1, "scale");
|
|
2528
3133
|
chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
|
|
2529
3134
|
}
|
|
@@ -2542,9 +3147,9 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2542
3147
|
const into = lead ? clip.transition.type : void 0;
|
|
2543
3148
|
if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
|
|
2544
3149
|
const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
|
|
2545
|
-
if (_optionalChain([after, 'optionalAccess',
|
|
3150
|
+
if (_optionalChain([after, 'optionalAccess', _64 => _64.transition, 'access', _65 => _65.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
|
|
2546
3151
|
const at = (axis, c) => {
|
|
2547
|
-
const v = _optionalChain([placed, 'optionalAccess',
|
|
3152
|
+
const v = _optionalChain([placed, 'optionalAccess', _66 => _66[axis]]);
|
|
2548
3153
|
const extra = shifts[axis].map((s) => `+${s}`).join("");
|
|
2549
3154
|
return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2${extra}'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2${extra}'`;
|
|
2550
3155
|
};
|
|
@@ -2578,11 +3183,12 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2578
3183
|
if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
|
|
2579
3184
|
else if (options.loudness && !options.loudness.measured)
|
|
2580
3185
|
notes.push("Loudness is normalised in one pass, which adjusts the level as it goes; for a fixed gain that keeps the dynamics, run toVideoLoudnessMeasure first and pass its result as loudness.measured.");
|
|
2581
|
-
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access',
|
|
3186
|
+
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _67 => _67.get, 'call', _68 => _68(c.asset), 'optionalAccess', _69 => _69.kind]) === "video" && _optionalChain([assets, 'access', _70 => _70.get, 'call', _71 => _71(c.asset), 'optionalAccess', _72 => _72.hasAudio]) === void 0)))
|
|
2582
3187
|
notes.push('Video assets are assumed to have sound; set "hasAudio": false on any that do not, or ffmpeg will stop at a missing audio stream.');
|
|
2583
3188
|
if (animated.size)
|
|
2584
3189
|
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
3190
|
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.");
|
|
3191
|
+
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
3192
|
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
3193
|
const filterComplex = graph.join(";");
|
|
2588
3194
|
if (measure) {
|
|
@@ -2612,26 +3218,40 @@ function buildFfmpeg(project, options, measure) {
|
|
|
2612
3218
|
}
|
|
2613
3219
|
|
|
2614
3220
|
// src/video-editor/preview.ts
|
|
2615
|
-
var VideoPreviewEngine = (
|
|
2616
|
-
constructor(source,
|
|
3221
|
+
var VideoPreviewEngine = (_class4 = class {
|
|
3222
|
+
constructor(source, canvas2, options = {}) {;_class4.prototype.__init9.call(this);_class4.prototype.__init10.call(this);_class4.prototype.__init11.call(this);_class4.prototype.__init12.call(this);_class4.prototype.__init13.call(this);_class4.prototype.__init14.call(this);_class4.prototype.__init15.call(this);_class4.prototype.__init16.call(this);_class4.prototype.__init17.call(this);_class4.prototype.__init18.call(this);
|
|
2617
3223
|
this.source = source;
|
|
2618
3224
|
this.options = options;
|
|
2619
|
-
this.canvas =
|
|
2620
|
-
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([
|
|
3225
|
+
this.canvas = canvas2;
|
|
3226
|
+
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas2, 'access', _73 => _73.getContext, 'optionalCall', _74 => _74("2d")]), () => ( null));
|
|
2621
3227
|
this.unsubscribe = source.subscribe(() => this.update());
|
|
3228
|
+
const pixels = options.pixels !== void 0 ? options.pixels : source.previewPixels;
|
|
3229
|
+
this.detachPixels = pixels ? pixels.attach(() => this.readPixels()) : () => {
|
|
3230
|
+
};
|
|
3231
|
+
this.pixelsTarget = _nullishCoalesce(pixels, () => ( null));
|
|
2622
3232
|
this.update();
|
|
2623
3233
|
}
|
|
2624
3234
|
|
|
2625
3235
|
|
|
2626
3236
|
|
|
2627
3237
|
|
|
2628
|
-
|
|
2629
|
-
|
|
3238
|
+
__init9() {this.media = /* @__PURE__ */ new Map()}
|
|
3239
|
+
__init10() {this.images = /* @__PURE__ */ new Map()}
|
|
2630
3240
|
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
3241
|
+
__init11() {this.scheduled = false}
|
|
3242
|
+
__init12() {this.destroyed = false}
|
|
3243
|
+
__init13() {this.filterHost = null}
|
|
3244
|
+
__init14() {this.filters = /* @__PURE__ */ new Set()}
|
|
3245
|
+
|
|
3246
|
+
|
|
3247
|
+
__init15() {this.drawn = null}
|
|
3248
|
+
__init16() {this.complete = true}
|
|
3249
|
+
/** The canvas's pixels and the frame they show, or `null` when its context cannot be read. */
|
|
3250
|
+
readPixels() {
|
|
3251
|
+
const ctx = this.ctx;
|
|
3252
|
+
if (!_optionalChain([ctx, 'optionalAccess', _75 => _75.getImageData]) || !this.drawn) return null;
|
|
3253
|
+
return { ...this.drawn, pixels: ctx.getImageData(0, 0, this.canvas.width, this.canvas.height) };
|
|
3254
|
+
}
|
|
2635
3255
|
createMedia(kind) {
|
|
2636
3256
|
if (this.options.createMedia) return this.options.createMedia(kind);
|
|
2637
3257
|
const el = document.createElement(kind);
|
|
@@ -2667,8 +3287,8 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2667
3287
|
entry.media.pause();
|
|
2668
3288
|
entry.media.removeEventListener("seeked", entry.onReady);
|
|
2669
3289
|
entry.media.removeEventListener("loadeddata", entry.onReady);
|
|
2670
|
-
_optionalChain([entry, 'access',
|
|
2671
|
-
_optionalChain([entry, 'access',
|
|
3290
|
+
_optionalChain([entry, 'access', _76 => _76.media, 'access', _77 => _77.removeAttribute, 'optionalCall', _78 => _78("src")]);
|
|
3291
|
+
_optionalChain([entry, 'access', _79 => _79.media, 'access', _80 => _80.load, 'optionalCall', _81 => _81()]);
|
|
2672
3292
|
this.media.delete(entry.clip);
|
|
2673
3293
|
}
|
|
2674
3294
|
/** Brings the media elements in step with the editor and draws the frame. */
|
|
@@ -2682,13 +3302,14 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2682
3302
|
const { media } = this.entry(clip, src, kind);
|
|
2683
3303
|
active.add(clip);
|
|
2684
3304
|
const target = sourceTime / 1e3;
|
|
2685
|
-
|
|
3305
|
+
const frozen = speed < 0.0625;
|
|
3306
|
+
if (!frozen && Math.abs(media.playbackRate - speed) > 1e-6) media.playbackRate = speed;
|
|
2686
3307
|
if (gain === null) media.muted = true;
|
|
2687
3308
|
else {
|
|
2688
3309
|
media.muted = gain <= 0;
|
|
2689
3310
|
media.volume = Math.min(1, Math.max(0, gain));
|
|
2690
3311
|
}
|
|
2691
|
-
if (playing) {
|
|
3312
|
+
if (playing && !frozen) {
|
|
2692
3313
|
if (media.paused || Math.abs(media.currentTime - target) > tolerance) media.currentTime = target;
|
|
2693
3314
|
if (media.paused) void Promise.resolve(media.play()).catch(() => {
|
|
2694
3315
|
});
|
|
@@ -2701,7 +3322,7 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2701
3322
|
for (const layer of frame.layers) {
|
|
2702
3323
|
if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
|
|
2703
3324
|
const sound = sounding.get(layer.clip);
|
|
2704
|
-
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess',
|
|
3325
|
+
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _82 => _82.speed]), () => ( 1)), sound ? sound.gain : null);
|
|
2705
3326
|
sounding.delete(layer.clip);
|
|
2706
3327
|
}
|
|
2707
3328
|
for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
|
|
@@ -2716,13 +3337,15 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2716
3337
|
if (!this.destroyed) this.draw(videoFrameAt(this.source.getState().project, this.source.getState().time));
|
|
2717
3338
|
});
|
|
2718
3339
|
}
|
|
2719
|
-
|
|
3340
|
+
__init17() {this.sourceFor = (layer) => {
|
|
2720
3341
|
if (layer.assetKind === "image" && layer.src) {
|
|
2721
3342
|
const img = this.image(layer.src);
|
|
2722
3343
|
return img.complete ? img : null;
|
|
2723
3344
|
}
|
|
2724
3345
|
const entry = this.media.get(layer.clip);
|
|
2725
|
-
|
|
3346
|
+
if (!entry || entry.media.readyState < 2) return null;
|
|
3347
|
+
if (entry.media.seeking) this.complete = false;
|
|
3348
|
+
return entry.media;
|
|
2726
3349
|
}}
|
|
2727
3350
|
draw(frame) {
|
|
2728
3351
|
if (!this.ctx) return;
|
|
@@ -2733,10 +3356,18 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2733
3356
|
const h = Math.max(1, Math.round(frame.height * resolution));
|
|
2734
3357
|
if (this.canvas.width !== w) this.canvas.width = w;
|
|
2735
3358
|
if (this.canvas.height !== h) this.canvas.height = h;
|
|
2736
|
-
|
|
2737
|
-
|
|
3359
|
+
this.complete = true;
|
|
3360
|
+
const sourceFor = (layer) => {
|
|
3361
|
+
const source = this.sourceFor(layer);
|
|
3362
|
+
if (!source) this.complete = false;
|
|
3363
|
+
return source;
|
|
3364
|
+
};
|
|
3365
|
+
drawVideoFrame(this.ctx, frame, sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter });
|
|
3366
|
+
this.drawn = { time: frame.time, complete: this.complete };
|
|
3367
|
+
_optionalChain([this, 'access', _83 => _83.options, 'access', _84 => _84.onFrame, 'optionalCall', _85 => _85(frame)]);
|
|
3368
|
+
_optionalChain([this, 'access', _86 => _86.pixelsTarget, 'optionalAccess', _87 => _87.drawn, 'call', _88 => _88(frame.time, this.complete)]);
|
|
2738
3369
|
}
|
|
2739
|
-
|
|
3370
|
+
__init18() {this.defineSvgFilter = (id, markup) => {
|
|
2740
3371
|
if (this.options.defineSvgFilter === null) return;
|
|
2741
3372
|
if (this.options.defineSvgFilter) return this.options.defineSvgFilter(id, markup);
|
|
2742
3373
|
if (this.filters.has(id) || typeof document === "undefined") return;
|
|
@@ -2755,33 +3386,36 @@ var VideoPreviewEngine = (_class3 = class {
|
|
|
2755
3386
|
/** Stops following the editor and releases every media element. */
|
|
2756
3387
|
destroy() {
|
|
2757
3388
|
this.destroyed = true;
|
|
2758
|
-
|
|
3389
|
+
this.detachPixels();
|
|
3390
|
+
_optionalChain([this, 'access', _89 => _89.filterHost, 'optionalAccess', _90 => _90.remove, 'call', _91 => _91()]);
|
|
2759
3391
|
this.filterHost = null;
|
|
2760
3392
|
this.filters.clear();
|
|
2761
3393
|
this.unsubscribe();
|
|
2762
3394
|
for (const entry of [...this.media.values()]) this.release(entry);
|
|
2763
3395
|
this.images.clear();
|
|
2764
3396
|
}
|
|
2765
|
-
},
|
|
3397
|
+
}, _class4);
|
|
2766
3398
|
|
|
2767
3399
|
// src/video-editor/controller.ts
|
|
2768
|
-
var VideoEditorController = (
|
|
3400
|
+
var VideoEditorController = (_class5 = class {
|
|
2769
3401
|
|
|
2770
3402
|
|
|
2771
3403
|
/** Sound levels per asset, loaded on demand for waveforms and agents. */
|
|
2772
3404
|
|
|
3405
|
+
/** The pixels the preview last drew, for scopes and `get_video_scopes` (a `VideoPreviewEngine` fills it). */
|
|
3406
|
+
__init19() {this.previewPixels = new VideoPreviewPixels()}
|
|
2773
3407
|
|
|
2774
3408
|
|
|
2775
|
-
|
|
3409
|
+
__init20() {this.selection = []}
|
|
2776
3410
|
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
3411
|
+
__init21() {this.error = null}
|
|
3412
|
+
__init22() {this.revision = 0}
|
|
3413
|
+
__init23() {this.applying = false}
|
|
2780
3414
|
|
|
2781
|
-
|
|
2782
|
-
|
|
3415
|
+
__init24() {this.listeners = /* @__PURE__ */ new Set()}
|
|
3416
|
+
__init25() {this.unsubscribers = []}
|
|
2783
3417
|
|
|
2784
|
-
constructor(options = {}) {;
|
|
3418
|
+
constructor(options = {}) {;_class5.prototype.__init19.call(this);_class5.prototype.__init20.call(this);_class5.prototype.__init21.call(this);_class5.prototype.__init22.call(this);_class5.prototype.__init23.call(this);_class5.prototype.__init24.call(this);_class5.prototype.__init25.call(this);_class5.prototype.__init26.call(this);_class5.prototype.__init27.call(this);
|
|
2785
3419
|
this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
|
|
2786
3420
|
this.audioPeaks = options.audioPeaks instanceof VideoAudioPeaksCache ? options.audioPeaks : new VideoAudioPeaksCache(options.audioPeaks);
|
|
2787
3421
|
this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
|
|
@@ -2806,8 +3440,8 @@ var VideoEditorController = (_class4 = class {
|
|
|
2806
3440
|
this.player.pause();
|
|
2807
3441
|
this.unsubscribers.splice(0).forEach((u) => u());
|
|
2808
3442
|
}
|
|
2809
|
-
|
|
2810
|
-
|
|
3443
|
+
__init26() {this.getState = () => this.state}
|
|
3444
|
+
__init27() {this.subscribe = (listener) => {
|
|
2811
3445
|
this.connect();
|
|
2812
3446
|
this.listeners.add(listener);
|
|
2813
3447
|
return () => {
|
|
@@ -2863,7 +3497,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
2863
3497
|
this.applying = false;
|
|
2864
3498
|
}
|
|
2865
3499
|
this.notify();
|
|
2866
|
-
_optionalChain([this, 'access',
|
|
3500
|
+
_optionalChain([this, 'access', _92 => _92.onChange, 'optionalCall', _93 => _93(project)]);
|
|
2867
3501
|
}
|
|
2868
3502
|
commit(label, next) {
|
|
2869
3503
|
const before = this.project;
|
|
@@ -2927,7 +3561,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
2927
3561
|
}
|
|
2928
3562
|
/** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
|
|
2929
3563
|
select(clips, options = {}) {
|
|
2930
|
-
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess',
|
|
3564
|
+
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _94 => _94.filter, 'call', _95 => _95((id) => findVideoClip(this.project, id))]), () => ( []));
|
|
2931
3565
|
let next;
|
|
2932
3566
|
if (options.add) {
|
|
2933
3567
|
next = [...this.selection];
|
|
@@ -3008,7 +3642,7 @@ var VideoEditorController = (_class4 = class {
|
|
|
3008
3642
|
toEdl(options = {}) {
|
|
3009
3643
|
return toVideoEdl(this.project, options);
|
|
3010
3644
|
}
|
|
3011
|
-
},
|
|
3645
|
+
}, _class5);
|
|
3012
3646
|
|
|
3013
3647
|
// src/video-editor/tools.ts
|
|
3014
3648
|
var round = (ms) => Math.round(ms * 10) / 10;
|
|
@@ -3221,14 +3855,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3221
3855
|
return null;
|
|
3222
3856
|
};
|
|
3223
3857
|
const placed = (o) => ({
|
|
3224
|
-
..._optionalChain([o, 'access',
|
|
3225
|
-
..._optionalChain([o, 'access',
|
|
3226
|
-
..._optionalChain([o, 'access',
|
|
3858
|
+
..._optionalChain([o, 'access', _96 => _96.created, 'optionalAccess', _97 => _97.length]) ? { created: o.created } : {},
|
|
3859
|
+
..._optionalChain([o, 'access', _98 => _98.trimmed, 'optionalAccess', _99 => _99.length]) ? { trimmed: o.trimmed } : {},
|
|
3860
|
+
..._optionalChain([o, 'access', _100 => _100.removed, 'optionalAccess', _101 => _101.length]) ? { removed: o.removed } : {},
|
|
3227
3861
|
...o.rippled ? { rippled: o.rippled } : {}
|
|
3228
3862
|
});
|
|
3229
3863
|
const keysOut = (p, clip, property) => {
|
|
3230
3864
|
const c = clipOut(p, clip);
|
|
3231
|
-
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess',
|
|
3865
|
+
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _102 => _102.transform, 'optionalAccess', _103 => _103[property]]) : _optionalChain([c, 'optionalAccess', _104 => _104[property]]);
|
|
3232
3866
|
return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
|
|
3233
3867
|
};
|
|
3234
3868
|
const getProject = {
|
|
@@ -3526,7 +4160,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3526
4160
|
const at = findVideoClip(p, id);
|
|
3527
4161
|
if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
|
|
3528
4162
|
const { clip } = at;
|
|
3529
|
-
if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access',
|
|
4163
|
+
if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _105 => _105.assets, 'access', _106 => _106.find, 'call', _107 => _107((a) => a.id === clip.asset), 'optionalAccess', _108 => _108.kind]) === "audio")
|
|
3530
4164
|
throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
|
|
3531
4165
|
}
|
|
3532
4166
|
};
|
|
@@ -3547,7 +4181,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3547
4181
|
gradeTargets(clips);
|
|
3548
4182
|
const grade = videoGradePreset(preset, _nullishCoalesce(amount, () => ( 1)));
|
|
3549
4183
|
const edits = clips.map((clip) => grade ? { op: "update_clip", clip, set: { grade } } : { op: "update_clip", clip, remove: ["grade"] });
|
|
3550
|
-
return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call',
|
|
4184
|
+
return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _109 => _109(r.project, id), 'optionalAccess', _110 => _110.clip, 'access', _111 => _111.grade]), () => ( null)) })) }));
|
|
3551
4185
|
}
|
|
3552
4186
|
};
|
|
3553
4187
|
const copyGrade = {
|
|
@@ -3614,7 +4248,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3614
4248
|
handler: ({ track, from, to, limit }) => {
|
|
3615
4249
|
const all = timelineWords(track).filter((w) => w.end > (_nullishCoalesce(from, () => ( 0))) && w.start < (_nullishCoalesce(to, () => ( Infinity))));
|
|
3616
4250
|
const words = all.slice(0, _nullishCoalesce(limit, () => ( 1e3)));
|
|
3617
|
-
return { track: _nullishCoalesce(_optionalChain([words, 'access',
|
|
4251
|
+
return { track: _nullishCoalesce(_optionalChain([words, 'access', _112 => _112[0], 'optionalAccess', _113 => _113.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
|
|
3618
4252
|
}
|
|
3619
4253
|
};
|
|
3620
4254
|
const findText = {
|
|
@@ -3671,6 +4305,83 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3671
4305
|
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
4306
|
}
|
|
3673
4307
|
};
|
|
4308
|
+
const rampTarget = (id) => {
|
|
4309
|
+
const at = findVideoClip(project(), id);
|
|
4310
|
+
if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
|
|
4311
|
+
const { clip } = at;
|
|
4312
|
+
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.`);
|
|
4313
|
+
if (_optionalChain([project, 'call', _114 => _114(), 'access', _115 => _115.assets, 'access', _116 => _116.find, 'call', _117 => _117((a) => a.id === clip.asset), 'optionalAccess', _118 => _118.kind]) === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
|
|
4314
|
+
return clip;
|
|
4315
|
+
};
|
|
4316
|
+
const rampOut = (r, id) => {
|
|
4317
|
+
const outcome2 = r.outcomes[r.outcomes.length - 1];
|
|
4318
|
+
return { clip: clipOut(r.project, id), ...outcome2.rippled ? { rippled: outcome2.rippled } : {}, duration: videoProjectDuration(r.project) };
|
|
4319
|
+
};
|
|
4320
|
+
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." };
|
|
4321
|
+
const setSpeedRamp = {
|
|
4322
|
+
name: "set_video_speed_ramp",
|
|
4323
|
+
title: "Ramp the speed",
|
|
4324
|
+
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).`,
|
|
4325
|
+
inputSchema: object(
|
|
4326
|
+
{
|
|
4327
|
+
clip: CLIP_ID,
|
|
4328
|
+
segments: {
|
|
4329
|
+
type: "array",
|
|
4330
|
+
minItems: 1,
|
|
4331
|
+
maxItems: 100,
|
|
4332
|
+
description: "The stretches in order, from the clip's start.",
|
|
4333
|
+
items: object(
|
|
4334
|
+
{
|
|
4335
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "Length of the stretch on the timeline, in ms." },
|
|
4336
|
+
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." },
|
|
4337
|
+
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." }
|
|
4338
|
+
},
|
|
4339
|
+
["duration", "speed"],
|
|
4340
|
+
"One stretch."
|
|
4341
|
+
)
|
|
4342
|
+
},
|
|
4343
|
+
ripple: RIPPLE_RAMP
|
|
4344
|
+
},
|
|
4345
|
+
["clip", "segments"]
|
|
4346
|
+
),
|
|
4347
|
+
annotations: { title: "Ramp the speed", ...WRITE_IDEMPOTENT },
|
|
4348
|
+
handler: ({ clip, segments, ripple }) => {
|
|
4349
|
+
const edit = videoSpeedRampEdit(rampTarget(clip), segments, _nullishCoalesce(ripple, () => ( true)));
|
|
4350
|
+
return apply(edit, "Speed ramp", (r) => ({ ...rampOut(r, clip), source: Math.round(videoSpeedRampKeys(segments).source) }));
|
|
4351
|
+
}
|
|
4352
|
+
};
|
|
4353
|
+
const addFreeze = {
|
|
4354
|
+
name: "add_video_freeze_frame",
|
|
4355
|
+
title: "Add a freeze frame",
|
|
4356
|
+
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.",
|
|
4357
|
+
inputSchema: object(
|
|
4358
|
+
{
|
|
4359
|
+
clip: CLIP_ID,
|
|
4360
|
+
at: MS("Timeline time of the frame to hold (inside the clip)"),
|
|
4361
|
+
duration: { type: "number", exclusiveMinimum: 0, description: "How long to hold it, in ms." },
|
|
4362
|
+
ripple: RIPPLE_RAMP
|
|
4363
|
+
},
|
|
4364
|
+
["clip", "at", "duration"]
|
|
4365
|
+
),
|
|
4366
|
+
annotations: { title: "Add a freeze frame", ...WRITE },
|
|
4367
|
+
handler: ({ clip, at, duration, ripple }) => {
|
|
4368
|
+
const c = rampTarget(clip);
|
|
4369
|
+
const t = at - c.start;
|
|
4370
|
+
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.`);
|
|
4371
|
+
return apply(videoFreezeFrameEdit(c, t, duration, _nullishCoalesce(ripple, () => ( true))), "Freeze frame", (r) => rampOut(r, clip));
|
|
4372
|
+
}
|
|
4373
|
+
};
|
|
4374
|
+
const removeRamp = {
|
|
4375
|
+
name: "remove_video_speed_ramp",
|
|
4376
|
+
title: "Remove a speed ramp",
|
|
4377
|
+
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.',
|
|
4378
|
+
inputSchema: object({ clip: CLIP_ID, duration: { type: "number", exclusiveMinimum: 0, description: "New length in ms (default: unchanged)." }, ripple: RIPPLE_RAMP }, ["clip"]),
|
|
4379
|
+
annotations: { title: "Remove a speed ramp", ...WRITE },
|
|
4380
|
+
handler: ({ clip, duration, ripple }) => {
|
|
4381
|
+
rampTarget(clip);
|
|
4382
|
+
return apply({ op: "set_time_remap", clip, keys: null, ...duration !== void 0 ? { duration } : {}, ripple: _nullishCoalesce(ripple, () => ( true)) }, "Remove speed ramp", (r) => rampOut(r, clip));
|
|
4383
|
+
}
|
|
4384
|
+
};
|
|
3674
4385
|
const setTransition = {
|
|
3675
4386
|
name: "set_video_transition",
|
|
3676
4387
|
title: "Set a transition",
|
|
@@ -3781,7 +4492,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3781
4492
|
};
|
|
3782
4493
|
const EDIT_OP = {
|
|
3783
4494
|
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 }.`,
|
|
4495
|
+
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
4496
|
properties: { op: { type: "string", enum: VIDEO_EDIT_OPS.filter((o) => o !== "replace_project"), description: "Which edit." } },
|
|
3786
4497
|
required: ["op"]
|
|
3787
4498
|
};
|
|
@@ -3888,8 +4599,47 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
3888
4599
|
return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((n2, x) => n2 + x.end - x.start, 0)) };
|
|
3889
4600
|
}
|
|
3890
4601
|
};
|
|
3891
|
-
const
|
|
3892
|
-
|
|
4602
|
+
const pixelsAt = async (time) => {
|
|
4603
|
+
if (options.framePixels) {
|
|
4604
|
+
const px = await options.framePixels(time);
|
|
4605
|
+
if (!px) throw new Error(`The host could not render the frame at ${Math.round(time)} ms; use render_video_frame to look at it instead.`);
|
|
4606
|
+
return px;
|
|
4607
|
+
}
|
|
4608
|
+
const feed = controller.previewPixels;
|
|
4609
|
+
if (!feed.attached) throw new Error("No preview is showing this project, so there are no pixels to measure. The host must show a <VideoPreview> for this editor (or pass framePixels to the tool provider); until then use get_video_frame and render_video_frame.");
|
|
4610
|
+
const state = controller.getState();
|
|
4611
|
+
if (Math.abs(time - state.time) < 0.5) return (await feed.next(state.time, options.framePixelsTimeout)).pixels;
|
|
4612
|
+
const { time: before, playing } = state;
|
|
4613
|
+
if (playing) controller.pause();
|
|
4614
|
+
controller.seek(time);
|
|
4615
|
+
try {
|
|
4616
|
+
return (await feed.next(controller.getState().time, options.framePixelsTimeout)).pixels;
|
|
4617
|
+
} finally {
|
|
4618
|
+
controller.seek(before);
|
|
4619
|
+
if (playing) controller.play();
|
|
4620
|
+
}
|
|
4621
|
+
};
|
|
4622
|
+
const getScopes = {
|
|
4623
|
+
name: "get_video_scopes",
|
|
4624
|
+
title: "Measure a frame with scopes",
|
|
4625
|
+
description: "Measures the rendered picture at a moment as colour scopes would, without changing the project: luma (0 = black, 1 = white) mean, median, min, max and 1st / 99th percentiles; the share of pixels crushed to black or blown to white (and per channel); saturation; the average colour (a cast shows as a tint); a coarse luma and RGB histogram; the luma spread of vertical strips left to right (a waveform); the main hue and strength on a vectorscope; and notes on what to fix. Use it before and after grading (update_video_clip grade, apply_video_grade_preset) to check exposure and colour. Needs the pixels the host draws: fails with a reason when no preview is showing.",
|
|
4626
|
+
inputSchema: object({
|
|
4627
|
+
time: MS("Time from the start to measure (default: the playhead). Another time is shown briefly and the playhead put back"),
|
|
4628
|
+
bins: { type: "integer", minimum: 2, maximum: 64, description: "Histogram steps, darkest first (default 16)." },
|
|
4629
|
+
columns: { type: "integer", minimum: 1, maximum: 32, description: "Waveform strips across the picture, left to right (default 8)." }
|
|
4630
|
+
}),
|
|
4631
|
+
annotations: { title: "Measure a frame with scopes", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
4632
|
+
handler: async ({ time, bins, columns }) => {
|
|
4633
|
+
const p = project();
|
|
4634
|
+
const end = videoProjectDuration(p);
|
|
4635
|
+
const t = _nullishCoalesce(time, () => ( controller.getState().time));
|
|
4636
|
+
if (time !== void 0 && time > end) throw new Error(`Time ${time} ms is past the end of the project (${end} ms).`);
|
|
4637
|
+
const pixels = await pixelsAt(t);
|
|
4638
|
+
return { time: t, timecode: formatVideoTimecode(t, p.fps), width: pixels.width, height: pixels.height, ...summarizeVideoScopes(pixels, { bins, columns }) };
|
|
4639
|
+
}
|
|
4640
|
+
};
|
|
4641
|
+
const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, getScopes, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
|
|
4642
|
+
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
4643
|
return {
|
|
3894
4644
|
id: _nullishCoalesce(options.id, () => ( "video")),
|
|
3895
4645
|
namespace: options.namespace,
|
|
@@ -3975,7 +4725,7 @@ function videoClipLabel(clip, asset) {
|
|
|
3975
4725
|
if (clip.name) return clip.name;
|
|
3976
4726
|
if (clip.type === "text") return clip.text.split("\n")[0];
|
|
3977
4727
|
if (clip.type === "color") return clip.color;
|
|
3978
|
-
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
4728
|
+
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _119 => _119.name]), () => ( _optionalChain([asset, 'optionalAccess', _120 => _120.src, 'access', _121 => _121.replace, 'call', _122 => _122(/^.*[\\/]/, "")]))), () => ( clip.asset));
|
|
3979
4729
|
const speed = videoClipSpeed(clip);
|
|
3980
4730
|
return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
|
|
3981
4731
|
}
|
|
@@ -4019,7 +4769,7 @@ function videoAssetDropEdit(project, assetId, options = {}) {
|
|
|
4019
4769
|
const asset = videoAssetMap(project).get(assetId);
|
|
4020
4770
|
if (!asset) return null;
|
|
4021
4771
|
const kind = asset.kind === "audio" ? "audio" : "video";
|
|
4022
|
-
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access',
|
|
4772
|
+
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _123 => _123.tracks, 'access', _124 => _124.find, 'call', _125 => _125((t) => t.kind === kind && !t.locked), 'optionalAccess', _126 => _126.id])));
|
|
4023
4773
|
if (!track) return null;
|
|
4024
4774
|
return { op: "add_clip", track, clip: { type: "media", asset: asset.id, ...options.start !== void 0 ? { start: options.start } : {} }, ...options.ripple ? { ripple: true } : {}, ...options.mode && options.mode !== "refuse" ? { mode: options.mode } : {} };
|
|
4025
4775
|
}
|
|
@@ -4068,14 +4818,14 @@ var KEY_LABELS = {
|
|
|
4068
4818
|
scale: { label: "Scale", step: 0.05 },
|
|
4069
4819
|
rotation: { label: "Rotation", step: 1 }
|
|
4070
4820
|
};
|
|
4071
|
-
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access',
|
|
4821
|
+
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _127 => _127.transform, 'optionalAccess', _128 => _128[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
|
|
4072
4822
|
var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
|
|
4073
4823
|
function videoKeyframeRows(project, clipId, time) {
|
|
4074
4824
|
const at = findVideoClip(project, clipId);
|
|
4075
4825
|
if (!at) return [];
|
|
4076
4826
|
const { clip } = at;
|
|
4077
4827
|
const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
|
|
4078
|
-
const visual = !(_optionalChain([asset, 'optionalAccess',
|
|
4828
|
+
const visual = !(_optionalChain([asset, 'optionalAccess', _129 => _129.kind]) === "audio" || at.track.kind === "audio");
|
|
4079
4829
|
const audible = clip.type === "media" && videoAssetHasSound(asset);
|
|
4080
4830
|
const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
|
|
4081
4831
|
const clipTime = playheadInClip(clip, time);
|
|
@@ -4289,5 +5039,33 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
|
|
|
4289
5039
|
|
|
4290
5040
|
|
|
4291
5041
|
|
|
4292
|
-
|
|
5042
|
+
|
|
5043
|
+
|
|
5044
|
+
|
|
5045
|
+
|
|
5046
|
+
|
|
5047
|
+
|
|
5048
|
+
|
|
5049
|
+
|
|
5050
|
+
|
|
5051
|
+
|
|
5052
|
+
|
|
5053
|
+
|
|
5054
|
+
|
|
5055
|
+
|
|
5056
|
+
|
|
5057
|
+
|
|
5058
|
+
|
|
5059
|
+
|
|
5060
|
+
|
|
5061
|
+
|
|
5062
|
+
|
|
5063
|
+
|
|
5064
|
+
|
|
5065
|
+
|
|
5066
|
+
|
|
5067
|
+
|
|
5068
|
+
|
|
5069
|
+
|
|
5070
|
+
exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_BLEND_MODE_LABELS = VIDEO_BLEND_MODE_LABELS; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_CLIP_ANIMATION_LABELS = VIDEO_CLIP_ANIMATION_LABELS; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_FILLERS = VIDEO_DEFAULT_FILLERS; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_SCOPE_KINDS = VIDEO_SCOPE_KINDS; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_VECTORSCOPE_TARGETS = VIDEO_VECTORSCOPE_TARGETS; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.VideoPreviewPixels = VideoPreviewPixels; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = applyVideoGrade; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoProject = checkVideoProject; exports.checkVideoTimeRemap = checkVideoTimeRemap; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = findVideoClip; exports.findVideoFillers = findVideoFillers; exports.findVideoGaps = findVideoGaps; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.mergeVideoRanges = mergeVideoRanges; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.normalizeVideoWord = normalizeVideoWord; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTimeRemap = parseVideoTimeRemap; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; exports.readVideoScopesPanel = readVideoScopesPanel; exports.removeVideoKey = removeVideoKey; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.setVideoKey = setVideoKey; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortVideoKeys = sortVideoKeys; exports.sortedVideoClips = sortedVideoClips; exports.splitVideoAnimatable = splitVideoAnimatable; exports.summarizeVideoScopes = summarizeVideoScopes; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoChroma = videoChroma; exports.videoClipAfter = videoClipAfter; exports.videoClipAnimated = videoClipAnimated; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = videoClipSoundGain; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSpeedAt = videoClipSpeedAt; exports.videoClipTint = videoClipTint; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoExposureNotes = videoExposureNotes; exports.videoExposureStats = videoExposureStats; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoFreezeFrameEdit = videoFreezeFrameEdit; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradePreset = videoGradePreset; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoHistogram = videoHistogram; exports.videoHistogramImage = videoHistogramImage; exports.videoHourFrames = videoHourFrames; exports.videoIrisRadius = videoIrisRadius; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoLuma = videoLuma; exports.videoLumaWaveform = videoLumaWaveform; exports.videoOverlap = videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoScopeImage = videoScopeImage; exports.videoSlowMotionRamp = videoSlowMotionRamp; exports.videoSnapPoints = videoSnapPoints; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.videoTextStyle = videoTextStyle; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoTimeRemapSpan = videoTimeRemapSpan; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoVectorscope = videoVectorscope; exports.videoVectorscopeAngle = videoVectorscopeAngle; exports.videoVectorscopeImage = videoVectorscopeImage; exports.videoVectorscopePoint = videoVectorscopePoint; exports.videoWaveformImage = videoWaveformImage; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange;
|
|
4293
5071
|
//# sourceMappingURL=index.cjs.map
|