Add track-specific conversion options, fix esbuild race condition

This commit is contained in:
Vanilagy
2025-08-11 00:00:04 +02:00
parent 975c1c42cb
commit 11c15c2392
6 changed files with 323 additions and 214 deletions
+4 -3
View File
@@ -24,7 +24,7 @@
chunked: true,
chunkSize: 2**20
});
const outputFormat = new Mediabunny.Mp3OutputFormat({});
const outputFormat = new Mediabunny.Mp4OutputFormat({});
const button = document.createElement('button');
button.textContent = 'Cancel';
@@ -41,6 +41,7 @@
target
}),
audio: {
discard: true,
//codec: 'opus',
//bitrate: 128000,
//numberOfChannels: 1,
@@ -72,7 +73,7 @@
bitrate: 320000
},
*/
video: {
video: () => ({
//frameRate: 27.123,
//width: 320,
//forceTranscode: true,
@@ -91,7 +92,7 @@
//height: 512,
//width: 200,
//height: 100,
},
}),
trim: {
start: 0,
end: 20
+48 -14
View File
@@ -92,11 +92,9 @@ This automatically frees up all resources used by the conversion process.
## Video options
You can set the `video` property in the conversion options to configure the converter's behavior for video tracks:
You can set the `video` property in the conversion options to configure the converter's behavior for video tracks. The options are:
```ts
type ConversionOptions = {
// ...
video?: {
type ConversionVideoOptions = {
discard?: boolean;
width?: number;
height?: number;
@@ -107,8 +105,6 @@ type ConversionOptions = {
bitrate?: number | Quality;
forceTranscode?: boolean;
};
// ...
};
```
For example, here we resize the video track to 720p:
@@ -125,7 +121,7 @@ const conversion = await Conversion.init({
```
::: info
The provided configuration will apply equally to all video tracks of the input.
The provided configuration will apply equally to all video tracks of the input. If you want to apply a separate configuration to each video track, check [track-specific options](#track-specific-options).
:::
### Discarding video
@@ -143,6 +139,8 @@ The `width`, `height` and `fit` properties control how the video is resized. If
If `width` or `height` is used in conjunction with `rotation`, they control the post-rotation dimensions.
If you want to apply max/min constraints to a video's dimensions, check out [track-specific options](#track-specific-options).
### Adjusting frame rate
The `frameRate` property can be used to set the frame rate of the output video in Hz. If not specified, the original input frame rate will be used (which may be variable).
@@ -157,11 +155,9 @@ If you want to prevent direct copying of media data and force a transcoding step
## Audio options
You can set the `audio` property in the conversion options to configure the converter's behavior for audio tracks:
You can set the `audio` property in the conversion options to configure the converter's behavior for audio tracks. The options are:
```ts
type ConversionOptions = {
// ...
audio?: {
type ConversionAudioOptions = {
discard?: boolean;
codec?: AudioCodec;
bitrate?: number | Quality;
@@ -169,8 +165,6 @@ type ConversionOptions = {
sampleRate?: number;
forceTranscode?: boolean;
};
// ...
};
```
For example, here we convert the audio track to mono and set a specific sample rate:
@@ -186,7 +180,7 @@ const conversion = await Conversion.init({
```
::: info
The provided configuration will apply equally to all audio tracks of the input.
The provided configuration will apply equally to all audio tracks of the input. If you want to apply a separate configuration to each audio track, check [track-specific options](#track-specific-options).
:::
### Discarding audio
@@ -207,6 +201,46 @@ Use the `bitrate` property to control the bitrate of the output audio. For examp
If you want to prevent direct copying of media data and force a transcoding step, use `forceTranscode: true`.
## Track-specific options
You may want to configure your video and audio options differently depending on the specifics of the input track. Or, in case a media file has multiple video or audio tracks, you may want to discard only specific tracks or configure each track separately.
For this, instead of passing an object for `video` and `audio`, you can instead pass a function:
```ts
const conversion = await Conversion.init({
input,
output,
// Function gets invoked for each video track:
video: (videoTrack, n) => {
if (n > 1) {
// Keep only the first video track
return { discard: true };
}
return {
// Shrink width to 640 only if the track is wider
width: Math.min(videoTrack.displayWidth, 640),
};
},
// Async functions work too:
audio: async (audioTrack, n) => {
if (audioTrack.languageCode !== 'rus') {
// Keep only Russian audio tracks
return { discard: true };
}
return {
codec: 'aac',
};
},
});
```
For documentation about the properties of video and audio tracks, refer to [Reading track metadata](./reading-media-files#reading-track-metadata).
## Trimming
Use the `trim` property in the conversion options to extract only a section of the input file into the output file:
+5
View File
@@ -71,13 +71,18 @@ if (cacheDir === undefined) {
throw new Error('Cache directory not found.');
}
let i = 0;
async function buildWorker(workerPath: string, extraConfig: esbuild.BuildOptions) {
const scriptNameParts = path.basename(workerPath).split('.');
scriptNameParts.pop();
scriptNameParts.push(String(i)); // To make sure it doesn't clash with other builds
scriptNameParts.push('js');
const scriptName = scriptNameParts.join('.');
const bundlePath = path.resolve(cacheDir!, scriptName);
i = (i + 1) % 32;
if (extraConfig) {
delete extraConfig.entryPoints;
delete extraConfig.outfile;
+199 -136
View File
@@ -35,22 +35,15 @@ import {
VideoSampleSource,
AudioSampleSource,
} from './media-source';
import { assert, clamp, normalizeRotation, promiseWithResolvers, Rotation } from './misc';
import { assert, clamp, MaybePromise, normalizeRotation, promiseWithResolvers, Rotation } from './misc';
import { Output, TrackType } from './output';
import { AudioSample, VideoSample } from './sample';
/**
* The options for media file conversion.
* Video-specific options.
* @public
*/
export type ConversionOptions = {
/** The input file. */
input: Input;
/** The output file. */
output: Output;
/** Video-specific options. */
video?: {
export type ConversionVideoOptions = {
/** If true, all video tracks will be discarded and will not be present in the output. */
discard?: boolean;
/**
@@ -90,8 +83,11 @@ export type ConversionOptions = {
forceTranscode?: boolean;
};
/** Audio-specific options. */
audio?: {
/**
* Audio-specific options.
* @public
*/
export type ConversionAudioOptions = {
/** If true, all audio tracks will be discarded and will not be present in the output. */
discard?: boolean;
/** The desired channel count of the output audio. */
@@ -106,6 +102,92 @@ export type ConversionOptions = {
forceTranscode?: boolean;
};
const validateVideoOptions = (videoOptions: ConversionVideoOptions | undefined) => {
if (videoOptions !== undefined && (!videoOptions || typeof videoOptions !== 'object')) {
throw new TypeError('options.video, when provided, must be an object.');
}
if (videoOptions?.discard !== undefined && typeof videoOptions.discard !== 'boolean') {
throw new TypeError('options.video.discard, when provided, must be a boolean.');
}
if (videoOptions?.forceTranscode !== undefined && typeof videoOptions.forceTranscode !== 'boolean') {
throw new TypeError('options.video.forceTranscode, when provided, must be a boolean.');
}
if (videoOptions?.codec !== undefined && !VIDEO_CODECS.includes(videoOptions.codec)) {
throw new TypeError(
`options.video.codec, when provided, must be one of: ${VIDEO_CODECS.join(', ')}.`,
);
}
if (
videoOptions?.bitrate !== undefined
&& !(videoOptions.bitrate instanceof Quality)
&& (!Number.isInteger(videoOptions.bitrate) || videoOptions.bitrate <= 0)
) {
throw new TypeError('options.video.bitrate, when provided, must be a positive integer or a quality.');
}
if (
videoOptions?.width !== undefined
&& (!Number.isInteger(videoOptions.width) || videoOptions.width <= 0)
) {
throw new TypeError('options.video.width, when provided, must be a positive integer.');
}
if (
videoOptions?.height !== undefined
&& (!Number.isInteger(videoOptions.height) || videoOptions.height <= 0)
) {
throw new TypeError('options.video.height, when provided, must be a positive integer.');
}
if (videoOptions?.fit !== undefined && !['fill', 'contain', 'cover'].includes(videoOptions.fit)) {
throw new TypeError('options.video.fit, when provided, must be one of "fill", "contain", or "cover".');
}
if (
videoOptions?.width !== undefined
&& videoOptions.height !== undefined
&& videoOptions.fit === undefined
) {
throw new TypeError(
'When both options.video.width and options.video.height are provided, options.video.fit must also be'
+ ' provided.',
);
}
if (videoOptions?.rotate !== undefined && ![0, 90, 180, 270].includes(videoOptions.rotate)) {
throw new TypeError('options.video.rotate, when provided, must be 0, 90, 180 or 270.');
}
if (
videoOptions?.frameRate !== undefined
&& (!Number.isFinite(videoOptions.frameRate) || videoOptions.frameRate <= 0)
) {
throw new TypeError('options.video.frameRate, when provided, must be a finite positive number.');
}
};
/**
* The options for media file conversion.
* @public
*/
export type ConversionOptions = {
/** The input file. */
input: Input;
/** The output file. */
output: Output;
/**
* Video-specific options. When passing an object, the same options are applied to all video tracks. When passing a
* function, it will be invoked for each video track and is expected to return or resolve to the options
* for that specific track. The function is passed an instance of `InputVideoTrack` as well as a number `n`, which
* is the 1-based index of the track in the list of all video tracks.
*/
video?: ConversionVideoOptions
| ((track: InputVideoTrack, n: number) => MaybePromise<ConversionVideoOptions | undefined>);
/**
* Audio-specific options. When passing an object, the same options are applied to all audio tracks. When passing a
* function, it will be invoked for each audio track and is expected to return or resolve to the options
* for that specific track. The function is passed an instance of `InputAudioTrack` as well as a number `n`, which
* is the 1-based index of the track in the list of all audio tracks.
*/
audio?: ConversionAudioOptions
| ((track: InputAudioTrack, n: number) => MaybePromise<ConversionAudioOptions | undefined>);
/** Options to trim the input file. */
trim?: {
/** The time in the input file in seconds at which the output file should start. Must be less than `end`. */
@@ -115,6 +197,42 @@ export type ConversionOptions = {
};
};
const validateAudioOptions = (audioOptions: ConversionAudioOptions | undefined) => {
if (audioOptions !== undefined && (!audioOptions || typeof audioOptions !== 'object')) {
throw new TypeError('options.audio, when provided, must be an object.');
}
if (audioOptions?.discard !== undefined && typeof audioOptions.discard !== 'boolean') {
throw new TypeError('options.audio.discard, when provided, must be a boolean.');
}
if (audioOptions?.forceTranscode !== undefined && typeof audioOptions.forceTranscode !== 'boolean') {
throw new TypeError('options.audio.forceTranscode, when provided, must be a boolean.');
}
if (audioOptions?.codec !== undefined && !AUDIO_CODECS.includes(audioOptions.codec)) {
throw new TypeError(
`options.audio.codec, when provided, must be one of: ${AUDIO_CODECS.join(', ')}.`,
);
}
if (
audioOptions?.bitrate !== undefined
&& !(audioOptions.bitrate instanceof Quality)
&& (!Number.isInteger(audioOptions.bitrate) || audioOptions.bitrate <= 0)
) {
throw new TypeError('options.audio.bitrate, when provided, must be a positive integer or a quality.');
}
if (
audioOptions?.numberOfChannels !== undefined
&& (!Number.isInteger(audioOptions.numberOfChannels) || audioOptions.numberOfChannels <= 0)
) {
throw new TypeError('options.audio.numberOfChannels, when provided, must be a positive integer.');
}
if (
audioOptions?.sampleRate !== undefined
&& (!Number.isInteger(audioOptions.sampleRate) || audioOptions.sampleRate <= 0)
) {
throw new TypeError('options.audio.sampleRate, when provided, must be a positive integer.');
}
};
const FALLBACK_NUMBER_OF_CHANNELS = 2;
const FALLBACK_SAMPLE_RATE = 48000;
@@ -217,94 +335,19 @@ export class Conversion {
if (options.output._tracks.length > 0 || options.output.state !== 'pending') {
throw new TypeError('options.output must be fresh: no tracks added and not started.');
}
if (options.video !== undefined && (!options.video || typeof options.video !== 'object')) {
throw new TypeError('options.video, when provided, must be an object.');
if (typeof options.video !== 'function') {
validateVideoOptions(options.video);
} else {
// We'll validate the return value later
}
if (options.video?.discard !== undefined && typeof options.video.discard !== 'boolean') {
throw new TypeError('options.video.discard, when provided, must be a boolean.');
}
if (options.video?.forceTranscode !== undefined && typeof options.video.forceTranscode !== 'boolean') {
throw new TypeError('options.video.forceTranscode, when provided, must be a boolean.');
}
if (options.video?.codec !== undefined && !VIDEO_CODECS.includes(options.video.codec)) {
throw new TypeError(
`options.video.codec, when provided, must be one of: ${VIDEO_CODECS.join(', ')}.`,
);
}
if (
options.video?.bitrate !== undefined
&& !(options.video.bitrate instanceof Quality)
&& (!Number.isInteger(options.video.bitrate) || options.video.bitrate <= 0)
) {
throw new TypeError('options.video.bitrate, when provided, must be a positive integer or a quality.');
}
if (
options.video?.width !== undefined
&& (!Number.isInteger(options.video.width) || options.video.width <= 0)
) {
throw new TypeError('options.video.width, when provided, must be a positive integer.');
}
if (
options.video?.height !== undefined
&& (!Number.isInteger(options.video.height) || options.video.height <= 0)
) {
throw new TypeError('options.video.height, when provided, must be a positive integer.');
}
if (options.video?.fit !== undefined && !['fill', 'contain', 'cover'].includes(options.video.fit)) {
throw new TypeError('options.video.fit, when provided, must be one of "fill", "contain", or "cover".');
}
if (
options.video?.width !== undefined
&& options.video.height !== undefined
&& options.video.fit === undefined
) {
throw new TypeError(
'When both options.video.width and options.video.height are provided, options.video.fit must also be'
+ ' provided.',
);
}
if (options.video?.rotate !== undefined && ![0, 90, 180, 270].includes(options.video.rotate)) {
throw new TypeError('options.video.rotate, when provided, must be 0, 90, 180 or 270.');
}
if (
options.video?.frameRate !== undefined
&& (!Number.isFinite(options.video.frameRate) || options.video.frameRate <= 0)
) {
throw new TypeError('options.video.frameRate, when provided, must be a finite positive number.');
}
if (options.audio !== undefined && (!options.audio || typeof options.audio !== 'object')) {
throw new TypeError('options.audio, when provided, must be an object.');
}
if (options.audio?.discard !== undefined && typeof options.audio.discard !== 'boolean') {
throw new TypeError('options.audio.discard, when provided, must be a boolean.');
}
if (options.audio?.forceTranscode !== undefined && typeof options.audio.forceTranscode !== 'boolean') {
throw new TypeError('options.audio.forceTranscode, when provided, must be a boolean.');
}
if (options.audio?.codec !== undefined && !AUDIO_CODECS.includes(options.audio.codec)) {
throw new TypeError(
`options.audio.codec, when provided, must be one of: ${AUDIO_CODECS.join(', ')}.`,
);
}
if (
options.audio?.bitrate !== undefined
&& !(options.audio.bitrate instanceof Quality)
&& (!Number.isInteger(options.audio.bitrate) || options.audio.bitrate <= 0)
) {
throw new TypeError('options.audio.bitrate, when provided, must be a positive integer or a quality.');
}
if (
options.audio?.numberOfChannels !== undefined
&& (!Number.isInteger(options.audio.numberOfChannels) || options.audio.numberOfChannels <= 0)
) {
throw new TypeError('options.audio.numberOfChannels, when provided, must be a positive integer.');
}
if (
options.audio?.sampleRate !== undefined
&& (!Number.isInteger(options.audio.sampleRate) || options.audio.sampleRate <= 0)
) {
throw new TypeError('options.audio.sampleRate, when provided, must be a positive integer.');
if (typeof options.audio !== 'function') {
validateAudioOptions(options.audio);
} else {
// We'll validate the return value later
}
if (options.trim !== undefined && (!options.trim || typeof options.trim !== 'object')) {
throw new TypeError('options.trim, when provided, must be an object.');
}
@@ -338,16 +381,36 @@ export class Conversion {
const inputTracks = await this.input.getTracks();
const outputTrackCounts = this.output.format.getSupportedTrackCounts();
let nVideo = 1;
let nAudio = 1;
for (const track of inputTracks) {
if (track.isVideoTrack() && this._options.video?.discard) {
this.discardedTracks.push({
track,
reason: 'discarded_by_user',
});
continue;
let trackOptions: ConversionVideoOptions | ConversionAudioOptions | undefined = undefined;
if (track.isVideoTrack()) {
if (this._options.video) {
if (typeof this._options.video === 'function') {
trackOptions = await this._options.video(track, nVideo);
validateVideoOptions(trackOptions);
nVideo++;
} else {
trackOptions = this._options.video;
}
}
} else if (track.isAudioTrack()) {
if (this._options.audio) {
if (typeof this._options.audio === 'function') {
trackOptions = await this._options.audio(track, nAudio);
validateAudioOptions(trackOptions);
nAudio++;
} else {
trackOptions = this._options.audio;
}
}
} else {
assert(false);
}
if (track.isAudioTrack() && this._options.audio?.discard) {
if (trackOptions?.discard) {
this.discardedTracks.push({
track,
reason: 'discarded_by_user',
@@ -372,9 +435,9 @@ export class Conversion {
}
if (track.isVideoTrack()) {
await this._processVideoTrack(track);
await this._processVideoTrack(track, (trackOptions ?? {}) as ConversionVideoOptions);
} else if (track.isAudioTrack()) {
await this._processAudioTrack(track);
await this._processAudioTrack(track, (trackOptions ?? {}) as ConversionAudioOptions);
}
}
@@ -443,7 +506,7 @@ export class Conversion {
}
/** @internal */
async _processVideoTrack(track: InputVideoTrack) {
async _processVideoTrack(track: InputVideoTrack, trackOptions: ConversionVideoOptions) {
const sourceCodec = track.codec;
if (!sourceCodec) {
this.discardedTracks.push({
@@ -455,7 +518,7 @@ export class Conversion {
let videoSource: VideoSource;
const totalRotation = normalizeRotation(track.rotation + (this._options.video?.rotate ?? 0));
const totalRotation = normalizeRotation(track.rotation + (trackOptions.rotate ?? 0));
const outputSupportsRotation = this.output.format.supportsVideoRotationMetadata;
const [originalWidth, originalHeight] = totalRotation % 180 === 0
@@ -469,22 +532,22 @@ export class Conversion {
// A lot of video encoders require that the dimensions be multiples of 2
const ceilToMultipleOfTwo = (value: number) => Math.ceil(value / 2) * 2;
if (this._options.video?.width !== undefined && this._options.video.height === undefined) {
width = ceilToMultipleOfTwo(this._options.video.width);
if (trackOptions.width !== undefined && trackOptions.height === undefined) {
width = ceilToMultipleOfTwo(trackOptions.width);
height = ceilToMultipleOfTwo(Math.round(width / aspectRatio));
} else if (this._options.video?.width === undefined && this._options.video?.height !== undefined) {
height = ceilToMultipleOfTwo(this._options.video.height);
} else if (trackOptions.width === undefined && trackOptions.height !== undefined) {
height = ceilToMultipleOfTwo(trackOptions.height);
width = ceilToMultipleOfTwo(Math.round(height * aspectRatio));
} else if (this._options.video?.width !== undefined && this._options.video.height !== undefined) {
width = ceilToMultipleOfTwo(this._options.video.width);
height = ceilToMultipleOfTwo(this._options.video.height);
} else if (trackOptions.width !== undefined && trackOptions.height !== undefined) {
width = ceilToMultipleOfTwo(trackOptions.width);
height = ceilToMultipleOfTwo(trackOptions.height);
}
const firstTimestamp = await track.getFirstTimestamp();
const needsTranscode = !!this._options.video?.forceTranscode
const needsTranscode = !!trackOptions.forceTranscode
|| this._startTimestamp > 0
|| firstTimestamp < 0
|| !!this._options.video?.frameRate;
|| !!trackOptions.frameRate;
const needsRerender = width !== originalWidth
|| height !== originalHeight
|| (totalRotation !== 0 && !outputSupportsRotation);
@@ -492,10 +555,10 @@ export class Conversion {
let videoCodecs = this.output.format.getSupportedVideoCodecs();
if (
!needsTranscode
&& !this._options.video?.bitrate
&& !trackOptions.bitrate
&& !needsRerender
&& videoCodecs.includes(sourceCodec)
&& (!this._options.video?.codec || this._options.video?.codec === sourceCodec)
&& (!trackOptions.codec || trackOptions.codec === sourceCodec)
) {
// Fast path, we can simply copy over the encoded packets
@@ -540,11 +603,11 @@ export class Conversion {
return;
}
if (this._options.video?.codec) {
videoCodecs = videoCodecs.filter(codec => codec === this._options.video?.codec);
if (trackOptions.codec) {
videoCodecs = videoCodecs.filter(codec => codec === trackOptions.codec);
}
const bitrate = this._options.video?.bitrate ?? QUALITY_HIGH;
const bitrate = trackOptions.bitrate ?? QUALITY_HIGH;
const encodableCodec = await getFirstEncodableVideoCodec(videoCodecs, { width, height, bitrate });
if (!encodableCodec) {
@@ -571,12 +634,12 @@ export class Conversion {
const sink = new CanvasSink(track, {
width,
height,
fit: this._options.video?.fit ?? 'fill',
fit: trackOptions.fit ?? 'fill',
rotation: totalRotation, // Bake the rotation into the output
poolSize: 1,
});
const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
const frameRate = this._options.video?.frameRate;
const frameRate = trackOptions.frameRate;
let lastCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
let lastCanvasTimestamp: number | null = null;
@@ -661,7 +724,7 @@ export class Conversion {
await this._started;
const sink = new VideoSampleSink(track);
const frameRate = this._options.video?.frameRate;
const frameRate = trackOptions.frameRate;
let lastSample: VideoSample | null = null;
let lastSampleTimestamp: number | null = null;
@@ -744,7 +807,7 @@ export class Conversion {
}
this.output.addVideoTrack(videoSource, {
frameRate: this._options.video?.frameRate,
frameRate: trackOptions.frameRate,
languageCode: track.languageCode,
rotation: needsRerender ? 0 : totalRotation, // Rerendering will bake the rotation into the output
});
@@ -755,7 +818,7 @@ export class Conversion {
}
/** @internal */
async _processAudioTrack(track: InputAudioTrack) {
async _processAudioTrack(track: InputAudioTrack, trackOptions: ConversionAudioOptions) {
const sourceCodec = track.codec;
if (!sourceCodec) {
this.discardedTracks.push({
@@ -772,8 +835,8 @@ export class Conversion {
const firstTimestamp = await track.getFirstTimestamp();
let numberOfChannels = this._options.audio?.numberOfChannels ?? originalNumberOfChannels;
let sampleRate = this._options.audio?.sampleRate ?? originalSampleRate;
let numberOfChannels = trackOptions.numberOfChannels ?? originalNumberOfChannels;
let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
let needsResample = numberOfChannels !== originalNumberOfChannels
|| sampleRate !== originalSampleRate
|| this._startTimestamp > 0
@@ -781,11 +844,11 @@ export class Conversion {
let audioCodecs = this.output.format.getSupportedAudioCodecs();
if (
!this._options.audio?.forceTranscode
&& !this._options.audio?.bitrate
!trackOptions.forceTranscode
&& !trackOptions.bitrate
&& !needsResample
&& audioCodecs.includes(sourceCodec)
&& (!this._options.audio?.codec || this._options.audio.codec === sourceCodec)
&& (!trackOptions.codec || trackOptions.codec === sourceCodec)
) {
// Fast path, we can simply copy over the encoded packets
@@ -832,11 +895,11 @@ export class Conversion {
let codecOfChoice: AudioCodec | null = null;
if (this._options.audio?.codec) {
audioCodecs = audioCodecs.filter(codec => codec === this._options.audio!.codec);
if (trackOptions.codec) {
audioCodecs = audioCodecs.filter(codec => codec === trackOptions.codec);
}
const bitrate = this._options.audio?.bitrate ?? QUALITY_HIGH;
const bitrate = trackOptions.bitrate ?? QUALITY_HIGH;
const encodableCodecs = await getEncodableAudioCodecs(audioCodecs, {
numberOfChannels,
+2 -2
View File
@@ -84,7 +84,7 @@ export {
getFirstEncodableSubtitleCodec,
} from './codec';
export { Target, BufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions } from './target';
export { Rotation, AnyIterable, SetRequired } from './misc';
export { Rotation, AnyIterable, SetRequired, MaybePromise } from './misc';
export {
Source,
BufferSource,
@@ -135,7 +135,7 @@ export {
AudioBufferSink,
WrappedAudioBuffer,
} from './media-sink';
export { ConversionOptions, Conversion } from './conversion';
export { Conversion, ConversionOptions, ConversionVideoOptions, ConversionAudioOptions } from './conversion';
export {
CustomVideoDecoder,
CustomAudioDecoder,
+6
View File
@@ -610,3 +610,9 @@ export const isSafari = () => {
isSafariCache = result;
return result;
};
/**
* T or a promise that resolves to T.
* @public
*/
export type MaybePromise<T> = T | Promise<T>;