Skip to content

Commit 4c254b8

Browse files
authored
Merge pull request #5342 from remotion-dev/decoder-async-methods
2 parents 12f78d0 + f5c2672 commit 4c254b8

File tree

8 files changed

+264
-19
lines changed

8 files changed

+264
-19
lines changed

packages/docs/docs/webcodecs/create-audio-decoder.mdx

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const decoder = createAudioDecoder({
3636
## Differences to `AudioDecoder`
3737

3838
- Samples with a `codec` of `pcm-s16` are accepted and passed through, even if the `AudioDecoder` object does not exist or support it.
39-
- Two new methods are added: [`.waitForQueueToBeLessThan()`](#waitforqueuetobelessthanqueue-size-number) and [`.waitForFinish()`](#waitforfinish).
39+
- Two new methods are added: [`.waitForQueueToBeLessThan()`](#waitforqueuetobelessthan) and [`.waitForFinish()`](#waitforfinish).
4040
- The [`dequeue`](https://developer.mozilla.org/en-US/docs/Web/API/AudioDecoder/dequeue_event) event is not supported as it is not reliable across browsers.
4141
- In addition to [`EncodedAudioChunk`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedAudioChunk), [`EncodedAudioChunkInit`](https://www.w3.org/TR/webcodecs/#dictdef-encodedaudiochunkinit) objects are also accepted for [`.decode()`](#decode).
4242
- A [`webcodecsController()`](/docs/webcodecs/webcodecs-controller) instance can be passed in to the function, allowing for decoding to be paused, resumed and aborted.
@@ -96,7 +96,9 @@ Decodes a sample. Same as [`AudioDecoder.decode()`](https://developer.mozilla.or
9696
You can pass in a [`MediaParserAudioSample`](/docs/media-parser/types#mediaparseraudiosample) object from [`parseMedia()`](/docs/media-parser/parse-media), which also satisfies the [`EncodedAudioChunkInit`](https://www.w3.org/TR/webcodecs/#dictdef-encodedaudiochunkinit) interface.
9797

9898

99-
### `waitForQueueToBeLessThan(queueSize: number)`
99+
### `waitForQueueToBeLessThan()`
100+
101+
Pass a number to wait for the queue to be less than the given number.
100102

101103
A promise that resolves when the queue size is less than the given number.
102104
The queue is only decremented when the[ `onFrame`](#onframe) callback resolves.
@@ -113,6 +115,15 @@ Clears the queue and resets the decoder. Same as [`AudioDecoder.reset()`](https:
113115

114116
Closes the decoder. Same as [`AudioDecoder.close()`](https://developer.mozilla.org/en-US/docs/Web/API/AudioDecoder/close).
115117

118+
### `checkReset()`<AvailableFrom v="4.0.312" />
119+
120+
Returns a handle with a `wasReset()` function. If the decoder was reset inbetween the call to `.checkReset()` and the call to `wasReset()`, `wasReset()` will return `true`. See [below](#checking-if-the-decoder-was-reset) for an example.
121+
122+
### `getMostRecentSampleInput()`<AvailableFrom v="4.0.312" />
123+
124+
Return the `.timestamp` of the most recently input sample.
125+
126+
116127
## Example usage with `@remotion/media-parser`
117128

118129
In this example, the whole audio track is decoded and the decoder is closed when the track is done.
@@ -146,6 +157,52 @@ await parseMedia({
146157
});
147158
```
148159

160+
## Checking if the decoder was reset
161+
162+
A potential race condition you may face is that `decoder.reset()` is called while a sample is waiting for the queue to be less than a certain number. Use `.checkReset()` to check if the decoder was reset after any asynchronous operation, and abort the processing of the sample if needed.
163+
164+
```tsx twoslash title="Check if the decoder was reset"
165+
import {parseMedia} from '@remotion/media-parser';
166+
import {createAudioDecoder} from '@remotion/webcodecs';
167+
168+
await parseMedia({
169+
src: 'https://parser.media/video.mp4',
170+
onAudioTrack: ({track, container}) => {
171+
172+
const decoder = createAudioDecoder({
173+
track,
174+
onFrame: console.log,
175+
onError: console.error,
176+
});
177+
178+
return async (sample) => {
179+
const {wasReset} = decoder.checkReset();
180+
181+
await decoder.waitForQueueToBeLessThan(10);
182+
if (wasReset()) {
183+
return
184+
}
185+
186+
await decoder.decode(sample);
187+
if (wasReset()) {
188+
return;
189+
}
190+
191+
192+
return async () => {
193+
if (wasReset()) {
194+
return;
195+
}
196+
197+
// Called when the track is done
198+
await decoder.flush();
199+
decoder.close()
200+
};
201+
};
202+
},
203+
});
204+
```
205+
149206

150207
## See also
151208

packages/docs/docs/webcodecs/create-video-decoder.mdx

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const decoder = createVideoDecoder({
3535

3636
## Differences to `VideoDecoder`
3737

38-
- Two new methods are added: [`.waitForQueueToBeLessThan()`](#waitforqueuetobelessthanqueue-size-number) and [`.waitForFinish()`](#waitforfinish).
38+
- Two new methods are added: [`.waitForQueueToBeLessThan()`](#waitforqueuetobelessthan) and [`.waitForFinish()`](#waitforfinish).
3939
- The [`dequeue`](https://developer.mozilla.org/en-US/docs/Web/API/VideoDecoder/dequeue_event) event is not supported as it is not reliable across browsers.
4040
- In addition to [`EncodedVideoChunk`](https://developer.mozilla.org/en-US/docs/Web/API/EncodedVideoChunk), [`EncodedVideoChunkInit`](https://www.w3.org/TR/webcodecs/#dictdef-encodedvideochunkinit) objects are also accepted for [`.decode()`](#decode).
4141
- A [`webcodecsController()`](/docs/webcodecs/webcodecs-controller) instance can be passed in to the function, allowing for decoding to be paused, resumed and aborted.
@@ -86,7 +86,9 @@ Default value: `"info"`, which logs only important information.
8686

8787
Returns an object with the following properties:
8888

89-
### `waitForQueueToBeLessThan(queueSize: number)`
89+
### `waitForQueueToBeLessThan()`
90+
91+
Pass a number to wait for the queue to be less than the given number.
9092

9193
A promise that resolves when the queue size is less than the given number.
9294
The queue is only decremented when the[ `onFrame`](#onframe) callback resolves.
@@ -103,6 +105,14 @@ Clears the queue and resets the decoder. Same as [`VideoDecoder.reset()`](https:
103105

104106
Closes the decoder. Same as [`AudioDecoder.close()`](https://developer.mozilla.org/en-US/docs/Web/API/AudioDecoder/close).
105107

108+
### `checkReset()`<AvailableFrom v="4.0.312" />
109+
110+
Returns a handle with a `wasReset()` function. If the decoder was reset inbetween the call to `.checkReset()` and the call to `wasReset()`, `wasReset()` will return `true`. See [below](#checking-if-the-decoder-was-reset) for an example.
111+
112+
### `getMostRecentSampleInput()`<AvailableFrom v="4.0.312" />
113+
114+
Return the `.timestamp` of the most recently input sample.
115+
106116
## Example usage with `@remotion/media-parser`
107117

108118
```tsx twoslash title="Decode a video track"
@@ -133,6 +143,50 @@ await parseMedia({
133143
});
134144
```
135145

146+
## Checking if the decoder was reset
147+
148+
A potential race condition you may face is that `decoder.reset()` is called while a sample is waiting for the queue to be less than a certain number. Use `.checkReset()` to check if the decoder was reset after any asynchronous operation, and abort the processing of the sample if needed.
149+
150+
```tsx twoslash title="Check if the decoder was reset"
151+
import {parseMedia} from '@remotion/media-parser';
152+
import {createVideoDecoder} from '@remotion/webcodecs';
153+
154+
await parseMedia({
155+
src: 'https://parser.media/video.mp4',
156+
onVideoTrack: ({track, container}) => {
157+
const decoder = createVideoDecoder({
158+
track,
159+
onFrame: console.log,
160+
onError: console.error,
161+
});
162+
163+
return async (sample) => {
164+
const {wasReset} = decoder.checkReset();
165+
166+
await decoder.waitForQueueToBeLessThan(10);
167+
if (wasReset()) {
168+
return
169+
}
170+
171+
await decoder.decode(sample);
172+
if (wasReset()) {
173+
return;
174+
}
175+
176+
return async () => {
177+
if (wasReset()) {
178+
return;
179+
}
180+
181+
// Called when the track is done
182+
await decoder.flush();
183+
decoder.close()
184+
};
185+
};
186+
},
187+
});
188+
```
189+
136190
## See also
137191

138192
- [Source code for this function](https://github.com/remotion-dev/remotion/blob/main/packages/webcodecs/src/create-video-decoder.ts)

packages/media-parser/src/webcodec-sample-types.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@ import type {MediaParserContainer} from './options';
33

44
export type MediaParserOnAudioSample = (
55
sample: MediaParserAudioSample,
6-
) => void | Promise<void> | OnTrackDoneCallback | Promise<OnTrackDoneCallback>;
6+
) =>
7+
| void
8+
| Promise<OnTrackDoneCallback | void>
9+
| Promise<void>
10+
| OnTrackDoneCallback;
711

812
export type MediaParserOnVideoSample = (
913
sample: MediaParserVideoSample,
10-
) => void | Promise<void> | OnTrackDoneCallback | Promise<OnTrackDoneCallback>;
14+
) =>
15+
| void
16+
| Promise<OnTrackDoneCallback | void>
17+
| Promise<void>
18+
| OnTrackDoneCallback;
1119

1220
export type OnTrackDoneCallback = () => void | Promise<void>;
1321

packages/webcodecs/src/create-audio-decoder.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type {MediaParserLogLevel} from '@remotion/media-parser';
2+
import type {FlushPending} from './flush-pending';
3+
import {makeFlushPending} from './flush-pending';
24
import {getWaveAudioDecoder} from './get-wave-audio-decoder';
35
import {makeIoSynchronizer} from './io-manager/io-synchronizer';
46
import type {WebCodecsController} from './webcodecs-controller';
@@ -11,6 +13,10 @@ export type WebCodecsAudioDecoder = {
1113
flush: () => Promise<void>;
1214
waitForQueueToBeLessThan: (items: number) => Promise<void>;
1315
reset: () => void;
16+
checkReset: () => {
17+
wasReset: () => boolean;
18+
};
19+
getMostRecentSampleInput: () => number | null;
1420
};
1521

1622
export type CreateAudioDecoderInit = {
@@ -41,6 +47,8 @@ export const internalCreateAudioDecoder = ({
4147
controller,
4248
});
4349

50+
let mostRecentSampleReceived: number | null = null;
51+
4452
if (config.codec === 'pcm-s16') {
4553
return getWaveAudioDecoder({
4654
onFrame,
@@ -111,6 +119,8 @@ export const internalCreateAudioDecoder = ({
111119
return;
112120
}
113121

122+
mostRecentSampleReceived = audioSample.timestamp;
123+
114124
// Don't flush, it messes up the audio
115125

116126
const chunk =
@@ -129,22 +139,47 @@ export const internalCreateAudioDecoder = ({
129139
}
130140
};
131141

142+
let flushPending: FlushPending | null = null;
143+
const lastReset: number | null = null;
144+
132145
return {
133146
decode,
134147
close,
135-
flush: async () => {
136-
// Firefox might throw "Needs to be configured first"
137-
try {
138-
await audioDecoder.flush();
139-
} catch {}
148+
flush: () => {
149+
if (flushPending) {
150+
throw new Error('Flush already pending');
151+
}
140152

141-
await ioSynchronizer.waitForQueueSize(0);
153+
const pendingFlush = makeFlushPending();
154+
flushPending = pendingFlush;
155+
Promise.resolve()
156+
.then(() => {
157+
return audioDecoder.flush();
158+
})
159+
.catch(() => {
160+
// Firefox might throw "Needs to be configured first"
161+
})
162+
.finally(() => {
163+
pendingFlush.resolve();
164+
flushPending = null;
165+
});
166+
167+
return pendingFlush.promise;
142168
},
143169
waitForQueueToBeLessThan: ioSynchronizer.waitForQueueSize,
144170
reset: () => {
145171
audioDecoder.reset();
146172
audioDecoder.configure(config);
147173
},
174+
checkReset: () => {
175+
const initTime = Date.now();
176+
return {
177+
wasReset: () => lastReset !== null && lastReset > initTime,
178+
};
179+
},
180+
getMostRecentSampleInput() {
181+
return mostRecentSampleReceived;
182+
},
148183
};
149184
};
150185

packages/webcodecs/src/create-video-decoder.ts

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type {MediaParserLogLevel} from '@remotion/media-parser';
2+
import type {FlushPending} from './flush-pending';
3+
import {makeFlushPending} from './flush-pending';
24
import {makeIoSynchronizer} from './io-manager/io-synchronizer';
35
import type {WebCodecsController} from './webcodecs-controller';
46

@@ -10,6 +12,10 @@ export type WebCodecsVideoDecoder = {
1012
flush: () => Promise<void>;
1113
waitForQueueToBeLessThan: (items: number) => Promise<void>;
1214
reset: () => void;
15+
checkReset: () => {
16+
wasReset: () => boolean;
17+
};
18+
getMostRecentSampleInput: () => number | null;
1319
};
1420

1521
export const internalCreateVideoDecoder = ({
@@ -25,12 +31,21 @@ export const internalCreateVideoDecoder = ({
2531
config: VideoDecoderConfig;
2632
logLevel: MediaParserLogLevel;
2733
}): WebCodecsVideoDecoder => {
34+
if (
35+
controller &&
36+
controller._internals._mediaParserController._internals.signal.aborted
37+
) {
38+
throw new Error('Not creating audio decoder, already aborted');
39+
}
40+
2841
const ioSynchronizer = makeIoSynchronizer({
2942
logLevel,
3043
label: 'Video decoder',
3144
controller,
3245
});
3346

47+
let mostRecentSampleReceived: number | null = null;
48+
3449
const videoDecoder = new VideoDecoder({
3550
async output(frame) {
3651
try {
@@ -88,6 +103,8 @@ export const internalCreateVideoDecoder = ({
88103
return;
89104
}
90105

106+
mostRecentSampleReceived = sample.timestamp;
107+
91108
const encodedChunk =
92109
sample instanceof EncodedVideoChunk
93110
? sample
@@ -96,22 +113,50 @@ export const internalCreateVideoDecoder = ({
96113
ioSynchronizer.inputItem(sample.timestamp);
97114
};
98115

116+
let flushPending: FlushPending | null = null;
117+
let lastReset: number | null = null;
118+
99119
return {
100120
decode,
101121
close,
102-
flush: async () => {
103-
// Firefox might throw "Needs to be configured first"
104-
try {
105-
await videoDecoder.flush();
106-
} catch {}
122+
flush: () => {
123+
if (flushPending) {
124+
throw new Error('Flush already pending');
125+
}
107126

108-
await ioSynchronizer.waitForQueueSize(0);
127+
const pendingFlush = makeFlushPending();
128+
flushPending = pendingFlush;
129+
Promise.resolve()
130+
.then(() => {
131+
return videoDecoder.flush();
132+
})
133+
.catch(() => {
134+
// Firefox might throw "Needs to be configured first"
135+
})
136+
.finally(() => {
137+
pendingFlush.resolve();
138+
flushPending = null;
139+
});
140+
141+
return pendingFlush.promise;
109142
},
110143
waitForQueueToBeLessThan: ioSynchronizer.waitForQueueSize,
111144
reset: () => {
145+
lastReset = Date.now();
146+
flushPending?.resolve();
147+
ioSynchronizer.clearQueue();
112148
videoDecoder.reset();
113149
videoDecoder.configure(config);
114150
},
151+
checkReset: () => {
152+
const initTime = Date.now();
153+
return {
154+
wasReset: () => lastReset !== null && lastReset > initTime,
155+
};
156+
},
157+
getMostRecentSampleInput() {
158+
return mostRecentSampleReceived;
159+
},
115160
};
116161
};
117162

0 commit comments

Comments
 (0)