rtsp_player.py 5.2 KB

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