From f7ae991c28bb20f4886475e9c8d912d6abb6d7b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Oct 2025 07:05:57 +0000 Subject: [PATCH 1/5] Add compression option to ConvertUi component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created CompressUi component with video and audio bitrate controls - Added compress section to ConvertSections type and ordering - Integrated compression state management in ConvertUi - Pass bitrate values to mediabunny video and audio conversion callbacks - Support separate bitrate control for video (500 Kbps - 10 Mbps) and audio (64 - 320 Kbps) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../convert/app/components/CompressUi.tsx | 94 +++++++++++++++++++ packages/convert/app/components/ConvertUi.tsx | 54 +++++++++++ packages/convert/app/lib/default-ui.ts | 9 +- 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 packages/convert/app/components/CompressUi.tsx diff --git a/packages/convert/app/components/CompressUi.tsx b/packages/convert/app/components/CompressUi.tsx new file mode 100644 index 00000000000..904a73611e8 --- /dev/null +++ b/packages/convert/app/components/CompressUi.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import {Label} from './ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from './ui/select'; + +const formatBitrate = (bitrate: number): string => { + if (bitrate >= 1000000) { + return `${(bitrate / 1000000).toFixed(1)} Mbps`; + } + return `${(bitrate / 1000).toFixed(0)} Kbps`; +}; + +export const CompressUi: React.FC<{ + videoBitrate: number | null; + setVideoBitrate: (bitrate: number | null) => void; + audioBitrate: number | null; + setAudioBitrate: (bitrate: number | null) => void; + hasVideo: boolean; + hasAudio: boolean; +}> = ({ + videoBitrate, + setVideoBitrate, + audioBitrate, + setAudioBitrate, + hasVideo, + hasAudio, +}) => { + return ( +
+
+ {hasVideo ? ( + <> + + +
+ Lower bitrate reduces file size but may affect quality. +
+ + ) : null} + {hasVideo && hasAudio ?
: null} + {hasAudio ? ( + <> + + +
+ Lower bitrate 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..69199602907 100644 --- a/packages/convert/app/components/ConvertUi.tsx +++ b/packages/convert/app/components/ConvertUi.tsx @@ -37,6 +37,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'; @@ -139,6 +140,10 @@ const ConvertUI = ({ useState(false); const [resampleRate, setResampleRate] = useState(16000); + const [compressActive, setCompressActive] = useState(false); + const [videoBitrate, setVideoBitrate] = useState(2000000); + const [audioBitrate, setAudioBitrate] = useState(128000); + const canResample = useMemo(() => { return tracks?.find((t) => t.isAudioTrack()); }, [tracks]); @@ -155,6 +160,28 @@ 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 actualVideoBitrate = useMemo(() => { + if (!hasVideo || !compressActive) { + return null; + } + return videoBitrate; + }, [videoBitrate, hasVideo, compressActive]); + + const actualAudioBitrate = useMemo(() => { + if (!hasAudio || !compressActive) { + return null; + } + return audioBitrate; + }, [audioBitrate, hasAudio, compressActive]); + const supportedConfigs = useSupportedConfigs({ outputContainer, tracks, @@ -316,6 +343,7 @@ const ConvertUI = ({ dimensionsAfterCrop ?? null, ), codec: operation.videoCodec, + bitrate: actualVideoBitrate ?? undefined, }; }, audio: (audioTrack) => { @@ -351,6 +379,7 @@ const ConvertUI = ({ return sample; }, + bitrate: actualAudioBitrate ?? undefined, }; }, }); @@ -429,6 +458,8 @@ const ConvertUI = ({ videoOperationSelection, crop, cropRect, + actualVideoBitrate, + actualAudioBitrate, ]); const dimissError = useCallback(() => { @@ -761,6 +792,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, }; } From 3a32329748cbd6dd007263b676bb01de934b8c4f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Oct 2025 07:59:32 +0000 Subject: [PATCH 2/5] Use mediabunny quality constants instead of bitrate values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace exact bitrate pickers with quality level selectors - Import QUALITY_* constants from mediabunny - Updated CompressUi to use quality levels (Very Low, Low, Medium, High, Very High) - Updated ConvertUi state to use quality instead of bitrate - Pass quality parameter to mediabunny video and audio conversion callbacks 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../convert/app/components/CompressUi.tsx | 102 +++++++++++------- packages/convert/app/components/ConvertUi.tsx | 51 ++++++--- 2 files changed, 95 insertions(+), 58 deletions(-) diff --git a/packages/convert/app/components/CompressUi.tsx b/packages/convert/app/components/CompressUi.tsx index 904a73611e8..e6caaabee45 100644 --- a/packages/convert/app/components/CompressUi.tsx +++ b/packages/convert/app/components/CompressUi.tsx @@ -1,3 +1,10 @@ +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 { @@ -8,84 +15,97 @@ import { SelectValue, } from './ui/select'; -const formatBitrate = (bitrate: number): string => { - if (bitrate >= 1000000) { - return `${(bitrate / 1000000).toFixed(1)} Mbps`; - } - return `${(bitrate / 1000).toFixed(0)} Kbps`; -}; +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<{ - videoBitrate: number | null; - setVideoBitrate: (bitrate: number | null) => void; - audioBitrate: number | null; - setAudioBitrate: (bitrate: number | null) => void; + videoQuality: QualityLevel; + setVideoQuality: (quality: QualityLevel) => void; + audioQuality: QualityLevel; + setAudioQuality: (quality: QualityLevel) => void; hasVideo: boolean; hasAudio: boolean; }> = ({ - videoBitrate, - setVideoBitrate, - audioBitrate, - setAudioBitrate, + 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 bitrate reduces file size but may affect quality. + Lower quality reduces file size but may affect visual quality.
) : null} {hasVideo && hasAudio ?
: null} {hasAudio ? ( <> - +
- Lower bitrate reduces file size but may affect audio quality. + 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 69199602907..9af56fc51d5 100644 --- a/packages/convert/app/components/ConvertUi.tsx +++ b/packages/convert/app/components/ConvertUi.tsx @@ -9,8 +9,25 @@ import type { InputVideoTrack, Rotation, } from 'mediabunny'; -import {Conversion, Output, StreamTarget} from 'mediabunny'; +import { + Conversion, + Output, + QUALITY_HIGH, + QUALITY_LOW, + QUALITY_MEDIUM, + QUALITY_VERY_HIGH, + QUALITY_VERY_LOW, + StreamTarget, +} from 'mediabunny'; import React, {useCallback, useMemo, useState} from 'react'; + +type QualityLevel = + | typeof QUALITY_VERY_LOW + | typeof QUALITY_LOW + | typeof QUALITY_MEDIUM + | typeof QUALITY_HIGH + | typeof QUALITY_VERY_HIGH + | null; import {applyCrop} from '~/lib/apply-crop'; import type {Dimensions} from '~/lib/calculate-new-dimensions-from-dimensions'; import {calculateNewDimensionsFromRotateAndScale} from '~/lib/calculate-new-dimensions-from-dimensions'; @@ -141,8 +158,8 @@ const ConvertUI = ({ const [resampleRate, setResampleRate] = useState(16000); const [compressActive, setCompressActive] = useState(false); - const [videoBitrate, setVideoBitrate] = useState(2000000); - const [audioBitrate, setAudioBitrate] = useState(128000); + const [videoQuality, setVideoQuality] = useState(QUALITY_MEDIUM); + const [audioQuality, setAudioQuality] = useState(QUALITY_MEDIUM); const canResample = useMemo(() => { return tracks?.find((t) => t.isAudioTrack()); @@ -168,19 +185,19 @@ const ConvertUI = ({ return (tracks?.filter((t) => t.isAudioTrack()).length ?? 0) > 0; }, [tracks]); - const actualVideoBitrate = useMemo(() => { + const actualVideoQuality = useMemo(() => { if (!hasVideo || !compressActive) { return null; } - return videoBitrate; - }, [videoBitrate, hasVideo, compressActive]); + return videoQuality; + }, [videoQuality, hasVideo, compressActive]); - const actualAudioBitrate = useMemo(() => { + const actualAudioQuality = useMemo(() => { if (!hasAudio || !compressActive) { return null; } - return audioBitrate; - }, [audioBitrate, hasAudio, compressActive]); + return audioQuality; + }, [audioQuality, hasAudio, compressActive]); const supportedConfigs = useSupportedConfigs({ outputContainer, @@ -343,7 +360,7 @@ const ConvertUI = ({ dimensionsAfterCrop ?? null, ), codec: operation.videoCodec, - bitrate: actualVideoBitrate ?? undefined, + quality: actualVideoQuality ?? undefined, }; }, audio: (audioTrack) => { @@ -379,7 +396,7 @@ const ConvertUI = ({ return sample; }, - bitrate: actualAudioBitrate ?? undefined, + quality: actualAudioQuality ?? undefined, }; }, }); @@ -458,8 +475,8 @@ const ConvertUI = ({ videoOperationSelection, crop, cropRect, - actualVideoBitrate, - actualAudioBitrate, + actualVideoQuality, + actualAudioQuality, ]); const dimissError = useCallback(() => { @@ -803,10 +820,10 @@ const ConvertUI = ({ {compressActive ? ( From 4ccf650de8c6bbf0ff43628b0037a54706b0735c Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Thu, 23 Oct 2025 10:05:16 +0200 Subject: [PATCH 3/5] Update ConvertUi.tsx --- packages/convert/app/components/ConvertUi.tsx | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/convert/app/components/ConvertUi.tsx b/packages/convert/app/components/ConvertUi.tsx index 9af56fc51d5..92c008dd315 100644 --- a/packages/convert/app/components/ConvertUi.tsx +++ b/packages/convert/app/components/ConvertUi.tsx @@ -7,27 +7,14 @@ import type { InputFormat, InputTrack, InputVideoTrack, - Rotation, -} from 'mediabunny'; -import { - Conversion, - Output, QUALITY_HIGH, QUALITY_LOW, - QUALITY_MEDIUM, QUALITY_VERY_HIGH, QUALITY_VERY_LOW, - StreamTarget, + Rotation, } from 'mediabunny'; +import {Conversion, Output, QUALITY_MEDIUM, StreamTarget} from 'mediabunny'; import React, {useCallback, useMemo, useState} from 'react'; - -type QualityLevel = - | typeof QUALITY_VERY_LOW - | typeof QUALITY_LOW - | typeof QUALITY_MEDIUM - | typeof QUALITY_HIGH - | typeof QUALITY_VERY_HIGH - | null; import {applyCrop} from '~/lib/apply-crop'; import type {Dimensions} from '~/lib/calculate-new-dimensions-from-dimensions'; import {calculateNewDimensionsFromRotateAndScale} from '~/lib/calculate-new-dimensions-from-dimensions'; @@ -69,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, @@ -158,8 +153,10 @@ const ConvertUI = ({ const [resampleRate, setResampleRate] = useState(16000); const [compressActive, setCompressActive] = useState(false); - const [videoQuality, setVideoQuality] = useState(QUALITY_MEDIUM); - const [audioQuality, setAudioQuality] = useState(QUALITY_MEDIUM); + const [videoQuality, setVideoQuality] = + useState(QUALITY_MEDIUM); + const [audioQuality, setAudioQuality] = + useState(QUALITY_MEDIUM); const canResample = useMemo(() => { return tracks?.find((t) => t.isAudioTrack()); @@ -189,6 +186,7 @@ const ConvertUI = ({ if (!hasVideo || !compressActive) { return null; } + return videoQuality; }, [videoQuality, hasVideo, compressActive]); @@ -196,6 +194,7 @@ const ConvertUI = ({ if (!hasAudio || !compressActive) { return null; } + return audioQuality; }, [audioQuality, hasAudio, compressActive]); @@ -360,7 +359,7 @@ const ConvertUI = ({ dimensionsAfterCrop ?? null, ), codec: operation.videoCodec, - quality: actualVideoQuality ?? undefined, + bitrate: actualVideoQuality ?? undefined, }; }, audio: (audioTrack) => { @@ -396,7 +395,7 @@ const ConvertUI = ({ return sample; }, - quality: actualAudioQuality ?? undefined, + bitrate: actualAudioQuality ?? undefined, }; }, }); From 451946ea6fcdf0168268769b3f23f5b071f9d8ed Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Thu, 23 Oct 2025 10:29:46 +0200 Subject: [PATCH 4/5] Update turbo.json --- turbo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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" } From a2cceca4510491336e7742831b52ff73df8df710 Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Thu, 23 Oct 2025 10:36:13 +0200 Subject: [PATCH 5/5] Update CompressUi.tsx --- packages/convert/app/components/CompressUi.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/convert/app/components/CompressUi.tsx b/packages/convert/app/components/CompressUi.tsx index e6caaabee45..de902b42697 100644 --- a/packages/convert/app/components/CompressUi.tsx +++ b/packages/convert/app/components/CompressUi.tsx @@ -24,12 +24,12 @@ type QualityLevel = | null; export const CompressUi: React.FC<{ - videoQuality: QualityLevel; - setVideoQuality: (quality: QualityLevel) => void; - audioQuality: QualityLevel; - setAudioQuality: (quality: QualityLevel) => void; - hasVideo: boolean; - hasAudio: boolean; + readonly videoQuality: QualityLevel; + readonly setVideoQuality: (quality: QualityLevel) => void; + readonly audioQuality: QualityLevel; + readonly setAudioQuality: (quality: QualityLevel) => void; + readonly hasVideo: boolean; + readonly hasAudio: boolean; }> = ({ videoQuality, setVideoQuality,