找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4738

积分

0

好友

616

主题
发表于 2 小时前 | 查看: 5| 回复: 0

在嵌入式视觉项目里,经常会遇到这样一个需求:

相机通过 RTSP 输出 H.265,RK3588 负责实时预览,同时还要把原始视频保存成 MP4,并且每 10 分钟自动切一个文件。

看起来并不复杂,但真正放到 RK3588 + Qt5 环境里,往往会碰到不少问题:

  • Qt 自带播放器播放不稳定;
  • H.265 硬解链路不好控制;
  • 预览一旦卡顿,录像也跟着受到影响;
  • MP4 分段后出现文件无法播放;
  • 程序退出时最后一个 MP4 损坏;
  • U 盘拔出后文件变成 0KB;
  • RTSP 网络抖动以后,整个 Pipeline 卡死。

这篇文章不打算把 GStreamer 每个插件都讲一遍,而是直接围绕一个实际工程问题来看:

RK3588 上,如何搭一套“低延迟预览 + 无二次编码 MP4 分段录像”的视频链路。


一、先明确我们的目标

假设现在有一台相机:

Camera
  │
  │ RTSP / H.265
  ▼
RK3588
  │
  ├── 实时预览 → Qt5
  │
  └── 视频录像 → MP4

我们希望最终做到:

  1. RTSP 视频低延迟显示;
  2. H.265 使用 RK3588 硬件解码;
  3. 预览和录像互不影响;
  4. 录像尽可能不重新编码;
  5. 每 10 分钟生成一个 MP4;
  6. 程序正常退出后,MP4 可以正常播放;
  7. 网络出现短暂抖动时能够恢复。

其中有一个非常关键的原则:

能不编码,就不要编码。

相机已经输出 H.265 了,如果 RK3588 再把 H.265 解码成 YUV,然后重新编码成 H.265 再保存,那么 CPU/GPU/NPU 之外还会增加编码器负担。

而我们真正需要的只是:

H.265
  ├──→ 解码 → 预览
  │
  └──→ 直接封装 → MP4

也就是说:

预览需要解码,录像不需要解码。


二、先看整个 Pipeline

一个比较典型的方案是:

                    ┌──→ MPP硬解 → videoconvert → appsink → Qt
                    │
RTSP → H265 Depay → Parse → Tee
                    │
                    └──→ Queue → H265 → MP4Mux → SplitMuxSink

对应 GStreamer Pipeline:

rtspsrc location=rtsp://... latency=100 protocols=tcp !
rtph265depay !
h265parse config-interval=1 !
tee name=t

t. ! queue !
mppvideodec !
videoconvert !
appsink name=video_sink
         emit-signals=true
         sync=false
         max-buffers=2
         drop=true

t. ! queue !
video/x-h265,stream-format=hvc1 !
splitmuxsink
    location=record/%05d.mp4
    max-size-time=600000000000
    muxer-factory=mp4mux

这里实际上有两条完全不同的链路。

预览链路

RTSP
  ↓
H265
  ↓
MPP硬解
  ↓
YUV
  ↓
appsink
  ↓
Qt

录像链路

RTSP
  ↓
H265
  ↓
h265parse
  ↓
splitmuxsink
  ↓
mp4mux
  ↓
MP4

这套设计最大的好处,就是:

录像完全绕过解码器。


三、为什么不用 Qt5 自己播放?

最开始很多项目都会想到:

QMediaPlayer

毕竟 Qt 自己已经提供了播放器。

但在嵌入式 Linux 上,这条路经常没有 PC 上那么顺利。

尤其是 RK3588 这种平台,真正的视频解码能力往往依赖:

Rockchip MPP

而 Qt Multimedia 是否能够正确使用对应的硬解链路,还取决于具体 Qt 版本、GStreamer 后端、系统镜像以及插件配置。

最终很容易出现:

Qt
  ↓
QMediaPlayer
  ↓
GStreamer
  ↓
不知道走了什么解码器
  ↓
CPU占用升高 / 延迟增加 / 播放异常

所以在工程项目里,与其把所有控制权交给 QMediaPlayer,不如直接控制 GStreamer Pipeline。

Qt 负责:

UI
窗口
用户操作
状态显示

GStreamer 负责:

RTSP
解包
解析
解码
缓存
录像

两者通过:

appsink

连接起来。

这样整个架构会清晰很多。


四、预览:为什么使用 appsink?

预览 Pipeline 最核心的一段:

mppvideodec !
videoconvert !
appsink

这里的 appsink 可以理解成:

把 GStreamer 里的视频帧交给 Qt/C++ 程序自己处理。

例如:

GStreamer
    │
    │ GstSample
    ▼
appsink
    │
    ▼
C++ / Qt
    │
    ▼
QImage / QVideoFrame
    │
    ▼
QWidget / QOpenGLWidget

这样我们就不会被某个特定的 GStreamer Video Sink 绑死。


五、低延迟的关键:不要让帧排队

实时预览最怕的事情其实不是丢帧。

而是:

延迟越来越大。

比如摄像头现在拍到:

14:00:00.000

结果 UI 显示的是:

13:59:55.000

虽然画面还在动,但已经落后 5 秒。

这对于监控、吊舱、机器人来说基本不可接受。

因此 appsink 可以设置:

max-buffers=2
drop=true

意思大致就是:

我只允许你这里积压很少的帧,如果消费者处理不过来,就丢掉旧帧。

例如:

Camera
  ↓
Frame 100
Frame 101
Frame 102
Frame 103
Frame 104
  ↓
Qt处理比较慢

如果没有限制:

100 → 101 → 102 → 103 → 104 → ...

队列不断积累,延迟越来越高。

而设置:

max-buffers=2
drop=true

之后更接近:

100
101
  ↓
丢弃旧帧
  ↓
104

我们真正关心的是:

现在这一帧。

而不是:

几秒钟以前那一帧。


六、Qt 侧最好采用“最新帧”思路

如果 UI 线程处理能力有限,也不要设计成:

每来一帧
  ↓
必须处理
  ↓
处理完再处理下一帧

更适合实时视频的方式是:

GStreamer线程
     │
     ▼
最新帧
     │
     │ 覆盖
     ▼
最新帧
     │
     ▼
Qt定时器
     │
     ▼
显示

这其实就是一种“黑板模式”。

例如:

GStreamer:

frame 100
frame 101
frame 102
frame 103
        ↓
   latestFrame

Qt:

每 30ms
  ↓
取 latestFrame
  ↓
显示

如果 Qt 某一次处理慢了:

frame 100
frame 101
frame 102
frame 103
frame 104

不需要把 100、101、102、103 全部显示一遍。

直接显示:

frame 104

这才是真正意义上的低延迟。


七、录像为什么可以不解码?

这是整个方案里面最值得注意的地方。

预览需要:

H.265 → YUV

但 MP4 本身可以存储 H.265。

所以录像完全可以:

H.265
  ↓
h265parse
  ↓
mp4mux
  ↓
MP4

也就是说:

                ┌──→ 解码 → Qt预览
                │
H.265 → Parse → Tee
                │
                └──→ MP4封装 → 文件

没有:

H265 → Decode → Encode → H265

自然就省掉了一次编码。

对于 RK3588 这种嵌入式设备来说,这一点非常重要。


八、为什么使用 tee?

因为我们需要“一份数据,两条路”。

GStreamer 的:

tee

就是干这个事情的。

例如:

tee name=t

之后:

t. ! queue ! preview
t. ! queue ! record

于是:

                    ┌── preview
                    │
H265 → Parse → Tee ──┤
                    │
                    └── record

这里有一个非常重要的细节:

tee 后面一定要考虑 queue。

不要简单写成:

tee
  ├── preview
  └── record

更推荐:

tee
  ├── queue → preview
  └── queue → record

因为两个分支的处理速度完全可能不同。

例如:

预览:正常
录像:磁盘突然变慢

如果没有 queue,录像分支的阻塞可能影响整个 Pipeline。

加上 queue 后,可以把两个分支进行一定程度的解耦。


九、splitmuxsink 是干什么的?

录像部分:

splitmuxsink

是这套方案里非常关键的插件。

它解决的问题就是:

如何持续写 MP4,同时按照时间自动切成多个文件。

例如:

record/
├── 00000.mp4
├── 00001.mp4
├── 00002.mp4
├── 00003.mp4
└── ...

每个文件 10 分钟。

对应:

max-size-time=600000000000

注意:

GStreamer 的时间单位是纳秒。

10 分钟:

10 × 60 × 1,000,000,000
=
600,000,000,000 ns

所以:

max-size-time=600000000000

就是:

10分钟切一个文件

十、为什么不用 multifilesink?

这是实际项目中非常容易踩的坑。

很多人第一反应是:

H265
  ↓
multifilesink
  ↓
1.h265
2.h265
3.h265

这个确实可以保存数据。

但是我们最终需要的是:

MP4

而 MP4 并不是简单地把 H.265 数据写进去就结束了。

MP4 是一个完整的容器格式,需要维护:

metadata
sample table
duration
offset
moov
mdat

等信息。

所以:

multifilesink

更适合做:

裸流分片
图片序列
简单buffer输出

而对于:

MP4分段录像

更合适的是:

splitmuxsink + mp4mux

十一、一个比较完整的录像 Pipeline

可以写成:

rtspsrc location=rtsp://admin:password@192.168.1.100:554/stream \
    latency=100 \
    protocols=tcp !

rtph265depay !

h265parse config-interval=1 !

tee name=t

t. ! queue !
video/x-h265,stream-format=hvc1 !
splitmuxsink
    location=record/%05d.mp4
    max-size-time=600000000000
    muxer-factory=mp4mux

这里:

rtspsrc

负责 RTSP。

rtph265depay

负责 RTP H.265 解包。

h265parse

负责 H.265 码流解析。

tee

负责分流。

queue

负责两个分支解耦。

mp4mux

负责 MP4 封装。

splitmuxsink

负责自动分段和文件管理。


十二、为什么 h265parse 很重要?

有些人会觉得:

rtph265depay

出来已经是 H.265 了,为什么还需要:

h265parse

因为后面的 MP4 封装以及解码器通常需要更明确、规范的码流信息。

因此更推荐:

rtph265depay
  ↓
h265parse
  ↓
后续处理

而不是:

rtph265depay
  ↓
直接写文件

另外:

config-interval=1

对于网络视频流也比较实用,可以周期性地提供编码配置参数,使得后续处理在断流、重新同步等场景下更加容易恢复。


十三、MP4 最后一个文件为什么容易出问题?

这是工程上非常常见的问题。

例如程序运行:

10:00
  ↓
10:10
  ↓
10:20
  ↓
10:27

于是:

00000.mp4  10分钟
00001.mp4  10分钟
00002.mp4  7分钟

前两个文件通常没问题。

但是如果程序在:

10:27

直接:

kill -9

或者设备突然断电:

掉电

最后一个 MP4 就可能出现:

文件存在
文件大小也不小
但是播放器打不开

原因就在于:

容器没有得到完整结束。

所以正常退出时,不应该简单地:

gst_element_set_state(pipeline, GST_STATE_NULL);

然后马上结束进程。

更合理的是让 Pipeline 正常处理:

EOS

让 muxer 有机会完成最后的容器信息写入。

当然,EOS 并不能解决突然断电问题

如果项目对录像完整性要求非常高,还需要考虑:

  • 文件系统;
  • fsync;
  • 电源异常;
  • 存储介质;
  • 分段策略;
  • 恢复机制。

十四、U 盘录像还有一个坑

实际项目中经常会出现这样的情况:

录像结束
  ↓
文件生成
  ↓
用户拔U盘
  ↓
电脑打开
  ↓
文件0KB / 文件损坏

程序看到:

close()

并不意味着数据已经真正落到物理存储介质。

尤其是 Linux 的缓存机制,会让:

write()

和:

真正写入磁盘

之间存在时间差。

因此对于需要用户立即拔盘的场景,应该设计:

停止录像
  ↓
EOS
  ↓
等待文件关闭
  ↓
sync / fsync
  ↓
提示“可以拔出设备”

而不是:

停止录像
  ↓
立刻提示用户拔U盘

这在无人机、吊舱、机器人等设备上尤其重要。


十五、低延迟和录像稳定性其实是两个目标

这里有一个很容易产生误区的地方。

很多人会认为:

我要低延迟,所以整个 Pipeline 都应该把缓存降到最低。

实际上并不是。

预览和录像的需求不同。

预览

目标:

越低延迟越好

所以可以:

max-buffers=2
drop=true
sync=false

录像

目标:

不能丢数据

所以录像分支反而不能简单粗暴地:

drop=true

否则可能出现:

预览正常
但是录像丢帧

因此一个比较合理的思路就是:

            ┌──→ 低延迟预览
            │
RTSP → Tee ──┤
            │
            └──→ 稳定录像

两条链路的缓存策略应该分别设计。


十六、RTSP 的 latency 也不能乱设

例如:

latency=100

表示 GStreamer 的 RTP 接收端允许一定的缓冲时间。

一般来说:

latency 越小
→ 延迟越低
→ 抗网络抖动能力越弱

反过来:

latency 越大
→ 延迟越高
→ 抗网络抖动能力更强

所以:

100ms

并不是一个绝对正确的数字。

实际项目需要根据:

网络质量
RTSP服务器
相机编码延迟
码率
帧率
无线/有线网络

进行调整。

如果是有线局域网,可以尝试比较低的值。

如果是无线链路或者网络质量不稳定,就需要适当增加。


十七、TCP 还是 UDP?

例如:

protocols=tcp

它的优点是稳定。

尤其是:

RTSP
+
H.265
+
无人机/吊舱
+
复杂网络环境

UDP 丢包以后,H.265 码流可能出现明显的花屏或者等待恢复。

TCP 虽然可能带来一定的延迟,但通常更容易获得稳定的视频流。

所以工程上经常会看到:

rtspsrc
    protocols=tcp
    latency=100

当然,如果你的网络环境非常稳定,并且对延迟极度敏感,也可以根据实际情况评估 UDP。


十八、RK3588 上为什么优先考虑 MPP 硬解?

RK3588 最大的优势之一,就是平台本身提供了专用的视频编解码硬件。

所以我们不希望变成:

RTSP
  ↓
H265
  ↓
CPU软件解码
  ↓
CPU占用很高
  ↓
Qt显示

而是:

RTSP
  ↓
H265
  ↓
Rockchip MPP
  ↓
硬件解码
  ↓
YUV

GStreamer 侧具体插件名称和可用属性,需要根据你的 Rockchip BSP、GStreamer 版本以及插件实现确认。

例如环境中可能看到:

mppvideodec

可以先检查:

gst-inspect-1.0 mppvideodec

不要直接认为网上某个 Pipeline 在你的系统上一定能跑。

嵌入式 Linux 最大的特点就是:

同样叫 RK3588,不同 BSP 上的软件环境可能完全不一样。


十九、真正工程化以后,还要解决断流重连

Demo 最简单的逻辑是:

启动Pipeline
  ↓
一直播放

但产品不能这么设计。

真实环境可能出现:

Camera重启
网络断开
交换机重启
RTSP服务异常
网线拔出
无线网络抖动

所以应该考虑:

            ┌──────────────┐
            │ RTSP Pipeline│
            └──────┬───────┘
                   │
                正常?
                /     \
              YES      NO
               │        │
               │      重建
               │        │
               └────────┘

不要指望一个 GStreamer Pipeline 在所有异常情况下都能自动恢复。

工程上更稳妥的方案往往是:

监控 Pipeline 状态
        ↓
Bus 消息
        ↓
ERROR / EOS / STATE异常
        ↓
停止当前Pipeline
        ↓
释放资源
        ↓
重新创建
        ↓
重新连接RTSP

二十、最终推荐的架构

如果把整个方案整理一下,我比较推荐:

                    RK3588
                      │
                RTSP H.265
                      │
                      ▼
                ┌───────────┐
                │ rtspsrc   │
                └─────┬─────┘
                      │
                rtph265depay
                      │
                 h265parse
                      │
                      ▼
                  ┌───────┐
                  │  tee  │
                  └───┬───┘
                      │
          ┌───────────┴───────────┐
          │                       │
        queue                    queue
          │                       │
          ▼                       ▼
   mppvideodec                H.265
          │                       │
   videoconvert               mp4mux
          │                       │
       appsink              splitmuxsink
          │                       │
          ▼                       ▼
         Qt                   00000.mp4
         UI                    00001.mp4
                               00002.mp4

这个架构最大的特点就是:

预览和录像从 Pipeline 层面解耦。

预览慢了:

丢预览帧

而不是:

录像停止

磁盘慢了:

录像分支自己处理

而不是:

Qt画面一起卡住

二十一、最终的几个核心结论

如果只记住这几个东西就够了。

1. RTSP/H.265 预览

rtspsrc
→ rtph265depay
→ h265parse
→ mppvideodec
→ videoconvert
→ appsink

RK3588 上优先使用硬件解码。

2. MP4 录像

H265
→ h265parse
→ mp4mux
→ splitmuxsink

尽量不要二次编码。

3. 预览和录像必须分支

tee
├── queue → preview
└── queue → record

不要让 UI 和磁盘互相拖累。

4. 低延迟预览

appsink
max-buffers=2
drop=true
sync=false

核心思想不是“每一帧都不能丢”,而是:

宁可丢旧帧,也不要让延迟越来越大。

5. MP4 分段

splitmuxsink
+
mp4mux

比简单使用 multifilesink 更适合 MP4 分段录像。

6. 产品级还要考虑

断流
重连
EOS
文件完整性
fsync
U盘拔出
磁盘空间
异常退出
突然断电

这些东西,往往比 Pipeline 本身更容易成为真正的坑。


写在最后

GStreamer 真正难的地方,其实不是记住:

rtspsrc
rtph265depay
h265parse
mppvideodec
appsink
splitmuxsink

这些插件名称。

真正难的是理解每个环节应该承担什么职责

对于 RK3588 上的相机应用,一个比较清晰的思路就是:

网络 → GStreamer
解码 → MPP
预览 → appsink + Qt
录像 → 原始H.265 + MP4封装
分段 → splitmuxsink
异常 → 状态监控 + 自动重连

这样设计之后,GStreamer 不再只是一个“能播放视频的工具”,而是整个视频数据链路的基础设施。

而 Qt,只需要专注于它最擅长的事情:

把最新的一帧稳定、低延迟地显示出来。




上一篇:Qwen3.8-Flash 上线模力方舟:6B 激活 MoE 为高频 Agent 循环而生
下一篇:偷学资深工程师的7个编码模式:写出更少意外的代码
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-28 04:45 , Processed in 0.801797 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表