@remotion/timeline-utils 4.0.513 → 4.0.514

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.
@@ -13,6 +13,8 @@ export type AudioWaveformWorkerRenderMessage = {
13
13
  readonly volume: WaveformVolume;
14
14
  readonly startFrom: number;
15
15
  readonly durationInFrames: number;
16
+ readonly displayOffsetInFrames: number;
17
+ readonly displayDurationInFrames: number;
16
18
  readonly fps: number;
17
19
  readonly playbackRate: number;
18
20
  readonly loopDisplay: TimelineLoopDisplay | undefined;
@@ -0,0 +1,8 @@
1
+ import { type TimelineLoopDisplay } from '../loop-display';
2
+ import type { WaveformVolume } from './draw-peaks';
3
+ export declare const getVisibleWaveformVolume: ({ displayDurationInFrames, displayOffsetInFrames, loopDisplay, volume, }: {
4
+ readonly displayDurationInFrames: number;
5
+ readonly displayOffsetInFrames: number;
6
+ readonly loopDisplay: TimelineLoopDisplay | undefined;
7
+ readonly volume: WaveformVolume;
8
+ }) => WaveformVolume;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getVisibleWaveformVolume = void 0;
4
+ const loop_display_1 = require("../loop-display");
5
+ const getVisibleWaveformVolume = ({ displayDurationInFrames, displayOffsetInFrames, loopDisplay, volume, }) => {
6
+ if (!Array.isArray(volume)) {
7
+ return volume;
8
+ }
9
+ if (!Number.isFinite(displayDurationInFrames) ||
10
+ displayDurationInFrames <= 0) {
11
+ return [];
12
+ }
13
+ if (!(0, loop_display_1.shouldTileLoopDisplay)(loopDisplay) ||
14
+ loopDisplay.durationInFrames <= 0) {
15
+ const start = Math.max(0, Math.floor(displayOffsetInFrames));
16
+ const end = Math.min(volume.length, Math.ceil(displayOffsetInFrames + displayDurationInFrames));
17
+ return volume.slice(start, end);
18
+ }
19
+ const segments = (0, loop_display_1.getLoopDisplaySegments)({
20
+ displayDurationInFrames,
21
+ displayOffsetInFrames,
22
+ loopDurationInFrames: loopDisplay.durationInFrames,
23
+ });
24
+ const result = [];
25
+ for (const segment of segments) {
26
+ const start = Math.max(0, Math.floor(segment.loopOffsetInFrames));
27
+ const end = Math.min(volume.length, Math.ceil(segment.loopOffsetInFrames + segment.durationInFrames));
28
+ // Do not `push(...slice)`: Chrome throws RangeError: Invalid array length
29
+ // once a loop segment is longer than ~65k frames.
30
+ for (let index = start; index < end; index++) {
31
+ result.push(volume[index]);
32
+ }
33
+ }
34
+ return result;
35
+ };
36
+ exports.getVisibleWaveformVolume = getVisibleWaveformVolume;
@@ -0,0 +1,11 @@
1
+ import type { TimelineLoopDisplay } from '../loop-display';
2
+ export declare const sliceVisibleWaveformPeaks: ({ displayDurationInFrames, displayOffsetInFrames, durationInFrames, fps, loopDisplay, peaks, playbackRate, startFrom, }: {
3
+ readonly displayDurationInFrames: number;
4
+ readonly displayOffsetInFrames: number;
5
+ readonly durationInFrames: number;
6
+ readonly fps: number;
7
+ readonly loopDisplay: TimelineLoopDisplay | undefined;
8
+ readonly peaks: Float32Array<ArrayBufferLike>;
9
+ readonly playbackRate: number;
10
+ readonly startFrom: number;
11
+ }) => Float32Array<ArrayBufferLike>;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sliceVisibleWaveformPeaks = void 0;
4
+ const loop_display_1 = require("../loop-display");
5
+ const slice_waveform_peaks_1 = require("./slice-waveform-peaks");
6
+ const sliceVisibleWaveformPeaks = ({ displayDurationInFrames, displayOffsetInFrames, durationInFrames, fps, loopDisplay, peaks, playbackRate, startFrom, }) => {
7
+ if (!(0, loop_display_1.shouldTileLoopDisplay)(loopDisplay) ||
8
+ loopDisplay.durationInFrames <= 0) {
9
+ return (0, slice_waveform_peaks_1.sliceWaveformPeaks)({
10
+ durationInFrames: Math.min(displayDurationInFrames, Math.max(0, durationInFrames - displayOffsetInFrames)),
11
+ fps,
12
+ peaks,
13
+ playbackRate,
14
+ startFrom: startFrom + displayOffsetInFrames * playbackRate,
15
+ });
16
+ }
17
+ const segments = (0, loop_display_1.getLoopDisplaySegments)({
18
+ displayDurationInFrames,
19
+ displayOffsetInFrames,
20
+ loopDurationInFrames: loopDisplay.durationInFrames,
21
+ });
22
+ const parts = [];
23
+ let totalLength = 0;
24
+ for (const segment of segments) {
25
+ const part = (0, slice_waveform_peaks_1.sliceWaveformPeaks)({
26
+ durationInFrames: segment.durationInFrames,
27
+ fps,
28
+ peaks,
29
+ playbackRate,
30
+ startFrom: startFrom + segment.loopOffsetInFrames * playbackRate,
31
+ });
32
+ parts.push(part);
33
+ totalLength += part.length;
34
+ }
35
+ if (parts.length === 1) {
36
+ return parts[0];
37
+ }
38
+ const result = new Float32Array(totalLength);
39
+ let writeOffset = 0;
40
+ for (const part of parts) {
41
+ result.set(part, writeOffset);
42
+ writeOffset += part.length;
43
+ }
44
+ return result;
45
+ };
46
+ exports.sliceVisibleWaveformPeaks = sliceVisibleWaveformPeaks;
@@ -3,8 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const draw_peaks_1 = require("./audio-waveform/draw-peaks");
5
5
  const load_waveform_peaks_1 = require("./audio-waveform/load-waveform-peaks");
6
- const slice_waveform_peaks_1 = require("./audio-waveform/slice-waveform-peaks");
7
- const loop_display_1 = require("./loop-display");
6
+ const slice_visible_waveform_peaks_1 = require("./audio-waveform/slice-visible-waveform-peaks");
8
7
  let canvas = null;
9
8
  let currentController = null;
10
9
  let latestRequestId = 0;
@@ -21,49 +20,23 @@ const drawPartialWaveform = (message, peaks) => {
21
20
  if (!canvas) {
22
21
  return;
23
22
  }
24
- const portionPeaks = (0, slice_waveform_peaks_1.sliceWaveformPeaks)({
25
- durationInFrames: (0, loop_display_1.shouldTileLoopDisplay)(message.loopDisplay)
26
- ? message.loopDisplay.durationInFrames
27
- : message.durationInFrames,
23
+ const portionPeaks = (0, slice_visible_waveform_peaks_1.sliceVisibleWaveformPeaks)({
24
+ displayDurationInFrames: message.displayDurationInFrames,
25
+ displayOffsetInFrames: message.displayOffsetInFrames,
26
+ durationInFrames: message.durationInFrames,
28
27
  fps: message.fps,
28
+ loopDisplay: message.loopDisplay,
29
29
  peaks,
30
30
  playbackRate: message.playbackRate,
31
31
  startFrom: message.startFrom,
32
32
  });
33
- if (!(0, loop_display_1.shouldTileLoopDisplay)(message.loopDisplay)) {
34
- (0, draw_peaks_1.drawBars)({
35
- canvas,
36
- peaks: portionPeaks,
37
- color: 'rgba(255, 255, 255, 0.6)',
38
- volume: message.volume,
39
- width: message.width,
40
- });
41
- return;
42
- }
43
- const loopWidth = (0, loop_display_1.getLoopDisplayWidth)({
44
- visualizationWidth: message.width,
45
- loopDisplay: message.loopDisplay,
46
- });
47
- const targetCanvas = new OffscreenCanvas(Math.max(1, Math.ceil(loopWidth)), message.height);
48
33
  (0, draw_peaks_1.drawBars)({
49
- canvas: targetCanvas,
34
+ canvas,
50
35
  peaks: portionPeaks,
51
36
  color: 'rgba(255, 255, 255, 0.6)',
52
37
  volume: message.volume,
53
- width: targetCanvas.width,
38
+ width: message.width,
54
39
  });
55
- const ctx = canvas.getContext('2d');
56
- if (!ctx) {
57
- throw new Error('Failed to get canvas context');
58
- }
59
- const pattern = ctx.createPattern(targetCanvas, 'repeat-x');
60
- if (!pattern) {
61
- return;
62
- }
63
- pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
64
- ctx.clearRect(0, 0, message.width, message.height);
65
- ctx.fillStyle = pattern;
66
- ctx.fillRect(0, 0, message.width, message.height);
67
40
  };
68
41
  const renderWaveform = async (message) => {
69
42
  if (!canvas) {
@@ -280,6 +280,49 @@ async function loadWaveformPeaks(url, signal, options) {
280
280
  }
281
281
  }
282
282
 
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
+
283
326
  // src/audio-waveform/slice-waveform-peaks.ts
284
327
  var sliceWaveformPeaks = ({
285
328
  durationInFrames,
@@ -310,18 +353,54 @@ var sliceWaveformPeaks = ({
310
353
  return portion;
311
354
  };
312
355
 
313
- // src/loop-display.ts
314
- var shouldTileLoopDisplay = (loopDisplay) => {
315
- return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
316
- };
317
- var getLoopDisplayWidth = ({
318
- visualizationWidth,
319
- loopDisplay
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
320
366
  }) => {
321
- if (!shouldTileLoopDisplay(loopDisplay)) {
322
- return visualizationWidth;
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
+ });
323
375
  }
324
- return visualizationWidth / loopDisplay.numberOfTimes;
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;
325
404
  };
326
405
 
327
406
  // src/audio-waveform-worker.ts
@@ -341,47 +420,23 @@ var drawPartialWaveform = (message, peaks) => {
341
420
  if (!canvas) {
342
421
  return;
343
422
  }
344
- const portionPeaks = sliceWaveformPeaks({
345
- durationInFrames: shouldTileLoopDisplay(message.loopDisplay) ? message.loopDisplay.durationInFrames : message.durationInFrames,
423
+ const portionPeaks = sliceVisibleWaveformPeaks({
424
+ displayDurationInFrames: message.displayDurationInFrames,
425
+ displayOffsetInFrames: message.displayOffsetInFrames,
426
+ durationInFrames: message.durationInFrames,
346
427
  fps: message.fps,
428
+ loopDisplay: message.loopDisplay,
347
429
  peaks,
348
430
  playbackRate: message.playbackRate,
349
431
  startFrom: message.startFrom
350
432
  });
351
- if (!shouldTileLoopDisplay(message.loopDisplay)) {
352
- drawBars({
353
- canvas,
354
- peaks: portionPeaks,
355
- color: "rgba(255, 255, 255, 0.6)",
356
- volume: message.volume,
357
- width: message.width
358
- });
359
- return;
360
- }
361
- const loopWidth = getLoopDisplayWidth({
362
- visualizationWidth: message.width,
363
- loopDisplay: message.loopDisplay
364
- });
365
- const targetCanvas = new OffscreenCanvas(Math.max(1, Math.ceil(loopWidth)), message.height);
366
433
  drawBars({
367
- canvas: targetCanvas,
434
+ canvas,
368
435
  peaks: portionPeaks,
369
436
  color: "rgba(255, 255, 255, 0.6)",
370
437
  volume: message.volume,
371
- width: targetCanvas.width
438
+ width: message.width
372
439
  });
373
- const ctx = canvas.getContext("2d");
374
- if (!ctx) {
375
- throw new Error("Failed to get canvas context");
376
- }
377
- const pattern = ctx.createPattern(targetCanvas, "repeat-x");
378
- if (!pattern) {
379
- return;
380
- }
381
- pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
382
- ctx.clearRect(0, 0, message.width, message.height);
383
- ctx.fillStyle = pattern;
384
- ctx.fillRect(0, 0, message.width, message.height);
385
440
  };
386
441
  var renderWaveform = async (message) => {
387
442
  if (!canvas) {
@@ -97,6 +97,82 @@ var drawBars = ({
97
97
  }
98
98
  ctx.putImageData(imageData, 0, 0);
99
99
  };
100
+ // src/loop-display.ts
101
+ var shouldTileLoopDisplay = (loopDisplay) => {
102
+ return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
103
+ };
104
+ var getLoopDisplaySegments = ({
105
+ displayDurationInFrames,
106
+ displayOffsetInFrames,
107
+ loopDurationInFrames
108
+ }) => {
109
+ if (!Number.isFinite(displayDurationInFrames) || displayDurationInFrames <= 0 || !Number.isFinite(displayOffsetInFrames) || !Number.isFinite(loopDurationInFrames) || loopDurationInFrames <= 0) {
110
+ return [];
111
+ }
112
+ const displayEndInFrames = displayOffsetInFrames + displayDurationInFrames;
113
+ const firstLoopIndex = Math.floor(displayOffsetInFrames / loopDurationInFrames);
114
+ const lastLoopIndex = Math.floor(displayEndInFrames / loopDurationInFrames);
115
+ const segments = [];
116
+ for (let loopIndex = firstLoopIndex;loopIndex <= lastLoopIndex; loopIndex++) {
117
+ const loopStartInFrames = loopIndex * loopDurationInFrames;
118
+ const segmentStart = Math.max(displayOffsetInFrames, loopStartInFrames);
119
+ const segmentEnd = Math.min(displayEndInFrames, loopStartInFrames + loopDurationInFrames);
120
+ const durationInFrames = segmentEnd - segmentStart;
121
+ if (durationInFrames <= 0) {
122
+ continue;
123
+ }
124
+ segments.push({
125
+ loopIndex,
126
+ absoluteOffsetInFrames: segmentStart,
127
+ loopOffsetInFrames: segmentStart - loopStartInFrames,
128
+ durationInFrames
129
+ });
130
+ }
131
+ return segments;
132
+ };
133
+ var getLoopDisplayWidth = ({
134
+ visualizationWidth,
135
+ loopDisplay
136
+ }) => {
137
+ if (!shouldTileLoopDisplay(loopDisplay)) {
138
+ return visualizationWidth;
139
+ }
140
+ return visualizationWidth / loopDisplay.numberOfTimes;
141
+ };
142
+
143
+ // src/audio-waveform/get-visible-waveform-volume.ts
144
+ var getVisibleWaveformVolume = ({
145
+ displayDurationInFrames,
146
+ displayOffsetInFrames,
147
+ loopDisplay,
148
+ volume
149
+ }) => {
150
+ if (!Array.isArray(volume)) {
151
+ return volume;
152
+ }
153
+ if (!Number.isFinite(displayDurationInFrames) || displayDurationInFrames <= 0) {
154
+ return [];
155
+ }
156
+ if (!shouldTileLoopDisplay(loopDisplay) || loopDisplay.durationInFrames <= 0) {
157
+ const start = Math.max(0, Math.floor(displayOffsetInFrames));
158
+ const end = Math.min(volume.length, Math.ceil(displayOffsetInFrames + displayDurationInFrames));
159
+ return volume.slice(start, end);
160
+ }
161
+ const segments = getLoopDisplaySegments({
162
+ displayDurationInFrames,
163
+ displayOffsetInFrames,
164
+ loopDurationInFrames: loopDisplay.durationInFrames
165
+ });
166
+ const result = [];
167
+ for (const segment of segments) {
168
+ const start = Math.max(0, Math.floor(segment.loopOffsetInFrames));
169
+ const end = Math.min(volume.length, Math.ceil(segment.loopOffsetInFrames + segment.durationInFrames));
170
+ for (let index = start;index < end; index++) {
171
+ result.push(volume[index]);
172
+ }
173
+ }
174
+ return result;
175
+ };
100
176
  // src/audio-waveform/load-waveform-peaks.ts
101
177
  import { ALL_FORMATS, AudioSampleSink, Input, UrlSource } from "mediabunny";
102
178
 
@@ -312,70 +388,55 @@ var sliceWaveformPeaks = ({
312
388
  }
313
389
  return portion;
314
390
  };
315
- // src/image-thumbnail/get-scaled-image-thumbnail-dimensions.ts
316
- var getScaledImageThumbnailDimensions = ({
317
- naturalWidth,
318
- naturalHeight,
319
- canvasHeight
320
- }) => {
321
- if (naturalWidth === 0 || naturalHeight === 0 || canvasHeight === 0) {
322
- return {
323
- scaledWidth: 0,
324
- scaledHeight: 0
325
- };
326
- }
327
- const scale = canvasHeight / naturalHeight;
328
- return {
329
- scaledWidth: naturalWidth * scale,
330
- scaledHeight: canvasHeight
331
- };
332
- };
333
391
 
334
- // src/image-thumbnail/draw-repeating-image-thumbnail.ts
335
- var createOffscreenCanvas = (width, height) => {
336
- if (typeof document !== "undefined") {
337
- const canvas = document.createElement("canvas");
338
- canvas.width = width;
339
- canvas.height = height;
340
- return canvas;
341
- }
342
- if (typeof OffscreenCanvas !== "undefined") {
343
- return new OffscreenCanvas(width, height);
344
- }
345
- throw new Error("No canvas implementation available");
346
- };
347
- var drawRepeatingImageThumbnail = ({
348
- canvas,
349
- image
392
+ // src/audio-waveform/slice-visible-waveform-peaks.ts
393
+ var sliceVisibleWaveformPeaks = ({
394
+ displayDurationInFrames,
395
+ displayOffsetInFrames,
396
+ durationInFrames,
397
+ fps,
398
+ loopDisplay,
399
+ peaks,
400
+ playbackRate,
401
+ startFrom
350
402
  }) => {
351
- const ctx = canvas.getContext("2d");
352
- if (!ctx) {
353
- throw new Error("Failed to get canvas context");
354
- }
355
- const { width, height } = canvas;
356
- if (width === 0 || height === 0) {
357
- return;
403
+ if (!shouldTileLoopDisplay(loopDisplay) || loopDisplay.durationInFrames <= 0) {
404
+ return sliceWaveformPeaks({
405
+ durationInFrames: Math.min(displayDurationInFrames, Math.max(0, durationInFrames - displayOffsetInFrames)),
406
+ fps,
407
+ peaks,
408
+ playbackRate,
409
+ startFrom: startFrom + displayOffsetInFrames * playbackRate
410
+ });
358
411
  }
359
- const { scaledWidth, scaledHeight } = getScaledImageThumbnailDimensions({
360
- naturalWidth: image.naturalWidth,
361
- naturalHeight: image.naturalHeight,
362
- canvasHeight: height
412
+ const segments = getLoopDisplaySegments({
413
+ displayDurationInFrames,
414
+ displayOffsetInFrames,
415
+ loopDurationInFrames: loopDisplay.durationInFrames
363
416
  });
364
- if (scaledWidth === 0 || scaledHeight === 0) {
365
- return;
417
+ const parts = [];
418
+ let totalLength = 0;
419
+ for (const segment of segments) {
420
+ const part = sliceWaveformPeaks({
421
+ durationInFrames: segment.durationInFrames,
422
+ fps,
423
+ peaks,
424
+ playbackRate,
425
+ startFrom: startFrom + segment.loopOffsetInFrames * playbackRate
426
+ });
427
+ parts.push(part);
428
+ totalLength += part.length;
366
429
  }
367
- const offscreen = createOffscreenCanvas(scaledWidth, scaledHeight);
368
- const offCtx = offscreen.getContext("2d");
369
- if (!offCtx) {
370
- throw new Error("Failed to get offscreen canvas context");
430
+ if (parts.length === 1) {
431
+ return parts[0];
371
432
  }
372
- offCtx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
373
- const pattern = ctx.createPattern(offscreen, "repeat-x");
374
- if (!pattern) {
375
- return;
433
+ const result = new Float32Array(totalLength);
434
+ let writeOffset = 0;
435
+ for (const part of parts) {
436
+ result.set(part, writeOffset);
437
+ writeOffset += part.length;
376
438
  }
377
- ctx.fillStyle = pattern;
378
- ctx.fillRect(0, 0, width, height);
439
+ return result;
379
440
  };
380
441
  // src/clamp-timestamps-to-duration.ts
381
442
  var LAST_FRAME_EPSILON_IN_SECONDS = 1 / 1e6;
@@ -540,18 +601,72 @@ var addFrameToCache = (key, frame) => {
540
601
  });
541
602
  evictLRU();
542
603
  };
543
- // src/loop-display.ts
544
- var shouldTileLoopDisplay = (loopDisplay) => {
545
- return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
604
+ // src/image-thumbnail/get-scaled-image-thumbnail-dimensions.ts
605
+ var getScaledImageThumbnailDimensions = ({
606
+ naturalWidth,
607
+ naturalHeight,
608
+ canvasHeight
609
+ }) => {
610
+ if (naturalWidth === 0 || naturalHeight === 0 || canvasHeight === 0) {
611
+ return {
612
+ scaledWidth: 0,
613
+ scaledHeight: 0
614
+ };
615
+ }
616
+ const scale = canvasHeight / naturalHeight;
617
+ return {
618
+ scaledWidth: naturalWidth * scale,
619
+ scaledHeight: canvasHeight
620
+ };
546
621
  };
547
- var getLoopDisplayWidth = ({
548
- visualizationWidth,
549
- loopDisplay
622
+
623
+ // src/image-thumbnail/draw-repeating-image-thumbnail.ts
624
+ var createOffscreenCanvas = (width, height) => {
625
+ if (typeof document !== "undefined") {
626
+ const canvas = document.createElement("canvas");
627
+ canvas.width = width;
628
+ canvas.height = height;
629
+ return canvas;
630
+ }
631
+ if (typeof OffscreenCanvas !== "undefined") {
632
+ return new OffscreenCanvas(width, height);
633
+ }
634
+ throw new Error("No canvas implementation available");
635
+ };
636
+ var drawRepeatingImageThumbnail = ({
637
+ canvas,
638
+ image,
639
+ offsetInPixels = 0
550
640
  }) => {
551
- if (!shouldTileLoopDisplay(loopDisplay)) {
552
- return visualizationWidth;
641
+ const ctx = canvas.getContext("2d");
642
+ if (!ctx) {
643
+ throw new Error("Failed to get canvas context");
553
644
  }
554
- return visualizationWidth / loopDisplay.numberOfTimes;
645
+ const { width, height } = canvas;
646
+ if (width === 0 || height === 0) {
647
+ return;
648
+ }
649
+ const { scaledWidth, scaledHeight } = getScaledImageThumbnailDimensions({
650
+ naturalWidth: image.naturalWidth,
651
+ naturalHeight: image.naturalHeight,
652
+ canvasHeight: height
653
+ });
654
+ if (scaledWidth === 0 || scaledHeight === 0) {
655
+ return;
656
+ }
657
+ const offscreen = createOffscreenCanvas(scaledWidth, scaledHeight);
658
+ const offCtx = offscreen.getContext("2d");
659
+ if (!offCtx) {
660
+ throw new Error("Failed to get offscreen canvas context");
661
+ }
662
+ offCtx.drawImage(image, 0, 0, scaledWidth, scaledHeight);
663
+ const pattern = ctx.createPattern(offscreen, "repeat-x");
664
+ if (!pattern) {
665
+ return;
666
+ }
667
+ pattern.setTransform(new DOMMatrix().translate(-offsetInPixels, 0));
668
+ ctx.fillStyle = pattern;
669
+ ctx.fillRect(0, 0, width, height);
555
670
  };
556
671
  // src/render-frame-strip.ts
557
672
  var WEBCODECS_TIMESCALE = 1e6;
@@ -892,15 +1007,18 @@ var renderFrameStripToCanvas = async ({
892
1007
  };
893
1008
  export {
894
1009
  sliceWaveformPeaks,
1010
+ sliceVisibleWaveformPeaks,
895
1011
  shouldTileLoopDisplay,
896
1012
  resizeVideoFrame,
897
1013
  renderFrameStripToCanvas,
898
1014
  makeFrameDatabaseKey,
899
1015
  makeAudioWaveformWorker,
900
1016
  loadWaveformPeaks,
1017
+ getVisibleWaveformVolume,
901
1018
  getTimestampFromFrameDatabaseKey,
902
1019
  getScaledImageThumbnailDimensions,
903
1020
  getLoopDisplayWidth,
1021
+ getLoopDisplaySegments,
904
1022
  getFrameDatabaseKeyPrefix,
905
1023
  getDurationOfOneFrame,
906
1024
  getAspectRatioFromCache,
@@ -2,8 +2,9 @@ type ImageWithNaturalDimensions = CanvasImageSource & {
2
2
  readonly naturalWidth: number;
3
3
  readonly naturalHeight: number;
4
4
  };
5
- export declare const drawRepeatingImageThumbnail: ({ canvas, image, }: {
5
+ export declare const drawRepeatingImageThumbnail: ({ canvas, image, offsetInPixels, }: {
6
6
  readonly canvas: HTMLCanvasElement | OffscreenCanvas;
7
7
  readonly image: ImageWithNaturalDimensions;
8
+ readonly offsetInPixels?: number | undefined;
8
9
  }) => void;
9
10
  export {};
@@ -14,7 +14,7 @@ const createOffscreenCanvas = (width, height) => {
14
14
  }
15
15
  throw new Error('No canvas implementation available');
16
16
  };
17
- const drawRepeatingImageThumbnail = ({ canvas, image, }) => {
17
+ const drawRepeatingImageThumbnail = ({ canvas, image, offsetInPixels = 0, }) => {
18
18
  const ctx = canvas.getContext('2d');
19
19
  if (!ctx) {
20
20
  throw new Error('Failed to get canvas context');
@@ -41,6 +41,7 @@ const drawRepeatingImageThumbnail = ({ canvas, image, }) => {
41
41
  if (!pattern) {
42
42
  return;
43
43
  }
44
+ pattern.setTransform(new DOMMatrix().translate(-offsetInPixels, 0));
44
45
  ctx.fillStyle = pattern;
45
46
  ctx.fillRect(0, 0, width, height);
46
47
  };
package/dist/index.d.ts CHANGED
@@ -1,20 +1,22 @@
1
1
  export type { AudioWaveformWorkerIncomingMessage, AudioWaveformWorkerOutgoingMessage, AudioWaveformWorkerRenderMessage, } 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
+ export { getVisibleWaveformVolume } from './audio-waveform/get-visible-waveform-volume';
4
5
  export { loadWaveformPeaks } from './audio-waveform/load-waveform-peaks';
5
6
  export { makeAudioWaveformWorker } from './audio-waveform/make-audio-waveform-worker';
7
+ export { sliceVisibleWaveformPeaks } from './audio-waveform/slice-visible-waveform-peaks';
6
8
  export { sliceWaveformPeaks } from './audio-waveform/slice-waveform-peaks';
7
9
  export { createWaveformPeakProcessor, emitWaveformProgress, } from './audio-waveform/waveform-peak-processor';
8
- export { drawRepeatingImageThumbnail } from './image-thumbnail/draw-repeating-image-thumbnail';
9
- export { getScaledImageThumbnailDimensions } from './image-thumbnail/get-scaled-image-thumbnail-dimensions';
10
10
  export { clampTimestampsToDuration } from './clamp-timestamps-to-duration';
11
11
  export { extractFrames } from './extract-frames';
12
12
  export type { ExtractFramesProps, ExtractFramesTimestampsInSecondsFn, } from './extract-frames';
13
13
  export { addFrameToCache, aspectRatioCache, frameDatabase, getAspectRatioFromCache, getFrameDatabaseKeyPrefix, getTimestampFromFrameDatabaseKey, makeFrameDatabaseKey, } from './frame-database';
14
14
  export type { FrameDatabaseKey } from './frame-database';
15
- export { getLoopDisplayWidth, shouldTileLoopDisplay } from './loop-display';
16
- export type { TimelineLoopDisplay } from './loop-display';
17
- export { calculateTimestampSlots, drawSlot, ensureSlots, fillFrameWhereItFits, fillWithCachedFrames, getDurationOfOneFrame, WEBCODECS_TIMESCALE, } from './render-frame-strip';
15
+ export { drawRepeatingImageThumbnail } from './image-thumbnail/draw-repeating-image-thumbnail';
16
+ export { getScaledImageThumbnailDimensions } from './image-thumbnail/get-scaled-image-thumbnail-dimensions';
17
+ export { getLoopDisplaySegments, getLoopDisplayWidth, shouldTileLoopDisplay, } from './loop-display';
18
+ export type { LoopDisplaySegment, TimelineLoopDisplay } from './loop-display';
19
+ export { WEBCODECS_TIMESCALE, calculateTimestampSlots, drawSlot, ensureSlots, fillFrameWhereItFits, fillWithCachedFrames, getDurationOfOneFrame, } from './render-frame-strip';
18
20
  export { renderFrameStripToCanvas } from './render-frame-strip-to-canvas';
19
21
  export type { RenderFrameStripToCanvasOptions } from './render-frame-strip-to-canvas';
20
22
  export { resizeVideoFrame } from './resize-video-frame';
package/dist/index.js CHANGED
@@ -1,23 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resizeVideoFrame = exports.renderFrameStripToCanvas = exports.WEBCODECS_TIMESCALE = exports.getDurationOfOneFrame = exports.fillWithCachedFrames = exports.fillFrameWhereItFits = exports.ensureSlots = exports.drawSlot = exports.calculateTimestampSlots = exports.shouldTileLoopDisplay = exports.getLoopDisplayWidth = exports.makeFrameDatabaseKey = exports.getTimestampFromFrameDatabaseKey = exports.getFrameDatabaseKeyPrefix = exports.getAspectRatioFromCache = exports.frameDatabase = exports.aspectRatioCache = exports.addFrameToCache = exports.extractFrames = exports.clampTimestampsToDuration = exports.getScaledImageThumbnailDimensions = exports.drawRepeatingImageThumbnail = exports.emitWaveformProgress = exports.createWaveformPeakProcessor = exports.sliceWaveformPeaks = exports.makeAudioWaveformWorker = exports.loadWaveformPeaks = 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.sliceVisibleWaveformPeaks = exports.makeAudioWaveformWorker = 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");
7
7
  Object.defineProperty(exports, "drawBars", { enumerable: true, get: function () { return draw_peaks_1.drawBars; } });
8
+ const get_visible_waveform_volume_1 = require("./audio-waveform/get-visible-waveform-volume");
9
+ Object.defineProperty(exports, "getVisibleWaveformVolume", { enumerable: true, get: function () { return get_visible_waveform_volume_1.getVisibleWaveformVolume; } });
8
10
  const load_waveform_peaks_1 = require("./audio-waveform/load-waveform-peaks");
9
11
  Object.defineProperty(exports, "loadWaveformPeaks", { enumerable: true, get: function () { return load_waveform_peaks_1.loadWaveformPeaks; } });
10
12
  const make_audio_waveform_worker_1 = require("./audio-waveform/make-audio-waveform-worker");
11
13
  Object.defineProperty(exports, "makeAudioWaveformWorker", { enumerable: true, get: function () { return make_audio_waveform_worker_1.makeAudioWaveformWorker; } });
14
+ const slice_visible_waveform_peaks_1 = require("./audio-waveform/slice-visible-waveform-peaks");
15
+ Object.defineProperty(exports, "sliceVisibleWaveformPeaks", { enumerable: true, get: function () { return slice_visible_waveform_peaks_1.sliceVisibleWaveformPeaks; } });
12
16
  const slice_waveform_peaks_1 = require("./audio-waveform/slice-waveform-peaks");
13
17
  Object.defineProperty(exports, "sliceWaveformPeaks", { enumerable: true, get: function () { return slice_waveform_peaks_1.sliceWaveformPeaks; } });
14
18
  const waveform_peak_processor_1 = require("./audio-waveform/waveform-peak-processor");
15
19
  Object.defineProperty(exports, "createWaveformPeakProcessor", { enumerable: true, get: function () { return waveform_peak_processor_1.createWaveformPeakProcessor; } });
16
20
  Object.defineProperty(exports, "emitWaveformProgress", { enumerable: true, get: function () { return waveform_peak_processor_1.emitWaveformProgress; } });
17
- const draw_repeating_image_thumbnail_1 = require("./image-thumbnail/draw-repeating-image-thumbnail");
18
- Object.defineProperty(exports, "drawRepeatingImageThumbnail", { enumerable: true, get: function () { return draw_repeating_image_thumbnail_1.drawRepeatingImageThumbnail; } });
19
- const get_scaled_image_thumbnail_dimensions_1 = require("./image-thumbnail/get-scaled-image-thumbnail-dimensions");
20
- Object.defineProperty(exports, "getScaledImageThumbnailDimensions", { enumerable: true, get: function () { return get_scaled_image_thumbnail_dimensions_1.getScaledImageThumbnailDimensions; } });
21
21
  const clamp_timestamps_to_duration_1 = require("./clamp-timestamps-to-duration");
22
22
  Object.defineProperty(exports, "clampTimestampsToDuration", { enumerable: true, get: function () { return clamp_timestamps_to_duration_1.clampTimestampsToDuration; } });
23
23
  const extract_frames_1 = require("./extract-frames");
@@ -30,17 +30,22 @@ Object.defineProperty(exports, "getAspectRatioFromCache", { enumerable: true, ge
30
30
  Object.defineProperty(exports, "getFrameDatabaseKeyPrefix", { enumerable: true, get: function () { return frame_database_1.getFrameDatabaseKeyPrefix; } });
31
31
  Object.defineProperty(exports, "getTimestampFromFrameDatabaseKey", { enumerable: true, get: function () { return frame_database_1.getTimestampFromFrameDatabaseKey; } });
32
32
  Object.defineProperty(exports, "makeFrameDatabaseKey", { enumerable: true, get: function () { return frame_database_1.makeFrameDatabaseKey; } });
33
+ const draw_repeating_image_thumbnail_1 = require("./image-thumbnail/draw-repeating-image-thumbnail");
34
+ Object.defineProperty(exports, "drawRepeatingImageThumbnail", { enumerable: true, get: function () { return draw_repeating_image_thumbnail_1.drawRepeatingImageThumbnail; } });
35
+ const get_scaled_image_thumbnail_dimensions_1 = require("./image-thumbnail/get-scaled-image-thumbnail-dimensions");
36
+ Object.defineProperty(exports, "getScaledImageThumbnailDimensions", { enumerable: true, get: function () { return get_scaled_image_thumbnail_dimensions_1.getScaledImageThumbnailDimensions; } });
33
37
  const loop_display_1 = require("./loop-display");
38
+ Object.defineProperty(exports, "getLoopDisplaySegments", { enumerable: true, get: function () { return loop_display_1.getLoopDisplaySegments; } });
34
39
  Object.defineProperty(exports, "getLoopDisplayWidth", { enumerable: true, get: function () { return loop_display_1.getLoopDisplayWidth; } });
35
40
  Object.defineProperty(exports, "shouldTileLoopDisplay", { enumerable: true, get: function () { return loop_display_1.shouldTileLoopDisplay; } });
36
41
  const render_frame_strip_1 = require("./render-frame-strip");
42
+ Object.defineProperty(exports, "WEBCODECS_TIMESCALE", { enumerable: true, get: function () { return render_frame_strip_1.WEBCODECS_TIMESCALE; } });
37
43
  Object.defineProperty(exports, "calculateTimestampSlots", { enumerable: true, get: function () { return render_frame_strip_1.calculateTimestampSlots; } });
38
44
  Object.defineProperty(exports, "drawSlot", { enumerable: true, get: function () { return render_frame_strip_1.drawSlot; } });
39
45
  Object.defineProperty(exports, "ensureSlots", { enumerable: true, get: function () { return render_frame_strip_1.ensureSlots; } });
40
46
  Object.defineProperty(exports, "fillFrameWhereItFits", { enumerable: true, get: function () { return render_frame_strip_1.fillFrameWhereItFits; } });
41
47
  Object.defineProperty(exports, "fillWithCachedFrames", { enumerable: true, get: function () { return render_frame_strip_1.fillWithCachedFrames; } });
42
48
  Object.defineProperty(exports, "getDurationOfOneFrame", { enumerable: true, get: function () { return render_frame_strip_1.getDurationOfOneFrame; } });
43
- Object.defineProperty(exports, "WEBCODECS_TIMESCALE", { enumerable: true, get: function () { return render_frame_strip_1.WEBCODECS_TIMESCALE; } });
44
49
  const render_frame_strip_to_canvas_1 = require("./render-frame-strip-to-canvas");
45
50
  Object.defineProperty(exports, "renderFrameStripToCanvas", { enumerable: true, get: function () { return render_frame_strip_to_canvas_1.renderFrameStripToCanvas; } });
46
51
  const resize_video_frame_1 = require("./resize-video-frame");
@@ -4,6 +4,17 @@ export type TimelineLoopDisplay = {
4
4
  startOffset: number;
5
5
  };
6
6
  export declare const shouldTileLoopDisplay: (loopDisplay: TimelineLoopDisplay | undefined) => loopDisplay is TimelineLoopDisplay;
7
+ export type LoopDisplaySegment = {
8
+ loopIndex: number;
9
+ absoluteOffsetInFrames: number;
10
+ loopOffsetInFrames: number;
11
+ durationInFrames: number;
12
+ };
13
+ export declare const getLoopDisplaySegments: ({ displayDurationInFrames, displayOffsetInFrames, loopDurationInFrames, }: {
14
+ readonly displayDurationInFrames: number;
15
+ readonly displayOffsetInFrames: number;
16
+ readonly loopDurationInFrames: number;
17
+ }) => LoopDisplaySegment[];
7
18
  export declare const getLoopDisplayWidth: ({ visualizationWidth, loopDisplay, }: {
8
19
  visualizationWidth: number;
9
20
  loopDisplay: TimelineLoopDisplay | undefined;
@@ -1,10 +1,45 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getLoopDisplayWidth = exports.shouldTileLoopDisplay = void 0;
3
+ exports.getLoopDisplayWidth = exports.getLoopDisplaySegments = exports.shouldTileLoopDisplay = void 0;
4
4
  const shouldTileLoopDisplay = (loopDisplay) => {
5
5
  return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
6
6
  };
7
7
  exports.shouldTileLoopDisplay = shouldTileLoopDisplay;
8
+ // Computes the visible loop segments by loop index instead of accumulating
9
+ // floating-point segment durations. With fractional display offsets (e.g. from
10
+ // horizontal timeline virtualization), an accumulating `processed += segment`
11
+ // loop can stall once the residue is smaller than the ULP of `processed`,
12
+ // growing arrays unboundedly until the tab runs out of memory.
13
+ const getLoopDisplaySegments = ({ displayDurationInFrames, displayOffsetInFrames, loopDurationInFrames, }) => {
14
+ if (!Number.isFinite(displayDurationInFrames) ||
15
+ displayDurationInFrames <= 0 ||
16
+ !Number.isFinite(displayOffsetInFrames) ||
17
+ !Number.isFinite(loopDurationInFrames) ||
18
+ loopDurationInFrames <= 0) {
19
+ return [];
20
+ }
21
+ const displayEndInFrames = displayOffsetInFrames + displayDurationInFrames;
22
+ const firstLoopIndex = Math.floor(displayOffsetInFrames / loopDurationInFrames);
23
+ const lastLoopIndex = Math.floor(displayEndInFrames / loopDurationInFrames);
24
+ const segments = [];
25
+ for (let loopIndex = firstLoopIndex; loopIndex <= lastLoopIndex; loopIndex++) {
26
+ const loopStartInFrames = loopIndex * loopDurationInFrames;
27
+ const segmentStart = Math.max(displayOffsetInFrames, loopStartInFrames);
28
+ const segmentEnd = Math.min(displayEndInFrames, loopStartInFrames + loopDurationInFrames);
29
+ const durationInFrames = segmentEnd - segmentStart;
30
+ if (durationInFrames <= 0) {
31
+ continue;
32
+ }
33
+ segments.push({
34
+ loopIndex,
35
+ absoluteOffsetInFrames: segmentStart,
36
+ loopOffsetInFrames: segmentStart - loopStartInFrames,
37
+ durationInFrames,
38
+ });
39
+ }
40
+ return segments;
41
+ };
42
+ exports.getLoopDisplaySegments = getLoopDisplaySegments;
8
43
  const getLoopDisplayWidth = ({ visualizationWidth, loopDisplay, }) => {
9
44
  if (!(0, exports.shouldTileLoopDisplay)(loopDisplay)) {
10
45
  return visualizationWidth;
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.513",
6
+ "version": "4.0.514",
7
7
  "description": "Internal utilities for rendering Remotion timelines",
8
8
  "main": "dist/index.js",
9
9
  "scripts": {
@@ -21,11 +21,11 @@
21
21
  "url": "https://github.com/remotion-dev/remotion/issues"
22
22
  },
23
23
  "dependencies": {
24
- "mediabunny": "1.50.8"
24
+ "mediabunny": "1.55.1"
25
25
  },
26
26
  "devDependencies": {
27
- "@mediabunny/server": "1.50.8",
28
- "@remotion/eslint-config-internal": "4.0.513",
27
+ "@mediabunny/server": "1.55.1",
28
+ "@remotion/eslint-config-internal": "4.0.514",
29
29
  "eslint": "9.19.0",
30
30
  "@typescript/native-preview": "7.0.0-dev.20260217.1"
31
31
  },