diff options
| -rw-r--r-- | client.cfg | 2 | ||||
| -rw-r--r-- | client.py | 17 | ||||
| -rw-r--r-- | helpers.py | 14 | ||||
| -rw-r--r-- | templates/client.html | 34 | ||||
| -rw-r--r-- | webserver.py | 13 | ||||
| -rw-r--r-- | worker.py | 2 |
6 files changed, 51 insertions, 31 deletions
@@ -6,7 +6,7 @@ router_address = 127.0.0.1 router_port = 5569 [logging] -logdir = logs +logdir = /var/log/zmq_client logfile = client.log loglevel = INFO @@ -35,6 +35,8 @@ from helpers import ( bytes_to_timestamp, write_yappi_stats, auth_service, + ComputingContoursException, + DetectMovementException, ) ################################################### @@ -187,8 +189,7 @@ class ClientVideo(Thread): List of contours to draw on a frame """ - raw_frame = self.frame_deque[-1][2] - scaling_factor = self.frame_deque[-1][1] + _, scaling_factor, raw_frame = self.frame_deque[-1] scaled_contours = scale_contours(contours, scaling_factor) try: frame_out = draw_contours( @@ -262,15 +263,13 @@ class ClientVideo(Thread): sample_frames = self.frame_deque[0][0], self.frame_deque[-1][0] try: contours = compute_contours(sample_frames) - except Exception as exc: - logger.error("ClientVideo %r: Error computing contours: %r", self.identity, exc) - continue - try: movement_now = detect_movement(contours, min_area=self.device_threshold) - except Exception as exc: - logger.error("ClientVideo %r: Error detecting movement: %r", self.identity, exc) + except ComputingContoursException: + logger.error("ClientVideo %r: Error computing contours", self.identity) + continue + except DetectMovementException: + logger.error("ClientVideo %r: Error detecting movement", self.identity) continue - # Only update movement start time and send start message # if movement is detected and was not detected before: if movement_now: @@ -96,15 +96,13 @@ def bytes_to_timestamp(byte_data): def compute_contours(sample_frames): """Compute contours between two frames""" - all_contours = [] frame_0, frame_1 = sample_frames frame_delta = cv2.absdiff(frame_0, frame_1) threshold = cv2.threshold(frame_delta, 25, 255, cv2.THRESH_BINARY)[1] threshold = cv2.dilate(threshold, None, iterations=2) - contours, _ = cv2.findContours(threshold, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - all_contours.extend(contours) + contours, _ = cv2.findContours(threshold.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - return all_contours + return contours def scale_contours(contours, scaling_factor): @@ -123,7 +121,7 @@ def scale_contours(contours, scaling_factor): def draw_contours(frame, contours, min_contour_area=500): """Draw contours on the frame.""" for contour in contours: - if cv2.contourArea(contour) > min_contour_area: + if cv2.contourArea(contour) >= min_contour_area: (x, y, w, h) = cv2.boundingRect(contour) cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2) @@ -272,3 +270,9 @@ def auth_service(context, cert_dir): auth.start() auth.configure_curve(location=cert_dir) zmq.auth.load_certificates(cert_dir) + +class ComputingContoursException(Exception): + pass + +class DetectMovementException(Exception): + pass
\ No newline at end of file diff --git a/templates/client.html b/templates/client.html index 785fbcf..09365df 100644 --- a/templates/client.html +++ b/templates/client.html @@ -3,6 +3,7 @@ <head> <title>Client {{ client_id }} - Camera Streams</title> <link rel="stylesheet" href="{{ url_for('static', path='css/main.css') }}"> + <link rel="icon" type="image/x-icon" href="images/favicon.ico"> <style> .camera-stream { display: inline-block; margin: 10px; } video { border: 1px solid #333; } @@ -53,16 +54,16 @@ <button onclick="sendConfig('{{ camera_id }}', 'modify_camera_grace_pd')">Send</button> </div> <div> - <h3>Recorded Videos for {{ camera_id }}</h3> - <ul id="video-list-{{ camera_id }}" class="scroll-box"> - {% for filename, video_url, timestamp in client_videos[camera_id] %} - <li><a href="{{ video_url }}">{{ filename }}</a> ({{ timestamp }})</li> - <!-- <h3>video file: {{ filename }}</h3> - <video src="{{ video_url }}" type="video/ogg" width="320" height="240" controls></video> - <hr4>creation: {{ timestamp }}</h3> - --> - {% endfor %} - </ul> + <h3> + <details> + <summary>Recorded videos for camera: {{ camera_id }}</summary> + <ul id="video-list-{{ camera_id }}" class="scroll-box"> + {% for filename, video_url, timestamp in client_videos[camera_id] %} + <li><a href="{{ video_url }}">{{ filename }}</a> ({{ timestamp }})</li> + {% endfor %} + </ul> + </details> + </h3> </div> </div> {% endfor %} @@ -91,14 +92,23 @@ // For each camera, open a WebSocket and update the corresponding <img> (function() { const cameraId = '{{ camera_id }}'; - const ws = new WebSocket('ws://' + window.location.host + '/ws/{{ client_id }}/' + cameraId); + const wsUrl = window.location.origin.replace(/^http/, 'ws') + '/ws/{{ client_id }}/' + cameraId; + const ws = new WebSocket(wsUrl); + ws.binaryType = 'arraybuffer'; let currentUrl = null; ws.onmessage = function(event) { let image = document.getElementById('video-' + cameraId); + if (!image) { + return; + } + if (currentUrl) { URL.revokeObjectURL(currentUrl); } - currentUrl = URL.createObjectURL(event.data); + + // Ensure we always have a Blob for createObjectURL + const blob = event.data instanceof Blob ? event.data : new Blob([event.data], { type: 'image/jpeg' }); + currentUrl = URL.createObjectURL(blob); image.src = currentUrl; }; ws.onclose = function(event) { diff --git a/webserver.py b/webserver.py index 3a95dca..1e1a068 100644 --- a/webserver.py +++ b/webserver.py @@ -76,7 +76,10 @@ auth_service(zmq_context, CERTIFICATE_DIR) zmq_socket = zmq_context.socket(zmq.DEALER) # Connect to ZMQ backend: -zmq_socket.connect(WEB_BACKEND_ADDR) +try: + zmq_socket.connect(WEB_BACKEND_ADDR) +except Exception as exc: + logger.error("Could not connect to backend address: %r" % exc) async def zmq_bridge(): @@ -125,7 +128,8 @@ async def zmq_bridge(): logger.error("Invalid control message on queue: %r", ve) if client_id and camera_id and command and args_list is not None: try: - zmq_socket.send_multipart([ + # TODO: check if this below is correct + await zmq_socket.send_multipart([ client_id, camera_id, command, @@ -190,7 +194,7 @@ async def main_route(request: Request) -> HTMLResponse: request : Request The incoming HTTP request. """ - logger.debug("Main route visited") + logger.debug("Main route visited. CLIENTS_DICT: %s" % CLIENTS_DICT) return templates.TemplateResponse( "main.html", { @@ -454,6 +458,9 @@ def validate_camera_name(name, client_id, camera_id) -> bool: if len(new_name) > MAX_CAMERA_NAME_LENGTH: logger.error("Camera name is too long (max %s characters).", MAX_CAMERA_NAME_LENGTH) retval = False + if ' ' in name: + logger.error("Camera name cannot contain spaces.") + retval = False return retval def validate_camera_threshold(threshold) -> bool: @@ -136,7 +136,7 @@ class MonitorTask(Thread): # Resolve the received event type: event_type = int.from_bytes(event[:2], "little") if event_type in (zmq.EVENT_CLOSED, zmq.EVENT_DISCONNECTED): - logger.warning("Monitor socket: closed | disconnected") + logger.debug("Monitor socket: closed | disconnected") stop_event.set() elif event_type == zmq.EVENT_CONNECT_DELAYED: logger.debug("Monitor socket: event connect delayed") |
