@remotion/studio 4.0.452 → 4.0.454

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 (34) 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 +168 -18
  5. package/dist/components/CurrentAsset.js +13 -5
  6. package/dist/components/Timeline/LoopedIndicator.js +5 -19
  7. package/dist/components/Timeline/TimelineSequence.js +18 -10
  8. package/dist/components/Timeline/TimelineVideoInfo.d.ts +2 -0
  9. package/dist/components/Timeline/TimelineVideoInfo.js +51 -12
  10. package/dist/components/audio-waveform-worker-types.d.ts +28 -0
  11. package/dist/components/audio-waveform-worker-types.js +2 -0
  12. package/dist/components/draw-peaks.d.ts +1 -1
  13. package/dist/components/load-waveform-peaks.d.ts +11 -1
  14. package/dist/components/load-waveform-peaks.js +33 -36
  15. package/dist/components/looped-media-timeline.d.ts +6 -0
  16. package/dist/components/looped-media-timeline.js +14 -0
  17. package/dist/components/slice-waveform-peaks.d.ts +7 -0
  18. package/dist/components/slice-waveform-peaks.js +15 -0
  19. package/dist/components/waveform-peak-processor.d.ts +23 -0
  20. package/dist/components/waveform-peak-processor.js +77 -0
  21. package/dist/esm/audio-waveform-worker.mjs +351 -0
  22. package/dist/esm/{chunk-hxr6txpe.js → chunk-g39hwn0a.js} +434 -108
  23. package/dist/esm/internals.mjs +434 -108
  24. package/dist/esm/previewEntry.mjs +434 -108
  25. package/dist/esm/renderEntry.mjs +1 -1
  26. package/dist/helpers/calculate-timeline.js +16 -0
  27. package/dist/helpers/extract-frames.js +12 -3
  28. package/dist/helpers/get-duration-or-compute.d.ts +2 -0
  29. package/dist/helpers/get-duration-or-compute.js +10 -0
  30. package/dist/helpers/get-timeline-nestedness.js +2 -1
  31. package/dist/helpers/use-max-media-duration.js +2 -2
  32. package/dist/make-audio-waveform-worker.d.ts +1 -0
  33. package/dist/make-audio-waveform-worker.js +10 -0
  34. package/package.json +19 -10
@@ -4366,6 +4366,13 @@ import { ALL_FORMATS, Input, UrlSource } from "mediabunny";
4366
4366
  import { useContext as useContext12, useEffect as useEffect14, useMemo as useMemo24, useState as useState17 } from "react";
4367
4367
  import { Internals as Internals9, staticFile } from "remotion";
4368
4368
 
4369
+ // src/helpers/get-duration-or-compute.ts
4370
+ var getDurationOrCompute = async (input) => {
4371
+ return await input.getDurationFromMetadata(undefined, {
4372
+ skipLiveWait: true
4373
+ }) ?? input.computeDuration(undefined, { skipLiveWait: true });
4374
+ };
4375
+
4369
4376
  // src/components/use-static-files.ts
4370
4377
  import React26, { createContext as createContext10, useContext as useContext11, useEffect as useEffect13, useState as useState16 } from "react";
4371
4378
  import { useRemotionEnvironment } from "remotion";
@@ -4509,15 +4516,21 @@ var CurrentAsset = () => {
4509
4516
  source: new UrlSource(url)
4510
4517
  });
4511
4518
  Promise.all([
4512
- input.computeDuration(),
4519
+ getDurationOrCompute(input),
4513
4520
  input.getFormat(),
4514
4521
  input.getPrimaryVideoTrack()
4515
- ]).then(([duration, format, videoTrack]) => {
4522
+ ]).then(async ([duration, format, videoTrack]) => {
4523
+ if (videoTrack && await videoTrack.isLive()) {
4524
+ throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + url);
4525
+ }
4526
+ if (videoTrack && await videoTrack.isRelativeToUnixEpoch()) {
4527
+ throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + url);
4528
+ }
4516
4529
  setMediaMetadata({
4517
4530
  duration,
4518
4531
  format: format.name,
4519
- width: videoTrack?.displayWidth ?? null,
4520
- height: videoTrack?.displayHeight ?? null
4532
+ width: videoTrack ? await videoTrack.getDisplayWidth() : null,
4533
+ height: videoTrack ? await videoTrack.getDisplayHeight() : null
4521
4534
  });
4522
4535
  }).catch(() => {});
4523
4536
  return () => {
@@ -20439,7 +20452,8 @@ var getTimelineNestedLevel = (sequence, allSequences, depth) => {
20439
20452
  if (!parentSequence) {
20440
20453
  throw new Error("has parentId but no parent");
20441
20454
  }
20442
- return getTimelineNestedLevel(parentSequence, allSequences, depth + 1);
20455
+ const parentContributes = parentSequence.showInTimeline;
20456
+ return getTimelineNestedLevel(parentSequence, allSequences, parentContributes ? depth + 1 : depth);
20443
20457
  };
20444
20458
 
20445
20459
  // src/helpers/get-timeline-sequence-hash.ts
@@ -20488,6 +20502,19 @@ var getTimelineSequenceSequenceSortKey = (track, tracks, sameHashes = {}, nonceR
20488
20502
  };
20489
20503
 
20490
20504
  // src/helpers/calculate-timeline.ts
20505
+ var getInheritedLoopDisplay = (sequence, sequences) => {
20506
+ if (sequence.loopDisplay) {
20507
+ return sequence.loopDisplay;
20508
+ }
20509
+ if (!sequence.parent) {
20510
+ return;
20511
+ }
20512
+ const parent = sequences.find((s) => s.id === sequence.parent);
20513
+ if (!parent) {
20514
+ return;
20515
+ }
20516
+ return getInheritedLoopDisplay(parent, sequences);
20517
+ };
20491
20518
  var calculateTimeline = ({
20492
20519
  sequences
20493
20520
  }) => {
@@ -20516,7 +20543,8 @@ var calculateTimeline = ({
20516
20543
  sequence: {
20517
20544
  ...sequence,
20518
20545
  from: visibleStart,
20519
- duration: visibleDuration
20546
+ duration: visibleDuration,
20547
+ loopDisplay: sequence.type === "audio" || sequence.type === "video" ? getInheritedLoopDisplay(sequence, sortedSequences) : sequence.loopDisplay
20520
20548
  },
20521
20549
  depth: getTimelineNestedLevel(sequence, sortedSequences, 0),
20522
20550
  hash: actualHash,
@@ -23125,7 +23153,7 @@ var useMaxMediaDuration = (s, fps) => {
23125
23153
  formats: ALL_FORMATS2,
23126
23154
  source: new UrlSource2(src)
23127
23155
  });
23128
- input2.computeDuration().then((duration) => {
23156
+ getDurationOrCompute(input2).then((duration) => {
23129
23157
  cache.set(src, Math.floor(duration * fps));
23130
23158
  setMaxMediaDuration(Math.floor(duration * fps));
23131
23159
  }).catch((e) => {
@@ -23152,6 +23180,13 @@ var useMaxMediaDuration = (s, fps) => {
23152
23180
  import { useEffect as useEffect72, useMemo as useMemo119, useRef as useRef43, useState as useState77 } from "react";
23153
23181
  import { Internals as Internals55 } from "remotion";
23154
23182
 
23183
+ // src/make-audio-waveform-worker.ts
23184
+ var makeAudioWaveformWorker = () => {
23185
+ return new Worker(new URL("./audio-waveform-worker.mjs", import.meta.url), {
23186
+ type: "module"
23187
+ });
23188
+ };
23189
+
23155
23190
  // src/components/parse-color.ts
23156
23191
  var colorCache = new Map;
23157
23192
  var parseColor = (color) => {
@@ -23227,12 +23262,107 @@ var drawBars = (canvas, peaks, color, volume, width) => {
23227
23262
 
23228
23263
  // src/components/load-waveform-peaks.ts
23229
23264
  import { ALL_FORMATS as ALL_FORMATS3, AudioSampleSink, Input as Input3, UrlSource as UrlSource3 } from "mediabunny";
23265
+
23266
+ // src/components/waveform-peak-processor.ts
23267
+ var emitWaveformProgress = ({
23268
+ completedPeaks,
23269
+ final,
23270
+ onProgress,
23271
+ peaks,
23272
+ totalPeaks
23273
+ }) => {
23274
+ onProgress?.({
23275
+ peaks,
23276
+ completedPeaks,
23277
+ totalPeaks,
23278
+ final
23279
+ });
23280
+ };
23281
+ var createWaveformPeakProcessor = ({
23282
+ totalPeaks,
23283
+ samplesPerPeak,
23284
+ onProgress,
23285
+ progressIntervalInMs,
23286
+ now
23287
+ }) => {
23288
+ const peaks = new Float32Array(totalPeaks);
23289
+ let peakIndex = 0;
23290
+ let peakMax = 0;
23291
+ let sampleInPeak = 0;
23292
+ let lastProgressAt = 0;
23293
+ let lastProgressPeak = 0;
23294
+ const emitProgress = (force) => {
23295
+ const timestamp = now();
23296
+ if (!force && peakIndex === lastProgressPeak && sampleInPeak === 0) {
23297
+ return;
23298
+ }
23299
+ if (!force && timestamp - lastProgressAt < progressIntervalInMs) {
23300
+ return;
23301
+ }
23302
+ lastProgressAt = timestamp;
23303
+ lastProgressPeak = peakIndex;
23304
+ emitWaveformProgress({
23305
+ peaks,
23306
+ completedPeaks: peakIndex,
23307
+ totalPeaks,
23308
+ final: force,
23309
+ onProgress
23310
+ });
23311
+ };
23312
+ return {
23313
+ peaks,
23314
+ processSampleChunk: (floats, channels) => {
23315
+ const frameCount = Math.floor(floats.length / Math.max(1, channels));
23316
+ for (let frame2 = 0;frame2 < frameCount; frame2++) {
23317
+ let framePeak = 0;
23318
+ for (let channel = 0;channel < channels; channel++) {
23319
+ const sampleIndex = frame2 * channels + channel;
23320
+ const abs = Math.abs(floats[sampleIndex] ?? 0);
23321
+ if (abs > framePeak) {
23322
+ framePeak = abs;
23323
+ }
23324
+ }
23325
+ if (framePeak > peakMax) {
23326
+ peakMax = framePeak;
23327
+ }
23328
+ sampleInPeak++;
23329
+ if (sampleInPeak >= samplesPerPeak) {
23330
+ if (peakIndex < totalPeaks) {
23331
+ peaks[peakIndex] = peakMax;
23332
+ }
23333
+ peakIndex++;
23334
+ peakMax = 0;
23335
+ sampleInPeak = 0;
23336
+ }
23337
+ }
23338
+ emitProgress(false);
23339
+ },
23340
+ finalize: () => {
23341
+ if (sampleInPeak > 0 && peakIndex < totalPeaks) {
23342
+ peaks[peakIndex] = peakMax;
23343
+ peakIndex++;
23344
+ }
23345
+ emitProgress(true);
23346
+ }
23347
+ };
23348
+ };
23349
+
23350
+ // src/components/load-waveform-peaks.ts
23230
23351
  var TARGET_SAMPLE_RATE = 100;
23352
+ var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
23231
23353
  var peaksCache = new Map;
23232
- async function loadWaveformPeaks(url, signal) {
23354
+ async function loadWaveformPeaks(url, signal, options) {
23233
23355
  const cached = peaksCache.get(url);
23234
- if (cached)
23356
+ if (cached) {
23357
+ emitWaveformProgress({
23358
+ peaks: cached,
23359
+ completedPeaks: cached.length,
23360
+ totalPeaks: cached.length,
23361
+ final: true,
23362
+ onProgress: options?.onProgress
23363
+ });
23235
23364
  return cached;
23365
+ }
23236
23366
  const input2 = new Input3({
23237
23367
  formats: ALL_FORMATS3,
23238
23368
  source: new UrlSource3(url)
@@ -23242,15 +23372,24 @@ async function loadWaveformPeaks(url, signal) {
23242
23372
  if (!audioTrack) {
23243
23373
  return new Float32Array(0);
23244
23374
  }
23245
- const { sampleRate } = audioTrack;
23246
- const durationInSeconds = await audioTrack.computeDuration();
23375
+ if (await audioTrack.isLive()) {
23376
+ throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + url);
23377
+ }
23378
+ if (await audioTrack.isRelativeToUnixEpoch()) {
23379
+ throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + url);
23380
+ }
23381
+ const sampleRate = await audioTrack.getSampleRate();
23382
+ const durationInSeconds = await audioTrack.getDurationFromMetadata({ skipLiveWait: true }) ?? await audioTrack.computeDuration({ skipLiveWait: true });
23247
23383
  const totalPeaks = Math.ceil(durationInSeconds * TARGET_SAMPLE_RATE);
23248
23384
  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
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
+ });
23254
23393
  for await (const sample of sink.samples()) {
23255
23394
  if (signal.aborted) {
23256
23395
  sample.close();
@@ -23263,34 +23402,11 @@ async function loadWaveformPeaks(url, signal) {
23263
23402
  const floats = new Float32Array(bytesNeeded / 4);
23264
23403
  sample.copyTo(floats, { format: "f32", planeIndex: 0 });
23265
23404
  const channels = Math.max(1, sample.numberOfChannels);
23266
- const frames = sample.numberOfFrames;
23267
23405
  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;
23406
+ processor.processSampleChunk(floats, channels);
23293
23407
  }
23408
+ processor.finalize();
23409
+ const { peaks } = processor;
23294
23410
  peaksCache.set(url, peaks);
23295
23411
  return peaks;
23296
23412
  } finally {
@@ -23298,8 +23414,50 @@ async function loadWaveformPeaks(url, signal) {
23298
23414
  }
23299
23415
  }
23300
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
+
23301
23449
  // src/components/AudioWaveform.tsx
23302
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
+ };
23303
23461
  var container42 = {
23304
23462
  display: "flex",
23305
23463
  flexDirection: "row",
@@ -23318,11 +23476,41 @@ var errorMessage = {
23318
23476
  opacity: 0.75
23319
23477
  };
23320
23478
  var waveformCanvasStyle = {
23321
- pointerEvents: "none"
23479
+ pointerEvents: "none",
23480
+ width: "100%",
23481
+ height: "100%"
23322
23482
  };
23323
23483
  var volumeCanvasStyle = {
23324
23484
  position: "absolute"
23325
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
+ };
23326
23514
  var AudioWaveform = ({
23327
23515
  src,
23328
23516
  startFrom,
@@ -23330,10 +23518,13 @@ var AudioWaveform = ({
23330
23518
  visualizationWidth,
23331
23519
  volume,
23332
23520
  doesVolumeChange,
23333
- playbackRate
23521
+ playbackRate,
23522
+ loopDisplay
23334
23523
  }) => {
23335
23524
  const [peaks, setPeaks] = useState77(null);
23336
23525
  const [error, setError] = useState77(null);
23526
+ const [waveformCanvasKey, setWaveformCanvasKey] = useState77(0);
23527
+ const canUseWorkerPath = useMemo119(() => canUseAudioWaveformWorker(), []);
23337
23528
  const vidConf = Internals55.useUnsafeVideoConfig();
23338
23529
  if (vidConf === null) {
23339
23530
  throw new Error("Expected video config");
@@ -23341,8 +23532,15 @@ var AudioWaveform = ({
23341
23532
  const containerRef = useRef43(null);
23342
23533
  const waveformCanvas = useRef43(null);
23343
23534
  const volumeCanvas = useRef43(null);
23535
+ const waveformWorker = useRef43(null);
23536
+ const hasTransferredCanvas = useRef43(false);
23537
+ const latestRequestId = useRef43(0);
23344
23538
  useEffect72(() => {
23539
+ if (canUseWorkerPath) {
23540
+ return;
23541
+ }
23345
23542
  const controller = new AbortController;
23543
+ setPeaks(null);
23346
23544
  setError(null);
23347
23545
  loadWaveformPeaks(src, controller.signal).then((p) => {
23348
23546
  if (!controller.signal.aborted) {
@@ -23354,30 +23552,127 @@ var AudioWaveform = ({
23354
23552
  }
23355
23553
  });
23356
23554
  return () => controller.abort();
23357
- }, [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]);
23358
23595
  const portionPeaks = useMemo119(() => {
23359
- if (!peaks || peaks.length === 0) {
23596
+ if (canUseWorkerPath || !peaks) {
23360
23597
  return null;
23361
23598
  }
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]);
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
+ ]);
23368
23615
  useEffect72(() => {
23369
23616
  const { current: canvasElement } = waveformCanvas;
23370
23617
  const { current: containerElement } = containerRef;
23371
- if (!canvasElement || !containerElement || !portionPeaks || portionPeaks.length === 0) {
23618
+ if (!canvasElement || !containerElement) {
23372
23619
  return;
23373
23620
  }
23374
23621
  const h = containerElement.clientHeight;
23375
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
+ }
23376
23647
  canvasElement.width = w;
23377
23648
  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]);
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
+ ]);
23381
23676
  useEffect72(() => {
23382
23677
  const { current: volumeCanvasElement } = volumeCanvas;
23383
23678
  const { current: containerElement } = containerRef;
@@ -23411,6 +23706,7 @@ var AudioWaveform = ({
23411
23706
  context.stroke();
23412
23707
  }, [visualizationWidth, volume, doesVolumeChange]);
23413
23708
  if (error) {
23709
+ console.error(error);
23414
23710
  return /* @__PURE__ */ jsx209("div", {
23415
23711
  style: container42,
23416
23712
  children: /* @__PURE__ */ jsx209("div", {
@@ -23419,7 +23715,7 @@ var AudioWaveform = ({
23419
23715
  })
23420
23716
  });
23421
23717
  }
23422
- if (!peaks) {
23718
+ if (!canUseWorkerPath && !peaks) {
23423
23719
  return null;
23424
23720
  }
23425
23721
  return /* @__PURE__ */ jsxs101("div", {
@@ -23429,7 +23725,7 @@ var AudioWaveform = ({
23429
23725
  /* @__PURE__ */ jsx209("canvas", {
23430
23726
  ref: waveformCanvas,
23431
23727
  style: waveformCanvasStyle
23432
- }),
23728
+ }, waveformCanvasKey),
23433
23729
  /* @__PURE__ */ jsx209("canvas", {
23434
23730
  ref: volumeCanvas,
23435
23731
  style: volumeCanvasStyle
@@ -23452,7 +23748,8 @@ var width = {
23452
23748
  position: "relative"
23453
23749
  };
23454
23750
  var icon4 = {
23455
- height: 12
23751
+ height: 12,
23752
+ filter: "drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8))"
23456
23753
  };
23457
23754
  var Icon = () => /* @__PURE__ */ jsx210("svg", {
23458
23755
  viewBox: "0 0 512 512",
@@ -23462,44 +23759,23 @@ var Icon = () => /* @__PURE__ */ jsx210("svg", {
23462
23759
  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
23760
  })
23464
23761
  });
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,
23762
+ var verticalLine = {
23763
+ height: "100%",
23474
23764
  width: 1,
23475
- background: LIGHT_COLOR
23476
- };
23477
- var topContainer = {
23478
- justifyContent: "flex-start",
23479
- alignItems: "center"
23765
+ background: "rgb(255,255,255, 0.5)"
23480
23766
  };
23481
23767
  var centerContainer = {
23482
23768
  justifyContent: "center",
23483
23769
  alignItems: "center"
23484
23770
  };
23485
- var bottomContainer = {
23486
- justifyContent: "flex-end",
23487
- alignItems: "center"
23488
- };
23489
23771
  var LoopedIndicator = () => {
23490
23772
  return /* @__PURE__ */ jsxs102("div", {
23491
23773
  style: width,
23492
23774
  children: [
23493
23775
  /* @__PURE__ */ jsx210(AbsoluteFill3, {
23494
- style: topContainer,
23495
- children: /* @__PURE__ */ jsx210("div", {
23496
- style: topLine
23497
- })
23498
- }),
23499
- /* @__PURE__ */ jsx210(AbsoluteFill3, {
23500
- style: bottomContainer,
23776
+ style: centerContainer,
23501
23777
  children: /* @__PURE__ */ jsx210("div", {
23502
- style: bottomLine
23778
+ style: verticalLine
23503
23779
  })
23504
23780
  }),
23505
23781
  /* @__PURE__ */ jsx210(AbsoluteFill3, {
@@ -23642,17 +23918,23 @@ async function extractFrames({
23642
23918
  }
23643
23919
  try {
23644
23920
  const [durationInSeconds, format, videoTrack] = await Promise.all([
23645
- input2.computeDuration(),
23921
+ getDurationOrCompute(input2),
23646
23922
  input2.getFormat(),
23647
23923
  input2.getPrimaryVideoTrack()
23648
23924
  ]);
23649
23925
  if (!videoTrack) {
23650
23926
  throw new Error("No video track found in the input");
23651
23927
  }
23928
+ if (await videoTrack.isLive()) {
23929
+ throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + src);
23930
+ }
23931
+ if (await videoTrack.isRelativeToUnixEpoch()) {
23932
+ throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + src);
23933
+ }
23652
23934
  const timestamps = typeof timestampsInSeconds === "function" ? await timestampsInSeconds({
23653
23935
  track: {
23654
- width: videoTrack.displayWidth,
23655
- height: videoTrack.displayHeight
23936
+ width: await videoTrack.getDisplayWidth(),
23937
+ height: await videoTrack.getDisplayHeight()
23656
23938
  },
23657
23939
  container: format.name,
23658
23940
  durationInSeconds
@@ -23971,7 +24253,8 @@ var TimelineVideoInfo = ({
23971
24253
  volume,
23972
24254
  doesVolumeChange,
23973
24255
  premountWidth,
23974
- postmountWidth
24256
+ postmountWidth,
24257
+ loopDisplay
23975
24258
  }) => {
23976
24259
  const { fps } = useVideoConfig5();
23977
24260
  const ref2 = useRef45(null);
@@ -23994,25 +24277,54 @@ var TimelineVideoInfo = ({
23994
24277
  return;
23995
24278
  }
23996
24279
  current.appendChild(canvas);
24280
+ const loopWidth = getLoopDisplayWidth({
24281
+ visualizationWidth: naturalWidth,
24282
+ loopDisplay
24283
+ });
24284
+ const shouldRepeatVideo = shouldTileLoopDisplay(loopDisplay);
24285
+ const targetCanvas = shouldRepeatVideo ? document.createElement("canvas") : canvas;
24286
+ targetCanvas.width = shouldRepeatVideo ? Math.max(1, Math.ceil(loopWidth)) : canvas.width;
24287
+ targetCanvas.height = canvas.height;
24288
+ const targetCtx = shouldRepeatVideo ? targetCanvas.getContext("2d") : ctx;
24289
+ if (!targetCtx) {
24290
+ current.removeChild(canvas);
24291
+ return;
24292
+ }
24293
+ const repeatTarget = () => {
24294
+ if (!shouldRepeatVideo) {
24295
+ return;
24296
+ }
24297
+ const pattern = ctx.createPattern(targetCanvas, "repeat-x");
24298
+ if (!pattern) {
24299
+ return;
24300
+ }
24301
+ pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
24302
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
24303
+ ctx.fillStyle = pattern;
24304
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
24305
+ };
23997
24306
  const filledSlots = new Map;
23998
24307
  const fromSeconds = trimBefore / fps;
23999
- const toSeconds = fromSeconds + durationInFrames * playbackRate / fps;
24308
+ const visibleDurationInFrames = shouldRepeatVideo && loopDisplay ? loopDisplay.durationInFrames : durationInFrames;
24309
+ const toSeconds = fromSeconds + visibleDurationInFrames * playbackRate / fps;
24310
+ const targetWidth = shouldRepeatVideo ? targetCanvas.width : naturalWidth;
24000
24311
  if (aspectRatio.current !== null) {
24001
24312
  ensureSlots({
24002
24313
  filledSlots,
24003
- naturalWidth,
24314
+ naturalWidth: targetWidth,
24004
24315
  fromSeconds,
24005
24316
  toSeconds,
24006
24317
  aspectRatio: aspectRatio.current
24007
24318
  });
24008
24319
  fillWithCachedFrames({
24009
- ctx,
24010
- naturalWidth,
24320
+ ctx: targetCtx,
24321
+ naturalWidth: targetWidth,
24011
24322
  filledSlots,
24012
24323
  src,
24013
24324
  segmentDuration: toSeconds - fromSeconds,
24014
24325
  fromSeconds
24015
24326
  });
24327
+ repeatTarget();
24016
24328
  const unfilled = Array.from(filledSlots.keys()).filter((timestamp) => !filledSlots.get(timestamp));
24017
24329
  if (unfilled.length === 0) {
24018
24330
  return () => {
@@ -24030,7 +24342,7 @@ var TimelineVideoInfo = ({
24030
24342
  filledSlots,
24031
24343
  fromSeconds,
24032
24344
  toSeconds,
24033
- naturalWidth,
24345
+ naturalWidth: targetWidth,
24034
24346
  aspectRatio: aspectRatio.current
24035
24347
  });
24036
24348
  return Array.from(filledSlots.keys()).map((timestamp) => timestamp / WEBCODECS_TIMESCALE);
@@ -24058,17 +24370,18 @@ var TimelineVideoInfo = ({
24058
24370
  filledSlots,
24059
24371
  fromSeconds,
24060
24372
  toSeconds,
24061
- naturalWidth,
24373
+ naturalWidth: targetWidth,
24062
24374
  aspectRatio: aspectRatio.current
24063
24375
  });
24064
24376
  fillFrameWhereItFits({
24065
- ctx,
24377
+ ctx: targetCtx,
24066
24378
  filledSlots,
24067
- visualizationWidth: naturalWidth,
24379
+ visualizationWidth: targetWidth,
24068
24380
  frame: transformed,
24069
24381
  segmentDuration: toSeconds - fromSeconds,
24070
24382
  fromSeconds
24071
24383
  });
24384
+ repeatTarget();
24072
24385
  } catch (e) {
24073
24386
  if (frame2) {
24074
24387
  frame2.close();
@@ -24084,13 +24397,14 @@ var TimelineVideoInfo = ({
24084
24397
  return;
24085
24398
  }
24086
24399
  fillWithCachedFrames({
24087
- ctx,
24088
- naturalWidth,
24400
+ ctx: targetCtx,
24401
+ naturalWidth: targetWidth,
24089
24402
  filledSlots,
24090
24403
  src,
24091
24404
  segmentDuration: toSeconds - fromSeconds,
24092
24405
  fromSeconds
24093
24406
  });
24407
+ repeatTarget();
24094
24408
  }).catch((e) => {
24095
24409
  setError(e);
24096
24410
  });
@@ -24102,6 +24416,7 @@ var TimelineVideoInfo = ({
24102
24416
  durationInFrames,
24103
24417
  error,
24104
24418
  fps,
24419
+ loopDisplay,
24105
24420
  naturalWidth,
24106
24421
  playbackRate,
24107
24422
  src,
@@ -24133,7 +24448,8 @@ var TimelineVideoInfo = ({
24133
24448
  durationInFrames,
24134
24449
  volume,
24135
24450
  doesVolumeChange,
24136
- playbackRate
24451
+ playbackRate,
24452
+ loopDisplay
24137
24453
  })
24138
24454
  })
24139
24455
  ]
@@ -24158,29 +24474,37 @@ var TimelineSequence = ({ s }) => {
24158
24474
  var Inner4 = ({ s, windowWidth }) => {
24159
24475
  const video = Internals56.useVideo();
24160
24476
  const maxMediaDuration = useMaxMediaDuration(s, video?.fps ?? 30);
24477
+ const effectiveMaxMediaDuration = s.loopDisplay ? null : maxMediaDuration;
24161
24478
  if (!video) {
24162
24479
  throw new TypeError("Expected video config");
24163
24480
  }
24164
24481
  const frame2 = useCurrentFrame2();
24165
24482
  const relativeFrame = frame2 - s.from;
24483
+ const displayDurationInFrames = s.loopDisplay ? s.loopDisplay.durationInFrames * s.loopDisplay.numberOfTimes : s.duration;
24166
24484
  const relativeFrameWithPremount = relativeFrame + (s.premountDisplay ?? 0);
24167
- const relativeFrameWithPostmount = relativeFrame - s.duration;
24485
+ const relativeFrameWithPostmount = relativeFrame - displayDurationInFrames;
24168
24486
  const roundedFrame = Math.round(relativeFrame * 100) / 100;
24169
- const isInRange = relativeFrame >= 0 && relativeFrame < s.duration;
24170
- const isPremounting = relativeFrameWithPremount >= 0 && relativeFrameWithPremount < s.duration && !isInRange;
24487
+ const isInRange = relativeFrame >= 0 && relativeFrame < displayDurationInFrames;
24488
+ const isPremounting = relativeFrameWithPremount >= 0 && relativeFrameWithPremount < displayDurationInFrames && !isInRange;
24171
24489
  const isPostmounting = relativeFrameWithPostmount >= 0 && relativeFrameWithPostmount < (s.postmountDisplay ?? 0) && !isInRange;
24172
24490
  const { marginLeft, width: width2, naturalWidth, premountWidth, postmountWidth } = useMemo121(() => {
24173
24491
  return getTimelineSequenceLayout({
24174
- durationInFrames: s.loopDisplay ? s.loopDisplay.durationInFrames * s.loopDisplay.numberOfTimes : s.duration,
24492
+ durationInFrames: displayDurationInFrames,
24175
24493
  startFrom: s.loopDisplay ? s.from + s.loopDisplay.startOffset : s.from,
24176
24494
  startFromMedia: s.type === "sequence" || s.type === "image" ? 0 : s.startMediaFrom,
24177
- maxMediaDuration,
24495
+ maxMediaDuration: effectiveMaxMediaDuration,
24178
24496
  video,
24179
24497
  windowWidth,
24180
24498
  premountDisplay: s.premountDisplay,
24181
24499
  postmountDisplay: s.postmountDisplay
24182
24500
  });
24183
- }, [maxMediaDuration, s, video, windowWidth]);
24501
+ }, [
24502
+ displayDurationInFrames,
24503
+ effectiveMaxMediaDuration,
24504
+ s,
24505
+ video,
24506
+ windowWidth
24507
+ ]);
24184
24508
  const style11 = useMemo121(() => {
24185
24509
  return {
24186
24510
  background: s.type === "audio" ? AUDIO_GRADIENT : s.type === "video" ? VIDEO_GRADIENT : s.type === "image" ? IMAGE_GRADIENT : BLUE,
@@ -24195,7 +24519,7 @@ var Inner4 = ({ s, windowWidth }) => {
24195
24519
  opacity: isInRange ? 1 : 0.5
24196
24520
  };
24197
24521
  }, [isInRange, marginLeft, s.type, width2]);
24198
- if (maxMediaDuration === null) {
24522
+ if (maxMediaDuration === null && !s.loopDisplay) {
24199
24523
  return null;
24200
24524
  }
24201
24525
  return /* @__PURE__ */ jsxs105("div", {
@@ -24238,7 +24562,8 @@ var Inner4 = ({ s, windowWidth }) => {
24238
24562
  startFrom: s.startMediaFrom,
24239
24563
  durationInFrames: s.duration,
24240
24564
  volume: s.volume,
24241
- playbackRate: s.playbackRate
24565
+ playbackRate: s.playbackRate,
24566
+ loopDisplay: s.loopDisplay
24242
24567
  }) : null,
24243
24568
  s.type === "video" ? /* @__PURE__ */ jsx215(TimelineVideoInfo, {
24244
24569
  src: s.src,
@@ -24250,7 +24575,8 @@ var Inner4 = ({ s, windowWidth }) => {
24250
24575
  volume: s.volume,
24251
24576
  doesVolumeChange: s.doesVolumeChange,
24252
24577
  premountWidth: premountWidth ?? 0,
24253
- postmountWidth: postmountWidth ?? 0
24578
+ postmountWidth: postmountWidth ?? 0,
24579
+ loopDisplay: s.loopDisplay
24254
24580
  }) : null,
24255
24581
  s.type === "image" ? /* @__PURE__ */ jsx215(TimelineImageInfo, {
24256
24582
  src: s.src,