@remotion/timeline-utils 4.0.522 → 4.0.523

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.
@@ -10,6 +10,7 @@ export type AudioWaveformWorkerPeaksMessage = {
10
10
  readonly requestId: number;
11
11
  readonly peaks: Float32Array;
12
12
  readonly final: boolean;
13
+ readonly averageVolume: number | null;
13
14
  };
14
15
  export type AudioWaveformWorkerErrorMessage = {
15
16
  readonly type: 'error';
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.drawBars = void 0;
4
4
  const parse_color_1 = require("./parse-color");
5
5
  const CLIPPING_COLOR = '#FF7F50';
6
+ const WAVEFORM_HEIGHT_SCALE = 0.8;
6
7
  const getVolumeAtBar = ({ barIndex, numBars, volume, }) => {
7
8
  var _a;
8
9
  if (typeof volume === 'number') {
@@ -37,6 +38,7 @@ const drawBars = ({ canvas, color, peaks, volume, width, }) => {
37
38
  const imageData = ctx.createImageData(w, height);
38
39
  const { data } = imageData;
39
40
  const numBars = width;
41
+ const fullScaleHalfBar = (height * WAVEFORM_HEIGHT_SCALE) / 2;
40
42
  for (let barIndex = 0; barIndex < numBars; barIndex++) {
41
43
  const x = barIndex;
42
44
  if (x >= w)
@@ -45,15 +47,20 @@ const drawBars = ({ canvas, color, peaks, volume, width, }) => {
45
47
  const peak = peaks[peakIndex] || 0;
46
48
  const barVolume = getVolumeAtBar({ barIndex, numBars, volume });
47
49
  const scaledPeak = peak * barVolume;
48
- const halfBar = Math.max(0, Math.min(height / 2, (scaledPeak * height) / 2));
50
+ const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * fullScaleHalfBar));
49
51
  if (halfBar === 0)
50
52
  continue;
51
53
  const mid = height / 2;
52
54
  const barY = Math.round(mid - halfBar);
53
55
  const barEnd = Math.round(mid + halfBar);
54
56
  const isClipping = scaledPeak > 1;
55
- const clipTopEnd = isClipping ? Math.min(barY + 2, barEnd) : barY;
56
- const clipBotStart = isClipping ? Math.max(barEnd - 2, barY) : barEnd;
57
+ // Color the overflow above full scale, keeping tiny overshoots visible.
58
+ const clipTopEnd = isClipping
59
+ ? Math.min(Math.max(barY + 2, Math.round(mid - fullScaleHalfBar)), barEnd)
60
+ : barY;
61
+ const clipBotStart = isClipping
62
+ ? Math.max(Math.min(barEnd - 2, Math.round(mid + fullScaleHalfBar)), barY)
63
+ : barEnd;
57
64
  for (let y = barY; y < clipTopEnd; y++) {
58
65
  const idx = (y * w + x) * 4;
59
66
  data[idx] = cr;
@@ -0,0 +1,2 @@
1
+ import type { InputAudioTrack } from 'mediabunny';
2
+ export declare const getWaveformCacheKey: (src: string | InputAudioTrack, waveformSampleRate: number) => string;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getWaveformCacheKey = void 0;
4
+ const trackIds = new WeakMap();
5
+ let nextTrackId = 0;
6
+ const getWaveformCacheKey = (src, waveformSampleRate) => {
7
+ if (typeof src === 'string') {
8
+ return `${waveformSampleRate}:url:${src}`;
9
+ }
10
+ let id = trackIds.get(src);
11
+ if (id === undefined) {
12
+ id = nextTrackId++;
13
+ trackIds.set(src, id);
14
+ }
15
+ return `${waveformSampleRate}:track:${id}`;
16
+ };
17
+ exports.getWaveformCacheKey = getWaveformCacheKey;
@@ -1,6 +1,12 @@
1
+ import type { InputAudioTrack } from 'mediabunny';
1
2
  import { TARGET_SAMPLE_RATE } from './constants';
3
+ export type WaveformResult = {
4
+ readonly peaks: Float32Array;
5
+ readonly averageVolume: number | null;
6
+ };
2
7
  export { TARGET_SAMPLE_RATE };
3
8
  type Progress = {
9
+ readonly averageVolume: number | null;
4
10
  readonly peaks: Float32Array;
5
11
  readonly completedPeaks: number;
6
12
  readonly totalPeaks: number;
@@ -11,4 +17,4 @@ type LoadWaveformPeaksOptions = {
11
17
  readonly progressIntervalInMs?: number;
12
18
  readonly waveformSampleRate?: number;
13
19
  };
14
- export declare function loadWaveformPeaks(url: string, signal: AbortSignal, options?: LoadWaveformPeaksOptions): Promise<Float32Array>;
20
+ export declare function loadWaveformPeaks(src: string | InputAudioTrack, signal: AbortSignal, options?: LoadWaveformPeaksOptions): Promise<WaveformResult>;
@@ -5,44 +5,45 @@ exports.loadWaveformPeaks = loadWaveformPeaks;
5
5
  const mediabunny_1 = require("mediabunny");
6
6
  const constants_1 = require("./constants");
7
7
  Object.defineProperty(exports, "TARGET_SAMPLE_RATE", { enumerable: true, get: function () { return constants_1.TARGET_SAMPLE_RATE; } });
8
+ const get_waveform_cache_key_1 = require("./get-waveform-cache-key");
8
9
  const trim_audio_sample_before_zero_1 = require("./trim-audio-sample-before-zero");
9
10
  const waveform_peak_processor_1 = require("./waveform-peak-processor");
10
11
  const DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
11
12
  const peaksCache = new Map();
12
- async function loadWaveformPeaks(url, signal, options) {
13
+ async function loadWaveformPeaks(src, signal, options) {
13
14
  var _a, _b, _c;
14
15
  const waveformSampleRate = (_a = options === null || options === void 0 ? void 0 : options.waveformSampleRate) !== null && _a !== void 0 ? _a : constants_1.TARGET_SAMPLE_RATE;
15
16
  if (!Number.isFinite(waveformSampleRate) || waveformSampleRate <= 0) {
16
17
  throw new Error('The waveform sample rate must be a positive number.');
17
18
  }
18
- const cacheKey = `${waveformSampleRate}:${url}`;
19
+ const cacheKey = (0, get_waveform_cache_key_1.getWaveformCacheKey)(src, waveformSampleRate);
19
20
  const cached = peaksCache.get(cacheKey);
20
21
  if (cached) {
21
22
  (0, waveform_peak_processor_1.emitWaveformProgress)({
22
- peaks: cached,
23
- completedPeaks: cached.length,
24
- totalPeaks: cached.length,
23
+ peaks: cached.peaks,
24
+ averageVolume: cached.averageVolume,
25
+ completedPeaks: cached.peaks.length,
26
+ totalPeaks: cached.peaks.length,
25
27
  final: true,
26
28
  onProgress: options === null || options === void 0 ? void 0 : options.onProgress,
27
29
  });
28
30
  return cached;
29
31
  }
30
- const input = new mediabunny_1.Input({
31
- formats: mediabunny_1.ALL_FORMATS,
32
- source: new mediabunny_1.UrlSource(url),
33
- });
32
+ const input = typeof src === 'string'
33
+ ? new mediabunny_1.Input({ formats: mediabunny_1.ALL_FORMATS, source: new mediabunny_1.UrlSource(src) })
34
+ : null;
34
35
  try {
35
- const audioTrack = await input.getPrimaryAudioTrack();
36
+ const audioTrack = typeof src === 'string' ? await input.getPrimaryAudioTrack() : src;
36
37
  if (!audioTrack) {
37
- return new Float32Array(0);
38
+ return { peaks: new Float32Array(0), averageVolume: null };
38
39
  }
39
40
  if (await audioTrack.isLive()) {
40
41
  throw new Error('Live streams are not currently supported by Remotion. Sorry! Source: ' +
41
- url);
42
+ (typeof src === 'string' ? src : `audio track ${src.id}`));
42
43
  }
43
44
  if (await audioTrack.isRelativeToUnixEpoch()) {
44
45
  throw new Error('Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: ' +
45
- url);
46
+ (typeof src === 'string' ? src : `audio track ${src.id}`));
46
47
  }
47
48
  const audioSampleRate = await audioTrack.getSampleRate();
48
49
  const durationInSeconds = (_b = (await audioTrack.getDurationFromMetadata({ skipLiveWait: true }))) !== null && _b !== void 0 ? _b : (await audioTrack.computeDuration({ skipLiveWait: true }));
@@ -60,7 +61,7 @@ async function loadWaveformPeaks(url, signal, options) {
60
61
  for await (const sample of sink.samples()) {
61
62
  if (signal.aborted) {
62
63
  sample.close();
63
- return new Float32Array(0);
64
+ return { peaks: new Float32Array(0), averageVolume: null };
64
65
  }
65
66
  const startFrame = (0, trim_audio_sample_before_zero_1.getAudioSampleStartFrameAtTimelineZero)(sample);
66
67
  if (startFrame === null) {
@@ -86,15 +87,19 @@ async function loadWaveformPeaks(url, signal, options) {
86
87
  frameCount,
87
88
  });
88
89
  const channels = Math.max(1, sample.numberOfChannels);
90
+ const { sampleRate } = sample;
89
91
  sample.close();
90
- processor.processSampleChunk(floats, channels);
92
+ processor.processSampleChunk(floats, channels, sampleRate);
91
93
  }
92
94
  processor.finalize();
93
- const { peaks } = processor;
94
- peaksCache.set(cacheKey, peaks);
95
- return peaks;
95
+ const result = {
96
+ peaks: processor.peaks,
97
+ averageVolume: processor.averageVolume,
98
+ };
99
+ peaksCache.set(cacheKey, result);
100
+ return result;
96
101
  }
97
102
  finally {
98
- input.dispose();
103
+ input === null || input === void 0 ? void 0 : input.dispose();
99
104
  }
100
105
  }
@@ -1,6 +1,7 @@
1
+ import type { InputAudioTrack } from 'mediabunny';
1
2
  export declare const subscribeToWaveformPeaks: ({ src, waveformSampleRate, onPeaks, onError, }: {
2
- readonly src: string;
3
+ readonly src: string | InputAudioTrack;
3
4
  readonly waveformSampleRate?: number | undefined;
4
- readonly onPeaks: (peaks: Float32Array<ArrayBufferLike>, final: boolean) => void;
5
+ readonly onPeaks: (peaks: Float32Array<ArrayBufferLike>, final: boolean, averageVolume: number | null) => void;
5
6
  readonly onError: (error: Error) => void;
6
7
  }) => () => void;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.subscribeToWaveformPeaks = void 0;
4
4
  const constants_1 = require("./constants");
5
+ const get_waveform_cache_key_1 = require("./get-waveform-cache-key");
5
6
  const load_waveform_peaks_1 = require("./load-waveform-peaks");
6
7
  const make_audio_waveform_worker_1 = require("./make-audio-waveform-worker");
7
8
  // All AudioWaveform instances share one decode worker and one peaks cache,
@@ -13,9 +14,9 @@ const inFlightByRequestId = new Map();
13
14
  let worker = null;
14
15
  let workerFailed = false;
15
16
  let nextRequestId = 0;
16
- const emitPeaks = (load, peaks, final) => {
17
+ const emitPeaks = (load, peaks, final, averageVolume) => {
17
18
  if (final) {
18
- peaksCache.set(load.cacheKey, peaks);
19
+ peaksCache.set(load.cacheKey, { peaks, averageVolume });
19
20
  load.latestPeaks = null;
20
21
  inFlightByCacheKey.delete(load.cacheKey);
21
22
  if (load.requestId !== null) {
@@ -26,7 +27,7 @@ const emitPeaks = (load, peaks, final) => {
26
27
  load.latestPeaks = peaks;
27
28
  }
28
29
  for (const listener of load.listeners) {
29
- listener.onPeaks(peaks, final);
30
+ listener.onPeaks(peaks, final, averageVolume);
30
31
  }
31
32
  };
32
33
  const emitError = (load, error) => {
@@ -51,11 +52,11 @@ const startMainThreadLoad = (load) => {
51
52
  }
52
53
  // The processor mutates the same array in place; snapshot it so
53
54
  // React state updates see a new reference.
54
- emitPeaks(load, peaks.slice(), false);
55
+ emitPeaks(load, peaks.slice(), false, null);
55
56
  },
56
57
  })
57
- .then((peaks) => {
58
- emitPeaks(load, peaks, true);
58
+ .then(({ peaks, averageVolume }) => {
59
+ emitPeaks(load, peaks, true, averageVolume);
59
60
  })
60
61
  .catch((error) => {
61
62
  emitError(load, error instanceof Error
@@ -97,7 +98,7 @@ const getOrCreateWorker = () => {
97
98
  emitError(load, new Error(message.message));
98
99
  return;
99
100
  }
100
- emitPeaks(load, message.peaks, message.final);
101
+ emitPeaks(load, message.peaks, message.final, message.averageVolume);
101
102
  });
102
103
  worker.addEventListener('error', (event) => {
103
104
  event.preventDefault();
@@ -106,10 +107,10 @@ const getOrCreateWorker = () => {
106
107
  return worker;
107
108
  };
108
109
  const subscribeToWaveformPeaks = ({ src, waveformSampleRate = constants_1.TARGET_SAMPLE_RATE, onPeaks, onError, }) => {
109
- const cacheKey = `${waveformSampleRate}:${src}`;
110
+ const cacheKey = (0, get_waveform_cache_key_1.getWaveformCacheKey)(src, waveformSampleRate);
110
111
  const cached = peaksCache.get(cacheKey);
111
112
  if (cached) {
112
- onPeaks(cached, true);
113
+ onPeaks(cached.peaks, true, cached.averageVolume);
113
114
  return () => undefined;
114
115
  }
115
116
  const listener = { onPeaks, onError };
@@ -117,7 +118,7 @@ const subscribeToWaveformPeaks = ({ src, waveformSampleRate = constants_1.TARGET
117
118
  if (existing) {
118
119
  existing.listeners.add(listener);
119
120
  if (existing.latestPeaks) {
120
- onPeaks(existing.latestPeaks, false);
121
+ onPeaks(existing.latestPeaks, false, null);
121
122
  }
122
123
  return () => {
123
124
  existing.listeners.delete(listener);
@@ -132,8 +133,8 @@ const subscribeToWaveformPeaks = ({ src, waveformSampleRate = constants_1.TARGET
132
133
  requestId: null,
133
134
  };
134
135
  inFlightByCacheKey.set(cacheKey, load);
135
- const workerInstance = getOrCreateWorker();
136
- if (workerInstance) {
136
+ const workerInstance = typeof src === 'string' ? getOrCreateWorker() : null;
137
+ if (workerInstance && typeof src === 'string') {
137
138
  const requestId = nextRequestId++;
138
139
  load.requestId = requestId;
139
140
  inFlightByRequestId.set(requestId, load);
@@ -1,5 +1,6 @@
1
1
  type Progress = {
2
2
  readonly peaks: Float32Array;
3
+ readonly averageVolume: number | null;
3
4
  readonly completedPeaks: number;
4
5
  readonly totalPeaks: number;
5
6
  readonly final: boolean;
@@ -13,10 +14,11 @@ type WaveformPeakProcessorOptions = {
13
14
  };
14
15
  type WaveformPeakProcessor = {
15
16
  readonly peaks: Float32Array;
16
- processSampleChunk: (floats: Float32Array, channels: number) => void;
17
+ readonly averageVolume: number | null;
18
+ processSampleChunk: (floats: Float32Array, channels: number, sampleRate: number) => void;
17
19
  finalize: () => void;
18
20
  };
19
- export declare const emitWaveformProgress: ({ completedPeaks, final, onProgress, peaks, totalPeaks, }: Progress & {
21
+ export declare const emitWaveformProgress: ({ averageVolume, completedPeaks, final, onProgress, peaks, totalPeaks, }: Progress & {
20
22
  readonly onProgress?: ((progress: Progress) => void) | undefined;
21
23
  }) => void;
22
24
  export declare const createWaveformPeakProcessor: ({ totalPeaks, samplesPerPeak, onProgress, progressIntervalInMs, now, }: WaveformPeakProcessorOptions) => WaveformPeakProcessor;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createWaveformPeakProcessor = exports.emitWaveformProgress = void 0;
4
- const emitWaveformProgress = ({ completedPeaks, final, onProgress, peaks, totalPeaks, }) => {
4
+ const emitWaveformProgress = ({ averageVolume, completedPeaks, final, onProgress, peaks, totalPeaks, }) => {
5
5
  onProgress === null || onProgress === void 0 ? void 0 : onProgress({
6
+ averageVolume,
6
7
  peaks,
7
8
  completedPeaks,
8
9
  totalPeaks,
@@ -12,6 +13,8 @@ const emitWaveformProgress = ({ completedPeaks, final, onProgress, peaks, totalP
12
13
  exports.emitWaveformProgress = emitWaveformProgress;
13
14
  const createWaveformPeakProcessor = ({ totalPeaks, samplesPerPeak, onProgress, progressIntervalInMs, now, }) => {
14
15
  const peaks = new Float32Array(totalPeaks);
16
+ let energy = 0;
17
+ let duration = 0;
15
18
  let peakIndex = 0;
16
19
  let peakMax = 0;
17
20
  let sampleInPeak = 0;
@@ -28,6 +31,7 @@ const createWaveformPeakProcessor = ({ totalPeaks, samplesPerPeak, onProgress, p
28
31
  lastProgressAt = timestamp;
29
32
  lastProgressPeak = peakIndex;
30
33
  (0, exports.emitWaveformProgress)({
34
+ averageVolume: force && duration > 0 ? 10 * Math.log10(energy / duration) : null,
31
35
  peaks,
32
36
  completedPeaks: peakIndex,
33
37
  totalPeaks,
@@ -37,19 +41,26 @@ const createWaveformPeakProcessor = ({ totalPeaks, samplesPerPeak, onProgress, p
37
41
  };
38
42
  return {
39
43
  peaks,
40
- processSampleChunk: (floats, channels) => {
44
+ get averageVolume() {
45
+ return duration > 0 ? 10 * Math.log10(energy / duration) : null;
46
+ },
47
+ processSampleChunk: (floats, channels, sampleRate) => {
41
48
  var _a;
42
49
  const frameCount = Math.floor(floats.length / Math.max(1, channels));
43
50
  for (let frame = 0; frame < frameCount; frame++) {
44
51
  // `f32` copies are interleaved, so timing advances per frame.
45
52
  let framePeak = 0;
53
+ let frameEnergy = 0;
46
54
  for (let channel = 0; channel < channels; channel++) {
47
55
  const sampleIndex = frame * channels + channel;
48
56
  const abs = Math.abs((_a = floats[sampleIndex]) !== null && _a !== void 0 ? _a : 0);
57
+ frameEnergy += abs * abs;
49
58
  if (abs > framePeak) {
50
59
  framePeak = abs;
51
60
  }
52
61
  }
62
+ energy += frameEnergy / channels / sampleRate;
63
+ duration += 1 / sampleRate;
53
64
  if (framePeak > peakMax) {
54
65
  peakMax = framePeak;
55
66
  }
@@ -2,11 +2,12 @@
2
2
  /// <reference lib="webworker" />
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const load_waveform_peaks_1 = require("./audio-waveform/load-waveform-peaks");
5
- const postPeaks = (requestId, peaks, final) => {
5
+ const postPeaks = (requestId, peaks, final, averageVolume) => {
6
6
  // Structured cloning copies the array, so the decoder can keep
7
7
  // mutating its buffer while the main thread reads the snapshot.
8
8
  const payload = {
9
9
  type: 'peaks',
10
+ averageVolume,
10
11
  requestId,
11
12
  peaks,
12
13
  final,
@@ -29,12 +30,12 @@ self.addEventListener('message', (event) => {
29
30
  waveformSampleRate: message.waveformSampleRate,
30
31
  onProgress: ({ peaks, final }) => {
31
32
  if (!final) {
32
- postPeaks(message.requestId, peaks, false);
33
+ postPeaks(message.requestId, peaks, false, null);
33
34
  }
34
35
  },
35
36
  })
36
- .then((peaks) => {
37
- postPeaks(message.requestId, peaks, true);
37
+ .then(({ peaks, averageVolume }) => {
38
+ postPeaks(message.requestId, peaks, true, averageVolume);
38
39
  })
39
40
  .catch((error) => {
40
41
  postError(message.requestId, error);
@@ -4,6 +4,21 @@ import { ALL_FORMATS, AudioSampleSink, Input, UrlSource } from "mediabunny";
4
4
  // src/audio-waveform/constants.ts
5
5
  var TARGET_SAMPLE_RATE = 100;
6
6
 
7
+ // src/audio-waveform/get-waveform-cache-key.ts
8
+ var trackIds = new WeakMap;
9
+ var nextTrackId = 0;
10
+ var getWaveformCacheKey = (src, waveformSampleRate) => {
11
+ if (typeof src === "string") {
12
+ return `${waveformSampleRate}:url:${src}`;
13
+ }
14
+ let id = trackIds.get(src);
15
+ if (id === undefined) {
16
+ id = nextTrackId++;
17
+ trackIds.set(src, id);
18
+ }
19
+ return `${waveformSampleRate}:track:${id}`;
20
+ };
21
+
7
22
  // src/audio-waveform/trim-audio-sample-before-zero.ts
8
23
  var getAudioSampleStartFrameAtTimelineZero = (sample) => {
9
24
  if (sample.timestamp + sample.duration <= 0) {
@@ -17,6 +32,7 @@ var getAudioSampleStartFrameAtTimelineZero = (sample) => {
17
32
 
18
33
  // src/audio-waveform/waveform-peak-processor.ts
19
34
  var emitWaveformProgress = ({
35
+ averageVolume,
20
36
  completedPeaks,
21
37
  final,
22
38
  onProgress,
@@ -24,6 +40,7 @@ var emitWaveformProgress = ({
24
40
  totalPeaks
25
41
  }) => {
26
42
  onProgress?.({
43
+ averageVolume,
27
44
  peaks,
28
45
  completedPeaks,
29
46
  totalPeaks,
@@ -38,6 +55,8 @@ var createWaveformPeakProcessor = ({
38
55
  now
39
56
  }) => {
40
57
  const peaks = new Float32Array(totalPeaks);
58
+ let energy = 0;
59
+ let duration = 0;
41
60
  let peakIndex = 0;
42
61
  let peakMax = 0;
43
62
  let sampleInPeak = 0;
@@ -54,6 +73,7 @@ var createWaveformPeakProcessor = ({
54
73
  lastProgressAt = timestamp;
55
74
  lastProgressPeak = peakIndex;
56
75
  emitWaveformProgress({
76
+ averageVolume: force && duration > 0 ? 10 * Math.log10(energy / duration) : null,
57
77
  peaks,
58
78
  completedPeaks: peakIndex,
59
79
  totalPeaks,
@@ -63,17 +83,24 @@ var createWaveformPeakProcessor = ({
63
83
  };
64
84
  return {
65
85
  peaks,
66
- processSampleChunk: (floats, channels) => {
86
+ get averageVolume() {
87
+ return duration > 0 ? 10 * Math.log10(energy / duration) : null;
88
+ },
89
+ processSampleChunk: (floats, channels, sampleRate) => {
67
90
  const frameCount = Math.floor(floats.length / Math.max(1, channels));
68
91
  for (let frame = 0;frame < frameCount; frame++) {
69
92
  let framePeak = 0;
93
+ let frameEnergy = 0;
70
94
  for (let channel = 0;channel < channels; channel++) {
71
95
  const sampleIndex = frame * channels + channel;
72
96
  const abs = Math.abs(floats[sampleIndex] ?? 0);
97
+ frameEnergy += abs * abs;
73
98
  if (abs > framePeak) {
74
99
  framePeak = abs;
75
100
  }
76
101
  }
102
+ energy += frameEnergy / channels / sampleRate;
103
+ duration += 1 / sampleRate;
77
104
  if (framePeak > peakMax) {
78
105
  peakMax = framePeak;
79
106
  }
@@ -102,37 +129,35 @@ var createWaveformPeakProcessor = ({
102
129
  // src/audio-waveform/load-waveform-peaks.ts
103
130
  var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
104
131
  var peaksCache = new Map;
105
- async function loadWaveformPeaks(url, signal, options) {
132
+ async function loadWaveformPeaks(src, signal, options) {
106
133
  const waveformSampleRate = options?.waveformSampleRate ?? TARGET_SAMPLE_RATE;
107
134
  if (!Number.isFinite(waveformSampleRate) || waveformSampleRate <= 0) {
108
135
  throw new Error("The waveform sample rate must be a positive number.");
109
136
  }
110
- const cacheKey = `${waveformSampleRate}:${url}`;
137
+ const cacheKey = getWaveformCacheKey(src, waveformSampleRate);
111
138
  const cached = peaksCache.get(cacheKey);
112
139
  if (cached) {
113
140
  emitWaveformProgress({
114
- peaks: cached,
115
- completedPeaks: cached.length,
116
- totalPeaks: cached.length,
141
+ peaks: cached.peaks,
142
+ averageVolume: cached.averageVolume,
143
+ completedPeaks: cached.peaks.length,
144
+ totalPeaks: cached.peaks.length,
117
145
  final: true,
118
146
  onProgress: options?.onProgress
119
147
  });
120
148
  return cached;
121
149
  }
122
- const input = new Input({
123
- formats: ALL_FORMATS,
124
- source: new UrlSource(url)
125
- });
150
+ const input = typeof src === "string" ? new Input({ formats: ALL_FORMATS, source: new UrlSource(src) }) : null;
126
151
  try {
127
- const audioTrack = await input.getPrimaryAudioTrack();
152
+ const audioTrack = typeof src === "string" ? await input.getPrimaryAudioTrack() : src;
128
153
  if (!audioTrack) {
129
- return new Float32Array(0);
154
+ return { peaks: new Float32Array(0), averageVolume: null };
130
155
  }
131
156
  if (await audioTrack.isLive()) {
132
- throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + url);
157
+ throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + (typeof src === "string" ? src : `audio track ${src.id}`));
133
158
  }
134
159
  if (await audioTrack.isRelativeToUnixEpoch()) {
135
- throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + url);
160
+ throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + (typeof src === "string" ? src : `audio track ${src.id}`));
136
161
  }
137
162
  const audioSampleRate = await audioTrack.getSampleRate();
138
163
  const durationInSeconds = await audioTrack.getDurationFromMetadata({ skipLiveWait: true }) ?? await audioTrack.computeDuration({ skipLiveWait: true });
@@ -149,7 +174,7 @@ async function loadWaveformPeaks(url, signal, options) {
149
174
  for await (const sample of sink.samples()) {
150
175
  if (signal.aborted) {
151
176
  sample.close();
152
- return new Float32Array(0);
177
+ return { peaks: new Float32Array(0), averageVolume: null };
153
178
  }
154
179
  const startFrame = getAudioSampleStartFrameAtTimelineZero(sample);
155
180
  if (startFrame === null) {
@@ -175,22 +200,27 @@ async function loadWaveformPeaks(url, signal, options) {
175
200
  frameCount
176
201
  });
177
202
  const channels = Math.max(1, sample.numberOfChannels);
203
+ const { sampleRate } = sample;
178
204
  sample.close();
179
- processor.processSampleChunk(floats, channels);
205
+ processor.processSampleChunk(floats, channels, sampleRate);
180
206
  }
181
207
  processor.finalize();
182
- const { peaks } = processor;
183
- peaksCache.set(cacheKey, peaks);
184
- return peaks;
208
+ const result = {
209
+ peaks: processor.peaks,
210
+ averageVolume: processor.averageVolume
211
+ };
212
+ peaksCache.set(cacheKey, result);
213
+ return result;
185
214
  } finally {
186
- input.dispose();
215
+ input?.dispose();
187
216
  }
188
217
  }
189
218
 
190
219
  // src/audio-waveform-worker.ts
191
- var postPeaks = (requestId, peaks, final) => {
220
+ var postPeaks = (requestId, peaks, final, averageVolume) => {
192
221
  const payload = {
193
222
  type: "peaks",
223
+ averageVolume,
194
224
  requestId,
195
225
  peaks,
196
226
  final
@@ -213,11 +243,11 @@ self.addEventListener("message", (event) => {
213
243
  waveformSampleRate: message.waveformSampleRate,
214
244
  onProgress: ({ peaks, final }) => {
215
245
  if (!final) {
216
- postPeaks(message.requestId, peaks, false);
246
+ postPeaks(message.requestId, peaks, false, null);
217
247
  }
218
248
  }
219
- }).then((peaks) => {
220
- postPeaks(message.requestId, peaks, true);
249
+ }).then(({ peaks, averageVolume }) => {
250
+ postPeaks(message.requestId, peaks, true, averageVolume);
221
251
  }).catch((error) => {
222
252
  postError(message.requestId, error);
223
253
  });
@@ -17,6 +17,7 @@ var parseColor = (color) => {
17
17
 
18
18
  // src/audio-waveform/draw-peaks.ts
19
19
  var CLIPPING_COLOR = "#FF7F50";
20
+ var WAVEFORM_HEIGHT_SCALE = 0.8;
20
21
  var getVolumeAtBar = ({
21
22
  barIndex,
22
23
  numBars,
@@ -56,6 +57,7 @@ var drawBars = ({
56
57
  const imageData = ctx.createImageData(w, height);
57
58
  const { data } = imageData;
58
59
  const numBars = width;
60
+ const fullScaleHalfBar = height * WAVEFORM_HEIGHT_SCALE / 2;
59
61
  for (let barIndex = 0;barIndex < numBars; barIndex++) {
60
62
  const x = barIndex;
61
63
  if (x >= w)
@@ -64,15 +66,15 @@ var drawBars = ({
64
66
  const peak = peaks[peakIndex] || 0;
65
67
  const barVolume = getVolumeAtBar({ barIndex, numBars, volume });
66
68
  const scaledPeak = peak * barVolume;
67
- const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * height / 2));
69
+ const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * fullScaleHalfBar));
68
70
  if (halfBar === 0)
69
71
  continue;
70
72
  const mid = height / 2;
71
73
  const barY = Math.round(mid - halfBar);
72
74
  const barEnd = Math.round(mid + halfBar);
73
75
  const isClipping = scaledPeak > 1;
74
- const clipTopEnd = isClipping ? Math.min(barY + 2, barEnd) : barY;
75
- const clipBotStart = isClipping ? Math.max(barEnd - 2, barY) : barEnd;
76
+ const clipTopEnd = isClipping ? Math.min(Math.max(barY + 2, Math.round(mid - fullScaleHalfBar)), barEnd) : barY;
77
+ const clipBotStart = isClipping ? Math.max(Math.min(barEnd - 2, Math.round(mid + fullScaleHalfBar)), barY) : barEnd;
76
78
  for (let y = barY;y < clipTopEnd; y++) {
77
79
  const idx = (y * w + x) * 4;
78
80
  data[idx] = cr;
@@ -176,6 +178,21 @@ var getVisibleWaveformVolume = ({
176
178
  // src/audio-waveform/load-waveform-peaks.ts
177
179
  import { ALL_FORMATS, AudioSampleSink, Input, UrlSource } from "mediabunny";
178
180
 
181
+ // src/audio-waveform/get-waveform-cache-key.ts
182
+ var trackIds = new WeakMap;
183
+ var nextTrackId = 0;
184
+ var getWaveformCacheKey = (src, waveformSampleRate) => {
185
+ if (typeof src === "string") {
186
+ return `${waveformSampleRate}:url:${src}`;
187
+ }
188
+ let id = trackIds.get(src);
189
+ if (id === undefined) {
190
+ id = nextTrackId++;
191
+ trackIds.set(src, id);
192
+ }
193
+ return `${waveformSampleRate}:track:${id}`;
194
+ };
195
+
179
196
  // src/audio-waveform/trim-audio-sample-before-zero.ts
180
197
  var getAudioSampleStartFrameAtTimelineZero = (sample) => {
181
198
  if (sample.timestamp + sample.duration <= 0) {
@@ -189,6 +206,7 @@ var getAudioSampleStartFrameAtTimelineZero = (sample) => {
189
206
 
190
207
  // src/audio-waveform/waveform-peak-processor.ts
191
208
  var emitWaveformProgress = ({
209
+ averageVolume,
192
210
  completedPeaks,
193
211
  final,
194
212
  onProgress,
@@ -196,6 +214,7 @@ var emitWaveformProgress = ({
196
214
  totalPeaks
197
215
  }) => {
198
216
  onProgress?.({
217
+ averageVolume,
199
218
  peaks,
200
219
  completedPeaks,
201
220
  totalPeaks,
@@ -210,6 +229,8 @@ var createWaveformPeakProcessor = ({
210
229
  now
211
230
  }) => {
212
231
  const peaks = new Float32Array(totalPeaks);
232
+ let energy = 0;
233
+ let duration = 0;
213
234
  let peakIndex = 0;
214
235
  let peakMax = 0;
215
236
  let sampleInPeak = 0;
@@ -226,6 +247,7 @@ var createWaveformPeakProcessor = ({
226
247
  lastProgressAt = timestamp;
227
248
  lastProgressPeak = peakIndex;
228
249
  emitWaveformProgress({
250
+ averageVolume: force && duration > 0 ? 10 * Math.log10(energy / duration) : null,
229
251
  peaks,
230
252
  completedPeaks: peakIndex,
231
253
  totalPeaks,
@@ -235,17 +257,24 @@ var createWaveformPeakProcessor = ({
235
257
  };
236
258
  return {
237
259
  peaks,
238
- processSampleChunk: (floats, channels) => {
260
+ get averageVolume() {
261
+ return duration > 0 ? 10 * Math.log10(energy / duration) : null;
262
+ },
263
+ processSampleChunk: (floats, channels, sampleRate) => {
239
264
  const frameCount = Math.floor(floats.length / Math.max(1, channels));
240
265
  for (let frame = 0;frame < frameCount; frame++) {
241
266
  let framePeak = 0;
267
+ let frameEnergy = 0;
242
268
  for (let channel = 0;channel < channels; channel++) {
243
269
  const sampleIndex = frame * channels + channel;
244
270
  const abs = Math.abs(floats[sampleIndex] ?? 0);
271
+ frameEnergy += abs * abs;
245
272
  if (abs > framePeak) {
246
273
  framePeak = abs;
247
274
  }
248
275
  }
276
+ energy += frameEnergy / channels / sampleRate;
277
+ duration += 1 / sampleRate;
249
278
  if (framePeak > peakMax) {
250
279
  peakMax = framePeak;
251
280
  }
@@ -274,37 +303,35 @@ var createWaveformPeakProcessor = ({
274
303
  // src/audio-waveform/load-waveform-peaks.ts
275
304
  var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
276
305
  var peaksCache = new Map;
277
- async function loadWaveformPeaks(url, signal, options) {
306
+ async function loadWaveformPeaks(src, signal, options) {
278
307
  const waveformSampleRate = options?.waveformSampleRate ?? TARGET_SAMPLE_RATE;
279
308
  if (!Number.isFinite(waveformSampleRate) || waveformSampleRate <= 0) {
280
309
  throw new Error("The waveform sample rate must be a positive number.");
281
310
  }
282
- const cacheKey = `${waveformSampleRate}:${url}`;
311
+ const cacheKey = getWaveformCacheKey(src, waveformSampleRate);
283
312
  const cached = peaksCache.get(cacheKey);
284
313
  if (cached) {
285
314
  emitWaveformProgress({
286
- peaks: cached,
287
- completedPeaks: cached.length,
288
- totalPeaks: cached.length,
315
+ peaks: cached.peaks,
316
+ averageVolume: cached.averageVolume,
317
+ completedPeaks: cached.peaks.length,
318
+ totalPeaks: cached.peaks.length,
289
319
  final: true,
290
320
  onProgress: options?.onProgress
291
321
  });
292
322
  return cached;
293
323
  }
294
- const input = new Input({
295
- formats: ALL_FORMATS,
296
- source: new UrlSource(url)
297
- });
324
+ const input = typeof src === "string" ? new Input({ formats: ALL_FORMATS, source: new UrlSource(src) }) : null;
298
325
  try {
299
- const audioTrack = await input.getPrimaryAudioTrack();
326
+ const audioTrack = typeof src === "string" ? await input.getPrimaryAudioTrack() : src;
300
327
  if (!audioTrack) {
301
- return new Float32Array(0);
328
+ return { peaks: new Float32Array(0), averageVolume: null };
302
329
  }
303
330
  if (await audioTrack.isLive()) {
304
- throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + url);
331
+ throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + (typeof src === "string" ? src : `audio track ${src.id}`));
305
332
  }
306
333
  if (await audioTrack.isRelativeToUnixEpoch()) {
307
- throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + url);
334
+ throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + (typeof src === "string" ? src : `audio track ${src.id}`));
308
335
  }
309
336
  const audioSampleRate = await audioTrack.getSampleRate();
310
337
  const durationInSeconds = await audioTrack.getDurationFromMetadata({ skipLiveWait: true }) ?? await audioTrack.computeDuration({ skipLiveWait: true });
@@ -321,7 +348,7 @@ async function loadWaveformPeaks(url, signal, options) {
321
348
  for await (const sample of sink.samples()) {
322
349
  if (signal.aborted) {
323
350
  sample.close();
324
- return new Float32Array(0);
351
+ return { peaks: new Float32Array(0), averageVolume: null };
325
352
  }
326
353
  const startFrame = getAudioSampleStartFrameAtTimelineZero(sample);
327
354
  if (startFrame === null) {
@@ -347,15 +374,19 @@ async function loadWaveformPeaks(url, signal, options) {
347
374
  frameCount
348
375
  });
349
376
  const channels = Math.max(1, sample.numberOfChannels);
377
+ const { sampleRate } = sample;
350
378
  sample.close();
351
- processor.processSampleChunk(floats, channels);
379
+ processor.processSampleChunk(floats, channels, sampleRate);
352
380
  }
353
381
  processor.finalize();
354
- const { peaks } = processor;
355
- peaksCache.set(cacheKey, peaks);
356
- return peaks;
382
+ const result = {
383
+ peaks: processor.peaks,
384
+ averageVolume: processor.averageVolume
385
+ };
386
+ peaksCache.set(cacheKey, result);
387
+ return result;
357
388
  } finally {
358
- input.dispose();
389
+ input?.dispose();
359
390
  }
360
391
  }
361
392
  // src/audio-waveform/slice-waveform-peaks.ts
@@ -455,9 +486,9 @@ var inFlightByRequestId = new Map;
455
486
  var worker = null;
456
487
  var workerFailed = false;
457
488
  var nextRequestId = 0;
458
- var emitPeaks = (load, peaks, final) => {
489
+ var emitPeaks = (load, peaks, final, averageVolume) => {
459
490
  if (final) {
460
- peaksCache2.set(load.cacheKey, peaks);
491
+ peaksCache2.set(load.cacheKey, { peaks, averageVolume });
461
492
  load.latestPeaks = null;
462
493
  inFlightByCacheKey.delete(load.cacheKey);
463
494
  if (load.requestId !== null) {
@@ -467,7 +498,7 @@ var emitPeaks = (load, peaks, final) => {
467
498
  load.latestPeaks = peaks;
468
499
  }
469
500
  for (const listener of load.listeners) {
470
- listener.onPeaks(peaks, final);
501
+ listener.onPeaks(peaks, final, averageVolume);
471
502
  }
472
503
  };
473
504
  var emitError = (load, error) => {
@@ -488,10 +519,10 @@ var startMainThreadLoad = (load) => {
488
519
  if (final) {
489
520
  return;
490
521
  }
491
- emitPeaks(load, peaks.slice(), false);
522
+ emitPeaks(load, peaks.slice(), false, null);
492
523
  }
493
- }).then((peaks) => {
494
- emitPeaks(load, peaks, true);
524
+ }).then(({ peaks, averageVolume }) => {
525
+ emitPeaks(load, peaks, true, averageVolume);
495
526
  }).catch((error) => {
496
527
  emitError(load, error instanceof Error ? error : new Error("Failed to load waveform peaks"));
497
528
  });
@@ -529,7 +560,7 @@ var getOrCreateWorker = () => {
529
560
  emitError(load, new Error(message.message));
530
561
  return;
531
562
  }
532
- emitPeaks(load, message.peaks, message.final);
563
+ emitPeaks(load, message.peaks, message.final, message.averageVolume);
533
564
  });
534
565
  worker.addEventListener("error", (event) => {
535
566
  event.preventDefault();
@@ -543,10 +574,10 @@ var subscribeToWaveformPeaks = ({
543
574
  onPeaks,
544
575
  onError
545
576
  }) => {
546
- const cacheKey = `${waveformSampleRate}:${src}`;
577
+ const cacheKey = getWaveformCacheKey(src, waveformSampleRate);
547
578
  const cached = peaksCache2.get(cacheKey);
548
579
  if (cached) {
549
- onPeaks(cached, true);
580
+ onPeaks(cached.peaks, true, cached.averageVolume);
550
581
  return () => {
551
582
  return;
552
583
  };
@@ -556,7 +587,7 @@ var subscribeToWaveformPeaks = ({
556
587
  if (existing) {
557
588
  existing.listeners.add(listener);
558
589
  if (existing.latestPeaks) {
559
- onPeaks(existing.latestPeaks, false);
590
+ onPeaks(existing.latestPeaks, false, null);
560
591
  }
561
592
  return () => {
562
593
  existing.listeners.delete(listener);
@@ -571,8 +602,8 @@ var subscribeToWaveformPeaks = ({
571
602
  requestId: null
572
603
  };
573
604
  inFlightByCacheKey.set(cacheKey, load);
574
- const workerInstance = getOrCreateWorker();
575
- if (workerInstance) {
605
+ const workerInstance = typeof src === "string" ? getOrCreateWorker() : null;
606
+ if (workerInstance && typeof src === "string") {
576
607
  const requestId = nextRequestId++;
577
608
  load.requestId = requestId;
578
609
  inFlightByRequestId.set(requestId, load);
@@ -1099,6 +1130,13 @@ var renderFrameStripToCanvas = async ({
1099
1130
  frameHeight
1100
1131
  });
1101
1132
  };
1133
+ // src/format-average-audio-volume.ts
1134
+ var formatAverageAudioVolume = (volume) => {
1135
+ if (volume === null) {
1136
+ return "Unavailable";
1137
+ }
1138
+ return volume === -Infinity ? "−∞ dBFS (silent)" : `${volume.toFixed(1)} dBFS`;
1139
+ };
1102
1140
  export {
1103
1141
  subscribeToWaveformPeaks,
1104
1142
  sliceWaveformPeaks,
@@ -1116,6 +1154,7 @@ export {
1116
1154
  getDurationOfOneFrame,
1117
1155
  getAspectRatioFromCache,
1118
1156
  frameDatabase,
1157
+ formatAverageAudioVolume,
1119
1158
  fillWithCachedFrames,
1120
1159
  fillFrameWhereItFits,
1121
1160
  extractFrames,
@@ -0,0 +1 @@
1
+ export declare const formatAverageAudioVolume: (volume: number | null) => string;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatAverageAudioVolume = void 0;
4
+ const formatAverageAudioVolume = (volume) => {
5
+ if (volume === null) {
6
+ return 'Unavailable';
7
+ }
8
+ return volume === -Infinity
9
+ ? '−∞ dBFS (silent)'
10
+ : `${volume.toFixed(1)} dBFS`;
11
+ };
12
+ exports.formatAverageAudioVolume = formatAverageAudioVolume;
package/dist/index.d.ts CHANGED
@@ -18,3 +18,4 @@ export { WEBCODECS_TIMESCALE, calculateTimestampSlots, drawSlot, ensureSlots, fi
18
18
  export { renderFrameStripToCanvas } from './render-frame-strip-to-canvas';
19
19
  export type { RenderFrameStripToCanvasOptions } from './render-frame-strip-to-canvas';
20
20
  export { resizeVideoFrame } from './resize-video-frame';
21
+ export { formatAverageAudioVolume } from './format-average-audio-volume';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resizeVideoFrame = exports.renderFrameStripToCanvas = exports.getDurationOfOneFrame = exports.fillWithCachedFrames = exports.fillFrameWhereItFits = exports.ensureSlots = exports.drawSlot = exports.calculateTimestampSlots = exports.WEBCODECS_TIMESCALE = exports.shouldTileLoopDisplay = exports.getLoopDisplayWidth = exports.getLoopDisplaySegments = exports.makeFrameDatabaseKey = exports.getTimestampFromFrameDatabaseKey = exports.getFrameDatabaseKeyPrefix = exports.getAspectRatioFromCache = exports.frameDatabase = exports.aspectRatioCache = exports.addFrameToCache = exports.extractFrames = exports.clampTimestampsToDuration = exports.emitWaveformProgress = exports.createWaveformPeakProcessor = exports.sliceWaveformPeaks = exports.subscribeToWaveformPeaks = exports.sliceVisibleWaveformPeaks = exports.loadWaveformPeaks = exports.getVisibleWaveformVolume = exports.drawBars = exports.TARGET_SAMPLE_RATE = void 0;
3
+ exports.formatAverageAudioVolume = exports.resizeVideoFrame = exports.renderFrameStripToCanvas = exports.getDurationOfOneFrame = exports.fillWithCachedFrames = exports.fillFrameWhereItFits = exports.ensureSlots = exports.drawSlot = exports.calculateTimestampSlots = exports.WEBCODECS_TIMESCALE = exports.shouldTileLoopDisplay = exports.getLoopDisplayWidth = exports.getLoopDisplaySegments = exports.makeFrameDatabaseKey = exports.getTimestampFromFrameDatabaseKey = exports.getFrameDatabaseKeyPrefix = exports.getAspectRatioFromCache = exports.frameDatabase = exports.aspectRatioCache = exports.addFrameToCache = exports.extractFrames = exports.clampTimestampsToDuration = exports.emitWaveformProgress = exports.createWaveformPeakProcessor = exports.sliceWaveformPeaks = exports.subscribeToWaveformPeaks = exports.sliceVisibleWaveformPeaks = exports.loadWaveformPeaks = exports.getVisibleWaveformVolume = exports.drawBars = exports.TARGET_SAMPLE_RATE = void 0;
4
4
  const constants_1 = require("./audio-waveform/constants");
5
5
  Object.defineProperty(exports, "TARGET_SAMPLE_RATE", { enumerable: true, get: function () { return constants_1.TARGET_SAMPLE_RATE; } });
6
6
  const draw_peaks_1 = require("./audio-waveform/draw-peaks");
@@ -46,3 +46,5 @@ const render_frame_strip_to_canvas_1 = require("./render-frame-strip-to-canvas")
46
46
  Object.defineProperty(exports, "renderFrameStripToCanvas", { enumerable: true, get: function () { return render_frame_strip_to_canvas_1.renderFrameStripToCanvas; } });
47
47
  const resize_video_frame_1 = require("./resize-video-frame");
48
48
  Object.defineProperty(exports, "resizeVideoFrame", { enumerable: true, get: function () { return resize_video_frame_1.resizeVideoFrame; } });
49
+ const format_average_audio_volume_1 = require("./format-average-audio-volume");
50
+ Object.defineProperty(exports, "formatAverageAudioVolume", { enumerable: true, get: function () { return format_average_audio_volume_1.formatAverageAudioVolume; } });
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "url": "https://github.com/remotion-dev/remotion/tree/main/packages/timeline-utils"
4
4
  },
5
5
  "name": "@remotion/timeline-utils",
6
- "version": "4.0.522",
6
+ "version": "4.0.523",
7
7
  "description": "Internal utilities for rendering Remotion timelines",
8
8
  "main": "dist/index.js",
9
9
  "scripts": {
@@ -22,11 +22,11 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "mediabunny": "1.55.5",
25
- "remotion": "4.0.522"
25
+ "remotion": "4.0.523"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@mediabunny/server": "1.55.5",
29
- "@remotion/eslint-config-internal": "4.0.522",
29
+ "@remotion/eslint-config-internal": "4.0.523",
30
30
  "eslint": "9.19.0",
31
31
  "@typescript/native-preview": "7.0.0-dev.20260217.1"
32
32
  },