@remotion/media 4.0.487 → 4.0.489

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,10 +1,11 @@
1
- import type { AudioBufferSink } from 'mediabunny';
1
+ import type { AudioBufferSlice } from '../make-iterator-with-priming';
2
2
  export declare const HEALTHY_BUFFER_THRESHOLD_SECONDS = 1;
3
3
  export declare const ALLOWED_GLOBAL_TIME_ANCHOR_SHIFT = 0.1;
4
4
  export type QueuedNode = {
5
5
  node: AudioBufferSourceNode;
6
6
  timestamp: number;
7
7
  buffer: AudioBuffer;
8
+ sourceDurationInSeconds: number;
8
9
  scheduledTime: number;
9
10
  playbackRate: number;
10
11
  scheduledAtAnchor: number;
@@ -13,26 +14,16 @@ export type QueuedPeriod = {
13
14
  from: number;
14
15
  until: number;
15
16
  };
16
- export declare const makeAudioIterator: ({ startFromSecond, maximumTimestamp, audioSink, loop, playbackRate, sequenceDurationInSeconds, unscheduleAudioNode, }: {
17
+ export type MakeAudioIteratorOptions = {
17
18
  startFromSecond: number;
18
- maximumTimestamp: number;
19
- logLevel: "error" | "info" | "trace" | "verbose" | "warn";
20
- audioSink: AudioBufferSink;
21
- loop: boolean;
22
- playbackRate: number;
23
- sequenceDurationInSeconds: number;
19
+ iterator: AsyncGenerator<AudioBufferSlice, void, unknown>;
24
20
  unscheduleAudioNode: (node: AudioBufferSourceNode) => void;
25
- }) => {
21
+ };
22
+ export declare const makeAudioIterator: ({ startFromSecond, iterator, unscheduleAudioNode, }: MakeAudioIteratorOptions) => {
26
23
  destroy: () => void;
27
- getNextFn: () => Promise<IteratorResult<import("../make-iterator-with-priming").BufferWithMediaTimestamp, void>>;
24
+ getNextFn: () => Promise<IteratorResult<AudioBufferSlice, void>>;
28
25
  isDestroyed: () => boolean;
29
- addQueuedAudioNode: ({ node, timestamp, buffer, scheduledTime, scheduledAtAnchor, }: {
30
- node: AudioBufferSourceNode;
31
- timestamp: number;
32
- buffer: AudioBuffer;
33
- scheduledTime: number;
34
- scheduledAtAnchor: number;
35
- }) => void;
26
+ addQueuedAudioNode: (queuedNode: QueuedNode) => void;
36
27
  guessNextTimestamp: () => number;
37
28
  getQueuedPeriod: () => {
38
29
  from: number;
@@ -4,15 +4,15 @@ export declare const getScheduledTime: ({ mediaTimestamp, targetTime, currentTim
4
4
  currentTime: number;
5
5
  sequenceStartTime: number;
6
6
  }) => number;
7
- export declare const getDurationOfNode: ({ bufferDuration, loopSegmentMediaEndTimestamp, offset, originalUnloopedMediaTimestamp, }: {
8
- bufferDuration: number;
9
- loopSegmentMediaEndTimestamp: number;
7
+ export declare const getDurationOfNode: ({ sourceDurationInSeconds, sourceOffsetInSeconds, offset, }: {
8
+ sourceDurationInSeconds: number;
9
+ sourceOffsetInSeconds: number;
10
10
  offset: number;
11
- originalUnloopedMediaTimestamp: number;
12
11
  }) => number;
13
- export declare const getTrimStartForAudioNode: ({ mediaTimestamp, targetTime, sequenceStartTime, combinedPlaybackRate, }: {
12
+ export declare const getTrimStartForAudioNode: ({ mediaTimestamp, targetTime, sequenceStartTime, combinedPlaybackRate, sourceStartOffsetInSeconds, }: {
14
13
  mediaTimestamp: number;
15
14
  targetTime: number;
16
15
  sequenceStartTime: number;
17
16
  combinedPlaybackRate: number;
17
+ sourceStartOffsetInSeconds: number;
18
18
  }) => number;
@@ -1,10 +1,19 @@
1
1
  import { type InputAudioTrack } from 'mediabunny';
2
2
  import { type ScheduleAudioNodeResult } from 'remotion';
3
3
  import type { DelayPlaybackIfNotPremounting } from './delay-playback-if-not-premounting';
4
- import type { BufferWithMediaTimestamp } from './make-iterator-with-priming';
4
+ import { type AudioBufferSlice } from './make-iterator-with-priming';
5
5
  import type { Nonce } from './nonce-manager';
6
6
  import type { SharedAudioContextForMediaPlayer } from './shared-audio-context-for-media-player';
7
- type ScheduleAudioNode = (node: AudioBufferSourceNode, mediaTimestamp: number, originalUnloopedMediaTimestamp: number) => ScheduleAudioNodeResult;
7
+ type ScheduleAudioNode = (node: AudioBufferSourceNode, mediaTimestamp: number, originalUnloopedMediaTimestamp: number, sourceOffsetInSeconds: number, sourceDurationInSeconds: number) => ScheduleAudioNodeResult;
8
+ export type AudioIteratorAnchor = {
9
+ unloopedStartInSeconds: number;
10
+ mediaStartInSeconds: number;
11
+ };
12
+ export declare const anchorToContinuousTime: ({ anchor, unloopedTimeInSeconds, playbackRate, }: {
13
+ anchor: AudioIteratorAnchor;
14
+ unloopedTimeInSeconds: number;
15
+ playbackRate: number;
16
+ }) => number;
8
17
  export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfNotPremounting, sharedAudioContext, getSequenceEndTimestamp, getSequenceDurationInSeconds, getMediaEndTimestamp, getStartTime, initialMuted, drawDebugOverlay, initialPlaybackRate, initialTrimBefore, initialTrimAfter, initialSequenceOffset, initialSequenceDurationInFrames, initialLoop, initialFps, }: {
9
18
  audioTrack: InputAudioTrack;
10
19
  delayPlaybackHandleIfNotPremounting: () => DelayPlaybackIfNotPremounting;
@@ -23,8 +32,9 @@ export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfN
23
32
  initialLoop: boolean;
24
33
  initialFps: number;
25
34
  }) => {
26
- startAudioIterator: ({ nonce, playbackRate, startFromSecond, scheduleAudioNode, getTargetTime, logLevel, loop, unscheduleAudioNode, getAudioContextCurrentTimeMockedInTest, }: {
35
+ startAudioIterator: ({ nonce, playbackRate, startFromSecond, unloopedStartFromSecond, scheduleAudioNode, getTargetTime, logLevel, loop, unscheduleAudioNode, getAudioContextCurrentTimeMockedInTest, }: {
27
36
  startFromSecond: number;
37
+ unloopedStartFromSecond: number;
28
38
  nonce: Nonce;
29
39
  playbackRate: number;
30
40
  scheduleAudioNode: ScheduleAudioNode;
@@ -36,26 +46,23 @@ export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfN
36
46
  }) => void;
37
47
  getAudioBufferIterator: () => {
38
48
  destroy: () => void;
39
- getNextFn: () => Promise<IteratorResult<BufferWithMediaTimestamp, void>>;
49
+ getNextFn: () => Promise<IteratorResult<AudioBufferSlice, void>>;
40
50
  isDestroyed: () => boolean;
41
- addQueuedAudioNode: ({ node, timestamp, buffer, scheduledTime, scheduledAtAnchor, }: {
42
- node: AudioBufferSourceNode;
43
- timestamp: number;
44
- buffer: AudioBuffer;
45
- scheduledTime: number;
46
- scheduledAtAnchor: number;
47
- }) => void;
51
+ addQueuedAudioNode: (queuedNode: import("./audio/audio-preview-iterator").QueuedNode) => void;
48
52
  guessNextTimestamp: () => number;
49
53
  getQueuedPeriod: () => {
50
54
  from: number;
51
55
  until: number;
52
56
  } | null;
53
57
  } | null;
58
+ getCurrentAnchor: () => AudioIteratorAnchor | null;
54
59
  destroyIterator: () => void;
55
- seek: ({ newTime, nonce, playbackRate, scheduleAudioNode, getTargetTime, logLevel, loop, trimBefore, trimAfter, sequenceOffset, sequenceDurationInFrames, fps, getAudioContextCurrentTimeMockedInTest, }: {
60
+ seek: ({ newTime, unloopedNewTime, nonce, playbackRate, localPlaybackRate, scheduleAudioNode, getTargetTime, logLevel, loop, trimBefore, trimAfter, sequenceOffset, sequenceDurationInFrames, fps, getAudioContextCurrentTimeMockedInTest, }: {
56
61
  newTime: number;
62
+ unloopedNewTime: number;
57
63
  nonce: Nonce;
58
64
  playbackRate: number;
65
+ localPlaybackRate: number;
59
66
  scheduleAudioNode: ScheduleAudioNode;
60
67
  getTargetTime: (mediaTimestamp: number, currentTime: number) => number | null;
61
68
  logLevel: "error" | "info" | "trace" | "verbose" | "warn";
@@ -71,13 +78,15 @@ export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfN
71
78
  getTotalAudioScheduledInSeconds: () => number;
72
79
  setMuted: (newMuted: boolean) => void;
73
80
  setVolume: (volume: number) => void;
74
- scheduleAudioChunk: ({ buffer, mediaTimestamp, originalUnloopedMediaTimestamp, playbackRate, scheduleAudioNode, logLevel, }: {
81
+ scheduleAudioChunk: ({ buffer, mediaTimestamp, originalUnloopedMediaTimestamp, sourceOffsetInSeconds, sourceDurationInSeconds, playbackRate, scheduleAudioNode, logLevel, }: {
75
82
  buffer: AudioBuffer;
76
83
  mediaTimestamp: number;
77
84
  playbackRate: number;
78
85
  scheduleAudioNode: ScheduleAudioNode;
79
86
  logLevel: "error" | "info" | "trace" | "verbose" | "warn";
80
87
  originalUnloopedMediaTimestamp: number;
88
+ sourceOffsetInSeconds: number;
89
+ sourceDurationInSeconds: number;
81
90
  }) => void;
82
91
  waitForNScheduledNodes: (n: number) => Promise<void>;
83
92
  };
@@ -7,11 +7,12 @@ export declare const drawPreviewOverlay: ({ context, audioTime, audioContextStat
7
7
  } | null;
8
8
  playing: boolean;
9
9
  audioIteratorManager: {
10
- startAudioIterator: ({ nonce, playbackRate, startFromSecond, scheduleAudioNode, getTargetTime, logLevel, loop, unscheduleAudioNode, getAudioContextCurrentTimeMockedInTest, }: {
10
+ startAudioIterator: ({ nonce, playbackRate, startFromSecond, unloopedStartFromSecond, scheduleAudioNode, getTargetTime, logLevel, loop, unscheduleAudioNode, getAudioContextCurrentTimeMockedInTest, }: {
11
11
  startFromSecond: number;
12
+ unloopedStartFromSecond: number;
12
13
  nonce: import("../nonce-manager").Nonce;
13
14
  playbackRate: number;
14
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, originalUnloopedMediaTimestamp: number) => import("remotion").ScheduleAudioNodeResult;
15
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, originalUnloopedMediaTimestamp: number, sourceOffsetInSeconds: number, sourceDurationInSeconds: number) => import("remotion").ScheduleAudioNodeResult;
15
16
  getTargetTime: (mediaTimestamp: number, currentTime: number) => number | null;
16
17
  logLevel: "error" | "info" | "trace" | "verbose" | "warn";
17
18
  loop: boolean;
@@ -20,27 +21,24 @@ export declare const drawPreviewOverlay: ({ context, audioTime, audioContextStat
20
21
  }) => void;
21
22
  getAudioBufferIterator: () => {
22
23
  destroy: () => void;
23
- getNextFn: () => Promise<IteratorResult<import("../make-iterator-with-priming").BufferWithMediaTimestamp, void>>;
24
+ getNextFn: () => Promise<IteratorResult<import("../make-iterator-with-priming").AudioBufferSlice, void>>;
24
25
  isDestroyed: () => boolean;
25
- addQueuedAudioNode: ({ node, timestamp, buffer, scheduledTime, scheduledAtAnchor, }: {
26
- node: AudioBufferSourceNode;
27
- timestamp: number;
28
- buffer: AudioBuffer;
29
- scheduledTime: number;
30
- scheduledAtAnchor: number;
31
- }) => void;
26
+ addQueuedAudioNode: (queuedNode: import("../audio/audio-preview-iterator").QueuedNode) => void;
32
27
  guessNextTimestamp: () => number;
33
28
  getQueuedPeriod: () => {
34
29
  from: number;
35
30
  until: number;
36
31
  } | null;
37
32
  } | null;
33
+ getCurrentAnchor: () => import("../audio-iterator-manager").AudioIteratorAnchor | null;
38
34
  destroyIterator: () => void;
39
- seek: ({ newTime, nonce, playbackRate, scheduleAudioNode, getTargetTime, logLevel, loop, trimBefore, trimAfter, sequenceOffset, sequenceDurationInFrames, fps, getAudioContextCurrentTimeMockedInTest, }: {
35
+ seek: ({ newTime, unloopedNewTime, nonce, playbackRate, localPlaybackRate, scheduleAudioNode, getTargetTime, logLevel, loop, trimBefore, trimAfter, sequenceOffset, sequenceDurationInFrames, fps, getAudioContextCurrentTimeMockedInTest, }: {
40
36
  newTime: number;
37
+ unloopedNewTime: number;
41
38
  nonce: import("../nonce-manager").Nonce;
42
39
  playbackRate: number;
43
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, originalUnloopedMediaTimestamp: number) => import("remotion").ScheduleAudioNodeResult;
40
+ localPlaybackRate: number;
41
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, originalUnloopedMediaTimestamp: number, sourceOffsetInSeconds: number, sourceDurationInSeconds: number) => import("remotion").ScheduleAudioNodeResult;
44
42
  getTargetTime: (mediaTimestamp: number, currentTime: number) => number | null;
45
43
  logLevel: "error" | "info" | "trace" | "verbose" | "warn";
46
44
  loop: boolean;
@@ -55,13 +53,15 @@ export declare const drawPreviewOverlay: ({ context, audioTime, audioContextStat
55
53
  getTotalAudioScheduledInSeconds: () => number;
56
54
  setMuted: (newMuted: boolean) => void;
57
55
  setVolume: (volume: number) => void;
58
- scheduleAudioChunk: ({ buffer, mediaTimestamp, originalUnloopedMediaTimestamp, playbackRate, scheduleAudioNode, logLevel, }: {
56
+ scheduleAudioChunk: ({ buffer, mediaTimestamp, originalUnloopedMediaTimestamp, sourceOffsetInSeconds, sourceDurationInSeconds, playbackRate, scheduleAudioNode, logLevel, }: {
59
57
  buffer: AudioBuffer;
60
58
  mediaTimestamp: number;
61
59
  playbackRate: number;
62
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, originalUnloopedMediaTimestamp: number) => import("remotion").ScheduleAudioNodeResult;
60
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, originalUnloopedMediaTimestamp: number, sourceOffsetInSeconds: number, sourceDurationInSeconds: number) => import("remotion").ScheduleAudioNodeResult;
63
61
  logLevel: "error" | "info" | "trace" | "verbose" | "warn";
64
62
  originalUnloopedMediaTimestamp: number;
63
+ sourceOffsetInSeconds: number;
64
+ sourceDurationInSeconds: number;
65
65
  }) => void;
66
66
  waitForNScheduledNodes: (n: number) => Promise<void>;
67
67
  } | null;
@@ -146,97 +146,21 @@ import {
146
146
  } from "mediabunny";
147
147
  import { Internals as Internals3 } from "remotion";
148
148
 
149
- // src/make-iterator-with-priming.ts
150
- var AUDIO_PRIMING_SECONDS = 0.5;
151
- async function* makeIteratorWithPrimingInner(audioSink, timeToSeek, maximumTimestamp) {
152
- const primingStart = Math.max(0, timeToSeek - AUDIO_PRIMING_SECONDS);
153
- const iterator = audioSink.buffers(primingStart, maximumTimestamp);
154
- for await (const buffer of iterator) {
155
- if (buffer.timestamp + buffer.duration <= timeToSeek) {
156
- continue;
157
- }
158
- yield {
159
- buffer,
160
- timestamp: buffer.timestamp
161
- };
162
- }
163
- }
164
- async function* makeLoopingIterator({
165
- audioSink,
166
- segmentStartInSeconds,
167
- segmentEndInSeconds,
168
- playbackRate,
169
- sequenceDurationInSeconds
170
- }) {
171
- const duration = segmentEndInSeconds - segmentStartInSeconds;
172
- let iteration = 0;
173
- let broken = false;
174
- while (true) {
175
- for await (const item of makeIteratorWithPrimingInner(audioSink, segmentStartInSeconds, segmentEndInSeconds)) {
176
- const timestamp = item.timestamp + iteration * duration;
177
- const endTimestamp = duration * iteration + (item.timestamp - segmentStartInSeconds + item.buffer.duration);
178
- if (endTimestamp > sequenceDurationInSeconds * playbackRate) {
179
- broken = true;
180
- break;
181
- }
182
- yield {
183
- buffer: item.buffer,
184
- timestamp
185
- };
186
- }
187
- if (broken) {
188
- break;
189
- }
190
- iteration++;
191
- }
192
- }
193
- var makeIteratorWithPriming = ({
194
- audioSink,
195
- timeToSeek,
196
- maximumTimestamp,
197
- loop,
198
- playbackRate,
199
- sequenceDurationInSeconds
200
- }) => {
201
- if (loop) {
202
- return makeLoopingIterator({
203
- audioSink,
204
- segmentStartInSeconds: timeToSeek,
205
- segmentEndInSeconds: maximumTimestamp,
206
- playbackRate,
207
- sequenceDurationInSeconds
208
- });
209
- }
210
- return makeIteratorWithPrimingInner(audioSink, timeToSeek, maximumTimestamp);
211
- };
212
-
213
149
  // src/audio/audio-preview-iterator.ts
214
150
  var ALLOWED_GLOBAL_TIME_ANCHOR_SHIFT = 0.1;
215
151
  var makeAudioIterator = ({
216
152
  startFromSecond,
217
- maximumTimestamp,
218
- audioSink,
219
- loop,
220
- playbackRate,
221
- sequenceDurationInSeconds,
153
+ iterator,
222
154
  unscheduleAudioNode
223
155
  }) => {
224
156
  let destroyed = false;
225
- const iterator = makeIteratorWithPriming({
226
- audioSink,
227
- timeToSeek: startFromSecond,
228
- maximumTimestamp,
229
- loop,
230
- playbackRate,
231
- sequenceDurationInSeconds
232
- });
233
157
  const queuedAudioNodes = [];
234
158
  let mostRecentTimestamp = -Infinity;
235
159
  const cleanupAudioQueue = () => {
236
- for (const node of queuedAudioNodes) {
237
- unscheduleAudioNode(node.node);
160
+ for (const { node } of queuedAudioNodes) {
161
+ unscheduleAudioNode(node);
238
162
  try {
239
- node.node.stop();
163
+ node.stop();
240
164
  } catch {}
241
165
  }
242
166
  queuedAudioNodes.length = 0;
@@ -244,7 +168,7 @@ var makeAudioIterator = ({
244
168
  const getNextFn = async () => {
245
169
  const next = await iterator.next();
246
170
  if (next.value) {
247
- mostRecentTimestamp = Math.max(mostRecentTimestamp, next.value.timestamp + next.value.buffer.duration);
171
+ mostRecentTimestamp = Math.max(mostRecentTimestamp, next.value.timelineTimestamp + next.value.sourceDurationInSeconds);
248
172
  }
249
173
  return next;
250
174
  };
@@ -260,21 +184,8 @@ var makeAudioIterator = ({
260
184
  isDestroyed: () => {
261
185
  return destroyed;
262
186
  },
263
- addQueuedAudioNode: ({
264
- node,
265
- timestamp,
266
- buffer,
267
- scheduledTime,
268
- scheduledAtAnchor
269
- }) => {
270
- queuedAudioNodes.push({
271
- node,
272
- timestamp,
273
- buffer,
274
- scheduledTime,
275
- playbackRate,
276
- scheduledAtAnchor
277
- });
187
+ addQueuedAudioNode: (queuedNode) => {
188
+ queuedAudioNodes.push(queuedNode);
278
189
  },
279
190
  guessNextTimestamp: () => {
280
191
  return !Number.isFinite(mostRecentTimestamp) ? startFromSecond : mostRecentTimestamp;
@@ -283,7 +194,7 @@ var makeAudioIterator = ({
283
194
  let until = -Infinity;
284
195
  let from = Infinity;
285
196
  for (const node of queuedAudioNodes) {
286
- until = Math.max(until, node.timestamp + node.buffer.duration);
197
+ until = Math.max(until, node.timestamp + node.sourceDurationInSeconds);
287
198
  from = Math.min(from, node.timestamp);
288
199
  }
289
200
  if (!Number.isFinite(from) || !Number.isFinite(until)) {
@@ -318,27 +229,23 @@ var getScheduledTime = ({
318
229
  return scheduledTime;
319
230
  };
320
231
  var getDurationOfNode = ({
321
- bufferDuration,
322
- loopSegmentMediaEndTimestamp,
323
- offset,
324
- originalUnloopedMediaTimestamp
232
+ sourceDurationInSeconds,
233
+ sourceOffsetInSeconds,
234
+ offset
325
235
  }) => {
326
- const originalUnloopedMediaEndTime = originalUnloopedMediaTimestamp + bufferDuration;
327
- const needsTrimEnd = originalUnloopedMediaEndTime > loopSegmentMediaEndTimestamp;
328
- const durationMinusOffset = bufferDuration - offset;
329
- const duration = needsTrimEnd ? durationMinusOffset - Math.max(0, originalUnloopedMediaEndTime - loopSegmentMediaEndTimestamp) : durationMinusOffset;
330
- return duration;
236
+ return Math.max(0, sourceDurationInSeconds - (offset - sourceOffsetInSeconds));
331
237
  };
332
238
  var getTrimStartForAudioNode = ({
333
239
  mediaTimestamp,
334
240
  targetTime,
335
241
  sequenceStartTime,
336
- combinedPlaybackRate
242
+ combinedPlaybackRate,
243
+ sourceStartOffsetInSeconds
337
244
  }) => {
338
245
  const needsTrimStart = mediaTimestamp < sequenceStartTime;
339
246
  const offsetBecauseOfTrim = needsTrimStart ? sequenceStartTime - mediaTimestamp : 0;
340
247
  const offsetBecauseOfTooLate = targetTime < 0 ? -targetTime * combinedPlaybackRate : 0;
341
- return offsetBecauseOfTrim + offsetBecauseOfTooLate;
248
+ return sourceStartOffsetInSeconds + offsetBecauseOfTrim + offsetBecauseOfTooLate;
342
249
  };
343
250
 
344
251
  // src/audio/sort-by-priority.ts
@@ -442,7 +349,79 @@ var waitForTurn = ({
442
349
  processNext();
443
350
  };
444
351
 
352
+ // src/make-iterator-with-priming.ts
353
+ var AUDIO_PRIMING_SECONDS = 0.5;
354
+ async function* makeIteratorWithPriming({
355
+ audioSink,
356
+ timeToSeek,
357
+ maximumTimestamp
358
+ }) {
359
+ const primingStart = Math.max(0, timeToSeek - AUDIO_PRIMING_SECONDS);
360
+ const iterator = audioSink.buffers(primingStart, maximumTimestamp);
361
+ for await (const buffer of iterator) {
362
+ const sourceStart = Math.max(buffer.timestamp, timeToSeek);
363
+ const sourceEnd = Math.min(buffer.timestamp + buffer.duration, maximumTimestamp);
364
+ if (sourceEnd <= sourceStart) {
365
+ continue;
366
+ }
367
+ yield {
368
+ buffer,
369
+ timelineTimestamp: sourceStart,
370
+ sourceOffsetInSeconds: sourceStart - buffer.timestamp,
371
+ sourceDurationInSeconds: sourceEnd - sourceStart
372
+ };
373
+ }
374
+ }
375
+ async function* makeLoopingIterator({
376
+ audioSink,
377
+ seekTimeInSeconds,
378
+ loopStartInSeconds,
379
+ segmentEndInSeconds,
380
+ maximumContinuousTimestamp
381
+ }) {
382
+ if (segmentEndInSeconds <= loopStartInSeconds) {
383
+ throw new Error(`Cannot loop audio over an empty range (${loopStartInSeconds} to ${segmentEndInSeconds})`);
384
+ }
385
+ let passStartInSeconds = seekTimeInSeconds;
386
+ let passBaseTimestamp = seekTimeInSeconds;
387
+ while (true) {
388
+ let yieldedInPass = false;
389
+ for await (const item of makeIteratorWithPriming({
390
+ audioSink,
391
+ timeToSeek: passStartInSeconds,
392
+ maximumTimestamp: segmentEndInSeconds
393
+ })) {
394
+ yieldedInPass = true;
395
+ const timelineTimestamp = passBaseTimestamp + (item.timelineTimestamp - passStartInSeconds);
396
+ const sourceDurationInSeconds = Math.min(item.sourceDurationInSeconds, maximumContinuousTimestamp - timelineTimestamp);
397
+ if (sourceDurationInSeconds <= 0) {
398
+ return;
399
+ }
400
+ yield {
401
+ ...item,
402
+ timelineTimestamp,
403
+ sourceDurationInSeconds
404
+ };
405
+ if (timelineTimestamp + sourceDurationInSeconds >= maximumContinuousTimestamp) {
406
+ return;
407
+ }
408
+ }
409
+ if (!yieldedInPass) {
410
+ throw new Error(`Audio loop pass from ${passStartInSeconds} to ${segmentEndInSeconds} yielded no buffers`);
411
+ }
412
+ passBaseTimestamp += segmentEndInSeconds - passStartInSeconds;
413
+ passStartInSeconds = loopStartInSeconds;
414
+ }
415
+ }
416
+
445
417
  // src/audio-iterator-manager.ts
418
+ var anchorToContinuousTime = ({
419
+ anchor,
420
+ unloopedTimeInSeconds,
421
+ playbackRate
422
+ }) => {
423
+ return anchor.mediaStartInSeconds + (unloopedTimeInSeconds - anchor.unloopedStartInSeconds) * playbackRate;
424
+ };
446
425
  var audioIteratorManager = ({
447
426
  audioTrack,
448
427
  delayPlaybackHandleIfNotPremounting,
@@ -477,6 +456,7 @@ var audioIteratorManager = ({
477
456
  gainNode.connect(sharedAudioContext.gainNode);
478
457
  const audioSink = new AudioBufferSink(audioTrack);
479
458
  let audioBufferIterator = null;
459
+ let currentAnchor = null;
480
460
  let audioIteratorsCreated = 0;
481
461
  let totalAudioScheduledInSeconds = 0;
482
462
  let currentDelayHandle = null;
@@ -509,6 +489,8 @@ var audioIteratorManager = ({
509
489
  buffer,
510
490
  mediaTimestamp,
511
491
  originalUnloopedMediaTimestamp,
492
+ sourceOffsetInSeconds,
493
+ sourceDurationInSeconds,
512
494
  playbackRate,
513
495
  scheduleAudioNode,
514
496
  logLevel
@@ -523,7 +505,7 @@ var audioIteratorManager = ({
523
505
  node.buffer = buffer;
524
506
  node.playbackRate.value = playbackRate;
525
507
  node.connect(gainNode);
526
- const started = scheduleAudioNode(node, mediaTimestamp, originalUnloopedMediaTimestamp);
508
+ const started = scheduleAudioNode(node, mediaTimestamp, originalUnloopedMediaTimestamp, sourceOffsetInSeconds, sourceDurationInSeconds);
527
509
  if (started.type === "not-started") {
528
510
  Internals3.Log.verbose({ logLevel, tag: "audio-scheduling" }, "not started, disconnected: %s %s", mediaTimestamp.toFixed(3), buffer.duration.toFixed(3));
529
511
  node.disconnect();
@@ -533,7 +515,9 @@ var audioIteratorManager = ({
533
515
  node,
534
516
  timestamp: mediaTimestamp,
535
517
  buffer,
518
+ sourceDurationInSeconds,
536
519
  scheduledTime: started.scheduledTime,
520
+ playbackRate,
537
521
  scheduledAtAnchor: sharedAudioContext.audioSyncAnchor.value
538
522
  });
539
523
  };
@@ -548,22 +532,24 @@ var audioIteratorManager = ({
548
532
  }
549
533
  const startTime = getStartTime();
550
534
  const sequenceEndTime = getSequenceEndTimestamp();
551
- if (buffer.timestamp + buffer.buffer.duration <= startTime) {
535
+ if (buffer.timelineTimestamp + buffer.sourceDurationInSeconds <= startTime) {
552
536
  return;
553
537
  }
554
- if (buffer.timestamp >= sequenceEndTime) {
538
+ if (buffer.timelineTimestamp >= sequenceEndTime) {
555
539
  return;
556
540
  }
557
- const scheduledStart = Math.max(buffer.timestamp, startTime);
558
- const scheduledEnd = Math.min(buffer.timestamp + buffer.buffer.duration, sequenceEndTime);
541
+ const scheduledStart = Math.max(buffer.timelineTimestamp, startTime);
542
+ const scheduledEnd = Math.min(buffer.timelineTimestamp + buffer.sourceDurationInSeconds, sequenceEndTime);
559
543
  totalAudioScheduledInSeconds += Math.max(0, scheduledEnd - scheduledStart);
560
544
  scheduleAudioChunk({
561
545
  buffer: buffer.buffer.buffer,
562
- mediaTimestamp: buffer.timestamp,
546
+ mediaTimestamp: buffer.timelineTimestamp,
563
547
  playbackRate,
564
548
  scheduleAudioNode,
565
549
  logLevel,
566
- originalUnloopedMediaTimestamp: buffer.buffer.timestamp
550
+ originalUnloopedMediaTimestamp: buffer.buffer.timestamp,
551
+ sourceOffsetInSeconds: buffer.sourceOffsetInSeconds,
552
+ sourceDurationInSeconds: buffer.sourceDurationInSeconds
567
553
  });
568
554
  drawDebugOverlay();
569
555
  };
@@ -611,7 +597,7 @@ var audioIteratorManager = ({
611
597
  onDone();
612
598
  return;
613
599
  }
614
- onScheduled(result.value.timestamp);
600
+ onScheduled(result.value.timelineTimestamp);
615
601
  notifyNodeScheduled();
616
602
  onAudioChunk({
617
603
  buffer: result.value,
@@ -651,6 +637,7 @@ var audioIteratorManager = ({
651
637
  nonce,
652
638
  playbackRate,
653
639
  startFromSecond,
640
+ unloopedStartFromSecond,
654
641
  scheduleAudioNode,
655
642
  getTargetTime,
656
643
  logLevel,
@@ -669,14 +656,25 @@ var audioIteratorManager = ({
669
656
  unblockCurrentDelayHandle();
670
657
  const delayHandle = delayPlaybackHandleIfNotPremounting();
671
658
  currentDelayHandle = delayHandle;
659
+ currentAnchor = {
660
+ unloopedStartInSeconds: unloopedStartFromSecond,
661
+ mediaStartInSeconds: startFromSecond
662
+ };
663
+ const maximumContinuousTimestamp = startFromSecond + getSequenceDurationInSeconds() * playbackRate;
664
+ const source = loop ? makeLoopingIterator({
665
+ audioSink,
666
+ seekTimeInSeconds: startFromSecond,
667
+ loopStartInSeconds: getStartTime(),
668
+ segmentEndInSeconds: maximumTimestamp,
669
+ maximumContinuousTimestamp
670
+ }) : makeIteratorWithPriming({
671
+ audioSink,
672
+ timeToSeek: startFromSecond,
673
+ maximumTimestamp
674
+ });
672
675
  const iterator = makeAudioIterator({
673
676
  startFromSecond,
674
- maximumTimestamp,
675
- audioSink,
676
- logLevel,
677
- loop,
678
- playbackRate,
679
- sequenceDurationInSeconds: getSequenceDurationInSeconds(),
677
+ iterator: source,
680
678
  unscheduleAudioNode
681
679
  });
682
680
  audioIteratorsCreated++;
@@ -707,8 +705,10 @@ var audioIteratorManager = ({
707
705
  };
708
706
  const seek = ({
709
707
  newTime,
708
+ unloopedNewTime,
710
709
  nonce,
711
710
  playbackRate,
711
+ localPlaybackRate,
712
712
  scheduleAudioNode,
713
713
  getTargetTime,
714
714
  logLevel,
@@ -740,18 +740,23 @@ var audioIteratorManager = ({
740
740
  return;
741
741
  }
742
742
  if (audioBufferIterator && !audioBufferIterator.isDestroyed()) {
743
+ const timeToCheck = loop && currentAnchor ? anchorToContinuousTime({
744
+ anchor: currentAnchor,
745
+ unloopedTimeInSeconds: unloopedNewTime,
746
+ playbackRate: localPlaybackRate
747
+ }) : newTime;
743
748
  const queuedPeriod = audioBufferIterator.getQueuedPeriod();
744
749
  const queuedPeriodMinusLatency = queuedPeriod ? {
745
750
  from: queuedPeriod.from - ALLOWED_GLOBAL_TIME_ANCHOR_SHIFT - sharedAudioContext.audioContext.baseLatency - sharedAudioContext.audioContext.outputLatency,
746
751
  until: queuedPeriod.until
747
752
  } : null;
748
- const currentTimeIsAlreadyQueued = isAlreadyQueued(newTime, queuedPeriodMinusLatency);
753
+ const currentTimeIsAlreadyQueued = isAlreadyQueued(timeToCheck, queuedPeriodMinusLatency);
749
754
  if (currentTimeIsAlreadyQueued) {
750
755
  processNext();
751
756
  return;
752
757
  }
753
758
  const currentIteratorTimestamp = audioBufferIterator.guessNextTimestamp();
754
- if (currentIteratorTimestamp < newTime && Math.abs(currentIteratorTimestamp - newTime) < 1) {
759
+ if (currentIteratorTimestamp < timeToCheck && Math.abs(currentIteratorTimestamp - timeToCheck) < 1) {
755
760
  processNext();
756
761
  return;
757
762
  }
@@ -760,6 +765,7 @@ var audioIteratorManager = ({
760
765
  nonce,
761
766
  playbackRate,
762
767
  startFromSecond: newTime,
768
+ unloopedStartFromSecond: unloopedNewTime,
763
769
  scheduleAudioNode,
764
770
  getTargetTime,
765
771
  logLevel,
@@ -771,9 +777,11 @@ var audioIteratorManager = ({
771
777
  return {
772
778
  startAudioIterator,
773
779
  getAudioBufferIterator: () => audioBufferIterator,
780
+ getCurrentAnchor: () => currentAnchor,
774
781
  destroyIterator: () => {
775
782
  audioBufferIterator?.destroy();
776
783
  audioBufferIterator = null;
784
+ currentAnchor = null;
777
785
  unblockCurrentDelayHandle();
778
786
  },
779
787
  seek,
@@ -1817,6 +1825,7 @@ class MediaPlayer {
1817
1825
  nonce,
1818
1826
  playbackRate: this.playbackRate * this.globalPlaybackRate,
1819
1827
  startFromSecond: startTime,
1828
+ unloopedStartFromSecond: startTimeUnresolved,
1820
1829
  scheduleAudioNode: this.scheduleAudioNode,
1821
1830
  getTargetTime: this.getTargetTime,
1822
1831
  logLevel: this.logLevel,
@@ -1848,10 +1857,10 @@ class MediaPlayer {
1848
1857
  __callDispose(__stack, _err, _hasErr);
1849
1858
  }
1850
1859
  }
1851
- seekToWithQueue = async (newTime) => {
1860
+ seekToWithQueue = async (newTime, unloopedNewTime) => {
1852
1861
  const nonce = this.nonceManager.createAsyncOperation();
1853
1862
  await this.seekPromiseChain;
1854
- this.seekPromiseChain = this.seekToDoNotCallDirectly(newTime, nonce);
1863
+ this.seekPromiseChain = this.seekToDoNotCallDirectly(newTime, unloopedNewTime, nonce);
1855
1864
  await this.seekPromiseChain;
1856
1865
  };
1857
1866
  async seekTo(time) {
@@ -1859,9 +1868,9 @@ class MediaPlayer {
1859
1868
  if (newTime === null) {
1860
1869
  throw new Error(`should have asserted that the time is not null`);
1861
1870
  }
1862
- await this.seekToWithQueue(newTime);
1871
+ await this.seekToWithQueue(newTime, time);
1863
1872
  }
1864
- async seekToDoNotCallDirectly(newTime, nonce) {
1873
+ async seekToDoNotCallDirectly(newTime, unloopedNewTime, nonce) {
1865
1874
  if (nonce.isStale()) {
1866
1875
  return;
1867
1876
  }
@@ -1873,8 +1882,10 @@ class MediaPlayer {
1873
1882
  }),
1874
1883
  this.audioIteratorManager?.seek({
1875
1884
  newTime,
1885
+ unloopedNewTime,
1876
1886
  nonce,
1877
1887
  playbackRate: this.playbackRate * this.globalPlaybackRate,
1888
+ localPlaybackRate: this.playbackRate,
1878
1889
  getTargetTime: this.getTargetTime,
1879
1890
  logLevel: this.logLevel,
1880
1891
  loop: this.loop,
@@ -2020,14 +2031,19 @@ class MediaPlayer {
2020
2031
  }
2021
2032
  const globalTime = (currentTime - this.sharedAudioContext.audioSyncAnchor.value) * this.globalPlaybackRate;
2022
2033
  const timeInSeconds = globalTime - this.sequenceOffset;
2023
- const localTime = this.getTrimmedTime(timeInSeconds);
2034
+ const anchor = this.loop ? this.audioIteratorManager?.getCurrentAnchor() ?? null : null;
2035
+ const localTime = anchor ? anchorToContinuousTime({
2036
+ anchor,
2037
+ unloopedTimeInSeconds: timeInSeconds,
2038
+ playbackRate: this.playbackRate
2039
+ }) : this.getTrimmedTime(timeInSeconds);
2024
2040
  if (localTime === null) {
2025
2041
  return null;
2026
2042
  }
2027
2043
  const targetTime = (mediaTimestamp - localTime) / (this.playbackRate * this.globalPlaybackRate);
2028
2044
  return targetTime;
2029
2045
  };
2030
- scheduleAudioNode = (node, mediaTimestamp, originalUnloopedMediaTimestamp) => {
2046
+ scheduleAudioNode = (node, mediaTimestamp, originalUnloopedMediaTimestamp, sourceOffsetInSeconds, sourceDurationInSeconds) => {
2031
2047
  if (!this.sharedAudioContext) {
2032
2048
  throw new Error("Shared audio context not found");
2033
2049
  }
@@ -2040,18 +2056,17 @@ class MediaPlayer {
2040
2056
  };
2041
2057
  }
2042
2058
  const sequenceStartTime = this.getStartTime();
2043
- const loopSegmentMediaEndTimestamp = this.getLoopSegmentMediaEndTimestamp();
2044
2059
  const offset = getTrimStartForAudioNode({
2045
2060
  mediaTimestamp,
2046
2061
  targetTime,
2047
2062
  sequenceStartTime,
2048
- combinedPlaybackRate
2063
+ combinedPlaybackRate,
2064
+ sourceStartOffsetInSeconds: sourceOffsetInSeconds
2049
2065
  });
2050
2066
  const duration = getDurationOfNode({
2051
- bufferDuration: node.buffer?.duration ?? 0,
2052
- loopSegmentMediaEndTimestamp,
2053
- offset,
2054
- originalUnloopedMediaTimestamp
2067
+ sourceDurationInSeconds,
2068
+ sourceOffsetInSeconds,
2069
+ offset
2055
2070
  });
2056
2071
  const scheduledTime = getScheduledTime({
2057
2072
  mediaTimestamp,
@@ -2062,6 +2077,7 @@ class MediaPlayer {
2062
2077
  return this.sharedAudioContext.scheduleAudioNode({
2063
2078
  node,
2064
2079
  mediaTimestamp,
2080
+ sourceOffset: sourceOffsetInSeconds,
2065
2081
  scheduledTime,
2066
2082
  duration,
2067
2083
  offset,
@@ -1,13 +1,21 @@
1
1
  import type { AudioBufferSink, WrappedAudioBuffer } from 'mediabunny';
2
- export type BufferWithMediaTimestamp = {
2
+ export type AudioBufferSlice = {
3
3
  buffer: WrappedAudioBuffer;
4
- timestamp: number;
4
+ timelineTimestamp: number;
5
+ sourceOffsetInSeconds: number;
6
+ sourceDurationInSeconds: number;
5
7
  };
6
- export declare const makeIteratorWithPriming: ({ audioSink, timeToSeek, maximumTimestamp, loop, playbackRate, sequenceDurationInSeconds, }: {
8
+ export declare function makeIteratorWithPriming({ audioSink, timeToSeek, maximumTimestamp }: {
7
9
  audioSink: AudioBufferSink;
8
10
  timeToSeek: number;
9
11
  maximumTimestamp: number;
10
- loop: boolean;
11
- playbackRate: number;
12
- sequenceDurationInSeconds: number;
13
- }) => AsyncGenerator<BufferWithMediaTimestamp, void, unknown>;
12
+ }): AsyncGenerator<AudioBufferSlice, void, unknown>;
13
+ type MakeLoopingIteratorOptions = {
14
+ audioSink: AudioBufferSink;
15
+ seekTimeInSeconds: number;
16
+ loopStartInSeconds: number;
17
+ segmentEndInSeconds: number;
18
+ maximumContinuousTimestamp: number;
19
+ };
20
+ export declare function makeLoopingIterator({ audioSink, seekTimeInSeconds, loopStartInSeconds, segmentEndInSeconds, maximumContinuousTimestamp }: MakeLoopingIteratorOptions): AsyncGenerator<AudioBufferSlice, void, unknown>;
21
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/media",
3
- "version": "4.0.487",
3
+ "version": "4.0.489",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/esm/index.mjs",
@@ -22,8 +22,8 @@
22
22
  "make": "tsgo && bun --env-file=../.env.bundle bundle.ts"
23
23
  },
24
24
  "dependencies": {
25
- "mediabunny": "1.50.7",
26
- "remotion": "4.0.487",
25
+ "mediabunny": "1.50.8",
26
+ "remotion": "4.0.489",
27
27
  "zod": "4.3.6"
28
28
  },
29
29
  "peerDependencies": {
@@ -31,8 +31,8 @@
31
31
  "react-dom": ">=16.8.0"
32
32
  },
33
33
  "devDependencies": {
34
- "@remotion/eslint-config-internal": "4.0.487",
35
- "@remotion/player": "4.0.487",
34
+ "@remotion/eslint-config-internal": "4.0.489",
35
+ "@remotion/player": "4.0.489",
36
36
  "@vitest/browser-webdriverio": "4.0.9",
37
37
  "eslint": "9.19.0",
38
38
  "react": "19.2.3",