wiktortokumpel

wesion z kopią

Nov 14th, 2025 (edited)
23
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 w buforze
  17. POST_SECONDS = 15 # ile sekund PO triggerze nagrywać
  18. FPS_DEFAULT = 25 # jeśli kamera nie poda sensownego FPS
  19.  
  20. OUTPUT_DIR = r"D:\wideo"
  21. RETENTION_DAYS = 1 # na testy, potem np. 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. for filename in os.listdir(OUTPUT_DIR):
  37. filepath = os.path.join(OUTPUT_DIR, filename)
  38. if os.path.isfile(filepath):
  39. if os.path.getmtime(filepath) < cutoff:
  40. print(f"[CLEANUP] Usuwam stary plik: {filename}")
  41. os.remove(filepath)
  42.  
  43.  
  44. def cleanup_worker():
  45. while True:
  46. cleanup_old_files()
  47. time.sleep(24 * 3600)
  48.  
  49.  
  50. def sanitize_part_code(part_code: str) -> str:
  51. part_code = part_code.strip()
  52. if not part_code:
  53. part_code = "NO_CODE"
  54. return re.sub(r"[^0-9A-Za-z_-]", "_", part_code)
  55.  
  56.  
  57. # ================== KAMERA – BUFOR + TRIGGER (LICZONY NA KLATKACH) ==================
  58.  
  59. class CameraRecorder(threading.Thread):
  60. def __init__(self, name, url):
  61. super().__init__(daemon=True)
  62. self.name = name
  63. self.url = url
  64.  
  65. self.cap = None
  66. self._open_stream()
  67.  
  68. fps = self.cap.get(cv2.CAP_PROP_FPS) if self.cap else 0
  69. if fps <= 0 or fps > 120:
  70. fps = FPS_DEFAULT
  71. self.fps = fps
  72.  
  73. self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH) or 640)
  74. self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT) or 480)
  75.  
  76. # ile klatek trzymamy w buforze
  77. self.pre_frames = int(self.fps * PRE_SECONDS)
  78. # ile klatek nagrywamy po triggerze
  79. self.post_frames_target = int(self.fps * POST_SECONDS)
  80.  
  81. # bufor z ostatnimi klatkami
  82. self.buffer = deque(maxlen=self.pre_frames)
  83.  
  84. self.lock = threading.Lock()
  85.  
  86. # stan nagrywania
  87. self.recording = False
  88. self.writer = None
  89. self.post_frames_left = 0 # ile klatek jeszcze zapisać po triggerze
  90.  
  91. # do reconnectów
  92. self.fail_count = 0
  93. self.max_fail_before_reopen = int(self.fps * 2) # ok. 2 sekundy błędów z rzędu
  94.  
  95. print(
  96. f"[{self.name}] Start. {self.width}x{self.height} @ {self.fps} fps, "
  97. f"bufor={self.pre_frames} klatek, post={self.post_frames_target} klatek"
  98. )
  99.  
  100. def _open_stream(self):
  101. if self.cap:
  102. self.cap.release()
  103. print(f"[{self.name}] Otwieram RTSP...")
  104. # ograniczamy buforowanie po stronie OpenCV (jeśli jest obsługiwane)
  105. self.cap = cv2.VideoCapture(self.url)
  106. try:
  107. self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
  108. except Exception:
  109. pass
  110.  
  111. if not self.cap.isOpened():
  112. print(f"[{self.name}] ❌ Nie można otworzyć strumienia RTSP!")
  113.  
  114. def _start_new_writer(self, part_code: str, use_prebuffer: bool):
  115. """
  116. Tworzy nowy plik:
  117. - use_prebuffer=True -> przed bieżącymi klatkami dopisuje cały bufor (ostatnie PRE_SECONDS)
  118. - use_prebuffer=False -> tylko od TERAZ, bez cofania
  119. Nazwa: RRRR_MM_DD_GG_MM_SS_KOD_KAMERA.mp4
  120. """
  121. now = time.time()
  122. ts_name = datetime.fromtimestamp(now).strftime("%Y_%m_%d_%H_%M_%S")
  123. safe_code = sanitize_part_code(part_code)
  124. filename = f"{ts_name}_{safe_code}_{self.name}.mp4"
  125. filepath = os.path.join(OUTPUT_DIR, filename)
  126.  
  127. print(f"[{self.name}] Start nagrania: {filename} | prebuffer={use_prebuffer}")
  128.  
  129. writer = cv2.VideoWriter(
  130. filepath,
  131. cv2.VideoWriter_fourcc(*"mp4v"),
  132. self.fps,
  133. (self.width, self.height)
  134. )
  135.  
  136. if not writer.isOpened():
  137. print(f"[{self.name}] ❌ Nie można otworzyć pliku do zapisu!")
  138. return None
  139.  
  140. if use_prebuffer:
  141. frames = list(self.buffer) # snapshot, żeby nie iterować po żywym deque
  142. print(f"[{self.name}] Zapis pre-buffer: {len(frames)} klatek")
  143. for frame in frames:
  144. writer.write(frame)
  145.  
  146. self.recording = True
  147. self.writer = writer
  148. self.post_frames_left = self.post_frames_target # koniec = post_frames_left == 0
  149.  
  150. return writer
  151.  
  152. def trigger(self, part_code: str):
  153. with self.lock:
  154. if self.recording and self.writer:
  155. # przerywamy stare nagranie natychmiast
  156. print(f"[{self.name}] Kończę poprzednie nagranie (nowy trigger).")
  157. try:
  158. self.writer.release()
  159. except Exception:
  160. pass
  161. self.writer = None
  162. self.recording = False
  163. self.post_frames_left = 0
  164.  
  165. # nowe nagranie bez pre-buffer
  166. self._start_new_writer(part_code, use_prebuffer=False)
  167. else:
  168. # pierwsze nagranie po przerwie – z PRE_SECONDS wstecz
  169. self._start_new_writer(part_code, use_prebuffer=True)
  170.  
  171. if self.writer:
  172. print(
  173. f"[{self.name}] Nagrywanie: pre={self.pre_frames} klatek, "
  174. f"post={self.post_frames_target} klatek"
  175. )
  176. else:
  177. print(f"[{self.name}] ❌ Nie udało się rozpocząć nagrania.")
  178.  
  179. def run(self):
  180. while True:
  181. if not self.cap or not self.cap.isOpened():
  182. self._open_stream()
  183. time.sleep(1)
  184. continue
  185.  
  186. ret, frame = self.cap.read()
  187. if not ret:
  188. # nie panikujemy od razu, dajemy trochę błędów pod rząd
  189. self.fail_count += 1
  190. if self.fail_count >= self.max_fail_before_reopen:
  191. print(f"[{self.name}] Za dużo błędów z rzędu, ponowne otwarcie RTSP.")
  192. self._open_stream()
  193. self.fail_count = 0
  194. # krótka pauza, ale bardzo mała
  195. time.sleep(0.01)
  196. continue
  197.  
  198. self.fail_count = 0 # mamy poprawną klatkę
  199.  
  200. with self.lock:
  201. # zawsze aktualizujemy bufor PRE_SECONDS
  202. self.buffer.append(frame.copy())
  203.  
  204. if self.recording and self.writer:
  205. # zapisujemy bieżącą klatkę
  206. self.writer.write(frame)
  207. self.post_frames_left -= 1
  208.  
  209. if self.post_frames_left <= 0:
  210. print(f"[{self.name}] Koniec nagrania (wyczerpane post-klatki).")
  211. try:
  212. self.writer.release()
  213. except Exception:
  214. pass
  215. self.writer = None
  216. self.recording = False
  217. self.post_frames_left = 0
  218.  
  219.  
  220. # ================== TCP SERVER ==================
  221.  
  222. def tcp_server(cameras):
  223. srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  224. srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  225. srv.bind((TCP_HOST, TCP_PORT))
  226. srv.listen(5)
  227. print(f"[TCP] Nasłuch na {TCP_HOST}:{TCP_PORT}")
  228.  
  229. while True:
  230. conn, addr = srv.accept()
  231. with conn:
  232. try:
  233. data = conn.recv(1024)
  234. if not data:
  235. continue
  236. code = data.decode(errors="ignore").strip()
  237. print(f"[TCP] Kod części: {code!r} od {addr}")
  238. if not code:
  239. continue
  240. for cam in cameras:
  241. cam.trigger(code)
  242. except Exception as e:
  243. print("[TCP] Błąd:", e)
  244.  
  245.  
  246. # ================== MAIN ==================
  247.  
  248. def main():
  249. ensure_output_dir()
  250.  
  251. cameras = []
  252. for name, url in CAMERAS.items():
  253. cam = CameraRecorder(name, url)
  254. cam.start()
  255. cameras.append(cam)
  256.  
  257. threading.Thread(target=cleanup_worker, daemon=True).start()
  258. threading.Thread(target=tcp_server, args=(cameras,), daemon=True).start()
  259.  
  260. print("=== SYSTEM NAGRYWANIA (KLATKOWY, ZMINIMALIZOWANE SKIPY) URUCHOMIONY ===")
  261. print("Katalog:", OUTPUT_DIR)
  262. print(f"Pre: {PRE_SECONDS} s, Post: {POST_SECONDS} s, Retencja: {RETENTION_DAYS} dni\n")
  263.  
  264. while True:
  265. time.sleep(1)
  266.  
  267.  
  268. if __name__ == "__main__":
  269. main()
  270.  
Advertisement
Add Comment
Please, Sign In to add comment