@remotion/media 4.0.365 → 4.0.367

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,113 +1,65 @@
1
- import { ALL_FORMATS, AudioBufferSink, CanvasSink, Input, UrlSource, } from 'mediabunny';
1
+ import { ALL_FORMATS, Input, UrlSource } from 'mediabunny';
2
2
  import { Internals } from 'remotion';
3
- import { isAlreadyQueued, makeAudioIterator, } from './audio/audio-preview-iterator';
3
+ import { audioIteratorManager, } from './audio-iterator-manager';
4
+ import { calculatePlaybackTime } from './calculate-playbacktime';
4
5
  import { drawPreviewOverlay } from './debug-overlay/preview-overlay';
5
6
  import { getTimeInSeconds } from './get-time-in-seconds';
6
7
  import { isNetworkError } from './is-network-error';
7
- import { createVideoIterator, } from './video/video-preview-iterator';
8
+ import { makeNonceManager } from './nonce-manager';
9
+ import { videoIteratorManager } from './video-iterator-manager';
8
10
  export class MediaPlayer {
9
- constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, audioStreamIndex, fps, debugOverlay, bufferState, }) {
10
- this.canvasSink = null;
11
- this.videoFrameIterator = null;
12
- this.debugStats = {
13
- videoIteratorsCreated: 0,
14
- audioIteratorsCreated: 0,
15
- framesRendered: 0,
16
- };
17
- this.audioSink = null;
18
- this.audioBufferIterator = null;
19
- this.gainNode = null;
20
- this.currentVolume = 1;
11
+ constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBefore, trimAfter, playbackRate, globalPlaybackRate, audioStreamIndex, fps, debugOverlay, bufferState, isPremounting, isPostmounting, }) {
12
+ this.audioIteratorManager = null;
13
+ this.videoIteratorManager = null;
21
14
  // this is the time difference between Web Audio timeline
22
15
  // and media file timeline
23
16
  this.audioSyncAnchor = 0;
24
17
  this.playing = false;
25
- this.muted = false;
26
18
  this.loop = false;
27
- this.initialized = false;
28
19
  this.debugOverlay = false;
20
+ this.onVideoFrameCallback = null;
29
21
  this.initializationPromise = null;
30
- this.input = null;
31
- this.currentSeekNonce = 0;
32
22
  this.seekPromiseChain = Promise.resolve();
33
- this.audioChunksForAfterResuming = [];
34
- this.drawFrame = (frame) => {
35
- if (!this.context) {
36
- throw new Error('Context not initialized');
37
- }
38
- this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
39
- this.context.drawImage(frame.canvas, 0, 0);
40
- this.debugStats.framesRendered++;
41
- this.drawDebugOverlay();
42
- if (this.onVideoFrameCallback && this.canvas) {
43
- this.onVideoFrameCallback(this.canvas);
23
+ this.delayPlaybackHandleIfNotPremounting = () => {
24
+ if (this.isPremounting || this.isPostmounting) {
25
+ return {
26
+ unblock: () => { },
27
+ };
44
28
  }
45
- Internals.Log.trace({ logLevel: this.logLevel, tag: '@remotion/media' }, `[MediaPlayer] Drew frame ${frame.timestamp.toFixed(3)}s`);
29
+ return this.bufferState.delayPlayback();
46
30
  };
47
- this.startAudioIterator = async (startFromSecond, nonce) => {
48
- if (!this.hasAudio())
49
- return;
50
- this.audioBufferIterator?.destroy();
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();
70
- return;
71
- }
72
- const { buffer, timestamp } = result.value;
73
- this.audioChunksForAfterResuming.push({
74
- buffer,
75
- timestamp,
76
- });
31
+ this.scheduleAudioNode = (node, mediaTimestamp) => {
32
+ const currentTime = this.getPlaybackTime();
33
+ const delayWithoutPlaybackRate = mediaTimestamp - currentTime;
34
+ const delay = delayWithoutPlaybackRate / (this.playbackRate * this.globalPlaybackRate);
35
+ if (delay >= 0) {
36
+ node.start(this.sharedAudioContext.currentTime + delay);
77
37
  }
78
- delayHandle.unblock();
79
- };
80
- this.startVideoIterator = async (timeToSeek, nonce) => {
81
- if (!this.canvasSink) {
82
- return;
83
- }
84
- this.videoFrameIterator?.destroy();
85
- const iterator = createVideoIterator(timeToSeek, this.canvasSink);
86
- this.debugStats.videoIteratorsCreated++;
87
- this.videoFrameIterator = iterator;
88
- const delayHandle = this.bufferState.delayPlayback();
89
- const frameResult = await iterator.getNext();
90
- delayHandle.unblock();
91
- if (iterator.isDestroyed()) {
92
- return;
93
- }
94
- if (nonce !== this.currentSeekNonce) {
95
- return;
96
- }
97
- if (this.videoFrameIterator.isDestroyed()) {
98
- return;
38
+ else {
39
+ node.start(this.sharedAudioContext.currentTime, -delay);
99
40
  }
100
- if (!frameResult.value) {
101
- // media ended
41
+ };
42
+ this.drawDebugOverlay = () => {
43
+ if (!this.debugOverlay)
102
44
  return;
45
+ if (this.context && this.canvas) {
46
+ drawPreviewOverlay({
47
+ context: this.context,
48
+ audioTime: this.sharedAudioContext.currentTime,
49
+ audioContextState: this.sharedAudioContext.state,
50
+ audioSyncAnchor: this.audioSyncAnchor,
51
+ audioIteratorManager: this.audioIteratorManager,
52
+ playing: this.playing,
53
+ videoIteratorManager: this.videoIteratorManager,
54
+ });
103
55
  }
104
- this.drawFrame(frameResult.value);
105
56
  };
106
57
  this.canvas = canvas ?? null;
107
58
  this.src = src;
108
59
  this.logLevel = logLevel ?? window.remotion_logLevel;
109
60
  this.sharedAudioContext = sharedAudioContext;
110
61
  this.playbackRate = playbackRate;
62
+ this.globalPlaybackRate = globalPlaybackRate;
111
63
  this.loop = loop;
112
64
  this.trimBefore = trimBefore;
113
65
  this.trimAfter = trimAfter;
@@ -115,6 +67,13 @@ export class MediaPlayer {
115
67
  this.fps = fps;
116
68
  this.debugOverlay = debugOverlay;
117
69
  this.bufferState = bufferState;
70
+ this.isPremounting = isPremounting;
71
+ this.isPostmounting = isPostmounting;
72
+ this.nonceManager = makeNonceManager();
73
+ this.input = new Input({
74
+ source: new UrlSource(this.src),
75
+ formats: ALL_FORMATS,
76
+ });
118
77
  if (canvas) {
119
78
  const context = canvas.getContext('2d', {
120
79
  alpha: true,
@@ -129,16 +88,8 @@ export class MediaPlayer {
129
88
  this.context = null;
130
89
  }
131
90
  }
132
- isReady() {
133
- return (this.initialized &&
134
- Boolean(this.sharedAudioContext) &&
135
- !this.input?.disposed);
136
- }
137
- hasAudio() {
138
- return Boolean(this.audioSink && this.sharedAudioContext && this.gainNode);
139
- }
140
91
  isDisposalError() {
141
- return this.input?.disposed === true;
92
+ return this.input.disposed === true;
142
93
  }
143
94
  initialize(startTimeUnresolved) {
144
95
  const promise = this._initialize(startTimeUnresolved);
@@ -147,17 +98,11 @@ export class MediaPlayer {
147
98
  }
148
99
  async _initialize(startTimeUnresolved) {
149
100
  try {
150
- const urlSource = new UrlSource(this.src);
151
- const input = new Input({
152
- source: urlSource,
153
- formats: ALL_FORMATS,
154
- });
155
- this.input = input;
156
- if (input.disposed) {
101
+ if (this.input.disposed) {
157
102
  return { type: 'disposed' };
158
103
  }
159
104
  try {
160
- await input.getFormat();
105
+ await this.input.getFormat();
161
106
  }
162
107
  catch (error) {
163
108
  if (this.isDisposalError()) {
@@ -171,10 +116,13 @@ export class MediaPlayer {
171
116
  return { type: 'unknown-container-format' };
172
117
  }
173
118
  const [durationInSeconds, videoTrack, audioTracks] = await Promise.all([
174
- input.computeDuration(),
175
- input.getPrimaryVideoTrack(),
176
- input.getAudioTracks(),
119
+ this.input.computeDuration(),
120
+ this.input.getPrimaryVideoTrack(),
121
+ this.input.getAudioTracks(),
177
122
  ]);
123
+ if (this.input.disposed) {
124
+ return { type: 'disposed' };
125
+ }
178
126
  this.totalDuration = durationInSeconds;
179
127
  const audioTrack = audioTracks[this.audioStreamIndex] ?? null;
180
128
  if (!videoTrack && !audioTrack) {
@@ -185,18 +133,18 @@ export class MediaPlayer {
185
133
  if (!canDecode) {
186
134
  return { type: 'cannot-decode' };
187
135
  }
188
- this.canvasSink = new CanvasSink(videoTrack, {
189
- poolSize: 2,
190
- fit: 'contain',
191
- alpha: true,
136
+ if (this.input.disposed) {
137
+ return { type: 'disposed' };
138
+ }
139
+ this.videoIteratorManager = videoIteratorManager({
140
+ videoTrack,
141
+ delayPlaybackHandleIfNotPremounting: this.delayPlaybackHandleIfNotPremounting,
142
+ context: this.context,
143
+ canvas: this.canvas,
144
+ getOnVideoFrameCallback: () => this.onVideoFrameCallback,
145
+ logLevel: this.logLevel,
146
+ drawDebugOverlay: this.drawDebugOverlay,
192
147
  });
193
- this.canvas.width = videoTrack.displayWidth;
194
- this.canvas.height = videoTrack.displayHeight;
195
- }
196
- if (audioTrack && this.sharedAudioContext) {
197
- this.audioSink = new AudioBufferSink(audioTrack);
198
- this.gainNode = this.sharedAudioContext.createGain();
199
- this.gainNode.connect(this.sharedAudioContext.destination);
200
148
  }
201
149
  const startTime = getTimeInSeconds({
202
150
  unloopedTimeInSeconds: startTimeUnresolved,
@@ -210,17 +158,29 @@ export class MediaPlayer {
210
158
  src: this.src,
211
159
  });
212
160
  if (startTime === null) {
213
- this.clearCanvas();
214
- return { type: 'success', durationInSeconds: this.totalDuration };
215
- }
216
- if (this.sharedAudioContext) {
217
- this.setPlaybackTime(startTime);
161
+ throw new Error(`should have asserted that the time is not null`);
162
+ }
163
+ this.setPlaybackTime(startTime, this.playbackRate * this.globalPlaybackRate);
164
+ if (audioTrack) {
165
+ this.audioIteratorManager = audioIteratorManager({
166
+ audioTrack,
167
+ delayPlaybackHandleIfNotPremounting: this.delayPlaybackHandleIfNotPremounting,
168
+ sharedAudioContext: this.sharedAudioContext,
169
+ });
218
170
  }
219
- this.initialized = true;
171
+ const nonce = this.nonceManager.createAsyncOperation();
220
172
  try {
221
173
  // intentionally not awaited
222
- this.startAudioIterator(startTime, this.currentSeekNonce);
223
- await this.startVideoIterator(startTime, this.currentSeekNonce);
174
+ if (this.audioIteratorManager) {
175
+ this.audioIteratorManager.startAudioIterator({
176
+ nonce,
177
+ playbackRate: this.playbackRate * this.globalPlaybackRate,
178
+ startFromSecond: startTime,
179
+ getIsPlaying: () => this.playing,
180
+ scheduleAudioNode: this.scheduleAudioNode,
181
+ });
182
+ }
183
+ await this.videoIteratorManager?.startVideoIterator(startTime, nonce);
224
184
  }
225
185
  catch (error) {
226
186
  if (this.isDisposalError()) {
@@ -240,24 +200,7 @@ export class MediaPlayer {
240
200
  throw error;
241
201
  }
242
202
  }
243
- clearCanvas() {
244
- if (this.context && this.canvas) {
245
- this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
246
- }
247
- }
248
203
  async seekTo(time) {
249
- this.currentSeekNonce++;
250
- const nonce = this.currentSeekNonce;
251
- await this.seekPromiseChain;
252
- this.seekPromiseChain = this.seekToDoNotCallDirectly(time, nonce);
253
- await this.seekPromiseChain;
254
- }
255
- async seekToDoNotCallDirectly(time, nonce) {
256
- if (nonce !== this.currentSeekNonce) {
257
- return;
258
- }
259
- if (!this.isReady())
260
- return;
261
204
  const newTime = getTimeInSeconds({
262
205
  unloopedTimeInSeconds: time,
263
206
  playbackRate: this.playbackRate,
@@ -270,138 +213,129 @@ export class MediaPlayer {
270
213
  src: this.src,
271
214
  });
272
215
  if (newTime === null) {
273
- // invalidate in-flight video operations
274
- this.videoFrameIterator?.destroy();
275
- this.videoFrameIterator = null;
276
- this.clearCanvas();
277
- this.audioBufferIterator?.destroy();
278
- this.audioBufferIterator = null;
216
+ throw new Error(`should have asserted that the time is not null`);
217
+ }
218
+ const nonce = this.nonceManager.createAsyncOperation();
219
+ await this.seekPromiseChain;
220
+ this.seekPromiseChain = this.seekToDoNotCallDirectly(newTime, nonce);
221
+ await this.seekPromiseChain;
222
+ }
223
+ async seekToDoNotCallDirectly(newTime, nonce) {
224
+ if (nonce.isStale()) {
279
225
  return;
280
226
  }
281
227
  const currentPlaybackTime = this.getPlaybackTime();
282
228
  if (currentPlaybackTime === newTime) {
283
229
  return;
284
230
  }
285
- const newAudioSyncAnchor = this.sharedAudioContext.currentTime - newTime;
231
+ const newAudioSyncAnchor = this.sharedAudioContext.currentTime -
232
+ newTime / (this.playbackRate * this.globalPlaybackRate);
286
233
  const diff = Math.abs(newAudioSyncAnchor - this.audioSyncAnchor);
287
- if (diff > 0.1) {
288
- this.setPlaybackTime(newTime);
289
- }
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
- }
234
+ if (diff > 0.04) {
235
+ this.setPlaybackTime(newTime, this.playbackRate * this.globalPlaybackRate);
346
236
  }
237
+ await this.videoIteratorManager?.seek({
238
+ newTime,
239
+ nonce,
240
+ });
241
+ await this.audioIteratorManager?.seek({
242
+ newTime,
243
+ nonce,
244
+ fps: this.fps,
245
+ playbackRate: this.playbackRate * this.globalPlaybackRate,
246
+ getIsPlaying: () => this.playing,
247
+ scheduleAudioNode: this.scheduleAudioNode,
248
+ });
347
249
  }
348
250
  async play(time) {
349
- if (!this.isReady())
350
- return;
351
- this.setPlaybackTime(time);
251
+ const newTime = getTimeInSeconds({
252
+ unloopedTimeInSeconds: time,
253
+ playbackRate: this.playbackRate,
254
+ loop: this.loop,
255
+ trimBefore: this.trimBefore,
256
+ trimAfter: this.trimAfter,
257
+ mediaDurationInSeconds: this.totalDuration ?? null,
258
+ fps: this.fps,
259
+ ifNoMediaDuration: 'infinity',
260
+ src: this.src,
261
+ });
262
+ if (newTime === null) {
263
+ throw new Error(`should have asserted that the time is not null`);
264
+ }
265
+ this.setPlaybackTime(newTime, this.playbackRate * this.globalPlaybackRate);
352
266
  this.playing = true;
353
- for (const chunk of this.audioChunksForAfterResuming) {
354
- this.scheduleAudioChunk(chunk.buffer, chunk.timestamp);
267
+ if (this.audioIteratorManager) {
268
+ this.audioIteratorManager.resumeScheduledAudioChunks({
269
+ playbackRate: this.playbackRate * this.globalPlaybackRate,
270
+ scheduleAudioNode: this.scheduleAudioNode,
271
+ });
355
272
  }
356
273
  if (this.sharedAudioContext.state === 'suspended') {
357
274
  await this.sharedAudioContext.resume();
358
275
  }
359
- this.audioChunksForAfterResuming.length = 0;
360
276
  this.drawDebugOverlay();
361
277
  }
362
278
  pause() {
363
279
  this.playing = false;
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
- }
280
+ this.audioIteratorManager?.pausePlayback();
373
281
  this.drawDebugOverlay();
374
282
  }
375
283
  setMuted(muted) {
376
- this.muted = muted;
377
- if (this.gainNode) {
378
- this.gainNode.gain.value = muted ? 0 : this.currentVolume;
379
- }
284
+ this.audioIteratorManager?.setMuted(muted);
380
285
  }
381
286
  setVolume(volume) {
382
- if (!this.gainNode) {
287
+ if (!this.audioIteratorManager) {
383
288
  return;
384
289
  }
385
- const appliedVolume = Math.max(0, volume);
386
- this.currentVolume = appliedVolume;
387
- if (!this.muted) {
388
- this.gainNode.gain.value = appliedVolume;
389
- }
290
+ this.audioIteratorManager.setVolume(volume);
291
+ }
292
+ setTrimBefore(trimBefore) {
293
+ this.trimBefore = trimBefore;
294
+ }
295
+ setTrimAfter(trimAfter) {
296
+ this.trimAfter = trimAfter;
390
297
  }
391
298
  setDebugOverlay(debugOverlay) {
392
299
  this.debugOverlay = debugOverlay;
393
300
  }
301
+ updateAfterPlaybackRateChange() {
302
+ if (!this.audioIteratorManager) {
303
+ return;
304
+ }
305
+ this.setPlaybackTime(this.getPlaybackTime(), this.playbackRate * this.globalPlaybackRate);
306
+ const iterator = this.audioIteratorManager.getAudioBufferIterator();
307
+ if (!iterator) {
308
+ return;
309
+ }
310
+ iterator.moveQueuedChunksToPauseQueue();
311
+ if (this.playing) {
312
+ this.audioIteratorManager.resumeScheduledAudioChunks({
313
+ playbackRate: this.playbackRate * this.globalPlaybackRate,
314
+ scheduleAudioNode: this.scheduleAudioNode,
315
+ });
316
+ }
317
+ }
394
318
  setPlaybackRate(rate) {
395
319
  this.playbackRate = rate;
320
+ this.updateAfterPlaybackRateChange();
321
+ }
322
+ setGlobalPlaybackRate(rate) {
323
+ this.globalPlaybackRate = rate;
324
+ this.updateAfterPlaybackRateChange();
396
325
  }
397
326
  setFps(fps) {
398
327
  this.fps = fps;
399
328
  }
329
+ setIsPremounting(isPremounting) {
330
+ this.isPremounting = isPremounting;
331
+ }
332
+ setIsPostmounting(isPostmounting) {
333
+ this.isPostmounting = isPostmounting;
334
+ }
400
335
  setLoop(loop) {
401
336
  this.loop = loop;
402
337
  }
403
338
  async dispose() {
404
- this.initialized = false;
405
339
  if (this.initializationPromise) {
406
340
  try {
407
341
  // wait for the init to finished
@@ -413,63 +347,24 @@ export class MediaPlayer {
413
347
  // Ignore initialization errors during disposal
414
348
  }
415
349
  }
416
- this.input?.dispose();
417
- this.videoFrameIterator?.destroy();
418
- this.videoFrameIterator = null;
419
- this.audioBufferIterator?.destroy();
420
- this.audioBufferIterator = null;
350
+ // Mark all async operations as stale
351
+ this.nonceManager.createAsyncOperation();
352
+ this.videoIteratorManager?.destroy();
353
+ this.audioIteratorManager?.destroy();
354
+ this.input.dispose();
421
355
  }
422
356
  getPlaybackTime() {
423
- return this.sharedAudioContext.currentTime - this.audioSyncAnchor;
424
- }
425
- setPlaybackTime(time) {
426
- this.audioSyncAnchor = this.sharedAudioContext.currentTime - time;
357
+ return calculatePlaybackTime({
358
+ audioSyncAnchor: this.audioSyncAnchor,
359
+ currentTime: this.sharedAudioContext.currentTime,
360
+ playbackRate: this.playbackRate * this.globalPlaybackRate,
361
+ });
427
362
  }
428
- scheduleAudioChunk(buffer, mediaTimestamp) {
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;
433
- const node = this.sharedAudioContext.createBufferSource();
434
- node.buffer = buffer;
435
- node.playbackRate.value = this.playbackRate;
436
- node.connect(this.gainNode);
437
- if (delay >= 0) {
438
- node.start(targetTime + this.audioSyncAnchor);
439
- }
440
- else {
441
- node.start(this.sharedAudioContext.currentTime, -delay);
442
- }
443
- this.audioBufferIterator.addQueuedAudioNode(node, mediaTimestamp, buffer);
444
- node.onended = () => {
445
- return this.audioBufferIterator.removeQueuedAudioNode(node);
446
- };
363
+ setPlaybackTime(time, playbackRate) {
364
+ this.audioSyncAnchor =
365
+ this.sharedAudioContext.currentTime - time / playbackRate;
447
366
  }
448
- onVideoFrame(callback) {
367
+ setVideoFrameCallback(callback) {
449
368
  this.onVideoFrameCallback = callback;
450
- if (this.initialized && callback && this.canvas) {
451
- callback(this.canvas);
452
- }
453
- return () => {
454
- if (this.onVideoFrameCallback === callback) {
455
- this.onVideoFrameCallback = undefined;
456
- }
457
- };
458
- }
459
- drawDebugOverlay() {
460
- if (!this.debugOverlay)
461
- return;
462
- if (this.context && this.canvas) {
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
- });
473
- }
474
369
  }
475
370
  }
@@ -0,0 +1,9 @@
1
+ export type Nonce = {
2
+ isStale: () => boolean;
3
+ };
4
+ export type NonceManager = ReturnType<typeof makeNonceManager>;
5
+ export declare const makeNonceManager: () => {
6
+ createAsyncOperation: () => {
7
+ isStale: () => boolean;
8
+ };
9
+ };
@@ -0,0 +1,13 @@
1
+ export const makeNonceManager = () => {
2
+ let nonce = 0;
3
+ const createAsyncOperation = () => {
4
+ nonce++;
5
+ const currentNonce = nonce;
6
+ return {
7
+ isStale: () => nonce !== currentNonce,
8
+ };
9
+ };
10
+ return {
11
+ createAsyncOperation,
12
+ };
13
+ };