agrobot_base/Python/raspi/pi/stream_sender.py

327 lines
10 KiB
Python
Raw Normal View History

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()