diff --git a/dev/convert.html b/dev/convert.html
index 790d8e5..24afb95 100644
--- a/dev/convert.html
+++ b/dev/convert.html
@@ -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
diff --git a/docs/guide/converting-media-files.md b/docs/guide/converting-media-files.md
index 69c9e51..14505df 100644
--- a/docs/guide/converting-media-files.md
+++ b/docs/guide/converting-media-files.md
@@ -92,22 +92,18 @@ 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?: {
- discard?: boolean;
- width?: number;
- height?: number;
- fit?: 'fill' | 'contain' | 'cover';
- rotate?: 0 | 90 | 180 | 270;
- frameRate?: number;
- codec?: VideoCodec;
- bitrate?: number | Quality;
- forceTranscode?: boolean;
- };
- // ...
+type ConversionVideoOptions = {
+ discard?: boolean;
+ width?: number;
+ height?: number;
+ fit?: 'fill' | 'contain' | 'cover';
+ rotate?: 0 | 90 | 180 | 270;
+ frameRate?: number;
+ codec?: VideoCodec;
+ bitrate?: number | Quality;
+ forceTranscode?: boolean;
};
```
@@ -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,19 +155,15 @@ 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?: {
- discard?: boolean;
- codec?: AudioCodec;
- bitrate?: number | Quality;
- numberOfChannels?: number;
- sampleRate?: number;
- forceTranscode?: boolean;
- };
- // ...
+type ConversionAudioOptions = {
+ discard?: boolean;
+ codec?: AudioCodec;
+ bitrate?: number | Quality;
+ numberOfChannels?: number;
+ sampleRate?: number;
+ forceTranscode?: boolean;
};
```
@@ -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:
diff --git a/scripts/esbuild/inlined-workers.ts b/scripts/esbuild/inlined-workers.ts
index a0a32e7..eee44ac 100644
--- a/scripts/esbuild/inlined-workers.ts
+++ b/scripts/esbuild/inlined-workers.ts
@@ -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;
diff --git a/src/conversion.ts b/src/conversion.ts
index c9a9b04..ef099cf 100644
--- a/src/conversion.ts
+++ b/src/conversion.ts
@@ -35,10 +35,131 @@ 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';
+/**
+ * Video-specific options.
+ * @public
+ */
+export type ConversionVideoOptions = {
+ /** If true, all video tracks will be discarded and will not be present in the output. */
+ discard?: boolean;
+ /**
+ * The desired width of the output video in pixels, defaulting to the video's natural display width. If height
+ * is not set, it will be deduced automatically based on aspect ratio.
+ */
+ width?: number;
+ /**
+ * The desired height of the output video in pixels, defaulting to the video's natural display height. If width
+ * is not set, it will be deduced automatically based on aspect ratio.
+ */
+ height?: number;
+ /**
+ * The fitting algorithm in case both width and height are set.
+ *
+ * - 'fill' will stretch the image to fill the entire box, potentially altering aspect ratio.
+ * - 'contain' will contain the entire image within the box while preserving aspect ratio. This may lead to
+ * letterboxing.
+ * - 'cover' will scale the image until the entire box is filled, while preserving aspect ratio.
+ */
+ fit?: 'fill' | 'contain' | 'cover';
+ /**
+ * The angle in degrees to rotate the input video by, clockwise. Rotation is applied before resizing. This
+ * rotation is _in addition to_ the natural rotation of the input video as specified in input file's metadata.
+ */
+ rotate?: Rotation;
+ /**
+ * The desired frame rate of the output video, in hertz. If not specified, the original input frame rate will
+ * be used (which may be variable).
+ */
+ frameRate?: number;
+ /** The desired output video codec. */
+ codec?: VideoCodec;
+ /** The desired bitrate of the output video. */
+ bitrate?: VideoEncodingConfig['bitrate'];
+ /** When true, video will always be re-encoded instead of directly copying over the encoded samples. */
+ forceTranscode?: boolean;
+};
+
+/**
+ * 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. */
+ numberOfChannels?: number;
+ /** The desired sample rate of the output audio, in hertz. */
+ sampleRate?: number;
+ /** The desired output audio codec. */
+ codec?: AudioCodec;
+ /** The desired bitrate of the output audio. */
+ bitrate?: AudioEncodingConfig['bitrate'];
+ /** When true, audio will always be re-encoded instead of directly copying over the encoded samples. */
+ 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
@@ -49,62 +170,23 @@ export type ConversionOptions = {
/** The output file. */
output: Output;
- /** Video-specific options. */
- video?: {
- /** If true, all video tracks will be discarded and will not be present in the output. */
- discard?: boolean;
- /**
- * The desired width of the output video in pixels, defaulting to the video's natural display width. If height
- * is not set, it will be deduced automatically based on aspect ratio.
- */
- width?: number;
- /**
- * The desired height of the output video in pixels, defaulting to the video's natural display height. If width
- * is not set, it will be deduced automatically based on aspect ratio.
- */
- height?: number;
- /**
- * The fitting algorithm in case both width and height are set.
- *
- * - 'fill' will stretch the image to fill the entire box, potentially altering aspect ratio.
- * - 'contain' will contain the entire image within the box while preserving aspect ratio. This may lead to
- * letterboxing.
- * - 'cover' will scale the image until the entire box is filled, while preserving aspect ratio.
- */
- fit?: 'fill' | 'contain' | 'cover';
- /**
- * The angle in degrees to rotate the input video by, clockwise. Rotation is applied before resizing. This
- * rotation is _in addition to_ the natural rotation of the input video as specified in input file's metadata.
- */
- rotate?: Rotation;
- /**
- * The desired frame rate of the output video, in hertz. If not specified, the original input frame rate will
- * be used (which may be variable).
- */
- frameRate?: number;
- /** The desired output video codec. */
- codec?: VideoCodec;
- /** The desired bitrate of the output video. */
- bitrate?: VideoEncodingConfig['bitrate'];
- /** When true, video will always be re-encoded instead of directly copying over the encoded samples. */
- forceTranscode?: boolean;
- };
+ /**
+ * 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);
- /** Audio-specific options. */
- audio?: {
- /** 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. */
- numberOfChannels?: number;
- /** The desired sample rate of the output audio, in hertz. */
- sampleRate?: number;
- /** The desired output audio codec. */
- codec?: AudioCodec;
- /** The desired bitrate of the output audio. */
- bitrate?: AudioEncodingConfig['bitrate'];
- /** When true, audio will always be re-encoded instead of directly copying over the encoded samples. */
- forceTranscode?: boolean;
- };
+ /**
+ * 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);
/** Options to trim the input file. */
trim?: {
@@ -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,
diff --git a/src/index.ts b/src/index.ts
index ba24c07..5c28b03 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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,
diff --git a/src/misc.ts b/src/misc.ts
index 079ab5e..25494f5 100644
--- a/src/misc.ts
+++ b/src/misc.ts
@@ -610,3 +610,9 @@ export const isSafari = () => {
isSafariCache = result;
return result;
};
+
+/**
+ * T or a promise that resolves to T.
+ * @public
+ */
+export type MaybePromise = T | Promise;