Skip to content

Commit ecef0e8

Browse files
Fix MJPEG streams not connecting.
1 parent 2245204 commit ecef0e8

14 files changed

+1122
-48
lines changed

BabbleApp/babbleapp.spec

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ exe = EXE(pyz,
2323
[],
2424
exclude_binaries=True,
2525
name='Babble_App',
26-
debug=False,
26+
debug=True,
2727
bootloader_ignore_signals=False,
2828
strip=False,
2929
upx=True,

BabbleApp/camera.py

+10-13
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@
1010
from enum import Enum
1111
import serial.tools.list_ports
1212
from lang_manager import LocaleStringManager as lang
13-
1413
from colorama import Fore
1514
from config import BabbleConfig, BabbleSettingsConfig
16-
from utils.misc_utils import get_camera_index_by_name, list_camera_names
15+
from utils.misc_utils import get_camera_index_by_name, list_camera_names, is_nt
1716

1817
from vivefacialtracker.vivetracker import ViveTracker
1918
from vivefacialtracker.camera_controller import FTCameraController
@@ -95,6 +94,7 @@ def run(self):
9594
self.config.capture_source is not None
9695
and self.config.capture_source != ""
9796
):
97+
self.current_capture_source = self.config.capture_source
9898
isSerial = any(x in str(self.config.capture_source) for x in PORTS)
9999

100100
if isSerial:
@@ -103,7 +103,7 @@ def run(self):
103103
self.cv2_camera = None
104104
if self.vft_camera is not None:
105105
self.vft_camera.close()
106-
self.device_is_vft = False;
106+
self.device_is_vft = False
107107
if (
108108
self.serial_connection is None
109109
or self.camera_status == CameraState.DISCONNECTED
@@ -116,7 +116,7 @@ def run(self):
116116
if self.cv2_camera is not None:
117117
self.cv2_camera.release()
118118
self.cv2_camera = None
119-
self.device_is_vft = True;
119+
self.device_is_vft = True
120120

121121
if self.vft_camera is None:
122122
print(self.error_message.format(self.config.capture_source))
@@ -141,18 +141,18 @@ def run(self):
141141
self.cv2_camera is None
142142
or not self.cv2_camera.isOpened()
143143
or self.camera_status == CameraState.DISCONNECTED
144-
or get_camera_index_by_name(self.config.capture_source) != self.current_capture_source
144+
#or get_camera_index_by_name(self.config.capture_source) != self.current_capture_source
145+
or self.config.capture_source != self.current_capture_source
145146
):
146147
if self.vft_camera is not None:
147148
self.vft_camera.close()
148-
self.device_is_vft = False;
149+
self.device_is_vft = False
149150

150151
print(self.error_message.format(self.config.capture_source))
151152
# This requires a wait, otherwise we can error and possible screw up the camera
152153
# firmware. Fickle things.
153154
if self.cancellation_event.wait(WAIT_TIME):
154155
return
155-
156156
if self.config.capture_source not in self.camera_list:
157157
self.current_capture_source = self.config.capture_source
158158
else:
@@ -163,9 +163,8 @@ def run(self):
163163
self.current_capture_source, cv2.CAP_FFMPEG
164164
)
165165
else:
166-
self.cv2_camera = cv2.VideoCapture(
167-
self.current_capture_source
168-
)
166+
self.cv2_camera = cv2.VideoCapture()
167+
self.cv2_camera.open(self.current_capture_source)
169168

170169
if not self.settings.gui_cam_resolution_x == 0:
171170
self.cv2_camera.set(
@@ -181,7 +180,6 @@ def run(self):
181180
self.cv2_camera.set(
182181
cv2.CAP_PROP_FPS, self.settings.gui_cam_framerate
183182
)
184-
185183
should_push = False
186184
else:
187185
# We don't have a capture source to try yet, wait for one to show up in the GUI.
@@ -215,15 +213,14 @@ def get_camera_picture(self, should_push):
215213
return
216214
self.frame_number = self.frame_number + 1
217215
elif self.cv2_camera is not None and self.cv2_camera.isOpened():
218-
ret, image = self.cv2_camera.read()
216+
ret, image = self.cv2_camera.read() # MJPEG Stream reconnects are currently limited by the hard coded 30 second timeout time on VideoCapture.read(). We can get around this by recompiling OpenCV or using a custom MJPEG stream imp.
219217
if not ret:
220218
self.cv2_camera.set(cv2.CAP_PROP_POS_FRAMES, 0)
221219
raise RuntimeError(lang._instance.get_string("error.frame"))
222220
self.frame_number = self.cv2_camera.get(cv2.CAP_PROP_POS_FRAMES) + 1
223221
else:
224222
# Switching from a Vive Facial Tracker to a CV2 camera
225223
return
226-
227224
self.FRAME_SIZE = image.shape
228225
# Calculate FPS
229226
current_frame_time = time.time() # Should be using "time.perf_counter()", not worth ~3x cycles?

BabbleApp/mjpeg_client.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from mjpeg.client import MJPEGClient
2+
from time import sleep
3+
import numpy as np
4+
import cv2
5+
6+
7+
url='http://192.168.1.186:8080/video'
8+
9+
# Create a new client thread
10+
client = MJPEGClient(url)
11+
12+
# Allocate memory buffers for frames
13+
bufs = client.request_buffers(65536, 50)
14+
for b in bufs:
15+
client.enqueue_buffer(b)
16+
17+
# Start the client in a background thread
18+
client.start()
19+
while True:
20+
client.print_stats()
21+
buf = client.dequeue_buffer()
22+
client.enqueue_buffer(buf)
23+
data = memoryview(buf.data)[:buf.used]
24+
img_array = np.frombuffer(data, dtype=np.uint8)
25+
frame = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
26+
# Check if the frame was decoded correctly
27+
if frame is not None:
28+
cv2.imshow("MJPEG Stream", frame)
29+
# Exit loop if 'q' is pressed
30+
if cv2.waitKey(1) & 0xFF == ord("q"):
31+
break
32+
#print(frame.shape)
33+
#print(len(array))

BabbleApp/mjpeg_test.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import requests
2+
import numpy as np
3+
import cv2
4+
import threading
5+
import io
6+
7+
class MJPEGVideoCapture:
8+
def __init__(self, url):
9+
self.url = url
10+
self.session = requests.Session()
11+
self.stream = None
12+
self.byte_buffer = b""
13+
self.frame = None
14+
self.running = False
15+
self.lock = threading.Lock()
16+
self.thread = None
17+
18+
def open(self):
19+
if not self.running:
20+
self.running = True
21+
self.thread = threading.Thread(target=self._update, daemon=True)
22+
self.thread.start()
23+
24+
def _update(self):
25+
while self.running:
26+
try:
27+
self.stream = self.session.get(self.url, stream=True, timeout=3)
28+
for chunk in self.stream.iter_content(chunk_size=1024):
29+
if not self.running:
30+
break
31+
self.byte_buffer += chunk
32+
start = self.byte_buffer.find(b'\xff\xd8') # JPEG start
33+
end = self.byte_buffer.find(b'\xff\xd9') # JPEG end
34+
35+
if start != -1 and end != -1:
36+
jpg = self.byte_buffer[start:end+2]
37+
self.byte_buffer = self.byte_buffer[end+2:]
38+
39+
image = np.frombuffer(jpg, dtype=np.uint8)
40+
if image.size != 0:
41+
frame = cv2.imdecode(image, cv2.IMREAD_GRAYSCALE)
42+
if frame is not None:
43+
with self.lock:
44+
self.frame = frame
45+
except requests.RequestException:
46+
continue # Retry on failure
47+
48+
def read(self):
49+
with self.lock:
50+
return self.frame is not None, self.frame.copy() if self.frame is not None else None
51+
52+
def isOpened(self):
53+
return self.running
54+
55+
def release(self):
56+
self.running = False
57+
if self.thread is not None:
58+
self.thread.join()
59+
self.stream = None
60+
self.frame = None
61+
self.byte_buffer = b""
62+
self.session.close()
63+
64+
if __name__ == "__main__":
65+
cap = MJPEGVideoCapture("http://openiristracker.local")
66+
cap.open()
67+
68+
while cap.isOpened():
69+
ret, frame = cap.read()
70+
if ret:
71+
cv2.imshow("MJPEG Stream", frame)
72+
73+
if cv2.waitKey(1) & 0xFF == ord("q"):
74+
break
75+
76+
cap.release()
77+
cv2.destroyAllWindows()

BabbleApp/mjpeg_videocapture.py

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import requests
2+
import numpy as np
3+
import cv2
4+
import threading
5+
import time
6+
import queue
7+
8+
class MJPEGVideoCapture:
9+
def __init__(self, url, max_buffer_size=1024*1024):
10+
self.url = url
11+
self.session = requests.Session()
12+
self.stream = None
13+
self.byte_buffer = b""
14+
self.max_buffer_size = max_buffer_size # Maximum allowed size for byte_buffer (e.g., 1 MB)
15+
self.running = False
16+
self.thread = None
17+
# Use a queue with maxsize=1 so that only the latest frame is kept.
18+
self.frame_queue = queue.Queue(maxsize=1)
19+
print("VC Opened")
20+
21+
def open(self):
22+
if not self.running:
23+
self.running = True
24+
self.thread = threading.Thread(target=self._update, daemon=True)
25+
self.thread.start()
26+
# Allow some time for the update thread to start and capture a frame.
27+
time.sleep(2)
28+
29+
def _update(self):
30+
while self.running:
31+
try:
32+
self.stream = self.session.get(self.url, stream=True, timeout=1)
33+
for chunk in self.stream.iter_content(chunk_size=512):
34+
if not self.running:
35+
break
36+
self.byte_buffer += chunk
37+
38+
# Flush the buffer if it grows too large.
39+
if len(self.byte_buffer) > self.max_buffer_size:
40+
print("Warning: byte_buffer exceeded maximum size; flushing buffer.")
41+
self.byte_buffer = b""
42+
continue
43+
44+
# Instead of taking the first complete JPEG frame, we look for the latest complete one.
45+
# Find the last occurrence of the JPEG start and end markers.
46+
start = self.byte_buffer.rfind(b'\xff\xd8')
47+
end = self.byte_buffer.rfind(b'\xff\xd9')
48+
49+
# If a complete JPEG is found, extract it and flush the entire buffer.
50+
if start != -1 and end != -1 and end > start:
51+
jpg = self.byte_buffer[start:end+2]
52+
self.byte_buffer = b"" # Discard all other data.
53+
54+
image = np.frombuffer(jpg, dtype=np.uint8)
55+
if image.size != 0:
56+
# Decode as grayscale.
57+
frame = cv2.imdecode(image, cv2.IMREAD_GRAYSCALE)
58+
if frame is not None:
59+
# Resize to 240x240.
60+
frame = cv2.resize(frame, (240, 240))
61+
# Convert grayscale to 3-channel image with shape (240, 240, 3).
62+
frame = np.stack([frame] * 3, axis=-1)
63+
64+
# Try to put the frame into the queue.
65+
# If the queue is full, remove the old frame first.
66+
try:
67+
self.frame_queue.put(frame, block=False)
68+
except queue.Full:
69+
try:
70+
self.frame_queue.get_nowait()
71+
except queue.Empty:
72+
pass
73+
self.frame_queue.put(frame, block=False)
74+
except requests.RequestException:
75+
# On failure, simply continue to try again.
76+
continue
77+
78+
def read(self):
79+
"""
80+
Block until a frame is available and return the latest frame.
81+
This mimics the blocking behavior of cap.read(), but always returns the latest frame.
82+
"""
83+
frame = self.frame_queue.get() # This call blocks until a frame is available.
84+
return True, frame.copy()
85+
86+
def isOpened(self):
87+
return self.running
88+
89+
def release(self):
90+
self.running = False
91+
if self.thread is not None:
92+
self.thread.join()
93+
self.stream = None
94+
self.byte_buffer = b""
95+
self.session.close()
96+
97+
# Testing code:
98+
test = True
99+
if test:
100+
if __name__ == "__main__":
101+
cap = MJPEGVideoCapture("http://192.168.1.186:8080/video")
102+
cap.open()
103+
104+
while cap.isOpened():
105+
ret, frame = cap.read()
106+
# Print the current byte_buffer length to monitor its size.
107+
print("Current byte_buffer length:", len(cap.byte_buffer))
108+
if ret:
109+
cv2.imshow("MJPEG Stream", frame)
110+
111+
if cv2.waitKey(1) & 0xFF == ord("q"):
112+
break
113+
114+
cap.release()
115+
cv2.destroyAllWindows()

0 commit comments

Comments
 (0)