import json import socket import threading import time import queue from numcodecs import Blosc class StreamSender: _QUEUE_SENTINEL = object() def __init__(self, state, frame_service): self.state = state self.frame_service = frame_service self._lock = threading.RLock() self._stop_event = threading.Event() self._sock = None self._thread_capture = None self._thread_send = None self._queue = queue.Queue(maxsize=2) self._codec = None self._codec_signature = None self._t_json_pack = 0.0 self._t_send_header = 0.0 self._t_send_payload = 0.0 self._frames_dropped = 0 self._capture_errors = 0 self._send_errors = 0 self._last_sent_camera_frame_id = 0 @property def is_running(self): return not self._stop_event.is_set() and ( self._thread_capture is not None or self._thread_send is not None ) def start(self, host: str, port: int, fps: float): with self._lock: if self.is_running: raise RuntimeError("Stream já está em execução") if not self.state.initialized: raise RuntimeError("Módulo não inicializado") self._stop_event.clear() self._clear_queue() self._t_json_pack = 0.0 self._t_send_header = 0.0 self._t_send_payload = 0.0 self._frames_dropped = 0 self._capture_errors = 0 self._send_errors = 0 self.state.streaming = True self.state.stream_host = host self.state.stream_port = port self.state.stream_fps = fps self._last_sent_camera_frame_id = 0 self._thread_capture = threading.Thread( target=self._worker_capture, args=(fps,), daemon=True ) self._thread_send = threading.Thread( target=self._worker_send, args=(host, port), daemon=True ) self._thread_capture.start() self._thread_send.start() def stop(self): with self._lock: self._stop_event.set() try: self._queue.put_nowait(self._QUEUE_SENTINEL) except queue.Full: try: self._queue.get_nowait() except queue.Empty: pass try: self._queue.put_nowait(self._QUEUE_SENTINEL) except queue.Full: pass sock = self._sock self._sock = None if sock is not None: try: sock.shutdown(socket.SHUT_RDWR) except Exception: pass try: sock.close() except Exception: pass if self._thread_capture is not None: self._thread_capture.join(timeout=2.0) self._thread_capture = None if self._thread_send is not None: self._thread_send.join(timeout=2.0) self._thread_send = None self._clear_queue() self.state.streaming = False self.state.stream_host = None self.state.stream_port = None self.state.stream_fps = None def _clear_queue(self): while not self._queue.empty(): try: self._queue.get_nowait() except queue.Empty: break def _normalize_shuffle(self, shuffle_value): if isinstance(shuffle_value, int): return shuffle_value mapping = { "NOSHUFFLE": Blosc.NOSHUFFLE, "SHUFFLE": Blosc.SHUFFLE, "BITSHUFFLE": Blosc.BITSHUFFLE, } key = str(shuffle_value).upper() if key not in mapping: raise ValueError(f"shuffle inválido: {shuffle_value}") return mapping[key] def _build_codec_from_state(self): family = self.state.codec_family name = self.state.codec_name params = dict(self.state.codec_params) if family == "none": return None if family != "numcodecs": raise ValueError(f"Família de codec não suportada: {family}") if name == "blosc": params["shuffle"] = self._normalize_shuffle(params.get("shuffle", "SHUFFLE")) return Blosc(**params) raise ValueError(f"Codec numcodecs não suportado: {name}") def _get_codec_signature_from_state(self): return ( self.state.codec_family, self.state.codec_name, tuple(sorted(self.state.codec_params.items())) ) def _ensure_codec(self): sig = self._get_codec_signature_from_state() if self._codec is None or self._codec_signature != sig: self._codec = self._build_codec_from_state() self._codec_signature = sig def _compress(self, frame_bytes: bytes) -> bytes: self._ensure_codec() if self.state.codec_family == "none": return frame_bytes return self._codec.encode(frame_bytes) def _send_packet(self, sock: socket.socket, header: dict, payload: bytes): t0 = time.perf_counter() header_bytes = json.dumps(header, separators=(",", ":")).encode("utf-8") t1 = time.perf_counter() sock.sendall(len(header_bytes).to_bytes(4, "big")) sock.sendall(header_bytes) sock.sendall(len(payload).to_bytes(4, "big")) t2 = time.perf_counter() sock.sendall(payload) t3 = time.perf_counter() self._t_json_pack = t1 - t0 self._t_send_header = t2 - t1 self._t_send_payload = t3 - t2 def _worker_capture(self, fps: float): frame_interval = 1.0 / fps if fps > 0 else 0.0 next_deadline = time.perf_counter() last_frame_ts = None while not self._stop_event.is_set(): t_loop0 = time.perf_counter() try: frame, meta = self.frame_service.capture_frame_raw() except Exception as e: self._capture_errors += 1 print(f"[WARN] Capture falhou: {e}") time.sleep(0.05) continue if frame is None: time.sleep(0.01) continue camera_frame_id = meta.get("camera_frame_id", 0) if camera_frame_id == self._last_sent_camera_frame_id: time.sleep(0.001) continue t_frame_ready = time.perf_counter() dt_frame_period = 0.0 if last_frame_ts is None else (t_frame_ready - last_frame_ts) last_frame_ts = t_frame_ready t_bytes0 = time.perf_counter() frame_bytes = frame.tobytes() t_bytes1 = time.perf_counter() t_comp0 = time.perf_counter() comp_bytes = self._compress(frame_bytes) t_comp1 = time.perf_counter() codec_name = None if self.state.codec_family == "none" else self.state.codec_name codec_params = {} if self.state.codec_family == "none" else dict(self.state.codec_params) header = { "frame_id": meta.get("frame_id"), "codec_family": self.state.codec_family, "codec_name": codec_name, "codec_params": codec_params, "dt_frame_period": dt_frame_period, "payload_size_raw": len(frame_bytes), "payload_size_comp": len(comp_bytes), "dt_bytes": t_bytes1 - t_bytes0, "dt_comp": t_comp1 - t_comp0, **meta } queued = False try: self._queue.put_nowait((header, comp_bytes)) queued = True except queue.Full: self._frames_dropped += 1 try: self._queue.get_nowait() except queue.Empty: pass try: self._queue.put_nowait((header, comp_bytes)) queued = True except queue.Full: self._frames_dropped += 1 if queued: self._last_sent_camera_frame_id = camera_frame_id if frame_interval > 0: next_deadline += frame_interval now = time.perf_counter() if next_deadline < now - frame_interval: next_deadline = now sleep_time = next_deadline - now if sleep_time > 0: time.sleep(sleep_time) def _worker_send(self, host: str, port: int): try: with socket.create_connection((host, port), timeout=5) as sock: sock.settimeout(10) with self._lock: self._sock = sock while not self._stop_event.is_set(): try: item = self._queue.get(timeout=0.2) except queue.Empty: continue if item is self._QUEUE_SENTINEL: break header, payload = item header["dt_pack_prev"] = self._t_json_pack header["dt_send_header_prev"] = self._t_send_header header["dt_send_payload_prev"] = self._t_send_payload header["stream_drops"] = self._frames_dropped header["capture_errors"] = self._capture_errors header["send_errors"] = self._send_errors self._send_packet(sock, header, payload) self.state.stream_frame_id = header["frame_id"] except Exception as e: self._send_errors += 1 if not self._stop_event.is_set(): print(f"[WARN] StreamSender encerrado com erro: {e}") finally: with self._lock: self._sock = None self.state.streaming = False self.state.stream_host = None self.state.stream_port = None self.state.stream_fps = None self._stop_event.set()