惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
H
Help Net Security
The Cloudflare Blog
Y
Y Combinator Blog
A
Arctic Wolf
Cyberwarzone
Cyberwarzone
G
Google Developers Blog
Recent Announcements
Recent Announcements
S
SegmentFault 最新的问题
Microsoft Security Blog
Microsoft Security Blog
WordPress大学
WordPress大学
博客园 - Franky
罗磊的独立博客
Martin Fowler
Martin Fowler
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 三生石上(FineUI控件)
N
News and Events Feed by Topic
F
Fortinet All Blogs
N
News | PayPal Newsroom
J
Java Code Geeks
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 【当耐特】
M
MIT News - Artificial intelligence
Google Online Security Blog
Google Online Security Blog
Recorded Future
Recorded Future
博客园 - 聂微东
S
Securelist
C
CERT Recently Published Vulnerability Notes
小众软件
小众软件
Cisco Talos Blog
Cisco Talos Blog
S
Security Affairs
NISL@THU
NISL@THU
A
About on SuperTechFans
PCI Perspectives
PCI Perspectives
N
News and Events Feed by Topic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Jina AI
Jina AI
Microsoft Azure Blog
Microsoft Azure Blog
AWS News Blog
AWS News Blog
GbyAI
GbyAI
C
Cyber Attacks, Cyber Crime and Cyber Security
V
Vulnerabilities – Threatpost
D
Docker
P
Proofpoint News Feed
W
WeLiveSecurity
Help Net Security
Help Net Security
The GitHub Blog
The GitHub Blog
The Last Watchdog
The Last Watchdog
The Hacker News
The Hacker News
博客园 - 叶小钗

博客园 - 飞扬青云

Qt电子地图叠加海图/全世界海图叠加到天地图图层/1-18级海图实时查看 超级屌的动态测距交互功能/耗时几万个脑细胞/自研原创地图控件 Qt纯原创代码实现GB28181组件/自动点播拉流并推流/支持录像回放和下载/性能凶残 全网原创再次熬夜升级/支持云台预置位的onvif设备模拟器/虚拟监控摄像头/可设置主码流子码流 使用ffmpeg播放不同节目/实时切换节目并推流/推流不同的节目流 新版监控GB28181调试神器V2026发布/对标wvp/绿色版开箱即用 对标国际Onvif工具odm/新版视频监控onvif调试助手V2026发布/简单好用开箱即用 全网唯一/不依赖浏览器和qlocation/纯代码绘制实现的地图组件/短小精悍 监控设备模拟神器V2026全新含泪发布/支持onvif和gb28181/支持云台控制和预置位 将地图组件运行在浏览器上/qtlocation都做不到/轻巧好用/自研原创 不依赖任何AI代码/纯qpainter实现的航线规划/飞行轨迹规划和模拟/无人机轨迹 如何快速搭建一套支持ONVIF的安防监控系统 没有想不到只有做不到/万能地图标注点marker设计/各种参数需求场景/你要的都有 呕心沥血/历时半年/古法编程/纯QPainter实现的地图控件/性能爆表 绘制1000个无人机只需7ms/优化到极致/地图组件海量标注点/纯QPainter绘制的威力 实时轨迹/轨迹回放/多条轨迹同时回放/轨迹平滑移动/移动速度和间隔/自动旋转角度 记一个优化改良的实施过程/纯QPainter实现的地图组件/支持天地图谷歌地图等 性能提升百倍/Qt地图组件纯QWidget绘制/缓存瓦片技术/多线程加载绘制 纯QWidget绘制实现电子地图控件/非qml非web/多线程下载和加载瓦片/支持各种图形 Qt实现离线地图下载/瓦片地图下载/划区域下载离线地图/多线程批量下载 Qt实现电子地图热力图的绘制/热力图叠加图层/拖曳缩放移动 Qt实现行政区划轮廓图下载/一键批量下载/可编辑/天地图高德地图百度地图 全网首个源码级的跨平台视频监控系统方案/持续迭代更新/视频监控系统轮询管理的优化 Qt/C++开发监控GB28181系统/个人理解/要点总结/经验分享 离线获取任意经纬度海拔高度?一个100行代码的跨平台方案 Qt开发集群地面站管理平台/飞行轨迹动态规划和模拟/自由规划路径/飞行轨迹模拟 记一次Qt视频监控系统的优化/双击打开分组可能崩溃的BUG/排对打开通道过程中关闭通道可能崩溃的BUG Qt优化onvif设备搜索/一键批量搜索/onvif设备模拟器/几千路并发/实时推流/虚拟监控摄像头 Qt/C++地图最简示例/在线离线切换/地图视图切换/执行各种js函数交互 十年磨一剑/支持海康大华宇视华为天地伟业等所有厂家的视频监控系统/支持264和265/硬解和GPU绘制/支持国产麒麟和UOS以及RK3588等环境 祖传独创/全网唯一/Qt结合ffmpeg实现读取ts文件节目流/动态切换多节目/实时切换不同轨道
GB28181视频监控推流服务/点播预览推流/录像回放推流/指令预览推流/指令回放推流/自动预览推流
飞扬青云 · 2026-07-09 · via 博客园 - 飞扬青云

一、前言说明

随着用户数量的增多,没有想过光一个推流还需要分这么多种,最开始只有个点播预览推流,就是直接在设备树双击通道点播画面,点播成功后,自动将这个通道的视音频流数据推出去,比如推到zlm或者mediamtx等,这样外部可以通过rtsp/rtmp/hls/flv/webrtc等方式访问,用久了你会发现,每次这样操作都很累,于是需要有个开关,开启自动推流,每次设备通道上线后,自动点播推流,相当于作为后台服务使用,这样方便是很方便了,但是挺浪费带宽的,只要是上线就推流,白白占用拉流带宽,于是后面有用户要求定义一套交互协议,通过指令来点播预览,而且还有超时时间,超时无人观看,自动关闭点播。后面又加了过滤,如果该通道已经存在点播,不是每个点播指令都点播,直接复用已经点播好了的流地址,返回给点播的用户即可,又精进了很多。

光有实时视频流的点播还是不够的,还有录像回放需要点播,而且切换播放进度或者倍速播放的时候,会立即推流最新的画面,指令回放也支持指定时间段回放录像文件,整个流程走下来,现在已经算是非常完善了,能够满足目前遇到的各种需求场景。

二、效果图

Snipaste_2026-07-04_08-47-08

三、相关地址

  1. 国内站点:https://gitee.com/feiyangqingyun
  2. 国际站点:https://github.com/feiyangqingyun
  3. 个人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 文件地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取码:01jf 文件名:bin_video_28181

四、功能特点

  1. 支持设备注册、注销、心跳、校时、注册认证、注销认证等。
  2. 设备上线后可以手动获取设备状态、设备信息、配置信息、预置位信息等。
  3. 设备上线后自动获取设备通道信息,包括中文通道名称。识别到通道上线离线变化,会重新获取该设备的所有通道信息。
  4. 支持视频点播,可以分别点播主码流和子码流,内置rtp解包线程,解包后发给视频播放组件解码播放。
  5. 每个设备每个通道支持点播多个视频,通过ssrc区分,支持共用端口和不同端口收流。
  6. 支持对某个设备下面所有通道、某个通道、某个通道对应的某个流分别关闭。
  7. 支持录像文件查询和回放,回放控制支持暂停播放、继续播放、倍速播放、切换播放进度。
  8. 支持录像文件下载,支持倍速比如8倍速下载,可同时多线程批量下载。
  9. 回放和下载同时支持IPC和NVR,比如摄像头自带的SD存储卡录像文件回放,NVR上的硬盘录像文件回放。
  10. 支持云台控制,向上、向下、向左、向右、左上、右上、左下、右下方位移动,镜头放大缩小,光圈放大缩小,镜头聚焦放焦。
  11. 支持预置位信息的查询、调用、添加、修改、删除等操作。
  12. 自动目录订阅功能,通道上线下线都有对应的信号通知。
  13. 内置定时读取通道信息机制,以保证通道信息是最新的,比如有些NVR是不断更新的通道信息。
  14. 内置订阅警情和位置移动功能,订阅后各种警情事件比如运动目标检测报警、入侵检测报警、徘徊检测报警等自动上报。
  15. 支持语音对讲功能,可以直接在视频窗体的悬浮条上单击语音对讲按钮,再次单击关闭对讲,对讲期间悬浮条常驻显示。
  16. 支持设备布防撤防,布防后警情信息会主动上报。
  17. 国标服务同时支持udp和tcp方式,可选只监听一种或者两种都监听,tcp方式自动处理粘包问题。
  18. 国标拉流同时支持udp、tcp被动、tcp主动三种方式,每个通道都可以自由选择何种拉流方式。
  19. 内置拉流端口池,每次拉流从中取出一个,关闭流自动回收端口号,重复利用。
  20. 收流端口自动纠错,自动跳过被占用的端口,不会出现端口占用导致收流失败的情况。
  21. 支持三种取流方式自动检测离线重连,检测到离线后,自动重启点播拉流整个流程。
  22. 录像文件回放,上一个完成后自动切换到下一个继续回放,直到所有回放完成。支持高达8倍速回放。
  23. 视频播放自适应硬解码,极低资源占用,实时性极好,带悬浮条显示视频流信息,可以直接在悬浮条单击按钮保存录像文件到本地。
  24. 支持几千路国标消息交互并发,实时视频流支持64路同时显示,可以拓展更多路数。
  25. 支持阿里云等云服务器,可以分别设置内网监听地址和外网访问地址,一般云服务器上是监听地址用内网,对外访问用外网地址。
  26. 支持视频分发,也就是推流,视频通道打开后可以自动推流到流媒体服务器,其他需要的地方拉流即可,支持rtsp、rtmp、hls、webrtc等方式拉流。
  27. 视频分发也叫推流分发,表格方式展示正在推流的信息,其中包括显示统计哪些流正在被多少个地址拉取,比如有两个地方通过rtsp打开了取流,则对应推流地址行所在rtsp列显示数量2,非常直观的展示有多少个拉流。
  28. 视频分发支持无人观看超时自动关闭推流和点播,提高带宽的利用率,没人观看太久的时候,没必要点播拉流和推流。在后台服务模式下,通道推流自动复用,当该通道已经存在点播推流,则复用该路流数据,不会再去点播,节约资源。
  29. 提供后台服务功能,定义了一套私有协议,根据私有协议进行交互,支持tcp、http、mqtt等方式交互,方便第三方程序接入集成。通信协议非常完整,支持获取设备列表、获取指定通道视频地址、云台控制、预置位操作、录像查询、录像回放、录像下载、回放倍速等控制、警情消息通知、视频点播和关闭等。
  30. 支持注册重定向,方便做负载均衡和区域化部署,这样可以支持几十万个设备连接都没问题。
  31. 支持图像抓拍,可以设置抓拍最多10张图片,可设置抓拍间隔,抓拍到的图片会通过信号通知。
  32. 实时预览和录像回放都支持推流,推流支持叠加文字和图片水印以及各种ffmpeg支持的滤镜效果,支持多个水印同时叠加。
  33. 支持各种国标平台上级域级联,比如easygbs、livegbs、海康大华国标平台,上级域级联到本软件,自动解析资源目录等。
  34. 可开启共享设备,默认开启,以便udp方式处理sip交互的同时,tcp方式接收平台推送的资源目录和录像文件信息。
  35. 极速响应设备上线,无需过多等待,只要有过一条消息,无论是收到注册指令、心跳指令、推送指令等,都能立即获取或者解析设备信息和通道信息并上线。
  36. 同时支持gb28181-2011、gb28181-2016、gb28181-2022以及后续可能的所有协议版本。
  37. SIP解析和交互采用纯Qt底层代码实现,udp/tcp通信交互,祖传原创代码解析,不依赖任何第三方。
  38. 代码量少,gb28181交互部分共几千行代码,注释详细,接口友好,使用极其简单,提供非常详细的使用示例。
  39. 支持海康、大华、宇视、华为、天地伟业等所有国标设备,包括一些没有ssrc的设备。
  40. 支持所有Qt版本和编译器以及操作系统,包括但不限于win、linux、mac、android、嵌入式linux、树莓派香橙派、国产os等。

五、相关代码

#include "gb28181widgetpush.h"
#include "gb28181helper.h"
#include "gb28181widget.h"
#include "gb28181server.h"
#include "rtpthreadreceive.h"

SINGLETON_IMPL(GB28181WidgetPush)
GB28181WidgetPush::GB28181WidgetPush(QObject *parent) : QObject(parent)
{
    server = NULL;

    //定时器排队推流
    timerPush = new QTimer(this);
    connect(timerPush, SIGNAL(timeout()), this, SLOT(checkPush()));

    //定时器处理重连
    timerStatus = new QTimer(this);
    connect(timerStatus, SIGNAL(timeout()), this, SLOT(checkStatus()));
}

void GB28181WidgetPush::setPara(int videoCore, int decodeType, bool decodeAudio, bool disableDecode, const QString &hardware, int profile, TransmitMode mode, const QString &serverIp, const QString &pushUrl)
{
    this->videoCore = videoCore;
    this->decodeType = decodeType;
    this->decodeAudio = decodeAudio;
    this->disableDecode = disableDecode;
    this->hardware = hardware;

    this->profile = profile;
    this->mode = mode;
    this->serverIp = serverIp;
    this->pushUrl = pushUrl;
}

void GB28181WidgetPush::setDecodeAudio(bool decodeAudio)
{
    this->decodeAudio = decodeAudio;
}

void GB28181WidgetPush::setServer(GB28181Server *server)
{
    this->server = server;
}

void GB28181WidgetPush::start(int interval)
{
    timerPush->start(1000);
    if (interval > 0) {
        timerStatus->stop();
        timerStatus->start(interval);
    }
}

void GB28181WidgetPush::stop()
{
    QMutexLocker locker(&mutex);
    timerPush->stop();
    timerStatus->stop();
    if (widgets.count() > 0) {
        qApp->processEvents();
    }

    foreach (GB28181Widget *widget, widgets) {
        widget->stop();
        widget->deleteLater();
    }

    server = NULL;
    times.clear();
    widgets.clear();
    deviceIds.clear();
    channelIds.clear();
    onlines.clear();
}

void GB28181WidgetPush::getLiveInfo(QStringList &deviceIds, QStringList &channelIds)
{
    foreach (GB28181Widget *widget, widgets) {
        if (widget->getIsRunning()) {
            deviceIds << widget->getDeviceId();
            channelIds << widget->getChannelId();
        }
    }
}

void GB28181WidgetPush::openVideo(const QString &deviceId, const QString &channelId, const QString &pushUrl, const QString &startTime, const QString &endTime)
{
#ifdef betaversion
    int maxCount = 4;
    if (widgets.count() >= maxCount) {
        //QMessageBox::critical(0, "错误", QString("试用版最多同时推流 %1 路, 请联系作者(微信feiyangqingyun)购买源码编译正式版!").arg(maxCount));
        return;
    }
#endif

    //服务不存在或者推流地址为空则不用继续
    if (!server || pushUrl.isEmpty()) {
        return;
    }

    //已经存在的则不用继续
    if (this->existPush(deviceId, channelId, pushUrl) >= 0) {
        return;
    }

    //创建一个视频窗体用来拉流
    bool preview = (startTime.isEmpty() && endTime.isEmpty());
    GB28181Widget *widget = new GB28181Widget;
    widget->setProperty("disableDecode", disableDecode);
    widget->setObjectName(QString("push_%1_%2").arg(preview ? "preview" : "record").arg(deviceId.right(3)));
    connect(widget, SIGNAL(pushStart(GB28181Widget *, QString)), this, SIGNAL(pushStart(GB28181Widget *, QString)));
    widget->initWidgetPara(ScaleMode_Auto, VideoMode_Hwnd, VideoCore(videoCore), decodeType, decodeAudio, hardware, 10000, true, false);

    //带了时间范围的是回放或者下载
    widget->setServer(server);
    widget->setTime(startTime, endTime);
    widget->setPara(1, profile, mode, serverIp, pushUrl, preview ? PlayType_Preview : PlayType_Playback);
    widget->openVideo(deviceId, channelId);
    times << QDateTime::currentDateTime();
    widgets << widget;
    //qDebug() << TIMEMS << "openVideo" << deviceId << channelId << pushUrl;
}

void GB28181WidgetPush::closeVideo(const QString &deviceId, const QString &channelId, const QString &pushUrl)
{
    this->existPush(deviceId, channelId, pushUrl, true);
    //qDebug() << TIMEMS << "closeVideo" << deviceId << channelId << pushUrl;
}

void GB28181WidgetPush::removeWidget(int index, GB28181Widget *widget, const QString &url)
{
    times.removeAt(index);
    widgets.removeAt(index);
    widget->stop();
    widget->deleteLater();
    emit pushStop(url);
}

int GB28181WidgetPush::existPush(const QString &deviceId, const QString &channelId, const QString &key, bool remove)
{
    //同一个通道可以支持同时预览和回放/限定两个同时预览或者同时回放
    //除了设备编码和通道编码一致/还需要过滤推流地址前缀/预览和录像推流地址前缀不一样
    for (int i = widgets.count() - 1; i >= 0; --i) {
        GB28181Widget *widget = widgets.at(i);
        QString url = widget->getRecordUrl();
        if (widget->getDeviceId() != deviceId) {
            continue;
        }

        //如果设备离线/通道编码为空/需要对这个设备的所有通道进行处理
        if (channelId.isEmpty()) {
            if (remove) {
                this->removeWidget(i, widget, url);
            }
        } else {
            if (widget->getChannelId() == channelId && url.contains(key)) {
                if (remove) {
                    this->removeWidget(i, widget, url);
                }

                return i;
            }
        }
    }

    return -1;
}

QString GB28181WidgetPush::getPushSsrc(const QString &flag, const QString &deviceId, const QString &channelId)
{
    int index = this->existPush(deviceId, channelId, flag);
    if (index >= 0) {
        return widgets.at(index)->getSsrcVideo();
    } else {
        return QString();
    }
}

void GB28181WidgetPush::checkPush()
{
    QMutexLocker locker(&mutex);
    if (deviceIds.count() == 0) {
        return;
    }

    QString deviceId = deviceIds.takeFirst();
    QString channelId = channelIds.takeFirst();
    bool online = onlines.takeFirst();
    if (online) {
        this->openVideo(deviceId, channelId, pushUrl);
    } else {
        this->closeVideo(deviceId, channelId, pushUrl);
    }
}

void GB28181WidgetPush::checkStatus()
{
    int count = widgets.count();
    QDateTime now = QDateTime::currentDateTime();
    for (int i = 0; i < count; ++i) {
        GB28181Widget *widget = widgets.at(i);
        RtpThreadReceive *rtpThread = widget->getRtpThread();
        int timeout = widget->getVideoPara().readTimeout;
        if (timeout <= 0) {
            continue;
        }

        qint64 offset = rtpThread->getLastTime().msecsTo(now);
        //qDebug() << TIMEMS << "GB28181WidgetPush" << widget->objectName() << widget->getIsRecord() << offset;
        if (widget->isPushOk() && offset < timeout) {
            continue;
        }

        //避免频繁的打开
        offset = times.at(i).msecsTo(now);
        QString deviceId = widget->getDeviceId();
        QString channelId = widget->getChannelId();
        emit pushStop(widget->getRecordUrl());
        //qDebug() << TIMEMS << "GB28181WidgetPush" << widget->objectName() << offset;

        if (offset > 15000) {
            times[i] = QDateTime::currentDateTime();
            widget->closeVideo(deviceId, channelId);
            GB28181Helper::sleep(1500);
            widget->openVideo(deviceId, channelId);
            qDebug() << TIMEMS << QString("推流重连 -> 设备: %1 通道: %2").arg(deviceId).arg(channelId);
            break;
        }
    }
}

//自动推流关联了这个信号/通知上线打开下线关闭
void GB28181WidgetPush::receiveEvent(GB28181Event event)
{
    if (event.alarmMethod != 5 || (event.alarmInfo != 13 && event.alarmInfo != 14)) {
        return;
    }

    this->channelChanged(event.deviceId, event.channelId, event.alarmInfo == 14);
}

//自动推流关联了这个信号/设备下线关闭所有通道
void GB28181WidgetPush::deviceChanged(const QString &deviceId, bool online)
{
    //只需要关注下线/上线会通过通道变化信号触发
    if (!online) {
        this->channelChanged(deviceId, "", online);
    }
}

//自动推流关联了这个信号/通道上线打开下线关闭
void GB28181WidgetPush::channelChanged(const QString &deviceId, const QList<GB28181Channel> &channels)
{
    foreach (GB28181Channel channel, channels) {
        this->channelChanged(deviceId, channel.channelId, channel.online);
    }
}

void GB28181WidgetPush::channelChanged(const QString &deviceId, const QString &channelId, bool online)
{
    QMutexLocker locker(&mutex);
#if 0
    if (online) {
        this->openVideo(deviceId, channelId, pushUrl);
    } else {
        this->closeVideo(deviceId, channelId, pushUrl);
    }
#else
    //过滤下重复的/设备上线后可能会触发多次通道变化信号
    int count = deviceIds.count();
    for (int i = 0; i < count; ++i) {
        if (deviceIds.at(i) == deviceId && channelIds.at(i) == channelId && onlines.at(i) == online) {
            return;
        }
    }

    //加入队列排队处理
    deviceIds << deviceId;
    channelIds << channelId;
    onlines << online;
#endif
}