@pixerate/editor 0.81.0 → 0.83.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/agent/index.cjs +36 -7
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +5 -3
- package/dist/agent/index.d.ts +5 -3
- package/dist/agent/index.js +43 -14
- package/dist/agent/index.js.map +1 -1
- package/dist/animation-editor/index.d.cts +1 -1
- package/dist/animation-editor/index.d.ts +1 -1
- package/dist/bridge/index.d.cts +3 -3
- package/dist/bridge/index.d.ts +3 -3
- package/dist/{bridge-client-C5dRjrz5.d.ts → bridge-client-CMGfnjTl.d.ts} +1 -1
- package/dist/{bridge-client-C5OziCRK.d.cts → bridge-client-VpnICVxP.d.cts} +1 -1
- package/dist/canvas/index.cjs +235 -104
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +59 -1
- package/dist/canvas/index.d.ts +59 -1
- package/dist/canvas/index.js +234 -103
- package/dist/canvas/index.js.map +1 -1
- package/dist/capture-DFEMFmVt.d.cts +133 -0
- package/dist/capture-wwNGaW05.d.ts +133 -0
- package/dist/chart/index.d.cts +1 -1
- package/dist/chart/index.d.ts +1 -1
- package/dist/{chunk-K73CKYUM.cjs → chunk-EI2MOVQ3.cjs} +198 -3
- package/dist/chunk-EI2MOVQ3.cjs.map +1 -0
- package/dist/chunk-FURT6ZFZ.cjs +1 -0
- package/dist/chunk-FURT6ZFZ.cjs.map +1 -0
- package/dist/{chunk-GPWNYQ7R.js → chunk-O2W2SP3C.js} +197 -2
- package/dist/chunk-O2W2SP3C.js.map +1 -0
- package/dist/chunk-REOJJIY3.js +201 -0
- package/dist/chunk-REOJJIY3.js.map +1 -0
- package/dist/chunk-RJ2BFKEN.js +1 -0
- package/dist/chunk-RJ2BFKEN.js.map +1 -0
- package/dist/chunk-VJUVNERT.cjs +201 -0
- package/dist/chunk-VJUVNERT.cjs.map +1 -0
- package/dist/commands/index.d.cts +1 -1
- package/dist/commands/index.d.ts +1 -1
- package/dist/diagram/index.d.cts +1 -1
- package/dist/diagram/index.d.ts +1 -1
- package/dist/dom-capture-7Wvf6Tyg.d.ts +59 -0
- package/dist/dom-capture-GyYuE-zq.d.cts +59 -0
- package/dist/generation/index.d.cts +2 -2
- package/dist/generation/index.d.ts +2 -2
- package/dist/image-editor/index.cjs +97 -1
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +24 -2
- package/dist/image-editor/index.d.ts +24 -2
- package/dist/image-editor/index.js +96 -0
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{registry-DIpIaCkH.d.cts → registry-BqA1AZt0.d.cts} +1 -1
- package/dist/{registry-7AZFuand.d.ts → registry-D-4fit9C.d.ts} +1 -1
- package/dist/tools/index.cjs +33 -4
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.d.cts +4 -2
- package/dist/tools/index.d.ts +4 -2
- package/dist/tools/index.js +43 -14
- package/dist/video-editor/index.cjs +557 -99
- package/dist/video-editor/index.cjs.map +1 -1
- package/dist/video-editor/index.d.cts +211 -6
- package/dist/video-editor/index.d.ts +211 -6
- package/dist/video-editor/index.js +493 -35
- package/dist/video-editor/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/pixerate-canvas/SKILL.md +1 -0
- package/skills/pixerate-image-editor/SKILL.md +1 -0
- package/skills/pixerate-video-editor/SKILL.md +24 -6
- package/dist/chunk-GPWNYQ7R.js.map +0 -1
- package/dist/chunk-K73CKYUM.cjs.map +0 -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;
|
|
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;
|
|
2
2
|
|
|
3
3
|
var _chunkWAJQKWQUcjs = require('../chunk-WAJQKWQU.cjs');
|
|
4
4
|
|
|
@@ -15,6 +15,7 @@ require('../chunk-2FHLHZAL.cjs');
|
|
|
15
15
|
var VIDEO_FITS = ["contain", "cover", "fill", "none"];
|
|
16
16
|
var VIDEO_TRANSITION_TYPES = ["crossfade", "dip", "wipe"];
|
|
17
17
|
var VIDEO_WIPE_DIRECTIONS = ["left", "right", "up", "down"];
|
|
18
|
+
var VIDEO_FADE_CURVES = ["linear", "equal-power", "s-curve", "exponential", "logarithmic"];
|
|
18
19
|
var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
|
|
19
20
|
var VIDEO_CLIP_TYPES = ["media", "text", "color"];
|
|
20
21
|
|
|
@@ -324,7 +325,9 @@ function parseVideoClip(v, path = "clip") {
|
|
|
324
325
|
in: num(v.in, `${path}.in`, { optional: true, min: 0 }),
|
|
325
326
|
speed: num(v.speed, `${path}.speed`, { optional: true, min: 0.05, max: 20 }),
|
|
326
327
|
volume: parseVideoAnimatable(v.volume, `${path}.volume`, { optional: true, min: 0, max: 4 }),
|
|
327
|
-
muted: bool(v.muted, `${path}.muted`)
|
|
328
|
+
muted: bool(v.muted, `${path}.muted`),
|
|
329
|
+
fadeInCurve: oneOf(v.fadeInCurve, VIDEO_FADE_CURVES, `${path}.fadeInCurve`),
|
|
330
|
+
fadeOutCurve: oneOf(v.fadeOutCurve, VIDEO_FADE_CURVES, `${path}.fadeOutCurve`)
|
|
328
331
|
});
|
|
329
332
|
if (type === "text")
|
|
330
333
|
return compact({
|
|
@@ -426,15 +429,41 @@ function snapToVideoFrame(time, fps) {
|
|
|
426
429
|
return Math.round(frame * 1e3 / fps * 1e3) / 1e3;
|
|
427
430
|
}
|
|
428
431
|
var videoFrames = (time, fps) => Math.round(time * fps / 1e3);
|
|
429
|
-
function
|
|
432
|
+
function isVideoDropFrameRate(fps) {
|
|
433
|
+
const nominal = Math.round(fps);
|
|
434
|
+
return (nominal === 30 || nominal === 60) && Math.abs(fps - nominal * 1e3 / 1001) < 5e-3;
|
|
435
|
+
}
|
|
436
|
+
var exactVideoRate = (fps) => {
|
|
437
|
+
const nominal = Math.round(fps);
|
|
438
|
+
return nominal !== fps && Math.abs(fps - nominal * 1e3 / 1001) < 5e-3 ? nominal * 1e3 / 1001 : fps;
|
|
439
|
+
};
|
|
440
|
+
var videoTimecodeFrames = (time, fps) => Math.max(0, Math.round(time * exactVideoRate(fps) / 1e3));
|
|
441
|
+
function formatVideoTimecode(time, fps, dropFrame = isVideoDropFrameRate(fps)) {
|
|
442
|
+
return formatVideoTimecodeFrames(videoTimecodeFrames(time, fps), fps, dropFrame);
|
|
443
|
+
}
|
|
444
|
+
function formatVideoTimecodeFrames(frames, fps, dropFrame = isVideoDropFrameRate(fps)) {
|
|
430
445
|
const base = Math.round(fps);
|
|
431
|
-
|
|
446
|
+
let total = Math.max(0, Math.round(frames));
|
|
447
|
+
const drop = dropFrame && isVideoDropFrameRate(fps);
|
|
448
|
+
if (drop) {
|
|
449
|
+
const skip = base / 15;
|
|
450
|
+
const perMinute = base * 60 - skip;
|
|
451
|
+
const perTen = perMinute * 10 + skip;
|
|
452
|
+
const tens = Math.floor(total / perTen);
|
|
453
|
+
const rest = total % perTen;
|
|
454
|
+
total += skip * 9 * tens + (rest > skip ? skip * Math.floor((rest - skip) / perMinute) : 0);
|
|
455
|
+
}
|
|
432
456
|
const ff = total % base;
|
|
433
457
|
const secs = Math.floor(total / base);
|
|
434
458
|
const pad = (n2) => String(n2).padStart(2, "0");
|
|
435
|
-
return `${pad(Math.floor(secs / 3600))}:${pad(Math.floor(secs / 60) % 60)}:${pad(secs % 60)}
|
|
459
|
+
return `${pad(Math.floor(secs / 3600))}:${pad(Math.floor(secs / 60) % 60)}:${pad(secs % 60)}${drop ? ";" : ":"}${pad(ff)}`;
|
|
436
460
|
}
|
|
461
|
+
var videoHourFrames = (fps, dropFrame = isVideoDropFrameRate(fps)) => {
|
|
462
|
+
const base = Math.round(fps);
|
|
463
|
+
return dropFrame && isVideoDropFrameRate(fps) ? base * 3600 - base / 15 * 54 : base * 3600;
|
|
464
|
+
};
|
|
437
465
|
var formatVideoSeconds = (time) => `${(time / 1e3).toFixed(2)} s`;
|
|
466
|
+
var videoAssetHasSound = (asset) => !!asset && (asset.kind === "audio" || asset.kind === "video" && asset.hasAudio !== false);
|
|
438
467
|
function videoAssetMap(project) {
|
|
439
468
|
return new Map(project.assets.map((a) => [a.id, a]));
|
|
440
469
|
}
|
|
@@ -472,13 +501,13 @@ function videoClipHandles(clip, assets) {
|
|
|
472
501
|
const speed = videoClipSpeed(clip);
|
|
473
502
|
const range = videoClipSourceRange(clip);
|
|
474
503
|
const before = range.in / speed;
|
|
475
|
-
if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess',
|
|
504
|
+
if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess', _2 => _2.kind]) === "image" ? Infinity : before, after: Infinity };
|
|
476
505
|
return { before, after: Math.max(0, (asset.duration - range.out) / speed) };
|
|
477
506
|
}
|
|
478
507
|
function videoClipOverhang(track, clip) {
|
|
479
508
|
const lead = clip.transition && clip.transition.type !== "dip" && videoClipBefore(track, clip) ? clip.transition.duration / 2 : 0;
|
|
480
509
|
const next = videoClipAfter(track, clip);
|
|
481
|
-
const tail = _optionalChain([next, 'optionalAccess',
|
|
510
|
+
const tail = _optionalChain([next, 'optionalAccess', _3 => _3.transition]) && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
|
|
482
511
|
return { lead, tail };
|
|
483
512
|
}
|
|
484
513
|
function videoSnapPoints(project, options = {}) {
|
|
@@ -539,7 +568,7 @@ function videoClipKeyframes(clip) {
|
|
|
539
568
|
const out = [
|
|
540
569
|
{ property: "opacity", keys: videoKeysOf(clip.opacity) },
|
|
541
570
|
{ property: "volume", keys: clip.type === "media" ? videoKeysOf(clip.volume) : [] },
|
|
542
|
-
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access',
|
|
571
|
+
...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _4 => _4.transform, 'optionalAccess', _5 => _5[p]])) }))
|
|
543
572
|
];
|
|
544
573
|
return out.filter((o) => o.keys.length);
|
|
545
574
|
}
|
|
@@ -692,7 +721,7 @@ function clipAnimatables(clip) {
|
|
|
692
721
|
{ get: () => c.opacity, set: (v) => c.opacity = v },
|
|
693
722
|
{ get: () => c.volume, set: (v) => c.volume = v }
|
|
694
723
|
];
|
|
695
|
-
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access',
|
|
724
|
+
for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _6 => _6.transform, 'optionalAccess', _7 => _7[p]]), set: (v) => c.transform && (c.transform[p] = v) });
|
|
696
725
|
return out;
|
|
697
726
|
}
|
|
698
727
|
function retimeClipKeys(clip, fn) {
|
|
@@ -701,7 +730,7 @@ function retimeClipKeys(clip, fn) {
|
|
|
701
730
|
if (isVideoKeyframes(v)) a.set(fn(v));
|
|
702
731
|
}
|
|
703
732
|
}
|
|
704
|
-
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", "transition", "in", "speed", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
733
|
+
var OPTIONAL_CLIP_FIELDS = ["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "transition", "in", "speed", "volume", "muted", "color", "fontSize", "fontFamily", "fontWeight", "background"];
|
|
705
734
|
var TIME_FIELDS = { slip_clip: ["delta"], roll_edit: ["delta"], slide_clip: ["delta"], lift_range: ["from", "to"], extract_range: ["from", "to"], split_at: ["at"] };
|
|
706
735
|
var Editor = (_class = class {
|
|
707
736
|
constructor(p) {;_class.prototype.__init.call(this);
|
|
@@ -740,7 +769,7 @@ var Editor = (_class = class {
|
|
|
740
769
|
kindFits(track, clip) {
|
|
741
770
|
const asset = clip.type === "media" ? this.asset(clip.asset) : null;
|
|
742
771
|
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.`);
|
|
743
|
-
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess',
|
|
772
|
+
if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _8 => _8.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
|
|
744
773
|
}
|
|
745
774
|
/** Moves every clip on `track` starting at or after `from` by `delta`. */
|
|
746
775
|
shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
|
|
@@ -782,7 +811,7 @@ var Editor = (_class = class {
|
|
|
782
811
|
}
|
|
783
812
|
/** Moves a media clip's in point by `delta` of timeline time (stills, titles and colours have none). */
|
|
784
813
|
moveIn(clip, delta) {
|
|
785
|
-
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call',
|
|
814
|
+
if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _9 => _9(this.p), 'access', _10 => _10.get, 'call', _11 => _11(clip.asset), 'optionalAccess', _12 => _12.kind]) === "image") return;
|
|
786
815
|
clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * videoClipSpeed(clip));
|
|
787
816
|
if (clip.in < 1e-9) delete clip.in;
|
|
788
817
|
}
|
|
@@ -797,6 +826,7 @@ var Editor = (_class = class {
|
|
|
797
826
|
const right = { ...structuredClone(clip), id: _nullishCoalesce(id, () => ( freshId(`${clip.id}-`, this.ids()))), start: at, duration: end - at };
|
|
798
827
|
delete right.transition;
|
|
799
828
|
delete right.fadeIn;
|
|
829
|
+
delete right.fadeInCurve;
|
|
800
830
|
const leftValues = clipAnimatables(clip);
|
|
801
831
|
clipAnimatables(right).forEach((r, i) => {
|
|
802
832
|
const [l, rr] = splitVideoAnimatable(leftValues[i].get(), at - clip.start);
|
|
@@ -806,6 +836,7 @@ var Editor = (_class = class {
|
|
|
806
836
|
this.moveIn(right, at - clip.start);
|
|
807
837
|
clip.duration = at - clip.start;
|
|
808
838
|
delete clip.fadeOut;
|
|
839
|
+
if (clip.type === "media") delete clip.fadeOutCurve;
|
|
809
840
|
track.clips.push(right);
|
|
810
841
|
track.clips.sort((a, b) => a.start - b.start);
|
|
811
842
|
return right;
|
|
@@ -1044,7 +1075,7 @@ var Editor = (_class = class {
|
|
|
1044
1075
|
if (clip.type === "media") {
|
|
1045
1076
|
const asset = assets.get(clip.asset);
|
|
1046
1077
|
const out = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
|
|
1047
|
-
if (_optionalChain([asset, 'optionalAccess',
|
|
1078
|
+
if (_optionalChain([asset, 'optionalAccess', _13 => _13.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
|
|
1048
1079
|
refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
|
|
1049
1080
|
}
|
|
1050
1081
|
if (e.ripple) {
|
|
@@ -1100,7 +1131,7 @@ var Editor = (_class = class {
|
|
|
1100
1131
|
let stretch = 1;
|
|
1101
1132
|
if (at.clip.type === "media" && !("duration" in set)) {
|
|
1102
1133
|
const was = videoClipSpeed(at.clip);
|
|
1103
|
-
const now = "speed" in set ? set.speed : _optionalChain([e, 'access',
|
|
1134
|
+
const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _14 => _14.remove, 'optionalAccess', _15 => _15.includes, 'call', _16 => _16("speed")]) ? 1 : was;
|
|
1104
1135
|
if (now !== was) {
|
|
1105
1136
|
set.duration = at.clip.duration * was / now;
|
|
1106
1137
|
stretch = was / now;
|
|
@@ -1196,7 +1227,7 @@ var Editor = (_class = class {
|
|
|
1196
1227
|
this.trimHead(after, delta);
|
|
1197
1228
|
}
|
|
1198
1229
|
track.clips.sort((a, b) => a.start - b.start);
|
|
1199
|
-
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess',
|
|
1230
|
+
return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _17 => _17.id]), clip.id, _optionalChain([after, 'optionalAccess', _18 => _18.id])].filter((x) => !!x), track: track.id, delta });
|
|
1200
1231
|
}
|
|
1201
1232
|
case "lift_range":
|
|
1202
1233
|
case "extract_range": {
|
|
@@ -1227,7 +1258,7 @@ var Editor = (_class = class {
|
|
|
1227
1258
|
const limits = VIDEO_KEYFRAME_LIMITS[e.property];
|
|
1228
1259
|
const transform = VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
|
|
1229
1260
|
const c = clip;
|
|
1230
|
-
const current = transform ? _optionalChain([c, 'access',
|
|
1261
|
+
const current = transform ? _optionalChain([c, 'access', _19 => _19.transform, 'optionalAccess', _20 => _20[e.property]]) : c[e.property];
|
|
1231
1262
|
let value;
|
|
1232
1263
|
if (e.keys === null) {
|
|
1233
1264
|
const v = videoValueAt(current, 0, limits.default);
|
|
@@ -1309,7 +1340,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1309
1340
|
editor.run(edit);
|
|
1310
1341
|
} catch (error) {
|
|
1311
1342
|
const message = error.message;
|
|
1312
|
-
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess',
|
|
1343
|
+
return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _21 => _21.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
|
|
1313
1344
|
}
|
|
1314
1345
|
}
|
|
1315
1346
|
let next;
|
|
@@ -1328,7 +1359,7 @@ function applyVideoEdits(project, edits) {
|
|
|
1328
1359
|
|
|
1329
1360
|
// src/video-editor/evaluate.ts
|
|
1330
1361
|
function videoTransformAt(transform, clipTime = 0) {
|
|
1331
|
-
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess',
|
|
1362
|
+
return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _22 => _22.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _23 => _23.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _24 => _24.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _25 => _25.rotation]), clipTime, 0) };
|
|
1332
1363
|
}
|
|
1333
1364
|
function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
|
|
1334
1365
|
let w = size.width;
|
|
@@ -1356,19 +1387,39 @@ function videoTitleFrame(project, clip, clipTime = 0) {
|
|
|
1356
1387
|
return { time: clip.start, width: project.width, height: project.height, background: "rgba(0,0,0,0)", layers: [layer], audio: [] };
|
|
1357
1388
|
}
|
|
1358
1389
|
var clamp01 = (v) => Math.min(1, Math.max(0, v));
|
|
1359
|
-
function
|
|
1390
|
+
function videoFadeCurveGain(curve, x) {
|
|
1391
|
+
const t = clamp01(x);
|
|
1392
|
+
switch (curve) {
|
|
1393
|
+
case "equal-power":
|
|
1394
|
+
return Math.sin(t * Math.PI / 2);
|
|
1395
|
+
case "s-curve":
|
|
1396
|
+
return (1 - Math.cos(t * Math.PI)) / 2;
|
|
1397
|
+
case "exponential":
|
|
1398
|
+
return t * t * t;
|
|
1399
|
+
case "logarithmic":
|
|
1400
|
+
return 1 - (1 - t) * (1 - t);
|
|
1401
|
+
default:
|
|
1402
|
+
return t;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function fadeGain(clip, time, sound = false) {
|
|
1360
1406
|
let g = 1;
|
|
1361
|
-
|
|
1362
|
-
if (clip.
|
|
1407
|
+
const shape = sound && clip.type === "media" ? clip : void 0;
|
|
1408
|
+
if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _26 => _26.fadeInCurve]), (time - clip.start) / clip.fadeIn);
|
|
1409
|
+
if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _27 => _27.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
|
|
1363
1410
|
return g;
|
|
1364
1411
|
}
|
|
1412
|
+
function videoClipSoundGain(clip, clipTime) {
|
|
1413
|
+
const volume = clip.type === "media" ? videoValueAt(clip.volume, clipTime, 1) : 1;
|
|
1414
|
+
return volume * fadeGain(clip, clip.start + Math.min(Math.max(clipTime, 0), clip.duration), true);
|
|
1415
|
+
}
|
|
1365
1416
|
function activeOnTrack(track, time) {
|
|
1366
1417
|
const out = [];
|
|
1367
1418
|
for (const clip of sortedVideoClips(track)) {
|
|
1368
1419
|
const before = clip.transition ? videoClipBefore(track, clip) : null;
|
|
1369
1420
|
const lead = before ? clip.transition.duration / 2 : 0;
|
|
1370
1421
|
const next = videoClipAfter(track, clip);
|
|
1371
|
-
const tail = _optionalChain([next, 'optionalAccess',
|
|
1422
|
+
const tail = _optionalChain([next, 'optionalAccess', _28 => _28.transition]) ? next.transition.duration / 2 : 0;
|
|
1372
1423
|
if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
|
|
1373
1424
|
const a = { clip };
|
|
1374
1425
|
if (lead && time < clip.start + lead) a.incoming = { progress: clamp01((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
|
|
@@ -1385,17 +1436,18 @@ function videoFrameAt(project, time) {
|
|
|
1385
1436
|
for (let ti = project.tracks.length - 1; ti >= 0; ti--) {
|
|
1386
1437
|
const track = project.tracks[ti];
|
|
1387
1438
|
for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
|
|
1388
|
-
if (_optionalChain([incoming, 'optionalAccess',
|
|
1389
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1390
|
-
const
|
|
1439
|
+
if (_optionalChain([incoming, 'optionalAccess', _29 => _29.type]) === "dip" && incoming.progress < 0.5) continue;
|
|
1440
|
+
if (_optionalChain([outgoing, 'optionalAccess', _30 => _30.type]) === "dip" && outgoing.progress >= 0.5) continue;
|
|
1441
|
+
const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
|
|
1442
|
+
const fade = fadeGain(clip, inside);
|
|
1391
1443
|
const level = (t, into) => t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress;
|
|
1392
1444
|
const mix = (incoming ? level(incoming, true) : 1) * (outgoing ? level(outgoing, false) : 1);
|
|
1393
1445
|
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
1394
1446
|
const sourceTime = sourceTimeAt(clip, time);
|
|
1395
1447
|
const clipTime = time - clip.start;
|
|
1396
|
-
const audible =
|
|
1448
|
+
const audible = videoAssetHasSound(asset);
|
|
1397
1449
|
if (audible && clip.type === "media" && !clip.muted && !track.muted) {
|
|
1398
|
-
const gain = videoValueAt(clip.volume, clipTime, 1) * (_nullishCoalesce(track.volume, () => ( 1))) *
|
|
1450
|
+
const gain = videoValueAt(clip.volume, clipTime, 1) * (_nullishCoalesce(track.volume, () => ( 1))) * fadeGain(clip, inside, true) * mix;
|
|
1399
1451
|
audio.push({ clip: clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeed(clip) });
|
|
1400
1452
|
}
|
|
1401
1453
|
if (track.kind !== "video" || track.hidden) continue;
|
|
@@ -1408,11 +1460,11 @@ function videoFrameAt(project, time) {
|
|
|
1408
1460
|
else if (incoming.type === "wipe") wipe = { direction: _nullishCoalesce(incoming.transition.direction, () => ( "right")), progress: incoming.progress };
|
|
1409
1461
|
else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
|
|
1410
1462
|
}
|
|
1411
|
-
if (_optionalChain([outgoing, 'optionalAccess',
|
|
1463
|
+
if (_optionalChain([outgoing, 'optionalAccess', _31 => _31.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
|
|
1412
1464
|
const size = clip.type === "media" ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
|
|
1413
1465
|
const fit = _nullishCoalesce(clip.fit, () => ( (clip.type === "media" ? "contain" : "fill")));
|
|
1414
1466
|
const rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, clip.transform, clipTime);
|
|
1415
|
-
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([clip, 'access',
|
|
1467
|
+
const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp01(opacity), rect, rotation: videoValueAt(_optionalChain([clip, 'access', _32 => _32.transform, 'optionalAccess', _33 => _33.rotation]), clipTime, 0) };
|
|
1416
1468
|
if (wipe) layer.wipe = wipe;
|
|
1417
1469
|
if (clip.type === "media") Object.assign(layer, { asset: asset.id, assetKind: asset.kind, src: asset.src, sourceTime });
|
|
1418
1470
|
if (clip.type === "text") layer.text = videoTextStyle(clip);
|
|
@@ -1424,6 +1476,223 @@ function videoFrameAt(project, time) {
|
|
|
1424
1476
|
return { time, width: project.width, height: project.height, background: _nullishCoalesce(project.background, () => ( "#000000")), layers, audio };
|
|
1425
1477
|
}
|
|
1426
1478
|
|
|
1479
|
+
// src/video-editor/audio.ts
|
|
1480
|
+
var VIDEO_PEAK_BLOCK = 32;
|
|
1481
|
+
function buildVideoAudioPeaks(channels, sampleRate, blockSize = VIDEO_PEAK_BLOCK) {
|
|
1482
|
+
if (!(sampleRate > 0)) throw new Error("The sample rate must be above 0.");
|
|
1483
|
+
const length = channels.reduce((n2, c) => Math.max(n2, c.length), 0);
|
|
1484
|
+
const blocks = Math.max(1, Math.ceil(length / blockSize));
|
|
1485
|
+
const level0 = new Float32Array(blocks * 2);
|
|
1486
|
+
for (let b = 0; b < blocks; b++) {
|
|
1487
|
+
let lo = Infinity;
|
|
1488
|
+
let hi = -Infinity;
|
|
1489
|
+
const end = Math.min(length, (b + 1) * blockSize);
|
|
1490
|
+
for (const channel of channels) {
|
|
1491
|
+
for (let i = b * blockSize; i < Math.min(end, channel.length); i++) {
|
|
1492
|
+
const v = channel[i];
|
|
1493
|
+
if (!Number.isFinite(v)) continue;
|
|
1494
|
+
if (v < lo) lo = v;
|
|
1495
|
+
if (v > hi) hi = v;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
level0[2 * b] = lo === Infinity ? 0 : lo;
|
|
1499
|
+
level0[2 * b + 1] = hi === -Infinity ? 0 : hi;
|
|
1500
|
+
}
|
|
1501
|
+
const levels = [level0];
|
|
1502
|
+
while (levels[levels.length - 1].length > 2) {
|
|
1503
|
+
const below = levels[levels.length - 1];
|
|
1504
|
+
const n2 = below.length / 2;
|
|
1505
|
+
const up = new Float32Array(Math.ceil(n2 / 4) * 2);
|
|
1506
|
+
for (let j = 0; j < up.length / 2; j++) {
|
|
1507
|
+
let lo = Infinity;
|
|
1508
|
+
let hi = -Infinity;
|
|
1509
|
+
for (let i = 4 * j; i < Math.min(n2, 4 * j + 4); i++) {
|
|
1510
|
+
lo = Math.min(lo, below[2 * i]);
|
|
1511
|
+
hi = Math.max(hi, below[2 * i + 1]);
|
|
1512
|
+
}
|
|
1513
|
+
up[2 * j] = lo;
|
|
1514
|
+
up[2 * j + 1] = hi;
|
|
1515
|
+
}
|
|
1516
|
+
levels.push(up);
|
|
1517
|
+
}
|
|
1518
|
+
return { sampleRate, length, blockSize, levels };
|
|
1519
|
+
}
|
|
1520
|
+
var videoAudioPeaksDuration = (peaks) => peaks.length * 1e3 / peaks.sampleRate;
|
|
1521
|
+
function videoAudioPeakRange(peaks, from, to) {
|
|
1522
|
+
const blocks = peaks.levels[0].length / 2;
|
|
1523
|
+
let a = Math.max(0, Math.floor(from * peaks.sampleRate / 1e3 / peaks.blockSize));
|
|
1524
|
+
let b = Math.min(blocks, Math.ceil(to * peaks.sampleRate / 1e3 / peaks.blockSize));
|
|
1525
|
+
if (b <= a) {
|
|
1526
|
+
if (a >= blocks || to < 0) return { min: 0, max: 0 };
|
|
1527
|
+
b = a + 1;
|
|
1528
|
+
}
|
|
1529
|
+
let min = Infinity;
|
|
1530
|
+
let max = -Infinity;
|
|
1531
|
+
const take = (level, i) => {
|
|
1532
|
+
if (2 * i + 1 >= level.length) return;
|
|
1533
|
+
if (level[2 * i] < min) min = level[2 * i];
|
|
1534
|
+
if (level[2 * i + 1] > max) max = level[2 * i + 1];
|
|
1535
|
+
};
|
|
1536
|
+
for (let k = 0; a < b; k++) {
|
|
1537
|
+
const level = peaks.levels[k];
|
|
1538
|
+
if (k === peaks.levels.length - 1) {
|
|
1539
|
+
for (let i = a; i < b; i++) take(level, i);
|
|
1540
|
+
break;
|
|
1541
|
+
}
|
|
1542
|
+
while (a < b && a % 4) take(level, a++);
|
|
1543
|
+
while (a < b && b % 4) take(level, --b);
|
|
1544
|
+
a /= 4;
|
|
1545
|
+
b /= 4;
|
|
1546
|
+
}
|
|
1547
|
+
return min === Infinity ? { min: 0, max: 0 } : { min, max };
|
|
1548
|
+
}
|
|
1549
|
+
function videoAudioPeakSpans(peaks, from, to, columns) {
|
|
1550
|
+
const n2 = Math.max(0, Math.floor(columns));
|
|
1551
|
+
const out = new Float32Array(n2 * 2);
|
|
1552
|
+
const step = (to - from) / Math.max(1, n2);
|
|
1553
|
+
for (let c = 0; c < n2; c++) {
|
|
1554
|
+
const { min, max } = videoAudioPeakRange(peaks, from + c * step, from + (c + 1) * step);
|
|
1555
|
+
out[2 * c] = min;
|
|
1556
|
+
out[2 * c + 1] = max;
|
|
1557
|
+
}
|
|
1558
|
+
return out;
|
|
1559
|
+
}
|
|
1560
|
+
function videoClipWaveform(clip, peaks, columns) {
|
|
1561
|
+
const n2 = Math.max(0, Math.floor(columns));
|
|
1562
|
+
const out = new Float32Array(n2 * 2);
|
|
1563
|
+
const speed = _nullishCoalesce(clip.speed, () => ( 1));
|
|
1564
|
+
const start = _nullishCoalesce(clip.in, () => ( 0));
|
|
1565
|
+
const step = clip.duration / Math.max(1, n2);
|
|
1566
|
+
for (let c = 0; c < n2; c++) {
|
|
1567
|
+
const { min, max } = videoAudioPeakRange(peaks, start + c * step * speed, start + (c + 1) * step * speed);
|
|
1568
|
+
const gain = videoClipSoundGain(clip, (c + 0.5) * step);
|
|
1569
|
+
out[2 * c] = min * gain;
|
|
1570
|
+
out[2 * c + 1] = max * gain;
|
|
1571
|
+
}
|
|
1572
|
+
return out;
|
|
1573
|
+
}
|
|
1574
|
+
function videoWaveformPath(spans, width, height) {
|
|
1575
|
+
const n2 = spans.length / 2;
|
|
1576
|
+
if (!n2 || width <= 0 || height <= 0) return "";
|
|
1577
|
+
const mid = height / 2;
|
|
1578
|
+
const y = (v) => (mid - Math.max(-1, Math.min(1, v)) * mid).toFixed(1);
|
|
1579
|
+
const x = (i) => ((i + 0.5) * (width / n2)).toFixed(1);
|
|
1580
|
+
const floor = 0.5 / mid;
|
|
1581
|
+
const top = [];
|
|
1582
|
+
const bottom = [];
|
|
1583
|
+
for (let i = 0; i < n2; i++) top.push(`${i ? "L" : "M"}${x(i)} ${y(Math.max(spans[2 * i + 1], floor))}`);
|
|
1584
|
+
for (let i = n2 - 1; i >= 0; i--) bottom.push(`L${x(i)} ${y(Math.min(spans[2 * i], -floor))}`);
|
|
1585
|
+
return `${top.join("")}${bottom.join("")}Z`;
|
|
1586
|
+
}
|
|
1587
|
+
function videoClipWaveformPath(clip, peaks, width, height) {
|
|
1588
|
+
const columns = Math.max(1, Math.min(2e3, Math.ceil(width / 2)));
|
|
1589
|
+
return videoWaveformPath(videoClipWaveform(clip, peaks, columns), width, height);
|
|
1590
|
+
}
|
|
1591
|
+
var videoPeakDb = (value) => Math.abs(value) > 1e-5 ? Math.max(-100, Math.round(200 * Math.log10(Math.abs(value))) / 10) : -100;
|
|
1592
|
+
function videoAudioLevels(peaks, from, to, buckets) {
|
|
1593
|
+
const spans = videoAudioPeakSpans(peaks, from, to, buckets);
|
|
1594
|
+
const step = (to - from) / Math.max(1, buckets);
|
|
1595
|
+
return Array.from({ length: spans.length / 2 }, (_, i) => ({ start: from + i * step, end: from + (i + 1) * step, peakDb: videoPeakDb(Math.max(Math.abs(spans[2 * i]), Math.abs(spans[2 * i + 1]))) }));
|
|
1596
|
+
}
|
|
1597
|
+
function findVideoAudioSilences(peaks, from, to, options = {}) {
|
|
1598
|
+
const threshold = _nullishCoalesce(options.thresholdDb, () => ( -40));
|
|
1599
|
+
const minDuration = _nullishCoalesce(options.minDuration, () => ( 300));
|
|
1600
|
+
const resolution = Math.max(1, _nullishCoalesce(options.resolution, () => ( 10)));
|
|
1601
|
+
const buckets = Math.max(1, Math.ceil((to - from) / resolution));
|
|
1602
|
+
const out = [];
|
|
1603
|
+
let run = null;
|
|
1604
|
+
const levels = videoAudioLevels(peaks, from, to, buckets);
|
|
1605
|
+
levels.forEach((l, i) => {
|
|
1606
|
+
const quiet = l.peakDb < threshold;
|
|
1607
|
+
if (quiet && run === null) run = l.start;
|
|
1608
|
+
if ((!quiet || i === levels.length - 1) && run !== null) {
|
|
1609
|
+
const end = quiet ? l.end : l.start;
|
|
1610
|
+
if (end - run >= minDuration) out.push({ start: run, end });
|
|
1611
|
+
run = null;
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
return out;
|
|
1615
|
+
}
|
|
1616
|
+
async function decodeVideoAudioPeaks(src, options = {}) {
|
|
1617
|
+
const Offline = globalThis.OfflineAudioContext;
|
|
1618
|
+
if (!Offline || typeof fetch === "undefined") throw new Error("Audio can only be decoded in a browser with Web Audio.");
|
|
1619
|
+
const response = await fetch(src, options.signal ? { signal: options.signal } : {});
|
|
1620
|
+
if (!response.ok) throw new Error(`"${src}" could not be loaded (${response.status}).`);
|
|
1621
|
+
const data = await response.arrayBuffer();
|
|
1622
|
+
let buffer;
|
|
1623
|
+
try {
|
|
1624
|
+
buffer = await new Offline(1, 1, 44100).decodeAudioData(data);
|
|
1625
|
+
} catch (e2) {
|
|
1626
|
+
throw new Error(`"${src}" has no sound this browser can decode.`);
|
|
1627
|
+
}
|
|
1628
|
+
return buildVideoAudioPeaks(Array.from({ length: buffer.numberOfChannels }, (_, i) => buffer.getChannelData(i)), buffer.sampleRate);
|
|
1629
|
+
}
|
|
1630
|
+
var VideoAudioPeaksCache = (_class2 = class {
|
|
1631
|
+
constructor(loader = (asset) => decodeVideoAudioPeaks(asset.src)) {;_class2.prototype.__init2.call(this);_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);_class2.prototype.__init5.call(this);
|
|
1632
|
+
this.loader = loader;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
__init2() {this.ready = /* @__PURE__ */ new Map()}
|
|
1636
|
+
__init3() {this.pending = /* @__PURE__ */ new Map()}
|
|
1637
|
+
__init4() {this.failed = /* @__PURE__ */ new Map()}
|
|
1638
|
+
__init5() {this.listeners = /* @__PURE__ */ new Set()}
|
|
1639
|
+
/** The peaks if ready, without starting to load them. */
|
|
1640
|
+
peek(asset) {
|
|
1641
|
+
return this.ready.get(asset.src);
|
|
1642
|
+
}
|
|
1643
|
+
/** The peaks if ready; otherwise `undefined`, and loading starts (once) unless it failed. */
|
|
1644
|
+
get(asset) {
|
|
1645
|
+
const peaks = this.ready.get(asset.src);
|
|
1646
|
+
if (!peaks && !this.failed.has(asset.src)) void this.load(asset).catch(() => {
|
|
1647
|
+
});
|
|
1648
|
+
return peaks;
|
|
1649
|
+
}
|
|
1650
|
+
/** Why loading an asset's peaks failed, if it did. */
|
|
1651
|
+
error(asset) {
|
|
1652
|
+
return this.failed.get(asset.src);
|
|
1653
|
+
}
|
|
1654
|
+
/** The peaks, loading them if needed. Rejects with a readable error. */
|
|
1655
|
+
load(asset) {
|
|
1656
|
+
const ready = this.ready.get(asset.src);
|
|
1657
|
+
if (ready) return Promise.resolve(ready);
|
|
1658
|
+
const failed = this.failed.get(asset.src);
|
|
1659
|
+
if (failed) return Promise.reject(new Error(failed));
|
|
1660
|
+
let pending = this.pending.get(asset.src);
|
|
1661
|
+
if (!pending) {
|
|
1662
|
+
pending = this.loader(asset).then(
|
|
1663
|
+
(peaks) => {
|
|
1664
|
+
this.pending.delete(asset.src);
|
|
1665
|
+
this.ready.set(asset.src, peaks);
|
|
1666
|
+
this.emit();
|
|
1667
|
+
return peaks;
|
|
1668
|
+
},
|
|
1669
|
+
(e) => {
|
|
1670
|
+
this.pending.delete(asset.src);
|
|
1671
|
+
const message = `The sound of "${asset.id}" could not be read: ${e instanceof Error ? e.message : String(e)}`;
|
|
1672
|
+
this.failed.set(asset.src, message);
|
|
1673
|
+
this.emit();
|
|
1674
|
+
throw new Error(message);
|
|
1675
|
+
}
|
|
1676
|
+
);
|
|
1677
|
+
this.pending.set(asset.src, pending);
|
|
1678
|
+
}
|
|
1679
|
+
return pending;
|
|
1680
|
+
}
|
|
1681
|
+
/** Uses peaks the host already has (computed earlier or stored). */
|
|
1682
|
+
set(src, peaks) {
|
|
1683
|
+
this.ready.set(src, peaks);
|
|
1684
|
+
this.failed.delete(src);
|
|
1685
|
+
this.emit();
|
|
1686
|
+
}
|
|
1687
|
+
subscribe(fn) {
|
|
1688
|
+
this.listeners.add(fn);
|
|
1689
|
+
return () => this.listeners.delete(fn);
|
|
1690
|
+
}
|
|
1691
|
+
emit() {
|
|
1692
|
+
for (const fn of this.listeners) fn();
|
|
1693
|
+
}
|
|
1694
|
+
}, _class2);
|
|
1695
|
+
|
|
1427
1696
|
// src/video-editor/render.ts
|
|
1428
1697
|
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1429
1698
|
var n = (v) => String(Math.round(v * 100) / 100);
|
|
@@ -1557,20 +1826,22 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1) {
|
|
|
1557
1826
|
}
|
|
1558
1827
|
|
|
1559
1828
|
// src/video-editor/export.ts
|
|
1560
|
-
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
1829
|
+
var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _34 => _34.name]), () => ( _optionalChain([asset, 'optionalAccess', _35 => _35.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
|
|
1561
1830
|
function toVideoEdl(project, options = {}) {
|
|
1562
1831
|
const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
|
|
1563
1832
|
if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
|
|
1564
1833
|
const fps = project.fps;
|
|
1565
|
-
const
|
|
1566
|
-
const
|
|
1834
|
+
const dropFrame = (_nullishCoalesce(options.dropFrame, () => ( true))) && isVideoDropFrameRate(fps);
|
|
1835
|
+
const tc = (ms) => formatVideoTimecode(ms, fps, dropFrame);
|
|
1836
|
+
const rec0 = options.recordStart !== void 0 ? videoTimecodeFrames(options.recordStart, fps) : videoHourFrames(fps, dropFrame);
|
|
1837
|
+
const rtc = (ms) => formatVideoTimecodeFrames(rec0 + videoTimecodeFrames(ms, fps), fps, dropFrame);
|
|
1567
1838
|
const assets = videoAssetMap(project);
|
|
1568
1839
|
const channel = track.kind === "video" ? "V" : "A";
|
|
1569
|
-
const lines = [`TITLE: ${(_nullishCoalesce(_nullishCoalesce(options.title, () => ( project.name)), () => ( "Untitled"))).toUpperCase()}`,
|
|
1840
|
+
const lines = [`TITLE: ${(_nullishCoalesce(_nullishCoalesce(options.title, () => ( project.name)), () => ( "Untitled"))).toUpperCase()}`, `FCM: ${dropFrame ? "DROP FRAME" : "NON-DROP FRAME"}`, ""];
|
|
1570
1841
|
let event = 0;
|
|
1571
1842
|
const num2 = () => String(++event).padStart(3, "0");
|
|
1572
1843
|
const sourceAt = (clip, time) => clip.type === "media" ? (_nullishCoalesce(clip.in, () => ( 0))) + (time - clip.start) * videoClipSpeed(clip) : time - clip.start;
|
|
1573
|
-
const row = (n2, reel, kind, srcIn, srcOut, recIn, recOut) => `${n2} ${reel.padEnd(8)} ${channel.padEnd(5)} ${kind.padEnd(8)} ${tc(srcIn)} ${tc(srcOut)} ${
|
|
1844
|
+
const row = (n2, reel, kind, srcIn, srcOut, recIn, recOut) => `${n2} ${reel.padEnd(8)} ${channel.padEnd(5)} ${kind.padEnd(8)} ${tc(srcIn)} ${tc(srcOut)} ${rtc(recIn)} ${rtc(recOut)}`;
|
|
1574
1845
|
for (const clip of sortedVideoClips(track)) {
|
|
1575
1846
|
const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
|
|
1576
1847
|
const reel = clip.type === "media" ? reelName(asset) : "BL";
|
|
@@ -1590,14 +1861,41 @@ function toVideoEdl(project, options = {}) {
|
|
|
1590
1861
|
lines.push(row(n2, reel, `${t.type === "wipe" ? "W001" : "D"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
1591
1862
|
} else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
|
|
1592
1863
|
if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
|
|
1593
|
-
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
1864
|
+
if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _36 => _36.name]), () => ( _optionalChain([asset, 'optionalAccess', _37 => _37.src]))), () => ( clip.asset))}`);
|
|
1594
1865
|
if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
|
|
1595
1866
|
if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
|
|
1596
1867
|
lines.push("");
|
|
1597
1868
|
}
|
|
1598
1869
|
return lines.join("\n");
|
|
1599
1870
|
}
|
|
1871
|
+
function loudnormFilter(target = {}, measure = false) {
|
|
1872
|
+
const I = _nullishCoalesce(target.integrated, () => ( -14));
|
|
1873
|
+
const TP = _nullishCoalesce(target.truePeak, () => ( -1));
|
|
1874
|
+
const LRA = _nullishCoalesce(target.range, () => ( 11));
|
|
1875
|
+
if (!(I >= -70 && I <= -5)) throw new Error(`The loudness target must be between -70 and -5 LUFS (got ${I}); -14 suits streaming, -23 broadcast.`);
|
|
1876
|
+
if (!(TP >= -9 && TP <= 0)) throw new Error(`The true peak ceiling must be between -9 and 0 dBTP (got ${TP}); -1 is usual.`);
|
|
1877
|
+
if (!(LRA >= 1 && LRA <= 50)) throw new Error(`The loudness range must be between 1 and 50 LU (got ${LRA}).`);
|
|
1878
|
+
const parts = [`loudnorm=I=${I}:TP=${TP}:LRA=${LRA}`];
|
|
1879
|
+
const m = target.measured;
|
|
1880
|
+
if (m && !measure) {
|
|
1881
|
+
for (const [k, v] of Object.entries(m)) if (!Number.isFinite(v)) throw new Error(`The measured ${k} is not a number (${v}); measure again, or leave "measured" out for a one-pass normalisation.`);
|
|
1882
|
+
parts.push(`measured_I=${m.integrated}:measured_TP=${m.truePeak}:measured_LRA=${m.range}:measured_thresh=${m.threshold}:offset=${m.offset}:linear=true`);
|
|
1883
|
+
}
|
|
1884
|
+
if (measure) parts.push("print_format=json");
|
|
1885
|
+
return `${parts.join(":")},aresample=48000`;
|
|
1886
|
+
}
|
|
1887
|
+
function parseVideoLoudnessMeasurement(output) {
|
|
1888
|
+
const blocks = output.match(/\{[^{}]*"input_i"[^{}]*\}/g);
|
|
1889
|
+
if (!blocks) throw new Error("No loudness measurement found: pass the stderr of the command from toVideoLoudnessMeasure.");
|
|
1890
|
+
const json = JSON.parse(blocks[blocks.length - 1]);
|
|
1891
|
+
const n2 = (k) => Number(json[k]);
|
|
1892
|
+
const m = { integrated: n2("input_i"), truePeak: n2("input_tp"), range: n2("input_lra"), threshold: n2("input_thresh"), offset: n2("target_offset") };
|
|
1893
|
+
if (!Number.isFinite(m.integrated)) throw new Error("The mix is silent, so its loudness cannot be normalised.");
|
|
1894
|
+
return m;
|
|
1895
|
+
}
|
|
1600
1896
|
var sec = (ms) => String(Math.round(ms) / 1e3);
|
|
1897
|
+
var AFADE_CURVES = { linear: "tri", "equal-power": "qsin", "s-curve": "hsin", exponential: "cub", logarithmic: "ipar" };
|
|
1898
|
+
var afadeCurve = (curve) => curve && curve !== "linear" ? `:curve=${AFADE_CURVES[curve]}` : "";
|
|
1601
1899
|
var even = (v) => Math.max(2, 2 * Math.round(v / 2));
|
|
1602
1900
|
var ffmpegFilterValue = (s) => s.replace(/[\\':%]/g, (c) => `\\${c}`).replace(/[\\'[\],;]/g, (c) => `\\${c}`);
|
|
1603
1901
|
var color = (c) => {
|
|
@@ -1622,6 +1920,12 @@ function atempo(speed) {
|
|
|
1622
1920
|
return out;
|
|
1623
1921
|
}
|
|
1624
1922
|
function toVideoFfmpeg(project, options = {}) {
|
|
1923
|
+
return buildFfmpeg(project, options, false);
|
|
1924
|
+
}
|
|
1925
|
+
function toVideoLoudnessMeasure(project, options = {}) {
|
|
1926
|
+
return buildFfmpeg(project, { ...options, loudness: { ...options.loudness, measured: void 0 } }, true);
|
|
1927
|
+
}
|
|
1928
|
+
function buildFfmpeg(project, options, measure) {
|
|
1625
1929
|
const notes = [];
|
|
1626
1930
|
const assets = videoAssetMap(project);
|
|
1627
1931
|
const D = videoProjectDuration(project);
|
|
@@ -1630,7 +1934,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1630
1934
|
const H = even(project.height);
|
|
1631
1935
|
const fps = project.fps;
|
|
1632
1936
|
const inputs = [];
|
|
1633
|
-
const graph = [`color=c=${color(_nullishCoalesce(project.background, () => ( "#000000")))}:s=${W}x${H}:r=${fps}:d=${sec(D)},format=yuva420p[base]`];
|
|
1937
|
+
const graph = measure ? [] : [`color=c=${color(_nullishCoalesce(project.background, () => ( "#000000")))}:s=${W}x${H}:r=${fps}:d=${sec(D)},format=yuva420p[base]`];
|
|
1634
1938
|
const audio = [];
|
|
1635
1939
|
let cur = "base";
|
|
1636
1940
|
let usesDrawtext = false;
|
|
@@ -1661,8 +1965,8 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1661
1965
|
continue;
|
|
1662
1966
|
}
|
|
1663
1967
|
let index;
|
|
1664
|
-
if (asset && asset.kind !== "audio" && track.kind === "video" && !track.hidden) index = input(clip, asset, lead, tail);
|
|
1665
|
-
const audible =
|
|
1968
|
+
if (!measure && asset && asset.kind !== "audio" && track.kind === "video" && !track.hidden) index = input(clip, asset, lead, tail);
|
|
1969
|
+
const audible = videoAssetHasSound(asset);
|
|
1666
1970
|
if (audible && clip.type === "media" && !clip.muted && !track.muted) {
|
|
1667
1971
|
const ai = _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
|
|
1668
1972
|
const trackGain = _nullishCoalesce(track.volume, () => ( 1));
|
|
@@ -1672,21 +1976,21 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1672
1976
|
if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}`);
|
|
1673
1977
|
if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}`);
|
|
1674
1978
|
if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}`);
|
|
1675
|
-
const after = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && _optionalChain([c, 'access',
|
|
1979
|
+
const after = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && _optionalChain([c, 'access', _38 => _38.transition, 'optionalAccess', _39 => _39.type]) === "dip");
|
|
1676
1980
|
if (after) chain2.push(`afade=t=out:st=${sec(span - after.transition.duration / 2)}:d=${sec(after.transition.duration / 2)}`);
|
|
1677
|
-
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}`);
|
|
1678
|
-
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}`);
|
|
1981
|
+
if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
|
|
1982
|
+
if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
|
|
1679
1983
|
chain2.push(`adelay=${Math.round(t0)}:all=1`);
|
|
1680
1984
|
graph.push(`[${ai}:a]${chain2.join(",")}[a${audio.length}]`);
|
|
1681
1985
|
audio.push(`[a${audio.length}]`);
|
|
1682
1986
|
}
|
|
1683
|
-
if (track.kind !== "video" || track.hidden) continue;
|
|
1987
|
+
if (measure || track.kind !== "video" || track.hidden) continue;
|
|
1684
1988
|
const enable = `enable='between(t,${sec(t0)},${sec(t0 + span)})'`;
|
|
1685
1989
|
const next = `v${k++}`;
|
|
1686
1990
|
const onTimeline = `(t*1000-${Math.round(clip.start)})`;
|
|
1687
1991
|
const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
|
|
1688
1992
|
const tf = clip.transform;
|
|
1689
|
-
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
1993
|
+
const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _40 => _40[p]])));
|
|
1690
1994
|
if (clip.type === "text" && options.titleImage) {
|
|
1691
1995
|
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 })]);
|
|
1692
1996
|
index = inputs.length - 1;
|
|
@@ -1695,7 +1999,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1695
1999
|
const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
|
|
1696
2000
|
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("*");
|
|
1697
2001
|
const coord = (axis) => {
|
|
1698
|
-
const v = _optionalChain([tf, 'optionalAccess',
|
|
2002
|
+
const v = _optionalChain([tf, 'optionalAccess', _41 => _41[axis]]);
|
|
1699
2003
|
const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
|
|
1700
2004
|
return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
|
|
1701
2005
|
};
|
|
@@ -1703,7 +2007,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1703
2007
|
const opts = [
|
|
1704
2008
|
`text=${ffmpegFilterValue(clip.text)}`,
|
|
1705
2009
|
options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
|
|
1706
|
-
isVideoKeyframes(_optionalChain([tf, 'optionalAccess',
|
|
2010
|
+
isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _42 => _42.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _43 => _43.scale]), () => ( 1))))}`,
|
|
1707
2011
|
`fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
|
|
1708
2012
|
coord("x"),
|
|
1709
2013
|
coord("y"),
|
|
@@ -1711,7 +2015,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1711
2015
|
...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
|
|
1712
2016
|
`enable='between(t,${sec(clip.start)},${sec(end)})'`
|
|
1713
2017
|
];
|
|
1714
|
-
if (_optionalChain([tf, 'optionalAccess',
|
|
2018
|
+
if (_optionalChain([tf, 'optionalAccess', _44 => _44.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
|
|
1715
2019
|
graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
|
|
1716
2020
|
cur = next;
|
|
1717
2021
|
continue;
|
|
@@ -1727,7 +2031,7 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1727
2031
|
source = `[${index}:v]`;
|
|
1728
2032
|
chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
|
|
1729
2033
|
}
|
|
1730
|
-
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess',
|
|
2034
|
+
if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _45 => _45.scale]))) {
|
|
1731
2035
|
const s = expr(placed.scale, onChain("t"), 1, "scale");
|
|
1732
2036
|
chain.push(`scale=w='max(2,trunc(${even(rect.width)}*(${s})/2)*2)':h='max(2,trunc(${even(rect.height)}*(${s})/2)*2)':eval=frame`);
|
|
1733
2037
|
}
|
|
@@ -1744,14 +2048,14 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1744
2048
|
const test = dir === "right" ? `lt(X,W*${p})` : dir === "left" ? `gt(X,W*(1-${p}))` : dir === "down" ? `lt(Y,H*${p})` : `gt(Y,H*(1-${p}))`;
|
|
1745
2049
|
chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(${test},alpha(X,Y),0)'`);
|
|
1746
2050
|
}
|
|
1747
|
-
const rotation = _optionalChain([placed, 'optionalAccess',
|
|
2051
|
+
const rotation = _optionalChain([placed, 'optionalAccess', _46 => _46.rotation]);
|
|
1748
2052
|
if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=none:ow='hypot(iw,ih)':oh=ow`);
|
|
1749
2053
|
else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=none:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
|
|
1750
2054
|
chain.push(`setpts=PTS+${sec(t0)}/TB`);
|
|
1751
2055
|
const cx = rect.x + rect.width / 2;
|
|
1752
2056
|
const cy = rect.y + rect.height / 2;
|
|
1753
2057
|
const at = (axis, c) => {
|
|
1754
|
-
const v = _optionalChain([placed, 'optionalAccess',
|
|
2058
|
+
const v = _optionalChain([placed, 'optionalAccess', _47 => _47[axis]]);
|
|
1755
2059
|
return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2'`;
|
|
1756
2060
|
};
|
|
1757
2061
|
graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[l${next}]`);
|
|
@@ -1767,14 +2071,23 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1767
2071
|
}
|
|
1768
2072
|
}
|
|
1769
2073
|
}
|
|
1770
|
-
graph.push(`[${cur}]format=yuv420p[vout]`);
|
|
1771
|
-
if (audio.length)
|
|
1772
|
-
|
|
2074
|
+
if (!measure) graph.push(`[${cur}]format=yuv420p[vout]`);
|
|
2075
|
+
if (measure && !audio.length) throw new Error("Nothing on the timeline makes a sound, so there is no loudness to measure.");
|
|
2076
|
+
const loudness = options.loudness || measure ? `,${loudnormFilter(options.loudness, measure)}` : "";
|
|
2077
|
+
if (audio.length) graph.push(`${audio.join("")}amix=inputs=${audio.length}:normalize=0:duration=longest,apad,atrim=0:${sec(D)}${loudness}[aout]`);
|
|
2078
|
+
if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
|
|
2079
|
+
else if (options.loudness && !options.loudness.measured)
|
|
2080
|
+
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.");
|
|
2081
|
+
if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _48 => _48.get, 'call', _49 => _49(c.asset), 'optionalAccess', _50 => _50.kind]) === "video" && _optionalChain([assets, 'access', _51 => _51.get, 'call', _52 => _52(c.asset), 'optionalAccess', _53 => _53.hasAudio]) === void 0)))
|
|
1773
2082
|
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.');
|
|
1774
2083
|
if (animated.size)
|
|
1775
2084
|
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)" : ""}.`);
|
|
1776
2085
|
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.");
|
|
1777
2086
|
const filterComplex = graph.join(";");
|
|
2087
|
+
if (measure) {
|
|
2088
|
+
const args2 = ["-hide_banner", "-nostats", ...inputs.flat(), "-filter_complex", filterComplex, "-map", "[aout]", "-f", "null", "-"];
|
|
2089
|
+
return { args: args2, command: ["ffmpeg", ...args2].map(shellQuote).join(" "), filterComplex, duration: D, notes };
|
|
2090
|
+
}
|
|
1778
2091
|
const args = [
|
|
1779
2092
|
"-y",
|
|
1780
2093
|
...inputs.flat(),
|
|
@@ -1798,12 +2111,12 @@ function toVideoFfmpeg(project, options = {}) {
|
|
|
1798
2111
|
}
|
|
1799
2112
|
|
|
1800
2113
|
// src/video-editor/preview.ts
|
|
1801
|
-
var VideoPreviewEngine = (
|
|
1802
|
-
constructor(source, canvas, options = {}) {;
|
|
2114
|
+
var VideoPreviewEngine = (_class3 = class {
|
|
2115
|
+
constructor(source, canvas, options = {}) {;_class3.prototype.__init6.call(this);_class3.prototype.__init7.call(this);_class3.prototype.__init8.call(this);_class3.prototype.__init9.call(this);_class3.prototype.__init10.call(this);
|
|
1803
2116
|
this.source = source;
|
|
1804
2117
|
this.options = options;
|
|
1805
2118
|
this.canvas = canvas;
|
|
1806
|
-
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access',
|
|
2119
|
+
this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas, 'access', _54 => _54.getContext, 'optionalCall', _55 => _55("2d")]), () => ( null));
|
|
1807
2120
|
this.unsubscribe = source.subscribe(() => this.update());
|
|
1808
2121
|
this.update();
|
|
1809
2122
|
}
|
|
@@ -1811,11 +2124,11 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1811
2124
|
|
|
1812
2125
|
|
|
1813
2126
|
|
|
1814
|
-
|
|
1815
|
-
|
|
2127
|
+
__init6() {this.media = /* @__PURE__ */ new Map()}
|
|
2128
|
+
__init7() {this.images = /* @__PURE__ */ new Map()}
|
|
1816
2129
|
|
|
1817
|
-
|
|
1818
|
-
|
|
2130
|
+
__init8() {this.scheduled = false}
|
|
2131
|
+
__init9() {this.destroyed = false}
|
|
1819
2132
|
createMedia(kind) {
|
|
1820
2133
|
if (this.options.createMedia) return this.options.createMedia(kind);
|
|
1821
2134
|
const el = document.createElement(kind);
|
|
@@ -1851,8 +2164,8 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1851
2164
|
entry.media.pause();
|
|
1852
2165
|
entry.media.removeEventListener("seeked", entry.onReady);
|
|
1853
2166
|
entry.media.removeEventListener("loadeddata", entry.onReady);
|
|
1854
|
-
_optionalChain([entry, 'access',
|
|
1855
|
-
_optionalChain([entry, 'access',
|
|
2167
|
+
_optionalChain([entry, 'access', _56 => _56.media, 'access', _57 => _57.removeAttribute, 'optionalCall', _58 => _58("src")]);
|
|
2168
|
+
_optionalChain([entry, 'access', _59 => _59.media, 'access', _60 => _60.load, 'optionalCall', _61 => _61()]);
|
|
1856
2169
|
this.media.delete(entry.clip);
|
|
1857
2170
|
}
|
|
1858
2171
|
/** Brings the media elements in step with the editor and draws the frame. */
|
|
@@ -1885,7 +2198,7 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1885
2198
|
for (const layer of frame.layers) {
|
|
1886
2199
|
if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
|
|
1887
2200
|
const sound = sounding.get(layer.clip);
|
|
1888
|
-
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess',
|
|
2201
|
+
sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _62 => _62.speed]), () => ( 1)), sound ? sound.gain : null);
|
|
1889
2202
|
sounding.delete(layer.clip);
|
|
1890
2203
|
}
|
|
1891
2204
|
for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
|
|
@@ -1900,7 +2213,7 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1900
2213
|
if (!this.destroyed) this.draw(videoFrameAt(this.source.getState().project, this.source.getState().time));
|
|
1901
2214
|
});
|
|
1902
2215
|
}
|
|
1903
|
-
|
|
2216
|
+
__init10() {this.sourceFor = (layer) => {
|
|
1904
2217
|
if (layer.assetKind === "image" && layer.src) {
|
|
1905
2218
|
const img = this.image(layer.src);
|
|
1906
2219
|
return img.complete ? img : null;
|
|
@@ -1918,7 +2231,7 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1918
2231
|
if (this.canvas.width !== w) this.canvas.width = w;
|
|
1919
2232
|
if (this.canvas.height !== h) this.canvas.height = h;
|
|
1920
2233
|
drawVideoFrame(this.ctx, frame, this.sourceFor, resolution);
|
|
1921
|
-
_optionalChain([this, 'access',
|
|
2234
|
+
_optionalChain([this, 'access', _63 => _63.options, 'access', _64 => _64.onFrame, 'optionalCall', _65 => _65(frame)]);
|
|
1922
2235
|
}
|
|
1923
2236
|
/** Stops following the editor and releases every media element. */
|
|
1924
2237
|
destroy() {
|
|
@@ -1927,25 +2240,28 @@ var VideoPreviewEngine = (_class2 = class {
|
|
|
1927
2240
|
for (const entry of [...this.media.values()]) this.release(entry);
|
|
1928
2241
|
this.images.clear();
|
|
1929
2242
|
}
|
|
1930
|
-
},
|
|
2243
|
+
}, _class3);
|
|
1931
2244
|
|
|
1932
2245
|
// src/video-editor/controller.ts
|
|
1933
|
-
var VideoEditorController = (
|
|
2246
|
+
var VideoEditorController = (_class4 = class {
|
|
2247
|
+
|
|
1934
2248
|
|
|
2249
|
+
/** Sound levels per asset, loaded on demand for waveforms and agents. */
|
|
1935
2250
|
|
|
1936
2251
|
|
|
1937
2252
|
|
|
1938
|
-
|
|
2253
|
+
__init11() {this.selection = []}
|
|
1939
2254
|
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
2255
|
+
__init12() {this.error = null}
|
|
2256
|
+
__init13() {this.revision = 0}
|
|
2257
|
+
__init14() {this.applying = false}
|
|
1943
2258
|
|
|
1944
|
-
|
|
1945
|
-
|
|
2259
|
+
__init15() {this.listeners = /* @__PURE__ */ new Set()}
|
|
2260
|
+
__init16() {this.unsubscribers = []}
|
|
1946
2261
|
|
|
1947
|
-
constructor(options = {}) {;
|
|
2262
|
+
constructor(options = {}) {;_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);
|
|
1948
2263
|
this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
|
|
2264
|
+
this.audioPeaks = options.audioPeaks instanceof VideoAudioPeaksCache ? options.audioPeaks : new VideoAudioPeaksCache(options.audioPeaks);
|
|
1949
2265
|
this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
|
|
1950
2266
|
this.issues = checkVideoProject(this.project);
|
|
1951
2267
|
this.readOnly = _nullishCoalesce(options.readOnly, () => ( false));
|
|
@@ -1968,8 +2284,8 @@ var VideoEditorController = (_class3 = class {
|
|
|
1968
2284
|
this.player.pause();
|
|
1969
2285
|
this.unsubscribers.splice(0).forEach((u) => u());
|
|
1970
2286
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
2287
|
+
__init17() {this.getState = () => this.state}
|
|
2288
|
+
__init18() {this.subscribe = (listener) => {
|
|
1973
2289
|
this.connect();
|
|
1974
2290
|
this.listeners.add(listener);
|
|
1975
2291
|
return () => {
|
|
@@ -2025,7 +2341,7 @@ var VideoEditorController = (_class3 = class {
|
|
|
2025
2341
|
this.applying = false;
|
|
2026
2342
|
}
|
|
2027
2343
|
this.notify();
|
|
2028
|
-
_optionalChain([this, 'access',
|
|
2344
|
+
_optionalChain([this, 'access', _66 => _66.onChange, 'optionalCall', _67 => _67(project)]);
|
|
2029
2345
|
}
|
|
2030
2346
|
commit(label, next) {
|
|
2031
2347
|
const before = this.project;
|
|
@@ -2089,7 +2405,7 @@ var VideoEditorController = (_class3 = class {
|
|
|
2089
2405
|
}
|
|
2090
2406
|
/** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
|
|
2091
2407
|
select(clips, options = {}) {
|
|
2092
|
-
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess',
|
|
2408
|
+
const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _68 => _68.filter, 'call', _69 => _69((id) => findVideoClip(this.project, id))]), () => ( []));
|
|
2093
2409
|
let next;
|
|
2094
2410
|
if (options.add) {
|
|
2095
2411
|
next = [...this.selection];
|
|
@@ -2162,13 +2478,18 @@ var VideoEditorController = (_class3 = class {
|
|
|
2162
2478
|
toFfmpeg(options = {}) {
|
|
2163
2479
|
return toVideoFfmpeg(this.project, options);
|
|
2164
2480
|
}
|
|
2481
|
+
/** The ffmpeg command that measures the mix's loudness (see `toVideoLoudnessMeasure`). */
|
|
2482
|
+
toLoudnessMeasure(options = {}) {
|
|
2483
|
+
return toVideoLoudnessMeasure(this.project, options);
|
|
2484
|
+
}
|
|
2165
2485
|
/** One track as a CMX 3600 EDL. */
|
|
2166
2486
|
toEdl(options = {}) {
|
|
2167
2487
|
return toVideoEdl(this.project, options);
|
|
2168
2488
|
}
|
|
2169
|
-
},
|
|
2489
|
+
}, _class4);
|
|
2170
2490
|
|
|
2171
2491
|
// src/video-editor/tools.ts
|
|
2492
|
+
var round = (ms) => Math.round(ms * 10) / 10;
|
|
2172
2493
|
var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
2173
2494
|
var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
|
|
2174
2495
|
var WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
@@ -2228,7 +2549,9 @@ var MEDIA_FIELDS = {
|
|
|
2228
2549
|
in: MS("Where in the source it starts (default 0)"),
|
|
2229
2550
|
speed: { type: "number", minimum: 0.05, maximum: 20, description: "Playback speed (default 1). Changing it keeps the stretch of source, so the clip gets shorter or longer." },
|
|
2230
2551
|
volume: { type: "number", minimum: 0, maximum: 4, description: "Gain (1 = unchanged, default 1). A number replaces any keyframes; set_video_keyframes animates it." },
|
|
2231
|
-
muted: { type: "boolean", description: "Silence this clip (default false)." }
|
|
2552
|
+
muted: { type: "boolean", description: "Silence this clip (default false)." },
|
|
2553
|
+
fadeInCurve: { type: "string", enum: [...VIDEO_FADE_CURVES], description: "Shape of the sound's fade in (default linear; the picture always fades linearly): linear, equal-power (steady loudness, best for crossfades), s-curve (gentle at both ends), exponential (slow start, natural from silence) or logarithmic (fast start)." },
|
|
2554
|
+
fadeOutCurve: { type: "string", enum: [...VIDEO_FADE_CURVES], description: "Shape of the sound's fade out (default linear), the same choices as fadeInCurve; exponential sounds natural fading to silence." }
|
|
2232
2555
|
};
|
|
2233
2556
|
var TEXT_FIELDS = {
|
|
2234
2557
|
text: { type: "string", minLength: 1, description: `The title's text ("\\n" breaks lines).` },
|
|
@@ -2354,14 +2677,14 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2354
2677
|
return null;
|
|
2355
2678
|
};
|
|
2356
2679
|
const placed = (o) => ({
|
|
2357
|
-
..._optionalChain([o, 'access',
|
|
2358
|
-
..._optionalChain([o, 'access',
|
|
2359
|
-
..._optionalChain([o, 'access',
|
|
2680
|
+
..._optionalChain([o, 'access', _70 => _70.created, 'optionalAccess', _71 => _71.length]) ? { created: o.created } : {},
|
|
2681
|
+
..._optionalChain([o, 'access', _72 => _72.trimmed, 'optionalAccess', _73 => _73.length]) ? { trimmed: o.trimmed } : {},
|
|
2682
|
+
..._optionalChain([o, 'access', _74 => _74.removed, 'optionalAccess', _75 => _75.length]) ? { removed: o.removed } : {},
|
|
2360
2683
|
...o.rippled ? { rippled: o.rippled } : {}
|
|
2361
2684
|
});
|
|
2362
2685
|
const keysOut = (p, clip, property) => {
|
|
2363
2686
|
const c = clipOut(p, clip);
|
|
2364
|
-
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess',
|
|
2687
|
+
const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _76 => _76.transform, 'optionalAccess', _77 => _77[property]]) : _optionalChain([c, 'optionalAccess', _78 => _78[property]]);
|
|
2365
2688
|
return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
|
|
2366
2689
|
};
|
|
2367
2690
|
const getProject = {
|
|
@@ -2432,27 +2755,59 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2432
2755
|
return { svg };
|
|
2433
2756
|
}
|
|
2434
2757
|
};
|
|
2758
|
+
const LOUDNESS_TARGET = {
|
|
2759
|
+
integrated: { type: "number", minimum: -70, maximum: -5, description: "Integrated loudness to reach, in LUFS (default -14, what streaming services play at; -16 for podcasts, -23 for EBU R128 broadcast)." },
|
|
2760
|
+
truePeak: { type: "number", minimum: -9, maximum: 0, description: "Highest true peak, in dBTP (default -1)." },
|
|
2761
|
+
range: { type: "number", minimum: 1, maximum: 50, description: "Loudness range, in LU (default 11)." }
|
|
2762
|
+
};
|
|
2435
2763
|
const exportFfmpeg = {
|
|
2436
2764
|
name: "export_video_ffmpeg",
|
|
2437
2765
|
title: "Export an ffmpeg command",
|
|
2438
|
-
description: "Returns the ffmpeg command that renders the project to a file: { args (for spawn), command (one shell line), filterComplex, duration, notes on anything approximated }. Fails when the project has no clips.",
|
|
2766
|
+
description: "Returns the ffmpeg command that renders the project to a file: { args (for spawn), command (one shell line), filterComplex, duration, notes on anything approximated }. With loudness, the mix is normalised to a target loudness (EBU R128); add loudness.measured from a measuring pass (export_video_loudness_measure) for an exact fixed gain. Fails when the project has no clips.",
|
|
2439
2767
|
inputSchema: object({
|
|
2440
2768
|
output: { type: "string", minLength: 1, description: "Output file name (default output.mp4)." },
|
|
2441
|
-
crf: { type: "integer", minimum: 0, maximum: 51, description: "x264 quality, lower is better (default 18)." }
|
|
2769
|
+
crf: { type: "integer", minimum: 0, maximum: 51, description: "x264 quality, lower is better (default 18)." },
|
|
2770
|
+
loudness: object(
|
|
2771
|
+
{
|
|
2772
|
+
...LOUDNESS_TARGET,
|
|
2773
|
+
measured: object(
|
|
2774
|
+
{
|
|
2775
|
+
integrated: { type: "number", description: `Measured integrated loudness, LUFS (loudnorm's "input_i").` },
|
|
2776
|
+
truePeak: { type: "number", description: 'Measured true peak, dBTP ("input_tp").' },
|
|
2777
|
+
range: { type: "number", description: 'Measured loudness range, LU ("input_lra").' },
|
|
2778
|
+
threshold: { type: "number", description: 'Measured gating threshold, LUFS ("input_thresh").' },
|
|
2779
|
+
offset: { type: "number", description: 'Offset for the second pass, LU ("target_offset").' }
|
|
2780
|
+
},
|
|
2781
|
+
["integrated", "truePeak", "range", "threshold", "offset"],
|
|
2782
|
+
"What the measuring pass printed (the JSON at the end of its stderr), for a linear second pass that keeps the dynamics. Leave out for one-pass normalisation."
|
|
2783
|
+
)
|
|
2784
|
+
},
|
|
2785
|
+
[],
|
|
2786
|
+
"Normalise the mix's loudness (default: not normalised). An empty object uses -14 LUFS, -1 dBTP."
|
|
2787
|
+
)
|
|
2442
2788
|
}),
|
|
2443
2789
|
annotations: { title: "Export an ffmpeg command", ...READ },
|
|
2444
|
-
handler: ({ output, crf }) => controller.toFfmpeg({ ...output ? { output } : {}, ...crf !== void 0 ? { crf } : {} })
|
|
2790
|
+
handler: ({ output, crf, loudness }) => controller.toFfmpeg({ ...output ? { output } : {}, ...crf !== void 0 ? { crf } : {}, ...loudness ? { loudness } : {} })
|
|
2791
|
+
};
|
|
2792
|
+
const exportLoudnessMeasure = {
|
|
2793
|
+
name: "export_video_loudness_measure",
|
|
2794
|
+
title: "Export a loudness measuring command",
|
|
2795
|
+
description: "Returns the ffmpeg command for the first pass of a loudness normalisation: it mixes only the sound and prints its loudness as JSON at the end of stderr (input_i, input_tp, input_lra, input_thresh, target_offset) instead of writing a file. Pass those numbers as loudness.measured (integrated, truePeak, range, threshold, offset) to export_video_ffmpeg, with the same target. Fails when nothing makes a sound.",
|
|
2796
|
+
inputSchema: object(LOUDNESS_TARGET),
|
|
2797
|
+
annotations: { title: "Export a loudness measuring command", ...READ },
|
|
2798
|
+
handler: (target) => controller.toLoudnessMeasure({ loudness: target })
|
|
2445
2799
|
};
|
|
2446
2800
|
const exportEdl = {
|
|
2447
2801
|
name: "export_video_edl",
|
|
2448
2802
|
title: "Export an EDL",
|
|
2449
|
-
description: "Returns one track as a CMX 3600 edit decision list (text) for conforming in another editor: events with source and record timecode, dissolves, speed changes and clip names.",
|
|
2803
|
+
description: "Returns one track as a CMX 3600 edit decision list (text) for conforming in another editor: events with source and record timecode (drop-frame at 29.97 and 59.94 fps unless dropFrame is false), dissolves, speed changes and clip names.",
|
|
2450
2804
|
inputSchema: object({
|
|
2451
2805
|
track: { ...TRACK_ID, description: "Track to export (default: the bottom video track)." },
|
|
2452
|
-
title: { type: "string", description: "TITLE line (default: the project name)." }
|
|
2806
|
+
title: { type: "string", description: "TITLE line (default: the project name)." },
|
|
2807
|
+
dropFrame: { type: "boolean", description: "Drop-frame timecode (HH:MM:SS;FF). Default true at 29.97 and 59.94 fps, where it is the norm; ignored at other rates, which have no drop-frame form." }
|
|
2453
2808
|
}),
|
|
2454
2809
|
annotations: { title: "Export an EDL", ...READ },
|
|
2455
|
-
handler: ({ track, title }) => ({ edl: controller.toEdl({ ...track ? { track } : {}, ...title ? { title } : {} }) })
|
|
2810
|
+
handler: ({ track, title, dropFrame }) => ({ edl: controller.toEdl({ ...track ? { track } : {}, ...title ? { title } : {}, ...dropFrame !== void 0 ? { dropFrame } : {} }) })
|
|
2456
2811
|
};
|
|
2457
2812
|
const seek = {
|
|
2458
2813
|
name: "seek_video",
|
|
@@ -2601,12 +2956,12 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2601
2956
|
const updateClip = {
|
|
2602
2957
|
name: "update_video_clip",
|
|
2603
2958
|
title: "Change a clip",
|
|
2604
|
-
description: `Changes a clip's properties (one undo step): opacity, transform, fit, fades, name; for media volume, mute, speed (keeps the stretch of source, so the length changes) or asset; for titles text and style; for colours the colour. Fields in "remove" go back to their defaults. Use move / trim for timing. Returns the clip.`,
|
|
2959
|
+
description: `Changes a clip's properties (one undo step): opacity, transform, fit, fades, name; for media volume, mute, the sound's fade curves, speed (keeps the stretch of source, so the length changes) or asset; for titles text and style; for colours the colour. Fields in "remove" go back to their defaults. Use move / trim for timing. Returns the clip.`,
|
|
2605
2960
|
inputSchema: object(
|
|
2606
2961
|
{
|
|
2607
2962
|
clip: CLIP_ID,
|
|
2608
2963
|
set: object({ ...CLIP_COMMON, ...MEDIA_FIELDS, asset: { ...ASSET_ID, description: "For media: play another asset (same in point)." }, ...TEXT_FIELDS, color: COLOR("Text colour, or a colour clip's fill") }, [], "Fields to replace (only those that fit the clip's type)."),
|
|
2609
|
-
remove: { type: "array", items: { type: "string", enum: ["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", "in", "speed", "volume", "muted", "fontSize", "fontFamily", "fontWeight", "background"] }, description: "Fields to reset to their defaults." }
|
|
2964
|
+
remove: { type: "array", items: { type: "string", enum: ["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", "fadeInCurve", "fadeOutCurve", "in", "speed", "volume", "muted", "fontSize", "fontFamily", "fontWeight", "background"] }, description: "Fields to reset to their defaults." }
|
|
2610
2965
|
},
|
|
2611
2966
|
["clip"]
|
|
2612
2967
|
),
|
|
@@ -2614,7 +2969,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2614
2969
|
handler: ({ clip, set, remove }) => {
|
|
2615
2970
|
const current = clipOut(project(), clip);
|
|
2616
2971
|
if (current && set) {
|
|
2617
|
-
const allowed = /* @__PURE__ */ new Set(["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", ...current.type === "media" ? ["in", "speed", "volume", "muted", "asset"] : current.type === "text" ? Object.keys(TEXT_FIELDS) : ["color"]]);
|
|
2972
|
+
const allowed = /* @__PURE__ */ new Set(["name", "opacity", "transform", "fit", "fadeIn", "fadeOut", ...current.type === "media" ? ["in", "speed", "volume", "muted", "asset", "fadeInCurve", "fadeOutCurve"] : current.type === "text" ? Object.keys(TEXT_FIELDS) : ["color"]]);
|
|
2618
2973
|
const wrong = Object.keys(set).filter((k) => !allowed.has(k));
|
|
2619
2974
|
if (wrong.length) throw new Error(`${wrong.map((k) => `"${k}"`).join(", ")} cannot be set on a ${current.type} clip; it takes ${[...allowed].join(", ")}.`);
|
|
2620
2975
|
}
|
|
@@ -2771,7 +3126,74 @@ function createVideoEditorToolProvider(controller, options = {}) {
|
|
|
2771
3126
|
return { redone, ...redone ? { label } : {} };
|
|
2772
3127
|
}
|
|
2773
3128
|
};
|
|
2774
|
-
const
|
|
3129
|
+
const soundRange = (args) => {
|
|
3130
|
+
const p = project();
|
|
3131
|
+
if (args.clip === void 0 === (args.asset === void 0)) throw new Error('Pass either "clip" (a media clip, times on the timeline) or "asset" (times in the source), not both.');
|
|
3132
|
+
let asset;
|
|
3133
|
+
let toSource = (t) => t;
|
|
3134
|
+
let toOut = (t) => t;
|
|
3135
|
+
let from;
|
|
3136
|
+
let to;
|
|
3137
|
+
if (args.clip !== void 0) {
|
|
3138
|
+
const found = findVideoClip(p, args.clip);
|
|
3139
|
+
if (!found) throw new Error(`There is no clip "${args.clip}"; get_video_project lists the clips.`);
|
|
3140
|
+
const c = found.clip;
|
|
3141
|
+
if (c.type !== "media") throw new Error(`Clip "${c.id}" is a ${c.type} clip and has no sound; pass a media clip.`);
|
|
3142
|
+
asset = p.assets.find((a) => a.id === c.asset);
|
|
3143
|
+
const speed = _nullishCoalesce(c.speed, () => ( 1));
|
|
3144
|
+
toSource = (t) => (_nullishCoalesce(c.in, () => ( 0))) + (t - c.start) * speed;
|
|
3145
|
+
toOut = (s) => c.start + (s - (_nullishCoalesce(c.in, () => ( 0)))) / speed;
|
|
3146
|
+
from = Math.max(c.start, _nullishCoalesce(args.from, () => ( c.start)));
|
|
3147
|
+
to = Math.min(videoClipEnd(c), _nullishCoalesce(args.to, () => ( videoClipEnd(c))));
|
|
3148
|
+
} else {
|
|
3149
|
+
asset = p.assets.find((a) => a.id === args.asset);
|
|
3150
|
+
if (!asset) throw new Error(`There is no asset "${args.asset}"; the assets are ${p.assets.map((a) => `"${a.id}"`).join(", ") || "none"}.`);
|
|
3151
|
+
from = _nullishCoalesce(args.from, () => ( 0));
|
|
3152
|
+
to = _nullishCoalesce(_nullishCoalesce(args.to, () => ( asset.duration)), () => ( 0));
|
|
3153
|
+
}
|
|
3154
|
+
if (!asset) throw new Error(`Clip "${args.clip}" uses a missing asset.`);
|
|
3155
|
+
if (asset.kind === "image" || asset.hasAudio === false) throw new Error(`Asset "${asset.id}" has no sound.`);
|
|
3156
|
+
if (!(to > from)) throw new Error(`The stretch to read is empty (${from}\u2013${to} ms); "to" must be after "from"${args.clip !== void 0 ? " and both inside the clip" : asset.duration === void 0 ? ', and the asset has no duration, so pass "to"' : ""}.`);
|
|
3157
|
+
return { asset, from, to, toSource, toOut };
|
|
3158
|
+
};
|
|
3159
|
+
const SOUND_TARGET = {
|
|
3160
|
+
clip: { ...CLIP_ID, description: "A media clip: read the stretch of source it plays, with times on the timeline. Pass this or asset." },
|
|
3161
|
+
asset: { ...ASSET_ID, description: "A video or audio asset: read its source, with times in the source. Pass this or clip." },
|
|
3162
|
+
from: MS("Start of the stretch (default: the clip's start, or 0 for an asset)"),
|
|
3163
|
+
to: MS("End of the stretch (default: the clip's end, or the asset's duration)")
|
|
3164
|
+
};
|
|
3165
|
+
const SOUND_ANNOTATIONS = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
3166
|
+
const audioLevels = {
|
|
3167
|
+
name: "get_video_audio_levels",
|
|
3168
|
+
title: "Read sound levels",
|
|
3169
|
+
description: 'Measures how loud the source sound is over a clip or an asset, without changing anything: the peak level in dBFS (0 = full scale, -100 = silence) for each of "buckets" equal stretches, plus the loudest peak. Levels are the recording as it is, before clip volume and fades. Use it to find where speech or a beat starts, or to judge whether a clip is too loud. The first call for an asset decodes its file, which can take a moment.',
|
|
3170
|
+
inputSchema: object({ ...SOUND_TARGET, buckets: { type: "integer", minimum: 1, maximum: 500, description: "How many equal stretches to measure (default 50)." } }),
|
|
3171
|
+
annotations: { title: "Read sound levels", ...SOUND_ANNOTATIONS },
|
|
3172
|
+
handler: async (args) => {
|
|
3173
|
+
const r = soundRange(args);
|
|
3174
|
+
const peaks = await controller.audioPeaks.load(r.asset);
|
|
3175
|
+
const levels = videoAudioLevels(peaks, r.toSource(r.from), r.toSource(r.to), _nullishCoalesce(args.buckets, () => ( 50))).map((l) => ({ start: round(r.toOut(l.start)), end: round(r.toOut(l.end)), peakDb: l.peakDb }));
|
|
3176
|
+
return { asset: r.asset.id, from: r.from, to: r.to, peakDb: Math.max(...levels.map((l) => l.peakDb)), levels };
|
|
3177
|
+
}
|
|
3178
|
+
};
|
|
3179
|
+
const findSilences = {
|
|
3180
|
+
name: "find_video_silences",
|
|
3181
|
+
title: "Find silences",
|
|
3182
|
+
description: "Lists the stretches of a clip or an asset whose source sound stays below a level for at least a minimum length, as { start, end } ms (timeline times for a clip, so they can go straight to extract_video_range or split_video_at; source times for an asset). Use it to cut pauses, dead air or the gaps between takes. The first call for an asset decodes its file.",
|
|
3183
|
+
inputSchema: object({
|
|
3184
|
+
...SOUND_TARGET,
|
|
3185
|
+
thresholdDb: { type: "number", minimum: -100, maximum: 0, description: "Quieter than this peak level counts as silence, in dBFS (default -40; -50 for a quiet room, -30 over background noise)." },
|
|
3186
|
+
minDuration: { type: "number", exclusiveMinimum: 0, description: "Shortest silence to report, in ms (default 300)." }
|
|
3187
|
+
}),
|
|
3188
|
+
annotations: { title: "Find silences", ...SOUND_ANNOTATIONS },
|
|
3189
|
+
handler: async (args) => {
|
|
3190
|
+
const r = soundRange(args);
|
|
3191
|
+
const peaks = await controller.audioPeaks.load(r.asset);
|
|
3192
|
+
const silences = findVideoAudioSilences(peaks, r.toSource(r.from), r.toSource(r.to), { thresholdDb: args.thresholdDb, minDuration: args.minDuration }).map((x) => ({ start: round(r.toOut(x.start)), end: round(r.toOut(x.end)) }));
|
|
3193
|
+
return { asset: r.asset.id, from: r.from, to: r.to, silences, total: round(silences.reduce((n2, x) => n2 + x.end - x.start, 0)) };
|
|
3194
|
+
}
|
|
3195
|
+
};
|
|
3196
|
+
const reads = [getProject, getFrame, findGaps, checkProject, renderFrame, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
|
|
2775
3197
|
const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
|
|
2776
3198
|
return {
|
|
2777
3199
|
id: _nullishCoalesce(options.id, () => ( "video")),
|
|
@@ -2804,6 +3226,17 @@ function videoRulerTicks(duration, pxPerSecond, fps) {
|
|
|
2804
3226
|
}
|
|
2805
3227
|
return ticks;
|
|
2806
3228
|
}
|
|
3229
|
+
function videoClipShowsWaveform(clip, asset, mode = "audio") {
|
|
3230
|
+
if (clip.type !== "media" || mode === "none" || !videoAssetHasSound(asset)) return false;
|
|
3231
|
+
return mode === "all" || asset.kind === "audio";
|
|
3232
|
+
}
|
|
3233
|
+
var VIDEO_FADE_CURVE_LABELS = {
|
|
3234
|
+
linear: "Linear",
|
|
3235
|
+
"equal-power": "Equal power",
|
|
3236
|
+
"s-curve": "S-curve",
|
|
3237
|
+
exponential: "Exponential",
|
|
3238
|
+
logarithmic: "Logarithmic"
|
|
3239
|
+
};
|
|
2807
3240
|
function videoClipTint(clip, asset) {
|
|
2808
3241
|
if (clip.type === "text") return "#a855f7";
|
|
2809
3242
|
if (clip.type === "color") return clip.color;
|
|
@@ -2814,7 +3247,7 @@ function videoClipLabel(clip, asset) {
|
|
|
2814
3247
|
if (clip.name) return clip.name;
|
|
2815
3248
|
if (clip.type === "text") return clip.text.split("\n")[0];
|
|
2816
3249
|
if (clip.type === "color") return clip.color;
|
|
2817
|
-
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess',
|
|
3250
|
+
const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _79 => _79.name]), () => ( _optionalChain([asset, 'optionalAccess', _80 => _80.src, 'access', _81 => _81.replace, 'call', _82 => _82(/^.*[\\/]/, "")]))), () => ( clip.asset));
|
|
2818
3251
|
const speed = videoClipSpeed(clip);
|
|
2819
3252
|
return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
|
|
2820
3253
|
}
|
|
@@ -2858,7 +3291,7 @@ function videoAssetDropEdit(project, assetId, options = {}) {
|
|
|
2858
3291
|
const asset = videoAssetMap(project).get(assetId);
|
|
2859
3292
|
if (!asset) return null;
|
|
2860
3293
|
const kind = asset.kind === "audio" ? "audio" : "video";
|
|
2861
|
-
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access',
|
|
3294
|
+
const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _83 => _83.tracks, 'access', _84 => _84.find, 'call', _85 => _85((t) => t.kind === kind && !t.locked), 'optionalAccess', _86 => _86.id])));
|
|
2862
3295
|
if (!track) return null;
|
|
2863
3296
|
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 } : {} };
|
|
2864
3297
|
}
|
|
@@ -2907,15 +3340,15 @@ var KEY_LABELS = {
|
|
|
2907
3340
|
scale: { label: "Scale", step: 0.05 },
|
|
2908
3341
|
rotation: { label: "Rotation", step: 1 }
|
|
2909
3342
|
};
|
|
2910
|
-
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access',
|
|
3343
|
+
var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _87 => _87.transform, 'optionalAccess', _88 => _88[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
|
|
2911
3344
|
var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
|
|
2912
3345
|
function videoKeyframeRows(project, clipId, time) {
|
|
2913
3346
|
const at = findVideoClip(project, clipId);
|
|
2914
3347
|
if (!at) return [];
|
|
2915
3348
|
const { clip } = at;
|
|
2916
3349
|
const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
|
|
2917
|
-
const visual = !(_optionalChain([asset, 'optionalAccess',
|
|
2918
|
-
const audible = clip.type === "media" && (
|
|
3350
|
+
const visual = !(_optionalChain([asset, 'optionalAccess', _89 => _89.kind]) === "audio" || at.track.kind === "audio");
|
|
3351
|
+
const audible = clip.type === "media" && videoAssetHasSound(asset);
|
|
2919
3352
|
const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
|
|
2920
3353
|
const clipTime = playheadInClip(clip, time);
|
|
2921
3354
|
return properties.map((property) => {
|
|
@@ -3060,5 +3493,30 @@ function videoKeyframeToggleEdit(project, clipId, property, time) {
|
|
|
3060
3493
|
|
|
3061
3494
|
|
|
3062
3495
|
|
|
3063
|
-
|
|
3496
|
+
|
|
3497
|
+
|
|
3498
|
+
|
|
3499
|
+
|
|
3500
|
+
|
|
3501
|
+
|
|
3502
|
+
|
|
3503
|
+
|
|
3504
|
+
|
|
3505
|
+
|
|
3506
|
+
|
|
3507
|
+
|
|
3508
|
+
|
|
3509
|
+
|
|
3510
|
+
|
|
3511
|
+
|
|
3512
|
+
|
|
3513
|
+
|
|
3514
|
+
|
|
3515
|
+
|
|
3516
|
+
|
|
3517
|
+
|
|
3518
|
+
|
|
3519
|
+
|
|
3520
|
+
|
|
3521
|
+
exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; 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_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_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoPreviewEngine = VideoPreviewEngine; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoProject = checkVideoProject; 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.findVideoGaps = findVideoGaps; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; 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.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.videoClipAfter = videoClipAfter; 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.videoClipSpeed = videoClipSpeed; 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.videoFadeCurveGain = videoFadeCurveGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoHourFrames = videoHourFrames; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoOverlap = videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRulerTicks = videoRulerTicks; exports.videoSnapPoints = videoSnapPoints; exports.videoTextStyle = videoTextStyle; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTransformAt = videoTransformAt; exports.videoValueAt = videoValueAt; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect;
|
|
3064
3522
|
//# sourceMappingURL=index.cjs.map
|