Skip to content

Commit

Permalink
Merge pull request #4659 from remotion-dev/webcodecs-scale
Browse files Browse the repository at this point in the history
  • Loading branch information
JonnyBurger authored Dec 21, 2024
2 parents dfbb2ce + 7137364 commit a96985a
Show file tree
Hide file tree
Showing 38 changed files with 1,261 additions and 132 deletions.
99 changes: 96 additions & 3 deletions packages/convert/app/components/ConvertUi.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Button} from '@/components/ui/button';
import {
Dimensions,
LogLevel,
MediaParserAudioCodec,
MediaParserInternals,
Expand All @@ -9,8 +10,13 @@ import {
} from '@remotion/media-parser';
import {fetchReader} from '@remotion/media-parser/fetch';
import {webFileReader} from '@remotion/media-parser/web-file';
import {convertMedia, ConvertMediaContainer} from '@remotion/webcodecs';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {
convertMedia,
ConvertMediaContainer,
ResizeOperation,
WebCodecsInternals,
} from '@remotion/webcodecs';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {canRotateOrMirror} from '~/lib/can-rotate-or-mirror';
import {ConvertState, Source} from '~/lib/convert-state';
import {
Expand All @@ -23,6 +29,7 @@ import {
getActualAudioConfigIndex,
getActualVideoConfigIndex,
} from '~/lib/get-audio-video-config-index';
import {getInitialResizeSuggestion} from '~/lib/get-initial-resize-suggestion';
import {isReencoding} from '~/lib/is-reencoding';
import {isSubmitDisabled} from '~/lib/is-submit-enabled';
import {RouteAction} from '~/seo';
Expand All @@ -34,8 +41,10 @@ import {ErrorState} from './ErrorState';
import {flipVideoFrame} from './flip-video';
import {getDefaultContainerForConversion} from './guess-codec-from-source';
import {MirrorComponents} from './MirrorComponents';
import {ResizeUi} from './ResizeUi';
import {RotateComponents} from './RotateComponents';
import {useSupportedConfigs} from './use-supported-configs';
import {VideoThumbnailRef} from './VideoThumbnail';

export default function ConvertUI({
src,
Expand All @@ -55,13 +64,21 @@ export default function ConvertUI({
setFlipHorizontal,
setFlipVertical,
inputContainer,
unrotatedDimensions,
videoThumbnailRef,
rotation,
dimensions,
}: {
readonly src: Source;
readonly setSrc: React.Dispatch<React.SetStateAction<Source | null>>;
readonly currentAudioCodec: MediaParserAudioCodec | null;
readonly currentVideoCodec: MediaParserVideoCodec | null;
readonly tracks: TracksField | null;
readonly videoThumbnailRef: React.RefObject<VideoThumbnailRef | null>;
readonly unrotatedDimensions: Dimensions | null;
readonly dimensions: Dimensions | null;
readonly duration: number | null;
readonly rotation: number | null;
readonly inputContainer: ParseMediaContainer | null;
readonly logLevel: LogLevel;
readonly action: RouteAction;
Expand Down Expand Up @@ -90,6 +107,12 @@ export default function ConvertUI({
const [enableConvert, setEnableConvert] = useState(() =>
isConvertEnabledByDefault(action),
);
const [resizeOperation, setResizeOperation] =
useState<ResizeOperation | null>(() => {
return action.type === 'resize-format' || action.type === 'generic-resize'
? getInitialResizeSuggestion(dimensions)
: null;
});

const order = useMemo(() => {
return Object.entries(getOrderOfSections(action))
Expand All @@ -105,6 +128,19 @@ export default function ConvertUI({
action,
userRotation,
inputContainer,
resizeOperation,
});

const isH264Reencode = supportedConfigs?.videoTrackOptions.some((o) => {
const index = getActualVideoConfigIndex({
enableConvert,
trackNumber: o.trackId,
videoConfigIndexSelection,
});
return (
o.operations[index].type === 'reencode' &&
o.operations[index].videoCodec === 'h264'
);
});

const setVideoConfigIndex = useCallback((trackId: number, i: number) => {
Expand Down Expand Up @@ -293,6 +329,35 @@ export default function ConvertUI({
});
}, [setEnableRotateOrMirror]);

const onResizeClick = useCallback(() => {
setResizeOperation((r) => {
if (r !== null || !dimensions) {
return null;
}

return getInitialResizeSuggestion(dimensions);
});
}, [dimensions]);

const newDimensions = useMemo(() => {
if (unrotatedDimensions === null) {
return null;
}

return WebCodecsInternals.calculateNewDimensionsFromDimensions({
...unrotatedDimensions,
rotation: userRotation - (rotation ?? 0),
resizeOperation,
videoCodec: isH264Reencode ? 'h264' : 'vp8',
});
}, [
unrotatedDimensions,
isH264Reencode,
resizeOperation,
rotation,
userRotation,
]);

if (state.type === 'error') {
return (
<>
Expand Down Expand Up @@ -452,7 +517,35 @@ export default function ConvertUI({
);
}

throw new Error('Unknown section');
if (section === 'resize') {
return (
<div key="resize">
<ConvertUiSection
active={resizeOperation !== null && newDimensions !== null}
setActive={onResizeClick}
>
Resize
</ConvertUiSection>
{resizeOperation !== null &&
newDimensions !== null &&
unrotatedDimensions !== null ? (
<>
<div className="h-2" />
<ResizeUi
originalDimensions={unrotatedDimensions}
dimensions={newDimensions}
thumbnailRef={videoThumbnailRef}
rotation={userRotation - (rotation ?? 0)}
setResizeMode={setResizeOperation}
requireTwoStep={Boolean(isH264Reencode)}
/>
</>
) : null}
</div>
);
}

throw new Error('Unknown section ' + (section satisfies never));
})}
</div>
<div className="h-8" />
Expand Down
6 changes: 5 additions & 1 deletion packages/convert/app/components/FileAvailable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export const FileAvailable: React.FC<{
Choose another file
</Button>
</div>
<div className="lg:inline-flex lg:flex-row">
<div className="lg:inline-flex lg:flex-row items-start">
<Probe
thumbnailError={err}
src={src}
Expand Down Expand Up @@ -144,6 +144,8 @@ export const FileAvailable: React.FC<{
src={src}
tracks={probeResult.tracks}
setSrc={setSrc}
unrotatedDimensions={probeResult.unrotatedDimensions}
dimensions={probeResult.dimensions}
duration={probeResult.durationInSeconds ?? null}
logLevel="verbose"
action={routeAction}
Expand All @@ -155,6 +157,8 @@ export const FileAvailable: React.FC<{
setFlipHorizontal={setFlipHorizontal}
flipVertical={flipVertical}
setFlipVertical={setFlipVertical}
videoThumbnailRef={videoThumbnailRef}
rotation={probeResult.rotation}
/>
</div>
</div>
Expand Down
92 changes: 92 additions & 0 deletions packages/convert/app/components/ResizeCorner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {Dimensions} from '@remotion/media-parser';
import {ResizeOperation} from '@remotion/webcodecs';
import React, {useCallback} from 'react';

export const ResizeCorner: React.FC<{
readonly innerDimensions: Dimensions;
readonly outerDimensions: Dimensions;
readonly videoDimensions: Dimensions;
readonly setResizeMode: React.Dispatch<
React.SetStateAction<ResizeOperation | null>
>;
readonly onStart: () => void;
readonly onEnd: () => void;
}> = ({
innerDimensions,
outerDimensions,
setResizeMode,
onEnd,
onStart,
videoDimensions,
}) => {
const onPointerDown: React.PointerEventHandler = useCallback(
(e) => {
if (e.button !== 0) {
// right-click
return;
}
onStart();
e.preventDefault();
const originalX = e.clientX;

const currentScale = innerDimensions.width / outerDimensions.width;

const getScale = (e: PointerEvent) => {
const dx = e.clientX - originalX;

const newScaleX = Math.max(
0.2,
Math.min(1, currentScale + dx / (outerDimensions.width / 2)),
);

const newSmallerSide = Math.min(
videoDimensions.width * newScaleX,
videoDimensions.height * newScaleX,
);

const snapPoints = [2160, 1080, 720, 480, 360, 240, 144, 16];
const isCloseToSnapPoint = snapPoints.find((point) => {
return Math.abs(newSmallerSide - point) < 20;
});
const snapPoint = isCloseToSnapPoint ?? newSmallerSide;
const scale = (snapPoint / newSmallerSide) * newScaleX;

const newResizeMode: ResizeOperation = {
mode: 'scale',
scale,
};
setResizeMode(newResizeMode);
};

const onPointerMove = (e: PointerEvent) => {
getScale(e);
};

const onPointerRelease = (e: PointerEvent) => {
getScale(e);
onEnd();
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerRelease);
};

window.addEventListener('pointermove', onPointerMove);
window.addEventListener('pointerup', onPointerRelease);
},
[
innerDimensions.width,
onEnd,
onStart,
outerDimensions.width,
setResizeMode,
videoDimensions.height,
videoDimensions.width,
],
);

return (
<div
className="w-6 h-6 rotate-45 bg-white border-2 border-black absolute -bottom-3 -right-3 cursor-nwse-resize"
onPointerDown={onPointerDown}
/>
);
};
72 changes: 72 additions & 0 deletions packages/convert/app/components/ResizeShortcuts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {Dimensions} from '@remotion/media-parser';
import {ResizeOperation} from '@remotion/webcodecs';
import React, {useCallback, useMemo} from 'react';

export const ResizeOption: React.FC<{
readonly max: number;
readonly selected: boolean;
readonly setResizeMode: React.Dispatch<
React.SetStateAction<ResizeOperation | null>
>;
readonly portrait: boolean;
}> = ({max, selected, setResizeMode, portrait}) => {
const onClick = useCallback(() => {
setResizeMode(() => {
if (portrait) {
return {
mode: 'max-width',
maxWidth: max,
};
}
return {
mode: 'max-height',
maxHeight: max,
};
});
}, [max, portrait, setResizeMode]);

return (
<div
data-selected={selected}
className="font-brand text-sm pl-2 pr-2 text-muted-foreground cursor-pointer transition-colors hover:text-black data-[selected=true]:text-brand"
onClick={onClick}
>
{max}p
</div>
);
};

export const ResizeShortcuts: React.FC<{
readonly originalDimensions: Dimensions;
readonly resolvedDimensions: Dimensions;
readonly setResizeMode: React.Dispatch<
React.SetStateAction<ResizeOperation | null>
>;
}> = ({originalDimensions, resolvedDimensions, setResizeMode}) => {
const options = useMemo(() => {
return [2160, 1080, 720, 480, 360, 240].filter(
(option) =>
option < Math.min(originalDimensions.width, originalDimensions.height),
);
}, [originalDimensions.height, originalDimensions.width]);

const smallerSideSelected = useMemo(() => {
return Math.min(resolvedDimensions.width, resolvedDimensions.height);
}, [resolvedDimensions.height, resolvedDimensions.width]);

const portrait = originalDimensions.height > originalDimensions.width;

return (
<div className="flex flex-row justify-center mb-6">
{options.map((option) => (
<ResizeOption
key={option}
portrait={portrait}
selected={smallerSideSelected === option}
max={option}
setResizeMode={setResizeMode}
/>
))}
</div>
);
};
Loading

0 comments on commit a96985a

Please sign in to comment.