title: 实战项目MQTT与视频监控 tags: [
嵌入式Linux,
项目实战,
MQTT,
mosquitto,
paho.mqtt.c,
MQTTClient,
发布订阅,
QoS,
遗嘱,
保留消息,
物联网,
RTMP,
Nginx,
FFmpeg,
VLC,
视频监控,
V4L2,
MJPEG,
Qt,
IMX6ULL,
] created: 2026-09-18 updated: 2026-09-18
💡 关联知识:[[04-网络编程与项目实战/01-网络基础与socket编程]]、[[04-网络编程与项目实战/03-CMake入门与进阶]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]、[[04-网络编程与项目实战/面试-网络编程与项目实战]]
本篇把前面的 socket、文件 I/O、CMake 知识落到两个完整项目上:MQTT 物联网(开发板作为客户端,远程控制 LED、上报 SoC 温度)和视频监控(两条技术路线:Nginx + FFmpeg 的 RTMP 推拉流,以及 V4L2 采集 + JPEG 广播的局域网 MJPEG 方案)。每个项目都给出移植步骤、完整可复制源码与测试方法。
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是一种基于客户端-服务端架构的发布/订阅模式消息传输协议,与 HTTP 一样工作在 TCP/IP 四层模型的应用层,构建于 TCP/IP 之上。它的设计思想是轻巧、开放、简单、规范,易于实现,因此特别适合机器与机器(M2M)、物联网(IoT)等受限环境。
| 特性 | 说明 |
|---|---|
| 发布/订阅模式 | 一对多的消息发布,解除应用程序耦合 |
| 基于 TCP/IP | 主流 MQTT 基于 TCP;也有基于 UDP 的 MQTT-SN |
| 支持 QoS | 按消息重要性设置不同服务质量等级(0/1/2) |
| 小型传输 | 开销很小,协议交换最小化,降低网络流量 |
| 遗嘱机制(will) | 客户端异常断线时通知其它客户端 |
| 主题寻址 | 基于主题发布/订阅,对负载内容屏蔽 |
| 心跳机制 | 检测客户端是否在线 |
| 角色 | 说明 |
|---|---|
| 服务端(Broker) | MQTT 信息传输的枢纽,负责把客户端发来的信息转发给其它客户端,并管理客户端 |
| 客户端 | 可以发布消息,也可以订阅主题后接收消息;角色不固定,随主题切换 |
| 主题(Topic) | 发布/订阅的纽带,服务端按主题决定消息转发给哪些订阅者 |
客户端发布消息时要指定主题;订阅者订阅主题后,服务端就把该主题的新消息推送给订阅者。一个客户端对不同主题可以既是发布者又是订阅者。
flowchart LR
subgraph P["发布者"]
Dev["开发板客户端"]
end
subgraph B["MQTT 服务端 Broker"]
T1["主题: dt_mqtt/temperature"]
T2["主题: dt_mqtt/led"]
end
subgraph S["订阅者"]
Phone["手机客户端"]
PC["电脑 MQTT.fx"]
end
Dev -->|"发布 temperature"| T1
T1 --> Phone
T1 --> PC
Phone -->|"发布 led=1/0/2"| T2
PC -->|"发布 led=1/0/2"| T2
T2 --> Dev
classDef pub fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
classDef broker fill:#fef9c3,stroke:#ca8a04,color:#713f12
classDef sub fill:#dcfce7,stroke:#16a34a,color:#14532d
class Dev pub
class T1,T2 broker
class Phone,PC sub
发布/订阅的三个特性都带一个"可"字,体现灵活性:
客户端连接服务端分两步:客户端发 CONNECT 报文 → 服务端回 CONNACK 报文。MQTT 报文由固定头、可变头、消息体(Payload)三部分组成。
CONNECT 报文关键字段:
| 字段 | 说明 |
|---|---|
clientId |
客户端标识,必须唯一;两个客户端用同一 clientId 会被当成同一个客户端 |
keepAlive |
心跳时间间隔,如 60 表示约每 60 秒发一次心跳包 |
cleanSession |
清除会话标志 |
cleanSession 的作用:
cleanSession = 0:创建持久性会话,客户端再次上线时可收到离线期间所有 QoS>0 的消息;服务端会记住客户端订阅过的主题,直到会话超时注销。cleanSession = 1:创建临时会话,不接收离线消息、服务端不保存订阅主题,每次连接都是全新会话,断开后会话自动销毁。CONNACK 报文包含 returnCode(连接返回码)与 sessionPresent:
| returnCode | 说明 |
|---|---|
| 0 | 连接成功 |
| 1 | 拒绝:不支持的 MQTT 协议版本 |
| 2 | 拒绝:不支持客户端标识符的编码 |
| 3 | 拒绝:服务端不可用 |
| 4 | 拒绝:用户名或密码无效 |
| 5 | 拒绝:客户端未授权 |
| 6-255 | 保留备用 |
sessionPresent 与 cleanSession 配合:cleanSession=0 时,若服务端保存了上次会话状态则返回 1,否则 0;cleanSession=1 时恒为 0。
| 报文 | 作用 |
|---|---|
| PUBLISH | 发布消息(含 packetId、topicName、payload、qos、retain、dup) |
| SUBSCRIBE / SUBACK | 订阅主题 / 订阅确认(一个 SUBSCRIBE 可含多个主题) |
| UNSUBSCRIBE / UNSUBACK | 取消订阅 / 取消订阅确认 |
| DISCONNECT | 客户端主动断开连接 |
SUBACK 的订阅返回码:
| 返回码 | 说明 |
|---|---|
| 0x00 | 订阅成功——QoS0 |
| 0x01 | 订阅成功——QoS1 |
| 0x02 | 订阅成功——QoS2 |
| 0x80 | 订阅失败 |
注意事项:
packetId(报文标识符)只有 QoS>0 时才为非零数值。dup 重发标志只在 QoS>0 时使用;接收方没有及时回确认时,发送方重发并置 dup=true。retain 保留标志见 2.6 节。/ 分隔,如 home/sensor/led/brightness 共四级;主题不要以 / 开头。+:匹配一个主题级别,如 home/sensor/+/status。#:匹配任意数量级别,如 home/sensor/#。$ 开头的是服务端系统保留主题(如 $SYS/#),客户端不可随意订阅或发布。| QoS | 语义 | 传输流程 |
|---|---|---|
| 0 | 最多发一次(最多一次) | 发送即结束,不确认、不重传,靠 TCP 保证 |
| 1 | 至少发一次(最少一次) | PUBLISH → PUBACK,超时未收到 PUBACK 则重发,可能重复 |
| 2 | 保证收一次 | PUBLISH → PUBREC → PUBREL → PUBCOMP,最安全也最慢 |
sequenceDiagram
participant S as 发送端
participant R as 接收端
Note over S,R: QoS 0:最多一次
S->>R: PUBLISH
Note over S,R: QoS 1:至少一次
S->>R: PUBLISH
R-->>S: PUBACK
Note right of S: 未收到 PUBACK 则重发(dup=1)
Note over S,R: QoS 2:保证收一次
S->>R: PUBLISH
R-->>S: PUBREC
S->>R: PUBREL
R-->>S: PUBCOMP
要点:
cleanSession 必须设为 false,否则收不到任何离线消息(含 QoS1/QoS2)。客户端在空闲时定时发送 PINGREQ 报文(心跳请求),服务端收到后回 PINGRESP(心跳响应)。服务端据此判断客户端是否在线;客户端若发 PINGREQ 后收不到 PINGRESP,则认为自己已断线。发送间隔由 CONNECT 报文的 keepAlive 指定。
客户端在"活着"时通过 CONNECT 报文预先写好遗嘱,一旦意外断线(非主动发 DISCONNECT,如断网、断电),服务端就把遗嘱消息发布出去。主动断开不会触发遗嘱。
| 字段 | 说明 |
|---|---|
willTopic |
遗嘱主题,订阅该主题的客户端才会收到遗嘱消息 |
willMessage |
遗嘱内容 |
willRetain |
遗嘱的保留标志 |
willQoS |
遗嘱的 QoS 等级(0/1/2) |
小技巧:客户端上线时主动向自己的遗嘱主题发一条"在线"消息,订阅者即可据此实现上线通知。
CONNECT 报文中的 username、password 是可选字段。服务端若开启认证,则客户端必须提供正确的用户名密码才能连接;有些服务端还用它识别客户端归属、管理私人主题权限。
| 服务器 | 地址 | 端口 |
|---|---|---|
| test.mosquitto.org(国外) | test.mosquitto.org |
1883(TCP)、8883(TLS)、8080(WebSocket) |
| broker.hivemq.com(国外) | broker.hivemq.com |
1883、8000(WebSocket) |
| 然也物联(国内) | test.ranye-iot.net |
1883、8883(TLS) |
| 通信猫(国内) | mq.tongxinmao.com |
1883 |
公用服务器仅供学习测试,不可商用;国内服务器连接快、延迟低。
sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa # 加入软件源
sudo apt-get update # 更新软件源
sudo apt-get install mosquitto # 安装 mosquitto 服务端
/etc/mosquitto/conf.d/ 下所有 *.conf 会被自动加载。新建 myconfig.conf:
# /etc/mosquitto/conf.d/myconfig.conf
# 添加监听端口 1883
listener 1883
# 关闭匿名访问,客户端必须使用用户名
allow_anonymous false
# 指定用于存放用户名和密码的文件
password_file /etc/mosquitto/pwfile.txt
创建用户名密码文件:
sudo touch /etc/mosquitto/pwfile.txt
sudo mosquitto_passwd /etc/mosquitto/pwfile.txt mqtt1 # 用户名 mqtt1,密码 123456(需输入两次)
启停与查看状态:
sudo service mosquitto start # 启动服务
sudo service mosquitto status # 查看服务状态
sudo service mosquitto stop # 停止服务
网络要求:开发板、Ubuntu、手机/电脑处于同一局域网且能互相
ping通。
192.168.6.161,即 Ubuntu 的 IP)、Client ID(如 test1)、用户名 mqtt1、密码 123456,Apply 后连接。dt2914/testTopic),点 Subscribe。手机端也可用 MQTT Client 软件连接同一服务器(Client ID 不能与其它客户端重复),实现跨设备收发。
官网 https://mqtt.org/ → Software → Client libraries → Eclipse Paho C。示例使用 1.3.8 版本(当前最新为 1.3.13)。
tar -xzf paho.mqtt.c-1.3.8.tar.gz
cd paho.mqtt.c-1.3.8
该工程顶级目录有 CMakeLists.txt,是 CMake 构建的工程。
在 cmake/ 目录新建 arm-linux-setup.cmake(内容与本库 [[04-网络编程与项目实战/03-CMake入门与进阶]] 第 5.1 节相同,按实际工具链路径修改):
##################################
# 配置 ARM 交叉编译
#################################
set(CMAKE_SYSTEM_NAME Linux) # 设置目标系统名字
set(CMAKE_SYSTEM_PROCESSOR arm) # 设置目标处理器架构
# 指定编译器的 sysroot 路径
set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
# 指定交叉编译器 arm-linux-gcc
set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
# 为编译器添加编译选项
set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
#################################
# end
##################################
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=~/tools/paho.mqtt.c-1.3.8/install \
-DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake \
-DPAHO_WITH_SSL=TRUE \
-DPAHO_BUILD_SAMPLES=TRUE ..
make
make install
CMAKE_INSTALL_PREFIX 指定安装路径(示例为源码目录下的 install)。PAHO_WITH_SSL、PAHO_BUILD_SAMPLES 是缓存变量,含义见工程 README.md。install/ 下有 bin、include、lib、share:
| 库文件 | 说明 |
|---|---|
libpaho-mqtt3a.so |
异步模式,不支持 SSL |
libpaho-mqtt3as.so |
异步模式,支持 SSL |
libpaho-mqtt3c.so |
同步模式,不支持 SSL |
libpaho-mqtt3cs.so |
同步模式,支持 SSL |
头文件:同步模式包含 MQTTClient.h,异步模式包含 MQTTAsync.h(其它头文件被这两个包含)。本项目使用同步模式 libpaho-mqtt3c.so + MQTTClient.h。
# 在 Ubuntu 上打包
tar -czf libmqtt.tar.gz -C ~/tools/paho.mqtt.c-1.3.8/install/lib .
# 拷贝到开发板 /home/root 后,解压到 /usr/lib
tar -xzf libmqtt.tar.gz -C /usr/lib
不要破坏库原有的软链接关系,建议打包
.so及其符号链接一起拷贝。
以下函数定义在 MQTTClient.h。文档见源码 docs/MQTTClient/html/index.html。
MQTTClient_message 结构体typedef struct
{
int payloadlen; // 负载长度
void* payload; // 负载
int qos; // 消息的 qos 等级
int retained; // 消息的保留标志
int dup; // dup 标志(重复标志)
int msgid; // 消息标识符,即 packetId
/* ... */
} MQTTClient_message;
发布消息需实例化一个 MQTTClient_message 对象;接收消息也是收到该对象。常用 MQTTClient_message_initializer 宏初始化。
int MQTTClient_create(MQTTClient *handle,
const char *serverURI,
const char *clientId,
int persistence_type,
void *persistence_context);
| 参数 | 说明 |
|---|---|
handle |
MQTT 客户端句柄 |
serverURI |
MQTT 服务器地址 |
clientId |
客户端 ID |
persistence_type |
持久化类型,见下表 |
persistence_context |
NONE 时传 NULL;DEFAULT 时为持久化目录(传 NULL 表示工作目录) |
| persistence_type | 说明 |
|---|---|
MQTTCLIENT_PERSISTENCE_NONE |
内存持久化,系统故障时传输中消息状态可能丢失 |
MQTTCLIENT_PERSISTENCE_DEFAULT |
默认(基于文件系统)持久化,提供一定防丢失保护 |
MQTTCLIENT_PERSISTENCE_USER |
应用自定义持久化实现 |
返回值:成功 MQTTCLIENT_SUCCESS,失败返回错误码。
MQTTClient client;
int rc;
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_create(&client, "192.168.6.161", "dt_mqtt_2_id",
MQTTCLIENT_PERSISTENCE_NONE, NULL))) {
printf("Failed to create client, return code %d\n", rc);
return EXIT_FAILURE;
}
int MQTTClient_connect(MQTTClient handle, MQTTClient_connectOptions *options);
typedef struct
{
int keepAliveInterval; // keepAlive
int cleansession; // cleanSession
MQTTClient_willOptions *will; // 遗嘱相关
const char *username; // 用户名
const char *password; // 密码
int reliable; // 同步/异步发布控制(默认 1=同步)
/* ... */
} MQTTClient_connectOptions;
typedef struct
{
const char *topicName; // 遗嘱主题
const char *message; // 遗嘱内容
int retained; // 遗嘱消息的保留标志
int qos; // 遗嘱消息的 QoS 等级
/* ... */
} MQTTClient_willOptions;
返回值错误码与 CONNACK 的连接返回码一致(1~5)。
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer;
will_opts.topicName = "dt_mqtt/willTopic";
will_opts.message = "Abnormally dropped";
will_opts.retained = 1;
will_opts.qos = 0;
conn_opts.will = &will_opts;
conn_opts.keepAliveInterval = 30;
conn_opts.cleansession = 0;
conn_opts.username = "mqtt1";
conn_opts.password = "123456";
if (MQTTCLIENT_SUCCESS != (rc = MQTTClient_connect(client, &conn_opts))) {
printf("Failed to connect, return code %d\n", rc);
return EXIT_FAILURE;
}
int MQTTClient_setCallbacks(MQTTClient handle,
void *context,
MQTTClient_connectionLost *cl,
MQTTClient_messageArrived *ma,
MQTTClient_deliveryComplete *dc);
| 回调 | 类型 | 说明 |
|---|---|---|
cl |
void (void *context, char *cause) |
断线时执行;设 NULL 表示不处理断线 |
ma |
int (void *context, char *topicName, int topicLen, MQTTClient_message *message) |
收到消息时执行,必须设置 |
dc |
void (void *context, MQTTClient_deliveryToken dt) |
发布确认时执行,可设 NULL |
ma 回调的两个关键点:
MQTTClient_freeMessage(&message) 与 MQTTClient_free(topicName)。返回值必须是 0 或 1:返回 1 表示处理成功;返回 0 表示处理有问题,客户端库会重新投递,此时不要释放内存,否则重新投递失败。
static void delivered(void *context, MQTTClient_deliveryToken dt)
{
printf("Message with token value %d delivery confirmed\n", dt);
}
static int msgarrvd(void *context, char *topicName, int topicLen,
MQTTClient_message *message)
{
printf("Message arrived\n");
printf("topic: %s\n", topicName);
printf("message: <%d>%s\n", message->payloadlen, (char *)message->payload);
MQTTClient_freeMessage(&message);
MQTTClient_free(topicName);
return 1;
}
static void connlost(void *context, char *cause)
{
printf("\nConnection lost\n");
printf(" cause: %s \n", cause);
}
MQTTClient_setCallbacks必须在连接服务器之前调用。reliable(默认 1)控制同步/异步发布:reliable=1时同步发布,必须完成当前消息确认后才能发布下一条。
int MQTTClient_publishMessage(MQTTClient handle,
const char *topicName,
MQTTClient_message *msg,
MQTTClient_deliveryToken *dt);
MQTTClient_message pubmsg = MQTTClient_message_initializer;
MQTTClient_deliveryToken token;
pubmsg.payload = "online";
pubmsg.payloadlen = 6;
pubmsg.qos = 0;
pubmsg.retained = 1;
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_publishMessage(client, "dt_mqtt/testTopic", &pubmsg, &token))) {
printf("Failed to publish message, return code %d\n", rc);
}
int MQTTClient_subscribe(MQTTClient handle, const char *topic, int qos);
int MQTTClient_unsubscribe(MQTTClient handle, const char *topic);
int MQTTClient_disconnect(MQTTClient handle, int timeout); // timeout 单位毫秒
void MQTTClient_destroy(MQTTClient *handle);
MQTTClient_subscribe(client, "dt_mqtt/testTopic", 0);
MQTTClient_unsubscribe(client, "dt_mqtt/testTopic");
MQTTClient_disconnect(client, 10000);
MQTTClient_destroy(&client);
工程路径:开发板光盘 → 11、Linux C 应用编程例程源码 → 33_mqtt/mqtt_prj。功能设计:
mqtt_prj/
├── CMakeLists.txt # 顶层 CMakeLists
├── cmake/
│ └── arm-linux-setup.cmake # 交叉编译工具链文件
├── mqttClient.c # 客户端应用程序
└── build/ # out-of-source 构建目录
cmake/arm-linux-setup.cmake##################################
# 配置 ARM 交叉编译
#################################
set(CMAKE_SYSTEM_NAME Linux) #设置目标系统名字
set(CMAKE_SYSTEM_PROCESSOR arm) #设置目标处理器架构
# 指定编译器的sysroot路径
set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
# 指定交叉编译器arm-linux-gcc
set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
# 为编译器添加编译选项
set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
#################################
# end
##################################
CMakeLists.txtcmake_minimum_required(VERSION 2.8.12)
project(MQTTClient C)
message(STATUS "CMake version: " ${CMAKE_VERSION})
message(STATUS "CMake system name: " ${CMAKE_SYSTEM_NAME})
message(STATUS "CMake system processor: " ${CMAKE_SYSTEM_PROCESSOR})
# 设置可执行文件输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
# 定义可执行文件目标
add_executable(mqttClient mqttClient.c)
# 指定MQTT客户端库头文件路径、库路径以及链接库
# ***大家需要根据MQTT的实际安装路径设置***
target_include_directories(mqttClient PRIVATE /home/alientek/tools/paho.mqtt.c-1.3.8/install/include) #MQTT头文件搜索路径
target_link_directories(mqttClient PRIVATE /home/alientek/tools/paho.mqtt.c-1.3.8/install/lib) #MQTT库文件搜索路径
target_link_libraries(mqttClient PRIVATE paho-mqtt3c) #MQTT链接库 libpaho-mqtt3c.so
逐段解释:
cmake_minimum_required(VERSION 2.8.12):设置最低 CMake 版本要求。project(MQTTClient C):工程名为 MQTTClient,语言 C。message(STATUS ...):打印 CMake 版本、目标系统名、目标处理器,确认交叉编译配置生效。set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin):可执行文件输出到 build/bin。add_executable(mqttClient mqttClient.c):定义目标 mqttClient。target_include_directories(... PRIVATE ...):只给该目标加 MQTT 头文件搜索路径。target_link_directories(... PRIVATE ...):只给该目标加 MQTT 库搜索路径。target_link_libraries(mqttClient PRIVATE paho-mqtt3c):链接同步模式 MQTT 库。mqttClient.c(完整源码)#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "MQTTClient.h" //包含MQTT客户端库头文件
/* ########################宏定义##################### */
/*
* 因为本地服务器是搭建在您的ubuntu上,所以此处的服务器地址
* 要填写您ubuntu的ip地址,笔者ubuntu的ip地址为192.168.6.161,
* 你们填写自己的ip地址即可
*/
#define BROKER_ADDRESS "192.168.6.161" //服务器地址
/*
* 客户端id、用户名、密码 *
* 客户端id可以随意填写,只需要保证跟MQTT.fx客户端使用的不同,
* 用户名要使用MQTT服务端注册的用户,即 mqtt1
* 密码也是之前设置的 123456
*/
#define CLIENTID "test3" //客户端id
#define USERNAME "mqtt1" //用户名
#define PASSWORD "123456" //密码
/*
* 以下 dt_mqtt/ 便是笔者的个人主题级别
*/
#define WILL_TOPIC "dt_mqtt/will" //遗嘱主题
#define LED_TOPIC "dt_mqtt/led" //LED主题
#define TEMP_TOPIC "dt_mqtt/temperature" //温度主题
/* ################################################# */
static int msgarrvd(void *context, char *topicName, int topicLen,
MQTTClient_message *message)
{
if (!strcmp(topicName, LED_TOPIC)) { //校验消息的主题
if (!strcmp("2", message->payload)) //如果接收到的消息是"2"则设置LED为呼吸灯模式
system("echo heartbeat > /sys/class/leds/sys-led/trigger");
if (!strcmp("1", message->payload)) { //如果是"1"则LED常亮
system("echo none > /sys/class/leds/sys-led/trigger");
system("echo 1 > /sys/class/leds/sys-led/brightness");
}
else if (!strcmp("0", message->payload)) { //如果是"0"则LED熄灭
system("echo none > /sys/class/leds/sys-led/trigger");
system("echo 0 > /sys/class/leds/sys-led/brightness");
}
// 接收到其它数据 不做处理
}
/* 释放占用的内存空间 */
MQTTClient_freeMessage(&message);
MQTTClient_free(topicName);
/* 退出 */
return 1;
}
static void connlost(void *context, char *cause)
{
printf("\nConnection lost\n");
printf(" cause: %s \n", cause);
}
int main(int argc, char *argv[])
{
MQTTClient client;
MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
MQTTClient_willOptions will_opts = MQTTClient_willOptions_initializer;
MQTTClient_message pubmsg = MQTTClient_message_initializer;
int rc;
/* 创建mqtt客户端对象 */
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_create(&client, BROKER_ADDRESS, CLIENTID,
MQTTCLIENT_PERSISTENCE_NONE, NULL))) {
printf("Failed to create client, return code %d\n", rc);
rc = EXIT_FAILURE;
goto exit;
}
/* 设置回调 */
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_setCallbacks(client, NULL, connlost,
msgarrvd, NULL))) {
printf("Failed to set callbacks, return code %d\n", rc);
rc = EXIT_FAILURE;
goto destroy_exit;
}
/* 连接MQTT服务器 */
will_opts.topicName = WILL_TOPIC; //遗嘱主题
will_opts.message = "Unexpected disconnection"; //遗嘱消息
will_opts.retained = 1; //保留消息
will_opts.qos = 0; //QoS0
conn_opts.will = &will_opts;
conn_opts.keepAliveInterval = 30; //心跳包间隔时间
conn_opts.cleansession = 0; //cleanSession标志
conn_opts.username = USERNAME; //用户名
conn_opts.password = PASSWORD; //密码
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_connect(client, &conn_opts))) {
printf("Failed to connect, return code %d\n", rc);
rc = EXIT_FAILURE;
goto destroy_exit;
}
printf("MQTT服务器连接成功!\n");
/* 发布上线消息 */
pubmsg.payload = "Online"; //消息的内容
pubmsg.payloadlen = 6; //内容的长度
pubmsg.qos = 0; //QoS等级
pubmsg.retained = 1; //保留消息
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_publishMessage(client, WILL_TOPIC, &pubmsg, NULL))) {
printf("Failed to publish message, return code %d\n", rc);
rc = EXIT_FAILURE;
goto disconnect_exit;
}
/* 订阅主题 dt_mqtt/led */
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_subscribe(client, LED_TOPIC, 0))) {
printf("Failed to subscribe, return code %d\n", rc);
rc = EXIT_FAILURE;
goto disconnect_exit;
}
/* 向服务端发布芯片温度信息 */
for ( ; ; ) {
MQTTClient_message tempmsg = MQTTClient_message_initializer;
char temp_str[10] = {0};
int fd;
/* 读取温度值 */
fd = open("/sys/class/thermal/thermal_zone0/temp", O_RDONLY);
read(fd, temp_str, sizeof(temp_str)); //读取temp属性文件即可获取温度
close(fd);
/* 发布温度信息 */
tempmsg.payload = temp_str; //消息的内容
tempmsg.payloadlen = strlen(temp_str); //内容的长度
tempmsg.qos = 0; //QoS等级
tempmsg.retained = 1; //保留消息
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_publishMessage(client, TEMP_TOPIC, &tempmsg, NULL))) {
printf("Failed to publish message, return code %d\n", rc);
rc = EXIT_FAILURE;
goto unsubscribe_exit;
}
sleep(30); //每隔30秒 更新一次数据
}
unsubscribe_exit:
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_unsubscribe(client, LED_TOPIC))) {
printf("Failed to unsubscribe, return code %d\n", rc);
rc = EXIT_FAILURE;
}
disconnect_exit:
if (MQTTCLIENT_SUCCESS !=
(rc = MQTTClient_disconnect(client, 10000))) {
printf("Failed to disconnect, return code %d\n", rc);
rc = EXIT_FAILURE;
}
destroy_exit:
MQTTClient_destroy(&client);
exit:
return rc;
}
三个主题的作用:
| 宏 | 主题 | 作用 |
|---|---|---|
WILL_TOPIC |
dt_mqtt/will |
遗嘱主题,正常上线时发布 "Online",意外断线时由服务端发布遗嘱消息 |
LED_TOPIC |
dt_mqtt/led |
开发板订阅;收到 "0"/"1"/"2" 分别控制 LED 熄灭/常亮/呼吸灯 |
TEMP_TOPIC |
dt_mqtt/temperature |
开发板每 30 秒发布一次 SoC 温度(读 /sys/class/thermal/thermal_zone0/temp) |
cd mqtt_prj/build
~/tools/cmake-3.16.0-Linux-x86_64/bin/cmake \
-DCMAKE_TOOLCHAIN_FILE=../cmake/arm-linux-setup.cmake \
-DCMAKE_BUILD_TYPE=Release ..
make
编译成功后生成 build/bin/mqttClient,拷贝到开发板 /home/root。
ping 通。./mqttClient,打印"MQTT服务器连接成功!"。test1)连接服务器,订阅 dt_mqtt/temperature,立即收到保留温度值,之后每 30 秒收到一次。dt_mqtt/led 发布 0/1/2,观察开发板 LED 熄灭/常亮/呼吸灯。⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。教材示例仅上报纯文本温度值,接入真实云平台时通常改为 JSON 格式并增加设备认证。
JSON 数据处理(cJSON / parson):物联网平台多要求负载为 JSON。可在开发板端引入轻量 JSON 库(如 cJSON、parson),把温度、状态打包成 JSON 发布,订阅端再解析。典型负载:
{ "temp": 45, "led": 1, "ts": 1730000000 }
cJSON 的典型用法(需自行移植该库并链接):
/* 扩展示例,非教材内容 */
cJSON *root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "temp", temperature);
cJSON_AddNumberToObject(root, "led", led_state);
char *out = cJSON_PrintUnformatted(root);
/* out 作为 pubmsg.payload 发布,payloadlen = strlen(out) */
cJSON_Delete(root);
原子云接入:正点原子原子云提供 MQTT 接入(设备三元组:产品 ID / 设备名 / 设备密钥),流程是先用三元组连接云平台,再按平台规定的主题上下行数据。具体主题、鉴权算法以原子云官方文档为准,此处不展开。
常见视频监控/直播使用 RTMP 和 RTSP 流媒体传输协议:
| 协议 | 提出方 | 特点 | 适用 |
|---|---|---|---|
| RTSP(Real-Time Stream Protocol) | Real Networks 与 Netscape | 基于文本的多媒体播放控制协议,定义流格式,流数据经 RTP 传输;实时性最好 | 视频聊天、视频监控 |
| RTMP(Real Time Message Protocol) | Adobe | 解决流媒体多路复用与分包,低延迟、稳定性高,支持所有摄像头格式,浏览器加载 flash 插件可直接播放 | 直播、推流 |
选择:RTSP 实时性最好但实现复杂;RTMP 浏览器支持好、生态成熟。本章用 FFmpeg + Nginx 通过 RTMP 推流实现视频监控/直播。
flowchart LR
Push["推流端<br/>开发板 (FFmpeg)"] -->|"RTMP 推流"| Srv["流媒体服务器<br/>Nginx + nginx-rtmp-module"]
Srv -->|"RTMP 拉流"| Pull["拉流端<br/>Windows VLC"]
classDef client fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
classDef server fill:#fef9c3,stroke:#ca8a04,color:#713f12
class Push,Pull client
class Srv server
三部分:推流客户端(FFmpeg)、流媒体服务器(Nginx + RTMP 模块)、拉流客户端(VLC)。
说明:本方案中开发板既是流媒体服务器又是推流端。出厂系统通常已移植好 Nginx 并在启动时自动运行,这里为了演示,重新手动移植。
wget http://nginx.org/download/nginx-1.20.0.tar.gz
git clone https://github.com/arut/nginx-rtmp-module.git
tar -xzf nginx-1.20.0.tar.gz
cd nginx-1.20.0
原生 Nginx 不支持 RTMP,需要第三方模块 nginx-rtmp-module。
初始化交叉编译环境:
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
修改两处源码,否则配置通不过:
| 文件 | 位置 | 修改 |
|---|---|---|
auto/cc/name |
第 21 行 | 注释掉 exit 1 |
auto/types/sizeof |
第 15 行 | ngx_size= 改为 ngx_size=4 |
auto/types/sizeof |
第 36 行 | $CC 改为 gcc |
./configure --prefix=/home/dt/tools/nginx-1.20.0/install \
--with-http_ssl_module \
--with-http_mp4_module \
--with-http_v2_module \
--without-http_upstream_zone_module \
--add-module=/home/dt/tools/nginx-rtmp-module
--prefix:Nginx 安装路径。--add-module:添加第三方模块(指向 nginx-rtmp-module 源码路径)。
make
首次 make 会报错,此时编辑 objs/ngx_auto_config.h,添加:
#ifndef NGX_HAVE_SYSVSHM
#define NGX_HAVE_SYSVSHM 1
#endif
再次 make 即可编译成功,然后:
make install
安装目录下:
conf/:配置文件,重点是 nginx.conf;sbin/nginx:可执行程序("not stripped",约 7.5MB,可执行 arm-poky-linux-gnueabi-strip --strip-debug nginx 去掉调试信息);logs/、html/。
# 开发板上先移除出厂系统自带的 nginx
rm -rf /usr/sbin/nginx
rm -rf /etc/nginx/*
# 拷贝新编译的 nginx 到 /home/root
# 拷贝 conf、logs、html 到 /etc/nginx
./nginx -V # 查看版本信息
./nginx -h # 查看帮助
./nginx -p /etc/nginx # 启动(-p 指定前缀路径)
ps -aux # 查看服务进程
在电脑浏览器输入开发板 IP,出现 "welcome to nginx" 即正常。
编辑 /etc/nginx/conf/nginx.conf,添加:
rtmp {
server {
listen 1935; # 监听 1935 端口
chunk_size 4096;
application live{
allow publish 127.0.0.1;
allow play all;
live on; # 打开直播
record off; # 关闭 record
meta copy;
}
application hls {
live on;
hls on;
hls_path /tmp/hls;
hls_fragment 8s;
}
}
}
./nginx -p /etc/nginx -s reload # 重载配置
ffmpeg -re -i /run/media/mmcblk0p1/testVideo.mp4 -c:av copy -f flv rtmp://127.0.0.1/live/mytest
-re:按原始帧率读取(实时推流)。-i:输入视频文件。rtmp://127.0.0.1/live/mytest:推给本机流媒体服务器的 live 应用,流名 mytest(服务器与推流端都是开发板,故 IP 为 127.0.0.1)。拉流:Windows 上打开 VLC → 媒体 → 打开网络串流 → 输入 rtmp://<开发板IP>/live/mytest → 播放,既有画面也有声音。
ffmpeg -f v4l2 -video_size 320x240 -framerate 15 -i /dev/video2 -q 10 -f flv rtmp://127.0.0.1/live/mytest
-f v4l2:使用 V4L2 采集。-video_size 320x240 -framerate 15:分辨率与帧率。-i /dev/video2:摄像头设备节点(USB 摄像头编号需按实际确认)。-q 10:视频质量参数。-f flv:输出 FLV 封装,便于 RTMP 传输。在 Windows 下用 VLC 拉流即可看到摄像头画面。
实测延迟约 5~6 秒,原因:
结论:普通开发板软编解码能力有限,追求低延迟应使用带硬件编解码的更高性能平台。
该实现来自本知识库配套的 Qt 例程
Embedded-Qt-Tutorial/Qt/04/05_video_surveillance,与 PDF 第三十四章的 Nginx 方案是两条不同路线:Nginx 方案适合广域/公网推拉流,本方案适合开发板与客户端同网段的轻量预览,结构简单、无需服务器进程。 摄像头采集部分"请参考正点原子 C 应用编程 V4L2 章节"(源码注释原文)。
flowchart LR
subgraph Server["video_server(开发板)"]
V4L2["/dev/video1<br/>V4L2 采集 RGB565"] --> QImg["QImage (640x480)"]
QImg -->|"save JPEG"| B64["base64 编码"]
B64 -->|"UDP 广播 :8888"| Net["QUdpSocket"]
end
subgraph Client["video_client(PC / 开发板)"]
Rcv["QUdpSocket bind :8888"] --> Dec["base64 解码 + loadFromData"]
Dec --> Show["QLabel 显示"]
end
Net --> Rcv
classDef dev fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
classDef pc fill:#dcfce7,stroke:#16a34a,color:#14532d
class V4L2,QImg,B64,Net dev
class Rcv,Dec,Show pc
QImage → 编码 JPEG → base64 → UDP 广播到 8888 端口。QImage → 显示。capture_thread.h#ifndef CAPTURE_THREAD_H
#define CAPTURE_THREAD_H
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#ifdef linux
#include <linux/fb.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <linux/input.h>
#endif
#include <QThread>
#include <QDebug>
#include <QPushButton>
#include <QImage>
#include <QByteArray>
#include <QBuffer>
#include <QTime>
#include <QUdpSocket>
#define VIDEO_DEV "/dev/video1"
#define FB_DEV "/dev/fb0"
#define VIDEO_BUFFER_COUNT 3
struct buffer_info {
void *start;
unsigned int length;
};
class CaptureThread : public QThread
{
Q_OBJECT
signals:
void imageReady(QImage);
void sendImage(QImage);
private:
bool startFlag = false; // 线程开启flag
bool startBroadcast = false; // 开启广播flag
bool startLocalDisplay = false; // 本地显示flag
void run() override;
public:
CaptureThread(QObject *parent = nullptr) {
Q_UNUSED(parent);
}
public slots:
void setThreadStart(bool start) {
startFlag = start;
if (start) {
if (!this->isRunning())
this->start();
} else {
this->quit();
}
}
void setBroadcast(bool start) { startBroadcast = start; }
void setLocalDisplay(bool start) { startLocalDisplay = start; }
};
#endif // CAPTURE_THREAD_H
capture_thread.cpp(V4L2 + UDP 广播核心)#include "capture_thread.h"
void CaptureThread::run()
{
/* 下面的代码请参考正点原子C应用编程V4L2章节,摄像头编程,这里不作解释 */
#ifdef linux
#ifndef __arm__
return;
#endif
int video_fd = -1;
struct v4l2_format fmt;
struct v4l2_requestbuffers req_bufs;
static struct v4l2_buffer buf;
int n_buf;
struct buffer_info bufs_info[VIDEO_BUFFER_COUNT];
enum v4l2_buf_type type;
video_fd = open(VIDEO_DEV, O_RDWR);
if (0 > video_fd) {
printf("ERROR: failed to open video device %s\n", VIDEO_DEV);
return ;
}
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
if (0 > ioctl(video_fd, VIDIOC_S_FMT, &fmt)) {
printf("ERROR: failed to VIDIOC_S_FMT\n");
close(video_fd);
return ;
}
req_bufs.count = VIDEO_BUFFER_COUNT;
req_bufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req_bufs.memory = V4L2_MEMORY_MMAP;
if (0 > ioctl(video_fd, VIDIOC_REQBUFS, &req_bufs)) {
printf("ERROR: failed to VIDIOC_REQBUFS\n");
return ;
}
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
buf.index = n_buf;
if (0 > ioctl(video_fd, VIDIOC_QUERYBUF, &buf)) {
printf("ERROR: failed to VIDIOC_QUERYBUF\n");
return ;
}
bufs_info[n_buf].length = buf.length;
bufs_info[n_buf].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE, MAP_SHARED,
video_fd, buf.m.offset);
if (MAP_FAILED == bufs_info[n_buf].start) {
printf("ERROR: failed to mmap video buffer, size 0x%x\n", buf.length);
return ;
}
}
for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
buf.index = n_buf;
if (0 > ioctl(video_fd, VIDIOC_QBUF, &buf)) {
printf("ERROR: failed to VIDIOC_QBUF\n");
return ;
}
}
type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (0 > ioctl(video_fd, VIDIOC_STREAMON, &type)) {
printf("ERROR: failed to VIDIOC_STREAMON\n");
return ;
}
while (startFlag) {
for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
buf.index = n_buf;
if (0 > ioctl(video_fd, VIDIOC_DQBUF, &buf)) {
printf("ERROR: failed to VIDIOC_DQBUF\n");
return;
}
QImage qImage((unsigned char*)bufs_info[n_buf].start,
fmt.fmt.pix.width, fmt.fmt.pix.height,
QImage::Format_RGB16);
/* 是否开启本地显示,开启本地显示可能会导致开启广播卡顿,它们互相制约 */
if (startLocalDisplay)
emit imageReady(qImage);
/* 是否开启广播,开启广播会导致本地显示卡顿,它们互相制约 */
if (startBroadcast) {
QUdpSocket udpSocket; /* udp套接字 */
QByteArray byte; /* QByteArray类型 */
QBuffer buff(&byte); /* 建立一个用于IO读写的缓冲区 */
qImage.save(&buff, "JPEG", -1); /* image转为byte的类型,再存入buff */
QByteArray base64Byte = byte.toBase64(); /* 转换为base64Byte类型 */
/* 由udpSocket以广播的形式传输数据,端口号为8888 */
udpSocket.writeDatagram(base64Byte.data(), base64Byte.size(),
QHostAddress::Broadcast, 8888);
}
if (0 > ioctl(video_fd, VIDIOC_QBUF, &buf)) {
printf("ERROR: failed to VIDIOC_QBUF\n");
return;
}
}
}
msleep(800);//at lease 650
for (int i = 0; i < VIDEO_BUFFER_COUNT; i++) {
munmap(bufs_info[i].start, buf.length);
}
close(video_fd);
#endif
}
V4L2 采集流程(与 C 应用编程 V4L2 章节一致):
| 步骤 | ioctl / 操作 | 说明 |
|---|---|---|
| 打开设备 | open(VIDEO_DEV, O_RDWR) |
/dev/video1 为摄像头节点(按实际确认) |
| 设置格式 | VIDIOC_S_FMT |
640×480、V4L2_PIX_FMT_RGB565 |
| 申请缓冲 | VIDIOC_REQBUFS |
3 个缓冲区,V4L2_MEMORY_MMAP |
| 查询并映射 | VIDIOC_QUERYBUF + mmap |
得到每个缓冲区的地址与长度 |
| 入队 | VIDIOC_QBUF |
把所有缓冲区放入采集队列 |
| 启动流 | VIDIOC_STREAMON |
开始采集 |
| 循环取帧 | VIDIOC_DQBUF → 处理 → VIDIOC_QBUF |
取出一帧、转 QImage、处理完再入队 |
| 停止 | 循环退出后 munmap + close |
while(startFlag) 控制 |
广播处理链:QImage → QBuffer(内存 IO)→ save(..., "JPEG", -1) 编码为 JPEG → toBase64() → QUdpSocket::writeDatagram(..., QHostAddress::Broadcast, 8888)。
注意:本地显示与广播会互相制约(同一线程里串行处理),源码注释也明确说明开启广播会导致本地显示卡顿,反之亦然。
mainwindow.cpp#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
this->setGeometry(0, 0, 800, 480);
videoLabel = new QLabel(this);
videoLabel->setText("未获取到图像数据或未开启本地显示");
videoLabel->setStyleSheet("QWidget {color: white;}");
videoLabel->setAlignment(Qt::AlignCenter);
videoLabel->resize(640, 480);
checkBox1 = new QCheckBox(this);
checkBox2 = new QCheckBox(this);
checkBox1->resize(120, 50);
checkBox2->resize(120, 50);
checkBox1->setText("本地显示");
checkBox2->setText("开启广播");
checkBox1->setStyleSheet("QCheckBox {color: yellow;}"
"QCheckBox:indicator {width: 40; height: 40;}");
checkBox2->setStyleSheet("QCheckBox {color: yellow;}"
"QCheckBox:indicator {width: 40; height: 40}");
startCaptureButton = new QPushButton(this);
startCaptureButton->setCheckable(true);
startCaptureButton->setText("开始采集摄像头数据");
QColor color = QColor(Qt::black);
QPalette p;
p.setColor(QPalette::Window, color);
this->setPalette(p);
startCaptureButton->setStyleSheet("QPushButton {background-color: white; border-radius: 30}"
"QPushButton:pressed {background-color: red;}");
captureThread = new CaptureThread(this);
connect(startCaptureButton, SIGNAL(clicked(bool)), captureThread, SLOT(setThreadStart(bool)));
connect(startCaptureButton, SIGNAL(clicked(bool)), this, SLOT(startCaptureButtonClicked(bool)));
connect(captureThread, SIGNAL(imageReady(QImage)), this, SLOT(showImage(QImage)));
connect(checkBox1, SIGNAL(clicked(bool)), captureThread, SLOT(setLocalDisplay(bool)));
connect(checkBox2, SIGNAL(clicked(bool)), captureThread, SLOT(setBroadcast(bool)));
}
MainWindow::~MainWindow() {}
void MainWindow::showImage(QImage image)
{
videoLabel->setPixmap(QPixmap::fromImage(image));
}
void MainWindow::startCaptureButtonClicked(bool start)
{
if (start)
startCaptureButton->setText("停止采集摄像头数据");
else
startCaptureButton->setText("开始采集摄像头数据");
}
mainwindow.cpp(接收并显示)#include "mainwindow.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QColor color = QColor(Qt::black);
QPalette p;
p.setColor(QPalette::Window, color);
this->setPalette(p);
udpSocket = new QUdpSocket(this);
udpSocket->bind(QHostAddress::Any, 8888); /* 绑定端口号 */
videoLabel = new QLabel(this);
videoLabel->resize(640, 480);
videoLabel->setText("未获取到图像数据");
videoLabel->setStyleSheet("QWidget {color: white;}");
videoLabel->setAlignment(Qt::AlignCenter);
connect(udpSocket, SIGNAL(readyRead()), this, SLOT(videoUpdate()));
this->setGeometry(0, 0, 800, 480);
}
MainWindow::~MainWindow() {}
void MainWindow::videoUpdate()
{
QByteArray datagram;
datagram.resize(udpSocket->pendingDatagramSize()); /* 数据大小重置 */
udpSocket->readDatagram(datagram.data(), datagram.size()); /* 读入datagram */
QByteArray decryptedByte;
decryptedByte = QByteArray::fromBase64(datagram.data()); /* base64 解码 */
QImage image;
image.loadFromData(decryptedByte); /* 从数据加载图像 */
videoLabel->setPixmap(QPixmap::fromImage(image)); /* 显示图像 */
}
.pro)video_server.pro 与 video_client.pro 都需要网络模块:
QT += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# video_server.pro
SOURCES += \
capture_thread.cpp \
main.cpp \
mainwindow.cpp
HEADERS += \
capture_thread.h \
mainwindow.h
# video_client.pro
# SOURCES += main.cpp mainwindow.cpp
# HEADERS += mainwindow.h
video_server(需摄像头 /dev/video1,且仅 ARM 平台编译采集代码)。video_client,绑定 8888 端口接收广播并显示。⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。
RTMP 推流常见编码为 H.264;而上面 Qt 方案用 JPEG 逐帧广播(MJPEG 风格),优点是实现简单、无需编码库,缺点是带宽占用大、无法跨公网高效传输。若要在开发板上做低延迟、低带宽的监控,思路是:
| 现象 | 原因 | 处理 |
|---|---|---|
| 开发板连不上 MQTT | 服务器 IP/端口错、用户名密码错、不在同一网段 | 确认 BROKER_ADDRESS、ping 通、mosquitto 已启动 |
| 连接返回码 4/5 | 用户名或密码无效 / 未授权 | 用 mosquitto_passwd 重新注册,核对 myconfig.conf |
编译 mqttClient 找不到 MQTTClient.h |
头文件路径未指定 | target_include_directories 指向 install/include |
链接时报找不到 paho-mqtt3c |
库路径未指定或库未安装 | target_link_directories + target_link_libraries,并 make install |
开发板运行报找不到 libpaho-mqtt3c.so |
动态库未拷入 | 打包 .so 解压到 /usr/lib,或设置 LD_LIBRARY_PATH |
| VLC 拉不到 RTMP 流 | nginx 未启动/未 reload、端口被占、推流地址错 | ps 查进程、nginx -s reload、核对 1935 与流名 |
| 视频延迟大 | 软编解码 + 服务器与推流端同板 | 降低分辨率/帧率,或换高性能平台 |
| ARM 上采集线程直接返回 | 采集代码用 #ifndef __arm__ return; 限定 ARM |
确认交叉编译宏 __arm__,PC 上不采集 |
| 项目 | IMX6U(本方案) | STM32(如 F4/F7 + LwIP) | 高性能 ARM Linux(RK3568 等) |
|---|---|---|---|
| MQTT 客户端 | paho.mqtt.c(POSIX,同步/异步) | MQTT 嵌入式库(如 Paho Embedded / coreMQTT) | 同 paho / 各云 SDK |
| 视频方案 | Nginx+FFmpeg(软编) 或 V4L2+JPEG 广播 | 一般无 Linux,靠专用编码/传输 | 硬件 H.264 编码 + RTSP/RTMP,低延迟 |
| 网络栈 | 内核 TCP/IP + BSD socket | LwIP | 内核 TCP/IP |
| 构建 | CMake + 交叉工具链 | Keil/IAR/Makefile | CMake/Yocto/Buildroot |
要点:Broker 为枢纽,客户端可发布/订阅,围绕主题通信;发布/订阅解耦,且可空间分离、时间异步。
详解:发布者把消息发到某个主题,Broker 按订阅关系转发给订阅者;客户端角色随主题切换,彼此无需知道对方存在。相比 HTTP 的请求-响应、短连接,MQTT 保持长连接、低开销、支持一对多推送与离线消息,更适合物联网。区别:
| 维度 | MQTT | HTTP |
|---|---|---|
| 模型 | 发布/订阅 | 请求/响应 |
| 连接 | 长连接 + 心跳 | 多为短连接 |
| 开销 | 极小 | 头部冗余大 |
| 推送 | 服务端可主动推送 | 需轮询/长轮询/WS |
追问:
要点:0 最多一次、1 至少一次、2 保证一次;越高质量越安全也越慢。
详解:QoS0 发完即弃,靠 TCP;QoS1 用 PUBACK 确认,超时重发,可能重复(协议不去重,需按 dup 处理);QoS2 经 PUBREC/PUBREL/PUBCOMP 四次交互,保证接收端只收一次。实现 QoS>0 必须 cleanSession=false。发布与订阅 QoS 不同时,服务端按较低值降级。
追问:
cleanSession 和会话保持有什么关系?保留消息有什么用?要点:cleanSession=0 建持久会话、可收离线消息并保留订阅;保留消息让新订阅者立刻拿到该主题最新值。
详解:cleanSession=1 每次连接都是全新会话,不保存订阅、不收离线消息,sessionPresent 恒为 0;cleanSession=0 时服务端保存会话,sessionPresent 表示是否恢复。保留消息针对"发布在前、订阅在后"的场景,每个主题只保留一条最新值,发布空保留消息可删除。
追问:
cleanSession=false)要点:心跳检测客户端在线状态;遗嘱在客户端意外断线时通知其它客户端。
详解:客户端空闲时按 keepAlive 定时发 PINGREQ,服务端回 PINGRESP,长时间收不到即判定掉线;客户端发 PINGREQ 收不到响应则认为自身断线。遗嘱在 CONNECT 时设置(willTopic/willMessage/willRetain/willQoS),仅意外断线触发,主动 DISCONNECT 不触发。
追问:
要点:推流端 FFmpeg + 服务器 Nginx(rtmp 模块) + 拉流端 VLC;延迟主要来自软编解码和同板部署。
详解:ffmpeg -re -i ... -f flv rtmp://.../live/xxx 推流,VLC 拉流;Nginx 加 nginx-rtmp-module 并配置 rtmp { server { application live {...} } }。延迟原因:I.MX6U 无硬件视频解码,全靠软件处理,且服务器与推流端在同一弱性能板子上。另一条 V4L2+JPEG 广播路线(Qt 例程)无需流媒体服务器:采集 RGB565 → JPEG → base64 → UDP 广播,客户端解码显示;实现简单、适合同网段,但带宽大、本地显示与广播互相制约。
追问:
内容来源:《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十三章 实战小项目之MQTT物联网、第三十四章 实战小项目之视频监控;例程 33_mqtt/mqtt_prj(CMakeLists.txt、mqttClient.c、cmake/arm-linux-setup.cmake);Qt 例程 Embedded-Qt-Tutorial/Qt/04/05_video_surveillance(video_server、video_client)。第 7 节(JSON/原子云)与第 12 节(H.264)为扩展知识。