mirror of
https://github.com/mediacms-io/mediacms.git
synced 2025-11-05 23:18:53 -05:00
346 lines
11 KiB
TypeScript
346 lines
11 KiB
TypeScript
import { useRef, useEffect, useState } from "react";
|
|
import { formatTime, formatDetailedTime } from "./lib/timeUtils";
|
|
import logger from "./lib/logger";
|
|
import VideoPlayer from "@/components/VideoPlayer";
|
|
import TimelineControls from "@/components/TimelineControls";
|
|
import EditingTools from "@/components/EditingTools";
|
|
import ClipSegments from "@/components/ClipSegments";
|
|
import MobilePlayPrompt from "@/components/IOSPlayPrompt";
|
|
import useVideoTrimmer from "@/hooks/useVideoTrimmer";
|
|
|
|
const App = () => {
|
|
const {
|
|
videoRef,
|
|
currentTime,
|
|
duration,
|
|
isPlaying,
|
|
setIsPlaying,
|
|
isMuted,
|
|
thumbnails,
|
|
trimStart,
|
|
trimEnd,
|
|
splitPoints,
|
|
zoomLevel,
|
|
clipSegments,
|
|
hasUnsavedChanges,
|
|
historyPosition,
|
|
history,
|
|
handleTrimStartChange,
|
|
handleTrimEndChange,
|
|
handleZoomChange,
|
|
handleMobileSafeSeek,
|
|
handleSplit,
|
|
handleReset,
|
|
handleUndo,
|
|
handleRedo,
|
|
toggleMute,
|
|
handleSave,
|
|
handleSaveACopy,
|
|
handleSaveSegments,
|
|
isMobile,
|
|
videoInitialized,
|
|
setVideoInitialized,
|
|
isPlayingSegments,
|
|
handlePlaySegments
|
|
} = useVideoTrimmer();
|
|
|
|
// Function to play from the beginning
|
|
const playFromBeginning = () => {
|
|
if (videoRef.current) {
|
|
videoRef.current.currentTime = 0;
|
|
handleMobileSafeSeek(0);
|
|
if (!isPlaying) {
|
|
handlePlay();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Function to jump 15 seconds backward
|
|
const jumpBackward15 = () => {
|
|
const newTime = Math.max(0, currentTime - 15);
|
|
handleMobileSafeSeek(newTime);
|
|
};
|
|
|
|
// Function to jump 15 seconds forward
|
|
const jumpForward15 = () => {
|
|
const newTime = Math.min(duration, currentTime + 15);
|
|
handleMobileSafeSeek(newTime);
|
|
};
|
|
|
|
const handlePlay = () => {
|
|
if (!videoRef.current) return;
|
|
|
|
const video = videoRef.current;
|
|
|
|
// If already playing, just pause the video
|
|
if (isPlaying) {
|
|
video.pause();
|
|
setIsPlaying(false);
|
|
return;
|
|
}
|
|
|
|
const currentPosition = Number(video.currentTime.toFixed(6)); // Fix to microsecond precision
|
|
|
|
// Find the next stopping point based on current position
|
|
let stopTime = duration;
|
|
let currentSegment = null;
|
|
let nextSegment = null;
|
|
|
|
// Sort segments by start time to ensure correct order
|
|
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
|
|
|
|
// First, check if we're inside a segment or exactly at its start/end
|
|
currentSegment = sortedSegments.find((seg) => {
|
|
const segStartTime = Number(seg.startTime.toFixed(6));
|
|
const segEndTime = Number(seg.endTime.toFixed(6));
|
|
|
|
// Check if we're inside the segment
|
|
if (currentPosition > segStartTime && currentPosition < segEndTime) {
|
|
return true;
|
|
}
|
|
// Check if we're exactly at the start
|
|
if (currentPosition === segStartTime) {
|
|
return true;
|
|
}
|
|
// Check if we're exactly at the end
|
|
if (currentPosition === segEndTime) {
|
|
// If we're at the end of a segment, we should look for the next one
|
|
return false;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// If we're not in a segment, find the next segment
|
|
if (!currentSegment) {
|
|
nextSegment = sortedSegments.find((seg) => {
|
|
const segStartTime = Number(seg.startTime.toFixed(6));
|
|
return segStartTime > currentPosition;
|
|
});
|
|
}
|
|
|
|
// Determine where to stop based on position
|
|
if (currentSegment) {
|
|
// If we're in a segment, stop at its end
|
|
stopTime = Number(currentSegment.endTime.toFixed(6));
|
|
} else if (nextSegment) {
|
|
// If we're in a cutaway and there's a next segment, stop at its start
|
|
stopTime = Number(nextSegment.startTime.toFixed(6));
|
|
}
|
|
|
|
// Create a boundary checker function with high precision
|
|
const checkBoundary = () => {
|
|
if (!video) return;
|
|
|
|
const currentPosition = Number(video.currentTime.toFixed(6));
|
|
const timeLeft = Number((stopTime - currentPosition).toFixed(6));
|
|
|
|
// If we've reached or passed the boundary
|
|
if (timeLeft <= 0 || currentPosition >= stopTime) {
|
|
// First pause playback
|
|
video.pause();
|
|
|
|
// Force exact position with multiple verification attempts
|
|
const setExactPosition = () => {
|
|
if (!video) return;
|
|
|
|
// Set to exact boundary time
|
|
video.currentTime = stopTime;
|
|
handleMobileSafeSeek(stopTime);
|
|
|
|
const actualPosition = Number(video.currentTime.toFixed(6));
|
|
const difference = Number(Math.abs(actualPosition - stopTime).toFixed(6));
|
|
|
|
logger.debug("Position verification:", {
|
|
target: formatDetailedTime(stopTime),
|
|
actual: formatDetailedTime(actualPosition),
|
|
difference: difference
|
|
});
|
|
|
|
// If we're not exactly at the target position, try one more time
|
|
if (difference > 0) {
|
|
video.currentTime = stopTime;
|
|
handleMobileSafeSeek(stopTime);
|
|
}
|
|
};
|
|
|
|
// Multiple attempts to ensure precision, with increasing delays
|
|
setExactPosition();
|
|
setTimeout(setExactPosition, 5); // Quick first retry
|
|
setTimeout(setExactPosition, 10); // Second retry
|
|
setTimeout(setExactPosition, 20); // Third retry if needed
|
|
setTimeout(setExactPosition, 50); // Final verification
|
|
|
|
// Remove our boundary checker
|
|
video.removeEventListener("timeupdate", checkBoundary);
|
|
setIsPlaying(false);
|
|
|
|
// Log the final position for debugging
|
|
logger.debug("Stopped at position:", {
|
|
target: formatDetailedTime(stopTime),
|
|
actual: formatDetailedTime(video.currentTime),
|
|
type: currentSegment
|
|
? "segment end"
|
|
: nextSegment
|
|
? "next segment start"
|
|
: "end of video",
|
|
segment: currentSegment
|
|
? {
|
|
id: currentSegment.id,
|
|
start: formatDetailedTime(currentSegment.startTime),
|
|
end: formatDetailedTime(currentSegment.endTime)
|
|
}
|
|
: null,
|
|
nextSegment: nextSegment
|
|
? {
|
|
id: nextSegment.id,
|
|
start: formatDetailedTime(nextSegment.startTime),
|
|
end: formatDetailedTime(nextSegment.endTime)
|
|
}
|
|
: null
|
|
});
|
|
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Start our boundary checker
|
|
video.addEventListener("timeupdate", checkBoundary);
|
|
|
|
// Start playing
|
|
video
|
|
.play()
|
|
.then(() => {
|
|
setIsPlaying(true);
|
|
setVideoInitialized(true);
|
|
logger.debug("Playback started:", {
|
|
from: formatDetailedTime(currentPosition),
|
|
to: formatDetailedTime(stopTime),
|
|
currentSegment: currentSegment
|
|
? {
|
|
id: currentSegment.id,
|
|
start: formatDetailedTime(currentSegment.startTime),
|
|
end: formatDetailedTime(currentSegment.endTime)
|
|
}
|
|
: "None",
|
|
nextSegment: nextSegment
|
|
? {
|
|
id: nextSegment.id,
|
|
start: formatDetailedTime(nextSegment.startTime),
|
|
end: formatDetailedTime(nextSegment.endTime)
|
|
}
|
|
: "None"
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
console.error("Error playing video:", err);
|
|
});
|
|
};
|
|
|
|
// Handle keyboard shortcuts
|
|
useEffect(() => {
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
// Don't handle keyboard shortcuts if user is typing in an input field
|
|
const target = event.target as HTMLElement;
|
|
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {
|
|
return;
|
|
}
|
|
|
|
switch (event.code) {
|
|
case 'Space':
|
|
event.preventDefault(); // Prevent default spacebar behavior (scrolling, button activation)
|
|
handlePlay();
|
|
break;
|
|
case 'ArrowLeft':
|
|
event.preventDefault();
|
|
if (videoRef.current) {
|
|
// Use the video element's current time directly to avoid stale state
|
|
const newTime = Math.max(videoRef.current.currentTime - 10, 0);
|
|
handleMobileSafeSeek(newTime);
|
|
logger.debug('Jumped backward 10 seconds to:', formatDetailedTime(newTime));
|
|
}
|
|
break;
|
|
case 'ArrowRight':
|
|
event.preventDefault();
|
|
if (videoRef.current) {
|
|
// Use the video element's current time directly to avoid stale state
|
|
const newTime = Math.min(videoRef.current.currentTime + 10, duration);
|
|
handleMobileSafeSeek(newTime);
|
|
logger.debug('Jumped forward 10 seconds to:', formatDetailedTime(newTime));
|
|
}
|
|
break;
|
|
}
|
|
};
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
};
|
|
}, [handlePlay, handleMobileSafeSeek, duration, videoRef]);
|
|
|
|
return (
|
|
<div className="bg-background min-h-screen">
|
|
<MobilePlayPrompt videoRef={videoRef} onPlay={handlePlay} />
|
|
|
|
<div className="container mx-auto px-4 py-6 max-w-6xl">
|
|
{/* Video Player */}
|
|
<VideoPlayer
|
|
videoRef={videoRef}
|
|
currentTime={currentTime}
|
|
duration={duration}
|
|
isPlaying={isPlaying}
|
|
isMuted={isMuted}
|
|
onPlayPause={handlePlay}
|
|
onSeek={handleMobileSafeSeek}
|
|
onToggleMute={toggleMute}
|
|
/>
|
|
|
|
{/* Editing Tools */}
|
|
<EditingTools
|
|
onSplit={handleSplit}
|
|
onReset={handleReset}
|
|
onUndo={handleUndo}
|
|
onRedo={handleRedo}
|
|
onPlaySegments={handlePlaySegments}
|
|
onPlay={handlePlay}
|
|
isPlaying={isPlaying}
|
|
isPlayingSegments={isPlayingSegments}
|
|
canUndo={historyPosition > 0}
|
|
canRedo={historyPosition < history.length - 1}
|
|
/>
|
|
|
|
{/* Timeline Controls */}
|
|
<TimelineControls
|
|
currentTime={currentTime}
|
|
duration={duration}
|
|
thumbnails={thumbnails}
|
|
trimStart={trimStart}
|
|
trimEnd={trimEnd}
|
|
splitPoints={splitPoints}
|
|
zoomLevel={zoomLevel}
|
|
clipSegments={clipSegments}
|
|
onTrimStartChange={handleTrimStartChange}
|
|
onTrimEndChange={handleTrimEndChange}
|
|
onZoomChange={handleZoomChange}
|
|
onSeek={handleMobileSafeSeek}
|
|
videoRef={videoRef}
|
|
onSave={handleSave}
|
|
onSaveACopy={handleSaveACopy}
|
|
onSaveSegments={handleSaveSegments}
|
|
hasUnsavedChanges={hasUnsavedChanges}
|
|
isIOSUninitialized={isMobile && !videoInitialized}
|
|
isPlaying={isPlaying}
|
|
setIsPlaying={setIsPlaying}
|
|
onPlayPause={handlePlay}
|
|
isPlayingSegments={isPlayingSegments}
|
|
/>
|
|
|
|
{/* Clip Segments */}
|
|
<ClipSegments segments={clipSegments} />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|