Danisz commited on
Commit
6cbbb50
·
1 Parent(s): 13d5100

Camera working on JS

Browse files
Files changed (4) hide show
  1. camera_utils.py +169 -0
  2. flet_app.py +25 -159
  3. flet_extensions.py +6 -4
  4. js_execution.py +35 -6
camera_utils.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import flet as ft
3
+ from js_execution import set_camera_frame_callback
4
+ from flet_extensions import eval_js
5
+ import asyncio
6
+ from typing import List, Dict
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class Frame:
11
+ def __init__(self, data: str, timestamp: float):
12
+ self.data = data
13
+ self.timestamp = timestamp
14
+
15
+ class CameraImage(ft.Container):
16
+ def __init__(self, *args, **kwargs):
17
+ super().__init__(*args, **kwargs)
18
+ self.stream_active = False
19
+ self.video_image1 = ft.Image(
20
+ src="",
21
+ width=self.width,
22
+ height=self.height,
23
+ fit=ft.ImageFit.CONTAIN,
24
+ visible=False,
25
+ )
26
+ self.video_image2 = ft.Image(
27
+ src="",
28
+ width=self.width,
29
+ height=self.height,
30
+ fit=ft.ImageFit.CONTAIN,
31
+ visible=False,
32
+ )
33
+ self.current_video_image = self.video_image1
34
+ self.video_stack = ft.Stack([self.video_image1, self.video_image2])
35
+ self.content = self.video_stack
36
+ set_camera_frame_callback(self.handle_camera_frame)
37
+ self.captured_frames: Dict[int, Frame] = {}
38
+ self.stream_start_time = None
39
+
40
+ async def start_camera_stream_async(self) -> str:
41
+ try:
42
+ result = await eval_js(self.page, """
43
+ window.cameraStreamActive = false;
44
+ window.cameraWebSocket = null;
45
+
46
+ async function setupCameraStream() {
47
+ console.log("setupCameraStream called");
48
+ try {
49
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
50
+ console.log("Camera access granted");
51
+ const video = document.createElement('video');
52
+ video.srcObject = stream;
53
+ await video.play();
54
+
55
+ const canvas = document.createElement('canvas');
56
+ canvas.width = video.videoWidth / 2;
57
+ canvas.height = video.videoHeight / 2;
58
+ const ctx = canvas.getContext('2d');
59
+
60
+ window.cameraWebSocket = new WebSocket(`ws://${window.location.host}/ws/camera`);
61
+
62
+ function sendFrame() {
63
+ if (window.cameraWebSocket && window.cameraWebSocket.readyState === WebSocket.OPEN) {
64
+ ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, canvas.width, canvas.height);
65
+ window.cameraWebSocket.send(canvas.toDataURL('image/jpeg', 0.2)); // Added quality parameter
66
+ window.cameraStreamActive && setTimeout(sendFrame, 1000 / 60);
67
+ }
68
+ }
69
+
70
+ window.cameraWebSocket.onopen = () => {
71
+ console.log("WebSocket connection opened");
72
+ window.cameraStreamActive = true;
73
+ sendFrame();
74
+ };
75
+
76
+ window.cameraWebSocket.onclose = () => {
77
+ console.log("WebSocket connection closed");
78
+ window.cameraStreamActive = false;
79
+ stream.getTracks().forEach(track => track.stop());
80
+ };
81
+
82
+ return "Camera stream setup complete";
83
+ } catch (error) {
84
+ console.error("Error accessing camera:", error);
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ return await setupCameraStream();
90
+ """)
91
+ logger.info(f"start_camera_stream_async result: {result}")
92
+ self.stream_active = True
93
+ self.stream_start_time = asyncio.get_event_loop().time()
94
+ self.captured_frames.clear()
95
+ return result
96
+ except Exception as e:
97
+ logger.error(f"Error in start_camera_stream_async: {str(e)}")
98
+ return f"Error: {str(e)}"
99
+
100
+ async def stop_camera_stream_async(self) -> str:
101
+ try:
102
+ result = await eval_js(self.page, """
103
+ if (window.cameraWebSocket) {
104
+ window.cameraStreamActive = false;
105
+ window.cameraWebSocket.close();
106
+ window.cameraWebSocket = null;
107
+ }
108
+ return 'Camera stream stopped';
109
+ """)
110
+ logger.info(f"stop_camera_stream_async result: {result}")
111
+ self.stream_active = False
112
+ self.stream_start_time = None
113
+ return result
114
+ except Exception as e:
115
+ logger.error(f"Error in stop_camera_stream_async: {str(e)}")
116
+ return f"Error: {str(e)}"
117
+
118
+ async def handle_camera_frame(self, frame_data: str):
119
+ if self.stream_active:
120
+ await self.update_webcam_image(frame_data)
121
+ self.capture_timed_frames(frame_data)
122
+
123
+ def capture_timed_frames(self, frame_data: str):
124
+ current_time = asyncio.get_event_loop().time()
125
+ if self.stream_start_time is None:
126
+ self.stream_start_time = current_time
127
+
128
+ elapsed_time = current_time - self.stream_start_time
129
+
130
+ for seconds in [1, 2, 3]:
131
+ if elapsed_time >= seconds and seconds not in self.captured_frames:
132
+ self.captured_frames[seconds] = Frame(frame_data, current_time)
133
+
134
+ def get_captured_frames(self) -> List[Frame]:
135
+ return list(self.captured_frames.values())
136
+
137
+ async def update_webcam_image(self, frame_data: str) -> None:
138
+ if not frame_data:
139
+ print("Empty data!")
140
+ return
141
+
142
+ # Frame data is already in Data URI format
143
+ new_src = frame_data
144
+
145
+ self.video_image1.visible = True
146
+ self.video_image2.visible = True
147
+
148
+ # Determine which image to update
149
+ if self.current_video_image == self.video_image1:
150
+ image_to_update = self.video_image2
151
+ image_to_hide = self.video_image1
152
+ else:
153
+ image_to_update = self.video_image1
154
+ image_to_hide = self.video_image2
155
+
156
+ # Update the hidden image
157
+ image_to_update.src = new_src
158
+ image_to_update.update()
159
+
160
+ # Wait for the browser to load the new image
161
+ await asyncio.sleep(0.02) # Adjust this delay if needed
162
+
163
+ # Update opacity
164
+ image_to_update.opacity = 1
165
+ image_to_hide.opacity = 0
166
+ self.current_video_image = image_to_update
167
+
168
+ # Final update to apply opacity changes
169
+ self.video_stack.update()
flet_app.py CHANGED
@@ -5,9 +5,12 @@ from domain.translations import spanish_translations
5
  from domain.localapi_dialog_processor import LocalApiDialogueProcessor
6
  import logging
7
  import asyncio
8
- from typing import Optional, List, Tuple
9
  from audio_utils import get_base64_audio
10
 
 
 
 
11
  logging.basicConfig(level=logging.INFO)
12
  logger = logging.getLogger(__name__)
13
 
@@ -163,106 +166,11 @@ class ChessModel:
163
  logger.info(f"Piece at {chess.SQUARE_NAMES[square]}: {piece} (Empathy: {piece.empathy:.2f})")
164
 
165
 
166
- from pydantic import BaseModel, Field
167
- import time
168
- from typing import List, Callable
169
- import cv2
170
- import numpy as np
171
- import base64
172
-
173
-
174
  class StreamConfig(BaseModel):
175
  max_width: int = Field(default=320, gt=0)
176
  fps_target: int = Field(default=40, gt=0)
177
  jpeg_quality: int = Field(default=40, ge=0, le=100)
178
-
179
- class Frame(BaseModel):
180
- data: str
181
- width: int
182
- height: int
183
-
184
- import cv2
185
- import asyncio
186
-
187
- class CameraModel:
188
- def __init__(self):
189
- self.cap = None
190
-
191
- def __enter__(self):
192
- self.cap = cv2.VideoCapture(0)
193
- if not self.cap.isOpened():
194
- raise RuntimeError("Failed to open camera")
195
- return self
196
-
197
- def __exit__(self, exc_type, exc_val, exc_tb):
198
- if self.cap:
199
- self.cap.release()
200
-
201
- def get_frame(self):
202
- if not self.cap:
203
- raise RuntimeError("Camera is not initialized")
204
- ret, frame = self.cap.read()
205
- if not ret:
206
- raise RuntimeError("Failed to capture frame")
207
- return frame
208
-
209
- class ImageProcessorModel:
210
- @staticmethod
211
- def process(frame, config: StreamConfig) -> Frame:
212
- height, width = frame.shape[:2]
213
- if width > config.max_width:
214
- scale = config.max_width / width
215
- new_height = int(height * scale)
216
- frame = cv2.resize(frame, (config.max_width, new_height))
217
-
218
- _, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, config.jpeg_quality])
219
- # return Frame(
220
- # data="", #base64.b64encode(buffer).decode('utf-8'),
221
- # width=0, #frame.shape[1],
222
- # height=0, #frame.shape[0]
223
- # )
224
- return Frame(
225
- data=base64.b64encode(buffer).decode('utf-8'),
226
- width=frame.shape[1],
227
- height=frame.shape[0]
228
- )
229
-
230
-
231
- class VideoCaptureService:
232
- def __init__(self, camera_model: CameraModel, image_processor: ImageProcessorModel):
233
- self.camera_model = camera_model
234
- self.image_processor = image_processor
235
- self.stream_config = StreamConfig()
236
- self.is_capturing = False
237
- self.captured_frames: List[Frame] = []
238
- self.frame_callback: Callable[[Frame], None] = None
239
-
240
- async def start_capturing(self, frame_callback: Callable[[Frame], None]):
241
- self.is_capturing = True
242
- self.captured_frames.clear()
243
- self.frame_callback = frame_callback
244
- await self._capture_frames()
245
-
246
- def stop_capturing(self):
247
- self.is_capturing = False
248
-
249
- async def _capture_frames(self):
250
- with self.camera_model as camera:
251
- while self.is_capturing:
252
- frame = camera.get_frame()
253
- processed_frame = self.image_processor.process(frame, self.stream_config)
254
-
255
- if self.frame_callback:
256
- self.frame_callback(processed_frame)
257
-
258
- if len(self.captured_frames) < 3:
259
- self.captured_frames.append(processed_frame)
260
-
261
- await asyncio.sleep(1 / self.stream_config.fps_target)
262
-
263
- def get_captured_frames(self) -> List[Frame]:
264
- return self.captured_frames
265
-
266
  class RecordingCountdown(ft.ProgressBar):
267
  def __init__(self, seconds):
268
  super().__init__()
@@ -304,32 +212,22 @@ class ChessPresenter:
304
  self.recording_timer = None
305
  self.countdown_timer = None
306
 
307
- camera_model = CameraModel()
308
- image_processor = ImageProcessorModel()
309
- video_capture_service = VideoCaptureService(camera_model, image_processor)
310
- self.video_capture_service = video_capture_service
311
-
312
  def toggle_video_recording(self):
313
- if self.video_capture_service.is_capturing:
314
  self.stop_video_recording()
315
  self.view.show_processing_feedback()
316
  else:
317
  self.start_video_recording()
318
 
319
  def start_video_recording(self):
320
- self.view.video_container.visible = True
321
- self.view.page.run_task(self.video_capture_service.start_capturing, self.on_frame_captured)
322
  self.view.update_video_button(is_recording=True)
323
 
324
  def stop_video_recording(self):
325
- self.video_capture_service.stop_capturing()
326
  self.view.update_video_button(is_recording=False)
327
- self.view.video_container.visible = False
328
- self.view.video_container.update()
329
-
330
- def on_frame_captured(self, frame: Frame):
331
- self.view.page.run_task(self.view.update_webcam_image, frame)
332
-
333
 
334
  def process_audio_move_argument(self, audio_base64: str):
335
  piece_symbol, chess_piece = self.model.get_piece_at(self.pending_move.from_square)
@@ -684,25 +582,9 @@ class ChessView(ft.Container):
684
  self.audio_player.autoplay = False
685
  self.audio_player.update()
686
 
687
- self.video_image1 = ft.Image(
688
- src="",
689
- width=self.square_size * 4,
690
- height=self.square_size * 4,
691
- fit=ft.ImageFit.CONTAIN,
692
- visible=True,
693
- )
694
- self.video_image2 = ft.Image(
695
- src="",
696
- width=self.square_size * 4,
697
- height=self.square_size * 4,
698
- fit=ft.ImageFit.CONTAIN,
699
- visible=True,
700
- )
701
- self.current_video_image = self.video_image1
702
-
703
- self.video_stack = ft.Stack([self.video_image1, self.video_image2])
704
  self.video_container = ft.Container(
705
- content=self.video_stack,
706
  visible=False,
707
  )
708
 
@@ -965,7 +847,6 @@ class ChessView(ft.Container):
965
  def update_video_button(self, is_recording: bool):
966
  self.video_record_button.icon = ft.icons.STOP if is_recording else ft.icons.VIDEO_CALL
967
  self.video_record_button.tooltip = "Stop recording" if is_recording else "Record video move argument"
968
- self.video_container.visible = is_recording
969
  self.update()
970
 
971
  def show_response(self, response, piece: str, transcription: str):
@@ -1313,37 +1194,22 @@ class ChessView(ft.Container):
1313
  bar.controls[i].bgcolor = color
1314
  self.update()
1315
 
1316
- async def update_webcam_image(self, frame: Frame) -> None:
1317
- if not frame.data:
1318
- print("Empty data!")
1319
- return
1320
 
1321
- new_src = f"data:image/jpeg;base64,{frame.data}"
 
1322
 
1323
- # Determine which image to update
1324
- if self.current_video_image == self.video_image1:
1325
- image_to_update = self.video_image2
1326
- image_to_hide = self.video_image1
1327
- else:
1328
- image_to_update = self.video_image1
1329
- image_to_hide = self.video_image2
1330
-
1331
- # Update the hidden image
1332
- image_to_update.src = new_src
1333
- image_to_update.width = frame.width
1334
- image_to_update.height = frame.height
1335
- image_to_update.update()
1336
-
1337
- # Wait for the browser to load the new image
1338
- await asyncio.sleep(0.03) # Adjust this delay if needed
1339
-
1340
- # Update opacity
1341
- image_to_update.opacity = 1
1342
- image_to_hide.opacity = 0
1343
- self.current_video_image = image_to_update
1344
 
1345
- # Final update to apply opacity changes
1346
- self.video_stack.update()
 
1347
 
1348
  def on_screen_click(self, e):
1349
  if self.empathy_bar_visible:
 
5
  from domain.localapi_dialog_processor import LocalApiDialogueProcessor
6
  import logging
7
  import asyncio
8
+ from typing import Optional, List, Callable, Tuple
9
  from audio_utils import get_base64_audio
10
 
11
+ from pydantic import BaseModel, Field
12
+ from camera_utils import CameraImage
13
+
14
  logging.basicConfig(level=logging.INFO)
15
  logger = logging.getLogger(__name__)
16
 
 
166
  logger.info(f"Piece at {chess.SQUARE_NAMES[square]}: {piece} (Empathy: {piece.empathy:.2f})")
167
 
168
 
 
 
 
 
 
 
 
 
169
  class StreamConfig(BaseModel):
170
  max_width: int = Field(default=320, gt=0)
171
  fps_target: int = Field(default=40, gt=0)
172
  jpeg_quality: int = Field(default=40, ge=0, le=100)
173
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  class RecordingCountdown(ft.ProgressBar):
175
  def __init__(self, seconds):
176
  super().__init__()
 
212
  self.recording_timer = None
213
  self.countdown_timer = None
214
 
 
 
 
 
 
215
  def toggle_video_recording(self):
216
+ if self.view.is_camera_active():
217
  self.stop_video_recording()
218
  self.view.show_processing_feedback()
219
  else:
220
  self.start_video_recording()
221
 
222
  def start_video_recording(self):
223
+ self.view.start_camera_stream()
224
+ self.view.show_video_container()
225
  self.view.update_video_button(is_recording=True)
226
 
227
  def stop_video_recording(self):
228
+ self.view.stop_camera_stream()
229
  self.view.update_video_button(is_recording=False)
230
+ self.view.hide_video_container()
 
 
 
 
 
231
 
232
  def process_audio_move_argument(self, audio_base64: str):
233
  piece_symbol, chess_piece = self.model.get_piece_at(self.pending_move.from_square)
 
582
  self.audio_player.autoplay = False
583
  self.audio_player.update()
584
 
585
+ self.camera_image = CameraImage(width=320, height=240)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  self.video_container = ft.Container(
587
+ content=self.camera_image,
588
  visible=False,
589
  )
590
 
 
847
  def update_video_button(self, is_recording: bool):
848
  self.video_record_button.icon = ft.icons.STOP if is_recording else ft.icons.VIDEO_CALL
849
  self.video_record_button.tooltip = "Stop recording" if is_recording else "Record video move argument"
 
850
  self.update()
851
 
852
  def show_response(self, response, piece: str, transcription: str):
 
1194
  bar.controls[i].bgcolor = color
1195
  self.update()
1196
 
1197
+ def is_camera_active(self):
1198
+ return self.camera_image.stream_active
 
 
1199
 
1200
+ def start_camera_stream(self):
1201
+ self.my_page.run_task(self.camera_image.start_camera_stream_async)
1202
 
1203
+ def stop_camera_stream(self):
1204
+ self.my_page.run_task(self.camera_image.stop_camera_stream_async)
1205
+
1206
+ def show_video_container(self):
1207
+ self.video_container.visible = True
1208
+ self.update()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1209
 
1210
+ def hide_video_container(self):
1211
+ self.video_container.visible = False
1212
+ self.update()
1213
 
1214
  def on_screen_click(self, e):
1215
  if self.empathy_bar_visible:
flet_extensions.py CHANGED
@@ -1,11 +1,13 @@
1
  import flet as ft
2
  from js_execution import js_execution_router
3
 
 
 
 
 
 
 
4
  def extend_flet_page():
5
- async def eval_js(self: ft.Page, js_code: str) -> str:
6
- async with js_execution_router.create_dynamic_endpoint(js_code) as context:
7
- self.launch_url(context.endpoint, web_window_name=context.hidden_iframe_name)
8
- return await context.result_future
9
 
10
  if not hasattr(ft.Page, 'eval_js'):
11
  ft.Page.eval_js = eval_js
 
1
  import flet as ft
2
  from js_execution import js_execution_router
3
 
4
+
5
+ async def eval_js(self: ft.Page, js_code: str) -> str:
6
+ async with js_execution_router.create_dynamic_endpoint(js_code) as context:
7
+ self.launch_url(context.endpoint, web_window_name=context.hidden_iframe_name)
8
+ return await context.result_future
9
+
10
  def extend_flet_page():
 
 
 
 
11
 
12
  if not hasattr(ft.Page, 'eval_js'):
13
  ft.Page.eval_js = eval_js
js_execution.py CHANGED
@@ -1,9 +1,10 @@
1
  import asyncio
2
  import logging
3
  import contextlib
4
- from fastapi import FastAPI, Request
5
  from fastapi.responses import HTMLResponse
6
  import uuid
 
7
 
8
  logger = logging.getLogger(__name__)
9
 
@@ -20,8 +21,10 @@ class JSExecutionRouter:
20
 
21
  def init_app(self, app: FastAPI):
22
  self.app = app
 
23
  def get_eval_js_html(self, js_code: str, result_id: str):
24
- escaped_js = js_code.replace('"', '&quot;')
 
25
  return f"""
26
  <!DOCTYPE html>
27
  <html lang="en">
@@ -31,9 +34,12 @@ class JSExecutionRouter:
31
  </head>
32
  <body>
33
  <script>
 
34
  async function executeAndReturn() {{
 
35
  try {{
36
- const result = await (async () => {{ {escaped_js} }})();
 
37
  await fetch('/js-result/{result_id}', {{
38
  method: 'POST',
39
  headers: {{ 'Content-Type': 'application/json' }},
@@ -46,8 +52,6 @@ class JSExecutionRouter:
46
  headers: {{ 'Content-Type': 'application/json' }},
47
  body: JSON.stringify({{ error: error.toString() }})
48
  }});
49
- }} finally {{
50
- window.close();
51
  }}
52
  }}
53
  executeAndReturn();
@@ -66,10 +70,12 @@ class JSExecutionRouter:
66
 
67
  @self.app.get(f"/eval-js/{result_id}", response_class=HTMLResponse)
68
  async def serve_eval_js_page():
 
69
  return HTMLResponse(content=self.get_eval_js_html(js_code, result_id))
70
 
71
  @self.app.post(f"/js-result/{result_id}")
72
  async def receive_js_result(data: dict):
 
73
  if 'error' in data:
74
  result_future.set_exception(Exception(data['error']))
75
  else:
@@ -92,9 +98,32 @@ js_execution_router = JSExecutionRouter()
92
  def add_js_execution_routes(app: FastAPI):
93
  js_execution_router.init_app(app)
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  def mount_flet_app(app: FastAPI, route: str, flet_app):
96
  # Mount the Flet app
97
  app.mount("/flet-app-internal", flet_app)
 
98
 
99
  @app.get(route, response_class=HTMLResponse)
100
  async def serve_main_page(request: Request):
@@ -104,7 +133,7 @@ def mount_flet_app(app: FastAPI, route: str, flet_app):
104
  <head>
105
  <meta charset="UTF-8">
106
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
107
- <title>Audio Recorder App</title>
108
  <style>
109
  body, html {{
110
  margin: 0;
 
1
  import asyncio
2
  import logging
3
  import contextlib
4
+ from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
5
  from fastapi.responses import HTMLResponse
6
  import uuid
7
+ import html
8
 
9
  logger = logging.getLogger(__name__)
10
 
 
21
 
22
  def init_app(self, app: FastAPI):
23
  self.app = app
24
+
25
  def get_eval_js_html(self, js_code: str, result_id: str):
26
+ # escaped_js = html.escape(js_code)
27
+ logger.info(f"Generating HTML for JavaScript execution. Result ID: {result_id}")
28
  return f"""
29
  <!DOCTYPE html>
30
  <html lang="en">
 
34
  </head>
35
  <body>
36
  <script>
37
+ console.log("JavaScript execution page loaded");
38
  async function executeAndReturn() {{
39
+ console.log("executeAndReturn function called");
40
  try {{
41
+ const result = await (async () => {{ {js_code} }})();
42
+ console.log("JavaScript execution result:", result);
43
  await fetch('/js-result/{result_id}', {{
44
  method: 'POST',
45
  headers: {{ 'Content-Type': 'application/json' }},
 
52
  headers: {{ 'Content-Type': 'application/json' }},
53
  body: JSON.stringify({{ error: error.toString() }})
54
  }});
 
 
55
  }}
56
  }}
57
  executeAndReturn();
 
70
 
71
  @self.app.get(f"/eval-js/{result_id}", response_class=HTMLResponse)
72
  async def serve_eval_js_page():
73
+ logger.info(f"Serving eval-js page for result ID: {result_id}")
74
  return HTMLResponse(content=self.get_eval_js_html(js_code, result_id))
75
 
76
  @self.app.post(f"/js-result/{result_id}")
77
  async def receive_js_result(data: dict):
78
+ logger.info(f"Received result for ID: {result_id}")
79
  if 'error' in data:
80
  result_future.set_exception(Exception(data['error']))
81
  else:
 
98
  def add_js_execution_routes(app: FastAPI):
99
  js_execution_router.init_app(app)
100
 
101
+ @app.websocket("/ws/camera")
102
+ async def websocket_endpoint(websocket: WebSocket):
103
+ await websocket.accept()
104
+ try:
105
+ while True:
106
+ data = await websocket.receive_text()
107
+ await broadcast_camera_frame(data)
108
+ except WebSocketDisconnect:
109
+ pass
110
+ except Exception as e:
111
+ logger.error(f"WebSocket error: {str(e)}")
112
+
113
+ # Add this new function to handle broadcasting
114
+ async def broadcast_camera_frame(frame_data: str):
115
+ # This function will be called from the Flet app to set the callback
116
+ if hasattr(broadcast_camera_frame, 'callback'):
117
+ await broadcast_camera_frame.callback(frame_data)
118
+
119
+ # Function to set the callback from the Flet app
120
+ def set_camera_frame_callback(callback):
121
+ broadcast_camera_frame.callback = callback
122
+
123
  def mount_flet_app(app: FastAPI, route: str, flet_app):
124
  # Mount the Flet app
125
  app.mount("/flet-app-internal", flet_app)
126
+ app.flet_app = flet_app # Store reference to the Flet app
127
 
128
  @app.get(route, response_class=HTMLResponse)
129
  async def serve_main_page(request: Request):
 
133
  <head>
134
  <meta charset="UTF-8">
135
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
136
+ <title>Camera Stream App</title>
137
  <style>
138
  body, html {{
139
  margin: 0;