@remotion/studio 4.0.452 → 4.0.453

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.
Files changed (29) hide show
  1. package/dist/audio-waveform-worker.d.ts +1 -0
  2. package/dist/audio-waveform-worker.js +102 -0
  3. package/dist/components/AudioWaveform.d.ts +2 -0
  4. package/dist/components/AudioWaveform.js +166 -18
  5. package/dist/components/Timeline/LoopedIndicator.js +5 -19
  6. package/dist/components/Timeline/TimelineSequence.js +18 -10
  7. package/dist/components/Timeline/TimelineVideoInfo.d.ts +2 -0
  8. package/dist/components/Timeline/TimelineVideoInfo.js +51 -12
  9. package/dist/components/audio-waveform-worker-types.d.ts +28 -0
  10. package/dist/components/audio-waveform-worker-types.js +2 -0
  11. package/dist/components/draw-peaks.d.ts +1 -1
  12. package/dist/components/load-waveform-peaks.d.ts +11 -1
  13. package/dist/components/load-waveform-peaks.js +22 -33
  14. package/dist/components/looped-media-timeline.d.ts +6 -0
  15. package/dist/components/looped-media-timeline.js +14 -0
  16. package/dist/components/slice-waveform-peaks.d.ts +7 -0
  17. package/dist/components/slice-waveform-peaks.js +15 -0
  18. package/dist/components/waveform-peak-processor.d.ts +23 -0
  19. package/dist/components/waveform-peak-processor.js +77 -0
  20. package/dist/esm/audio-waveform-worker.mjs +345 -0
  21. package/dist/esm/{chunk-hxr6txpe.js → chunk-hn4803e7.js} +398 -98
  22. package/dist/esm/internals.mjs +398 -98
  23. package/dist/esm/previewEntry.mjs +398 -98
  24. package/dist/esm/renderEntry.mjs +1 -1
  25. package/dist/helpers/calculate-timeline.js +16 -0
  26. package/dist/helpers/get-timeline-nestedness.js +2 -1
  27. package/dist/make-audio-waveform-worker.d.ts +1 -0
  28. package/dist/make-audio-waveform-worker.js +10 -0
  29. package/package.json +18 -9
@@ -20458,7 +20458,8 @@ var getTimelineNestedLevel = (sequence, allSequences, depth) => {
20458
20458
  if (!parentSequence) {
20459
20459
  throw new Error("has parentId but no parent");
20460
20460
  }
20461
- return getTimelineNestedLevel(parentSequence, allSequences, depth + 1);
20461
+ const parentContributes = parentSequence.showInTimeline;
20462
+ return getTimelineNestedLevel(parentSequence, allSequences, parentContributes ? depth + 1 : depth);
20462
20463
  };
20463
20464
 
20464
20465
  // src/helpers/get-timeline-sequence-hash.ts
@@ -20507,6 +20508,19 @@ var getTimelineSequenceSequenceSortKey = (track, tracks, sameHashes = {}, nonceR
20507
20508
  };
20508
20509
 
20509
20510
  // src/helpers/calculate-timeline.ts
20511
+ var getInheritedLoopDisplay = (sequence, sequences) => {
20512
+ if (sequence.loopDisplay) {
20513
+ return sequence.loopDisplay;
20514
+ }
20515
+ if (!sequence.parent) {
20516
+ return;
20517
+ }
20518
+ const parent = sequences.find((s) => s.id === sequence.parent);
20519
+ if (!parent) {
20520
+ return;
20521
+ }
20522
+ return getInheritedLoopDisplay(parent, sequences);
20523
+ };
20510
20524
  var calculateTimeline = ({
20511
20525
  sequences
20512
20526
  }) => {
@@ -20535,7 +20549,8 @@ var calculateTimeline = ({
20535
20549
  sequence: {
20536
20550
  ...sequence,
20537
20551
  from: visibleStart,
20538
- duration: visibleDuration
20552
+ duration: visibleDuration,
20553
+ loopDisplay: sequence.type === "audio" || sequence.type === "video" ? getInheritedLoopDisplay(sequence, sortedSequences) : sequence.loopDisplay
20539
20554
  },
20540
20555
  depth: getTimelineNestedLevel(sequence, sortedSequences, 0),
20541
20556
  hash: actualHash,
@@ -23171,6 +23186,13 @@ var useMaxMediaDuration = (s, fps) => {
23171
23186
  import { useEffect as useEffect72, useMemo as useMemo119, useRef as useRef43, useState as useState77 } from "react";
23172
23187
  import { Internals as Internals55 } from "remotion";
23173
23188
 
23189
+ // src/make-audio-waveform-worker.ts
23190
+ var makeAudioWaveformWorker = () => {
23191
+ return new Worker(new URL("./audio-waveform-worker.mjs", import.meta.url), {
23192
+ type: "module"
23193
+ });
23194
+ };
23195
+
23174
23196
  // src/components/parse-color.ts
23175
23197
  var colorCache = new Map;
23176
23198
  var parseColor = (color) => {
@@ -23246,12 +23268,107 @@ var drawBars = (canvas, peaks, color, volume, width) => {
23246
23268
 
23247
23269
  // src/components/load-waveform-peaks.ts
23248
23270
  import { ALL_FORMATS as ALL_FORMATS3, AudioSampleSink, Input as Input3, UrlSource as UrlSource3 } from "mediabunny";
23271
+
23272
+ // src/components/waveform-peak-processor.ts
23273
+ var emitWaveformProgress = ({
23274
+ completedPeaks,
23275
+ final,
23276
+ onProgress,
23277
+ peaks,
23278
+ totalPeaks
23279
+ }) => {
23280
+ onProgress?.({
23281
+ peaks,
23282
+ completedPeaks,
23283
+ totalPeaks,
23284
+ final
23285
+ });
23286
+ };
23287
+ var createWaveformPeakProcessor = ({
23288
+ totalPeaks,
23289
+ samplesPerPeak,
23290
+ onProgress,
23291
+ progressIntervalInMs,
23292
+ now
23293
+ }) => {
23294
+ const peaks = new Float32Array(totalPeaks);
23295
+ let peakIndex = 0;
23296
+ let peakMax = 0;
23297
+ let sampleInPeak = 0;
23298
+ let lastProgressAt = 0;
23299
+ let lastProgressPeak = 0;
23300
+ const emitProgress = (force) => {
23301
+ const timestamp = now();
23302
+ if (!force && peakIndex === lastProgressPeak && sampleInPeak === 0) {
23303
+ return;
23304
+ }
23305
+ if (!force && timestamp - lastProgressAt < progressIntervalInMs) {
23306
+ return;
23307
+ }
23308
+ lastProgressAt = timestamp;
23309
+ lastProgressPeak = peakIndex;
23310
+ emitWaveformProgress({
23311
+ peaks,
23312
+ completedPeaks: peakIndex,
23313
+ totalPeaks,
23314
+ final: force,
23315
+ onProgress
23316
+ });
23317
+ };
23318
+ return {
23319
+ peaks,
23320
+ processSampleChunk: (floats, channels) => {
23321
+ const frameCount = Math.floor(floats.length / Math.max(1, channels));
23322
+ for (let frame2 = 0;frame2 < frameCount; frame2++) {
23323
+ let framePeak = 0;
23324
+ for (let channel = 0;channel < channels; channel++) {
23325
+ const sampleIndex = frame2 * channels + channel;
23326
+ const abs = Math.abs(floats[sampleIndex] ?? 0);
23327
+ if (abs > framePeak) {
23328
+ framePeak = abs;
23329
+ }
23330
+ }
23331
+ if (framePeak > peakMax) {
23332
+ peakMax = framePeak;
23333
+ }
23334
+ sampleInPeak++;
23335
+ if (sampleInPeak >= samplesPerPeak) {
23336
+ if (peakIndex < totalPeaks) {
23337
+ peaks[peakIndex] = peakMax;
23338
+ }
23339
+ peakIndex++;
23340
+ peakMax = 0;
23341
+ sampleInPeak = 0;
23342
+ }
23343
+ }
23344
+ emitProgress(false);
23345
+ },
23346
+ finalize: () => {
23347
+ if (sampleInPeak > 0 && peakIndex < totalPeaks) {
23348
+ peaks[peakIndex] = peakMax;
23349
+ peakIndex++;
23350
+ }
23351
+ emitProgress(true);
23352
+ }
23353
+ };
23354
+ };
23355
+
23356
+ // src/components/load-waveform-peaks.ts
23249
23357
  var TARGET_SAMPLE_RATE = 100;
23358
+ var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
23250
23359
  var peaksCache = new Map;
23251
- async function loadWaveformPeaks(url, signal) {
23360
+ async function loadWaveformPeaks(url, signal, options) {
23252
23361
  const cached = peaksCache.get(url);
23253
- if (cached)
23362
+ if (cached) {
23363
+ emitWaveformProgress({
23364
+ peaks: cached,
23365
+ completedPeaks: cached.length,
23366
+ totalPeaks: cached.length,
23367
+ final: true,
23368
+ onProgress: options?.onProgress
23369
+ });
23254
23370
  return cached;
23371
+ }
23255
23372
  const input2 = new Input3({
23256
23373
  formats: ALL_FORMATS3,
23257
23374
  source: new UrlSource3(url)
@@ -23265,11 +23382,14 @@ async function loadWaveformPeaks(url, signal) {
23265
23382
  const durationInSeconds = await audioTrack.computeDuration();
23266
23383
  const totalPeaks = Math.ceil(durationInSeconds * TARGET_SAMPLE_RATE);
23267
23384
  const samplesPerPeak = Math.max(1, Math.floor(sampleRate / TARGET_SAMPLE_RATE));
23268
- const peaks = new Float32Array(totalPeaks);
23269
- let peakIndex = 0;
23270
- let peakMax = 0;
23271
- let sampleInPeak = 0;
23272
23385
  const sink = new AudioSampleSink(audioTrack);
23386
+ const processor = createWaveformPeakProcessor({
23387
+ totalPeaks,
23388
+ samplesPerPeak,
23389
+ onProgress: options?.onProgress,
23390
+ progressIntervalInMs: options?.progressIntervalInMs ?? DEFAULT_PROGRESS_INTERVAL_IN_MS,
23391
+ now: () => Date.now()
23392
+ });
23273
23393
  for await (const sample of sink.samples()) {
23274
23394
  if (signal.aborted) {
23275
23395
  sample.close();
@@ -23282,34 +23402,11 @@ async function loadWaveformPeaks(url, signal) {
23282
23402
  const floats = new Float32Array(bytesNeeded / 4);
23283
23403
  sample.copyTo(floats, { format: "f32", planeIndex: 0 });
23284
23404
  const channels = Math.max(1, sample.numberOfChannels);
23285
- const frames = sample.numberOfFrames;
23286
23405
  sample.close();
23287
- for (let frame2 = 0;frame2 < frames; frame2++) {
23288
- let framePeak = 0;
23289
- for (let channel = 0;channel < channels; channel++) {
23290
- const sampleIndex = frame2 * channels + channel;
23291
- const abs = Math.abs(floats[sampleIndex] ?? 0);
23292
- if (abs > framePeak) {
23293
- framePeak = abs;
23294
- }
23295
- }
23296
- if (framePeak > peakMax) {
23297
- peakMax = framePeak;
23298
- }
23299
- sampleInPeak++;
23300
- if (sampleInPeak >= samplesPerPeak) {
23301
- if (peakIndex < totalPeaks) {
23302
- peaks[peakIndex] = peakMax;
23303
- }
23304
- peakIndex++;
23305
- peakMax = 0;
23306
- sampleInPeak = 0;
23307
- }
23308
- }
23309
- }
23310
- if (sampleInPeak > 0 && peakIndex < totalPeaks) {
23311
- peaks[peakIndex] = peakMax;
23406
+ processor.processSampleChunk(floats, channels);
23312
23407
  }
23408
+ processor.finalize();
23409
+ const { peaks } = processor;
23313
23410
  peaksCache.set(url, peaks);
23314
23411
  return peaks;
23315
23412
  } finally {
@@ -23317,8 +23414,50 @@ async function loadWaveformPeaks(url, signal) {
23317
23414
  }
23318
23415
  }
23319
23416
 
23417
+ // src/components/looped-media-timeline.ts
23418
+ var shouldTileLoopDisplay = (loopDisplay) => {
23419
+ return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
23420
+ };
23421
+ var getLoopDisplayWidth = ({
23422
+ visualizationWidth,
23423
+ loopDisplay
23424
+ }) => {
23425
+ if (!shouldTileLoopDisplay(loopDisplay)) {
23426
+ return visualizationWidth;
23427
+ }
23428
+ return visualizationWidth / loopDisplay.numberOfTimes;
23429
+ };
23430
+
23431
+ // src/components/slice-waveform-peaks.ts
23432
+ var sliceWaveformPeaks = ({
23433
+ durationInFrames,
23434
+ fps,
23435
+ peaks,
23436
+ playbackRate,
23437
+ startFrom
23438
+ }) => {
23439
+ if (peaks.length === 0) {
23440
+ return peaks;
23441
+ }
23442
+ const startTimeInSeconds = startFrom / fps;
23443
+ const durationInSeconds = durationInFrames / fps * playbackRate;
23444
+ const startPeakIndex = Math.floor(startTimeInSeconds * TARGET_SAMPLE_RATE);
23445
+ const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * TARGET_SAMPLE_RATE);
23446
+ return peaks.subarray(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
23447
+ };
23448
+
23320
23449
  // src/components/AudioWaveform.tsx
23321
23450
  import { jsx as jsx209, jsxs as jsxs101 } from "react/jsx-runtime";
23451
+ var EMPTY_PEAKS = new Float32Array(0);
23452
+ var canRetryCanvasTransfer = (err) => {
23453
+ return err instanceof DOMException && err.name === "InvalidStateError";
23454
+ };
23455
+ var canUseAudioWaveformWorker = () => {
23456
+ if (typeof Worker === "undefined" || typeof OffscreenCanvas === "undefined" || typeof HTMLCanvasElement === "undefined") {
23457
+ return false;
23458
+ }
23459
+ return "transferControlToOffscreen" in HTMLCanvasElement.prototype;
23460
+ };
23322
23461
  var container42 = {
23323
23462
  display: "flex",
23324
23463
  flexDirection: "row",
@@ -23337,11 +23476,41 @@ var errorMessage = {
23337
23476
  opacity: 0.75
23338
23477
  };
23339
23478
  var waveformCanvasStyle = {
23340
- pointerEvents: "none"
23479
+ pointerEvents: "none",
23480
+ width: "100%",
23481
+ height: "100%"
23341
23482
  };
23342
23483
  var volumeCanvasStyle = {
23343
23484
  position: "absolute"
23344
23485
  };
23486
+ var drawLoopedWaveform = ({
23487
+ canvas,
23488
+ peaks,
23489
+ volume,
23490
+ visualizationWidth,
23491
+ loopWidth
23492
+ }) => {
23493
+ const h = canvas.height;
23494
+ const w = Math.ceil(visualizationWidth);
23495
+ const targetCanvas = document.createElement("canvas");
23496
+ targetCanvas.width = Math.max(1, Math.ceil(loopWidth));
23497
+ targetCanvas.height = h;
23498
+ drawBars(targetCanvas, peaks, "rgba(255, 255, 255, 0.6)", volume, targetCanvas.width);
23499
+ canvas.width = w;
23500
+ canvas.height = h;
23501
+ const ctx = canvas.getContext("2d");
23502
+ if (!ctx) {
23503
+ throw new Error("Failed to get canvas context");
23504
+ }
23505
+ const pattern = ctx.createPattern(targetCanvas, "repeat-x");
23506
+ if (!pattern) {
23507
+ return;
23508
+ }
23509
+ pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
23510
+ ctx.clearRect(0, 0, w, h);
23511
+ ctx.fillStyle = pattern;
23512
+ ctx.fillRect(0, 0, w, h);
23513
+ };
23345
23514
  var AudioWaveform = ({
23346
23515
  src,
23347
23516
  startFrom,
@@ -23349,10 +23518,13 @@ var AudioWaveform = ({
23349
23518
  visualizationWidth,
23350
23519
  volume,
23351
23520
  doesVolumeChange,
23352
- playbackRate
23521
+ playbackRate,
23522
+ loopDisplay
23353
23523
  }) => {
23354
23524
  const [peaks, setPeaks] = useState77(null);
23355
23525
  const [error, setError] = useState77(null);
23526
+ const [waveformCanvasKey, setWaveformCanvasKey] = useState77(0);
23527
+ const canUseWorkerPath = useMemo119(() => canUseAudioWaveformWorker(), []);
23356
23528
  const vidConf = Internals55.useUnsafeVideoConfig();
23357
23529
  if (vidConf === null) {
23358
23530
  throw new Error("Expected video config");
@@ -23360,8 +23532,15 @@ var AudioWaveform = ({
23360
23532
  const containerRef = useRef43(null);
23361
23533
  const waveformCanvas = useRef43(null);
23362
23534
  const volumeCanvas = useRef43(null);
23535
+ const waveformWorker = useRef43(null);
23536
+ const hasTransferredCanvas = useRef43(false);
23537
+ const latestRequestId = useRef43(0);
23363
23538
  useEffect72(() => {
23539
+ if (canUseWorkerPath) {
23540
+ return;
23541
+ }
23364
23542
  const controller = new AbortController;
23543
+ setPeaks(null);
23365
23544
  setError(null);
23366
23545
  loadWaveformPeaks(src, controller.signal).then((p) => {
23367
23546
  if (!controller.signal.aborted) {
@@ -23373,30 +23552,127 @@ var AudioWaveform = ({
23373
23552
  }
23374
23553
  });
23375
23554
  return () => controller.abort();
23376
- }, [src]);
23555
+ }, [canUseWorkerPath, src]);
23556
+ useEffect72(() => {
23557
+ if (!canUseWorkerPath) {
23558
+ return;
23559
+ }
23560
+ const canvasElement = waveformCanvas.current;
23561
+ if (!canvasElement || hasTransferredCanvas.current) {
23562
+ return;
23563
+ }
23564
+ const worker = makeAudioWaveformWorker();
23565
+ waveformWorker.current = worker;
23566
+ worker.addEventListener("message", (event) => {
23567
+ if (event.data.type === "error") {
23568
+ if (event.data.requestId !== latestRequestId.current) {
23569
+ return;
23570
+ }
23571
+ setError(new Error(event.data.message));
23572
+ }
23573
+ });
23574
+ let offscreen;
23575
+ try {
23576
+ offscreen = canvasElement.transferControlToOffscreen();
23577
+ } catch (err) {
23578
+ worker.terminate();
23579
+ waveformWorker.current = null;
23580
+ if (canRetryCanvasTransfer(err)) {
23581
+ setWaveformCanvasKey((key4) => key4 + 1);
23582
+ return;
23583
+ }
23584
+ throw err;
23585
+ }
23586
+ hasTransferredCanvas.current = true;
23587
+ worker.postMessage({ type: "init", canvas: offscreen }, [offscreen]);
23588
+ return () => {
23589
+ worker.postMessage({ type: "dispose" });
23590
+ worker.terminate();
23591
+ waveformWorker.current = null;
23592
+ hasTransferredCanvas.current = false;
23593
+ };
23594
+ }, [canUseWorkerPath, waveformCanvasKey]);
23377
23595
  const portionPeaks = useMemo119(() => {
23378
- if (!peaks || peaks.length === 0) {
23596
+ if (canUseWorkerPath || !peaks) {
23379
23597
  return null;
23380
23598
  }
23381
- const startTimeInSeconds = startFrom / vidConf.fps;
23382
- const durationInSeconds = durationInFrames / vidConf.fps * playbackRate;
23383
- const startPeakIndex = Math.floor(startTimeInSeconds * TARGET_SAMPLE_RATE);
23384
- const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * TARGET_SAMPLE_RATE);
23385
- return peaks.slice(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
23386
- }, [peaks, startFrom, durationInFrames, vidConf.fps, playbackRate]);
23599
+ return sliceWaveformPeaks({
23600
+ durationInFrames: shouldTileLoopDisplay(loopDisplay) ? loopDisplay.durationInFrames : durationInFrames,
23601
+ fps: vidConf.fps,
23602
+ peaks,
23603
+ playbackRate,
23604
+ startFrom
23605
+ });
23606
+ }, [
23607
+ canUseWorkerPath,
23608
+ durationInFrames,
23609
+ loopDisplay,
23610
+ peaks,
23611
+ playbackRate,
23612
+ startFrom,
23613
+ vidConf.fps
23614
+ ]);
23387
23615
  useEffect72(() => {
23388
23616
  const { current: canvasElement } = waveformCanvas;
23389
23617
  const { current: containerElement } = containerRef;
23390
- if (!canvasElement || !containerElement || !portionPeaks || portionPeaks.length === 0) {
23618
+ if (!canvasElement || !containerElement) {
23391
23619
  return;
23392
23620
  }
23393
23621
  const h = containerElement.clientHeight;
23394
23622
  const w = Math.ceil(visualizationWidth);
23623
+ const vol = typeof volume === "number" ? volume : 1;
23624
+ if (canUseWorkerPath) {
23625
+ const worker = waveformWorker.current;
23626
+ if (!worker || !hasTransferredCanvas.current) {
23627
+ return;
23628
+ }
23629
+ latestRequestId.current += 1;
23630
+ setError(null);
23631
+ const message = {
23632
+ type: "render",
23633
+ requestId: latestRequestId.current,
23634
+ src,
23635
+ width: w,
23636
+ height: h,
23637
+ volume: vol,
23638
+ startFrom,
23639
+ durationInFrames,
23640
+ fps: vidConf.fps,
23641
+ playbackRate,
23642
+ loopDisplay
23643
+ };
23644
+ worker.postMessage(message);
23645
+ return;
23646
+ }
23395
23647
  canvasElement.width = w;
23396
23648
  canvasElement.height = h;
23397
- const vol = typeof volume === "number" ? volume : 1;
23398
- drawBars(canvasElement, portionPeaks, "rgba(255, 255, 255, 0.6)", vol, w);
23399
- }, [portionPeaks, visualizationWidth, volume]);
23649
+ if (shouldTileLoopDisplay(loopDisplay)) {
23650
+ drawLoopedWaveform({
23651
+ canvas: canvasElement,
23652
+ peaks: portionPeaks ?? EMPTY_PEAKS,
23653
+ volume: vol,
23654
+ visualizationWidth,
23655
+ loopWidth: getLoopDisplayWidth({
23656
+ visualizationWidth,
23657
+ loopDisplay
23658
+ })
23659
+ });
23660
+ } else {
23661
+ drawBars(canvasElement, portionPeaks ?? EMPTY_PEAKS, "rgba(255, 255, 255, 0.6)", vol, w);
23662
+ }
23663
+ }, [
23664
+ canUseWorkerPath,
23665
+ durationInFrames,
23666
+ loopDisplay,
23667
+ playbackRate,
23668
+ portionPeaks,
23669
+ src,
23670
+ startFrom,
23671
+ vidConf.fps,
23672
+ visualizationWidth,
23673
+ volume,
23674
+ waveformCanvasKey
23675
+ ]);
23400
23676
  useEffect72(() => {
23401
23677
  const { current: volumeCanvasElement } = volumeCanvas;
23402
23678
  const { current: containerElement } = containerRef;
@@ -23438,7 +23714,7 @@ var AudioWaveform = ({
23438
23714
  })
23439
23715
  });
23440
23716
  }
23441
- if (!peaks) {
23717
+ if (!canUseWorkerPath && !peaks) {
23442
23718
  return null;
23443
23719
  }
23444
23720
  return /* @__PURE__ */ jsxs101("div", {
@@ -23448,7 +23724,7 @@ var AudioWaveform = ({
23448
23724
  /* @__PURE__ */ jsx209("canvas", {
23449
23725
  ref: waveformCanvas,
23450
23726
  style: waveformCanvasStyle
23451
- }),
23727
+ }, waveformCanvasKey),
23452
23728
  /* @__PURE__ */ jsx209("canvas", {
23453
23729
  ref: volumeCanvas,
23454
23730
  style: volumeCanvasStyle
@@ -23471,7 +23747,8 @@ var width = {
23471
23747
  position: "relative"
23472
23748
  };
23473
23749
  var icon4 = {
23474
- height: 12
23750
+ height: 12,
23751
+ filter: "drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8))"
23475
23752
  };
23476
23753
  var Icon = () => /* @__PURE__ */ jsx210("svg", {
23477
23754
  viewBox: "0 0 512 512",
@@ -23481,44 +23758,23 @@ var Icon = () => /* @__PURE__ */ jsx210("svg", {
23481
23758
  d: "M512 256c0 88.224-71.775 160-160 160H170.067l34.512 32.419c9.875 9.276 10.119 24.883.539 34.464l-10.775 10.775c-9.373 9.372-24.568 9.372-33.941 0l-92.686-92.686c-9.373-9.373-9.373-24.568 0-33.941l92.686-92.686c9.373-9.373 24.568-9.373 33.941 0l10.775 10.775c9.581 9.581 9.337 25.187-.539 34.464L170.067 352H352c52.935 0 96-43.065 96-96 0-13.958-2.996-27.228-8.376-39.204-4.061-9.039-2.284-19.626 4.723-26.633l12.183-12.183c11.499-11.499 30.965-8.526 38.312 5.982C505.814 205.624 512 230.103 512 256zM72.376 295.204C66.996 283.228 64 269.958 64 256c0-52.935 43.065-96 96-96h181.933l-34.512 32.419c-9.875 9.276-10.119 24.883-.539 34.464l10.775 10.775c9.373 9.372 24.568 9.372 33.941 0l92.686-92.686c9.373-9.373 9.373-24.568 0-33.941l-92.686-92.686c-9.373-9.373-24.568-9.373-33.941 0L306.882 29.12c-9.581 9.581-9.337 25.187.539 34.464L341.933 96H160C71.775 96 0 167.776 0 256c0 25.897 6.186 50.376 17.157 72.039 7.347 14.508 26.813 17.481 38.312 5.982l12.183-12.183c7.008-7.008 8.786-17.595 4.724-26.634z"
23482
23759
  })
23483
23760
  });
23484
- var topLine = {
23485
- top: 0,
23486
- height: 2,
23487
- width: 1,
23488
- background: LIGHT_COLOR
23489
- };
23490
- var bottomLine = {
23491
- top: 0,
23492
- height: 2,
23761
+ var verticalLine = {
23762
+ height: "100%",
23493
23763
  width: 1,
23494
- background: LIGHT_COLOR
23495
- };
23496
- var topContainer = {
23497
- justifyContent: "flex-start",
23498
- alignItems: "center"
23764
+ background: "rgb(255,255,255, 0.5)"
23499
23765
  };
23500
23766
  var centerContainer = {
23501
23767
  justifyContent: "center",
23502
23768
  alignItems: "center"
23503
23769
  };
23504
- var bottomContainer = {
23505
- justifyContent: "flex-end",
23506
- alignItems: "center"
23507
- };
23508
23770
  var LoopedIndicator = () => {
23509
23771
  return /* @__PURE__ */ jsxs102("div", {
23510
23772
  style: width,
23511
23773
  children: [
23512
23774
  /* @__PURE__ */ jsx210(AbsoluteFill3, {
23513
- style: topContainer,
23514
- children: /* @__PURE__ */ jsx210("div", {
23515
- style: topLine
23516
- })
23517
- }),
23518
- /* @__PURE__ */ jsx210(AbsoluteFill3, {
23519
- style: bottomContainer,
23775
+ style: centerContainer,
23520
23776
  children: /* @__PURE__ */ jsx210("div", {
23521
- style: bottomLine
23777
+ style: verticalLine
23522
23778
  })
23523
23779
  }),
23524
23780
  /* @__PURE__ */ jsx210(AbsoluteFill3, {
@@ -23990,7 +24246,8 @@ var TimelineVideoInfo = ({
23990
24246
  volume,
23991
24247
  doesVolumeChange,
23992
24248
  premountWidth,
23993
- postmountWidth
24249
+ postmountWidth,
24250
+ loopDisplay
23994
24251
  }) => {
23995
24252
  const { fps } = useVideoConfig5();
23996
24253
  const ref2 = useRef45(null);
@@ -24013,25 +24270,54 @@ var TimelineVideoInfo = ({
24013
24270
  return;
24014
24271
  }
24015
24272
  current.appendChild(canvas);
24273
+ const loopWidth = getLoopDisplayWidth({
24274
+ visualizationWidth: naturalWidth,
24275
+ loopDisplay
24276
+ });
24277
+ const shouldRepeatVideo = shouldTileLoopDisplay(loopDisplay);
24278
+ const targetCanvas = shouldRepeatVideo ? document.createElement("canvas") : canvas;
24279
+ targetCanvas.width = shouldRepeatVideo ? Math.max(1, Math.ceil(loopWidth)) : canvas.width;
24280
+ targetCanvas.height = canvas.height;
24281
+ const targetCtx = shouldRepeatVideo ? targetCanvas.getContext("2d") : ctx;
24282
+ if (!targetCtx) {
24283
+ current.removeChild(canvas);
24284
+ return;
24285
+ }
24286
+ const repeatTarget = () => {
24287
+ if (!shouldRepeatVideo) {
24288
+ return;
24289
+ }
24290
+ const pattern = ctx.createPattern(targetCanvas, "repeat-x");
24291
+ if (!pattern) {
24292
+ return;
24293
+ }
24294
+ pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
24295
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
24296
+ ctx.fillStyle = pattern;
24297
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
24298
+ };
24016
24299
  const filledSlots = new Map;
24017
24300
  const fromSeconds = trimBefore / fps;
24018
- const toSeconds = fromSeconds + durationInFrames * playbackRate / fps;
24301
+ const visibleDurationInFrames = shouldRepeatVideo && loopDisplay ? loopDisplay.durationInFrames : durationInFrames;
24302
+ const toSeconds = fromSeconds + visibleDurationInFrames * playbackRate / fps;
24303
+ const targetWidth = shouldRepeatVideo ? targetCanvas.width : naturalWidth;
24019
24304
  if (aspectRatio.current !== null) {
24020
24305
  ensureSlots({
24021
24306
  filledSlots,
24022
- naturalWidth,
24307
+ naturalWidth: targetWidth,
24023
24308
  fromSeconds,
24024
24309
  toSeconds,
24025
24310
  aspectRatio: aspectRatio.current
24026
24311
  });
24027
24312
  fillWithCachedFrames({
24028
- ctx,
24029
- naturalWidth,
24313
+ ctx: targetCtx,
24314
+ naturalWidth: targetWidth,
24030
24315
  filledSlots,
24031
24316
  src,
24032
24317
  segmentDuration: toSeconds - fromSeconds,
24033
24318
  fromSeconds
24034
24319
  });
24320
+ repeatTarget();
24035
24321
  const unfilled = Array.from(filledSlots.keys()).filter((timestamp) => !filledSlots.get(timestamp));
24036
24322
  if (unfilled.length === 0) {
24037
24323
  return () => {
@@ -24049,7 +24335,7 @@ var TimelineVideoInfo = ({
24049
24335
  filledSlots,
24050
24336
  fromSeconds,
24051
24337
  toSeconds,
24052
- naturalWidth,
24338
+ naturalWidth: targetWidth,
24053
24339
  aspectRatio: aspectRatio.current
24054
24340
  });
24055
24341
  return Array.from(filledSlots.keys()).map((timestamp) => timestamp / WEBCODECS_TIMESCALE);
@@ -24077,17 +24363,18 @@ var TimelineVideoInfo = ({
24077
24363
  filledSlots,
24078
24364
  fromSeconds,
24079
24365
  toSeconds,
24080
- naturalWidth,
24366
+ naturalWidth: targetWidth,
24081
24367
  aspectRatio: aspectRatio.current
24082
24368
  });
24083
24369
  fillFrameWhereItFits({
24084
- ctx,
24370
+ ctx: targetCtx,
24085
24371
  filledSlots,
24086
- visualizationWidth: naturalWidth,
24372
+ visualizationWidth: targetWidth,
24087
24373
  frame: transformed,
24088
24374
  segmentDuration: toSeconds - fromSeconds,
24089
24375
  fromSeconds
24090
24376
  });
24377
+ repeatTarget();
24091
24378
  } catch (e) {
24092
24379
  if (frame2) {
24093
24380
  frame2.close();
@@ -24103,13 +24390,14 @@ var TimelineVideoInfo = ({
24103
24390
  return;
24104
24391
  }
24105
24392
  fillWithCachedFrames({
24106
- ctx,
24107
- naturalWidth,
24393
+ ctx: targetCtx,
24394
+ naturalWidth: targetWidth,
24108
24395
  filledSlots,
24109
24396
  src,
24110
24397
  segmentDuration: toSeconds - fromSeconds,
24111
24398
  fromSeconds
24112
24399
  });
24400
+ repeatTarget();
24113
24401
  }).catch((e) => {
24114
24402
  setError(e);
24115
24403
  });
@@ -24121,6 +24409,7 @@ var TimelineVideoInfo = ({
24121
24409
  durationInFrames,
24122
24410
  error,
24123
24411
  fps,
24412
+ loopDisplay,
24124
24413
  naturalWidth,
24125
24414
  playbackRate,
24126
24415
  src,
@@ -24152,7 +24441,8 @@ var TimelineVideoInfo = ({
24152
24441
  durationInFrames,
24153
24442
  volume,
24154
24443
  doesVolumeChange,
24155
- playbackRate
24444
+ playbackRate,
24445
+ loopDisplay
24156
24446
  })
24157
24447
  })
24158
24448
  ]
@@ -24177,29 +24467,37 @@ var TimelineSequence = ({ s }) => {
24177
24467
  var Inner4 = ({ s, windowWidth }) => {
24178
24468
  const video = Internals56.useVideo();
24179
24469
  const maxMediaDuration = useMaxMediaDuration(s, video?.fps ?? 30);
24470
+ const effectiveMaxMediaDuration = s.loopDisplay ? null : maxMediaDuration;
24180
24471
  if (!video) {
24181
24472
  throw new TypeError("Expected video config");
24182
24473
  }
24183
24474
  const frame2 = useCurrentFrame2();
24184
24475
  const relativeFrame = frame2 - s.from;
24476
+ const displayDurationInFrames = s.loopDisplay ? s.loopDisplay.durationInFrames * s.loopDisplay.numberOfTimes : s.duration;
24185
24477
  const relativeFrameWithPremount = relativeFrame + (s.premountDisplay ?? 0);
24186
- const relativeFrameWithPostmount = relativeFrame - s.duration;
24478
+ const relativeFrameWithPostmount = relativeFrame - displayDurationInFrames;
24187
24479
  const roundedFrame = Math.round(relativeFrame * 100) / 100;
24188
- const isInRange = relativeFrame >= 0 && relativeFrame < s.duration;
24189
- const isPremounting = relativeFrameWithPremount >= 0 && relativeFrameWithPremount < s.duration && !isInRange;
24480
+ const isInRange = relativeFrame >= 0 && relativeFrame < displayDurationInFrames;
24481
+ const isPremounting = relativeFrameWithPremount >= 0 && relativeFrameWithPremount < displayDurationInFrames && !isInRange;
24190
24482
  const isPostmounting = relativeFrameWithPostmount >= 0 && relativeFrameWithPostmount < (s.postmountDisplay ?? 0) && !isInRange;
24191
24483
  const { marginLeft, width: width2, naturalWidth, premountWidth, postmountWidth } = useMemo121(() => {
24192
24484
  return getTimelineSequenceLayout({
24193
- durationInFrames: s.loopDisplay ? s.loopDisplay.durationInFrames * s.loopDisplay.numberOfTimes : s.duration,
24485
+ durationInFrames: displayDurationInFrames,
24194
24486
  startFrom: s.loopDisplay ? s.from + s.loopDisplay.startOffset : s.from,
24195
24487
  startFromMedia: s.type === "sequence" || s.type === "image" ? 0 : s.startMediaFrom,
24196
- maxMediaDuration,
24488
+ maxMediaDuration: effectiveMaxMediaDuration,
24197
24489
  video,
24198
24490
  windowWidth,
24199
24491
  premountDisplay: s.premountDisplay,
24200
24492
  postmountDisplay: s.postmountDisplay
24201
24493
  });
24202
- }, [maxMediaDuration, s, video, windowWidth]);
24494
+ }, [
24495
+ displayDurationInFrames,
24496
+ effectiveMaxMediaDuration,
24497
+ s,
24498
+ video,
24499
+ windowWidth
24500
+ ]);
24203
24501
  const style11 = useMemo121(() => {
24204
24502
  return {
24205
24503
  background: s.type === "audio" ? AUDIO_GRADIENT : s.type === "video" ? VIDEO_GRADIENT : s.type === "image" ? IMAGE_GRADIENT : BLUE,
@@ -24214,7 +24512,7 @@ var Inner4 = ({ s, windowWidth }) => {
24214
24512
  opacity: isInRange ? 1 : 0.5
24215
24513
  };
24216
24514
  }, [isInRange, marginLeft, s.type, width2]);
24217
- if (maxMediaDuration === null) {
24515
+ if (maxMediaDuration === null && !s.loopDisplay) {
24218
24516
  return null;
24219
24517
  }
24220
24518
  return /* @__PURE__ */ jsxs105("div", {
@@ -24257,7 +24555,8 @@ var Inner4 = ({ s, windowWidth }) => {
24257
24555
  startFrom: s.startMediaFrom,
24258
24556
  durationInFrames: s.duration,
24259
24557
  volume: s.volume,
24260
- playbackRate: s.playbackRate
24558
+ playbackRate: s.playbackRate,
24559
+ loopDisplay: s.loopDisplay
24261
24560
  }) : null,
24262
24561
  s.type === "video" ? /* @__PURE__ */ jsx215(TimelineVideoInfo, {
24263
24562
  src: s.src,
@@ -24269,7 +24568,8 @@ var Inner4 = ({ s, windowWidth }) => {
24269
24568
  volume: s.volume,
24270
24569
  doesVolumeChange: s.doesVolumeChange,
24271
24570
  premountWidth: premountWidth ?? 0,
24272
- postmountWidth: postmountWidth ?? 0
24571
+ postmountWidth: postmountWidth ?? 0,
24572
+ loopDisplay: s.loopDisplay
24273
24573
  }) : null,
24274
24574
  s.type === "image" ? /* @__PURE__ */ jsx215(TimelineImageInfo, {
24275
24575
  src: s.src,