Explicitly throw for odd video dimensions for AVC and HEVC (closes #142)

This commit is contained in:
Vanilagy
2025-09-22 15:37:31 +02:00
parent 6aee4e6a6a
commit f9a7735aae
4 changed files with 110 additions and 7 deletions
+2 -2
View File
@@ -33,8 +33,8 @@
"watch": "tsx scripts/bundle.ts --watch",
"lint": "eslint .",
"test-node": "cd test && vitest node --run",
"test-web": "cd test && vitest browser --run --browser",
"test": "npm run test-node && npm run test-web",
"test-browser": "cd test && vitest browser --run --browser",
"test": "npm run test-node && npm run test-browser",
"check": "tsc -p src --noEmit && tsc -p packages/mp3-encoder/src --noEmit && tsc -p scripts --noEmit && tsc -p tsconfig.vite.json --noEmit && rm tsconfig.vite.tsbuildinfo",
"check-docblocks": "tsx scripts/check-docblocks.ts dist/mediabunny.d.ts",
"docs:dev": "vitepress dev docs",
+9
View File
@@ -490,6 +490,15 @@ export const canEncodeVideo = async (
return false;
}
const hasOddDimension = width % 2 === 1 || height % 2 === 1;
if (
hasOddDimension
&& (codec === 'avc' || codec === 'hevc')
) {
// Disallow odd dimensions for certain codecs
return false;
}
encoderConfig ??= buildVideoEncoderConfig({
codec,
width,
+18 -5
View File
@@ -292,7 +292,7 @@ class VideoEncoderWrapper {
if (!this.encoderInitialized) {
if (!this.ensureEncoderPromise) {
void this.ensureEncoder(videoSample);
this.ensureEncoder(videoSample);
}
// No, this "if" statement is not useless. Sometimes, the above call to `ensureEncoder` might have
@@ -363,13 +363,13 @@ class VideoEncoderWrapper {
}
}
private async ensureEncoder(videoSample: VideoSample) {
private ensureEncoder(videoSample: VideoSample) {
if (this.encoder) {
return;
}
const encoderError = new Error();
return this.ensureEncoderPromise = (async () => {
this.ensureEncoderPromise = (async () => {
const encoderConfig = buildVideoEncoderConfig({
width: videoSample.codedWidth,
height: videoSample.codedHeight,
@@ -409,6 +409,19 @@ class VideoEncoderWrapper {
throw new Error('VideoEncoder is not supported by this browser.');
}
const hasOddDimension = encoderConfig.width % 2 === 1 || encoderConfig.height % 2 === 1;
if (
hasOddDimension
&& (this.encodingConfig.codec === 'avc' || this.encodingConfig.codec === 'hevc')
) {
// Throw a special error for this case as it gets hit often
throw new Error(
`The dimensions ${encoderConfig.width}x${encoderConfig.height} are not supported for codec`
+ ` '${this.encodingConfig.codec}'; both width and height must be even numbers. Make sure to`
+ ` round your dimensions to the nearest even number.`,
);
}
const support = await VideoEncoder.isConfigSupported(encoderConfig);
if (!support.supported) {
throw new Error(
@@ -873,7 +886,7 @@ class AudioEncoderWrapper {
if (!this.encoderInitialized) {
if (!this.ensureEncoderPromise) {
void this.ensureEncoder(audioSample);
this.ensureEncoder(audioSample);
}
// No, this "if" statement is not useless. Sometimes, the above call to `ensureEncoder` might have
@@ -1010,7 +1023,7 @@ class AudioEncoderWrapper {
}
const encoderError = new Error();
return this.ensureEncoderPromise = (async () => {
this.ensureEncoderPromise = (async () => {
const { numberOfChannels, sampleRate } = audioSample;
const encoderConfig = buildAudioEncoderConfig({
+81
View File
@@ -0,0 +1,81 @@
import { expect, test } from 'vitest';
import { Output } from '../../src/output.js';
import { Mp4OutputFormat } from '../../src/output-format.js';
import { NullTarget } from '../../src/target.js';
import { VideoSampleSource } from '../../src/media-source.js';
import { canEncodeVideo, QUALITY_HIGH } from '../../src/encode.js';
import { VideoSample } from '../../src/sample.js';
test('Odd video dimensions fail for AVC', async () => {
const output = new Output({
format: new Mp4OutputFormat(),
target: new NullTarget(),
});
const source = new VideoSampleSource({
codec: 'avc',
bitrate: QUALITY_HIGH,
});
output.addVideoTrack(source);
await output.start();
const canvas = document.createElement('canvas');
canvas.width = 1281;
canvas.height = 720;
const sample = new VideoSample(canvas, { timestamp: 0 });
await expect(source.add(sample)).rejects.toThrow('even number'); // The error message is explicit
});
test('Odd video dimensions fail for HEVC', async () => {
const output = new Output({
format: new Mp4OutputFormat(),
target: new NullTarget(),
});
const source = new VideoSampleSource({
codec: 'hevc',
bitrate: QUALITY_HIGH,
});
output.addVideoTrack(source);
await output.start();
const canvas = document.createElement('canvas');
canvas.width = 1281;
canvas.height = 720;
const sample = new VideoSample(canvas, { timestamp: 0 });
await expect(source.add(sample)).rejects.toThrow('even number');
});
test('Odd video dimensions pass for VP9', async () => {
const output = new Output({
format: new Mp4OutputFormat(),
target: new NullTarget(),
});
const source = new VideoSampleSource({
codec: 'vp9',
bitrate: QUALITY_HIGH,
});
output.addVideoTrack(source);
await output.start();
const canvas = document.createElement('canvas');
canvas.width = 1281;
canvas.height = 720;
const sample = new VideoSample(canvas, { timestamp: 0 });
await source.add(sample);
});
test('Odd video dimensions encodability checks', async () => {
expect(await canEncodeVideo('avc', { width: 1920, height: 1081 })).toBe(false);
expect(await canEncodeVideo('hevc', { width: 1920, height: 1081 })).toBe(false);
expect(await canEncodeVideo('vp8', { width: 1920, height: 1081 })).toBe(true);
expect(await canEncodeVideo('vp9', { width: 1920, height: 1081 })).toBe(true);
expect(await canEncodeVideo('av1', { width: 1920, height: 1081 })).toBe(true);
});