@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
package/dist/media-player.d.ts
CHANGED
|
@@ -37,9 +37,6 @@ export declare class MediaPlayer {
|
|
|
37
37
|
private trimAfter;
|
|
38
38
|
private initialized;
|
|
39
39
|
private totalDuration;
|
|
40
|
-
private isBuffering;
|
|
41
|
-
private onBufferingChangeCallback?;
|
|
42
|
-
private mediaEnded;
|
|
43
40
|
private debugOverlay;
|
|
44
41
|
private onVideoFrameCallback?;
|
|
45
42
|
private initializationPromise;
|
|
@@ -61,7 +58,6 @@ export declare class MediaPlayer {
|
|
|
61
58
|
private input;
|
|
62
59
|
private isReady;
|
|
63
60
|
private hasAudio;
|
|
64
|
-
private isCurrentlyBuffering;
|
|
65
61
|
private isDisposalError;
|
|
66
62
|
initialize(startTimeUnresolved: number): Promise<MediaPlayerInitResult>;
|
|
67
63
|
private _initialize;
|
|
@@ -70,7 +66,7 @@ export declare class MediaPlayer {
|
|
|
70
66
|
private seekPromiseChain;
|
|
71
67
|
seekTo(time: number): Promise<void>;
|
|
72
68
|
seekToDoNotCallDirectly(time: number, nonce: number): Promise<void>;
|
|
73
|
-
play(): Promise<void>;
|
|
69
|
+
play(time: number): Promise<void>;
|
|
74
70
|
pause(): void;
|
|
75
71
|
setMuted(muted: boolean): void;
|
|
76
72
|
setVolume(volume: number): void;
|
|
@@ -80,16 +76,12 @@ export declare class MediaPlayer {
|
|
|
80
76
|
setLoop(loop: boolean): void;
|
|
81
77
|
dispose(): Promise<void>;
|
|
82
78
|
private getPlaybackTime;
|
|
79
|
+
private setPlaybackTime;
|
|
80
|
+
private audioChunksForAfterResuming;
|
|
83
81
|
private scheduleAudioChunk;
|
|
84
|
-
onBufferingChange(callback: (isBuffering: boolean) => void): () => void;
|
|
85
82
|
onVideoFrame(callback: (frame: CanvasImageSource) => void): () => void;
|
|
86
83
|
private drawFrame;
|
|
87
84
|
private startAudioIterator;
|
|
88
85
|
private drawDebugOverlay;
|
|
89
86
|
private startVideoIterator;
|
|
90
|
-
private bufferingStartedAtMs;
|
|
91
|
-
private minBufferingTimeoutMs;
|
|
92
|
-
private setBufferingState;
|
|
93
|
-
private maybeResumeFromBuffering;
|
|
94
|
-
private runAudioIterator;
|
|
95
87
|
}
|
package/dist/media-player.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { ALL_FORMATS, AudioBufferSink, CanvasSink, Input, UrlSource, } from 'mediabunny';
|
|
2
2
|
import { Internals } from 'remotion';
|
|
3
|
-
import {
|
|
3
|
+
import { isAlreadyQueued, makeAudioIterator, } from './audio/audio-preview-iterator';
|
|
4
4
|
import { drawPreviewOverlay } from './debug-overlay/preview-overlay';
|
|
5
5
|
import { getTimeInSeconds } from './get-time-in-seconds';
|
|
6
6
|
import { isNetworkError } from './is-network-error';
|
|
7
|
-
import { sleep, TimeoutError, withTimeout } from './video/timeout-utils';
|
|
8
7
|
import { createVideoIterator, } from './video/video-preview-iterator';
|
|
9
|
-
const AUDIO_BUFFER_TOLERANCE_THRESHOLD = 0.1;
|
|
10
8
|
export class MediaPlayer {
|
|
11
9
|
constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, audioStreamIndex, fps, debugOverlay, bufferState, }) {
|
|
12
10
|
this.canvasSink = null;
|
|
13
11
|
this.videoFrameIterator = null;
|
|
14
12
|
this.debugStats = {
|
|
15
13
|
videoIteratorsCreated: 0,
|
|
14
|
+
audioIteratorsCreated: 0,
|
|
16
15
|
framesRendered: 0,
|
|
17
16
|
};
|
|
18
17
|
this.audioSink = null;
|
|
@@ -26,14 +25,12 @@ export class MediaPlayer {
|
|
|
26
25
|
this.muted = false;
|
|
27
26
|
this.loop = false;
|
|
28
27
|
this.initialized = false;
|
|
29
|
-
// for remotion buffer state
|
|
30
|
-
this.isBuffering = false;
|
|
31
|
-
this.mediaEnded = false;
|
|
32
28
|
this.debugOverlay = false;
|
|
33
29
|
this.initializationPromise = null;
|
|
34
30
|
this.input = null;
|
|
35
31
|
this.currentSeekNonce = 0;
|
|
36
32
|
this.seekPromiseChain = Promise.resolve();
|
|
33
|
+
this.audioChunksForAfterResuming = [];
|
|
37
34
|
this.drawFrame = (frame) => {
|
|
38
35
|
if (!this.context) {
|
|
39
36
|
throw new Error('Context not initialized');
|
|
@@ -47,22 +44,38 @@ export class MediaPlayer {
|
|
|
47
44
|
}
|
|
48
45
|
Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Drew frame ${frame.timestamp.toFixed(3)}s`);
|
|
49
46
|
};
|
|
50
|
-
this.startAudioIterator = (startFromSecond) => {
|
|
47
|
+
this.startAudioIterator = async (startFromSecond, nonce) => {
|
|
51
48
|
if (!this.hasAudio())
|
|
52
49
|
return;
|
|
53
|
-
// Clean up existing audio iterator
|
|
54
50
|
this.audioBufferIterator?.destroy();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
this.audioChunksForAfterResuming = [];
|
|
52
|
+
const delayHandle = this.bufferState.delayPlayback();
|
|
53
|
+
const iterator = makeAudioIterator(this.audioSink, startFromSecond);
|
|
54
|
+
this.debugStats.audioIteratorsCreated++;
|
|
55
|
+
this.audioBufferIterator = iterator;
|
|
56
|
+
// Schedule up to 3 buffers ahead of the current time
|
|
57
|
+
for (let i = 0; i < 3; i++) {
|
|
58
|
+
const result = await iterator.getNext();
|
|
59
|
+
if (iterator.isDestroyed()) {
|
|
60
|
+
delayHandle.unblock();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (nonce !== this.currentSeekNonce) {
|
|
64
|
+
delayHandle.unblock();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!result.value) {
|
|
68
|
+
// media ended
|
|
69
|
+
delayHandle.unblock();
|
|
62
70
|
return;
|
|
63
71
|
}
|
|
64
|
-
|
|
72
|
+
const { buffer, timestamp } = result.value;
|
|
73
|
+
this.audioChunksForAfterResuming.push({
|
|
74
|
+
buffer,
|
|
75
|
+
timestamp,
|
|
76
|
+
});
|
|
65
77
|
}
|
|
78
|
+
delayHandle.unblock();
|
|
66
79
|
};
|
|
67
80
|
this.startVideoIterator = async (timeToSeek, nonce) => {
|
|
68
81
|
if (!this.canvasSink) {
|
|
@@ -72,9 +85,9 @@ export class MediaPlayer {
|
|
|
72
85
|
const iterator = createVideoIterator(timeToSeek, this.canvasSink);
|
|
73
86
|
this.debugStats.videoIteratorsCreated++;
|
|
74
87
|
this.videoFrameIterator = iterator;
|
|
75
|
-
const delayHandle = this.bufferState
|
|
88
|
+
const delayHandle = this.bufferState.delayPlayback();
|
|
76
89
|
const frameResult = await iterator.getNext();
|
|
77
|
-
delayHandle
|
|
90
|
+
delayHandle.unblock();
|
|
78
91
|
if (iterator.isDestroyed()) {
|
|
79
92
|
return;
|
|
80
93
|
}
|
|
@@ -84,89 +97,11 @@ export class MediaPlayer {
|
|
|
84
97
|
if (this.videoFrameIterator.isDestroyed()) {
|
|
85
98
|
return;
|
|
86
99
|
}
|
|
87
|
-
if (frameResult.value) {
|
|
88
|
-
this.audioSyncAnchor =
|
|
89
|
-
this.sharedAudioContext.currentTime - frameResult.value.timestamp;
|
|
90
|
-
this.drawFrame(frameResult.value);
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
100
|
+
if (!frameResult.value) {
|
|
93
101
|
// media ended
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
this.bufferingStartedAtMs = null;
|
|
97
|
-
this.minBufferingTimeoutMs = 500;
|
|
98
|
-
this.runAudioIterator = async (startFromSecond, audioIterator) => {
|
|
99
|
-
if (!this.hasAudio())
|
|
100
102
|
return;
|
|
101
|
-
try {
|
|
102
|
-
let totalBufferDuration = 0;
|
|
103
|
-
let isFirstBuffer = true;
|
|
104
|
-
audioIterator.setAudioIteratorStarted(true);
|
|
105
|
-
while (true) {
|
|
106
|
-
if (audioIterator.isDestroyed()) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const BUFFERING_TIMEOUT_MS = 50;
|
|
110
|
-
let result;
|
|
111
|
-
try {
|
|
112
|
-
result = await withTimeout(audioIterator.getNext(), BUFFERING_TIMEOUT_MS, 'Iterator timeout');
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
if (error instanceof TimeoutError && !this.mediaEnded) {
|
|
116
|
-
this.setBufferingState(true);
|
|
117
|
-
}
|
|
118
|
-
await sleep(10);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
// media has ended
|
|
122
|
-
if (result.done || !result.value) {
|
|
123
|
-
this.mediaEnded = true;
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
const { buffer, timestamp, duration } = result.value;
|
|
127
|
-
totalBufferDuration += duration;
|
|
128
|
-
audioIterator.setAudioBufferHealth(Math.max(0, totalBufferDuration / this.playbackRate));
|
|
129
|
-
this.maybeResumeFromBuffering(totalBufferDuration / this.playbackRate);
|
|
130
|
-
if (this.playing) {
|
|
131
|
-
if (isFirstBuffer) {
|
|
132
|
-
this.audioSyncAnchor =
|
|
133
|
-
this.sharedAudioContext.currentTime - timestamp;
|
|
134
|
-
isFirstBuffer = false;
|
|
135
|
-
}
|
|
136
|
-
// if timestamp is less than timeToSeek, skip
|
|
137
|
-
// context: for some reason, mediabunny returns buffer at 9.984s, when requested at 10s
|
|
138
|
-
if (timestamp < startFromSecond - AUDIO_BUFFER_TOLERANCE_THRESHOLD) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
this.scheduleAudioChunk(buffer, timestamp);
|
|
142
|
-
}
|
|
143
|
-
const playbackTime = this.getPlaybackTime();
|
|
144
|
-
if (playbackTime === null) {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (timestamp - playbackTime >= 1) {
|
|
148
|
-
await new Promise((resolve) => {
|
|
149
|
-
const check = () => {
|
|
150
|
-
const currentPlaybackTime = this.getPlaybackTime();
|
|
151
|
-
if (currentPlaybackTime !== null &&
|
|
152
|
-
timestamp - currentPlaybackTime < 1) {
|
|
153
|
-
resolve();
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
requestAnimationFrame(check);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
check();
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
if (this.isDisposalError()) {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to run audio iterator', error);
|
|
169
103
|
}
|
|
104
|
+
this.drawFrame(frameResult.value);
|
|
170
105
|
};
|
|
171
106
|
this.canvas = canvas ?? null;
|
|
172
107
|
this.src = src;
|
|
@@ -202,9 +137,6 @@ export class MediaPlayer {
|
|
|
202
137
|
hasAudio() {
|
|
203
138
|
return Boolean(this.audioSink && this.sharedAudioContext && this.gainNode);
|
|
204
139
|
}
|
|
205
|
-
isCurrentlyBuffering() {
|
|
206
|
-
return this.isBuffering && Boolean(this.bufferingStartedAtMs);
|
|
207
|
-
}
|
|
208
140
|
isDisposalError() {
|
|
209
141
|
return this.input?.disposed === true;
|
|
210
142
|
}
|
|
@@ -282,11 +214,12 @@ export class MediaPlayer {
|
|
|
282
214
|
return { type: 'success', durationInSeconds: this.totalDuration };
|
|
283
215
|
}
|
|
284
216
|
if (this.sharedAudioContext) {
|
|
285
|
-
this.
|
|
217
|
+
this.setPlaybackTime(startTime);
|
|
286
218
|
}
|
|
287
219
|
this.initialized = true;
|
|
288
220
|
try {
|
|
289
|
-
|
|
221
|
+
// intentionally not awaited
|
|
222
|
+
this.startAudioIterator(startTime, this.currentSeekNonce);
|
|
290
223
|
await this.startVideoIterator(startTime, this.currentSeekNonce);
|
|
291
224
|
}
|
|
292
225
|
catch (error) {
|
|
@@ -349,32 +282,95 @@ export class MediaPlayer {
|
|
|
349
282
|
if (currentPlaybackTime === newTime) {
|
|
350
283
|
return;
|
|
351
284
|
}
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
285
|
+
const newAudioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
|
|
286
|
+
const diff = Math.abs(newAudioSyncAnchor - this.audioSyncAnchor);
|
|
287
|
+
if (diff > 0.1) {
|
|
288
|
+
this.setPlaybackTime(newTime);
|
|
356
289
|
}
|
|
357
|
-
|
|
358
|
-
|
|
290
|
+
// Should return immediately, so it's okay to not use Promise.all here
|
|
291
|
+
const videoSatisfyResult = await this.videoFrameIterator?.tryToSatisfySeek(newTime);
|
|
292
|
+
if (videoSatisfyResult?.type === 'satisfied') {
|
|
293
|
+
this.drawFrame(videoSatisfyResult.frame);
|
|
294
|
+
}
|
|
295
|
+
else if (videoSatisfyResult && this.currentSeekNonce === nonce) {
|
|
296
|
+
this.startVideoIterator(newTime, nonce);
|
|
297
|
+
}
|
|
298
|
+
const queuedPeriod = this.audioBufferIterator?.getQueuedPeriod();
|
|
299
|
+
const currentTimeIsAlreadyQueued = isAlreadyQueued(newTime, queuedPeriod);
|
|
300
|
+
const toBeScheduled = [];
|
|
301
|
+
if (!currentTimeIsAlreadyQueued) {
|
|
302
|
+
const audioSatisfyResult = await this.audioBufferIterator?.tryToSatisfySeek(newTime);
|
|
303
|
+
if (this.currentSeekNonce !== nonce) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!audioSatisfyResult) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (audioSatisfyResult.type === 'not-satisfied') {
|
|
310
|
+
await this.startAudioIterator(newTime, nonce);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
toBeScheduled.push(...audioSatisfyResult.buffers);
|
|
314
|
+
}
|
|
315
|
+
// TODO: What is this is beyond the end of the video
|
|
316
|
+
const nextTime = newTime +
|
|
317
|
+
// start of next frame
|
|
318
|
+
(1 / this.fps) * this.playbackRate +
|
|
319
|
+
// need the full duration of the next frame to be queued
|
|
320
|
+
(1 / this.fps) * this.playbackRate;
|
|
321
|
+
const nextIsAlreadyQueued = isAlreadyQueued(nextTime, queuedPeriod);
|
|
322
|
+
if (!nextIsAlreadyQueued) {
|
|
323
|
+
const audioSatisfyResult = await this.audioBufferIterator?.tryToSatisfySeek(nextTime);
|
|
324
|
+
if (this.currentSeekNonce !== nonce) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (!audioSatisfyResult) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (audioSatisfyResult.type === 'not-satisfied') {
|
|
331
|
+
await this.startAudioIterator(nextTime, nonce);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
toBeScheduled.push(...audioSatisfyResult.buffers);
|
|
335
|
+
}
|
|
336
|
+
for (const buffer of toBeScheduled) {
|
|
337
|
+
if (this.playing) {
|
|
338
|
+
this.scheduleAudioChunk(buffer.buffer, buffer.timestamp);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
this.audioChunksForAfterResuming.push({
|
|
342
|
+
buffer: buffer.buffer,
|
|
343
|
+
timestamp: buffer.timestamp,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
359
346
|
}
|
|
360
|
-
this.mediaEnded = false;
|
|
361
|
-
this.audioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
|
|
362
|
-
this.startAudioIterator(newTime);
|
|
363
|
-
this.startVideoIterator(newTime, nonce);
|
|
364
347
|
}
|
|
365
|
-
async play() {
|
|
348
|
+
async play(time) {
|
|
366
349
|
if (!this.isReady())
|
|
367
350
|
return;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
this.playing = true;
|
|
351
|
+
this.setPlaybackTime(time);
|
|
352
|
+
this.playing = true;
|
|
353
|
+
for (const chunk of this.audioChunksForAfterResuming) {
|
|
354
|
+
this.scheduleAudioChunk(chunk.buffer, chunk.timestamp);
|
|
373
355
|
}
|
|
356
|
+
if (this.sharedAudioContext.state === 'suspended') {
|
|
357
|
+
await this.sharedAudioContext.resume();
|
|
358
|
+
}
|
|
359
|
+
this.audioChunksForAfterResuming.length = 0;
|
|
360
|
+
this.drawDebugOverlay();
|
|
374
361
|
}
|
|
375
362
|
pause() {
|
|
376
363
|
this.playing = false;
|
|
377
|
-
this.audioBufferIterator?.
|
|
364
|
+
const toQueue = this.audioBufferIterator?.removeAndReturnAllQueuedAudioNodes();
|
|
365
|
+
if (toQueue) {
|
|
366
|
+
for (const chunk of toQueue) {
|
|
367
|
+
this.audioChunksForAfterResuming.push({
|
|
368
|
+
buffer: chunk.buffer,
|
|
369
|
+
timestamp: chunk.timestamp,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
this.drawDebugOverlay();
|
|
378
374
|
}
|
|
379
375
|
setMuted(muted) {
|
|
380
376
|
this.muted = muted;
|
|
@@ -426,28 +422,27 @@ export class MediaPlayer {
|
|
|
426
422
|
getPlaybackTime() {
|
|
427
423
|
return this.sharedAudioContext.currentTime - this.audioSyncAnchor;
|
|
428
424
|
}
|
|
425
|
+
setPlaybackTime(time) {
|
|
426
|
+
this.audioSyncAnchor = this.sharedAudioContext.currentTime - time;
|
|
427
|
+
}
|
|
429
428
|
scheduleAudioChunk(buffer, mediaTimestamp) {
|
|
430
|
-
|
|
431
|
-
|
|
429
|
+
// TODO: Might already be scheduled, and then the playback rate changes
|
|
430
|
+
// TODO: Playbackrate does not yet work
|
|
431
|
+
const targetTime = (mediaTimestamp - (this.trimBefore ?? 0) / this.fps) / this.playbackRate;
|
|
432
|
+
const delay = targetTime + this.audioSyncAnchor - this.sharedAudioContext.currentTime;
|
|
432
433
|
const node = this.sharedAudioContext.createBufferSource();
|
|
433
434
|
node.buffer = buffer;
|
|
434
435
|
node.playbackRate.value = this.playbackRate;
|
|
435
436
|
node.connect(this.gainNode);
|
|
436
437
|
if (delay >= 0) {
|
|
437
|
-
node.start(targetTime);
|
|
438
|
+
node.start(targetTime + this.audioSyncAnchor);
|
|
438
439
|
}
|
|
439
440
|
else {
|
|
440
441
|
node.start(this.sharedAudioContext.currentTime, -delay);
|
|
441
442
|
}
|
|
442
|
-
this.audioBufferIterator
|
|
443
|
-
node.onended = () =>
|
|
444
|
-
|
|
445
|
-
onBufferingChange(callback) {
|
|
446
|
-
this.onBufferingChangeCallback = callback;
|
|
447
|
-
return () => {
|
|
448
|
-
if (this.onBufferingChangeCallback === callback) {
|
|
449
|
-
this.onBufferingChangeCallback = undefined;
|
|
450
|
-
}
|
|
443
|
+
this.audioBufferIterator.addQueuedAudioNode(node, mediaTimestamp, buffer);
|
|
444
|
+
node.onended = () => {
|
|
445
|
+
return this.audioBufferIterator.removeQueuedAudioNode(node);
|
|
451
446
|
};
|
|
452
447
|
}
|
|
453
448
|
onVideoFrame(callback) {
|
|
@@ -465,32 +460,16 @@ export class MediaPlayer {
|
|
|
465
460
|
if (!this.debugOverlay)
|
|
466
461
|
return;
|
|
467
462
|
if (this.context && this.canvas) {
|
|
468
|
-
drawPreviewOverlay(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
this.
|
|
476
|
-
this.
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
this.bufferingStartedAtMs = null;
|
|
480
|
-
this.onBufferingChangeCallback?.(false);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
maybeResumeFromBuffering(currentBufferDuration) {
|
|
485
|
-
if (!this.isCurrentlyBuffering())
|
|
486
|
-
return;
|
|
487
|
-
const now = performance.now();
|
|
488
|
-
const bufferingDuration = now - this.bufferingStartedAtMs;
|
|
489
|
-
const minTimeElapsed = bufferingDuration >= this.minBufferingTimeoutMs;
|
|
490
|
-
const bufferHealthy = currentBufferDuration >= HEALTHY_BUFFER_THRESHOLD_SECONDS;
|
|
491
|
-
if (minTimeElapsed && bufferHealthy) {
|
|
492
|
-
Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Resuming from buffering after ${bufferingDuration}ms - buffer recovered`);
|
|
493
|
-
this.setBufferingState(false);
|
|
463
|
+
drawPreviewOverlay({
|
|
464
|
+
context: this.context,
|
|
465
|
+
stats: this.debugStats,
|
|
466
|
+
audioTime: this.sharedAudioContext.currentTime,
|
|
467
|
+
audioContextState: this.sharedAudioContext.state,
|
|
468
|
+
audioSyncAnchor: this.audioSyncAnchor,
|
|
469
|
+
audioIterator: this.audioBufferIterator,
|
|
470
|
+
audioChunksForAfterResuming: this.audioChunksForAfterResuming,
|
|
471
|
+
playing: this.playing,
|
|
472
|
+
});
|
|
494
473
|
}
|
|
495
474
|
}
|
|
496
475
|
}
|
|
@@ -60,6 +60,11 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
|
|
|
60
60
|
const currentTimeRef = useRef(currentTime);
|
|
61
61
|
currentTimeRef.current = currentTime;
|
|
62
62
|
const preloadedSrc = usePreload(src);
|
|
63
|
+
const buffering = useContext(Internals.BufferingContextReact);
|
|
64
|
+
if (!buffering) {
|
|
65
|
+
throw new Error('useMediaPlayback must be used inside a <BufferingContext>');
|
|
66
|
+
}
|
|
67
|
+
const isPlayerBuffering = Internals.useIsPlayerBuffering(buffering);
|
|
63
68
|
useEffect(() => {
|
|
64
69
|
if (!canvasRef.current)
|
|
65
70
|
return;
|
|
@@ -167,15 +172,13 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
|
|
|
167
172
|
const mediaPlayer = mediaPlayerRef.current;
|
|
168
173
|
if (!mediaPlayer)
|
|
169
174
|
return;
|
|
170
|
-
if (playing) {
|
|
171
|
-
mediaPlayer.play(
|
|
172
|
-
Internals.Log.error({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] Failed to play', error);
|
|
173
|
-
});
|
|
175
|
+
if (playing && !isPlayerBuffering) {
|
|
176
|
+
mediaPlayer.play(currentTimeRef.current);
|
|
174
177
|
}
|
|
175
178
|
else {
|
|
176
179
|
mediaPlayer.pause();
|
|
177
180
|
}
|
|
178
|
-
}, [playing, logLevel, mediaPlayerReady]);
|
|
181
|
+
}, [isPlayerBuffering, playing, logLevel, mediaPlayerReady]);
|
|
179
182
|
useLayoutEffect(() => {
|
|
180
183
|
const mediaPlayer = mediaPlayerRef.current;
|
|
181
184
|
if (!mediaPlayer || !mediaPlayerReady)
|
|
@@ -183,30 +186,6 @@ export const VideoForPreview = ({ src: unpreloadedSrc, style, playbackRate, logL
|
|
|
183
186
|
mediaPlayer.seekTo(currentTime);
|
|
184
187
|
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, `[VideoForPreview] Updating target time to ${currentTime.toFixed(3)}s`);
|
|
185
188
|
}, [currentTime, logLevel, mediaPlayerReady]);
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
const mediaPlayer = mediaPlayerRef.current;
|
|
188
|
-
if (!mediaPlayer || !mediaPlayerReady)
|
|
189
|
-
return;
|
|
190
|
-
let currentBlock = null;
|
|
191
|
-
const unsubscribe = mediaPlayer.onBufferingChange((newBufferingState) => {
|
|
192
|
-
if (newBufferingState && !currentBlock) {
|
|
193
|
-
currentBlock = buffer.delayPlayback();
|
|
194
|
-
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] MediaPlayer buffering - blocking Remotion playback');
|
|
195
|
-
}
|
|
196
|
-
else if (!newBufferingState && currentBlock) {
|
|
197
|
-
currentBlock.unblock();
|
|
198
|
-
currentBlock = null;
|
|
199
|
-
Internals.Log.trace({ logLevel, tag: '@remotion/media' }, '[VideoForPreview] MediaPlayer unbuffering - unblocking Remotion playback');
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
return () => {
|
|
203
|
-
unsubscribe();
|
|
204
|
-
if (currentBlock) {
|
|
205
|
-
currentBlock.unblock();
|
|
206
|
-
currentBlock = null;
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
}, [mediaPlayerReady, buffer, logLevel]);
|
|
210
189
|
const effectiveMuted = isSequenceHidden || muted || mediaMuted || userPreferredVolume <= 0;
|
|
211
190
|
useEffect(() => {
|
|
212
191
|
const mediaPlayer = mediaPlayerRef.current;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remotion/media",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.365",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"module": "dist/esm/index.mjs",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"make": "tsc -d && bun --env-file=../.env.bundle bundle.ts"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"mediabunny": "1.24.
|
|
25
|
-
"remotion": "4.0.
|
|
24
|
+
"mediabunny": "1.24.2",
|
|
25
|
+
"remotion": "4.0.365",
|
|
26
26
|
"webdriverio": "9.19.2"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"react-dom": ">=16.8.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@remotion/eslint-config-internal": "4.0.
|
|
33
|
+
"@remotion/eslint-config-internal": "4.0.365",
|
|
34
34
|
"@vitest/browser": "^3.2.4",
|
|
35
35
|
"eslint": "9.19.0",
|
|
36
36
|
"react": "19.0.0",
|