Add isValid field to Conversion if output track counts haven't been reached, improve error messages to be more clear (closes #148)

This commit is contained in:
Vanilagy
2025-09-26 13:11:35 +02:00
parent 1b8e4f9f4b
commit be151d3494
4 changed files with 133 additions and 15 deletions
+5 -5
View File
@@ -4,7 +4,7 @@
<script src="../packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js"></script> <script src="../packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js"></script>
<script type="module"> <script type="module">
MediabunnyMp3Encoder.registerMp3Encoder(); //MediabunnyMp3Encoder.registerMp3Encoder();
const fileInput = document.createElement('input'); const fileInput = document.createElement('input');
fileInput.type = 'file'; fileInput.type = 'file';
@@ -24,7 +24,7 @@
chunked: true, chunked: true,
chunkSize: 2**20 chunkSize: 2**20
}); });
const outputFormat = new Mediabunny.WebMOutputFormat({}); const outputFormat = new Mediabunny.Mp3OutputFormat({});
const button = document.createElement('button'); const button = document.createElement('button');
button.textContent = 'Cancel'; button.textContent = 'Cancel';
@@ -92,8 +92,8 @@
}, },
*/ */
video: () => ({ video: () => ({
alpha: 'keep', //alpha: 'keep',
width: 320, //width: 320,
//discard: true, //discard: true,
//discard: true, //discard: true,
//crop: { //crop: {
@@ -146,7 +146,7 @@
} }
}, },
trim: { trim: {
//start: 10, start: 10,
//end: 20 //end: 20
}, },
}); });
+12 -1
View File
@@ -13,6 +13,7 @@ It has the following features:
- Video rotation - Video rotation
- Video cropping - Video cropping
- Video frame rate adjustment - Video frame rate adjustment
- Video transparency removal/preservation
- Audio resampling - Audio resampling
- Audio up/downmixing - Audio up/downmixing
@@ -41,6 +42,14 @@ const output = new Output({
}); });
const conversion = await Conversion.init({ input, output }); const conversion = await Conversion.init({ input, output });
if (!conversion.isValid) {
// Conversion is invalid and cannot be executed without error.
// This field gives reasons for why tracks were discarded:
conversion.discardedTracks; // => DiscardedTrack[]
return;
}
await conversion.execute(); await conversion.execute();
// output.target.buffer contains the final file // output.target.buffer contains the final file
@@ -57,7 +66,7 @@ Unconfigured, the conversion process handles all the details automatically, such
- Copying media data whenever possible, otherwise transcoding it - Copying media data whenever possible, otherwise transcoding it
- Dropping tracks that aren't supported in the output format - Dropping tracks that aren't supported in the output format
You should consider inspecting the [discarded tracks](#discarded-tracks) before executing a `Conversion`. You should consider inspecting `isValid` and the [discarded tracks](#discarded-tracks) before executing a `Conversion`.
### Monitoring progress ### Monitoring progress
@@ -353,6 +362,8 @@ type DiscardedTrack = {
Since you can inspect this list before executing a `Conversion`, this gives you the option to decide if you still want to move forward with the conversion process. Since you can inspect this list before executing a `Conversion`, this gives you the option to decide if you still want to move forward with the conversion process.
If `isValid` is `false`, then the discarded tracks caused the `Conversion` to become invalid. For example, this can happen when a format requires a specific codec but that codec cannot be encoded.
--- ---
The following reasons exist: The following reasons exist:
+8 -1
View File
@@ -484,7 +484,14 @@ const output = new Output({
}); });
const conversion = await Conversion.init({ input, output }); const conversion = await Conversion.init({ input, output });
conversion.discardedTracks; // List of tracks that won't make it into the output if (!conversion.isValid) {
// The conversion isn't possible and would error upon execution.
// Check `discardedTracks` for the reasons.
return;
}
// List of tracks that won't make it into the output:
conversion.discardedTracks;
conversion.onProgress = (progress) => { conversion.onProgress = (progress) => {
progress; // Number between 0 and 1, inclusive progress; // Number between 0 and 1, inclusive
+108 -8
View File
@@ -96,6 +96,13 @@ export type ConversionOptions = {
* If no function is set, the input's metadata tags will be copied to the output. * If no function is set, the input's metadata tags will be copied to the output.
*/ */
tags?: MetadataTags | ((inputTags: MetadataTags) => MaybePromise<MetadataTags>); tags?: MetadataTags | ((inputTags: MetadataTags) => MaybePromise<MetadataTags>);
/**
* Whether to show potential console warnings about discarded tracks after calling `Conversion.init()`, defaults to
* `true`. Set this to `false` if you're properly handling the `discardedTracks` and `isValid` fields already and
* want to keep the console output clean.
*/
showWarnings?: boolean;
}; };
/** /**
@@ -380,6 +387,11 @@ export class Conversion {
/** @internal */ /** @internal */
_lastProgress = 0; _lastProgress = 0;
/**
* Whether this conversion, as it has been configured, is valid and can be executed. If this field is `false`, check
* the `discardedTracks` field for reasons.
*/
isValid = false;
/** The list of tracks that are included in the output file. */ /** The list of tracks that are included in the output file. */
readonly utilizedTracks: InputTrack[] = []; readonly utilizedTracks: InputTrack[] = [];
/** The list of tracks from the input file that have been discarded, alongside the discard reason. */ /** The list of tracks from the input file that have been discarded, alongside the discard reason. */
@@ -449,6 +461,9 @@ export class Conversion {
if (typeof options.tags === 'object') { if (typeof options.tags === 'object') {
validateMetadataTags(options.tags); validateMetadataTags(options.tags);
} }
if (options.showWarnings !== undefined && typeof options.showWarnings !== 'boolean') {
throw new TypeError('options.showWarnings, when provided, must be a boolean.');
}
this._options = options; this._options = options;
this.input = options.input; this.input = options.input;
@@ -527,12 +542,6 @@ export class Conversion {
} }
} }
const unintentionallyDiscardedTracks = this.discardedTracks.filter(x => x.reason !== 'discarded_by_user');
if (unintentionallyDiscardedTracks.length > 0) {
// Let's give the user a notice/warning about discarded tracks so they aren't confused
console.warn('Some tracks had to be discarded from the conversion:', unintentionallyDiscardedTracks);
}
// Now, let's deal with metadata tags // Now, let's deal with metadata tags
const inputTags = await this.input.getMetadataTags(); const inputTags = await this.input.getMetadataTags();
@@ -560,14 +569,105 @@ export class Conversion {
} }
this.output.setMetadataTags(outputTags); this.output.setMetadataTags(outputTags);
// Let's check if the conversion can actually be executed
this.isValid = this._totalTrackCount >= outputTrackCounts.total.min
&& this._addedCounts.video >= outputTrackCounts.video.min
&& this._addedCounts.audio >= outputTrackCounts.audio.min
&& this._addedCounts.subtitle >= outputTrackCounts.subtitle.min;
if (this._options.showWarnings ?? true) {
const warnElements: unknown[] = [];
const unintentionallyDiscardedTracks = this.discardedTracks.filter(x => x.reason !== 'discarded_by_user');
if (unintentionallyDiscardedTracks.length > 0) {
// Let's give the user a notice/warning about discarded tracks so they aren't confused
warnElements.push(
'Some tracks had to be discarded from the conversion:', unintentionallyDiscardedTracks,
);
} }
/** Executes the conversion process. Resolves once conversion is complete. */ if (!this.isValid) {
warnElements.push('\n\n' + this._getInvalidityExplanation().join(''));
}
if (warnElements.length > 0) {
console.warn(...warnElements);
}
}
}
/** @internal */
_getInvalidityExplanation() {
const elements: string[] = [];
if (this.discardedTracks.length === 0) {
elements.push(
'Due to missing tracks, this conversion cannot be executed.',
);
} else {
const encodabilityIsTheProblem = this.discardedTracks.every(x =>
x.reason === 'discarded_by_user' || x.reason === 'no_encodable_target_codec',
);
elements.push(
'Due to discarded tracks, this conversion cannot be executed.',
);
if (encodabilityIsTheProblem) {
const codecs = this.discardedTracks.flatMap((x) => {
if (x.reason === 'discarded_by_user') return [];
if (x.track.type === 'video') {
return this.output.format.getSupportedVideoCodecs();
} else if (x.track.type === 'audio') {
return this.output.format.getSupportedAudioCodecs();
} else {
return this.output.format.getSupportedSubtitleCodecs();
}
});
if (codecs.length === 1) {
elements.push(
`\nTracks were discarded because your environment is not able to encode '${codecs[0]}'.`,
);
} else {
elements.push(
'\nTracks were discarded because your environment is not able to encode any of the following'
+ ` codecs: ${codecs.map(x => `'${x}'`).join(', ')}.`,
);
}
if (codecs.includes('mp3')) {
elements.push(
`\nThe @mediabunny/mp3-encoder extension package provides support for encoding MP3.`,
);
}
} else {
elements.push('\nCheck the discardedTracks field for more info.');
}
}
return elements;
}
/**
* Executes the conversion process. Resolves once conversion is complete.
*
* Will throw if `isValid` is `false`.
*/
async execute() { async execute() {
if (!this.isValid) {
throw new Error(
'Cannot execute this conversion because its output configuration is invalid. Make sure to always check'
+ ' the isValid field before executing a conversion.\n'
+ this._getInvalidityExplanation().join(''),
);
}
if (this._executed) { if (this._executed) {
throw new Error('Conversion cannot be executed twice.'); throw new Error('Conversion cannot be executed twice.');
} }
this._executed = true; this._executed = true;
if (this.onProgress) { if (this.onProgress) {