wiktortokumpel

chuh1

Nov 13th, 2025 (edited)
20
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import cv2
  2. import time
  3. import threading
  4. from collections import deque
  5. from datetime import datetime
  6. import socket
  7. import os
  8. import re
  9.  
  10. # ================== KONFIGURACJA ==================
  11.  
  12. CAMERAS = {
  13. "KAMERA1": "rtsp://admin:[email protected]:554/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif",
  14. }
  15.  
  16. PRE_SECONDS = 10 # ile sekund PRZED triggerem ma być w nagraniu
  17. POST_SECONDS = 15 # ile sekund PO triggerze ma być w nagraniu
  18. FPS_DEFAULT = 25 # stały fps do pliku
  19.  
  20. OUTPUT_DIR = r"D:\wideo"
  21. RETENTION_DAYS = 1 # na testy 1 dzień, potem możesz dać 30
  22.  
  23. TCP_HOST = "0.0.0.0"
  24. TCP_PORT = 5000
  25.  
  26.  
  27. # ================== POMOCNICZE ==================
  28.  
  29. def ensure_output_dir():
  30. os.makedirs(OUTPUT_DIR, exist_ok=True)
  31.  
  32.  
  33. def cleanup_old_files():
  34. now = time.time()
  35. cutoff = now - RETENTION_DAYS * 24 * 3600
  36.  
  37. for filename in os.listdir(OUTPUT_DIR):
  38. filepath = os.path.join(OUTPUT_DIR, filename)
  39. if os.path.isfile(filepath):
  40. if os.path.getmtime(filepath) < cutoff:
  41. print(f"[CLEANUP] Usuwam stary plik: {filename}")
  42. os.remove(filepath)
  43.  
  44.  
  45. def cleanup_worker():
  46. while True:
  47. cleanup_old_files()
  48. time.sleep(24 * 3600)
  49.  
  50.  
  51. def sanitize_part_code(part_code: str) -> str:
  52. part_code = part_code.strip()
  53. if not part_code:
  54. part_code = "NO_CODE"
  55. return re.sub(r"[^0-9A-Za-z_-]", "_", part_code)
  56.  
  57.  
  58. # ================== KAMERA ==================
  59.  
  60. class CameraRecorder(threading.Thread):
  61. def __init__(self, name, url):
  62. super().__init__(daemon=True)
  63. self.name = name
  64. self.url = url
  65. self.lock = threading.Lock()
  66.  
  67. self.cap = None
  68. self._open_stream()
  69.  
  70. self.fps = FPS_DEFAULT
  71. self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
  72. self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
  73.  
  74. # bufor: (timestamp, frame)
  75. self.buffer = deque()
  76. self.buffer_max_seconds = PRE_SECONDS + 2 # mały zapas
  77.  
  78. self.recording = False
  79. self.record_end_time = 0.0
  80. self.writer = None
  81.  
  82. print(f"[{self.name}] Start. W={self.width} H={self.height}, fps={self.fps}")
  83.  
  84. def _open_stream(self):
  85. if self.cap:
  86. self.cap.release()
  87. print(f"[{self.name}] Otwieram RTSP...")
  88. self.cap = cv2.VideoCapture(self.url)
  89. if not self.cap.isOpened():
  90. print(f"[{self.name}] ❌ Nie można otworzyć strumienia RTSP!")
  91.  
  92. def _trim_buffer(self, now: float):
  93. cutoff = now - self.buffer_max_seconds
  94. while self.buffer and self.buffer[0][0] < cutoff:
  95. self.buffer.popleft()
  96.  
  97. def _start_new_writer(self, part_code: str, use_prebuffer: bool):
  98. """
  99. Tworzy nowy plik i:
  100. - jeśli use_prebuffer=True -> zapisuje dokładnie PRE_SECONDS wstecz,
  101. a POTEM ustawia record_end_time = teraz + POST_SECONDS
  102. - jeśli use_prebuffer=False -> od razu ustawia record_end_time = teraz + POST_SECONDS
  103. """
  104. # czas wyzwolenia do nazwy pliku
  105. trigger_time_for_name = time.time()
  106. ts_name = datetime.fromtimestamp(trigger_time_for_name).strftime("%Y_%m_%d_%H_%M_%S")
  107. code = sanitize_part_code(part_code)
  108. filename = f"{ts_name}_{code}_{self.name}.mp4"
  109. filepath = os.path.join(OUTPUT_DIR, filename)
  110.  
  111. print(f"[{self.name}] Start nagrania: {filename} | pre={use_prebuffer}")
  112.  
  113. writer = cv2.VideoWriter(
  114. filepath,
  115. cv2.VideoWriter_fourcc(*"mp4v"),
  116. self.fps,
  117. (self.width, self.height)
  118. )
  119.  
  120. if not writer.isOpened():
  121. print(f"[{self.name}] ❌ Błąd otwierania pliku!")
  122. return None
  123.  
  124. # najpierw zapisujemy pre-buffer (10s wstecz)
  125. if use_prebuffer:
  126. # cutoff liczymy od momentu triggera (sprzed chwili)
  127. cutoff = trigger_time_for_name - PRE_SECONDS
  128. frames_to_write = [frame for (t, frame) in list(self.buffer) if t >= cutoff]
  129. print(f"[{self.name}] Zapisuję pre-buffer: {len(frames_to_write)} klatek (~{PRE_SECONDS}s)")
  130. for frame in frames_to_write:
  131. writer.write(frame)
  132.  
  133. # UWAGA: koniec nagrywania liczymy OD TEGO MOMENTU,
  134. # czyli po zapisaniu pre-buffer (żeby nie ucinało po 5s).
  135. self.recording = True
  136. self.record_end_time = time.time() + POST_SECONDS
  137.  
  138. return writer
  139.  
  140. def trigger(self, part_code: str):
  141. with self.lock:
  142. now = time.time()
  143. if self.recording and self.writer:
  144. print(f"[{self.name}] Kończę poprzednie nagranie (nowy trigger).")
  145. try:
  146. self.writer.release()
  147. except Exception:
  148. pass
  149. self.writer = None
  150. self.recording = False
  151. # nowe nagranie BEZ pre-buffer
  152. self.writer = self._start_new_writer(part_code, use_prebuffer=False)
  153. else:
  154. # pierwsze / po przerwie – z pre-buffer
  155. self.writer = self._start_new_writer(part_code, use_prebuffer=True)
  156.  
  157. if self.writer:
  158. print(f"[{self.name}] Nagrywam do: {time.ctime(self.record_end_time)}")
  159. else:
  160. print(f"[{self.name}] ❌ Nie udało się rozpocząć nagrania.")
  161.  
  162. def run(self):
  163. while True:
  164. if not self.cap.isOpened():
  165. time.sleep(1)
  166. self._open_stream()
  167. continue
  168.  
  169. ret, frame = self.cap.read()
  170. if not ret:
  171. time.sleep(0.5)
  172. self._open_stream()
  173. continue
  174.  
  175. now = time.time()
  176.  
  177. with self.lock:
  178. # aktualizacja bufora
  179. self.buffer.append((now, frame.copy()))
  180. self._trim_buffer(now)
  181.  
  182. # zapis post-trigger
  183. if self.recording and self.writer:
  184. self.writer.write(frame)
  185. if now >= self.record_end_time:
  186. print(f"[{self.name}] Koniec nagrania (post {POST_SECONDS}s).")
  187. try:
  188. self.writer.release()
  189. except Exception:
  190. pass
  191. self.writer = None
  192. self.recording = False
  193.  
  194.  
  195. # ================== TCP SERVER ==================
  196.  
  197. def tcp_server(cameras):
  198. srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  199. srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  200. srv.bind((TCP_HOST, TCP_PORT))
  201. srv.listen(5)
  202.  
  203. print(f"[TCP] Nasłuch na {TCP_HOST}:{TCP_PORT}")
  204.  
  205. while True:
  206. conn, addr = srv.accept()
  207. with conn:
  208. try:
  209. data = conn.recv(1024)
  210. if not data:
  211. continue
  212. code = data.decode(errors="ignore").strip()
  213. print(f"[TCP] Kod części: {code!r}")
  214. if not code:
  215. continue
  216. for cam in cameras:
  217. cam.trigger(code)
  218. except Exception as e:
  219. print("[TCP] Błąd:", e)
  220.  
  221.  
  222. # ================== MAIN ==================
  223.  
  224. def main():
  225. ensure_output_dir()
  226.  
  227. cameras = []
  228. for name, url in CAMERAS.items():
  229. cam = CameraRecorder(name, url)
  230. cam.start()
  231. cameras.append(cam)
  232.  
  233. threading.Thread(target=cleanup_worker, daemon=True).start()
  234. threading.Thread(target=tcp_server, args=(cameras,), daemon=True).start()
  235.  
  236. print("=== SYSTEM NAGRYWANIA URUCHOMIONY ===")
  237. print("Katalog:", OUTPUT_DIR)
  238. print(f"Pre: {PRE_SECONDS} s, Post: {POST_SECONDS} s")
  239. print(f"Retencja: {RETENTION_DAYS} dni\n")
  240.  
  241. while True:
  242. time.sleep(1)
  243.  
  244.  
  245. if __name__ == "__main__":
  246. main()
  247.  
  248.  
Advertisement
Add Comment
Please, Sign In to add comment