Files
openscreen/src/lib/exporter/mp4ExportSettings.ts
T
2026-05-16 20:19:00 +02:00

144 lines
3.4 KiB
TypeScript

import type { ExportQuality } from "./types";
export interface Mp4ExportSettings {
width: number;
height: number;
bitrate: number;
}
interface SourceCropRegion {
width: number;
height: number;
}
const MEDIUM_SHORT_SIDE = 720;
const HIGH_SHORT_SIDE = 1080;
function even(value: number) {
return Math.floor(value / 2) * 2;
}
function atLeastEven(value: number) {
return Math.max(2, even(value));
}
export function calculateEffectiveSourceDimensions(
sourceWidth: number,
sourceHeight: number,
cropRegion?: SourceCropRegion,
) {
const cropWidth = cropRegion?.width ?? 1;
const cropHeight = cropRegion?.height ?? 1;
return {
width: atLeastEven(Math.round(sourceWidth * cropWidth)),
height: atLeastEven(Math.round(sourceHeight * cropHeight)),
};
}
function calculateDimensionsForShortSide(targetShortSide: number, aspectRatioValue: number) {
if (aspectRatioValue >= 1) {
const height = even(targetShortSide);
return {
width: even(height * aspectRatioValue),
height,
};
}
const width = even(targetShortSide);
return {
width,
height: even(width / aspectRatioValue),
};
}
function calculateSourceDimensions(
sourceWidth: number,
sourceHeight: number,
aspectRatioValue: number,
) {
const sourceLongDim = Math.max(sourceWidth, sourceHeight);
if (aspectRatioValue === 1) {
const baseDimension = even(Math.min(sourceWidth, sourceHeight));
return {
width: baseDimension,
height: baseDimension,
};
}
if (aspectRatioValue > 1) {
const baseWidth = even(sourceLongDim);
for (let width = baseWidth; width >= 100; width -= 2) {
const height = Math.round(width / aspectRatioValue);
if (height % 2 === 0 && Math.abs(width / height - aspectRatioValue) < 0.0001) {
return { width, height };
}
}
return {
width: baseWidth,
height: even(baseWidth / aspectRatioValue),
};
}
const baseHeight = even(sourceLongDim);
for (let height = baseHeight; height >= 100; height -= 2) {
const width = Math.round(height * aspectRatioValue);
if (width % 2 === 0 && Math.abs(width / height - aspectRatioValue) < 0.0001) {
return { width, height };
}
}
return {
width: even(baseHeight * aspectRatioValue),
height: baseHeight,
};
}
function calculateBitrate(width: number, height: number, quality: ExportQuality) {
const totalPixels = width * height;
if (quality === "source") {
if (totalPixels > 2560 * 1440) return 80_000_000;
if (totalPixels > 1920 * 1080) return 50_000_000;
return 30_000_000;
}
if (totalPixels <= 1280 * 720) return 10_000_000;
if (totalPixels <= 1920 * 1080) return 20_000_000;
return 30_000_000;
}
export function calculateMp4ExportSettings({
quality,
sourceWidth,
sourceHeight,
aspectRatioValue,
}: {
quality: ExportQuality;
sourceWidth: number;
sourceHeight: number;
aspectRatioValue: number;
}): Mp4ExportSettings {
if (quality === "medium") {
const dimensions = calculateDimensionsForShortSide(MEDIUM_SHORT_SIDE, aspectRatioValue);
return {
...dimensions,
bitrate: calculateBitrate(dimensions.width, dimensions.height, quality),
};
}
if (quality === "good") {
const dimensions = calculateDimensionsForShortSide(HIGH_SHORT_SIDE, aspectRatioValue);
return {
...dimensions,
bitrate: calculateBitrate(dimensions.width, dimensions.height, quality),
};
}
const sourceDimensions = calculateSourceDimensions(sourceWidth, sourceHeight, aspectRatioValue);
return {
...sourceDimensions,
bitrate: calculateBitrate(sourceDimensions.width, sourceDimensions.height, quality),
};
}