@remotion/media 4.0.364 → 4.0.365
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/audio-for-preview.js +8 -21
- package/dist/audio/audio-preview-iterator.d.ts +24 -7
- package/dist/audio/audio-preview-iterator.js +143 -18
- package/dist/debug-overlay/preview-overlay.d.ts +15 -1
- package/dist/debug-overlay/preview-overlay.js +32 -8
- package/dist/esm/index.mjs +322 -249
- package/dist/media-player.d.ts +3 -11
- package/dist/media-player.js +137 -158
- package/dist/video/video-for-preview.js +8 -29
- package/package.json +4 -4
|
@@ -59,6 +59,11 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
59
59
|
trimAfter,
|
|
60
60
|
trimBefore,
|
|
61
61
|
});
|
|
62
|
+
const buffering = useContext(Internals.BufferingContextReact);
|
|
63
|
+
if (!buffering) {
|
|
64
|
+
throw new Error('useMediaPlayback must be used inside a <BufferingContext>');
|
|
65
|
+
}
|
|
66
|
+
const isPlayerBuffering = Internals.useIsPlayerBuffering(buffering);
|
|
62
67
|
useEffect(() => {
|
|
63
68
|
if (!sharedAudioContext)
|
|
64
69
|
return;
|
|
@@ -160,15 +165,13 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
160
165
|
const audioPlayer = mediaPlayerRef.current;
|
|
161
166
|
if (!audioPlayer)
|
|
162
167
|
return;
|
|
163
|
-
if (playing) {
|
|
164
|
-
audioPlayer.play(
|
|
165
|
-
Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[NewAudioForPreview] Failed to play', error);
|
|
166
|
-
});
|
|
168
|
+
if (playing && !isPlayerBuffering) {
|
|
169
|
+
audioPlayer.play(currentTimeRef.current);
|
|
167
170
|
}
|
|
168
171
|
else {
|
|
169
172
|
audioPlayer.pause();
|
|
170
173
|
}
|
|
171
|
-
}, [
|
|
174
|
+
}, [isPlayerBuffering, logLevel, playing]);
|
|
172
175
|
useEffect(() => {
|
|
173
176
|
const audioPlayer = mediaPlayerRef.current;
|
|
174
177
|
if (!audioPlayer || !mediaPlayerReady)
|
|
@@ -176,22 +179,6 @@ const NewAudioForPreview = ({ src, playbackRate, logLevel, muted, volume, loopVo
|
|
|
176
179
|
audioPlayer.seekTo(currentTime);
|
|
177
180
|
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[NewAudioForPreview] Updating target time to ${currentTime.toFixed(3)}s`);
|
|
178
181
|
}, [currentTime, logLevel, mediaPlayerReady]);
|
|
179
|
-
useEffect(() => {
|
|
180
|
-
const audioPlayer = mediaPlayerRef.current;
|
|
181
|
-
if (!audioPlayer || !mediaPlayerReady)
|
|
182
|
-
return;
|
|
183
|
-
audioPlayer.onBufferingChange((newBufferingState) => {
|
|
184
|
-
if (newBufferingState && !delayHandleRef.current) {
|
|
185
|
-
delayHandleRef.current = buffer.delayPlayback();
|
|
186
|
-
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[NewAudioForPreview] MediaPlayer buffering - blocking Remotion playback');
|
|
187
|
-
}
|
|
188
|
-
else if (!newBufferingState && delayHandleRef.current) {
|
|
189
|
-
delayHandleRef.current.unblock();
|
|
190
|
-
delayHandleRef.current = null;
|
|
191
|
-
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[NewAudioForPreview] MediaPlayer unbuffering - unblocking Remotion playback');
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
}, [mediaPlayerReady, buffer, logLevel]);
|
|
195
182
|
const effectiveMuted = muted || mediaMuted || userPreferredVolume <= 0;
|
|
196
183
|
useEffect(() => {
|
|
197
184
|
const audioPlayer = mediaPlayerRef.current;
|
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
import type { AudioBufferSink } from 'mediabunny';
|
|
1
|
+
import type { AudioBufferSink, WrappedAudioBuffer } from 'mediabunny';
|
|
2
2
|
export declare const HEALTHY_BUFFER_THRESHOLD_SECONDS = 1;
|
|
3
|
+
export type QueuedNode = {
|
|
4
|
+
node: AudioBufferSourceNode;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
buffer: AudioBuffer;
|
|
7
|
+
};
|
|
3
8
|
export declare const makeAudioIterator: (audioSink: AudioBufferSink, startFromSecond: number) => {
|
|
4
|
-
cleanupAudioQueue: () => void;
|
|
5
9
|
destroy: () => void;
|
|
6
|
-
|
|
7
|
-
setAudioIteratorStarted: (started: boolean) => void;
|
|
8
|
-
getNext: () => Promise<IteratorResult<import("mediabunny").WrappedAudioBuffer, void>>;
|
|
9
|
-
setAudioBufferHealth: (health: number) => void;
|
|
10
|
+
getNext: () => Promise<IteratorResult<WrappedAudioBuffer, void>>;
|
|
10
11
|
isDestroyed: () => boolean;
|
|
11
|
-
addQueuedAudioNode: (node: AudioBufferSourceNode) => void;
|
|
12
|
+
addQueuedAudioNode: (node: AudioBufferSourceNode, timestamp: number, buffer: AudioBuffer) => void;
|
|
12
13
|
removeQueuedAudioNode: (node: AudioBufferSourceNode) => void;
|
|
14
|
+
removeAndReturnAllQueuedAudioNodes: () => QueuedNode[];
|
|
15
|
+
getQueuedPeriod: () => {
|
|
16
|
+
from: number;
|
|
17
|
+
until: number;
|
|
18
|
+
} | null;
|
|
19
|
+
tryToSatisfySeek: (time: number) => Promise<{
|
|
20
|
+
type: "not-satisfied";
|
|
21
|
+
reason: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: "satisfied";
|
|
24
|
+
buffers: WrappedAudioBuffer[];
|
|
25
|
+
}>;
|
|
13
26
|
};
|
|
14
27
|
export type AudioIterator = ReturnType<typeof makeAudioIterator>;
|
|
28
|
+
export declare const isAlreadyQueued: (time: number, queuedPeriod: {
|
|
29
|
+
from: number;
|
|
30
|
+
until: number;
|
|
31
|
+
} | undefined | null) => boolean;
|
|
@@ -1,43 +1,168 @@
|
|
|
1
|
+
import { roundTo4Digits } from '../helpers/round-to-4-digits';
|
|
1
2
|
export const HEALTHY_BUFFER_THRESHOLD_SECONDS = 1;
|
|
2
3
|
export const makeAudioIterator = (audioSink, startFromSecond) => {
|
|
3
4
|
let destroyed = false;
|
|
4
5
|
const iterator = audioSink.buffers(startFromSecond);
|
|
5
|
-
|
|
6
|
-
let audioBufferHealth = 0;
|
|
7
|
-
const queuedAudioNodes = new Set();
|
|
6
|
+
const queuedAudioNodes = [];
|
|
8
7
|
const cleanupAudioQueue = () => {
|
|
9
8
|
for (const node of queuedAudioNodes) {
|
|
10
|
-
node.stop();
|
|
9
|
+
node.node.stop();
|
|
10
|
+
}
|
|
11
|
+
queuedAudioNodes.length = 0;
|
|
12
|
+
};
|
|
13
|
+
let lastReturnedBuffer = null;
|
|
14
|
+
let iteratorEnded = false;
|
|
15
|
+
const getNextOrNullIfNotAvailable = async () => {
|
|
16
|
+
const next = iterator.next();
|
|
17
|
+
const result = await Promise.race([
|
|
18
|
+
next,
|
|
19
|
+
new Promise((resolve) => {
|
|
20
|
+
Promise.resolve().then(() => resolve());
|
|
21
|
+
}),
|
|
22
|
+
]);
|
|
23
|
+
if (!result) {
|
|
24
|
+
return {
|
|
25
|
+
type: 'need-to-wait-for-it',
|
|
26
|
+
waitPromise: async () => {
|
|
27
|
+
const res = await next;
|
|
28
|
+
if (res.value) {
|
|
29
|
+
lastReturnedBuffer = res.value;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
iteratorEnded = true;
|
|
33
|
+
}
|
|
34
|
+
return res.value;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (result.value) {
|
|
39
|
+
lastReturnedBuffer = result.value;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
iteratorEnded = true;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
type: 'got-buffer-or-end',
|
|
46
|
+
buffer: result.value ?? null,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
const tryToSatisfySeek = async (time) => {
|
|
50
|
+
if (lastReturnedBuffer) {
|
|
51
|
+
const bufferTimestamp = roundTo4Digits(lastReturnedBuffer.timestamp);
|
|
52
|
+
const bufferEndTimestamp = roundTo4Digits(lastReturnedBuffer.timestamp + lastReturnedBuffer.duration);
|
|
53
|
+
if (roundTo4Digits(time) < bufferTimestamp) {
|
|
54
|
+
return {
|
|
55
|
+
type: 'not-satisfied',
|
|
56
|
+
reason: `iterator is too far, most recently returned ${bufferTimestamp}-${bufferEndTimestamp}, requested ${time}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (roundTo4Digits(time) <= bufferEndTimestamp) {
|
|
60
|
+
return {
|
|
61
|
+
type: 'satisfied',
|
|
62
|
+
buffers: [lastReturnedBuffer],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// fall through
|
|
66
|
+
}
|
|
67
|
+
if (iteratorEnded) {
|
|
68
|
+
if (lastReturnedBuffer) {
|
|
69
|
+
return {
|
|
70
|
+
type: 'satisfied',
|
|
71
|
+
buffers: [lastReturnedBuffer],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
type: 'not-satisfied',
|
|
76
|
+
reason: 'iterator ended',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const toBeReturned = [];
|
|
80
|
+
while (true) {
|
|
81
|
+
const buffer = await getNextOrNullIfNotAvailable();
|
|
82
|
+
if (buffer.type === 'need-to-wait-for-it') {
|
|
83
|
+
return {
|
|
84
|
+
type: 'not-satisfied',
|
|
85
|
+
reason: 'iterator did not have buffer ready',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (buffer.type === 'got-buffer-or-end') {
|
|
89
|
+
if (buffer.buffer === null) {
|
|
90
|
+
iteratorEnded = true;
|
|
91
|
+
if (lastReturnedBuffer) {
|
|
92
|
+
return {
|
|
93
|
+
type: 'satisfied',
|
|
94
|
+
buffers: [lastReturnedBuffer],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
type: 'not-satisfied',
|
|
99
|
+
reason: 'iterator ended and did not have buffer ready',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const bufferTimestamp = roundTo4Digits(buffer.buffer.timestamp);
|
|
103
|
+
const bufferEndTimestamp = roundTo4Digits(buffer.buffer.timestamp + buffer.buffer.duration);
|
|
104
|
+
const timestamp = roundTo4Digits(time);
|
|
105
|
+
if (bufferTimestamp <= timestamp && bufferEndTimestamp > timestamp) {
|
|
106
|
+
return {
|
|
107
|
+
type: 'satisfied',
|
|
108
|
+
buffers: [...toBeReturned, buffer.buffer],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
toBeReturned.push(buffer.buffer);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
throw new Error('Unreachable');
|
|
11
115
|
}
|
|
12
|
-
queuedAudioNodes.clear();
|
|
13
116
|
};
|
|
14
117
|
return {
|
|
15
|
-
cleanupAudioQueue,
|
|
16
118
|
destroy: () => {
|
|
17
119
|
cleanupAudioQueue();
|
|
18
120
|
destroyed = true;
|
|
19
121
|
iterator.return().catch(() => undefined);
|
|
20
122
|
},
|
|
21
|
-
isReadyToPlay: () => {
|
|
22
|
-
return audioIteratorStarted && audioBufferHealth > 0;
|
|
23
|
-
},
|
|
24
|
-
setAudioIteratorStarted: (started) => {
|
|
25
|
-
audioIteratorStarted = started;
|
|
26
|
-
},
|
|
27
123
|
getNext: () => {
|
|
28
124
|
return iterator.next();
|
|
29
125
|
},
|
|
30
|
-
setAudioBufferHealth: (health) => {
|
|
31
|
-
audioBufferHealth = health;
|
|
32
|
-
},
|
|
33
126
|
isDestroyed: () => {
|
|
34
127
|
return destroyed;
|
|
35
128
|
},
|
|
36
|
-
addQueuedAudioNode: (node) => {
|
|
37
|
-
queuedAudioNodes.
|
|
129
|
+
addQueuedAudioNode: (node, timestamp, buffer) => {
|
|
130
|
+
queuedAudioNodes.push({ node, timestamp, buffer });
|
|
38
131
|
},
|
|
39
132
|
removeQueuedAudioNode: (node) => {
|
|
40
|
-
queuedAudioNodes.
|
|
133
|
+
const index = queuedAudioNodes.findIndex((n) => n.node === node);
|
|
134
|
+
if (index !== -1) {
|
|
135
|
+
queuedAudioNodes.splice(index, 1);
|
|
136
|
+
}
|
|
41
137
|
},
|
|
138
|
+
removeAndReturnAllQueuedAudioNodes: () => {
|
|
139
|
+
const nodes = queuedAudioNodes.slice();
|
|
140
|
+
for (const node of nodes) {
|
|
141
|
+
node.node.stop();
|
|
142
|
+
}
|
|
143
|
+
queuedAudioNodes.length = 0;
|
|
144
|
+
return nodes;
|
|
145
|
+
},
|
|
146
|
+
getQueuedPeriod: () => {
|
|
147
|
+
const lastNode = queuedAudioNodes[queuedAudioNodes.length - 1];
|
|
148
|
+
if (!lastNode) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const firstNode = queuedAudioNodes[0];
|
|
152
|
+
if (!firstNode) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
from: firstNode.timestamp,
|
|
157
|
+
until: lastNode.timestamp + lastNode.buffer.duration,
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
tryToSatisfySeek,
|
|
42
161
|
};
|
|
43
162
|
};
|
|
163
|
+
export const isAlreadyQueued = (time, queuedPeriod) => {
|
|
164
|
+
if (!queuedPeriod) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
return time >= queuedPeriod.from && time < queuedPeriod.until;
|
|
168
|
+
};
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import type { AudioIterator } from '../audio/audio-preview-iterator';
|
|
1
2
|
export type DebugStats = {
|
|
2
3
|
videoIteratorsCreated: number;
|
|
4
|
+
audioIteratorsCreated: number;
|
|
3
5
|
framesRendered: number;
|
|
4
6
|
};
|
|
5
|
-
export declare const drawPreviewOverlay: (context
|
|
7
|
+
export declare const drawPreviewOverlay: ({ context, stats, audioTime, audioContextState, audioIterator, audioSyncAnchor, audioChunksForAfterResuming, playing, }: {
|
|
8
|
+
context: CanvasRenderingContext2D;
|
|
9
|
+
stats: DebugStats;
|
|
10
|
+
audioTime: number;
|
|
11
|
+
audioContextState: AudioContextState;
|
|
12
|
+
audioSyncAnchor: number;
|
|
13
|
+
audioIterator: AudioIterator | null;
|
|
14
|
+
audioChunksForAfterResuming: {
|
|
15
|
+
buffer: AudioBuffer;
|
|
16
|
+
timestamp: number;
|
|
17
|
+
}[];
|
|
18
|
+
playing: boolean;
|
|
19
|
+
}) => void;
|
|
@@ -1,13 +1,37 @@
|
|
|
1
|
-
export const drawPreviewOverlay = (context, stats, audioContextState, audioSyncAnchor) => {
|
|
2
|
-
//
|
|
1
|
+
export const drawPreviewOverlay = ({ context, stats, audioTime, audioContextState, audioIterator, audioSyncAnchor, audioChunksForAfterResuming, playing, }) => {
|
|
2
|
+
// Collect all lines to be rendered
|
|
3
|
+
const lines = [
|
|
4
|
+
'Debug overlay',
|
|
5
|
+
`Video iterators created: ${stats.videoIteratorsCreated}`,
|
|
6
|
+
`Audio iterators created: ${stats.audioIteratorsCreated}`,
|
|
7
|
+
`Frames rendered: ${stats.framesRendered}`,
|
|
8
|
+
`Audio context state: ${audioContextState}`,
|
|
9
|
+
`Audio time: ${(audioTime - audioSyncAnchor).toFixed(3)}s`,
|
|
10
|
+
];
|
|
11
|
+
if (audioIterator) {
|
|
12
|
+
const queuedPeriod = audioIterator.getQueuedPeriod();
|
|
13
|
+
if (queuedPeriod) {
|
|
14
|
+
lines.push(`Audio queued until: ${(queuedPeriod.until - (audioTime - audioSyncAnchor)).toFixed(3)}s`);
|
|
15
|
+
}
|
|
16
|
+
else if (audioChunksForAfterResuming.length > 0) {
|
|
17
|
+
lines.push(`Audio chunks for after resuming: ${audioChunksForAfterResuming.length}`);
|
|
18
|
+
}
|
|
19
|
+
lines.push(`Playing: ${playing}`);
|
|
20
|
+
}
|
|
21
|
+
const lineHeight = 30; // px, should match or exceed font size
|
|
22
|
+
const boxPaddingX = 10;
|
|
23
|
+
const boxPaddingY = 10;
|
|
24
|
+
const boxLeft = 20;
|
|
25
|
+
const boxTop = 20;
|
|
26
|
+
const boxWidth = 600;
|
|
27
|
+
const boxHeight = lines.length * lineHeight + 2 * boxPaddingY;
|
|
28
|
+
// Draw background for text legibility
|
|
3
29
|
context.fillStyle = 'rgba(0, 0, 0, 1)';
|
|
4
|
-
context.fillRect(
|
|
30
|
+
context.fillRect(boxLeft, boxTop, boxWidth, boxHeight);
|
|
5
31
|
context.fillStyle = 'white';
|
|
6
32
|
context.font = '24px sans-serif';
|
|
7
33
|
context.textBaseline = 'top';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
context.fillText(`Audio context state: ${audioContextState}`, 30, 120);
|
|
12
|
-
context.fillText(`Audio time: ${audioSyncAnchor.toFixed(3)}s`, 30, 150);
|
|
34
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35
|
+
context.fillText(lines[i], boxLeft + boxPaddingX, boxTop + boxPaddingY + i * lineHeight);
|
|
36
|
+
}
|
|
13
37
|
};
|