本文将结合基于 Qt 框架开发的视频处理工程实例,浅析FFmpeg 音视频处理库的使用逻辑与工程化适配要点;同时,通过一个基于RK3588的视频处理工程,浅析 GStreamer 流媒体框架在嵌入式硬件加速场景下的使用方法与技术要点。
目录
[1.1 基础概念](#1.1 基础概念)
[1.2 音视频技术的完整流程](#1.2 音视频技术的完整流程)
[1.3 音视频同步技术](#1.3 音视频同步技术)
[1.4 主流音视频标准与组织](#1.4 主流音视频标准与组织)
[1.5 嵌入式平台音视频技术特点(以 RK3588 为例)](#1.5 嵌入式平台音视频技术特点(以 RK3588 为例))
[2.1 FFmpeg 基础知识](#2.1 FFmpeg 基础知识)
[2.2 工程实现](#2.2 工程实现)
[2.2.1 视频解码线程](#2.2.1 视频解码线程)
[2.2.2 视频保存线程](#2.2.2 视频保存线程)
[3.1 基础知识](#3.1 基础知识)
[3.1.1 核心概念](#3.1.1 核心概念)
[3.1.2 核心工作流程](#3.1.2 核心工作流程)
[3.2 工程实现](#3.2 工程实现)
[3.2.1 主线程核心代码](#3.2.1 主线程核心代码)
[3.2.2 线程A核心代码](#3.2.2 线程A核心代码)
1、音视频的基础知识
1.1 基础概念
1.1.1 音频基础参数
采样率:单位时间内对模拟音频信号的采样次数,单位为 Hz(赫兹)。根据奈奎斯特采样定理,采样率需大于信号最高频率的 2 倍才能无失真还原信号。常见规格有 8kHz(电话语音)、16kHz(语音通话)、44.1kHz(CD 音质)、48kHz(专业音频 / 视频配套音频)
采样位深:每个采样点用多少二进制位数来表示,决定了音频的动态范围(音量的变化范围)。常见规格有 8bit(民用低端设备)、16bit(CD 标准)、24bit(专业录音)、32bit(超高保真)。动态范围计算,动态范围≈6× 采样位深(dB),例如 16bit 音频动态范围约 96dB,能覆盖人耳可听的音量区间。
声道数:音频信号的独立发声通道数量,决定了声音的空间感。常见类型有 单声道(语音广播)、立体声(双声道,音乐播放)、5.1 声道(家庭影院,含左 / 右 / 中 / 左环绕 / 右环绕 / 低音炮)、7.1 声道(专业影院)。
码率:单位时间内传输或存储的音频数据量,单位为 bps(比特每秒),也常用 kbps、Mbps 表示。分为恒定码率和可变码率。
- 恒定码率(CBR)码率固定,适合实时传输(如直播),但对复杂音频场景可能压缩过度。
- 可变码率(VBR)根据音频复杂度动态调整码率,复杂片段用高码率,简单片段用低码率,兼顾音质和存储效率,适合音乐文件存储。
1.1.2 视频基础参数
分辨率 :视频画面的像素总数,通常以 "水平像素数 × 垂直像素数" 表示,决定了画面的清晰度。
帧率:单位时间内显示的视频帧数,单位为 fps(帧每秒),决定了视频的流畅度。原理是人眼存在 "视觉暂留" 效应(约 1/24 秒),当帧率≥24fps 时,会感知到连续的动态画面。
常见规格有 24fps/25fps/30fps(电影 / 普通视频)、60fps(高流畅度视频,如游戏 / 体育赛事)
色彩空间:描述颜色的数学模型,用于标准化颜色的表示和转换。主流类型:
- RGB:以红(R)、绿(G)、蓝(B)三原色为基础,适用于显示器、图像编辑等场景(如 RGB 24bit,每个通道 8bit)。
- YUV:分离亮度(Y)和色度(U/V),人眼对亮度更敏感,可通过色度亚采样节省带宽,是视频编码和传输的核心格式。
- HSV/HSB:以色相(H)、饱和度(S)、明度(V/B)描述颜色,更符合人眼对颜色的感知,常用于图像特效处理。
码率 :单位时间内的视频数据量,直接影响视频画质和存储 / 传输成本。如 1080p 30fps 的视频,若为未压缩的 RGB 格式,码率约 1920×1080×24bit×30fps≈1.49Gbps;经过 H.264 编码后,码率可降至 2-5Mbps,实现大幅压缩。
1.2 音视频技术的完整流程
可分为 采集→预处理→编码→封装→传输→解码→渲染
1.2.1 采集环节
嵌入式平台可通过 I2S/PCM 接口对接音频采集模块、用MIPI-CSI接口摄像头采集视频。
- 音频采集的原理是麦克风将声波转化为模拟电信号,经声卡完成采样和量化,生成原始数字音频数据(如 PCM 格式)
- 视频采集的原理是传感器将光信号转化为电信号,经 ISP(图像信号处理器)完成曝光、白平衡、降噪等初步处理,输出原始视频数据(如 YUV 4:2:0 格式)。
1.2.2 预处理环节
预处理的目的是优化原始音视频数据,提升后续编码效率和最终呈现效果。
音频预处理包括降噪回声消除、音频增益、静音检测。视频预处理包括去噪、图像增强、缩放 / 裁剪、帧率转换
1.2.3 编码环节
原始音视频数据量极大,无法直接存储和传输,编码的核心是通过压缩算法减少数据量,同时尽可能保证画质 / 音质。编码分为音频编码和视频编码,核心是去除数据中的冗余信息。
冗余信息类型
空间冗余:图像中相邻像素的颜色 / 亮度相似(如纯色背景),音频中相邻采样点的幅值相近。
时间冗余:视频中相邻帧的画面内容高度重叠(如静态场景的连续帧),音频中连续时间段的信号特征相似。
编码冗余:原始数据的表示方式存在冗余,可通过更高效的编码方式压缩。
音频编码标准
无损编码:压缩后可完全还原原始数据,无音质损失,适合音乐归档。(FLAC(免费开源)、APE、ALAC)
有损编码:通过舍弃人耳不敏感的音频信息实现压缩,平衡音质和体积,是主流应用格式。
有损编码分为基础编码(MP3、AAC)、低延迟编码(Opus、G.711、G.729)
视频编码标准
视频编码是音视频技术的核心,主流标准均为国际标准化组织制定,核心是通过帧内预测、帧间预测、变换编码、熵编码实现压缩。
- 第一代标准:MPEG-1(VCD 格式)、MPEG-2(DVD 格式)。
- 第二代标准:H.264/AVC(MPEG-4 AVC,目前应用最广泛的标准,压缩效率高,支持各种分辨率,兼顾实时性和画质,广泛用于直播、点播、安防)。
- 第三代标准:H.265/HEVC(压缩效率比 H.264 提升 50%,支持 4K/8K,用于超高清视频)、VP9(谷歌开源,YouTube 超高清视频标准)、AV1(AOM 联盟推出,开源免费,压缩效率优于 H.265,面向下一代音视频)。
- 专用标准:MJPEG(逐帧编码,无帧间压缩,适合监控摄像头实时预览)、M-JPEG( Motion-JPEG,简单帧间优化,用于早期视频会议)。
1.2.4 封装环节
编码后的音频流和视频流是分离的,封装的目的是将音视频流、字幕流、元数据(如分辨率、帧率、时长)等打包为一个完整的容器文件,方便存储和传输。核心作用:
- 同步音视频:通过时间戳(PTS/DTS)保证音频和视频的播放节奏一致,避免音画不同步。
- 管理多路流:支持同时封装多路音频(如多语言配音)、多路字幕。
常见封装格式:
| 格式 | 特点 | 典型应用场景 |
|---|---|---|
| MP4 | 兼容性强,支持 H.264/AAC,体积小,适合点播 / 移动端 | 短视频平台、手机本地视频 |
| FLV | 适合实时流传输,延迟低,支持 H.264/AAC | 直播平台(早期抖音 / 快手) |
| MKV | 开源免费,支持多路音视频 / 字幕,兼容几乎所有编码格式 | 高清影视归档、本地收藏 |
| TS | 传输容错性强,支持断网重连,适合广播 / 实时传输 | IPTV、数字电视、直播流 |
| AVI | 早期主流格式,兼容性差,体积大 | 老式视频文件、本地存储 |
| WebM | 谷歌开源,支持 VP9/Opus,适合网页播放 | 浏览器端视频、YouTube |
(视频文件本身是一个容器,其中包含了视频、音频、字幕等数据流)
1.2.5 传输环节
音视频传输分为实时传输 (如直播、视频会议)和非实时传输(如点播、文件下载),核心是保障传输的稳定性、低延迟和完整性。
传输协议分类
- 基于 TCP 的协议:TCP 是面向连接的可靠协议,可保证数据无丢失,但重传机制会带来延迟,适合非实时场景。代表:HTTP(点播文件下载)、HLS(HTTP Live Streaming,苹果推出的直播协议,基于切片传输,延迟约 10-30 秒)、DASH(动态自适应流,国际标准,支持多码率自适应)。
- 基于 UDP 的协议:UDP 是无连接的不可靠协议,传输速度快、延迟低,适合实时场景,需通过上层协议保障可靠性。代表:RTP(实时传输协议,负责音视频数据传输)、RTCP(实时传输控制协议,负责传输质量监控)、RTSP(实时流协议,用于摄像头实时预览)、WebRTC(网页实时通信,支持浏览器端实时音视频,延迟可低至 100ms 以内)。
关键技术
- 码率自适应:根据网络带宽动态调整音视频码率,避免卡顿(如 HLS/DASH 的多码率切片切换)。
- 丢包重传 / 纠错:实时场景中,对关键数据进行重传,或通过前向纠错(FEC)提前发送冗余数据,减少丢包对画质的影响。
- 网络拥塞控制:通过算法感知网络状态,调整发送速率,避免网络拥塞导致的延迟飙升。
1.2.6 解码环节
解码是编码的逆过程,接收端将接收到的封装文件 / 流数据,先解封装分离出音视频码流,再通过解码器还原为原始音视频数据(音频 PCM、视频 YUV/RGB)。解码方式:
- 软解码:通过 CPU 运行解码算法,兼容性强,无需专用硬件,但会占用 CPU 资源,适合低码率、低分辨率场景。
- 硬解码:通过专用硬件模块(如 GPU、DSP、专用解码芯片,RK3588 内置 NPU / 解码单元)完成解码,效率高、功耗低,是嵌入式和高性能场景的首选,例如 RK3588 可硬解 4K 10bit H.265 视频。
1.2.7 渲染环节
渲染是将解码后的音视频数据转化为可感知的声音和图像的过程。
- 音频渲染:将 PCM 数据通过声卡转化为模拟电信号,驱动扬声器 / 耳机发声,嵌入式平台可通过 I2S 接口对接功放模块。
- 视频渲染:将 YUV/RGB 数据通过显示驱动(如 DRM/KMS)输出到屏幕(LCD/HDMI 显示器),嵌入式场景需结合平台的显示框架(如 RK3588 的 Rockchip 显示驱动)实现画面叠加、多窗口显示等功能。
1.3 音视频同步技术
音视频同步是音视频播放的核心难题,若同步失败会出现音画不同步 (声音超前或滞后画面)的问题,其核心是基于时间戳实现对齐。
时间戳类型
- DTS(解码时间戳):指示解码器何时开始解码该帧数据。
- PTS(显示时间戳):指示渲染器何时开始显示 / 播放该帧数据。
- 对于 I 帧(帧内编码帧),DTS=PTS;对于 B 帧(双向预测帧),需先解码前后参考帧,因此 DTS 和 PTS 存在差值。
同步策略
- 以音频为基准:音频的时间感知更敏感,通常将视频的 PTS 与音频的 PTS 对齐,若视频超前则等待,若滞后则丢弃部分帧或插帧补偿。
- 以系统时钟为基准:通过 NTP 等协议同步设备时钟,将音视频的 PTS 与系统时钟对比,调整播放节奏。
1.4 主流音视频标准与组织
MPEG(Moving Picture Experts Group):国际标准化组织,制定了 MPEG-1、MPEG-2、MPEG-4(含 H.264)等核心标准。
ITU-T(国际电信联盟电信标准化部门):与 MPEG 联合制定 H.264、H.265 标准,同时制定了 G 系列音频编码标准(如 G.711、G.729)。
AOM(Alliance for Open Media):由谷歌、亚马逊、微软等企业组成,推出开源免费的 AV1 视频编码标准。
3GPP:制定移动领域音视频标准,推动 AAC、H.264 在手机端的应用,以及 5G 时代的低延迟音视频传输协议。
1.5 嵌入式平台音视频技术特点(以 RK3588 为例)
硬件加速 :RK3588 内置专用的音视频编解码单元(VPU),支持 H.264/H.265/AV1 的硬编硬解,同时 NPU 可辅助 AI 相关的音视频预处理(如智能降噪、人脸识别)。
低功耗:硬编硬解相比软解码可大幅降低功耗,适合嵌入式设备的续航需求。
接口适配:支持 MIPI-CSI(摄像头)、MIPI-DSI/HDMI(显示)、I2S/PCM(音频)等嵌入式专用接口,可直接对接外设模组。
实时性:Linux 内核层的音视频驱动(如 V4L2、ALSA)可保障采集和渲染的低延迟,满足机器人视觉、安防监控等实时场景需求。
2、FFmpeg实际应用
2.1 FFmpeg 基础知识
核心组件
FFmpeg 包含 4 个核心可执行程序(命令行使用)和对应的开发库(编程调用):
| 组件 | 功能说明 |
|---|---|
ffmpeg |
核心工具:音视频转码、格式转换、编解码、流处理、滤镜添加、截图等(最常用) |
ffplay |
轻量级播放器:用于快速播放音视频文件 / 网络流(如测试 MJPEG 流) |
ffprobe |
分析工具:查看音视频文件 / 流的详细信息(编码、分辨率、帧率、码率等) |
ffserver |
流媒体服务器:推送音视频流(如将 MJPEG 流推送到 HTTP/RTSP 服务) |
| 开发库 | libavcodec(编解码)、libavformat(格式 / 协议)、libavfilter(滤镜)等 |
封装格式
又称 "容器格式",是音视频流、字幕等数据的 "打包格式",不负责编码,仅负责组织数据结构。
注意,MJPEG(Motion JPEG)是基于 JPEG 图像压缩的视频编码流,本质是连续的独立 JPEG 图像序列。它不对帧间数据做关联处理,每一帧画面都是单独经过 JPEG 压缩的完整图像,数据流本身就承载着视频的视觉信息,不存在 "封装"(mp4、avi) 的概念。
协议(Protocol)
FFmpeg 支持的音视频传输协议,核心包括:
文件协议:file://(本地文件);
网络协议:http://(如 MJPEG 流)、rtsp://、rtmp://、udp://。
2.2 工程实现
本工程采用 Robot-Link模块,代码上通过 HTTP 获取 MJPEG 格式的视频流,通过ffmpeg进行解码,将处理好的图像(QPixmap)发送给UI线程进行显示。
2.2.1 视频解码线程
解码流程:
- 初始化与打开输入流
- 查找视频流并初始化解码器
- 准备数据容器并开始循环解码
- 像素格式转换与图像缩放
- 图像处理与信号发射
- 清理与资源释放
cpp
void VideoDecoder::videoDecode(){
// 步骤 1: 初始化与打开输入流
// 分配一个 AVFormatContext 结构体m_formatCtx。它包含了媒体文件或流的全部格式信息,比如容器格式、流的数量、时长等。
m_formatCtx = avformat_alloc_context();
// 打开一个媒体文件或网络流。m_formatCtx并将信息填充到m_formatCtx结构体中
if(avformat_open_input(&m_formatCtx, "http://192.xxx.x.x:xxxx/?action=stream", nullptr, nullptr) != 0) {
qDebug() << "Couldn't open input stream."; // 如果打开失败,通常是因为网络问题、URL无效或不支持的协议。
return;
}
// 读取媒体文件的一部分数据来尝试获取流的详细信息。
if(avformat_find_stream_info(m_formatCtx, nullptr) < 0) {
qDebug() << "Couldn't find stream information.";
return;
}
// 步骤 2: 查找视频流并初始化解码器
m_videoStreamIndex = -1; // 初始化视频流索引为-1,表示未找到。
// 遍历所有流 (m_formatCtx->nb_streams 是流的总数)
for(unsigned int i = 0; i < m_formatCtx->nb_streams; i++) {
// 每个流的编解码器参数都存储在 codecpar 结构体中。codec_type 字段标识了流的类型(视频、音频、字幕等)。
if(m_formatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
m_videoStreamIndex = i; // 找到视频流,记录其索引。
break; // 假设我们只需要第一个视频流,找到后即可退出循环。
}
}
if(m_videoStreamIndex == -1) {
qDebug() << "Didn't find a video stream.";
return; // 没有找到视频流,无法继续。
}
// 分配一个AVCodecContext结构体。其包含了编解码所需的所有上下文信息(编解码器类型、像素格式、宽高、比特率等)
m_codecCtx = avcodec_alloc_context3(nullptr); //传入nullptr表示让函数自动选择合适的编解码器。
// 函数将从流中获取的编解码器参数 (codecpar) 复制到 (m_codecCtx) 中。
avcodec_parameters_to_context(m_codecCtx, m_formatCtx->streams[m_videoStreamIndex]->codecpar);
// avcodec_find_decoder() 根据编解码器ID (m_codecCtx->codec_id) 查找对应的解码器。
const AVCodec* codec = avcodec_find_decoder(m_codecCtx->codec_id);
if (!codec) {
qDebug() << "Unsupported codec!";
return;
}
// 使用找到的解码器codec来初始化解码器上下文。
avcodec_open2(m_codecCtx, codec, nullptr);
// 步骤 3: 准备数据容器并开始循环解码
// AVPacket 用于存储从流中读取的压缩数据。它只是数据的引用,并不包含实际的数据拷贝。
// 在送入解码器之前,原始数据被打包成一个个的AVPacket。
AVPacket packet;
av_init_packet(&packet); // 初始化packet的内部字段为默认值。
// AVFrame 用于存储解码后的原始(未压缩)视频数据
AVFrame* frame = av_frame_alloc();
// av_read_frame() 是解码循环的核心。它从输入流(m_formatCtx)中读取一个数据包(packet)。
while(av_read_frame(m_formatCtx, &packet) == 0) { // 成功时返回0,读到文件末尾或出错时返回负值。
// 检查这个数据包是否属于我们关心的视频流。
if(packet.stream_index == m_videoStreamIndex) {
// avcodec_send_packet() 将一个压缩的数据包发送给解码器。
avcodec_send_packet(m_codecCtx, &packet);
// 从解码器接收一个已解码的原始帧。
if (avcodec_receive_frame(m_codecCtx, frame) == 0) {
// 步骤 4: 像素格式转换与图像缩放
int width = frame->width;
int height = frame->height;
// 创建一个QImage来存放转换后的RGB数据。Format_RGB32是32位RGB格式,
// 对应FFmpeg中的 AV_PIX_FMT_RGB32,这对于Qt显示非常方便。
QImage img(width, height, QImage::Format_RGB32);
// sws_getContext() 初始化一个SwsContext,用于图像的缩放和像素格式转换。
// - frame->width, frame->height: 输入图像的宽高。
// - (AVPixelFormat)frame->format: 输入图像的像素格式(如 AV_PIX_FMT_YUV420P)。
// - width, height: 输出图像的宽高(这里不缩放)。
// - AV_PIX_FMT_RGB32: 输出图像的像素格式。
// - SWS_BILINEAR: 缩放算法(双线性插值),即使不缩放也要指定一个。
// - ... (nullptr): 高级选项,一般设为nullptr。
SwsContext* sws_ctx = sws_getContext(
width, height, static_cast<AVPixelFormat>(frame->format),
width, height, AV_PIX_FMT_RGB32,
SWS_BILINEAR, nullptr, nullptr, nullptr
);
// sws_scale() 执行实际的转换。
// - sws_ctx: 上一步创建的转换上下文。
// - frame->data: 输入的YUV数据,它是一个指针数组,对应Y, U, V等平面。
// - frame->linesize: 输入数据每个平面的行大小(stride)。
// - 0, height: 从第0行开始,转换整个图像高度。
// - &img.bits(): 输出RGB数据的缓冲区地址。
// - &img.bytesPerLine(): 输出RGB数据的行大小(stride)。
sws_scale(sws_ctx,
frame->data,
frame->linesize,
0,
height,
&img.bits(),
&img.bytesPerLine()
);
// 转换完成后,立即释放SwsContext,防止内存泄漏。
sws_freeContext(sws_ctx);
// 步骤 5: 图像处理与信号发射
// 将转换后的图像缩放到640x480,以适应UI界面的显示区域。
QImage show_frame = img.scaled(640, 480);
// 将QImage转换为QPixmap,QPixmap是专门为在屏幕上显示图像而优化的。
QPixmap pix = QPixmap::fromImage(show_frame, Qt::AutoColor);
// 发射信号,将处理好的QPixmap传递给UI线程。
// UI线程的槽函数接收到这个信号后,就可以更新界面上的QLabel了
emit sendImage(pix);
}
}
// av_packet_unref() 释放AVPacket引用的压缩数据缓冲区。
// av_read_frame会为packet分配内部缓冲区,每次循环结束后必须释放,否则会内存泄漏。
av_packet_unref(&packet);
}
// 步骤 6: 清理与资源释放
// 循环结束后(例如流结束或出错),必须按分配的逆序释放所有资源。
av_frame_free(&frame); // 释放AVFrame
avcodec_close(m_codecCtx); // 关闭解码器
avformat_close_input(&m_formatCtx); // 关闭输入流并释放相关资源
avcodec_free_context(&m_codecCtx); // 释放解码器上下文
avformat_free_context(m_formatCtx); // 释放格式上下文
// 所有指针都应设为nullptr,防止悬挂指针。
m_formatCtx = nullptr;
m_codecCtx = nullptr;
}
2.2.2 视频保存线程
视频保存流程:
- 初始化与打开输入
- 查找流信息与创建输出上下文
- 配置输出视频流
- 打开输出文件并写入文件头
- 循环读取输入数据包并写入输出文件
- 写入文件尾并清理资源
cpp
void SaveStreamToMP4::saveStreamTomp4(){
// 1. 初始化与打开输入
// 定义输入网络流地址
const char* inputUrl = "http://192.168.1.1:8080/?action=stream";
// 定义输出文件路径
const char* outputFile = "output.mp4";
// 创建输入格式上下文,用于管理输入流
AVFormatContext* inputFormatContext = nullptr;
// 打开输入流并读取头部信息,自动检测格式
if (avformat_open_input(&inputFormatContext, inputUrl, nullptr, nullptr) < 0) {
qDebug() << "无法打开输入流。";
return;
}
// 2. 查找流信息与创建输出上下文
// 读取数据包以获取流的详细信息(如编码、分辨率等)
if (avformat_find_stream_info(inputFormatContext, nullptr) < 0) {
qDebug() << "无法找到流信息。";
avformat_close_input(&inputFormatContext);
return;
}
// 创建输出格式上下文,用于管理输出文件
AVFormatContext* outputFormatContext = nullptr;
// 根据文件名 "output.mp4" 自动推断并分配MP4格式的上下文
avformat_alloc_output_context2(&outputFormatContext, nullptr, "mp4", outputFile);
if (!outputFormatContext) {
qDebug() << "无法创建输出上下文。";
avformat_close_input(&inputFormatContext);
return;
}
// 3. 配置输出视频流
// 声明一个指向输出视频流的指针
AVStream* videoStream = nullptr;
// 遍历输入上下文中的所有流
for (unsigned int i = 0; i < inputFormatContext->nb_streams; i++) {
// 判断当前流是否为视频流
if (inputFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
// 在输出上下文中创建一个新的流
videoStream = avformat_new_stream(outputFormatContext, nullptr);
if (!videoStream) {
qDebug() << "创建输出流失败。";
avformat_close_input(&inputFormatContext);
avformat_free_context(outputFormatContext);
return;
}
// 将输入视频流的编解码器参数 完整复制到输出流。这是实现流复制(remuxing)的关键,避免了重新编码
avcodec_parameters_copy(videoStream->codecpar, inputFormatContext->streams[i]->codecpar);
// 复制时间基,确保后续时间戳转换的正确性
videoStream->time_base = inputFormatContext->streams[i]->time_base;
// 找到并配置好视频流后,即可退出循环
break;
}
}
// 4. 打开输出文件并写入文件头
// 以写入模式打开指定的输出文件
if (avio_open(&outputFormatContext->pb, outputFile, AVIO_FLAG_WRITE) < 0) {
qDebug() << "无法打开输出文件。";
avformat_close_input(&inputFormatContext);
avformat_free_context(outputFormatContext);
return;
}
// 将流的元数据等信息写入输出文件头
if (avformat_write_header(outputFormatContext, nullptr) < 0) {
qDebug() << "无法写入文件头。";
avio_close(outputFormatContext->pb);
avformat_free_context(outputFormatContext);
avformat_close_input(&inputFormatContext);
return;
}
// 5. 循环读取输入数据包并写入输出文件
// AVPacket 用于存储单个编码后的数据帧
AVPacket packet;
while (runing) {
// 从输入流中读取一帧数据包
if (av_read_frame(inputFormatContext, &packet) < 0) {
// 如果读取失败(例如,流结束),则退出循环
break;
}
// 仅处理属于我们配置的视频流的数据包
if (packet.stream_index == videoStream->index) {
// 转换时间戳(PTS/DTS)
// 将数据包的时间戳从输入流的时间基 转换到输出流的时间基
av_packet_rescale_ts(&packet, inputFormatContext->streams[packet.stream_index]->time_base, videoStream->time_base);
// 将处理后的数据包写入输出文件。 av_interleaved_write_frame 能够处理音视频交错,确保播放顺序正确
if (av_interleaved_write_frame(outputFormatContext, &packet) < 0) {
qDebug() << "写入数据帧时发生错误。";
break;
}
}
// 释放数据包引用的内部缓冲区,防止内存泄漏
av_packet_unref(&packet);
}
// 6. 写入文件尾并清理资源
// 写入文件尾部信息(让文件"完整"),对于某些格式(如MP4)至关重要
av_write_trailer(outputFormatContext);
// 关闭输出文件的IO上下文
avio_close(outputFormatContext->pb);
// 释放输出格式上下文
avformat_free_context(outputFormatContext);
// 关闭输入流并释放输入格式上下文
avformat_close_input(&inputFormatContext);
}
3、GStreamer实际应用
3.1 基础知识
GStreamer ,一个开源多媒体框架,用于构建实时的音视频处理和流传输应用,支持跨平台(Linux、Windows、macOS、Android、iOS 等),核心特点是模块化 和可扩展,广泛应用于音视频播放、录制、转码、流媒体服务器等场景。
3.1.1 核心概念
1、元件(Element):GStreamer 的最小功能单元,可以通过创建一系列的元件,并把它们连接起来,从而让数据流在这个被连接的各个元件之间传输。每个元件都有一个特殊的函数接口。一个元件在被创建后,它不会执行任何操作。所以需要改变元件的状态,使得它能够做某些事。元件有四种状态,每种状态都有其特定的意义。
2、衬垫(Pad):元素之间的 "数据接口",用于连接不同元素。源衬垫:元素输出数据的端口。接收衬垫:元素输入数据的端口
3、管道(Pipeline):多个元素通过 Pad 连接形成的完整处理链路,是 GStreamer 应用的核心容器。管道会管理所有元素的生命周期(启动、暂停、停止),并处理数据流转。
4、总线(Bus) :管道的 "消息通道",用于将元素的状态变化、错误信息、EOS(流结束)等事件从后台线程传递到主线程,避免线程安全问题。
3.1.2 核心工作流程
1、初始化GStreamer框架
2、创建GStreamer元素(构建视频流处理链路)
3、配置元素参数
4、组装GStreamer管道并链接元素
5、动态链接RTSP源(source)和解封装器(depay)
6、启动视频流处理管道
7、 获取管道的消息总线,等待消息
8、停止管道并释放总线、管道等资源
3.2 工程实现
本工程在主线程中创建了从数据源(rtspsrc)到数据终端(multifilesink)的所有处理单元(GstElement),并将它们链接(gst_element_link 和 g_signal_connect)在了一起,形成了一个逻辑上完整的数据处理流水线。
工程的输入是一个来自网络摄像头的实时视频流,视频流是 H.265 (HEVC) 编码格式。输出是一系列连续的JPEG格式的图片文件。通过线程A循环地读取这些图片文件(图片帧),并显示,然后将帧放入一个共享队列、最后删除原始文件。再通过线程B处理共享队列中的图片文件,实现具体需求。
3.2.1 主线程核心代码
(RK3588硬件加速插件的使用)
代码中用到的mppvideodec(H.265 解码器)和mppjpegenc(JPEG 编码器),均基于瑞芯微 MPP(Media Process Platform) 框架,这个框架是RK3588专为音视频硬件加速设计的底层驱动。
MPP 框架为 GStreamer 提供了插件封装,因此可以直接通过GStreamer 的元素(element)调用插件。
cpp
int main(int argc, char *argv[]) {
// 1、初始化GStreamer框架(用于处理RTSP视频流的采集、解码、保存)
gst_init(&argc, &argv);
// 2、创建GStreamer管道元素(构建视频流处理链路)
// 管道容器:所有元素的载体,管理元素间的数据流向
GstElement *pipeline = gst_pipeline_new("pipeline");
// RTSP源:从网络摄像头获取RTSP视频流(H.265编码)
GstElement *source = gst_element_factory_make("rtspsrc", "source");
// H.265解封装器:将RTSP流中的H.265数据包解封装为原始H.265码流
GstElement *depay = gst_element_factory_make("rtph265depay", "depay");
// H.265解析器:解析H.265码流,提取编码信息(如帧率、分辨率)
GstElement *parser = gst_element_factory_make("h265parse", "parser");
// 硬件解码器:将H.265码流解码为原始图像(YUV/RGB格式)
GstElement *decoder = gst_element_factory_make("mppvideodec", "decoder");
// 视频格式转换器:将解码后的图像格式转换为JPEG编码器支持的格式
GstElement *converter = gst_element_factory_make("videoconvert", "converter");
// JPEG编码器:将原始图像编码为JPEG格式(便于保存和后续处理)
GstElement *jpegenc = gst_element_factory_make("mppjpegenc", "jpegenc");
// 多文件保存器:将编码后的JPEG图像按指定格式保存到本地文件夹
GstElement *multifilesink = gst_element_factory_make("multifilesink", "multifilesink");
// 3、检查所有GStreamer元素是否创建成功
if (!pipeline || !source || !depay || !parser || !decoder || !converter || !jpegenc || !multifilesink) {
g_printerr("Not all elements could be created.\n"); // 输出错误信息
return -1; // 元素创建失败,程序退出
}
// 4、配置元素参数
// 设置RTSP源的地址
// 地址格式:rtsp://[用户名]:[密码]@[设备IP]:[端口]/[流路径],对应网络摄像头的 RTSP 服务地址,用于身份验证和流定位。
// 网络摄像头的 RTSP 服务,简单说就是它内置的一种 "视频流接口功能",能让电脑、手机或监控平台通过RTSP 协议,远程控制和获取摄像头的实时视频流。
g_object_set(source, "location", "rtsp://xxx", NULL);
// 设置JPEG图像的保存路径和命名格式(f_0001.jpg、f_0002.jpg...)
g_object_set(multifilesink, "location", "../pic/f_%04d.jpg", NULL);
// 5、组装GStreamer管道并链接元素
// 将所有元素添加到管道容器中
gst_bin_add_many(GST_BIN(pipeline), source, depay, parser, decoder, converter, jpegenc, multifilesink, NULL);
// 静态链接后续元素(除source外,因为rtspsrc的输出端口是动态创建的)
gst_element_link(depay, parser); // 解封装器→解析器
gst_element_link(parser, decoder); // 解析器→解码器
gst_element_link(decoder, converter); // 解码器→格式转换器
gst_element_link(converter, jpegenc); // 格式转换器→JPEG编码器
gst_element_link(jpegenc, multifilesink);// JPEG编码器→文件保存器
// 6、动态链接RTSP源(source)和解封装器(depay)
// 由于rtspsrc的输出端口(pad)在收到流后才动态创建,需通过信号回调实现链接
g_signal_connect(source, "pad-added", G_CALLBACK(
// 回调函数:当source创建输出pad时,自动链接到depay的输入pad
+[](GstElement *src, GstPad *pad, gpointer data) {
GstElement *depay = (GstElement *)data; // 获取depay元素
GstPad *sinkpad = gst_element_get_static_pad(depay, "sink"); // 获取depay的输入pad
gst_pad_link(pad, sinkpad); // 链接source的输出pad和depay的输入pad
gst_object_unref(sinkpad); // 释放depay的输入pad引用
}), depay); // 传递depay作为回调函数的参数
// 7、启动视频流处理管道
gst_element_set_state(pipeline, GST_STATE_PLAYING); // 管道进入播放状态,开始采集→解码→保存流程
// 8、启动业务线程
std::thread display1(threadA);
// 9、主循环:维持程序运行,处理GStreamer管道消息
GstBus *bus = gst_element_get_bus(pipeline); // 获取管道的消息总线(用于接收错误、流结束等消息)
GstMessage *msg;
while (true)
{
// 阻塞等待消息(无超时,一直等待)
msg = gst_bus_timed_pop(bus, GST_CLOCK_TIME_NONE);
} while (msg != NULL); // 循环直到消息为空(实际因阻塞等待,通常需外部信号终止)
// 10、线程同步与资源清理
// 等待所有业务线程执行完毕
display1.join();
// 释放GStreamer资源
gst_object_unref(bus); // 释放消息总线
gst_element_set_state(pipeline, GST_STATE_NULL); // 停止管道,释放所有流资源
gst_object_unref(pipeline); // 释放管道
return 0; // 程序正常退出
}
3.2.2 线程A核心代码
cpp
void readdisplay()
{
std::string folderPath = "/xxx/pic/";
cv::namedWindow("Image",cv::WINDOW_AUTOSIZE);
while (true)
{
for (const auto& entry : fs::directory_iterator(folderPath))
{
if (entry.is_regular_file() && entry.path().extension() == ".jpg")
{
std::string imagePath = entry.path().string(); // 处理图像
cv::Mat frame = cv::imread(imagePath);
if (!frame.empty())
{
cv::imshow("Image", frame);
{
std::lock_guard<std::mutex> lock(frame_mutex);
if(frame_queue.size()>=5)
{
frame_queue.pop();
}
frame_queue.push(frame);
}
//frame_cv.notify_one();
cv::waitKey(1);
}
// 删除文件
if (fs::remove(imagePath))
{
// std::cout << "Deleted: " << imagePath << std::endl;
}
else
{
std::cerr << "Failed to delete: " << imagePath << std::endl;
}
}
}
}
}