@pixerate/editor 0.90.0 → 0.92.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.
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6;
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6; var _class7;
2
2
 
3
3
  var _chunkWAJQKWQUcjs = require('../chunk-WAJQKWQU.cjs');
4
4
 
@@ -20,7 +20,7 @@ var VIDEO_BLEND_MODES = ["normal", "multiply", "screen", "overlay", "darken", "l
20
20
  var VIDEO_CLIP_ANIMATION_EFFECTS = ["fade", "rise", "drop", "slide-left", "slide-right", "zoom", "pop"];
21
21
  var VIDEO_FADE_CURVES = ["linear", "equal-power", "s-curve", "exponential", "logarithmic"];
22
22
  var VIDEO_PLACEMENT_MODES = ["refuse", "overwrite", "insert"];
23
- var VIDEO_CLIP_TYPES = ["media", "text", "color"];
23
+ var VIDEO_CLIP_TYPES = ["media", "text", "color", "sequence"];
24
24
 
25
25
  // src/video-editor/keyframes.ts
26
26
  var VIDEO_EASE_NAMES = ["linear", "hold", "ease-in", "ease-out", "ease-in-out"];
@@ -852,12 +852,33 @@ function parseVideoAsset(v, path = "asset") {
852
852
  height: num(v.height, `${path}.height`, { optional: true, positive: true }),
853
853
  hasAudio: bool(v.hasAudio, `${path}.hasAudio`),
854
854
  poster: str(v.poster, `${path}.poster`, true),
855
- transcript: parseVideoTranscriptWords(v.transcript, `${path}.transcript`)
855
+ transcript: parseVideoTranscriptWords(v.transcript, `${path}.transcript`),
856
+ proxies: parseVideoAssetProxies(v.proxies, `${path}.proxies`)
856
857
  });
858
+ if (kind !== "video" && asset.proxies) fail(`${path}.proxies`, "are only for video assets");
857
859
  if (kind === "image" && asset.transcript) fail(`${path}.transcript`, "is only for video and audio assets");
858
860
  if (kind === "image" && asset.duration !== void 0) fail(`${path}.duration`, "is only for video and audio assets");
859
861
  return asset;
860
862
  }
863
+ function parseVideoAssetProxies(v, path) {
864
+ if (v === void 0) return void 0;
865
+ if (!Array.isArray(v)) return fail(path, "must be a list of { scale, src }");
866
+ const out = v.map((p, i) => {
867
+ const at = `${path}[${i}]`;
868
+ if (!isObject(p)) return fail(at, "must be an object { scale, src }");
869
+ const unknown = Object.keys(p).find((k) => !["scale", "src", "width", "height"].includes(k));
870
+ if (unknown) fail(`${at}.${unknown}`, "is not a proxy setting; use scale, src, width, height");
871
+ const scale = num(p.scale, `${at}.scale`, { positive: true });
872
+ if (scale >= 1) fail(`${at}.scale`, "must be less than 1 (0.5 is half size)");
873
+ return compact({ scale, src: str(p.src, `${at}.src`), width: num(p.width, `${at}.width`, { optional: true, positive: true }), height: num(p.height, `${at}.height`, { optional: true, positive: true }) });
874
+ });
875
+ const scales = /* @__PURE__ */ new Set();
876
+ for (const [i, p] of out.entries()) {
877
+ if (scales.has(p.scale)) fail(`${path}[${i}].scale`, `repeats ${p.scale}; give each proxy its own scale`);
878
+ scales.add(p.scale);
879
+ }
880
+ return out.length ? out : void 0;
881
+ }
861
882
  function parseVideoClip(v, path = "clip") {
862
883
  if (!isObject(v)) return fail(path, "must be an object");
863
884
  const type = oneOf(_nullishCoalesce(v.type, () => ( "media")), VIDEO_CLIP_TYPES, `${path}.type`, false);
@@ -890,6 +911,20 @@ function parseVideoClip(v, path = "clip") {
890
911
  fadeInCurve: oneOf(v.fadeInCurve, VIDEO_FADE_CURVES, `${path}.fadeInCurve`),
891
912
  fadeOutCurve: oneOf(v.fadeOutCurve, VIDEO_FADE_CURVES, `${path}.fadeOutCurve`)
892
913
  });
914
+ if (type === "sequence") {
915
+ for (const key of ["speed", "timeRemap"])
916
+ if (v[key] !== void 0 && !(key === "speed" && v[key] === 1)) fail(`${path}.${key}`, 'is not supported on a compound clip (type "sequence"), which plays its sequence at normal speed; remove it, or change the speed of the clips inside the sequence');
917
+ return compact({
918
+ type,
919
+ ...base,
920
+ sequence: str(v.sequence, `${path}.sequence`),
921
+ in: num(v.in, `${path}.in`, { optional: true, min: 0 }),
922
+ volume: parseVideoAnimatable(v.volume, `${path}.volume`, { optional: true, min: 0, max: 4 }),
923
+ muted: bool(v.muted, `${path}.muted`),
924
+ fadeInCurve: oneOf(v.fadeInCurve, VIDEO_FADE_CURVES, `${path}.fadeInCurve`),
925
+ fadeOutCurve: oneOf(v.fadeOutCurve, VIDEO_FADE_CURVES, `${path}.fadeOutCurve`)
926
+ });
927
+ }
893
928
  if (type === "text")
894
929
  return compact({
895
930
  type,
@@ -918,6 +953,21 @@ function parseVideoTrack(v, path = "track") {
918
953
  clips: (_nullishCoalesce(v.clips, () => ( []))).map((c, i) => parseVideoClip(c, `${path}.clips[${i}]`))
919
954
  });
920
955
  }
956
+ function parseVideoSequence(v, path = "sequence") {
957
+ if (!isObject(v)) return fail(path, "must be an object { id, name, tracks }");
958
+ if (v.tracks !== void 0 && !Array.isArray(v.tracks)) fail(`${path}.tracks`, "must be an array");
959
+ const sequence = compact({
960
+ id: str(v.id, `${path}.id`),
961
+ name: str(v.name, `${path}.name`, true),
962
+ tracks: (_nullishCoalesce(v.tracks, () => ( []))).map((t, i) => parseVideoTrack(t, `${path}.tracks[${i}]`))
963
+ });
964
+ const seen = /* @__PURE__ */ new Set();
965
+ for (const t of sequence.tracks) {
966
+ if (seen.has(t.id)) throw new Error(`Two tracks in sequence "${sequence.id}" share the id "${t.id}"; ids must be unique.`);
967
+ seen.add(t.id);
968
+ }
969
+ return sequence;
970
+ }
921
971
  function parseVideoMarker(v, path = "marker") {
922
972
  if (!isObject(v)) return fail(path, "must be an object");
923
973
  return compact({ id: str(v.id, `${path}.id`), time: num(v.time, `${path}.time`, { min: 0 }), label: str(v.label, `${path}.label`, true), color: str(v.color, `${path}.color`, true) });
@@ -933,7 +983,7 @@ function parseVideoProject(input) {
933
983
  }
934
984
  if (!isObject(v)) return fail("The project", "must be an object");
935
985
  if (v.version !== void 0 && v.version !== 1) fail("version", "must be 1");
936
- for (const key of ["assets", "tracks", "markers"]) if (v[key] !== void 0 && !Array.isArray(v[key])) fail(key, "must be an array");
986
+ for (const key of ["assets", "tracks", "sequences", "markers"]) if (v[key] !== void 0 && !Array.isArray(v[key])) fail(key, "must be an array");
937
987
  const project = compact({
938
988
  version: 1,
939
989
  name: str(v.name, "name", true),
@@ -943,6 +993,7 @@ function parseVideoProject(input) {
943
993
  background: str(v.background, "background", true),
944
994
  assets: (_nullishCoalesce(v.assets, () => ( []))).map((a, i) => parseVideoAsset(a, `assets[${i}]`)),
945
995
  tracks: (_nullishCoalesce(v.tracks, () => ( []))).map((t, i) => parseVideoTrack(t, `tracks[${i}]`)),
996
+ sequences: v.sequences === void 0 ? void 0 : v.sequences.map((q, i) => parseVideoSequence(q, `sequences[${i}]`)),
946
997
  markers: v.markers === void 0 ? void 0 : v.markers.map((m, i) => parseVideoMarker(m, `markers[${i}]`))
947
998
  });
948
999
  const unique = (ids, what) => {
@@ -954,7 +1005,8 @@ function parseVideoProject(input) {
954
1005
  };
955
1006
  unique(project.assets.map((a) => a.id), "assets");
956
1007
  unique(project.tracks.map((t) => t.id), "tracks");
957
- unique(project.tracks.flatMap((t) => t.clips.map((c) => c.id)), "clips");
1008
+ unique((_nullishCoalesce(project.sequences, () => ( []))).map((q) => q.id), "sequences");
1009
+ unique([...project.tracks, ...(_nullishCoalesce(project.sequences, () => ( []))).flatMap((q) => q.tracks)].flatMap((t) => t.clips.map((c) => c.id)), "clips");
958
1010
  unique((_nullishCoalesce(project.markers, () => ( []))).map((m) => m.id), "markers");
959
1011
  return project;
960
1012
  }
@@ -980,6 +1032,7 @@ var videoClipEnd = (clip) => clip.start + clip.duration;
980
1032
  var videoTrackSyncLocked = (track) => track.syncLocked !== false;
981
1033
  var videoClipSpeed = (clip) => clip.type === "media" ? _nullishCoalesce(clip.speed, () => ( 1)) : 1;
982
1034
  function videoClipSourceRange(clip) {
1035
+ if (clip.type === "sequence") return { in: _nullishCoalesce(clip.in, () => ( 0)), out: (_nullishCoalesce(clip.in, () => ( 0))) + clip.duration };
983
1036
  if (clip.type !== "media") return null;
984
1037
  const start = _nullishCoalesce(clip.in, () => ( 0));
985
1038
  if (_optionalChain([clip, 'access', _5 => _5.timeRemap, 'optionalAccess', _6 => _6.keys, 'access', _7 => _7.length])) return { in: start + clip.timeRemap.keys[0].v, out: start + clip.timeRemap.keys[clip.timeRemap.keys.length - 1].v };
@@ -1029,6 +1082,33 @@ var videoAssetHasSound = (asset) => !!asset && (asset.kind === "audio" || asset.
1029
1082
  function videoAssetMap(project) {
1030
1083
  return new Map(project.assets.map((a) => [a.id, a]));
1031
1084
  }
1085
+ function videoSequenceMap(project) {
1086
+ return new Map((_nullishCoalesce(project.sequences, () => ( []))).map((q) => [q.id, q]));
1087
+ }
1088
+ function videoSequenceDuration(sequence) {
1089
+ let end = 0;
1090
+ for (const track of sequence.tracks) for (const clip of track.clips) end = Math.max(end, videoClipEnd(clip));
1091
+ return end;
1092
+ }
1093
+ var videoSequenceChildren = (sequence) => [...new Set(sequence.tracks.flatMap((t) => t.clips.flatMap((c) => c.type === "sequence" ? [c.sequence] : [])))];
1094
+ function findVideoSequenceCycle(project, id) {
1095
+ const sequences = videoSequenceMap(project);
1096
+ const walk = (at, path) => {
1097
+ const seq = sequences.get(at);
1098
+ if (!seq) return null;
1099
+ for (const child of videoSequenceChildren(seq)) {
1100
+ if (child === id) return [...path, child];
1101
+ if (path.includes(child)) continue;
1102
+ const found = walk(child, [...path, child]);
1103
+ if (found) return found;
1104
+ }
1105
+ return null;
1106
+ };
1107
+ return walk(id, [id]);
1108
+ }
1109
+ function allVideoTracks(project) {
1110
+ return [...project.tracks.map((track) => ({ track })), ...(_nullishCoalesce(project.sequences, () => ( []))).flatMap((q) => q.tracks.map((track) => ({ sequence: q.id, track })))];
1111
+ }
1032
1112
  function sortedVideoClips(track) {
1033
1113
  return [...track.clips].sort((a, b) => a.start - b.start || a.duration - b.duration);
1034
1114
  }
@@ -1041,7 +1121,15 @@ function findVideoClip(project, id) {
1041
1121
  return null;
1042
1122
  }
1043
1123
  function allVideoClipIds(project) {
1044
- return project.tracks.flatMap((t) => t.clips.map((c) => c.id));
1124
+ return allVideoTracks(project).flatMap(({ track }) => track.clips.map((c) => c.id));
1125
+ }
1126
+ function findVideoSequenceClip(project, id) {
1127
+ for (const sequence of _nullishCoalesce(project.sequences, () => ( [])))
1128
+ for (const track of sequence.tracks) {
1129
+ const clip = track.clips.find((c) => c.id === id);
1130
+ if (clip) return { sequence, track, clip };
1131
+ }
1132
+ return null;
1045
1133
  }
1046
1134
  function videoProjectDuration(project) {
1047
1135
  let end = 0;
@@ -1057,19 +1145,24 @@ function videoClipAfter(track, clip) {
1057
1145
  for (const c of track.clips) if (c !== clip && Math.abs(c.start - videoClipEnd(clip)) <= VIDEO_TIME_EPSILON) return c;
1058
1146
  return null;
1059
1147
  }
1060
- function videoClipHandles(clip, assets) {
1148
+ function videoClipHandles(clip, assets, sequences) {
1149
+ if (clip.type === "sequence") {
1150
+ const seq = _optionalChain([sequences, 'optionalAccess', _8 => _8.get, 'call', _9 => _9(clip.sequence)]);
1151
+ const range2 = videoClipSourceRange(clip);
1152
+ return { before: range2.in, after: seq ? Math.max(0, videoSequenceDuration(seq) - range2.out) : Infinity };
1153
+ }
1061
1154
  if (clip.type !== "media") return { before: Infinity, after: Infinity };
1062
1155
  const asset = assets.get(clip.asset);
1063
1156
  const speed = videoClipSpeed(clip);
1064
1157
  const range = videoClipSourceRange(clip);
1065
1158
  const before = range.in / speed;
1066
- if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess', _8 => _8.kind]) === "image" ? Infinity : before, after: Infinity };
1159
+ if (!asset || asset.kind === "image" || asset.duration === void 0) return { before: _optionalChain([asset, 'optionalAccess', _10 => _10.kind]) === "image" ? Infinity : before, after: Infinity };
1067
1160
  return { before, after: Math.max(0, (asset.duration - range.out) / speed) };
1068
1161
  }
1069
1162
  function videoClipOverhang(track, clip) {
1070
1163
  const lead = clip.transition && clip.transition.type !== "dip" && videoClipBefore(track, clip) ? clip.transition.duration / 2 : 0;
1071
1164
  const next = videoClipAfter(track, clip);
1072
- const tail = _optionalChain([next, 'optionalAccess', _9 => _9.transition]) && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
1165
+ const tail = _optionalChain([next, 'optionalAccess', _11 => _11.transition]) && next.transition.type !== "dip" ? next.transition.duration / 2 : 0;
1073
1166
  return { lead, tail };
1074
1167
  }
1075
1168
  function videoSnapPoints(project, options = {}) {
@@ -1153,7 +1246,7 @@ function videoClipAnimated(clip, project) {
1153
1246
  const spans = videoClipAnimationSpans(clip);
1154
1247
  const out = { ...clip, transform: { ...clip.transform } };
1155
1248
  for (const prop of PROPS) {
1156
- const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _10 => _10.transform, 'optionalAccess', _11 => _11[prop]]);
1249
+ const current = prop === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _12 => _12.transform, 'optionalAccess', _13 => _13[prop]]);
1157
1250
  if (isVideoKeyframes(current)) continue;
1158
1251
  const base = _nullishCoalesce(current, () => ( (prop === "opacity" || prop === "scale" ? 1 : 0)));
1159
1252
  const away = (spec) => {
@@ -1189,13 +1282,15 @@ function videoClipAnimated(clip, project) {
1189
1282
  function videoClipKeyframes(clip) {
1190
1283
  const out = [
1191
1284
  { property: "opacity", keys: videoKeysOf(clip.opacity) },
1192
- { property: "volume", keys: clip.type === "media" ? videoKeysOf(clip.volume) : [] },
1193
- ...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _12 => _12.transform, 'optionalAccess', _13 => _13[p]])) }))
1285
+ { property: "volume", keys: clip.type === "media" || clip.type === "sequence" ? videoKeysOf(clip.volume) : [] },
1286
+ ...VIDEO_TRANSFORM_PROPERTIES.map((p) => ({ property: p, keys: videoKeysOf(_optionalChain([clip, 'access', _14 => _14.transform, 'optionalAccess', _15 => _15[p]])) }))
1194
1287
  ];
1195
1288
  return out.filter((o) => o.keys.length);
1196
1289
  }
1197
1290
  var VIDEO_ISSUE_CODES = [
1198
1291
  "missing-asset",
1292
+ "missing-sequence",
1293
+ "sequence-cycle",
1199
1294
  "wrong-track",
1200
1295
  "overlap",
1201
1296
  "source-overrun",
@@ -1208,23 +1303,41 @@ var VIDEO_ISSUE_CODES = [
1208
1303
  "keyframes-outside-clip",
1209
1304
  "unknown-duration",
1210
1305
  "unused-asset",
1306
+ "unused-sequence",
1211
1307
  "marker-past-end",
1212
1308
  "empty"
1213
1309
  ];
1214
- var videoIssueKey = (i) => [i.code, _nullishCoalesce(i.track, () => ( "")), _nullishCoalesce(i.clip, () => ( "")), _nullishCoalesce(i.asset, () => ( "")), _nullishCoalesce(i.marker, () => ( ""))].join("|");
1310
+ var videoIssueKey = (i) => [i.code, _nullishCoalesce(i.sequence, () => ( "")), _nullishCoalesce(i.track, () => ( "")), _nullishCoalesce(i.clip, () => ( "")), _nullishCoalesce(i.asset, () => ( "")), _nullishCoalesce(i.marker, () => ( ""))].join("|");
1215
1311
  function checkVideoProject(project) {
1216
1312
  const issues = [];
1217
1313
  const assets = videoAssetMap(project);
1314
+ const sequences = videoSequenceMap(project);
1218
1315
  const used = /* @__PURE__ */ new Set();
1316
+ const usedSequences = /* @__PURE__ */ new Set();
1317
+ const cycles = /* @__PURE__ */ new Map();
1219
1318
  for (const asset of project.assets)
1220
1319
  if ((asset.kind === "video" || asset.kind === "audio") && asset.duration === void 0)
1221
1320
  issues.push({ code: "unknown-duration", severity: "warning", asset: asset.id, message: `Asset "${asset.id}" has no duration, so clips of it cannot be checked against its length; set "duration" in ms.` });
1222
- for (const track of project.tracks) {
1321
+ const checkTrack = (track, sequence) => {
1223
1322
  const clips = sortedVideoClips(track);
1323
+ const where = sequence ? ` in sequence "${sequence}"` : "";
1224
1324
  for (let i = 0; i < clips.length; i++) {
1225
1325
  const clip = clips[i];
1226
- const at = { track: track.id, clip: clip.id };
1227
- if (clip.type === "media") {
1326
+ const at = { ...sequence ? { sequence } : {}, track: track.id, clip: clip.id };
1327
+ if (clip.type === "sequence") {
1328
+ usedSequences.add(clip.sequence);
1329
+ const seq = sequences.get(clip.sequence);
1330
+ if (!seq) issues.push({ code: "missing-sequence", severity: "error", ...at, message: `Compound clip "${clip.id}" plays sequence "${clip.sequence}", which is not in the project; add the sequence or delete the clip.` });
1331
+ else {
1332
+ if (!cycles.has(seq.id)) cycles.set(seq.id, findVideoSequenceCycle(project, seq.id));
1333
+ const cycle = cycles.get(seq.id);
1334
+ if (cycle) issues.push({ code: "sequence-cycle", severity: "error", ...at, message: `Compound clip "${clip.id}" plays sequence "${seq.id}", which contains itself (${cycle.join(" \u2192 ")}), so it is left out of the picture and sound; remove the compound clip that closes the loop.` });
1335
+ const length = videoSequenceDuration(seq);
1336
+ const range = videoClipSourceRange(clip);
1337
+ if (range.out > length + VIDEO_TIME_EPSILON)
1338
+ issues.push({ code: "source-overrun", severity: "error", ...at, message: `Compound clip "${clip.id}" plays to ${formatVideoSeconds(range.out)} of sequence "${seq.id}", which is only ${formatVideoSeconds(length)} long; shorten the clip or move its in point.` });
1339
+ }
1340
+ } else if (clip.type === "media") {
1228
1341
  used.add(clip.asset);
1229
1342
  const asset = assets.get(clip.asset);
1230
1343
  if (!asset) issues.push({ code: "missing-asset", severity: "error", ...at, asset: clip.asset, message: `Clip "${clip.id}" plays asset "${clip.asset}", which is not in the project; add the asset or delete the clip.` });
@@ -1238,7 +1351,7 @@ function checkVideoProject(project) {
1238
1351
  } else if (track.kind === "audio") issues.push({ code: "wrong-track", severity: "error", ...at, message: `Clip "${clip.id}" is a ${clip.type} clip on audio track "${track.id}"; move it to a video track.` });
1239
1352
  const next = clips[i + 1];
1240
1353
  if (next && next.start < videoClipEnd(clip) - VIDEO_TIME_EPSILON)
1241
- issues.push({ code: "overlap", severity: "error", ...at, message: `Clips "${clip.id}" and "${next.id}" overlap on track "${track.id}" from ${formatVideoSeconds(next.start)}; move one of them.` });
1354
+ issues.push({ code: "overlap", severity: "error", ...at, message: `Clips "${clip.id}" and "${next.id}" overlap on track "${track.id}"${where} from ${formatVideoSeconds(next.start)}; move one of them.` });
1242
1355
  if ((_nullishCoalesce(clip.fadeIn, () => ( 0))) + (_nullishCoalesce(clip.fadeOut, () => ( 0))) > clip.duration + VIDEO_TIME_EPSILON)
1243
1356
  issues.push({ code: "fades-too-long", severity: "warning", ...at, message: `Clip "${clip.id}"'s fades (${_nullishCoalesce(clip.fadeIn, () => ( 0))} + ${_nullishCoalesce(clip.fadeOut, () => ( 0))} ms) are longer than the clip (${clip.duration} ms); shorten them.` });
1244
1357
  const anim = (clip.animateIn ? _nullishCoalesce(clip.animateIn.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0) + (clip.animateOut ? _nullishCoalesce(clip.animateOut.duration, () => ( VIDEO_CLIP_ANIMATION_DURATION)) : 0);
@@ -1247,7 +1360,7 @@ function checkVideoProject(project) {
1247
1360
  for (const which of ["animateIn", "animateOut"]) {
1248
1361
  const a = clip[which];
1249
1362
  if (!a) continue;
1250
- const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _14 => _14.transform, 'optionalAccess', _15 => _15[p]])).length);
1363
+ const keyed = videoClipAnimationProperties(a).filter((p) => videoKeysOf(p === "opacity" ? clip.opacity : _optionalChain([clip, 'access', _16 => _16.transform, 'optionalAccess', _17 => _17[p]])).length);
1251
1364
  if (keyed.length)
1252
1365
  issues.push({ code: "animation-keyed", severity: "note", ...at, message: `Clip "${clip.id}"'s ${a.effect} ${which === "animateIn" ? "entrance" : "exit"} leaves ${keyed.join(", ")} alone, because the clip keys ${keyed.length > 1 ? "them" : "it"} itself; remove those keyframes or the animation.` });
1253
1366
  }
@@ -1273,8 +1386,8 @@ function checkVideoProject(project) {
1273
1386
  continue;
1274
1387
  }
1275
1388
  if (clip.transition.type === "dip") continue;
1276
- const outgoing = videoClipHandles(before, assets).after;
1277
- const incoming = videoClipHandles(clip, assets).before;
1389
+ const outgoing = videoClipHandles(before, assets, sequences).after;
1390
+ const incoming = videoClipHandles(clip, assets, sequences).before;
1278
1391
  const half = d / 2;
1279
1392
  if (outgoing < half - VIDEO_TIME_EPSILON || incoming < half - VIDEO_TIME_EPSILON) {
1280
1393
  const fits = 2 * Math.min(outgoing, incoming);
@@ -1287,7 +1400,11 @@ function checkVideoProject(project) {
1287
1400
  }
1288
1401
  }
1289
1402
  }
1290
- }
1403
+ };
1404
+ for (const track of project.tracks) checkTrack(track);
1405
+ for (const sequence of _nullishCoalesce(project.sequences, () => ( []))) for (const track of sequence.tracks) checkTrack(track, sequence.id);
1406
+ for (const sequence of _nullishCoalesce(project.sequences, () => ( [])))
1407
+ if (!usedSequences.has(sequence.id)) issues.push({ code: "unused-sequence", severity: "note", sequence: sequence.id, message: `Sequence "${sequence.id}" is not played by any compound clip.` });
1291
1408
  for (const asset of project.assets) if (!used.has(asset.id)) issues.push({ code: "unused-asset", severity: "note", asset: asset.id, message: `Asset "${asset.id}" is not used by any clip.` });
1292
1409
  const end = videoProjectDuration(project);
1293
1410
  for (const marker of _nullishCoalesce(project.markers, () => ( [])))
@@ -1359,7 +1476,7 @@ function clipAnimatables(clip) {
1359
1476
  // A speed ramp is keys in clip time too, so trims and splits keep it on the same frames of source.
1360
1477
  { get: () => c.timeRemap, set: (v) => c.timeRemap = v }
1361
1478
  ];
1362
- for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _16 => _16.transform, 'optionalAccess', _17 => _17[p]]), set: (v) => c.transform && (c.transform[p] = v) });
1479
+ for (const p of VIDEO_TRANSFORM_PROPERTIES) out.push({ get: () => _optionalChain([c, 'access', _18 => _18.transform, 'optionalAccess', _19 => _19[p]]), set: (v) => c.transform && (c.transform[p] = v) });
1363
1480
  return out;
1364
1481
  }
1365
1482
  function retimeClipKeys(clip, fn) {
@@ -1383,6 +1500,8 @@ var Editor = (_class2 = class {
1383
1500
  }
1384
1501
  clip(id) {
1385
1502
  const at = findVideoClip(this.p, id);
1503
+ const nested = at ? null : findVideoSequenceClip(this.p, id);
1504
+ if (nested) refuse(`Clip "${id}" is inside sequence "${nested.sequence.id}", and clips inside sequences cannot be edited yet; edit the compound clip that plays the sequence instead (list_video_sequences shows which).`);
1386
1505
  if (!at) {
1387
1506
  const ids = this.p.tracks.flatMap((t) => t.clips.map((c) => c.id));
1388
1507
  refuse(`There is no clip ${_chunkWAJQKWQUcjs.namedWithSuggestion.call(void 0, id, ids)}${ids.length ? "" : "; the project has no clips yet"}.`);
@@ -1406,8 +1525,13 @@ var Editor = (_class2 = class {
1406
1525
  }
1407
1526
  kindFits(track, clip) {
1408
1527
  const asset = clip.type === "media" ? this.asset(clip.asset) : null;
1528
+ if (clip.type === "sequence") {
1529
+ const ids = (_nullishCoalesce(this.p.sequences, () => ( []))).map((q) => q.id);
1530
+ if (typeof clip.sequence !== "string" || !ids.includes(clip.sequence)) refuse(`There is no sequence ${_chunkWAJQKWQUcjs.namedWithSuggestion.call(void 0, String(clip.sequence), ids)}; the sequences are ${ids.map((q) => `"${q}"`).join(", ") || "none"}.`);
1531
+ return;
1532
+ }
1409
1533
  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.`);
1410
- if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _18 => _18.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
1534
+ if (track.kind === "video" && _optionalChain([asset, 'optionalAccess', _20 => _20.kind]) === "audio") refuse(`Track "${track.id}" is a video track; audio asset "${asset.id}" goes on an audio track.`);
1411
1535
  }
1412
1536
  /** Moves every clip on `track` starting at or after `from` by `delta`. */
1413
1537
  shift(track, from, delta, except = /* @__PURE__ */ new Set()) {
@@ -1419,7 +1543,7 @@ var Editor = (_class2 = class {
1419
1543
  return 1e3 / this.p.fps;
1420
1544
  }
1421
1545
  ids() {
1422
- return this.p.tracks.flatMap((t) => t.clips.map((c) => c.id));
1546
+ return allVideoClipIds(this.p);
1423
1547
  }
1424
1548
  /**
1425
1549
  * The tracks a ripple on `sources` moves, top to bottom: the sources, and every unlocked
@@ -1447,9 +1571,14 @@ var Editor = (_class2 = class {
1447
1571
  range(from, to) {
1448
1572
  if (!(to - from > VIDEO_TIME_EPSILON)) refuse(`"to" (${formatVideoSeconds(to)}) must be after "from" (${formatVideoSeconds(from)}).`);
1449
1573
  }
1450
- /** Moves a media clip's in point by `delta` of timeline time (stills, titles and colours have none). */
1574
+ /** Moves a media or compound clip's in point by `delta` of timeline time (stills, titles and colours have none). */
1451
1575
  moveIn(clip, delta) {
1452
- if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _19 => _19(this.p), 'access', _20 => _20.get, 'call', _21 => _21(clip.asset), 'optionalAccess', _22 => _22.kind]) === "image") return;
1576
+ if (clip.type === "sequence") {
1577
+ clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta);
1578
+ if (clip.in < 1e-9) delete clip.in;
1579
+ return;
1580
+ }
1581
+ if (clip.type !== "media" || _optionalChain([videoAssetMap, 'call', _21 => _21(this.p), 'access', _22 => _22.get, 'call', _23 => _23(clip.asset), 'optionalAccess', _24 => _24.kind]) === "image") return;
1453
1582
  clip.in = Math.max(0, (_nullishCoalesce(clip.in, () => ( 0))) + delta * videoClipSpeed(clip));
1454
1583
  if (clip.in < 1e-9) delete clip.in;
1455
1584
  }
@@ -1476,7 +1605,7 @@ var Editor = (_class2 = class {
1476
1605
  clip.duration = at - clip.start;
1477
1606
  delete clip.fadeOut;
1478
1607
  delete clip.animateOut;
1479
- if (clip.type === "media") delete clip.fadeOutCurve;
1608
+ if (clip.type === "media" || clip.type === "sequence") delete clip.fadeOutCurve;
1480
1609
  track.clips.push(right);
1481
1610
  track.clips.sort((a, b) => a.start - b.start);
1482
1611
  return right;
@@ -1598,7 +1727,7 @@ var Editor = (_class2 = class {
1598
1727
  }
1599
1728
  case "remove_asset": {
1600
1729
  const asset = this.asset(e.asset);
1601
- const users = this.p.tracks.flatMap((t) => t.clips.filter((c) => c.type === "media" && c.asset === asset.id).map((c) => c.id));
1730
+ const users = allVideoTracks(this.p).flatMap(({ track: t }) => t.clips.filter((c) => c.type === "media" && c.asset === asset.id).map((c) => c.id));
1602
1731
  if (users.length) refuse(`Asset "${asset.id}" is used by clip${users.length > 1 ? "s" : ""} ${users.map((u) => `"${u}"`).join(", ")}; delete ${users.length > 1 ? "them" : "it"} first.`);
1603
1732
  this.p.assets = this.p.assets.filter((a) => a !== asset);
1604
1733
  return this.outcomes.push({ op: e.op, asset: asset.id });
@@ -1640,7 +1769,7 @@ var Editor = (_class2 = class {
1640
1769
  const mode = this.placement(e.mode, e.ripple);
1641
1770
  const draft = { ...e.clip, type: _nullishCoalesce(e.clip.type, () => ( "media")) };
1642
1771
  this.kindFits(track, draft);
1643
- const ids = this.p.tracks.flatMap((t) => t.clips.map((c) => c.id));
1772
+ const ids = this.ids();
1644
1773
  if (draft.id !== void 0 && ids.includes(draft.id)) refuse(`There is already a clip "${draft.id}".`);
1645
1774
  draft.id ??= freshId("c", ids);
1646
1775
  if (draft.duration === void 0) {
@@ -1650,6 +1779,10 @@ var Editor = (_class2 = class {
1650
1779
  else if (asset.duration === void 0) refuse(`Asset "${asset.id}" has no duration, so give the clip a "duration".`);
1651
1780
  else draft.duration = (asset.duration - (_nullishCoalesce(draft.in, () => ( 0)))) / (_nullishCoalesce(draft.speed, () => ( 1)));
1652
1781
  if (!(draft.duration > 0)) refuse(`"in" (${draft.in} ms) is at or past the end of "${asset.id}" (${asset.duration} ms).`);
1782
+ } else if (draft.type === "sequence") {
1783
+ const length = videoSequenceDuration(videoSequenceMap(this.p).get(draft.sequence)) - (_nullishCoalesce(draft.in, () => ( 0)));
1784
+ if (!(length > 0)) refuse(`"in" (${_nullishCoalesce(draft.in, () => ( 0))} ms) is at or past the end of sequence "${draft.sequence}".`);
1785
+ draft.duration = length;
1653
1786
  } else draft.duration = VIDEO_DEFAULT_TITLE_DURATION;
1654
1787
  }
1655
1788
  const end = sortedVideoClips(track).reduce((m, c) => Math.max(m, videoClipEnd(c)), 0);
@@ -1684,7 +1817,7 @@ var Editor = (_class2 = class {
1684
1817
  if (e.edge === "start") {
1685
1818
  const delta = e.to - clip.start;
1686
1819
  if (clip.duration - delta < minimum - VIDEO_TIME_EPSILON) refuse(`Trimming the start to ${formatVideoSeconds(e.to)} would leave "${clip.id}" shorter than a frame; it ends at ${formatVideoSeconds(end)}.`);
1687
- if (clip.type === "media" && !isRamped(clip)) {
1820
+ if (clip.type === "media" && !isRamped(clip) || clip.type === "sequence") {
1688
1821
  const sourceIn = (_nullishCoalesce(clip.in, () => ( 0))) + delta * speed;
1689
1822
  if (sourceIn < -VIDEO_TIME_EPSILON) refuse(`"${clip.id}" starts ${Math.round((_nullishCoalesce(clip.in, () => ( 0))) / speed)} ms into its source, so its start can move at most that far earlier (to ${formatVideoSeconds(clip.start - (_nullishCoalesce(clip.in, () => ( 0))) / speed)}).`);
1690
1823
  clip.in = Math.max(0, sourceIn);
@@ -1715,9 +1848,14 @@ var Editor = (_class2 = class {
1715
1848
  if (clip.type === "media" && !isRamped(clip)) {
1716
1849
  const asset = assets.get(clip.asset);
1717
1850
  const out = (_nullishCoalesce(clip.in, () => ( 0))) + duration * speed;
1718
- if (_optionalChain([asset, 'optionalAccess', _23 => _23.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
1851
+ if (_optionalChain([asset, 'optionalAccess', _25 => _25.duration]) !== void 0 && out > asset.duration + VIDEO_TIME_EPSILON)
1719
1852
  refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + (asset.duration - (_nullishCoalesce(clip.in, () => ( 0)))) / speed)}, where "${asset.id}" runs out.`);
1720
1853
  }
1854
+ if (clip.type === "sequence") {
1855
+ const seq = videoSequenceMap(this.p).get(clip.sequence);
1856
+ const length = seq ? videoSequenceDuration(seq) : Infinity;
1857
+ if ((_nullishCoalesce(clip.in, () => ( 0))) + duration > length + VIDEO_TIME_EPSILON) refuse(`"${clip.id}" can end at most at ${formatVideoSeconds(clip.start + length - (_nullishCoalesce(clip.in, () => ( 0))))}, where sequence "${clip.sequence}" ends.`);
1858
+ }
1721
1859
  if (e.ripple) {
1722
1860
  const tracks = this.rippleTracks([track]);
1723
1861
  let created = [];
@@ -1773,7 +1911,7 @@ var Editor = (_class2 = class {
1773
1911
  let stretch = 1;
1774
1912
  if (at.clip.type === "media" && !("duration" in set)) {
1775
1913
  const was = videoClipSpeed(at.clip);
1776
- const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _24 => _24.remove, 'optionalAccess', _25 => _25.includes, 'call', _26 => _26("speed")]) ? 1 : was;
1914
+ const now = "speed" in set ? set.speed : _optionalChain([e, 'access', _26 => _26.remove, 'optionalAccess', _27 => _27.includes, 'call', _28 => _28("speed")]) ? 1 : was;
1777
1915
  if (now !== was) {
1778
1916
  set.duration = at.clip.duration * was / now;
1779
1917
  stretch = was / now;
@@ -1792,6 +1930,7 @@ var Editor = (_class2 = class {
1792
1930
  case "set_time_remap": {
1793
1931
  const { track, clip } = this.clip(e.clip);
1794
1932
  this.unlocked(track);
1933
+ if (clip.type === "sequence") refuse(`Clip "${clip.id}" is a compound clip, which plays its sequence at normal speed; ramp the clips inside the sequence instead.`);
1795
1934
  if (clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to ramp; only video and audio clips change speed.`);
1796
1935
  const asset = this.asset(clip.asset);
1797
1936
  if (asset.kind === "image") refuse(`Clip "${clip.id}" is a still ("${asset.id}"), so it has no motion to ramp.`);
@@ -1849,16 +1988,16 @@ var Editor = (_class2 = class {
1849
1988
  case "slip_clip": {
1850
1989
  const { track, clip } = this.clip(e.clip);
1851
1990
  this.unlocked(track);
1852
- if (clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to slip; only video and audio clips slip.`);
1853
- const asset = this.asset(clip.asset);
1854
- if (asset.kind === "image") refuse(`Clip "${clip.id}" is a still ("${asset.id}"), so it has no source to slip; only video and audio clips slip.`);
1855
- const handles = videoClipHandles(clip, videoAssetMap(this.p));
1991
+ if (clip.type !== "media" && clip.type !== "sequence") refuse(`Clip "${clip.id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to slip; only video, audio and compound clips slip.`);
1992
+ const source = clip.type === "sequence" ? `sequence "${clip.sequence}"` : `"${this.asset(clip.asset).id}"`;
1993
+ if (clip.type === "media" && this.asset(clip.asset).kind === "image") refuse(`Clip "${clip.id}" is a still ("${clip.asset}"), so it has no source to slip; only video and audio clips slip.`);
1994
+ const handles = videoClipHandles(clip, videoAssetMap(this.p), videoSequenceMap(this.p));
1856
1995
  const { lead, tail } = videoClipOverhang(track, clip);
1857
1996
  const delta = this.clamp(
1858
1997
  e.delta,
1859
1998
  lead - handles.before,
1860
1999
  handles.after - tail,
1861
- () => e.delta > 0 ? `"${clip.id}" already plays to the end of "${asset.id}"${tail ? ` (less the ${tail} ms the transition after it needs)` : ""}, so it cannot slip later; slip it earlier (a negative delta).` : `"${clip.id}" already starts at the beginning of "${asset.id}"${lead ? ` (plus the ${lead} ms its transition needs)` : ""}, so it cannot slip earlier; slip it later (a positive delta).`
2000
+ () => e.delta > 0 ? `"${clip.id}" already plays to the end of ${source}${tail ? ` (less the ${tail} ms the transition after it needs)` : ""}, so it cannot slip later; slip it earlier (a negative delta).` : `"${clip.id}" already starts at the beginning of ${source}${lead ? ` (plus the ${lead} ms its transition needs)` : ""}, so it cannot slip earlier; slip it later (a positive delta).`
1862
2001
  );
1863
2002
  this.moveIn(clip, delta);
1864
2003
  return this.outcomes.push({ op: e.op, clip: clip.id, track: track.id, delta });
@@ -1876,8 +2015,8 @@ var Editor = (_class2 = class {
1876
2015
  refuse(`"${L.id}" ends at ${formatVideoSeconds(videoClipEnd(L))} but "${R.id}" starts at ${formatVideoSeconds(R.start)}; a roll needs two clips that meet at a cut (use trim_clip to move one edge).`);
1877
2016
  }
1878
2017
  const assets = videoAssetMap(this.p);
1879
- const lo = Math.max(-(L.duration - this.frame), -(videoClipHandles(R, assets).before - videoClipOverhang(track, R).lead));
1880
- const hi = Math.min(R.duration - this.frame, videoClipHandles(L, assets).after - videoClipOverhang(track, L).tail);
2018
+ const lo = Math.max(-(L.duration - this.frame), -(videoClipHandles(R, assets, videoSequenceMap(this.p)).before - videoClipOverhang(track, R).lead));
2019
+ const hi = Math.min(R.duration - this.frame, videoClipHandles(L, assets, videoSequenceMap(this.p)).after - videoClipOverhang(track, L).tail);
1881
2020
  const delta = this.clamp(
1882
2021
  e.delta,
1883
2022
  lo,
@@ -1903,8 +2042,8 @@ var Editor = (_class2 = class {
1903
2042
  earlier: before ? before.duration - this.frame : clip.start - others.filter((c) => videoClipEnd(c) <= clip.start + VIDEO_TIME_EPSILON).reduce((m, c) => Math.max(m, videoClipEnd(c)), 0),
1904
2043
  later: after ? after.duration - this.frame : others.filter((c) => c.start >= end - VIDEO_TIME_EPSILON).reduce((m, c) => Math.min(m, c.start - end), Infinity)
1905
2044
  };
1906
- const lo = -Math.min(room.earlier, after ? videoClipHandles(after, assets).before - videoClipOverhang(track, after).lead : Infinity);
1907
- const hi = Math.min(room.later, before ? videoClipHandles(before, assets).after - videoClipOverhang(track, before).tail : Infinity);
2045
+ const lo = -Math.min(room.earlier, after ? videoClipHandles(after, assets, videoSequenceMap(this.p)).before - videoClipOverhang(track, after).lead : Infinity);
2046
+ const hi = Math.min(room.later, before ? videoClipHandles(before, assets, videoSequenceMap(this.p)).after - videoClipOverhang(track, before).tail : Infinity);
1908
2047
  const delta = this.clamp(
1909
2048
  e.delta,
1910
2049
  lo,
@@ -1919,7 +2058,7 @@ var Editor = (_class2 = class {
1919
2058
  this.trimHead(after, delta);
1920
2059
  }
1921
2060
  track.clips.sort((a, b) => a.start - b.start);
1922
- return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _27 => _27.id]), clip.id, _optionalChain([after, 'optionalAccess', _28 => _28.id])].filter((x) => !!x), track: track.id, delta });
2061
+ return this.outcomes.push({ op: e.op, clip: clip.id, clips: [_optionalChain([before, 'optionalAccess', _29 => _29.id]), clip.id, _optionalChain([after, 'optionalAccess', _30 => _30.id])].filter((x) => !!x), track: track.id, delta });
1923
2062
  }
1924
2063
  case "lift_range":
1925
2064
  case "extract_range": {
@@ -1946,11 +2085,11 @@ var Editor = (_class2 = class {
1946
2085
  const { track, clip } = this.clip(e.clip);
1947
2086
  this.unlocked(track);
1948
2087
  if (!VIDEO_KEYFRAME_PROPERTIES.includes(e.property)) refuse(`"${e.property}" cannot be keyframed; use one of ${VIDEO_KEYFRAME_PROPERTIES.join(", ")}.`);
1949
- if (e.property === "volume" && clip.type !== "media") refuse(`Clip "${clip.id}" is a ${clip.type} clip and has no volume; volume keys go on media clips.`);
2088
+ if (e.property === "volume" && clip.type !== "media" && clip.type !== "sequence") refuse(`Clip "${clip.id}" is a ${clip.type} clip and has no volume; volume keys go on media and compound clips.`);
1950
2089
  const limits = VIDEO_KEYFRAME_LIMITS[e.property];
1951
2090
  const transform = VIDEO_TRANSFORM_PROPERTIES.includes(e.property);
1952
2091
  const c = clip;
1953
- const current = transform ? _optionalChain([c, 'access', _29 => _29.transform, 'optionalAccess', _30 => _30[e.property]]) : c[e.property];
2092
+ const current = transform ? _optionalChain([c, 'access', _31 => _31.transform, 'optionalAccess', _32 => _32[e.property]]) : c[e.property];
1954
2093
  let value;
1955
2094
  if (e.keys === null) {
1956
2095
  const v = videoValueAt(current, 0, limits.default);
@@ -2032,7 +2171,7 @@ function applyVideoEdits(project, edits) {
2032
2171
  editor.run(edit);
2033
2172
  } catch (error) {
2034
2173
  const message = error.message;
2035
- return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _31 => _31.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
2174
+ return { ok: false, error: `${list.length > 1 ? `op ${i + 1} (${_optionalChain([edit, 'optionalAccess', _33 => _33.op])}): ` : ""}${message}${/Nothing was changed\.$/.test(message) ? "" : " Nothing was changed."}` };
2036
2175
  }
2037
2176
  }
2038
2177
  let next;
@@ -2051,7 +2190,7 @@ function applyVideoEdits(project, edits) {
2051
2190
 
2052
2191
  // src/video-editor/evaluate.ts
2053
2192
  function videoTransformAt(transform, clipTime = 0) {
2054
- return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _32 => _32.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _33 => _33.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _34 => _34.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _35 => _35.rotation]), clipTime, 0) };
2193
+ return { x: videoValueAt(_optionalChain([transform, 'optionalAccess', _34 => _34.x]), clipTime, 0), y: videoValueAt(_optionalChain([transform, 'optionalAccess', _35 => _35.y]), clipTime, 0), scale: videoValueAt(_optionalChain([transform, 'optionalAccess', _36 => _36.scale]), clipTime, 1), rotation: videoValueAt(_optionalChain([transform, 'optionalAccess', _37 => _37.rotation]), clipTime, 0) };
2055
2194
  }
2056
2195
  function placeVideoRect(frame, size, fit = "contain", transform = {}, clipTime = 0) {
2057
2196
  let w = size.width;
@@ -2097,13 +2236,13 @@ function videoFadeCurveGain(curve, x) {
2097
2236
  }
2098
2237
  function fadeGain(clip, time, sound = false) {
2099
2238
  let g = 1;
2100
- const shape = sound && clip.type === "media" ? clip : void 0;
2101
- if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _36 => _36.fadeInCurve]), (time - clip.start) / clip.fadeIn);
2102
- if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _37 => _37.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
2239
+ const shape = sound && (clip.type === "media" || clip.type === "sequence") ? clip : void 0;
2240
+ if (clip.fadeIn) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _38 => _38.fadeInCurve]), (time - clip.start) / clip.fadeIn);
2241
+ if (clip.fadeOut) g *= videoFadeCurveGain(_optionalChain([shape, 'optionalAccess', _39 => _39.fadeOutCurve]), (videoClipEnd(clip) - time) / clip.fadeOut);
2103
2242
  return g;
2104
2243
  }
2105
2244
  function videoClipSoundGain(clip, clipTime) {
2106
- const volume = clip.type === "media" ? videoValueAt(clip.volume, clipTime, 1) : 1;
2245
+ const volume = clip.type === "media" || clip.type === "sequence" ? videoValueAt(clip.volume, clipTime, 1) : 1;
2107
2246
  return volume * fadeGain(clip, clip.start + Math.min(Math.max(clipTime, 0), clip.duration), true);
2108
2247
  }
2109
2248
  function activeOnTrack(track, time) {
@@ -2112,7 +2251,7 @@ function activeOnTrack(track, time) {
2112
2251
  const before = clip.transition ? videoClipBefore(track, clip) : null;
2113
2252
  const lead = before ? clip.transition.duration / 2 : 0;
2114
2253
  const next = videoClipAfter(track, clip);
2115
- const tail = _optionalChain([next, 'optionalAccess', _38 => _38.transition]) ? next.transition.duration / 2 : 0;
2254
+ const tail = _optionalChain([next, 'optionalAccess', _40 => _40.transition]) ? next.transition.duration / 2 : 0;
2116
2255
  if (time < clip.start - lead || time >= videoClipEnd(clip) + tail) continue;
2117
2256
  const a = { clip };
2118
2257
  if (lead && time < clip.start + lead) a.incoming = { progress: clamp012((time - (clip.start - lead)) / (2 * lead)), type: clip.transition.type, transition: clip.transition };
@@ -2123,28 +2262,43 @@ function activeOnTrack(track, time) {
2123
2262
  }
2124
2263
  var sourceTimeAt = (clip, time) => clip.type === "media" ? videoClipSourceTime(clip, time - clip.start) : 0;
2125
2264
  function videoFrameAt(project, time) {
2126
- const assets = videoAssetMap(project);
2265
+ const { layers, audio } = tracksAt(project, project.tracks, time, { assets: videoAssetMap(project), sequences: videoSequenceMap(project), within: [], prefix: "" });
2266
+ return { time, width: project.width, height: project.height, background: _nullishCoalesce(project.background, () => ( "#000000")), layers, audio };
2267
+ }
2268
+ function tracksAt(project, tracks, time, scope) {
2269
+ const { assets, prefix } = scope;
2127
2270
  const layers = [];
2128
2271
  const audio = [];
2129
- for (let ti = project.tracks.length - 1; ti >= 0; ti--) {
2130
- const track = project.tracks[ti];
2272
+ for (let ti = tracks.length - 1; ti >= 0; ti--) {
2273
+ const track = tracks[ti];
2131
2274
  for (const { clip, incoming, outgoing } of activeOnTrack(track, time)) {
2132
- if (_optionalChain([incoming, 'optionalAccess', _39 => _39.type]) === "dip" && incoming.progress < 0.5) continue;
2133
- if (_optionalChain([outgoing, 'optionalAccess', _40 => _40.type]) === "dip" && outgoing.progress >= 0.5) continue;
2275
+ if (_optionalChain([incoming, 'optionalAccess', _41 => _41.type]) === "dip" && incoming.progress < 0.5) continue;
2276
+ if (_optionalChain([outgoing, 'optionalAccess', _42 => _42.type]) === "dip" && outgoing.progress >= 0.5) continue;
2134
2277
  const inside = Math.min(Math.max(time, clip.start), videoClipEnd(clip));
2135
2278
  const fade = fadeGain(clip, inside);
2136
2279
  const level = (t, transition, into) => videoFadeCurveGain(videoTransitionAudioCurve(transition), t.type === "dip" ? Math.abs(1 - 2 * t.progress) : into ? t.progress : 1 - t.progress);
2137
2280
  const mix = (incoming ? level(incoming, incoming.transition, true) : 1) * (outgoing ? level(outgoing, outgoing.next.transition, false) : 1);
2138
2281
  const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
2139
- const sourceTime = sourceTimeAt(clip, time);
2282
+ const sourceTime = clip.type === "sequence" ? (_nullishCoalesce(clip.in, () => ( 0))) + time - clip.start : sourceTimeAt(clip, time);
2140
2283
  const clipTime = time - clip.start;
2284
+ let nested = null;
2285
+ if (clip.type === "sequence") {
2286
+ const seq = scope.sequences.get(clip.sequence);
2287
+ if (!seq || scope.within.includes(seq.id)) continue;
2288
+ nested = tracksAt(project, seq.tracks, sourceTime, { ...scope, within: [...scope.within, seq.id], prefix: `${prefix}${clip.id}/` });
2289
+ }
2141
2290
  const audible = videoAssetHasSound(asset);
2142
2291
  if (audible && clip.type === "media" && !clip.muted && !track.muted) {
2143
2292
  const gain = videoValueAt(clip.volume, clipTime, 1) * (_nullishCoalesce(track.volume, () => ( 1))) * fadeGain(clip, inside, true) * mix;
2144
- audio.push({ clip: clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeedAt(clip, clipTime) });
2293
+ audio.push({ clip: prefix + clip.id, track: track.id, asset: asset.id, src: asset.src, sourceTime, gain, speed: videoClipSpeedAt(clip, clipTime) });
2294
+ }
2295
+ if (nested && clip.type === "sequence" && !clip.muted && !track.muted) {
2296
+ const gain = videoValueAt(clip.volume, clipTime, 1) * (_nullishCoalesce(track.volume, () => ( 1))) * fadeGain(clip, inside, true) * mix;
2297
+ for (const a of nested.audio) audio.push({ ...a, gain: a.gain * gain });
2145
2298
  }
2146
2299
  if (track.kind !== "video" || track.hidden) continue;
2147
2300
  if (clip.type === "media" && (!asset || asset.kind === "audio")) continue;
2301
+ if (nested && !nested.layers.length) continue;
2148
2302
  const shown = videoClipAnimated(clip, project);
2149
2303
  let opacity = videoValueAt(shown.opacity, clipTime, 1) * fade;
2150
2304
  let wipe;
@@ -2161,31 +2315,35 @@ function videoFrameAt(project, time) {
2161
2315
  shift = videoTransitionEase(incoming.progress) - 1;
2162
2316
  } else dip = { color: _nullishCoalesce(incoming.transition.color, () => ( "#000000")), opacity: (1 - incoming.progress) * 2 };
2163
2317
  }
2164
- if (_optionalChain([outgoing, 'optionalAccess', _41 => _41.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
2165
- if (_optionalChain([outgoing, 'optionalAccess', _42 => _42.type]) === "push") {
2318
+ if (_optionalChain([outgoing, 'optionalAccess', _43 => _43.type]) === "dip") dip = { color: _nullishCoalesce(outgoing.next.transition.color, () => ( "#000000")), opacity: outgoing.progress * 2 };
2319
+ if (_optionalChain([outgoing, 'optionalAccess', _44 => _44.type]) === "push") {
2166
2320
  shiftDirection = _nullishCoalesce(outgoing.next.transition.direction, () => ( "right"));
2167
2321
  shift = videoTransitionEase(outgoing.progress);
2168
2322
  }
2169
2323
  const size = clip.type === "media" ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
2170
- const fit = _nullishCoalesce(clip.fit, () => ( (clip.type === "media" ? "contain" : "fill")));
2324
+ const fit = _nullishCoalesce(clip.fit, () => ( (clip.type === "media" || clip.type === "sequence" ? "contain" : "fill")));
2171
2325
  const placed = placeVideoRect(project, size, clip.type === "text" ? "fill" : fit, shown.transform, clipTime);
2172
2326
  const across = shiftDirection === "left" || shiftDirection === "right";
2173
2327
  const sign = shiftDirection === "left" || shiftDirection === "up" ? -1 : 1;
2174
2328
  const rect = shift ? { ...placed, ...across ? { x: placed.x + sign * shift * project.width } : { y: placed.y + sign * shift * project.height } } : placed;
2175
- const layer = { clip: clip.id, track: track.id, type: clip.type, opacity: clamp012(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access', _43 => _43.transform, 'optionalAccess', _44 => _44.rotation]), clipTime, 0) };
2329
+ const layer = { clip: prefix + clip.id, track: track.id, type: clip.type, opacity: clamp012(opacity), rect, rotation: videoValueAt(_optionalChain([shown, 'access', _45 => _45.transform, 'optionalAccess', _46 => _46.rotation]), clipTime, 0) };
2176
2330
  if (wipe) layer.wipe = wipe;
2177
2331
  if (iris) layer.iris = iris;
2178
2332
  if (clip.blend && clip.blend !== "normal") layer.blend = clip.blend;
2179
2333
  const grade = normalizeVideoGrade(clip.grade);
2180
2334
  if (grade) layer.grade = grade;
2181
2335
  if (clip.type === "media") Object.assign(layer, { asset: asset.id, assetKind: asset.kind, src: asset.src, sourceTime });
2336
+ if (clip.type === "sequence") Object.assign(layer, { sequence: clip.sequence, sourceTime, layers: nested.layers });
2182
2337
  if (clip.type === "text") layer.text = videoTextStyle(clip);
2183
2338
  if (clip.type === "color") layer.color = clip.color;
2184
2339
  if (layer.opacity > 0) layers.push(layer);
2185
- if (dip && dip.opacity > 0) layers.push({ clip: `transition:${incoming ? clip.id : outgoing.next.id}`, track: track.id, type: "color", color: dip.color, opacity: clamp012(dip.opacity), rect: { x: 0, y: 0, width: project.width, height: project.height }, rotation: 0 });
2340
+ if (dip && dip.opacity > 0) layers.push({ clip: `transition:${prefix}${incoming ? clip.id : outgoing.next.id}`, track: track.id, type: "color", color: dip.color, opacity: clamp012(dip.opacity), rect: { x: 0, y: 0, width: project.width, height: project.height }, rotation: 0 });
2186
2341
  }
2187
2342
  }
2188
- return { time, width: project.width, height: project.height, background: _nullishCoalesce(project.background, () => ( "#000000")), layers, audio };
2343
+ return { layers, audio };
2344
+ }
2345
+ function videoFrameLeafLayers(layers) {
2346
+ return layers.flatMap((l) => l.layers ? videoFrameLeafLayers(l.layers) : [l]);
2189
2347
  }
2190
2348
 
2191
2349
  // src/video-editor/audio.ts
@@ -2843,8 +3001,8 @@ function videoTranscriptTrack(project, track) {
2843
3001
  return track;
2844
3002
  }
2845
3003
  const assets = videoAssetMap(project);
2846
- const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _45 => _45.get, 'call', _46 => _46(c.asset), 'optionalAccess', _47 => _47.transcript, 'optionalAccess', _48 => _48.length]));
2847
- return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _49 => _49.id]), () => ( null));
3004
+ const transcribed = (t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _47 => _47.get, 'call', _48 => _48(c.asset), 'optionalAccess', _49 => _49.transcript, 'optionalAccess', _50 => _50.length]));
3005
+ return _nullishCoalesce(_optionalChain([(_nullishCoalesce(project.tracks.find((t) => t.kind === "audio" && transcribed(t)), () => ( project.tracks.find(transcribed)))), 'optionalAccess', _51 => _51.id]), () => ( null));
2848
3006
  }
2849
3007
  function videoTimelineTranscript(project, options = {}) {
2850
3008
  const id = videoTranscriptTrack(project, options.track);
@@ -2854,8 +3012,8 @@ function videoTimelineTranscript(project, options = {}) {
2854
3012
  const out = [];
2855
3013
  for (const clip of sortedVideoClips(track)) {
2856
3014
  if (clip.type !== "media") continue;
2857
- const words = _optionalChain([assets, 'access', _50 => _50.get, 'call', _51 => _51(clip.asset), 'optionalAccess', _52 => _52.transcript]);
2858
- if (!_optionalChain([words, 'optionalAccess', _53 => _53.length])) continue;
3015
+ const words = _optionalChain([assets, 'access', _52 => _52.get, 'call', _53 => _53(clip.asset), 'optionalAccess', _54 => _54.transcript]);
3016
+ if (!_optionalChain([words, 'optionalAccess', _55 => _55.length])) continue;
2859
3017
  const speed = videoClipSpeed(clip);
2860
3018
  const from = _nullishCoalesce(clip.in, () => ( 0));
2861
3019
  const to = from + clip.duration * speed;
@@ -2946,6 +3104,7 @@ function videoWordAt(words, time) {
2946
3104
  // src/video-editor/render.ts
2947
3105
  var esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2948
3106
  var n = (v) => String(Math.round(v * 100) / 100);
3107
+ var n4 = (v) => String(Math.round(v * 1e4) / 1e4);
2949
3108
  var videoIrisRadius = (frame, progress) => progress * Math.hypot(frame.width / 2, frame.height / 2);
2950
3109
  function videoWipeRect(rect, wipe) {
2951
3110
  const { direction, progress } = wipe;
@@ -2958,15 +3117,18 @@ function videoWipeRect(rect, wipe) {
2958
3117
  }
2959
3118
  function videoFrameToSvg(frame, options = {}, assets = []) {
2960
3119
  const defs = [];
2961
- const body = [];
2962
3120
  const posters = new Map(assets.map((a) => [a.id, a.poster]));
2963
- frame.layers.forEach((layer, i) => {
3121
+ const draw = (layers, prefix) => layers.map((layer, index) => {
3122
+ const i = `${prefix}${index}`;
2964
3123
  const { rect } = layer;
2965
3124
  const attrs = [];
3125
+ const style = [];
2966
3126
  if (layer.opacity < 1) attrs.push(`opacity="${n(layer.opacity)}"`);
2967
- if (layer.blend) attrs.push(`style="mix-blend-mode:${layer.blend === "add" ? "plus-lighter" : layer.blend}"`);
3127
+ if (layer.blend) style.push(`mix-blend-mode:${layer.blend === "add" ? "plus-lighter" : layer.blend}`);
3128
+ if (layer.layers) style.push("isolation:isolate");
3129
+ if (style.length) attrs.push(`style="${style.join(";")}"`);
2968
3130
  if (layer.grade) {
2969
- defs.push(videoGradeSvgFilter(`g${i}`, layer.grade, layer.grade.lut ? _optionalChain([options, 'access', _54 => _54.lutFor, 'optionalCall', _55 => _55(layer.grade.lut.src)]) : null));
3131
+ defs.push(videoGradeSvgFilter(`g${i}`, layer.grade, layer.grade.lut ? _optionalChain([options, 'access', _56 => _56.lutFor, 'optionalCall', _57 => _57(layer.grade.lut.src)]) : null));
2970
3132
  attrs.push(`filter="url(#g${i})"`);
2971
3133
  }
2972
3134
  if (layer.rotation) attrs.push(`transform="rotate(${n(layer.rotation)} ${n(rect.x + rect.width / 2)} ${n(rect.y + rect.height / 2)})"`);
@@ -2977,7 +3139,9 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
2977
3139
  }
2978
3140
  const box = `x="${n(rect.x)}" y="${n(rect.y)}" width="${n(rect.width)}" height="${n(rect.height)}"`;
2979
3141
  let inner = "";
2980
- if (layer.type === "color") inner = `<rect ${box} fill="${esc(_nullishCoalesce(layer.color, () => ( "#000000")))}"/>`;
3142
+ if (layer.layers) {
3143
+ inner = `<g transform="translate(${n(rect.x)} ${n(rect.y)}) scale(${n4(rect.width / frame.width)} ${n4(rect.height / frame.height)})">${draw(layer.layers, `${i}-`)}</g>`;
3144
+ } else if (layer.type === "color") inner = `<rect ${box} fill="${esc(_nullishCoalesce(layer.color, () => ( "#000000")))}"/>`;
2981
3145
  else if (layer.type === "text" && layer.text) {
2982
3146
  const t = layer.text;
2983
3147
  const lines = t.text.split("\n");
@@ -3005,9 +3169,10 @@ function videoFrameToSvg(frame, options = {}, assets = []) {
3005
3169
  defs.push(`<clipPath id="i${i}"><circle cx="${n(frame.width / 2)}" cy="${n(frame.height / 2)}" r="${n(videoIrisRadius(frame, layer.iris.progress))}"/></clipPath>`);
3006
3170
  item = `<g clip-path="url(#i${i})">${item}</g>`;
3007
3171
  }
3008
- body.push(item);
3009
- });
3010
- return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(frame.width)} ${n(frame.height)}" width="${n(frame.width)}" height="${n(frame.height)}">${defs.length ? `<defs>${defs.join("")}</defs>` : ""}<rect width="100%" height="100%" fill="${esc(frame.background)}"/>${body.join("")}</svg>`;
3172
+ return item;
3173
+ }).join("");
3174
+ const body = draw(frame.layers, "");
3175
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(frame.width)} ${n(frame.height)}" width="${n(frame.width)}" height="${n(frame.height)}">${defs.length ? `<defs>${defs.join("")}</defs>` : ""}<rect width="100%" height="100%" fill="${esc(frame.background)}"/>${body}</svg>`;
3011
3176
  }
3012
3177
  function videoProjectSvg(project, time, options = {}) {
3013
3178
  return videoFrameToSvg(videoFrameAt(project, time), { fps: project.fps, ...options }, project.assets);
@@ -3091,13 +3256,36 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
3091
3256
  ctx.globalAlpha = 1;
3092
3257
  ctx.fillStyle = frame.background;
3093
3258
  ctx.fillRect(0, 0, frame.width, frame.height);
3259
+ drawVideoLayers(ctx, frame, frame.layers, sourceFor, scale, options, 0);
3260
+ ctx.restore();
3261
+ }
3262
+ function drawVideoLayers(ctx, frame, layers, sourceFor, scale, options, depth) {
3094
3263
  const cw = Math.max(1, Math.round(frame.width * scale));
3095
3264
  const ch = Math.max(1, Math.round(frame.height * scale));
3096
- for (const layer of frame.layers) {
3097
- const setting = _optionalChain([layer, 'access', _56 => _56.grade, 'optionalAccess', _57 => _57.lut]);
3098
- const lut = setting ? _optionalChain([options, 'access', _58 => _58.lutFor, 'optionalCall', _59 => _59(setting.src)]) : null;
3099
- const scratch = lut ? _optionalChain([options, 'access', _60 => _60.scratch, 'optionalCall', _61 => _61(cw, ch)]) : null;
3265
+ for (const layer of layers) {
3266
+ let source = sourceFor;
3267
+ if (layer.layers) {
3268
+ const group = _optionalChain([options, 'access', _58 => _58.scratch, 'optionalCall', _59 => _59(cw, ch, 2 * depth)]);
3269
+ if (!group) {
3270
+ drawVideoLayers(ctx, frame, layer.layers, sourceFor, scale, options, depth + 1);
3271
+ continue;
3272
+ }
3273
+ const g = group.ctx;
3274
+ g.save();
3275
+ g.setTransform(1, 0, 0, 1, 0, 0);
3276
+ g.clearRect(0, 0, cw, ch);
3277
+ g.setTransform(scale, 0, 0, scale, 0, 0);
3278
+ g.globalAlpha = 1;
3279
+ g.globalCompositeOperation = "source-over";
3280
+ drawVideoLayers(g, frame, layer.layers, sourceFor, scale, options, depth + 1);
3281
+ g.restore();
3282
+ source = (l) => l === layer ? group.canvas : sourceFor(l);
3283
+ }
3284
+ const setting = _optionalChain([layer, 'access', _60 => _60.grade, 'optionalAccess', _61 => _61.lut]);
3285
+ const lut = setting ? _optionalChain([options, 'access', _62 => _62.lutFor, 'optionalCall', _63 => _63(setting.src)]) : null;
3286
+ const scratch = lut ? _optionalChain([options, 'access', _64 => _64.scratch, 'optionalCall', _65 => _65(cw, ch, 2 * depth + 1)]) : null;
3100
3287
  ctx.save();
3288
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
3101
3289
  if (lut && scratch) {
3102
3290
  const s = scratch.ctx;
3103
3291
  s.save();
@@ -3106,7 +3294,7 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
3106
3294
  s.setTransform(scale, 0, 0, scale, 0, 0);
3107
3295
  s.globalAlpha = 1;
3108
3296
  s.globalCompositeOperation = "source-over";
3109
- paintVideoLayer(s, frame, layer, sourceFor, options);
3297
+ paintVideoLayer(s, frame, layer, source, options);
3110
3298
  s.restore();
3111
3299
  const img = s.getImageData(0, 0, cw, ch);
3112
3300
  applyVideoLutToPixels(lut, img.data, _nullishCoalesce(setting.strength, () => ( 1)));
@@ -3118,15 +3306,14 @@ function drawVideoFrame(ctx, frame, sourceFor, scale = 1, options = {}) {
3118
3306
  } else {
3119
3307
  ctx.globalAlpha = layer.opacity;
3120
3308
  if (layer.blend && layer.blend !== "normal") ctx.globalCompositeOperation = layer.blend === "add" ? "lighter" : layer.blend;
3121
- paintVideoLayer(ctx, frame, layer, sourceFor, options);
3309
+ paintVideoLayer(ctx, frame, layer, source, options);
3122
3310
  }
3123
3311
  ctx.restore();
3124
3312
  }
3125
- ctx.restore();
3126
3313
  }
3127
3314
 
3128
3315
  // src/video-editor/export.ts
3129
- var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _62 => _62.name]), () => ( _optionalChain([asset, 'optionalAccess', _63 => _63.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
3316
+ var reelName = (asset) => (_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _66 => _66.name]), () => ( _optionalChain([asset, 'optionalAccess', _67 => _67.id]))), () => ( "AX"))).replace(/\.[a-z0-9]+$/i, "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 8) || "AX";
3130
3317
  function toVideoEdl(project, options = {}) {
3131
3318
  const track = options.track !== void 0 ? project.tracks.find((t) => t.id === options.track) : [...project.tracks].reverse().find((t) => t.kind === "video");
3132
3319
  if (!track) throw new Error(options.track !== void 0 ? `There is no track "${options.track}".` : "The project has no video track.");
@@ -3161,9 +3348,10 @@ function toVideoEdl(project, options = {}) {
3161
3348
  lines.push(row(n2, reel, `${t.type === "crossfade" || t.type === "dip" ? "D" : "W001"} ${frames}`, sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
3162
3349
  } else lines.push(row(n2, reel, "C", sourceAt(clip, recIn), sourceAt(clip, recOut), recIn, recOut));
3163
3350
  if (clip.type === "media" && videoClipSpeed(clip) !== 1) lines.push(`M2 ${reel.padEnd(8)} ${(videoClipSpeed(clip) * fps).toFixed(1).padStart(6)} ${tc(sourceAt(clip, recIn))}`);
3164
- if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _64 => _64.name]), () => ( _optionalChain([asset, 'optionalAccess', _65 => _65.src]))), () => ( clip.asset))}`);
3351
+ if (clip.type === "media") lines.push(`* FROM CLIP NAME: ${_nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _68 => _68.name]), () => ( _optionalChain([asset, 'optionalAccess', _69 => _69.src]))), () => ( clip.asset))}`);
3165
3352
  if (clip.type === "text") lines.push(`* TITLE: ${clip.text.replace(/\n/g, " ")}`);
3166
3353
  if (clip.type === "color") lines.push(`* COLOR: ${clip.color}`);
3354
+ if (clip.type === "sequence") lines.push(`* COMPOUND CLIP: ${_nullishCoalesce(clip.name, () => ( clip.sequence))} (sequence ${clip.sequence} from ${tc(_nullishCoalesce(clip.in, () => ( 0)))}, not flattened)`);
3167
3355
  lines.push("");
3168
3356
  }
3169
3357
  return lines.join("\n");
@@ -3219,7 +3407,7 @@ var color = (c) => {
3219
3407
  const h = hex[1].length === 3 ? [...hex[1]].map((x) => x + x).join("") : hex[1];
3220
3408
  return `0x${h.slice(0, 6)}${h.length === 8 ? `@${(parseInt(h.slice(6), 16) / 255).toFixed(3)}` : ""}`;
3221
3409
  };
3222
- var shellQuote = (s) => /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
3410
+ var videoShellQuote = (s) => /^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
3223
3411
  function atempo(speed) {
3224
3412
  const out = [];
3225
3413
  let s = speed;
@@ -3243,6 +3431,7 @@ function toVideoLoudnessMeasure(project, options = {}) {
3243
3431
  function buildFfmpeg(project, options, measure) {
3244
3432
  const notes = [];
3245
3433
  const assets = videoAssetMap(project);
3434
+ const sequences = videoSequenceMap(project);
3246
3435
  const D = videoProjectDuration(project);
3247
3436
  if (D <= 0) throw new Error("The project has no clips, so there is nothing to render.");
3248
3437
  const W = even(project.width);
@@ -3250,8 +3439,6 @@ function buildFfmpeg(project, options, measure) {
3250
3439
  const fps = project.fps;
3251
3440
  const inputs = [];
3252
3441
  const graph = measure ? [] : [`color=c=${color(_nullishCoalesce(project.background, () => ( "#000000")))}:s=${W}x${H}:r=${fps}:d=${sec(D)},format=yuva420p[base]`];
3253
- const audio = [];
3254
- let cur = "base";
3255
3442
  let usesDrawtext = false;
3256
3443
  const animated = /* @__PURE__ */ new Set();
3257
3444
  const ramped = /* @__PURE__ */ new Set();
@@ -3275,7 +3462,7 @@ function buildFfmpeg(project, options, measure) {
3275
3462
  const pts = videoTimeRemapBreakpoints(remap, Math.max(80, 2e3 / fps));
3276
3463
  const first = pts[0];
3277
3464
  const last = pts[pts.length - 1];
3278
- const n2 = audio.length;
3465
+ const n2 = ak;
3279
3466
  const fmtA = "aformat=sample_fmts=fltp:sample_rates=48000:channel_layouts=stereo";
3280
3467
  const silence = (ms) => `anullsrc=r=48000:cl=stereo,atrim=duration=${sec(ms)},${fmtA}`;
3281
3468
  const pieces = [];
@@ -3324,180 +3511,228 @@ function buildFfmpeg(project, options, measure) {
3324
3511
  } else inputs.push(["-ss", sec((_nullishCoalesce(clip.in, () => ( 0))) - lead * speed), "-t", sec(span * speed), "-i", src]);
3325
3512
  return inputs.length - 1;
3326
3513
  };
3327
- for (const track of [...project.tracks].reverse()) {
3328
- for (const raw of sortedVideoClips(track)) {
3329
- const clip = videoClipAnimated(raw, project);
3330
- const { lead, tail } = videoClipOverhang(track, clip);
3331
- const t0 = clip.start - lead;
3332
- const span = clip.duration + lead + tail;
3333
- const end = videoClipEnd(clip);
3334
- const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
3335
- if (clip.type === "media" && !asset) {
3336
- notes.push(`Clip "${clip.id}" was left out: its asset "${clip.asset}" is missing.`);
3337
- continue;
3338
- }
3339
- let index;
3340
- if (!measure && asset && asset.kind !== "audio" && track.kind === "video" && !track.hidden) index = input(clip, asset, lead, tail);
3341
- const audible = videoAssetHasSound(asset);
3342
- if (audible && clip.type === "media" && !clip.muted && !track.muted) {
3343
- const ai = _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
3344
- const trackGain = _nullishCoalesce(track.volume, () => ( 1));
3345
- const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(_nullishCoalesce(clip.volume, () => ( 1))) * trackGain}`;
3346
- let source2 = `[${ai}:a]`;
3347
- if (clip.timeRemap) {
3348
- source2 = `[r${audio.length}]`;
3349
- graph.push(...rampedSound(clip.timeRemap, `${ai}:a`, source2, lead, clip.duration + tail));
3350
- ramped.add("sound");
3514
+ let ak = 0;
3515
+ let q = 0;
3516
+ const compose = (tracks, base, length, scope) => {
3517
+ let cur2 = base;
3518
+ const sounds = [];
3519
+ for (const track of [...tracks].reverse()) {
3520
+ for (const raw of sortedVideoClips(track)) {
3521
+ const clip = videoClipAnimated(raw, project);
3522
+ const { lead, tail } = videoClipOverhang(track, clip);
3523
+ const t0 = clip.start - lead;
3524
+ const span = clip.duration + lead + tail;
3525
+ const end = videoClipEnd(clip);
3526
+ const asset = clip.type === "media" ? assets.get(clip.asset) : void 0;
3527
+ if (clip.type === "media" && !asset) {
3528
+ notes.push(`Clip "${clip.id}" was left out: its asset "${clip.asset}" is missing.`);
3529
+ continue;
3351
3530
  }
3352
- const chain2 = [...clip.timeRemap ? [] : ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip))], volume];
3353
- const before2 = clip.transition ? videoClipBefore(track, clip) : null;
3354
- const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
3355
- const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
3356
- const outCurve = next2 ? afadeCurve(videoTransitionAudioCurve(next2.transition)) : "";
3357
- if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
3358
- if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
3359
- if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
3360
- if (_optionalChain([next2, 'optionalAccess', _66 => _66.transition, 'access', _67 => _67.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
3361
- if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
3362
- if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
3363
- chain2.push(`adelay=${Math.round(t0)}:all=1`);
3364
- graph.push(`${source2}${chain2.join(",")}[a${audio.length}]`);
3365
- audio.push(`[a${audio.length}]`);
3366
- }
3367
- if (measure || track.kind !== "video" || track.hidden) continue;
3368
- const enable = `enable='between(t,${sec(t0)},${sec(t0 + span)})'`;
3369
- const next = `v${k++}`;
3370
- const onTimeline = `(t*1000-${Math.round(clip.start)})`;
3371
- const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
3372
- const tf = clip.transform;
3373
- const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _68 => _68[p]])));
3374
- if (clip.type === "text" && options.titleImage) {
3375
- 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 })]);
3376
- index = inputs.length - 1;
3377
- } else if (clip.type === "text") {
3378
- usesDrawtext = true;
3379
- const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
3380
- 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("*");
3381
- const coord = (axis) => {
3382
- const v = _optionalChain([tf, 'optionalAccess', _69 => _69[axis]]);
3383
- const base = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
3384
- return isVideoKeyframes(v) ? `${axis}='${base}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base}+${_nullishCoalesce(v, () => ( 0))}`;
3531
+ const shows = scope.picture && track.kind === "video" && !track.hidden;
3532
+ let compound = null;
3533
+ if (clip.type === "sequence") {
3534
+ const seq = sequences.get(clip.sequence);
3535
+ if (!seq || scope.within.includes(seq.id)) {
3536
+ notes.push(`Compound clip "${clip.id}" was left out: its sequence "${clip.sequence}" ${seq ? "contains itself" : "is missing"}.`);
3537
+ continue;
3538
+ }
3539
+ const from = (_nullishCoalesce(clip.in, () => ( 0))) - lead;
3540
+ if (from < -0.5) notes.push(`Compound clip "${clip.id}"'s transition needs ${Math.round(-from)} ms of sequence "${seq.id}" before its start, which is not there; it shows nothing then.`);
3541
+ const length2 = Math.max(videoSequenceDuration(seq), from + span);
3542
+ const hears = scope.sound && !clip.muted && !track.muted;
3543
+ const n2 = q++;
3544
+ const nestedBase = shows ? `nb${n2}` : null;
3545
+ if (nestedBase) graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(length2)},format=yuva420p[${nestedBase}]`);
3546
+ const sub = compose(seq.tracks, nestedBase, length2, { within: [...scope.within, seq.id], picture: shows, sound: hears });
3547
+ compound = { picture: null, sound: null };
3548
+ if (sub.video) {
3549
+ graph.push(`[${sub.video}]trim=start=${sec(Math.max(0, from))}:duration=${sec(span)},setpts=PTS-STARTPTS[sq${n2}]`);
3550
+ compound.picture = `[sq${n2}]`;
3551
+ }
3552
+ if (sub.audio.length) {
3553
+ graph.push(`${sub.audio.join("")}amix=inputs=${sub.audio.length}:normalize=0:duration=longest,apad,atrim=start=${sec(Math.max(0, from))}:end=${sec(from + span)}[sm${n2}]`);
3554
+ compound.sound = `[sm${n2}]`;
3555
+ }
3556
+ }
3557
+ let index;
3558
+ if (shows && asset && asset.kind !== "audio") index = input(clip, asset, lead, tail);
3559
+ const audible = clip.type === "sequence" ? !!_optionalChain([compound, 'optionalAccess', _70 => _70.sound]) : videoAssetHasSound(asset);
3560
+ if (scope.sound && audible && (clip.type === "media" || clip.type === "sequence") && !clip.muted && !track.muted) {
3561
+ const trackGain = _nullishCoalesce(track.volume, () => ( 1));
3562
+ const volume = isVideoKeyframes(clip.volume) ? `volume='${trackGain}*(${expr(clip.volume, `(t*1000${lead ? `-${Math.round(lead)}` : ""})`, 1, "volume")})':eval=frame` : `volume=${(_nullishCoalesce(clip.volume, () => ( 1))) * trackGain}`;
3563
+ const ramp = clip.type === "media" ? clip.timeRemap : void 0;
3564
+ const ai = compound ? -1 : _nullishCoalesce(index, () => ( input(clip, asset, lead, tail)));
3565
+ let source2 = _nullishCoalesce(_optionalChain([compound, 'optionalAccess', _71 => _71.sound]), () => ( `[${ai}:a]`));
3566
+ if (ramp) {
3567
+ source2 = `[r${ak}]`;
3568
+ graph.push(...rampedSound(ramp, `${ai}:a`, source2, lead, clip.duration + tail));
3569
+ ramped.add("sound");
3570
+ }
3571
+ const chain2 = [...ramp ? [] : ["asetpts=PTS-STARTPTS", ...atempo(videoClipSpeed(clip))], volume];
3572
+ const before2 = clip.transition ? videoClipBefore(track, clip) : null;
3573
+ const next2 = sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition);
3574
+ const inCurve = clip.transition ? afadeCurve(videoTransitionAudioCurve(clip.transition)) : "";
3575
+ const outCurve = next2 ? afadeCurve(videoTransitionAudioCurve(next2.transition)) : "";
3576
+ if (lead) chain2.push(`afade=t=in:st=0:d=${sec(2 * lead)}${inCurve}`);
3577
+ if (tail) chain2.push(`afade=t=out:st=${sec(span - 2 * tail)}:d=${sec(2 * tail)}${outCurve}`);
3578
+ if (before2 && clip.transition.type === "dip") chain2.push(`afade=t=in:st=0:d=${sec(clip.transition.duration / 2)}${inCurve}`);
3579
+ if (_optionalChain([next2, 'optionalAccess', _72 => _72.transition, 'access', _73 => _73.type]) === "dip") chain2.push(`afade=t=out:st=${sec(span - next2.transition.duration / 2)}:d=${sec(next2.transition.duration / 2)}${outCurve}`);
3580
+ if (clip.fadeIn) chain2.push(`afade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}${afadeCurve(clip.fadeInCurve)}`);
3581
+ if (clip.fadeOut) chain2.push(`afade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}${afadeCurve(clip.fadeOutCurve)}`);
3582
+ chain2.push(`adelay=${Math.round(t0)}:all=1`);
3583
+ graph.push(`${source2}${chain2.join(",")}[a${ak}]`);
3584
+ sounds.push(`[a${ak++}]`);
3585
+ }
3586
+ if (!shows || compound && !compound.picture) continue;
3587
+ const enable = `enable='between(t,${sec(t0)},${sec(t0 + span)})'`;
3588
+ const next = `v${k++}`;
3589
+ const onTimeline = `(t*1000-${Math.round(clip.start)})`;
3590
+ const onChain = (v) => `(${v}*1000${lead ? `-${Math.round(lead)}` : ""})`;
3591
+ const tf = clip.transform;
3592
+ const moving = VIDEO_TRANSFORM_PROPERTIES.some((p) => isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _74 => _74[p]])));
3593
+ if (clip.type === "text" && options.titleImage) {
3594
+ 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 })]);
3595
+ index = inputs.length - 1;
3596
+ } else if (clip.type === "text") {
3597
+ usesDrawtext = true;
3598
+ if (scope.within.length) notes.push(`Title "${clip.id}" is inside a compound clip and drawn with drawtext, which leaves the sequence's transparent areas transparent, so it shows only over pictures in the sequence; pass titleImage to draw it anywhere.`);
3599
+ const op = isVideoKeyframes(clip.opacity) ? `clip(${expr(clip.opacity, onTimeline, 1, "opacity")},0,1)` : String(_nullishCoalesce(clip.opacity, () => ( 1)));
3600
+ 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("*");
3601
+ const coord = (axis) => {
3602
+ const v = _optionalChain([tf, 'optionalAccess', _75 => _75[axis]]);
3603
+ const base2 = `(${axis === "x" ? "w-text_w" : "h-text_h"})/2`;
3604
+ return isVideoKeyframes(v) ? `${axis}='${base2}+(${expr(v, onTimeline, 0, "position")})'` : `${axis}=${base2}+${_nullishCoalesce(v, () => ( 0))}`;
3605
+ };
3606
+ const fontSize = _nullishCoalesce(clip.fontSize, () => ( 64));
3607
+ const opts = [
3608
+ `text=${ffmpegFilterValue(clip.text)}`,
3609
+ options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
3610
+ isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _76 => _76.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _77 => _77.scale]), () => ( 1))))}`,
3611
+ `fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
3612
+ coord("x"),
3613
+ coord("y"),
3614
+ `alpha='${alpha}'`,
3615
+ ...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
3616
+ `enable='between(t,${sec(clip.start)},${sec(end)})'`
3617
+ ];
3618
+ if (_optionalChain([tf, 'optionalAccess', _78 => _78.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
3619
+ if (videoGradeFfmpegFilters(clip.grade).length || normalizeVideoGradeLut(_optionalChain([clip, 'access', _79 => _79.grade, 'optionalAccess', _80 => _80.lut]))) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
3620
+ if (clip.blend && clip.blend !== "normal") notes.push(`Title "${clip.id}" is drawn without its blend mode (drawtext draws straight onto the picture); pass titleImage to blend titles.`);
3621
+ graph.push(`[${cur2}]drawtext=${opts.join(":")}[${next}]`);
3622
+ cur2 = next;
3623
+ continue;
3624
+ }
3625
+ const size = asset ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
3626
+ const placed = clip.type === "text" && !moving ? void 0 : tf;
3627
+ const still = Object.fromEntries(Object.entries(_nullishCoalesce(placed, () => ( {}))).filter(([, v]) => !isVideoKeyframes(v)));
3628
+ const rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : _nullishCoalesce(clip.fit, () => ( (clip.type === "media" || clip.type === "sequence" ? "contain" : "fill"))), still);
3629
+ const chain = [];
3630
+ let source;
3631
+ if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
3632
+ else if (_optionalChain([compound, 'optionalAccess', _81 => _81.picture])) {
3633
+ source = compound.picture;
3634
+ chain.push(`scale=${even(rect.width)}:${even(rect.height)}`);
3635
+ } else {
3636
+ source = `[${index}:v]`;
3637
+ if (clip.type === "media" && clip.timeRemap) chain.push(...rampedPicture(clip.timeRemap, lead, clip.duration + tail), `scale=${even(rect.width)}:${even(rect.height)}`);
3638
+ else chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
3639
+ }
3640
+ const grade = videoGradeFfmpegFilters(clip.grade);
3641
+ if (grade.length) chain.push("format=rgba", ...grade);
3642
+ const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _82 => _82.grade, 'optionalAccess', _83 => _83.lut]));
3643
+ if (lut) {
3644
+ chain.push("format=gbrap");
3645
+ const path = options.resolveLut ? options.resolveLut(lut.src) : lut.src;
3646
+ if (/^(blob|data|https?):/i.test(path)) notes.push(`ffmpeg's lut3d reads only local files, and clip "${clip.id}"'s LUT is "${path.slice(0, 80)}"; save it and pass resolveLut to give its path.`);
3647
+ const file = ffmpegFilterValue(path);
3648
+ const into2 = `q${next}`;
3649
+ graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[${into2}]`);
3650
+ const lut3d = `lut3d=file=${file}:interp=tetrahedral`;
3651
+ if ((_nullishCoalesce(lut.strength, () => ( 1))) >= 1) graph.push(`[${into2}]${lut3d}[r${next}]`);
3652
+ else graph.push(`[${into2}]split[o${next}][p${next}]`, `[p${next}]${lut3d}[s${next}]`, `[s${next}][o${next}]blend=all_mode=normal:all_opacity=${lut.strength}[r${next}]`);
3653
+ source = `[r${next}]`;
3654
+ chain.length = 0;
3655
+ }
3656
+ chain.push("format=yuva420p");
3657
+ if (isVideoKeyframes(clip.opacity)) chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='alpha(X,Y)*clip(${expr(clip.opacity, onChain("T"), 1, "opacity")},0,1)'`);
3658
+ else if (clip.opacity !== void 0 && clip.opacity < 1) chain.push(`colorchannelmixer=aa=${clip.opacity}`);
3659
+ if (clip.fadeIn) chain.push(`fade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}:alpha=1`);
3660
+ if (clip.fadeOut) chain.push(`fade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}:alpha=1`);
3661
+ if (lead && clip.transition.type === "crossfade") chain.push(`fade=t=in:st=0:d=${sec(2 * lead)}:alpha=1`);
3662
+ if (lead && clip.transition.type === "wipe") {
3663
+ const d = sec(2 * lead);
3664
+ const p = `min(1,T/${d})`;
3665
+ const dir = _nullishCoalesce(clip.transition.direction, () => ( "right"));
3666
+ 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}))`;
3667
+ chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(${test},alpha(X,Y),0)'`);
3668
+ }
3669
+ if (lead && clip.transition.type === "iris") {
3670
+ const r = videoIrisRadius({ width: W, height: H }, 1);
3671
+ chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(lte(hypot(X+${Math.round(rect.x)}-${W / 2},Y+${Math.round(rect.y)}-${H / 2}),${Math.round(r)}*min(1,T/${sec(2 * lead)})),alpha(X,Y),0)'`);
3672
+ }
3673
+ const rotation = _optionalChain([placed, 'optionalAccess', _84 => _84.rotation]);
3674
+ if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
3675
+ else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
3676
+ if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _85 => _85.scale]))) {
3677
+ const s = expr(placed.scale, onChain("t"), 1, "scale");
3678
+ chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
3679
+ }
3680
+ chain.push(`setpts=PTS+${sec(t0)}/TB`);
3681
+ const cx = rect.x + rect.width / 2;
3682
+ const cy = rect.y + rect.height / 2;
3683
+ const shifts = { x: [], y: [] };
3684
+ const shiftBy = (direction, from, d, entering) => {
3685
+ const dir = _nullishCoalesce(direction, () => ( "right"));
3686
+ const axis = dir === "left" || dir === "right" ? "x" : "y";
3687
+ const size2 = axis === "x" ? W : H;
3688
+ const sign = dir === "left" || dir === "up" ? -1 : 1;
3689
+ const ease = `(1-cos(PI*clip((t-${sec(from)})/${sec(d)},0,1)))/2`;
3690
+ shifts[axis].push(`${sign * size2}*(${ease}${entering ? "-1" : ""})`);
3385
3691
  };
3386
- const fontSize = _nullishCoalesce(clip.fontSize, () => ( 64));
3387
- const opts = [
3388
- `text=${ffmpegFilterValue(clip.text)}`,
3389
- options.fontFile ? `fontfile=${ffmpegFilterValue(options.fontFile)}` : `font=${ffmpegFilterValue(_nullishCoalesce(clip.fontFamily, () => ( "sans-serif")))}`,
3390
- isVideoKeyframes(_optionalChain([tf, 'optionalAccess', _70 => _70.scale])) ? `fontsize='${fontSize}*(${expr(tf.scale, onTimeline, 1, "scale")})'` : `fontsize=${Math.round(fontSize * (_nullishCoalesce(_optionalChain([tf, 'optionalAccess', _71 => _71.scale]), () => ( 1))))}`,
3391
- `fontcolor=${color(_nullishCoalesce(clip.color, () => ( "#ffffff")))}`,
3392
- coord("x"),
3393
- coord("y"),
3394
- `alpha='${alpha}'`,
3395
- ...clip.background ? ["box=1", `boxcolor=${color(clip.background)}`, `boxborderw=${Math.round((_nullishCoalesce(clip.fontSize, () => ( 64))) / 2)}`] : [],
3396
- `enable='between(t,${sec(clip.start)},${sec(end)})'`
3397
- ];
3398
- if (_optionalChain([tf, 'optionalAccess', _72 => _72.rotation])) notes.push(`Title "${clip.id}" is drawn without its rotation (drawtext cannot rotate).`);
3399
- if (videoGradeFfmpegFilters(clip.grade).length || normalizeVideoGradeLut(_optionalChain([clip, 'access', _73 => _73.grade, 'optionalAccess', _74 => _74.lut]))) notes.push(`Title "${clip.id}" is drawn without its grade (drawtext draws straight onto the picture); pass titleImage to grade titles.`);
3400
- if (clip.blend && clip.blend !== "normal") notes.push(`Title "${clip.id}" is drawn without its blend mode (drawtext draws straight onto the picture); pass titleImage to blend titles.`);
3401
- graph.push(`[${cur}]drawtext=${opts.join(":")}[${next}]`);
3402
- cur = next;
3403
- continue;
3404
- }
3405
- const size = asset ? { width: _nullishCoalesce(asset.width, () => ( project.width)), height: _nullishCoalesce(asset.height, () => ( project.height)) } : { width: project.width, height: project.height };
3406
- const placed = clip.type === "text" && !moving ? void 0 : tf;
3407
- const still = Object.fromEntries(Object.entries(_nullishCoalesce(placed, () => ( {}))).filter(([, v]) => !isVideoKeyframes(v)));
3408
- const rect = placeVideoRect(project, size, clip.type === "text" ? "fill" : _nullishCoalesce(clip.fit, () => ( (clip.type === "media" ? "contain" : "fill"))), still);
3409
- const chain = [];
3410
- let source;
3411
- if (clip.type === "color") source = `color=c=${color(clip.color)}:s=${even(rect.width)}x${even(rect.height)}:r=${fps}:d=${sec(span)}`;
3412
- else {
3413
- source = `[${index}:v]`;
3414
- if (clip.type === "media" && clip.timeRemap) chain.push(...rampedPicture(clip.timeRemap, lead, clip.duration + tail), `scale=${even(rect.width)}:${even(rect.height)}`);
3415
- else chain.push(`setpts=${videoClipSpeed(clip) !== 1 ? `(PTS-STARTPTS)/${videoClipSpeed(clip)}` : "PTS-STARTPTS"}`, `fps=${fps}`, `scale=${even(rect.width)}:${even(rect.height)}`);
3416
- }
3417
- const grade = videoGradeFfmpegFilters(clip.grade);
3418
- if (grade.length) chain.push("format=rgba", ...grade);
3419
- const lut = normalizeVideoGradeLut(_optionalChain([clip, 'access', _75 => _75.grade, 'optionalAccess', _76 => _76.lut]));
3420
- if (lut) {
3421
- chain.push("format=gbrap");
3422
- const path = options.resolveLut ? options.resolveLut(lut.src) : lut.src;
3423
- if (/^(blob|data|https?):/i.test(path)) notes.push(`ffmpeg's lut3d reads only local files, and clip "${clip.id}"'s LUT is "${path.slice(0, 80)}"; save it and pass resolveLut to give its path.`);
3424
- const file = ffmpegFilterValue(path);
3425
- const into2 = `q${next}`;
3426
- graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[${into2}]`);
3427
- const lut3d = `lut3d=file=${file}:interp=tetrahedral`;
3428
- if ((_nullishCoalesce(lut.strength, () => ( 1))) >= 1) graph.push(`[${into2}]${lut3d}[r${next}]`);
3429
- else graph.push(`[${into2}]split[o${next}][p${next}]`, `[p${next}]${lut3d}[s${next}]`, `[s${next}][o${next}]blend=all_mode=normal:all_opacity=${lut.strength}[r${next}]`);
3430
- source = `[r${next}]`;
3431
- chain.length = 0;
3432
- }
3433
- chain.push("format=yuva420p");
3434
- if (isVideoKeyframes(clip.opacity)) chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='alpha(X,Y)*clip(${expr(clip.opacity, onChain("T"), 1, "opacity")},0,1)'`);
3435
- else if (clip.opacity !== void 0 && clip.opacity < 1) chain.push(`colorchannelmixer=aa=${clip.opacity}`);
3436
- if (clip.fadeIn) chain.push(`fade=t=in:st=${sec(lead)}:d=${sec(clip.fadeIn)}:alpha=1`);
3437
- if (clip.fadeOut) chain.push(`fade=t=out:st=${sec(lead + clip.duration - clip.fadeOut)}:d=${sec(clip.fadeOut)}:alpha=1`);
3438
- if (lead && clip.transition.type === "crossfade") chain.push(`fade=t=in:st=0:d=${sec(2 * lead)}:alpha=1`);
3439
- if (lead && clip.transition.type === "wipe") {
3440
- const d = sec(2 * lead);
3441
- const p = `min(1,T/${d})`;
3442
- const dir = _nullishCoalesce(clip.transition.direction, () => ( "right"));
3443
- 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}))`;
3444
- chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(${test},alpha(X,Y),0)'`);
3445
- }
3446
- if (lead && clip.transition.type === "iris") {
3447
- const r = videoIrisRadius({ width: W, height: H }, 1);
3448
- chain.push(`geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='if(lte(hypot(X+${Math.round(rect.x)}-${W / 2},Y+${Math.round(rect.y)}-${H / 2}),${Math.round(r)}*min(1,T/${sec(2 * lead)})),alpha(X,Y),0)'`);
3449
- }
3450
- const rotation = _optionalChain([placed, 'optionalAccess', _77 => _77.rotation]);
3451
- if (isVideoKeyframes(rotation)) chain.push(`rotate=a='(${expr(rotation, onChain("t"), 0, "rotation")})*PI/180':c=black@0:ow='hypot(iw,ih)':oh=ow`);
3452
- else if (rotation) chain.push(`rotate=${rotation}*PI/180:c=black@0:ow=rotw(${rotation}*PI/180):oh=roth(${rotation}*PI/180)`);
3453
- if (isVideoKeyframes(_optionalChain([placed, 'optionalAccess', _78 => _78.scale]))) {
3454
- const s = expr(placed.scale, onChain("t"), 1, "scale");
3455
- chain.push(`scale=w='max(2,trunc(iw*(${s})/2)*2)':h='max(2,trunc(ih*(${s})/2)*2)':eval=frame`);
3456
- }
3457
- chain.push(`setpts=PTS+${sec(t0)}/TB`);
3458
- const cx = rect.x + rect.width / 2;
3459
- const cy = rect.y + rect.height / 2;
3460
- const shifts = { x: [], y: [] };
3461
- const shiftBy = (direction, from, d, entering) => {
3462
- const dir = _nullishCoalesce(direction, () => ( "right"));
3463
- const axis = dir === "left" || dir === "right" ? "x" : "y";
3464
- const size2 = axis === "x" ? W : H;
3465
- const sign = dir === "left" || dir === "up" ? -1 : 1;
3466
- const ease = `(1-cos(PI*clip((t-${sec(from)})/${sec(d)},0,1)))/2`;
3467
- shifts[axis].push(`${sign * size2}*(${ease}${entering ? "-1" : ""})`);
3468
- };
3469
- const into = lead ? clip.transition.type : void 0;
3470
- if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
3471
- const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
3472
- if (_optionalChain([after, 'optionalAccess', _79 => _79.transition, 'access', _80 => _80.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
3473
- const at = (axis, c) => {
3474
- const v = _optionalChain([placed, 'optionalAccess', _81 => _81[axis]]);
3475
- const extra = shifts[axis].map((s) => `+${s}`).join("");
3476
- return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2${extra}'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2${extra}'`;
3477
- };
3478
- graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[l${next}]`);
3479
- if (clip.blend && clip.blend !== "normal") {
3480
- graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(D)},format=yuva420p[t${next}]`);
3481
- graph.push(`[t${next}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable},format=gbrap,split[f${next}][g${next}]`);
3482
- graph.push(`[${cur}]format=gbrap,split[c${next}][d${next}]`);
3483
- const layerFirst = clip.blend === "color-dodge" || clip.blend === "color-burn";
3484
- graph.push(`${layerFirst ? `[f${next}][c${next}]` : `[c${next}][f${next}]`}blend=all_mode=${FFMPEG_BLEND[clip.blend]}[b${next}]`);
3485
- graph.push(`[g${next}]alphaextract[m${next}]`);
3486
- graph.push(`[b${next}][m${next}]alphamerge[x${next}]`);
3487
- graph.push(`[d${next}][x${next}]overlay=format=gbrp,format=yuva420p[${next}]`);
3488
- blended.add(clip.blend);
3489
- } else graph.push(`[${cur}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable}[${next}]`);
3490
- cur = next;
3491
- const before = clip.transition ? videoClipBefore(track, clip) : null;
3492
- if (before && clip.transition.type === "dip") {
3493
- const d = clip.transition.duration;
3494
- const dip = `d${k}`;
3495
- graph.push(`color=c=${color(_nullishCoalesce(clip.transition.color, () => ( "#000000")))}:s=${W}x${H}:r=${fps}:d=${sec(d)},format=yuva420p,fade=t=in:st=0:d=${sec(d / 2)}:alpha=1,fade=t=out:st=${sec(d / 2)}:d=${sec(d / 2)}:alpha=1,setpts=PTS+${sec(clip.start - d / 2)}/TB[l${dip}]`);
3496
- graph.push(`[${cur}][l${dip}]overlay=eof_action=pass:enable='between(t,${sec(clip.start - d / 2)},${sec(clip.start + d / 2)})'[${dip}]`);
3497
- cur = dip;
3692
+ const into = lead ? clip.transition.type : void 0;
3693
+ if (into === "push" || into === "slide") shiftBy(clip.transition.direction, clip.start - lead, 2 * lead, true);
3694
+ const after = tail ? sortedVideoClips(track).find((c) => c !== clip && Math.abs(c.start - end) < 0.5 && c.transition) : void 0;
3695
+ if (_optionalChain([after, 'optionalAccess', _86 => _86.transition, 'access', _87 => _87.type]) === "push") shiftBy(after.transition.direction, end - tail, 2 * tail, false);
3696
+ const at = (axis, c) => {
3697
+ const v = _optionalChain([placed, 'optionalAccess', _88 => _88[axis]]);
3698
+ const extra = shifts[axis].map((s) => `+${s}`).join("");
3699
+ return isVideoKeyframes(v) ? `${axis}='${Math.round(axis === "x" ? project.width / 2 : project.height / 2)}+(${expr(v, onTimeline, 0, "position")})-${axis === "x" ? "w" : "h"}/2${extra}'` : `${axis}='${Math.round(c)}-${axis === "x" ? "w" : "h"}/2${extra}'`;
3700
+ };
3701
+ graph.push(`${source.startsWith("[") ? source : `${source},`}${chain.join(",")}[l${next}]`);
3702
+ if (clip.blend && clip.blend !== "normal") {
3703
+ graph.push(`color=c=black@0:s=${W}x${H}:r=${fps}:d=${sec(length)},format=yuva420p[t${next}]`);
3704
+ const clear = scope.within.length > 0;
3705
+ graph.push(`[t${next}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable},format=gbrap,split${clear ? `=3` : ""}[f${next}][g${next}]${clear ? `[h${next}]` : ""}`);
3706
+ graph.push(`[${cur2}]format=gbrap,split${clear ? `=3` : ""}[c${next}][d${next}]${clear ? `[e${next}]` : ""}`);
3707
+ const layerFirst = clip.blend === "color-dodge" || clip.blend === "color-burn";
3708
+ graph.push(`${layerFirst ? `[f${next}][c${next}]` : `[c${next}][f${next}]`}blend=all_mode=${FFMPEG_BLEND[clip.blend]}[b${next}]`);
3709
+ graph.push(`[g${next}]alphaextract[m${next}]`);
3710
+ if (clear) {
3711
+ graph.push(`[e${next}]alphaextract[ea${next}]`);
3712
+ graph.push(`[b${next}][ea${next}]alphamerge[bb${next}]`);
3713
+ graph.push(`[h${next}]format=gbrp[ho${next}]`);
3714
+ graph.push(`[ho${next}][bb${next}]overlay=format=gbrp[bm${next}]`);
3715
+ graph.push(`[bm${next}][m${next}]alphamerge[x${next}]`);
3716
+ } else graph.push(`[b${next}][m${next}]alphamerge[x${next}]`);
3717
+ graph.push(`[d${next}][x${next}]overlay=format=gbrp,format=yuva420p[${next}]`);
3718
+ blended.add(clip.blend);
3719
+ } else graph.push(`[${cur2}][l${next}]overlay=${at("x", cx)}:${at("y", cy)}:eof_action=pass:${enable}[${next}]`);
3720
+ cur2 = next;
3721
+ const before = clip.transition ? videoClipBefore(track, clip) : null;
3722
+ if (before && clip.transition.type === "dip") {
3723
+ const d = clip.transition.duration;
3724
+ const dip = `d${k}`;
3725
+ graph.push(`color=c=${color(_nullishCoalesce(clip.transition.color, () => ( "#000000")))}:s=${W}x${H}:r=${fps}:d=${sec(d)},format=yuva420p,fade=t=in:st=0:d=${sec(d / 2)}:alpha=1,fade=t=out:st=${sec(d / 2)}:d=${sec(d / 2)}:alpha=1,setpts=PTS+${sec(clip.start - d / 2)}/TB[l${dip}]`);
3726
+ graph.push(`[${cur2}][l${dip}]overlay=eof_action=pass:enable='between(t,${sec(clip.start - d / 2)},${sec(clip.start + d / 2)})'[${dip}]`);
3727
+ cur2 = dip;
3728
+ }
3498
3729
  }
3499
3730
  }
3500
- }
3731
+ return { video: cur2, audio: sounds };
3732
+ };
3733
+ const top = compose(project.tracks, measure ? null : "base", D, { within: [], picture: !measure, sound: true });
3734
+ const audio = top.audio;
3735
+ const cur = top.video;
3501
3736
  if (!measure) graph.push(`[${cur}]format=yuv420p[vout]`);
3502
3737
  if (measure && !audio.length) throw new Error("Nothing on the timeline makes a sound, so there is no loudness to measure.");
3503
3738
  const loudness = options.loudness || measure ? `,${loudnormFilter(options.loudness, measure)}` : "";
@@ -3505,7 +3740,7 @@ function buildFfmpeg(project, options, measure) {
3505
3740
  if (options.loudness && !audio.length) notes.push("Nothing makes a sound, so there was no loudness to normalise.");
3506
3741
  else if (options.loudness && !options.loudness.measured)
3507
3742
  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.");
3508
- if (project.tracks.some((t) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _82 => _82.get, 'call', _83 => _83(c.asset), 'optionalAccess', _84 => _84.kind]) === "video" && _optionalChain([assets, 'access', _85 => _85.get, 'call', _86 => _86(c.asset), 'optionalAccess', _87 => _87.hasAudio]) === void 0)))
3743
+ if (allVideoTracks(project).some(({ track: t }) => t.clips.some((c) => c.type === "media" && _optionalChain([assets, 'access', _89 => _89.get, 'call', _90 => _90(c.asset), 'optionalAccess', _91 => _91.kind]) === "video" && _optionalChain([assets, 'access', _92 => _92.get, 'call', _93 => _93(c.asset), 'optionalAccess', _94 => _94.hasAudio]) === void 0)))
3509
3744
  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.');
3510
3745
  if (animated.size)
3511
3746
  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)" : ""}.`);
@@ -3515,7 +3750,7 @@ function buildFfmpeg(project, options, measure) {
3515
3750
  const filterComplex = graph.join(";");
3516
3751
  if (measure) {
3517
3752
  const args2 = ["-hide_banner", "-nostats", ...inputs.flat(), "-filter_complex", filterComplex, "-map", "[aout]", "-f", "null", "-"];
3518
- return { args: args2, command: ["ffmpeg", ...args2].map(shellQuote).join(" "), filterComplex, duration: D, notes };
3753
+ return { args: args2, command: ["ffmpeg", ...args2].map(videoShellQuote).join(" "), filterComplex, duration: D, notes };
3519
3754
  }
3520
3755
  const args = [
3521
3756
  "-y",
@@ -3536,20 +3771,349 @@ function buildFfmpeg(project, options, measure) {
3536
3771
  sec(D),
3537
3772
  _nullishCoalesce(options.output, () => ( "output.mp4"))
3538
3773
  ];
3539
- return { args, command: ["ffmpeg", ...args].map(shellQuote).join(" "), filterComplex, duration: D, notes };
3774
+ return { args, command: ["ffmpeg", ...args].map(videoShellQuote).join(" "), filterComplex, duration: D, notes };
3775
+ }
3776
+
3777
+ // src/video-editor/proxy.ts
3778
+ var VIDEO_PROXY_SCALES = [0.5, 0.25];
3779
+ function videoAssetProxy(asset, scale) {
3780
+ if (!_optionalChain([asset, 'optionalAccess', _95 => _95.proxies, 'optionalAccess', _96 => _96.length]) || !(scale > 0) || scale >= 1) return null;
3781
+ let best = null;
3782
+ for (const p of asset.proxies) if (p.scale >= scale - 1e-9 && p.scale < 1 && (!best || p.scale < best.scale)) best = p;
3783
+ return best;
3784
+ }
3785
+ var videoPreviewSrc = (asset, scale, fallback = _nullishCoalesce(_optionalChain([asset, 'optionalAccess', _97 => _97.src]), () => ( ""))) => _nullishCoalesce(_optionalChain([videoAssetProxy, 'call', _98 => _98(asset, scale), 'optionalAccess', _99 => _99.src]), () => ( fallback));
3786
+ function resolveVideoPreviewScale(scale, shown) {
3787
+ if (scale === void 0 || scale === "auto") return Math.min(1, Math.max(0, shown));
3788
+ if (scale === "full") return 1;
3789
+ return Math.min(1, Math.max(0, scale));
3790
+ }
3791
+ function videoProxyName(src, scale = 0.5) {
3792
+ const clean = src.replace(/[?#].*$/, "");
3793
+ const base = clean.replace(/\.[^./\\]+$/, "");
3794
+ return `${base}.proxy-${Math.round(scale * 100)}.mp4`;
3795
+ }
3796
+ function videoProxyFfmpeg(asset, options = {}) {
3797
+ if (asset.kind !== "video") throw new Error(`Asset "${asset.id}" is ${asset.kind === "image" ? "an image" : "audio"}; only video assets have proxies.`);
3798
+ const scale = _nullishCoalesce(options.scale, () => ( 0.5));
3799
+ if (!(scale > 0 && scale < 1)) throw new Error(`A proxy's scale must be between 0 and 1 (not including either), not ${scale}; 0.5 is half size.`);
3800
+ const gop = Math.max(1, Math.round(_nullishCoalesce(options.gop, () => ( 12))));
3801
+ const output = _nullishCoalesce(options.output, () => ( videoProxyName(asset.src, scale)));
3802
+ const src = options.resolve ? options.resolve(asset) : asset.src;
3803
+ const size = `scale=trunc(iw*${scale}/2)*2:trunc(ih*${scale}/2)*2`;
3804
+ const args = [
3805
+ "-y",
3806
+ "-i",
3807
+ src,
3808
+ "-vf",
3809
+ size,
3810
+ "-c:v",
3811
+ "libx264",
3812
+ "-preset",
3813
+ "veryfast",
3814
+ "-tune",
3815
+ "fastdecode",
3816
+ "-crf",
3817
+ String(_nullishCoalesce(options.crf, () => ( 28))),
3818
+ "-g",
3819
+ String(gop),
3820
+ "-keyint_min",
3821
+ String(gop),
3822
+ "-sc_threshold",
3823
+ "0",
3824
+ "-bf",
3825
+ "0",
3826
+ "-pix_fmt",
3827
+ "yuv420p",
3828
+ ...asset.hasAudio === false ? ["-an"] : ["-c:a", "aac", "-b:a", "128k"],
3829
+ "-movflags",
3830
+ "+faststart",
3831
+ output
3832
+ ];
3833
+ const proxy = { scale, src: output, ...asset.width && asset.height ? { width: Math.trunc(asset.width * scale / 2) * 2, height: Math.trunc(asset.height * scale / 2) * 2 } : {} };
3834
+ return { asset: asset.id, args, command: ["ffmpeg", ...args].map(videoShellQuote).join(" "), proxy };
3835
+ }
3836
+ function videoProxyPlan(project, options = {}) {
3837
+ const scale = _nullishCoalesce(options.scale, () => ( 0.5));
3838
+ const wanted = options.assets ? new Set(options.assets) : null;
3839
+ if (options.assets) for (const id of options.assets) {
3840
+ const a = project.assets.find((x) => x.id === id);
3841
+ if (!a) throw new Error(`There is no asset "${id}"; the assets are ${project.assets.map((x) => `"${x.id}"`).join(", ") || "none"}.`);
3842
+ if (a.kind !== "video") throw new Error(`Asset "${id}" is ${a.kind === "image" ? "an image" : "audio"}; only video assets have proxies.`);
3843
+ }
3844
+ return project.assets.filter((a) => a.kind === "video" && (!wanted || wanted.has(a.id)) && !_optionalChain([a, 'access', _100 => _100.proxies, 'optionalAccess', _101 => _101.some, 'call', _102 => _102((p) => Math.abs(p.scale - scale) < 1e-9)])).map((a) => videoProxyFfmpeg(a, { ...options, scale }));
3845
+ }
3846
+ var VIDEO_PREVIEW_SCALE_OPTIONS = [
3847
+ { value: "auto", label: "Auto", scale: "auto" },
3848
+ { value: "full", label: "Full", scale: "full" },
3849
+ ...VIDEO_PROXY_SCALES.map((s) => ({ value: String(s), label: s === 0.5 ? "\xBD" : s === 0.25 ? "\xBC" : `${s * 100}%`, scale: s }))
3850
+ ];
3851
+ var parseVideoPreviewScale = (value) => value === "full" ? "full" : Number(value) > 0 && Number(value) <= 1 ? Number(value) : "auto";
3852
+ var videoProjectHasProxies = (project) => project.assets.some((a) => !!_optionalChain([a, 'access', _103 => _103.proxies, 'optionalAccess', _104 => _104.length]));
3853
+
3854
+ // src/video-editor/frame-cache.ts
3855
+ var videoSourceFrame = (sourceTime, fps) => Math.max(0, Math.floor(sourceTime * fps / 1e3 + 1e-6));
3856
+ var videoSourceFrameTime = (frame, fps) => (frame + 0.5) * 1e3 / fps;
3857
+ var videoFrameCacheKey = (src, frame) => `${frame}@${src}`;
3858
+ function videoFrameRequestsAt(project, time, srcFor = (l) => l.src) {
3859
+ const out = [];
3860
+ for (const layer of videoFrameLeafLayers(videoFrameAt(project, time).layers)) {
3861
+ if (layer.type !== "media" || layer.assetKind !== "video") continue;
3862
+ const src = srcFor(layer);
3863
+ if (!src) continue;
3864
+ const frame = videoSourceFrame(_nullishCoalesce(layer.sourceTime, () => ( 0)), project.fps);
3865
+ out.push({ key: videoFrameCacheKey(src, frame), src, frame, sourceTime: videoSourceFrameTime(frame, project.fps), time });
3866
+ }
3867
+ return out;
3868
+ }
3869
+ function planVideoPrefetch(project, time, options = {}) {
3870
+ const window2 = _nullishCoalesce(options.window, () => ( 2e3));
3871
+ const dir = _nullishCoalesce(options.direction, () => ( 1));
3872
+ const step = 1e3 / project.fps;
3873
+ const seen = /* @__PURE__ */ new Set();
3874
+ const out = [];
3875
+ for (let i = 0; i * step <= window2 + 1e-6; i++) {
3876
+ const t = time + dir * i * step;
3877
+ if (t < 0) break;
3878
+ for (const r of videoFrameRequestsAt(project, t, options.srcFor)) {
3879
+ if (seen.has(r.key)) continue;
3880
+ seen.add(r.key);
3881
+ out.push(r);
3882
+ }
3883
+ }
3884
+ return out;
3885
+ }
3886
+ function videoCacheEvictions(entries, options) {
3887
+ let total = entries.reduce((n2, e) => n2 + e.bytes, 0);
3888
+ if (total <= options.maxBytes) return [];
3889
+ const dir = _nullishCoalesce(options.direction, () => ( 1));
3890
+ const behind = _nullishCoalesce(options.behind, () => ( 2));
3891
+ const cost = (e) => {
3892
+ const d = (e.time - options.playhead) * dir;
3893
+ return d >= 0 ? d : -d * behind;
3894
+ };
3895
+ const out = [];
3896
+ for (const e of entries.filter((x) => !_optionalChain([options, 'access', _105 => _105.protect, 'optionalAccess', _106 => _106.has, 'call', _107 => _107(x.key)])).sort((a, b) => cost(b) - cost(a))) {
3897
+ if (total <= options.maxBytes) break;
3898
+ out.push(e.key);
3899
+ total -= e.bytes;
3900
+ }
3901
+ return out;
3902
+ }
3903
+ var defaultCapture = async (source) => {
3904
+ if (typeof createImageBitmap !== "function") return null;
3905
+ const image = await createImageBitmap(source);
3906
+ return { image, width: image.width, height: image.height };
3907
+ };
3908
+ var VideoFrameCache = (_class5 = class {
3909
+ constructor(options = {}) {;_class5.prototype.__init13.call(this);_class5.prototype.__init14.call(this);_class5.prototype.__init15.call(this);_class5.prototype.__init16.call(this);_class5.prototype.__init17.call(this);_class5.prototype.__init18.call(this);_class5.prototype.__init19.call(this);_class5.prototype.__init20.call(this);_class5.prototype.__init21.call(this);_class5.prototype.__init22.call(this);_class5.prototype.__init23.call(this);_class5.prototype.__init24.call(this);
3910
+ this.options = options;
3911
+ this.maxBytes = _nullishCoalesce(options.maxBytes, () => ( 512 * 1024 * 1024));
3912
+ this.prefetch = _nullishCoalesce(options.prefetch, () => ( 2e3));
3913
+ this.decode = options.decode !== void 0 ? options.decode : typeof document !== "undefined" ? createVideoElementDecoder() : null;
3914
+ }
3915
+
3916
+ __init13() {this.entries = /* @__PURE__ */ new Map()}
3917
+ __init14() {this.inFlight = /* @__PURE__ */ new Set()}
3918
+ __init15() {this.queue = []}
3919
+ __init16() {this.protect = /* @__PURE__ */ new Set()}
3920
+ __init17() {this.listeners = /* @__PURE__ */ new Set()}
3921
+ __init18() {this.playhead = 0}
3922
+ __init19() {this.direction = 1}
3923
+ __init20() {this.hits = 0}
3924
+ __init21() {this.misses = 0}
3925
+ __init22() {this.bytes = 0}
3926
+ __init23() {this.generation = 0}
3927
+ __init24() {this.destroyed = false}
3928
+
3929
+
3930
+
3931
+
3932
+ /** The cached frame of `src` showing at a source time (ms), on the project's `fps` grid. Counts a hit or a miss. */
3933
+ get(src, sourceTime, fps) {
3934
+ const entry = this.entries.get(videoFrameCacheKey(src, videoSourceFrame(sourceTime, fps)));
3935
+ if (entry) this.hits++;
3936
+ else this.misses++;
3937
+ return _optionalChain([entry, 'optionalAccess', _108 => _108.frame, 'access', _109 => _109.image]);
3938
+ }
3939
+ /** Whether a frame is cached, without counting it. */
3940
+ has(src, sourceTime, fps) {
3941
+ return this.entries.has(videoFrameCacheKey(src, videoSourceFrame(sourceTime, fps)));
3942
+ }
3943
+ /** Keeps a decoded frame (replacing any with the same key), then evicts to the budget. */
3944
+ put(request, frame) {
3945
+ if (this.destroyed) return void _optionalChain([frame, 'access', _110 => _110.image, 'access', _111 => _111.close, 'optionalCall', _112 => _112()]);
3946
+ const old = this.entries.get(request.key);
3947
+ if (old) this.drop(old);
3948
+ const bytes = Math.max(1, frame.width * frame.height * 4);
3949
+ this.entries.set(request.key, { key: request.key, time: request.time, bytes, frame });
3950
+ this.bytes += bytes;
3951
+ this.evict();
3952
+ this.emit();
3953
+ }
3954
+ /** Keeps what a ready media element shows now as the frame of `src` at `sourceTime` (async; does nothing if it is cached). */
3955
+ capture(src, sourceTime, fps, time, source) {
3956
+ const frame = videoSourceFrame(sourceTime, fps);
3957
+ const key = videoFrameCacheKey(src, frame);
3958
+ if (this.entries.has(key) || this.inFlight.has(key)) return;
3959
+ this.inFlight.add(key);
3960
+ void (_nullishCoalesce(this.options.capture, () => ( defaultCapture)))(source).then((f) => {
3961
+ if (f) this.put({ key, time }, f);
3962
+ }).catch(() => {
3963
+ }).finally(() => this.inFlight.delete(key));
3964
+ }
3965
+ /**
3966
+ * Follows the playhead: protects the prefetch window, evicts to the budget, and queues the
3967
+ * window's missing frames for decoding (nearest first). `srcFor` gives the file a layer plays.
3968
+ */
3969
+ update(project, time, options = {}) {
3970
+ if (this.destroyed) return;
3971
+ this.direction = _nullishCoalesce(options.direction, () => ( (time < this.playhead && !options.playing ? -1 : 1)));
3972
+ this.playhead = time;
3973
+ this.srcFor = options.srcFor;
3974
+ const plan = this.prefetch > 0 ? planVideoPrefetch(project, time, { window: this.prefetch, direction: this.direction, srcFor: options.srcFor }) : videoFrameRequestsAt(project, time, options.srcFor);
3975
+ this.protect = new Set(plan.map((r) => r.key));
3976
+ for (const r of plan) {
3977
+ const e = this.entries.get(r.key);
3978
+ if (e) e.time = r.time;
3979
+ }
3980
+ this.evict();
3981
+ this.generation++;
3982
+ this.queue = this.decode ? plan.filter((r) => !this.entries.has(r.key) && !this.inFlight.has(r.key)) : [];
3983
+ this.pump();
3984
+ }
3985
+ /**
3986
+ * The stretches of the timeline whose video frames are all cached, between `from` and `to` (ms),
3987
+ * for the files the preview last played (its `srcFor`, e.g. proxies) unless `srcFor` is given.
3988
+ * `step` (ms, at least a frame) samples more coarsely.
3989
+ */
3990
+ cachedRanges(project, options = {}) {
3991
+ const step = Math.max(1e3 / project.fps, _nullishCoalesce(options.step, () => ( 0)));
3992
+ const from = Math.max(0, _nullishCoalesce(options.from, () => ( 0)));
3993
+ const to = _nullishCoalesce(options.to, () => ( Math.max(...project.tracks.flatMap((t) => t.clips.map((c) => c.start + c.duration)), 0)));
3994
+ const out = [];
3995
+ for (let t = from; t < to - 1e-6; t += step) {
3996
+ const requests = videoFrameRequestsAt(project, t, _nullishCoalesce(options.srcFor, () => ( this.srcFor)));
3997
+ const cached = requests.length > 0 && requests.every((r) => this.entries.has(r.key));
3998
+ const last = out[out.length - 1];
3999
+ if (!cached) continue;
4000
+ const end = Math.min(to, t + step);
4001
+ if (last && Math.abs(last.end - t) < 1e-6) last.end = end;
4002
+ else out.push({ start: t, end });
4003
+ }
4004
+ return out.map((r) => ({ start: Math.round(r.start * 10) / 10, end: Math.round(r.end * 10) / 10 }));
4005
+ }
4006
+ stats() {
4007
+ return { frames: this.entries.size, bytes: this.bytes, maxBytes: this.maxBytes, hits: this.hits, misses: this.misses, pending: this.inFlight.size + this.queue.length, prefetch: this.prefetch };
4008
+ }
4009
+ /** Called when frames arrive or leave. */
4010
+ subscribe(listener) {
4011
+ this.listeners.add(listener);
4012
+ return () => {
4013
+ this.listeners.delete(listener);
4014
+ };
4015
+ }
4016
+ /** Drops every frame (closing them) and the queue. */
4017
+ clear() {
4018
+ for (const e of [...this.entries.values()]) this.drop(e);
4019
+ this.queue = [];
4020
+ this.generation++;
4021
+ this.emit();
4022
+ }
4023
+ /** Clears and stops decoding for good. */
4024
+ destroy() {
4025
+ this.clear();
4026
+ this.destroyed = true;
4027
+ this.listeners.clear();
4028
+ }
4029
+ pump() {
4030
+ const limit = Math.max(1, _nullishCoalesce(this.options.concurrency, () => ( 2)));
4031
+ while (this.decode && this.inFlight.size < limit && this.queue.length) {
4032
+ const r = this.queue.shift();
4033
+ if (this.entries.has(r.key) || this.inFlight.has(r.key)) continue;
4034
+ if (this.bytes >= this.maxBytes && [...this.entries.keys()].every((k) => this.protect.has(k))) {
4035
+ this.queue = [];
4036
+ break;
4037
+ }
4038
+ const generation = this.generation;
4039
+ this.inFlight.add(r.key);
4040
+ this.decode(r.src, r.sourceTime).then(
4041
+ (frame) => {
4042
+ this.inFlight.delete(r.key);
4043
+ if (generation === this.generation || this.bytes + frame.width * frame.height * 4 <= this.maxBytes) this.put(r, frame);
4044
+ else _optionalChain([frame, 'access', _113 => _113.image, 'access', _114 => _114.close, 'optionalCall', _115 => _115()]);
4045
+ },
4046
+ () => this.inFlight.delete(r.key)
4047
+ ).finally(() => this.pump());
4048
+ }
4049
+ }
4050
+ evict() {
4051
+ const drop = videoCacheEvictions([...this.entries.values()], { playhead: this.playhead, maxBytes: this.maxBytes, direction: this.direction, behind: this.options.behind, protect: this.protect });
4052
+ for (const key of drop) this.drop(this.entries.get(key));
4053
+ }
4054
+ drop(e) {
4055
+ this.entries.delete(e.key);
4056
+ this.bytes -= e.bytes;
4057
+ _optionalChain([e, 'access', _116 => _116.frame, 'access', _117 => _117.image, 'access', _118 => _118.close, 'optionalCall', _119 => _119()]);
4058
+ }
4059
+ emit() {
4060
+ for (const l of [...this.listeners]) l();
4061
+ }
4062
+ }, _class5);
4063
+ function createVideoElementDecoder(options = {}) {
4064
+ const videos = /* @__PURE__ */ new Map();
4065
+ const make = _nullishCoalesce(options.createVideo, () => ( (() => {
4066
+ const el = document.createElement("video");
4067
+ el.muted = true;
4068
+ el.preload = "auto";
4069
+ el.crossOrigin = "anonymous";
4070
+ el.playsInline = true;
4071
+ return el;
4072
+ })));
4073
+ const timeout = _nullishCoalesce(options.timeout, () => ( 5e3));
4074
+ const once = (el, type) => new Promise((resolve, reject) => {
4075
+ const timer = setTimeout(() => reject(new Error(`The video did not ${type} within ${timeout} ms.`)), timeout);
4076
+ el.addEventListener(type, () => (clearTimeout(timer), resolve()), { once: true });
4077
+ el.addEventListener("error", () => (clearTimeout(timer), reject(new Error("The video could not be loaded."))), { once: true });
4078
+ });
4079
+ return (src, sourceTime) => {
4080
+ let v = videos.get(src);
4081
+ if (!v) {
4082
+ const el2 = make();
4083
+ el2.src = src;
4084
+ v = { el: el2, chain: el2.readyState >= 1 ? Promise.resolve() : once(el2, "loadedmetadata") };
4085
+ videos.set(src, v);
4086
+ }
4087
+ const { el } = v;
4088
+ const next = v.chain.then(async () => {
4089
+ const presented = "requestVideoFrameCallback" in el ? new Promise((r) => el.requestVideoFrameCallback(() => r())) : null;
4090
+ const seeked = once(el, "seeked");
4091
+ el.currentTime = sourceTime / 1e3;
4092
+ await seeked;
4093
+ if (presented) await Promise.race([presented, new Promise((r) => setTimeout(r, 100))]);
4094
+ const image = await createImageBitmap(el);
4095
+ return { image, width: image.width, height: image.height };
4096
+ });
4097
+ v.chain = next.catch(() => {
4098
+ });
4099
+ return next;
4100
+ };
3540
4101
  }
3541
4102
 
3542
4103
  // src/video-editor/preview.ts
3543
- var VideoPreviewEngine = (_class5 = class {
3544
- constructor(source, canvas2, options = {}) {;_class5.prototype.__init13.call(this);_class5.prototype.__init14.call(this);_class5.prototype.__init15.call(this);_class5.prototype.__init16.call(this);_class5.prototype.__init17.call(this);_class5.prototype.__init18.call(this);_class5.prototype.__init19.call(this);_class5.prototype.__init20.call(this);_class5.prototype.__init21.call(this);_class5.prototype.__init22.call(this);_class5.prototype.__init23.call(this);_class5.prototype.__init24.call(this);
4104
+ var VideoPreviewEngine = (_class6 = class {
4105
+ constructor(source, canvas2, options = {}) {;_class6.prototype.__init25.call(this);_class6.prototype.__init26.call(this);_class6.prototype.__init27.call(this);_class6.prototype.__init28.call(this);_class6.prototype.__init29.call(this);_class6.prototype.__init30.call(this);_class6.prototype.__init31.call(this);_class6.prototype.__init32.call(this);_class6.prototype.__init33.call(this);_class6.prototype.__init34.call(this);_class6.prototype.__init35.call(this);_class6.prototype.__init36.call(this);_class6.prototype.__init37.call(this);_class6.prototype.__init38.call(this);_class6.prototype.__init39.call(this);_class6.prototype.__init40.call(this);
3545
4106
  this.source = source;
3546
4107
  this.options = options;
3547
4108
  this.canvas = canvas2;
3548
- this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas2, 'access', _91 => _91.getContext, 'optionalCall', _92 => _92("2d")]), () => ( null));
4109
+ this.ctx = options.context !== void 0 ? options.context : _nullishCoalesce(_optionalChain([canvas2, 'access', _124 => _124.getContext, 'optionalCall', _125 => _125("2d")]), () => ( null));
3549
4110
  this.unsubscribe = source.subscribe(() => this.update());
3550
4111
  this.lutCache = options.luts !== void 0 ? options.luts : _nullishCoalesce(source.luts, () => ( null));
3551
4112
  this.unsubscribeLuts = this.lutCache ? this.lutCache.subscribe(() => this.redraw()) : () => {
3552
4113
  };
4114
+ this.frameCache = options.frameCache !== void 0 ? options.frameCache : _nullishCoalesce(source.frameCache, () => ( null));
4115
+ this.unsubscribeFrames = this.frameCache ? this.frameCache.subscribe(() => this.redraw()) : () => {
4116
+ };
3553
4117
  const pixels = options.pixels !== void 0 ? options.pixels : source.previewPixels;
3554
4118
  this.detachPixels = pixels ? pixels.attach(() => this.readPixels()) : () => {
3555
4119
  };
@@ -3560,24 +4124,29 @@ var VideoPreviewEngine = (_class5 = class {
3560
4124
 
3561
4125
 
3562
4126
 
3563
- __init13() {this.media = /* @__PURE__ */ new Map()}
3564
- __init14() {this.images = /* @__PURE__ */ new Map()}
4127
+ __init25() {this.media = /* @__PURE__ */ new Map()}
4128
+ __init26() {this.images = /* @__PURE__ */ new Map()}
3565
4129
 
3566
- __init15() {this.scheduled = false}
3567
- __init16() {this.destroyed = false}
3568
- __init17() {this.filterHost = null}
3569
- __init18() {this.filters = /* @__PURE__ */ new Set()}
4130
+ __init27() {this.scheduled = false}
4131
+ __init28() {this.destroyed = false}
4132
+ __init29() {this.filterHost = null}
4133
+ __init30() {this.filters = /* @__PURE__ */ new Set()}
3570
4134
 
3571
4135
 
3572
- __init19() {this.drawn = null}
3573
- __init20() {this.complete = true}
4136
+ __init31() {this.drawn = null}
4137
+ __init32() {this.complete = true}
3574
4138
 
3575
4139
 
3576
4140
 
4141
+ __init33() {this.srcFor = (layer) => layer.src}
4142
+ __init34() {this.playing = false}
4143
+ __init35() {this.fps = 30}
4144
+
4145
+ __init36() {this.scratchCanvases = /* @__PURE__ */ new Map()}
3577
4146
  /** The canvas's pixels and the frame they show, or `null` when its context cannot be read. */
3578
4147
  readPixels() {
3579
4148
  const ctx = this.ctx;
3580
- if (!_optionalChain([ctx, 'optionalAccess', _93 => _93.getImageData]) || !this.drawn) return null;
4149
+ if (!_optionalChain([ctx, 'optionalAccess', _126 => _126.getImageData]) || !this.drawn) return null;
3581
4150
  return { ...this.drawn, pixels: ctx.getImageData(0, 0, this.canvas.width, this.canvas.height) };
3582
4151
  }
3583
4152
  createMedia(kind) {
@@ -3615,15 +4184,22 @@ var VideoPreviewEngine = (_class5 = class {
3615
4184
  entry.media.pause();
3616
4185
  entry.media.removeEventListener("seeked", entry.onReady);
3617
4186
  entry.media.removeEventListener("loadeddata", entry.onReady);
3618
- _optionalChain([entry, 'access', _94 => _94.media, 'access', _95 => _95.removeAttribute, 'optionalCall', _96 => _96("src")]);
3619
- _optionalChain([entry, 'access', _97 => _97.media, 'access', _98 => _98.load, 'optionalCall', _99 => _99()]);
4187
+ _optionalChain([entry, 'access', _127 => _127.media, 'access', _128 => _128.removeAttribute, 'optionalCall', _129 => _129("src")]);
4188
+ _optionalChain([entry, 'access', _130 => _130.media, 'access', _131 => _131.load, 'optionalCall', _132 => _132()]);
3620
4189
  this.media.delete(entry.clip);
3621
4190
  }
3622
4191
  /** Brings the media elements in step with the editor and draws the frame. */
3623
4192
  update() {
3624
4193
  if (this.destroyed) return;
3625
- const { project, time, playing } = this.source.getState();
4194
+ const state = this.source.getState();
4195
+ const { project, time, playing } = state;
3626
4196
  const frame = videoFrameAt(project, time);
4197
+ const scale = resolveVideoPreviewScale(_nullishCoalesce(this.options.previewScale, () => ( state.previewScale)), this.resolutionFor(project.width));
4198
+ const assets = new Map(project.assets.map((a) => [a.id, a]));
4199
+ this.srcFor = (layer) => videoPreviewSrc(assets.get(_nullishCoalesce(layer.asset, () => ( ""))), scale, layer.src);
4200
+ this.playing = playing;
4201
+ this.fps = project.fps;
4202
+ _optionalChain([this, 'access', _133 => _133.frameCache, 'optionalAccess', _134 => _134.update, 'call', _135 => _135(project, time, { playing, srcFor: this.srcFor })]);
3627
4203
  const tolerance = (_nullishCoalesce(this.options.tolerance, () => ( 200))) / 1e3;
3628
4204
  const active = /* @__PURE__ */ new Set();
3629
4205
  const sync = (clip, src, kind, sourceTime, speed, gain) => {
@@ -3647,10 +4223,10 @@ var VideoPreviewEngine = (_class5 = class {
3647
4223
  }
3648
4224
  };
3649
4225
  const sounding = new Map(frame.audio.map((a) => [a.clip, a]));
3650
- for (const layer of frame.layers) {
4226
+ for (const layer of videoFrameLeafLayers(frame.layers)) {
3651
4227
  if (layer.type !== "media" || layer.assetKind !== "video" || !layer.src) continue;
3652
4228
  const sound = sounding.get(layer.clip);
3653
- sync(layer.clip, layer.src, "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _100 => _100.speed]), () => ( 1)), sound ? sound.gain : null);
4229
+ sync(layer.clip, this.srcFor(layer), "video", _nullishCoalesce(layer.sourceTime, () => ( 0)), _nullishCoalesce(_optionalChain([sound, 'optionalAccess', _136 => _136.speed]), () => ( 1)), sound ? sound.gain : null);
3654
4230
  sounding.delete(layer.clip);
3655
4231
  }
3656
4232
  for (const a of sounding.values()) sync(a.clip, a.src, "audio", a.sourceTime, a.speed, a.gain);
@@ -3665,21 +4241,35 @@ var VideoPreviewEngine = (_class5 = class {
3665
4241
  if (!this.destroyed) this.draw(videoFrameAt(this.source.getState().project, this.source.getState().time));
3666
4242
  });
3667
4243
  }
3668
- __init21() {this.sourceFor = (layer) => {
4244
+ __init37() {this.sourceFor = (layer) => {
3669
4245
  if (layer.assetKind === "image" && layer.src) {
3670
4246
  const img = this.image(layer.src);
3671
4247
  return img.complete ? img : null;
3672
4248
  }
3673
4249
  const entry = this.media.get(layer.clip);
3674
- if (!entry || entry.media.readyState < 2) return null;
4250
+ const ready = !!entry && entry.media.readyState >= 2;
4251
+ const cache = this.frameCache;
4252
+ const src = _nullishCoalesce(_optionalChain([entry, 'optionalAccess', _137 => _137.src]), () => ( this.srcFor(layer)));
4253
+ const sourceTime = _nullishCoalesce(layer.sourceTime, () => ( 0));
4254
+ if (cache && src && (!this.playing || !ready || entry.media.seeking)) {
4255
+ const hit = cache.get(src, sourceTime, this.fps);
4256
+ if (hit) return hit;
4257
+ }
4258
+ if (!ready) return null;
3675
4259
  if (entry.media.seeking) this.complete = false;
4260
+ else if (cache && !this.playing) cache.capture(entry.src, sourceTime, this.fps, this.source.getState().time, entry.media);
3676
4261
  return entry.media;
3677
4262
  }}
4263
+ /** Canvas pixels per frame pixel: the option, else the displayed width at the device pixel ratio (at most 1). */
4264
+ resolutionFor(frameWidth) {
4265
+ if (this.options.resolution !== void 0) return this.options.resolution;
4266
+ const shown = this.canvas.clientWidth || frameWidth;
4267
+ const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
4268
+ return Math.min(1, shown * dpr / frameWidth);
4269
+ }
3678
4270
  draw(frame) {
3679
4271
  if (!this.ctx) return;
3680
- const shown = this.canvas.clientWidth || frame.width;
3681
- const dpr = typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1;
3682
- const resolution = _nullishCoalesce(this.options.resolution, () => ( Math.min(1, shown * dpr / frame.width)));
4272
+ const resolution = this.resolutionFor(frame.width);
3683
4273
  const w = Math.max(1, Math.round(frame.width * resolution));
3684
4274
  const h = Math.max(1, Math.round(frame.height * resolution));
3685
4275
  if (this.canvas.width !== w) this.canvas.width = w;
@@ -3692,32 +4282,34 @@ var VideoPreviewEngine = (_class5 = class {
3692
4282
  };
3693
4283
  drawVideoFrame(this.ctx, frame, sourceFor, resolution, { defineSvgFilter: this.defineSvgFilter, lutFor: this.lutFor, scratch: this.scratch });
3694
4284
  this.drawn = { time: frame.time, complete: this.complete };
3695
- _optionalChain([this, 'access', _101 => _101.options, 'access', _102 => _102.onFrame, 'optionalCall', _103 => _103(frame)]);
3696
- _optionalChain([this, 'access', _104 => _104.pixelsTarget, 'optionalAccess', _105 => _105.drawn, 'call', _106 => _106(frame.time, this.complete)]);
4285
+ _optionalChain([this, 'access', _138 => _138.options, 'access', _139 => _139.onFrame, 'optionalCall', _140 => _140(frame)]);
4286
+ _optionalChain([this, 'access', _141 => _141.pixelsTarget, 'optionalAccess', _142 => _142.drawn, 'call', _143 => _143(frame.time, this.complete)]);
3697
4287
  }
3698
4288
  /** A grade's LUT once loaded; while it loads (or if it fails) the frame is drawn without it and counts as incomplete. */
3699
- __init22() {this.lutFor = (src) => {
3700
- const lut = _optionalChain([this, 'access', _107 => _107.lutCache, 'optionalAccess', _108 => _108.get, 'call', _109 => _109(src)]);
4289
+ __init38() {this.lutFor = (src) => {
4290
+ const lut = _optionalChain([this, 'access', _144 => _144.lutCache, 'optionalAccess', _145 => _145.get, 'call', _146 => _146(src)]);
3701
4291
  if (!lut) this.complete = false;
3702
4292
  return lut;
3703
4293
  }}
3704
- __init23() {this.scratch = (width, height) => {
3705
- if (this.scratchCanvas === void 0) {
3706
- if (this.options.createScratch) this.scratchCanvas = this.options.createScratch(width, height);
4294
+ __init39() {this.scratch = (width, height, slot) => {
4295
+ if (!this.scratchCanvases.has(slot)) {
4296
+ let made = null;
4297
+ if (this.options.createScratch) made = this.options.createScratch(width, height, slot);
3707
4298
  else if (typeof document !== "undefined") {
3708
4299
  const canvas2 = document.createElement("canvas");
3709
4300
  const ctx = canvas2.getContext("2d", { willReadFrequently: true });
3710
- this.scratchCanvas = ctx ? { canvas: canvas2, ctx } : null;
3711
- } else this.scratchCanvas = null;
4301
+ made = ctx ? { canvas: canvas2, ctx } : null;
4302
+ }
4303
+ this.scratchCanvases.set(slot, made);
3712
4304
  }
3713
- const s = this.scratchCanvas;
4305
+ const s = this.scratchCanvases.get(slot);
3714
4306
  if (!s) return null;
3715
4307
  const c = s.canvas;
3716
4308
  if (c.width !== width) c.width = width;
3717
4309
  if (c.height !== height) c.height = height;
3718
4310
  return s;
3719
4311
  }}
3720
- __init24() {this.defineSvgFilter = (id, markup) => {
4312
+ __init40() {this.defineSvgFilter = (id, markup) => {
3721
4313
  if (this.options.defineSvgFilter === null) return;
3722
4314
  if (this.options.defineSvgFilter) return this.options.defineSvgFilter(id, markup);
3723
4315
  if (this.filters.has(id) || typeof document === "undefined") return;
@@ -3738,43 +4330,49 @@ var VideoPreviewEngine = (_class5 = class {
3738
4330
  this.destroyed = true;
3739
4331
  this.detachPixels();
3740
4332
  this.unsubscribeLuts();
3741
- _optionalChain([this, 'access', _110 => _110.filterHost, 'optionalAccess', _111 => _111.remove, 'call', _112 => _112()]);
4333
+ this.unsubscribeFrames();
4334
+ _optionalChain([this, 'access', _147 => _147.filterHost, 'optionalAccess', _148 => _148.remove, 'call', _149 => _149()]);
3742
4335
  this.filterHost = null;
3743
4336
  this.filters.clear();
3744
4337
  this.unsubscribe();
3745
4338
  for (const entry of [...this.media.values()]) this.release(entry);
3746
4339
  this.images.clear();
3747
4340
  }
3748
- }, _class5);
4341
+ }, _class6);
3749
4342
 
3750
4343
  // src/video-editor/controller.ts
3751
- var VideoEditorController = (_class6 = class {
4344
+ var VideoEditorController = (_class7 = class {
3752
4345
 
3753
4346
 
3754
4347
  /** Sound levels per asset, loaded on demand for waveforms and agents. */
3755
4348
 
3756
4349
  /** The pixels the preview last drew, for scopes and `get_video_scopes` (a `VideoPreviewEngine` fills it). */
3757
- __init25() {this.previewPixels = new VideoPreviewPixels()}
4350
+ __init41() {this.previewPixels = new VideoPreviewPixels()}
3758
4351
  /** Grades' LUTs, loaded on demand for the preview, SVG frames and agents. */
3759
4352
 
4353
+ /** The host's frame cache, if it gave one (the preview fills and reads it). */
3760
4354
 
3761
4355
 
3762
- __init26() {this.selection = []}
3763
4356
 
3764
- __init27() {this.error = null}
3765
- __init28() {this.revision = 0}
3766
- __init29() {this.applying = false}
4357
+ __init42() {this.selection = []}
3767
4358
 
3768
- __init30() {this.listeners = /* @__PURE__ */ new Set()}
3769
- __init31() {this.unsubscribers = []}
3770
4359
 
3771
- constructor(options = {}) {;_class6.prototype.__init25.call(this);_class6.prototype.__init26.call(this);_class6.prototype.__init27.call(this);_class6.prototype.__init28.call(this);_class6.prototype.__init29.call(this);_class6.prototype.__init30.call(this);_class6.prototype.__init31.call(this);_class6.prototype.__init32.call(this);_class6.prototype.__init33.call(this);
4360
+ __init43() {this.error = null}
4361
+ __init44() {this.revision = 0}
4362
+ __init45() {this.applying = false}
4363
+
4364
+ __init46() {this.listeners = /* @__PURE__ */ new Set()}
4365
+ __init47() {this.unsubscribers = []}
4366
+
4367
+ constructor(options = {}) {;_class7.prototype.__init41.call(this);_class7.prototype.__init42.call(this);_class7.prototype.__init43.call(this);_class7.prototype.__init44.call(this);_class7.prototype.__init45.call(this);_class7.prototype.__init46.call(this);_class7.prototype.__init47.call(this);_class7.prototype.__init48.call(this);_class7.prototype.__init49.call(this);
3772
4368
  this.history = _nullishCoalesce(options.history, () => ( new (0, _chunkQKLRAA4Tcjs.HistoryManager)()));
3773
4369
  this.audioPeaks = options.audioPeaks instanceof VideoAudioPeaksCache ? options.audioPeaks : new VideoAudioPeaksCache(options.audioPeaks);
3774
4370
  this.luts = options.luts instanceof VideoLutCache ? options.luts : new VideoLutCache(options.luts);
4371
+ this.frameCache = _nullishCoalesce(options.frameCache, () => ( null));
3775
4372
  this.project = options.project ? parseVideoProject(options.project) : createVideoProject();
3776
4373
  this.issues = checkVideoProject(this.project);
3777
4374
  this.readOnly = _nullishCoalesce(options.readOnly, () => ( false));
4375
+ this.previewScale = checkPreviewScale(_nullishCoalesce(options.previewScale, () => ( "auto")));
3778
4376
  this.onChange = options.onChange;
3779
4377
  this.player = new (0, _chunk2FGRTYJNcjs.AnimationPlayer)({ clock: options.clock, duration: () => videoProjectDuration(this.project), loop: "none" });
3780
4378
  this.state = this.computeState();
@@ -3794,8 +4392,8 @@ var VideoEditorController = (_class6 = class {
3794
4392
  this.player.pause();
3795
4393
  this.unsubscribers.splice(0).forEach((u) => u());
3796
4394
  }
3797
- __init32() {this.getState = () => this.state}
3798
- __init33() {this.subscribe = (listener) => {
4395
+ __init48() {this.getState = () => this.state}
4396
+ __init49() {this.subscribe = (listener) => {
3799
4397
  this.connect();
3800
4398
  this.listeners.add(listener);
3801
4399
  return () => {
@@ -3824,6 +4422,7 @@ var VideoEditorController = (_class6 = class {
3824
4422
  canRedo: h.canRedo,
3825
4423
  undoLabel: h.undoName,
3826
4424
  redoLabel: h.redoName,
4425
+ previewScale: this.previewScale,
3827
4426
  revision: this.revision
3828
4427
  };
3829
4428
  }
@@ -3851,7 +4450,7 @@ var VideoEditorController = (_class6 = class {
3851
4450
  this.applying = false;
3852
4451
  }
3853
4452
  this.notify();
3854
- _optionalChain([this, 'access', _113 => _113.onChange, 'optionalCall', _114 => _114(project)]);
4453
+ _optionalChain([this, 'access', _150 => _150.onChange, 'optionalCall', _151 => _151(project)]);
3855
4454
  }
3856
4455
  commit(label, next) {
3857
4456
  const before = this.project;
@@ -3913,9 +4512,20 @@ var VideoEditorController = (_class6 = class {
3913
4512
  this.readOnly = readOnly;
3914
4513
  this.notify();
3915
4514
  }
4515
+ /**
4516
+ * Sets how large a picture the preview plays: `full` (the originals), a fraction such as 0.5
4517
+ * (the smallest proxy at least that large, else the original) or `auto` (as large as the canvas
4518
+ * shows). A view setting, not part of the project: no undo step.
4519
+ */
4520
+ setPreviewScale(scale) {
4521
+ const next = checkPreviewScale(scale);
4522
+ if (next === this.previewScale) return;
4523
+ this.previewScale = next;
4524
+ this.notify();
4525
+ }
3916
4526
  /** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
3917
4527
  select(clips, options = {}) {
3918
- const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _115 => _115.filter, 'call', _116 => _116((id) => findVideoClip(this.project, id))]), () => ( []));
4528
+ const known = _nullishCoalesce(_optionalChain([clips, 'optionalAccess', _152 => _152.filter, 'call', _153 => _153((id) => findVideoClip(this.project, id))]), () => ( []));
3919
4529
  let next;
3920
4530
  if (options.add) {
3921
4531
  next = [...this.selection];
@@ -3996,7 +4606,12 @@ var VideoEditorController = (_class6 = class {
3996
4606
  toEdl(options = {}) {
3997
4607
  return toVideoEdl(this.project, options);
3998
4608
  }
3999
- }, _class6);
4609
+ }, _class7);
4610
+ function checkPreviewScale(scale) {
4611
+ if (scale === "auto" || scale === "full") return scale;
4612
+ if (typeof scale !== "number" || !(scale > 0 && scale <= 1)) throw new Error(`A preview scale is "auto", "full" or a fraction above 0 up to 1 (0.5 is half size), not ${JSON.stringify(scale)}.`);
4613
+ return scale === 1 ? "full" : scale;
4614
+ }
4000
4615
 
4001
4616
  // src/video-editor/ui.ts
4002
4617
  var STEPS = [40, 100, 200, 500, 1e3, 2e3, 5e3, 1e4, 15e3, 3e4, 6e4, 12e4, 3e5, 6e5];
@@ -4058,15 +4673,17 @@ var VIDEO_FADE_CURVE_LABELS = {
4058
4673
  };
4059
4674
  function videoClipTint(clip, asset) {
4060
4675
  if (clip.type === "text") return "#a855f7";
4676
+ if (clip.type === "sequence") return "#0d9488";
4061
4677
  if (clip.type === "color") return clip.color;
4062
4678
  if (!asset) return "#ef4444";
4063
4679
  return asset.kind === "audio" ? "#10b981" : asset.kind === "image" ? "#f59e0b" : "#3b82f6";
4064
4680
  }
4065
- function videoClipLabel(clip, asset) {
4681
+ function videoClipLabel(clip, asset, sequence) {
4066
4682
  if (clip.name) return clip.name;
4067
4683
  if (clip.type === "text") return clip.text.split("\n")[0];
4068
4684
  if (clip.type === "color") return clip.color;
4069
- const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _117 => _117.name]), () => ( _optionalChain([asset, 'optionalAccess', _118 => _118.src, 'access', _119 => _119.replace, 'call', _120 => _120(/^.*[\\/]/, "")]))), () => ( clip.asset));
4685
+ if (clip.type === "sequence") return `Compound: ${_nullishCoalesce(_optionalChain([sequence, 'optionalAccess', _154 => _154.name]), () => ( clip.sequence))}`;
4686
+ const base = _nullishCoalesce(_nullishCoalesce(_optionalChain([asset, 'optionalAccess', _155 => _155.name]), () => ( _optionalChain([asset, 'optionalAccess', _156 => _156.src, 'access', _157 => _157.replace, 'call', _158 => _158(/^.*[\\/]/, "")]))), () => ( clip.asset));
4070
4687
  const speed = videoClipSpeed(clip);
4071
4688
  return speed !== 1 ? `${base} (${Math.round(speed * 100)}%)` : base;
4072
4689
  }
@@ -4110,7 +4727,7 @@ function videoAssetDropEdit(project, assetId, options = {}) {
4110
4727
  const asset = videoAssetMap(project).get(assetId);
4111
4728
  if (!asset) return null;
4112
4729
  const kind = asset.kind === "audio" ? "audio" : "video";
4113
- const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _121 => _121.tracks, 'access', _122 => _122.find, 'call', _123 => _123((t) => t.kind === kind && !t.locked), 'optionalAccess', _124 => _124.id])));
4730
+ const track = _nullishCoalesce(options.track, () => ( _optionalChain([project, 'access', _159 => _159.tracks, 'access', _160 => _160.find, 'call', _161 => _161((t) => t.kind === kind && !t.locked), 'optionalAccess', _162 => _162.id])));
4114
4731
  if (!track) return null;
4115
4732
  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 } : {} };
4116
4733
  }
@@ -4159,15 +4776,15 @@ var KEY_LABELS = {
4159
4776
  scale: { label: "Scale", step: 0.05 },
4160
4777
  rotation: { label: "Rotation", step: 1 }
4161
4778
  };
4162
- var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _125 => _125.transform, 'optionalAccess', _126 => _126[property]]) : property === "volume" ? clip.type === "media" ? clip.volume : void 0 : clip.opacity;
4779
+ var animatableOf = (clip, property) => VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([clip, 'access', _163 => _163.transform, 'optionalAccess', _164 => _164[property]]) : property === "volume" ? clip.type === "media" || clip.type === "sequence" ? clip.volume : void 0 : clip.opacity;
4163
4780
  var playheadInClip = (clip, time) => Math.round(Math.min(clip.duration, Math.max(0, time - clip.start)));
4164
4781
  function videoKeyframeRows(project, clipId, time) {
4165
4782
  const at = findVideoClip(project, clipId);
4166
4783
  if (!at) return [];
4167
4784
  const { clip } = at;
4168
4785
  const asset = clip.type === "media" ? videoAssetMap(project).get(clip.asset) : void 0;
4169
- const visual = !(_optionalChain([asset, 'optionalAccess', _127 => _127.kind]) === "audio" || at.track.kind === "audio");
4170
- const audible = clip.type === "media" && videoAssetHasSound(asset);
4786
+ const visual = !(_optionalChain([asset, 'optionalAccess', _165 => _165.kind]) === "audio" || at.track.kind === "audio");
4787
+ const audible = clip.type === "media" && videoAssetHasSound(asset) || clip.type === "sequence";
4171
4788
  const properties = [...visual ? ["opacity"] : [], ...audible ? ["volume"] : [], ...visual ? VIDEO_TRANSFORM_PROPERTIES : []];
4172
4789
  const clipTime = playheadInClip(clip, time);
4173
4790
  return properties.map((property) => {
@@ -4400,6 +5017,34 @@ var videoAssetOut = (a) => {
4400
5017
  const { transcript, ...rest } = a;
4401
5018
  return transcript ? { ...rest, transcriptWords: transcript.length } : rest;
4402
5019
  };
5020
+ var describeVideoTracks = (tracks) => tracks.map((t, index) => ({
5021
+ id: t.id,
5022
+ kind: t.kind,
5023
+ index,
5024
+ ...t.name ? { name: t.name } : {},
5025
+ ...t.muted ? { muted: true } : {},
5026
+ ...t.hidden ? { hidden: true } : {},
5027
+ ...t.locked ? { locked: true } : {},
5028
+ ...t.syncLocked === false ? { syncLocked: false } : {},
5029
+ ...t.volume !== void 0 ? { volume: t.volume } : {},
5030
+ clips: sortedVideoClips(t).map((c) => ({ ...c, end: videoClipEnd(c), ...c.type === "media" || c.type === "sequence" ? { source: videoClipSourceRange(c) } : {} }))
5031
+ }));
5032
+ var videoSequenceUsers = (project, id) => allVideoTracks(project).flatMap(
5033
+ ({ sequence, track }) => track.clips.flatMap((c) => c.type === "sequence" && c.sequence === id ? [{ clip: c.id, ...sequence ? { inSequence: sequence } : {}, track: track.id, start: c.start, end: videoClipEnd(c), source: videoClipSourceRange(c) }] : [])
5034
+ );
5035
+ function describeVideoSequence(project, sequence, tracks = true) {
5036
+ const cycle = findVideoSequenceCycle(project, sequence.id);
5037
+ return {
5038
+ id: sequence.id,
5039
+ ...sequence.name ? { name: sequence.name } : {},
5040
+ duration: videoSequenceDuration(sequence),
5041
+ clipCount: sequence.tracks.reduce((n2, t) => n2 + t.clips.length, 0),
5042
+ nests: videoSequenceChildren(sequence),
5043
+ usedBy: videoSequenceUsers(project, sequence.id),
5044
+ ...cycle ? { cycle } : {},
5045
+ ...tracks ? { tracks: describeVideoTracks(sequence.tracks) } : { trackCount: sequence.tracks.length }
5046
+ };
5047
+ }
4403
5048
  function describeVideoProject(project) {
4404
5049
  return {
4405
5050
  ...project.name ? { name: project.name } : {},
@@ -4409,18 +5054,8 @@ function describeVideoProject(project) {
4409
5054
  duration: videoProjectDuration(project),
4410
5055
  ...project.background ? { background: project.background } : {},
4411
5056
  assets: project.assets.map(videoAssetOut),
4412
- tracks: project.tracks.map((t, index) => ({
4413
- id: t.id,
4414
- kind: t.kind,
4415
- index,
4416
- ...t.name ? { name: t.name } : {},
4417
- ...t.muted ? { muted: true } : {},
4418
- ...t.hidden ? { hidden: true } : {},
4419
- ...t.locked ? { locked: true } : {},
4420
- ...t.syncLocked === false ? { syncLocked: false } : {},
4421
- ...t.volume !== void 0 ? { volume: t.volume } : {},
4422
- clips: sortedVideoClips(t).map((c) => ({ ...c, end: videoClipEnd(c), ...c.type === "media" ? { source: videoClipSourceRange(c) } : {} }))
4423
- })),
5057
+ tracks: describeVideoTracks(project.tracks),
5058
+ ..._optionalChain([project, 'access', _166 => _166.sequences, 'optionalAccess', _167 => _167.length]) ? { sequences: project.sequences.map((q) => describeVideoSequence(project, q, false)) } : {},
4424
5059
  markers: _nullishCoalesce(project.markers, () => ( []))
4425
5060
  };
4426
5061
  }
@@ -4442,37 +5077,37 @@ function createVideoEditorToolProvider(controller, options = {}) {
4442
5077
  const clipOut = (p, id) => {
4443
5078
  for (const t of p.tracks) {
4444
5079
  const c = t.clips.find((x) => x.id === id);
4445
- if (c) return { track: t.id, ...c, end: videoClipEnd(c), ...c.type === "media" ? { source: videoClipSourceRange(c) } : {} };
5080
+ if (c) return { track: t.id, ...c, end: videoClipEnd(c), ...c.type === "media" || c.type === "sequence" ? { source: videoClipSourceRange(c) } : {} };
4446
5081
  }
4447
5082
  return null;
4448
5083
  };
4449
5084
  const placed = (o) => ({
4450
- ..._optionalChain([o, 'access', _128 => _128.created, 'optionalAccess', _129 => _129.length]) ? { created: o.created } : {},
4451
- ..._optionalChain([o, 'access', _130 => _130.trimmed, 'optionalAccess', _131 => _131.length]) ? { trimmed: o.trimmed } : {},
4452
- ..._optionalChain([o, 'access', _132 => _132.removed, 'optionalAccess', _133 => _133.length]) ? { removed: o.removed } : {},
5085
+ ..._optionalChain([o, 'access', _168 => _168.created, 'optionalAccess', _169 => _169.length]) ? { created: o.created } : {},
5086
+ ..._optionalChain([o, 'access', _170 => _170.trimmed, 'optionalAccess', _171 => _171.length]) ? { trimmed: o.trimmed } : {},
5087
+ ..._optionalChain([o, 'access', _172 => _172.removed, 'optionalAccess', _173 => _173.length]) ? { removed: o.removed } : {},
4453
5088
  ...o.rippled ? { rippled: o.rippled } : {}
4454
5089
  });
4455
5090
  const keysOut = (p, clip, property) => {
4456
5091
  const c = clipOut(p, clip);
4457
- const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _134 => _134.transform, 'optionalAccess', _135 => _135[property]]) : _optionalChain([c, 'optionalAccess', _136 => _136[property]]);
5092
+ const value = VIDEO_TRANSFORM_PROPERTIES.includes(property) ? _optionalChain([c, 'optionalAccess', _174 => _174.transform, 'optionalAccess', _175 => _175[property]]) : _optionalChain([c, 'optionalAccess', _176 => _176[property]]);
4458
5093
  return typeof value === "object" ? { keys: value.keys } : { keys: [], value: _nullishCoalesce(value, () => ( null)) };
4459
5094
  };
4460
5095
  const getProject = {
4461
5096
  name: "get_video_project",
4462
5097
  title: "Read the video project",
4463
- description: "Returns the whole project: frame size, fps, length (ms), assets (id, kind, src, duration), tracks top to bottom (an earlier video track draws over a later one) with each clip in time order (id, type, start, duration, end, source in/out for media, fades, transition) and markers, plus the playhead, selection, issues and whether it can be edited.",
5098
+ description: `Returns the whole project: frame size, fps, length (ms), assets (id, kind, src, duration), tracks top to bottom (an earlier video track draws over a later one) with each clip in time order (id, type, start, duration, end, source in/out for media and compound clips, fades, transition), a summary of each nested sequence (id, name, length, clip count, the sequences it nests, the compound clips that play it) and markers, plus the playhead, selection, issues and whether it can be edited. Compound clips (type "sequence") play a sequence's tracks as one picture and sound; list_video_sequences reads what is inside them.`,
4464
5099
  inputSchema: object({}),
4465
5100
  annotations: { title: "Read the video project", ...READ },
4466
5101
  handler: () => {
4467
5102
  const state = controller.getState();
4468
5103
  const reason = readOnlyReason();
4469
- return { ...describeVideoProject(state.project), playhead: state.time, selection: state.selection, issues: issueSummary(state.issues), editable: !reason, ...reason ? { readOnlyReason: reason } : {} };
5104
+ return { ...describeVideoProject(state.project), playhead: state.time, previewScale: state.previewScale, selection: state.selection, issues: issueSummary(state.issues), editable: !reason, ...reason ? { readOnlyReason: reason } : {} };
4470
5105
  }
4471
5106
  };
4472
5107
  const getFrame = {
4473
5108
  name: "get_video_frame",
4474
5109
  title: "What plays at a moment",
4475
- description: "Returns what plays at a time without changing anything: the pictures back to front (clip, track, asset, source time in ms, opacity, placement rect, rotation, any wipe or iris) and the sounds (clip, source time, gain after volumes, fades and transitions).",
5110
+ description: `Returns what plays at a time without changing anything: the pictures back to front (clip, track, asset, source time in ms, opacity, placement rect, rotation, any wipe or iris) and the sounds (clip, source time, gain after volumes, fades and transitions). A compound clip is one picture with its sequence, the moment of the sequence it shows (sourceTime) and the sequence's own pictures as "layers"; its sounds are listed with clip paths such as "compound/inner".`,
4476
5111
  inputSchema: object({ time: MS("Time from the start (default: the playhead)") }),
4477
5112
  annotations: { title: "What plays at a moment", ...READ },
4478
5113
  handler: ({ time }) => {
@@ -4480,6 +5115,23 @@ function createVideoEditorToolProvider(controller, options = {}) {
4480
5115
  return { ...frame, timecode: formatVideoTimecode(frame.time, project().fps) };
4481
5116
  }
4482
5117
  };
5118
+ const listSequences = {
5119
+ name: "list_video_sequences",
5120
+ title: "List nested sequences",
5121
+ description: `Lists the project's nested sequences, the tracks that compound clips (type "sequence") play as one picture and sound, without changing anything. For each: id, name, length (ms), the sequences it nests, the compound clips that play it (clip id, the track and sequence they sit on, timeline start and end, and the stretch of the sequence they play as source in / out) and its tracks with their clips, in the same shape as get_video_project. A sequence that contains itself lists the loop as "cycle". Clips inside sequences can be read here but not edited yet; edit the compound clip instead (move, trim, its opacity, blend, grade, volume).`,
5122
+ inputSchema: object({ sequence: { type: "string", minLength: 1, description: "Only this sequence id (default: every sequence)." } }),
5123
+ annotations: { title: "List nested sequences", ...READ },
5124
+ handler: ({ sequence }) => {
5125
+ const p = project();
5126
+ const all = _nullishCoalesce(p.sequences, () => ( []));
5127
+ if (sequence !== void 0) {
5128
+ const found = all.find((q) => q.id === sequence);
5129
+ if (!found) throw new Error(`There is no sequence "${sequence}"; ${all.length ? `the sequences are ${all.map((q) => `"${q.id}"`).join(", ")}` : "the project has no sequences"}.`);
5130
+ return { sequences: [describeVideoSequence(p, found)] };
5131
+ }
5132
+ return { sequences: all.map((q) => describeVideoSequence(p, q)) };
5133
+ }
5134
+ };
4483
5135
  const findGaps = {
4484
5136
  name: "find_video_gaps",
4485
5137
  title: "Find gaps",
@@ -4515,7 +5167,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4515
5167
  annotations: { title: "Render a frame", ...READ },
4516
5168
  handler: async ({ time, times, columns, format = "svg" }) => {
4517
5169
  const p = project();
4518
- const lutSrcs = [...new Set(p.tracks.flatMap((t) => t.clips.map((c) => _optionalChain([c, 'access', _137 => _137.grade, 'optionalAccess', _138 => _138.lut, 'optionalAccess', _139 => _139.src])).filter((x) => !!x)))];
5170
+ const lutSrcs = [...new Set(p.tracks.flatMap((t) => t.clips.map((c) => _optionalChain([c, 'access', _177 => _177.grade, 'optionalAccess', _178 => _178.lut, 'optionalAccess', _179 => _179.src])).filter((x) => !!x)))];
4519
5171
  const missing = (await Promise.all(lutSrcs.map((src) => controller.luts.load(src).then(() => null, (e) => e.message)))).filter((m) => !!m);
4520
5172
  const svgOptions = { lutFor: (src) => controller.luts.peek(src), ...columns ? { columns } : {} };
4521
5173
  const svg = times ? videoContactSheetSvg(p, times, svgOptions) : videoProjectSvg(p, _nullishCoalesce(time, () => ( controller.getState().time)), svgOptions);
@@ -4634,7 +5286,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4634
5286
  name: "update_video_asset",
4635
5287
  title: "Change media details",
4636
5288
  description: "Changes an asset's src, name, duration, size, sound or poster (e.g. relinking moved media). Clips that now run past its end are refused. Returns the asset.",
4637
- inputSchema: object({ asset: ASSET_ID, set: object(ASSET_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "duration", "width", "height", "hasAudio", "poster", "transcript"] }, description: "Fields to clear (transcript removes its words)." } }, ["asset"]),
5289
+ inputSchema: object({ asset: ASSET_ID, set: object(ASSET_FIELDS, [], "Fields to replace."), remove: { type: "array", items: { type: "string", enum: ["name", "duration", "width", "height", "hasAudio", "poster", "transcript", "proxies"] }, description: "Fields to clear (transcript removes its words, proxies its proxies)." } }, ["asset"]),
4638
5290
  annotations: { title: "Change media details", ...WRITE_IDEMPOTENT },
4639
5291
  handler: ({ asset, set, remove }) => apply({ op: "update_asset", asset, set, remove }, "Change media", (r) => ({ asset: videoAssetOut(r.project.assets.find((a) => a.id === asset)) }))
4640
5292
  };
@@ -4730,7 +5382,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4730
5382
  const updateClip = {
4731
5383
  name: "update_video_clip",
4732
5384
  title: "Change a clip",
4733
- description: `Changes a clip's properties (one undo step): opacity, transform, fit, blend mode, colour grade, entrance and exit animations, 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.`,
5385
+ description: `Changes a clip's properties (one undo step): opacity, transform, fit, blend mode, colour grade, entrance and exit animations, fades, name; for media volume, mute, the sound's fade curves, speed (keeps the stretch of source, so the length changes) or asset; for compound clips in point, volume, mute and fade curves (no speed); 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.`,
4734
5386
  inputSchema: object(
4735
5387
  {
4736
5388
  clip: CLIP_ID,
@@ -4743,7 +5395,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4743
5395
  handler: ({ clip, set, remove }) => {
4744
5396
  const current = clipOut(project(), clip);
4745
5397
  if (current && set) {
4746
- const allowed = /* @__PURE__ */ new Set(["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", ...current.type === "media" ? ["in", "speed", "volume", "muted", "asset", "fadeInCurve", "fadeOutCurve"] : current.type === "text" ? Object.keys(TEXT_FIELDS) : ["color"]]);
5398
+ const allowed = /* @__PURE__ */ new Set(["name", "opacity", "transform", "fit", "blend", "grade", "animateIn", "animateOut", "fadeIn", "fadeOut", ...current.type === "media" ? ["in", "speed", "volume", "muted", "asset", "fadeInCurve", "fadeOutCurve"] : current.type === "sequence" ? ["in", "volume", "muted", "fadeInCurve", "fadeOutCurve"] : current.type === "text" ? Object.keys(TEXT_FIELDS) : ["color"]]);
4747
5399
  const wrong = Object.keys(set).filter((k) => !allowed.has(k));
4748
5400
  if (wrong.length) throw new Error(`${wrong.map((k) => `"${k}"`).join(", ")} cannot be set on a ${current.type} clip; it takes ${[...allowed].join(", ")}.`);
4749
5401
  }
@@ -4756,7 +5408,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4756
5408
  const at = findVideoClip(p, id);
4757
5409
  if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
4758
5410
  const { clip } = at;
4759
- if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _140 => _140.assets, 'access', _141 => _141.find, 'call', _142 => _142((a) => a.id === clip.asset), 'optionalAccess', _143 => _143.kind]) === "audio")
5411
+ if (at.track.kind !== "video" || clip.type === "media" && _optionalChain([p, 'access', _180 => _180.assets, 'access', _181 => _181.find, 'call', _182 => _182((a) => a.id === clip.asset), 'optionalAccess', _183 => _183.kind]) === "audio")
4760
5412
  throw new Error(`Clip "${id}" is sound only; grades change pictures. Pass clips on video tracks.`);
4761
5413
  }
4762
5414
  };
@@ -4779,7 +5431,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4779
5431
  const grade = videoGradeLook(findVideoClip(project(), clip).clip.grade, preset, _nullishCoalesce(amount, () => ( 1)));
4780
5432
  return grade ? { op: "update_clip", clip, set: { grade } } : { op: "update_clip", clip, remove: ["grade"] };
4781
5433
  });
4782
- return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _144 => _144(r.project, id), 'optionalAccess', _145 => _145.clip, 'access', _146 => _146.grade]), () => ( null)) })) }));
5434
+ return apply(edits, `Apply ${preset} look`, (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _184 => _184(r.project, id), 'optionalAccess', _185 => _185.clip, 'access', _186 => _186.grade]), () => ( null)) })) }));
4783
5435
  }
4784
5436
  };
4785
5437
  const copyGrade = {
@@ -4815,7 +5467,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4815
5467
  const lut = await controller.luts.load(src);
4816
5468
  const label = _nullishCoalesce(name, () => ( lut.title));
4817
5469
  const setting = { src, ...strength !== void 0 && strength !== 1 ? { strength } : {}, ...label ? { name: label } : {} };
4818
- return apply(videoLutEdits(project(), clips, setting), "Apply LUT", (r) => ({ lut: { src, ...lut.title ? { title: lut.title } : {}, size: lut.size }, clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _147 => _147(r.project, id), 'optionalAccess', _148 => _148.clip, 'access', _149 => _149.grade]), () => ( null)) })) }));
5470
+ return apply(videoLutEdits(project(), clips, setting), "Apply LUT", (r) => ({ lut: { src, ...lut.title ? { title: lut.title } : {}, size: lut.size }, clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _187 => _187(r.project, id), 'optionalAccess', _188 => _188.clip, 'access', _189 => _189.grade]), () => ( null)) })) }));
4819
5471
  }
4820
5472
  };
4821
5473
  const removeLut = {
@@ -4827,9 +5479,82 @@ function createVideoEditorToolProvider(controller, options = {}) {
4827
5479
  handler: ({ clips }) => {
4828
5480
  gradeTargets(clips);
4829
5481
  const p = project();
4830
- const withLut = clips.filter((id) => _optionalChain([findVideoClip, 'call', _150 => _150(p, id), 'access', _151 => _151.clip, 'access', _152 => _152.grade, 'optionalAccess', _153 => _153.lut]));
5482
+ const withLut = clips.filter((id) => _optionalChain([findVideoClip, 'call', _190 => _190(p, id), 'access', _191 => _191.clip, 'access', _192 => _192.grade, 'optionalAccess', _193 => _193.lut]));
4831
5483
  if (!withLut.length) throw new Error(`None of ${clips.map((c) => `"${c}"`).join(", ")} has a LUT; get_video_project shows each clip's grade.`);
4832
- return apply(videoLutEdits(p, withLut, null), "Remove LUT", (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _154 => _154(r.project, id), 'optionalAccess', _155 => _155.clip, 'access', _156 => _156.grade]), () => ( null)) })) }));
5484
+ return apply(videoLutEdits(p, withLut, null), "Remove LUT", (r) => ({ clips: clips.map((id) => ({ id, grade: _nullishCoalesce(_optionalChain([findVideoClip, 'call', _194 => _194(r.project, id), 'optionalAccess', _195 => _195.clip, 'access', _196 => _196.grade]), () => ( null)) })) }));
5485
+ }
5486
+ };
5487
+ const PROXY_SCALE = { type: "number", exclusiveMinimum: 0, exclusiveMaximum: 1, description: "Proxy size as a fraction of the original: 0.5 (half width and height, the default) or 0.25 for very large footage." };
5488
+ const planProxies = {
5489
+ name: "plan_video_proxies",
5490
+ title: "Plan proxy files",
5491
+ description: 'Returns the ffmpeg commands that make smaller proxy copies of video assets for smooth preview playback, without changing anything: one per video asset (or each of "assets") that has no proxy at that scale yet, each with { asset, args, command, proxy }. Proxies are scaled, H.264 with a keyframe every few frames so scrubbing is quick, and keep the sound. After the host runs a command, attach its "proxy" with set_video_proxies. The export always uses the originals.',
5492
+ inputSchema: object({
5493
+ scale: PROXY_SCALE,
5494
+ assets: { type: "array", items: ASSET_ID, minItems: 1, uniqueItems: true, description: "Only these video assets (default: every video asset without a proxy at this scale)." },
5495
+ gop: { type: "integer", minimum: 1, maximum: 300, description: "Frames between keyframes (default 12): fewer makes seeking cheaper and files larger." },
5496
+ crf: { type: "integer", minimum: 0, maximum: 51, description: "x264 quality, lower is better (default 28: proxies are for editing)." }
5497
+ }),
5498
+ annotations: { title: "Plan proxy files", ...READ },
5499
+ handler: ({ scale, assets, gop, crf }) => {
5500
+ const commands = videoProxyPlan(project(), { scale, assets, gop, crf });
5501
+ return { scale: _nullishCoalesce(scale, () => ( 0.5)), commands, ...commands.length ? {} : { note: "Every video asset already has a proxy at this scale." } };
5502
+ }
5503
+ };
5504
+ const setProxies = {
5505
+ name: "set_video_proxies",
5506
+ title: "Attach proxies",
5507
+ description: "Sets a video asset's proxies, the smaller copies the preview plays instead of the original (one undo step); an empty list removes them. Each proxy must match the original's length and timing (plan_video_proxies makes such files). The export, EDL and source times always use the original. Returns the asset.",
5508
+ inputSchema: object(
5509
+ {
5510
+ asset: { ...ASSET_ID, description: "A video asset id from get_video_project." },
5511
+ proxies: {
5512
+ type: "array",
5513
+ maxItems: 8,
5514
+ description: "Its proxies, one per scale (empty removes them all).",
5515
+ items: object({ scale: PROXY_SCALE, src: { type: "string", minLength: 1, description: "Where the preview loads the proxy from (URL or path)." }, width: { type: "number", exclusiveMinimum: 0, description: "Its width in pixels." }, height: { type: "number", exclusiveMinimum: 0, description: "Its height in pixels." } }, ["scale", "src"], "One proxy.")
5516
+ }
5517
+ },
5518
+ ["asset", "proxies"]
5519
+ ),
5520
+ annotations: { title: "Attach proxies", ...WRITE_IDEMPOTENT },
5521
+ handler: ({ asset, proxies }) => {
5522
+ const a = project().assets.find((x) => x.id === asset);
5523
+ if (!a) throw new Error(`There is no asset "${asset}"; the assets are ${project().assets.map((x) => `"${x.id}"`).join(", ") || "none"}.`);
5524
+ if (a.kind !== "video") throw new Error(`Asset "${asset}" is ${a.kind === "image" ? "an image" : "audio"}; only video assets have proxies.`);
5525
+ const edit = proxies.length ? { op: "update_asset", asset, set: { proxies } } : { op: "update_asset", asset, remove: ["proxies"] };
5526
+ return apply(edit, proxies.length ? "Attach proxies" : "Remove proxies", (r) => ({ asset: videoAssetOut(r.project.assets.find((x) => x.id === asset)) }));
5527
+ }
5528
+ };
5529
+ const setPreviewScale = {
5530
+ name: "set_video_preview_scale",
5531
+ title: "Set the preview size",
5532
+ description: 'Sets how large a picture the preview plays, without changing the project (no undo step): "full" plays the originals, a fraction (0.5, 0.25) plays the smallest proxy at least that large (or the original where there is none), "auto" (the default) picks by the size the preview is shown at. Use a proxy scale when playback stutters on large footage. Returns the setting and, for each video asset, what the preview plays at a fixed scale.',
5533
+ inputSchema: object(
5534
+ { scale: { anyOf: [{ type: "string", enum: ["auto", "full"] }, { type: "number", exclusiveMinimum: 0, maximum: 1 }], description: '"auto", "full", or a fraction above 0 up to 1 (0.5 is half size).' } },
5535
+ ["scale"]
5536
+ ),
5537
+ annotations: { title: "Set the preview size", ...WRITE_IDEMPOTENT },
5538
+ handler: ({ scale }) => {
5539
+ controller.setPreviewScale(scale);
5540
+ const s = controller.getState().previewScale;
5541
+ const fixed = s === "full" ? 1 : typeof s === "number" ? s : null;
5542
+ const plays = fixed === null ? void 0 : project().assets.filter((a) => a.kind === "video").map((a) => ({ asset: a.id, src: _nullishCoalesce(_optionalChain([videoAssetProxy, 'call', _197 => _197(a, fixed), 'optionalAccess', _198 => _198.src]), () => ( a.src)), proxy: !!videoAssetProxy(a, fixed) }));
5543
+ return { previewScale: s, ...plays ? { plays } : { note: "auto picks the smallest proxy at least as large as the preview canvas shows, so it depends on the preview's size." } };
5544
+ }
5545
+ };
5546
+ const previewCache = {
5547
+ name: "get_video_preview_cache",
5548
+ title: "Read the preview's frame cache",
5549
+ description: `Reports the preview's cache of decoded frames, without changing anything: whether the host has one, how many frames and bytes it holds against its budget, hits and misses, frames still decoding, how far ahead it prefetches (ms), and the stretches of the timeline between "from" and "to" whose frames are all cached (these play and scrub without waiting). Use it to tell whether stuttering is the cache filling, and before asking the person to review a passage. With no cache, says so.`,
5550
+ inputSchema: object({ from: MS("Start of the stretch to report cached ranges for (default 0)"), to: MS("End of it (default: the end of the project)") }),
5551
+ annotations: { title: "Read the preview's frame cache", ...READ },
5552
+ handler: ({ from, to }) => {
5553
+ const cache = controller.frameCache;
5554
+ if (!cache) return { enabled: false, reason: "The host has not given this editor a frame cache (VideoEditorController frameCache), so the preview decodes each frame as it plays." };
5555
+ if (from !== void 0 && to !== void 0 && !(to > from)) throw new Error(`"to" (${to} ms) must be after "from" (${from} ms).`);
5556
+ const ranges = cache.cachedRanges(project(), { from, to });
5557
+ return { enabled: true, ...cache.stats(), cachedRanges: ranges };
4833
5558
  }
4834
5559
  };
4835
5560
  const TRANSCRIPT_TRACK = { ...TRACK_ID, description: "Track whose words to read (default: the first audio track with a transcribed clip, else the first video track with one)." };
@@ -4883,7 +5608,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
4883
5608
  handler: ({ track, from, to, limit }) => {
4884
5609
  const all = timelineWords(track).filter((w) => w.end > (_nullishCoalesce(from, () => ( 0))) && w.start < (_nullishCoalesce(to, () => ( Infinity))));
4885
5610
  const words = all.slice(0, _nullishCoalesce(limit, () => ( 1e3)));
4886
- return { track: _nullishCoalesce(_optionalChain([words, 'access', _157 => _157[0], 'optionalAccess', _158 => _158.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
5611
+ return { track: _nullishCoalesce(_optionalChain([words, 'access', _199 => _199[0], 'optionalAccess', _200 => _200.track]), () => ( videoTranscriptTrack(project(), track))), text: words.map((w) => w.word).join(" "), words: words.map((w) => ({ word: w.word, start: round(w.start), end: round(w.end), clip: w.clip })), more: all.length > words.length };
4887
5612
  }
4888
5613
  };
4889
5614
  const findText = {
@@ -4944,8 +5669,9 @@ function createVideoEditorToolProvider(controller, options = {}) {
4944
5669
  const at = findVideoClip(project(), id);
4945
5670
  if (!at) throw new Error(`There is no clip "${id}"; get_video_project lists the clips.`);
4946
5671
  const { clip } = at;
5672
+ if (clip.type === "sequence") throw new Error(`Clip "${id}" is a compound clip, which plays its sequence at normal speed; change the speed of the clips inside the sequence instead.`);
4947
5673
  if (clip.type !== "media") throw new Error(`Clip "${id}" is a ${clip.type === "text" ? "title" : "colour"}, so it has no source to change the speed of; use a video or audio clip.`);
4948
- if (_optionalChain([project, 'call', _159 => _159(), 'access', _160 => _160.assets, 'access', _161 => _161.find, 'call', _162 => _162((a) => a.id === clip.asset), 'optionalAccess', _163 => _163.kind]) === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
5674
+ if (_optionalChain([project, 'call', _201 => _201(), 'access', _202 => _202.assets, 'access', _203 => _203.find, 'call', _204 => _204((a) => a.id === clip.asset), 'optionalAccess', _205 => _205.kind]) === "image") throw new Error(`Clip "${id}" is a still, so it has no motion to change the speed of.`);
4949
5675
  return clip;
4950
5676
  };
4951
5677
  const rampOut = (r, id) => {
@@ -5179,6 +5905,7 @@ function createVideoEditorToolProvider(controller, options = {}) {
5179
5905
  const found = findVideoClip(p, args.clip);
5180
5906
  if (!found) throw new Error(`There is no clip "${args.clip}"; get_video_project lists the clips.`);
5181
5907
  const c = found.clip;
5908
+ if (c.type === "sequence") throw new Error(`Clip "${c.id}" is a compound clip; its sound is the mix of its sequence "${c.sequence}", so read the levels of the media in it (list_video_sequences lists its clips and assets).`);
5182
5909
  if (c.type !== "media") throw new Error(`Clip "${c.id}" is a ${c.type} clip and has no sound; pass a media clip.`);
5183
5910
  asset = p.assets.find((a) => a.id === c.asset);
5184
5911
  const speed = _nullishCoalesce(c.speed, () => ( 1));
@@ -5273,8 +6000,8 @@ function createVideoEditorToolProvider(controller, options = {}) {
5273
6000
  return { time: t, timecode: formatVideoTimecode(t, p.fps), width: pixels.width, height: pixels.height, ...summarizeVideoScopes(pixels, { bins, columns }) };
5274
6001
  }
5275
6002
  };
5276
- const reads = [getProject, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, getScopes, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek];
5277
- const writes = [setSettings, addAsset, updateAsset, removeAsset, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setLut, removeLut, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
6003
+ const reads = [getProject, listSequences, getFrame, getTranscript, findText, findFillers, findPauses, findGaps, checkProject, renderFrame, getScopes, previewCache, planProxies, exportFfmpeg, exportLoudnessMeasure, exportEdl, audioLevels, findSilences, seek, setPreviewScale];
6004
+ const writes = [setSettings, addAsset, updateAsset, removeAsset, setProxies, addTrack, updateTrack, removeTrack, moveTrack, addClip, moveClip, trimClip, splitClip, deleteClips, updateClip, setTransition, setSpeedRamp, addFreeze, removeRamp, setTranscript, removeRanges, applyGradePreset, copyGrade, setLut, removeLut, setKeyframes, removeKeyframes, slipClip, rollEdit, slideClip, liftRange, extractRange, splitAt, closeGaps, addMarker, removeMarker, editVideo, undo, redo];
5278
6005
  return {
5279
6006
  id: _nullishCoalesce(options.id, () => ( "video")),
5280
6007
  namespace: options.namespace,
@@ -5494,5 +6221,34 @@ function createVideoEditorToolProvider(controller, options = {}) {
5494
6221
 
5495
6222
 
5496
6223
 
5497
- exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_BLEND_MODE_LABELS = VIDEO_BLEND_MODE_LABELS; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_CLIP_ANIMATION_LABELS = VIDEO_CLIP_ANIMATION_LABELS; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_FILLERS = VIDEO_DEFAULT_FILLERS; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_LUT_MAX_SIZE = VIDEO_LUT_MAX_SIZE; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_SCOPE_KINDS = VIDEO_SCOPE_KINDS; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_VECTORSCOPE_TARGETS = VIDEO_VECTORSCOPE_TARGETS; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoLutCache = VideoLutCache; exports.VideoPreviewEngine = VideoPreviewEngine; exports.VideoPreviewPixels = VideoPreviewPixels; exports.allVideoClipIds = allVideoClipIds; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = applyVideoGrade; exports.applyVideoLut = applyVideoLut; exports.applyVideoLutToPixels = applyVideoLutToPixels; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoProject = checkVideoProject; exports.checkVideoTimeRemap = checkVideoTimeRemap; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoProject = describeVideoProject; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = findVideoClip; exports.findVideoFillers = findVideoFillers; exports.findVideoGaps = findVideoGaps; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.mergeVideoRanges = mergeVideoRanges; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.normalizeVideoGradeLut = normalizeVideoGradeLut; exports.normalizeVideoWord = normalizeVideoWord; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoCubeLut = parseVideoCubeLut; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoGradeLut = parseVideoGradeLut; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoProject = parseVideoProject; exports.parseVideoTimeRemap = parseVideoTimeRemap; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.probeVideoMedia = probeVideoMedia; exports.readVideoScopesPanel = readVideoScopesPanel; exports.removeVideoKey = removeVideoKey; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.setVideoKey = setVideoKey; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortVideoKeys = sortVideoKeys; exports.sortedVideoClips = sortedVideoClips; exports.splitVideoAnimatable = splitVideoAnimatable; exports.summarizeVideoScopes = summarizeVideoScopes; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoChroma = videoChroma; exports.videoClipAfter = videoClipAfter; exports.videoClipAnimated = videoClipAnimated; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = videoClipSoundGain; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSpeedAt = videoClipSpeedAt; exports.videoClipTint = videoClipTint; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoExposureNotes = videoExposureNotes; exports.videoExposureStats = videoExposureStats; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoFreezeFrameEdit = videoFreezeFrameEdit; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradeLook = videoGradeLook; exports.videoGradePreset = videoGradePreset; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoHistogram = videoHistogram; exports.videoHistogramImage = videoHistogramImage; exports.videoHourFrames = videoHourFrames; exports.videoIrisRadius = videoIrisRadius; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoLuma = videoLuma; exports.videoLumaWaveform = videoLumaWaveform; exports.videoLutEdits = videoLutEdits; exports.videoLutFromFunction = videoLutFromFunction; exports.videoLutSvgApproximation = videoLutSvgApproximation; exports.videoLutSvgPrimitives = videoLutSvgPrimitives; exports.videoLutToCube = videoLutToCube; exports.videoOverlap = videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectSvg = videoProjectSvg; exports.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoScopeImage = videoScopeImage; exports.videoSlowMotionRamp = videoSlowMotionRamp; exports.videoSnapPoints = videoSnapPoints; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.videoTextStyle = videoTextStyle; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoTimeRemapSpan = videoTimeRemapSpan; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoVectorscope = videoVectorscope; exports.videoVectorscopeAngle = videoVectorscopeAngle; exports.videoVectorscopeImage = videoVectorscopeImage; exports.videoVectorscopePoint = videoVectorscopePoint; exports.videoWaveformImage = videoWaveformImage; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange;
6224
+
6225
+
6226
+
6227
+
6228
+
6229
+
6230
+
6231
+
6232
+
6233
+
6234
+
6235
+
6236
+
6237
+
6238
+
6239
+
6240
+
6241
+
6242
+
6243
+
6244
+
6245
+
6246
+
6247
+
6248
+
6249
+
6250
+
6251
+
6252
+
6253
+ exports.VIDEO_BLEND_MODES = VIDEO_BLEND_MODES; exports.VIDEO_BLEND_MODE_LABELS = VIDEO_BLEND_MODE_LABELS; exports.VIDEO_CLIP_ANIMATION_DURATION = VIDEO_CLIP_ANIMATION_DURATION; exports.VIDEO_CLIP_ANIMATION_EFFECTS = VIDEO_CLIP_ANIMATION_EFFECTS; exports.VIDEO_CLIP_ANIMATION_LABELS = VIDEO_CLIP_ANIMATION_LABELS; exports.VIDEO_CLIP_TYPES = VIDEO_CLIP_TYPES; exports.VIDEO_DEFAULT_FILLERS = VIDEO_DEFAULT_FILLERS; exports.VIDEO_DEFAULT_STILL_DURATION = VIDEO_DEFAULT_STILL_DURATION; exports.VIDEO_DEFAULT_TITLE_DURATION = VIDEO_DEFAULT_TITLE_DURATION; exports.VIDEO_EASE_NAMES = VIDEO_EASE_NAMES; exports.VIDEO_EDIT_OPS = VIDEO_EDIT_OPS; exports.VIDEO_FADE_CURVES = VIDEO_FADE_CURVES; exports.VIDEO_FADE_CURVE_LABELS = VIDEO_FADE_CURVE_LABELS; exports.VIDEO_FITS = VIDEO_FITS; exports.VIDEO_GRADE_CONTROLS = VIDEO_GRADE_CONTROLS; exports.VIDEO_GRADE_PRESETS = VIDEO_GRADE_PRESETS; exports.VIDEO_GRADE_PRESET_NAMES = VIDEO_GRADE_PRESET_NAMES; exports.VIDEO_ISSUE_CODES = VIDEO_ISSUE_CODES; exports.VIDEO_KEYFRAME_LIMITS = VIDEO_KEYFRAME_LIMITS; exports.VIDEO_KEYFRAME_PROPERTIES = VIDEO_KEYFRAME_PROPERTIES; exports.VIDEO_LUT_MAX_SIZE = VIDEO_LUT_MAX_SIZE; exports.VIDEO_PEAK_BLOCK = VIDEO_PEAK_BLOCK; exports.VIDEO_PLACEMENT_MODES = VIDEO_PLACEMENT_MODES; exports.VIDEO_PREVIEW_SCALE_OPTIONS = VIDEO_PREVIEW_SCALE_OPTIONS; exports.VIDEO_PROXY_SCALES = VIDEO_PROXY_SCALES; exports.VIDEO_SCOPE_KINDS = VIDEO_SCOPE_KINDS; exports.VIDEO_TIME_EPSILON = VIDEO_TIME_EPSILON; exports.VIDEO_TRANSFORM_PROPERTIES = VIDEO_TRANSFORM_PROPERTIES; exports.VIDEO_TRANSITION_LABELS = VIDEO_TRANSITION_LABELS; exports.VIDEO_TRANSITION_TYPES = VIDEO_TRANSITION_TYPES; exports.VIDEO_VECTORSCOPE_TARGETS = VIDEO_VECTORSCOPE_TARGETS; exports.VIDEO_WIPE_DIRECTIONS = VIDEO_WIPE_DIRECTIONS; exports.VideoAudioPeaksCache = VideoAudioPeaksCache; exports.VideoEditorController = VideoEditorController; exports.VideoFrameCache = VideoFrameCache; exports.VideoLutCache = VideoLutCache; exports.VideoPreviewEngine = VideoPreviewEngine; exports.VideoPreviewPixels = VideoPreviewPixels; exports.allVideoClipIds = allVideoClipIds; exports.allVideoTracks = allVideoTracks; exports.applyVideoEdits = applyVideoEdits; exports.applyVideoGrade = applyVideoGrade; exports.applyVideoLut = applyVideoLut; exports.applyVideoLutToPixels = applyVideoLutToPixels; exports.buildVideoAudioPeaks = buildVideoAudioPeaks; exports.checkVideoProject = checkVideoProject; exports.checkVideoTimeRemap = checkVideoTimeRemap; exports.createVideoEditorToolProvider = createVideoEditorToolProvider; exports.createVideoElementDecoder = createVideoElementDecoder; exports.createVideoProject = createVideoProject; exports.decodeVideoAudioPeaks = decodeVideoAudioPeaks; exports.describeVideoProject = describeVideoProject; exports.describeVideoSequence = describeVideoSequence; exports.drawVideoFrame = drawVideoFrame; exports.ffmpegFilterValue = ffmpegFilterValue; exports.findVideoAudioSilences = findVideoAudioSilences; exports.findVideoClip = findVideoClip; exports.findVideoFillers = findVideoFillers; exports.findVideoGaps = findVideoGaps; exports.findVideoSequenceClip = findVideoSequenceClip; exports.findVideoSequenceCycle = findVideoSequenceCycle; exports.findVideoTranscriptPauses = findVideoTranscriptPauses; exports.findVideoTranscriptText = findVideoTranscriptText; exports.formatVideoSeconds = formatVideoSeconds; exports.formatVideoTimecode = formatVideoTimecode; exports.formatVideoTimecodeFrames = formatVideoTimecodeFrames; exports.isVideoDropFrameRate = isVideoDropFrameRate; exports.isVideoKeyframes = isVideoKeyframes; exports.mergeVideoRanges = mergeVideoRanges; exports.nextFreeVideoSlot = nextFreeVideoSlot; exports.normalizeVideoGrade = normalizeVideoGrade; exports.normalizeVideoGradeLut = normalizeVideoGradeLut; exports.normalizeVideoWord = normalizeVideoWord; exports.parseVideoAnimatable = parseVideoAnimatable; exports.parseVideoAsset = parseVideoAsset; exports.parseVideoAssetProxies = parseVideoAssetProxies; exports.parseVideoClip = parseVideoClip; exports.parseVideoClipAnimation = parseVideoClipAnimation; exports.parseVideoCubeLut = parseVideoCubeLut; exports.parseVideoGrade = parseVideoGrade; exports.parseVideoGradeLut = parseVideoGradeLut; exports.parseVideoKeyframes = parseVideoKeyframes; exports.parseVideoLoudnessMeasurement = parseVideoLoudnessMeasurement; exports.parseVideoMarker = parseVideoMarker; exports.parseVideoPreviewScale = parseVideoPreviewScale; exports.parseVideoProject = parseVideoProject; exports.parseVideoSequence = parseVideoSequence; exports.parseVideoTimeRemap = parseVideoTimeRemap; exports.parseVideoTrack = parseVideoTrack; exports.parseVideoTranscript = parseVideoTranscript; exports.parseVideoTranscriptWords = parseVideoTranscriptWords; exports.parseVideoTransform = parseVideoTransform; exports.parseVideoTransition = parseVideoTransition; exports.placeVideoRect = placeVideoRect; exports.planVideoClipDrag = planVideoClipDrag; exports.planVideoPrefetch = planVideoPrefetch; exports.probeVideoMedia = probeVideoMedia; exports.readVideoScopesPanel = readVideoScopesPanel; exports.removeVideoKey = removeVideoKey; exports.resolveVideoPreviewScale = resolveVideoPreviewScale; exports.retimeVideoAnimatable = retimeVideoAnimatable; exports.setVideoKey = setVideoKey; exports.snapToVideoFrame = snapToVideoFrame; exports.snapVideoTime = snapVideoTime; exports.sortVideoKeys = sortVideoKeys; exports.sortedVideoClips = sortedVideoClips; exports.splitVideoAnimatable = splitVideoAnimatable; exports.summarizeVideoScopes = summarizeVideoScopes; exports.toVideoEdl = toVideoEdl; exports.toVideoFfmpeg = toVideoFfmpeg; exports.toVideoLoudnessMeasure = toVideoLoudnessMeasure; exports.videoAssetDropEdit = videoAssetDropEdit; exports.videoAssetHasSound = videoAssetHasSound; exports.videoAssetKindOf = videoAssetKindOf; exports.videoAssetMap = videoAssetMap; exports.videoAssetProxy = videoAssetProxy; exports.videoAudioLevels = videoAudioLevels; exports.videoAudioPeakRange = videoAudioPeakRange; exports.videoAudioPeakSpans = videoAudioPeakSpans; exports.videoAudioPeaksDuration = videoAudioPeaksDuration; exports.videoCacheEvictions = videoCacheEvictions; exports.videoChroma = videoChroma; exports.videoClipAfter = videoClipAfter; exports.videoClipAnimated = videoClipAnimated; exports.videoClipAnimationProperties = videoClipAnimationProperties; exports.videoClipAnimationSpans = videoClipAnimationSpans; exports.videoClipBefore = videoClipBefore; exports.videoClipEnd = videoClipEnd; exports.videoClipHandles = videoClipHandles; exports.videoClipKeyframes = videoClipKeyframes; exports.videoClipLabel = videoClipLabel; exports.videoClipOverhang = videoClipOverhang; exports.videoClipShowsWaveform = videoClipShowsWaveform; exports.videoClipSoundGain = videoClipSoundGain; exports.videoClipSourceRange = videoClipSourceRange; exports.videoClipSourceTime = videoClipSourceTime; exports.videoClipSpeed = videoClipSpeed; exports.videoClipSpeedAt = videoClipSpeedAt; exports.videoClipTint = videoClipTint; exports.videoClipWaveform = videoClipWaveform; exports.videoClipWaveformPath = videoClipWaveformPath; exports.videoClipsAt = videoClipsAt; exports.videoContactSheetSvg = videoContactSheetSvg; exports.videoDragLabel = videoDragLabel; exports.videoDropTracks = videoDropTracks; exports.videoEaseBezier = videoEaseBezier; exports.videoEaseProgress = videoEaseProgress; exports.videoExposureNotes = videoExposureNotes; exports.videoExposureStats = videoExposureStats; exports.videoFadeCurveGain = videoFadeCurveGain; exports.videoFrameAt = videoFrameAt; exports.videoFrameCacheKey = videoFrameCacheKey; exports.videoFrameDuration = videoFrameDuration; exports.videoFrameLeafLayers = videoFrameLeafLayers; exports.videoFrameRequestsAt = videoFrameRequestsAt; exports.videoFrameToSvg = videoFrameToSvg; exports.videoFrames = videoFrames; exports.videoFreezeFrameEdit = videoFreezeFrameEdit; exports.videoGradeCssFilter = videoGradeCssFilter; exports.videoGradeFfmpegFilters = videoGradeFfmpegFilters; exports.videoGradeGainsFilterId = videoGradeGainsFilterId; exports.videoGradeLook = videoGradeLook; exports.videoGradePreset = videoGradePreset; exports.videoGradeStepMatrix = videoGradeStepMatrix; exports.videoGradeStepSvgValues = videoGradeStepSvgValues; exports.videoGradeSteps = videoGradeSteps; exports.videoGradeSvgDefs = videoGradeSvgDefs; exports.videoGradeSvgFilter = videoGradeSvgFilter; exports.videoHistogram = videoHistogram; exports.videoHistogramImage = videoHistogramImage; exports.videoHourFrames = videoHourFrames; exports.videoIrisRadius = videoIrisRadius; exports.videoIssueKey = videoIssueKey; exports.videoKeyframeBreakpoints = videoKeyframeBreakpoints; exports.videoKeyframeFfmpegExpr = videoKeyframeFfmpegExpr; exports.videoKeyframeRows = videoKeyframeRows; exports.videoKeyframeToggleEdit = videoKeyframeToggleEdit; exports.videoKeyframeValueEdit = videoKeyframeValueEdit; exports.videoKeysOf = videoKeysOf; exports.videoLuma = videoLuma; exports.videoLumaWaveform = videoLumaWaveform; exports.videoLutEdits = videoLutEdits; exports.videoLutFromFunction = videoLutFromFunction; exports.videoLutSvgApproximation = videoLutSvgApproximation; exports.videoLutSvgPrimitives = videoLutSvgPrimitives; exports.videoLutToCube = videoLutToCube; exports.videoOverlap = videoOverlap; exports.videoPeakDb = videoPeakDb; exports.videoPreviewSrc = videoPreviewSrc; exports.videoProjectDuration = videoProjectDuration; exports.videoProjectHasProxies = videoProjectHasProxies; exports.videoProjectSvg = videoProjectSvg; exports.videoProxyFfmpeg = videoProxyFfmpeg; exports.videoProxyName = videoProxyName; exports.videoProxyPlan = videoProxyPlan; exports.videoRemoveRangesEdits = videoRemoveRangesEdits; exports.videoRulerTicks = videoRulerTicks; exports.videoScopeImage = videoScopeImage; exports.videoSequenceChildren = videoSequenceChildren; exports.videoSequenceDuration = videoSequenceDuration; exports.videoSequenceMap = videoSequenceMap; exports.videoShellQuote = videoShellQuote; exports.videoSlowMotionRamp = videoSlowMotionRamp; exports.videoSnapPoints = videoSnapPoints; exports.videoSourceFrame = videoSourceFrame; exports.videoSourceFrameTime = videoSourceFrameTime; exports.videoSpeedRampEdit = videoSpeedRampEdit; exports.videoSpeedRampKeys = videoSpeedRampKeys; exports.videoTextStyle = videoTextStyle; exports.videoTimeRemapBreakpoints = videoTimeRemapBreakpoints; exports.videoTimeRemapSpan = videoTimeRemapSpan; exports.videoTimecodeFrames = videoTimecodeFrames; exports.videoTimelineTranscript = videoTimelineTranscript; exports.videoTitleFrame = videoTitleFrame; exports.videoTrackSyncLocked = videoTrackSyncLocked; exports.videoTranscriptEdit = videoTranscriptEdit; exports.videoTranscriptTrack = videoTranscriptTrack; exports.videoTransformAt = videoTransformAt; exports.videoTransitionAudioCurve = videoTransitionAudioCurve; exports.videoTransitionEase = videoTransitionEase; exports.videoTransitionHasDirection = videoTransitionHasDirection; exports.videoValueAt = videoValueAt; exports.videoVectorscope = videoVectorscope; exports.videoVectorscopeAngle = videoVectorscopeAngle; exports.videoVectorscopeImage = videoVectorscopeImage; exports.videoVectorscopePoint = videoVectorscopePoint; exports.videoWaveformImage = videoWaveformImage; exports.videoWaveformPath = videoWaveformPath; exports.videoWipeRect = videoWipeRect; exports.videoWordAt = videoWordAt; exports.videoWordsRange = videoWordsRange;
5498
6254
  //# sourceMappingURL=index.cjs.map