1- import React , { useEffect , useState , useCallback } from 'react' ;
2- import { useTranslator } from '../../hooks/translateResults' ;
3- import { processLetters } from '../../utils/apiCalls' ;
1+ import React , { useEffect , useRef , useState , useCallback } from 'react' ;
42import PropTypes from 'prop-types' ;
3+ import { useTranslationSocket } from '../../hooks/translationSocket' ;
54
65export function CameraInput ( { progress = 0 , show = true , onSkip, onLetterDetected } ) {
7- const { videoRef, canvasRef2 } = useTranslator ( ) ;
8- const [ frameBlobs , setFrameBlobs ] = useState ( [ ] ) ;
9- const [ processing , setProcessing ] = useState ( false ) ;
6+ const videoRef = useRef ( null ) ;
7+ const canvasRef = useRef ( null ) ;
8+ const lastDetectedLetterRef = useRef ( null ) ;
9+
10+ const {
11+ wsRef,
12+ startRecording,
13+ stopRecording,
14+ sendFrame,
15+ wsStatus,
16+ isProcessing,
17+ setResult
18+ } = useTranslationSocket ( 'right' ) ;
19+
20+ const [ detectedLetter , setDetectedLetter ] = useState ( null ) ;
1021
1122 const captureFrame = useCallback ( ( ) => {
1223 const video = videoRef . current ;
13- const canvas = canvasRef2 . current ;
14- if ( ! video || ! canvas ) return ;
24+ const canvas = canvasRef . current ;
25+ if ( ! video || ! canvas || ! wsRef . current || wsRef . current . readyState !== WebSocket . OPEN )
26+ return ;
1527
1628 const ctx = canvas . getContext ( '2d' ) ;
1729 canvas . width = video . videoWidth ;
1830 canvas . height = video . videoHeight ;
31+
1932 ctx . save ( ) ;
2033 ctx . translate ( canvas . width , 0 ) ;
2134 ctx . scale ( - 1 , 1 ) ;
2235 ctx . drawImage ( video , 0 , 0 , canvas . width , canvas . height ) ;
2336 ctx . restore ( ) ;
2437
25- canvas . toBlob ( blob => {
26- if ( blob ) setFrameBlobs ( prev => [ ...prev , blob ] ) ;
27- } , 'image/jpeg' , 0.8 ) ;
28- } , [ videoRef , canvasRef2 ] ) ;
38+ sendFrame ( video ) ;
39+ } , [ sendFrame , wsRef ] ) ;
2940
3041 useEffect ( ( ) => {
31- if ( ! show || processing ) return ;
42+ if ( ! show ) return ;
43+ const enableCamera = async ( ) => {
44+ try {
45+ const stream = await navigator . mediaDevices . getUserMedia ( {
46+ video : { width : { ideal : 1280 } , height : { ideal : 720 } } ,
47+ } ) ;
48+ if ( videoRef . current ) videoRef . current . srcObject = stream ;
49+ } catch ( err ) {
50+ console . error ( 'Camera access denied:' , err ) ;
51+ }
52+ } ;
53+ enableCamera ( ) ;
3254
33- const interval = setInterval ( ( ) => {
34- captureFrame ( ) ;
35- } , 100 ) ; // 10 fps
55+ const currentVideo = videoRef . current
3656
37- return ( ) => clearInterval ( interval ) ;
38- } , [ show , processing , captureFrame ] ) ;
57+ return ( ) => {
58+ if ( currentVideo ?. srcObject ) {
59+ currentVideo . srcObject . getTracks ( ) . forEach ( t => t . stop ( ) ) ;
60+ }
61+ } ;
62+ } , [ show ] ) ;
3963
4064 useEffect ( ( ) => {
41- const sendFrames = async ( ) => {
42- const requiredFrames = 20 ;
43- if ( frameBlobs . length < requiredFrames || processing ) return ;
65+ if ( ! show ) {
66+ stopRecording ( ) ;
67+ return ;
68+ }
4469
45- setProcessing ( true ) ;
46- try {
47- const formData = new FormData ( ) ;
48- frameBlobs . forEach ( ( blob , idx ) => {
49- formData . append ( 'frames' , blob , `frame_${ idx } .jpg` ) ;
50- } ) ;
70+ if ( ! wsRef . current || wsRef . current . readyState !== WebSocket . OPEN ) {
71+ startRecording ( 'alpha' , 10 ) ;
72+ }
5173
52- const result = await processLetters ( formData ) ;
53- console . log ( "API response:" , result ) ;
74+ const interval = setInterval ( ( ) => {
75+ captureFrame ( ) ;
76+ } , 120 ) ;
77+
78+ return ( ) => {
79+ clearInterval ( interval ) ;
80+ stopRecording ( ) ;
81+ } ;
82+ } , [ show , startRecording , stopRecording , captureFrame , wsRef ] ) ;
5483
55- if ( result ?. letter ) {
56- onLetterDetected ?. ( result . letter . toUpperCase ( ) ) ;
84+ useEffect ( ( ) => {
85+ const ws = wsRef . current ;
86+ if ( ! ws ) return ;
87+
88+ ws . onmessage = async ( event ) => {
89+ const data = JSON . parse ( event . data ) ;
90+ const letter = data ?. letter ?. toUpperCase ?. ( ) ?? '' ;
91+
92+ if ( / ^ [ A - Z ] $ / . test ( letter ) ) {
93+ if ( letter !== detectedLetter && letter !== lastDetectedLetterRef . current ) {
94+ // console.log('New letter detected:', letter);
95+
96+ setDetectedLetter ( letter ) ;
97+ lastDetectedLetterRef . current = letter ;
98+ onLetterDetected ?. ( letter ) ;
99+ stopRecording ( ) ;
100+
101+ setTimeout ( ( ) => {
102+ setResult ( null ) ;
103+ setDetectedLetter ( null ) ;
104+ startRecording ( 'alpha' , 10 ) ;
105+ } , 1500 ) ;
106+ } else {
107+ // console.log(`Ignored duplicate letter: ${letter}`);
57108 }
58- }
59- catch ( err ) {
60- console . error ( "Error sending frames:" , err ) ;
61- }
62- finally {
63- setFrameBlobs ( [ ] ) ;
64- setProcessing ( false ) ;
65109 }
66110 } ;
67111
68- sendFrames ( ) ;
69- } , [ frameBlobs , processing , onLetterDetected ] ) ;
112+ ws . onclose = ( ) => {
113+ // console.log('WebSocket closed from CameraInput');
114+ } ;
70115
116+ return ( ) => {
117+ ws . onmessage = null ;
118+ ws . onclose = null ;
119+ } ;
120+ } , [
121+ wsRef ,
122+ onLetterDetected ,
123+ detectedLetter ,
124+ stopRecording ,
125+ startRecording ,
126+ setResult ,
127+ ] ) ;
71128 if ( ! show ) return null ;
72129
73130 return (
74- < div style = { {
75- position : 'absolute' ,
76- top : '30%' ,
77- left : '50 %' ,
78- transform : 'translateX(- 50%)' ,
79- display : 'flex ' ,
80- flexDirection : 'column ' ,
81- alignItems : 'center ' ,
82- zIndex : 50 ,
83- gap : '1rem'
84- } } >
85- < div style = { {
86- width : '25vw' ,
87- maxWidth : '300px' ,
88- aspectRatio : '1 / 1' ,
89- borderRadius : '50% ' ,
90- overflow : 'hidden ' ,
91- position : 'relative ' ,
92- background : 'white ' ,
93- } } >
94- < video
95- ref = { videoRef }
96- autoPlay
97- playsInline
98- style = { { width : '100%' , height : '100%' , objectFit : 'cover' } }
99- />
100- < canvas
101- ref = { canvasRef2 }
102- hidden
131+ < div
132+ style = { {
133+ position : 'absolute' ,
134+ top : '30 %' ,
135+ left : '50%' ,
136+ transform : 'translateX(-50%) ' ,
137+ display : 'flex ' ,
138+ flexDirection : 'column ' ,
139+ alignItems : 'center' ,
140+ zIndex : 50 ,
141+ gap : '1rem' ,
142+ } }
143+ >
144+ < div
145+ style = { {
146+ width : '25vw ' ,
147+ maxWidth : '300px ' ,
148+ aspectRatio : '1 / 1 ' ,
149+ borderRadius : '50% ' ,
150+ overflow : 'hidden' ,
151+ position : 'relative' ,
152+ background : 'white' ,
153+ } }
154+ >
155+ < video
156+ ref = { videoRef }
157+ autoPlay
158+ playsInline
159+ style = { { width : '100%' , height : '100%' , objectFit : 'cover' } }
103160 />
161+ < canvas ref = { canvasRef } hidden />
104162 < svg
105163 viewBox = "0 0 100 100"
106164 style = { {
@@ -109,7 +167,7 @@ export function CameraInput({ progress = 0, show = true, onSkip, onLetterDetecte
109167 left : 0 ,
110168 width : '100%' ,
111169 height : '100%' ,
112- zIndex : 1
170+ zIndex : 1 ,
113171 } }
114172 >
115173 < circle
@@ -124,14 +182,17 @@ export function CameraInput({ progress = 0, show = true, onSkip, onLetterDetecte
124182 style = { {
125183 transition : 'stroke-dashoffset 0.1s linear' ,
126184 transform : 'rotate(-90deg)' ,
127- transformOrigin : '50% 50%'
185+ transformOrigin : '50% 50%' ,
128186 } }
129187 />
130188 </ svg >
131189 </ div >
132190
133- < button
134- onClick = { onSkip }
191+ < button
192+ onClick = { ( ) => {
193+ stopRecording ( ) ;
194+ onSkip ?. ( ) ;
195+ } }
135196 style = { {
136197 padding : '0.5rem 1rem' ,
137198 background : 'red' ,
@@ -140,23 +201,31 @@ export function CameraInput({ progress = 0, show = true, onSkip, onLetterDetecte
140201 borderRadius : '8px' ,
141202 fontWeight : 'bold' ,
142203 cursor : 'pointer' ,
143- zIndex : 51
204+ zIndex : 51 ,
144205 } }
145206 >
146207 Skip
147208 </ button >
148209
149- { processing && (
150- < div style = { {
151- color : 'red' ,
152- fontSize : '1.2rem' ,
153- fontWeight : 'bold' ,
154- textAlign : 'center' ,
155- minHeight : '1.5rem'
156- } } >
210+ { isProcessing && (
211+ < div
212+ style = { {
213+ color : 'red' ,
214+ fontSize : '1rem' ,
215+ fontWeight : 'bold' ,
216+ textAlign : 'center' ,
217+ minHeight : '1.5rem' ,
218+ } }
219+ >
157220 PROCESSING...
158221 </ div >
159222 ) }
223+
224+ { wsStatus === 'collecting' && ! isProcessing && (
225+ < div style = { { color : 'green' , fontWeight : 'bold' , fontSize : '1rem' } } >
226+ COLLECTING FRAMES...
227+ </ div >
228+ ) }
160229 </ div >
161230 ) ;
162231}
@@ -166,4 +235,4 @@ CameraInput.propTypes = {
166235 show : PropTypes . bool . isRequired ,
167236 onSkip : PropTypes . func . isRequired ,
168237 onLetterDetected : PropTypes . func . isRequired ,
169- } ;
238+ } ;
0 commit comments