@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
@@ -20439,7 +20439,8 @@ var getTimelineNestedLevel = (sequence, allSequences, depth) => {
20439
20439
  if (!parentSequence) {
20440
20440
  throw new Error("has parentId but no parent");
20441
20441
  }
20442
- return getTimelineNestedLevel(parentSequence, allSequences, depth + 1);
20442
+ const parentContributes = parentSequence.showInTimeline;
20443
+ return getTimelineNestedLevel(parentSequence, allSequences, parentContributes ? depth + 1 : depth);
20443
20444
  };
20444
20445
 
20445
20446
  // src/helpers/get-timeline-sequence-hash.ts
@@ -20488,6 +20489,19 @@ var getTimelineSequenceSequenceSortKey = (track, tracks, sameHashes = {}, nonceR
20488
20489
  };
20489
20490
 
20490
20491
  // src/helpers/calculate-timeline.ts
20492
+ var getInheritedLoopDisplay = (sequence, sequences) => {
20493
+ if (sequence.loopDisplay) {
20494
+ return sequence.loopDisplay;
20495
+ }
20496
+ if (!sequence.parent) {
20497
+ return;
20498
+ }
20499
+ const parent = sequences.find((s) => s.id === sequence.parent);
20500
+ if (!parent) {
20501
+ return;
20502
+ }
20503
+ return getInheritedLoopDisplay(parent, sequences);
20504
+ };
20491
20505
  var calculateTimeline = ({
20492
20506
  sequences
20493
20507
  }) => {
@@ -20516,7 +20530,8 @@ var calculateTimeline = ({
20516
20530
  sequence: {
20517
20531
  ...sequence,
20518
20532
  from: visibleStart,
20519
- duration: visibleDuration
20533
+ duration: visibleDuration,
20534
+ loopDisplay: sequence.type === "audio" || sequence.type === "video" ? getInheritedLoopDisplay(sequence, sortedSequences) : sequence.loopDisplay
20520
20535
  },
20521
20536
  depth: getTimelineNestedLevel(sequence, sortedSequences, 0),
20522
20537
  hash: actualHash,
@@ -23152,6 +23167,13 @@ var useMaxMediaDuration = (s, fps) => {
23152
23167
  import { useEffect as useEffect72, useMemo as useMemo119, useRef as useRef43, useState as useState77 } from "react";
23153
23168
  import { Internals as Internals55 } from "remotion";
23154
23169
 
23170
+ // src/make-audio-waveform-worker.ts
23171
+ var makeAudioWaveformWorker = () => {
23172
+ return new Worker(new URL("./audio-waveform-worker.mjs", import.meta.url), {
23173
+ type: "module"
23174
+ });
23175
+ };
23176
+
23155
23177
  // src/components/parse-color.ts
23156
23178
  var colorCache = new Map;
23157
23179
  var parseColor = (color) => {
@@ -23227,12 +23249,107 @@ var drawBars = (canvas, peaks, color, volume, width) => {
23227
23249
 
23228
23250
  // src/components/load-waveform-peaks.ts
23229
23251
  import { ALL_FORMATS as ALL_FORMATS3, AudioSampleSink, Input as Input3, UrlSource as UrlSource3 } from "mediabunny";
23252
+
23253
+ // src/components/waveform-peak-processor.ts
23254
+ var emitWaveformProgress = ({
23255
+ completedPeaks,
23256
+ final,
23257
+ onProgress,
23258
+ peaks,
23259
+ totalPeaks
23260
+ }) => {
23261
+ onProgress?.({
23262
+ peaks,
23263
+ completedPeaks,
23264
+ totalPeaks,
23265
+ final
23266
+ });
23267
+ };
23268
+ var createWaveformPeakProcessor = ({
23269
+ totalPeaks,
23270
+ samplesPerPeak,
23271
+ onProgress,
23272
+ progressIntervalInMs,
23273
+ now
23274
+ }) => {
23275
+ const peaks = new Float32Array(totalPeaks);
23276
+ let peakIndex = 0;
23277
+ let peakMax = 0;
23278
+ let sampleInPeak = 0;
23279
+ let lastProgressAt = 0;
23280
+ let lastProgressPeak = 0;
23281
+ const emitProgress = (force) => {
23282
+ const timestamp = now();
23283
+ if (!force && peakIndex === lastProgressPeak && sampleInPeak === 0) {
23284
+ return;
23285
+ }
23286
+ if (!force && timestamp - lastProgressAt < progressIntervalInMs) {
23287
+ return;
23288
+ }
23289
+ lastProgressAt = timestamp;
23290
+ lastProgressPeak = peakIndex;
23291
+ emitWaveformProgress({
23292
+ peaks,
23293
+ completedPeaks: peakIndex,
23294
+ totalPeaks,
23295
+ final: force,
23296
+ onProgress
23297
+ });
23298
+ };
23299
+ return {
23300
+ peaks,
23301
+ processSampleChunk: (floats, channels) => {
23302
+ const frameCount = Math.floor(floats.length / Math.max(1, channels));
23303
+ for (let frame2 = 0;frame2 < frameCount; frame2++) {
23304
+ let framePeak = 0;
23305
+ for (let channel = 0;channel < channels; channel++) {
23306
+ const sampleIndex = frame2 * channels + channel;
23307
+ const abs = Math.abs(floats[sampleIndex] ?? 0);
23308
+ if (abs > framePeak) {
23309
+ framePeak = abs;
23310
+ }
23311
+ }
23312
+ if (framePeak > peakMax) {
23313
+ peakMax = framePeak;
23314
+ }
23315
+ sampleInPeak++;
23316
+ if (sampleInPeak >= samplesPerPeak) {
23317
+ if (peakIndex < totalPeaks) {
23318
+ peaks[peakIndex] = peakMax;
23319
+ }
23320
+ peakIndex++;
23321
+ peakMax = 0;
23322
+ sampleInPeak = 0;
23323
+ }
23324
+ }
23325
+ emitProgress(false);
23326
+ },
23327
+ finalize: () => {
23328
+ if (sampleInPeak > 0 && peakIndex < totalPeaks) {
23329
+ peaks[peakIndex] = peakMax;
23330
+ peakIndex++;
23331
+ }
23332
+ emitProgress(true);
23333
+ }
23334
+ };
23335
+ };
23336
+
23337
+ // src/components/load-waveform-peaks.ts
23230
23338
  var TARGET_SAMPLE_RATE = 100;
23339
+ var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
23231
23340
  var peaksCache = new Map;
23232
- async function loadWaveformPeaks(url, signal) {
23341
+ async function loadWaveformPeaks(url, signal, options) {
23233
23342
  const cached = peaksCache.get(url);
23234
- if (cached)
23343
+ if (cached) {
23344
+ emitWaveformProgress({
23345
+ peaks: cached,
23346
+ completedPeaks: cached.length,
23347
+ totalPeaks: cached.length,
23348
+ final: true,
23349
+ onProgress: options?.onProgress
23350
+ });
23235
23351
  return cached;
23352
+ }
23236
23353
  const input2 = new Input3({
23237
23354
  formats: ALL_FORMATS3,
23238
23355
  source: new UrlSource3(url)
@@ -23246,11 +23363,14 @@ async function loadWaveformPeaks(url, signal) {
23246
23363
  const durationInSeconds = await audioTrack.computeDuration();
23247
23364
  const totalPeaks = Math.ceil(durationInSeconds * TARGET_SAMPLE_RATE);
23248
23365
  const samplesPerPeak = Math.max(1, Math.floor(sampleRate / TARGET_SAMPLE_RATE));
23249
- const peaks = new Float32Array(totalPeaks);
23250
- let peakIndex = 0;
23251
- let peakMax = 0;
23252
- let sampleInPeak = 0;
23253
23366
  const sink = new AudioSampleSink(audioTrack);
23367
+ const processor = createWaveformPeakProcessor({
23368
+ totalPeaks,
23369
+ samplesPerPeak,
23370
+ onProgress: options?.onProgress,
23371
+ progressIntervalInMs: options?.progressIntervalInMs ?? DEFAULT_PROGRESS_INTERVAL_IN_MS,
23372
+ now: () => Date.now()
23373
+ });
23254
23374
  for await (const sample of sink.samples()) {
23255
23375
  if (signal.aborted) {
23256
23376
  sample.close();
@@ -23263,34 +23383,11 @@ async function loadWaveformPeaks(url, signal) {
23263
23383
  const floats = new Float32Array(bytesNeeded / 4);
23264
23384
  sample.copyTo(floats, { format: "f32", planeIndex: 0 });
23265
23385
  const channels = Math.max(1, sample.numberOfChannels);
23266
- const frames = sample.numberOfFrames;
23267
23386
  sample.close();
23268
- for (let frame2 = 0;frame2 < frames; frame2++) {
23269
- let framePeak = 0;
23270
- for (let channel = 0;channel < channels; channel++) {
23271
- const sampleIndex = frame2 * channels + channel;
23272
- const abs = Math.abs(floats[sampleIndex] ?? 0);
23273
- if (abs > framePeak) {
23274
- framePeak = abs;
23275
- }
23276
- }
23277
- if (framePeak > peakMax) {
23278
- peakMax = framePeak;
23279
- }
23280
- sampleInPeak++;
23281
- if (sampleInPeak >= samplesPerPeak) {
23282
- if (peakIndex < totalPeaks) {
23283
- peaks[peakIndex] = peakMax;
23284
- }
23285
- peakIndex++;
23286
- peakMax = 0;
23287
- sampleInPeak = 0;
23288
- }
23289
- }
23290
- }
23291
- if (sampleInPeak > 0 && peakIndex < totalPeaks) {
23292
- peaks[peakIndex] = peakMax;
23387
+ processor.processSampleChunk(floats, channels);
23293
23388
  }
23389
+ processor.finalize();
23390
+ const { peaks } = processor;
23294
23391
  peaksCache.set(url, peaks);
23295
23392
  return peaks;
23296
23393
  } finally {
@@ -23298,8 +23395,50 @@ async function loadWaveformPeaks(url, signal) {
23298
23395
  }
23299
23396
  }
23300
23397
 
23398
+ // src/components/looped-media-timeline.ts
23399
+ var shouldTileLoopDisplay = (loopDisplay) => {
23400
+ return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
23401
+ };
23402
+ var getLoopDisplayWidth = ({
23403
+ visualizationWidth,
23404
+ loopDisplay
23405
+ }) => {
23406
+ if (!shouldTileLoopDisplay(loopDisplay)) {
23407
+ return visualizationWidth;
23408
+ }
23409
+ return visualizationWidth / loopDisplay.numberOfTimes;
23410
+ };
23411
+
23412
+ // src/components/slice-waveform-peaks.ts
23413
+ var sliceWaveformPeaks = ({
23414
+ durationInFrames,
23415
+ fps,
23416
+ peaks,
23417
+ playbackRate,
23418
+ startFrom
23419
+ }) => {
23420
+ if (peaks.length === 0) {
23421
+ return peaks;
23422
+ }
23423
+ const startTimeInSeconds = startFrom / fps;
23424
+ const durationInSeconds = durationInFrames / fps * playbackRate;
23425
+ const startPeakIndex = Math.floor(startTimeInSeconds * TARGET_SAMPLE_RATE);
23426
+ const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * TARGET_SAMPLE_RATE);
23427
+ return peaks.subarray(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
23428
+ };
23429
+
23301
23430
  // src/components/AudioWaveform.tsx
23302
23431
  import { jsx as jsx209, jsxs as jsxs101 } from "react/jsx-runtime";
23432
+ var EMPTY_PEAKS = new Float32Array(0);
23433
+ var canRetryCanvasTransfer = (err) => {
23434
+ return err instanceof DOMException && err.name === "InvalidStateError";
23435
+ };
23436
+ var canUseAudioWaveformWorker = () => {
23437
+ if (typeof Worker === "undefined" || typeof OffscreenCanvas === "undefined" || typeof HTMLCanvasElement === "undefined") {
23438
+ return false;
23439
+ }
23440
+ return "transferControlToOffscreen" in HTMLCanvasElement.prototype;
23441
+ };
23303
23442
  var container42 = {
23304
23443
  display: "flex",
23305
23444
  flexDirection: "row",
@@ -23318,11 +23457,41 @@ var errorMessage = {
23318
23457
  opacity: 0.75
23319
23458
  };
23320
23459
  var waveformCanvasStyle = {
23321
- pointerEvents: "none"
23460
+ pointerEvents: "none",
23461
+ width: "100%",
23462
+ height: "100%"
23322
23463
  };
23323
23464
  var volumeCanvasStyle = {
23324
23465
  position: "absolute"
23325
23466
  };
23467
+ var drawLoopedWaveform = ({
23468
+ canvas,
23469
+ peaks,
23470
+ volume,
23471
+ visualizationWidth,
23472
+ loopWidth
23473
+ }) => {
23474
+ const h = canvas.height;
23475
+ const w = Math.ceil(visualizationWidth);
23476
+ const targetCanvas = document.createElement("canvas");
23477
+ targetCanvas.width = Math.max(1, Math.ceil(loopWidth));
23478
+ targetCanvas.height = h;
23479
+ drawBars(targetCanvas, peaks, "rgba(255, 255, 255, 0.6)", volume, targetCanvas.width);
23480
+ canvas.width = w;
23481
+ canvas.height = h;
23482
+ const ctx = canvas.getContext("2d");
23483
+ if (!ctx) {
23484
+ throw new Error("Failed to get canvas context");
23485
+ }
23486
+ const pattern = ctx.createPattern(targetCanvas, "repeat-x");
23487
+ if (!pattern) {
23488
+ return;
23489
+ }
23490
+ pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
23491
+ ctx.clearRect(0, 0, w, h);
23492
+ ctx.fillStyle = pattern;
23493
+ ctx.fillRect(0, 0, w, h);
23494
+ };
23326
23495
  var AudioWaveform = ({
23327
23496
  src,
23328
23497
  startFrom,
@@ -23330,10 +23499,13 @@ var AudioWaveform = ({
23330
23499
  visualizationWidth,
23331
23500
  volume,
23332
23501
  doesVolumeChange,
23333
- playbackRate
23502
+ playbackRate,
23503
+ loopDisplay
23334
23504
  }) => {
23335
23505
  const [peaks, setPeaks] = useState77(null);
23336
23506
  const [error, setError] = useState77(null);
23507
+ const [waveformCanvasKey, setWaveformCanvasKey] = useState77(0);
23508
+ const canUseWorkerPath = useMemo119(() => canUseAudioWaveformWorker(), []);
23337
23509
  const vidConf = Internals55.useUnsafeVideoConfig();
23338
23510
  if (vidConf === null) {
23339
23511
  throw new Error("Expected video config");
@@ -23341,8 +23513,15 @@ var AudioWaveform = ({
23341
23513
  const containerRef = useRef43(null);
23342
23514
  const waveformCanvas = useRef43(null);
23343
23515
  const volumeCanvas = useRef43(null);
23516
+ const waveformWorker = useRef43(null);
23517
+ const hasTransferredCanvas = useRef43(false);
23518
+ const latestRequestId = useRef43(0);
23344
23519
  useEffect72(() => {
23520
+ if (canUseWorkerPath) {
23521
+ return;
23522
+ }
23345
23523
  const controller = new AbortController;
23524
+ setPeaks(null);
23346
23525
  setError(null);
23347
23526
  loadWaveformPeaks(src, controller.signal).then((p) => {
23348
23527
  if (!controller.signal.aborted) {
@@ -23354,30 +23533,127 @@ var AudioWaveform = ({
23354
23533
  }
23355
23534
  });
23356
23535
  return () => controller.abort();
23357
- }, [src]);
23536
+ }, [canUseWorkerPath, src]);
23537
+ useEffect72(() => {
23538
+ if (!canUseWorkerPath) {
23539
+ return;
23540
+ }
23541
+ const canvasElement = waveformCanvas.current;
23542
+ if (!canvasElement || hasTransferredCanvas.current) {
23543
+ return;
23544
+ }
23545
+ const worker = makeAudioWaveformWorker();
23546
+ waveformWorker.current = worker;
23547
+ worker.addEventListener("message", (event) => {
23548
+ if (event.data.type === "error") {
23549
+ if (event.data.requestId !== latestRequestId.current) {
23550
+ return;
23551
+ }
23552
+ setError(new Error(event.data.message));
23553
+ }
23554
+ });
23555
+ let offscreen;
23556
+ try {
23557
+ offscreen = canvasElement.transferControlToOffscreen();
23558
+ } catch (err) {
23559
+ worker.terminate();
23560
+ waveformWorker.current = null;
23561
+ if (canRetryCanvasTransfer(err)) {
23562
+ setWaveformCanvasKey((key4) => key4 + 1);
23563
+ return;
23564
+ }
23565
+ throw err;
23566
+ }
23567
+ hasTransferredCanvas.current = true;
23568
+ worker.postMessage({ type: "init", canvas: offscreen }, [offscreen]);
23569
+ return () => {
23570
+ worker.postMessage({ type: "dispose" });
23571
+ worker.terminate();
23572
+ waveformWorker.current = null;
23573
+ hasTransferredCanvas.current = false;
23574
+ };
23575
+ }, [canUseWorkerPath, waveformCanvasKey]);
23358
23576
  const portionPeaks = useMemo119(() => {
23359
- if (!peaks || peaks.length === 0) {
23577
+ if (canUseWorkerPath || !peaks) {
23360
23578
  return null;
23361
23579
  }
23362
- const startTimeInSeconds = startFrom / vidConf.fps;
23363
- const durationInSeconds = durationInFrames / vidConf.fps * playbackRate;
23364
- const startPeakIndex = Math.floor(startTimeInSeconds * TARGET_SAMPLE_RATE);
23365
- const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * TARGET_SAMPLE_RATE);
23366
- return peaks.slice(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
23367
- }, [peaks, startFrom, durationInFrames, vidConf.fps, playbackRate]);
23580
+ return sliceWaveformPeaks({
23581
+ durationInFrames: shouldTileLoopDisplay(loopDisplay) ? loopDisplay.durationInFrames : durationInFrames,
23582
+ fps: vidConf.fps,
23583
+ peaks,
23584
+ playbackRate,
23585
+ startFrom
23586
+ });
23587
+ }, [
23588
+ canUseWorkerPath,
23589
+ durationInFrames,
23590
+ loopDisplay,
23591
+ peaks,
23592
+ playbackRate,
23593
+ startFrom,
23594
+ vidConf.fps
23595
+ ]);
23368
23596
  useEffect72(() => {
23369
23597
  const { current: canvasElement } = waveformCanvas;
23370
23598
  const { current: containerElement } = containerRef;
23371
- if (!canvasElement || !containerElement || !portionPeaks || portionPeaks.length === 0) {
23599
+ if (!canvasElement || !containerElement) {
23372
23600
  return;
23373
23601
  }
23374
23602
  const h = containerElement.clientHeight;
23375
23603
  const w = Math.ceil(visualizationWidth);
23604
+ const vol = typeof volume === "number" ? volume : 1;
23605
+ if (canUseWorkerPath) {
23606
+ const worker = waveformWorker.current;
23607
+ if (!worker || !hasTransferredCanvas.current) {
23608
+ return;
23609
+ }
23610
+ latestRequestId.current += 1;
23611
+ setError(null);
23612
+ const message = {
23613
+ type: "render",
23614
+ requestId: latestRequestId.current,
23615
+ src,
23616
+ width: w,
23617
+ height: h,
23618
+ volume: vol,
23619
+ startFrom,
23620
+ durationInFrames,
23621
+ fps: vidConf.fps,
23622
+ playbackRate,
23623
+ loopDisplay
23624
+ };
23625
+ worker.postMessage(message);
23626
+ return;
23627
+ }
23376
23628
  canvasElement.width = w;
23377
23629
  canvasElement.height = h;
23378
- const vol = typeof volume === "number" ? volume : 1;
23379
- drawBars(canvasElement, portionPeaks, "rgba(255, 255, 255, 0.6)", vol, w);
23380
- }, [portionPeaks, visualizationWidth, volume]);
23630
+ if (shouldTileLoopDisplay(loopDisplay)) {
23631
+ drawLoopedWaveform({
23632
+ canvas: canvasElement,
23633
+ peaks: portionPeaks ?? EMPTY_PEAKS,
23634
+ volume: vol,
23635
+ visualizationWidth,
23636
+ loopWidth: getLoopDisplayWidth({
23637
+ visualizationWidth,
23638
+ loopDisplay
23639
+ })
23640
+ });
23641
+ } else {
23642
+ drawBars(canvasElement, portionPeaks ?? EMPTY_PEAKS, "rgba(255, 255, 255, 0.6)", vol, w);
23643
+ }
23644
+ }, [
23645
+ canUseWorkerPath,
23646
+ durationInFrames,
23647
+ loopDisplay,
23648
+ playbackRate,
23649
+ portionPeaks,
23650
+ src,
23651
+ startFrom,
23652
+ vidConf.fps,
23653
+ visualizationWidth,
23654
+ volume,
23655
+ waveformCanvasKey
23656
+ ]);
23381
23657
  useEffect72(() => {
23382
23658
  const { current: volumeCanvasElement } = volumeCanvas;
23383
23659
  const { current: containerElement } = containerRef;
@@ -23419,7 +23695,7 @@ var AudioWaveform = ({
23419
23695
  })
23420
23696
  });
23421
23697
  }
23422
- if (!peaks) {
23698
+ if (!canUseWorkerPath && !peaks) {
23423
23699
  return null;
23424
23700
  }
23425
23701
  return /* @__PURE__ */ jsxs101("div", {
@@ -23429,7 +23705,7 @@ var AudioWaveform = ({
23429
23705
  /* @__PURE__ */ jsx209("canvas", {
23430
23706
  ref: waveformCanvas,
23431
23707
  style: waveformCanvasStyle
23432
- }),
23708
+ }, waveformCanvasKey),
23433
23709
  /* @__PURE__ */ jsx209("canvas", {
23434
23710
  ref: volumeCanvas,
23435
23711
  style: volumeCanvasStyle
@@ -23452,7 +23728,8 @@ var width = {
23452
23728
  position: "relative"
23453
23729
  };
23454
23730
  var icon4 = {
23455
- height: 12
23731
+ height: 12,
23732
+ filter: "drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8))"
23456
23733
  };
23457
23734
  var Icon = () => /* @__PURE__ */ jsx210("svg", {
23458
23735
  viewBox: "0 0 512 512",
@@ -23462,44 +23739,23 @@ var Icon = () => /* @__PURE__ */ jsx210("svg", {
23462
23739
  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"
23463
23740
  })
23464
23741
  });
23465
- var topLine = {
23466
- top: 0,
23467
- height: 2,
23468
- width: 1,
23469
- background: LIGHT_COLOR
23470
- };
23471
- var bottomLine = {
23472
- top: 0,
23473
- height: 2,
23742
+ var verticalLine = {
23743
+ height: "100%",
23474
23744
  width: 1,
23475
- background: LIGHT_COLOR
23476
- };
23477
- var topContainer = {
23478
- justifyContent: "flex-start",
23479
- alignItems: "center"
23745
+ background: "rgb(255,255,255, 0.5)"
23480
23746
  };
23481
23747
  var centerContainer = {
23482
23748
  justifyContent: "center",
23483
23749
  alignItems: "center"
23484
23750
  };
23485
- var bottomContainer = {
23486
- justifyContent: "flex-end",
23487
- alignItems: "center"
23488
- };
23489
23751
  var LoopedIndicator = () => {
23490
23752
  return /* @__PURE__ */ jsxs102("div", {
23491
23753
  style: width,
23492
23754
  children: [
23493
23755
  /* @__PURE__ */ jsx210(AbsoluteFill3, {
23494
- style: topContainer,
23495
- children: /* @__PURE__ */ jsx210("div", {
23496
- style: topLine
23497
- })
23498
- }),
23499
- /* @__PURE__ */ jsx210(AbsoluteFill3, {
23500
- style: bottomContainer,
23756
+ style: centerContainer,
23501
23757
  children: /* @__PURE__ */ jsx210("div", {
23502
- style: bottomLine
23758
+ style: verticalLine
23503
23759
  })
23504
23760
  }),
23505
23761
  /* @__PURE__ */ jsx210(AbsoluteFill3, {
@@ -23971,7 +24227,8 @@ var TimelineVideoInfo = ({
23971
24227
  volume,
23972
24228
  doesVolumeChange,
23973
24229
  premountWidth,
23974
- postmountWidth
24230
+ postmountWidth,
24231
+ loopDisplay
23975
24232
  }) => {
23976
24233
  const { fps } = useVideoConfig5();
23977
24234
  const ref2 = useRef45(null);
@@ -23994,25 +24251,54 @@ var TimelineVideoInfo = ({
23994
24251
  return;
23995
24252
  }
23996
24253
  current.appendChild(canvas);
24254
+ const loopWidth = getLoopDisplayWidth({
24255
+ visualizationWidth: naturalWidth,
24256
+ loopDisplay
24257
+ });
24258
+ const shouldRepeatVideo = shouldTileLoopDisplay(loopDisplay);
24259
+ const targetCanvas = shouldRepeatVideo ? document.createElement("canvas") : canvas;
24260
+ targetCanvas.width = shouldRepeatVideo ? Math.max(1, Math.ceil(loopWidth)) : canvas.width;
24261
+ targetCanvas.height = canvas.height;
24262
+ const targetCtx = shouldRepeatVideo ? targetCanvas.getContext("2d") : ctx;
24263
+ if (!targetCtx) {
24264
+ current.removeChild(canvas);
24265
+ return;
24266
+ }
24267
+ const repeatTarget = () => {
24268
+ if (!shouldRepeatVideo) {
24269
+ return;
24270
+ }
24271
+ const pattern = ctx.createPattern(targetCanvas, "repeat-x");
24272
+ if (!pattern) {
24273
+ return;
24274
+ }
24275
+ pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
24276
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
24277
+ ctx.fillStyle = pattern;
24278
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
24279
+ };
23997
24280
  const filledSlots = new Map;
23998
24281
  const fromSeconds = trimBefore / fps;
23999
- const toSeconds = fromSeconds + durationInFrames * playbackRate / fps;
24282
+ const visibleDurationInFrames = shouldRepeatVideo && loopDisplay ? loopDisplay.durationInFrames : durationInFrames;
24283
+ const toSeconds = fromSeconds + visibleDurationInFrames * playbackRate / fps;
24284
+ const targetWidth = shouldRepeatVideo ? targetCanvas.width : naturalWidth;
24000
24285
  if (aspectRatio.current !== null) {
24001
24286
  ensureSlots({
24002
24287
  filledSlots,
24003
- naturalWidth,
24288
+ naturalWidth: targetWidth,
24004
24289
  fromSeconds,
24005
24290
  toSeconds,
24006
24291
  aspectRatio: aspectRatio.current
24007
24292
  });
24008
24293
  fillWithCachedFrames({
24009
- ctx,
24010
- naturalWidth,
24294
+ ctx: targetCtx,
24295
+ naturalWidth: targetWidth,
24011
24296
  filledSlots,
24012
24297
  src,
24013
24298
  segmentDuration: toSeconds - fromSeconds,
24014
24299
  fromSeconds
24015
24300
  });
24301
+ repeatTarget();
24016
24302
  const unfilled = Array.from(filledSlots.keys()).filter((timestamp) => !filledSlots.get(timestamp));
24017
24303
  if (unfilled.length === 0) {
24018
24304
  return () => {
@@ -24030,7 +24316,7 @@ var TimelineVideoInfo = ({
24030
24316
  filledSlots,
24031
24317
  fromSeconds,
24032
24318
  toSeconds,
24033
- naturalWidth,
24319
+ naturalWidth: targetWidth,
24034
24320
  aspectRatio: aspectRatio.current
24035
24321
  });
24036
24322
  return Array.from(filledSlots.keys()).map((timestamp) => timestamp / WEBCODECS_TIMESCALE);
@@ -24058,17 +24344,18 @@ var TimelineVideoInfo = ({
24058
24344
  filledSlots,
24059
24345
  fromSeconds,
24060
24346
  toSeconds,
24061
- naturalWidth,
24347
+ naturalWidth: targetWidth,
24062
24348
  aspectRatio: aspectRatio.current
24063
24349
  });
24064
24350
  fillFrameWhereItFits({
24065
- ctx,
24351
+ ctx: targetCtx,
24066
24352
  filledSlots,
24067
- visualizationWidth: naturalWidth,
24353
+ visualizationWidth: targetWidth,
24068
24354
  frame: transformed,
24069
24355
  segmentDuration: toSeconds - fromSeconds,
24070
24356
  fromSeconds
24071
24357
  });
24358
+ repeatTarget();
24072
24359
  } catch (e) {
24073
24360
  if (frame2) {
24074
24361
  frame2.close();
@@ -24084,13 +24371,14 @@ var TimelineVideoInfo = ({
24084
24371
  return;
24085
24372
  }
24086
24373
  fillWithCachedFrames({
24087
- ctx,
24088
- naturalWidth,
24374
+ ctx: targetCtx,
24375
+ naturalWidth: targetWidth,
24089
24376
  filledSlots,
24090
24377
  src,
24091
24378
  segmentDuration: toSeconds - fromSeconds,
24092
24379
  fromSeconds
24093
24380
  });
24381
+ repeatTarget();
24094
24382
  }).catch((e) => {
24095
24383
  setError(e);
24096
24384
  });
@@ -24102,6 +24390,7 @@ var TimelineVideoInfo = ({
24102
24390
  durationInFrames,
24103
24391
  error,
24104
24392
  fps,
24393
+ loopDisplay,
24105
24394
  naturalWidth,
24106
24395
  playbackRate,
24107
24396
  src,
@@ -24133,7 +24422,8 @@ var TimelineVideoInfo = ({
24133
24422
  durationInFrames,
24134
24423
  volume,
24135
24424
  doesVolumeChange,
24136
- playbackRate
24425
+ playbackRate,
24426
+ loopDisplay
24137
24427
  })
24138
24428
  })
24139
24429
  ]
@@ -24158,29 +24448,37 @@ var TimelineSequence = ({ s }) => {
24158
24448
  var Inner4 = ({ s, windowWidth }) => {
24159
24449
  const video = Internals56.useVideo();
24160
24450
  const maxMediaDuration = useMaxMediaDuration(s, video?.fps ?? 30);
24451
+ const effectiveMaxMediaDuration = s.loopDisplay ? null : maxMediaDuration;
24161
24452
  if (!video) {
24162
24453
  throw new TypeError("Expected video config");
24163
24454
  }
24164
24455
  const frame2 = useCurrentFrame2();
24165
24456
  const relativeFrame = frame2 - s.from;
24457
+ const displayDurationInFrames = s.loopDisplay ? s.loopDisplay.durationInFrames * s.loopDisplay.numberOfTimes : s.duration;
24166
24458
  const relativeFrameWithPremount = relativeFrame + (s.premountDisplay ?? 0);
24167
- const relativeFrameWithPostmount = relativeFrame - s.duration;
24459
+ const relativeFrameWithPostmount = relativeFrame - displayDurationInFrames;
24168
24460
  const roundedFrame = Math.round(relativeFrame * 100) / 100;
24169
- const isInRange = relativeFrame >= 0 && relativeFrame < s.duration;
24170
- const isPremounting = relativeFrameWithPremount >= 0 && relativeFrameWithPremount < s.duration && !isInRange;
24461
+ const isInRange = relativeFrame >= 0 && relativeFrame < displayDurationInFrames;
24462
+ const isPremounting = relativeFrameWithPremount >= 0 && relativeFrameWithPremount < displayDurationInFrames && !isInRange;
24171
24463
  const isPostmounting = relativeFrameWithPostmount >= 0 && relativeFrameWithPostmount < (s.postmountDisplay ?? 0) && !isInRange;
24172
24464
  const { marginLeft, width: width2, naturalWidth, premountWidth, postmountWidth } = useMemo121(() => {
24173
24465
  return getTimelineSequenceLayout({
24174
- durationInFrames: s.loopDisplay ? s.loopDisplay.durationInFrames * s.loopDisplay.numberOfTimes : s.duration,
24466
+ durationInFrames: displayDurationInFrames,
24175
24467
  startFrom: s.loopDisplay ? s.from + s.loopDisplay.startOffset : s.from,
24176
24468
  startFromMedia: s.type === "sequence" || s.type === "image" ? 0 : s.startMediaFrom,
24177
- maxMediaDuration,
24469
+ maxMediaDuration: effectiveMaxMediaDuration,
24178
24470
  video,
24179
24471
  windowWidth,
24180
24472
  premountDisplay: s.premountDisplay,
24181
24473
  postmountDisplay: s.postmountDisplay
24182
24474
  });
24183
- }, [maxMediaDuration, s, video, windowWidth]);
24475
+ }, [
24476
+ displayDurationInFrames,
24477
+ effectiveMaxMediaDuration,
24478
+ s,
24479
+ video,
24480
+ windowWidth
24481
+ ]);
24184
24482
  const style11 = useMemo121(() => {
24185
24483
  return {
24186
24484
  background: s.type === "audio" ? AUDIO_GRADIENT : s.type === "video" ? VIDEO_GRADIENT : s.type === "image" ? IMAGE_GRADIENT : BLUE,
@@ -24195,7 +24493,7 @@ var Inner4 = ({ s, windowWidth }) => {
24195
24493
  opacity: isInRange ? 1 : 0.5
24196
24494
  };
24197
24495
  }, [isInRange, marginLeft, s.type, width2]);
24198
- if (maxMediaDuration === null) {
24496
+ if (maxMediaDuration === null && !s.loopDisplay) {
24199
24497
  return null;
24200
24498
  }
24201
24499
  return /* @__PURE__ */ jsxs105("div", {
@@ -24238,7 +24536,8 @@ var Inner4 = ({ s, windowWidth }) => {
24238
24536
  startFrom: s.startMediaFrom,
24239
24537
  durationInFrames: s.duration,
24240
24538
  volume: s.volume,
24241
- playbackRate: s.playbackRate
24539
+ playbackRate: s.playbackRate,
24540
+ loopDisplay: s.loopDisplay
24242
24541
  }) : null,
24243
24542
  s.type === "video" ? /* @__PURE__ */ jsx215(TimelineVideoInfo, {
24244
24543
  src: s.src,
@@ -24250,7 +24549,8 @@ var Inner4 = ({ s, windowWidth }) => {
24250
24549
  volume: s.volume,
24251
24550
  doesVolumeChange: s.doesVolumeChange,
24252
24551
  premountWidth: premountWidth ?? 0,
24253
- postmountWidth: postmountWidth ?? 0
24552
+ postmountWidth: postmountWidth ?? 0,
24553
+ loopDisplay: s.loopDisplay
24254
24554
  }) : null,
24255
24555
  s.type === "image" ? /* @__PURE__ */ jsx215(TimelineImageInfo, {
24256
24556
  src: s.src,