4、读取IP摄像头(RTSP):RTSP协议简介、OpenCV连接RTSP流、处理网络延迟与丢包

好,咱们进入第四章。这一章聊的是IP摄像头,也就是通过网络传输的视频流。说实话,我刚开始做嵌入式视觉那会儿,最头疼的就是跟IP摄像头打交道。USB摄像头插上就能用,但IP摄像头?嗯,它得先“聊”上协议才行。

4.1 RTSP协议简介

RTSP,全称Real Time Streaming Protocol,实时流传输协议。说白了,它就是个“遥控器”。它不负责传视频数据本身,而是负责控制——比如“播放”、“暂停”、“快进”。

你想想看,你对着摄像头说“开始传数据”,RTSP就是干这个的。真正传视频画面的,是RTP(Real-time Transport Protocol)协议。RTSP负责建立会话,RTP负责搬运数据。

一个典型的RTSP地址长这样:

rtsp://admin:password@192.168.1.100:554/stream1

拆开来看:

  • rtsp:// —— 协议头,告诉系统用RTSP协议
  • admin:password —— 用户名和密码,很多摄像头默认就是这个
  • 192.168.1.100 —— 摄像头的IP地址
  • 554 —— 端口号,RTSP默认端口就是554
  • /stream1 —— 流路径,有些摄像头有主码流和子码流之分
小提示: 我建议你拿到摄像头后,先查一下厂商文档。不同品牌的RTSP地址格式差别挺大。海康、大华、宇视,每家都有自己的“方言”。

4.2 OpenCV连接RTSP流

OpenCV连接RTSP流,其实就一行代码的事。但这一行背后,坑不少。

先看最基本的写法:

import cv2

# RTSP地址
rtsp_url = "rtsp://admin:123456@192.168.1.100:554/h264/ch1/main/av_stream"

# 打开视频流
cap = cv2.VideoCapture(rtsp_url)

# 检查是否打开成功
if not cap.isOpened():
    print("无法连接到摄像头")
    exit()

while True:
    ret, frame = cap.read()
    if not ret:
        print("读取帧失败")
        break
    
    cv2.imshow("RTSP Stream", frame)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

看起来很简单对吧?但我在项目中遇到过,这段代码在弱网环境下根本跑不起来。为什么呢?因为cap.read()是阻塞的,网络一卡,整个程序就卡死了。

注意: 默认的cv2.VideoCapture对RTSP的支持依赖于OpenCV编译时是否开启了FFmpeg或GStreamer后端。如果你用的是pip安装的OpenCV,大概率是带FFmpeg的。但如果你自己编译OpenCV,记得加上-D WITH_FFMPEG=ON

4.3 处理网络延迟与丢包

好,重点来了。网络延迟和丢包,这是IP摄像头最让人头疼的问题。我曾经在一个项目中,摄像头装在厂区东门,服务器在西门,中间隔了三堵墙。画面延迟能到5秒以上,这谁受得了?

怎么解决?我总结了几条实战经验:

4.3.1 设置缓冲区大小

OpenCV默认会缓存一些帧,导致你读到的是几秒前的画面。我们可以手动调小缓冲区:

cap = cv2.VideoCapture(rtsp_url)

# 设置缓冲区大小为1,减少延迟
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)

嗯,这里要注意,CAP_PROP_BUFFERSIZE这个参数不是所有后端都支持。如果设置无效,可以试试另一种方法——自己维护一个队列,只取最新帧。

4.3.2 多线程读取

把读取帧的操作放到独立线程里,主线程只负责显示或处理。这样即使网络卡了,主线程也不会卡死。

import threading
import queue

frame_queue = queue.Queue(maxsize=2)

def read_rtsp():
    cap = cv2.VideoCapture(rtsp_url)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
    while True:
        ret, frame = cap.read()
        if ret:
            # 如果队列满了,丢弃旧帧
            if frame_queue.full():
                try:
                    frame_queue.get_nowait()
                except queue.Empty:
                    pass
            frame_queue.put(frame)

# 启动读取线程
thread = threading.Thread(target=read_rtsp, daemon=True)
thread.start()

while True:
    if not frame_queue.empty():
        frame = frame_queue.get()
        cv2.imshow("RTSP Stream", frame)
    
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break
核心思路: 用队列做缓冲,只保留最新的1-2帧。这样即使网络抖动,你看到的也是尽可能新的画面。

4.3.3 调整传输协议

RTSP底层可以用TCP或UDP。UDP延迟低但容易丢包,TCP稳定但延迟稍高。你可以通过URL参数指定:

# 使用TCP传输(更稳定)
rtsp_url_tcp = "rtsp://admin:123456@192.168.1.100:554/stream1?tcp"

# 使用UDP传输(延迟更低)
rtsp_url_udp = "rtsp://admin:123456@192.168.1.100:554/stream1?udp"

我个人习惯在局域网内用UDP,延迟低。跨网段或者WiFi环境下,用TCP更靠谱。

4.3.4 丢包检测与重连

网络不可能永远稳定。我曾经遇到过摄像头半夜掉线,第二天早上才发现。所以,一定要加心跳检测和自动重连机制。

import time

def connect_with_retry(rtsp_url, max_retries=5):
    for i in range(max_retries):
        cap = cv2.VideoCapture(rtsp_url)
        if cap.isOpened():
            print(f"连接成功,重试次数:{i}")
            return cap
        print(f"连接失败,第{i+1}次重试...")
        time.sleep(2)
    return None

cap = connect_with_retry(rtsp_url)
if cap is None:
    print("无法连接到摄像头,请检查网络")

4.4 实战建议总结

问题 解决方案 适用场景
画面延迟大 调小缓冲区、用UDP传输 局域网、实时性要求高
画面卡顿 多线程读取、丢帧策略 弱网环境、WiFi
摄像头掉线 心跳检测、自动重连 7x24小时运行
连接失败 检查RTSP地址、用户名密码、端口 初次部署
避坑指南: 我曾经在一个项目里,摄像头死活连不上,折腾了两天。最后发现是摄像头固件版本太老,不支持RTSP over TCP。升级固件后一切正常。所以,拿到新摄像头,先升级固件,能省很多事。

好了,这一章就到这里。IP摄像头接入其实不难,难的是在各种网络环境下稳定运行。记住一句话:网络不可靠,代码要可靠。下一章咱们聊聊如何对视频帧做预处理,比如缩放、裁剪、颜色空间转换这些基本功。