From ac7fb13487d6aae3997b6d81aff73ebab2d9200e Mon Sep 17 00:00:00 2001 From: "Bernise.john" Date: Fri, 9 Jan 2026 17:25:02 +0530 Subject: [PATCH 1/7] added seekbar --- .../EditorPage/VideoEditor/VideoRecorder.js | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js index ceaf2bb0..8f224bad 100644 --- a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js +++ b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js @@ -115,6 +115,10 @@ const VideoRecorder = ({ const [showCameraMenu, setShowCameraMenu] = useState(false); const cameraMenuRef = useRef(null); + const seekBarRef = useRef(null); + const [hoverTime, setHoverTime] = useState(null); + const [showHoverTime, setShowHoverTime] = useState(false); + const fs = window.require('fs'); const path = window.require('path'); @@ -393,6 +397,13 @@ const VideoRecorder = ({ }; }, [isPlaying, currentMode]); + useEffect(() => { + if (!seekBarRef.current || !videoDuration) { return; } + + const percent = (playbackTime / videoDuration) * 100; + seekBarRef.current.style.setProperty('--progress', `${percent}%`); + }, [playbackTime, videoDuration]); + const saveVideo = useCallback(async (blob) => { setIsProcessing(true); @@ -662,6 +673,26 @@ const VideoRecorder = ({ } }; + const handleSeekHover = (e) => { + if (!seekBarRef.current || !videoDuration) { return; } + + const rect = seekBarRef.current.getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + + const time = Math.max( + 0, + Math.min(videoDuration, percent * videoDuration), + ); + + setHoverTime(time); + setShowHoverTime(true); + }; + + const clearSeekHover = () => { + setShowHoverTime(false); + setHoverTime(null); + }; + const formatTime = (seconds) => { if (!Number.isFinite(seconds) || seconds <= 0) { return '00:00'; } const mins = Math.floor(seconds / 60); @@ -775,6 +806,41 @@ const VideoRecorder = ({ )} + + {currentMode === 'view' && hasVideo && ( +
+ { + const time = Number(e.target.value); + if (videoPreviewRef.current) { + videoPreviewRef.current.currentTime = time; + setPlaybackTime(time); + } + }} + onMouseMove={handleSeekHover} + onMouseLeave={clearSeekHover} + className="w-full accent-primary cursor-pointer" + /> + + {showHoverTime && hoverTime !== null && ( +
+ {formatTime(hoverTime)} +
+ )} +
+ )} +
From d55d8bf4cf380e20c66b272621e106fec9804f26 Mon Sep 17 00:00:00 2001 From: "Bernise.john" Date: Mon, 12 Jan 2026 12:15:41 +0530 Subject: [PATCH 2/7] Added playback speed to video player --- .../EditorPage/VideoEditor/VideoRecorder.js | 62 +++++++++++++++++-- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js index 8f224bad..49763077 100644 --- a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js +++ b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js @@ -118,6 +118,7 @@ const VideoRecorder = ({ const seekBarRef = useRef(null); const [hoverTime, setHoverTime] = useState(null); const [showHoverTime, setShowHoverTime] = useState(false); + const [playbackSpeed, setPlaybackSpeed] = useState(1); const fs = window.require('fs'); const path = window.require('path'); @@ -397,6 +398,22 @@ const VideoRecorder = ({ }; }, [isPlaying, currentMode]); + useEffect(() => { + const handleClickOutside = (event) => { + if (cameraMenuRef.current && !cameraMenuRef.current.contains(event.target)) { + setShowCameraMenu(false); + } + }; + + if (showCameraMenu) { + document.addEventListener('mousedown', handleClickOutside); + } + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [showCameraMenu]); + useEffect(() => { if (!seekBarRef.current || !videoDuration) { return; } @@ -404,6 +421,12 @@ const VideoRecorder = ({ seekBarRef.current.style.setProperty('--progress', `${percent}%`); }, [playbackTime, videoDuration]); + useEffect(() => { + if (videoPreviewRef.current && currentMode === 'view') { + videoPreviewRef.current.playbackRate = playbackSpeed; + } + }, [playbackSpeed, currentMode]); + const saveVideo = useCallback(async (blob) => { setIsProcessing(true); @@ -959,23 +982,22 @@ const VideoRecorder = ({ -
- Camera Settings +
+ {currentMode === 'view' ? 'Settings' : 'Camera Settings'}
- {showCameraMenu && ( + {showCameraMenu && currentMode === 'record' && (

Select Camera

- {' '} {videoDevices.map((device) => ( + ))} +
+ )}
From 6004882e62a2e219fe24bcc990afb84026cfd4a9 Mon Sep 17 00:00:00 2001 From: "Bernise.john" Date: Tue, 13 Jan 2026 10:49:14 +0530 Subject: [PATCH 3/7] Added Fullscreen toggle --- .../EditorPage/VideoEditor/VideoRecorder.js | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js index 49763077..c639493f 100644 --- a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js +++ b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js @@ -14,6 +14,8 @@ import { ExclamationCircleIcon, TrashIcon, Cog6ToothIcon, + ArrowsPointingOutIcon, + ArrowsPointingInIcon, } from '@heroicons/react/24/outline'; import * as logger from '../../../logger'; @@ -119,6 +121,8 @@ const VideoRecorder = ({ const [hoverTime, setHoverTime] = useState(null); const [showHoverTime, setShowHoverTime] = useState(false); const [playbackSpeed, setPlaybackSpeed] = useState(1); + const [isFullscreen, setIsFullscreen] = useState(false); + const containerRef = useRef(null); const fs = window.require('fs'); const path = window.require('path'); @@ -723,6 +727,24 @@ const VideoRecorder = ({ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; + const toggleFullscreen = useCallback(() => { + setIsFullscreen(!isFullscreen); + }, [isFullscreen]); + + useEffect(() => { + const handleKeyDown = (e) => { + if (e.key === 'Escape' && isFullscreen) { + setIsFullscreen(false); + } else if (e.key === 'f' && !e.target.matches('input, textarea')) { + e.preventDefault(); + toggleFullscreen(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isFullscreen, toggleFullscreen]); + const hasPreviousVerse = () => getPreviousVerseNumber(verse, content) !== null; const hasNextVerse = () => getNextVerseNumber(verse, content) !== null; @@ -732,8 +754,14 @@ const VideoRecorder = ({ className={`fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-[50] p-4 ${!isVisible ? 'hidden' : '' }`} > -
-
+
+
@@ -760,6 +788,7 @@ const VideoRecorder = ({
+ +
-
+
{error && (
@@ -781,7 +811,12 @@ const VideoRecorder = ({
)} -
+
+ {' '} + {' '} + {currentMode === 'record' && !cameraReady && !error && (
@@ -831,7 +879,7 @@ const VideoRecorder = ({
{currentMode === 'view' && hasVideo && ( -
+
)} -
+
From c9eb0c1d1256ff5da23d117e504044726d51d2ff Mon Sep 17 00:00:00 2001 From: "Bernise.john" Date: Tue, 13 Jan 2026 15:23:43 +0530 Subject: [PATCH 4/7] Added thmbnail preview for seekbar --- .../EditorPage/VideoEditor/VideoPlayer.js | 3 - .../EditorPage/VideoEditor/VideoRecorder.js | 186 +++++++++++++----- 2 files changed, 137 insertions(+), 52 deletions(-) diff --git a/renderer/src/components/EditorPage/VideoEditor/VideoPlayer.js b/renderer/src/components/EditorPage/VideoEditor/VideoPlayer.js index 62129694..c9f0cd55 100644 --- a/renderer/src/components/EditorPage/VideoEditor/VideoPlayer.js +++ b/renderer/src/components/EditorPage/VideoEditor/VideoPlayer.js @@ -37,7 +37,6 @@ const VideoPlayer = ({ const [showVideoRecorder, setShowVideoRecorder] = useState(false); const [currentRecordingVerse, setCurrentRecordingVerse] = useState(null); const [recorderMode, setRecorderMode] = useState('record'); - const [isRecorderVisible, setIsRecorderVisible] = useState(true); const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, @@ -300,11 +299,9 @@ const VideoPlayer = ({ onClose={() => { setShowVideoRecorder(false); setCurrentRecordingVerse(null); - setIsRecorderVisible; }} onVerseChange={handleVerseChangeInRecorder} setOpenModal={setOpenModal} - isVisible={isRecorderVisible} setNotify={setNotify} setSnackText={setSnackText} setOpenSnackBar={setOpenSnackBar} diff --git a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js index c639493f..d9692c2f 100644 --- a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js +++ b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js @@ -17,6 +17,7 @@ import { ArrowsPointingOutIcon, ArrowsPointingInIcon, } from '@heroicons/react/24/outline'; +import { debounce } from 'lodash'; import * as logger from '../../../logger'; const getNextVerseNumber = (currentVerse, content) => { @@ -118,11 +119,14 @@ const VideoRecorder = ({ const cameraMenuRef = useRef(null); const seekBarRef = useRef(null); + const containerRef = useRef(null); + const thumbnailVideoRef = useRef(null); + const thumbnailCanvasRef = useRef(null); const [hoverTime, setHoverTime] = useState(null); - const [showHoverTime, setShowHoverTime] = useState(false); const [playbackSpeed, setPlaybackSpeed] = useState(1); + const [showHoverTime, setShowHoverTime] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); - const containerRef = useRef(null); + const [thumbnailData, setThumbnailData] = useState(null); const fs = window.require('fs'); const path = window.require('path'); @@ -133,8 +137,6 @@ const VideoRecorder = ({ const hasVideo = fs.existsSync(filePath); useEffect(() => { - const fs = window.require('fs'); - const path = window.require('path'); const filename = `${chapter}_${verse}.webm`; const filePath = path.join(projectPath, filename); @@ -353,21 +355,6 @@ const VideoRecorder = ({ }, 50); } }, [currentMode, hasVideo, verse, projectPath]); - useEffect(() => { - if (streamRef.current) { - streamRef.current.getTracks().forEach((t) => t.stop()); - streamRef.current = null; - } - - if (videoPreviewRef.current) { - videoPreviewRef.current.pause(); - videoPreviewRef.current.srcObject = null; - videoPreviewRef.current.removeAttribute('src'); - videoPreviewRef.current.load(); - } - - setCameraReady(false); - }, [verse, chapter]); useEffect(() => { if (currentMode === 'view' && videoPreviewRef.current) { @@ -402,22 +389,6 @@ const VideoRecorder = ({ }; }, [isPlaying, currentMode]); - useEffect(() => { - const handleClickOutside = (event) => { - if (cameraMenuRef.current && !cameraMenuRef.current.contains(event.target)) { - setShowCameraMenu(false); - } - }; - - if (showCameraMenu) { - document.addEventListener('mousedown', handleClickOutside); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [showCameraMenu]); - useEffect(() => { if (!seekBarRef.current || !videoDuration) { return; } @@ -431,13 +402,44 @@ const VideoRecorder = ({ } }, [playbackSpeed, currentMode]); + useEffect(() => () => { + if (videoPreviewRef.current) { + videoPreviewRef.current.pause(); + videoPreviewRef.current.srcObject = null; + videoPreviewRef.current.src = ''; + videoPreviewRef.current.load(); + } + if (streamRef.current) { + streamRef.current.getTracks().forEach((track) => { + track.stop(); + track.enabled = false; + }); + streamRef.current = null; + } + + if (timerRef.current) { + clearInterval(timerRef.current); + timerRef.current = null; + } + if (playbackTimerRef.current) { + clearInterval(playbackTimerRef.current); + playbackTimerRef.current = null; + } + + if (thumbnailCanvasRef.current) { + const ctx = thumbnailCanvasRef.current.getContext('2d'); + ctx?.clearRect(0, 0, thumbnailCanvasRef.current.width, thumbnailCanvasRef.current.height); + } + + if (videoPreviewRef.current?.src?.startsWith('blob:')) { + URL.revokeObjectURL(videoPreviewRef.current.src); + } + }, []); + const saveVideo = useCallback(async (blob) => { setIsProcessing(true); try { - const fs = window.require('fs'); - const path = window.require('path'); - const arrayBuffer = await blob.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); @@ -536,8 +538,6 @@ const VideoRecorder = ({ const startNewRecording = useCallback(() => { try { - const fs = window.require('fs'); - const path = window.require('path'); const filename = `${chapter}_${verse}.webm`; const filePath = path.join(projectPath, filename); @@ -700,24 +700,80 @@ const VideoRecorder = ({ } }; + useEffect(() => { + if (currentMode === 'view' && hasVideo && !thumbnailVideoRef.current) { + thumbnailVideoRef.current = document.createElement('video'); + thumbnailVideoRef.current.muted = true; + thumbnailVideoRef.current.preload = 'metadata'; + thumbnailVideoRef.current.style.display = 'none'; + document.body.appendChild(thumbnailVideoRef.current); + } + + return () => { + if (thumbnailVideoRef.current) { + thumbnailVideoRef.current.remove(); + thumbnailVideoRef.current = null; + } + }; + }, [currentMode, hasVideo]); + + const generateThumbnail = useCallback((time) => { + if (!videoPreviewRef.current || !thumbnailVideoRef.current) { return; } + + const thumbnailVideo = thumbnailVideoRef.current; + + if (thumbnailVideo.src !== videoPreviewRef.current.src) { + thumbnailVideo.src = videoPreviewRef.current.src; + } + + if (!thumbnailCanvasRef.current) { + thumbnailCanvasRef.current = document.createElement('canvas'); + thumbnailCanvasRef.current.width = 160; + thumbnailCanvasRef.current.height = 90; + } + + const canvas = thumbnailCanvasRef.current; + const ctx = canvas.getContext('2d', { willReadFrequently: true }); + + thumbnailVideo.currentTime = time; + + const drawFrame = () => { + try { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(thumbnailVideo, 0, 0, canvas.width, canvas.height); + const dataUrl = canvas.toDataURL('image/jpeg', 0.6); + setThumbnailData(dataUrl); + } catch (err) { + logger.error('Error generating thumbnail:', err); + } + }; + + thumbnailVideo.onseeked = drawFrame; + }, []); + + const debouncedGenerateThumbnail = useCallback( + debounce((time) => { + if (videoPreviewRef.current && currentMode === 'view') { + generateThumbnail(time); + } + }, 100), + [generateThumbnail, currentMode], + ); const handleSeekHover = (e) => { if (!seekBarRef.current || !videoDuration) { return; } const rect = seekBarRef.current.getBoundingClientRect(); const percent = (e.clientX - rect.left) / rect.width; - - const time = Math.max( - 0, - Math.min(videoDuration, percent * videoDuration), - ); + const time = Math.max(0, Math.min(videoDuration, percent * videoDuration)); setHoverTime(time); setShowHoverTime(true); + debouncedGenerateThumbnail(time); }; - const clearSeekHover = () => { setShowHoverTime(false); setHoverTime(null); + setThumbnailData(null); }; const formatTime = (seconds) => { @@ -901,12 +957,44 @@ const VideoRecorder = ({ {showHoverTime && hoverTime !== null && (
{ + const thumbnailWidth = 160; + const rect = seekBarRef.current?.getBoundingClientRect(); + if (!rect) { return '0px'; } + + const position = (hoverTime / videoDuration) * 100; + const pixelPosition = (position / 100) * rect.width; + const halfThumb = thumbnailWidth / 2; + + const clampedPosition = Math.max( + halfThumb, + Math.min(rect.width - halfThumb, pixelPosition), + ); + + return `${clampedPosition}px`; + })(), + transform: 'translateX(-50%)', + bottom: '100%', + marginBottom: '0.5rem', }} > - {formatTime(hoverTime)} +
+ {thumbnailData && ( +
+ Video preview +
+ )} +
+ {formatTime(hoverTime)} +
+
)}
From 6102c56f0cc85d95ce2114893b5536ac3ff85b0c Mon Sep 17 00:00:00 2001 From: "Bernise.john" Date: Fri, 23 Jan 2026 11:17:31 +0530 Subject: [PATCH 5/7] fixed ui chnages --- .../EditorPage/VideoEditor/VideoRecorder.js | 323 ++++++++++-------- 1 file changed, 175 insertions(+), 148 deletions(-) diff --git a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js index d9692c2f..5f600cb9 100644 --- a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js +++ b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js @@ -127,6 +127,7 @@ const VideoRecorder = ({ const [showHoverTime, setShowHoverTime] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const [thumbnailData, setThumbnailData] = useState(null); + const errorTimeoutRef = useRef(null); const fs = window.require('fs'); const path = window.require('path'); @@ -136,6 +137,24 @@ const VideoRecorder = ({ const hasVideo = fs.existsSync(filePath); + const clearError = useCallback(() => { + setError(null); + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + errorTimeoutRef.current = null; + } + }, []); + + const setErrorWithTimeout = useCallback((message, timeout = 5000) => { + setError(message); + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + } + errorTimeoutRef.current = setTimeout(() => { + setError(null); + }, timeout); + }, []); + useEffect(() => { const filename = `${chapter}_${verse}.webm`; const filePath = path.join(projectPath, filename); @@ -231,7 +250,7 @@ const VideoRecorder = ({ } catch (err) { if (mounted) { logger.error('Camera initialization error:', err); - setError(getCameraErrorMessage(err)); + setErrorWithTimeout(getCameraErrorMessage(err)); if (err.name === 'OverconstrainedError') { try { @@ -247,7 +266,7 @@ const VideoRecorder = ({ if (mounted) { videoPreviewRef.current.play(); setCameraReady(true); - setError(null); + clearError(null); } }; } @@ -269,8 +288,11 @@ const VideoRecorder = ({ if (timerRef.current) { clearInterval(timerRef.current); } + if (errorTimeoutRef.current) { + clearTimeout(errorTimeoutRef.current); + } }; - }, [isAudioEnabled, currentMode, selectedCamera, verse, chapter]); + }, [isAudioEnabled, currentMode, selectedCamera, verse, chapter, setErrorWithTimeout, clearError]); useEffect(() => { if (currentMode === 'view' && hasVideo && videoPreviewRef.current) { @@ -283,7 +305,7 @@ const VideoRecorder = ({ try { if (!fs.existsSync(fullPath)) { logger.error('Video file does not exist:', fullPath); - setError(`Video file not found: ${filename}`); + setErrorWithTimeout(`Video file not found: ${filename}`); return; } @@ -291,12 +313,12 @@ const VideoRecorder = ({ logger.debug('Video file size:', stats.size); if (stats.size === 0) { - setError('Video file is empty'); + setErrorWithTimeout('Video file is empty'); return; } } catch (err) { logger.error('Error checking video file:', err); - setError(`Cannot access video: ${err.message}`); + setErrorWithTimeout(`Cannot access video: ${err.message}`); return; } @@ -865,6 +887,14 @@ const VideoRecorder = ({

Error

{error}

+
)}
{' '} - {currentMode === 'record' && !cameraReady && !error && (
@@ -1001,9 +1019,29 @@ const VideoRecorder = ({ )}
-
-
-
+
+
+ {currentMode === 'view' && hasVideo && ( +
+ Speed + +
+ )} +
+ +
-
- - {currentMode === 'record' ? ( +
- ) : ( - - )} - {isRecording && ( + {currentMode === 'record' ? ( + + ) : ( + + )} + + {isRecording && ( + + )} + - )} - - - -
+
-
+
-
- {currentMode === 'view' ? 'Settings' : 'Camera Settings'} -
- {showCameraMenu && currentMode === 'record' && ( -
-
- -

Select Camera

+
+ + {currentMode === 'record' && ( +
+ Camera Settings
- {videoDevices.map((device) => ( - - ))} -
- )} + )} - {showCameraMenu && currentMode === 'view' && ( -
-
- -

Playback Speed

+ {showCameraMenu && currentMode === 'record' && ( +
+
+ +

Select Camera

+
+ {videoDevices.map((device) => ( + + ))}
- {[0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2].map((speed) => ( - - ))} -
- )} + )} +
From a05af0b2d8f78083bcba01540441ec1ca9e6dc61 Mon Sep 17 00:00:00 2001 From: "Bernise.john" Date: Wed, 28 Jan 2026 12:47:52 +0530 Subject: [PATCH 6/7] Code refactored --- .../EditorPage/VideoEditor/VideoEditor.js | 2 +- .../EditorPage/VideoEditor/VideoRecorder.js | 833 +++--------------- .../src/components/hooks/video/useCamera.js | 191 ++++ .../components/hooks/video/useCameraMenu.js | 28 + .../components/hooks/video/useErrorHandler.js | 38 + .../components/hooks/video/useFullscreen.js | 28 + .../hooks/{ => video}/useVerseJoining.js | 2 +- .../hooks/video/useVideoPlayback.js | 185 ++++ .../hooks/video/useVideoRecording.js | 163 ++++ .../hooks/video/useVideoThumbnail.js | 107 +++ 10 files changed, 862 insertions(+), 715 deletions(-) create mode 100644 renderer/src/components/hooks/video/useCamera.js create mode 100644 renderer/src/components/hooks/video/useCameraMenu.js create mode 100644 renderer/src/components/hooks/video/useErrorHandler.js create mode 100644 renderer/src/components/hooks/video/useFullscreen.js rename renderer/src/components/hooks/{ => video}/useVerseJoining.js (99%) create mode 100644 renderer/src/components/hooks/video/useVideoPlayback.js create mode 100644 renderer/src/components/hooks/video/useVideoRecording.js create mode 100644 renderer/src/components/hooks/video/useVideoThumbnail.js diff --git a/renderer/src/components/EditorPage/VideoEditor/VideoEditor.js b/renderer/src/components/EditorPage/VideoEditor/VideoEditor.js index 9587791c..e4a748e1 100644 --- a/renderer/src/components/EditorPage/VideoEditor/VideoEditor.js +++ b/renderer/src/components/EditorPage/VideoEditor/VideoEditor.js @@ -5,7 +5,7 @@ import { SnackBar } from '@/components/SnackBar'; import { readFile } from '@/core/editor/readFile'; import { isElectron } from '@/core/handleElectron'; import { useState, useEffect, useContext } from 'react'; -import { useVerseJoining } from '@/hooks/useVerseJoining'; +import { useVerseJoining } from '@/hooks/video/useVerseJoining'; import EmptyScreen from '@/components/Loading/EmptySrceen'; import { readRefMeta } from '@/core/reference/readRefMeta'; import LoadingScreen from '@/components/Loading/LoadingScreen'; diff --git a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js index 5f600cb9..c04d48df 100644 --- a/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js +++ b/renderer/src/components/EditorPage/VideoEditor/VideoRecorder.js @@ -1,5 +1,5 @@ import React, { - useState, useRef, useEffect, useCallback, + useState, useRef, useEffect, } from 'react'; import PropTypes from 'prop-types'; import { @@ -17,8 +17,14 @@ import { ArrowsPointingOutIcon, ArrowsPointingInIcon, } from '@heroicons/react/24/outline'; -import { debounce } from 'lodash'; -import * as logger from '../../../logger'; + +import { useCamera } from '@/hooks/video/useCamera'; +import { useCameraMenu } from '@/hooks/video/useCameraMenu'; +import { useFullscreen } from '@/hooks/video/useFullscreen'; +import { useErrorHandler } from '@/hooks/video/useErrorHandler'; +import { useVideoPlayback } from '@/hooks/video/useVideoPlayback'; +import { useVideoRecording } from '@/hooks/video/useVideoRecording'; +import { useVideoThumbnail } from '@/hooks/video/useVideoThumbnail'; const getNextVerseNumber = (currentVerse, content) => { const currentIndex = content.findIndex((v) => v.verseNumber === currentVerse); @@ -31,53 +37,6 @@ const getPreviousVerseNumber = (currentVerse, content) => { if (currentIndex === -1 || currentIndex === 0) { return null; } return content[currentIndex - 1].verseNumber; }; -const getCameraErrorMessage = (err) => { - if (!err) { return 'Unknown error'; } - - switch (err.name) { - case 'NotAllowedError': - return 'Camera permission denied. Please allow camera access.'; - case 'NotFoundError': - return 'No camera found. Please connect a camera.'; - case 'OverconstrainedError': - return 'Camera constraints not supported. Trying fallback...'; - default: - return 'Failed to access camera. Please check your camera connection.'; - } -}; - -const getPreferredCamera = async () => { - try { - const devices = await navigator.mediaDevices.enumerateDevices(); - const videoDevices = devices.filter((device) => device.kind === 'videoinput'); - - if (videoDevices.length === 0) { - throw new Error('No camera devices found'); - } - - const builtInKeywords = ['integrated', 'built-in', 'webcam', 'facetime']; - - const usbCamera = videoDevices.find((device) => device.label.toLowerCase().includes('usb')); - - if (usbCamera) { - return usbCamera.deviceId; - } - - const externalCamera = videoDevices.find((device) => { - const label = device.label.toLowerCase(); - return !builtInKeywords.some((keyword) => label.includes(keyword)); - }); - - if (externalCamera) { - return externalCamera.deviceId; - } - - return videoDevices[0].deviceId; - } catch (err) { - logger.error('Error enumerating devices:', err); - return null; - } -}; const VideoRecorder = ({ verse, @@ -95,39 +54,14 @@ const VideoRecorder = ({ setSnackText, setOpenSnackBar, }) => { - const [isRecording, setIsRecording] = useState(false); - const [isPaused, setIsPaused] = useState(false); const [isAudioEnabled, setIsAudioEnabled] = useState(true); - const [recordingTime, setRecordingTime] = useState(0); - const [playbackTime, setPlaybackTime] = useState(0); - const [videoDuration, setVideoDuration] = useState(0); - const [error, setError] = useState(null); - const [cameraReady, setCameraReady] = useState(false); - const [isProcessing, setIsProcessing] = useState(false); - const [isPlaying, setIsPlaying] = useState(false); + const [selectedCamera, setSelectedCamera] = useState(null); + const [existingVideo, setExistingVideo] = useState(false); const [currentMode, setCurrentMode] = useState(mode); const videoPreviewRef = useRef(null); - const mediaRecorderRef = useRef(null); - const streamRef = useRef(null); - const timerRef = useRef(null); - const playbackTimerRef = useRef(null); - const [existingVideo, setExistingVideo] = useState(false); - const [videoDevices, setVideoDevices] = useState([]); - const [selectedCamera, setSelectedCamera] = useState(null); - const [showCameraMenu, setShowCameraMenu] = useState(false); - const cameraMenuRef = useRef(null); - - const seekBarRef = useRef(null); const containerRef = useRef(null); - const thumbnailVideoRef = useRef(null); - const thumbnailCanvasRef = useRef(null); - const [hoverTime, setHoverTime] = useState(null); - const [playbackSpeed, setPlaybackSpeed] = useState(1); - const [showHoverTime, setShowHoverTime] = useState(false); - const [isFullscreen, setIsFullscreen] = useState(false); - const [thumbnailData, setThumbnailData] = useState(null); - const errorTimeoutRef = useRef(null); + const seekBarRef = useRef(null); const fs = window.require('fs'); const path = window.require('path'); @@ -137,293 +71,122 @@ const VideoRecorder = ({ const hasVideo = fs.existsSync(filePath); - const clearError = useCallback(() => { - setError(null); - if (errorTimeoutRef.current) { - clearTimeout(errorTimeoutRef.current); - errorTimeoutRef.current = null; - } - }, []); - - const setErrorWithTimeout = useCallback((message, timeout = 5000) => { - setError(message); - if (errorTimeoutRef.current) { - clearTimeout(errorTimeoutRef.current); - } - errorTimeoutRef.current = setTimeout(() => { - setError(null); - }, timeout); - }, []); - - useEffect(() => { - const filename = `${chapter}_${verse}.webm`; - const filePath = path.join(projectPath, filename); - - const videoExists = fs.existsSync(filePath); - setExistingVideo(videoExists); - - if (videoExists) { - setCurrentMode('view'); - } else { - setCurrentMode('record'); - } - }, [chapter, verse, projectPath, mode]); - - useEffect(() => { - const handleClickOutside = (event) => { - if (cameraMenuRef.current && !cameraMenuRef.current.contains(event.target)) { - setShowCameraMenu(false); - } - }; - - if (showCameraMenu) { - document.addEventListener('mousedown', handleClickOutside); + const { error, setError, clearError } = useErrorHandler(); + + const { + cameraReady, videoDevices, streamRef, toggleAudio, + } = useCamera({ + currentMode, + isAudioEnabled, + selectedCamera, + videoPreviewRef, + onError: setError, + clearError, + verse, + chapter, + }); + + const { + isPlaying, + playbackTime, + videoDuration, + playbackSpeed, + togglePlayPause, + seek, + cycleSpeed, + resetPlayback, + setIsPlaying, + } = useVideoPlayback({ + currentMode, + hasVideo, + verse, + chapter, + projectPath, + videoPreviewRef, + onError: setError, + }); + + const handleSaveComplete = ({ + verse, chapter, filePath, filename, buffer, + }) => { + if (onRecordingComplete) { + onRecordingComplete({ + verse, chapter, filePath, filename, + }); } - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [showCameraMenu]); - - useEffect(() => { - let mounted = true; - - const initCamera = async () => { - if (currentMode === 'view') { - if (streamRef.current) { - streamRef.current.getTracks().forEach((track) => track.stop()); - streamRef.current = null; - } - setCameraReady(false); - return; - } - - try { - const tempStream = await navigator.mediaDevices.getUserMedia({ - video: true, - audio: false, - }); - tempStream.getTracks().forEach((track) => track.stop()); - - const devices = await navigator.mediaDevices.enumerateDevices(); - const cams = devices.filter((d) => d.kind === 'videoinput'); - if (mounted) { - setVideoDevices(cams); - } + setExistingVideo(true); + setCurrentMode('view'); - const targetCameraId = selectedCamera || (await getPreferredCamera()); - const constraints = { - video: targetCameraId ? { - deviceId: { exact: targetCameraId }, - width: { ideal: 1920 }, - height: { ideal: 1080 }, - frameRate: { ideal: 30 }, - } : { - width: { ideal: 1920 }, - height: { ideal: 1080 }, - frameRate: { ideal: 30 }, - }, - audio: isAudioEnabled, - }; + setNotify('success'); + setSnackText('Video saved successfully'); + setOpenSnackBar(true); - const stream = await navigator.mediaDevices.getUserMedia(constraints); - - if (!mounted) { - stream.getTracks().forEach((track) => track.stop()); - return; - } - - streamRef.current = stream; - - if (videoPreviewRef.current) { - videoPreviewRef.current.srcObject = stream; - videoPreviewRef.current.onloadedmetadata = () => { - if (mounted) { - videoPreviewRef.current.play(); - setCameraReady(true); - - const videoTrack = stream.getVideoTracks()[0]; - logger.debug('Using camera:', videoTrack.label); - } - }; - } - } catch (err) { - if (mounted) { - logger.error('Camera initialization error:', err); - setErrorWithTimeout(getCameraErrorMessage(err)); - - if (err.name === 'OverconstrainedError') { - try { - const fallbackStream = await navigator.mediaDevices.getUserMedia({ - video: true, - audio: isAudioEnabled, - }); - - if (mounted && videoPreviewRef.current) { - streamRef.current = fallbackStream; - videoPreviewRef.current.srcObject = fallbackStream; - videoPreviewRef.current.onloadedmetadata = () => { - if (mounted) { - videoPreviewRef.current.play(); - setCameraReady(true); - clearError(null); - } - }; - } - } catch (fallbackErr) { - logger.error('Fallback camera access failed:', fallbackErr); - } - } - } - } - }; - - initCamera(); - - return () => { - mounted = false; - if (streamRef.current) { - streamRef.current.getTracks().forEach((track) => track.stop()); - } - if (timerRef.current) { - clearInterval(timerRef.current); - } - if (errorTimeoutRef.current) { - clearTimeout(errorTimeoutRef.current); - } - }; - }, [isAudioEnabled, currentMode, selectedCamera, verse, chapter, setErrorWithTimeout, clearError]); - - useEffect(() => { - if (currentMode === 'view' && hasVideo && videoPreviewRef.current) { - const path = require('path'); - const fs = window.require('fs'); - - const filename = `${chapter}_${verse}.webm`; - const fullPath = path.join(projectPath, filename); - - try { - if (!fs.existsSync(fullPath)) { - logger.error('Video file does not exist:', fullPath); - setErrorWithTimeout(`Video file not found: ${filename}`); - return; - } + setTimeout(() => { + if (videoPreviewRef.current) { + const blob = new Blob([buffer], { type: 'video/webm' }); + const objectUrl = URL.createObjectURL(blob); - const stats = fs.statSync(fullPath); - logger.debug('Video file size:', stats.size); + videoPreviewRef.current.pause(); + videoPreviewRef.current.srcObject = null; + videoPreviewRef.current.removeAttribute('src'); + videoPreviewRef.current.load(); - if (stats.size === 0) { - setErrorWithTimeout('Video file is empty'); - return; - } - } catch (err) { - logger.error('Error checking video file:', err); - setErrorWithTimeout(`Cannot access video: ${err.message}`); - return; + videoPreviewRef.current.src = objectUrl; + videoPreviewRef.current.load(); } + }, 100); + }; - const buffer = fs.readFileSync(fullPath); - const blob = new Blob([buffer], { type: 'video/webm' }); - const videoPath = URL.createObjectURL(blob); - - logger.debug('Loading video from Blob URL:', videoPath); - - setIsPlaying(false); - setPlaybackTime(0); - - const video = videoPreviewRef.current; - - video.pause(); - video.srcObject = null; - video.removeAttribute('src'); - video.load(); - - setTimeout(() => { - if (!videoPreviewRef.current) { return; } - video.src = videoPath; - video.load(); - video.onloadedmetadata = () => { - if (!videoPreviewRef.current) { return; } - let dur = video.duration; - - if (!Number.isFinite(dur) || dur === 0) { - logger.warn('Duration invalid, forcing recalculation...'); - video.currentTime = 1e101; - video.ontimeupdate = () => { - if (!videoPreviewRef.current) { return; } - video.ontimeupdate = null; - dur = video.duration; - if (Number.isFinite(dur)) { - setVideoDuration(dur); - } else { - setVideoDuration(0); - } - video.currentTime = 0; - }; - } else { - setVideoDuration(dur); - } - }; - - video.onloadeddata = () => { - if (!videoPreviewRef.current) { return; } - logger.debug('Video data loaded and ready to play'); - }; - - video.onerror = (e) => { - logger.error('Video load error:', e); - setError('Failed to load video file'); - }; - }, 50); - } - }, [currentMode, hasVideo, verse, projectPath]); + const { + isRecording, + isPaused, + recordingTime, + isProcessing, + startRecording, + pauseRecording, + resumeRecording, + stopRecording, + } = useVideoRecording({ + chapter, + verse, + projectPath, + streamRef, + onSaveComplete: handleSaveComplete, + onError: setError, + }); + + const { + thumbnailData, + hoverTime, + showHoverTime, + handleSeekHover, + clearSeekHover, + } = useVideoThumbnail({ + currentMode, + hasVideo, + videoPreviewRef, + }); + + const { isFullscreen, toggleFullscreen } = useFullscreen(); + + const { showCameraMenu, setShowCameraMenu, cameraMenuRef } = useCameraMenu(); useEffect(() => { - if (currentMode === 'view' && videoPreviewRef.current) { - if (isPlaying) { - videoPreviewRef.current.play().catch((err) => { - logger.error('Error playing video:', err); - setIsPlaying(false); - }); - - playbackTimerRef.current = setInterval(() => { - if (videoPreviewRef.current) { - setPlaybackTime(videoPreviewRef.current.currentTime); - } - }, 100); - } else { - videoPreviewRef.current.pause(); - - if (playbackTimerRef.current) { - clearInterval(playbackTimerRef.current); - playbackTimerRef.current = null; - } - if (videoPreviewRef.current) { - setPlaybackTime(videoPreviewRef.current.currentTime); - } - } - } + const filename = `${chapter}_${verse}.webm`; + const filePath = path.join(projectPath, filename); - return () => { - if (playbackTimerRef.current) { - clearInterval(playbackTimerRef.current); - } - }; - }, [isPlaying, currentMode]); + const videoExists = fs.existsSync(filePath); + setExistingVideo(videoExists); + setCurrentMode(videoExists ? 'view' : 'record'); + }, [chapter, verse, projectPath, mode]); useEffect(() => { if (!seekBarRef.current || !videoDuration) { return; } - const percent = (playbackTime / videoDuration) * 100; seekBarRef.current.style.setProperty('--progress', `${percent}%`); }, [playbackTime, videoDuration]); - useEffect(() => { - if (videoPreviewRef.current && currentMode === 'view') { - videoPreviewRef.current.playbackRate = playbackSpeed; - } - }, [playbackSpeed, currentMode]); - useEffect(() => () => { if (videoPreviewRef.current) { videoPreviewRef.current.pause(); @@ -431,117 +194,14 @@ const VideoRecorder = ({ videoPreviewRef.current.src = ''; videoPreviewRef.current.load(); } - if (streamRef.current) { - streamRef.current.getTracks().forEach((track) => { - track.stop(); - track.enabled = false; - }); - streamRef.current = null; - } - - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } - if (playbackTimerRef.current) { - clearInterval(playbackTimerRef.current); - playbackTimerRef.current = null; - } - - if (thumbnailCanvasRef.current) { - const ctx = thumbnailCanvasRef.current.getContext('2d'); - ctx?.clearRect(0, 0, thumbnailCanvasRef.current.width, thumbnailCanvasRef.current.height); - } if (videoPreviewRef.current?.src?.startsWith('blob:')) { URL.revokeObjectURL(videoPreviewRef.current.src); } }, []); - const saveVideo = useCallback(async (blob) => { - setIsProcessing(true); - - try { - const arrayBuffer = await blob.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - const filename = `${chapter}_${verse}.webm`; - const filePath = path.join(projectPath, filename); - - fs.writeFileSync(filePath, buffer); - if (onRecordingComplete) { - onRecordingComplete({ - verse, - chapter, - filePath, - filename, - }); - } - - setExistingVideo(true); - setCurrentMode('view'); - setIsProcessing(false); - - setNotify('success'); - setSnackText('Video saved successfully'); - setOpenSnackBar(true); - - setTimeout(() => { - if (videoPreviewRef.current) { - const buffer = fs.readFileSync(filePath); - const blob = new Blob([buffer], { type: 'video/webm' }); - const objectUrl = URL.createObjectURL(blob); - - videoPreviewRef.current.pause(); - videoPreviewRef.current.srcObject = null; - videoPreviewRef.current.removeAttribute('src'); - videoPreviewRef.current.load(); - - videoPreviewRef.current.src = objectUrl; - videoPreviewRef.current.load(); - - videoPreviewRef.current.onloadedmetadata = () => { - const video = videoPreviewRef.current; - if (!video) { return; } - let dur = video.duration; - - if (!Number.isFinite(dur) || dur === 0) { - logger.warn('Duration invalid, forcing recalculation...'); - video.currentTime = 1e101; - video.ontimeupdate = () => { - if (!videoPreviewRef.current) { return; } - video.ontimeupdate = null; - dur = video.duration; - if (Number.isFinite(dur)) { - setVideoDuration(dur); - logger.debug('Duration fixed:', dur); - } else { - setVideoDuration(0); - } - video.currentTime = 0; - }; - } else { - setVideoDuration(dur); - } - }; - } - }, 100); - } catch (err) { - logger.error('Error saving video:', err); - setNotify('failure'); - setSnackText(`Failed to save video: ${err.message}`); - setOpenSnackBar(true); - setIsProcessing(false); - } - }, [chapter, verse, projectPath, onRecordingComplete]); - const handleDeleteClick = () => { - const path = window.require('path'); - const filename = `${chapter}_${verse}.webm`; - const filePath = path.join(projectPath, filename); - onClose(); - setOpenModal({ openModel: true, title: 'Delete Video Recording?', @@ -558,151 +218,17 @@ const VideoRecorder = ({ }); }; - const startNewRecording = useCallback(() => { - try { - const filename = `${chapter}_${verse}.webm`; - const filePath = path.join(projectPath, filename); - - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - } - const options = { - mimeType: 'video/webm;codecs=vp9', - videoBitsPerSecond: 5000000, - }; - - if (!MediaRecorder.isTypeSupported(options.mimeType)) { - options.mimeType = 'video/webm;codecs=vp8'; - } - - const mediaRecorder = new MediaRecorder(streamRef.current, options); - mediaRecorderRef.current = mediaRecorder; - - const chunks = []; - mediaRecorder.ondataavailable = (event) => { - if (event.data && event.data.size > 0) { - chunks.push(event.data); - } - }; - - mediaRecorder.onstop = async () => { - const blob = new Blob(chunks, { type: 'video/webm' }); - await saveVideo(blob); - }; - - mediaRecorder.onerror = () => { - setError('Recording failed. Please try again.'); - setIsRecording(false); - setIsPaused(false); - }; - - mediaRecorder.start(100); - setIsRecording(true); - setIsPaused(false); - setRecordingTime(0); - - timerRef.current = setInterval(() => { - setRecordingTime((prev) => prev + 1); - }, 1000); - } catch (err) { - setError('Failed to start recording. Please try again.'); - } - }, [chapter, verse, projectPath, saveVideo]); - - const pauseRecording = () => { - if (mediaRecorderRef.current && isRecording && !isPaused) { - mediaRecorderRef.current.pause(); - setIsPaused(true); - - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } - - logger.debug('Recording paused'); - } - }; - - const resumeRecording = () => { - if (mediaRecorderRef.current && isRecording && isPaused) { - mediaRecorderRef.current.resume(); - setIsPaused(false); - - timerRef.current = setInterval(() => { - setRecordingTime((prev) => prev + 1); - }, 1000); - - logger.debug('Recording resumed'); - } - }; - - const stopRecording = () => { - if (mediaRecorderRef.current && isRecording) { - mediaRecorderRef.current.stop(); - setIsRecording(false); - setIsPaused(false); - - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } - } - }; - - const startRecording = async () => { - if (!streamRef.current) { - setError('Camera not ready'); - return; + const handleToggleAudio = async () => { + const success = await toggleAudio(isRecording); + if (success) { + setIsAudioEnabled(!isAudioEnabled); } - - try { - if (existingVideo) { - const fs = window.require('fs'); - const path = window.require('path'); - const filename = `${chapter}_${verse}.webm`; - const filePath = path.join(projectPath, filename); - - if (fs.existsSync(filePath)) { - fs.unlinkSync(filePath); - logger.debug('Deleted existing video for re-recording'); - } - } - - startNewRecording(); - } catch (err) { - setError('Failed to access file system.'); - } - }; - - const toggleAudio = async () => { - if (isRecording) { - setError('Cannot change audio settings while recording'); - return; - } - - setIsAudioEnabled(!isAudioEnabled); - setCameraReady(false); - - if (streamRef.current) { - streamRef.current.getTracks().forEach((track) => track.stop()); - } - }; - - const togglePlayPause = () => { - setIsPlaying(!isPlaying); }; const handlePreviousVerse = () => { const prevVerse = getPreviousVerseNumber(verse, content); if (prevVerse && onVerseChange) { - if (videoPreviewRef.current) { - const video = videoPreviewRef.current; - video.pause(); - video.srcObject = null; - video.removeAttribute('src'); - video.load(); - } - setIsPlaying(false); + resetPlayback(); onVerseChange(prevVerse); } }; @@ -710,94 +236,11 @@ const VideoRecorder = ({ const handleNextVerse = () => { const nextVerse = getNextVerseNumber(verse, content); if (nextVerse && onVerseChange) { - if (videoPreviewRef.current) { - const video = videoPreviewRef.current; - video.pause(); - video.srcObject = null; - video.removeAttribute('src'); - video.load(); - } - setIsPlaying(false); + resetPlayback(); onVerseChange(nextVerse); } }; - useEffect(() => { - if (currentMode === 'view' && hasVideo && !thumbnailVideoRef.current) { - thumbnailVideoRef.current = document.createElement('video'); - thumbnailVideoRef.current.muted = true; - thumbnailVideoRef.current.preload = 'metadata'; - thumbnailVideoRef.current.style.display = 'none'; - document.body.appendChild(thumbnailVideoRef.current); - } - - return () => { - if (thumbnailVideoRef.current) { - thumbnailVideoRef.current.remove(); - thumbnailVideoRef.current = null; - } - }; - }, [currentMode, hasVideo]); - - const generateThumbnail = useCallback((time) => { - if (!videoPreviewRef.current || !thumbnailVideoRef.current) { return; } - - const thumbnailVideo = thumbnailVideoRef.current; - - if (thumbnailVideo.src !== videoPreviewRef.current.src) { - thumbnailVideo.src = videoPreviewRef.current.src; - } - - if (!thumbnailCanvasRef.current) { - thumbnailCanvasRef.current = document.createElement('canvas'); - thumbnailCanvasRef.current.width = 160; - thumbnailCanvasRef.current.height = 90; - } - - const canvas = thumbnailCanvasRef.current; - const ctx = canvas.getContext('2d', { willReadFrequently: true }); - - thumbnailVideo.currentTime = time; - - const drawFrame = () => { - try { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(thumbnailVideo, 0, 0, canvas.width, canvas.height); - const dataUrl = canvas.toDataURL('image/jpeg', 0.6); - setThumbnailData(dataUrl); - } catch (err) { - logger.error('Error generating thumbnail:', err); - } - }; - - thumbnailVideo.onseeked = drawFrame; - }, []); - - const debouncedGenerateThumbnail = useCallback( - debounce((time) => { - if (videoPreviewRef.current && currentMode === 'view') { - generateThumbnail(time); - } - }, 100), - [generateThumbnail, currentMode], - ); - const handleSeekHover = (e) => { - if (!seekBarRef.current || !videoDuration) { return; } - - const rect = seekBarRef.current.getBoundingClientRect(); - const percent = (e.clientX - rect.left) / rect.width; - const time = Math.max(0, Math.min(videoDuration, percent * videoDuration)); - - setHoverTime(time); - setShowHoverTime(true); - debouncedGenerateThumbnail(time); - }; - const clearSeekHover = () => { - setShowHoverTime(false); - setHoverTime(null); - setThumbnailData(null); - }; - const formatTime = (seconds) => { if (!Number.isFinite(seconds) || seconds <= 0) { return '00:00'; } const mins = Math.floor(seconds / 60); @@ -805,24 +248,6 @@ const VideoRecorder = ({ return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; - const toggleFullscreen = useCallback(() => { - setIsFullscreen(!isFullscreen); - }, [isFullscreen]); - - useEffect(() => { - const handleKeyDown = (e) => { - if (e.key === 'Escape' && isFullscreen) { - setIsFullscreen(false); - } else if (e.key === 'f' && !e.target.matches('input, textarea')) { - e.preventDefault(); - toggleFullscreen(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [isFullscreen, toggleFullscreen]); - const hasPreviousVerse = () => getPreviousVerseNumber(verse, content) !== null; const hasNextVerse = () => getNextVerseNumber(verse, content) !== null; @@ -910,10 +335,6 @@ const VideoRecorder = ({ playsInline className="w-full h-full object-contain" onEnded={() => setIsPlaying(false)} - onError={(e) => { - logger.error('Video playback error:', e); - setIsPlaying(false); - }} > @@ -961,14 +382,8 @@ const VideoRecorder = ({ max={videoDuration || 0} step={0.01} value={playbackTime} - onChange={(e) => { - const time = Number(e.target.value); - if (videoPreviewRef.current) { - videoPreviewRef.current.currentTime = time; - setPlaybackTime(time); - } - }} - onMouseMove={handleSeekHover} + onChange={(e) => seek(Number(e.target.value))} + onMouseMove={(e) => handleSeekHover(e, seekBarRef, videoDuration)} onMouseLeave={clearSeekHover} className="w-full accent-primary cursor-pointer" /> @@ -1026,11 +441,7 @@ const VideoRecorder = ({ Speed