嵌入式音视频开发——MPP编码

瑞芯微MPP硬件编解码从入门到实战


1. MPP基础概念:什么是MPP?为什么要用它?

1.1 MPP的定义

MPP = Media Process Platform(媒体处理平台),是瑞芯微、海思、全志等嵌入式芯片厂商提供的专用软件框架,核心作用是:屏蔽复杂的硬件细节,让你用统一的API调用芯片内置的硬件编解码单元(VPU)

一句话理解

  • 芯片里有个专门干视频编解码的"工人"(VPU硬件)
  • MPP就是这个工人的"调度员",你告诉调度员要做什么(编码/解码)、怎么做(分辨率、码率),调度员就会指挥工人高效完成任务
  • 你不用关心工人具体怎么干活,也不用管不同芯片的工人有什么不同

1.2 为什么嵌入式必须用MPP?(软件vs硬件编解码)

嵌入式设备(摄像头、NVR、广告机、智能座舱)的CPU性能有限,如果用纯软件编解码(比如FFmpeg默认的x264),会出现:

  • CPU占用率100%,系统卡顿
  • 功耗高,发热严重
  • 只能处理低分辨率、低帧率视频
  • 无法同时处理多路视频流

MPP硬件编解码的优势

对比项 纯软件编解码 MPP硬件编解码
CPU占用率 高(单路4K可能占满1个核心) 极低(<5%,仅负责控制)
功耗 低(专用电路效率是CPU的10-100倍)
性能 弱(RK3568软件解码4K@10fps都困难) 强(RK3568支持4K@60fps编解码)
多路能力 强(RK3568可同时解码8路1080P)
延迟 低(适合实时监控、直播)

1.3 MPP的核心架构(以瑞芯微为例)

瑞芯微MPP是目前最流行、生态最好的实现,架构分为4层:

应用层(你的程序)
    ↓
MPP用户态层(librockchip_mpp.so)
    ↓ 提供统一MPI接口
内核驱动层(/dev/mpp_service)
    ↓
硬件层(VPU视频处理单元 + RGA图形加速单元)

各层作用

  1. 硬件层:真正执行编解码计算的地方,VPU负责视频编解码,RGA负责色彩空间转换、缩放等
  2. 内核驱动层:管理硬件资源,提供用户态和内核态的通信通道
  3. MPP用户态层:核心层,屏蔽不同芯片差异,提供统一的MPI(MPP Program Interface)接口
  4. 应用层:你的程序通过MPI接口调用MPP功能,也可以通过FFmpeg、GStreamer等中间件间接调用

1.4 MPP支持的功能和格式

核心功能

  • 视频编码:H.264、H.265/HEVC、MJPEG、VP8
  • 视频解码:H.264、H.265/HEVC、VP9、VP8、MJPEG、MPEG-4、MPEG-2、VC-1
  • 视频处理:色彩空间转换、缩放、裁剪、旋转、图像拼接

常见芯片厂商的MPP实现

  • 瑞芯微(Rockchip):RKMPP,支持RK3568、RK3588、RV1126等,生态最好,资料最多
  • 海思(Hisilicon):HiMPP,功能最强大,广泛用于安防监控领域
  • 全志(Allwinner):AWMPP,性价比高,用于消费类电子
  • 芯驰(ArtInChip):AICMPP,新兴国产芯片

2. MPI接口基础:统一API的核心设计

2.1 MPI接口的固定性

瑞芯微MPP的 MPI(Media Process Interface)接口是统一且相对固定的。其核心设计目标是屏蔽不同瑞芯微芯片(如RK3568、RK3588、RV1126等)的硬件差异,让应用层代码无需修改或仅需少量修改,即可在不同芯片上调用硬件VPU进行编解码。

不同芯片的底层HAL(硬件抽象层)会适配各自的VPU,但向上暴露的MPI API保持一致。这意味着你在RK3568上写的解码代码,几乎可以直接移植到RK3588上运行。

2.2 常用MPI API分类

MPI API定义在rk_mpi.h等头文件中,核心API分为上下文管理、数据输入输出、参数配置三类:

2.2.1 上下文管理API
API函数 功能说明
mpp_create() 创建MPP编解码上下文(MppCtx)和API操作接口(MppApi),是使用MPP的第一步
mpp_init() 初始化MPP上下文,指定是编码还是解码,以及编解码的格式(如H.264、H.265)
mpp_destroy() 销毁MPP上下文,释放所有相关资源,是使用MPP的最后一步
2.2.2 数据输入输出API
API函数 功能说明
mpp_put_packet()/mpi->decode_put_packet() 将H.264/H.265等压缩码流(MppPacket)送入MPP解码器(解码用)
mpp_get_frame()/mpi->decode_get_frame() 从MPP解码器获取解码后的原始图像帧(MppFrame,通常是YUV格式,解码用)
mpp_put_frame()/mpi->encode_put_frame() 将YUV等原始图像帧(MppFrame)送入MPP编码器(编码用)
mpp_get_packet()/mpi->encode_get_packet() 从MPP编码器获取编码后的压缩码流(MppPacket,编码用)
2.2.3 参数配置API
API函数 功能说明
mpp_control()/mpi->control() 配置编解码的各种参数,如分辨率、码率、帧率等,是万能配置接口

3. 环境准备:MPP源码获取与编译

3.1 源码获取

MPP源码托管在GitHub,直接克隆即可:

git clone https://github.com/rockchip-linux/mpp.git
cd mpp

3.2 交叉编译(以RK3568为例)

3.2.1 配置交叉编译工具链

build/linux/arm/目录下修改arm.linux.cross.cmake,指定交叉编译器路径:

set(CMAKE_C_COMPILER "arm-linux-gnueabihf-gcc")
set(CMAKE_CXX_COMPILER "arm-linux-gnueabihf-g++")
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

注意:RK3588等64位芯片请使用aarch64-linux-gnu-gcc编译器。

3.2.2 编译安装
# 创建编译目录
mkdir build && cd build
# 配置(指定安装路径,便于后续链接)
cmake -DCMAKE_INSTALL_PREFIX=./install ..
# 编译(使用所有CPU核心加速)
make -j$(nproc)
# 安装(生成头文件+库文件)
make install

编译完成后,install目录下会生成:

  • 头文件:include/rk_mpi.h(核心MPI头文件)
  • 库文件:lib/librockchip_mpp.so(动态库,体积小,适合嵌入式)

4. 核心API与数据结构详解

4.1 核心数据结构

MPI的核心是上下文管理数据载体,以下是必须掌握的结构体:

结构体 作用
MppCtx MPP上下文,每个编解码实例对应一个,管理硬件资源、状态、任务队列
MppApi MPI接口指针,通过它调用编解码核心函数(put_packet/get_frame等)
MppPacket 编码/解码的输入/输出码流载体(H.264/H.265等压缩数据)
MppFrame 编码/解码的输入/输出帧载体(YUV420/NV12等原始图像数据)
MppDecCfg 解码器配置结构体,设置分辨率、码流格式、输出格式等
MppEncCfg 编码器配置结构体,设置码率、帧率、GOP、编码格式等

4.2 核心API详细说明

4.2.1 上下文管理API
// 创建MPP上下文与MPI接口(第一步)
// ctx: 输出参数,返回创建的MPP上下文
// mpi: 输出参数,返回MPI接口指针
MPP_RET mpp_create(MppCtx *ctx, MppApi **mpi);

// 初始化解码/编码器
// ctx: MPP上下文
// type: 上下文类型,MPP_CTX_DEC(解码)或MPP_CTX_ENC(编码)
// coding: 编解码格式,如MPP_VIDEO_CodingAVC(H.264)、MPP_VIDEO_CodingHEVC(H.265)
MPP_RET mpp_init(MppCtx ctx, MppCtxType type, MppCodingType coding);

// 销毁MPP上下文,释放所有资源(最后一步)
MPP_RET mpp_destroy(MppCtx ctx);
4.2.2 数据交互API
  • 解码流程:输入MppPacket → 输出MppFrame
// 向解码器送入压缩码流
// ctx: MPP上下文
// packet: 包含压缩码流的MppPacket
MPP_RET mpi->decode_put_packet(MppCtx ctx, MppPacket packet);

// 从解码器获取解码后的原始帧
// ctx: MPP上下文
// frame: 输出参数,返回解码后的MppFrame
MPP_RET mpi->decode_get_frame(MppCtx ctx, MppFrame *frame);
  • 编码流程:输入MppFrame → 输出MppPacket
// 向编码器送入原始帧
// ctx: MPP上下文
// frame: 包含原始图像数据的MppFrame
MPP_RET mpi->encode_put_frame(MppCtx ctx, MppFrame frame);

// 从编码器获取编码后的压缩码流
// ctx: MPP上下文
// packet: 输出参数,返回编码后的MppPacket
MPP_RET mpi->encode_get_packet(MppCtx ctx, MppPacket *packet);
4.2.3 参数配置API(mpp_control)

mpp_control是MPI的“万能配置接口”,通过命令字实现所有参数设置/获取:

// 配置或获取MPP参数
// ctx: MPP上下文
// cmd: 命令字,指定要执行的操作
// param: 输入/输出参数,根据cmd不同而不同
MPP_RET mpi->control(MppCtx ctx, MppCmd cmd, void *param);

常用命令字:

命令字 功能
MPP_DEC_GET_CFG/MPP_DEC_SET_CFG 获取/设置解码器配置(MppDecCfg)
MPP_ENC_GET_CFG/MPP_ENC_SET_CFG 获取/设置编码器配置(MppEncCfg)
MPP_DEC_SET_EXT_BUF_GROUP 为解码器设置外部缓冲区组(零拷贝优化)
MPP_DEC_SET_INFO_CHANGE_READY 通知解码器分辨率变化已处理(必做,否则卡死)
MPP_ENC_SET_RC_CFG 设置编码器码率控制模式(CBR/VBR/CQP)
4.2.4 数据载体操作API
// 初始化MppPacket(绑定数据+长度)
// packet: 输出参数,返回初始化后的MppPacket
// data: 码流数据指针
// size: 码流数据长度
MPP_RET mpp_packet_init(MppPacket *packet, void *data, size_t size);

// 释放MppPacket
MPP_RET mpp_packet_deinit(MppPacket *packet);

// 初始化MppFrame
MPP_RET mpp_frame_init(MppFrame *frame);

// 释放MppFrame
MPP_RET mpp_frame_deinit(MppFrame *frame);

// 获取MppFrame的YUV数据指针
void* mpp_frame_get_buffer(MppFrame frame);

// 获取MppFrame的有效宽度
RK_U32 mpp_frame_get_width(MppFrame frame);

// 获取MppFrame的硬件行跨度(对齐后的宽度,≥ width)
RK_U32 mpp_frame_get_hor_stride(MppFrame frame);

// 获取MppFrame的有效高度
RK_U32 mpp_frame_get_height(MppFrame frame);

// 获取MppFrame的硬件列跨度(对齐后的高度,≥ height)
RK_U32 mpp_frame_get_ver_stride(MppFrame frame);

// 获取MppFrame的格式(YUV420P/NV12等)
MppFrameFormat mpp_frame_get_fmt(MppFrame frame);

5. 实战一:H.264硬件解码完整实现

5.1 解码标准流程

  1. 创建MPP上下文 → 2. 初始化解码器(H.264)→ 3. 配置解码器 → 4. 循环:读码流 → 送Packet → 取Frame → 处理YUV → 5. 刷新解码器 → 6. 释放资源

5.2 完整代码示例(含详细注释)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "rk_mpi.h"

// 配置参数
#define INPUT_H264_FILE  "input.h264"   // 输入H.264裸流文件
#define OUTPUT_YUV_FILE  "output.yuv"   // 输出YUV420文件
#define BUF_SIZE         (1024 * 1024)  // 码流读取缓冲区(1MB,可根据实际调整)

int main() {
   
   
    MPP_RET ret = MPP_OK;
    MppCtx ctx = NULL;
    MppApi *mpi = NULL;
    MppPacket packet = NULL;
    MppFrame frame = NULL;
    MppDecCfg dec_cfg = NULL;
    FILE *fp_in = NULL, *fp_out = NULL;
    RK_U8 *buf_in = NULL;
    RK_S32 read_len = 0;

    // ------------------- 1. 打开文件 + 分配缓冲区 -------------------
    fp_in = fopen(INPUT_H264_FILE, "rb");
    fp_out = fopen(OUTPUT_YUV_FILE, "wb");
    if (!fp_in || !fp_out) {
   
   
        printf("错误:无法打开输入或输出文件!\n");
        goto EXIT;
    }

    buf_in = (RK_U8 *)malloc(BUF_SIZE);
    if (!buf_in) {
   
   
        printf("错误:内存分配失败!\n");
        goto EXIT;
    }

    // ------------------- 2. 创建MPP上下文 -------------------
    ret = mpp_create(&ctx, &mpi);
    if (ret != MPP_OK) {
   
   
        printf("错误:mpp_create失败,错误码=%d\n", ret);
        goto EXIT;
    }

    // ------------------- 3. 初始化解码器(H.264解码) -------------------
    // MPP_VIDEO_CodingAVC = H.264
    // MPP_VIDEO_CodingHEVC = H.265
    ret = mpp_init(ctx, MPP_CTX_DEC, MPP_VIDEO_CodingAVC);
    if (ret != MPP_OK) {
   
   
        printf("错误:mpp_init失败,错误码=%d\n", ret);
        goto EXIT;
    }

    // ------------------- 4. 配置解码器(关键:启用自动切分NALU) -------------------
    mpp_dec_cfg_init(&dec_cfg);
    // 获取解码器默认配置
    ret = mpi->control(ctx, MPP_DEC_GET_CFG, dec_cfg);
    if (ret != MPP_OK) {
   
   
        printf("错误:获取解码器配置失败,错误码=%d\n", ret);
        goto EXIT;
    }

    // 启用码流自动切分(无需手动拆分NALU,新手必开)
    // 当设置为1时,MPP内部会自动切分任意长度的码流为NALU
    mpp_dec_cfg_set_u32(dec_cfg, "base:split_parse", 1);

    // 应用配置到解码器
    ret = mpi->control(ctx, MPP_DEC_SET_CFG, dec_cfg);
    if (ret != MPP_OK) {
   
   
        printf("错误:设置解码器配置失败,错误码=%d\n", ret);
        goto EXIT;
    }

    // 释放配置结构体
    mpp_dec_cfg_deinit(&dec_cfg);
    dec_cfg = NULL;

    printf("MPP H.264解码器初始化成功!\n");

    // ------------------- 5. 循环解码:读码流 → 送Packet → 取Frame -------------------
    while (1) {
   
   
        // 5.1 从文件读取H.264码流
        read_len = fread(buf_in, 1, BUF_SIZE, fp_in);
        if (read_len <= 0) {
   
   
            printf("码流读取完毕!\n");
            break;
        }

        // 5.2 初始化MppPacket,绑定读取到的码流数据
        ret = mpp_packet_init(&packet, buf_in, read_len);
        if (ret != MPP_OK) {
   
   
            printf("错误:mpp_packet_init失败,错误码=%d\n", ret);
            break;
        }

        // 5.3 将码流包送入解码器
        ret = mpi->decode_put_packet(ctx, packet);
        if (ret != MPP_OK) {
   
   
            printf("错误:decode_put_packet失败,错误码=%d\n", ret);
            mpp_packet_deinit(&packet);
            break;
        }

        // 送入后立即释放Packet(数据已拷贝到MPP内部队列)
        mpp_packet_deinit(&packet);
        packet = NULL;

        // 5.4 循环获取解码后的帧
        // 注意:一次put_packet可能对应多次get_frame
        while (1) {
   
   
            // 尝试获取解码帧
            ret = mpi->decode_get_frame(ctx, &frame);
            
            // 超时处理:硬件解码需要时间,通常几毫秒
            if (ret == MPP_ERR_TIMEOUT) {
   
   
                usleep(1000); // 休眠1毫秒后重试
                continue;
            }

            // 没有可获取的帧,退出内层循环,继续读取下一个码流包
            if (ret != MPP_OK || !frame) {
   
   
                break;
            }

            // ------------------- 处理分辨率变化(必做!否则解码器卡死) -------------------
            // 当解码器解析到SPS/PPS等参数集,发现分辨率变化时,会返回带有info_change标志的帧
            if (mpp_frame_get_info_change
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值