mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-10 01:03:45 +02:00
Add track-specific conversion options, fix esbuild race condition
This commit is contained in:
+4
-3
@@ -24,7 +24,7 @@
|
|||||||
chunked: true,
|
chunked: true,
|
||||||
chunkSize: 2**20
|
chunkSize: 2**20
|
||||||
});
|
});
|
||||||
const outputFormat = new Mediabunny.Mp3OutputFormat({});
|
const outputFormat = new Mediabunny.Mp4OutputFormat({});
|
||||||
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.textContent = 'Cancel';
|
button.textContent = 'Cancel';
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
target
|
target
|
||||||
}),
|
}),
|
||||||
audio: {
|
audio: {
|
||||||
|
discard: true,
|
||||||
//codec: 'opus',
|
//codec: 'opus',
|
||||||
//bitrate: 128000,
|
//bitrate: 128000,
|
||||||
//numberOfChannels: 1,
|
//numberOfChannels: 1,
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
bitrate: 320000
|
bitrate: 320000
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
video: {
|
video: () => ({
|
||||||
//frameRate: 27.123,
|
//frameRate: 27.123,
|
||||||
//width: 320,
|
//width: 320,
|
||||||
//forceTranscode: true,
|
//forceTranscode: true,
|
||||||
@@ -91,7 +92,7 @@
|
|||||||
//height: 512,
|
//height: 512,
|
||||||
//width: 200,
|
//width: 200,
|
||||||
//height: 100,
|
//height: 100,
|
||||||
},
|
}),
|
||||||
trim: {
|
trim: {
|
||||||
start: 0,
|
start: 0,
|
||||||
end: 20
|
end: 20
|
||||||
|
|||||||
@@ -92,11 +92,9 @@ This automatically frees up all resources used by the conversion process.
|
|||||||
|
|
||||||
## Video options
|
## 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
|
```ts
|
||||||
type ConversionOptions = {
|
type ConversionVideoOptions = {
|
||||||
// ...
|
|
||||||
video?: {
|
|
||||||
discard?: boolean;
|
discard?: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
@@ -107,8 +105,6 @@ type ConversionOptions = {
|
|||||||
bitrate?: number | Quality;
|
bitrate?: number | Quality;
|
||||||
forceTranscode?: boolean;
|
forceTranscode?: boolean;
|
||||||
};
|
};
|
||||||
// ...
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For example, here we resize the video track to 720p:
|
For example, here we resize the video track to 720p:
|
||||||
@@ -125,7 +121,7 @@ const conversion = await Conversion.init({
|
|||||||
```
|
```
|
||||||
|
|
||||||
::: info
|
::: 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
|
### 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 `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
|
### 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).
|
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
|
## 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
|
```ts
|
||||||
type ConversionOptions = {
|
type ConversionAudioOptions = {
|
||||||
// ...
|
|
||||||
audio?: {
|
|
||||||
discard?: boolean;
|
discard?: boolean;
|
||||||
codec?: AudioCodec;
|
codec?: AudioCodec;
|
||||||
bitrate?: number | Quality;
|
bitrate?: number | Quality;
|
||||||
@@ -169,8 +165,6 @@ type ConversionOptions = {
|
|||||||
sampleRate?: number;
|
sampleRate?: number;
|
||||||
forceTranscode?: boolean;
|
forceTranscode?: boolean;
|
||||||
};
|
};
|
||||||
// ...
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For example, here we convert the audio track to mono and set a specific sample rate:
|
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
|
::: 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
|
### 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`.
|
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
|
## Trimming
|
||||||
|
|
||||||
Use the `trim` property in the conversion options to extract only a section of the input file into the output file:
|
Use the `trim` property in the conversion options to extract only a section of the input file into the output file:
|
||||||
|
|||||||
@@ -71,13 +71,18 @@ if (cacheDir === undefined) {
|
|||||||
throw new Error('Cache directory not found.');
|
throw new Error('Cache directory not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
async function buildWorker(workerPath: string, extraConfig: esbuild.BuildOptions) {
|
async function buildWorker(workerPath: string, extraConfig: esbuild.BuildOptions) {
|
||||||
const scriptNameParts = path.basename(workerPath).split('.');
|
const scriptNameParts = path.basename(workerPath).split('.');
|
||||||
scriptNameParts.pop();
|
scriptNameParts.pop();
|
||||||
|
scriptNameParts.push(String(i)); // To make sure it doesn't clash with other builds
|
||||||
scriptNameParts.push('js');
|
scriptNameParts.push('js');
|
||||||
const scriptName = scriptNameParts.join('.');
|
const scriptName = scriptNameParts.join('.');
|
||||||
const bundlePath = path.resolve(cacheDir!, scriptName);
|
const bundlePath = path.resolve(cacheDir!, scriptName);
|
||||||
|
|
||||||
|
i = (i + 1) % 32;
|
||||||
|
|
||||||
if (extraConfig) {
|
if (extraConfig) {
|
||||||
delete extraConfig.entryPoints;
|
delete extraConfig.entryPoints;
|
||||||
delete extraConfig.outfile;
|
delete extraConfig.outfile;
|
||||||
|
|||||||
+199
-136
@@ -35,22 +35,15 @@ import {
|
|||||||
VideoSampleSource,
|
VideoSampleSource,
|
||||||
AudioSampleSource,
|
AudioSampleSource,
|
||||||
} from './media-source';
|
} 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 { Output, TrackType } from './output';
|
||||||
import { AudioSample, VideoSample } from './sample';
|
import { AudioSample, VideoSample } from './sample';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The options for media file conversion.
|
* Video-specific options.
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type ConversionOptions = {
|
export type ConversionVideoOptions = {
|
||||||
/** The input file. */
|
|
||||||
input: Input;
|
|
||||||
/** 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. */
|
/** If true, all video tracks will be discarded and will not be present in the output. */
|
||||||
discard?: boolean;
|
discard?: boolean;
|
||||||
/**
|
/**
|
||||||
@@ -90,8 +83,11 @@ export type ConversionOptions = {
|
|||||||
forceTranscode?: boolean;
|
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. */
|
/** If true, all audio tracks will be discarded and will not be present in the output. */
|
||||||
discard?: boolean;
|
discard?: boolean;
|
||||||
/** The desired channel count of the output audio. */
|
/** The desired channel count of the output audio. */
|
||||||
@@ -106,6 +102,92 @@ export type ConversionOptions = {
|
|||||||
forceTranscode?: boolean;
|
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. */
|
/** Options to trim the input file. */
|
||||||
trim?: {
|
trim?: {
|
||||||
/** The time in the input file in seconds at which the output file should start. Must be less than `end`. */
|
/** 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_NUMBER_OF_CHANNELS = 2;
|
||||||
const FALLBACK_SAMPLE_RATE = 48000;
|
const FALLBACK_SAMPLE_RATE = 48000;
|
||||||
|
|
||||||
@@ -217,94 +335,19 @@ export class Conversion {
|
|||||||
if (options.output._tracks.length > 0 || options.output.state !== 'pending') {
|
if (options.output._tracks.length > 0 || options.output.state !== 'pending') {
|
||||||
throw new TypeError('options.output must be fresh: no tracks added and not started.');
|
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 (typeof options.audio !== 'function') {
|
||||||
}
|
validateAudioOptions(options.audio);
|
||||||
if (options.video?.forceTranscode !== undefined && typeof options.video.forceTranscode !== 'boolean') {
|
} else {
|
||||||
throw new TypeError('options.video.forceTranscode, when provided, must be a boolean.');
|
// We'll validate the return value later
|
||||||
}
|
|
||||||
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 (options.trim !== undefined && (!options.trim || typeof options.trim !== 'object')) {
|
if (options.trim !== undefined && (!options.trim || typeof options.trim !== 'object')) {
|
||||||
throw new TypeError('options.trim, when provided, must be an 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 inputTracks = await this.input.getTracks();
|
||||||
const outputTrackCounts = this.output.format.getSupportedTrackCounts();
|
const outputTrackCounts = this.output.format.getSupportedTrackCounts();
|
||||||
|
|
||||||
|
let nVideo = 1;
|
||||||
|
let nAudio = 1;
|
||||||
|
|
||||||
for (const track of inputTracks) {
|
for (const track of inputTracks) {
|
||||||
if (track.isVideoTrack() && this._options.video?.discard) {
|
let trackOptions: ConversionVideoOptions | ConversionAudioOptions | undefined = undefined;
|
||||||
this.discardedTracks.push({
|
if (track.isVideoTrack()) {
|
||||||
track,
|
if (this._options.video) {
|
||||||
reason: 'discarded_by_user',
|
if (typeof this._options.video === 'function') {
|
||||||
});
|
trackOptions = await this._options.video(track, nVideo);
|
||||||
continue;
|
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({
|
this.discardedTracks.push({
|
||||||
track,
|
track,
|
||||||
reason: 'discarded_by_user',
|
reason: 'discarded_by_user',
|
||||||
@@ -372,9 +435,9 @@ export class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (track.isVideoTrack()) {
|
if (track.isVideoTrack()) {
|
||||||
await this._processVideoTrack(track);
|
await this._processVideoTrack(track, (trackOptions ?? {}) as ConversionVideoOptions);
|
||||||
} else if (track.isAudioTrack()) {
|
} else if (track.isAudioTrack()) {
|
||||||
await this._processAudioTrack(track);
|
await this._processAudioTrack(track, (trackOptions ?? {}) as ConversionAudioOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +506,7 @@ export class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
async _processVideoTrack(track: InputVideoTrack) {
|
async _processVideoTrack(track: InputVideoTrack, trackOptions: ConversionVideoOptions) {
|
||||||
const sourceCodec = track.codec;
|
const sourceCodec = track.codec;
|
||||||
if (!sourceCodec) {
|
if (!sourceCodec) {
|
||||||
this.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
@@ -455,7 +518,7 @@ export class Conversion {
|
|||||||
|
|
||||||
let videoSource: VideoSource;
|
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 outputSupportsRotation = this.output.format.supportsVideoRotationMetadata;
|
||||||
|
|
||||||
const [originalWidth, originalHeight] = totalRotation % 180 === 0
|
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
|
// A lot of video encoders require that the dimensions be multiples of 2
|
||||||
const ceilToMultipleOfTwo = (value: number) => Math.ceil(value / 2) * 2;
|
const ceilToMultipleOfTwo = (value: number) => Math.ceil(value / 2) * 2;
|
||||||
|
|
||||||
if (this._options.video?.width !== undefined && this._options.video.height === undefined) {
|
if (trackOptions.width !== undefined && trackOptions.height === undefined) {
|
||||||
width = ceilToMultipleOfTwo(this._options.video.width);
|
width = ceilToMultipleOfTwo(trackOptions.width);
|
||||||
height = ceilToMultipleOfTwo(Math.round(width / aspectRatio));
|
height = ceilToMultipleOfTwo(Math.round(width / aspectRatio));
|
||||||
} else if (this._options.video?.width === undefined && this._options.video?.height !== undefined) {
|
} else if (trackOptions.width === undefined && trackOptions.height !== undefined) {
|
||||||
height = ceilToMultipleOfTwo(this._options.video.height);
|
height = ceilToMultipleOfTwo(trackOptions.height);
|
||||||
width = ceilToMultipleOfTwo(Math.round(height * aspectRatio));
|
width = ceilToMultipleOfTwo(Math.round(height * aspectRatio));
|
||||||
} else if (this._options.video?.width !== undefined && this._options.video.height !== undefined) {
|
} else if (trackOptions.width !== undefined && trackOptions.height !== undefined) {
|
||||||
width = ceilToMultipleOfTwo(this._options.video.width);
|
width = ceilToMultipleOfTwo(trackOptions.width);
|
||||||
height = ceilToMultipleOfTwo(this._options.video.height);
|
height = ceilToMultipleOfTwo(trackOptions.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstTimestamp = await track.getFirstTimestamp();
|
const firstTimestamp = await track.getFirstTimestamp();
|
||||||
const needsTranscode = !!this._options.video?.forceTranscode
|
const needsTranscode = !!trackOptions.forceTranscode
|
||||||
|| this._startTimestamp > 0
|
|| this._startTimestamp > 0
|
||||||
|| firstTimestamp < 0
|
|| firstTimestamp < 0
|
||||||
|| !!this._options.video?.frameRate;
|
|| !!trackOptions.frameRate;
|
||||||
const needsRerender = width !== originalWidth
|
const needsRerender = width !== originalWidth
|
||||||
|| height !== originalHeight
|
|| height !== originalHeight
|
||||||
|| (totalRotation !== 0 && !outputSupportsRotation);
|
|| (totalRotation !== 0 && !outputSupportsRotation);
|
||||||
@@ -492,10 +555,10 @@ export class Conversion {
|
|||||||
let videoCodecs = this.output.format.getSupportedVideoCodecs();
|
let videoCodecs = this.output.format.getSupportedVideoCodecs();
|
||||||
if (
|
if (
|
||||||
!needsTranscode
|
!needsTranscode
|
||||||
&& !this._options.video?.bitrate
|
&& !trackOptions.bitrate
|
||||||
&& !needsRerender
|
&& !needsRerender
|
||||||
&& videoCodecs.includes(sourceCodec)
|
&& 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
|
// Fast path, we can simply copy over the encoded packets
|
||||||
|
|
||||||
@@ -540,11 +603,11 @@ export class Conversion {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._options.video?.codec) {
|
if (trackOptions.codec) {
|
||||||
videoCodecs = videoCodecs.filter(codec => codec === this._options.video?.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 });
|
const encodableCodec = await getFirstEncodableVideoCodec(videoCodecs, { width, height, bitrate });
|
||||||
if (!encodableCodec) {
|
if (!encodableCodec) {
|
||||||
@@ -571,12 +634,12 @@ export class Conversion {
|
|||||||
const sink = new CanvasSink(track, {
|
const sink = new CanvasSink(track, {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fit: this._options.video?.fit ?? 'fill',
|
fit: trackOptions.fit ?? 'fill',
|
||||||
rotation: totalRotation, // Bake the rotation into the output
|
rotation: totalRotation, // Bake the rotation into the output
|
||||||
poolSize: 1,
|
poolSize: 1,
|
||||||
});
|
});
|
||||||
const iterator = sink.canvases(this._startTimestamp, this._endTimestamp);
|
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 lastCanvas: HTMLCanvasElement | OffscreenCanvas | null = null;
|
||||||
let lastCanvasTimestamp: number | null = null;
|
let lastCanvasTimestamp: number | null = null;
|
||||||
@@ -661,7 +724,7 @@ export class Conversion {
|
|||||||
await this._started;
|
await this._started;
|
||||||
|
|
||||||
const sink = new VideoSampleSink(track);
|
const sink = new VideoSampleSink(track);
|
||||||
const frameRate = this._options.video?.frameRate;
|
const frameRate = trackOptions.frameRate;
|
||||||
|
|
||||||
let lastSample: VideoSample | null = null;
|
let lastSample: VideoSample | null = null;
|
||||||
let lastSampleTimestamp: number | null = null;
|
let lastSampleTimestamp: number | null = null;
|
||||||
@@ -744,7 +807,7 @@ export class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.output.addVideoTrack(videoSource, {
|
this.output.addVideoTrack(videoSource, {
|
||||||
frameRate: this._options.video?.frameRate,
|
frameRate: trackOptions.frameRate,
|
||||||
languageCode: track.languageCode,
|
languageCode: track.languageCode,
|
||||||
rotation: needsRerender ? 0 : totalRotation, // Rerendering will bake the rotation into the output
|
rotation: needsRerender ? 0 : totalRotation, // Rerendering will bake the rotation into the output
|
||||||
});
|
});
|
||||||
@@ -755,7 +818,7 @@ export class Conversion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
async _processAudioTrack(track: InputAudioTrack) {
|
async _processAudioTrack(track: InputAudioTrack, trackOptions: ConversionAudioOptions) {
|
||||||
const sourceCodec = track.codec;
|
const sourceCodec = track.codec;
|
||||||
if (!sourceCodec) {
|
if (!sourceCodec) {
|
||||||
this.discardedTracks.push({
|
this.discardedTracks.push({
|
||||||
@@ -772,8 +835,8 @@ export class Conversion {
|
|||||||
|
|
||||||
const firstTimestamp = await track.getFirstTimestamp();
|
const firstTimestamp = await track.getFirstTimestamp();
|
||||||
|
|
||||||
let numberOfChannels = this._options.audio?.numberOfChannels ?? originalNumberOfChannels;
|
let numberOfChannels = trackOptions.numberOfChannels ?? originalNumberOfChannels;
|
||||||
let sampleRate = this._options.audio?.sampleRate ?? originalSampleRate;
|
let sampleRate = trackOptions.sampleRate ?? originalSampleRate;
|
||||||
let needsResample = numberOfChannels !== originalNumberOfChannels
|
let needsResample = numberOfChannels !== originalNumberOfChannels
|
||||||
|| sampleRate !== originalSampleRate
|
|| sampleRate !== originalSampleRate
|
||||||
|| this._startTimestamp > 0
|
|| this._startTimestamp > 0
|
||||||
@@ -781,11 +844,11 @@ export class Conversion {
|
|||||||
|
|
||||||
let audioCodecs = this.output.format.getSupportedAudioCodecs();
|
let audioCodecs = this.output.format.getSupportedAudioCodecs();
|
||||||
if (
|
if (
|
||||||
!this._options.audio?.forceTranscode
|
!trackOptions.forceTranscode
|
||||||
&& !this._options.audio?.bitrate
|
&& !trackOptions.bitrate
|
||||||
&& !needsResample
|
&& !needsResample
|
||||||
&& audioCodecs.includes(sourceCodec)
|
&& 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
|
// Fast path, we can simply copy over the encoded packets
|
||||||
|
|
||||||
@@ -832,11 +895,11 @@ export class Conversion {
|
|||||||
|
|
||||||
let codecOfChoice: AudioCodec | null = null;
|
let codecOfChoice: AudioCodec | null = null;
|
||||||
|
|
||||||
if (this._options.audio?.codec) {
|
if (trackOptions.codec) {
|
||||||
audioCodecs = audioCodecs.filter(codec => codec === this._options.audio!.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, {
|
const encodableCodecs = await getEncodableAudioCodecs(audioCodecs, {
|
||||||
numberOfChannels,
|
numberOfChannels,
|
||||||
|
|||||||
+2
-2
@@ -84,7 +84,7 @@ export {
|
|||||||
getFirstEncodableSubtitleCodec,
|
getFirstEncodableSubtitleCodec,
|
||||||
} from './codec';
|
} from './codec';
|
||||||
export { Target, BufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions } from './target';
|
export { Target, BufferTarget, StreamTarget, StreamTargetChunk, StreamTargetOptions } from './target';
|
||||||
export { Rotation, AnyIterable, SetRequired } from './misc';
|
export { Rotation, AnyIterable, SetRequired, MaybePromise } from './misc';
|
||||||
export {
|
export {
|
||||||
Source,
|
Source,
|
||||||
BufferSource,
|
BufferSource,
|
||||||
@@ -135,7 +135,7 @@ export {
|
|||||||
AudioBufferSink,
|
AudioBufferSink,
|
||||||
WrappedAudioBuffer,
|
WrappedAudioBuffer,
|
||||||
} from './media-sink';
|
} from './media-sink';
|
||||||
export { ConversionOptions, Conversion } from './conversion';
|
export { Conversion, ConversionOptions, ConversionVideoOptions, ConversionAudioOptions } from './conversion';
|
||||||
export {
|
export {
|
||||||
CustomVideoDecoder,
|
CustomVideoDecoder,
|
||||||
CustomAudioDecoder,
|
CustomAudioDecoder,
|
||||||
|
|||||||
@@ -610,3 +610,9 @@ export const isSafari = () => {
|
|||||||
isSafariCache = result;
|
isSafariCache = result;
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* T or a promise that resolves to T.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type MaybePromise<T> = T | Promise<T>;
|
||||||
|
|||||||
Reference in New Issue
Block a user