@vosjs/studio-core 0.13.0 → 0.14.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
@@ -511,6 +511,44 @@ function lastLayerEnd(doc) {
511
511
 
512
512
  // src/lower/motion.ts
513
513
  import { splitBySpeed, totalDuration } from "@vosjs/timeline";
514
+
515
+ // src/media.ts
516
+ var PRIMARY_MEDIA = "";
517
+ var mediaKey = (id) => id ?? PRIMARY_MEDIA;
518
+ var sameMedia = (a, b) => mediaKey(a) === mediaKey(b);
519
+ function docMediaList(doc) {
520
+ return [
521
+ { id: PRIMARY_MEDIA, source: doc.source },
522
+ ...(doc.media ?? []).map((m) => ({ id: m.id, source: m }))
523
+ ];
524
+ }
525
+ function mediaSource(doc, id) {
526
+ const key = mediaKey(id);
527
+ if (key === PRIMARY_MEDIA) return doc.source;
528
+ return (doc.media ?? []).find((m) => m.id === key) ?? null;
529
+ }
530
+ function mediaDuration(source) {
531
+ return (source.meta.durationMs || 0) / 1e3;
532
+ }
533
+ function mediaAtOutput(rated, t) {
534
+ let acc = 0;
535
+ let last = null;
536
+ for (const p of rated) {
537
+ const len = Math.max(0, p.out - p.in) / (p.rate && p.rate > 0 ? p.rate : 1);
538
+ if (t < acc + len) return mediaKey(p.media);
539
+ acc += len;
540
+ last = p;
541
+ }
542
+ return last ? mediaKey(last.media) : PRIMARY_MEDIA;
543
+ }
544
+ function nextMediaId(doc) {
545
+ const taken = new Set((doc.media ?? []).map((m) => m.id));
546
+ let n = 1;
547
+ while (taken.has(`m${n}`)) n++;
548
+ return `m${n}`;
549
+ }
550
+
551
+ // src/lower/motion.ts
514
552
  function docRestTime(doc) {
515
553
  if (doc.endCard) return null;
516
554
  const freezes = docFreezes(doc);
@@ -575,17 +613,19 @@ function exitSeconds(e) {
575
613
  }
576
614
  function placeFreezes(rated, freezes) {
577
615
  const marks = /* @__PURE__ */ new Map();
578
- const pending = [...freezes].filter((f) => f.seconds > 0).sort((a, b) => a.at - b.at);
616
+ const placed = /* @__PURE__ */ new Set();
579
617
  const pieces = [];
580
618
  let acc = 0;
581
- const freezePiece = (f) => {
619
+ const freezePiece = (f, media) => {
582
620
  const at = Math.max(HOLD_SOURCE_SPAN, f.at);
583
621
  marks.set(f.id, { t: acc, duration: f.seconds });
584
622
  pieces.push({
585
623
  in: at - HOLD_SOURCE_SPAN,
586
624
  out: at,
587
- rate: HOLD_SOURCE_SPAN / f.seconds
625
+ rate: HOLD_SOURCE_SPAN / f.seconds,
626
+ ...media !== void 0 ? { media } : {}
588
627
  });
628
+ placed.add(f.id);
589
629
  acc += f.seconds;
590
630
  };
591
631
  const push2 = (p) => {
@@ -595,32 +635,23 @@ function placeFreezes(rated, freezes) {
595
635
  for (let i = 0; i < rated.length; i++) {
596
636
  let piece = rated[i];
597
637
  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
- }
638
+ const continues = !!next && sameMedia(next.media, piece.media) && Math.abs(next.in - piece.out) < 1e-9 && next.out > piece.out;
639
+ const mine = freezes.filter(
640
+ (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)
641
+ ).sort((a, b) => a.at - b.at);
642
+ for (const f of mine) {
605
643
  if (f.at <= piece.in + 1e-9) {
606
- pending.shift();
607
- freezePiece(f);
608
- continue;
609
- }
610
- if (f.at < piece.out - 1e-9) {
644
+ freezePiece(f, piece.media);
645
+ } else if (f.at < piece.out - 1e-9) {
611
646
  push2({ ...piece, out: f.at });
612
- pending.shift();
613
- freezePiece(f);
647
+ freezePiece(f, piece.media);
614
648
  piece = { ...piece, in: f.at };
615
- continue;
649
+ } else {
650
+ const media = piece.media;
651
+ push2(piece);
652
+ piece = null;
653
+ freezePiece(f, media);
616
654
  }
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
655
  }
625
656
  if (piece) push2(piece);
626
657
  }
@@ -1570,7 +1601,7 @@ import {
1570
1601
  sample,
1571
1602
  segmentRate as segmentRate2,
1572
1603
  sortKeyframes as sortKeyframes2,
1573
- sourceToTimeline as sourceToTimeline2,
1604
+ sourceToTimeline,
1574
1605
  splitBySpeed as splitBySpeed2,
1575
1606
  totalDuration as totalDuration2
1576
1607
  } from "@vosjs/timeline";
@@ -3242,7 +3273,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
3242
3273
  }`;
3243
3274
 
3244
3275
  // src/lower/extractClicks.ts
3245
- import { segmentRate, sourceToTimeline } from "@vosjs/timeline";
3276
+ import { segmentRate } from "@vosjs/timeline";
3246
3277
  var CLICK_FX_PRE = 0.06;
3247
3278
  var CLICK_RIPPLE_DUR = 0.45;
3248
3279
  var CLICK_PULSE_DUR = 0.35;
@@ -3251,14 +3282,27 @@ var CLICK_SYNTH_RELEASE = 0.12;
3251
3282
  var CLICK_PAIR_MAX = 10;
3252
3283
  var CLICK_RECT_MAX_FRAC = 0.35;
3253
3284
  var RECT_CONTAIN_MARGIN = 8;
3254
- function keptOutputEnd(segments, sIn, sOut) {
3285
+ function outputAt(segments, sourceT, media) {
3286
+ if (!segments.length) return sourceT;
3287
+ let acc = 0;
3288
+ for (const s of segments) {
3289
+ const rate = segmentRate(s);
3290
+ if (sameMedia(s.media, media) && sourceT >= s.in && sourceT <= s.out)
3291
+ return acc + (sourceT - s.in) / rate;
3292
+ acc += Math.max(0, s.out - s.in) / rate;
3293
+ }
3294
+ return null;
3295
+ }
3296
+ function keptOutputEnd(segments, sIn, sOut, media) {
3255
3297
  let acc = 0;
3256
3298
  let end = null;
3257
3299
  for (const p of segments) {
3258
3300
  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;
3301
+ if (sameMedia(p.media, media)) {
3302
+ const ovIn = Math.max(sIn, p.in);
3303
+ const ovOut = Math.min(sOut, p.out);
3304
+ if (ovOut > ovIn) end = acc + (ovOut - p.in) / rate;
3305
+ }
3262
3306
  acc += Math.max(0, p.out - p.in) / rate;
3263
3307
  }
3264
3308
  return end;
@@ -3290,10 +3334,10 @@ function extractClicks(track, segments, opts) {
3290
3334
  break;
3291
3335
  }
3292
3336
  }
3293
- const ot = sourceToTimeline(segments, st);
3337
+ const ot = outputAt(segments, st, opts.media);
3294
3338
  if (ot === null) continue;
3295
3339
  const upSrc = st + Math.max(pressLen, 0.02);
3296
- const up = keptOutputEnd(segments, st, upSrc) ?? ot + CLICK_SYNTH_RELEASE;
3340
+ const up = keptOutputEnd(segments, st, upSrc, opts.media) ?? ot + CLICK_SYNTH_RELEASE;
3297
3341
  const click = {
3298
3342
  ot: round5(ot),
3299
3343
  up: round5(Math.max(up, ot + 0.02)),
@@ -3346,19 +3390,32 @@ var PROGRAM_DURATION = 1;
3346
3390
  function ratedSegments(doc) {
3347
3391
  const segs = isRecordingDoc(doc) ? doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }] : [{ in: 0, out: programDuration(doc) }];
3348
3392
  const freezes = isRecordingDoc(doc) ? docFreezes(doc) : [];
3349
- return withFreezes(splitBySpeed2(segs, doc.speed ?? []), freezes);
3393
+ const speeds = doc.speed ?? [];
3394
+ const pieces = segs.flatMap(
3395
+ (seg) => splitBySpeed2(
3396
+ [seg],
3397
+ speeds.filter(
3398
+ (sp) => sameMedia(sp.media, seg.media)
3399
+ )
3400
+ )
3401
+ );
3402
+ return withFreezes(pieces, freezes);
3350
3403
  }
3351
3404
  function durationSec(doc, rated) {
3352
3405
  const trimmed = totalDuration2(rated);
3353
3406
  return trimmed > 0 ? trimmed : doc.source.meta.durationMs / 1e3;
3354
3407
  }
3355
- function spanOutputExtent(segments, sIn, sOut) {
3408
+ function spanOutputExtent(segments, sIn, sOut, media) {
3356
3409
  let acc = 0;
3357
3410
  let start = null;
3358
3411
  let end = null;
3359
3412
  for (const p of segments) {
3360
3413
  const rate = segmentRate2(p);
3361
3414
  const len = Math.max(0, p.out - p.in) / rate;
3415
+ if (!sameMedia(p.media, media)) {
3416
+ acc += len;
3417
+ continue;
3418
+ }
3362
3419
  const ovIn = Math.max(sIn, p.in);
3363
3420
  const ovOut = Math.min(sOut, p.out);
3364
3421
  if (ovOut > ovIn) {
@@ -3389,7 +3446,7 @@ function trackEmitter() {
3389
3446
  function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE]) {
3390
3447
  const panEase = style.panEase;
3391
3448
  const mapped = [...zoom].sort((a, b) => a.in - b.in).flatMap((z) => {
3392
- const ext = spanOutputExtent(segments, z.in, z.out);
3449
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
3393
3450
  return ext ? [{ z, tIn: ext.start, tOut: ext.end }] : [];
3394
3451
  });
3395
3452
  const { keyframes, push: push2 } = trackEmitter();
@@ -3408,7 +3465,7 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
3408
3465
  push2(start + style.rampIn * m, entry, spanEase(z.ease, style.ease));
3409
3466
  }
3410
3467
  for (const e of z.followEvents ?? []) {
3411
- const eOut = sourceToTimeline2(segments, e.t);
3468
+ const eOut = sourceToTimeline(segments, e.t);
3412
3469
  if (eOut === null || eOut <= tIn || eOut >= tOut) continue;
3413
3470
  const next2 = [cur[0], e.cx, e.cy];
3414
3471
  push2(eOut, cur, "none");
@@ -3451,7 +3508,7 @@ function tiltTrackFromDoc(tilt, segments, motion = {}) {
3451
3508
  const panEase = TILT_PAN_EASE;
3452
3509
  const rest = [0, 0];
3453
3510
  const mapped = [...tilt].sort((a, b) => a.in - b.in).flatMap((z) => {
3454
- const ext = spanOutputExtent(segments, z.in, z.out);
3511
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
3455
3512
  return ext ? [{ z, tIn: ext.start, tOut: ext.end }] : [];
3456
3513
  });
3457
3514
  const { keyframes, push: push2 } = trackEmitter();
@@ -3494,7 +3551,7 @@ function camTrackFromDoc(cam, spans, segments, W, H = 1080) {
3494
3551
  ];
3495
3552
  const panEase = CAM_PAN_EASE;
3496
3553
  const mapped = [...spans].sort((a, b) => a.in - b.in).flatMap((z) => {
3497
- const ext = spanOutputExtent(segments, z.in, z.out);
3554
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
3498
3555
  return ext ? [{ z, tIn: ext.start, tOut: ext.end }] : [];
3499
3556
  });
3500
3557
  const { keyframes, push: push2 } = trackEmitter();
@@ -3870,6 +3927,26 @@ var SETUP = `async (ctx) => {
3870
3927
  // the same sync as the screen video (source-anchored, so element time == the
3871
3928
  // video's). Unmuted \u2014 syncVid's autoplay net re-mutes on policy rejection.
3872
3929
  const mic = ctx.data.micSrc ? await load(ctx.data.micSrc, false) : null
3930
+ // The take's OTHER media (concat): one element each, decoded the way the
3931
+ // primary is; ON_FRAME drives the one under the playhead and pauses the
3932
+ // rest. Absent = one media, byte-identical.
3933
+ const mediaEls = {}
3934
+ for (const m of ctx.data.media || []) {
3935
+ try {
3936
+ const el = m.isImage ? await loadImage(m.src) : await load(m.src, !m.hasAudio)
3937
+ if (!m.isImage && ctx.data.videoDecodeMode === 'webcodecs') {
3938
+ try {
3939
+ const wp = await makeWcProvider(m.src)
3940
+ if (wp && (!el.videoWidth || (wp.width === el.videoWidth && wp.height === el.videoHeight))) el.__voilaWc = wp
3941
+ } catch (e) {
3942
+ console.warn('[voila] webcodecs provider unavailable for a media, seeks stay html5', e)
3943
+ }
3944
+ }
3945
+ mediaEls[m.id] = el
3946
+ } catch (e) {
3947
+ console.warn('[voila] a media failed to load', m.id, e)
3948
+ }
3949
+ }
3873
3950
  // Background media (frame.backgroundMedia): warm-load so the first paint is
3874
3951
  // complete. FAIL-OPEN \u2014 a bad key degrades to the CSS fill underneath, never
3875
3952
  // a dead LOAD (unlike the recording, which is the comp's reason to exist).
@@ -3898,9 +3975,27 @@ var SETUP = `async (ctx) => {
3898
3975
  const d = ctx.data
3899
3976
  const TL = globalThis.__vosTimeline
3900
3977
  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)
3978
+ // The media under the playhead (concat): step its element; the
3979
+ // primary's sidecars belong to the primary's moments only.
3980
+ let am = ''
3981
+ {
3982
+ let acc = 0
3983
+ const sg = d.segments || []
3984
+ for (let i = 0; i < sg.length; i++) {
3985
+ const s0 = sg[i]
3986
+ const ln = Math.max(0, s0.out - s0.in) / (s0.rate || 1)
3987
+ if (t < acc + ln || i === sg.length - 1) { am = s0.media || ''; break }
3988
+ acc += ln
3989
+ }
3990
+ }
3991
+ const amEl = am ? mediaEls[am] : null
3992
+ if (amEl) {
3993
+ if (amEl.play) stepPaused(amEl, srcT)
3994
+ } else {
3995
+ if (video.play) stepPaused(video, srcT)
3996
+ if (cam) stepPaused(cam, srcT)
3997
+ if (mic) stepPaused(mic, srcT)
3998
+ }
3904
3999
  const bg2 = d.frame && d.frame.backgroundMedia
3905
4000
  if (bg2 && bg2.key && bg2.kind !== 'image') {
3906
4001
  const bgEl2 = cache.get(bg2.key)
@@ -3910,7 +4005,7 @@ var SETUP = `async (ctx) => {
3910
4005
  }
3911
4006
  }
3912
4007
  })
3913
- return { video: video, cam: cam, mic: mic }
4008
+ return { video: video, cam: cam, mic: mic, media: mediaEls }
3914
4009
  }`;
3915
4010
  var CREATE_CONTENT = `(ctx, setupData) => {
3916
4011
  const THREE = ctx.THREE
@@ -3961,6 +4056,7 @@ var CREATE_CONTENT = `(ctx, setupData) => {
3961
4056
  // { c2d, canvas, texture }) drive the card and let bg/overlay fall back.
3962
4057
  canvas: card.canvas, c2d: card.c2d, texture: card.texture,
3963
4058
  video: setupData.video, cam: setupData.cam, mic: setupData.mic,
4059
+ media: setupData.media,
3964
4060
  },
3965
4061
  }
3966
4062
  }`;
@@ -4071,6 +4167,26 @@ var ON_FRAME = `(ctx, content, dt) => {
4071
4167
  // Output-timeline seconds (engine-fed master clock) \u2192 source seconds on screen.
4072
4168
  var t = ctx.time || 0
4073
4169
  var srcT = TL.mapTime(d.segments || [], t)
4170
+ // The media under the playhead (concat): its element, crop, cursor and
4171
+ // space stand in for the primary's below, and every other media's
4172
+ // element rests. An absent media list is the primary alone.
4173
+ var amId = ''
4174
+ { var amAcc = 0, amSg = d.segments || []
4175
+ for (var amI = 0; amI < amSg.length; amI++) {
4176
+ var amS = amSg[amI], amLn = Math.max(0, amS.out - amS.in) / (amS.rate || 1)
4177
+ if (t < amAcc + amLn || amI === amSg.length - 1) { amId = amS.media || ''; break }
4178
+ amAcc += amLn
4179
+ } }
4180
+ var actM = null
4181
+ if (amId && d.media) for (var amJ = 0; amJ < d.media.length; amJ++) if (d.media[amJ].id === amId) actM = d.media[amJ]
4182
+ if (actM && r.media && r.media[amId]) video = r.media[amId]
4183
+ else amId = ''
4184
+ 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() }
4185
+ if (amId) {
4186
+ if (r.video && r.video.pause && r.video.paused === false) r.video.pause()
4187
+ if (r.cam && r.cam.pause && r.cam.paused === false) r.cam.pause()
4188
+ if (r.mic && r.mic.pause && r.mic.paused === false) r.mic.pause()
4189
+ }
4074
4190
  var s = H / 1080 // scale design-px controls to comp px
4075
4191
 
4076
4192
  // Play natively while playing (smooth); seek precisely otherwise (paused, scrubbing,
@@ -4152,8 +4268,8 @@ var ON_FRAME = `(ctx, content, dt) => {
4152
4268
  } catch (e) {}
4153
4269
  }
4154
4270
  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)
4271
+ if (r.cam && !amId) syncVid(r.cam)
4272
+ if (r.mic && !amId) syncVid(r.mic)
4157
4273
  // Gain routing (live via SET_DATA). With a mic sidecar (AT split) the
4158
4274
  // recording <video> carries SYSTEM audio \u2014 its volume is the system fader \u2014
4159
4275
  // and the sidecar element is the voice (micGain). Legacy takes have one
@@ -4379,7 +4495,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4379
4495
  // Window takes carry a viewport crop (drawImage source rect, capture px) that
4380
4496
  // removes the real browser chrome \u2014 the card's source dims are then the CROP
4381
4497
  // dims (meta/cursor were rewritten into crop space at doc build).
4382
- var crp = d.crop || null
4498
+ var crp = actM ? (actM.crop || null) : (d.crop || null)
4383
4499
  var vw = crp ? crp.w : (video.videoWidth || video.naturalWidth || 16)
4384
4500
  var vh = crp ? crp.h : (video.videoHeight || video.naturalHeight || 9)
4385
4501
  // Card-chrome scale: everything that belongs to the CARD (browser bar +
@@ -4472,13 +4588,16 @@ var ON_FRAME = `(ctx, content, dt) => {
4472
4588
  // corners, drawn by the clip alone, hid theirs inside the arc.
4473
4589
  var shD = ceZs * (cardW + cardH) + 2 * (W + H)
4474
4590
  var shX = cardX - shD / ceZs
4475
- // The card shadow is LAYERED: three shadows whose blur and offset grow
4476
- // while each stays at a low alpha (shadow is the strength they share),
4477
- // so the card reads as lifted a little off the ground. One wide layer at
4478
- // the full strength was the old paint, and at the default 0.4 it read as
4479
- // a dark pool under the card.
4591
+ // The card shadow is LAYERED: four shadows whose blur and offset grow
4592
+ // while each stays at a low alpha (shadow is the strength they share):
4593
+ // a hairline that defines the edge, two that ground it, and a WIDE,
4594
+ // faint ambient bloom (120 design px of blur, dropped well below the
4595
+ // card) that is what lets a large card read as lifted off a light
4596
+ // ground instead of outlined on it. One wide layer at the full strength
4597
+ // was the old paint, and at the default 0.4 it read as a dark pool
4598
+ // under the card; three tight layers read as a rim.
4480
4599
  if (shadow > 0) {
4481
- var shL = [[3, 1, 0.2], [14, 6, 0.22], [48, 22, 0.3]]
4600
+ var shL = [[2, 1, 0.1], [12, 4, 0.1], [40, 16, 0.14], [120, 44, 0.3]]
4482
4601
  for (var shI = 0; shI < shL.length; shI++) {
4483
4602
  c.save()
4484
4603
  c.shadowColor = 'rgba(' + shRgb + ',' + +(shadow * shL[shI][2]).toFixed(3) + ')'
@@ -4595,7 +4714,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4595
4714
  // card. Contain never needs it (the video rect IS the card).
4596
4715
  if (fitCover) { c.save(); rr(cardX, cardY, cardW, cardH, radius); c.clip() }
4597
4716
  // cursor coordinate space + drawn radius (shared by click effects + the dot)
4598
- var space = d.cursorSpace || { w: vw, h: vh }
4717
+ var space = (actM ? actM.cursorSpace : d.cursorSpace) || { w: vw, h: vh }
4599
4718
  var curSize = ((d.cursorStyle && d.cursorStyle.size) || 24) * s2 * 0.5
4600
4719
 
4601
4720
  // click effects \u2014 pure f(t): d.clicks are
@@ -4627,8 +4746,9 @@ var ON_FRAME = `(ctx, content, dt) => {
4627
4746
  // click's source moment, or an effect near a cut would keep painting
4628
4747
  // over the NEXT segment's unrelated footage
4629
4748
  if (Math.abs(srcT - ck.st) > 2) continue
4630
- var ckAx = dx + (ck.x / (space.w || vw)) * dw
4631
- var ckAy = dy + (ck.y / (space.h || vh)) * dh
4749
+ var ckSp = ck.sp || space
4750
+ var ckAx = dx + (ck.x / (ckSp.w || vw)) * dw
4751
+ var ckAy = dy + (ck.y / (ckSp.h || vh)) * dh
4632
4752
  // press dip: smoothstep down around the real mousedown (80 ms lead \u2014
4633
4753
  // anticipation is what makes effects feel synced), hold through the real
4634
4754
  // down\u2192up span (drags dip long), easeOutBack rebound with a slight
@@ -4741,7 +4861,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4741
4861
  // The dot is the only thing cursorStyle.visible hides \u2014 the track still drives
4742
4862
  // cursor-follow zoom, and click effects draw above on their own switch.
4743
4863
  // Undefined reads as visible so pre-toggle docs are unchanged.
4744
- var cur = d.cursor || []
4864
+ var cur = (actM ? actM.cursor : d.cursor) || []
4745
4865
  if (cur.length && !(d.cursorStyle && d.cursorStyle.visible === false)) {
4746
4866
  var px = cur[0].x, py = cur[0].y
4747
4867
  for (var j = 0; j < cur.length; j++) { if (cur[j].t <= srcT) { px = cur[j].x; py = cur[j].y } }
@@ -4750,7 +4870,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4750
4870
  // Idle fade: a sparse SOURCE-time opacity curve baked by cursorIdleFade.
4751
4871
  // Linear between keys \u2014 opacity needs no easing, and the ramps are already
4752
4872
  // shaped by where the keys sit. Absent/empty = the cursor never dwells.
4753
- var cuA = 1, cuK = d.cursorFade
4873
+ var cuA = 1, cuK = actM ? actM.cursorFade : d.cursorFade
4754
4874
  if (cuK && cuK.length) {
4755
4875
  if (srcT <= cuK[0].t) cuA = cuK[0].a
4756
4876
  else if (srcT >= cuK[cuK.length - 1].t) cuA = cuK[cuK.length - 1].a
@@ -4783,7 +4903,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4783
4903
  // runtime; the card c2d under stubs). Redraw + re-upload only while the
4784
4904
  // bubble is active (a video \u2192 every frame), on resize, or once when it turns
4785
4905
  // off (to clear) \u2014 so a cam-less take never uploads the overlay after frame 1.
4786
- var camV = r.cam, camS = d.cam || {}
4906
+ var camV = amId ? null : r.cam, camS = d.cam || {}
4787
4907
  var camOn = !camS.window || (srcT >= camS.window.in && srcT <= camS.window.out)
4788
4908
  // Readiness is STICKY: readyState drops to HAVE_METADATA while a seek is in
4789
4909
  // flight, so gating each frame on it makes the bubble vanish on every scrub
@@ -5125,7 +5245,8 @@ function lowerToComposition(input) {
5125
5245
  segments: rated.map((seg) => ({
5126
5246
  in: round6(seg.in),
5127
5247
  out: round6(seg.out),
5128
- ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round6(seg.rate) } : {}
5248
+ ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round6(seg.rate) } : {},
5249
+ ...seg.media ? { media: seg.media } : {}
5129
5250
  })),
5130
5251
  frame: doc.frame,
5131
5252
  micGain: doc.micGain ?? 1,
@@ -5136,6 +5257,34 @@ function lowerToComposition(input) {
5136
5257
  })),
5137
5258
  cursorStyle: doc.cursor,
5138
5259
  cursorSpace: { w: doc.source.meta.width, h: doc.source.meta.height },
5260
+ // The other media (concat): each with its element's source, its crop
5261
+ // and its own cursor in its own capture space; absent = one media.
5262
+ ...doc.media && doc.media.length ? {
5263
+ media: doc.media.map((m) => {
5264
+ const sm = smoothCursor(m.cursor, {
5265
+ factor: doc.cursor.smoothing,
5266
+ clickSnap: fx.style !== "none" || fx.press
5267
+ });
5268
+ const fade = doc.cursor.hideWhenIdle !== false && doc.cursor.visible !== false ? cursorIdleFade(m.cursor, {
5269
+ space: { w: m.meta.width, h: m.meta.height },
5270
+ sourceDuration: (m.meta.durationMs || 0) / 1e3
5271
+ }) : [];
5272
+ return {
5273
+ id: m.id,
5274
+ src: m.videoKey,
5275
+ isImage: m.sourceKind === "image",
5276
+ crop: m.crop ?? null,
5277
+ hasAudio: !!m.meta.hasAudio,
5278
+ cursor: sm.map((p) => ({
5279
+ t: round6(p.t),
5280
+ x: round6(p.x),
5281
+ y: round6(p.y)
5282
+ })),
5283
+ cursorSpace: { w: m.meta.width, h: m.meta.height },
5284
+ ...fade.length ? { cursorFade: fade } : {}
5285
+ };
5286
+ })
5287
+ } : {},
5139
5288
  ...cursorFade.length ? { cursorFade } : {},
5140
5289
  // Click effects: OUTPUT-anchored click records + resolved styling (named
5141
5290
  // intensities/colors become numbers HERE — ON_FRAME reads no registry).
@@ -5204,10 +5353,24 @@ function clickFxData(doc, rated) {
5204
5353
  const meta = doc.source.meta;
5205
5354
  const level = CLICK_FX_INTENSITY[fx.intensity];
5206
5355
  return {
5207
- clicks: on ? extractClicks(doc.source.cursor, rated, {
5208
- rects: fx.style === "highlight",
5209
- space: { w: meta.width, h: meta.height }
5210
- }) : [],
5356
+ clicks: on ? [
5357
+ ...extractClicks(doc.source.cursor, rated, {
5358
+ rects: fx.style === "highlight",
5359
+ space: { w: meta.width, h: meta.height }
5360
+ }),
5361
+ // The other media's clicks map through their own pieces and carry
5362
+ // their own capture space, so the ring lands where the press was.
5363
+ ...(doc.media ?? []).flatMap(
5364
+ (m) => extractClicks(m.cursor, rated, {
5365
+ rects: fx.style === "highlight",
5366
+ space: { w: m.meta.width, h: m.meta.height },
5367
+ media: m.id
5368
+ }).map((ck) => ({
5369
+ ...ck,
5370
+ sp: { w: m.meta.width, h: m.meta.height }
5371
+ }))
5372
+ )
5373
+ ].sort((a, b) => a.ot - b.ot) : [],
5211
5374
  clickFx: {
5212
5375
  style: fx.style,
5213
5376
  press: fx.press,
@@ -6832,7 +6995,7 @@ import {
6832
6995
  mapTime as mapTime2,
6833
6996
  removeSegment,
6834
6997
  segmentRate as segmentRate3,
6835
- sourceToTimeline as sourceToTimeline3,
6998
+ sourceToTimeline as sourceToTimeline2,
6836
6999
  splitBySpeed as splitBySpeed3,
6837
7000
  totalDuration as totalDuration6,
6838
7001
  trimSegment
@@ -6917,7 +7080,7 @@ var videoLane = {
6917
7080
  freezes.filter((f) => f.at < seg.out - 1e-9),
6918
7081
  index === 0 || !contiguous(segments, index)
6919
7082
  );
6920
- const anchorOut = sourceToTimeline3(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
7083
+ const anchorOut = sourceToTimeline2(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
6921
7084
  const sourceT = mapTime2(fullMap, anchorOut + (g.t - edgeOutNow));
6922
7085
  const next = trimSegment(
6923
7086
  segments,
@@ -7047,10 +7210,16 @@ var freezeLane = {
7047
7210
  const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
7048
7211
  if (!kept(srcT) || !clear(srcT)) return null;
7049
7212
  const id = nextFreezeId(doc);
7213
+ const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
7050
7214
  return (d) => {
7051
7215
  d.freeze = [
7052
7216
  ...d.freeze ?? [],
7053
- { id, at: srcT, seconds: FREEZE_DEFAULT_SECONDS }
7217
+ {
7218
+ id,
7219
+ at: srcT,
7220
+ seconds: FREEZE_DEFAULT_SECONDS,
7221
+ ...mediaHere ? { media: mediaHere } : {}
7222
+ }
7054
7223
  ].sort((a, b) => a.at - b.at);
7055
7224
  };
7056
7225
  }
@@ -7135,7 +7304,7 @@ var zoomLane = {
7135
7304
  items(doc) {
7136
7305
  const segments = ratedSegments(doc);
7137
7306
  return doc.zoom.flatMap((z) => {
7138
- const ext = spanOutputExtent(segments, z.in, z.out);
7307
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
7139
7308
  return ext === null ? [] : [
7140
7309
  {
7141
7310
  id: z.id,
@@ -7153,10 +7322,13 @@ var zoomLane = {
7153
7322
  const sourceDuration = anchorSourceDuration(doc);
7154
7323
  if (g.type === "create") {
7155
7324
  const srcT = mapTime2(rated, Math.max(0, g.t));
7156
- if (spans.some((z) => srcT >= z.in && srcT < z.out)) return null;
7157
- const next = spans.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
7158
- const limit = Math.min(sourceDuration, next ? next.in : sourceDuration);
7159
- const len = Math.min(spanDefaultLen(sourceDuration), limit - srcT);
7325
+ const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
7326
+ const here = spans.filter((z) => (z.media ?? "") === mediaHere);
7327
+ const mediaLen = mediaDuration(mediaSource(doc, mediaHere) ?? doc.source);
7328
+ if (here.some((z) => srcT >= z.in && srcT < z.out)) return null;
7329
+ const next = here.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
7330
+ const limit = Math.min(mediaLen, next ? next.in : mediaLen);
7331
+ const len = Math.min(spanDefaultLen(mediaLen), limit - srcT);
7160
7332
  if (len < ZOOM_SPAN_MIN * rateAt(rated, srcT)) return null;
7161
7333
  const id = nextZoomId(doc);
7162
7334
  const { cx, cy } = cursorFocusAt(doc, srcT);
@@ -7170,7 +7342,8 @@ var zoomLane = {
7170
7342
  level: DEFAULT_ZOOM_LEVEL,
7171
7343
  cx,
7172
7344
  cy,
7173
- source: "manual"
7345
+ source: "manual",
7346
+ ...mediaHere ? { media: mediaHere } : {}
7174
7347
  }
7175
7348
  ].sort((a, b) => a.in - b.in);
7176
7349
  };
@@ -7260,7 +7433,7 @@ var tiltLane = {
7260
7433
  items(doc) {
7261
7434
  const segments = ratedSegments(doc);
7262
7435
  return (doc.tilt ?? []).flatMap((z) => {
7263
- const ext = spanOutputExtent(segments, z.in, z.out);
7436
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
7264
7437
  return ext === null ? [] : [
7265
7438
  {
7266
7439
  id: z.id,
@@ -7278,10 +7451,13 @@ var tiltLane = {
7278
7451
  const sourceDuration = anchorSourceDuration(doc);
7279
7452
  if (g.type === "create") {
7280
7453
  const srcT = mapTime2(rated, Math.max(0, g.t));
7281
- if (spans.some((z) => srcT >= z.in && srcT < z.out)) return null;
7282
- const next = spans.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
7283
- const limit = Math.min(sourceDuration, next ? next.in : sourceDuration);
7284
- const len = Math.min(spanDefaultLen(sourceDuration), limit - srcT);
7454
+ const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
7455
+ const here = spans.filter((z) => (z.media ?? "") === mediaHere);
7456
+ const mediaLen = mediaDuration(mediaSource(doc, mediaHere) ?? doc.source);
7457
+ if (here.some((z) => srcT >= z.in && srcT < z.out)) return null;
7458
+ const next = here.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
7459
+ const limit = Math.min(mediaLen, next ? next.in : mediaLen);
7460
+ const len = Math.min(spanDefaultLen(mediaLen), limit - srcT);
7285
7461
  if (len < TILT_SPAN_MIN * rateAt(rated, srcT)) return null;
7286
7462
  const id = nextTiltId(doc);
7287
7463
  return (d) => {
@@ -7293,7 +7469,8 @@ var tiltLane = {
7293
7469
  out: round11(srcT + len),
7294
7470
  rx: DEFAULT_TILT_POSE.rx,
7295
7471
  ry: DEFAULT_TILT_POSE.ry,
7296
- source: "manual"
7472
+ source: "manual",
7473
+ ...mediaHere ? { media: mediaHere } : {}
7297
7474
  }
7298
7475
  ].sort((a, b) => a.in - b.in);
7299
7476
  };
@@ -7373,7 +7550,7 @@ var camMoveLane = {
7373
7550
  if (!doc.source.camKey) return [];
7374
7551
  const segments = ratedSegments(doc);
7375
7552
  return (doc.camMotion ?? []).flatMap((z) => {
7376
- const ext = spanOutputExtent(segments, z.in, z.out);
7553
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
7377
7554
  return ext === null ? [] : [
7378
7555
  {
7379
7556
  id: z.id,
@@ -7639,6 +7816,7 @@ var speedLane = {
7639
7816
  const len = Math.min(spanDefaultLen(sourceDuration), limit - srcT);
7640
7817
  if (len < SPEED_SPAN_MIN * DEFAULT_SPEED_RATE) return null;
7641
7818
  const id = nextSpeedId(doc);
7819
+ const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
7642
7820
  return (d) => {
7643
7821
  d.speed = [
7644
7822
  ...d.speed ?? [],
@@ -7647,7 +7825,8 @@ var speedLane = {
7647
7825
  in: round11(srcT),
7648
7826
  out: round11(srcT + len),
7649
7827
  rate: DEFAULT_SPEED_RATE,
7650
- source: "manual"
7828
+ source: "manual",
7829
+ ...mediaHere ? { media: mediaHere } : {}
7651
7830
  }
7652
7831
  ].sort((a, b) => a.in - b.in);
7653
7832
  };
@@ -7695,7 +7874,7 @@ var speedLane = {
7695
7874
  out: sp.out
7696
7875
  };
7697
7876
  } else {
7698
- const startOut = sourceToTimeline3(base, sp.in) ?? sp.in;
7877
+ const startOut = sourceToTimeline2(base, sp.in) ?? sp.in;
7699
7878
  const sourceT = sp.in + sp.rate * Math.max(0, g.t - startOut);
7700
7879
  next = {
7701
7880
  in: sp.in,
@@ -8258,6 +8437,7 @@ export {
8258
8437
  OVERLAY_Z,
8259
8438
  PLATE_GROUND,
8260
8439
  PLAYBACK_ACTIVITY,
8440
+ PRIMARY_MEDIA,
8261
8441
  PROGRAM_RETIME,
8262
8442
  REJECT_OVERLAP,
8263
8443
  REST_TILT_ID,
@@ -8345,6 +8525,7 @@ export {
8345
8525
  destinationsForChannel,
8346
8526
  docCardLayout,
8347
8527
  docFreezes,
8528
+ docMediaList,
8348
8529
  docOutputDuration,
8349
8530
  docRestTime,
8350
8531
  docToCropSpace,
@@ -8390,12 +8571,17 @@ export {
8390
8571
  lowerProgramDoc,
8391
8572
  lowerStudioDoc,
8392
8573
  lowerToComposition,
8574
+ mediaAtOutput,
8575
+ mediaDuration,
8576
+ mediaKey,
8577
+ mediaSource,
8393
8578
  micLane,
8394
8579
  migrateHostedDoc,
8395
8580
  migrateMotion,
8396
8581
  momentsFromDoc,
8397
8582
  motionTrack,
8398
8583
  musicBedClip,
8584
+ nextMediaId,
8399
8585
  normalizeCaptureSpace,
8400
8586
  objectMotionBase,
8401
8587
  objectMotionPoseAt,
@@ -8438,6 +8624,7 @@ export {
8438
8624
  resolveText3dAsset,
8439
8625
  resolveZoomStyle,
8440
8626
  restLeanOf,
8627
+ sameMedia,
8441
8628
  sceneChanges,
8442
8629
  scrollRuns,
8443
8630
  setSpeedInRange,