工业监控RTSP取流-实践指南

导读 :产线上把监控画面接到工控机,经常被写成一行 VideoCapture。一路还好,多路、掉线、换品牌之后这行就不够用了。监控取流是四层决策叠在一起:选什么相机、电口还是网口、像素怎么进进程、C++ 怎么管线程和重连。本文按这四层写完,并给出一份只依赖 FFmpeg 的最小封装:配置与界面分开,一路一个线程,断线自己重连。


前言

「监控取流」不是单一 API 调用。现场要先回答四件事:

  1. 设备层:选什么相机。
  2. 链路层:电口 / 网口怎么进工控机。
  3. 协议层:像素怎么从相机到进程(RTSP 或其它)。
  4. 软件层:C++ 如何封装线程、重连、显示,以及和业务解耦。

给人看的实时画面,和给算法测距 / 定位,通常是两条链路。监控优先网络摄像机(IPC)加 RTSP;检测相机另选工业视觉标准,不要用监控 IPC 硬扛尺寸精度,也不要把 GigE 检测相机塞进监控预览页。

下面按这四层往下写。示例工程分两层:库 rtsp_link 负责拉流,界面 rtsp_demo 只消费最新一帧。


一、相机 / 摄像头怎么选

1、先定场景,再定品类

场景 推荐品类 不推荐
产线旁路监控、看工位全貌(本地监控) 网络摄像机 IPC(支持 RTSP) 消费级 USB 摄像头作主方案
近距离调试、实验室 USB UVC 摄像头 当作长期现场方案
高精度尺寸 / 缺陷检测 工业面阵(GigE / USB3 Vision) 用监控 IPC 硬扛检测精度
远程监控上云 网络摄像机 IPC(支持 RTMP) 网络摄像机 IPC(支持 RTSP)

监控页要的是「给人看实时画面」。USB 摄像头线短、多路扩展差,适合实验台,不适合当长期现场方案。

2、厂区本地监控预览

这台可以当室内工位全貌的采购起点:

项 选型
型号 海康威视 DS-2CD2146F(D)WDA1-IS(400 万定焦智能半球)
场景 工位全貌、旁路监控、多路预览页(室内顶装优先)
分辨率 约 2560×1440 @25fps(主码流);预览优先子码流约 720p@15--25fps
匹配点 官方支持 RTSP / ONVIF、H.264/H.265、主/子码流齐全;采购选不带 D 的型号以支持 PoE
外形与安装 半球,适合室内吸顶;相对筒机外形更收敛
接入 固定 IP + 只读监控账号,写入设备表的 url

同像素备选:海螺 DS-2CD2346(F)(D)WDV3-I(S),调角度更方便。协议和取流方式一致,换机主要改 URL。

3、监控 IPC 选型清单

采购规格可以直接按这几条卡:

  • 协议:官方文档明确支持 RTSP,能给出主码流、子码流 URL。
  • 编码:优先 H.264,兼容最好。H.265 省带宽,解码更吃 CPU / GPU。
  • 分辨率 / 帧率:预览用子码流 720p@15--25fps;要看细节再开主码流。
  • 供电与安装:优先 PoE;固定支架、工业温宽。
  • 网络:固定 IP,或 DHCP 预留;和 PLC、工控机同网段可达。
  • 账号:独立只读监控账号。管理员口令不要写进配置文件。

4、可迁移的配置模型

URL 不要写死在代码里。用一份有序设备表:

json 复制代码
{
  "num": 2,
  "device_list": [
    { "enable": true,  "function": "cam_left",  "url": "rtsp://user:pwd@192.168.1.64:554/stream1" },
    { "enable": true,  "function": "cam_right", "url": "rtsp://user:pwd@192.168.1.65:554/stream1" }
  ]
}

使用规则就四条:

  • 只拉 enable=true 的项。
  • 数组顺序就是画面格子顺序。
  • function 是业务名;url 才是开源栈真正打开的地址。
  • UI 格子数有上限时,只取前 N 路,并打日志说明截断。

二、视频进工控机,硬件接口怎么选

接口 典型设备 距离 多路扩展 监控推荐度
千兆以太网 + PoE 交换机 RTSP IPC 长 极易 ★★★★★
USB2 / USB3 UVC / 部分工业相机 短 差 ★★(调试)
GigE Vision 工业相机 中长 好 ★★★(检测)
HDMI / SDI + 采集卡 广播 / 工控显示源 中 一般 ★★
Camera Link / CXP 高速工业相机 短 需采集卡 ★(高速检测)

监控默认方案是:IPC → PoE 交换机 → 工控机网口。

软件侧看到的是网络上的 RTSP URL,不是某个 /dev/video0,也不是采集卡设备名。

选型口诀:

  • 要远、要多路、要可换品牌 → 以太网 + RTSP。
  • 要触发、全局快门、低延迟检测 → GigE / USB3 Vision。这条不要硬套进监控架构。

三、什么是 RTSP

RTSP(Real Time Streaming Protocol)是应用层的流会话控制协议,负责三件事:

  • 描述流(DESCRIBE)
  • 建立传输通道(SETUP)
  • 播放、暂停、拆除(PLAY / TEARDOWN)

它不负责像素字节本身。媒体载荷通常由 RTP / RTCP 传输:

  • UDP:延迟更低,丢包要自己处理。
  • TCP(RTSP interleaved):穿透防火墙更好。工业现场网络不干净时,往往更稳。

一个典型地址:

text 复制代码
rtsp://username:password@192.168.1.64:554/Streaming/Channels/101

路径因厂商而异,海康、大华、华为各不相同。所以 URL 必须可配置,不能写死在代码里。

可以把 RTSP 理解成遥控器,RTP 才是电视信号线。


四、除了 RTSP,还有哪些取流方式

1、常见方式对照

方式 延迟 生态 实现复杂度 典型用途
RTSP + RTP 低~中 IPC 事实标准 中 产线监控
HTTP-FLV / HLS / DASH 中~高 Web 友好 低~中 远程回看、大屏网页
WebRTC 很低 浏览器强 高 互动通话、超低延迟 Web
本机 V4L2 / DirectShow / Media Foundation 低 本机设备 中 USB 摄像头
GigE Vision / USB3 Vision 低 工业视觉 中~高 检测算法取图
厂商私有 SDK 低 绑定品牌 中 要 SDK 独占功能时

2、监控场景仍常选 RTSP

  • 设备通用:绝大多数 IPC 开箱支持。换品牌主要改 URL。
  • 网络化部署:天然多路、跨交换机,和产线布线一致。
  • 传输可选 TCP / UDP:现场网络差时优先 TCP。

WebRTC 和 HLS 解决的是浏览器和公网回看。产线工控机要的是「固定 IP 的 IPC,进程里能拿到帧」,RTSP 仍然是这条路上的通用解。


五、用 FFmpeg 做 C++ 封装

拉流配置和业务界面分开。库不认识 Qt,界面不碰套接字。

参考示例资源链接如下:Rtsp+FFmpeg使用示例资源-CSDN下载

1、两层各管什么

职责 库 rtsp_link 界面 rtsp_demo
依赖 FFmpeg,无 Qt Qt Widgets + rtsp::rtsp_link
配置 RtspConfig 只读、校验、解析多路地址 CameraConfig 调库来读;保存仍由界面写回 JSON
拉流 RtspPuller:一个地址、一条线程、断线自己重连 不碰套接字。地址合法就 start(),按钮不决定连不断
图像 只保留最新一帧 RGB24 FrameCache 收成 QImage;「开始拉流」才画到窗口

调用关系是:

text 复制代码
rtsp.json
    │
    ▼
rtsp_link(纯 C++)
    RtspConfig::load          文件是否存在、JSON 是否合法,失败给出中文原因
    RtspPuller × N            调用方对每个启用设备各建一个;Open → Read → Decode
                              失败则关闭,隔 1 秒再连;只换掉最新 RGB 帧
    │  install/ 里的头文件和 DLL
    ▼
上位机(Qt + C++)
    CameraConfig              当前窗口只用第一个 enable=true 的设备
    FrameCache                显示缓存线程,把 latestFrame() 转成 QImage
    MainWindow                「开始拉流」只决定要不要把这张图画到 VideoWidget

2、依赖与链接边界

FFmpeg 只链进 rtsp_link.dll。上位机不要再链 avformat 等导入库。运行时把对应配置的 FFmpeg DLL 和 rtsp_link.dll(Debug 为 rtsp_linkd.dll)放到 exe 旁边即可。

库 用途 链到哪
libavformat 打开 RTSP、解复用 仅 rtsp_link
libavcodec H.264 / H.265 解码 仅 rtsp_link
libavutil 时间、错误码、地址拆分 仅 rtsp_link
libswscale YUV → RGB24 仅 rtsp_link
Qt Widgets 窗口、按钮、把 RGB 收成 QImage 仅上位机

FFmpeg 来自仓库根目录 ffmpeg_install_7.1.5(7.1.5,用 v142 编译)。Debug 用其中的 debug,其余配置用 release。

上位机只导入已经安装好的库(rtsp_demo/CMakeLists.txt):

cpp 复制代码
find_package(rtsp_link REQUIRED CONFIG
  PATHS "${CMAKE_CURRENT_SOURCE_DIR}/rtsp/install/cmake"
  NO_DEFAULT_PATH)

target_link_libraries(rtsp_demo PRIVATE
  rtsp::rtsp_link
  Qt5::Widgets
)

3.1、读配置 RtspConfig

RtspConfig::load 只读文件,不访问网络,不启动拉流。路径按 UTF-8 传入。程序实际读的是 exe 同目录的 rtsp.json,模板在 rtsp_demo/config/rtsp.json。

json 复制代码
{
  "num": 2,
  "device_list": [
    {
      "enable": true,
      "function": "cam_right",
      "url": "rtsp://user:pwd@192.168.1.65:554/stream1",
      "transport": "tcp"
    }
  ]
}

对外接口在 rtsp_demo/rtsp/include/rtsp_config.h。失败时设备列表为空,loadConfigMessage() 是中文原因:

cpp 复制代码
class RtspConfig {
public:
  bool load(const std::string& utf8Path);
  ConfigLoadStatus status() const;
  std::string loadConfigMessage() const;             // 失败原因,或成功时的路数统计
  std::vector<DeviceEntry> devices() const;    // 文件原顺序,含 enable=false
  std::vector<DeviceEntry> enabledDevices() const;
private:
  class Impl;
  std::unique_ptr<Impl> impl_;
};

实现藏在 Impl 里,头文件只暴露上面这几个调用。解析核心在 rtsp_config.cpp。结构不合法就整份失败;某一路地址为空或格式不对,只记在该路的 addressStatus 上,整份文件仍然成功。enable 缺省为 true;transport 缺省、为空或不是 udp 时为 TCP。

cpp 复制代码
const nlohmann::json root = nlohmann::json::parse(bytes);
if (!root.is_object()) {
  fail(ConfigLoadStatus::RootNotObject, "配置根节点必须是 JSON 对象");
  return false;
}
const nlohmann::json& list = root.at("device_list");
if (!list.is_array()) {
  fail(ConfigLoadStatus::DeviceListNotArray, "device_list 必须是数组");
  return false;
}

for (std::size_t index = 0; index < list.size(); ++index) {
  if (!list.at(index).is_object()) {
    fail(ConfigLoadStatus::DeviceItemInvalid,
         "device_list[" + std::to_string(index) + "] 必须是对象");
    return false;
  }
  const nlohmann::json& item = list.at(index);
  DeviceEntry entry;
  entry.enable = parseEnable(item);          // 缺省 true
  entry.function = parseString(item, "function");
  entry.url = trimCopy(parseString(item, "url"));
  entry.transport = parseTransport(item.contains("transport") ? item.at("transport")
                                                               : nlohmann::json());
  entry.addressStatus = RtspPuller::checkAddress(entry.url);  // 不访问网络
  devices.push_back(std::move(entry));
}

文件不存在、打不开、空文件、JSON 语法错误,都在进入这个循环之前失败,并把原因写进 message。

3.2、一路一线程 RtspPuller

一个实例对应一个地址和一条线程。控制接口从同一线程调用;state、latestFrame、hasFreshFrame 可以在别的线程读。头文件在 rtsp_demo/rtsp/include/rtsp_puller.h:

cpp 复制代码
enum class PullState { Stopped, Connecting, Streaming, Reconnecting };

class RtspPuller {
public:
  void setTransport(Transport transport);     // 默认 TCP,下次连接或重连生效
  bool start(const std::string& url);         // true 只表示线程已启动
  void stop();
  PullState state() const;
  std::shared_ptr<const VideoFrame> latestFrame() const;
  bool hasFreshFrame(int timeoutMs = 2000) const;
  static AddressStatus checkAddress(const std::string& url);  // 不访问网络
private:
  class Impl;
  std::unique_ptr<Impl> impl_;
};

打开、解码、重连都在 RtspPuller::Impl(rtsp_puller.cpp)。打开时强制低延迟,并挂上中断回调,这样 stop() 不用干等套接字超时:

cpp 复制代码
const char* transportName = transport_.load() == Transport::Udp ? "udp" : "tcp";
av_dict_set(&options, "rtsp_transport", transportName, 0);
av_dict_set(&options, "timeout", "2000000", 0);       // 套接字超时,微秒
av_dict_set(&options, "fflags", "nobuffer", 0);
av_dict_set(&options, "max_delay", "500000", 0);
av_dict_set(&options, "reorder_queue_size", "0", 0);

format->interrupt_callback.callback = [](void* opaque) -> int {
  const auto* self = static_cast<const Impl*>(opaque);
  return self->stopRequested_.load() ? 1 : 0;
};
format->interrupt_callback.opaque = this;
// 随后 avformat_open_input → find_stream_info → 打开视频解码器

读失败或打开失败都不退出线程。解出的帧转成 RGB24 后直接换掉上一帧,不排队:

cpp 复制代码
void publishFrame(const AVFrame* frame) {
  // sws_scale 转到 AV_PIX_FMT_RGB24,写入新建的 VideoFrame
  image->stride = frame->width * 3;
  image->timestampMs = RtspPuller::nowMs();
  std::lock_guard<std::mutex> lock(frameMutex_);
  latest_ = std::move(image);              // 调用方拿到的 shared_ptr 不再被改写
  state_.store(PullState::Streaming);
}

void run() {
  state_.store(PullState::Connecting);
  while (!stopRequested_.load()) {
    if (!openInput()) {
      state_.store(PullState::Reconnecting);
      if (!sleepUnlessStopped(1000)) break;   // 停止请求会立刻打断这 1 秒
      continue;
    }
    while (!stopRequested_.load()) {
      const int readError = av_read_frame(fmt_, packet);
      if (readError == AVERROR(EAGAIN)) continue;
      if (readError < 0) break;               // 中断,下面关闭后重连
      if (packet->stream_index == videoIndex_)
        decodePacket(packet, frame);          // 内部 publishFrame()
      av_packet_unref(packet);
    }
    closeInput();
    if (stopRequested_.load()) break;
    state_.store(PullState::Reconnecting);
    if (!sleepUnlessStopped(1000)) break;
  }
  state_.store(PullState::Stopped);
}

不带界面时,多路就是多份 RtspPuller:

cpp 复制代码
rtsp::RtspConfig config;
if (!config.load("rtsp.json")) {
  // config.loadConfigMessage() 即失败原因
  return;
}
for (const rtsp::DeviceEntry& device : config.enabledDevices()) {
  if (device.addressStatus != rtsp::AddressStatus::Ok) continue;
  auto puller = std::make_unique<rtsp::RtspPuller>();
  puller->setTransport(device.transport);
  puller->start(device.url);               // 一路一个实例、一条线程
}

4、界面只消费库

界面不自己判断 JSON 结构是否合法,读配置直接问库。当前窗口记下全部设备,但只用第一个 enable=true 的地址(cameraconfig.cpp):

cpp 复制代码
void CameraConfig::load() {
  path_ = QCoreApplication::applicationDirPath() + "/rtsp.json";
  rtsp::RtspConfig config;
  if (!config.load(toUtf8(path_))) {
    loadMessage_ = fromUtf8(config.loadConfigMessage());   // 顶部红字用这句,不启动拉流
    return;
  }
  for (const rtsp::DeviceEntry& entry : config.devices()) {
    devices_.push_back(/* enable、function、url、transport */);
  }
  // 取第一个 enable == true 作为当前地址
}

点「保存」只改这台的 url,再用 Qt 把整个列表写回 rtsp.json。写文件留在界面,不进库。

窗口构造完就按地址启动库,不等按钮(mainwindow.cpp)。地址格式不合法时不 start(),拉流线程保持停止:

cpp 复制代码
void MainWindow::applyBackend(const QString& url) {
  cache_->stop();
  puller_->stop();
  cache_->clear();
  ui_->videoWidget->clearFrame();

  if (rtsp::RtspPuller::checkAddress(toStdUtf8(savedUrl_)) == rtsp::AddressStatus::Ok) {
    puller_->setTransport(useUdp ? rtsp::Transport::Udp : rtsp::Transport::Tcp);
    puller_->start(toStdUtf8(savedUrl_));      // 后台开始连,与按钮无关
  }
  cache_->start();
}

FrameCache 在自己的线程里取库的最新帧。时间戳没变就不动缓存,避免同一帧被反复拷贝(framecache.cpp):

cpp 复制代码
void FrameCache::storeLatest() {
  const auto frame = puller_->latestFrame();   // RGB24;空指针表示还没有图像
  if (!frame || frame->rgb.empty()) return;
  if (hasImage_ && timestampMs_ == frame->timestampMs) return;

  QImage detached(frame->width, frame->height, QImage::Format_RGB888);
  for (int y = 0; y < frame->height; ++y) {
    const auto* src = frame->rgb.data() + static_cast<size_t>(y) * frame->stride;
    std::memcpy(detached.scanLine(y), src, static_cast<size_t>(frame->stride));
  }
  image_ = std::move(detached);
  hasImage_ = true;
  timestampMs_ = frame->timestampMs;
}

「开始拉流」能点,仅当最近 2 秒内有新图像。正在显示时若超过 2 秒没有新帧,按钮回到灰色的「开始拉流」,画面留下最后画上去的那一帧,顶部红字「相机掉线」。重连后再出图,空闲提示变回绿色「相机已经就绪」。

cpp 复制代码
const bool fresh = puller_ && puller_->hasFreshFrame(2000);
if (displaying_) {
  if (!fresh) {
    displaying_ = false;                       // 只停上屏,库继续重连
    updateStreamButton();
  } else {
    const FrameCache::Snapshot snap = cache_->snapshot();
    if (snap.hasImage) ui_->videoWidget->setFrame(snap.image);
    ui_->videoWidget->setHint(QString(), QColor());
    return;
  }
}
showHint(fresh);   // 配置失败 / 空地址 / 地址错误 / 就绪 / 掉线

按钮管的是「要不要把图画到窗口」,不管「套接字还连不连」。掉线之后库仍在 Reconnecting,画面停在最后一帧,避免预览区闪成空白。


一句话结论

  • 相机:监控选支持 RTSP 的 IPC,H.264,优先 PoE。
  • 接口:千兆以太网。现场不要靠 USB 撑多路。
  • 协议:RTSP 控会话,RTP 传媒体。现场优先 TCP。
  • 替代方案很多,IPC 监控里 RTSP 仍是通用解。
  • C++:这份封装只基于 FFmpeg 自建会话、状态和多路管理。配置可迁移,链接边界清楚,断线重连不把界面线程拖死。
相关推荐
进化中的刀盾兽3 小时前
从零实现 C++ Json-Rpc(八):补齐 detail.hpp——JSON 封装与请求 ID 生成
c++·rpc
longlongzihan3 小时前
LeetCode 41. 缺失的第一个正数:从基础解法到原地哈希
c++·算法·leetcode
VidDown4 小时前
自动给视频挑一张能看的封面:帧评分、构图与批量生成
python·ffmpeg·音视频·视频
Lhan.zzZ4 小时前
Qt/QML:业务记录单展示、签名确认与 PDF 导出开发
c++·qt·pdf
傲世仙尊4 小时前
类和对象上-class与struct实例化内存对齐与this指针
c++
C++ 老炮儿的技术栈4 小时前
C++ const 的3个核心作用
服务器·c语言·c++·算法·c
小黄人软件4 小时前
粘贴助手C++版 支持 Windows 7 / 10 / 11 不会出现缺少api-ms-win-core-path-11-1-0.dll
开发语言·c++·windows
有毒的教程4 小时前
Dev-C++ 避免爆栈(Stack Overflow)完整方案
c++·算法·深度优先
程序员老陆4 小时前
从汇编层面理解 C++ 多线程
开发语言·c++·程序设计