04-实战项目MQTT与视频监控.md 63 KB


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

pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第三十三章 实战小项目之MQTT物联网、第三十四章 实战小项目之视频监控;《嵌入式Qt开发指南》视频监控例程"

实战项目MQTT与视频监控

💡 关联知识:[[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 物联网

1. MQTT 简介

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是一种基于客户端-服务端架构的发布/订阅模式消息传输协议,与 HTTP 一样工作在 TCP/IP 四层模型的应用层,构建于 TCP/IP 之上。它的设计思想是轻巧、开放、简单、规范,易于实现,因此特别适合机器与机器(M2M)、物联网(IoT)等受限环境。

1.1 主要特性

特性 说明
发布/订阅模式 一对多的消息发布,解除应用程序耦合
基于 TCP/IP 主流 MQTT 基于 TCP;也有基于 UDP 的 MQTT-SN
支持 QoS 按消息重要性设置不同服务质量等级(0/1/2)
小型传输 开销很小,协议交换最小化,降低网络流量
遗嘱机制(will) 客户端异常断线时通知其它客户端
主题寻址 基于主题发布/订阅,对负载内容屏蔽
心跳机制 检测客户端是否在线

1.2 历史与版本

  • 1999 年由 Andy Stanford-Clark 和 Arlen Nipper 发明,最初用于石油管道传感器与卫星之间的数据传输。
  • 面向"低带宽、高延迟或不可靠网络",要求:易于实现、服务质量可控、占用带宽小、能适应掉线断网、云端与设备保持长连接。
  • 主流版本:MQTT 3.1.1(2014-10,经典主流)与 MQTT 5(2019-03,完全兼容 3.1.1 并增加新功能)。
  • MQTT 更适合需要实时控制的场合(尤其执行器);由于要保持长连接、定时发心跳,并不适合低功耗场合

2. MQTT 协议原理

2.1 三个核心角色

角色 说明
服务端(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

发布/订阅的三个特性都带一个"可"字,体现灵活性:

  • 客户端相互独立:发布者不知道有多少订阅者,订阅者之间也不知道彼此存在。
  • 空间上可分离:双方只要联网,无论远近都能通信。
  • 时间上可异步:发送与接收无需同步,服务端可暂存离线消息。

2.2 连接服务端:CONNECT 与 CONNACK

客户端连接服务端分两步:客户端发 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 保留备用

sessionPresentcleanSession 配合:cleanSession=0 时,若服务端保存了上次会话状态则返回 1,否则 0;cleanSession=1 时恒为 0。

2.3 发布、订阅、取消订阅

报文 作用
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 节。

2.4 主题进阶

  • 区分大小写,可用空格但建议不用,不要用中文(多数服务器不支持),建议使用 ASCII 字符。
  • 分级:用 / 分隔,如 home/sensor/led/brightness 共四级;主题不要以 / 开头。
  • 通配符(只能在订阅时用):
    • 单级通配符 +:匹配一个主题级别,如 home/sensor/+/status
    • 多级通配符 #:匹配任意数量级别,如 home/sensor/#
  • $ 开头的是服务端系统保留主题(如 $SYS/#),客户端不可随意订阅或发布。

2.5 QoS 服务质量

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

要点:

  • 要实现 QoS>0,连接时 cleanSession 必须设为 false,否则收不到任何离线消息(含 QoS1/QoS2)。
  • QoS=1 时协议本身不去重,需应用根据 dup 标志自行处理。
  • 服务质量降级:若发布用 QoS2、订阅用 QoS1,服务端会采用两者中较低的 QoS 提供服务。

2.6 保留消息(retain)

  • 作用:让服务端保留该主题的最新一条消息,任何客户端订阅该主题后立即收到它,不必等下一次发布。
  • 每个主题只能有一条保留消息,发布新的保留消息会覆盖旧的。
  • 删除保留消息:向该主题发布一条空的保留消息即可。

2.7 心跳机制

客户端在空闲时定时发送 PINGREQ 报文(心跳请求),服务端收到后回 PINGRESP(心跳响应)。服务端据此判断客户端是否在线;客户端若发 PINGREQ 后收不到 PINGRESP,则认为自己已断线。发送间隔由 CONNECT 报文的 keepAlive 指定。

2.8 遗嘱机制(will)

客户端在"活着"时通过 CONNECT 报文预先写好遗嘱,一旦意外断线(非主动发 DISCONNECT,如断网、断电),服务端就把遗嘱消息发布出去。主动断开不会触发遗嘱。

字段 说明
willTopic 遗嘱主题,订阅该主题的客户端才会收到遗嘱消息
willMessage 遗嘱内容
willRetain 遗嘱的保留标志
willQoS 遗嘱的 QoS 等级(0/1/2)

小技巧:客户端上线时主动向自己的遗嘱主题发一条"在线"消息,订阅者即可据此实现上线通知。

2.9 用户名密码认证

CONNECT 报文中的 usernamepassword 是可选字段。服务端若开启认证,则客户端必须提供正确的用户名密码才能连接;有些服务端还用它识别客户端归属、管理私人主题权限。

3. 搭建 MQTT 服务器

3.1 公用 MQTT 服务器

服务器 地址 端口
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

公用服务器仅供学习测试,不可商用;国内服务器连接快、延迟低。

3.2 用 mosquitto 自建服务器(Ubuntu)

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 通。

3.3 用 MQTT.fx 初体验

  1. 下载安装 MQTT.fx(官网 http://mqttfx.jensd.de/,教程使用 1.7.1 版本)。
  2. 点击齿轮按钮新建配置:填写 Broker 地址(如 192.168.6.161,即 Ubuntu 的 IP)、Client ID(如 test1)、用户名 mqtt1、密码 123456,Apply 后连接。
  3. 订阅主题:切到 Subscribe 页,填写主题(如 dt2914/testTopic),点 Subscribe。
  4. 发布消息:切到 Publish 页,填写主题与内容,点 Publish;订阅同一个主题的客户端即可收到。
  5. 取消订阅:在已订阅主题上点击取消即可。

手机端也可用 MQTT Client 软件连接同一服务器(Client ID 不能与其它客户端重复),实现跨设备收发。

4. 移植 paho.mqtt.c 客户端库

4.1 下载源码

官网 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 构建的工程。

4.2 交叉编译

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_SSLPAHO_BUILD_SAMPLES 是缓存变量,含义见工程 README.md
  • MQTT 库依赖 openssl;示例未单独移植 openssl,因为开发板出厂系统已带 openssl,且交叉编译器会经 sysroot 链接它。

4.3 安装产物

install/ 下有 binincludelibshare

库文件 说明
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

4.4 拷贝库到开发板

# 在 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 及其符号链接一起拷贝。

5. MQTT 客户端库 API(同步模式)

以下函数定义在 MQTTClient.h。文档见源码 docs/MQTTClient/html/index.html

5.1 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 宏初始化。

5.2 创建客户端对象

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 时传 NULLDEFAULT 时为持久化目录(传 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;
}

5.3 连接服务端

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;
}

5.4 设置回调

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 时同步发布,必须完成当前消息确认后才能发布下一条。

5.5 发布消息

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);
}

5.6 订阅 / 取消订阅 / 断开 / 销毁

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);

6. 完整可复制工程:mqtt_prj

工程路径:开发板光盘 → 11、Linux C 应用编程例程源码33_mqtt/mqtt_prj。功能设计:

  • 基于 mosquitto 搭建的本地服务器,实现个人物联网小项目;
  • 手机/电脑可远程控制开发板上的一颗 LED;
  • 开发板每隔 30 秒向服务端发布 SoC 温度,手机/电脑可订阅查看。

6.1 工程目录结构

mqtt_prj/
├── CMakeLists.txt              # 顶层 CMakeLists
├── cmake/
│   └── arm-linux-setup.cmake   # 交叉编译工具链文件
├── mqttClient.c                # 客户端应用程序
└── build/                      # out-of-source 构建目录

6.2 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
##################################

6.3 CMakeLists.txt

cmake_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 库。

6.4 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

6.5 构建与编译

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

6.6 测试演示

  1. 确保开发板与 Ubuntu 在同一局域网、能互相 ping 通。
  2. 开发板执行 ./mqttClient,打印"MQTT服务器连接成功!"。
  3. 用 MQTT.fx(Client ID 用 test1)连接服务器,订阅 dt_mqtt/temperature,立即收到保留温度值,之后每 30 秒收到一次。
  4. dt_mqtt/led 发布 0/1/2,观察开发板 LED 熄灭/常亮/呼吸灯。
  5. 也可用手机 MQTT Client(Client ID 不能重复)订阅温度、发布 LED 控制。

7. 扩展:JSON 数据处理与云平台接入

⚠️ 来源说明:本节不属于《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 / 设备名 / 设备密钥),流程是先用三元组连接云平台,再按平台规定的主题上下行数据。具体主题、鉴权算法以原子云官方文档为准,此处不展开。


第二部分:视频监控

8. 方案概览

常见视频监控/直播使用 RTMPRTSP 流媒体传输协议:

协议 提出方 特点 适用
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)。

9. Nginx 移植

说明:本方案中开发板既是流媒体服务器又是推流端。出厂系统通常已移植好 Nginx 并在启动时自动运行,这里为了演示,重新手动移植。

9.1 下载源码与 RTMP 模块

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

9.2 交叉编译前的源码修改

初始化交叉编译环境:

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

9.3 配置、编译、安装

./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

9.4 部署到开发板

安装目录下:

  • 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
    

9.5 测试 Nginx

./nginx -V                 # 查看版本信息
./nginx -h                 # 查看帮助
./nginx -p /etc/nginx      # 启动(-p 指定前缀路径)
ps -aux                    # 查看服务进程

在电脑浏览器输入开发板 IP,出现 "welcome to nginx" 即正常。

9.6 配置 RTMP

编辑 /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    # 重载配置

10. 用 FFmpeg 推流、VLC 拉流

10.1 推送视频文件

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 → 播放,既有画面也有声音。

10.2 推送摄像头(USB 摄像头)

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 拉流即可看到摄像头画面。

10.3 延迟分析

实测延迟约 5~6 秒,原因:

  • I.MX6U 没有硬件视频解码,FFmpeg 内部对音视频的处理全靠软件,耗时大;
  • 服务器和推流端都在同一块性能较弱的开发板上,负担更重。

结论:普通开发板软编解码能力有限,追求低延迟应使用带硬件编解码的更高性能平台。

11. 方案二:V4L2 + JPEG 广播的局域网 MJPEG 监控

该实现来自本知识库配套的 Qt 例程 Embedded-Qt-Tutorial/Qt/04/05_video_surveillance,与 PDF 第三十四章的 Nginx 方案是两条不同路线:Nginx 方案适合广域/公网推拉流,本方案适合开发板与客户端同网段的轻量预览,结构简单、无需服务器进程。 摄像头采集部分"请参考正点原子 C 应用编程 V4L2 章节"(源码注释原文)。

11.1 整体结构

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
  • 服务端:V4L2 采集一帧 RGB565 → 转 QImage → 编码 JPEG → base64 → UDP 广播到 8888 端口。
  • 客户端:绑定 8888 端口接收 UDP 数据报 → base64 解码 → 从数据加载 QImage → 显示。

11.2 服务端采集线程 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

11.3 服务端采集线程 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) 控制

广播处理链:QImageQBuffer(内存 IO)→ save(..., "JPEG", -1) 编码为 JPEG → toBase64()QUdpSocket::writeDatagram(..., QHostAddress::Broadcast, 8888)

注意:本地显示与广播会互相制约(同一线程里串行处理),源码注释也明确说明开启广播会导致本地显示卡顿,反之亦然。

11.4 服务端窗口 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("开始采集摄像头数据");
}

11.5 客户端 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));          /* 显示图像 */
}

11.6 工程文件(.pro

video_server.provideo_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

11.7 运行与测试

  1. 在开发板上交叉编译、运行 video_server(需摄像头 /dev/video1,且仅 ARM 平台编译采集代码)。
  2. 勾选"本地显示"在开发板屏幕预览;勾选"开启广播"后,同网段客户端才能收到画面。
  3. 在 PC(或另一块开发板)上运行 video_client,绑定 8888 端口接收广播并显示。
  4. 注意:本地显示与广播互相制约,追求流畅广播时建议关闭本地显示。

12. 扩展:H.264 编码与低延迟方向

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。

RTMP 推流常见编码为 H.264;而上面 Qt 方案用 JPEG 逐帧广播(MJPEG 风格),优点是实现简单、无需编码库,缺点是带宽占用大、无法跨公网高效传输。若要在开发板上做低延迟、低带宽的监控,思路是:

  • 采集端用 V4L2 取 YUV 帧,经硬件/软件 H.264 编码器压缩后再传输;
  • 传输可用 RTSP/RTMP(配合 FFmpeg/Nginx)或自定义 TCP/UDP 协议;
  • I.MX6U 无硬件视频编解码单元,软编码 H.264 耗 CPU,实际低延迟方案应评估平台能力。

13. 实验步骤与调试

现象 原因 处理
开发板连不上 MQTT 服务器 IP/端口错、用户名密码错、不在同一网段 确认 BROKER_ADDRESSping 通、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 上不采集

14. 跨平台对比

项目 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

15. 面试精选(5 题)

Q1 简述 MQTT 的发布/订阅模型与三大角色。它和 HTTP 的区别?

要点:Broker 为枢纽,客户端可发布/订阅,围绕主题通信;发布/订阅解耦,且可空间分离、时间异步。

详解:发布者把消息发到某个主题,Broker 按订阅关系转发给订阅者;客户端角色随主题切换,彼此无需知道对方存在。相比 HTTP 的请求-响应、短连接,MQTT 保持长连接、低开销、支持一对多推送与离线消息,更适合物联网。区别:

维度 MQTT HTTP
模型 发布/订阅 请求/响应
连接 长连接 + 心跳 多为短连接
开销 极小 头部冗余大
推送 服务端可主动推送 需轮询/长轮询/WS

追问

  1. 发布/订阅的"三个可"是什么?(客户端相互独立、空间可分离、时间可异步)
  2. Broker 的职责有哪些?(接收、存储、转发消息,管理客户端与会话)

Q2 MQTT 的 QoS 0/1/2 分别是什么?如何选择?

要点:0 最多一次、1 至少一次、2 保证一次;越高质量越安全也越慢。

详解:QoS0 发完即弃,靠 TCP;QoS1 用 PUBACK 确认,超时重发,可能重复(协议不去重,需按 dup 处理);QoS2 经 PUBREC/PUBREL/PUBCOMP 四次交互,保证接收端只收一次。实现 QoS>0 必须 cleanSession=false。发布与订阅 QoS 不同时,服务端按较低值降级。

追问

  1. 为什么 QoS1 需要应用自己去重?(协议只保证至少一次,重复由重发引起)
  2. QoS2 的报文顺序?(PUBLISH → PUBREC → PUBREL → PUBCOMP)

Q3 cleanSession 和会话保持有什么关系?保留消息有什么用?

要点cleanSession=0 建持久会话、可收离线消息并保留订阅;保留消息让新订阅者立刻拿到该主题最新值。

详解cleanSession=1 每次连接都是全新会话,不保存订阅、不收离线消息,sessionPresent 恒为 0;cleanSession=0 时服务端保存会话,sessionPresent 表示是否恢复。保留消息针对"发布在前、订阅在后"的场景,每个主题只保留一条最新值,发布空保留消息可删除。

追问

  1. 想接收离线 QoS1/QoS2 消息该怎样设置?(cleanSession=false
  2. 如何删除某主题的保留消息?(向该主题发布一条 payload 为空的保留消息)

Q4 MQTT 心跳与遗嘱机制分别在解决什么问题?

要点:心跳检测客户端在线状态;遗嘱在客户端意外断线时通知其它客户端。

详解:客户端空闲时按 keepAlive 定时发 PINGREQ,服务端回 PINGRESP,长时间收不到即判定掉线;客户端发 PINGREQ 收不到响应则认为自身断线。遗嘱在 CONNECT 时设置(willTopic/willMessage/willRetain/willQoS),仅意外断线触发,主动 DISCONNECT 不触发。

追问

  1. 遗嘱在什么情况下不会发布?(客户端主动发送 DISCONNECT 断开)
  2. 如何用遗嘱实现上线通知?(上线时主动向自己的遗嘱主题发布一条"在线"消息)

Q5 RTMP 视频监控方案由哪几部分组成?延迟来源是什么?另一条 V4L2+JPEG 路线有何特点?

要点:推流端 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 广播,客户端解码显示;实现简单、适合同网段,但带宽大、本地显示与广播互相制约。

追问

  1. RTMP 与 RTSP 的主要区别?(RTSP 实时性最好但实现复杂;RTMP 浏览器支持好、低延迟、生态成熟)
  2. 为什么 Qt 方案选择 UDP 广播而不是 TCP?(一对多、无连接、实现简单;代价是可能丢包)

内容来源:《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)为扩展知识。