4. 视频编码H.264

任务目标

本案例基于嵌入式Linux平台(以Rockchip RK3588为例),调用硬件编码器(MPP编码模块)将采集到的原始NV12视频帧编码为H.264码流,并输出可写入文件的编码数据。通过本案例,你将掌握硬件编码器的初始化、帧输入、码流输出及资源释放的完整流程。

环境依赖

  • 硬件平台:RK3588(或其他Rockchip系列芯片)
  • 系统依赖:Linux内核启用V4L2及Rockchip MPP驱动
  • 用户态库:librga(可选,用于格式转换)、rockchip_mpp(核心编码库)
  • 编译工具:交叉编译器(如aarch64-linux-gnu-gcc)

核心实现

以下代码实现从NV12原始帧到H.264码流的完整编码流程。代码通过MPP的编码通道API完成硬件编码,并将编码后的数据包写入输出文件。输入帧数据由程序内部生成(模拟采集),实际使用时替换为摄像头或HDMI采集的帧数据即可。

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

#define FRAME_WIDTH  1280
#define FRAME_HEIGHT 720
#define FRAME_SIZE   (FRAME_WIDTH * FRAME_HEIGHT * 3 / 2)  // NV12格式

static int create_test_frame(void *buf, int width, int height) {
    // 生成测试图像:上半部分为灰色,下半部分为白色
    uint8_t *y = (uint8_t *)buf;
    uint8_t *uv = y + width * height;
    for (int i = 0; i < height; i++) {
        memset(y + i * width, (i < height / 2) ? 128 : 235, width);
    }
    memset(uv, 128, width * height / 2);  // UV分量设为中性值
    return 0;
}

int main(int argc, char **argv) {
    MppCtx ctx = NULL;
    MppApi *mpi = NULL;
    MppEncCfg cfg = NULL;
    MppBuffer frm_buf = NULL;
    MppPacket pkt = NULL;
    FILE *out_fp = NULL;
    int ret = 0;
    int frame_count = 30;  // 编码30帧

    // 1. 初始化编码器上下文
    ret = mpp_create(&ctx, &mpi);
    if (ret != MPP_OK) {
        printf("mpp_create failed\n");
        return -1;
    }

    // 2. 配置编码参数
    mpp_enc_cfg_init(&cfg);
    mpp_enc_cfg_set_s32(cfg, "prep:width", FRAME_WIDTH);
    mpp_enc_cfg_set_s32(cfg, "prep:height", FRAME_HEIGHT);
    mpp_enc_cfg_set_s32(cfg, "prep:format", MPP_FMT_NV12);
    mpp_enc_cfg_set_s32(cfg, "codec:type", MPP_VIDEO_CodingAVC);
    mpp_enc_cfg_set_s32(cfg, "rc:mode", MPP_ENC_RC_MODE_CBR);
    mpp_enc_cfg_set_s32(cfg, "rc:bps", 2000000);       // 2Mbps
    mpp_enc_cfg_set_s32(cfg, "rc:bps_max", 2000000);
    mpp_enc_cfg_set_s32(cfg, "rc:bps_min", 2000000);
    mpp_enc_cfg_set_s32(cfg, "codec:gop", 30);          // I帧间隔
    mpp_enc_cfg_set_s32(cfg, "codec:qp_init", 26);

    ret = mpi->control(ctx, MPP_ENC_SET_CFG, cfg);
    if (ret != MPP_OK) {
        printf("set cfg failed\n");
        goto exit;
    }

    // 3. 准备输入帧缓冲区
    ret = mpp_buffer_get(NULL, &frm_buf, FRAME_SIZE);
    if (ret != MPP_OK) {
        printf("get buffer failed\n");
        goto exit;
    }

    // 4. 打开输出文件
    out_fp = fopen("output.h264", "wb");
    if (!out_fp) {
        printf("open output file failed\n");
        goto exit;
    }

    // 5. 编码循环
    for (int i = 0; i < frame_count; i++) {
        // 生成或获取一帧原始数据
        create_test_frame(mpp_buffer_get_ptr(frm_buf), FRAME_WIDTH, FRAME_HEIGHT);

        // 发送帧到编码器
        MppFrame frame = NULL;
        mpp_frame_init(&frame);
        mpp_frame_set_buffer(frame, frm_buf);
        mpp_frame_set_width(frame, FRAME_WIDTH);
        mpp_frame_set_height(frame, FRAME_HEIGHT);
        mpp_frame_set_fmt(frame, MPP_FMT_NV12);
        mpp_frame_set_pts(frame, i * 33333);  // 33.3ms间隔

        ret = mpi->encode_put_frame(ctx, frame);
        mpp_frame_deinit(&frame);
        if (ret != MPP_OK) {
            printf("encode_put_frame failed at frame %d\n", i);
            break;
        }

        // 获取编码后的码流包
        ret = mpi->encode_get_packet(ctx, &pkt);
        if (ret == MPP_OK && pkt) {
            void *data = mpp_packet_get_pos(pkt);
            size_t len = mpp_packet_get_length(pkt);
            fwrite(data, 1, len, out_fp);
            printf("Frame %d encoded, size: %zu bytes\n", i, len);
            mpp_packet_deinit(&pkt);
        } else {
            printf("encode_get_packet failed at frame %d\n", i);
        }
    }

    // 6. 发送EOS并获取剩余码流
    mpi->encode_put_frame(ctx, NULL);
    while (1) {
        ret = mpi->encode_get_packet(ctx, &pkt);
        if (ret != MPP_OK || !pkt) break;
        void *data = mpp_packet_get_pos(pkt);
        size_t len = mpp_packet_get_length(pkt);
        fwrite(data, 1, len, out_fp);
        mpp_packet_deinit(&pkt);
    }

exit:
    // 7. 资源清理
    if (out_fp) fclose(out_fp);
    if (frm_buf) mpp_buffer_put(frm_buf);
    if (cfg) mpp_enc_cfg_deinit(cfg);
    if (ctx) {
        mpi->reset(ctx);
        mpp_destroy(ctx);
    }
    printf("Encoding complete. Output: output.h264\n");
    return ret;
}

编译与运行

# 交叉编译(假设使用RK提供的工具链)
aarch64-linux-gnu-gcc -o h264_encoder h264_encoder.c -I/usr/include/rockchip -lmpp

# 将可执行文件拷贝到目标板并运行
./h264_encoder

验证方法

  1. 运行输出检查:程序执行后应打印每帧编码后的字节数(通常I帧较大,P帧较小),最终生成output.h264文件。
  2. 码流完整性验证:在PC上使用FFmpeg检查输出文件:
ffprobe output.h264
# 预期输出:编码格式为h264,分辨率1280x720,像素格式yuv420p
  1. 解码回放验证:使用FFmpeg解码并统计帧数:
ffmpeg -i output.h264 -f null -
# 预期输出:解码30帧无错误,无丢帧警告

通过条件:ffprobe能正确识别H.264编码格式与分辨率,ffmpeg解码无报错且帧数为30,即证明硬件编码器工作正常。

常见问题

  • 编码器初始化失败:确认内核已加载rkvdec/rkvenc驱动,且/dev/mpp_service节点存在。
  • 输出文件为空:检查编码参数中rc:mode是否设置为CBR或VBR,部分固件默认VBR可能导致码率波动。
  • 帧率不匹配:实际采集帧率与pts设置不一致时,播放可能出现快慢不均,需根据实际采集时间戳设置pts。