@vosjs/studio-core 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -209,6 +209,20 @@ function exportSizeFor(ratio, resolution) {
209
209
  const safe = ratio > 0 && Number.isFinite(ratio) ? ratio : 16 / 9;
210
210
  return safe >= 1 ? { width: even(short * safe), height: even(short) } : { width: even(short), height: even(short / safe) };
211
211
  }
212
+ var MEDIA_FRAME_KEYS = [
213
+ "browserBar",
214
+ "radius",
215
+ "shadow",
216
+ "shadowContact",
217
+ "shadowColor",
218
+ "inset",
219
+ "border",
220
+ "borderWidth",
221
+ "borderColor",
222
+ "fit",
223
+ "focus",
224
+ "focusFollow"
225
+ ];
212
226
 
213
227
  // src/capture.ts
214
228
  var CHROME_TOP_MIN = 20;
@@ -511,6 +525,71 @@ function lastLayerEnd(doc) {
511
525
 
512
526
  // src/lower/motion.ts
513
527
  import { splitBySpeed, totalDuration } from "@vosjs/timeline";
528
+
529
+ // src/media.ts
530
+ var PRIMARY_MEDIA = "";
531
+ var mediaKey = (id) => id ?? PRIMARY_MEDIA;
532
+ var sameMedia = (a, b) => mediaKey(a) === mediaKey(b);
533
+ function docMediaList(doc) {
534
+ return [
535
+ { id: PRIMARY_MEDIA, source: doc.source },
536
+ ...(doc.media ?? []).map((m) => ({ id: m.id, source: m }))
537
+ ];
538
+ }
539
+ function mediaSource(doc, id) {
540
+ const key = mediaKey(id);
541
+ if (key === PRIMARY_MEDIA) return doc.source;
542
+ return (doc.media ?? []).find((m) => m.id === key) ?? null;
543
+ }
544
+ function mediaDuration(source) {
545
+ return (source.meta.durationMs || 0) / 1e3;
546
+ }
547
+ function mediaAtOutput(rated, t) {
548
+ let acc = 0;
549
+ let last = null;
550
+ for (const p of rated) {
551
+ const len = Math.max(0, p.out - p.in) / (p.rate && p.rate > 0 ? p.rate : 1);
552
+ if (t < acc + len) return mediaKey(p.media);
553
+ acc += len;
554
+ last = p;
555
+ }
556
+ return last ? mediaKey(last.media) : PRIMARY_MEDIA;
557
+ }
558
+ function mediaFrame(doc, id) {
559
+ const key = mediaKey(id);
560
+ if (key === PRIMARY_MEDIA) return doc.frame;
561
+ const m = (doc.media ?? []).find((x) => x.id === key);
562
+ if (!m) return doc.frame;
563
+ const over = m.frame ?? {};
564
+ const out = { ...doc.frame };
565
+ for (const k of MEDIA_FRAME_KEYS) {
566
+ if (k === "browserBar") continue;
567
+ const v = over[k];
568
+ if (v !== void 0) out[k] = v;
569
+ }
570
+ const bar = { ...doc.frame.browserBar };
571
+ const page = pageDisplayUrl(m.meta.pageUrl);
572
+ if (page) bar.url = page;
573
+ if (!m.meta.pageUrl) bar.kind = "none";
574
+ out.browserBar = { ...bar, ...over.browserBar ?? {} };
575
+ return out;
576
+ }
577
+ function cardFields(frame) {
578
+ const out = {};
579
+ for (const k of MEDIA_FRAME_KEYS) {
580
+ const v = frame[k];
581
+ if (v !== void 0) out[k] = v;
582
+ }
583
+ return out;
584
+ }
585
+ function nextMediaId(doc) {
586
+ const taken = new Set((doc.media ?? []).map((m) => m.id));
587
+ let n = 1;
588
+ while (taken.has(`m${n}`)) n++;
589
+ return `m${n}`;
590
+ }
591
+
592
+ // src/lower/motion.ts
514
593
  function docRestTime(doc) {
515
594
  if (doc.endCard) return null;
516
595
  const freezes = docFreezes(doc);
@@ -575,17 +654,19 @@ function exitSeconds(e) {
575
654
  }
576
655
  function placeFreezes(rated, freezes) {
577
656
  const marks = /* @__PURE__ */ new Map();
578
- const pending = [...freezes].filter((f) => f.seconds > 0).sort((a, b) => a.at - b.at);
657
+ const placed = /* @__PURE__ */ new Set();
579
658
  const pieces = [];
580
659
  let acc = 0;
581
- const freezePiece = (f) => {
660
+ const freezePiece = (f, media) => {
582
661
  const at = Math.max(HOLD_SOURCE_SPAN, f.at);
583
662
  marks.set(f.id, { t: acc, duration: f.seconds });
584
663
  pieces.push({
585
664
  in: at - HOLD_SOURCE_SPAN,
586
665
  out: at,
587
- rate: HOLD_SOURCE_SPAN / f.seconds
666
+ rate: HOLD_SOURCE_SPAN / f.seconds,
667
+ ...media !== void 0 ? { media } : {}
588
668
  });
669
+ placed.add(f.id);
589
670
  acc += f.seconds;
590
671
  };
591
672
  const push2 = (p) => {
@@ -595,32 +676,23 @@ function placeFreezes(rated, freezes) {
595
676
  for (let i = 0; i < rated.length; i++) {
596
677
  let piece = rated[i];
597
678
  const next = rated[i + 1];
598
- while (pending.length) {
599
- const f = pending[0];
600
- if (f.at > piece.out + 1e-9) break;
601
- if (f.at < piece.in - 1e-9) {
602
- pending.shift();
603
- continue;
604
- }
679
+ const continues = !!next && sameMedia(next.media, piece.media) && Math.abs(next.in - piece.out) < 1e-9 && next.out > piece.out;
680
+ const mine = freezes.filter(
681
+ (f) => f.seconds > 0 && !placed.has(f.id) && sameMedia(f.media, piece.media) && f.at >= piece.in - 1e-9 && (continues ? f.at < piece.out - 1e-9 : f.at <= piece.out + 1e-9)
682
+ ).sort((a, b) => a.at - b.at);
683
+ for (const f of mine) {
605
684
  if (f.at <= piece.in + 1e-9) {
606
- pending.shift();
607
- freezePiece(f);
608
- continue;
609
- }
610
- if (f.at < piece.out - 1e-9) {
685
+ freezePiece(f, piece.media);
686
+ } else if (f.at < piece.out - 1e-9) {
611
687
  push2({ ...piece, out: f.at });
612
- pending.shift();
613
- freezePiece(f);
688
+ freezePiece(f, piece.media);
614
689
  piece = { ...piece, in: f.at };
615
- continue;
690
+ } else {
691
+ const media = piece.media;
692
+ push2(piece);
693
+ piece = null;
694
+ freezePiece(f, media);
616
695
  }
617
- const continues = next && Math.abs(next.in - piece.out) < 1e-9 && next.out > piece.out;
618
- if (continues) break;
619
- push2(piece);
620
- piece = null;
621
- pending.shift();
622
- freezePiece(f);
623
- break;
624
696
  }
625
697
  if (piece) push2(piece);
626
698
  }
@@ -1570,7 +1642,7 @@ import {
1570
1642
  sample,
1571
1643
  segmentRate as segmentRate2,
1572
1644
  sortKeyframes as sortKeyframes2,
1573
- sourceToTimeline as sourceToTimeline2,
1645
+ sourceToTimeline,
1574
1646
  splitBySpeed as splitBySpeed2,
1575
1647
  totalDuration as totalDuration2
1576
1648
  } from "@vosjs/timeline";
@@ -1640,18 +1712,20 @@ function computeCardLayout(frame, video, W, H, zoomFocus) {
1640
1712
  cardH: dh + barH
1641
1713
  };
1642
1714
  }
1643
- function docCardLayout(doc) {
1715
+ function docCardLayout(doc, media) {
1644
1716
  const meta = doc.source.meta;
1645
1717
  const H = 1080;
1646
1718
  const W = Math.max(
1647
1719
  2,
1648
1720
  Math.round(H * aspectRatioValue(doc.frame.aspectRatio, meta))
1649
1721
  );
1722
+ const src = mediaSource(doc, media) ?? doc.source;
1723
+ const sm = src.meta;
1650
1724
  return computeCardLayout(
1651
- doc.frame,
1725
+ mediaFrame(doc, media),
1652
1726
  {
1653
- width: meta.captureWidth ?? meta.width,
1654
- height: meta.captureHeight ?? meta.height
1727
+ width: sm.captureWidth ?? sm.width,
1728
+ height: sm.captureHeight ?? sm.height
1655
1729
  },
1656
1730
  W,
1657
1731
  H
@@ -3242,7 +3316,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
3242
3316
  }`;
3243
3317
 
3244
3318
  // src/lower/extractClicks.ts
3245
- import { segmentRate, sourceToTimeline } from "@vosjs/timeline";
3319
+ import { segmentRate } from "@vosjs/timeline";
3246
3320
  var CLICK_FX_PRE = 0.06;
3247
3321
  var CLICK_RIPPLE_DUR = 0.45;
3248
3322
  var CLICK_PULSE_DUR = 0.35;
@@ -3251,14 +3325,27 @@ var CLICK_SYNTH_RELEASE = 0.12;
3251
3325
  var CLICK_PAIR_MAX = 10;
3252
3326
  var CLICK_RECT_MAX_FRAC = 0.35;
3253
3327
  var RECT_CONTAIN_MARGIN = 8;
3254
- function keptOutputEnd(segments, sIn, sOut) {
3328
+ function outputAt(segments, sourceT, media) {
3329
+ if (!segments.length) return sourceT;
3330
+ let acc = 0;
3331
+ for (const s of segments) {
3332
+ const rate = segmentRate(s);
3333
+ if (sameMedia(s.media, media) && sourceT >= s.in && sourceT <= s.out)
3334
+ return acc + (sourceT - s.in) / rate;
3335
+ acc += Math.max(0, s.out - s.in) / rate;
3336
+ }
3337
+ return null;
3338
+ }
3339
+ function keptOutputEnd(segments, sIn, sOut, media) {
3255
3340
  let acc = 0;
3256
3341
  let end = null;
3257
3342
  for (const p of segments) {
3258
3343
  const rate = segmentRate(p);
3259
- const ovIn = Math.max(sIn, p.in);
3260
- const ovOut = Math.min(sOut, p.out);
3261
- if (ovOut > ovIn) end = acc + (ovOut - p.in) / rate;
3344
+ if (sameMedia(p.media, media)) {
3345
+ const ovIn = Math.max(sIn, p.in);
3346
+ const ovOut = Math.min(sOut, p.out);
3347
+ if (ovOut > ovIn) end = acc + (ovOut - p.in) / rate;
3348
+ }
3262
3349
  acc += Math.max(0, p.out - p.in) / rate;
3263
3350
  }
3264
3351
  return end;
@@ -3290,10 +3377,10 @@ function extractClicks(track, segments, opts) {
3290
3377
  break;
3291
3378
  }
3292
3379
  }
3293
- const ot = sourceToTimeline(segments, st);
3380
+ const ot = outputAt(segments, st, opts.media);
3294
3381
  if (ot === null) continue;
3295
3382
  const upSrc = st + Math.max(pressLen, 0.02);
3296
- const up = keptOutputEnd(segments, st, upSrc) ?? ot + CLICK_SYNTH_RELEASE;
3383
+ const up = keptOutputEnd(segments, st, upSrc, opts.media) ?? ot + CLICK_SYNTH_RELEASE;
3297
3384
  const click = {
3298
3385
  ot: round5(ot),
3299
3386
  up: round5(Math.max(up, ot + 0.02)),
@@ -3346,19 +3433,32 @@ var PROGRAM_DURATION = 1;
3346
3433
  function ratedSegments(doc) {
3347
3434
  const segs = isRecordingDoc(doc) ? doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }] : [{ in: 0, out: programDuration(doc) }];
3348
3435
  const freezes = isRecordingDoc(doc) ? docFreezes(doc) : [];
3349
- return withFreezes(splitBySpeed2(segs, doc.speed ?? []), freezes);
3436
+ const speeds = doc.speed ?? [];
3437
+ const pieces = segs.flatMap(
3438
+ (seg) => splitBySpeed2(
3439
+ [seg],
3440
+ speeds.filter(
3441
+ (sp) => sameMedia(sp.media, seg.media)
3442
+ )
3443
+ )
3444
+ );
3445
+ return withFreezes(pieces, freezes);
3350
3446
  }
3351
3447
  function durationSec(doc, rated) {
3352
3448
  const trimmed = totalDuration2(rated);
3353
3449
  return trimmed > 0 ? trimmed : doc.source.meta.durationMs / 1e3;
3354
3450
  }
3355
- function spanOutputExtent(segments, sIn, sOut) {
3451
+ function spanOutputExtent(segments, sIn, sOut, media) {
3356
3452
  let acc = 0;
3357
3453
  let start = null;
3358
3454
  let end = null;
3359
3455
  for (const p of segments) {
3360
3456
  const rate = segmentRate2(p);
3361
3457
  const len = Math.max(0, p.out - p.in) / rate;
3458
+ if (!sameMedia(p.media, media)) {
3459
+ acc += len;
3460
+ continue;
3461
+ }
3362
3462
  const ovIn = Math.max(sIn, p.in);
3363
3463
  const ovOut = Math.min(sOut, p.out);
3364
3464
  if (ovOut > ovIn) {
@@ -3389,7 +3489,7 @@ function trackEmitter() {
3389
3489
  function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE]) {
3390
3490
  const panEase = style.panEase;
3391
3491
  const mapped = [...zoom].sort((a, b) => a.in - b.in).flatMap((z) => {
3392
- const ext = spanOutputExtent(segments, z.in, z.out);
3492
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
3393
3493
  return ext ? [{ z, tIn: ext.start, tOut: ext.end }] : [];
3394
3494
  });
3395
3495
  const { keyframes, push: push2 } = trackEmitter();
@@ -3408,7 +3508,7 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
3408
3508
  push2(start + style.rampIn * m, entry, spanEase(z.ease, style.ease));
3409
3509
  }
3410
3510
  for (const e of z.followEvents ?? []) {
3411
- const eOut = sourceToTimeline2(segments, e.t);
3511
+ const eOut = sourceToTimeline(segments, e.t);
3412
3512
  if (eOut === null || eOut <= tIn || eOut >= tOut) continue;
3413
3513
  const next2 = [cur[0], e.cx, e.cy];
3414
3514
  push2(eOut, cur, "none");
@@ -3451,7 +3551,7 @@ function tiltTrackFromDoc(tilt, segments, motion = {}) {
3451
3551
  const panEase = TILT_PAN_EASE;
3452
3552
  const rest = [0, 0];
3453
3553
  const mapped = [...tilt].sort((a, b) => a.in - b.in).flatMap((z) => {
3454
- const ext = spanOutputExtent(segments, z.in, z.out);
3554
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
3455
3555
  return ext ? [{ z, tIn: ext.start, tOut: ext.end }] : [];
3456
3556
  });
3457
3557
  const { keyframes, push: push2 } = trackEmitter();
@@ -3494,7 +3594,7 @@ function camTrackFromDoc(cam, spans, segments, W, H = 1080) {
3494
3594
  ];
3495
3595
  const panEase = CAM_PAN_EASE;
3496
3596
  const mapped = [...spans].sort((a, b) => a.in - b.in).flatMap((z) => {
3497
- const ext = spanOutputExtent(segments, z.in, z.out);
3597
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
3498
3598
  return ext ? [{ z, tIn: ext.start, tOut: ext.end }] : [];
3499
3599
  });
3500
3600
  const { keyframes, push: push2 } = trackEmitter();
@@ -3870,6 +3970,26 @@ var SETUP = `async (ctx) => {
3870
3970
  // the same sync as the screen video (source-anchored, so element time == the
3871
3971
  // video's). Unmuted \u2014 syncVid's autoplay net re-mutes on policy rejection.
3872
3972
  const mic = ctx.data.micSrc ? await load(ctx.data.micSrc, false) : null
3973
+ // The take's OTHER media (concat): one element each, decoded the way the
3974
+ // primary is; ON_FRAME drives the one under the playhead and pauses the
3975
+ // rest. Absent = one media, byte-identical.
3976
+ const mediaEls = {}
3977
+ for (const m of ctx.data.media || []) {
3978
+ try {
3979
+ const el = m.isImage ? await loadImage(m.src) : await load(m.src, !m.hasAudio)
3980
+ if (!m.isImage && ctx.data.videoDecodeMode === 'webcodecs') {
3981
+ try {
3982
+ const wp = await makeWcProvider(m.src)
3983
+ if (wp && (!el.videoWidth || (wp.width === el.videoWidth && wp.height === el.videoHeight))) el.__voilaWc = wp
3984
+ } catch (e) {
3985
+ console.warn('[voila] webcodecs provider unavailable for a media, seeks stay html5', e)
3986
+ }
3987
+ }
3988
+ mediaEls[m.id] = el
3989
+ } catch (e) {
3990
+ console.warn('[voila] a media failed to load', m.id, e)
3991
+ }
3992
+ }
3873
3993
  // Background media (frame.backgroundMedia): warm-load so the first paint is
3874
3994
  // complete. FAIL-OPEN \u2014 a bad key degrades to the CSS fill underneath, never
3875
3995
  // a dead LOAD (unlike the recording, which is the comp's reason to exist).
@@ -3898,9 +4018,27 @@ var SETUP = `async (ctx) => {
3898
4018
  const d = ctx.data
3899
4019
  const TL = globalThis.__vosTimeline
3900
4020
  const srcT = TL.mapTime(d.segments || [], t)
3901
- if (video.play) stepPaused(video, srcT)
3902
- if (cam) stepPaused(cam, srcT)
3903
- if (mic) stepPaused(mic, srcT)
4021
+ // The media under the playhead (concat): step its element; the
4022
+ // primary's sidecars belong to the primary's moments only.
4023
+ let am = ''
4024
+ {
4025
+ let acc = 0
4026
+ const sg = d.segments || []
4027
+ for (let i = 0; i < sg.length; i++) {
4028
+ const s0 = sg[i]
4029
+ const ln = Math.max(0, s0.out - s0.in) / (s0.rate || 1)
4030
+ if (t < acc + ln || i === sg.length - 1) { am = s0.media || ''; break }
4031
+ acc += ln
4032
+ }
4033
+ }
4034
+ const amEl = am ? mediaEls[am] : null
4035
+ if (amEl) {
4036
+ if (amEl.play) stepPaused(amEl, srcT)
4037
+ } else {
4038
+ if (video.play) stepPaused(video, srcT)
4039
+ if (cam) stepPaused(cam, srcT)
4040
+ if (mic) stepPaused(mic, srcT)
4041
+ }
3904
4042
  const bg2 = d.frame && d.frame.backgroundMedia
3905
4043
  if (bg2 && bg2.key && bg2.kind !== 'image') {
3906
4044
  const bgEl2 = cache.get(bg2.key)
@@ -3910,7 +4048,7 @@ var SETUP = `async (ctx) => {
3910
4048
  }
3911
4049
  }
3912
4050
  })
3913
- return { video: video, cam: cam, mic: mic }
4051
+ return { video: video, cam: cam, mic: mic, media: mediaEls }
3914
4052
  }`;
3915
4053
  var CREATE_CONTENT = `(ctx, setupData) => {
3916
4054
  const THREE = ctx.THREE
@@ -3961,6 +4099,7 @@ var CREATE_CONTENT = `(ctx, setupData) => {
3961
4099
  // { c2d, canvas, texture }) drive the card and let bg/overlay fall back.
3962
4100
  canvas: card.canvas, c2d: card.c2d, texture: card.texture,
3963
4101
  video: setupData.video, cam: setupData.cam, mic: setupData.mic,
4102
+ media: setupData.media,
3964
4103
  },
3965
4104
  }
3966
4105
  }`;
@@ -4071,6 +4210,29 @@ var ON_FRAME = `(ctx, content, dt) => {
4071
4210
  // Output-timeline seconds (engine-fed master clock) \u2192 source seconds on screen.
4072
4211
  var t = ctx.time || 0
4073
4212
  var srcT = TL.mapTime(d.segments || [], t)
4213
+ // The media under the playhead (concat): its element, crop, cursor and
4214
+ // space stand in for the primary's below, and every other media's
4215
+ // element rests. An absent media list is the primary alone.
4216
+ var amId = ''
4217
+ { var amAcc = 0, amSg = d.segments || []
4218
+ for (var amI = 0; amI < amSg.length; amI++) {
4219
+ var amS = amSg[amI], amLn = Math.max(0, amS.out - amS.in) / (amS.rate || 1)
4220
+ if (t < amAcc + amLn || amI === amSg.length - 1) { amId = amS.media || ''; break }
4221
+ amAcc += amLn
4222
+ } }
4223
+ var actM = null
4224
+ if (amId && d.media) for (var amJ = 0; amJ < d.media.length; amJ++) if (d.media[amJ].id === amId) actM = d.media[amJ]
4225
+ if (actM && r.media && r.media[amId]) video = r.media[amId]
4226
+ else amId = ''
4227
+ // The card under the playhead: another media's own card fields over the
4228
+ // take's frame (resolved at lowering), the take's frame for the primary.
4229
+ var cfr = amId && actM && actM.frame ? actM.frame : frame
4230
+ if (r.media) for (var amK in r.media) { var amE = r.media[amK]; if (amK !== amId && amE && amE.pause && amE.paused === false) amE.pause() }
4231
+ if (amId) {
4232
+ if (r.video && r.video.pause && r.video.paused === false) r.video.pause()
4233
+ if (r.cam && r.cam.pause && r.cam.paused === false) r.cam.pause()
4234
+ if (r.mic && r.mic.pause && r.mic.paused === false) r.mic.pause()
4235
+ }
4074
4236
  var s = H / 1080 // scale design-px controls to comp px
4075
4237
 
4076
4238
  // Play natively while playing (smooth); seek precisely otherwise (paused, scrubbing,
@@ -4152,8 +4314,8 @@ var ON_FRAME = `(ctx, content, dt) => {
4152
4314
  } catch (e) {}
4153
4315
  }
4154
4316
  if (video.play) syncVid(video) // stills (HTMLImageElement) have nothing to sync
4155
- if (r.cam) syncVid(r.cam)
4156
- if (r.mic) syncVid(r.mic)
4317
+ if (r.cam && !amId) syncVid(r.cam)
4318
+ if (r.mic && !amId) syncVid(r.mic)
4157
4319
  // Gain routing (live via SET_DATA). With a mic sidecar (AT split) the
4158
4320
  // recording <video> carries SYSTEM audio \u2014 its volume is the system fader \u2014
4159
4321
  // and the sidecar element is the voice (micGain). Legacy takes have one
@@ -4367,19 +4529,19 @@ var ON_FRAME = `(ctx, content, dt) => {
4367
4529
  // strip is part of the card: it takes barH from the available height and the video
4368
4530
  // sits below it \u2014 bar + video share the rounded clip and zoom together.
4369
4531
  var pad = (frame.padding || 0) * s
4370
- // Per-side placement (frame.inset: fractions of the frame, a negative side
4532
+ // Per-side placement (cfr.inset: fractions of the frame, a negative side
4371
4533
  // bleeds the card past the edge) overrides the symmetric padding on the
4372
4534
  // sides it names; absent sides keep pad, so the old math falls out.
4373
- var ipIns = frame.inset || {}
4535
+ var ipIns = cfr.inset || {}
4374
4536
  var ipL = ipIns.left == null ? pad : ipIns.left * W
4375
4537
  var ipR = ipIns.right == null ? pad : ipIns.right * W
4376
4538
  var ipT = ipIns.top == null ? pad : ipIns.top * H
4377
4539
  var ipB = ipIns.bottom == null ? pad : ipIns.bottom * H
4378
- var bar = frame.browserBar || {}
4540
+ var bar = cfr.browserBar || {}
4379
4541
  // Window takes carry a viewport crop (drawImage source rect, capture px) that
4380
4542
  // removes the real browser chrome \u2014 the card's source dims are then the CROP
4381
4543
  // dims (meta/cursor were rewritten into crop space at doc build).
4382
- var crp = d.crop || null
4544
+ var crp = actM ? (actM.crop || null) : (d.crop || null)
4383
4545
  var vw = crp ? crp.w : (video.videoWidth || video.naturalWidth || 16)
4384
4546
  var vh = crp ? crp.h : (video.videoHeight || video.naturalHeight || 9)
4385
4547
  // Card-chrome scale: everything that belongs to the CARD (browser bar +
@@ -4393,11 +4555,11 @@ var ON_FRAME = `(ctx, content, dt) => {
4393
4555
  // Cover fit: the CARD rect and the VIDEO rect separate. Under
4394
4556
  // contain (default, byte-identical for every existing doc) the card IS the
4395
4557
  // fitted video; under cover the card is the padded area itself and the
4396
- // footage cover-fills it, cropped around frame.focus (normalized video
4558
+ // footage cover-fills it, cropped around cfr.focus (normalized video
4397
4559
  // fractions, the zoom cx/cy convention; clamped so no gap ever shows).
4398
4560
  // Chrome under cover scales by s alone (cf = 1): the card is as wide as
4399
4561
  // the frame allows, which is the case cf existed to protect against.
4400
- var fitCover = frame.fit === 'cover'
4562
+ var fitCover = cfr.fit === 'cover'
4401
4563
  var cf = fitCover ? 1 : Math.min(1, (W / H) / (vw / vh))
4402
4564
  var s2 = s * cf
4403
4565
  // current zoom \u2014 a standard keyframe track in OUTPUT time (hold + arrival pairs
@@ -4416,11 +4578,11 @@ var ON_FRAME = `(ctx, content, dt) => {
4416
4578
  sc = Math.max(availW / vw, availH / vh)
4417
4579
  dw = vw * sc; dh = vh * sc
4418
4580
  cardX = ipL; cardY = ipT; cardW = availW; cardH = availH + barH
4419
- var fcv = frame.focus || {}
4581
+ var fcv = cfr.focus || {}
4420
4582
  var fcx = fcv.cx == null ? 0.5 : Math.max(0, Math.min(1, fcv.cx))
4421
4583
  var fcy = fcv.cy == null ? 0.5 : Math.max(0, Math.min(1, fcv.cy))
4422
4584
  // focusFollow: the crop keeps the camera's focus in frame (a 9:16 cut).
4423
- if (frame.focusFollow === 'camera' && zt && zt.keyframes && zt.keyframes.length) { fcx = zx; fcy = zy }
4585
+ if (cfr.focusFollow === 'camera' && zt && zt.keyframes && zt.keyframes.length) { fcx = zx; fcy = zy }
4424
4586
  var vTop = ipT + barH
4425
4587
  dx = Math.min(cardX, Math.max(cardX + availW - dw, cardX + availW / 2 - fcx * dw))
4426
4588
  dy = Math.min(vTop, Math.max(vTop + availH - dh, vTop + availH / 2 - fcy * dh))
@@ -4432,14 +4594,14 @@ var ON_FRAME = `(ctx, content, dt) => {
4432
4594
  dx = ipL + (availW - dw) / 2; dy = ipT + barH + (availH - dh) / 2
4433
4595
  cardX = dx; cardY = dy - barH; cardW = dw; cardH = dh + barH
4434
4596
  }
4435
- var radius = (frame.radius || 0) * s2
4436
- var shadow = frame.shadow || 0
4597
+ var radius = (cfr.radius || 0) * s2
4598
+ var shadow = cfr.shadow || 0
4437
4599
  // The shadow's colour (a #rrggbb; the strengths are its alpha) and the
4438
4600
  // optional tight CONTACT layer that makes a light card sit on a light
4439
4601
  // ground.
4440
- var shC = frame.shadowContact || 0
4602
+ var shC = cfr.shadowContact || 0
4441
4603
  var shRgb = '0,0,0'
4442
- var shHex = frame.shadowColor
4604
+ var shHex = cfr.shadowColor
4443
4605
  if (typeof shHex === 'string' && /^#[0-9a-fA-F]{6}$/.test(shHex)) {
4444
4606
  shRgb = parseInt(shHex.slice(1, 3), 16) + ',' + parseInt(shHex.slice(3, 5), 16) + ',' + parseInt(shHex.slice(5, 7), 16)
4445
4607
  }
@@ -4579,15 +4741,15 @@ var ON_FRAME = `(ctx, content, dt) => {
4579
4741
  // expanded by half the width so the stroke's inner edge lands on the card's
4580
4742
  // own edge and never covers footage (inset, a 24px width ate 24px of the
4581
4743
  // recording). Outer corner radius grows with the width, the CSS-border rule;
4582
- // a square card stays square. frame.border is the ALPHA (0 = off) and rides
4744
+ // a square card stays square. cfr.border is the ALPHA (0 = off) and rides
4583
4745
  // globalAlpha, so borderColor takes any CSS colour notation without this
4584
4746
  // having to parse one. Card chrome, so it scales by s2.
4585
- if (frame.border) {
4586
- var bdW = (frame.borderWidth > 0 ? frame.borderWidth : ${FRAME_BORDER_WIDTH_DEFAULT}) * s2
4747
+ if (cfr.border) {
4748
+ var bdW = (cfr.borderWidth > 0 ? cfr.borderWidth : ${FRAME_BORDER_WIDTH_DEFAULT}) * s2
4587
4749
  var bdH = bdW / 2
4588
4750
  c.save()
4589
- c.globalAlpha = Math.min(1, frame.border)
4590
- c.strokeStyle = frame.borderColor || '${FRAME_BORDER_COLOR_DEFAULT}'
4751
+ c.globalAlpha = Math.min(1, cfr.border)
4752
+ c.strokeStyle = cfr.borderColor || '${FRAME_BORDER_COLOR_DEFAULT}'
4591
4753
  c.lineWidth = bdW
4592
4754
  rr(cardX - bdH, cardY - bdH, cardW + bdW, cardH + bdW, radius > 0 ? radius + bdH : 0)
4593
4755
  c.stroke()
@@ -4598,7 +4760,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4598
4760
  // card. Contain never needs it (the video rect IS the card).
4599
4761
  if (fitCover) { c.save(); rr(cardX, cardY, cardW, cardH, radius); c.clip() }
4600
4762
  // cursor coordinate space + drawn radius (shared by click effects + the dot)
4601
- var space = d.cursorSpace || { w: vw, h: vh }
4763
+ var space = (actM ? actM.cursorSpace : d.cursorSpace) || { w: vw, h: vh }
4602
4764
  var curSize = ((d.cursorStyle && d.cursorStyle.size) || 24) * s2 * 0.5
4603
4765
 
4604
4766
  // click effects \u2014 pure f(t): d.clicks are
@@ -4630,8 +4792,9 @@ var ON_FRAME = `(ctx, content, dt) => {
4630
4792
  // click's source moment, or an effect near a cut would keep painting
4631
4793
  // over the NEXT segment's unrelated footage
4632
4794
  if (Math.abs(srcT - ck.st) > 2) continue
4633
- var ckAx = dx + (ck.x / (space.w || vw)) * dw
4634
- var ckAy = dy + (ck.y / (space.h || vh)) * dh
4795
+ var ckSp = ck.sp || space
4796
+ var ckAx = dx + (ck.x / (ckSp.w || vw)) * dw
4797
+ var ckAy = dy + (ck.y / (ckSp.h || vh)) * dh
4635
4798
  // press dip: smoothstep down around the real mousedown (80 ms lead \u2014
4636
4799
  // anticipation is what makes effects feel synced), hold through the real
4637
4800
  // down\u2192up span (drags dip long), easeOutBack rebound with a slight
@@ -4744,7 +4907,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4744
4907
  // The dot is the only thing cursorStyle.visible hides \u2014 the track still drives
4745
4908
  // cursor-follow zoom, and click effects draw above on their own switch.
4746
4909
  // Undefined reads as visible so pre-toggle docs are unchanged.
4747
- var cur = d.cursor || []
4910
+ var cur = (actM ? actM.cursor : d.cursor) || []
4748
4911
  if (cur.length && !(d.cursorStyle && d.cursorStyle.visible === false)) {
4749
4912
  var px = cur[0].x, py = cur[0].y
4750
4913
  for (var j = 0; j < cur.length; j++) { if (cur[j].t <= srcT) { px = cur[j].x; py = cur[j].y } }
@@ -4753,7 +4916,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4753
4916
  // Idle fade: a sparse SOURCE-time opacity curve baked by cursorIdleFade.
4754
4917
  // Linear between keys \u2014 opacity needs no easing, and the ramps are already
4755
4918
  // shaped by where the keys sit. Absent/empty = the cursor never dwells.
4756
- var cuA = 1, cuK = d.cursorFade
4919
+ var cuA = 1, cuK = actM ? actM.cursorFade : d.cursorFade
4757
4920
  if (cuK && cuK.length) {
4758
4921
  if (srcT <= cuK[0].t) cuA = cuK[0].a
4759
4922
  else if (srcT >= cuK[cuK.length - 1].t) cuA = cuK[cuK.length - 1].a
@@ -4786,7 +4949,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4786
4949
  // runtime; the card c2d under stubs). Redraw + re-upload only while the
4787
4950
  // bubble is active (a video \u2192 every frame), on resize, or once when it turns
4788
4951
  // off (to clear) \u2014 so a cam-less take never uploads the overlay after frame 1.
4789
- var camV = r.cam, camS = d.cam || {}
4952
+ var camV = amId ? null : r.cam, camS = d.cam || {}
4790
4953
  var camOn = !camS.window || (srcT >= camS.window.in && srcT <= camS.window.out)
4791
4954
  // Readiness is STICKY: readyState drops to HAVE_METADATA while a seek is in
4792
4955
  // flight, so gating each frame on it makes the bubble vanish on every scrub
@@ -5128,7 +5291,8 @@ function lowerToComposition(input) {
5128
5291
  segments: rated.map((seg) => ({
5129
5292
  in: round6(seg.in),
5130
5293
  out: round6(seg.out),
5131
- ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round6(seg.rate) } : {}
5294
+ ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round6(seg.rate) } : {},
5295
+ ...seg.media ? { media: seg.media } : {}
5132
5296
  })),
5133
5297
  frame: doc.frame,
5134
5298
  micGain: doc.micGain ?? 1,
@@ -5139,6 +5303,38 @@ function lowerToComposition(input) {
5139
5303
  })),
5140
5304
  cursorStyle: doc.cursor,
5141
5305
  cursorSpace: { w: doc.source.meta.width, h: doc.source.meta.height },
5306
+ // The other media (concat): each with its element's source, its crop
5307
+ // and its own cursor in its own capture space; absent = one media.
5308
+ ...doc.media && doc.media.length ? {
5309
+ media: doc.media.map((m) => {
5310
+ const sm = smoothCursor(m.cursor, {
5311
+ factor: doc.cursor.smoothing,
5312
+ clickSnap: fx.style !== "none" || fx.press
5313
+ });
5314
+ const fade = doc.cursor.hideWhenIdle !== false && doc.cursor.visible !== false ? cursorIdleFade(m.cursor, {
5315
+ space: { w: m.meta.width, h: m.meta.height },
5316
+ sourceDuration: (m.meta.durationMs || 0) / 1e3
5317
+ }) : [];
5318
+ return {
5319
+ id: m.id,
5320
+ src: m.videoKey,
5321
+ isImage: m.sourceKind === "image",
5322
+ crop: m.crop ?? null,
5323
+ hasAudio: !!m.meta.hasAudio,
5324
+ cursor: sm.map((p) => ({
5325
+ t: round6(p.t),
5326
+ x: round6(p.x),
5327
+ y: round6(p.y)
5328
+ })),
5329
+ cursorSpace: { w: m.meta.width, h: m.meta.height },
5330
+ // The card this media wears (its own placement, bar, corner,
5331
+ // shadow, border, fit and focus over the take's), resolved
5332
+ // here so ON_FRAME swaps one object and reads no document.
5333
+ frame: cardFields(mediaFrame(doc, m.id)),
5334
+ ...fade.length ? { cursorFade: fade } : {}
5335
+ };
5336
+ })
5337
+ } : {},
5142
5338
  ...cursorFade.length ? { cursorFade } : {},
5143
5339
  // Click effects: OUTPUT-anchored click records + resolved styling (named
5144
5340
  // intensities/colors become numbers HERE — ON_FRAME reads no registry).
@@ -5207,10 +5403,24 @@ function clickFxData(doc, rated) {
5207
5403
  const meta = doc.source.meta;
5208
5404
  const level = CLICK_FX_INTENSITY[fx.intensity];
5209
5405
  return {
5210
- clicks: on ? extractClicks(doc.source.cursor, rated, {
5211
- rects: fx.style === "highlight",
5212
- space: { w: meta.width, h: meta.height }
5213
- }) : [],
5406
+ clicks: on ? [
5407
+ ...extractClicks(doc.source.cursor, rated, {
5408
+ rects: fx.style === "highlight",
5409
+ space: { w: meta.width, h: meta.height }
5410
+ }),
5411
+ // The other media's clicks map through their own pieces and carry
5412
+ // their own capture space, so the ring lands where the press was.
5413
+ ...(doc.media ?? []).flatMap(
5414
+ (m) => extractClicks(m.cursor, rated, {
5415
+ rects: fx.style === "highlight",
5416
+ space: { w: m.meta.width, h: m.meta.height },
5417
+ media: m.id
5418
+ }).map((ck) => ({
5419
+ ...ck,
5420
+ sp: { w: m.meta.width, h: m.meta.height }
5421
+ }))
5422
+ )
5423
+ ].sort((a, b) => a.ot - b.ot) : [],
5214
5424
  clickFx: {
5215
5425
  style: fx.style,
5216
5426
  press: fx.press,
@@ -6835,7 +7045,7 @@ import {
6835
7045
  mapTime as mapTime2,
6836
7046
  removeSegment,
6837
7047
  segmentRate as segmentRate3,
6838
- sourceToTimeline as sourceToTimeline3,
7048
+ sourceToTimeline as sourceToTimeline2,
6839
7049
  splitBySpeed as splitBySpeed3,
6840
7050
  totalDuration as totalDuration6,
6841
7051
  trimSegment
@@ -6920,7 +7130,7 @@ var videoLane = {
6920
7130
  freezes.filter((f) => f.at < seg.out - 1e-9),
6921
7131
  index === 0 || !contiguous(segments, index)
6922
7132
  );
6923
- const anchorOut = sourceToTimeline3(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
7133
+ const anchorOut = sourceToTimeline2(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
6924
7134
  const sourceT = mapTime2(fullMap, anchorOut + (g.t - edgeOutNow));
6925
7135
  const next = trimSegment(
6926
7136
  segments,
@@ -7050,10 +7260,16 @@ var freezeLane = {
7050
7260
  const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
7051
7261
  if (!kept(srcT) || !clear(srcT)) return null;
7052
7262
  const id = nextFreezeId(doc);
7263
+ const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
7053
7264
  return (d) => {
7054
7265
  d.freeze = [
7055
7266
  ...d.freeze ?? [],
7056
- { id, at: srcT, seconds: FREEZE_DEFAULT_SECONDS }
7267
+ {
7268
+ id,
7269
+ at: srcT,
7270
+ seconds: FREEZE_DEFAULT_SECONDS,
7271
+ ...mediaHere ? { media: mediaHere } : {}
7272
+ }
7057
7273
  ].sort((a, b) => a.at - b.at);
7058
7274
  };
7059
7275
  }
@@ -7138,7 +7354,7 @@ var zoomLane = {
7138
7354
  items(doc) {
7139
7355
  const segments = ratedSegments(doc);
7140
7356
  return doc.zoom.flatMap((z) => {
7141
- const ext = spanOutputExtent(segments, z.in, z.out);
7357
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
7142
7358
  return ext === null ? [] : [
7143
7359
  {
7144
7360
  id: z.id,
@@ -7156,10 +7372,13 @@ var zoomLane = {
7156
7372
  const sourceDuration = anchorSourceDuration(doc);
7157
7373
  if (g.type === "create") {
7158
7374
  const srcT = mapTime2(rated, Math.max(0, g.t));
7159
- if (spans.some((z) => srcT >= z.in && srcT < z.out)) return null;
7160
- const next = spans.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
7161
- const limit = Math.min(sourceDuration, next ? next.in : sourceDuration);
7162
- const len = Math.min(spanDefaultLen(sourceDuration), limit - srcT);
7375
+ const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
7376
+ const here = spans.filter((z) => (z.media ?? "") === mediaHere);
7377
+ const mediaLen = mediaDuration(mediaSource(doc, mediaHere) ?? doc.source);
7378
+ if (here.some((z) => srcT >= z.in && srcT < z.out)) return null;
7379
+ const next = here.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
7380
+ const limit = Math.min(mediaLen, next ? next.in : mediaLen);
7381
+ const len = Math.min(spanDefaultLen(mediaLen), limit - srcT);
7163
7382
  if (len < ZOOM_SPAN_MIN * rateAt(rated, srcT)) return null;
7164
7383
  const id = nextZoomId(doc);
7165
7384
  const { cx, cy } = cursorFocusAt(doc, srcT);
@@ -7173,7 +7392,8 @@ var zoomLane = {
7173
7392
  level: DEFAULT_ZOOM_LEVEL,
7174
7393
  cx,
7175
7394
  cy,
7176
- source: "manual"
7395
+ source: "manual",
7396
+ ...mediaHere ? { media: mediaHere } : {}
7177
7397
  }
7178
7398
  ].sort((a, b) => a.in - b.in);
7179
7399
  };
@@ -7263,7 +7483,7 @@ var tiltLane = {
7263
7483
  items(doc) {
7264
7484
  const segments = ratedSegments(doc);
7265
7485
  return (doc.tilt ?? []).flatMap((z) => {
7266
- const ext = spanOutputExtent(segments, z.in, z.out);
7486
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
7267
7487
  return ext === null ? [] : [
7268
7488
  {
7269
7489
  id: z.id,
@@ -7281,10 +7501,13 @@ var tiltLane = {
7281
7501
  const sourceDuration = anchorSourceDuration(doc);
7282
7502
  if (g.type === "create") {
7283
7503
  const srcT = mapTime2(rated, Math.max(0, g.t));
7284
- if (spans.some((z) => srcT >= z.in && srcT < z.out)) return null;
7285
- const next = spans.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
7286
- const limit = Math.min(sourceDuration, next ? next.in : sourceDuration);
7287
- const len = Math.min(spanDefaultLen(sourceDuration), limit - srcT);
7504
+ const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
7505
+ const here = spans.filter((z) => (z.media ?? "") === mediaHere);
7506
+ const mediaLen = mediaDuration(mediaSource(doc, mediaHere) ?? doc.source);
7507
+ if (here.some((z) => srcT >= z.in && srcT < z.out)) return null;
7508
+ const next = here.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
7509
+ const limit = Math.min(mediaLen, next ? next.in : mediaLen);
7510
+ const len = Math.min(spanDefaultLen(mediaLen), limit - srcT);
7288
7511
  if (len < TILT_SPAN_MIN * rateAt(rated, srcT)) return null;
7289
7512
  const id = nextTiltId(doc);
7290
7513
  return (d) => {
@@ -7296,7 +7519,8 @@ var tiltLane = {
7296
7519
  out: round11(srcT + len),
7297
7520
  rx: DEFAULT_TILT_POSE.rx,
7298
7521
  ry: DEFAULT_TILT_POSE.ry,
7299
- source: "manual"
7522
+ source: "manual",
7523
+ ...mediaHere ? { media: mediaHere } : {}
7300
7524
  }
7301
7525
  ].sort((a, b) => a.in - b.in);
7302
7526
  };
@@ -7376,7 +7600,7 @@ var camMoveLane = {
7376
7600
  if (!doc.source.camKey) return [];
7377
7601
  const segments = ratedSegments(doc);
7378
7602
  return (doc.camMotion ?? []).flatMap((z) => {
7379
- const ext = spanOutputExtent(segments, z.in, z.out);
7603
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
7380
7604
  return ext === null ? [] : [
7381
7605
  {
7382
7606
  id: z.id,
@@ -7642,6 +7866,7 @@ var speedLane = {
7642
7866
  const len = Math.min(spanDefaultLen(sourceDuration), limit - srcT);
7643
7867
  if (len < SPEED_SPAN_MIN * DEFAULT_SPEED_RATE) return null;
7644
7868
  const id = nextSpeedId(doc);
7869
+ const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
7645
7870
  return (d) => {
7646
7871
  d.speed = [
7647
7872
  ...d.speed ?? [],
@@ -7650,7 +7875,8 @@ var speedLane = {
7650
7875
  in: round11(srcT),
7651
7876
  out: round11(srcT + len),
7652
7877
  rate: DEFAULT_SPEED_RATE,
7653
- source: "manual"
7878
+ source: "manual",
7879
+ ...mediaHere ? { media: mediaHere } : {}
7654
7880
  }
7655
7881
  ].sort((a, b) => a.in - b.in);
7656
7882
  };
@@ -7698,7 +7924,7 @@ var speedLane = {
7698
7924
  out: sp.out
7699
7925
  };
7700
7926
  } else {
7701
- const startOut = sourceToTimeline3(base, sp.in) ?? sp.in;
7927
+ const startOut = sourceToTimeline2(base, sp.in) ?? sp.in;
7702
7928
  const sourceT = sp.in + sp.rate * Math.max(0, g.t - startOut);
7703
7929
  next = {
7704
7930
  in: sp.in,
@@ -8245,6 +8471,7 @@ export {
8245
8471
  LOOK_KINDS,
8246
8472
  MEDIA_ENTER_KINDS,
8247
8473
  MEDIA_EXIT_KINDS,
8474
+ MEDIA_FRAME_KEYS,
8248
8475
  MINIMAL_BAR_THEMES,
8249
8476
  MOTION_DELTA,
8250
8477
  MOTION_EASE,
@@ -8261,6 +8488,7 @@ export {
8261
8488
  OVERLAY_Z,
8262
8489
  PLATE_GROUND,
8263
8490
  PLAYBACK_ACTIVITY,
8491
+ PRIMARY_MEDIA,
8264
8492
  PROGRAM_RETIME,
8265
8493
  REJECT_OVERLAP,
8266
8494
  REST_TILT_ID,
@@ -8323,6 +8551,7 @@ export {
8323
8551
  camTrackFromDoc,
8324
8552
  cardEnter,
8325
8553
  cardExit,
8554
+ cardFields,
8326
8555
  cardInset,
8327
8556
  cardPointToScreen,
8328
8557
  cardPoseTrack,
@@ -8348,6 +8577,7 @@ export {
8348
8577
  destinationsForChannel,
8349
8578
  docCardLayout,
8350
8579
  docFreezes,
8580
+ docMediaList,
8351
8581
  docOutputDuration,
8352
8582
  docRestTime,
8353
8583
  docToCropSpace,
@@ -8393,12 +8623,18 @@ export {
8393
8623
  lowerProgramDoc,
8394
8624
  lowerStudioDoc,
8395
8625
  lowerToComposition,
8626
+ mediaAtOutput,
8627
+ mediaDuration,
8628
+ mediaFrame,
8629
+ mediaKey,
8630
+ mediaSource,
8396
8631
  micLane,
8397
8632
  migrateHostedDoc,
8398
8633
  migrateMotion,
8399
8634
  momentsFromDoc,
8400
8635
  motionTrack,
8401
8636
  musicBedClip,
8637
+ nextMediaId,
8402
8638
  normalizeCaptureSpace,
8403
8639
  objectMotionBase,
8404
8640
  objectMotionPoseAt,
@@ -8441,6 +8677,7 @@ export {
8441
8677
  resolveText3dAsset,
8442
8678
  resolveZoomStyle,
8443
8679
  restLeanOf,
8680
+ sameMedia,
8444
8681
  sceneChanges,
8445
8682
  scrollRuns,
8446
8683
  setSpeedInRange,