[codex] Add webcam crop and mirror controls#394
[codex] Add webcam crop and mirror controls#394wizardAEI wants to merge 9 commits intowebadderallorg:mainfrom
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 Walkthrough📝 Walkthrough🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/lib/exporter/modernFrameRenderer.ts (1)
1814-1831:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRefresh cropped webcam frames every render.
Because Line 1919 now serves every non-default crop from
webcamFrameCacheCanvas, the 250 ms throttle at Line 1831 effectively drops cropped webcam export to about 4 fps. Keep that throttle only for stale-frame fallback, or bypass it whenevercropRegionis non-default.Proposed fix
if (this.lastWebcamCacheRefreshTime === null) { return true; } - return Math.abs(this.currentVideoTime - this.lastWebcamCacheRefreshTime) >= 0.25; + return ( + !isWebcamCropRegionDefault(this.config.webcam?.cropRegion) || + Math.abs(this.currentVideoTime - this.lastWebcamCacheRefreshTime) >= 0.25 + ); }Also applies to: 1915-1925
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/exporter/modernFrameRenderer.ts` around lines 1814 - 1831, The throttle in shouldRefreshWebcamFrameCache causes cropped webcam exports to be updated only every 250ms; change the logic in shouldRefreshWebcamFrameCache (which uses getWebcamCropSourceRect, this.config.webcam?.cropRegion, this.webcamFrameCacheCanvas, this.lastWebcamCacheRefreshTime and this.currentVideoTime) so that when a non-default cropRegion is present you bypass the 0.25s time check and always return true (refresh every render), while retaining the existing time-based fallback only for the default/no-crop case; ensure the existing checks for null/mismatched canvas size and lastWebcamCacheRefreshTime remain intact so stale-frame fallback still uses the throttle.src/i18n/locales/zh-TW/settings.json (1)
25-104:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep the existing padding-advanced keys until the caller is removed.
SettingsPanel.tsxstill looks upeffects.paddingAdvanced(and the related show/hide labels), so dropping those keys here makes the zh-TW UI fall back to English for that section instead of staying localized. Either keep the translations in this file or remove the callers in the same PR.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/i18n/locales/zh-TW/settings.json` around lines 25 - 104, The zh-TW locale removed the effects.padding-advanced keys but SettingsPanel.tsx still references effects.paddingAdvanced and its show/hide labels, causing English fallback; restore the missing keys in src/i18n/locales/zh-TW/settings.json (re-add effects.paddingAdvanced and the related show/hide label keys that SettingsPanel.tsx expects) with the proper Chinese strings, or alternatively remove the callers in SettingsPanel.tsx that reference paddingAdvanced so the keys are no longer needed.src/components/video-editor/VideoPlayback.tsx (1)
1308-1369:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep the webcam draw effect stable across timeline updates.
syncWebcamMediacloses overcurrentTime/isPlaying, so its identity changes on every playback tick. Because the draw effect depends on that callback, it keeps tearing down and rebuilding the webcam listeners/frame callbacks during playback, which is a pretty easy way to introduce dropped frames and visible jitter here. Keep the time/playback inputs in refs or split sync from drawing so the render subscription only depends on the source element and visual settings.Also applies to: 1378-1493
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/video-editor/VideoPlayback.tsx` around lines 1308 - 1369, syncWebcamMedia is unstable because it closes over currentTime/isPlaying causing the draw/listener effect to re-register each tick; fix by moving time/playback state into refs and making syncWebcamMedia stable (so effects depending on it don't tear down). Concretely: add refs like currentTimeRef and isPlayingRef and update them in an effect when currentTime/isPlaying change; change syncWebcamMedia to read currentTime/isPlaying from those refs instead of from props/state so its dependency array can omit currentTime/isPlaying; ensure handleWebcamMediaReady and the useEffect that calls syncWebcamMedia depend only on stable inputs (webcamVideoRef, webcamEnabled, webcamVideoPath, webcamTimeOffsetMs, speedRegionsRef, lastWebcamSyncTimeRef) and still call setWebcamSourceReady and syncWebcamMedia as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 2504-2529: The raw <video> is being shown when webcamSourceReady
turns true and can flash uncropped content; ensure the bubble remains hidden
until the cropped canvas/frame is ready by changing the visibility logic to
depend on webcamFrameReady (or a new "firstFrameDrawn" flag set in
handleWebcamMediaReady/after the first canvas draw) instead of
webcamSourceReady, and also ensure the <video> (webcamVideoRef) is visually
suppressed until that flag is true (e.g., keep its opacity/display/visibility
hidden or pointer-events:none until webcamFrameReady/firstFrameDrawn). Update
the visibility/style checks around the container and the video element
(references: webcamFrameReady, webcamSourceReady, webcamVideoRef,
webcamCropRegion, handleWebcamMediaReady, webcamBubbleInnerRef) so the uncropped
video never briefly appears before the cropped canvas is ready.
- Around line 1448-1480: The code starts a continuous RAF loop even when
requestVideoFrameCallback (rVFC) is available, causing double-draws and wasted
CPU; fix by using RAF only as the fallback: only call queueVideoFrameCallback()
and avoid starting the perpetual drawOnAnimationFrame loop when
hasVideoFrameCallback is true, and conversely only start the RAF loop when rVFC
is unavailable. In handleDrawableMediaEvent, if hasVideoFrameCallback then
perform a single one-off redraw via drawWebcamFrame(true) (do not start
requestAnimationFrame) and requeue rVFC as needed; if !hasVideoFrameCallback
then cancel any rVFC state and start animationFrame =
requestAnimationFrame(drawOnAnimationFrame). Also ensure the initial startup
calls are conditional: call queueVideoFrameCallback() when
hasVideoFrameCallback, otherwise call drawOnAnimationFrame() to begin the RAF
loop; reference symbols: webcamVideo.requestVideoFrameCallback /
cancelVideoFrameCallback, queueVideoFrameCallback, drawOnAnimationFrame,
drawWebcamFrame, handleDrawableMediaEvent, animationFrame, videoFrameCallback.
In `@src/components/video-editor/WebcamCropControl.tsx`:
- Around line 206-272: The crop control is currently drag-only and inaccessible
to keyboard users; make the resize handles and the move area focusable and add
keyboard handlers to adjust crop values. Convert the RESIZE_HANDLES render to
render focusable elements (e.g., add tabIndex=0 and aria-labels) and wire an
onKeyDown handler (new helper like handleKeyDownForHandle) that responds to
Arrow keys, Shift+Arrow for larger steps, and Home/End/PageUp/PageDown where
appropriate to update cropLeft/cropTop/cropWidth/cropHeight using the same
clamping logic used by handlePointerMove/endDrag; also make the move box
focusable and add an onKeyDown handler (e.g., handleKeyDownMove) to nudge the
entire crop with arrows, and ensure activeHandle state is updated consistently
(activeHandle used for styling). Ensure proper ARIA attributes
(role/aria-valuenow/aria-valuemin/aria-valuemax or aria-label) for screen
readers so keyboard users can operate the crop without a pointer.
---
Outside diff comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 1308-1369: syncWebcamMedia is unstable because it closes over
currentTime/isPlaying causing the draw/listener effect to re-register each tick;
fix by moving time/playback state into refs and making syncWebcamMedia stable
(so effects depending on it don't tear down). Concretely: add refs like
currentTimeRef and isPlayingRef and update them in an effect when
currentTime/isPlaying change; change syncWebcamMedia to read
currentTime/isPlaying from those refs instead of from props/state so its
dependency array can omit currentTime/isPlaying; ensure handleWebcamMediaReady
and the useEffect that calls syncWebcamMedia depend only on stable inputs
(webcamVideoRef, webcamEnabled, webcamVideoPath, webcamTimeOffsetMs,
speedRegionsRef, lastWebcamSyncTimeRef) and still call setWebcamSourceReady and
syncWebcamMedia as before.
In `@src/i18n/locales/zh-TW/settings.json`:
- Around line 25-104: The zh-TW locale removed the effects.padding-advanced keys
but SettingsPanel.tsx still references effects.paddingAdvanced and its show/hide
labels, causing English fallback; restore the missing keys in
src/i18n/locales/zh-TW/settings.json (re-add effects.paddingAdvanced and the
related show/hide label keys that SettingsPanel.tsx expects) with the proper
Chinese strings, or alternatively remove the callers in SettingsPanel.tsx that
reference paddingAdvanced so the keys are no longer needed.
In `@src/lib/exporter/modernFrameRenderer.ts`:
- Around line 1814-1831: The throttle in shouldRefreshWebcamFrameCache causes
cropped webcam exports to be updated only every 250ms; change the logic in
shouldRefreshWebcamFrameCache (which uses getWebcamCropSourceRect,
this.config.webcam?.cropRegion, this.webcamFrameCacheCanvas,
this.lastWebcamCacheRefreshTime and this.currentVideoTime) so that when a
non-default cropRegion is present you bypass the 0.25s time check and always
return true (refresh every render), while retaining the existing time-based
fallback only for the default/no-crop case; ensure the existing checks for
null/mismatched canvas size and lastWebcamCacheRefreshTime remain intact so
stale-frame fallback still uses the throttle.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: eb407cfb-5a90-45c0-be06-b7872f2ab986
📒 Files selected for processing (17)
src/components/video-editor/SettingsPanel.tsxsrc/components/video-editor/VideoPlayback.tsxsrc/components/video-editor/WebcamCropControl.tsxsrc/components/video-editor/projectPersistence.tssrc/components/video-editor/types.tssrc/components/video-editor/webcamOverlay.test.tssrc/components/video-editor/webcamOverlay.tssrc/i18n/locales/en/settings.jsonsrc/i18n/locales/es/settings.jsonsrc/i18n/locales/fr/settings.jsonsrc/i18n/locales/ko/settings.jsonsrc/i18n/locales/nl/settings.jsonsrc/i18n/locales/pt-BR/settings.jsonsrc/i18n/locales/zh-CN/settings.jsonsrc/i18n/locales/zh-TW/settings.jsonsrc/lib/exporter/frameRenderer.tssrc/lib/exporter/modernFrameRenderer.ts
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/video-editor/VideoPlayback.tsx (1)
1327-1333:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSpeed region lookup uses wrong time reference.
The speed region lookup at line 1328 uses
targetTime * 1000, buttargetTimeis the webcam's timeline (after applying offset), not the main timeline. Speed regions are defined on the main timeline, so this should usecurrentTime * 1000instead, matching the background video sync at lines 1055-1057.For example, with a -2s webcam offset and a 2x speed region from 8-12s: when main is at 10s,
targetTimeis 12s, causing the check12000 < 12000to fail and the speed adjustment to be skipped.Proposed fix
const activeSpeedRegion = speedRegionsRef.current.find( - (region) => targetTime * 1000 >= region.startMs && targetTime * 1000 < region.endMs, + (region) => currentTime * 1000 >= region.startMs && currentTime * 1000 < region.endMs, );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/video-editor/VideoPlayback.tsx` around lines 1327 - 1333, The speed-region lookup is using the webcam timeline (targetTime) instead of the main timeline, so change the lookup in the block using speedRegionsRef.current.find(...) to compare regions against currentTime * 1000 (the main timeline) rather than targetTime * 1000; keep the rest of the logic (computing targetPlaybackRate and setting webcamVideo.playbackRate) the same so speedRegions apply correctly regardless of webcam offset.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 1327-1333: The speed-region lookup is using the webcam timeline
(targetTime) instead of the main timeline, so change the lookup in the block
using speedRegionsRef.current.find(...) to compare regions against currentTime *
1000 (the main timeline) rather than targetTime * 1000; keep the rest of the
logic (computing targetPlaybackRate and setting webcamVideo.playbackRate) the
same so speedRegions apply correctly regardless of webcam offset.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 87b43797-58d0-4dae-b7ac-1227ed4ed616
📒 Files selected for processing (1)
src/components/video-editor/VideoPlayback.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Line 1506: The effect is re-running every frame because it depends on
syncWebcamMedia (which changes with currentTime); to fix it add a ref (e.g.,
syncWebcamMediaRef) to hold the latest syncWebcamMedia callback, update that ref
whenever syncWebcamMedia changes, and inside the canvas RAF/drawing effect have
handleDrawableMediaEvent read from syncWebcamMediaRef.current instead of
capturing syncWebcamMedia; then remove syncWebcamMedia from the effect
dependency array (keep webcamCropRegion, webcamEnabled, webcamMirror,
webcamVideoPath as needed). Ensure syncWebcamMediaRef is initialized near the
other refs and updated immediately when syncWebcamMedia changes so the drawing
loop always calls the latest callback without restarting.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3f184a6b-40b0-4bdb-9347-df345a7547db
📒 Files selected for processing (2)
src/components/video-editor/VideoPlayback.tsxsrc/components/video-editor/WebcamCropControl.tsx
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/video-editor/VideoPlayback.tsx (1)
1345-1361:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse the editor timeline for speed-region selection.
targetTimeis the offset/clamped webcam media time. Using it here shifts the activespeedRegionwheneverwebcamTimeOffsetMsis non-zero, so the webcam playback rate can diverge from the main clip. Pick the speed region fromcurrentTime, and only usetargetTime/mediaTargetTimefor seeking the webcam element.Suggested fix
- const activeSpeedRegion = speedRegionsRef.current.find( - (region) => targetTime * 1000 >= region.startMs && targetTime * 1000 < region.endMs, - ); + const activeSpeedRegion = speedRegionsRef.current.find( + (region) => currentTime * 1000 >= region.startMs && currentTime * 1000 < region.endMs, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/video-editor/VideoPlayback.tsx` around lines 1345 - 1361, The active speed-region is being chosen using the clamped webcam media time (`targetTime`), which causes divergence when `webcamTimeOffsetMs` is non-zero; change the lookup to use the editor timeline time (`currentTime`) instead. Specifically, update the `speedRegionsRef.current.find(...)` call used to compute `activeSpeedRegion` so its comparisons use `currentTime * 1000` (not `targetTime * 1000`), leaving the `getWebcamMediaTargetTimeSeconds`/`mediaTargetTime` logic and the subsequent `webcamVideo.playbackRate = targetPlaybackRate` behavior intact.
♻️ Duplicate comments (1)
src/components/video-editor/VideoPlayback.tsx (1)
1390-1401:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winKeep the cropped webcam hidden until a frame is actually drawable.
handleWebcamMediaReadyruns for bothonLoadedMetadataandonLoadedData, but Line 2433 reveals the layer as soon aswebcamVideoDimensionsis set.loadedmetadataonly guarantees dimensions, not a decoded frame, so this can still flash an empty/black webcam bubble before the first cropped frame is ready.Also applies to: 2430-2450
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/video-editor/VideoPlayback.tsx` around lines 1390 - 1401, The cropped webcam layer is being revealed as soon as webcamVideoDimensions is set in handleWebcamMediaReady even if no decoded frame exists; update handleWebcamMediaReady (and related logic around syncWebcamMedia, webcamVideoDimensions, and the layer visibility flag) to only reveal the cropped webcam after a drawable frame is available—e.g., require video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA or wait for the onLoadedData event (or a successful first draw from syncWebcamMedia) before setting the visibility flag (introduce a webcamFrameReady/webcamVisible state or set visibility inside the onLoadedData path) so the layer does not flash an empty/black bubble.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 1345-1361: The active speed-region is being chosen using the
clamped webcam media time (`targetTime`), which causes divergence when
`webcamTimeOffsetMs` is non-zero; change the lookup to use the editor timeline
time (`currentTime`) instead. Specifically, update the
`speedRegionsRef.current.find(...)` call used to compute `activeSpeedRegion` so
its comparisons use `currentTime * 1000` (not `targetTime * 1000`), leaving the
`getWebcamMediaTargetTimeSeconds`/`mediaTargetTime` logic and the subsequent
`webcamVideo.playbackRate = targetPlaybackRate` behavior intact.
---
Duplicate comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 1390-1401: The cropped webcam layer is being revealed as soon as
webcamVideoDimensions is set in handleWebcamMediaReady even if no decoded frame
exists; update handleWebcamMediaReady (and related logic around syncWebcamMedia,
webcamVideoDimensions, and the layer visibility flag) to only reveal the cropped
webcam after a drawable frame is available—e.g., require video.readyState >=
HTMLMediaElement.HAVE_CURRENT_DATA or wait for the onLoadedData event (or a
successful first draw from syncWebcamMedia) before setting the visibility flag
(introduce a webcamFrameReady/webcamVisible state or set visibility inside the
onLoadedData path) so the layer does not flash an empty/black bubble.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 20e36578-d854-4647-a220-48fb3240897a
📒 Files selected for processing (2)
src/components/video-editor/VideoPlayback.tsxsrc/components/video-editor/WebcamCropControl.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/video-editor/WebcamCropControl.tsx
|
This seems to modify the corner radiuses in Recordly to use circle radiuses instead of squircles Please revert your corner radius style changes |
Thanks for the heads up, I’ve made the change. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/video-editor/VideoPlayback.tsx (1)
1355-1357:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse timeline time for webcam speed-region lookup.
targetTimehere is webcam-media time becausegetWebcamMediaTargetTimeSeconds(...)already appliestimeOffsetMsand duration clamping. Looking upspeedRegionsRefagainst that value makes the webcam playback rate wrong whenever a speed region overlaps a non-zero webcam offset, so the preview can drift until the hard-seek correction kicks in. The background-video sync just above still keys speed regions offcurrentTime; this path should do the same.Suggested fix
- const activeSpeedRegion = speedRegionsRef.current.find( - (region) => targetTime * 1000 >= region.startMs && targetTime * 1000 < region.endMs, - ); + const timelineTimeMs = currentTime * 1000; + const activeSpeedRegion = speedRegionsRef.current.find( + (region) => timelineTimeMs >= region.startMs && timelineTimeMs < region.endMs, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/video-editor/VideoPlayback.tsx` around lines 1355 - 1357, The speed-region lookup is using webcam-media time (targetTime from getWebcamMediaTargetTimeSeconds) which is offset/clamped and causes wrong playback rate when webcam has a non-zero offset; instead, use the timeline time that the background-video sync uses. Replace the activeSpeedRegion lookup that references targetTime with one that uses currentTime (i.e., compare currentTime * 1000 to region.startMs/endMs) so speedRegionsRef.current.find(...) is keyed off currentTime, ensuring webcam preview uses the same speed-region decisions as the background timeline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/components/video-editor/VideoPlayback.tsx`:
- Around line 1355-1357: The speed-region lookup is using webcam-media time
(targetTime from getWebcamMediaTargetTimeSeconds) which is offset/clamped and
causes wrong playback rate when webcam has a non-zero offset; instead, use the
timeline time that the background-video sync uses. Replace the activeSpeedRegion
lookup that references targetTime with one that uses currentTime (i.e., compare
currentTime * 1000 to region.startMs/endMs) so speedRegionsRef.current.find(...)
is keyed off currentTime, ensuring webcam preview uses the same speed-region
decisions as the background timeline.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: ac9df99f-12fb-494d-85c9-edb20fed810a
📒 Files selected for processing (1)
src/components/video-editor/VideoPlayback.tsx
|
please fix merge conflicts |
|
have done✌️ |
|
Hey, I noticed the cropper doesn't have a preview of the webcam, it would be great if you could use the webcam's image from the frame the preview shows! Other than this I don't see any more issues |
|
Hey, the bottom-right preview is actually live right now Initially I did try using the webcam frame in the left tab as well for the crop preview, but that ended up creating two competing visual focus points, which felt a bit confusing from a UX perspective. (So I switched the cropper to a grid instead.) |
How about this? If you think having a preview in the left sidebar would be better, I can refine it. |
|
I do think that would be better so people can directly crop with a reference :) |
|
Quick review-aid pass on the current head: merge is clean and the webcam speed-region lookup now appears to use timeline time, so that earlier concern looks addressed. One export correctness issue still looks real: in Suggested next step: bypass the 250ms throttle when |
Summary
Why
Webcam recordings from a fixed Mac camera often need a tighter face/head crop. The editor now supports visual cropping while keeping preview/export behavior consistent, including mirrored webcam workflows.
Validation
Note: Biome still reports existing hook dependency warnings when checking the full VideoPlayback file; the targeted checks above pass.
Summary by CodeRabbit
New Features
Internationalization
UI/UX Improvements
Refactor
Tests