文章目录
  1. 1. RTSP 协议简介
  2. 2. Python 里可用的代码库
  3. 3. 方案一:OpenCV 实时读取并保存
  4. 4. 方案二:ffmpeg 子进程转封装(备份首选)
    1. 4.1. 下载摄像头里的历史回放录像
  5. 5. 踩坑经验
    1. 5.1. 1. 回放流时间戳错乱:视频只有十几秒、播放快进
    2. 5.2. 2. UDP 花屏 / 绿块 → 强制 TCP
    3. 5.3. 3. 事件录像设备:不能按固定时间窗口下载
    4. 5.4. 4. 凭证与地址里的特殊字符要 URL 编码
    5. 5.5. 5. 没有 ffprobe 时,用 ffmpeg 读时长
    6. 5.6. 6. 断流与超时要显式处理
    7. 5.7. 7. 多实例 / 回放通道数限制
  6. 6. 小结

做家用 / 小型监控时,摄像头(海康、萤石、大华等)几乎都提供 RTSP 视频流。用 Python 把 RTSP 流读出来、实时保存成文件、或者把摄像头 SD 卡里的历史录像拉下来备份,是很常见的需求。这篇文章梳理一下:RTSP 是什么、Python 里有哪些库可选、两种典型实现(OpenCV 实时录制、ffmpeg 子进程转封装)的示例代码,以及一路踩下来的坑。

文中涉及的摄像头地址一律用 192.168.1.* 占位,不包含任何真实凭证。

RTSP 协议简介

RTSP(Real Time Streaming Protocol,实时流传输协议) 是一套用来「控制」流媒体服务器的网络协议,本身不传输音视频数据,只负责「播放、暂停、快进」这类控制信令,默认端口 554。真正的媒体数据走 RTP(实时传输协议),控制与统计走 RTCP。可以粗略理解为:RTSP 是「遥控器」,RTP 是「画面本身」。

一条 RTSP 地址长这样:

1
rtsp://<用户名>:<密码>@<IP>:554/<码流路径>

不同厂商的码流路径不一样,常见的有:

厂商 / 协议 主码流路径示例 说明
海康 / 萤石 ISAPI /Streaming/tracks/101 101 = 通道1主码流,102 = 子码流
海康 / 萤石 GB28181 风格 /h264/ch1/main/av_stream main 主码流,sub 子码流
大华 /cam/realmonitor?channel=1&subtype=0 subtype=0 主码流,1 子码流
通用 RTSP 服务器 /live、/stream1 等 取决于 MediaMTX / Live555 等服务端配置

连接时客户端和服务端的大致交互:

  1. OPTIONS —— 询问服务端支持哪些方法;
  2. DESCRIBE —— 服务端返回 SDP(会话描述),里面有编码(H.264/H.265)、时钟基、负载类型等;
  3. SETUP —— 协商传输方式(UDP 或 TCP);
  4. PLAY —— 开始推拉 RTP 数据;
  5. 持续收 RTP 包,直到 TEARDOWN 断开。

两个关键点会反复在「踩坑」里出现:

  • 传输协议分 UDP / TCP:UDP 低延迟但可能丢包(花屏、绿块),TCP 可靠但有延迟、握手慢。公网 / NAT 环境通常必须用 TCP。
  • 时钟基(timescale):RTP 时间戳是相对某个起点的累计值。摄像头回放流的起点常常是「开机以来」,累计值会非常大,直接封装成 MP4 容易时长错乱——这是后面要重点讲的一个坑。

Python 里可用的代码库

读 RTSP 没有唯一标准库,主流是下面几条路线:

库 方式 优点 缺点 / 适用
opencv-python (cv2) cv2.VideoCapture(rtsp_url) 一帧帧拿 numpy 数组,便于做检测 / 处理后再存 封装参数难调,长时间录制稳定性一般;对回放流支持弱
ffmpeg 命令行 + subprocess 起 ffmpeg 子进程拉流 / 转封装 最稳、参数最全、支持转封装(-c copy 不重编码)、可回放下载 需要系统装 ffmpeg(或 imageio-ffmpeg 自带)
imageio-ffmpeg 提供 ffmpeg 可执行文件路径 免安装、跨平台,get_ffmpeg_exe() 拿路径 只是「自带 ffmpeg」,仍需自己拼命令
PyAV Python 绑定 ffmpeg 库 可编程控制、逐包处理、可读写容器 学习曲线陡,部署依赖重
python-ffmpeg 异步封装 ffmpeg 异步友好 生态相对小

经验上:要做画面分析 / 实时处理 → OpenCV;要稳定录制、备份、转封装、下载历史回放 → 直接调 ffmpeg。下面分别给示例。

方案一:OpenCV 实时读取并保存

适合「边读边检测(人形 / 车牌),检测到了再录」这类场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import os
import cv2

# 关键:让 OpenCV 底层的 FFmpeg 走 TCP,避免 UDP 丢包花屏
os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = "rtsp_transport;tcp"

RTSP_URL = "rtsp://admin:password@192.168.1.108:554/h264/ch1/main/av_stream"

cap = cv2.VideoCapture(RTSP_URL, cv2.CAP_FFMPEG)
if not cap.isOpened():
raise RuntimeError(f"无法打开 RTSP 流: {RTSP_URL}")

# 读一帧拿真实尺寸 / 帧率,比拍脑袋写 1920x1080 更稳
ok, frame = cap.read()
if not ok:
raise RuntimeError("首帧读取失败")
h, w = frame.shape[:2]
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0

fourcc = cv2.VideoWriter_fourcc(*"mp4v")
writer = cv2.VideoWriter("out.mp4", fourcc, fps, (w, h))

try:
while True:
ok, frame = cap.read()
if not ok: # 读不到帧 = 流断了,应重连而非静默结束
print("读帧失败,流可能断开")
break
writer.write(frame)
cv2.imshow("rtsp", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
finally:
cap.release()
writer.release()
cv2.destroyAllWindows()

要点:

  • 走 TCP 通过环境变量 OPENCV_FFMPEG_CAPTURE_OPTIONS 设置;
  • 用首帧反查分辨率,别硬编码;
  • read() 返回 False 要当作断流处理——不要回退到本地摄像头(VideoCapture(0)),否则「以为在录监控,其实在录自己屏幕」。

方案二:ffmpeg 子进程转封装(备份首选)

如果目的是「把流原样存成文件」,没必要解码再编码——用 -c copy 直接把 RTP 里的 H.264 包搬进 MP4,零重编码、CPU 占用极低、速度极快。这也是摄像头历史录像备份的标准做法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import subprocess
import imageio_ffmpeg

ffmpeg = imageio_ffmpeg.get_ffmpeg_exe() # 免安装的 ffmpeg
rtsp = "rtsp://admin:password@192.168.1.108:554/h264/ch1/main/av_stream"
out = "backup.mp4"

cmd = [
ffmpeg,
"-rtsp_transport", "tcp", # TCP,公网/NAT 必备
"-i", rtsp,
"-c", "copy", # 不重编码,直接封装
"-y",
out,
]
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
print("ffmpeg 失败:", proc.stderr[-500:])

需要按「固定时长分段」录制时,加 -t <秒> 限制录制时长;需要在收到数据前就超时(防卡死),外层用 subprocess.run(..., timeout=...) 或 Popen + poll。

下载摄像头里的历史回放录像

很多摄像头支持用 RTSP 拉指定时间段的历史录像,URL 上带 starttime / endtime:

1
2
3
4
5
def build_playback_url(ip, user, pwd, start, end, port=554):
# 时间格式示例:20261011T090601Z
fmt = lambda dt: dt.strftime("%Y%m%dT%H%M%SZ")
return (f"rtsp://{user}:{pwd}@{ip}:{port}"
f"/Streaming/tracks/101?starttime={fmt(start)}&endtime={fmt(end)}")

然后对每个时间段复用上面的 ffmpeg 命令下载即可,文件名按实际起止时间命名(如 090601_092417.mp4)。

踩坑经验

这些都是实际备份家用摄像头录像时一个个踩出来的。

1. 回放流时间戳错乱:视频只有十几秒、播放快进

拉回放流(带 starttime/endtime 的历史录像)时,RTP 的时间戳起点是「摄像头开机以来」,累计值可能是几十个小时、时钟基 90000。直接封装成 MP4,容器按这个超大基算时长,结果就是:文件只有十几秒、播放飞速、fps 显示上万。

修复:ffmpeg 命令在 -i 之前加 -use_wallclock_as_timestamps 1,用系统墙钟重写时间戳:

1
ffmpeg -use_wallclock_as_timestamps 1 -rtsp_transport tcp -i "rtsp://..." -c copy out.mp4

注意参数位置——必须在 -i 之前,它作用于输入。

2. UDP 花屏 / 绿块 → 强制 TCP

局域网内 UDP 通常没问题,但跨网段、Wi-Fi、公网、NAT 后面,UDP 丢包会表现为花屏、绿块、间歇性卡死。统一加 -rtsp_transport tcp(或 OpenCV 的环境变量)最省心,代价是延迟略高、对录制无影响。

3. 事件录像设备:不能按固定时间窗口下载

不少家用摄像头是事件(移动侦测 / 报警)录像,不是全天连续——一天里是多段、中间有大段空白。这时如果你给一个「1 小时宽窗口」的回放 URL,摄像头只返回窗口内第一段就停(遇到空白就结束),于是大量片段漏掉,备份出来全是残缺的十几秒文件。

正确做法:先通过厂商开放平台 / ONVIF 枚举当天全部片段的精确起止时间,再逐段构造回放 URL 下载。判断某一天有没有录像,先对「全天」做一次探测或一次 API 查询,返回空就直接跳过,别逐小时空转。

4. 凭证与地址里的特殊字符要 URL 编码

密码里若有 @ : / ? & 等字符,直接拼进 URL 会把解析搞乱(@ 会被当成用户名/主机分隔符)。要么对密码做 URL 编码,要么用 ffmpeg 的 -rtsp_transport 之外的鉴权参数。调试时打印 URL 要把密码脱敏成 ****,但真正执行的命令里必须是真实地址,否则会 401。

5. 没有 ffprobe 时,用 ffmpeg 读时长

imageio-ffmpeg 只自带 ffmpeg,没有 ffprobe。要校验一个 mp4 是否完好、时长多少,用:

1
ffmpeg -hide_banner -i out.mp4   # stderr 里抓 Duration: 00:00:55.83

备份脚本里可据此判断下载是否成功(文件存在但 Duration 为 0 / 读不出流 = 失败,删掉重试或跳过)。

6. 断流与超时要显式处理

RTSP 会断、摄像头会重启、网络会抖。subprocess.run 给 timeout,OpenCV 的 read() 失败要重连。失败时分层报错(连接失败 vs 读取失败),并确保失败后不会悄悄切到别的源。

7. 多实例 / 回放通道数限制

很多家用摄像头同时只允许 1~2 路回放流。两个备份进程并发拉同一台摄像头,往往一路拿到空文件。需要多路备份时尽量串行,或错开时间。

小结

  • RTSP 是「控制协议」,数据走 RTP,端口 554,传输分 UDP/TCP;
  • Python 读取:画面分析用 OpenCV,稳定录制 / 备份 / 回放下载用 ffmpeg 子进程,-c copy 不重编码最省 CPU;
  • 回放下载务必加 -use_wallclock_as_timestamps 1 修时间戳,强制 TCP 防花屏;
  • 事件录像设备要先枚举片段再逐段下载,不能用固定时间窗口;
  • 凭证脱敏、特殊字符编码、断流重连、无 ffprobe 时用 ffmpeg -i 读时长,这些细节决定脚本能不能长期稳定跑。

把这些坑避开后,一个 while 循环 + ffmpeg 子进程,就能稳定地把摄像头的实时流和历史录像都备份到本地磁盘。

文章目录
  1. 1. RTSP 协议简介
  2. 2. Python 里可用的代码库
  3. 3. 方案一:OpenCV 实时读取并保存
  4. 4. 方案二:ffmpeg 子进程转封装(备份首选)
    1. 4.1. 下载摄像头里的历史回放录像
  5. 5. 踩坑经验
    1. 5.1. 1. 回放流时间戳错乱:视频只有十几秒、播放快进
    2. 5.2. 2. UDP 花屏 / 绿块 → 强制 TCP
    3. 5.3. 3. 事件录像设备:不能按固定时间窗口下载
    4. 5.4. 4. 凭证与地址里的特殊字符要 URL 编码
    5. 5.5. 5. 没有 ffprobe 时,用 ffmpeg 读时长
    6. 5.6. 6. 断流与超时要显式处理
    7. 5.7. 7. 多实例 / 回放通道数限制
  6. 6. 小结