【Linux】音视频处理(gstreamer和ffmpeg的实际应用)

本文将结合基于 Qt 框架开发的视频处理工程实例,浅析FFmpeg 音视频处理库的使用逻辑与工程化适配要点;同时,通过一个基于RK3588的视频处理工程,浅析 GStreamer 流媒体框架在嵌入式硬件加速场景下的使用方法与技术要点。

目录

1、音视频的基础知识

1.1 基础概念

1.2 音视频技术的完整流程

1.3 音视频同步技术

1.4 主流音视频标准与组织

1.5 嵌入式平台音视频技术特点(以 RK3588 为例)

2、FFmpeg实际应用

2.1 FFmpeg 基础知识

核心组件

2.2 工程实现

2.2.1 视频解码线程

2.2.2 视频保存线程

3、GStreamer实际应用

3.1 基础知识

3.1.1 核心概念

3.1.2 核心工作流程

3.2 工程实现

3.2.1 主线程核心代码

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 视频解码线程

解码流程:

  1. 初始化与打开输入流
  2. 查找视频流并初始化解码器
  3. 准备数据容器并开始循环解码
  4. 像素格式转换与图像缩放
  5. 图像处理与信号发射
  6. 清理与资源释放
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 视频保存线程

视频保存流程:

  1. 初始化与打开输入
  2. 查找流信息与创建输出上下文
  3. 配置输出视频流
  4. 打开输出文件并写入文件头
  5. 循环读取输入数据包并写入输出文件
  6. 写入文件尾并清理资源
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)调用插件。

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核心代码

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; 
                }
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值