@vosjs/studio-core 0.13.1 → 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 +
@@ -4598,7 +4714,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4598
4714
  // card. Contain never needs it (the video rect IS the card).
4599
4715
  if (fitCover) { c.save(); rr(cardX, cardY, cardW, cardH, radius); c.clip() }
4600
4716
  // cursor coordinate space + drawn radius (shared by click effects + the dot)
4601
- var space = d.cursorSpace || { w: vw, h: vh }
4717
+ var space = (actM ? actM.cursorSpace : d.cursorSpace) || { w: vw, h: vh }
4602
4718
  var curSize = ((d.cursorStyle && d.cursorStyle.size) || 24) * s2 * 0.5
4603
4719
 
4604
4720
  // click effects \u2014 pure f(t): d.clicks are
@@ -4630,8 +4746,9 @@ var ON_FRAME = `(ctx, content, dt) => {
4630
4746
  // click's source moment, or an effect near a cut would keep painting
4631
4747
  // over the NEXT segment's unrelated footage
4632
4748
  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
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
4635
4752
  // press dip: smoothstep down around the real mousedown (80 ms lead \u2014
4636
4753
  // anticipation is what makes effects feel synced), hold through the real
4637
4754
  // down\u2192up span (drags dip long), easeOutBack rebound with a slight
@@ -4744,7 +4861,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4744
4861
  // The dot is the only thing cursorStyle.visible hides \u2014 the track still drives
4745
4862
  // cursor-follow zoom, and click effects draw above on their own switch.
4746
4863
  // Undefined reads as visible so pre-toggle docs are unchanged.
4747
- var cur = d.cursor || []
4864
+ var cur = (actM ? actM.cursor : d.cursor) || []
4748
4865
  if (cur.length && !(d.cursorStyle && d.cursorStyle.visible === false)) {
4749
4866
  var px = cur[0].x, py = cur[0].y
4750
4867
  for (var j = 0; j < cur.length; j++) { if (cur[j].t <= srcT) { px = cur[j].x; py = cur[j].y } }
@@ -4753,7 +4870,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4753
4870
  // Idle fade: a sparse SOURCE-time opacity curve baked by cursorIdleFade.
4754
4871
  // Linear between keys \u2014 opacity needs no easing, and the ramps are already
4755
4872
  // shaped by where the keys sit. Absent/empty = the cursor never dwells.
4756
- var cuA = 1, cuK = d.cursorFade
4873
+ var cuA = 1, cuK = actM ? actM.cursorFade : d.cursorFade
4757
4874
  if (cuK && cuK.length) {
4758
4875
  if (srcT <= cuK[0].t) cuA = cuK[0].a
4759
4876
  else if (srcT >= cuK[cuK.length - 1].t) cuA = cuK[cuK.length - 1].a
@@ -4786,7 +4903,7 @@ var ON_FRAME = `(ctx, content, dt) => {
4786
4903
  // runtime; the card c2d under stubs). Redraw + re-upload only while the
4787
4904
  // bubble is active (a video \u2192 every frame), on resize, or once when it turns
4788
4905
  // off (to clear) \u2014 so a cam-less take never uploads the overlay after frame 1.
4789
- var camV = r.cam, camS = d.cam || {}
4906
+ var camV = amId ? null : r.cam, camS = d.cam || {}
4790
4907
  var camOn = !camS.window || (srcT >= camS.window.in && srcT <= camS.window.out)
4791
4908
  // Readiness is STICKY: readyState drops to HAVE_METADATA while a seek is in
4792
4909
  // flight, so gating each frame on it makes the bubble vanish on every scrub
@@ -5128,7 +5245,8 @@ function lowerToComposition(input) {
5128
5245
  segments: rated.map((seg) => ({
5129
5246
  in: round6(seg.in),
5130
5247
  out: round6(seg.out),
5131
- ...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 } : {}
5132
5250
  })),
5133
5251
  frame: doc.frame,
5134
5252
  micGain: doc.micGain ?? 1,
@@ -5139,6 +5257,34 @@ function lowerToComposition(input) {
5139
5257
  })),
5140
5258
  cursorStyle: doc.cursor,
5141
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
+ } : {},
5142
5288
  ...cursorFade.length ? { cursorFade } : {},
5143
5289
  // Click effects: OUTPUT-anchored click records + resolved styling (named
5144
5290
  // intensities/colors become numbers HERE — ON_FRAME reads no registry).
@@ -5207,10 +5353,24 @@ function clickFxData(doc, rated) {
5207
5353
  const meta = doc.source.meta;
5208
5354
  const level = CLICK_FX_INTENSITY[fx.intensity];
5209
5355
  return {
5210
- clicks: on ? extractClicks(doc.source.cursor, rated, {
5211
- rects: fx.style === "highlight",
5212
- space: { w: meta.width, h: meta.height }
5213
- }) : [],
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) : [],
5214
5374
  clickFx: {
5215
5375
  style: fx.style,
5216
5376
  press: fx.press,
@@ -6835,7 +6995,7 @@ import {
6835
6995
  mapTime as mapTime2,
6836
6996
  removeSegment,
6837
6997
  segmentRate as segmentRate3,
6838
- sourceToTimeline as sourceToTimeline3,
6998
+ sourceToTimeline as sourceToTimeline2,
6839
6999
  splitBySpeed as splitBySpeed3,
6840
7000
  totalDuration as totalDuration6,
6841
7001
  trimSegment
@@ -6920,7 +7080,7 @@ var videoLane = {
6920
7080
  freezes.filter((f) => f.at < seg.out - 1e-9),
6921
7081
  index === 0 || !contiguous(segments, index)
6922
7082
  );
6923
- const anchorOut = sourceToTimeline3(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
7083
+ const anchorOut = sourceToTimeline2(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
6924
7084
  const sourceT = mapTime2(fullMap, anchorOut + (g.t - edgeOutNow));
6925
7085
  const next = trimSegment(
6926
7086
  segments,
@@ -7050,10 +7210,16 @@ var freezeLane = {
7050
7210
  const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
7051
7211
  if (!kept(srcT) || !clear(srcT)) return null;
7052
7212
  const id = nextFreezeId(doc);
7213
+ const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
7053
7214
  return (d) => {
7054
7215
  d.freeze = [
7055
7216
  ...d.freeze ?? [],
7056
- { id, at: srcT, seconds: FREEZE_DEFAULT_SECONDS }
7217
+ {
7218
+ id,
7219
+ at: srcT,
7220
+ seconds: FREEZE_DEFAULT_SECONDS,
7221
+ ...mediaHere ? { media: mediaHere } : {}
7222
+ }
7057
7223
  ].sort((a, b) => a.at - b.at);
7058
7224
  };
7059
7225
  }
@@ -7138,7 +7304,7 @@ var zoomLane = {
7138
7304
  items(doc) {
7139
7305
  const segments = ratedSegments(doc);
7140
7306
  return doc.zoom.flatMap((z) => {
7141
- const ext = spanOutputExtent(segments, z.in, z.out);
7307
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
7142
7308
  return ext === null ? [] : [
7143
7309
  {
7144
7310
  id: z.id,
@@ -7156,10 +7322,13 @@ var zoomLane = {
7156
7322
  const sourceDuration = anchorSourceDuration(doc);
7157
7323
  if (g.type === "create") {
7158
7324
  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);
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);
7163
7332
  if (len < ZOOM_SPAN_MIN * rateAt(rated, srcT)) return null;
7164
7333
  const id = nextZoomId(doc);
7165
7334
  const { cx, cy } = cursorFocusAt(doc, srcT);
@@ -7173,7 +7342,8 @@ var zoomLane = {
7173
7342
  level: DEFAULT_ZOOM_LEVEL,
7174
7343
  cx,
7175
7344
  cy,
7176
- source: "manual"
7345
+ source: "manual",
7346
+ ...mediaHere ? { media: mediaHere } : {}
7177
7347
  }
7178
7348
  ].sort((a, b) => a.in - b.in);
7179
7349
  };
@@ -7263,7 +7433,7 @@ var tiltLane = {
7263
7433
  items(doc) {
7264
7434
  const segments = ratedSegments(doc);
7265
7435
  return (doc.tilt ?? []).flatMap((z) => {
7266
- const ext = spanOutputExtent(segments, z.in, z.out);
7436
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
7267
7437
  return ext === null ? [] : [
7268
7438
  {
7269
7439
  id: z.id,
@@ -7281,10 +7451,13 @@ var tiltLane = {
7281
7451
  const sourceDuration = anchorSourceDuration(doc);
7282
7452
  if (g.type === "create") {
7283
7453
  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);
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);
7288
7461
  if (len < TILT_SPAN_MIN * rateAt(rated, srcT)) return null;
7289
7462
  const id = nextTiltId(doc);
7290
7463
  return (d) => {
@@ -7296,7 +7469,8 @@ var tiltLane = {
7296
7469
  out: round11(srcT + len),
7297
7470
  rx: DEFAULT_TILT_POSE.rx,
7298
7471
  ry: DEFAULT_TILT_POSE.ry,
7299
- source: "manual"
7472
+ source: "manual",
7473
+ ...mediaHere ? { media: mediaHere } : {}
7300
7474
  }
7301
7475
  ].sort((a, b) => a.in - b.in);
7302
7476
  };
@@ -7376,7 +7550,7 @@ var camMoveLane = {
7376
7550
  if (!doc.source.camKey) return [];
7377
7551
  const segments = ratedSegments(doc);
7378
7552
  return (doc.camMotion ?? []).flatMap((z) => {
7379
- const ext = spanOutputExtent(segments, z.in, z.out);
7553
+ const ext = spanOutputExtent(segments, z.in, z.out, z.media);
7380
7554
  return ext === null ? [] : [
7381
7555
  {
7382
7556
  id: z.id,
@@ -7642,6 +7816,7 @@ var speedLane = {
7642
7816
  const len = Math.min(spanDefaultLen(sourceDuration), limit - srcT);
7643
7817
  if (len < SPEED_SPAN_MIN * DEFAULT_SPEED_RATE) return null;
7644
7818
  const id = nextSpeedId(doc);
7819
+ const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
7645
7820
  return (d) => {
7646
7821
  d.speed = [
7647
7822
  ...d.speed ?? [],
@@ -7650,7 +7825,8 @@ var speedLane = {
7650
7825
  in: round11(srcT),
7651
7826
  out: round11(srcT + len),
7652
7827
  rate: DEFAULT_SPEED_RATE,
7653
- source: "manual"
7828
+ source: "manual",
7829
+ ...mediaHere ? { media: mediaHere } : {}
7654
7830
  }
7655
7831
  ].sort((a, b) => a.in - b.in);
7656
7832
  };
@@ -7698,7 +7874,7 @@ var speedLane = {
7698
7874
  out: sp.out
7699
7875
  };
7700
7876
  } else {
7701
- const startOut = sourceToTimeline3(base, sp.in) ?? sp.in;
7877
+ const startOut = sourceToTimeline2(base, sp.in) ?? sp.in;
7702
7878
  const sourceT = sp.in + sp.rate * Math.max(0, g.t - startOut);
7703
7879
  next = {
7704
7880
  in: sp.in,
@@ -8261,6 +8437,7 @@ export {
8261
8437
  OVERLAY_Z,
8262
8438
  PLATE_GROUND,
8263
8439
  PLAYBACK_ACTIVITY,
8440
+ PRIMARY_MEDIA,
8264
8441
  PROGRAM_RETIME,
8265
8442
  REJECT_OVERLAP,
8266
8443
  REST_TILT_ID,
@@ -8348,6 +8525,7 @@ export {
8348
8525
  destinationsForChannel,
8349
8526
  docCardLayout,
8350
8527
  docFreezes,
8528
+ docMediaList,
8351
8529
  docOutputDuration,
8352
8530
  docRestTime,
8353
8531
  docToCropSpace,
@@ -8393,12 +8571,17 @@ export {
8393
8571
  lowerProgramDoc,
8394
8572
  lowerStudioDoc,
8395
8573
  lowerToComposition,
8574
+ mediaAtOutput,
8575
+ mediaDuration,
8576
+ mediaKey,
8577
+ mediaSource,
8396
8578
  micLane,
8397
8579
  migrateHostedDoc,
8398
8580
  migrateMotion,
8399
8581
  momentsFromDoc,
8400
8582
  motionTrack,
8401
8583
  musicBedClip,
8584
+ nextMediaId,
8402
8585
  normalizeCaptureSpace,
8403
8586
  objectMotionBase,
8404
8587
  objectMotionPoseAt,
@@ -8441,6 +8624,7 @@ export {
8441
8624
  resolveText3dAsset,
8442
8625
  resolveZoomStyle,
8443
8626
  restLeanOf,
8627
+ sameMedia,
8444
8628
  sceneChanges,
8445
8629
  scrollRuns,
8446
8630
  setSpeedInRange,