@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.
- package/dist/audio/allow-wait.js +15 -0
- package/dist/audio/audio-for-preview.d.ts +0 -1
- package/dist/audio/audio-for-preview.js +304 -0
- package/dist/audio/audio-for-rendering.js +194 -0
- package/dist/audio/audio-preview-iterator.d.ts +4 -2
- package/dist/audio/audio-preview-iterator.js +176 -0
- package/dist/audio/audio.js +20 -0
- package/dist/audio/props.js +1 -0
- package/dist/audio-extraction/audio-cache.js +66 -0
- package/dist/audio-extraction/audio-iterator.js +132 -0
- package/dist/audio-extraction/audio-manager.js +113 -0
- package/dist/audio-extraction/extract-audio.js +132 -0
- package/dist/audio-iterator-manager.d.ts +10 -9
- package/dist/audio-iterator-manager.js +228 -0
- package/dist/browser-can-use-webgl2.js +13 -0
- package/dist/caches.js +61 -0
- package/dist/calculate-playbacktime.js +4 -0
- package/dist/convert-audiodata/apply-volume.js +17 -0
- package/dist/convert-audiodata/combine-audiodata.js +23 -0
- package/dist/convert-audiodata/convert-audiodata.js +73 -0
- package/dist/convert-audiodata/resample-audiodata.js +94 -0
- package/dist/debug-overlay/preview-overlay.d.ts +9 -7
- package/dist/debug-overlay/preview-overlay.js +42 -0
- package/dist/esm/index.mjs +384 -13811
- package/dist/extract-frame-and-audio.js +101 -0
- package/dist/get-sink.js +15 -0
- package/dist/get-time-in-seconds.js +40 -0
- package/dist/helpers/round-to-4-digits.js +4 -0
- package/dist/index.js +12 -0
- package/dist/is-type-of-error.js +20 -0
- package/dist/looped-frame.js +10 -0
- package/dist/media-player.d.ts +9 -5
- package/dist/media-player.js +431 -0
- package/dist/nonce-manager.js +13 -0
- package/dist/prewarm-iterator-for-looping.js +56 -0
- package/dist/render-timestamp-range.js +9 -0
- package/dist/show-in-timeline.js +31 -0
- package/dist/use-media-in-timeline.d.ts +1 -1
- package/dist/use-media-in-timeline.js +103 -0
- package/dist/video/props.js +1 -0
- package/dist/video/video-for-preview.js +331 -0
- package/dist/video/video-for-rendering.js +263 -0
- package/dist/video/video-preview-iterator.js +122 -0
- package/dist/video/video.js +35 -0
- package/dist/video-extraction/add-broadcast-channel-listener.js +125 -0
- package/dist/video-extraction/extract-frame-via-broadcast-channel.js +113 -0
- package/dist/video-extraction/extract-frame.js +85 -0
- package/dist/video-extraction/get-allocation-size.js +6 -0
- package/dist/video-extraction/get-frames-since-keyframe.js +108 -0
- package/dist/video-extraction/keyframe-bank.js +159 -0
- package/dist/video-extraction/keyframe-manager.js +206 -0
- package/dist/video-extraction/remember-actual-matroska-timestamps.js +19 -0
- package/dist/video-extraction/rotate-frame.js +34 -0
- package/dist/video-iterator-manager.js +109 -0
- 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,
|
|
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,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: {
|