@remotion/media 4.0.370 → 4.0.372

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,98 +0,0 @@
1
- import type { LogLevel } from 'remotion';
2
- export declare const SEEK_THRESHOLD = 0.05;
3
- export type MediaPlayerInitResult = {
4
- type: 'success';
5
- durationInSeconds: number;
6
- } | {
7
- type: 'unknown-container-format';
8
- } | {
9
- type: 'cannot-decode';
10
- } | {
11
- type: 'network-error';
12
- } | {
13
- type: 'no-tracks';
14
- };
15
- export declare class MediaPlayer {
16
- private canvas;
17
- private context;
18
- private src;
19
- private logLevel;
20
- private playbackRate;
21
- private audioStreamIndex;
22
- private canvasSink;
23
- private videoFrameIterator;
24
- private nextFrame;
25
- private audioSink;
26
- private audioBufferIterator;
27
- private queuedAudioNodes;
28
- private gainNode;
29
- private currentVolume;
30
- private sharedAudioContext;
31
- private audioSyncAnchor;
32
- private playing;
33
- private muted;
34
- private loop;
35
- private fps;
36
- private trimBefore;
37
- private trimAfter;
38
- private animationFrameId;
39
- private videoAsyncId;
40
- private audioAsyncId;
41
- private initialized;
42
- private totalDuration;
43
- private isBuffering;
44
- private onBufferingChangeCallback?;
45
- private audioBufferHealth;
46
- private audioIteratorStarted;
47
- private readonly HEALTHY_BUFER_THRESHOLD_SECONDS;
48
- private mediaEnded;
49
- private onVideoFrameCallback?;
50
- constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, audioStreamIndex, fps, }: {
51
- canvas: HTMLCanvasElement | null;
52
- src: string;
53
- logLevel: LogLevel;
54
- sharedAudioContext: AudioContext;
55
- loop: boolean;
56
- trimBefore: number | undefined;
57
- trimAfter: number | undefined;
58
- playbackRate: number;
59
- audioStreamIndex: number;
60
- fps: number;
61
- });
62
- private input;
63
- private isReady;
64
- private hasAudio;
65
- private isCurrentlyBuffering;
66
- initialize(startTimeUnresolved: number): Promise<MediaPlayerInitResult>;
67
- private clearCanvas;
68
- private cleanupAudioQueue;
69
- private cleanAudioIteratorAndNodes;
70
- seekTo(time: number): Promise<void>;
71
- play(): Promise<void>;
72
- pause(): void;
73
- setMuted(muted: boolean): void;
74
- setVolume(volume: number): void;
75
- setPlaybackRate(rate: number): void;
76
- setFps(fps: number): void;
77
- setLoop(loop: boolean): void;
78
- dispose(): void;
79
- private getPlaybackTime;
80
- private scheduleAudioChunk;
81
- onBufferingChange(callback: (isBuffering: boolean) => void): () => void;
82
- onVideoFrame(callback: (frame: CanvasImageSource) => void): () => void;
83
- private canRenderVideo;
84
- private startRenderLoop;
85
- private stopRenderLoop;
86
- private render;
87
- private shouldRenderFrame;
88
- private drawCurrentFrame;
89
- private startAudioIterator;
90
- private startVideoIterator;
91
- private updateNextFrame;
92
- private bufferingStartedAtMs;
93
- private minBufferingTimeoutMs;
94
- private setBufferingState;
95
- private maybeResumeFromBuffering;
96
- private maybeForceResumeFromBuffering;
97
- private runAudioIterator;
98
- }
@@ -1,532 +0,0 @@
1
- import { ALL_FORMATS, AudioBufferSink, CanvasSink, Input, UrlSource, } from 'mediabunny';
2
- import { Internals } from 'remotion';
3
- import { getTimeInSeconds } from '../get-time-in-seconds';
4
- import { isNetworkError } from '../is-network-error';
5
- import { sleep, TimeoutError, withTimeout } from './timeout-utils';
6
- export const SEEK_THRESHOLD = 0.05;
7
- const AUDIO_BUFFER_TOLERANCE_THRESHOLD = 0.1;
8
- export class MediaPlayer {
9
- constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, audioStreamIndex, fps, }) {
10
- this.canvasSink = null;
11
- this.videoFrameIterator = null;
12
- this.nextFrame = null;
13
- this.audioSink = null;
14
- this.audioBufferIterator = null;
15
- this.queuedAudioNodes = new Set();
16
- this.gainNode = null;
17
- this.currentVolume = 1;
18
- // this is the time difference between Web Audio timeline
19
- // and media file timeline
20
- this.audioSyncAnchor = 0;
21
- this.playing = false;
22
- this.muted = false;
23
- this.loop = false;
24
- this.animationFrameId = null;
25
- this.videoAsyncId = 0;
26
- this.audioAsyncId = 0;
27
- this.initialized = false;
28
- // for remotion buffer state
29
- this.isBuffering = false;
30
- this.audioBufferHealth = 0;
31
- this.audioIteratorStarted = false;
32
- this.HEALTHY_BUFER_THRESHOLD_SECONDS = 1;
33
- this.mediaEnded = false;
34
- this.input = null;
35
- this.render = () => {
36
- if (this.isBuffering) {
37
- this.maybeForceResumeFromBuffering();
38
- }
39
- if (this.shouldRenderFrame()) {
40
- this.drawCurrentFrame();
41
- }
42
- if (this.playing) {
43
- this.animationFrameId = requestAnimationFrame(this.render);
44
- }
45
- else {
46
- this.animationFrameId = null;
47
- }
48
- };
49
- this.startAudioIterator = async (startFromSecond) => {
50
- if (!this.hasAudio())
51
- return;
52
- this.audioAsyncId++;
53
- const currentAsyncId = this.audioAsyncId;
54
- // Clean up existing audio iterator
55
- await this.audioBufferIterator?.return();
56
- this.audioIteratorStarted = false;
57
- this.audioBufferHealth = 0;
58
- try {
59
- this.audioBufferIterator = this.audioSink.buffers(startFromSecond);
60
- this.runAudioIterator(startFromSecond, currentAsyncId);
61
- }
62
- catch (error) {
63
- Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to start audio iterator', error);
64
- }
65
- };
66
- this.startVideoIterator = async (timeToSeek) => {
67
- if (!this.canvasSink) {
68
- return;
69
- }
70
- this.videoAsyncId++;
71
- const currentAsyncId = this.videoAsyncId;
72
- this.videoFrameIterator?.return().catch(() => undefined);
73
- this.videoFrameIterator = this.canvasSink.canvases(timeToSeek);
74
- try {
75
- const firstFrame = (await this.videoFrameIterator.next()).value ?? null;
76
- const secondFrame = (await this.videoFrameIterator.next()).value ?? null;
77
- if (currentAsyncId !== this.videoAsyncId) {
78
- return;
79
- }
80
- if (firstFrame && this.context) {
81
- Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Drew initial frame ${firstFrame.timestamp.toFixed(3)}s`);
82
- this.context.drawImage(firstFrame.canvas, 0, 0);
83
- if (this.onVideoFrameCallback && this.canvas) {
84
- this.onVideoFrameCallback(this.canvas);
85
- }
86
- }
87
- this.nextFrame = secondFrame ?? null;
88
- if (secondFrame) {
89
- Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Buffered next frame ${secondFrame.timestamp.toFixed(3)}s`);
90
- }
91
- }
92
- catch (error) {
93
- Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to start video iterator', error);
94
- }
95
- };
96
- this.updateNextFrame = async () => {
97
- if (!this.videoFrameIterator) {
98
- return;
99
- }
100
- try {
101
- while (true) {
102
- const newNextFrame = (await this.videoFrameIterator.next()).value ?? null;
103
- if (!newNextFrame) {
104
- this.mediaEnded = true;
105
- break;
106
- }
107
- const playbackTime = this.getPlaybackTime();
108
- if (playbackTime === null) {
109
- continue;
110
- }
111
- if (newNextFrame.timestamp <= playbackTime) {
112
- continue;
113
- }
114
- else {
115
- this.nextFrame = newNextFrame;
116
- Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Buffered next frame ${newNextFrame.timestamp.toFixed(3)}s`);
117
- break;
118
- }
119
- }
120
- }
121
- catch (error) {
122
- Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to update next frame', error);
123
- }
124
- };
125
- this.bufferingStartedAtMs = null;
126
- this.minBufferingTimeoutMs = 500;
127
- this.runAudioIterator = async (startFromSecond, audioAsyncId) => {
128
- if (!this.hasAudio() || !this.audioBufferIterator)
129
- return;
130
- try {
131
- let totalBufferDuration = 0;
132
- let isFirstBuffer = true;
133
- this.audioIteratorStarted = true;
134
- while (true) {
135
- if (audioAsyncId !== this.audioAsyncId) {
136
- return;
137
- }
138
- const BUFFERING_TIMEOUT_MS = 50;
139
- let result;
140
- try {
141
- result = await withTimeout(this.audioBufferIterator.next(), BUFFERING_TIMEOUT_MS, 'Iterator timeout');
142
- }
143
- catch (error) {
144
- if (error instanceof TimeoutError && !this.mediaEnded) {
145
- this.setBufferingState(true);
146
- }
147
- await sleep(10);
148
- continue;
149
- }
150
- // media has ended
151
- if (result.done || !result.value) {
152
- this.mediaEnded = true;
153
- break;
154
- }
155
- const { buffer, timestamp, duration } = result.value;
156
- totalBufferDuration += duration;
157
- this.audioBufferHealth = Math.max(0, totalBufferDuration / this.playbackRate);
158
- this.maybeResumeFromBuffering(totalBufferDuration / this.playbackRate);
159
- if (this.playing) {
160
- if (isFirstBuffer) {
161
- this.audioSyncAnchor =
162
- this.sharedAudioContext.currentTime - timestamp;
163
- isFirstBuffer = false;
164
- }
165
- // if timestamp is less than timeToSeek, skip
166
- // context: for some reason, mediabunny returns buffer at 9.984s, when requested at 10s
167
- if (timestamp < startFromSecond - AUDIO_BUFFER_TOLERANCE_THRESHOLD) {
168
- continue;
169
- }
170
- this.scheduleAudioChunk(buffer, timestamp);
171
- }
172
- const playbackTime = this.getPlaybackTime();
173
- if (playbackTime === null) {
174
- continue;
175
- }
176
- if (timestamp - playbackTime >= 1) {
177
- await new Promise((resolve) => {
178
- const check = () => {
179
- const currentPlaybackTime = this.getPlaybackTime();
180
- if (currentPlaybackTime !== null &&
181
- timestamp - currentPlaybackTime < 1) {
182
- resolve();
183
- }
184
- else {
185
- requestAnimationFrame(check);
186
- }
187
- };
188
- check();
189
- });
190
- }
191
- }
192
- }
193
- catch (error) {
194
- Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to run audio iterator', error);
195
- }
196
- };
197
- this.canvas = canvas ?? null;
198
- this.src = src;
199
- this.logLevel = logLevel ?? window.remotion_logLevel;
200
- this.sharedAudioContext = sharedAudioContext;
201
- this.playbackRate = playbackRate;
202
- this.loop = loop;
203
- this.trimBefore = trimBefore;
204
- this.trimAfter = trimAfter;
205
- this.audioStreamIndex = audioStreamIndex ?? 0;
206
- this.fps = fps;
207
- if (canvas) {
208
- const context = canvas.getContext('2d', {
209
- alpha: true,
210
- desynchronized: true,
211
- });
212
- if (!context) {
213
- throw new Error('Could not get 2D context from canvas');
214
- }
215
- this.context = context;
216
- }
217
- else {
218
- this.context = null;
219
- }
220
- }
221
- isReady() {
222
- return this.initialized && Boolean(this.sharedAudioContext);
223
- }
224
- hasAudio() {
225
- return Boolean(this.audioSink && this.sharedAudioContext && this.gainNode);
226
- }
227
- isCurrentlyBuffering() {
228
- return this.isBuffering && Boolean(this.bufferingStartedAtMs);
229
- }
230
- async initialize(startTimeUnresolved) {
231
- try {
232
- const urlSource = new UrlSource(this.src);
233
- const input = new Input({
234
- source: urlSource,
235
- formats: ALL_FORMATS,
236
- });
237
- this.input = input;
238
- try {
239
- await this.input.getFormat();
240
- }
241
- catch (error) {
242
- const err = error;
243
- if (isNetworkError(err)) {
244
- throw error;
245
- }
246
- Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Failed to recognize format for ${this.src}`, error);
247
- return { type: 'unknown-container-format' };
248
- }
249
- const [durationInSeconds, videoTrack, audioTracks] = await Promise.all([
250
- input.computeDuration(),
251
- input.getPrimaryVideoTrack(),
252
- input.getAudioTracks(),
253
- ]);
254
- this.totalDuration = durationInSeconds;
255
- const audioTrack = audioTracks[this.audioStreamIndex] ?? null;
256
- if (!videoTrack && !audioTrack) {
257
- return { type: 'no-tracks' };
258
- }
259
- if (videoTrack && this.canvas && this.context) {
260
- const canDecode = await videoTrack.canDecode();
261
- if (!canDecode) {
262
- return { type: 'cannot-decode' };
263
- }
264
- this.canvasSink = new CanvasSink(videoTrack, {
265
- poolSize: 2,
266
- fit: 'contain',
267
- alpha: true,
268
- });
269
- this.canvas.width = videoTrack.displayWidth;
270
- this.canvas.height = videoTrack.displayHeight;
271
- }
272
- if (audioTrack && this.sharedAudioContext) {
273
- this.audioSink = new AudioBufferSink(audioTrack);
274
- this.gainNode = this.sharedAudioContext.createGain();
275
- this.gainNode.connect(this.sharedAudioContext.destination);
276
- }
277
- const startTime = getTimeInSeconds({
278
- unloopedTimeInSeconds: startTimeUnresolved,
279
- playbackRate: this.playbackRate,
280
- loop: this.loop,
281
- trimBefore: this.trimBefore,
282
- trimAfter: this.trimAfter,
283
- mediaDurationInSeconds: this.totalDuration,
284
- fps: this.fps,
285
- ifNoMediaDuration: 'infinity',
286
- src: this.src,
287
- });
288
- if (startTime === null) {
289
- this.clearCanvas();
290
- return { type: 'success', durationInSeconds: this.totalDuration };
291
- }
292
- if (this.sharedAudioContext) {
293
- this.audioSyncAnchor = this.sharedAudioContext.currentTime - startTime;
294
- }
295
- this.initialized = true;
296
- await Promise.all([
297
- this.startAudioIterator(startTime),
298
- this.startVideoIterator(startTime),
299
- ]);
300
- this.startRenderLoop();
301
- return { type: 'success', durationInSeconds };
302
- }
303
- catch (error) {
304
- const err = error;
305
- if (isNetworkError(err)) {
306
- Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Network/CORS error for ${this.src}`, err);
307
- return { type: 'network-error' };
308
- }
309
- Internals.Log.error({ logLevel: this.logLevel, tag: '@remotion/media' }, '[MediaPlayer] Failed to initialize', error);
310
- throw error;
311
- }
312
- }
313
- clearCanvas() {
314
- if (this.context && this.canvas) {
315
- this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
316
- }
317
- }
318
- cleanupAudioQueue() {
319
- for (const node of this.queuedAudioNodes) {
320
- node.stop();
321
- }
322
- this.queuedAudioNodes.clear();
323
- }
324
- async cleanAudioIteratorAndNodes() {
325
- await this.audioBufferIterator?.return();
326
- this.audioBufferIterator = null;
327
- this.audioIteratorStarted = false;
328
- this.audioBufferHealth = 0;
329
- this.cleanupAudioQueue();
330
- }
331
- async seekTo(time) {
332
- if (!this.isReady())
333
- return;
334
- const newTime = getTimeInSeconds({
335
- unloopedTimeInSeconds: time,
336
- playbackRate: this.playbackRate,
337
- loop: this.loop,
338
- trimBefore: this.trimBefore,
339
- trimAfter: this.trimAfter,
340
- mediaDurationInSeconds: this.totalDuration ?? null,
341
- fps: this.fps,
342
- ifNoMediaDuration: 'infinity',
343
- src: this.src,
344
- });
345
- if (newTime === null) {
346
- // invalidate in-flight video operations
347
- this.videoAsyncId++;
348
- this.nextFrame = null;
349
- this.clearCanvas();
350
- await this.cleanAudioIteratorAndNodes();
351
- return;
352
- }
353
- const currentPlaybackTime = this.getPlaybackTime();
354
- const isSignificantSeek = currentPlaybackTime === null ||
355
- Math.abs(newTime - currentPlaybackTime) > SEEK_THRESHOLD;
356
- if (isSignificantSeek) {
357
- this.nextFrame = null;
358
- this.audioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
359
- this.mediaEnded = false;
360
- if (this.audioSink) {
361
- await this.cleanAudioIteratorAndNodes();
362
- }
363
- await Promise.all([
364
- this.startAudioIterator(newTime),
365
- this.startVideoIterator(newTime),
366
- ]);
367
- }
368
- if (!this.playing) {
369
- this.render();
370
- }
371
- }
372
- async play() {
373
- if (!this.isReady())
374
- return;
375
- if (!this.playing) {
376
- if (this.sharedAudioContext.state === 'suspended') {
377
- await this.sharedAudioContext.resume();
378
- }
379
- this.playing = true;
380
- this.startRenderLoop();
381
- }
382
- }
383
- pause() {
384
- this.playing = false;
385
- this.cleanupAudioQueue();
386
- this.stopRenderLoop();
387
- }
388
- setMuted(muted) {
389
- this.muted = muted;
390
- if (this.gainNode) {
391
- this.gainNode.gain.value = muted ? 0 : this.currentVolume;
392
- }
393
- }
394
- setVolume(volume) {
395
- if (!this.gainNode) {
396
- return;
397
- }
398
- const appliedVolume = Math.max(0, volume);
399
- this.currentVolume = appliedVolume;
400
- if (!this.muted) {
401
- this.gainNode.gain.value = appliedVolume;
402
- }
403
- }
404
- setPlaybackRate(rate) {
405
- this.playbackRate = rate;
406
- }
407
- setFps(fps) {
408
- this.fps = fps;
409
- }
410
- setLoop(loop) {
411
- this.loop = loop;
412
- }
413
- dispose() {
414
- this.input?.dispose();
415
- this.stopRenderLoop();
416
- this.videoFrameIterator?.return();
417
- this.cleanAudioIteratorAndNodes();
418
- this.videoAsyncId++;
419
- }
420
- getPlaybackTime() {
421
- return this.sharedAudioContext.currentTime - this.audioSyncAnchor;
422
- }
423
- scheduleAudioChunk(buffer, mediaTimestamp) {
424
- const targetTime = mediaTimestamp + this.audioSyncAnchor;
425
- const delay = targetTime - this.sharedAudioContext.currentTime;
426
- const node = this.sharedAudioContext.createBufferSource();
427
- node.buffer = buffer;
428
- node.playbackRate.value = this.playbackRate;
429
- node.connect(this.gainNode);
430
- if (delay >= 0) {
431
- node.start(targetTime);
432
- }
433
- else {
434
- node.start(this.sharedAudioContext.currentTime, -delay);
435
- }
436
- this.queuedAudioNodes.add(node);
437
- node.onended = () => this.queuedAudioNodes.delete(node);
438
- }
439
- onBufferingChange(callback) {
440
- this.onBufferingChangeCallback = callback;
441
- return () => {
442
- if (this.onBufferingChangeCallback === callback) {
443
- this.onBufferingChangeCallback = undefined;
444
- }
445
- };
446
- }
447
- onVideoFrame(callback) {
448
- this.onVideoFrameCallback = callback;
449
- if (this.initialized && callback && this.canvas) {
450
- callback(this.canvas);
451
- }
452
- return () => {
453
- if (this.onVideoFrameCallback === callback) {
454
- this.onVideoFrameCallback = undefined;
455
- }
456
- };
457
- }
458
- canRenderVideo() {
459
- return (!this.hasAudio() ||
460
- (this.audioIteratorStarted &&
461
- this.audioBufferHealth >= this.HEALTHY_BUFER_THRESHOLD_SECONDS));
462
- }
463
- startRenderLoop() {
464
- if (this.animationFrameId !== null) {
465
- return;
466
- }
467
- this.render();
468
- }
469
- stopRenderLoop() {
470
- if (this.animationFrameId !== null) {
471
- cancelAnimationFrame(this.animationFrameId);
472
- this.animationFrameId = null;
473
- }
474
- }
475
- shouldRenderFrame() {
476
- const playbackTime = this.getPlaybackTime();
477
- if (playbackTime === null) {
478
- return false;
479
- }
480
- return (!this.isBuffering &&
481
- this.canRenderVideo() &&
482
- this.nextFrame !== null &&
483
- this.nextFrame.timestamp <= playbackTime);
484
- }
485
- drawCurrentFrame() {
486
- if (this.context && this.nextFrame) {
487
- this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
488
- this.context.drawImage(this.nextFrame.canvas, 0, 0);
489
- }
490
- if (this.onVideoFrameCallback && this.canvas) {
491
- this.onVideoFrameCallback(this.canvas);
492
- }
493
- this.nextFrame = null;
494
- this.updateNextFrame();
495
- }
496
- setBufferingState(isBuffering) {
497
- if (this.isBuffering !== isBuffering) {
498
- this.isBuffering = isBuffering;
499
- if (isBuffering) {
500
- this.bufferingStartedAtMs = performance.now();
501
- this.onBufferingChangeCallback?.(true);
502
- }
503
- else {
504
- this.bufferingStartedAtMs = null;
505
- this.onBufferingChangeCallback?.(false);
506
- }
507
- }
508
- }
509
- maybeResumeFromBuffering(currentBufferDuration) {
510
- if (!this.isCurrentlyBuffering())
511
- return;
512
- const now = performance.now();
513
- const bufferingDuration = now - this.bufferingStartedAtMs;
514
- const minTimeElapsed = bufferingDuration >= this.minBufferingTimeoutMs;
515
- const bufferHealthy = currentBufferDuration >= this.HEALTHY_BUFER_THRESHOLD_SECONDS;
516
- if (minTimeElapsed && bufferHealthy) {
517
- Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Resuming from buffering after ${bufferingDuration}ms - buffer recovered`);
518
- this.setBufferingState(false);
519
- }
520
- }
521
- maybeForceResumeFromBuffering() {
522
- if (!this.isCurrentlyBuffering())
523
- return;
524
- const now = performance.now();
525
- const bufferingDuration = now - this.bufferingStartedAtMs;
526
- const forceTimeout = bufferingDuration > this.minBufferingTimeoutMs * 10;
527
- if (forceTimeout) {
528
- Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Force resuming from buffering after ${bufferingDuration}ms`);
529
- this.setBufferingState(false);
530
- }
531
- }
532
- }
@@ -1,5 +0,0 @@
1
- export declare const sleep: (ms: number) => Promise<unknown>;
2
- export declare class TimeoutError extends Error {
3
- constructor(message?: string);
4
- }
5
- export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage?: string): Promise<T>;
@@ -1,24 +0,0 @@
1
- /* eslint-disable no-promise-executor-return */
2
- export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3
- export class TimeoutError extends Error {
4
- constructor(message = 'Operation timed out') {
5
- super(message);
6
- this.name = 'TimeoutError';
7
- }
8
- }
9
- export function withTimeout(promise, timeoutMs, errorMessage = 'Operation timed out') {
10
- let timeoutId = null;
11
- const timeoutPromise = new Promise((_, reject) => {
12
- timeoutId = window.setTimeout(() => {
13
- reject(new TimeoutError(errorMessage));
14
- }, timeoutMs);
15
- });
16
- return Promise.race([
17
- promise.finally(() => {
18
- if (timeoutId) {
19
- clearTimeout(timeoutId);
20
- }
21
- }),
22
- timeoutPromise,
23
- ]);
24
- }