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" }