diff --git a/packages/convert/app/components/CompressUi.tsx b/packages/convert/app/components/CompressUi.tsx
new file mode 100644
index 00000000000..de902b42697
--- /dev/null
+++ b/packages/convert/app/components/CompressUi.tsx
@@ -0,0 +1,114 @@
+import {
+ QUALITY_HIGH,
+ QUALITY_LOW,
+ QUALITY_MEDIUM,
+ QUALITY_VERY_HIGH,
+ QUALITY_VERY_LOW,
+} from 'mediabunny';
+import React from 'react';
+import {Label} from './ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from './ui/select';
+
+type QualityLevel =
+ | typeof QUALITY_VERY_LOW
+ | typeof QUALITY_LOW
+ | typeof QUALITY_MEDIUM
+ | typeof QUALITY_HIGH
+ | typeof QUALITY_VERY_HIGH
+ | null;
+
+export const CompressUi: React.FC<{
+ readonly videoQuality: QualityLevel;
+ readonly setVideoQuality: (quality: QualityLevel) => void;
+ readonly audioQuality: QualityLevel;
+ readonly setAudioQuality: (quality: QualityLevel) => void;
+ readonly hasVideo: boolean;
+ readonly hasAudio: boolean;
+}> = ({
+ videoQuality,
+ setVideoQuality,
+ audioQuality,
+ setAudioQuality,
+ hasVideo,
+ hasAudio,
+}) => {
+ const getQualityValue = (quality: QualityLevel): string => {
+ if (quality === null) return 'none';
+ if (quality === QUALITY_VERY_LOW) return 'very-low';
+ if (quality === QUALITY_LOW) return 'low';
+ if (quality === QUALITY_MEDIUM) return 'medium';
+ if (quality === QUALITY_HIGH) return 'high';
+ if (quality === QUALITY_VERY_HIGH) return 'very-high';
+ return 'none';
+ };
+
+ const setQualityFromValue = (value: string): QualityLevel => {
+ if (value === 'very-low') return QUALITY_VERY_LOW;
+ if (value === 'low') return QUALITY_LOW;
+ if (value === 'medium') return QUALITY_MEDIUM;
+ if (value === 'high') return QUALITY_HIGH;
+ if (value === 'very-high') return QUALITY_VERY_HIGH;
+ return null;
+ };
+
+ return (
+
+
+ {hasVideo ? (
+ <>
+
+
+
+ Lower quality reduces file size but may affect visual quality.
+
+ >
+ ) : null}
+ {hasVideo && hasAudio ?
: null}
+ {hasAudio ? (
+ <>
+
+
+
+ Lower quality reduces file size but may affect audio quality.
+
+ >
+ ) : null}
+
+ );
+};
diff --git a/packages/convert/app/components/ConvertUi.tsx b/packages/convert/app/components/ConvertUi.tsx
index 31b6726ba58..92c008dd315 100644
--- a/packages/convert/app/components/ConvertUi.tsx
+++ b/packages/convert/app/components/ConvertUi.tsx
@@ -7,9 +7,13 @@ import type {
InputFormat,
InputTrack,
InputVideoTrack,
+ QUALITY_HIGH,
+ QUALITY_LOW,
+ QUALITY_VERY_HIGH,
+ QUALITY_VERY_LOW,
Rotation,
} from 'mediabunny';
-import {Conversion, Output, StreamTarget} from 'mediabunny';
+import {Conversion, Output, QUALITY_MEDIUM, StreamTarget} from 'mediabunny';
import React, {useCallback, useMemo, useState} from 'react';
import {applyCrop} from '~/lib/apply-crop';
import type {Dimensions} from '~/lib/calculate-new-dimensions-from-dimensions';
@@ -37,6 +41,7 @@ import type {ConvertProgressType} from '~/lib/progress';
import {makeWaveformVisualizer} from '~/lib/waveform-visualizer';
import {makeWebFsTarget} from '~/lib/web-fs-target';
import type {OutputContainer, RouteAction} from '~/seo';
+import {CompressUi} from './CompressUi';
import {ConversionDone} from './ConversionDone';
import {ConvertForm} from './ConvertForm';
import {ConvertProgress, convertProgressRef} from './ConvertProgress';
@@ -51,6 +56,14 @@ import {RotateComponents} from './RotateComponents';
import {useSupportedConfigs} from './use-supported-configs';
import type {VideoThumbnailRef} from './VideoThumbnail';
+type QualityLevel =
+ | typeof QUALITY_VERY_LOW
+ | typeof QUALITY_LOW
+ | typeof QUALITY_MEDIUM
+ | typeof QUALITY_HIGH
+ | typeof QUALITY_VERY_HIGH
+ | null;
+
const ConvertUI = ({
currentAudioCodec,
currentVideoCodec,
@@ -139,6 +152,12 @@ const ConvertUI = ({
useState(false);
const [resampleRate, setResampleRate] = useState(16000);
+ const [compressActive, setCompressActive] = useState(false);
+ const [videoQuality, setVideoQuality] =
+ useState(QUALITY_MEDIUM);
+ const [audioQuality, setAudioQuality] =
+ useState(QUALITY_MEDIUM);
+
const canResample = useMemo(() => {
return tracks?.find((t) => t.isAudioTrack());
}, [tracks]);
@@ -155,6 +174,30 @@ const ConvertUI = ({
return resampleRate;
}, [resampleRate, canResample, resampleUserPreferenceActive]);
+ const hasVideo = useMemo(() => {
+ return (tracks?.filter((t) => t.isVideoTrack()).length ?? 0) > 0;
+ }, [tracks]);
+
+ const hasAudio = useMemo(() => {
+ return (tracks?.filter((t) => t.isAudioTrack()).length ?? 0) > 0;
+ }, [tracks]);
+
+ const actualVideoQuality = useMemo(() => {
+ if (!hasVideo || !compressActive) {
+ return null;
+ }
+
+ return videoQuality;
+ }, [videoQuality, hasVideo, compressActive]);
+
+ const actualAudioQuality = useMemo(() => {
+ if (!hasAudio || !compressActive) {
+ return null;
+ }
+
+ return audioQuality;
+ }, [audioQuality, hasAudio, compressActive]);
+
const supportedConfigs = useSupportedConfigs({
outputContainer,
tracks,
@@ -316,6 +359,7 @@ const ConvertUI = ({
dimensionsAfterCrop ?? null,
),
codec: operation.videoCodec,
+ bitrate: actualVideoQuality ?? undefined,
};
},
audio: (audioTrack) => {
@@ -351,6 +395,7 @@ const ConvertUI = ({
return sample;
},
+ bitrate: actualAudioQuality ?? undefined,
};
},
});
@@ -429,6 +474,8 @@ const ConvertUI = ({
videoOperationSelection,
crop,
cropRect,
+ actualVideoQuality,
+ actualAudioQuality,
]);
const dimissError = useCallback(() => {
@@ -761,6 +808,29 @@ const ConvertUI = ({
);
}
+ if (section === 'compress') {
+ return (
+
+
+ Compress
+
+ {compressActive ? (
+
+ ) : null}
+
+ );
+ }
+
throw new Error('Unknown section ' + (section satisfies never));
})}
diff --git a/packages/convert/app/lib/default-ui.ts b/packages/convert/app/lib/default-ui.ts
index 416972a12d5..4a4bf4e217b 100644
--- a/packages/convert/app/lib/default-ui.ts
+++ b/packages/convert/app/lib/default-ui.ts
@@ -126,7 +126,8 @@ export type ConvertSections =
| 'mirror'
| 'resize'
| 'crop'
- | 'resample';
+ | 'resample'
+ | 'compress';
export const getOrderOfSections = (
action: RouteAction,
@@ -139,6 +140,7 @@ export const getOrderOfSections = (
mirror: 3,
convert: 4,
resample: 5,
+ compress: 6,
};
}
@@ -150,6 +152,7 @@ export const getOrderOfSections = (
mirror: 3,
convert: 4,
resample: 5,
+ compress: 6,
};
}
@@ -161,6 +164,7 @@ export const getOrderOfSections = (
rotate: 3,
mirror: 4,
resample: 5,
+ compress: 6,
};
}
@@ -172,6 +176,7 @@ export const getOrderOfSections = (
rotate: 3,
mirror: 4,
resample: 5,
+ compress: 6,
};
}
@@ -183,6 +188,7 @@ export const getOrderOfSections = (
rotate: 3,
convert: 4,
resample: 5,
+ compress: 6,
};
}
@@ -199,6 +205,7 @@ export const getOrderOfSections = (
mirror: 3,
convert: 4,
resample: 5,
+ compress: 6,
};
}
diff --git a/turbo.json b/turbo.json
index adbdc32f98d..2a7493c07c5 100644
--- a/turbo.json
+++ b/turbo.json
@@ -89,12 +89,12 @@
"outputLogs": "new-only"
},
"@remotion/convert#build-page": {
- "dependsOn": ["^make"],
+ "dependsOn": ["make"],
"outputs": ["build", ".vercel"],
"outputLogs": "new-only"
},
"@remotion/convert#build-spa": {
- "dependsOn": ["^make"],
+ "dependsOn": ["make"],
"outputs": ["spa-dist"],
"outputLogs": "new-only"
}