diff options
| author | iza <59828082+izadoesdev@users.noreply.github.com> | 2025-06-25 12:09:05 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-25 12:09:05 +0300 |
| commit | a4201c8be67818e765c9ee00736955b44a8eeab2 (patch) | |
| tree | cbe1d45819f3559c302d43bd0ee59ec1a7551d8c | |
| parent | 3989da93c379e319c02d47bac093e6c933e5adfe (diff) | |
| parent | 01b9dbccc277cff3b3d53ec241170f5c04102620 (diff) | |
| download | opencut-a4201c8be67818e765c9ee00736955b44a8eeab2.tar.gz | |
Merge pull request #93 from DevloperAmanSingh/feat/global-mute-control
Feature - Global Mute Control
| -rw-r--r-- | apps/web/src/components/editor/preview-panel.tsx | 67 | ||||
| -rw-r--r-- | apps/web/src/components/ui/video-player.tsx | 196 | ||||
| -rw-r--r-- | apps/web/src/stores/playback-store.ts | 61 | ||||
| -rw-r--r-- | apps/web/src/types/playback.ts | 7 |
4 files changed, 212 insertions, 119 deletions
diff --git a/apps/web/src/components/editor/preview-panel.tsx b/apps/web/src/components/editor/preview-panel.tsx index 624a549e..33bfe688 100644 --- a/apps/web/src/components/editor/preview-panel.tsx +++ b/apps/web/src/components/editor/preview-panel.tsx @@ -5,16 +5,17 @@ import { useMediaStore } from "@/stores/media-store"; import { usePlaybackStore } from "@/stores/playback-store";
import { VideoPlayer } from "@/components/ui/video-player";
import { Button } from "@/components/ui/button";
-import { Play, Pause } from "lucide-react";
+import { Play, Pause, Volume2, VolumeX } from "lucide-react";
import { useState, useRef } from "react";
// Debug flag - set to false to hide active clips info
-const SHOW_DEBUG_INFO = process.env.NODE_ENV === 'development';
+const SHOW_DEBUG_INFO = process.env.NODE_ENV === "development";
export function PreviewPanel() {
const { tracks } = useTimelineStore();
const { mediaItems } = useMediaStore();
- const { isPlaying, toggle, currentTime } = usePlaybackStore();
+ const { isPlaying, toggle, currentTime, muted, toggleMute, volume } =
+ usePlaybackStore();
const [canvasSize, setCanvasSize] = useState({ width: 1920, height: 1080 });
const [showDebug, setShowDebug] = useState(SHOW_DEBUG_INFO);
const previewRef = useRef<HTMLDivElement>(null);
@@ -30,12 +31,14 @@ export function PreviewPanel() { tracks.forEach((track) => {
track.clips.forEach((clip) => {
const clipStart = clip.startTime;
- const clipEnd = clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd);
+ const clipEnd =
+ clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd);
if (currentTime >= clipStart && currentTime < clipEnd) {
- const mediaItem = clip.mediaId === "test"
- ? { type: "test", name: clip.name, url: "", thumbnailUrl: "" }
- : mediaItems.find((item) => item.id === clip.mediaId);
+ const mediaItem =
+ clip.mediaId === "test"
+ ? { type: "test", name: clip.name, url: "", thumbnailUrl: "" }
+ : mediaItems.find((item) => item.id === clip.mediaId);
if (mediaItem || clip.mediaId === "test") {
activeClips.push({ clip, track, mediaItem });
@@ -134,13 +137,19 @@ export function PreviewPanel() { <select
value={`${canvasSize.width}x${canvasSize.height}`}
onChange={(e) => {
- const preset = canvasPresets.find(p => `${p.width}x${p.height}` === e.target.value);
- if (preset) setCanvasSize({ width: preset.width, height: preset.height });
+ const preset = canvasPresets.find(
+ (p) => `${p.width}x${p.height}` === e.target.value
+ );
+ if (preset)
+ setCanvasSize({ width: preset.width, height: preset.height });
}}
className="bg-background border rounded px-2 py-1 text-xs"
>
- {canvasPresets.map(preset => (
- <option key={preset.name} value={`${preset.width}x${preset.height}`}>
+ {canvasPresets.map((preset) => (
+ <option
+ key={preset.name}
+ value={`${preset.width}x${preset.height}`}
+ >
{preset.name} ({preset.width}×{preset.height})
</option>
))}
@@ -154,12 +163,30 @@ export function PreviewPanel() { onClick={() => setShowDebug(!showDebug)}
className="text-xs"
>
- Debug {showDebug ? 'ON' : 'OFF'}
+ Debug {showDebug ? "ON" : "OFF"}
</Button>
)}
- <Button variant="outline" size="sm" onClick={toggle} className="ml-auto">
- {isPlaying ? <Pause className="h-3 w-3 mr-1" /> : <Play className="h-3 w-3 mr-1" />}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={toggleMute}
+ className="ml-auto"
+ >
+ {muted || volume === 0 ? (
+ <VolumeX className="h-3 w-3 mr-1" />
+ ) : (
+ <Volume2 className="h-3 w-3 mr-1" />
+ )}
+ {muted || volume === 0 ? "Unmute" : "Mute"}
+ </Button>
+
+ <Button variant="outline" size="sm" onClick={toggle}>
+ {isPlaying ? (
+ <Pause className="h-3 w-3 mr-1" />
+ ) : (
+ <Play className="h-3 w-3 mr-1" />
+ )}
{isPlaying ? "Pause" : "Play"}
</Button>
</div>
@@ -177,7 +204,9 @@ export function PreviewPanel() { >
{activeClips.length === 0 ? (
<div className="absolute inset-0 flex items-center justify-center text-white/50">
- {tracks.length === 0 ? "Drop media to start editing" : "No clips at current time"}
+ {tracks.length === 0
+ ? "Drop media to start editing"
+ : "No clips at current time"}
</div>
) : (
activeClips.map((clipData, index) => renderClip(clipData, index))
@@ -188,7 +217,9 @@ export function PreviewPanel() { {/* Debug Info Panel - Conditionally rendered */}
{showDebug && (
<div className="border-t bg-background p-2 flex-shrink-0">
- <div className="text-xs font-medium mb-1">Debug: Active Clips ({activeClips.length})</div>
+ <div className="text-xs font-medium mb-1">
+ Debug: Active Clips ({activeClips.length})
+ </div>
<div className="flex gap-2 overflow-x-auto">
{activeClips.map((clipData, index) => (
<div
@@ -199,7 +230,9 @@ export function PreviewPanel() { {index + 1}
</span>
<span>{clipData.clip.name}</span>
- <span className="text-muted-foreground">({clipData.mediaItem?.type || 'test'})</span>
+ <span className="text-muted-foreground">
+ ({clipData.mediaItem?.type || "test"})
+ </span>
</div>
))}
{activeClips.length === 0 && (
diff --git a/apps/web/src/components/ui/video-player.tsx b/apps/web/src/components/ui/video-player.tsx index 32a12916..d8a72e40 100644 --- a/apps/web/src/components/ui/video-player.tsx +++ b/apps/web/src/components/ui/video-player.tsx @@ -4,108 +4,128 @@ import { useRef, useEffect } from "react"; import { usePlaybackStore } from "@/stores/playback-store"; interface VideoPlayerProps { - src: string; - poster?: string; - className?: string; - clipStartTime: number; - trimStart: number; - trimEnd: number; - clipDuration: number; + src: string; + poster?: string; + className?: string; + clipStartTime: number; + trimStart: number; + trimEnd: number; + clipDuration: number; } export function VideoPlayer({ - src, - poster, - className = "", - clipStartTime, - trimStart, - trimEnd, - clipDuration + src, + poster, + className = "", + clipStartTime, + trimStart, + trimEnd, + clipDuration, }: VideoPlayerProps) { - const videoRef = useRef<HTMLVideoElement>(null); - const { isPlaying, currentTime, volume, speed } = usePlaybackStore(); + const videoRef = useRef<HTMLVideoElement>(null); + const { isPlaying, currentTime, volume, speed, muted } = usePlaybackStore(); - // Calculate if we're within this clip's timeline range - const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd); - const isInClipRange = currentTime >= clipStartTime && currentTime < clipEndTime; + // Calculate if we're within this clip's timeline range + const clipEndTime = clipStartTime + (clipDuration - trimStart - trimEnd); + const isInClipRange = + currentTime >= clipStartTime && currentTime < clipEndTime; - // Sync playback events - useEffect(() => { - const video = videoRef.current; - if (!video || !isInClipRange) return; + // Sync playback events + useEffect(() => { + const video = videoRef.current; + if (!video || !isInClipRange) return; - const handleSeekEvent = (e: CustomEvent) => { - // Always update video time, even if outside clip range - const timelineTime = e.detail.time; - const videoTime = Math.max(trimStart, Math.min( - clipDuration - trimEnd, - timelineTime - clipStartTime + trimStart - )); - video.currentTime = videoTime; - }; + const handleSeekEvent = (e: CustomEvent) => { + // Always update video time, even if outside clip range + const timelineTime = e.detail.time; + const videoTime = Math.max( + trimStart, + Math.min( + clipDuration - trimEnd, + timelineTime - clipStartTime + trimStart + ) + ); + video.currentTime = videoTime; + }; - const handleUpdateEvent = (e: CustomEvent) => { - // Always update video time, even if outside clip range - const timelineTime = e.detail.time; - const targetTime = Math.max(trimStart, Math.min( - clipDuration - trimEnd, - timelineTime - clipStartTime + trimStart - )); + const handleUpdateEvent = (e: CustomEvent) => { + // Always update video time, even if outside clip range + const timelineTime = e.detail.time; + const targetTime = Math.max( + trimStart, + Math.min( + clipDuration - trimEnd, + timelineTime - clipStartTime + trimStart + ) + ); - if (Math.abs(video.currentTime - targetTime) > 0.5) { - video.currentTime = targetTime; - } - }; + if (Math.abs(video.currentTime - targetTime) > 0.5) { + video.currentTime = targetTime; + } + }; - const handleSpeed = (e: CustomEvent) => { - video.playbackRate = e.detail.speed; - }; + const handleSpeed = (e: CustomEvent) => { + video.playbackRate = e.detail.speed; + }; - window.addEventListener("playback-seek", handleSeekEvent as EventListener); - window.addEventListener("playback-update", handleUpdateEvent as EventListener); - window.addEventListener("playback-speed", handleSpeed as EventListener); + window.addEventListener("playback-seek", handleSeekEvent as EventListener); + window.addEventListener( + "playback-update", + handleUpdateEvent as EventListener + ); + window.addEventListener("playback-speed", handleSpeed as EventListener); - return () => { - window.removeEventListener("playback-seek", handleSeekEvent as EventListener); - window.removeEventListener("playback-update", handleUpdateEvent as EventListener); - window.removeEventListener("playback-speed", handleSpeed as EventListener); - }; - }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]); + return () => { + window.removeEventListener( + "playback-seek", + handleSeekEvent as EventListener + ); + window.removeEventListener( + "playback-update", + handleUpdateEvent as EventListener + ); + window.removeEventListener( + "playback-speed", + handleSpeed as EventListener + ); + }; + }, [clipStartTime, trimStart, trimEnd, clipDuration, isInClipRange]); - // Sync playback state - useEffect(() => { - const video = videoRef.current; - if (!video) return; + // Sync playback state + useEffect(() => { + const video = videoRef.current; + if (!video) return; - if (isPlaying && isInClipRange) { - video.play().catch(() => { }); - } else { - video.pause(); - } - }, [isPlaying, isInClipRange]); + if (isPlaying && isInClipRange) { + video.play().catch(() => {}); + } else { + video.pause(); + } + }, [isPlaying, isInClipRange]); - // Sync volume and speed - useEffect(() => { - const video = videoRef.current; - if (!video) return; + // Sync volume and speed + useEffect(() => { + const video = videoRef.current; + if (!video) return; - video.volume = volume; - video.playbackRate = speed; - }, [volume, speed]); + video.volume = volume; + video.muted = muted; + video.playbackRate = speed; + }, [volume, speed, muted]); - return ( - <video - ref={videoRef} - src={src} - poster={poster} - className={`w-full h-full object-cover ${className}`} - playsInline - preload="auto" - controls={false} - disablePictureInPicture - disableRemotePlayback - style={{ pointerEvents: 'none' }} - onContextMenu={(e) => e.preventDefault()} - /> - ); -}
\ No newline at end of file + return ( + <video + ref={videoRef} + src={src} + poster={poster} + className={`w-full h-full object-cover ${className}`} + playsInline + preload="auto" + controls={false} + disablePictureInPicture + disableRemotePlayback + style={{ pointerEvents: "none" }} + onContextMenu={(e) => e.preventDefault()} + /> + ); +} diff --git a/apps/web/src/stores/playback-store.ts b/apps/web/src/stores/playback-store.ts index a4794b7f..0127edff 100644 --- a/apps/web/src/stores/playback-store.ts +++ b/apps/web/src/stores/playback-store.ts @@ -10,7 +10,7 @@ let playbackTimer: number | null = null; const startTimer = (store: any) => { if (playbackTimer) cancelAnimationFrame(playbackTimer); - + // Use requestAnimationFrame for smoother updates const updateTime = () => { const state = store(); @@ -18,14 +18,16 @@ const startTimer = (store: any) => { const now = performance.now(); const delta = (now - lastUpdate) / 1000; // Convert to seconds lastUpdate = now; - - const newTime = state.currentTime + (delta * state.speed); + + const newTime = state.currentTime + delta * state.speed; if (newTime >= state.duration) { state.pause(); } else { state.setCurrentTime(newTime); // Notify video elements to sync - window.dispatchEvent(new CustomEvent('playback-update', { detail: { time: newTime } })); + window.dispatchEvent( + new CustomEvent("playback-update", { detail: { time: newTime } }) + ); } } playbackTimer = requestAnimationFrame(updateTime); @@ -47,6 +49,8 @@ export const usePlaybackStore = create<PlaybackStore>((set, get) => ({ currentTime: 0, duration: 0, volume: 1, + muted: false, + previousVolume: 1, speed: 1.0, play: () => { @@ -72,22 +76,53 @@ export const usePlaybackStore = create<PlaybackStore>((set, get) => ({ const { duration } = get(); const clampedTime = Math.max(0, Math.min(duration, time)); set({ currentTime: clampedTime }); - - // Notify video elements to seek - const event = new CustomEvent('playback-seek', { detail: { time: clampedTime } }); + + const event = new CustomEvent("playback-seek", { + detail: { time: clampedTime }, + }); window.dispatchEvent(event); }, - - setVolume: (volume: number) => set({ volume: Math.max(0, Math.min(1, volume)) }), - + + setVolume: (volume: number) => + set((state) => ({ + volume: Math.max(0, Math.min(1, volume)), + muted: volume === 0, + previousVolume: volume > 0 ? volume : state.previousVolume, + })), + setSpeed: (speed: number) => { const newSpeed = Math.max(0.1, Math.min(2.0, speed)); set({ speed: newSpeed }); - - const event = new CustomEvent('playback-speed', { detail: { speed: newSpeed } }); + + const event = new CustomEvent("playback-speed", { + detail: { speed: newSpeed }, + }); window.dispatchEvent(event); }, setDuration: (duration: number) => set({ duration }), setCurrentTime: (time: number) => set({ currentTime: time }), -}));
\ No newline at end of file + + mute: () => { + const { volume, previousVolume } = get(); + set({ + muted: true, + previousVolume: volume > 0 ? volume : previousVolume, + volume: 0, + }); + }, + + unmute: () => { + const { previousVolume } = get(); + set({ muted: false, volume: previousVolume ?? 1 }); + }, + + toggleMute: () => { + const { muted } = get(); + if (muted) { + get().unmute(); + } else { + get().mute(); + } + }, +})); diff --git a/apps/web/src/types/playback.ts b/apps/web/src/types/playback.ts index 3fbd1a02..8758ea03 100644 --- a/apps/web/src/types/playback.ts +++ b/apps/web/src/types/playback.ts @@ -4,6 +4,8 @@ export interface PlaybackState { duration: number; volume: number; speed: number; + muted: boolean; + previousVolume?: number; } export interface PlaybackControls { @@ -13,4 +15,7 @@ export interface PlaybackControls { setVolume: (volume: number) => void; setSpeed: (speed: number) => void; toggle: () => void; -}
\ No newline at end of file + mute: () => void; + unmute: () => void; + toggleMute: () => void; +} |
