crashing from wav file size i think
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
import { Loader2, Scissors } from 'lucide-react';
|
||||
import { Loader2, Scissors, Trash2, Play, Pause } from 'lucide-react';
|
||||
|
||||
type SilenceRange = {
|
||||
start: number;
|
||||
@ -8,14 +8,31 @@ type SilenceRange = {
|
||||
duration: number;
|
||||
};
|
||||
|
||||
type TrimAction = 'cut' | 'mute';
|
||||
|
||||
export default function SilenceTrimmerPanel() {
|
||||
const { videoPath, backendUrl, addCutRange, duration } = useEditorStore();
|
||||
const {
|
||||
videoPath,
|
||||
backendUrl,
|
||||
addCutRange,
|
||||
addMuteRange,
|
||||
removeCutRange,
|
||||
removeMuteRange,
|
||||
cutRanges,
|
||||
muteRanges,
|
||||
duration,
|
||||
pauseUndo,
|
||||
resumeUndo
|
||||
} = useEditorStore();
|
||||
|
||||
const [minSilenceMs, setMinSilenceMs] = useState(500);
|
||||
const [silenceDb, setSilenceDb] = useState(-35);
|
||||
const [preBufferMs, setPreBufferMs] = useState(80);
|
||||
const [postBufferMs, setPostBufferMs] = useState(120);
|
||||
const [isDetecting, setIsDetecting] = useState(false);
|
||||
const [ranges, setRanges] = useState<SilenceRange[]>([]);
|
||||
const [trimAction, setTrimAction] = useState<TrimAction>('cut');
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
|
||||
const detectSilence = async () => {
|
||||
if (!videoPath) return;
|
||||
@ -58,6 +75,9 @@ export default function SilenceTrimmerPanel() {
|
||||
};
|
||||
|
||||
const applyAsCuts = () => {
|
||||
// Pause undo tracking to group all cuts into a single undo operation
|
||||
pauseUndo();
|
||||
|
||||
const preBufferSeconds = preBufferMs / 1000;
|
||||
const postBufferSeconds = postBufferMs / 1000;
|
||||
const maxEnd = duration > 0 ? duration : Number.POSITIVE_INFINITY;
|
||||
@ -67,11 +87,71 @@ export default function SilenceTrimmerPanel() {
|
||||
const start = Math.max(0, r.start + preBufferSeconds);
|
||||
const end = Math.min(maxEnd, r.end - postBufferSeconds);
|
||||
if (end - start >= 0.01) {
|
||||
addCutRange(start, end);
|
||||
if (trimAction === 'cut') {
|
||||
addCutRange(start, end);
|
||||
} else {
|
||||
addMuteRange(start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resume undo tracking - this creates a single undo entry for the entire batch
|
||||
resumeUndo();
|
||||
};
|
||||
|
||||
const removeExistingTrims = () => {
|
||||
pauseUndo();
|
||||
try {
|
||||
// Remove all cut ranges that match detected silence ranges
|
||||
cutRanges.forEach(range => {
|
||||
ranges.forEach(silenceRange => {
|
||||
if (Math.abs(range.start - silenceRange.start) < 0.1 &&
|
||||
Math.abs(range.end - silenceRange.end) < 0.1) {
|
||||
removeCutRange(range.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Remove all mute ranges that match detected silence ranges
|
||||
muteRanges.forEach(range => {
|
||||
ranges.forEach(silenceRange => {
|
||||
if (Math.abs(range.start - silenceRange.start) < 0.1 &&
|
||||
Math.abs(range.end - silenceRange.end) < 0.1) {
|
||||
removeMuteRange(range.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
resumeUndo();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleActive = () => {
|
||||
setIsActive(!isActive);
|
||||
if (!isActive) {
|
||||
// When activating, detect silence and apply
|
||||
detectSilence().then(() => {
|
||||
if (ranges.length > 0) {
|
||||
applyAsCuts();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// When deactivating, remove existing trims
|
||||
removeExistingTrims();
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-detect when video changes
|
||||
useEffect(() => {
|
||||
if (videoPath && isActive) {
|
||||
detectSilence().then(() => {
|
||||
if (ranges.length > 0) {
|
||||
applyAsCuts();
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [videoPath]);
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="space-y-1">
|
||||
@ -142,43 +222,95 @@ export default function SilenceTrimmerPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={detectSilence}
|
||||
disabled={isDetecting || !videoPath}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-editor-accent hover:bg-editor-accent-hover disabled:opacity-50 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
{isDetecting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Detecting pauses...
|
||||
</>
|
||||
) : (
|
||||
'Detect Pauses'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{ranges.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium">Detected {ranges.length} pause ranges</span>
|
||||
<button
|
||||
onClick={applyAsCuts}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-editor-accent/20 text-editor-accent rounded hover:bg-editor-accent/30"
|
||||
>
|
||||
<Scissors className="w-3 h-3" />
|
||||
Apply As Cuts
|
||||
</button>
|
||||
</div>
|
||||
<div className="max-h-56 overflow-y-auto space-y-1 pr-1">
|
||||
{ranges.slice(0, 50).map((r, i) => (
|
||||
<div key={`${r.start}-${r.end}-${i}`} className="px-2 py-1.5 rounded bg-editor-surface border border-editor-border text-xs">
|
||||
{r.start.toFixed(2)}s - {r.end.toFixed(2)}s ({r.duration.toFixed(2)}s)
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-[11px] text-editor-text-muted font-medium">
|
||||
Trim Action
|
||||
</label>
|
||||
<select
|
||||
value={trimAction}
|
||||
onChange={(e) => setTrimAction(e.target.value as TrimAction)}
|
||||
className="w-full px-2.5 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none"
|
||||
>
|
||||
<option value="cut">Cut (remove silence)</option>
|
||||
<option value="mute">Mute (silence audio)</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[11px] text-editor-text-muted font-medium">
|
||||
Active Mode
|
||||
</label>
|
||||
<button
|
||||
onClick={toggleActive}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 text-xs rounded transition-colors ${
|
||||
isActive
|
||||
? 'bg-green-600 hover:bg-green-700 text-white'
|
||||
: 'bg-editor-surface border border-editor-border hover:bg-editor-surface-hover'
|
||||
}`}
|
||||
>
|
||||
{isActive ? (
|
||||
<>
|
||||
<Pause className="w-3 h-3" />
|
||||
Active
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="w-3 h-3" />
|
||||
Inactive
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={detectSilence}
|
||||
disabled={isDetecting || !videoPath}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-editor-accent hover:bg-editor-accent-hover disabled:opacity-50 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
{isDetecting ? (
|
||||
<>
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
Detecting...
|
||||
</>
|
||||
) : (
|
||||
'Detect Pauses'
|
||||
)}
|
||||
</button>
|
||||
|
||||
{ranges.length > 0 && (
|
||||
<button
|
||||
onClick={removeExistingTrims}
|
||||
className="flex items-center gap-1 px-3 py-2 text-xs bg-red-600 hover:bg-red-700 text-white rounded transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Remove Trims
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{ranges.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium">Detected {ranges.length} pause ranges</span>
|
||||
<button
|
||||
onClick={applyAsCuts}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs bg-editor-accent/20 text-editor-accent rounded hover:bg-editor-accent/30"
|
||||
>
|
||||
<Scissors className="w-3 h-3" />
|
||||
Apply As {trimAction === 'cut' ? 'Cuts' : 'Mutes'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="max-h-56 overflow-y-auto space-y-1 pr-1">
|
||||
{ranges.slice(0, 50).map((r, i) => (
|
||||
<div key={`${r.start}-${r.end}-${i}`} className="px-2 py-1.5 rounded bg-editor-surface border border-editor-border text-xs">
|
||||
{r.start.toFixed(2)}s - {r.end.toFixed(2)}s ({r.duration.toFixed(2)}s)
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -5,24 +5,31 @@ import { Play, Pause, SkipBack, SkipForward, Volume2 } from 'lucide-react';
|
||||
|
||||
export default function VideoPlayer() {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const audioRef = useRef<HTMLAudioElement>(null);
|
||||
const videoUrl = useEditorStore((s) => s.videoUrl);
|
||||
const isPlaying = useEditorStore((s) => s.isPlaying);
|
||||
const duration = useEditorStore((s) => s.duration);
|
||||
const { seekTo, togglePlay } = useVideoSync(videoRef);
|
||||
|
||||
// Determine if this is an audio file based on the URL
|
||||
const isAudioFile = videoUrl && (videoUrl.includes('.wav') || videoUrl.includes('.mp3') || videoUrl.includes('.m4a') || videoUrl.includes('.aac'));
|
||||
|
||||
const mediaRef = isAudioFile ? audioRef : videoRef;
|
||||
|
||||
const { seekTo, togglePlay } = useVideoSync(mediaRef as React.RefObject<HTMLVideoElement | HTMLAudioElement | null>);
|
||||
|
||||
const [displayTime, setDisplayTime] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const media = mediaRef.current;
|
||||
if (!media) return;
|
||||
let raf = 0;
|
||||
const tick = () => {
|
||||
setDisplayTime(video.currentTime);
|
||||
setDisplayTime(media.currentTime);
|
||||
raf = requestAnimationFrame(tick);
|
||||
};
|
||||
raf = requestAnimationFrame(tick);
|
||||
return () => cancelAnimationFrame(raf);
|
||||
}, [videoUrl]);
|
||||
}, [videoUrl, mediaRef]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const m = Math.floor(seconds / 60);
|
||||
@ -41,11 +48,11 @@ export default function VideoPlayer() {
|
||||
|
||||
const skip = useCallback(
|
||||
(delta: number) => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
seekTo(Math.max(0, Math.min(duration, video.currentTime + delta)));
|
||||
const media = mediaRef.current;
|
||||
if (!media) return;
|
||||
seekTo(Math.max(0, Math.min(duration, media.currentTime + delta)));
|
||||
},
|
||||
[seekTo, duration],
|
||||
[seekTo, duration, mediaRef],
|
||||
);
|
||||
|
||||
if (!videoUrl) {
|
||||
@ -59,13 +66,37 @@ export default function VideoPlayer() {
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col">
|
||||
<div className="flex-1 flex items-center justify-center bg-black rounded-lg overflow-hidden min-h-0">
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoUrl}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
playsInline
|
||||
onClick={togglePlay}
|
||||
/>
|
||||
{isAudioFile ? (
|
||||
<audio
|
||||
ref={audioRef}
|
||||
src={videoUrl}
|
||||
className="max-w-full max-h-full"
|
||||
controls={false}
|
||||
onClick={togglePlay}
|
||||
onError={(e) => {
|
||||
console.error('Audio load error:', e);
|
||||
console.error('Audio src:', videoUrl);
|
||||
}}
|
||||
onLoadStart={() => console.log('Audio load start:', videoUrl)}
|
||||
onLoadedData={() => console.log('Audio loaded data')}
|
||||
onCanPlay={() => console.log('Audio can play')}
|
||||
/>
|
||||
) : (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={videoUrl}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
playsInline
|
||||
onClick={togglePlay}
|
||||
onError={(e) => {
|
||||
console.error('Video load error:', e);
|
||||
console.error('Video src:', videoUrl);
|
||||
}}
|
||||
onLoadStart={() => console.log('Video load start:', videoUrl)}
|
||||
onLoadedData={() => console.log('Video loaded data')}
|
||||
onCanPlay={() => console.log('Video can play')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-2 space-y-1.5 shrink-0">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useCallback, useRef, useEffect } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
|
||||
export function useVideoSync(videoRef: React.RefObject<HTMLVideoElement | null>) {
|
||||
export function useVideoSync(videoRef: React.RefObject<HTMLVideoElement | HTMLAudioElement | null>) {
|
||||
const rafRef = useRef<number>(0);
|
||||
const {
|
||||
setCurrentTime,
|
||||
@ -52,13 +52,13 @@ export function useVideoSync(videoRef: React.RefObject<HTMLVideoElement | null>)
|
||||
}, [videoRef]);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const media = videoRef.current;
|
||||
if (!media) return;
|
||||
|
||||
const onTimeUpdate = () => {
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
let t = video.currentTime;
|
||||
let t = media.currentTime;
|
||||
|
||||
// Skip over deleted ranges and cut ranges (handle overlapping/chained ranges)
|
||||
const allSkipRanges = [...deletedRanges, ...cutRanges];
|
||||
@ -79,19 +79,21 @@ export function useVideoSync(videoRef: React.RefObject<HTMLVideoElement | null>)
|
||||
}
|
||||
|
||||
if (skipCount > 0) {
|
||||
video.currentTime = t;
|
||||
media.currentTime = t;
|
||||
return;
|
||||
}
|
||||
|
||||
// Mute/unmute based on mute ranges
|
||||
let shouldMute = false;
|
||||
for (const range of muteRanges) {
|
||||
if (t >= range.start && t < range.end) {
|
||||
shouldMute = true;
|
||||
break;
|
||||
// Mute/unmute based on mute ranges (only for video elements)
|
||||
if ('muted' in media) {
|
||||
let shouldMute = false;
|
||||
for (const range of muteRanges) {
|
||||
if (t >= range.start && t < range.end) {
|
||||
shouldMute = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
media.muted = shouldMute;
|
||||
}
|
||||
video.muted = shouldMute;
|
||||
|
||||
setCurrentTime(t);
|
||||
});
|
||||
@ -99,18 +101,18 @@ export function useVideoSync(videoRef: React.RefObject<HTMLVideoElement | null>)
|
||||
|
||||
const onPlay = () => setIsPlaying(true);
|
||||
const onPause = () => setIsPlaying(false);
|
||||
const onLoadedMetadata = () => setDuration(video.duration);
|
||||
const onLoadedMetadata = () => setDuration(media.duration);
|
||||
|
||||
video.addEventListener('timeupdate', onTimeUpdate);
|
||||
video.addEventListener('play', onPlay);
|
||||
video.addEventListener('pause', onPause);
|
||||
video.addEventListener('loadedmetadata', onLoadedMetadata);
|
||||
media.addEventListener('timeupdate', onTimeUpdate);
|
||||
media.addEventListener('play', onPlay);
|
||||
media.addEventListener('pause', onPause);
|
||||
media.addEventListener('loadedmetadata', onLoadedMetadata);
|
||||
|
||||
return () => {
|
||||
video.removeEventListener('timeupdate', onTimeUpdate);
|
||||
video.removeEventListener('play', onPlay);
|
||||
video.removeEventListener('pause', onPause);
|
||||
video.removeEventListener('loadedmetadata', onLoadedMetadata);
|
||||
media.removeEventListener('timeupdate', onTimeUpdate);
|
||||
media.removeEventListener('play', onPlay);
|
||||
media.removeEventListener('pause', onPause);
|
||||
media.removeEventListener('loadedmetadata', onLoadedMetadata);
|
||||
cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, [videoRef, deletedRanges, cutRanges, muteRanges, setCurrentTime, setIsPlaying, setDuration]);
|
||||
|
||||
@ -55,6 +55,8 @@ interface EditorActions {
|
||||
getWordAtTime: (time: number) => number;
|
||||
loadProject: (projectData: any) => void;
|
||||
reset: () => void;
|
||||
pauseUndo: () => void;
|
||||
resumeUndo: () => void;
|
||||
}
|
||||
|
||||
const initialState: EditorState = {
|
||||
@ -327,6 +329,22 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
||||
},
|
||||
|
||||
reset: () => set(initialState),
|
||||
|
||||
pauseUndo: () => {
|
||||
// Access the temporal store through the useEditorStore
|
||||
const temporalStore = (useEditorStore as any).temporal;
|
||||
if (temporalStore) {
|
||||
temporalStore.getState().pause();
|
||||
}
|
||||
},
|
||||
|
||||
resumeUndo: () => {
|
||||
// Access the temporal store through the useEditorStore
|
||||
const temporalStore = (useEditorStore as any).temporal;
|
||||
if (temporalStore) {
|
||||
temporalStore.getState().resume();
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ limit: 100 },
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user