## 一、前言说明
最近搞嵌入式板子开发搞上瘾了,一连远程了十几个开发板上测试视频相关项目,比如视频综合应用和视频监控系统以及推流,试下来效果都不错,有RK3568/RK3576/RK3588等,厂家至少四五种,都大差不差,毕竟核心都是瑞星微的,可能涉及到一些版本
问题,就是有些板子是老的mpp硬解,有些是新的,这样就需要用对应的硬解的ffmpeg库去做解码,不通用,因为ffmpeg编译的时候就链接了对应的库,版本不一样的话会导致
编译通不过。在这几款板子中,3588是性能最强劲的,据说还有个3588s的要横空出世,又是一片血雨腥风。
现在好多都是用来做智能AI盒子,尤其是用来做行为分析,主要用yolo啥的,刚好我的视频监控系统集成了yolo运算后,对应的结果rect区域
显示到对应视频上,多线程运行,也提供了从视频通道取一张
图片数据传入yolo分析,搭配起来使用非常完美,**后期可能会定义一套私有协议,json格式数据,用于tcp/udp/http/mqtt等方式接收数据,用于插入日志记录、显示图文警情、显示人工智能结果(含物体区域比如人脸框和文字描述)等。这样第三方(比如只懂python的用户)只需要根据私有协议来就行。这样就不需要如何熟悉本软件源码,只需要会简单的使用即可。**
- 本软件目前本身没有AI人工智能算法加持,需要自行加上AI算法。
- 本软件内置的各种事件订阅是基于onvif协议,只要设备支持onvif协议,都可以正常接入。比如海康大华的摄像头和NVR等设备都支持前端做AI算法识别,将识别的结果通过onvif发给软件。
- 本软件内置了各种结果展示,包括报警文字记录,报警图像展示,报警视频录像、报警视频回放等,这些都是提供的通用的API函数接口调用使用,也就是直接Qt程序中调用全局静态API函数去处理。
- 本软件目前提供了相关类是DeviceThreadUI和DeviceVideoSmart。
- DeviceThreadUI专门演示如何在线程中插入警情记录和图文警情等信息、演示每隔一段时间插入消息、演示设备GPS移动、演示报警闪烁、演示指定通道编号设置视频和通道交换等功能。
- DeviceVideoSmart专门演示如何拿到一路视频数据,然后视频数据自己去做人工智能算法识别(比如用yolo),识别后的结果可以是一堆rect矩形区域,然后这些区域可以发回视频控件显示。这些计算后的区域信息也可以保存到视频
文件或者重新推流出去(如果是推流需要额外购买推流组件)。
- **后期可能会定义一套私有协议,json格式数据,用于tcp/udp/http/mqtt等方式接收数据,用于插入日志记录、显示图文警情、显示人工智能结果(含物体区域比如人脸框和文字描述)等。这样第三方(比如只懂python的用户)只需要根据私有协议来就行。这样就不需要如何熟悉本软件源码,只需要会简单的使用即可。**
- 建议做二次开发的时候,单独起一个目录,新建个pri,后面所有的自己的处理都在这个目录中,隔离开来,方便后期拓展,这样如果监控系统源码有更新,也不需要大动干戈,只需要替换原有的监控系统源码即可。
- 如果想录像存储或者推流带上人工智能运算后的各种信息比如一堆rect或者文字水印,需要在系统设置中视频参数部分,编码下拉框选择自动,默认是不处理,不处理意味着保存的源头裸流264/265数据,自动的话就是编码保存,各种图形和文字水印都需要重新编码才能写入到文件。
- 如果开启了编码保存,则需要占用CPU资源,分辨率越大,占用越多,265比264占用更多。所以建议如果需要编码保存,运算那边尽量用子码流,也就是低分辨率的视频。
## 二、效果图



## 三、相关地址
1. 国内站点:[
https://gitee.com/feiyangqingyun](https://gitee.com/feiyangqingyun)
2. 国际站点:[
https://github.com/feiyangqingyun](https://github.com/feiyangqingyun)
3. 个人作品:[
https://blog.csdn.net/feiyangqingyun/article/details/97565652](https://blog.csdn.net/feiyangqingyun/article/details/97565652)
4. 文件地址:[
https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g](https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g) 提取码:01jf 文件名:bin_video_system。
## 四、功能特点
1. 主
界面采用停靠窗体模式,各种组件以小模块的形式加入,可自定义任意模块加入。
2. 停靠模块可拖动任意位置嵌入和悬浮,支持最大化全屏,支持多屏幕。
3. 双重布局文件存储机制,正常模式、全屏模式都对应不同的布局方案,自动切换和保存,比如全屏模式可以突出几个模块透明显示在指定位置,更具科幻感现代化。
4. 原创onvif协议机制,采用底层协议解析(udp广播搜索+http请求执行命令)更轻量易懂易学习拓展,不依赖任何第三方组件比如gsoap。
5. 原创数据导入、导出、打印机制,跨平台不依赖任何组件,瞬间导出数据。
6. 内置多个原创组件,宇宙超值超级牛逼,包括数据导入导出组件(导出到xls、pdf、打印)、
数据库组件(数据库管理线程、自动清理数据线程、万能分页、数据请求等)、地图组件、视频监控组件、文件多线程收发组件、onvif通信组件、通用浏览器内核组件等。
7. 自定义信息框、
错误框、询问框、右下角提示框(包含多种格式)等。
8. 精美换肤,高达20套皮肤样式随意更换,所有样式全部统一,包括菜单等。
9. 选中通道对应设备树节点高亮,选中通道节点对应视频控件高亮,方便查看当前通道信息。
10. 视频控件悬浮条可以自行增加多个按钮,监控界面底部小工具栏也可自行增加按钮。
11. 双击摄像机节点自动播放视频,双击节点自动依次添加视频,会自动跳到下一个,双击父节点自动添加该节点下的所有视频。可选主码流、子码流。
12. 录像机管理、摄像机管理,可添加删除修改导入导出打印信息,立即应用新的设备信息生成树状列表,不需重启。
13. 摄像机搜索支持一键搜索和批量添加,支持onvif的NVR一键添加子设备,可以手动设置开始地址和数量一键生成摄像机信息。
14. 可选多种内核自由切换,ffmpeg、vlc、mpv等,均可在pro中设置。推荐用ffmpeg,跨平台最多,默认提供好了linux和mac平台上编译好的库。
15. 支持windows、linux、macos等系统硬解码,还支持嵌入式linux RKMPP硬解码,可设置硬解码类型(dxva2、d3d11va、vaapi、vdpau等)。
16. 各种模块可以勾选是否激活,方便根据实际需求搭配各种组合,比如隐藏电子地图模块,隐藏远程回放模块只保留本地回放等。
17. 尽最大化可能,将常用的功能封装接口,全局静态函数调用,极其容易使用,提供各种使用示例,方便用户二开。
18. 默认采用opengl绘制视频,超低的CPU资源占用,支持yuyv和nv12两种格式绘制,性能爆表。
19. 标签和图形信息支持三种绘制方式,绘制到遮罩层、绘制到图片、源头绘制(对应信息可以存储到文件)。
20. 包括但不限于视频监控内核组件的所有功能,可参阅说明书中功能介绍 [视频监控内核](###8.1 视频监控内核)。
21. 高度可定制化,用户可以很方便的在此基础上衍生自己的功能,比如增加自定义模块,增加运行模式、机器人监控、无人机监控、挖掘机监控、广播监控等。
22. 支持xp、win7、win10、win11、linux、mac、各种国产系统(UOS、中标麒麟、银河麒麟等)、嵌入式linux等系统。
23. 注释完整,项目结构清晰,超级详细完整的使用开发手册,精确到每个代码文件的功能说明,不断持续迭代版本。
## 五、相关代码
```cpp
//DeviceThreadUI类源码
#include "devicethreadui.h"
#include "devicehelper.h"
#include "dbquery.h"
DeviceThreadUI::DeviceThreadUI(QObject *parent) : QThread(parent)
{
stopped = false;
}
void DeviceThreadUI::run()
{
//先初始化随机数种子 否则生成的随机数很可能一致
srand((quint64)currentThreadId());
while (!stopped) {
//演示每隔一段时间插入消息
QString msg = "测试消息";
QString result = "已处理";
QString file = QString(":/image/liuyifei%1.jpg").arg(rand() % 3);
//异步调用添加表格消息和图文警情
QMetaObject::invokeMethod(this, "addMsg", Q_ARG(QString, msg));
QMetaObject::invokeMethod(this, "addMsgList", Q_ARG(QString, msg), Q_ARG(QString, result), Q_ARG(QImage, QImage(file)));
//演示设备GPS移动
AppEvent::Instance()->slot_moveDevice(0, "121.415086", "31.183769");
//演示报警闪烁
if (1) {
static bool isAlarm = true;
isAlarm = !isAlarm;
if (!isAlarm) {
AppEvent::Instance()->slot_startAlarm();
//演示插入一条带视频文件地址的报警记录用于双击播放
DbQuery::addUserLog("0", "设备日志", "测试报警", "https://hls01open.ys7.com/openlive/6e0b2be040a943489ef0b9bb344b96b8.hd.m3u8");
} else {
AppEvent::Instance()->slot_stopAlarm();
DbQuery::addUserLog("停止报警");
}
}
//演示指定通道编号设置视频和通道交换
if (1) {
static int index = 0;
if (index == 0) {
AppEvent::Instance()->slot_loadVideo(0, "http://vfx.mtime.cn/Video/2023/03/09/mp4/230309152143524121.mp4");
} else if (index == 1) {
AppEvent::Instance()->slot_loadVideo(0, "http://vfx.mtime.cn/Video/2023/03/07/mp4/230307085324679124.mp4");
} else if (index == 2) {
AppEvent::Instance()->slot_loadVideo(1, "http://vfx.mtime.cn/Video/2022/07/18/mp4/220718132929585151.mp4");
} else if (index == 3) {
AppEvent::Instance()->slot_loadVideo(1, "http://vfx.mtime.cn/Video/2022/12/17/mp4/221217153424902164.mp4");
} else if (index == 4) {
AppEvent::Instance()->slot_changeVideo(2, 3);
} else if (index == 5) {
AppEvent::Instance()->slot_changeVideo(3, 2);
}
index++;
if (index == 6) {
index = 0;
}
}
//延时一段时间
msleep(5 * 1000);
}
stopped = false;
}
void DeviceThreadUI::addMsg(const QString &msg, quint8 type)
{
DeviceHelper::addMsg(msg, type);
}
void DeviceThreadUI::addMsgList(const QString &msg, const QString &result, const QImage &image)
{
DeviceHelper::addMsgList(msg, result, image);
}
```
//DeviceVideoSmart类源码:
#include "devicevideosmart.h"
#include "qthelper.h"
#include "videowidgetx.h"
DeviceVideoSmart::DeviceVideoSmart(QObject *parent) : QThread(parent)
{
stopped = false;
isPause = false;
videoWidget = 0;
//需要注册下这个类型
qRegisterMetaType<QList<QRect> >("QList<QRect>");
}
void DeviceVideoSmart::run()
{
//随机延时启动线程
msleep(rand() % 500 + 1000);
while (!stopped) {
if (isPause || !AppConfig::VideoSmart || !videoWidget || !videoWidget->getIsRunning()) {
msleep(1);
continue;
}
//定义区域集合
QList<QRect> rects;
if (0) {
//获取一张图片用于人工智能处理
QImage image = videoWidget->getImage();
qDebug() << TIMEMS << videoWidget->getVideoThread()->getFlag() << image.size();
//人工智能处理算法放在这里进行运算
//yolo->take(image);
}
//这里演示图形区域信息
if (1) {
//限定下区域
int w = videoWidget->getVideoWidth();
int h = videoWidget->getVideoHeight();
int width = w - 100;
int height = h - 100;
int wx = w / 50;
int hx = h / 50;
//随机生成矩形
for (int i = 0; i < 30; ++i) {
int x = rand() % width;
int y = rand() % height;
int w = wx + rand() % 40;
int h = hx + rand() % 30;
rects << QRect(x, y, w, h);
}
//异步执行
QMetaObject::invokeMethod(this, "receiveRects", Q_ARG(QList<QRect>, rects));
msleep(40);
}
//这里演示文字标签信息
if (0) {
int currentCount = rand() % 15;
int maxCount = rand() % 20 + 15;
//qDebug() << TIMEMS << currentCount << currentThreadId();
QMetaObject::invokeMethod(this, "receiveData", Q_ARG(QString, "dateTime"), Q_ARG(QVariant, 0));
QMetaObject::invokeMethod(this, "receiveData", Q_ARG(QString, "currentCount"), Q_ARG(QVariant, currentCount));
QMetaObject::invokeMethod(this, "receiveData", Q_ARG(QString, "maxCount"), Q_ARG(QVariant, maxCount));
msleep(100);
}
//必须要有这个延时休息一下
msleep(1);
}
stopped = false;
isPause = false;
}
void DeviceVideoSmart::receiveKbps(qreal kbps, int frameRate)
{
OsdInfo osd;
osd.name = "kbps";
osd.color = "#FF0000";
osd.fontSize = fontSize;
osd.position = OsdPosition_RightTop;
osd.text = QString("%1 kbps\n%2 fps").arg((int)kbps).arg(frameRate);
videoWidget->setOsd(osd);
}
void DeviceVideoSmart::receivePlayStart(int time)
{
//字体大小按照视频宽度比例来
fontSize = videoWidget->getVideoWidth() / 30;
}
void DeviceVideoSmart::receiveRects(const QList<QRect> &rects)
{
if (!videoWidget || !videoWidget->getIsRunning()) {
return;
}
int borderWidth = videoWidget->getVideoWidth() / 500;
borderWidth = borderWidth < 1 ? 1 : borderWidth;
//取随机颜色
static QStringList colors = QColor::colorNames();
int count = rects.count();
for (int i = 0; i < count; ++i) {
GraphInfo graph;
graph.name = QString("graph%1").arg(i);
graph.borderColor = colors.at(rand() % colors.count());
graph.borderWidth = borderWidth;
graph.rect = rects.at(i);
videoWidget->setGraph(graph);
}
}
void DeviceVideoSmart::receiveData(const QString &type, const QVariant &data)
{
if (!videoWidget || !videoWidget->getIsRunning()) {
return;
}
OsdInfo osd;
osd.name = type;
if (type == "dateTime") {
osd.fontSize = fontSize;
osd.color = "#FFFFFF";
osd.format = OsdFormat_DateTime;
osd.position = OsdPosition_LeftTop;
videoWidget->setOsd(osd);
} else if (type == "currentCount") {
osd.fontSize = fontSize;
osd.text = QString("当前人数: %1").arg(data.toInt());
osd.color = "#D64D54";
osd.format = OsdFormat_Text;
osd.position = OsdPosition_LeftBottom;
videoWidget->setOsd(osd);
} else if (type == "maxCount") {
osd.fontSize = fontSize;
osd.text = QString("限定人数: %1").arg(data.toInt());
osd.color = "#FDCD72";
osd.format = OsdFormat_Text;
osd.position = OsdPosition_RightBottom;
videoWidget->setOsd(osd);
}
}
void DeviceVideoSmart::setVideoWidget(VideoWidget *videoWidget)
{
this->videoWidget = videoWidget;
//关联实时码率信号将实时码率作为文字水印贴到视频上
connect(videoWidget, SIGNAL(sig_receiveKbps(qreal, int)), this, SLOT(receiveKbps(qreal, int)));
connect(videoWidget, SIGNAL(sig_receivePlayStart(int)), this, SLOT(receivePlayStart(int)));
}
void DeviceVideoSmart::stop()
{
stopped = true;
}
void DeviceVideoSmart::pause()
{
isPause = true;
}
```