@remotion/media 4.0.401 → 4.0.403

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.
@@ -4,7 +4,7 @@ import { extractFrame } from './video-extraction/extract-frame';
4
4
  import { rotateFrame } from './video-extraction/rotate-frame';
5
5
  export const extractFrameAndAudio = async ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, loop, audioStreamIndex, trimAfter, trimBefore, fps, maxCacheSize, }) => {
6
6
  try {
7
- const [frame, audio] = await Promise.all([
7
+ const [video, audio] = await Promise.all([
8
8
  includeVideo
9
9
  ? extractFrame({
10
10
  src,
@@ -34,59 +34,44 @@ export const extractFrameAndAudio = async ({ src, timeInSeconds, logLevel, durat
34
34
  })
35
35
  : null,
36
36
  ]);
37
- if (frame?.type === 'cannot-decode') {
37
+ if (video?.type === 'cannot-decode') {
38
38
  return {
39
39
  type: 'cannot-decode',
40
- durationInSeconds: frame.durationInSeconds,
40
+ durationInSeconds: video.durationInSeconds,
41
41
  };
42
42
  }
43
- if (frame?.type === 'unknown-container-format') {
43
+ if (video?.type === 'unknown-container-format') {
44
44
  return { type: 'unknown-container-format' };
45
45
  }
46
- if (frame?.type === 'cannot-decode-alpha') {
46
+ if (video?.type === 'cannot-decode-alpha') {
47
47
  return {
48
48
  type: 'cannot-decode-alpha',
49
- durationInSeconds: frame.durationInSeconds,
49
+ durationInSeconds: video.durationInSeconds,
50
50
  };
51
51
  }
52
- if (frame?.type === 'network-error') {
52
+ if (video?.type === 'network-error') {
53
53
  return { type: 'network-error' };
54
54
  }
55
55
  if (audio === 'unknown-container-format') {
56
- if (frame !== null) {
57
- frame?.frame?.close();
58
- }
59
56
  return { type: 'unknown-container-format' };
60
57
  }
61
58
  if (audio === 'network-error') {
62
- if (frame !== null) {
63
- frame?.frame?.close();
64
- }
65
59
  return { type: 'network-error' };
66
60
  }
67
61
  if (audio === 'cannot-decode') {
68
- if (frame?.type === 'success' && frame.frame !== null) {
69
- frame?.frame.close();
70
- }
71
62
  return {
72
63
  type: 'cannot-decode',
73
- durationInSeconds: frame?.type === 'success' ? frame.durationInSeconds : null,
74
- };
75
- }
76
- if (!frame?.frame) {
77
- return {
78
- type: 'success',
79
- frame: null,
80
- audio: audio?.data ?? null,
81
- durationInSeconds: audio?.durationInSeconds ?? null,
64
+ durationInSeconds: video?.type === 'success' ? video.durationInSeconds : null,
82
65
  };
83
66
  }
84
67
  return {
85
68
  type: 'success',
86
- frame: await rotateFrame({
87
- frame: frame.frame.toVideoFrame(),
88
- rotation: frame.frame.rotation,
89
- }),
69
+ frame: video?.sample
70
+ ? await rotateFrame({
71
+ frame: video.sample.toVideoFrame(),
72
+ rotation: video.sample.rotation,
73
+ })
74
+ : null,
90
75
  audio: audio?.data ?? null,
91
76
  durationInSeconds: audio?.durationInSeconds ?? null,
92
77
  };
@@ -46,7 +46,7 @@ export declare class MediaPlayer {
46
46
  canvas: HTMLCanvasElement | OffscreenCanvas | null;
47
47
  src: string;
48
48
  logLevel: LogLevel;
49
- sharedAudioContext: AudioContext;
49
+ sharedAudioContext: AudioContext | null;
50
50
  loop: boolean;
51
51
  trimBefore: number | undefined;
52
52
  trimAfter: number | undefined;
@@ -78,7 +78,7 @@ export declare class MediaPlayer {
78
78
  setTrimBefore(trimBefore: number | undefined, unloopedTimeInSeconds: number): void;
79
79
  setTrimAfter(trimAfter: number | undefined, unloopedTimeInSeconds: number): void;
80
80
  setDebugOverlay(debugOverlay: boolean): void;
81
- private updateAfterPlaybackRateChange;
81
+ private updateAudioTimeAfterPlaybackRateChange;
82
82
  setPlaybackRate(rate: number): void;
83
83
  setGlobalPlaybackRate(rate: number): void;
84
84
  setFps(fps: number): void;
@@ -87,7 +87,7 @@ export declare class MediaPlayer {
87
87
  setLoop(loop: boolean): void;
88
88
  dispose(): Promise<void>;
89
89
  private scheduleAudioNode;
90
- private getPlaybackTime;
90
+ private getAudioPlaybackTime;
91
91
  private setPlaybackTime;
92
92
  setVideoFrameCallback(callback: null | ((frame: CanvasImageSource) => void)): void;
93
93
  private drawDebugOverlay;
@@ -29,9 +29,12 @@ export class MediaPlayer {
29
29
  return this.bufferState.delayPlayback();
30
30
  };
31
31
  this.scheduleAudioNode = (node, mediaTimestamp) => {
32
- const currentTime = this.getPlaybackTime();
32
+ const currentTime = this.getAudioPlaybackTime();
33
33
  const delayWithoutPlaybackRate = mediaTimestamp - currentTime;
34
34
  const delay = delayWithoutPlaybackRate / (this.playbackRate * this.globalPlaybackRate);
35
+ if (!this.sharedAudioContext) {
36
+ throw new Error('Shared audio context not found');
37
+ }
35
38
  if (delay >= 0) {
36
39
  node.start(this.sharedAudioContext.currentTime + delay);
37
40
  }
@@ -45,8 +48,8 @@ export class MediaPlayer {
45
48
  if (this.context && this.canvas) {
46
49
  drawPreviewOverlay({
47
50
  context: this.context,
48
- audioTime: this.sharedAudioContext.currentTime,
49
- audioContextState: this.sharedAudioContext.state,
51
+ audioTime: this.sharedAudioContext?.currentTime ?? null,
52
+ audioContextState: this.sharedAudioContext?.state ?? null,
50
53
  audioSyncAnchor: this.audioSyncAnchor,
51
54
  audioIteratorManager: this.audioIteratorManager,
52
55
  playing: this.playing,
@@ -56,7 +59,7 @@ export class MediaPlayer {
56
59
  };
57
60
  this.canvas = canvas ?? null;
58
61
  this.src = src;
59
- this.logLevel = logLevel ?? window.remotion_logLevel;
62
+ this.logLevel = logLevel;
60
63
  this.sharedAudioContext = sharedAudioContext;
61
64
  this.playbackRate = playbackRate;
62
65
  this.globalPlaybackRate = globalPlaybackRate;
@@ -181,7 +184,7 @@ export class MediaPlayer {
181
184
  throw new Error(`should have asserted that the time is not null`);
182
185
  }
183
186
  this.setPlaybackTime(startTime, this.playbackRate * this.globalPlaybackRate);
184
- if (audioTrack) {
187
+ if (audioTrack && this.sharedAudioContext) {
185
188
  this.audioIteratorManager = audioIteratorManager({
186
189
  audioTrack,
187
190
  delayPlaybackHandleIfNotPremounting: this.delayPlaybackHandleIfNotPremounting,
@@ -254,24 +257,25 @@ export class MediaPlayer {
254
257
  if (nonce.isStale()) {
255
258
  return;
256
259
  }
257
- const currentPlaybackTime = this.getPlaybackTime();
258
- if (currentPlaybackTime === newTime) {
259
- return;
260
- }
260
+ const shouldSeekAudio = this.audioIteratorManager &&
261
+ this.sharedAudioContext &&
262
+ this.getAudioPlaybackTime() !== newTime;
261
263
  await Promise.all([
262
264
  this.videoIteratorManager?.seek({
263
265
  newTime,
264
266
  nonce,
265
267
  }),
266
- this.audioIteratorManager?.seek({
267
- newTime,
268
- nonce,
269
- fps: this.fps,
270
- playbackRate: this.playbackRate * this.globalPlaybackRate,
271
- getIsPlaying: () => this.playing,
272
- scheduleAudioNode: this.scheduleAudioNode,
273
- bufferState: this.bufferState,
274
- }),
268
+ shouldSeekAudio
269
+ ? this.audioIteratorManager?.seek({
270
+ newTime,
271
+ nonce,
272
+ fps: this.fps,
273
+ playbackRate: this.playbackRate * this.globalPlaybackRate,
274
+ getIsPlaying: () => this.playing,
275
+ scheduleAudioNode: this.scheduleAudioNode,
276
+ bufferState: this.bufferState,
277
+ })
278
+ : null,
275
279
  ]);
276
280
  }
277
281
  async play(time) {
@@ -300,7 +304,8 @@ export class MediaPlayer {
300
304
  scheduleAudioNode: this.scheduleAudioNode,
301
305
  });
302
306
  }
303
- if (this.sharedAudioContext.state === 'suspended') {
307
+ if (this.sharedAudioContext &&
308
+ this.sharedAudioContext.state === 'suspended') {
304
309
  await this.sharedAudioContext.resume();
305
310
  }
306
311
  this.drawDebugOverlay();
@@ -359,11 +364,14 @@ export class MediaPlayer {
359
364
  setDebugOverlay(debugOverlay) {
360
365
  this.debugOverlay = debugOverlay;
361
366
  }
362
- updateAfterPlaybackRateChange() {
367
+ updateAudioTimeAfterPlaybackRateChange() {
363
368
  if (!this.audioIteratorManager) {
364
369
  return;
365
370
  }
366
- this.setPlaybackTime(this.getPlaybackTime(), this.playbackRate * this.globalPlaybackRate);
371
+ if (!this.sharedAudioContext) {
372
+ return;
373
+ }
374
+ this.setPlaybackTime(this.getAudioPlaybackTime(), this.playbackRate * this.globalPlaybackRate);
367
375
  const iterator = this.audioIteratorManager.getAudioBufferIterator();
368
376
  if (!iterator) {
369
377
  return;
@@ -378,11 +386,11 @@ export class MediaPlayer {
378
386
  }
379
387
  setPlaybackRate(rate) {
380
388
  this.playbackRate = rate;
381
- this.updateAfterPlaybackRateChange();
389
+ this.updateAudioTimeAfterPlaybackRateChange();
382
390
  }
383
391
  setGlobalPlaybackRate(rate) {
384
392
  this.globalPlaybackRate = rate;
385
- this.updateAfterPlaybackRateChange();
393
+ this.updateAudioTimeAfterPlaybackRateChange();
386
394
  }
387
395
  setFps(fps) {
388
396
  this.fps = fps;
@@ -414,7 +422,10 @@ export class MediaPlayer {
414
422
  this.audioIteratorManager?.destroyIterator();
415
423
  this.input.dispose();
416
424
  }
417
- getPlaybackTime() {
425
+ getAudioPlaybackTime() {
426
+ if (!this.sharedAudioContext) {
427
+ throw new Error('Shared audio context not found');
428
+ }
418
429
  return calculatePlaybackTime({
419
430
  audioSyncAnchor: this.audioSyncAnchor,
420
431
  currentTime: this.sharedAudioContext.currentTime,
@@ -422,6 +433,9 @@ export class MediaPlayer {
422
433
  });
423
434
  }
424
435
  setPlaybackTime(time, playbackRate) {
436
+ if (!this.sharedAudioContext) {
437
+ return;
438
+ }
425
439
  this.audioSyncAnchor =
426
440
  this.sharedAudioContext.currentTime - time / playbackRate;
427
441
  }
@@ -79,8 +79,6 @@ const VideoForPreviewAssertedShowing = ({ src: unpreloadedSrc, style, playbackRa
79
79
  useEffect(() => {
80
80
  if (!sharedAudioContext)
81
81
  return;
82
- if (!sharedAudioContext.audioContext)
83
- return;
84
82
  try {
85
83
  const player = new MediaPlayer({
86
84
  canvas: canvasRef.current,
@@ -27,9 +27,7 @@ const InnerVideo = ({ src, audioStreamIndex, className, delayRenderRetries, dela
27
27
  return (_jsx(VideoForPreview, { audioStreamIndex: audioStreamIndex ?? 0, className: className, name: name, logLevel: logLevel, loop: loop, loopVolumeCurveBehavior: loopVolumeCurveBehavior, muted: muted, onVideoFrame: onVideoFrame, playbackRate: playbackRate, src: src, style: style, volume: volume, showInTimeline: showInTimeline, trimAfter: trimAfterValue, trimBefore: trimBeforeValue, stack: stack ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps, debugOverlay: debugOverlay ?? false, headless: headless ?? false }));
28
28
  };
29
29
  export const Video = ({ src, audioStreamIndex, className, delayRenderRetries, delayRenderTimeoutInMilliseconds, disallowFallbackToOffthreadVideo, fallbackOffthreadVideoProps, logLevel, loop, loopVolumeCurveBehavior, muted, name, onVideoFrame, playbackRate, showInTimeline, style, trimAfter, trimBefore, volume, stack, toneFrequency, debugOverlay, headless, }) => {
30
- return (_jsx(InnerVideo, { audioStreamIndex: audioStreamIndex ?? 0, className: className, delayRenderRetries: delayRenderRetries ?? null, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {}, logLevel: logLevel ??
31
- (typeof window !== 'undefined'
32
- ? (window.remotion_logLevel ?? 'info')
33
- : 'info'), loop: loop ?? false, loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? 'repeat', muted: muted ?? false, name: name, onVideoFrame: onVideoFrame, playbackRate: playbackRate ?? 1, showInTimeline: showInTimeline ?? true, src: src, style: style ?? {}, trimAfter: trimAfter, trimBefore: trimBefore, volume: volume ?? 1, toneFrequency: toneFrequency ?? 1, stack: stack, debugOverlay: debugOverlay ?? false, headless: headless ?? false }));
30
+ const fallbackLogLevel = Internals.useLogLevel();
31
+ return (_jsx(InnerVideo, { audioStreamIndex: audioStreamIndex ?? 0, className: className, delayRenderRetries: delayRenderRetries ?? null, delayRenderTimeoutInMilliseconds: delayRenderTimeoutInMilliseconds ?? null, disallowFallbackToOffthreadVideo: disallowFallbackToOffthreadVideo ?? false, fallbackOffthreadVideoProps: fallbackOffthreadVideoProps ?? {}, logLevel: logLevel ?? fallbackLogLevel, loop: loop ?? false, loopVolumeCurveBehavior: loopVolumeCurveBehavior ?? 'repeat', muted: muted ?? false, name: name, onVideoFrame: onVideoFrame, playbackRate: playbackRate ?? 1, showInTimeline: showInTimeline ?? true, src: src, style: style ?? {}, trimAfter: trimAfter, trimBefore: trimBefore, volume: volume ?? 1, toneFrequency: toneFrequency ?? 1, stack: stack, debugOverlay: debugOverlay ?? false, headless: headless ?? false }));
34
32
  };
35
33
  Internals.addSequenceStackTraces(Video);
@@ -2,7 +2,7 @@ import type { VideoSample } from 'mediabunny';
2
2
  import { type LogLevel } from 'remotion';
3
3
  type ExtractFrameResult = {
4
4
  type: 'success';
5
- frame: VideoSample | null;
5
+ sample: VideoSample | null;
6
6
  durationInSeconds: number | null;
7
7
  } | {
8
8
  type: 'cannot-decode';
@@ -23,6 +23,12 @@ const extractFrameInternal = async ({ src, timeInSeconds: unloopedTimeInSeconds,
23
23
  if (video === 'network-error') {
24
24
  return { type: 'network-error' };
25
25
  }
26
+ if (video === 'cannot-decode-alpha') {
27
+ return {
28
+ type: 'cannot-decode-alpha',
29
+ durationInSeconds: mediaDurationInSeconds,
30
+ };
31
+ }
26
32
  const timeInSeconds = getTimeInSeconds({
27
33
  loop,
28
34
  mediaDurationInSeconds,
@@ -37,7 +43,7 @@ const extractFrameInternal = async ({ src, timeInSeconds: unloopedTimeInSeconds,
37
43
  if (timeInSeconds === null) {
38
44
  return {
39
45
  type: 'success',
40
- frame: null,
46
+ sample: null,
41
47
  durationInSeconds: await sink.getDuration(),
42
48
  };
43
49
  }
@@ -46,30 +52,23 @@ const extractFrameInternal = async ({ src, timeInSeconds: unloopedTimeInSeconds,
46
52
  // Should be able to remove once upgraded to Chrome 145
47
53
  try {
48
54
  const keyframeBank = await keyframeManager.requestKeyframeBank({
49
- packetSink: video.packetSink,
50
55
  videoSampleSink: video.sampleSink,
51
56
  timestamp: timeInSeconds,
52
57
  src,
53
58
  logLevel,
54
59
  maxCacheSize,
55
60
  });
56
- if (keyframeBank === 'has-alpha') {
57
- return {
58
- type: 'cannot-decode-alpha',
59
- durationInSeconds: await sink.getDuration(),
60
- };
61
- }
62
61
  if (!keyframeBank) {
63
62
  return {
64
63
  type: 'success',
65
- frame: null,
64
+ sample: null,
66
65
  durationInSeconds: await sink.getDuration(),
67
66
  };
68
67
  }
69
68
  const frame = await keyframeBank.getFrameFromTimestamp(timeInSeconds);
70
69
  return {
71
70
  type: 'success',
72
- frame,
71
+ sample: frame,
73
72
  durationInSeconds: await sink.getDuration(),
74
73
  };
75
74
  }
@@ -1,15 +1,12 @@
1
- import type { EncodedPacket } from 'mediabunny';
2
- import { AudioSampleSink, EncodedPacketSink, VideoSampleSink } from 'mediabunny';
3
- import type { LogLevel } from 'remotion';
1
+ import { AudioSampleSink, VideoSampleSink } from 'mediabunny';
4
2
  type VideoSinks = {
5
3
  sampleSink: VideoSampleSink;
6
- packetSink: EncodedPacketSink;
7
4
  };
8
5
  type AudioSinks = {
9
6
  sampleSink: AudioSampleSink;
10
7
  };
11
8
  export type AudioSinkResult = AudioSinks | 'no-audio-track' | 'cannot-decode-audio' | 'unknown-container-format' | 'network-error';
12
- export type VideoSinkResult = VideoSinks | 'no-video-track' | 'cannot-decode' | 'unknown-container-format' | 'network-error';
9
+ export type VideoSinkResult = VideoSinks | 'no-video-track' | 'cannot-decode' | 'cannot-decode-alpha' | 'unknown-container-format' | 'network-error';
13
10
  export declare const getSinks: (src: string) => Promise<{
14
11
  getVideo: () => Promise<VideoSinkResult>;
15
12
  getAudio: (index: number) => Promise<AudioSinkResult>;
@@ -21,11 +18,4 @@ export declare const getSinks: (src: string) => Promise<{
21
18
  getDuration: () => Promise<number>;
22
19
  }>;
23
20
  export type GetSink = Awaited<ReturnType<typeof getSinks>>;
24
- export declare const getFramesSinceKeyframe: ({ packetSink, videoSampleSink, startPacket, logLevel, src, }: {
25
- packetSink: EncodedPacketSink;
26
- videoSampleSink: VideoSampleSink;
27
- startPacket: EncodedPacket;
28
- logLevel: LogLevel;
29
- src: string;
30
- }) => Promise<import("./keyframe-bank").KeyframeBank>;
31
21
  export {};
@@ -1,6 +1,6 @@
1
1
  import { ALL_FORMATS, AudioSampleSink, EncodedPacketSink, Input, MATROSKA, UrlSource, VideoSampleSink, WEBM, } from 'mediabunny';
2
+ import { canBrowserUseWebGl2 } from '../browser-can-use-webgl2';
2
3
  import { isNetworkError } from '../is-type-of-error';
3
- import { makeKeyframeBank } from './keyframe-bank';
4
4
  import { rememberActualMatroskaTimestamps } from './remember-actual-matroska-timestamps';
5
5
  const getRetryDelay = (() => {
6
6
  return null;
@@ -40,9 +40,20 @@ export const getSinks = async (src) => {
40
40
  if (!canDecode) {
41
41
  return 'cannot-decode';
42
42
  }
43
+ const sampleSink = new VideoSampleSink(videoTrack);
44
+ const packetSink = new EncodedPacketSink(videoTrack);
45
+ // Try to get the keypacket at the requested timestamp.
46
+ // If it returns null (timestamp is before the first keypacket), fall back to the first packet.
47
+ // This matches mediabunny's internal behavior and handles videos that don't start at timestamp 0.
48
+ const startPacket = await packetSink.getFirstPacket({
49
+ verifyKeyPackets: true,
50
+ });
51
+ const hasAlpha = startPacket?.sideData.alpha;
52
+ if (hasAlpha && !canBrowserUseWebGl2()) {
53
+ return 'cannot-decode-alpha';
54
+ }
43
55
  return {
44
- sampleSink: new VideoSampleSink(videoTrack),
45
- packetSink: new EncodedPacketSink(videoTrack),
56
+ sampleSink,
46
57
  };
47
58
  };
48
59
  let videoSinksPromise = null;
@@ -92,17 +103,3 @@ export const getSinks = async (src) => {
92
103
  },
93
104
  };
94
105
  };
95
- export const getFramesSinceKeyframe = async ({ packetSink, videoSampleSink, startPacket, logLevel, src, }) => {
96
- const nextKeyPacket = await packetSink.getNextKeyPacket(startPacket, {
97
- verifyKeyPackets: true,
98
- });
99
- const sampleIterator = videoSampleSink.samples(startPacket.timestamp, nextKeyPacket ? nextKeyPacket.timestamp : Infinity);
100
- const keyframeBank = makeKeyframeBank({
101
- startTimestampInSeconds: startPacket.timestamp,
102
- endTimestampInSeconds: nextKeyPacket ? nextKeyPacket.timestamp : Infinity,
103
- sampleIterator,
104
- logLevel,
105
- src,
106
- });
107
- return keyframeBank;
108
- };
@@ -1,11 +1,9 @@
1
- import type { VideoSample } from 'mediabunny';
1
+ import type { VideoSample, VideoSampleSink } from 'mediabunny';
2
2
  import { type LogLevel } from 'remotion';
3
3
  export type KeyframeBank = {
4
4
  src: string;
5
- startTimestampInSeconds: number;
6
- endTimestampInSeconds: number;
7
5
  getFrameFromTimestamp: (timestamp: number) => Promise<VideoSample | null>;
8
- prepareForDeletion: (logLevel: LogLevel) => {
6
+ prepareForDeletion: (logLevel: LogLevel, reason: string) => {
9
7
  framesDeleted: number;
10
8
  };
11
9
  deleteFramesBeforeTimestamp: ({ logLevel, timestampInSeconds, }: {
@@ -13,17 +11,21 @@ export type KeyframeBank = {
13
11
  logLevel: LogLevel;
14
12
  }) => void;
15
13
  hasTimestampInSecond: (timestamp: number) => Promise<boolean>;
16
- addFrame: (frame: VideoSample) => void;
14
+ addFrame: (frame: VideoSample, logLevel: LogLevel) => void;
17
15
  getOpenFrameCount: () => {
18
16
  size: number;
19
17
  timestamps: number[];
20
18
  };
21
19
  getLastUsed: () => number;
20
+ canSatisfyTimestamp: (timestamp: number) => boolean;
21
+ getRangeOfTimestamps: () => {
22
+ firstTimestamp: number;
23
+ lastTimestamp: number;
24
+ } | null;
22
25
  };
23
- export declare const makeKeyframeBank: ({ startTimestampInSeconds, endTimestampInSeconds, sampleIterator, logLevel: parentLogLevel, src, }: {
24
- startTimestampInSeconds: number;
25
- endTimestampInSeconds: number;
26
- sampleIterator: AsyncGenerator<VideoSample, void, unknown>;
26
+ export declare const makeKeyframeBank: ({ logLevel: parentLogLevel, src, videoSampleSink, requestedTimestamp, }: {
27
27
  logLevel: LogLevel;
28
28
  src: string;
29
- }) => KeyframeBank;
29
+ videoSampleSink: VideoSampleSink;
30
+ requestedTimestamp: number;
31
+ }) => Promise<KeyframeBank>;