infinicode 2.8.87 → 2.8.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -70,6 +70,14 @@ for actual PCM frames before reporting `microphone_published`, reports a blocked
70
70
  stage when capture does not start within five seconds, and applies bounded mic
71
71
  gain before publication.
72
72
 
73
+ ### Camera Relay With Multiple Viewers
74
+
75
+ RoboVision must have exactly one OpenCV camera reader. Preview, dashboard, and
76
+ motion detection consume a shared latest-frame stream. Starting one
77
+ `generate_frames()` camera loop per MJPEG client causes concurrent
78
+ `VideoCapture.read()` calls, which can block the second client and make the hub
79
+ relay time out with zero bytes.
80
+
73
81
  ### Duplicate Speaker Tests
74
82
 
75
83
  Only one robot process may claim a scheduler `speaker_test`. The scheduler
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinicode",
3
- "version": "2.8.87",
3
+ "version": "2.8.88",
4
4
  "description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
5
5
  "type": "module",
6
6
  "main": "./dist/kernel/index.js",
@@ -18,6 +18,11 @@ CORS(app)
18
18
  # Camera management
19
19
  current_camera_index = 0
20
20
  camera = None
21
+ camera_lock = threading.Lock()
22
+ frame_condition = threading.Condition()
23
+ latest_frame_bytes = None
24
+ latest_frame_sequence = 0
25
+ camera_worker_started = False
21
26
  audio_input_device = "default"
22
27
  audio_output_device = "default"
23
28
  ROBOVISION_AUDIO_URL = os.getenv("ROBOVISION_AUDIO_URL", "http://127.0.0.1:8000")
@@ -224,22 +229,25 @@ def send_webhook(frame_data):
224
229
  except Exception as e:
225
230
  print(f"Error preparing webhook: {e}")
226
231
 
227
- def generate_frames():
228
- global latest_detections, motion_detection_active, motion_detected_state
229
- global last_motion_time, motion_frame_buffer
230
-
231
- prev_gray = None
232
-
233
- while True:
234
- cam = get_camera()
235
- if cam is None or not cam.isOpened():
236
- time.sleep(1)
237
- continue
238
-
239
- success, frame = cam.read()
240
- if not success:
241
- time.sleep(0.1)
242
- continue
232
+ def camera_worker():
233
+ global latest_detections, motion_detection_active, motion_detected_state
234
+ global last_motion_time, motion_frame_buffer
235
+ global latest_frame_bytes, latest_frame_sequence
236
+
237
+ prev_gray = None
238
+
239
+ while True:
240
+ with camera_lock:
241
+ cam = get_camera()
242
+ if cam is None or not cam.isOpened():
243
+ time.sleep(1)
244
+ continue
245
+
246
+ with camera_lock:
247
+ success, frame = cam.read()
248
+ if not success:
249
+ time.sleep(0.1)
250
+ continue
243
251
 
244
252
  # Run object detection
245
253
  processed_frame, detections = detect_objects(frame, conf_threshold=0.5)
@@ -288,11 +296,40 @@ def generate_frames():
288
296
 
289
297
  prev_gray = gray
290
298
 
291
- ret, buffer = cv2.imencode('.jpg', processed_frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
292
- frame_bytes = buffer.tobytes()
293
-
294
- yield (b'--frame\r\n'
295
- b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
299
+ ret, buffer = cv2.imencode('.jpg', processed_frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
300
+ if not ret:
301
+ continue
302
+ with frame_condition:
303
+ latest_frame_bytes = buffer.tobytes()
304
+ latest_frame_sequence += 1
305
+ frame_condition.notify_all()
306
+
307
+
308
+ def _ensure_camera_worker():
309
+ global camera_worker_started
310
+ with frame_condition:
311
+ if camera_worker_started:
312
+ return
313
+ camera_worker_started = True
314
+ threading.Thread(target=camera_worker, name='robovision-camera', daemon=True).start()
315
+
316
+
317
+ def generate_frames():
318
+ _ensure_camera_worker()
319
+ sequence = -1
320
+ while True:
321
+ with frame_condition:
322
+ frame_condition.wait_for(
323
+ lambda: latest_frame_bytes is not None and latest_frame_sequence != sequence,
324
+ timeout=5.0,
325
+ )
326
+ if latest_frame_bytes is None or latest_frame_sequence == sequence:
327
+ continue
328
+ frame_bytes = latest_frame_bytes
329
+ sequence = latest_frame_sequence
330
+
331
+ yield (b'--frame\r\n'
332
+ b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
296
333
 
297
334
  @app.route('/')
298
335
  def index():
@@ -379,11 +416,11 @@ def switch_camera():
379
416
  if isinstance(new_index, str) and new_index.isdigit():
380
417
  new_index = int(new_index)
381
418
 
382
- if camera:
383
- camera.release()
384
-
385
- current_camera_index = new_index
386
- camera = None
419
+ with camera_lock:
420
+ if camera:
421
+ camera.release()
422
+ current_camera_index = new_index
423
+ camera = None
387
424
 
388
425
  return jsonify({"status": "ok", "camera_index": current_camera_index})
389
426