From f72f5c100badb8ad744a20b9110f9f5e7a9bf774 Mon Sep 17 00:00:00 2001 From: StarKnightt Date: Mon, 23 Jun 2025 20:01:12 +0530 Subject: Add video speed control and improve playback smoothness Added a speed control dropdown in the timeline toolbar that lets users change video playback speed between 0.5x to 2x. The dropdown shows the current speed and offers preset options. Made video playback smoother by: - Using better timing for speed changes - Improving video synchronization - Reducing playback stutters - Making speed changes more responsive The speed control is now easily accessible while editing and works smoothly with all video clips. --- .../web/src/components/editor/properties-panel.tsx | 415 +++++++++++---------- apps/web/src/components/editor/speed-control.tsx | 46 +++ apps/web/src/components/editor/timeline.tsx | 32 +- 3 files changed, 295 insertions(+), 198 deletions(-) create mode 100644 apps/web/src/components/editor/speed-control.tsx (limited to 'apps/web/src/components/editor') diff --git a/apps/web/src/components/editor/properties-panel.tsx b/apps/web/src/components/editor/properties-panel.tsx index af80f464..2b211c21 100644 --- a/apps/web/src/components/editor/properties-panel.tsx +++ b/apps/web/src/components/editor/properties-panel.tsx @@ -1,197 +1,218 @@ -"use client"; - -import { Input } from "../ui/input"; -import { Label } from "../ui/label"; -import { Slider } from "../ui/slider"; -import { ScrollArea } from "../ui/scroll-area"; -import { Separator } from "../ui/separator"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "../ui/select"; -import { useTimelineStore } from "@/stores/timeline-store"; -import { useMediaStore } from "@/stores/media-store"; -import { ImageTimelineTreatment } from "@/components/ui/image-timeline-treatment"; -import { useState } from "react"; - -export function PropertiesPanel() { - const { tracks } = useTimelineStore(); - const { mediaItems } = useMediaStore(); - const [backgroundType, setBackgroundType] = useState< - "blur" | "mirror" | "color" - >("blur"); - const [backgroundColor, setBackgroundColor] = useState("#000000"); - - // Get the first image clip for preview (simplified) - const firstImageClip = tracks - .flatMap((track) => track.clips) - .find((clip) => { - const mediaItem = mediaItems.find((item) => item.id === clip.mediaId); - return mediaItem?.type === "image"; - }); - - const firstImageItem = firstImageClip - ? mediaItems.find((item) => item.id === firstImageClip.mediaId) - : null; - - return ( - -
- {/* Image Treatment - only show if an image is selected */} - {firstImageItem && ( - <> -
-

Image Treatment

-
- {/* Preview */} -
- -
- -
-
- - {/* Background Type */} -
- - -
- - {/* Background Color - only show for color type */} - {backgroundType === "color" && ( -
- -
- setBackgroundColor(e.target.value)} - className="w-16 h-10 p-1" - /> - setBackgroundColor(e.target.value)} - placeholder="#000000" - className="flex-1" - /> -
-
- )} -
-
- - - - )} - - {/* Transform */} -
-

Transform

-
-
-
- - -
-
- - -
-
-
- - -
-
-
- - - - {/* Effects */} -
-

Effects

-
-
- - -
-
- - -
-
-
- - - - {/* Timing */} -
-

Timing

-
-
- - -
-
- - -
-
-
-
-
- ); -} +"use client"; + +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; +import { Slider } from "../ui/slider"; +import { ScrollArea } from "../ui/scroll-area"; +import { Separator } from "../ui/separator"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { useMediaStore } from "@/stores/media-store"; +import { ImageTimelineTreatment } from "@/components/ui/image-timeline-treatment"; +import { useState } from "react"; +import { SpeedControl } from "./speed-control"; + +export function PropertiesPanel() { + const { tracks } = useTimelineStore(); + const { mediaItems } = useMediaStore(); + const [backgroundType, setBackgroundType] = useState< + "blur" | "mirror" | "color" + >("blur"); + const [backgroundColor, setBackgroundColor] = useState("#000000"); + + // Get the first video clip for preview (simplified) + const firstVideoClip = tracks + .flatMap((track) => track.clips) + .find((clip) => { + const mediaItem = mediaItems.find((item) => item.id === clip.mediaId); + return mediaItem?.type === "video"; + }); + + const firstVideoItem = firstVideoClip + ? mediaItems.find((item) => item.id === firstVideoClip.mediaId) + : null; + + // Get the first image clip for preview (simplified) + const firstImageClip = tracks + .flatMap((track) => track.clips) + .find((clip) => { + const mediaItem = mediaItems.find((item) => item.id === clip.mediaId); + return mediaItem?.type === "image"; + }); + + const firstImageItem = firstImageClip + ? mediaItems.find((item) => item.id === firstImageClip.mediaId) + : null; + + return ( + +
+ {/* Image Treatment - only show if an image is selected */} + {firstImageItem && ( + <> +
+

Image Treatment

+
+ {/* Preview */} +
+ +
+ +
+
+ + {/* Background Type */} +
+ + +
+ + {/* Background Color - only show for color type */} + {backgroundType === "color" && ( +
+ +
+ setBackgroundColor(e.target.value)} + className="w-16 h-10 p-1" + /> + setBackgroundColor(e.target.value)} + placeholder="#000000" + className="flex-1" + /> +
+
+ )} +
+
+ + + + )} + + {/* Video Controls - only show if a video is selected */} + {firstVideoItem && ( + <> + + + + )} + + {/* Transform */} +
+

Transform

+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + + + {/* Effects */} +
+

Effects

+
+
+ + +
+
+ + +
+
+
+ + + + {/* Timing */} +
+

Timing

+
+
+ + +
+
+ + +
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/editor/speed-control.tsx b/apps/web/src/components/editor/speed-control.tsx new file mode 100644 index 00000000..39281a9a --- /dev/null +++ b/apps/web/src/components/editor/speed-control.tsx @@ -0,0 +1,46 @@ +import { Slider } from "../ui/slider"; +import { Label } from "../ui/label"; +import { Button } from "../ui/button"; +import { usePlaybackStore } from "@/stores/playback-store"; + +const SPEED_PRESETS = [ + { label: "0.5x", value: 0.5 }, + { label: "1x", value: 1.0 }, + { label: "1.5x", value: 1.5 }, + { label: "2x", value: 2.0 }, +]; + +export function SpeedControl() { + const { speed, setSpeed } = usePlaybackStore(); + + return ( +
+

Playback Speed

+
+
+ {SPEED_PRESETS.map((preset) => ( + + ))} +
+
+ + setSpeed(value[0])} + className="mt-2" + /> +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/components/editor/timeline.tsx b/apps/web/src/components/editor/timeline.tsx index 1d2df66d..7c13bff8 100644 --- a/apps/web/src/components/editor/timeline.tsx +++ b/apps/web/src/components/editor/timeline.tsx @@ -28,6 +28,13 @@ import { usePlaybackStore } from "@/stores/playback-store"; import { processMediaFiles } from "@/lib/media-processing"; import { toast } from "sonner"; import { useState, useRef, useEffect } from "react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; export function Timeline() { // Timeline shows all tracks (video, audio, effects) and their clips. @@ -36,7 +43,7 @@ export function Timeline() { const { tracks, addTrack, addClipToTrack, removeTrack, toggleTrackMute, removeClipFromTrack, moveClipToTrack, getTotalDuration } = useTimelineStore(); const { mediaItems, addMediaItem } = useMediaStore(); - const { currentTime, duration, seek, setDuration, isPlaying, play, pause, toggle } = usePlaybackStore(); + const { currentTime, duration, seek, setDuration, isPlaying, play, pause, toggle, setSpeed, speed } = usePlaybackStore(); const [isDragOver, setIsDragOver] = useState(false); const [isProcessing, setIsProcessing] = useState(false); const [zoomLevel, setZoomLevel] = useState(1); @@ -331,6 +338,29 @@ export function Timeline() { Delete clip (Delete) + +
+ + {/* Speed Control */} + + + + + Playback Speed +
-- cgit v1.3.1