01 Codec 模块简述

Codec(Coder-Decoder)是指编解码器,用于压缩或解压缩视频、图像、音频等媒体数据;J6 Soc 中存在两种硬件编解码单元,分别是 VPU(Video process unit)和 JPU(Jpeg process unit),可提供 4K@90fps 的视频编解码能力和 4K@90fps 的图像编解码能力。

1.1 硬件特性

1.1.1 JPU 硬件特性:

img

1.1.2 VPU 硬件特性:

img

img

1.2 软件功能

MediaCodec 子系统会提供音视频和图像的编解码组件,原始流封装和视频录像等功能。该系统主要会封装底层 codec 硬件资源和软件编解码库,为上层提供编解码能力。开发者可以基于提供的编解码接口实现 H265 和 H264 视频的编解码功能,也可以使用 JPEG 编码功能将摄像头数据存成 JPEG 图片,还可以使用视频录像功能实现摄像头数据的录制。

1.2.1 整体框架:

img

1.2.2 控制接口:

hb_s32 hb_mm_mc_initialize(media_codec_context_t *context):初始化编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_INITIALIZED 状态。

hb_s32 hb_mm_mc_configure(media_codec_context_t *context):配置编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_CONFIGURED 状态。

hb_s32 hb_mm_mc_start(media_codec_context_t *context, const mc_av_codec_startup_params_t *info):启动编码/解码流程,MediaCodec 将创建编解码实例、设置序列或解析数据流、注册 Framebuffer、编码头信息等,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_STARTED 状态。

hb_s32 hb_mm_mc_stop(media_codec_context_t *context):停止编码/解码流程,退出所有子线程并释放相关资源,调用成功后 MediaCodec 回到 MEDIA_CODEC_STATE_INITIALIZED 状态。

hb_s32 hb_mm_mc_release(media_codec_context_t *context):释放 MediaCodec 内部所有资源,用户需要在调用该函数前调用 hb_mm_mc_stop 来停止编解码,操作成功后 MediaCodec 进入 MEDIA_CODEC_STATE_UNINITIALIZED 状态。

hb_s32 hb_mm_mc_queue_input_buffer(media_codec_context_t *context, media_codec_buffer_t *buffer, hb_s32 timeout):填充需要处理的 buffer 到 MediaCodec 中。

hb_s32 hb_mm_mc_dequeue_input_buffer(media_codec_context_t *context, media_codec_buffer_t *buffer, hb_s32 timeout):获取输入的 buffer。

hb_s32 hb_mm_mc_queue_output_buffer(media_codec_context_t *context, media_codec_buffer_t *buffer, hb_s32 timeout):返还处理完的 output buffer 到 MediaCodec 中。

hb_s32 hb_mm_mc_dequeue_output_buffer(media_codec_context_t *context, media_codec_buffer_t *buffer, media_codec_output_buffer_info_t *info, hb_s32 timeout):获取输出的 buffer。

1.2.3 码率控制模式:

MediaCodec 支持对 H264/H265 和 MJPEG 协议的码率控制,分别支持 H264/H265 编码通道的 CBR、VBR、AVBR、FixQp 和 QpMap 五种码率控制方式,以及支持 MJPGE 编码通道的 FixQp 码率控制方式。

*1.2.3.1 CBR 说明:*

CBR 表示恒定码率,能够保证整体的编码码率稳定。下面是 CBR 模式下各个参数含义:

img

*1.2.3.2 VBR 说明:*

VBR 表示可变码率,简单场景分配比较大的 qp,压缩率小,质量高。复杂场景分配较小 qp,可以保证编码图像的质量稳定。下面是 VBR 模式下各个参数含义:

img

1.2.3.3 AVBR 说明:

ABR 表示恒定平均目标码率,简单场景分配较低码率,复杂场景分配足够码率,使得有限的码率能够在不同场景下合理分配,这类似 VBR。同时一定时间内,平均码率又接近设置的目标码率,这样可以控制输出文件的大小,这又类似 CBR。可以认为是 CBR 和 VBR 的折中方案,产生码率和图像质量相对稳定的码流。下面是 AVBR 模式下各个参数含义:

img

1.2.3.4 FixQp 说明:

FixQp 表示固定每一个 I 帧、P 帧的 QP 值,对于 I/P 帧可以分别设值。下面是 FixQp 模式下各个参数含义:

img

1.2.3.5 QPMAP 说明:

img

1.2.4 编码效果:

根据当前客户使用 codec 进行视频编码的场景,多将码率模式设置为 CBR,当编码的场景较为复杂时,为了保证视频质量,硬件会自动提高码率值,导致输出的视频较预期更大。因此为了兼顾视频质量和实际码率,需要统筹 bit_rate 和 max_qp_I/P 值的设置。下面给出了全 I 帧模式下,不同复杂场景下,码率设置为 15000kbps 时,不同 max_qp_I 下实际码率和 qp 的情况(不同场景复杂程度不同,下列数据仅供参考):

img

H264/H265 编码支持 GOP 结构的设置,用户可从预置的 3 种 GOP 结构种选择,也可自定义 GOP 结构。

GOP 结构表可定义一组周期性的 GOP 结构,该 GOP 结构将用于整个编码过程。单个结构表中的元素如下表所示,其中可以指定该图像的参考帧,如果 IDR 帧后的其他帧指定的参考帧为 IDR 帧前的数据帧,编码器内部会自动处理这种情况使其不参考其他帧,用户无需关心这种情况。用户在自定义 GOP 结构时需要指明结构表的数量,最多可定义 3 个结构表,结构表的顺序需要按照解码顺序排列。下面表示了结构表中各个元素的含义:

img

征程6 中一共支持设置九种 GOP 预置结构:

img

02 Codec-Sample 使用

编码 yuv 图像, 生成 h264/h265 视频或 jpg 图片。

2.1 encoder

2.1.1 调用流程

采用 MediaCodec 的 poll 模式来解耦输入和输出,可使编码帧率性能达到最优。在主线程中灌 YUV 数据:取出一个空的 input buffer,配置 YUV 数据的地址信息(如 phys addr),再 queue input buffer 并通知编码器处理该帧数据;另一个线程取输出码流:通过 select 接收硬件编码完成通知,取出一个硬件填满输出码流的 output buffer,将编码结果写到文件中后归还 output buffer。

img

check_and_init_test:打开输入文件(yuv),并打开内存管理模块申请内存缓冲;

hb_mm_mc_initialize:初始化编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_INITIALIZED 状态;

hb_mm_mc_configure:配置编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_CONFIGURED 状态;

hb_mm_mc_start:启动编码/解码流程,MediaCodec 将创建编解码实例、设置序列或解析数据流、注册 Framebuffer、编码头信息等,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_STARTED 状态;

hb_mm_mc_dequeue_input_buffer:获取输入的 buffer;

read_input_frames:从输入文件(yuv)中读取视频帧数据,并将其刷新到 buffer 中;

hb_mm_mc_queue_input_buffer:填充需要处理的 buffer 到 MediaCodec 中;

hb_mm_mc_dequeue_output_buffer:获取输出的 buffer;

write_output_streams:outputbuffer 写入 outFile 中;

hb_mm_mc_queue_output_buffer:返还处理完的 output buffer 到 MediaCodec 中;

hb_mm_mc_stop:停止编码/解码流程,退出所有子线程并释放相关资源,调用成功后 MediaCodec 回到 MEDIA_CODEC_STATE_INITIALIZED 状态;

hb_mm_mc_release:释放 MediaCodec 内部所有资源,操作成功后 MediaCodec 进入 MEDIA_CODEC_STATE_UNINITIALIZED 状态。

2.1.2 源码主干:

#Sample源码路径
/test/samples/platform_samples/source/S83_Sample/S83E04_Module/codec_sample

Encoder:

void do_sync_encoding(void *arg)
{
    hb_s32 ret = 0;
    MediaCodecTestContext *ctx = (MediaCodecTestContext *)arg;
    media_codec_context_t *context = NULL;
    media_codec_buffer_t inputBuffer;
    media_codec_buffer_t outputBuffer;
    media_codec_output_buffer_info_t info;

    ret = check_and_init_test(ctx);
    if (ret != 0) {
        printf("check_and_init_test failed(%d).\n", ret);
        return;
    }

    context = ctx->context;
    ret = hb_mm_mc_initialize(context);
    if (ret != 0) {
        printf("hb_mm_mc_initialize failed(%d).\n", ret);
        return;
    }

    // set_message(ctx);

    ret = hb_mm_mc_configure(context);
    if (ret != 0) {
        printf("hb_mm_mc_configure failed(%d).\n", ret);
        hb_mm_mc_release(context);
        return;
    }

    mc_av_codec_startup_params_t startup_params;
    startup_params.video_enc_startup_params.receive_frame_number = 0;
    ret = hb_mm_mc_start(context, &startup_params);
    if (ret != 0) {
        printf("hb_mm_mc_start failed(%d).\n", ret);
        hb_mm_mc_release(context);
        return;
    }

    do {
        // process input buffers
        memset(&inputBuffer, 0x00, sizeof(media_codec_buffer_t));
        ret = hb_mm_mc_dequeue_input_buffer(context, &inputBuffer, 3000);
        if (ret == 0) {
            ret = read_input_frames(ctx, &inputBuffer);
            if (ret <= 0) {
                printf("There is no more input data(ret=%d)!\n", ret);
                inputBuffer.vframe_buf.size = 0;
                inputBuffer.vframe_buf.frame_end = TRUE;
            }

            ctx->input_num++;
            // ASSERT_EQ(do_encode_params_setting(ctx, &inputBuffer), 0);

            ret = hb_mm_mc_queue_input_buffer(context, &inputBuffer, 100);
            if (ret != 0) {
                break;
            }
        } else {
            printf("dequeue input buffer fail(%d).\n", ret);
            if (ret != (int32_t)HB_MEDIA_ERR_WAIT_TIMEOUT) {
                break;
            }
        }

        // process output buffers
        memset(&outputBuffer, 0x00, sizeof(media_codec_buffer_t));
        memset(&info, 0x00, sizeof(media_codec_output_buffer_info_t));
        ret = hb_mm_mc_dequeue_output_buffer(context, &outputBuffer, &info, 3000);
        if (ret == 0) {
            write_output_streams(ctx, &outputBuffer);
            ret = hb_mm_mc_queue_output_buffer(context, &outputBuffer, 100);
            if (outputBuffer.vstream_buf.stream_end) {
                printf("There is no more output data!\n");
                break;
            }
            if (ret) {
                break;
            }
        } else {
            printf("dequeue output buffer fail(ret=0x%x).\n", ret);
            if (ret != (int32_t)HB_MEDIA_ERR_WAIT_TIMEOUT) {
                break;
            }
        }

    } while(TRUE);

    ret = hb_mm_mc_stop(context);
    if (ret != 0) {
        printf("hb_mm_mc_stop failed(%d).\n", ret);
    }

    ret = hb_mm_mc_release(context);
    if (ret != 0) {
        printf("hb_mm_mc_release failed(%d).\n", ret);
    }

    check_and_release_test(ctx);

}

2.2 decoder

VPU/JPU 从 DDR 中获得 H265/H264/JPG 输入源,经过硬件解码后生成 yuv 图像

2.2.1 调用流程

img

check_and_init_test:打开输入文件(h265),并打开内存管理模块申请内存缓冲;

hb_mm_mc_initialize:初始化编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_INITIALIZED 状态;

hb_mm_mc_configure:配置编码或解码器,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_CONFIGURED 状态;

hb_mm_mc_start:启动编码/解码流程,MediaCodec 将创建编解码实例、设置序列或解析数据流、注册 Framebuffer、编码头信息等,调用成功后 MediaCodec 进入 MEDIA_CODEC_STATE_STARTED 状态;

hb_mm_mc_dequeue_input_buffer:获取输入的 buffer;

hb_mm_mc_dequeue_output_buffer:获取输出的 buffer;

write_output_streams:outputbuffer 写入 outFile 中;

hb_mm_mc_queue_output_buffer:返还处理完的 output buffer 到 MediaCodec 中;

hb_mm_mc_stop:停止编码/解码流程,退出所有子线程并释放相关资源,调用成功后 MediaCodec 回到 MEDIA_CODEC_STATE_INITIALIZED 状态;

hb_mm_mc_release:释放 MediaCodec 内部所有资源,操作成功后 MediaCodec 进入 MEDIA_CODEC_STATE_UNINITIALIZED 状态。

** **

2.2.2 源码主干:

void do_sync_decoding(void *arg)
{
    int ret = 0;
    MediaCodecTestContext *ctx = (MediaCodecTestContext *)arg;
    media_codec_context_t *context = NULL;
    media_codec_buffer_t inputBuffer;
    media_codec_buffer_t outputBuffer;
    media_codec_output_buffer_info_t info;

    ret = check_and_init_test(ctx);
    if (ret != 0) {
        printf("check_and_init_test failed(%d).\n", ret);
        return;
    }   context = ctx->context;

    context = ctx->context;
    ret = hb_mm_mc_initialize(context);
    if (ret != 0) {
        printf("hb_mm_mc_initialize failed(%d).\n", ret);
        return;
    }

    ret = hb_mm_mc_configure(context);
    if (ret != 0) {
        printf("hb_mm_mc_configure failed(%d).\n", ret);
        hb_mm_mc_release(context);
        return;
    }

    mc_av_codec_startup_params_t startup_params;
    startup_params.video_enc_startup_params.receive_frame_number = 0;
    ret = hb_mm_mc_start(context, &startup_params);
    if (ret != 0) {
        printf("hb_mm_mc_start failed(%d).\n", ret);
        hb_mm_mc_release(context);
        return;
    }

    do {
        // process input buffers
        ret = hb_mm_mc_dequeue_input_buffer(context, &inputBuffer, 3000);
        if (ret == 0) {
            ret = read_input_streams(ctx, &inputBuffer);
            if (ret <= 0) {
                printf("There is no more input data(ret=%d)!\n", ret);
                inputBuffer.vstream_buf.stream_end = TRUE;
                inputBuffer.vstream_buf.size = 0;
                ctx->lastStream = 1;
            }

            ret = hb_mm_mc_queue_input_buffer(context, &inputBuffer, 100);
            if (ret != 0) {
                break;
            }
        } else {
            if (ret != (int32_t)HB_MEDIA_ERR_WAIT_TIMEOUT) {
                char info[256];
                hb_mm_strerror(ret, info, 256);
                printf("dequeue input buffer fail.(%s)\n", info);
                break;
            }
        }

        // process output buffers
        memset(&outputBuffer, 0x00, sizeof(media_codec_buffer_t));
        memset(&info, 0x00, sizeof(media_codec_output_buffer_info_t));
        ret = hb_mm_mc_dequeue_output_buffer(context, &outputBuffer, &info, 100);
        if (ret == 0) {
            printf("info.video_frame_info.nalu_type %d\n", info.video_frame_info.nalu_type);
            write_output_frames(ctx, &outputBuffer);

            if (ctx->enable_get_userdata) {
                mc_user_data_buffer_t userdata = {0};
                ret = hb_mm_mc_get_user_data(context, &userdata, 0);
                if (!ret) {
                    printf("Get userdata %d:\n", userdata.size);
                    for (uint32_t i = 0; i < userdata.size; i++) {
                        if (i < 16) {
                            printf("userdata[i]:%x\n", userdata.virt_addr[i]);
                        } else {
                            printf("userdata[i]:%c\n", userdata.virt_addr[i]);
                        }
                    }
                    ret = hb_mm_mc_release_user_data(context, &userdata);
                } else {
                    ret = 0;
                }
            }

            ret = hb_mm_mc_queue_output_buffer(context, &outputBuffer, 100);
            if (outputBuffer.vframe_buf.frame_end) {
                printf("There is no more output data!\n");
                break;
            }
            if (ret) {
                break;
            }
        } else {
            char info[256];
            hb_mm_strerror(ret, info, 256);
            printf("dequeue output buffer fail.(%s)\n", info);
            if (ret != (int32_t)HB_MEDIA_ERR_WAIT_TIMEOUT) {
                break;
            }
        }
    } while (TRUE);

    ret = hb_mm_mc_stop(context);
    if (ret != 0) {
        printf("hb_mm_mc_stop failed(%d).\n", ret);
    }

    ret = hb_mm_mc_release(context);
    if (ret != 0) {
        printf("hb_mm_mc_release failed(%d).\n", ret);
    }

    check_and_release_test(ctx);
}

2.3 编译&运行

获取 AppSDK 包后,进入 appuser 执行:

*其中 hbrootfs-sdk_0.0.1.XXX_all.deb 是地平线自己的库和头文件,rootfs-sdk-focal_0.0.1.XXX_all.deb 是系统库,aarch64-linux-hb-gcc_12.2.0_amd64.deb 是 gcc 12.2.0 工具链,目前在 ubuntu22.04 非 docker 环境下运行正常。其它环境不能保证。

dpkg-deb -x rootfs-sdk*.deb ./sdk
dpkg-deb -x hbrootfs-sdk*.deb ./sdk
##移动sdk库路径,本文档放入/usr/lib中
sudo mv sdk/ /usr/lib

进入 toolchain 执行:

dpkg -x aarch64-linux-hb-gcc_12.2.0_amd64.deb ./arm-gnu-toolchain
##移动toolchain库路径,本文档放入/usr/lib中
sudo mv arm-gnu-toolchain/ /usr/lib
nano ~/.bashrc
##添加系统路径
export PATH="/usr/lib/arm-gnu-toolchain/bin:$PATH"
export LD_LIBRARY_PATH="/usr/lib/arm-gnu-toolchain/lib:$LD_LIBRARY_PATH"
##
source ~/.bashrc

Sample 代码路径:

#Sample源码路径
/test/samples/platform_samples/source/S83_Sample/S83E04_Module/codec_sample

运行参数说明:

img

复制/src 源码到新建文件夹 codec 并构建新 Makefile:

codec
├── Makefile
└── src
    ├── sample.c
    ├── sample_common.c
    ├── sample.h
    ├── sample_vdec.c
    └── sample_venc.c

Makefile:

CROSS_COMPILE = aarch64-none-linux-gnu-
OUTPUT_HBROOTFS_DIR = /usr/lib/sdk

CXX := ${CROSS_COMPILE}gcc

INC_DIR := ${OUTPUT_HBROOTFS_DIR}/usr/hobot/include
INC_DIR += ${OUTPUT_HBROOTFS_DIR}/include
LIB_DIR := ${OUTPUT_HBROOTFS_DIR}/usr/hobot/lib 
LIB_DIR += ${OUTPUT_HBROOTFS_DIR}/usr/lib/aarch64-linux-gnu
LIBS += -lpthread -ldl -lhbmem -lalog  -lmultimedia
LIBS += -lavformat -lavcodec -lavutil -lswresample
CXXFLAGS := -Wall -O2 $(foreach dir,$(INC_DIR),-I$(dir))
LDFLAGS := $(addprefix -L, $(LIB_DIR)) $(LIBS)

SRC_DIR := src
TARGET := program
SRCS := $(wildcard $(SRC_DIR)/*.c)

OBJS := $(SRCS:.c=.o)

$(TARGET): $(OBJS)
    $(CXX) $(CXXFLAGS) $(LDFLAGS) $^ -o $@
%.o: %.c
    $(CXX) $(CXXFLAGS) -c $< -o $@
clean:
    rm -f $(OBJS) $(TARGET)

执行 make 完成编译,生成的文件为。/program

img

使用 WinScp 将 program 传输到单板上。*需要 encode 的 yuv 文件请用户自行准备,本文档使用 PYM 生成的 yuv 文件。

*WinScp 使用方法请参考征程 6E/M 底软开发 Sample-IPC 2.1.2

通过 ssh 或串口进入/home/hobot/执行:*w 和 h 需要进行 16 字节对齐,如原始 yuv 不支持则会出现数据丢失

chmod +x program
#encode
#代码可参考源码目录中的codec_sample.sh
./program -m 0 -c 3 -w 3840 -h 2160 -p 1 -i 3840x2160_pipe0_pym_ds0_f0_roi_1.yuv -o ./3840x2160.jpg
./program -m 0 -c 1 -w 3840 -h 2160 -p 1 -i 3840x2160_pipe0_pym_ds0_f0_roi_1.yuv -o ./3840x2160.h265

Sample 运行时日志:

g_samplemode = 0
g_codecid = 3
g_width = 3840
g_height = 2160
g_pixfmt = 1
g_pixfmt = 1
g_inputfilename = 3840x2160_pipe0_pym_ds0_f0_roi_1.yuv
g_outputfilename = ./3840x2160.jpg
InputFileName = 3840x2160_pipe0_pym_ds0_f0_roi_1.yuv
OutputFileName = ./3840x2160.jpg
Thread use internal buffer mode, 0 rc mode
Failed to read input file (size=12441600)
There is no more input data(ret=0)!
There is no more output data!

生成的缩放 jpg 文件存放在指定-o 目录下。

生成的 Jpeg 效果如下:

img

*有关 jpg 工具查看 1080p 图片时出现绿边问题:

img

问题说明:这是因为当前 ip 进行编码时按照 16 位对齐进行,假如到最后如果是 8 位对齐而不是 16 位对齐,那么编码器就会在后面补齐,这部分补齐的数据是随机产生的,不属于有效数据;

#decode
#使用上述生成的3840x2160.h265为例
./program -m 1 -c 1 -w 3840 -h 2160 -p 1 -i 3840x2160.h265 -o ./3840x2160.yuv

img

使用 YUView 打开生成的 3840x2160.yuv:

img

*注意配置 offset:

img

img

附件:

1、codec.7z

Logo

加入社区

更多推荐