@remotion/timeline-utils 4.0.514 → 4.0.515

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.
@@ -1,31 +1,18 @@
1
- import type { TimelineLoopDisplay } from '../loop-display';
2
- import type { WaveformVolume } from './draw-peaks';
3
- export type AudioWaveformWorkerInitMessage = {
4
- readonly type: 'init';
5
- readonly canvas: OffscreenCanvas;
6
- };
7
- export type AudioWaveformWorkerRenderMessage = {
8
- readonly type: 'render';
1
+ export type AudioWaveformWorkerLoadMessage = {
2
+ readonly type: 'load';
9
3
  readonly requestId: number;
10
4
  readonly src: string;
11
- readonly width: number;
12
- readonly height: number;
13
- readonly volume: WaveformVolume;
14
- readonly startFrom: number;
15
- readonly durationInFrames: number;
16
- readonly displayOffsetInFrames: number;
17
- readonly displayDurationInFrames: number;
18
- readonly fps: number;
19
- readonly playbackRate: number;
20
- readonly loopDisplay: TimelineLoopDisplay | undefined;
21
5
  };
22
- export type AudioWaveformWorkerDisposeMessage = {
23
- readonly type: 'dispose';
6
+ export type AudioWaveformWorkerIncomingMessage = AudioWaveformWorkerLoadMessage;
7
+ export type AudioWaveformWorkerPeaksMessage = {
8
+ readonly type: 'peaks';
9
+ readonly requestId: number;
10
+ readonly peaks: Float32Array;
11
+ readonly final: boolean;
24
12
  };
25
- export type AudioWaveformWorkerIncomingMessage = AudioWaveformWorkerInitMessage | AudioWaveformWorkerRenderMessage | AudioWaveformWorkerDisposeMessage;
26
13
  export type AudioWaveformWorkerErrorMessage = {
27
14
  readonly type: 'error';
28
15
  readonly requestId: number;
29
16
  readonly message: string;
30
17
  };
31
- export type AudioWaveformWorkerOutgoingMessage = AudioWaveformWorkerErrorMessage;
18
+ export type AudioWaveformWorkerOutgoingMessage = AudioWaveformWorkerPeaksMessage | AudioWaveformWorkerErrorMessage;
@@ -0,0 +1,5 @@
1
+ export declare const subscribeToWaveformPeaks: ({ src, onPeaks, onError, }: {
2
+ readonly src: string;
3
+ readonly onPeaks: (peaks: Float32Array<ArrayBufferLike>, final: boolean) => void;
4
+ readonly onError: (error: Error) => void;
5
+ }) => () => void;
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.subscribeToWaveformPeaks = void 0;
4
+ const load_waveform_peaks_1 = require("./load-waveform-peaks");
5
+ const make_audio_waveform_worker_1 = require("./make-audio-waveform-worker");
6
+ // All AudioWaveform instances share one decode worker and one peaks cache,
7
+ // so remounts (timeline virtualization, zoom window cropping) and multiple
8
+ // clips with the same src never re-fetch or re-decode the audio.
9
+ const peaksCache = new Map();
10
+ const inFlightBySrc = new Map();
11
+ const inFlightByRequestId = new Map();
12
+ let worker = null;
13
+ let workerFailed = false;
14
+ let nextRequestId = 0;
15
+ const emitPeaks = (load, peaks, final) => {
16
+ if (final) {
17
+ peaksCache.set(load.src, peaks);
18
+ inFlightBySrc.delete(load.src);
19
+ if (load.requestId !== null) {
20
+ inFlightByRequestId.delete(load.requestId);
21
+ }
22
+ }
23
+ else {
24
+ load.latestPeaks = peaks;
25
+ }
26
+ for (const listener of load.listeners) {
27
+ listener.onPeaks(peaks, final);
28
+ }
29
+ };
30
+ const emitError = (load, error) => {
31
+ inFlightBySrc.delete(load.src);
32
+ if (load.requestId !== null) {
33
+ inFlightByRequestId.delete(load.requestId);
34
+ }
35
+ for (const listener of load.listeners) {
36
+ listener.onError(error);
37
+ }
38
+ };
39
+ const startMainThreadLoad = (load) => {
40
+ load.requestId = null;
41
+ // Never aborted: even if all subscribers unsubscribe, finishing the
42
+ // decode fills the cache for the next mount.
43
+ const controller = new AbortController();
44
+ (0, load_waveform_peaks_1.loadWaveformPeaks)(load.src, controller.signal, {
45
+ onProgress: ({ peaks, final }) => {
46
+ if (final) {
47
+ return;
48
+ }
49
+ // The processor mutates the same array in place; snapshot it so
50
+ // React state updates see a new reference.
51
+ emitPeaks(load, peaks.slice(), false);
52
+ },
53
+ })
54
+ .then((peaks) => {
55
+ emitPeaks(load, peaks, true);
56
+ })
57
+ .catch((error) => {
58
+ emitError(load, error instanceof Error
59
+ ? error
60
+ : new Error('Failed to load waveform peaks'));
61
+ });
62
+ };
63
+ const handleWorkerFailure = () => {
64
+ workerFailed = true;
65
+ worker === null || worker === void 0 ? void 0 : worker.terminate();
66
+ worker = null;
67
+ const pending = [...inFlightByRequestId.values()];
68
+ inFlightByRequestId.clear();
69
+ for (const load of pending) {
70
+ startMainThreadLoad(load);
71
+ }
72
+ };
73
+ const getOrCreateWorker = () => {
74
+ if (workerFailed || typeof Worker === 'undefined') {
75
+ return null;
76
+ }
77
+ if (worker) {
78
+ return worker;
79
+ }
80
+ try {
81
+ worker = (0, make_audio_waveform_worker_1.makeAudioWaveformWorker)();
82
+ }
83
+ catch (_a) {
84
+ workerFailed = true;
85
+ return null;
86
+ }
87
+ worker.addEventListener('message', (event) => {
88
+ const message = event.data;
89
+ const load = inFlightByRequestId.get(message.requestId);
90
+ if (!load) {
91
+ return;
92
+ }
93
+ if (message.type === 'error') {
94
+ emitError(load, new Error(message.message));
95
+ return;
96
+ }
97
+ emitPeaks(load, message.peaks, message.final);
98
+ });
99
+ worker.addEventListener('error', (event) => {
100
+ event.preventDefault();
101
+ handleWorkerFailure();
102
+ });
103
+ return worker;
104
+ };
105
+ const subscribeToWaveformPeaks = ({ src, onPeaks, onError, }) => {
106
+ const cached = peaksCache.get(src);
107
+ if (cached) {
108
+ onPeaks(cached, true);
109
+ return () => undefined;
110
+ }
111
+ const listener = { onPeaks, onError };
112
+ const existing = inFlightBySrc.get(src);
113
+ if (existing) {
114
+ existing.listeners.add(listener);
115
+ if (existing.latestPeaks) {
116
+ onPeaks(existing.latestPeaks, false);
117
+ }
118
+ return () => {
119
+ existing.listeners.delete(listener);
120
+ };
121
+ }
122
+ const load = {
123
+ src,
124
+ listeners: new Set([listener]),
125
+ latestPeaks: null,
126
+ requestId: null,
127
+ };
128
+ inFlightBySrc.set(src, load);
129
+ const workerInstance = getOrCreateWorker();
130
+ if (workerInstance) {
131
+ const requestId = nextRequestId++;
132
+ load.requestId = requestId;
133
+ inFlightByRequestId.set(requestId, load);
134
+ const message = {
135
+ type: 'load',
136
+ requestId,
137
+ src,
138
+ };
139
+ workerInstance.postMessage(message);
140
+ }
141
+ else {
142
+ startMainThreadLoad(load);
143
+ }
144
+ return () => {
145
+ load.listeners.delete(listener);
146
+ };
147
+ };
148
+ exports.subscribeToWaveformPeaks = subscribeToWaveformPeaks;
@@ -1,14 +1,20 @@
1
1
  "use strict";
2
2
  /// <reference lib="webworker" />
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- const draw_peaks_1 = require("./audio-waveform/draw-peaks");
5
4
  const load_waveform_peaks_1 = require("./audio-waveform/load-waveform-peaks");
6
- const slice_visible_waveform_peaks_1 = require("./audio-waveform/slice-visible-waveform-peaks");
7
- let canvas = null;
8
- let currentController = null;
9
- let latestRequestId = 0;
5
+ const postPeaks = (requestId, peaks, final) => {
6
+ // Structured cloning copies the array, so the decoder can keep
7
+ // mutating its buffer while the main thread reads the snapshot.
8
+ const payload = {
9
+ type: 'peaks',
10
+ requestId,
11
+ peaks,
12
+ final,
13
+ };
14
+ self.postMessage(payload);
15
+ };
10
16
  const postError = (requestId, error) => {
11
- const message = error instanceof Error ? error.message : 'Failed to render waveform';
17
+ const message = error instanceof Error ? error.message : 'Failed to load waveform';
12
18
  const payload = {
13
19
  type: 'error',
14
20
  requestId,
@@ -16,74 +22,20 @@ const postError = (requestId, error) => {
16
22
  };
17
23
  self.postMessage(payload);
18
24
  };
19
- const drawPartialWaveform = (message, peaks) => {
20
- if (!canvas) {
21
- return;
22
- }
23
- const portionPeaks = (0, slice_visible_waveform_peaks_1.sliceVisibleWaveformPeaks)({
24
- displayDurationInFrames: message.displayDurationInFrames,
25
- displayOffsetInFrames: message.displayOffsetInFrames,
26
- durationInFrames: message.durationInFrames,
27
- fps: message.fps,
28
- loopDisplay: message.loopDisplay,
29
- peaks,
30
- playbackRate: message.playbackRate,
31
- startFrom: message.startFrom,
32
- });
33
- (0, draw_peaks_1.drawBars)({
34
- canvas,
35
- peaks: portionPeaks,
36
- color: 'rgba(255, 255, 255, 0.6)',
37
- volume: message.volume,
38
- width: message.width,
39
- });
40
- };
41
- const renderWaveform = async (message) => {
42
- if (!canvas) {
43
- postError(message.requestId, new Error('Waveform canvas not initialized'));
44
- return;
45
- }
46
- const controller = new AbortController();
47
- currentController === null || currentController === void 0 ? void 0 : currentController.abort();
48
- currentController = controller;
49
- latestRequestId = message.requestId;
50
- try {
51
- canvas.width = message.width;
52
- canvas.height = message.height;
53
- const peaks = await (0, load_waveform_peaks_1.loadWaveformPeaks)(message.src, controller.signal, {
54
- onProgress: ({ peaks: nextPeaks }) => {
55
- if (controller.signal.aborted ||
56
- latestRequestId !== message.requestId) {
57
- return;
58
- }
59
- drawPartialWaveform(message, nextPeaks);
60
- },
61
- });
62
- if (controller.signal.aborted || latestRequestId !== message.requestId) {
63
- return;
64
- }
65
- drawPartialWaveform(message, peaks);
66
- }
67
- catch (error) {
68
- if (controller.signal.aborted || latestRequestId !== message.requestId) {
69
- return;
70
- }
71
- postError(message.requestId, error);
72
- }
73
- };
74
25
  self.addEventListener('message', (event) => {
75
26
  const message = event.data;
76
- if (message.type === 'init') {
77
- canvas = message.canvas;
78
- return;
79
- }
80
- if (message.type === 'dispose') {
81
- currentController === null || currentController === void 0 ? void 0 : currentController.abort();
82
- currentController = null;
83
- canvas = null;
84
- return;
85
- }
86
- renderWaveform(message).catch((error) => {
27
+ const controller = new AbortController();
28
+ (0, load_waveform_peaks_1.loadWaveformPeaks)(message.src, controller.signal, {
29
+ onProgress: ({ peaks, final }) => {
30
+ if (!final) {
31
+ postPeaks(message.requestId, peaks, false);
32
+ }
33
+ },
34
+ })
35
+ .then((peaks) => {
36
+ postPeaks(message.requestId, peaks, true);
37
+ })
38
+ .catch((error) => {
87
39
  postError(message.requestId, error);
88
40
  });
89
41
  });
@@ -1,101 +1,3 @@
1
- // src/audio-waveform/parse-color.ts
2
- var colorCache = new Map;
3
- var parseColor = (color) => {
4
- const cached = colorCache.get(color);
5
- if (cached)
6
- return cached;
7
- const ctx = new OffscreenCanvas(1, 1).getContext("2d");
8
- ctx.fillStyle = color;
9
- ctx.fillRect(0, 0, 1, 1);
10
- const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
11
- const result = [r, g, b, a];
12
- colorCache.set(color, result);
13
- return result;
14
- };
15
-
16
- // src/audio-waveform/draw-peaks.ts
17
- var CLIPPING_COLOR = "#FF7F50";
18
- var getVolumeAtBar = ({
19
- barIndex,
20
- numBars,
21
- volume
22
- }) => {
23
- if (typeof volume === "number") {
24
- return volume;
25
- }
26
- if (volume.length === 0) {
27
- return 1;
28
- }
29
- if (volume.length === 1 || numBars <= 1) {
30
- return volume[0];
31
- }
32
- const volumeIndex = Math.round(barIndex / (numBars - 1) * (volume.length - 1));
33
- return volume[volumeIndex] ?? 1;
34
- };
35
- var drawBars = ({
36
- canvas,
37
- color,
38
- peaks,
39
- volume,
40
- width
41
- }) => {
42
- const ctx = canvas.getContext("2d");
43
- if (!ctx) {
44
- throw new Error("Failed to get canvas context");
45
- }
46
- const { height } = canvas;
47
- const w = canvas.width;
48
- if (w === 0 || height === 0) {
49
- return;
50
- }
51
- ctx.clearRect(0, 0, w, height);
52
- const [r, g, b, a] = parseColor(color);
53
- const [cr, cg, cb, ca] = parseColor(CLIPPING_COLOR);
54
- const imageData = ctx.createImageData(w, height);
55
- const { data } = imageData;
56
- const numBars = width;
57
- for (let barIndex = 0;barIndex < numBars; barIndex++) {
58
- const x = barIndex;
59
- if (x >= w)
60
- break;
61
- const peakIndex = Math.floor(barIndex / numBars * peaks.length);
62
- const peak = peaks[peakIndex] || 0;
63
- const barVolume = getVolumeAtBar({ barIndex, numBars, volume });
64
- const scaledPeak = peak * barVolume;
65
- const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * height / 2));
66
- if (halfBar === 0)
67
- continue;
68
- const mid = height / 2;
69
- const barY = Math.round(mid - halfBar);
70
- const barEnd = Math.round(mid + halfBar);
71
- const isClipping = scaledPeak > 1;
72
- const clipTopEnd = isClipping ? Math.min(barY + 2, barEnd) : barY;
73
- const clipBotStart = isClipping ? Math.max(barEnd - 2, barY) : barEnd;
74
- for (let y = barY;y < clipTopEnd; y++) {
75
- const idx = (y * w + x) * 4;
76
- data[idx] = cr;
77
- data[idx + 1] = cg;
78
- data[idx + 2] = cb;
79
- data[idx + 3] = ca;
80
- }
81
- for (let y = clipTopEnd;y < clipBotStart; y++) {
82
- const idx = (y * w + x) * 4;
83
- data[idx] = r;
84
- data[idx + 1] = g;
85
- data[idx + 2] = b;
86
- data[idx + 3] = a;
87
- }
88
- for (let y = clipBotStart;y < barEnd; y++) {
89
- const idx = (y * w + x) * 4;
90
- data[idx] = cr;
91
- data[idx + 1] = cg;
92
- data[idx + 2] = cb;
93
- data[idx + 3] = ca;
94
- }
95
- }
96
- ctx.putImageData(imageData, 0, 0);
97
- };
98
-
99
1
  // src/audio-waveform/load-waveform-peaks.ts
100
2
  import { ALL_FORMATS, AudioSampleSink, Input, UrlSource } from "mediabunny";
101
3
 
@@ -280,135 +182,18 @@ async function loadWaveformPeaks(url, signal, options) {
280
182
  }
281
183
  }
282
184
 
283
- // src/loop-display.ts
284
- var shouldTileLoopDisplay = (loopDisplay) => {
285
- return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
286
- };
287
- var getLoopDisplaySegments = ({
288
- displayDurationInFrames,
289
- displayOffsetInFrames,
290
- loopDurationInFrames
291
- }) => {
292
- if (!Number.isFinite(displayDurationInFrames) || displayDurationInFrames <= 0 || !Number.isFinite(displayOffsetInFrames) || !Number.isFinite(loopDurationInFrames) || loopDurationInFrames <= 0) {
293
- return [];
294
- }
295
- const displayEndInFrames = displayOffsetInFrames + displayDurationInFrames;
296
- const firstLoopIndex = Math.floor(displayOffsetInFrames / loopDurationInFrames);
297
- const lastLoopIndex = Math.floor(displayEndInFrames / loopDurationInFrames);
298
- const segments = [];
299
- for (let loopIndex = firstLoopIndex;loopIndex <= lastLoopIndex; loopIndex++) {
300
- const loopStartInFrames = loopIndex * loopDurationInFrames;
301
- const segmentStart = Math.max(displayOffsetInFrames, loopStartInFrames);
302
- const segmentEnd = Math.min(displayEndInFrames, loopStartInFrames + loopDurationInFrames);
303
- const durationInFrames = segmentEnd - segmentStart;
304
- if (durationInFrames <= 0) {
305
- continue;
306
- }
307
- segments.push({
308
- loopIndex,
309
- absoluteOffsetInFrames: segmentStart,
310
- loopOffsetInFrames: segmentStart - loopStartInFrames,
311
- durationInFrames
312
- });
313
- }
314
- return segments;
315
- };
316
- var getLoopDisplayWidth = ({
317
- visualizationWidth,
318
- loopDisplay
319
- }) => {
320
- if (!shouldTileLoopDisplay(loopDisplay)) {
321
- return visualizationWidth;
322
- }
323
- return visualizationWidth / loopDisplay.numberOfTimes;
324
- };
325
-
326
- // src/audio-waveform/slice-waveform-peaks.ts
327
- var sliceWaveformPeaks = ({
328
- durationInFrames,
329
- fps,
330
- peaks,
331
- playbackRate,
332
- startFrom
333
- }) => {
334
- if (peaks.length === 0) {
335
- return peaks;
336
- }
337
- const startTimeInSeconds = startFrom / fps;
338
- const durationInSeconds = durationInFrames / fps * playbackRate;
339
- const startPeakIndex = Math.floor(startTimeInSeconds * TARGET_SAMPLE_RATE);
340
- const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * TARGET_SAMPLE_RATE);
341
- if (!Number.isFinite(startPeakIndex) || !Number.isFinite(endPeakIndex)) {
342
- return peaks.subarray(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
343
- }
344
- if (startPeakIndex >= 0 && endPeakIndex <= peaks.length) {
345
- return peaks.subarray(startPeakIndex, endPeakIndex);
346
- }
347
- const portion = new Float32Array(Math.max(0, endPeakIndex - startPeakIndex));
348
- const sourceStart = Math.max(0, startPeakIndex);
349
- const sourceEnd = Math.min(peaks.length, endPeakIndex);
350
- if (sourceStart < sourceEnd) {
351
- portion.set(peaks.subarray(sourceStart, sourceEnd), sourceStart - startPeakIndex);
352
- }
353
- return portion;
354
- };
355
-
356
- // src/audio-waveform/slice-visible-waveform-peaks.ts
357
- var sliceVisibleWaveformPeaks = ({
358
- displayDurationInFrames,
359
- displayOffsetInFrames,
360
- durationInFrames,
361
- fps,
362
- loopDisplay,
363
- peaks,
364
- playbackRate,
365
- startFrom
366
- }) => {
367
- if (!shouldTileLoopDisplay(loopDisplay) || loopDisplay.durationInFrames <= 0) {
368
- return sliceWaveformPeaks({
369
- durationInFrames: Math.min(displayDurationInFrames, Math.max(0, durationInFrames - displayOffsetInFrames)),
370
- fps,
371
- peaks,
372
- playbackRate,
373
- startFrom: startFrom + displayOffsetInFrames * playbackRate
374
- });
375
- }
376
- const segments = getLoopDisplaySegments({
377
- displayDurationInFrames,
378
- displayOffsetInFrames,
379
- loopDurationInFrames: loopDisplay.durationInFrames
380
- });
381
- const parts = [];
382
- let totalLength = 0;
383
- for (const segment of segments) {
384
- const part = sliceWaveformPeaks({
385
- durationInFrames: segment.durationInFrames,
386
- fps,
387
- peaks,
388
- playbackRate,
389
- startFrom: startFrom + segment.loopOffsetInFrames * playbackRate
390
- });
391
- parts.push(part);
392
- totalLength += part.length;
393
- }
394
- if (parts.length === 1) {
395
- return parts[0];
396
- }
397
- const result = new Float32Array(totalLength);
398
- let writeOffset = 0;
399
- for (const part of parts) {
400
- result.set(part, writeOffset);
401
- writeOffset += part.length;
402
- }
403
- return result;
404
- };
405
-
406
185
  // src/audio-waveform-worker.ts
407
- var canvas = null;
408
- var currentController = null;
409
- var latestRequestId = 0;
186
+ var postPeaks = (requestId, peaks, final) => {
187
+ const payload = {
188
+ type: "peaks",
189
+ requestId,
190
+ peaks,
191
+ final
192
+ };
193
+ self.postMessage(payload);
194
+ };
410
195
  var postError = (requestId, error) => {
411
- const message = error instanceof Error ? error.message : "Failed to render waveform";
196
+ const message = error instanceof Error ? error.message : "Failed to load waveform";
412
197
  const payload = {
413
198
  type: "error",
414
199
  requestId,
@@ -416,72 +201,18 @@ var postError = (requestId, error) => {
416
201
  };
417
202
  self.postMessage(payload);
418
203
  };
419
- var drawPartialWaveform = (message, peaks) => {
420
- if (!canvas) {
421
- return;
422
- }
423
- const portionPeaks = sliceVisibleWaveformPeaks({
424
- displayDurationInFrames: message.displayDurationInFrames,
425
- displayOffsetInFrames: message.displayOffsetInFrames,
426
- durationInFrames: message.durationInFrames,
427
- fps: message.fps,
428
- loopDisplay: message.loopDisplay,
429
- peaks,
430
- playbackRate: message.playbackRate,
431
- startFrom: message.startFrom
432
- });
433
- drawBars({
434
- canvas,
435
- peaks: portionPeaks,
436
- color: "rgba(255, 255, 255, 0.6)",
437
- volume: message.volume,
438
- width: message.width
439
- });
440
- };
441
- var renderWaveform = async (message) => {
442
- if (!canvas) {
443
- postError(message.requestId, new Error("Waveform canvas not initialized"));
444
- return;
445
- }
204
+ self.addEventListener("message", (event) => {
205
+ const message = event.data;
446
206
  const controller = new AbortController;
447
- currentController?.abort();
448
- currentController = controller;
449
- latestRequestId = message.requestId;
450
- try {
451
- canvas.width = message.width;
452
- canvas.height = message.height;
453
- const peaks = await loadWaveformPeaks(message.src, controller.signal, {
454
- onProgress: ({ peaks: nextPeaks }) => {
455
- if (controller.signal.aborted || latestRequestId !== message.requestId) {
456
- return;
457
- }
458
- drawPartialWaveform(message, nextPeaks);
207
+ loadWaveformPeaks(message.src, controller.signal, {
208
+ onProgress: ({ peaks, final }) => {
209
+ if (!final) {
210
+ postPeaks(message.requestId, peaks, false);
459
211
  }
460
- });
461
- if (controller.signal.aborted || latestRequestId !== message.requestId) {
462
- return;
463
212
  }
464
- drawPartialWaveform(message, peaks);
465
- } catch (error) {
466
- if (controller.signal.aborted || latestRequestId !== message.requestId) {
467
- return;
468
- }
469
- postError(message.requestId, error);
470
- }
471
- };
472
- self.addEventListener("message", (event) => {
473
- const message = event.data;
474
- if (message.type === "init") {
475
- canvas = message.canvas;
476
- return;
477
- }
478
- if (message.type === "dispose") {
479
- currentController?.abort();
480
- currentController = null;
481
- canvas = null;
482
- return;
483
- }
484
- renderWaveform(message).catch((error) => {
213
+ }).then((peaks) => {
214
+ postPeaks(message.requestId, peaks, true);
215
+ }).catch((error) => {
485
216
  postError(message.requestId, error);
486
217
  });
487
218
  });
@@ -353,12 +353,6 @@ async function loadWaveformPeaks(url, signal, options) {
353
353
  input.dispose();
354
354
  }
355
355
  }
356
- // src/audio-waveform/make-audio-waveform-worker.ts
357
- var makeAudioWaveformWorker = () => {
358
- return new Worker(new URL("./audio-waveform-worker.mjs", import.meta.url), {
359
- type: "module"
360
- });
361
- };
362
356
  // src/audio-waveform/slice-waveform-peaks.ts
363
357
  var sliceWaveformPeaks = ({
364
358
  durationInFrames,
@@ -438,6 +432,148 @@ var sliceVisibleWaveformPeaks = ({
438
432
  }
439
433
  return result;
440
434
  };
435
+ // src/audio-waveform/make-audio-waveform-worker.ts
436
+ var makeAudioWaveformWorker = () => {
437
+ return new Worker(new URL("./audio-waveform-worker.mjs", import.meta.url), {
438
+ type: "module"
439
+ });
440
+ };
441
+
442
+ // src/audio-waveform/subscribe-to-waveform-peaks.ts
443
+ var peaksCache2 = new Map;
444
+ var inFlightBySrc = new Map;
445
+ var inFlightByRequestId = new Map;
446
+ var worker = null;
447
+ var workerFailed = false;
448
+ var nextRequestId = 0;
449
+ var emitPeaks = (load, peaks, final) => {
450
+ if (final) {
451
+ peaksCache2.set(load.src, peaks);
452
+ inFlightBySrc.delete(load.src);
453
+ if (load.requestId !== null) {
454
+ inFlightByRequestId.delete(load.requestId);
455
+ }
456
+ } else {
457
+ load.latestPeaks = peaks;
458
+ }
459
+ for (const listener of load.listeners) {
460
+ listener.onPeaks(peaks, final);
461
+ }
462
+ };
463
+ var emitError = (load, error) => {
464
+ inFlightBySrc.delete(load.src);
465
+ if (load.requestId !== null) {
466
+ inFlightByRequestId.delete(load.requestId);
467
+ }
468
+ for (const listener of load.listeners) {
469
+ listener.onError(error);
470
+ }
471
+ };
472
+ var startMainThreadLoad = (load) => {
473
+ load.requestId = null;
474
+ const controller = new AbortController;
475
+ loadWaveformPeaks(load.src, controller.signal, {
476
+ onProgress: ({ peaks, final }) => {
477
+ if (final) {
478
+ return;
479
+ }
480
+ emitPeaks(load, peaks.slice(), false);
481
+ }
482
+ }).then((peaks) => {
483
+ emitPeaks(load, peaks, true);
484
+ }).catch((error) => {
485
+ emitError(load, error instanceof Error ? error : new Error("Failed to load waveform peaks"));
486
+ });
487
+ };
488
+ var handleWorkerFailure = () => {
489
+ workerFailed = true;
490
+ worker?.terminate();
491
+ worker = null;
492
+ const pending = [...inFlightByRequestId.values()];
493
+ inFlightByRequestId.clear();
494
+ for (const load of pending) {
495
+ startMainThreadLoad(load);
496
+ }
497
+ };
498
+ var getOrCreateWorker = () => {
499
+ if (workerFailed || typeof Worker === "undefined") {
500
+ return null;
501
+ }
502
+ if (worker) {
503
+ return worker;
504
+ }
505
+ try {
506
+ worker = makeAudioWaveformWorker();
507
+ } catch {
508
+ workerFailed = true;
509
+ return null;
510
+ }
511
+ worker.addEventListener("message", (event) => {
512
+ const message = event.data;
513
+ const load = inFlightByRequestId.get(message.requestId);
514
+ if (!load) {
515
+ return;
516
+ }
517
+ if (message.type === "error") {
518
+ emitError(load, new Error(message.message));
519
+ return;
520
+ }
521
+ emitPeaks(load, message.peaks, message.final);
522
+ });
523
+ worker.addEventListener("error", (event) => {
524
+ event.preventDefault();
525
+ handleWorkerFailure();
526
+ });
527
+ return worker;
528
+ };
529
+ var subscribeToWaveformPeaks = ({
530
+ src,
531
+ onPeaks,
532
+ onError
533
+ }) => {
534
+ const cached = peaksCache2.get(src);
535
+ if (cached) {
536
+ onPeaks(cached, true);
537
+ return () => {
538
+ return;
539
+ };
540
+ }
541
+ const listener = { onPeaks, onError };
542
+ const existing = inFlightBySrc.get(src);
543
+ if (existing) {
544
+ existing.listeners.add(listener);
545
+ if (existing.latestPeaks) {
546
+ onPeaks(existing.latestPeaks, false);
547
+ }
548
+ return () => {
549
+ existing.listeners.delete(listener);
550
+ };
551
+ }
552
+ const load = {
553
+ src,
554
+ listeners: new Set([listener]),
555
+ latestPeaks: null,
556
+ requestId: null
557
+ };
558
+ inFlightBySrc.set(src, load);
559
+ const workerInstance = getOrCreateWorker();
560
+ if (workerInstance) {
561
+ const requestId = nextRequestId++;
562
+ load.requestId = requestId;
563
+ inFlightByRequestId.set(requestId, load);
564
+ const message = {
565
+ type: "load",
566
+ requestId,
567
+ src
568
+ };
569
+ workerInstance.postMessage(message);
570
+ } else {
571
+ startMainThreadLoad(load);
572
+ }
573
+ return () => {
574
+ load.listeners.delete(listener);
575
+ };
576
+ };
441
577
  // src/clamp-timestamps-to-duration.ts
442
578
  var LAST_FRAME_EPSILON_IN_SECONDS = 1 / 1e6;
443
579
  var clampTimestampsToDuration = ({
@@ -458,6 +594,7 @@ import {
458
594
  UrlSource as UrlSource2,
459
595
  VideoSampleSink
460
596
  } from "mediabunny";
597
+ import { Internals } from "remotion";
461
598
 
462
599
  // src/get-duration-or-compute.ts
463
600
  var getDurationOrCompute = async (input) => {
@@ -474,22 +611,34 @@ async function extractFrames({
474
611
  signal,
475
612
  repeatLastFrame = false
476
613
  }) {
477
- const input = new Input2({
478
- formats: ALL_FORMATS2,
479
- source: new UrlSource2(src)
614
+ const lease = Internals.globalMediaResourceManager.acquire({
615
+ key: Internals.getMediabunnyInputResourceKey({
616
+ src,
617
+ credentials: null,
618
+ requestInitFingerprint: null,
619
+ revision: null
620
+ }),
621
+ create: () => {
622
+ const createdInput = new Input2({
623
+ formats: ALL_FORMATS2,
624
+ source: new UrlSource2(src)
625
+ });
626
+ return {
627
+ resource: createdInput,
628
+ dispose: () => createdInput.dispose()
629
+ };
630
+ }
480
631
  });
481
- const dispose = () => {
482
- input.dispose();
483
- };
484
- if (signal) {
485
- signal.addEventListener("abort", dispose, { once: true });
486
- }
632
+ const input = lease.resource;
487
633
  try {
488
634
  const [durationInSeconds, format, videoTrack] = await Promise.all([
489
- getDurationOrCompute(input),
635
+ lease.getOrCreateValue(Internals.MEDIABUNNY_DURATION_VALUE_KEY, () => getDurationOrCompute(input)),
490
636
  input.getFormat(),
491
637
  input.getPrimaryVideoTrack()
492
638
  ]);
639
+ if (signal?.aborted) {
640
+ return;
641
+ }
493
642
  if (!videoTrack) {
494
643
  throw new Error("No video track found in the input");
495
644
  }
@@ -538,10 +687,7 @@ async function extractFrames({
538
687
  }
539
688
  throw error;
540
689
  } finally {
541
- dispose();
542
- if (signal) {
543
- signal.removeEventListener("abort", dispose);
544
- }
690
+ lease.release();
545
691
  }
546
692
  }
547
693
  // src/frame-database.ts
@@ -1006,13 +1152,13 @@ var renderFrameStripToCanvas = async ({
1006
1152
  });
1007
1153
  };
1008
1154
  export {
1155
+ subscribeToWaveformPeaks,
1009
1156
  sliceWaveformPeaks,
1010
1157
  sliceVisibleWaveformPeaks,
1011
1158
  shouldTileLoopDisplay,
1012
1159
  resizeVideoFrame,
1013
1160
  renderFrameStripToCanvas,
1014
1161
  makeFrameDatabaseKey,
1015
- makeAudioWaveformWorker,
1016
1162
  loadWaveformPeaks,
1017
1163
  getVisibleWaveformVolume,
1018
1164
  getTimestampFromFrameDatabaseKey,
@@ -2,26 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.extractFrames = extractFrames;
4
4
  const mediabunny_1 = require("mediabunny");
5
+ const remotion_1 = require("remotion");
5
6
  const clamp_timestamps_to_duration_1 = require("./clamp-timestamps-to-duration");
6
7
  const get_duration_or_compute_1 = require("./get-duration-or-compute");
7
8
  async function extractFrames({ src, timestampsInSeconds, onVideoSample, signal, repeatLastFrame = false, }) {
8
9
  var _a;
9
- const input = new mediabunny_1.Input({
10
- formats: mediabunny_1.ALL_FORMATS,
11
- source: new mediabunny_1.UrlSource(src),
10
+ const lease = remotion_1.Internals.globalMediaResourceManager.acquire({
11
+ key: remotion_1.Internals.getMediabunnyInputResourceKey({
12
+ src,
13
+ credentials: null,
14
+ requestInitFingerprint: null,
15
+ revision: null,
16
+ }),
17
+ create: () => {
18
+ const createdInput = new mediabunny_1.Input({
19
+ formats: mediabunny_1.ALL_FORMATS,
20
+ source: new mediabunny_1.UrlSource(src),
21
+ });
22
+ return {
23
+ resource: createdInput,
24
+ dispose: () => createdInput.dispose(),
25
+ };
26
+ },
12
27
  });
13
- const dispose = () => {
14
- input.dispose();
15
- };
16
- if (signal) {
17
- signal.addEventListener('abort', dispose, { once: true });
18
- }
28
+ const input = lease.resource;
19
29
  try {
20
30
  const [durationInSeconds, format, videoTrack] = await Promise.all([
21
- (0, get_duration_or_compute_1.getDurationOrCompute)(input),
31
+ lease.getOrCreateValue(remotion_1.Internals.MEDIABUNNY_DURATION_VALUE_KEY, () => (0, get_duration_or_compute_1.getDurationOrCompute)(input)),
22
32
  input.getFormat(),
23
33
  input.getPrimaryVideoTrack(),
24
34
  ]);
35
+ if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
36
+ return;
37
+ }
25
38
  if (!videoTrack) {
26
39
  throw new Error('No video track found in the input');
27
40
  }
@@ -84,9 +97,6 @@ async function extractFrames({ src, timestampsInSeconds, onVideoSample, signal,
84
97
  throw error;
85
98
  }
86
99
  finally {
87
- dispose();
88
- if (signal) {
89
- signal.removeEventListener('abort', dispose);
90
- }
100
+ lease.release();
91
101
  }
92
102
  }
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- export type { AudioWaveformWorkerIncomingMessage, AudioWaveformWorkerOutgoingMessage, AudioWaveformWorkerRenderMessage, } from './audio-waveform/audio-waveform-worker-types';
1
+ export type { AudioWaveformWorkerIncomingMessage, AudioWaveformWorkerOutgoingMessage, } from './audio-waveform/audio-waveform-worker-types';
2
2
  export { TARGET_SAMPLE_RATE } from './audio-waveform/constants';
3
3
  export { drawBars, type WaveformVolume } from './audio-waveform/draw-peaks';
4
4
  export { getVisibleWaveformVolume } from './audio-waveform/get-visible-waveform-volume';
5
5
  export { loadWaveformPeaks } from './audio-waveform/load-waveform-peaks';
6
- export { makeAudioWaveformWorker } from './audio-waveform/make-audio-waveform-worker';
7
6
  export { sliceVisibleWaveformPeaks } from './audio-waveform/slice-visible-waveform-peaks';
7
+ export { subscribeToWaveformPeaks } from './audio-waveform/subscribe-to-waveform-peaks';
8
8
  export { sliceWaveformPeaks } from './audio-waveform/slice-waveform-peaks';
9
9
  export { createWaveformPeakProcessor, emitWaveformProgress, } from './audio-waveform/waveform-peak-processor';
10
10
  export { clampTimestampsToDuration } from './clamp-timestamps-to-duration';
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.getScaledImageThumbnailDimensions = exports.drawRepeatingImageThumbnail = 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.sliceVisibleWaveformPeaks = exports.makeAudioWaveformWorker = exports.loadWaveformPeaks = exports.getVisibleWaveformVolume = exports.drawBars = exports.TARGET_SAMPLE_RATE = void 0;
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.getScaledImageThumbnailDimensions = exports.drawRepeatingImageThumbnail = 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");
@@ -9,10 +9,10 @@ const get_visible_waveform_volume_1 = require("./audio-waveform/get-visible-wave
9
9
  Object.defineProperty(exports, "getVisibleWaveformVolume", { enumerable: true, get: function () { return get_visible_waveform_volume_1.getVisibleWaveformVolume; } });
10
10
  const load_waveform_peaks_1 = require("./audio-waveform/load-waveform-peaks");
11
11
  Object.defineProperty(exports, "loadWaveformPeaks", { enumerable: true, get: function () { return load_waveform_peaks_1.loadWaveformPeaks; } });
12
- const make_audio_waveform_worker_1 = require("./audio-waveform/make-audio-waveform-worker");
13
- Object.defineProperty(exports, "makeAudioWaveformWorker", { enumerable: true, get: function () { return make_audio_waveform_worker_1.makeAudioWaveformWorker; } });
14
12
  const slice_visible_waveform_peaks_1 = require("./audio-waveform/slice-visible-waveform-peaks");
15
13
  Object.defineProperty(exports, "sliceVisibleWaveformPeaks", { enumerable: true, get: function () { return slice_visible_waveform_peaks_1.sliceVisibleWaveformPeaks; } });
14
+ const subscribe_to_waveform_peaks_1 = require("./audio-waveform/subscribe-to-waveform-peaks");
15
+ Object.defineProperty(exports, "subscribeToWaveformPeaks", { enumerable: true, get: function () { return subscribe_to_waveform_peaks_1.subscribeToWaveformPeaks; } });
16
16
  const slice_waveform_peaks_1 = require("./audio-waveform/slice-waveform-peaks");
17
17
  Object.defineProperty(exports, "sliceWaveformPeaks", { enumerable: true, get: function () { return slice_waveform_peaks_1.sliceWaveformPeaks; } });
18
18
  const waveform_peak_processor_1 = require("./audio-waveform/waveform-peak-processor");
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.514",
6
+ "version": "4.0.515",
7
7
  "description": "Internal utilities for rendering Remotion timelines",
8
8
  "main": "dist/index.js",
9
9
  "scripts": {
@@ -21,11 +21,12 @@
21
21
  "url": "https://github.com/remotion-dev/remotion/issues"
22
22
  },
23
23
  "dependencies": {
24
- "mediabunny": "1.55.1"
24
+ "mediabunny": "1.55.1",
25
+ "remotion": "4.0.515"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@mediabunny/server": "1.55.1",
28
- "@remotion/eslint-config-internal": "4.0.514",
29
+ "@remotion/eslint-config-internal": "4.0.515",
29
30
  "eslint": "9.19.0",
30
31
  "@typescript/native-preview": "7.0.0-dev.20260217.1"
31
32
  },