@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
@@ -21050,7 +21050,8 @@ var getTimelineNestedLevel = (sequence, allSequences, depth) => {
21050
21050
  if (!parentSequence) {
21051
21051
  throw new Error("has parentId but no parent");
21052
21052
  }
21053
- return getTimelineNestedLevel(parentSequence, allSequences, depth + 1);
21053
+ const parentContributes = parentSequence.showInTimeline;
21054
+ return getTimelineNestedLevel(parentSequence, allSequences, parentContributes ? depth + 1 : depth);
21054
21055
  };
21055
21056
 
21056
21057
  // src/helpers/get-timeline-sequence-hash.ts
@@ -21099,6 +21100,19 @@ var getTimelineSequenceSequenceSortKey = (track, tracks, sameHashes = {}, nonceR
21099
21100
  };
21100
21101
 
21101
21102
  // src/helpers/calculate-timeline.ts
21103
+ var getInheritedLoopDisplay = (sequence, sequences) => {
21104
+ if (sequence.loopDisplay) {
21105
+ return sequence.loopDisplay;
21106
+ }
21107
+ if (!sequence.parent) {
21108
+ return;
21109
+ }
21110
+ const parent = sequences.find((s) => s.id === sequence.parent);
21111
+ if (!parent) {
21112
+ return;
21113
+ }
21114
+ return getInheritedLoopDisplay(parent, sequences);
21115
+ };
21102
21116
  var calculateTimeline = ({
21103
21117
  sequences
21104
21118
  }) => {
@@ -21127,7 +21141,8 @@ var calculateTimeline = ({
21127
21141
  sequence: {
21128
21142
  ...sequence,
21129
21143
  from: visibleStart,
21130
- duration: visibleDuration
21144
+ duration: visibleDuration,
21145
+ loopDisplay: sequence.type === "audio" || sequence.type === "video" ? getInheritedLoopDisplay(sequence, sortedSequences) : sequence.loopDisplay
21131
21146
  },
21132
21147
  depth: getTimelineNestedLevel(sequence, sortedSequences, 0),
21133
21148
  hash: actualHash,
@@ -23763,6 +23778,13 @@ var useMaxMediaDuration = (s, fps) => {
23763
23778
  import { useEffect as useEffect73, useMemo as useMemo119, useRef as useRef43, useState as useState79 } from "react";
23764
23779
  import { Internals as Internals55 } from "remotion";
23765
23780
 
23781
+ // src/make-audio-waveform-worker.ts
23782
+ var makeAudioWaveformWorker = () => {
23783
+ return new Worker(new URL("./audio-waveform-worker.mjs", import.meta.url), {
23784
+ type: "module"
23785
+ });
23786
+ };
23787
+
23766
23788
  // src/components/parse-color.ts
23767
23789
  var colorCache = new Map;
23768
23790
  var parseColor = (color) => {
@@ -23838,12 +23860,107 @@ var drawBars = (canvas, peaks, color, volume, width) => {
23838
23860
 
23839
23861
  // src/components/load-waveform-peaks.ts
23840
23862
  import { ALL_FORMATS as ALL_FORMATS3, AudioSampleSink, Input as Input3, UrlSource as UrlSource3 } from "mediabunny";
23863
+
23864
+ // src/components/waveform-peak-processor.ts
23865
+ var emitWaveformProgress = ({
23866
+ completedPeaks,
23867
+ final,
23868
+ onProgress,
23869
+ peaks,
23870
+ totalPeaks
23871
+ }) => {
23872
+ onProgress?.({
23873
+ peaks,
23874
+ completedPeaks,
23875
+ totalPeaks,
23876
+ final
23877
+ });
23878
+ };
23879
+ var createWaveformPeakProcessor = ({
23880
+ totalPeaks,
23881
+ samplesPerPeak,
23882
+ onProgress,
23883
+ progressIntervalInMs,
23884
+ now
23885
+ }) => {
23886
+ const peaks = new Float32Array(totalPeaks);
23887
+ let peakIndex = 0;
23888
+ let peakMax = 0;
23889
+ let sampleInPeak = 0;
23890
+ let lastProgressAt = 0;
23891
+ let lastProgressPeak = 0;
23892
+ const emitProgress = (force) => {
23893
+ const timestamp = now();
23894
+ if (!force && peakIndex === lastProgressPeak && sampleInPeak === 0) {
23895
+ return;
23896
+ }
23897
+ if (!force && timestamp - lastProgressAt < progressIntervalInMs) {
23898
+ return;
23899
+ }
23900
+ lastProgressAt = timestamp;
23901
+ lastProgressPeak = peakIndex;
23902
+ emitWaveformProgress({
23903
+ peaks,
23904
+ completedPeaks: peakIndex,
23905
+ totalPeaks,
23906
+ final: force,
23907
+ onProgress
23908
+ });
23909
+ };
23910
+ return {
23911
+ peaks,
23912
+ processSampleChunk: (floats, channels) => {
23913
+ const frameCount = Math.floor(floats.length / Math.max(1, channels));
23914
+ for (let frame2 = 0;frame2 < frameCount; frame2++) {
23915
+ let framePeak = 0;
23916
+ for (let channel = 0;channel < channels; channel++) {
23917
+ const sampleIndex = frame2 * channels + channel;
23918
+ const abs = Math.abs(floats[sampleIndex] ?? 0);
23919
+ if (abs > framePeak) {
23920
+ framePeak = abs;
23921
+ }
23922
+ }
23923
+ if (framePeak > peakMax) {
23924
+ peakMax = framePeak;
23925
+ }
23926
+ sampleInPeak++;
23927
+ if (sampleInPeak >= samplesPerPeak) {
23928
+ if (peakIndex < totalPeaks) {
23929
+ peaks[peakIndex] = peakMax;
23930
+ }
23931
+ peakIndex++;
23932
+ peakMax = 0;
23933
+ sampleInPeak = 0;
23934
+ }
23935
+ }
23936
+ emitProgress(false);
23937
+ },
23938
+ finalize: () => {
23939
+ if (sampleInPeak > 0 && peakIndex < totalPeaks) {
23940
+ peaks[peakIndex] = peakMax;
23941
+ peakIndex++;
23942
+ }
23943
+ emitProgress(true);
23944
+ }
23945
+ };
23946
+ };
23947
+
23948
+ // src/components/load-waveform-peaks.ts
23841
23949
  var TARGET_SAMPLE_RATE = 100;
23950
+ var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
23842
23951
  var peaksCache = new Map;
23843
- async function loadWaveformPeaks(url, signal) {
23952
+ async function loadWaveformPeaks(url, signal, options) {
23844
23953
  const cached = peaksCache.get(url);
23845
- if (cached)
23954
+ if (cached) {
23955
+ emitWaveformProgress({
23956
+ peaks: cached,
23957
+ completedPeaks: cached.length,
23958
+ totalPeaks: cached.length,
23959
+ final: true,
23960
+ onProgress: options?.onProgress
23961
+ });
23846
23962
  return cached;
23963
+ }
23847
23964
  const input2 = new Input3({
23848
23965
  formats: ALL_FORMATS3,
23849
23966
  source: new UrlSource3(url)
@@ -23857,11 +23974,14 @@ async function loadWaveformPeaks(url, signal) {
23857
23974
  const durationInSeconds = await audioTrack.computeDuration();
23858
23975
  const totalPeaks = Math.ceil(durationInSeconds * TARGET_SAMPLE_RATE);
23859
23976
  const samplesPerPeak = Math.max(1, Math.floor(sampleRate / TARGET_SAMPLE_RATE));
23860
- const peaks = new Float32Array(totalPeaks);
23861
- let peakIndex = 0;
23862
- let peakMax = 0;
23863
- let sampleInPeak = 0;
23864
23977
  const sink = new AudioSampleSink(audioTrack);
23978
+ const processor = createWaveformPeakProcessor({
23979
+ totalPeaks,
23980
+ samplesPerPeak,
23981
+ onProgress: options?.onProgress,
23982
+ progressIntervalInMs: options?.progressIntervalInMs ?? DEFAULT_PROGRESS_INTERVAL_IN_MS,
23983
+ now: () => Date.now()
23984
+ });
23865
23985
  for await (const sample of sink.samples()) {
23866
23986
  if (signal.aborted) {
23867
23987
  sample.close();
@@ -23874,34 +23994,11 @@ async function loadWaveformPeaks(url, signal) {
23874
23994
  const floats = new Float32Array(bytesNeeded / 4);
23875
23995
  sample.copyTo(floats, { format: "f32", planeIndex: 0 });
23876
23996
  const channels = Math.max(1, sample.numberOfChannels);
23877
- const frames = sample.numberOfFrames;
23878
23997
  sample.close();
23879
- for (let frame2 = 0;frame2 < frames; frame2++) {
23880
- let framePeak = 0;
23881
- for (let channel = 0;channel < channels; channel++) {
23882
- const sampleIndex = frame2 * channels + channel;
23883
- const abs = Math.abs(floats[sampleIndex] ?? 0);
23884
- if (abs > framePeak) {
23885
- framePeak = abs;
23886
- }
23887
- }
23888
- if (framePeak > peakMax) {
23889
- peakMax = framePeak;
23890
- }
23891
- sampleInPeak++;
23892
- if (sampleInPeak >= samplesPerPeak) {
23893
- if (peakIndex < totalPeaks) {
23894
- peaks[peakIndex] = peakMax;
23895
- }
23896
- peakIndex++;
23897
- peakMax = 0;
23898
- sampleInPeak = 0;
23899
- }
23900
- }
23901
- }
23902
- if (sampleInPeak > 0 && peakIndex < totalPeaks) {
23903
- peaks[peakIndex] = peakMax;
23998
+ processor.processSampleChunk(floats, channels);
23904
23999
  }
24000
+ processor.finalize();
24001
+ const { peaks } = processor;
23905
24002
  peaksCache.set(url, peaks);
23906
24003
  return peaks;
23907
24004
  } finally {
@@ -23909,8 +24006,50 @@ async function loadWaveformPeaks(url, signal) {
23909
24006
  }
23910
24007
  }
23911
24008
 
24009
+ // src/components/looped-media-timeline.ts
24010
+ var shouldTileLoopDisplay = (loopDisplay) => {
24011
+ return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
24012
+ };
24013
+ var getLoopDisplayWidth = ({
24014
+ visualizationWidth,
24015
+ loopDisplay
24016
+ }) => {
24017
+ if (!shouldTileLoopDisplay(loopDisplay)) {
24018
+ return visualizationWidth;
24019
+ }
24020
+ return visualizationWidth / loopDisplay.numberOfTimes;
24021
+ };
24022
+
24023
+ // src/components/slice-waveform-peaks.ts
24024
+ var sliceWaveformPeaks = ({
24025
+ durationInFrames,
24026
+ fps,
24027
+ peaks,
24028
+ playbackRate,
24029
+ startFrom
24030
+ }) => {
24031
+ if (peaks.length === 0) {
24032
+ return peaks;
24033
+ }
24034
+ const startTimeInSeconds = startFrom / fps;
24035
+ const durationInSeconds = durationInFrames / fps * playbackRate;
24036
+ const startPeakIndex = Math.floor(startTimeInSeconds * TARGET_SAMPLE_RATE);
24037
+ const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * TARGET_SAMPLE_RATE);
24038
+ return peaks.subarray(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
24039
+ };
24040
+
23912
24041
  // src/components/AudioWaveform.tsx
23913
24042
  import { jsx as jsx212, jsxs as jsxs102 } from "react/jsx-runtime";
24043
+ var EMPTY_PEAKS = new Float32Array(0);
24044
+ var canRetryCanvasTransfer = (err) => {
24045
+ return err instanceof DOMException && err.name === "InvalidStateError";
24046
+ };
24047
+ var canUseAudioWaveformWorker = () => {
24048
+ if (typeof Worker === "undefined" || typeof OffscreenCanvas === "undefined" || typeof HTMLCanvasElement === "undefined") {
24049
+ return false;
24050
+ }
24051
+ return "transferControlToOffscreen" in HTMLCanvasElement.prototype;
24052
+ };
23914
24053
  var container43 = {
23915
24054
  display: "flex",
23916
24055
  flexDirection: "row",
@@ -23929,11 +24068,41 @@ var errorMessage = {
23929
24068
  opacity: 0.75
23930
24069
  };
23931
24070
  var waveformCanvasStyle = {
23932
- pointerEvents: "none"
24071
+ pointerEvents: "none",
24072
+ width: "100%",
24073
+ height: "100%"
23933
24074
  };
23934
24075
  var volumeCanvasStyle = {
23935
24076
  position: "absolute"
23936
24077
  };
24078
+ var drawLoopedWaveform = ({
24079
+ canvas,
24080
+ peaks,
24081
+ volume,
24082
+ visualizationWidth,
24083
+ loopWidth
24084
+ }) => {
24085
+ const h = canvas.height;
24086
+ const w = Math.ceil(visualizationWidth);
24087
+ const targetCanvas = document.createElement("canvas");
24088
+ targetCanvas.width = Math.max(1, Math.ceil(loopWidth));
24089
+ targetCanvas.height = h;
24090
+ drawBars(targetCanvas, peaks, "rgba(255, 255, 255, 0.6)", volume, targetCanvas.width);
24091
+ canvas.width = w;
24092
+ canvas.height = h;
24093
+ const ctx = canvas.getContext("2d");
24094
+ if (!ctx) {
24095
+ throw new Error("Failed to get canvas context");
24096
+ }
24097
+ const pattern = ctx.createPattern(targetCanvas, "repeat-x");
24098
+ if (!pattern) {
24099
+ return;
24100
+ }
24101
+ pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
24102
+ ctx.clearRect(0, 0, w, h);
24103
+ ctx.fillStyle = pattern;
24104
+ ctx.fillRect(0, 0, w, h);
24105
+ };
23937
24106
  var AudioWaveform = ({
23938
24107
  src,
23939
24108
  startFrom,
@@ -23941,10 +24110,13 @@ var AudioWaveform = ({
23941
24110
  visualizationWidth,
23942
24111
  volume,
23943
24112
  doesVolumeChange,
23944
- playbackRate
24113
+ playbackRate,
24114
+ loopDisplay
23945
24115
  }) => {
23946
24116
  const [peaks, setPeaks] = useState79(null);
23947
24117
  const [error, setError] = useState79(null);
24118
+ const [waveformCanvasKey, setWaveformCanvasKey] = useState79(0);
24119
+ const canUseWorkerPath = useMemo119(() => canUseAudioWaveformWorker(), []);
23948
24120
  const vidConf = Internals55.useUnsafeVideoConfig();
23949
24121
  if (vidConf === null) {
23950
24122
  throw new Error("Expected video config");
@@ -23952,8 +24124,15 @@ var AudioWaveform = ({
23952
24124
  const containerRef = useRef43(null);
23953
24125
  const waveformCanvas = useRef43(null);
23954
24126
  const volumeCanvas = useRef43(null);
24127
+ const waveformWorker = useRef43(null);
24128
+ const hasTransferredCanvas = useRef43(false);
24129
+ const latestRequestId = useRef43(0);
23955
24130
  useEffect73(() => {
24131
+ if (canUseWorkerPath) {
24132
+ return;
24133
+ }
23956
24134
  const controller = new AbortController;
24135
+ setPeaks(null);
23957
24136
  setError(null);
23958
24137
  loadWaveformPeaks(src, controller.signal).then((p) => {
23959
24138
  if (!controller.signal.aborted) {
@@ -23965,30 +24144,127 @@ var AudioWaveform = ({
23965
24144
  }
23966
24145
  });
23967
24146
  return () => controller.abort();
23968
- }, [src]);
24147
+ }, [canUseWorkerPath, src]);
24148
+ useEffect73(() => {
24149
+ if (!canUseWorkerPath) {
24150
+ return;
24151
+ }
24152
+ const canvasElement = waveformCanvas.current;
24153
+ if (!canvasElement || hasTransferredCanvas.current) {
24154
+ return;
24155
+ }
24156
+ const worker = makeAudioWaveformWorker();
24157
+ waveformWorker.current = worker;
24158
+ worker.addEventListener("message", (event) => {
24159
+ if (event.data.type === "error") {
24160
+ if (event.data.requestId !== latestRequestId.current) {
24161
+ return;
24162
+ }
24163
+ setError(new Error(event.data.message));
24164
+ }
24165
+ });
24166
+ let offscreen;
24167
+ try {
24168
+ offscreen = canvasElement.transferControlToOffscreen();
24169
+ } catch (err) {
24170
+ worker.terminate();
24171
+ waveformWorker.current = null;
24172
+ if (canRetryCanvasTransfer(err)) {
24173
+ setWaveformCanvasKey((key4) => key4 + 1);
24174
+ return;
24175
+ }
24176
+ throw err;
24177
+ }
24178
+ hasTransferredCanvas.current = true;
24179
+ worker.postMessage({ type: "init", canvas: offscreen }, [offscreen]);
24180
+ return () => {
24181
+ worker.postMessage({ type: "dispose" });
24182
+ worker.terminate();
24183
+ waveformWorker.current = null;
24184
+ hasTransferredCanvas.current = false;
24185
+ };
24186
+ }, [canUseWorkerPath, waveformCanvasKey]);
23969
24187
  const portionPeaks = useMemo119(() => {
23970
- if (!peaks || peaks.length === 0) {
24188
+ if (canUseWorkerPath || !peaks) {
23971
24189
  return null;
23972
24190
  }
23973
- const startTimeInSeconds = startFrom / vidConf.fps;
23974
- const durationInSeconds = durationInFrames / vidConf.fps * playbackRate;
23975
- const startPeakIndex = Math.floor(startTimeInSeconds * TARGET_SAMPLE_RATE);
23976
- const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * TARGET_SAMPLE_RATE);
23977
- return peaks.slice(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
23978
- }, [peaks, startFrom, durationInFrames, vidConf.fps, playbackRate]);
24191
+ return sliceWaveformPeaks({
24192
+ durationInFrames: shouldTileLoopDisplay(loopDisplay) ? loopDisplay.durationInFrames : durationInFrames,
24193
+ fps: vidConf.fps,
24194
+ peaks,
24195
+ playbackRate,
24196
+ startFrom
24197
+ });
24198
+ }, [
24199
+ canUseWorkerPath,
24200
+ durationInFrames,
24201
+ loopDisplay,
24202
+ peaks,
24203
+ playbackRate,
24204
+ startFrom,
24205
+ vidConf.fps
24206
+ ]);
23979
24207
  useEffect73(() => {
23980
24208
  const { current: canvasElement } = waveformCanvas;
23981
24209
  const { current: containerElement } = containerRef;
23982
- if (!canvasElement || !containerElement || !portionPeaks || portionPeaks.length === 0) {
24210
+ if (!canvasElement || !containerElement) {
23983
24211
  return;
23984
24212
  }
23985
24213
  const h = containerElement.clientHeight;
23986
24214
  const w = Math.ceil(visualizationWidth);
24215
+ const vol = typeof volume === "number" ? volume : 1;
24216
+ if (canUseWorkerPath) {
24217
+ const worker = waveformWorker.current;
24218
+ if (!worker || !hasTransferredCanvas.current) {
24219
+ return;
24220
+ }
24221
+ latestRequestId.current += 1;
24222
+ setError(null);
24223
+ const message = {
24224
+ type: "render",
24225
+ requestId: latestRequestId.current,
24226
+ src,
24227
+ width: w,
24228
+ height: h,
24229
+ volume: vol,
24230
+ startFrom,
24231
+ durationInFrames,
24232
+ fps: vidConf.fps,
24233
+ playbackRate,
24234
+ loopDisplay
24235
+ };
24236
+ worker.postMessage(message);
24237
+ return;
24238
+ }
23987
24239
  canvasElement.width = w;
23988
24240
  canvasElement.height = h;
23989
- const vol = typeof volume === "number" ? volume : 1;
23990
- drawBars(canvasElement, portionPeaks, "rgba(255, 255, 255, 0.6)", vol, w);
23991
- }, [portionPeaks, visualizationWidth, volume]);
24241
+ if (shouldTileLoopDisplay(loopDisplay)) {
24242
+ drawLoopedWaveform({
24243
+ canvas: canvasElement,
24244
+ peaks: portionPeaks ?? EMPTY_PEAKS,
24245
+ volume: vol,
24246
+ visualizationWidth,
24247
+ loopWidth: getLoopDisplayWidth({
24248
+ visualizationWidth,
24249
+ loopDisplay
24250
+ })
24251
+ });
24252
+ } else {
24253
+ drawBars(canvasElement, portionPeaks ?? EMPTY_PEAKS, "rgba(255, 255, 255, 0.6)", vol, w);
24254
+ }
24255
+ }, [
24256
+ canUseWorkerPath,
24257
+ durationInFrames,
24258
+ loopDisplay,
24259
+ playbackRate,
24260
+ portionPeaks,
24261
+ src,
24262
+ startFrom,
24263
+ vidConf.fps,
24264
+ visualizationWidth,
24265
+ volume,
24266
+ waveformCanvasKey
24267
+ ]);
23992
24268
  useEffect73(() => {
23993
24269
  const { current: volumeCanvasElement } = volumeCanvas;
23994
24270
  const { current: containerElement } = containerRef;
@@ -24030,7 +24306,7 @@ var AudioWaveform = ({
24030
24306
  })
24031
24307
  });
24032
24308
  }
24033
- if (!peaks) {
24309
+ if (!canUseWorkerPath && !peaks) {
24034
24310
  return null;
24035
24311
  }
24036
24312
  return /* @__PURE__ */ jsxs102("div", {
@@ -24040,7 +24316,7 @@ var AudioWaveform = ({
24040
24316
  /* @__PURE__ */ jsx212("canvas", {
24041
24317
  ref: waveformCanvas,
24042
24318
  style: waveformCanvasStyle
24043
- }),
24319
+ }, waveformCanvasKey),
24044
24320
  /* @__PURE__ */ jsx212("canvas", {
24045
24321
  ref: volumeCanvas,
24046
24322
  style: volumeCanvasStyle
@@ -24063,7 +24339,8 @@ var width = {
24063
24339
  position: "relative"
24064
24340
  };
24065
24341
  var icon4 = {
24066
- height: 12
24342
+ height: 12,
24343
+ filter: "drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8))"
24067
24344
  };
24068
24345
  var Icon = () => /* @__PURE__ */ jsx213("svg", {
24069
24346
  viewBox: "0 0 512 512",
@@ -24073,44 +24350,23 @@ var Icon = () => /* @__PURE__ */ jsx213("svg", {
24073
24350
  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"
24074
24351
  })
24075
24352
  });
24076
- var topLine = {
24077
- top: 0,
24078
- height: 2,
24079
- width: 1,
24080
- background: LIGHT_COLOR
24081
- };
24082
- var bottomLine = {
24083
- top: 0,
24084
- height: 2,
24353
+ var verticalLine = {
24354
+ height: "100%",
24085
24355
  width: 1,
24086
- background: LIGHT_COLOR
24087
- };
24088
- var topContainer = {
24089
- justifyContent: "flex-start",
24090
- alignItems: "center"
24356
+ background: "rgb(255,255,255, 0.5)"
24091
24357
  };
24092
24358
  var centerContainer = {
24093
24359
  justifyContent: "center",
24094
24360
  alignItems: "center"
24095
24361
  };
24096
- var bottomContainer = {
24097
- justifyContent: "flex-end",
24098
- alignItems: "center"
24099
- };
24100
24362
  var LoopedIndicator = () => {
24101
24363
  return /* @__PURE__ */ jsxs103("div", {
24102
24364
  style: width,
24103
24365
  children: [
24104
24366
  /* @__PURE__ */ jsx213(AbsoluteFill5, {
24105
- style: topContainer,
24106
- children: /* @__PURE__ */ jsx213("div", {
24107
- style: topLine
24108
- })
24109
- }),
24110
- /* @__PURE__ */ jsx213(AbsoluteFill5, {
24111
- style: bottomContainer,
24367
+ style: centerContainer,
24112
24368
  children: /* @__PURE__ */ jsx213("div", {
24113
- style: bottomLine
24369
+ style: verticalLine
24114
24370
  })
24115
24371
  }),
24116
24372
  /* @__PURE__ */ jsx213(AbsoluteFill5, {
@@ -24582,7 +24838,8 @@ var TimelineVideoInfo = ({
24582
24838
  volume,
24583
24839
  doesVolumeChange,
24584
24840
  premountWidth,
24585
- postmountWidth
24841
+ postmountWidth,
24842
+ loopDisplay
24586
24843
  }) => {
24587
24844
  const { fps } = useVideoConfig5();
24588
24845
  const ref2 = useRef45(null);
@@ -24605,25 +24862,54 @@ var TimelineVideoInfo = ({
24605
24862
  return;
24606
24863
  }
24607
24864
  current.appendChild(canvas);
24865
+ const loopWidth = getLoopDisplayWidth({
24866
+ visualizationWidth: naturalWidth,
24867
+ loopDisplay
24868
+ });
24869
+ const shouldRepeatVideo = shouldTileLoopDisplay(loopDisplay);
24870
+ const targetCanvas = shouldRepeatVideo ? document.createElement("canvas") : canvas;
24871
+ targetCanvas.width = shouldRepeatVideo ? Math.max(1, Math.ceil(loopWidth)) : canvas.width;
24872
+ targetCanvas.height = canvas.height;
24873
+ const targetCtx = shouldRepeatVideo ? targetCanvas.getContext("2d") : ctx;
24874
+ if (!targetCtx) {
24875
+ current.removeChild(canvas);
24876
+ return;
24877
+ }
24878
+ const repeatTarget = () => {
24879
+ if (!shouldRepeatVideo) {
24880
+ return;
24881
+ }
24882
+ const pattern = ctx.createPattern(targetCanvas, "repeat-x");
24883
+ if (!pattern) {
24884
+ return;
24885
+ }
24886
+ pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
24887
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
24888
+ ctx.fillStyle = pattern;
24889
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
24890
+ };
24608
24891
  const filledSlots = new Map;
24609
24892
  const fromSeconds = trimBefore / fps;
24610
- const toSeconds = fromSeconds + durationInFrames * playbackRate / fps;
24893
+ const visibleDurationInFrames = shouldRepeatVideo && loopDisplay ? loopDisplay.durationInFrames : durationInFrames;
24894
+ const toSeconds = fromSeconds + visibleDurationInFrames * playbackRate / fps;
24895
+ const targetWidth = shouldRepeatVideo ? targetCanvas.width : naturalWidth;
24611
24896
  if (aspectRatio.current !== null) {
24612
24897
  ensureSlots({
24613
24898
  filledSlots,
24614
- naturalWidth,
24899
+ naturalWidth: targetWidth,
24615
24900
  fromSeconds,
24616
24901
  toSeconds,
24617
24902
  aspectRatio: aspectRatio.current
24618
24903
  });
24619
24904
  fillWithCachedFrames({
24620
- ctx,
24621
- naturalWidth,
24905
+ ctx: targetCtx,
24906
+ naturalWidth: targetWidth,
24622
24907
  filledSlots,
24623
24908
  src,
24624
24909
  segmentDuration: toSeconds - fromSeconds,
24625
24910
  fromSeconds
24626
24911
  });
24912
+ repeatTarget();
24627
24913
  const unfilled = Array.from(filledSlots.keys()).filter((timestamp) => !filledSlots.get(timestamp));
24628
24914
  if (unfilled.length === 0) {
24629
24915
  return () => {
@@ -24641,7 +24927,7 @@ var TimelineVideoInfo = ({
24641
24927
  filledSlots,
24642
24928
  fromSeconds,
24643
24929
  toSeconds,
24644
- naturalWidth,
24930
+ naturalWidth: targetWidth,
24645
24931
  aspectRatio: aspectRatio.current
24646
24932
  });
24647
24933
  return Array.from(filledSlots.keys()).map((timestamp) => timestamp / WEBCODECS_TIMESCALE);
@@ -24669,17 +24955,18 @@ var TimelineVideoInfo = ({
24669
24955
  filledSlots,
24670
24956
  fromSeconds,
24671
24957
  toSeconds,
24672
- naturalWidth,
24958
+ naturalWidth: targetWidth,
24673
24959
  aspectRatio: aspectRatio.current
24674
24960
  });
24675
24961
  fillFrameWhereItFits({
24676
- ctx,
24962
+ ctx: targetCtx,
24677
24963
  filledSlots,
24678
- visualizationWidth: naturalWidth,
24964
+ visualizationWidth: targetWidth,
24679
24965
  frame: transformed,
24680
24966
  segmentDuration: toSeconds - fromSeconds,
24681
24967
  fromSeconds
24682
24968
  });
24969
+ repeatTarget();
24683
24970
  } catch (e) {
24684
24971
  if (frame2) {
24685
24972
  frame2.close();
@@ -24695,13 +24982,14 @@ var TimelineVideoInfo = ({
24695
24982
  return;
24696
24983
  }
24697
24984
  fillWithCachedFrames({
24698
- ctx,
24699
- naturalWidth,
24985
+ ctx: targetCtx,
24986
+ naturalWidth: targetWidth,
24700
24987
  filledSlots,
24701
24988
  src,
24702
24989
  segmentDuration: toSeconds - fromSeconds,
24703
24990
  fromSeconds
24704
24991
  });
24992
+ repeatTarget();
24705
24993
  }).catch((e) => {
24706
24994
  setError(e);
24707
24995
  });
@@ -24713,6 +25001,7 @@ var TimelineVideoInfo = ({
24713
25001
  durationInFrames,
24714
25002
  error,
24715
25003
  fps,
25004
+ loopDisplay,
24716
25005
  naturalWidth,
24717
25006
  playbackRate,
24718
25007
  src,
@@ -24744,7 +25033,8 @@ var TimelineVideoInfo = ({
24744
25033
  durationInFrames,
24745
25034
  volume,
24746
25035
  doesVolumeChange,
24747
- playbackRate
25036
+ playbackRate,
25037
+ loopDisplay
24748
25038
  })
24749
25039
  })
24750
25040
  ]
@@ -24769,29 +25059,37 @@ var TimelineSequence = ({ s }) => {
24769
25059
  var Inner4 = ({ s, windowWidth }) => {
24770
25060
  const video = Internals56.useVideo();
24771
25061
  const maxMediaDuration = useMaxMediaDuration(s, video?.fps ?? 30);
25062
+ const effectiveMaxMediaDuration = s.loopDisplay ? null : maxMediaDuration;
24772
25063
  if (!video) {
24773
25064
  throw new TypeError("Expected video config");
24774
25065
  }
24775
25066
  const frame2 = useCurrentFrame2();
24776
25067
  const relativeFrame = frame2 - s.from;
25068
+ const displayDurationInFrames = s.loopDisplay ? s.loopDisplay.durationInFrames * s.loopDisplay.numberOfTimes : s.duration;
24777
25069
  const relativeFrameWithPremount = relativeFrame + (s.premountDisplay ?? 0);
24778
- const relativeFrameWithPostmount = relativeFrame - s.duration;
25070
+ const relativeFrameWithPostmount = relativeFrame - displayDurationInFrames;
24779
25071
  const roundedFrame = Math.round(relativeFrame * 100) / 100;
24780
- const isInRange = relativeFrame >= 0 && relativeFrame < s.duration;
24781
- const isPremounting = relativeFrameWithPremount >= 0 && relativeFrameWithPremount < s.duration && !isInRange;
25072
+ const isInRange = relativeFrame >= 0 && relativeFrame < displayDurationInFrames;
25073
+ const isPremounting = relativeFrameWithPremount >= 0 && relativeFrameWithPremount < displayDurationInFrames && !isInRange;
24782
25074
  const isPostmounting = relativeFrameWithPostmount >= 0 && relativeFrameWithPostmount < (s.postmountDisplay ?? 0) && !isInRange;
24783
25075
  const { marginLeft, width: width2, naturalWidth, premountWidth, postmountWidth } = useMemo121(() => {
24784
25076
  return getTimelineSequenceLayout({
24785
- durationInFrames: s.loopDisplay ? s.loopDisplay.durationInFrames * s.loopDisplay.numberOfTimes : s.duration,
25077
+ durationInFrames: displayDurationInFrames,
24786
25078
  startFrom: s.loopDisplay ? s.from + s.loopDisplay.startOffset : s.from,
24787
25079
  startFromMedia: s.type === "sequence" || s.type === "image" ? 0 : s.startMediaFrom,
24788
- maxMediaDuration,
25080
+ maxMediaDuration: effectiveMaxMediaDuration,
24789
25081
  video,
24790
25082
  windowWidth,
24791
25083
  premountDisplay: s.premountDisplay,
24792
25084
  postmountDisplay: s.postmountDisplay
24793
25085
  });
24794
- }, [maxMediaDuration, s, video, windowWidth]);
25086
+ }, [
25087
+ displayDurationInFrames,
25088
+ effectiveMaxMediaDuration,
25089
+ s,
25090
+ video,
25091
+ windowWidth
25092
+ ]);
24795
25093
  const style11 = useMemo121(() => {
24796
25094
  return {
24797
25095
  background: s.type === "audio" ? AUDIO_GRADIENT : s.type === "video" ? VIDEO_GRADIENT : s.type === "image" ? IMAGE_GRADIENT : BLUE,
@@ -24806,7 +25104,7 @@ var Inner4 = ({ s, windowWidth }) => {
24806
25104
  opacity: isInRange ? 1 : 0.5
24807
25105
  };
24808
25106
  }, [isInRange, marginLeft, s.type, width2]);
24809
- if (maxMediaDuration === null) {
25107
+ if (maxMediaDuration === null && !s.loopDisplay) {
24810
25108
  return null;
24811
25109
  }
24812
25110
  return /* @__PURE__ */ jsxs106("div", {
@@ -24849,7 +25147,8 @@ var Inner4 = ({ s, windowWidth }) => {
24849
25147
  startFrom: s.startMediaFrom,
24850
25148
  durationInFrames: s.duration,
24851
25149
  volume: s.volume,
24852
- playbackRate: s.playbackRate
25150
+ playbackRate: s.playbackRate,
25151
+ loopDisplay: s.loopDisplay
24853
25152
  }) : null,
24854
25153
  s.type === "video" ? /* @__PURE__ */ jsx218(TimelineVideoInfo, {
24855
25154
  src: s.src,
@@ -24861,7 +25160,8 @@ var Inner4 = ({ s, windowWidth }) => {
24861
25160
  volume: s.volume,
24862
25161
  doesVolumeChange: s.doesVolumeChange,
24863
25162
  premountWidth: premountWidth ?? 0,
24864
- postmountWidth: postmountWidth ?? 0
25163
+ postmountWidth: postmountWidth ?? 0,
25164
+ loopDisplay: s.loopDisplay
24865
25165
  }) : null,
24866
25166
  s.type === "image" ? /* @__PURE__ */ jsx218(TimelineImageInfo, {
24867
25167
  src: s.src,