Merge pull request #598 from EtienneLescot/codex/fix-high-quality-export

Clarify export resolution presets
This commit is contained in:
Sid
2026-05-16 12:34:33 -07:00
committed by GitHub
18 changed files with 518 additions and 161 deletions
+99 -35
View File
@@ -40,7 +40,11 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useScopedT } from "@/contexts/I18nContext";
import { WEBCAM_LAYOUT_PRESETS } from "@/lib/compositeLayout";
import type { ExportFormat, ExportQuality, GifFrameRate, GifSizePreset } from "@/lib/exporter";
import { GIF_FRAME_RATES, GIF_SIZE_PRESETS } from "@/lib/exporter";
import {
calculateEffectiveSourceDimensions,
GIF_FRAME_RATES,
GIF_SIZE_PRESETS,
} from "@/lib/exporter";
import { cn } from "@/lib/utils";
import { resolveImageWallpaperUrl, WALLPAPER_PATHS } from "@/lib/wallpaper";
import { type AspectRatio, isPortraitAspectRatio } from "@/utils/aspectRatioUtils";
@@ -328,6 +332,23 @@ const ZOOM_DEPTH_OPTIONS: Array<{ depth: ZoomDepth; label: string }> = [
type SettingsPanelMode = "background" | "effects" | "layout" | "cursor" | "export";
const MP4_EXPORT_SHORT_SIDES = {
medium: 720,
good: 1080,
} as const;
function formatSourceDimensions(videoElement?: HTMLVideoElement | null, cropRegion?: CropRegion) {
const width = videoElement?.videoWidth ?? 0;
const height = videoElement?.videoHeight ?? 0;
if (width <= 0 || height <= 0) {
return null;
}
const dimensions = calculateEffectiveSourceDimensions(width, height, cropRegion);
return { ...dimensions, shortSide: Math.min(dimensions.width, dimensions.height) };
}
export function SettingsPanel({
selected,
onWallpaperChange,
@@ -421,6 +442,7 @@ export function SettingsPanel({
}: SettingsPanelProps) {
const t = useScopedT("settings");
const [activePanelMode, setActivePanelMode] = useState<SettingsPanelMode>("background");
const sourceDimensions = formatSourceDimensions(videoElement, cropRegion);
// Resolved URLs are for DOM rendering only (backgroundImage). The canonical
// `/wallpapers/wallpaperN.jpg` form in WALLPAPER_PATHS is what gets persisted
// on click — never the machine-specific file:// URL.
@@ -1776,40 +1798,82 @@ export function SettingsPanel({
</div>
{exportFormat === "mp4" && (
<div className="mb-3 bg-white/5 border border-white/5 p-0.5 w-full grid grid-cols-3 h-7 rounded-lg">
<button
onClick={() => onExportQualityChange?.("medium")}
className={cn(
"rounded-md transition-all text-[10px] font-medium",
exportQuality === "medium"
? "bg-white text-black"
: "text-slate-400 hover:text-slate-200",
)}
>
{t("exportQuality.low")}
</button>
<button
onClick={() => onExportQualityChange?.("good")}
className={cn(
"rounded-md transition-all text-[10px] font-medium",
exportQuality === "good"
? "bg-white text-black"
: "text-slate-400 hover:text-slate-200",
)}
>
{t("exportQuality.medium")}
</button>
<button
onClick={() => onExportQualityChange?.("source")}
className={cn(
"rounded-md transition-all text-[10px] font-medium",
exportQuality === "source"
? "bg-white text-black"
: "text-slate-400 hover:text-slate-200",
)}
>
{t("exportQuality.high")}
</button>
<div className="mb-3 space-y-1.5">
{sourceDimensions && (
<div className="flex items-center justify-between px-0.5 text-[10px] leading-none text-slate-500">
<span>{t("exportQuality.title")}</span>
<span>
Source {sourceDimensions.width}x{sourceDimensions.height}
</span>
</div>
)}
<div className="bg-white/5 border border-white/5 p-0.5 w-full grid grid-cols-3 h-9 rounded-lg">
<button
onClick={() => onExportQualityChange?.("medium")}
className={cn(
"rounded-md transition-all text-[10px] font-medium flex flex-col items-center justify-center leading-none gap-0.5",
exportQuality === "medium"
? "bg-white text-black"
: "text-slate-400 hover:text-slate-200",
)}
>
<span>{t("exportQuality.low")}</span>
{sourceDimensions &&
sourceDimensions.shortSide < MP4_EXPORT_SHORT_SIDES.medium && (
<span
className={cn(
"text-[8px] font-medium",
exportQuality === "medium" ? "text-black/55" : "text-amber-300/80",
)}
>
Upscale
</span>
)}
</button>
<button
onClick={() => onExportQualityChange?.("good")}
className={cn(
"rounded-md transition-all text-[10px] font-medium flex flex-col items-center justify-center leading-none gap-0.5",
exportQuality === "good"
? "bg-white text-black"
: "text-slate-400 hover:text-slate-200",
)}
>
<span>{t("exportQuality.medium")}</span>
{sourceDimensions &&
sourceDimensions.shortSide < MP4_EXPORT_SHORT_SIDES.good && (
<span
className={cn(
"text-[8px] font-medium",
exportQuality === "good" ? "text-black/55" : "text-amber-300/80",
)}
>
Upscale
</span>
)}
</button>
<button
onClick={() => onExportQualityChange?.("source")}
className={cn(
"rounded-md transition-all text-[10px] font-medium flex flex-col items-center justify-center leading-none gap-0.5",
exportQuality === "source"
? "bg-white text-black"
: "text-slate-400 hover:text-slate-200",
)}
>
<span>{t("exportQuality.high")}</span>
{sourceDimensions && (
<span
className={cn(
"text-[8px] font-medium",
exportQuality === "source" ? "text-black/55" : "text-slate-500",
)}
>
{sourceDimensions.shortSide}p
</span>
)}
</button>
</div>
</div>
)}
+34 -81
View File
@@ -18,6 +18,8 @@ import { type Locale } from "@/i18n/config";
import { getAvailableLocales, getLocaleName } from "@/i18n/loader";
import { hasNativeCursorRecordingData } from "@/lib/cursor/nativeCursor";
import {
calculateEffectiveSourceDimensions,
calculateMp4ExportSettings,
calculateOutputDimensions,
type ExportFormat,
type ExportProgress,
@@ -1574,6 +1576,11 @@ export default function VideoEditor() {
const sourceWidth = video.videoWidth || 1920;
const sourceHeight = video.videoHeight || 1080;
const effectiveSourceDimensions = calculateEffectiveSourceDimensions(
sourceWidth,
sourceHeight,
cropRegion,
);
const aspectRatioValue =
aspectRatio === "native"
? getNativeAspectRatioValue(sourceWidth, sourceHeight, cropRegion)
@@ -1667,83 +1674,16 @@ export default function VideoEditor() {
} else {
// MP4 Export
const quality = settings.quality || exportQuality;
let exportWidth: number;
let exportHeight: number;
let bitrate: number;
if (quality === "source") {
exportWidth = sourceWidth;
exportHeight = sourceHeight;
// Use the source's longer dimension as the long axis of the export so
// a landscape recording can still fill a portrait target (and vice versa).
const sourceLongDim = Math.max(sourceWidth, sourceHeight);
if (aspectRatioValue === 1) {
const baseDimension = Math.floor(Math.min(sourceWidth, sourceHeight) / 2) * 2;
exportWidth = baseDimension;
exportHeight = baseDimension;
} else if (aspectRatioValue > 1) {
const baseWidth = Math.floor(sourceLongDim / 2) * 2;
let found = false;
for (let w = baseWidth; w >= 100 && !found; w -= 2) {
const h = Math.round(w / aspectRatioValue);
if (h % 2 === 0 && Math.abs(w / h - aspectRatioValue) < 0.0001) {
exportWidth = w;
exportHeight = h;
found = true;
}
}
if (!found) {
exportWidth = baseWidth;
exportHeight = Math.floor(baseWidth / aspectRatioValue / 2) * 2;
}
} else {
const baseHeight = Math.floor(sourceLongDim / 2) * 2;
let found = false;
for (let h = baseHeight; h >= 100 && !found; h -= 2) {
const w = Math.round(h * aspectRatioValue);
if (w % 2 === 0 && Math.abs(w / h - aspectRatioValue) < 0.0001) {
exportWidth = w;
exportHeight = h;
found = true;
}
}
if (!found) {
exportHeight = baseHeight;
exportWidth = Math.floor((baseHeight * aspectRatioValue) / 2) * 2;
}
}
const totalPixels = exportWidth * exportHeight;
bitrate = 30_000_000;
if (totalPixels > 1920 * 1080 && totalPixels <= 2560 * 1440) {
bitrate = 50_000_000;
} else if (totalPixels > 2560 * 1440) {
bitrate = 80_000_000;
}
} else {
// Quality presets target the SHORT side; the long side derives from the
// aspect ratio. This keeps 1080p portrait at 1080×1920 instead of 607×1080.
const targetShortDim = quality === "medium" ? 720 : 1080;
if (aspectRatioValue >= 1) {
exportHeight = Math.floor(targetShortDim / 2) * 2;
exportWidth = Math.floor((exportHeight * aspectRatioValue) / 2) * 2;
} else {
exportWidth = Math.floor(targetShortDim / 2) * 2;
exportHeight = Math.floor(exportWidth / aspectRatioValue / 2) * 2;
}
const totalPixels = exportWidth * exportHeight;
if (totalPixels <= 1280 * 720) {
bitrate = 10_000_000;
} else if (totalPixels <= 1920 * 1080) {
bitrate = 20_000_000;
} else {
bitrate = 30_000_000;
}
}
const {
width: exportWidth,
height: exportHeight,
bitrate,
} = calculateMp4ExportSettings({
quality,
sourceWidth: effectiveSourceDimensions.width,
sourceHeight: effectiveSourceDimensions.height,
aspectRatioValue,
});
const exporter = new VideoExporter({
videoUrl: videoPath,
@@ -1903,13 +1843,18 @@ export default function VideoEditor() {
// Build export settings from current state
const sourceWidth = video.videoWidth || 1920;
const sourceHeight = video.videoHeight || 1080;
const effectiveSourceDimensions = calculateEffectiveSourceDimensions(
sourceWidth,
sourceHeight,
cropRegion,
);
const aspectRatioValue =
aspectRatio === "native"
? getNativeAspectRatioValue(sourceWidth, sourceHeight, cropRegion)
: getAspectRatioValue(aspectRatio);
const gifDimensions = calculateOutputDimensions(
sourceWidth,
sourceHeight,
effectiveSourceDimensions.width,
effectiveSourceDimensions.height,
gifSizePreset,
GIF_SIZE_PRESETS,
aspectRatioValue,
@@ -2266,8 +2211,16 @@ export default function VideoEditor() {
gifSizePreset={gifSizePreset}
onGifSizePresetChange={setGifSizePreset}
gifOutputDimensions={calculateOutputDimensions(
videoPlaybackRef.current?.video?.videoWidth || 1920,
videoPlaybackRef.current?.video?.videoHeight || 1080,
calculateEffectiveSourceDimensions(
videoPlaybackRef.current?.video?.videoWidth || 1920,
videoPlaybackRef.current?.video?.videoHeight || 1080,
cropRegion,
).width,
calculateEffectiveSourceDimensions(
videoPlaybackRef.current?.video?.videoWidth || 1920,
videoPlaybackRef.current?.video?.videoHeight || 1080,
cropRegion,
).height,
gifSizePreset,
GIF_SIZE_PRESETS,
aspectRatio === "native"
+4 -4
View File
@@ -69,10 +69,10 @@
"gifDescription": "صورة متحركة للمشاركة"
},
"exportQuality": {
"title": "جودة التصدير",
"low": "منخفضة",
"medium": "متوسطة",
"high": "عالية"
"title": قة التصدير",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "معدل إطارات GIF",
+4 -4
View File
@@ -85,10 +85,10 @@
"gifDescription": "Animated image for sharing"
},
"exportQuality": {
"title": "Export Quality",
"low": "Low",
"medium": "Medium",
"high": "High"
"title": "Export resolution",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "GIF Frame Rate",
+4 -4
View File
@@ -76,10 +76,10 @@
"gifDescription": "Imagen animada para compartir"
},
"exportQuality": {
"title": "Calidad de exportación",
"low": "Baja",
"medium": "Media",
"high": "Alta"
"title": "Resolución de exportación",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "Velocidad de cuadros del GIF",
+4 -4
View File
@@ -83,10 +83,10 @@
"gifDescription": "Image animée pour le partage"
},
"exportQuality": {
"title": "Qualité d'export",
"low": "Faible",
"medium": "Moyenne",
"high": "Haute"
"title": "Résolution d'export",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "Fréquence d'images GIF",
+4 -4
View File
@@ -76,10 +76,10 @@
"gifDescription": "共有用のアニメーション画像"
},
"exportQuality": {
"title": "エクスポート品質",
"low": "低画質",
"medium": "中画質",
"high": "高画質"
"title": "書き出し解像度",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "GIF フレームレート",
+4 -4
View File
@@ -76,10 +76,10 @@
"gifDescription": "공유용 애니메이션 이미지"
},
"exportQuality": {
"title": "내보내기 품질",
"low": "낮음",
"medium": "보통",
"high": "높음"
"title": "내보내기 해상도",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "GIF 프레임 속도",
+4 -4
View File
@@ -77,10 +77,10 @@
"gifDescription": "Анимированное изображение для обмена"
},
"exportQuality": {
"title": "Качество экспорта",
"low": "Низкое",
"medium": "Среднее",
"high": "Высокое"
"title": "Разрешение экспорта",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "Частота кадров GIF",
+4 -4
View File
@@ -76,10 +76,10 @@
"gifDescription": "Paylaşım için hareketli görüntü"
},
"exportQuality": {
"title": "Dışa Aktarım Kalitesi",
"low": "Düşük",
"medium": "Orta",
"high": "Yüksek"
"title": "Dışa aktarma çözünürlüğü",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "GIF Kare Hızı",
+4 -4
View File
@@ -66,10 +66,10 @@
"gifDescription": "Hình ảnh động để chia sẻ"
},
"exportQuality": {
"title": "Chất lượng xuất",
"low": "Thấp",
"medium": "Trung bình",
"high": "Cao"
"title": "Độ phân giải xuất",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "Tốc độ khung hình GIF",
+4 -4
View File
@@ -76,10 +76,10 @@
"gifDescription": "可分享的动态图片"
},
"exportQuality": {
"title": "导出质量",
"low": "",
"medium": "",
"high": ""
"title": "导出分辨率",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "GIF 帧率",
+4 -4
View File
@@ -83,10 +83,10 @@
"gifDescription": "可分享的動態圖片"
},
"exportQuality": {
"title": "匯出品質",
"low": "",
"medium": "",
"high": ""
"title": "匯出解析度",
"low": "720p",
"medium": "1080p",
"high": "Source"
},
"gifSettings": {
"frameRate": "GIF 影格率",
+30
View File
@@ -17,6 +17,21 @@ describe("computeCompositeLayout", () => {
expect(layout!.webcamRect!.y).toBeGreaterThan(1080 / 2);
});
it("scales small screen content up to the export canvas when no padding is applied", () => {
const layout = computeCompositeLayout({
canvasSize: { width: 1280, height: 720 },
screenSize: { width: 854, height: 480 },
});
expect(layout).not.toBeNull();
expect(layout!.screenRect).toEqual({
x: 0,
y: 0,
width: 1280,
height: 720,
});
});
it("keeps the overlay within the configured stage fraction while preserving aspect ratio", () => {
const layout = computeCompositeLayout({
canvasSize: { width: 1280, height: 720 },
@@ -128,6 +143,21 @@ describe("computeCompositeLayout", () => {
expect(aboveMax!.webcamRect!.height).toBe(atMax!.webcamRect!.height);
});
it("snaps rounding-only source aspect gaps to the full canvas", () => {
const layout = computeCompositeLayout({
canvasSize: { width: 319, height: 199 },
maxContentSize: { width: 319, height: 199 },
screenSize: { width: 1680, height: 1050 },
});
expect(layout?.screenRect).toEqual({
x: 0,
y: 0,
width: 319,
height: 199,
});
});
it("centers the combined screen and webcam stack in vertical stack mode", () => {
const layout = computeCompositeLayout({
canvasSize: { width: 1920, height: 1080 },
+15 -1
View File
@@ -401,10 +401,24 @@ function centerRectInBounds(params: { bounds: RenderRect; size: Size; maxSize: S
const { x: boundsX, y: boundsY, width: boundsWidth, height: boundsHeight } = bounds;
const { width, height } = size;
const { width: maxWidth, height: maxHeight } = maxSize;
const scale = Math.min(maxWidth / width, maxHeight / height, 1);
const scale = Math.min(maxWidth / width, maxHeight / height);
const resolvedWidth = Math.round(width * scale);
const resolvedHeight = Math.round(height * scale);
if (
maxWidth >= boundsWidth &&
maxHeight >= boundsHeight &&
Math.abs(boundsWidth - resolvedWidth) <= 1 &&
Math.abs(boundsHeight - resolvedHeight) <= 1
) {
return {
x: boundsX,
y: boundsY,
width: boundsWidth,
height: boundsHeight,
};
}
return {
x: boundsX + Math.max(0, Math.floor((boundsWidth - resolvedWidth) / 2)),
y: boundsY + Math.max(0, Math.floor((boundsHeight - resolvedHeight) / 2)),
+5
View File
@@ -1,5 +1,10 @@
export { FrameRenderer } from "./frameRenderer";
export { calculateOutputDimensions, GifExporter } from "./gifExporter";
export {
calculateEffectiveSourceDimensions,
calculateMp4ExportSettings,
type Mp4ExportSettings,
} from "./mp4ExportSettings";
export { VideoMuxer } from "./muxer";
export { StreamingVideoDecoder } from "./streamingDecoder";
export type {
+148
View File
@@ -0,0 +1,148 @@
import { describe, expect, it } from "vitest";
import {
calculateEffectiveSourceDimensions,
calculateMp4ExportSettings,
} from "./mp4ExportSettings";
describe("calculateMp4ExportSettings", () => {
it("keeps 1080p explicit even when it upscales short native captures", () => {
const aspectRatioValue = 1920 / 1032;
expect(
calculateMp4ExportSettings({
quality: "good",
sourceWidth: 1920,
sourceHeight: 1032,
aspectRatioValue,
}),
).toMatchObject({
width: 2008,
height: 1080,
bitrate: 30_000_000,
});
expect(
calculateMp4ExportSettings({
quality: "source",
sourceWidth: 1920,
sourceHeight: 1032,
aspectRatioValue,
}),
).toMatchObject({
width: 1920,
height: 1032,
bitrate: 30_000_000,
});
});
it("keeps lower quality presets below the source size when downscaling is useful", () => {
expect(
calculateMp4ExportSettings({
quality: "medium",
sourceWidth: 1920,
sourceHeight: 1032,
aspectRatioValue: 1920 / 1032,
}),
).toMatchObject({
width: 1338,
height: 720,
bitrate: 20_000_000,
});
});
it("keeps 1080p explicit even for 720p source dimensions", () => {
expect(
calculateMp4ExportSettings({
quality: "good",
sourceWidth: 1280,
sourceHeight: 720,
aspectRatioValue: 16 / 9,
}),
).toMatchObject({
width: 1920,
height: 1080,
bitrate: 20_000_000,
});
});
it("preserves source-sized High exports when the source is already 1080p or larger", () => {
expect(
calculateMp4ExportSettings({
quality: "source",
sourceWidth: 1920,
sourceHeight: 1080,
aspectRatioValue: 16 / 9,
}),
).toMatchObject({
width: 1920,
height: 1080,
bitrate: 30_000_000,
});
expect(
calculateMp4ExportSettings({
quality: "source",
sourceWidth: 3840,
sourceHeight: 2160,
aspectRatioValue: 16 / 9,
}),
).toMatchObject({
width: 3840,
height: 2160,
bitrate: 80_000_000,
});
});
it("keeps portrait presets on the short side", () => {
expect(
calculateMp4ExportSettings({
quality: "good",
sourceWidth: 1080,
sourceHeight: 1920,
aspectRatioValue: 9 / 16,
}),
).toMatchObject({
width: 1080,
height: 1920,
bitrate: 20_000_000,
});
});
it("uses the cropped area as the effective source size", () => {
const effectiveSource = calculateEffectiveSourceDimensions(3840, 2160, {
width: 854 / 3840,
height: 480 / 2160,
});
expect(effectiveSource).toEqual({
width: 854,
height: 480,
});
expect(
calculateMp4ExportSettings({
quality: "source",
sourceWidth: effectiveSource.width,
sourceHeight: effectiveSource.height,
aspectRatioValue: effectiveSource.width / effectiveSource.height,
}),
).toMatchObject({
width: 854,
height: 480,
bitrate: 30_000_000,
});
expect(
calculateMp4ExportSettings({
quality: "good",
sourceWidth: effectiveSource.width,
sourceHeight: effectiveSource.height,
aspectRatioValue: effectiveSource.width / effectiveSource.height,
}),
).toMatchObject({
width: 1920,
height: 1080,
bitrate: 20_000_000,
});
});
});
+143
View File
@@ -0,0 +1,143 @@
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),
};
}