@remotion/media 4.0.429 → 4.0.430

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/audio/allow-wait.js +15 -0
  2. package/dist/audio/audio-for-preview.d.ts +0 -1
  3. package/dist/audio/audio-for-preview.js +304 -0
  4. package/dist/audio/audio-for-rendering.js +194 -0
  5. package/dist/audio/audio-preview-iterator.d.ts +4 -2
  6. package/dist/audio/audio-preview-iterator.js +176 -0
  7. package/dist/audio/audio.js +20 -0
  8. package/dist/audio/props.js +1 -0
  9. package/dist/audio-extraction/audio-cache.js +66 -0
  10. package/dist/audio-extraction/audio-iterator.js +132 -0
  11. package/dist/audio-extraction/audio-manager.js +113 -0
  12. package/dist/audio-extraction/extract-audio.js +132 -0
  13. package/dist/audio-iterator-manager.d.ts +10 -9
  14. package/dist/audio-iterator-manager.js +228 -0
  15. package/dist/browser-can-use-webgl2.js +13 -0
  16. package/dist/caches.js +61 -0
  17. package/dist/calculate-playbacktime.js +4 -0
  18. package/dist/convert-audiodata/apply-volume.js +17 -0
  19. package/dist/convert-audiodata/combine-audiodata.js +23 -0
  20. package/dist/convert-audiodata/convert-audiodata.js +73 -0
  21. package/dist/convert-audiodata/resample-audiodata.js +94 -0
  22. package/dist/debug-overlay/preview-overlay.d.ts +9 -7
  23. package/dist/debug-overlay/preview-overlay.js +42 -0
  24. package/dist/esm/index.mjs +384 -13811
  25. package/dist/extract-frame-and-audio.js +101 -0
  26. package/dist/get-sink.js +15 -0
  27. package/dist/get-time-in-seconds.js +40 -0
  28. package/dist/helpers/round-to-4-digits.js +4 -0
  29. package/dist/index.js +12 -0
  30. package/dist/is-type-of-error.js +20 -0
  31. package/dist/looped-frame.js +10 -0
  32. package/dist/media-player.d.ts +9 -5
  33. package/dist/media-player.js +431 -0
  34. package/dist/nonce-manager.js +13 -0
  35. package/dist/prewarm-iterator-for-looping.js +56 -0
  36. package/dist/render-timestamp-range.js +9 -0
  37. package/dist/show-in-timeline.js +31 -0
  38. package/dist/use-media-in-timeline.d.ts +1 -1
  39. package/dist/use-media-in-timeline.js +103 -0
  40. package/dist/video/props.js +1 -0
  41. package/dist/video/video-for-preview.js +331 -0
  42. package/dist/video/video-for-rendering.js +263 -0
  43. package/dist/video/video-preview-iterator.js +122 -0
  44. package/dist/video/video.js +35 -0
  45. package/dist/video-extraction/add-broadcast-channel-listener.js +125 -0
  46. package/dist/video-extraction/extract-frame-via-broadcast-channel.js +113 -0
  47. package/dist/video-extraction/extract-frame.js +85 -0
  48. package/dist/video-extraction/get-allocation-size.js +6 -0
  49. package/dist/video-extraction/get-frames-since-keyframe.js +108 -0
  50. package/dist/video-extraction/keyframe-bank.js +159 -0
  51. package/dist/video-extraction/keyframe-manager.js +206 -0
  52. package/dist/video-extraction/remember-actual-matroska-timestamps.js +19 -0
  53. package/dist/video-extraction/rotate-frame.js +34 -0
  54. package/dist/video-iterator-manager.js +109 -0
  55. package/package.json +3 -2
@@ -1,7 +1,7 @@
1
1
  import type { InputAudioTrack, WrappedAudioBuffer } from 'mediabunny';
2
2
  import type { DelayPlaybackIfNotPremounting } from './delay-playback-if-not-premounting';
3
3
  import type { Nonce } from './nonce-manager';
4
- export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfNotPremounting, sharedAudioContext, getIsLooping, getEndTime, getStartTime, updatePlaybackTime, initialMuted, drawDebugOverlay, }: {
4
+ export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfNotPremounting, sharedAudioContext, getIsLooping, getEndTime, getStartTime, initialMuted, drawDebugOverlay, }: {
5
5
  audioTrack: InputAudioTrack;
6
6
  delayPlaybackHandleIfNotPremounting: () => DelayPlaybackIfNotPremounting;
7
7
  sharedAudioContext: AudioContext;
@@ -9,7 +9,6 @@ export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfN
9
9
  getEndTime: () => number;
10
10
  getStartTime: () => number;
11
11
  initialMuted: boolean;
12
- updatePlaybackTime: (time: number) => void;
13
12
  drawDebugOverlay: () => void;
14
13
  }) => {
15
14
  startAudioIterator: ({ nonce, playbackRate, startFromSecond, getIsPlaying, scheduleAudioNode, }: {
@@ -17,22 +16,23 @@ export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfN
17
16
  nonce: Nonce;
18
17
  playbackRate: number;
19
18
  getIsPlaying: () => boolean;
20
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
19
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, maxDuration: number | null) => void;
21
20
  }) => Promise<void>;
22
21
  resumeScheduledAudioChunks: ({ playbackRate, scheduleAudioNode, }: {
23
22
  playbackRate: number;
24
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
23
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, maxDuration: number | null) => void;
25
24
  }) => void;
26
25
  pausePlayback: () => void;
27
26
  getAudioBufferIterator: () => {
28
27
  destroy: () => void;
29
28
  getNext: () => Promise<IteratorResult<WrappedAudioBuffer, void>>;
30
29
  isDestroyed: () => boolean;
31
- addQueuedAudioNode: (node: AudioBufferSourceNode, timestamp: number, buffer: AudioBuffer) => void;
30
+ addQueuedAudioNode: (node: AudioBufferSourceNode, timestamp: number, buffer: AudioBuffer, maxDuration: number | null) => void;
32
31
  removeQueuedAudioNode: (node: AudioBufferSourceNode) => void;
33
32
  getAndClearAudioChunksForAfterResuming: () => {
34
33
  buffer: AudioBuffer;
35
34
  timestamp: number;
35
+ maxDuration: number | null;
36
36
  }[];
37
37
  getQueuedPeriod: () => {
38
38
  from: number;
@@ -53,7 +53,7 @@ export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfN
53
53
  } | {
54
54
  type: "max-reached";
55
55
  }>;
56
- addChunkForAfterResuming: (buffer: AudioBuffer, timestamp: number) => void;
56
+ addChunkForAfterResuming: (buffer: AudioBuffer, timestamp: number, maxDuration: number | null) => void;
57
57
  moveQueuedChunksToPauseQueue: () => void;
58
58
  getNumberOfChunksAfterResuming: () => number;
59
59
  } | null;
@@ -63,16 +63,17 @@ export declare const audioIteratorManager: ({ audioTrack, delayPlaybackHandleIfN
63
63
  nonce: Nonce;
64
64
  playbackRate: number;
65
65
  getIsPlaying: () => boolean;
66
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
66
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, maxDuration: number | null) => void;
67
67
  }) => Promise<void>;
68
68
  getAudioIteratorsCreated: () => number;
69
69
  setMuted: (newMuted: boolean) => void;
70
70
  setVolume: (volume: number) => void;
71
- scheduleAudioChunk: ({ buffer, mediaTimestamp, playbackRate, scheduleAudioNode, }: {
71
+ scheduleAudioChunk: ({ buffer, mediaTimestamp, playbackRate, scheduleAudioNode, maxDuration, }: {
72
72
  buffer: AudioBuffer;
73
73
  mediaTimestamp: number;
74
74
  playbackRate: number;
75
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
75
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, maxDuration: number | null) => void;
76
+ maxDuration: number | null;
76
77
  }) => void;
77
78
  };
78
79
  export type AudioIteratorManager = ReturnType<typeof audioIteratorManager>;
@@ -0,0 +1,228 @@
1
+ import { AudioBufferSink, InputDisposedError } from 'mediabunny';
2
+ import { isAlreadyQueued, makeAudioIterator, } from './audio/audio-preview-iterator';
3
+ import { makePrewarmedAudioIteratorCache } from './prewarm-iterator-for-looping';
4
+ export const audioIteratorManager = ({ audioTrack, delayPlaybackHandleIfNotPremounting, sharedAudioContext, getIsLooping, getEndTime, getStartTime, updatePlaybackTime, }) => {
5
+ let muted = false;
6
+ let currentVolume = 1;
7
+ const gainNode = sharedAudioContext.createGain();
8
+ gainNode.connect(sharedAudioContext.destination);
9
+ const audioSink = new AudioBufferSink(audioTrack);
10
+ const prewarmedAudioIteratorCache = makePrewarmedAudioIteratorCache(audioSink);
11
+ let audioBufferIterator = null;
12
+ let audioIteratorsCreated = 0;
13
+ let currentDelayHandle = null;
14
+ const scheduleAudioChunk = ({ buffer, mediaTimestamp, playbackRate, scheduleAudioNode, }) => {
15
+ if (!audioBufferIterator) {
16
+ throw new Error('Audio buffer iterator not found');
17
+ }
18
+ const node = sharedAudioContext.createBufferSource();
19
+ node.buffer = buffer;
20
+ node.playbackRate.value = playbackRate;
21
+ node.connect(gainNode);
22
+ scheduleAudioNode(node, mediaTimestamp);
23
+ const iterator = audioBufferIterator;
24
+ iterator.addQueuedAudioNode(node, mediaTimestamp, buffer);
25
+ node.onended = () => {
26
+ // Some leniancy is needed as we find that sometimes onended is fired a bit too early
27
+ setTimeout(() => {
28
+ iterator.removeQueuedAudioNode(node);
29
+ }, 30);
30
+ };
31
+ };
32
+ const onAudioChunk = ({ getIsPlaying, buffer, playbackRate, scheduleAudioNode, }) => {
33
+ if (getIsPlaying()) {
34
+ scheduleAudioChunk({
35
+ buffer: buffer.buffer,
36
+ mediaTimestamp: buffer.timestamp,
37
+ playbackRate,
38
+ scheduleAudioNode,
39
+ });
40
+ }
41
+ else {
42
+ if (!audioBufferIterator) {
43
+ throw new Error('Audio buffer iterator not found');
44
+ }
45
+ audioBufferIterator.addChunkForAfterResuming(buffer.buffer, buffer.timestamp);
46
+ }
47
+ };
48
+ const startAudioIterator = async ({ nonce, playbackRate, startFromSecond, getIsPlaying, scheduleAudioNode, }) => {
49
+ updatePlaybackTime(startFromSecond);
50
+ audioBufferIterator?.destroy();
51
+ const delayHandle = delayPlaybackHandleIfNotPremounting();
52
+ currentDelayHandle = delayHandle;
53
+ const iterator = makeAudioIterator(startFromSecond, prewarmedAudioIteratorCache);
54
+ audioIteratorsCreated++;
55
+ audioBufferIterator = iterator;
56
+ try {
57
+ // Schedule up to 3 buffers ahead of the current time
58
+ for (let i = 0; i < 3; i++) {
59
+ const result = await iterator.getNext();
60
+ if (iterator.isDestroyed()) {
61
+ return;
62
+ }
63
+ if (nonce.isStale()) {
64
+ return;
65
+ }
66
+ if (!result.value) {
67
+ // media ended
68
+ return;
69
+ }
70
+ onAudioChunk({
71
+ getIsPlaying,
72
+ buffer: result.value,
73
+ playbackRate,
74
+ scheduleAudioNode,
75
+ });
76
+ }
77
+ }
78
+ catch (e) {
79
+ if (e instanceof InputDisposedError) {
80
+ // iterator was disposed by a newer startAudioIterator call
81
+ // this is expected during rapid seeking
82
+ return;
83
+ }
84
+ throw e;
85
+ }
86
+ finally {
87
+ delayHandle.unblock();
88
+ currentDelayHandle = null;
89
+ }
90
+ };
91
+ const pausePlayback = () => {
92
+ if (!audioBufferIterator) {
93
+ return;
94
+ }
95
+ audioBufferIterator.moveQueuedChunksToPauseQueue();
96
+ };
97
+ const seek = async ({ newTime, nonce, fps, playbackRate, getIsPlaying, scheduleAudioNode, bufferState, }) => {
98
+ if (getIsLooping()) {
99
+ // If less than 1 second from the end away, we pre-warm a new iterator
100
+ if (getEndTime() - newTime < 1) {
101
+ prewarmedAudioIteratorCache.prewarmIteratorForLooping({
102
+ timeToSeek: getStartTime(),
103
+ });
104
+ }
105
+ }
106
+ if (!audioBufferIterator) {
107
+ await startAudioIterator({
108
+ nonce,
109
+ playbackRate,
110
+ startFromSecond: newTime,
111
+ getIsPlaying,
112
+ scheduleAudioNode,
113
+ });
114
+ return;
115
+ }
116
+ const queuedPeriod = audioBufferIterator.getQueuedPeriod();
117
+ const currentTimeIsAlreadyQueued = isAlreadyQueued(newTime, queuedPeriod);
118
+ if (!currentTimeIsAlreadyQueued) {
119
+ const audioSatisfyResult = await audioBufferIterator.tryToSatisfySeek(newTime, null, (buffer) => {
120
+ if (!nonce.isStale()) {
121
+ onAudioChunk({
122
+ getIsPlaying,
123
+ buffer,
124
+ playbackRate,
125
+ scheduleAudioNode,
126
+ });
127
+ }
128
+ });
129
+ if (nonce.isStale()) {
130
+ return;
131
+ }
132
+ if (audioSatisfyResult.type === 'ended') {
133
+ return;
134
+ }
135
+ if (audioSatisfyResult.type === 'not-satisfied') {
136
+ await startAudioIterator({
137
+ nonce,
138
+ playbackRate,
139
+ startFromSecond: newTime,
140
+ getIsPlaying,
141
+ scheduleAudioNode,
142
+ });
143
+ return;
144
+ }
145
+ }
146
+ const nextTime = newTime +
147
+ // 3 frames ahead to get enough of a buffer
148
+ (1 / fps) * Math.max(1, playbackRate) * 3;
149
+ const nextIsAlreadyQueued = isAlreadyQueued(nextTime, audioBufferIterator.getQueuedPeriod());
150
+ if (!nextIsAlreadyQueued) {
151
+ // here we allow waiting for the next buffer to be loaded
152
+ // it's better than to create a new iterator
153
+ // because we already know we are in the right spot
154
+ const audioSatisfyResult = await audioBufferIterator.tryToSatisfySeek(nextTime, {
155
+ type: 'allow-wait',
156
+ waitCallback: () => {
157
+ const handle = bufferState.delayPlayback();
158
+ return () => {
159
+ handle.unblock();
160
+ };
161
+ },
162
+ }, (buffer) => {
163
+ if (!nonce.isStale()) {
164
+ onAudioChunk({
165
+ getIsPlaying,
166
+ buffer,
167
+ playbackRate,
168
+ scheduleAudioNode,
169
+ });
170
+ }
171
+ });
172
+ if (nonce.isStale()) {
173
+ return;
174
+ }
175
+ if (audioSatisfyResult.type === 'ended') {
176
+ return;
177
+ }
178
+ if (audioSatisfyResult.type === 'not-satisfied') {
179
+ await startAudioIterator({
180
+ nonce,
181
+ playbackRate,
182
+ startFromSecond: newTime,
183
+ getIsPlaying,
184
+ scheduleAudioNode,
185
+ });
186
+ }
187
+ }
188
+ };
189
+ const resumeScheduledAudioChunks = ({ playbackRate, scheduleAudioNode, }) => {
190
+ if (!audioBufferIterator) {
191
+ return;
192
+ }
193
+ for (const chunk of audioBufferIterator.getAndClearAudioChunksForAfterResuming()) {
194
+ scheduleAudioChunk({
195
+ buffer: chunk.buffer,
196
+ mediaTimestamp: chunk.timestamp,
197
+ playbackRate,
198
+ scheduleAudioNode,
199
+ });
200
+ }
201
+ };
202
+ return {
203
+ startAudioIterator,
204
+ resumeScheduledAudioChunks,
205
+ pausePlayback,
206
+ getAudioBufferIterator: () => audioBufferIterator,
207
+ destroyIterator: () => {
208
+ prewarmedAudioIteratorCache.destroy();
209
+ audioBufferIterator?.destroy();
210
+ audioBufferIterator = null;
211
+ if (currentDelayHandle) {
212
+ currentDelayHandle.unblock();
213
+ currentDelayHandle = null;
214
+ }
215
+ },
216
+ seek,
217
+ getAudioIteratorsCreated: () => audioIteratorsCreated,
218
+ setMuted: (newMuted) => {
219
+ muted = newMuted;
220
+ gainNode.gain.value = muted ? 0 : currentVolume;
221
+ },
222
+ setVolume: (volume) => {
223
+ currentVolume = Math.max(0, volume);
224
+ gainNode.gain.value = muted ? 0 : currentVolume;
225
+ },
226
+ scheduleAudioChunk,
227
+ };
228
+ };
@@ -0,0 +1,13 @@
1
+ let browserCanUseWebGl2 = null;
2
+ const browserCanUseWebGl2Uncached = () => {
3
+ const canvas = new OffscreenCanvas(1, 1);
4
+ const context = canvas.getContext('webgl2');
5
+ return context !== null;
6
+ };
7
+ export const canBrowserUseWebGl2 = () => {
8
+ if (browserCanUseWebGl2 !== null) {
9
+ return browserCanUseWebGl2;
10
+ }
11
+ browserCanUseWebGl2 = browserCanUseWebGl2Uncached();
12
+ return browserCanUseWebGl2;
13
+ };
package/dist/caches.js ADDED
@@ -0,0 +1,61 @@
1
+ import React from 'react';
2
+ import { cancelRender, Internals } from 'remotion';
3
+ import { makeAudioManager } from './audio-extraction/audio-manager';
4
+ import { makeKeyframeManager } from './video-extraction/keyframe-manager';
5
+ // TODO: make it dependent on the fps and concurrency
6
+ export const SAFE_BACK_WINDOW_IN_SECONDS = 1;
7
+ export const keyframeManager = makeKeyframeManager();
8
+ export const audioManager = makeAudioManager();
9
+ export const getTotalCacheStats = async () => {
10
+ const keyframeManagerCacheStats = await keyframeManager.getCacheStats();
11
+ const audioManagerCacheStats = audioManager.getCacheStats();
12
+ return {
13
+ count: keyframeManagerCacheStats.count + audioManagerCacheStats.count,
14
+ totalSize: keyframeManagerCacheStats.totalSize + audioManagerCacheStats.totalSize,
15
+ };
16
+ };
17
+ const getUncachedMaxCacheSize = (logLevel) => {
18
+ if (typeof window !== 'undefined' &&
19
+ window.remotion_mediaCacheSizeInBytes !== undefined &&
20
+ window.remotion_mediaCacheSizeInBytes !== null) {
21
+ if (window.remotion_mediaCacheSizeInBytes < 240 * 1024 * 1024) {
22
+ cancelRender(new Error(`The minimum value for the "mediaCacheSizeInBytes" prop is 240MB (${240 * 1024 * 1024}), got: ${window.remotion_mediaCacheSizeInBytes}`));
23
+ }
24
+ if (window.remotion_mediaCacheSizeInBytes > 20000 * 1024 * 1024) {
25
+ cancelRender(new Error(`The maximum value for the "mediaCacheSizeInBytes" prop is 20GB (${20000 * 1024 * 1024}), got: ${window.remotion_mediaCacheSizeInBytes}`));
26
+ }
27
+ Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Using cache size set using "mediaCacheSizeInBytes": ${(window.remotion_mediaCacheSizeInBytes / 1024 / 1024).toFixed(1)} MB`);
28
+ return window.remotion_mediaCacheSizeInBytes;
29
+ }
30
+ if (typeof window !== 'undefined' &&
31
+ window.remotion_initialMemoryAvailable !== undefined &&
32
+ window.remotion_initialMemoryAvailable !== null) {
33
+ const value = window.remotion_initialMemoryAvailable / 2;
34
+ if (value < 500 * 1024 * 1024) {
35
+ Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Using cache size set based on minimum value of 500MB (which is more than half of the available system memory!)`);
36
+ return 500 * 1024 * 1024;
37
+ }
38
+ if (value > 20000 * 1024 * 1024) {
39
+ Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Using cache size set based on maximum value of 20GB (which is less than half of the available system memory)`);
40
+ return 20000 * 1024 * 1024;
41
+ }
42
+ Internals.Log.verbose({ logLevel, tag: '@remotion/media' }, `Using cache size set based on available memory (50% of available memory): ${(value / 1024 / 1024).toFixed(1)} MB`);
43
+ return value;
44
+ }
45
+ return 1000 * 1000 * 1000; // 1GB
46
+ };
47
+ let cachedMaxCacheSize = null;
48
+ export const getMaxVideoCacheSize = (logLevel) => {
49
+ if (cachedMaxCacheSize !== null) {
50
+ return cachedMaxCacheSize;
51
+ }
52
+ cachedMaxCacheSize = getUncachedMaxCacheSize(logLevel);
53
+ return cachedMaxCacheSize;
54
+ };
55
+ export const useMaxMediaCacheSize = (logLevel) => {
56
+ const context = React.useContext(Internals.MaxMediaCacheSizeContext);
57
+ if (context === null) {
58
+ return getMaxVideoCacheSize(logLevel);
59
+ }
60
+ return context;
61
+ };
@@ -0,0 +1,4 @@
1
+ export const calculatePlaybackTime = ({ audioSyncAnchor, currentTime, playbackRate, }) => {
2
+ const timeSinceAnchor = currentTime - audioSyncAnchor;
3
+ return timeSinceAnchor * playbackRate;
4
+ };
@@ -0,0 +1,17 @@
1
+ export const applyVolume = (array, volume) => {
2
+ if (volume === 1) {
3
+ return;
4
+ }
5
+ for (let i = 0; i < array.length; i++) {
6
+ const newValue = array[i] * volume;
7
+ if (newValue < -32768) {
8
+ array[i] = -32768;
9
+ }
10
+ else if (newValue > 32767) {
11
+ array[i] = 32767;
12
+ }
13
+ else {
14
+ array[i] = newValue;
15
+ }
16
+ }
17
+ };
@@ -0,0 +1,23 @@
1
+ import { fixFloatingPoint } from './convert-audiodata';
2
+ import { TARGET_NUMBER_OF_CHANNELS } from './resample-audiodata';
3
+ export const combineAudioDataAndClosePrevious = (audioDataArray) => {
4
+ let numberOfFrames = 0;
5
+ let durationInMicroSeconds = 0;
6
+ const { timestamp } = audioDataArray[0];
7
+ for (const audioData of audioDataArray) {
8
+ numberOfFrames += audioData.numberOfFrames;
9
+ durationInMicroSeconds += audioData.durationInMicroSeconds;
10
+ }
11
+ const arr = new Int16Array(numberOfFrames * TARGET_NUMBER_OF_CHANNELS);
12
+ let offset = 0;
13
+ for (const audioData of audioDataArray) {
14
+ arr.set(audioData.data, offset);
15
+ offset += audioData.data.length;
16
+ }
17
+ return {
18
+ data: arr,
19
+ numberOfFrames,
20
+ timestamp: fixFloatingPoint(timestamp),
21
+ durationInMicroSeconds: fixFloatingPoint(durationInMicroSeconds),
22
+ };
23
+ };
@@ -0,0 +1,73 @@
1
+ import { resampleAudioData, TARGET_NUMBER_OF_CHANNELS, TARGET_SAMPLE_RATE, } from './resample-audiodata';
2
+ const FORMAT = 's16';
3
+ export const fixFloatingPoint = (value) => {
4
+ const decimal = Math.abs(value % 1);
5
+ if (decimal < 0.0000001) {
6
+ return value < 0 ? Math.ceil(value) : Math.floor(value);
7
+ }
8
+ if (decimal > 0.9999999) {
9
+ return value < 0 ? Math.floor(value) : Math.ceil(value);
10
+ }
11
+ return value;
12
+ };
13
+ const ceilButNotIfFloatingPointIssue = (value) => {
14
+ const fixed = fixFloatingPoint(value);
15
+ return Math.ceil(fixed);
16
+ };
17
+ export const convertAudioData = ({ audioData, trimStartInSeconds, trimEndInSeconds, playbackRate, audioDataTimestamp, isLast, }) => {
18
+ const { numberOfChannels: srcNumberOfChannels, sampleRate: currentSampleRate, numberOfFrames, } = audioData;
19
+ const ratio = currentSampleRate / TARGET_SAMPLE_RATE;
20
+ // Always rounding down start timestamps and rounding up end durations
21
+ // to ensure there are no gaps when the samples don't align
22
+ // In @remotion/renderer inline audio mixing, we also round down the sample start
23
+ // timestamp and round up the end timestamp
24
+ // This might lead to overlapping, hopefully aligning perfectly!
25
+ // Test case: https://github.com/remotion-dev/remotion/issues/5758
26
+ const frameOffset = Math.floor(fixFloatingPoint(trimStartInSeconds * audioData.sampleRate));
27
+ const unroundedFrameCount = numberOfFrames - trimEndInSeconds * audioData.sampleRate - frameOffset;
28
+ const frameCount = isLast
29
+ ? ceilButNotIfFloatingPointIssue(unroundedFrameCount)
30
+ : Math.round(unroundedFrameCount);
31
+ const newNumberOfFrames = isLast
32
+ ? ceilButNotIfFloatingPointIssue(unroundedFrameCount / ratio / playbackRate)
33
+ : Math.round(unroundedFrameCount / ratio / playbackRate);
34
+ if (newNumberOfFrames === 0) {
35
+ throw new Error('Cannot resample - the given sample rate would result in less than 1 sample');
36
+ }
37
+ const srcChannels = new Int16Array(srcNumberOfChannels * frameCount);
38
+ audioData.copyTo(srcChannels, {
39
+ planeIndex: 0,
40
+ format: FORMAT,
41
+ frameOffset,
42
+ frameCount,
43
+ });
44
+ const data = new Int16Array(newNumberOfFrames * TARGET_NUMBER_OF_CHANNELS);
45
+ const chunkSize = frameCount / newNumberOfFrames;
46
+ const timestampOffsetMicroseconds = (frameOffset / audioData.sampleRate) * 1000000;
47
+ if (newNumberOfFrames === frameCount &&
48
+ TARGET_NUMBER_OF_CHANNELS === srcNumberOfChannels &&
49
+ playbackRate === 1) {
50
+ return {
51
+ data: srcChannels,
52
+ numberOfFrames: newNumberOfFrames,
53
+ timestamp: audioDataTimestamp * 1000000 +
54
+ fixFloatingPoint(timestampOffsetMicroseconds),
55
+ durationInMicroSeconds: fixFloatingPoint((newNumberOfFrames / TARGET_SAMPLE_RATE) * 1000000),
56
+ };
57
+ }
58
+ resampleAudioData({
59
+ srcNumberOfChannels,
60
+ sourceChannels: srcChannels,
61
+ destination: data,
62
+ targetFrames: newNumberOfFrames,
63
+ chunkSize,
64
+ });
65
+ const newAudioData = {
66
+ data,
67
+ numberOfFrames: newNumberOfFrames,
68
+ timestamp: audioDataTimestamp * 1000000 +
69
+ fixFloatingPoint(timestampOffsetMicroseconds),
70
+ durationInMicroSeconds: fixFloatingPoint((newNumberOfFrames / TARGET_SAMPLE_RATE) * 1000000),
71
+ };
72
+ return newAudioData;
73
+ };
@@ -0,0 +1,94 @@
1
+ // Remotion exports all videos with 2 channels.
2
+ export const TARGET_NUMBER_OF_CHANNELS = 2;
3
+ // Remotion exports all videos with 48kHz sample rate.
4
+ export const TARGET_SAMPLE_RATE = 48000;
5
+ const fixFloatingPoint = (value) => {
6
+ if (value % 1 < 0.0000001) {
7
+ return Math.floor(value);
8
+ }
9
+ if (value % 1 > 0.9999999) {
10
+ return Math.ceil(value);
11
+ }
12
+ return value;
13
+ };
14
+ export const resampleAudioData = ({ srcNumberOfChannels, sourceChannels, destination, targetFrames, chunkSize, }) => {
15
+ const getSourceValues = (startUnfixed, endUnfixed, channelIndex) => {
16
+ const start = fixFloatingPoint(startUnfixed);
17
+ const end = fixFloatingPoint(endUnfixed);
18
+ const startFloor = Math.floor(start);
19
+ const startCeil = Math.ceil(start);
20
+ const startFraction = start - startFloor;
21
+ const endFraction = end - Math.floor(end);
22
+ const endFloor = Math.floor(end);
23
+ let weightedSum = 0;
24
+ let totalWeight = 0;
25
+ // Handle first fractional sample
26
+ if (startFraction > 0) {
27
+ const firstSample = sourceChannels[startFloor * srcNumberOfChannels + channelIndex];
28
+ weightedSum += firstSample * (1 - startFraction);
29
+ totalWeight += 1 - startFraction;
30
+ }
31
+ // Handle full samples
32
+ for (let k = startCeil; k < endFloor; k++) {
33
+ const num = sourceChannels[k * srcNumberOfChannels + channelIndex];
34
+ weightedSum += num;
35
+ totalWeight += 1;
36
+ }
37
+ // Handle last fractional sample
38
+ if (endFraction > 0) {
39
+ const lastSample = sourceChannels[endFloor * srcNumberOfChannels + channelIndex];
40
+ weightedSum += lastSample * endFraction;
41
+ totalWeight += endFraction;
42
+ }
43
+ const average = weightedSum / totalWeight;
44
+ return average;
45
+ };
46
+ for (let newFrameIndex = 0; newFrameIndex < targetFrames; newFrameIndex++) {
47
+ const start = newFrameIndex * chunkSize;
48
+ const end = start + chunkSize;
49
+ if (TARGET_NUMBER_OF_CHANNELS === srcNumberOfChannels) {
50
+ for (let i = 0; i < srcNumberOfChannels; i++) {
51
+ destination[newFrameIndex * srcNumberOfChannels + i] = getSourceValues(start, end, i);
52
+ }
53
+ }
54
+ // The following formulas were taken from Mediabunnys audio resampler:
55
+ // https://github.com/Vanilagy/mediabunny/blob/b9f7ab2fa2b9167784cbded044d466185308999f/src/conversion.ts
56
+ // Mono to Stereo: M -> L, M -> R
57
+ if (srcNumberOfChannels === 1) {
58
+ const m = getSourceValues(start, end, 0);
59
+ destination[newFrameIndex * 2 + 0] = m;
60
+ destination[newFrameIndex * 2 + 1] = m;
61
+ }
62
+ // Quad to Stereo: 0.5 * (L + SL), 0.5 * (R + SR)
63
+ else if (srcNumberOfChannels === 4) {
64
+ const l = getSourceValues(start, end, 0);
65
+ const r = getSourceValues(start, end, 1);
66
+ const sl = getSourceValues(start, end, 2);
67
+ const sr = getSourceValues(start, end, 3);
68
+ const l2 = 0.5 * (l + sl);
69
+ const r2 = 0.5 * (r + sr);
70
+ destination[newFrameIndex * 2 + 0] = l2;
71
+ destination[newFrameIndex * 2 + 1] = r2;
72
+ }
73
+ // 5.1 to Stereo: L + sqrt(1/2) * (C + SL), R + sqrt(1/2) * (C + SR)
74
+ else if (srcNumberOfChannels === 6) {
75
+ const l = getSourceValues(start, end, 0);
76
+ const r = getSourceValues(start, end, 1);
77
+ const c = getSourceValues(start, end, 2);
78
+ const sl = getSourceValues(start, end, 3);
79
+ const sr = getSourceValues(start, end, 4);
80
+ const sq = Math.sqrt(1 / 2);
81
+ const l2 = l + sq * (c + sl);
82
+ const r2 = r + sq * (c + sr);
83
+ destination[newFrameIndex * 2 + 0] = l2;
84
+ destination[newFrameIndex * 2 + 1] = r2;
85
+ }
86
+ // Discrete fallback: direct mapping with zero-fill or drop
87
+ else {
88
+ for (let i = 0; i < srcNumberOfChannels; i++) {
89
+ destination[newFrameIndex * TARGET_NUMBER_OF_CHANNELS + i] =
90
+ getSourceValues(start, end, i);
91
+ }
92
+ }
93
+ }
94
+ };
@@ -10,22 +10,23 @@ export declare const drawPreviewOverlay: ({ context, audioTime, audioContextStat
10
10
  nonce: import("../nonce-manager").Nonce;
11
11
  playbackRate: number;
12
12
  getIsPlaying: () => boolean;
13
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
13
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, maxDuration: number | null) => void;
14
14
  }) => Promise<void>;
15
15
  resumeScheduledAudioChunks: ({ playbackRate, scheduleAudioNode, }: {
16
16
  playbackRate: number;
17
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
17
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, maxDuration: number | null) => void;
18
18
  }) => void;
19
19
  pausePlayback: () => void;
20
20
  getAudioBufferIterator: () => {
21
21
  destroy: () => void;
22
22
  getNext: () => Promise<IteratorResult<import("mediabunny").WrappedAudioBuffer, void>>;
23
23
  isDestroyed: () => boolean;
24
- addQueuedAudioNode: (node: AudioBufferSourceNode, timestamp: number, buffer: AudioBuffer) => void;
24
+ addQueuedAudioNode: (node: AudioBufferSourceNode, timestamp: number, buffer: AudioBuffer, maxDuration: number | null) => void;
25
25
  removeQueuedAudioNode: (node: AudioBufferSourceNode) => void;
26
26
  getAndClearAudioChunksForAfterResuming: () => {
27
27
  buffer: AudioBuffer;
28
28
  timestamp: number;
29
+ maxDuration: number | null;
29
30
  }[];
30
31
  getQueuedPeriod: () => {
31
32
  from: number;
@@ -46,7 +47,7 @@ export declare const drawPreviewOverlay: ({ context, audioTime, audioContextStat
46
47
  } | {
47
48
  type: "max-reached";
48
49
  }>;
49
- addChunkForAfterResuming: (buffer: AudioBuffer, timestamp: number) => void;
50
+ addChunkForAfterResuming: (buffer: AudioBuffer, timestamp: number, maxDuration: number | null) => void;
50
51
  moveQueuedChunksToPauseQueue: () => void;
51
52
  getNumberOfChunksAfterResuming: () => number;
52
53
  } | null;
@@ -56,16 +57,17 @@ export declare const drawPreviewOverlay: ({ context, audioTime, audioContextStat
56
57
  nonce: import("../nonce-manager").Nonce;
57
58
  playbackRate: number;
58
59
  getIsPlaying: () => boolean;
59
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
60
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, maxDuration: number | null) => void;
60
61
  }) => Promise<void>;
61
62
  getAudioIteratorsCreated: () => number;
62
63
  setMuted: (newMuted: boolean) => void;
63
64
  setVolume: (volume: number) => void;
64
- scheduleAudioChunk: ({ buffer, mediaTimestamp, playbackRate, scheduleAudioNode, }: {
65
+ scheduleAudioChunk: ({ buffer, mediaTimestamp, playbackRate, scheduleAudioNode, maxDuration, }: {
65
66
  buffer: AudioBuffer;
66
67
  mediaTimestamp: number;
67
68
  playbackRate: number;
68
- scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number) => void;
69
+ scheduleAudioNode: (node: AudioBufferSourceNode, mediaTimestamp: number, maxDuration: number | null) => void;
70
+ maxDuration: number | null;
69
71
  }) => void;
70
72
  } | null;
71
73
  videoIteratorManager: {