Repository: remotion-dev/remotion (2025-07-25T08:00:15Z)

Issue:
Be more lenient with scale factors
Tweak the logic in packages/renderer/src/validate-even-dimensions-with-codec.ts and write some tests

Currently if we are rendering a video with dimensions 4000x2592 and scale factor 0.35

it results in 1400x907 which fails because it is uneven

Be more lenient and round down if one dimension is uneven


Discussion:

Patch:
diff --git a/packages/renderer/src/validate-even-dimensions-with-codec.ts b/packages/renderer/src/validate-even-dimensions-with-codec.ts
index 430dce99baa..914e9617ee7 100644
--- a/packages/renderer/src/validate-even-dimensions-with-codec.ts
+++ b/packages/renderer/src/validate-even-dimensions-with-codec.ts
@@ -1,7 +1,6 @@
 import type {Codec} from './codec';
 import type {LogLevel} from './log-level';
 import {Log} from './logger';
-import {truthy} from './truthy';
 
 export const validateEvenDimensionsWithCodec = ({
 	width,
@@ -66,27 +65,19 @@ export const validateEvenDimensionsWithCodec = ({
 	const displayName = codec === 'h265' ? 'H265' : 'H264';
 
 	if (actualWidth % 2 !== 0) {
-		const message = [
-			`Codec error: You are trying to render a video with a ${displayName} codec that has a width of ${actualWidth}px, which is an uneven number.`,
-			`The ${displayName} codec does only support dimensions that are evenly divisible by two.`,
-			scale === 1
-				? `Change the width to ${Math.floor(width - 1)}px to fix this issue.`
-				: `You have used the "scale" option which might be the reason for the problem: The original width is ${width} and the scale is ${scale}x, which was multiplied to get the actual width.`,
-		]
-			.filter(truthy)
-			.join(' ');
-		throw new Error(message);
+		Log.verbose(
+			{indent, logLevel},
+			`Rounding width down to an even number from ${actualWidth} to ${actualWidth - 1} for ${displayName} codec compatibility`,
+		);
+		actualWidth -= 1;
 	}
 
 	if (actualHeight % 2 !== 0) {
-		const message = [
-			`Codec error: You are trying to render a video with a ${displayName} codec that has a height of ${actualHeight}px, which is an uneven number.`,
-			`The ${displayName} codec does only support dimensions that are evenly divisible by two. `,
-			scale === 1
-				? `Change the height to ${Math.floor(actualHeight - 1)}px to fix this issue.`
-				: `You have used the "scale" option which might be the reason for the problem: The original height is ${height} and the scale is ${scale}x, which was multiplied to get the actual height.`,
-		].join(' ');
-		throw new Error(message);
+		Log.verbose(
+			{indent, logLevel},
+			`Rounding height down to an even number from ${actualHeight} to ${actualHeight - 1} for ${displayName} codec compatibility`,
+		);
+		actualHeight -= 1;
 	}
 
 	return {


Test patch:
diff --git a/packages/renderer/src/test/validate-even-dimensions-with-codec.test.ts b/packages/renderer/src/test/validate-even-dimensions-with-codec.test.ts
new file mode 100644
index 00000000000..39e230e0d42
--- /dev/null
+++ b/packages/renderer/src/test/validate-even-dimensions-with-codec.test.ts
@@ -0,0 +1,192 @@
+import {describe, expect, test} from 'bun:test';
+import {validateEvenDimensionsWithCodec} from '../validate-even-dimensions-with-codec';
+
+describe('validateEvenDimensionsWithCodec', () => {
+	const defaultConfig = {
+		indent: false,
+		logLevel: 'info' as const,
+		wantsImageSequence: false,
+	};
+
+	describe('H264/H265 codec behavior with odd dimensions', () => {
+		test('should round down odd width to even when using H264', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 4000,
+				height: 2592,
+				scale: 0.35,
+				codec: 'h264',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(1400); // 4000 * 0.35 = 1400 (even)
+			expect(result.actualHeight).toBe(906); // 2592 * 0.35 = 907.2 -> 907 (odd) -> 906 (even)
+		});
+
+		test('should round down odd width to even when using H265', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 301,
+				height: 200,
+				scale: 1,
+				codec: 'h265',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(300); // 301 (odd) -> 300 (even)
+			expect(result.actualHeight).toBe(200); // 200 (already even)
+		});
+
+		test('should round down odd height to even when using H264', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 200,
+				height: 301,
+				scale: 1,
+				codec: 'h264',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(200); // 200 (already even)
+			expect(result.actualHeight).toBe(300); // 301 (odd) -> 300 (even)
+		});
+
+		test('should round down both dimensions when both are odd', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 301,
+				height: 201,
+				scale: 1,
+				codec: 'h264',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(300); // 301 (odd) -> 300 (even)
+			expect(result.actualHeight).toBe(200); // 201 (odd) -> 200 (even)
+		});
+
+		test('should work with H264-ts codec', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 301,
+				height: 200,
+				scale: 1,
+				codec: 'h264-ts',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(300); // 301 (odd) -> 300 (even)
+			expect(result.actualHeight).toBe(200); // 200 (already even)
+		});
+
+		test('should work with H264-mkv codec', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 301,
+				height: 200,
+				scale: 1,
+				codec: 'h264-mkv',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(300); // 301 (odd) -> 300 (even)
+			expect(result.actualHeight).toBe(200); // 200 (already even)
+		});
+	});
+
+	describe('Even dimensions should remain unchanged', () => {
+		test('should keep even dimensions as-is for H264', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 1920,
+				height: 1080,
+				scale: 1,
+				codec: 'h264',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(1920);
+			expect(result.actualHeight).toBe(1080);
+		});
+
+		test('should keep even dimensions as-is for H265', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 1000,
+				height: 600,
+				scale: 0.5,
+				codec: 'h265',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(500);
+			expect(result.actualHeight).toBe(300);
+		});
+	});
+
+	describe('Other codecs should not be affected', () => {
+		test('should allow odd dimensions for VP8', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 301,
+				height: 201,
+				scale: 1,
+				codec: 'vp8',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(301);
+			expect(result.actualHeight).toBe(201);
+		});
+
+		test('should allow odd dimensions for VP9', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 301,
+				height: 201,
+				scale: 1,
+				codec: 'vp9',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(301);
+			expect(result.actualHeight).toBe(201);
+		});
+
+		test('should allow odd dimensions for ProRes', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 301,
+				height: 201,
+				scale: 1,
+				codec: 'prores',
+				...defaultConfig,
+			});
+
+			expect(result.actualWidth).toBe(301);
+			expect(result.actualHeight).toBe(201);
+		});
+	});
+
+	describe('Image sequences should not be affected', () => {
+		test('should allow odd dimensions for image sequences even with H264', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 301,
+				height: 201,
+				scale: 1,
+				codec: 'h264',
+				wantsImageSequence: true,
+				indent: false,
+				logLevel: 'info' as const,
+			});
+
+			expect(result.actualWidth).toBe(301);
+			expect(result.actualHeight).toBe(201);
+		});
+	});
+
+	describe('Fractional dimensions rounding', () => {
+		test('should handle fractional dimensions correctly', () => {
+			const result = validateEvenDimensionsWithCodec({
+				width: 1000,
+				height: 1000,
+				scale: 0.333,
+				codec: 'h264',
+				...defaultConfig,
+			});
+
+			// 1000 * 0.333 = 333, which is odd, should become 332
+			expect(result.actualWidth).toBe(332);
+			expect(result.actualHeight).toBe(332);
+		});
+	});
+});