用 Python 读取 RTSP 视频流并保存备份
更新日期:
做家用 / 小型监控时,摄像头(海康、萤石、大华等)几乎都提供 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 等服务端配置 |
连接时客户端和服务端的大致交互:
OPTIONS—— 询问服务端支持哪些方法;DESCRIBE—— 服务端返回 SDP(会话描述),里面有编码(H.264/H.265)、时钟基、负载类型等;SETUP—— 协商传输方式(UDP 或 TCP);PLAY—— 开始推拉 RTP 数据;- 持续收 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 | import os |
要点:
- 走 TCP 通过环境变量
OPENCV_FFMPEG_CAPTURE_OPTIONS设置; - 用首帧反查分辨率,别硬编码;
read()返回False要当作断流处理——不要回退到本地摄像头(VideoCapture(0)),否则「以为在录监控,其实在录自己屏幕」。
方案二:ffmpeg 子进程转封装(备份首选)
如果目的是「把流原样存成文件」,没必要解码再编码——用 -c copy 直接把 RTP 里的 H.264 包搬进 MP4,零重编码、CPU 占用极低、速度极快。这也是摄像头历史录像备份的标准做法。
1 | import subprocess |
需要按「固定时长分段」录制时,加 -t <秒> 限制录制时长;需要在收到数据前就超时(防卡死),外层用 subprocess.run(..., timeout=...) 或 Popen + poll。
下载摄像头里的历史回放录像
很多摄像头支持用 RTSP 拉指定时间段的历史录像,URL 上带 starttime / endtime:
1 | def build_playback_url(ip, user, pwd, start, end, port=554): |
然后对每个时间段复用上面的 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 子进程,就能稳定地把摄像头的实时流和历史录像都备份到本地磁盘。