fix: Chapters: Play should not stop at the end of a chapter or a cutaway area, but should just continue play through.

This commit is contained in:
Yiannis Christodoulou 2025-10-13 01:35:12 +03:00
parent ee7fb7950c
commit f67021b17b
2 changed files with 15 additions and 546 deletions

View File

@ -39,8 +39,6 @@ const App = () => {
isMobile,
videoInitialized,
setVideoInitialized,
isPlayingSegments,
handlePlaySegments,
} = useVideoChapters();
const handlePlay = () => {
@ -52,156 +50,17 @@ const App = () => {
if (isPlaying) {
video.pause();
setIsPlaying(false);
logger.debug('Video paused');
return;
}
const currentPosition = Number(video.currentTime.toFixed(6));
// 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
// Start playing - no boundary checking, play through entire timeline
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',
});
logger.debug('Continuous playback started from:', formatDetailedTime(video.currentTime));
})
.catch((err) => {
console.error('Error playing video:', err);
@ -231,10 +90,8 @@ const App = () => {
onReset={handleReset}
onUndo={handleUndo}
onRedo={handleRedo}
onPlaySegments={handlePlaySegments}
onPlay={handlePlay}
isPlaying={isPlaying}
isPlayingSegments={isPlayingSegments}
canUndo={historyPosition > 0}
canRedo={historyPosition < history.length - 1}
/>
@ -243,6 +100,7 @@ const App = () => {
<TimelineControls
currentTime={currentTime}
duration={duration}
thumbnails={[]}
trimStart={trimStart}
trimEnd={trimEnd}
splitPoints={splitPoints}
@ -262,7 +120,6 @@ const App = () => {
isPlaying={isPlaying}
setIsPlaying={setIsPlaying}
onPlayPause={handlePlay}
isPlayingSegments={isPlayingSegments}
/>
{/* Clip Segments */}

View File

@ -561,111 +561,11 @@ const TimelineControls = ({
}
}, [currentTime, zoomLevel, duration, selectedSegmentId, showEmptySpaceTooltip, currentTimePercent]);
// Effect to check active segment boundaries during playback
// Effect to check active segment boundaries during playback - DISABLED for continuous playback
useEffect(() => {
// Skip if no video or no active segment
const video = videoRef.current;
if (!video || !activeSegment || !isPlayingSegment) {
// Log why we're skipping
if (!video) logger.debug('Skipping segment boundary check - no video element');
else if (!activeSegment) logger.debug('Skipping segment boundary check - no active segment');
else if (!isPlayingSegment) logger.debug('Skipping segment boundary check - not in segment playback mode');
// Boundary checking disabled - allow continuous playback through all segments
logger.debug('Segment boundary checking disabled - continuous playback enabled');
return;
}
// Skip boundary checking when playing all segments
if (isPlayingSegments) {
logger.debug('Skipping segment boundary check during segments playback');
return;
}
logger.debug(
'Segment boundary check ACTIVATED for segment:',
activeSegment.id,
'Start:',
formatDetailedTime(activeSegment.startTime),
'End:',
formatDetailedTime(activeSegment.endTime)
);
const handleTimeUpdate = () => {
const timeLeft = activeSegment.endTime - video.currentTime;
// Log every second to show we're actually checking
if (Math.round(timeLeft * 10) % 10 === 0) {
logger.debug(
'Segment playback - time remaining:',
formatDetailedTime(timeLeft),
'Current:',
formatDetailedTime(video.currentTime),
'End:',
formatDetailedTime(activeSegment.endTime),
'ContinuePastBoundary:',
continuePastBoundary
);
}
// If we've already passed the segment end, stop immediately
if (video.currentTime > activeSegment.endTime) {
video.pause();
video.currentTime = activeSegment.endTime;
setIsPlayingSegment(false);
// Reset continuePastBoundary when stopping at boundary
setContinuePastBoundary(false);
logger.debug(
'Passed segment end - setting back to exact boundary:',
formatDetailedTime(activeSegment.endTime)
);
return;
}
// If we've reached very close to the end of the active segment
// Use a small tolerance to ensure we stop as close as possible to boundary
// But not exactly at the boundary to avoid rounding errors
if (activeSegment.endTime - video.currentTime < 0.05) {
if (!continuePastBoundary) {
// Pause playback and set the time exactly at the end boundary
video.pause();
video.currentTime = activeSegment.endTime;
setIsPlayingSegment(false);
logger.debug('Paused at segment end boundary:', formatDetailedTime(activeSegment.endTime));
// Look for the next segment after this one (for potential continuation)
const sortedSegments = [...clipSegments].sort((a, b) => a.startTime - b.startTime);
const nextSegment = sortedSegments.find((seg) => seg.startTime > activeSegment.endTime);
// If there's a next segment immediately after this one, update the tooltip to show that segment
if (nextSegment && Math.abs(nextSegment.startTime - activeSegment.endTime) < 0.1) {
logger.debug('Found adjacent next segment:', nextSegment.id);
setSelectedSegmentId(nextSegment.id);
setActiveSegment(nextSegment);
setDisplayTime(nextSegment.startTime);
setClickedTime(nextSegment.startTime);
video.currentTime = nextSegment.startTime;
}
} else {
// We're continuing past the boundary
logger.debug('Continuing past segment boundary:', formatDetailedTime(activeSegment.endTime));
// Reset the flag after we've passed the boundary to ensure we stop at the next boundary
if (video.currentTime > activeSegment.endTime) {
setContinuePastBoundary(false);
logger.debug('Past segment boundary - resetting continuePastBoundary flag');
// Remove the active segment to avoid boundary checking until next segment is activated
setActiveSegment(null);
sessionStorage.removeItem('continuingPastSegment');
}
}
}
};
// Add event listener for timeupdate to check segment boundaries
video.addEventListener('timeupdate', handleTimeUpdate);
return () => {
video.removeEventListener('timeupdate', handleTimeUpdate);
logger.debug('Segment boundary check DEACTIVATED');
};
}, [activeSegment, isPlayingSegment, continuePastBoundary, clipSegments]);
// Update display time and check for transitions between segments and empty spaces
@ -2140,129 +2040,15 @@ const TimelineControls = ({
if (!video) return;
const handlePlay = () => {
if (!videoRef.current) return;
const video = videoRef.current;
const currentPosition = video.currentTime;
// Reset continuePastBoundary flag when starting new playback
setContinuePastBoundary(false);
// Find the next stopping point based on current position
let stopTime = duration;
let currentSegment = null;
let nextSegment = null;
// First, check if we're inside a segment with high precision
currentSegment = clipSegments.find((seg) => {
const isWithinSegment = currentPosition >= seg.startTime && currentPosition <= seg.endTime;
const isAtExactStart = Math.abs(currentPosition - seg.startTime) < 0.001; // Within 1ms of start
const isAtExactEnd = Math.abs(currentPosition - seg.endTime) < 0.001; // Within 1ms of end
return isWithinSegment || isAtExactStart || isAtExactEnd;
});
// Find the next segment with high precision
nextSegment = clipSegments
.filter((seg) => {
const isAfterCurrent = seg.startTime > currentPosition;
const isNotAtExactPosition = Math.abs(seg.startTime - currentPosition) > 0.001;
return isAfterCurrent && isNotAtExactPosition;
})
.sort((a, b) => a.startTime - b.startTime)[0];
// Determine where to stop based on position
if (currentSegment) {
// If we're in a segment, stop at its end
stopTime = currentSegment.endTime;
setActiveSegment(currentSegment);
} else if (nextSegment) {
// If we're in a cutaway and there's a next segment, stop at its start
stopTime = nextSegment.startTime;
// Don't set active segment since we're in a cutaway
}
// Create a boundary checker function with high precision
const checkBoundary = () => {
if (!video) return;
const currentPosition = video.currentTime;
const timeLeft = stopTime - currentPosition;
// If we're approaching the boundary (within 1ms) or have passed it
if (timeLeft <= 0.001 || 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;
onSeek(stopTime);
setDisplayTime(stopTime);
setClickedTime(stopTime);
logger.debug('Position verification:', {
target: formatDetailedTime(stopTime),
actual: formatDetailedTime(video.currentTime),
difference: Math.abs(video.currentTime - stopTime).toFixed(3),
});
};
// Multiple attempts to ensure precision
setExactPosition();
setTimeout(setExactPosition, 10);
setTimeout(setExactPosition, 20);
setTimeout(setExactPosition, 50);
// Update UI based on where we stopped
if (currentSegment) {
setSelectedSegmentId(currentSegment.id);
setShowEmptySpaceTooltip(false);
} else if (nextSegment) {
setSelectedSegmentId(nextSegment.id);
setShowEmptySpaceTooltip(false);
setActiveSegment(nextSegment);
} else {
setSelectedSegmentId(null);
setShowEmptySpaceTooltip(true);
setActiveSegment(null);
}
// Remove our boundary checker
video.removeEventListener('timeupdate', checkBoundary);
setIsPlaying(false);
setIsPlayingSegment(false);
// Reset continuePastBoundary flag when stopping at boundary
setContinuePastBoundary(false);
return;
}
};
// Start our boundary checker
video.addEventListener('timeupdate', checkBoundary);
// Start playing
video
.play()
.then(() => {
// Simple play handler - just update UI state, no boundary checking
setIsPlaying(true);
setIsPlayingSegment(true);
logger.debug('Playback started:', {
from: formatDetailedTime(currentPosition),
to: formatDetailedTime(stopTime),
currentSegment: currentSegment ? `Segment ${currentSegment.id}` : 'None',
nextSegment: nextSegment ? `Segment ${nextSegment.id}` : 'None',
});
})
.catch((err) => {
console.error('Error playing video:', err);
});
logger.debug('Continuous playback started from TimelineControls');
};
const handlePause = () => {
logger.debug('Video paused from external control');
setIsPlaying(false);
setIsPlayingSegment(false);
};
@ -2273,7 +2059,7 @@ const TimelineControls = ({
video.removeEventListener('play', handlePlay);
video.removeEventListener('pause', handlePause);
};
}, [clipSegments, duration, onSeek]);
}, []);
// Handle mouse movement over timeline to remember position
const handleTimelineMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
@ -3281,181 +3067,7 @@ const TimelineControls = ({
setActiveSegment(cutawaySegment);
}, 0);
// Add a manual boundary check specifically for cutaway playback
// This ensures we detect when we reach the next segment's start
const checkCutawayBoundary = () => {
if (!videoRef.current) return;
// Check if we've entered a segment (i.e., reached a boundary)
const currentPosition = videoRef.current.currentTime;
const segments = [...clipSegments].sort(
(a, b) => a.startTime - b.startTime
);
// Find the next segment we're approaching - use a wider detection range
// to catch the boundary earlier
const nextSegment = segments.find(
(seg) => seg.startTime > currentPosition - 0.3
);
// We need to detect boundaries much earlier to allow for time to react
// This is a key fix - we need to detect the boundary BEFORE we reach it
// But don't stop if we're in continuePastBoundary mode
const shouldStop =
nextSegment &&
currentPosition >= nextSegment.startTime - 0.25 &&
currentPosition <= nextSegment.startTime + 0.1 &&
!continuePastBoundary;
// Add logging to show boundary check decisions
if (
nextSegment &&
currentPosition >= nextSegment.startTime - 0.25 &&
currentPosition <= nextSegment.startTime + 0.1
) {
logger.debug(
`Approaching boundary at ${formatDetailedTime(
nextSegment.startTime
)}, continuePastBoundary=${continuePastBoundary}, willStop=${shouldStop}`
);
}
// Also check if we've entered a different segment - we need to detect this too
const segmentAtCurrentTime = segments.find(
(seg) =>
currentPosition >= seg.startTime &&
currentPosition <= seg.endTime
);
// If we've moved directly into a segment during playback, we need to update the active segment
if (
segmentAtCurrentTime &&
activeSegment?.id !== segmentAtCurrentTime.id
) {
logger.debug(
`Entered segment ${segmentAtCurrentTime.id} during cutaway playback`
);
setActiveSegment(segmentAtCurrentTime);
setSelectedSegmentId(segmentAtCurrentTime.id);
setShowEmptySpaceTooltip(false);
// Remove our boundary checker since we're now in a standard segment
videoRef.current.removeEventListener(
'timeupdate',
checkCutawayBoundary
);
// Reset continuation flags
setContinuePastBoundary(false);
sessionStorage.removeItem('continuingPastSegment');
return;
}
// If we've entered a segment, stop at its boundary
if (shouldStop && nextSegment) {
logger.debug(
`CUTAWAY MANUAL BOUNDARY CHECK: Current position ${formatDetailedTime(
currentPosition
)} approaching segment at ${formatDetailedTime(
nextSegment.startTime
)} (distance: ${Math.abs(
currentPosition - nextSegment.startTime
).toFixed(3)}s) - STOPPING`
);
videoRef.current.pause();
// Force exact time position with high precision and multiple attempts
setTimeout(() => {
if (videoRef.current) {
// First seek directly to exact start time, no offset
videoRef.current.currentTime = nextSegment.startTime;
// Update UI immediately to match video position
onSeek(nextSegment.startTime);
// Also update tooltip time displays
setDisplayTime(nextSegment.startTime);
setClickedTime(nextSegment.startTime);
// Reset continuePastBoundary when stopping at a boundary
setContinuePastBoundary(false);
// Update tooltip to show the segment at the boundary
setSelectedSegmentId(nextSegment.id);
setShowEmptySpaceTooltip(false);
setActiveSegment(nextSegment);
// Force multiple adjustments to ensure exact precision
const verifyPosition = () => {
if (videoRef.current) {
// Always force the exact time in every verification
videoRef.current.currentTime =
nextSegment.startTime;
// Make sure we update the UI to reflect the corrected position
onSeek(nextSegment.startTime);
// Update the displayTime and clickedTime state to match exact position
setDisplayTime(nextSegment.startTime);
setClickedTime(nextSegment.startTime);
logger.debug(
`Position corrected to exact segment boundary: ${formatDetailedTime(
videoRef.current.currentTime
)} (target: ${formatDetailedTime(nextSegment.startTime)})`
);
}
};
// Apply multiple correction attempts with increasing delays
setTimeout(verifyPosition, 10); // Immediate correction
setTimeout(verifyPosition, 20); // First correction
setTimeout(verifyPosition, 50); // Second correction
setTimeout(verifyPosition, 100); // Third correction
setTimeout(verifyPosition, 200); // Final correction
// Also add event listeners to ensure position is corrected whenever video state changes
videoRef.current.addEventListener('seeked', verifyPosition);
videoRef.current.addEventListener(
'canplay',
verifyPosition
);
videoRef.current.addEventListener(
'waiting',
verifyPosition
);
// Remove these event listeners after a short time
setTimeout(() => {
if (videoRef.current) {
videoRef.current.removeEventListener(
'seeked',
verifyPosition
);
videoRef.current.removeEventListener(
'canplay',
verifyPosition
);
videoRef.current.removeEventListener(
'waiting',
verifyPosition
);
}
}, 300);
}
}, 10);
setIsPlayingSegment(false);
setActiveSegment(null);
// Remove our boundary checker
videoRef.current.removeEventListener(
'timeupdate',
checkCutawayBoundary
);
return;
}
};
// Start our manual boundary checker
videoRef.current.addEventListener('timeupdate', checkCutawayBoundary);
// No boundary checking - allow continuous playback
// Start playing with proper promise handling - use setTimeout to ensure
// that our activeSegment setting has had time to take effect