rtsp_player.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import av
  2. import numpy as np
  3. import time
  4. import cv2
  5. import threading
  6. import logging
  7. from PySide6.QtCore import QObject, Signal, QMutex
  8. from PySide6.QtGui import QImage, QPixmap
  9. from typing import List, Dict, Any, Optional
  10. logger = logging.getLogger(__name__)
  11. class RTSPPlayer(QObject):
  12. frame_ready = Signal(QPixmap)
  13. first_frame_received = Signal()
  14. timeout = Signal()
  15. def __init__(self, rtsp_url: str, target_width: int = 640, target_height: int = 360, timeout_sec: float = 5.0):
  16. super().__init__()
  17. self.rtsp_url = rtsp_url
  18. self.target_width = target_width
  19. self.target_height = target_height
  20. self.timeout_sec = timeout_sec
  21. self.running = False
  22. self._thread: Optional[threading.Thread] = None
  23. self._stop_socket: Optional[socket.socket] = None
  24. self.ai_objects: List[Dict[str, Any]] = []
  25. self.target_fps = 25
  26. self.mutex = QMutex()
  27. def set_target_size(self, width: int, height: int):
  28. self.mutex.lock()
  29. self.target_width = width
  30. self.target_height = height
  31. self.mutex.unlock()
  32. def set_ai_objects(self, objects):
  33. self.mutex.lock()
  34. self.ai_objects = list(objects)
  35. self.mutex.unlock()
  36. def run(self):
  37. self.running = True
  38. options = {
  39. 'rtsp_transport': 'tcp',
  40. 'buffer_size': '1024',
  41. 'max_delay': '50000',
  42. 'flags': 'low_delay',
  43. 'fflags': 'nobuffer+discardcorrupt',
  44. 'analyzeduration': '50000',
  45. 'probesize': '50000',
  46. 'timeout': str(int(self.timeout_sec * 1000000)),
  47. 'stimeout': str(int(self.timeout_sec * 1000000)),
  48. }
  49. start_time = time.time()
  50. first_packet = True
  51. container = None
  52. try:
  53. container = av.open(self.rtsp_url, options=options)
  54. open_time = time.time() - start_time
  55. logger.info(f"RTSP av.open 耗时 {open_time:.1f}s: {self.rtsp_url}")
  56. video_stream = container.streams.video[0]
  57. video_stream.thread_type = 'AUTO'
  58. video_stream.thread_count = 1
  59. last_emit = 0
  60. frame_interval = 1.0 / self.target_fps
  61. for packet in container.demux(video_stream):
  62. if not self.running:
  63. break
  64. if first_packet:
  65. first_packet = False
  66. logger.info(f"RTSP 首包到达耗时 {time.time() - start_time:.1f}s: {self.rtsp_url}")
  67. self.first_frame_received.emit()
  68. for frame in packet.decode():
  69. if not self.running:
  70. break
  71. img = frame.to_rgb().to_ndarray()
  72. now = time.time()
  73. if now - last_emit >= frame_interval:
  74. self.process_frame(img)
  75. last_emit = now
  76. if first_packet and (time.time() - start_time) > self.timeout_sec:
  77. logger.warning("RTSP连接超时(未收到任何包)")
  78. self.timeout.emit()
  79. self.running = False
  80. break
  81. if not self.running:
  82. break
  83. except Exception as e:
  84. logger.error(f"RTSP错误: {e}")
  85. if first_packet:
  86. self.timeout.emit()
  87. finally:
  88. if container:
  89. container.close()
  90. self.running = False
  91. def start(self):
  92. self._thread = threading.Thread(target=self.run, daemon=True)
  93. self._thread.start()
  94. def stop(self):
  95. self.running = False
  96. def is_alive(self):
  97. return self._thread is not None and self._thread.is_alive()
  98. def process_frame(self, frame):
  99. self.mutex.lock()
  100. tw, th = self.target_width, self.target_height
  101. objects = list(self.ai_objects)
  102. self.mutex.unlock()
  103. orig_h, orig_w = frame.shape[:2]
  104. if tw > 0 and th > 0 and (orig_w != tw or orig_h != th):
  105. # 保持原始宽高比缩放到 target 内,不足补黑边
  106. scale = min(tw / orig_w, th / orig_h)
  107. new_w = int(orig_w * scale)
  108. new_h = int(orig_h * scale)
  109. resized = cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
  110. canvas = np.zeros((th, tw, 3), dtype=np.uint8)
  111. x_off = (tw - new_w) // 2
  112. y_off = (th - new_h) // 2
  113. canvas[y_off:y_off+new_h, x_off:x_off+new_w] = resized
  114. frame = canvas
  115. # AI 坐标从 display 空间映射到实际画布内图像区域
  116. sx = new_w / tw
  117. sy = new_h / th
  118. else:
  119. sx = sy = 1.0
  120. x_off = y_off = 0
  121. for obj in objects:
  122. x = int(obj.get('x', 0) * sx + x_off)
  123. y = int(obj.get('y', 0) * sy + y_off)
  124. w = int(obj.get('w', 0) * sx)
  125. h = int(obj.get('h', 0) * sy)
  126. label = obj.get('label', '')
  127. color = obj.get('color', (0, 255, 0))
  128. dimmed = obj.get('dimmed', False)
  129. thickness = 1 if dimmed else 2
  130. if dimmed:
  131. color = tuple(c // 2 for c in color)
  132. cv2.rectangle(frame, (x, y), (x+w, y+h), color, thickness)
  133. cv2.putText(frame, label, (x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, thickness)
  134. h, w, ch = frame.shape
  135. bytes_per_line = ch * w
  136. qt_img = QImage(frame.data, w, h, bytes_per_line, QImage.Format_RGB888).copy()
  137. pixmap = QPixmap.fromImage(qt_img)
  138. self.frame_ready.emit(pixmap)