Yazılım ve Araçlar··6 dk okuma
RTSP Video Akışını Python'da İşlemek: Gecikme ve Buffer Sorunları
Tespit modeli doğru çalışıyor ama kutu hep hedefin arkasında kalıyorsa sorun modelde değil, okumadığınız karelerde birikiyor.
Saha testinde ilk kez fark ettik: dron hedefin üzerinden geçtikten epey sonra tespit kutusu ekranda belirdi. Model çalışıyordu, kutu doğru yerdeydi — ama o “doğru yer” birkaç saniye önceki dünyaydı. Uçuş sürerken bu fark büyüyordu; ilk saniyelerde göze batmıyor, birkaç dakika sonra görüntü tamamen geçmişte kalıyordu.
Bu, tespit modelinin değil, video okuma katmanının sorunu. RTSP akışı sabit hızda kare üretir; sizin döngünüz ise model çıkarımı yaptığı için daha yavaş dönebilir. Aradaki fark bir yerde birikmek zorunda ve o yer, siz istemeseniz de var olan bir kuyruktur. Kuyruk boşalmadığı sürece cap.read() size en yeni kareyi değil, sıradaki en eski kareyi verir.
Bu yazıda SUAS 2026 aracımızda kullandığımız çözümü anlatıyoruz: SIYI A8 mini gimbal kameradan gelen 1080p RTSP akışını Jetson Orin NX üzerinde okurken gecikmeyi biriktirmeden nasıl tuttuğumuzu, hangi ayarların gerçekten işe yaradığını ve hangilerinin sessizce hiçbir şey yapmadığını.
RTSP nedir, neden dron kamerasında karşımıza çıkıyor
RTSP (Real Time Streaming Protocol), video verisinin kendisini taşımaz; oturumu kontrol eder — “başlat”, “duraklat”, “şu akışı gönder” gibi komutları taşıyan bir protokoldür. Asıl görüntü paketleri RTP (Real-time Transport Protocol) üzerinden akar. Kamera bir RTSP sunucusu çalıştırır, siz bir URL ile bağlanırsınız.
Dron kameralarında yaygın olmasının nedeni pratik: gimbal kamera Ethernet üzerinden companion computer’a bağlanır ve tek bir adresle hem yer istasyonuna hem de tespit koduna aynı akışı sunar. ArduPilot dokümantasyonunda SIYI A8 mini ve ZR10 için verilen adres rtsp://192.168.144.25:8554/main.264, varsayılan IP ise 192.168.144.25.
OpenCV ile akışı açmak
OpenCV’nin FFmpeg arka ucu RTSP’yi doğrudan destekler. FFmpeg tarafına verilecek seçenekler OPENCV_FFMPEG_CAPTURE_OPTIONS ortam değişkeniyle geçirilir. Biçim alışılmadıktır: anahtar ile değeri noktalı virgül, çiftleri birbirinden dikey çizgi ayırır.
import os
# VideoCapture oluşturulmadan ÖNCE ayarlanmalı: OpenCV bu değişkeni
# akışı açarken okur. Biçim: "anahtar;deger|anahtar;deger"
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = (
"rtsp_transport;tcp"
"|fflags;nobuffer" # ilk analiz sırasındaki tamponlamayı azaltır
"|max_delay;300000" # mikrosaniye -> 0.3 s
"|reorder_queue_size;0" # sıra bozulmuş paketleri bekleme
"|probesize;100000" # varsayılan 5.000.000 bayt
"|analyzeduration;500000" # varsayılan 5.000.000 us (5 s)
)
import cv2
URL = "rtsp://192.168.144.25:8554/main.264"
cap = cv2.VideoCapture(URL, cv2.CAP_FFMPEG)
print(cap.isOpened(), cap.get(cv2.CAP_PROP_FRAME_WIDTH), cap.get(cv2.CAP_PROP_FPS))
probesize ve analyzeduration varsayılanları akış açılışını uzatır; düşürmek ilk kareye ulaşma süresini kısaltır. Fazla düşürürseniz FFmpeg akış formatını çözemez — kademeli düşürüp isOpened() sonucunu izleyin.
Asıl problem: kuyruk büyür, görüntü geçmişte kalır
Basit aritmetik: akış 30 FPS (kare başına ~33 ms) üretiyor, sizin döngünüz saniyede 15 kare işliyorsa (kare başına ~67 ms), her saniye 15 kare geride kalırsınız. 10 saniye sonra kuyrukta 150 kare, yani yaklaşık 5 saniyelik gecikme vardır. Bizim uçuş konfigürasyonumuzda YOLO11m 640 piksel tam-kare çıkarımla 15-20 FPS veriyor; akış hızı bunun üzerinde olduğu sürece fark kaçınılmaz olarak birikir.
Önemli nokta: bu gecikme sabit değil, büyüyen bir gecikmedir. Sabit 200 ms’lik bir gecikmeyle yaşayabilirsiniz; her dakika artan bir gecikmeyle otonom bırakma yapamazsınız.

Çözüm: sadece en yeni kareyi tutan okuyucu thread
Doğru zihinsel model şu: akışı kuyruk değil, tek elemanlı bir kutu olarak ele alın. Arka planda bir thread durmadan okur ve her yeni kareyi bir öncekinin üzerine yazar. İşleme döngüsü kutuya baktığında her zaman en güncel kareyi bulur; arada kaçan kareler sessizce düşer. Bir tespit sisteminde bu tam olarak istediğimiz davranıştır — kare kaybı, güncelliğe tercih edilir.
import threading
import time
import cv2
class FreshestFrame:
"""RTSP akışını arka planda okur, yalnızca EN SON kareyi tutar.
Kuyruk yok: her yeni kare bir öncekinin üzerine yazılır. İşleme
yavaşsa eski kareler düşürülür, böylece gecikme birikmez.
"""
def __init__(self, url, backend=cv2.CAP_FFMPEG, reconnect_delay=2.0):
self.url = url
self.backend = backend
self.reconnect_delay = reconnect_delay
self._lock = threading.Lock()
self._frame = None # en son kare (BGR ndarray)
self._seq = 0 # sayaç: aynı kareyi iki kez işlemeyi engeller
self._stamp = 0.0 # karenin yakalandığı an (monotonic)
self._running = True
self._cap = self._open()
self._thread = threading.Thread(target=self._loop, daemon=True)
self._thread.start()
def _open(self):
cap = cv2.VideoCapture(self.url, self.backend)
# FFMPEG arka ucunda etkisiz, GStreamer/V4L2 gibi arka uçlarda işe yarar.
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
return cap
def _loop(self):
fails = 0
while self._running:
if self._cap is None or not self._cap.isOpened():
time.sleep(self.reconnect_delay)
self._cap = self._open()
continue
ok, frame = self._cap.read()
if not ok:
fails += 1
if fails >= 5: # geçici hata değil, bağlantı kopmuş
self._cap.release()
self._cap = None
fails = 0
continue
fails = 0
with self._lock:
self._frame = frame
self._seq += 1
self._stamp = time.monotonic()
def read(self, last_seq=0, timeout=1.0):
"""En son kareyi döndürür; last_seq verilirse YENİ bir kare bekler."""
deadline = time.monotonic() + timeout
while True:
with self._lock:
if self._frame is not None and self._seq > last_seq:
return self._seq, self._frame, self._stamp
if time.monotonic() > deadline:
return last_seq, None, 0.0
time.sleep(0.002)
def release(self):
self._running = False
self._thread.join(timeout=2.0)
if self._cap is not None:
self._cap.release()
Kullanımı, kare yaşını ölçerek:
stream = FreshestFrame("rtsp://192.168.144.25:8554/main.264")
seq = 0
try:
while True:
seq, frame, stamp = stream.read(last_seq=seq, timeout=2.0)
if frame is None:
continue # akış yok, döngü boşa dönmesin
age_ms = (time.monotonic() - stamp) * 1000.0
results = model.predict(frame, imgsz=640, conf=0.25, verbose=False)
print(f"kare {seq} | işlemeye başlarken yaş: {age_ms:.0f} ms")
finally:
stream.release()
O age_ms satırı yazının en değerli parçası olabilir: gecikmeyi tahmin etmeyi bırakıp ölçmeye başlarsınız. Sabit bir sayı görüyorsanız iyisiniz; sürekli artan bir sayı görüyorsanız hâlâ bir yerde kuyruk var.
CAP_PROP_BUFFERSIZE neden çoğu zaman işe yaramıyor
İnternette en sık önerilen çözüm cap.set(cv2.CAP_PROP_BUFFERSIZE, 1). OpenCV 4.x kaynağında FFmpeg arka ucunun setProperty fonksiyonu bu özelliği hiç ele almaz — yalnızca CAP_PROP_POS_MSEC, CAP_PROP_POS_FRAMES, CAP_PROP_POS_AVI_RATIO, CAP_PROP_FORMAT ve CAP_PROP_CONVERT_RGB işlenir. Yani RTSP + FFmpeg kombinasyonunda çağrı sessizce hiçbir şey yapmaz ve set() dönüş değeri False gelir.
Yine de yukarıdaki sınıfta bıraktık: aynı kod bir USB kamerayla (V4L2) veya GStreamer arka ucuyla çalıştırıldığında etkili oluyor. Kural şu: set() çağrılarının dönüş değerini kontrol edin. OpenCV desteklenmeyen özellikte hata atmaz, sadece False döner.
FFmpeg seçenekleri ve TCP/UDP kararı
| Seçenek | Ne yapar | Ne zaman kullanılır |
|---|---|---|
rtsp_transport |
Taşıma katmanını seçer: udp, tcp, udp_multicast, http, https |
Varsayılanı bilerek değiştirmek istediğinizde |
rtsp_flags;prefer_tcp |
TCP varsa önce onu dener | OpenCV zaten bunu varsayılan yapıyor |
fflags;nobuffer |
Akış analizi sırasındaki tamponlamayı azaltır | Her canlı akışta |
max_delay |
Demux gecikme üst sınırı (mikrosaniye) | Kayıp pakete uzun süre takılmasın istediğinizde |
reorder_queue_size |
Sırası bozulmuş paketler için tampon paket sayısı | UDP’de kayıp yerine güncellik istediğinizde |
buffer_size |
Soket tamponunun bayt cinsinden üst sınırı | Yüksek bit hızlı akışlarda paket düşüyorsa |
timeout |
Soket TCP G/Ç zaman aşımı (mikrosaniye) | Kopan bağlantıda sonsuza kadar beklememek için |
OpenCV, rtsp:// adreslerinde libavformat sürümüne göre varsayılan olarak rtsp_flags=prefer_tcp (yeni sürümler) ya da rtsp_transport=tcp (eski sürümler) uygular. Yani hiçbir şey yapmazsanız zaten TCP’desiniz.
| TCP | UDP | |
|---|---|---|
| Paket kaybı | Yeniden gönderilir, kare bozulmaz | Kayıp kalır, blok artefaktı görülür |
| Gecikme davranışı | Kayıp anında gecikme sıçrar | Kayıpta gecikme sabit kalır |
| Ağ geçidi/NAT | Genelde sorunsuz | Bloke edilebilir |
| Ne zaman | Kablolu Ethernet, kayıp az; ayrıca kayıt alacaksanız | Telsiz/paylaşımlı bağlantı, güncellik kare bütünlüğünden önemliyse |
Dronda gimbal ile companion computer arasında kablolu Ethernet olduğu için biz TCP’de kaldık; kayıp zaten yok denecek kadar azken UDP’nin sağladığı avantaj ortadan kalkıyor.
Yeniden bağlanma: uçuşta kaçınılmaz
Kamera yeniden başlar, kablo mikro-kesinti yaşar, switch bir paket yutar. Yukarıdaki _loop bunu üstlenir: art arda 5 başarısız read() sonrası capture nesnesini kapatır, sonraki turda yeniden açar. Tek False dönüşünde bağlantıyı kapatmamak önemli — geçici kare hataları normaldir.
Bir uyarı: OpenCV’nin read() ve açılış çağrıları bloke edicidir ve varsayılan bekleme süreleri uzundur. CAP_PROP_OPEN_TIMEOUT_MSEC ve CAP_PROP_READ_TIMEOUT_MSEC bunu kısaltır, ancak ikisi de “yalnızca açılışta” ayarlanabilir: cv2.VideoCapture(url, cv2.CAP_FFMPEG, [cv2.CAP_PROP_OPEN_TIMEOUT_MSEC, 5000]) biçiminde parametre listesiyle verilmelidir; sonradan set() ile değişmez. Bazı opencv-python sürümlerinde bu sabitler açığa çıkmıyor, hasattr(cv2, "CAP_PROP_OPEN_TIMEOUT_MSEC") ile kontrol edin.
Sık düşülen tuzaklar
cap.set(CAP_PROP_BUFFERSIZE, 1)yazıp sorunu çözdüğünü sanmak. FFmpeg arka ucunda etkisizdir; dönüş değerini yazdırın.- Ortam değişkenini
cv2.VideoCaptureçağrısından sonra ayarlamak. Değişken akış açılırken okunur, sonradan yapılan değişiklik o oturumu etkilemez. - Ayraçları karıştırmak.
rtsp_transport;tcp|fflags;nobufferdoğru; virgül veya=kullanmak seçeneği sessizce yok sayar. - Kodek seviyesi bayrakları demuxer sözlüğüne koymak.
OPENCV_FFMPEG_CAPTURE_OPTIONSiçeriği akış açılışına (avformat_open_input) gider.ffplay -flags low_delaygibi kodek bayraklarının bu yoldan geçtiğini varsaymayın; etkisini ölçmeden kabul etmeyin. - Kuyruğu
grab()döngüsüyle “temizlemeye” çalışmak. Sabit sayıda kare atmak yalnızca o anki hız farkına uyar; hız değişince ya gecikme geri gelir ya da CPU’yu boşa yakarsınız. Thread yaklaşımı kendi kendine ayarlanır. - Kare yaşını hiç ölçmemek. Gözle “gecikmeli görünüyor” demek yerine tek satır zaman damgası ekleyin; hangi değişikliğin işe yaradığını ancak böyle görürsünüz.
- Jetson’da yazılım kod çözücüde kalmak. Orin NX’te donanım kod çözücüyü kullanan bir GStreamer hattı (
rtspsrc latency=0 ! rtph264depay ! h264parse ! nvv4l2decoder ! nvvidconv ! video/x-raw,format=BGRx ! videoconvert ! video/x-raw,format=BGR ! appsink drop=true max-buffers=1) CPU’yu tespit modeline bırakır;appsink’tekidrop=true max-buffers=1de aynı “en yeni kare” mantığını hattın içine taşır.
Pratik özet
- Akışı ana döngüde okumayın. Arka planda okuyup tek elemanlı kutuda en yeni kareyi tutan bir thread kurun; kare düşürmek gecikmeye tercih edilir.
- Her karenin yakalanma zamanını saklayın ve işlemeye başlarken yaşını milisaniye cinsinden yazdırın. Sabit bir sayı iyi, artan bir sayı hâlâ kuyruk var demektir.
- FFmpeg seçeneklerini
OPENCV_FFMPEG_CAPTURE_OPTIONSile, capture nesnesini oluşturmadan önce,anahtar;deger|anahtar;degerbiçiminde verin.probesizeveanalyzedurationdüşürmek ilk kareye ulaşmayı hızlandırır. - Kablolu Ethernet’te TCP’de kalın (OpenCV zaten varsayılan olarak TCP tercih ediyor). UDP’yi ancak telsiz/paylaşımlı bir bağlantıda ve kare bütünlüğünden vazgeçebiliyorsanız düşünün.
- Yeniden bağlanmayı baştan yazın: tek
read()hatasında değil, art arda birkaç hatadan sonra capture’ı kapatıp yeniden açın; açılış/okuma zaman aşımlarını constructor parametresiyle kısaltın.
- rtsp
- opencv
- python
- gecikme
- video-isleme