Skip to content

Commit 8bd9b65

Browse files
authored
Merge pull request #105 from RamesTheGeneric/custom-mjpeg-streamer
Use Alternative MJPEG streamer instead of OpenCV
2 parents 50d03ce + ccb588e commit 8bd9b65

File tree

2 files changed

+149
-33
lines changed

2 files changed

+149
-33
lines changed

BabbleApp/camera.py

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from vivefacialtracker.vivetracker import ViveTracker
2020
from vivefacialtracker.camera_controller import FTCameraController
2121

22+
from mjpeg_streamer import MJPEGVideoCapture
23+
2224
WAIT_TIME = 0.1
2325
BUFFER_SIZE = 32768
2426
MAX_RESOLUTION: int = 600
@@ -61,6 +63,7 @@ def __init__(
6163
self.current_capture_source = config.capture_source
6264
self.cv2_camera: "cv2.VideoCapture" = None
6365
self.vft_camera: FTCameraController = None
66+
self.http: bool = None
6467

6568
self.serial_connection = None
6669
self.last_frame_time = time.time()
@@ -156,6 +159,8 @@ def run(self):
156159
if self.cancellation_event.wait(WAIT_TIME):
157160
return
158161
if self.config.capture_source not in self.camera_list:
162+
if "http://" in str(self.config.capture_source): self.http=True
163+
else: self.http=False
159164
self.current_capture_source = self.config.capture_source
160165
else:
161166
self.current_capture_source = get_camera_index_by_name(self.config.capture_source)
@@ -165,23 +170,27 @@ def run(self):
165170
self.current_capture_source, cv2.CAP_FFMPEG
166171
)
167172
else:
168-
self.cv2_camera = cv2.VideoCapture()
169-
self.cv2_camera.open(self.current_capture_source)
170-
171-
if not self.settings.gui_cam_resolution_x == 0:
172-
self.cv2_camera.set(
173-
cv2.CAP_PROP_FRAME_WIDTH,
174-
self.settings.gui_cam_resolution_x,
175-
)
176-
if not self.settings.gui_cam_resolution_y == 0:
177-
self.cv2_camera.set(
178-
cv2.CAP_PROP_FRAME_HEIGHT,
179-
self.settings.gui_cam_resolution_y,
180-
)
181-
if not self.settings.gui_cam_framerate == 0:
182-
self.cv2_camera.set(
183-
cv2.CAP_PROP_FPS, self.settings.gui_cam_framerate
184-
)
173+
if not self.http:
174+
self.cv2_camera = cv2.VideoCapture()
175+
self.cv2_camera.open(self.current_capture_source)
176+
else:
177+
self.cv2_camera = MJPEGVideoCapture(self.current_capture_source)
178+
self.cv2_camera.open()
179+
if not self.http:
180+
if not self.settings.gui_cam_resolution_x == 0:
181+
self.cv2_camera.set(
182+
cv2.CAP_PROP_FRAME_WIDTH,
183+
self.settings.gui_cam_resolution_x,
184+
)
185+
if not self.settings.gui_cam_resolution_y == 0:
186+
self.cv2_camera.set(
187+
cv2.CAP_PROP_FRAME_HEIGHT,
188+
self.settings.gui_cam_resolution_y,
189+
)
190+
if not self.settings.gui_cam_framerate == 0:
191+
self.cv2_camera.set(
192+
cv2.CAP_PROP_FPS, self.settings.gui_cam_framerate
193+
)
185194
should_push = False
186195
else:
187196
# We don't have a capture source to try yet, wait for one to show up in the GUI.
@@ -216,22 +225,24 @@ def get_camera_picture(self, should_push):
216225
self.frame_number = self.frame_number + 1
217226
elif self.cv2_camera is not None and self.cv2_camera.isOpened():
218227
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.
219-
if not ret:
220-
self.cv2_camera.set(cv2.CAP_PROP_POS_FRAMES, 0)
221-
raise RuntimeError(lang._instance.get_string("error.frame"))
222-
self.frame_number = self.cv2_camera.get(cv2.CAP_PROP_POS_FRAMES) + 1
223-
else:
224-
# Switching from a Vive Facial Tracker to a CV2 camera
225-
return
226-
self.FRAME_SIZE = image.shape
227-
# Calculate FPS
228-
current_frame_time = time.time() # Should be using "time.perf_counter()", not worth ~3x cycles?
229-
delta_time = current_frame_time - self.last_frame_time
230-
self.last_frame_time = current_frame_time
231-
current_fps = 1 / delta_time if delta_time > 0 else 0
232-
# Exponential moving average (EMA). ~1100ns savings, delicious..
233-
self.fps = 0.02 * current_fps + 0.98 * self.fps
234-
self.bps = image.nbytes * self.fps
228+
if ret and image is not None:
229+
if not ret:
230+
if not self.http:
231+
self.cv2_camera.set(cv2.CAP_PROP_POS_FRAMES, 0)
232+
raise RuntimeError(lang._instance.get_string("error.frame"))
233+
self.frame_number = self.cv2_camera.get(cv2.CAP_PROP_POS_FRAMES) + 1
234+
else:
235+
# Switching from a Vive Facial Tracker to a CV2 camera
236+
return
237+
self.FRAME_SIZE = image.shape
238+
# Calculate FPS
239+
current_frame_time = time.time() # Should be using "time.perf_counter()", not worth ~3x cycles?
240+
delta_time = current_frame_time - self.last_frame_time
241+
self.last_frame_time = current_frame_time
242+
current_fps = 1 / delta_time if delta_time > 0 else 0
243+
# Exponential moving average (EMA). ~1100ns savings, delicious..
244+
self.fps = 0.02 * current_fps + 0.98 * self.fps
245+
self.bps = image.nbytes * self.fps
235246

236247
if should_push:
237248
self.push_image_to_queue(image, self.frame_number, self.fps)

BabbleApp/mjpeg_streamer.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import requests
2+
import numpy as np
3+
import cv2
4+
import threading
5+
import time
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.frame_ready = False
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=1)
28+
for chunk in self.stream.iter_content(chunk_size=1024):
29+
if not self.running:
30+
break
31+
self.byte_buffer += chunk
32+
# Process all available complete frames in the buffer
33+
while True:
34+
start = self.byte_buffer.find(b'\xff\xd8') # JPEG start marker
35+
end = self.byte_buffer.find(b'\xff\xd9') # JPEG end marker
36+
if start != -1 and end != -1:
37+
jpg = self.byte_buffer[start:end+2]
38+
self.byte_buffer = self.byte_buffer[end+2:]
39+
40+
image = np.frombuffer(jpg, dtype=np.uint8)
41+
if image.size != 0:
42+
frame = cv2.imdecode(image, cv2.IMREAD_COLOR)
43+
if frame is not None:
44+
self.frame = frame # Always update to the latest frame
45+
self.frame_ready = True
46+
else:
47+
break
48+
except requests.RequestException:
49+
# If a network error occurs, wait briefly and retry
50+
time.sleep(0.1)
51+
continue
52+
53+
def read(self):
54+
# Return whether a frame exists and its copy
55+
start = time.time()
56+
while True:
57+
if self.frame is not None and self.frame_ready:
58+
#time.sleep(self.sleep_time)
59+
self.frame_old = self.frame
60+
self.frame_ready = False
61+
return True, self.frame.copy()
62+
else:
63+
end = time.time()
64+
time.sleep(1/120)
65+
if end-start>1:
66+
return False, None
67+
68+
#return False, None
69+
70+
def isOpened(self):
71+
return self.running
72+
73+
def isPrimed(self):
74+
if self.frame is not None:
75+
return True
76+
else: return False
77+
78+
def release(self):
79+
self.running = False
80+
if self.thread is not None:
81+
self.thread.join()
82+
self.stream = None
83+
self.frame = None
84+
self.byte_buffer = b""
85+
self.session.close()
86+
87+
def get(self, item):
88+
pass
89+
return 1
90+
91+
92+
if __name__ == "__main__":
93+
cap = MJPEGVideoCapture("http://openiristracker.local")
94+
cap.open()
95+
96+
while cap.isOpened():
97+
ret, frame = cap.read()
98+
if ret and frame is not None:
99+
cv2.imshow("MJPEG Stream", frame)
100+
101+
if cv2.waitKey(1) & 0xFF == ord("q"):
102+
break
103+
104+
cap.release()
105+
cv2.destroyAllWindows()

0 commit comments

Comments
 (0)