在 Jetson Orin 开发套件上使用 Hardware Encoder / Decoder 构建 FFmpeg
目录
1、构建 Jetson-FFmpeg
2、构建 FFmpeg
3、通过代码实现硬解 H264 文件为 YUV420P
3.1 运行可能遇到的问题
3.2 补丁应用失败的解决方法
1、构建 Jetson-FFmpeg
通过 Git 下载 Jetson-ffmpeg:
git clone https://github.com/Keylost/jetson-ffmpeg.git
cd jetson-ffmpeg
用文本编辑器打开 CMakeLists.txt ,注释掉以下行:
# find_library(LIB_NVBUF nvbuf_utils PATHS /usr/lib/aarch64-linux-gnu/tegra)
创建 build 文件夹,构建并安装 Jetson-ffmpeg:
mkdir build
cd build
cmake ..
make
sudo make install
sudo ldconfig
此时已经准备好将 NVMPI 与 FFmpeg 一起使用。
2、构建 FFmpeg
通过 Git 下载特定版本的 ffmpeg,并下载 ffmpeg_nvmpi 的补丁:
git clone git://source.ffmpeg.org/ffmpeg.git -b release/6.0 --depth=1
cd ffmpeg
wget -O ffmpeg_nvmpi.patch https://github.com/Keylost/jetson-ffmpeg/raw/master/ffmpeg_patches/ffmpeg6.0_nvmpi.patch
安装依赖,设置 ffmpeg 的安装路径以及依赖,编译并安装:
git apply ffmpeg_nvmpi.patch # 根据需要上传到自己的分支
sudo apt install -y libpulse-dev
sudo apt-get install -y build-essential yasm nasm libx265-dev libx264-dev libnuma-de
./configure --prefix=/home/user/work/ffmpeg/install --enable-nvmpi --enable-libpulse --enable-shared --enable-libx264 --enable-gpl --enable-libx265 --enable-nonfree --enable-swresample --enable-swscale
make
sudo make install
尝试用 ffmpeg 解码 h264 文件:
ffmpeg -re -i ../data/video.h264 -c:v h264_nvmpi -r 25 -f null -
3、通过代码实现硬解 H264 文件为 YUV420P
以 Ubuntu 20.04 版本为例,通过 C 实现硬解 H264 文件为 YUV420P:
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>
#include <stdio.h>typedef struct StreamContext {AVCodecContext *dec_ctx;FILE *yuv_file; // YUV输出文件
} StreamContext;static int open_input_file(const char *filename, AVFormatContext **input_ctx) {int ret;if ((ret = avformat_open_input(input_ctx, filename, NULL, NULL)) < 0) {av_log(NULL, AV_LOG_ERROR, "无法打开输入文件 '%s'\n", filename);return ret;}if ((ret = avformat_find_stream_info(*input_ctx, NULL)) < 0) {av_log(NULL, AV_LOG_ERROR, "无法获取流信息\n");return ret;}return 0;
}static int init_decoder(AVCodecContext **dec_ctx, AVStream *stream) {// 尝试使用 NVMPI 硬件解码器const AVCodec *dec = avcodec_find_decoder_by_name("h264_nvmpi");int ret;if (!dec) {av_log(NULL, AV_LOG_WARNING, "无法找到 NVMPI 硬件解码器,尝试使用软解码器\n");// 如果找不到硬件解码器,回退到软解码器dec = avcodec_find_decoder(stream->codecpar->codec_id);if (!dec) {av_log(NULL, AV_LOG_ERROR, "无法找到解码器\n");return AVERROR_DECODER_NOT_FOUND;}} else {av_log(NULL, AV_LOG_INFO, "使用 NVMPI 硬件解码器\n");}*dec_ctx = avcodec_alloc_context3(dec);if (!*dec_ctx) {av_log(NULL, AV_LOG_ERROR, "无法分配解码器上下文\n");return AVERROR(ENOMEM);}if ((ret = avcodec_parameters_to_context(*dec_ctx, stream->codecpar)) < 0) {av_log(NULL, AV_LOG_ERROR, "无法复制解码器参数\n");return ret;}// 设置线程数为1,避免多线程导致的问题(*dec_ctx)->thread_count = 1;// 确保输出格式为 YUV420P(*dec_ctx)->pix_fmt = AV_PIX_FMT_YUV420P;// 设置一些 NVMPI 特定的参数(如果需要的话)if (dec->id == AV_CODEC_ID_H264) {av_opt_set_int(*dec_ctx, "refcounted_frames", 1, 0);}if ((ret = avcodec_open2(*dec_ctx, dec, NULL)) < 0) {av_log(NULL, AV_LOG_ERROR, "无法打开解码器\n");return ret;}return 0;
}static int write_yuv_frame(FILE *f, AVFrame *frame) {// 检查帧数据是否有效if (!frame || !frame->data[0] || !frame->data[1] || !frame->data[2]) {av_log(NULL, AV_LOG_ERROR, "无效的帧数据\n");return AVERROR(EINVAL);}// 检查帧格式if (frame->format != AV_PIX_FMT_YUV420P) {const char *fmt_name = av_get_pix_fmt_name(frame->format);av_log(NULL, AV_LOG_ERROR, "不支持的像素格式: %s (需要 YUV420P)\n",fmt_name ? fmt_name : "unknown");return AVERROR(EINVAL);}// 检查帧尺寸if (frame->width <= 0 || frame->height <= 0) {av_log(NULL, AV_LOG_ERROR, "无效的帧尺寸: %dx%d\n", frame->width, frame->height);return AVERROR(EINVAL);}// 写入Y平面for (int i = 0; i < frame->height; i++) {size_t bytes_written = fwrite(frame->data[0] + i * frame->linesize[0], 1, frame->width, f);if (bytes_written != frame->width) {av_log(NULL, AV_LOG_ERROR, "写入Y平面失败: 写入了 %zu 字节,应该写入 %d 字节\n",bytes_written, frame->width);return AVERROR(EIO);}}// 写入U平面for (int i = 0; i < frame->height/2; i++) {size_t bytes_written = fwrite(frame->data[1] + i * frame->linesize[1], 1, frame->width/2, f);if (bytes_written != frame->width/2) {av_log(NULL, AV_LOG_ERROR, "写入U平面失败: 写入了 %zu 字节,应该写入 %d 字节\n",bytes_written, frame->width/2);return AVERROR(EIO);}}// 写入V平面for (int i = 0; i < frame->height/2; i++) {size_t bytes_written = fwrite(frame->data[2] + i * frame->linesize[2], 1, frame->width/2, f);if (bytes_written != frame->width/2) {av_log(NULL, AV_LOG_ERROR, "写入V平面失败: 写入了 %zu 字节,应该写入 %d 字节\n",bytes_written, frame->width/2);return AVERROR(EIO);}}return 0;
}int main(int argc, char **argv) {if (argc != 3) {av_log(NULL, AV_LOG_ERROR, "用法: %s <input.h264> <output.yuv>\n", argv[0]);return 1;}AVFormatContext *input_ctx = NULL;StreamContext stream_ctx = {0};AVPacket *packet = NULL;AVFrame *frame = NULL;int ret = 0;int video_stream_idx = -1;int frame_count = 0;// 打开输入文件if ((ret = open_input_file(argv[1], &input_ctx)) < 0)goto end;// 打开输出YUV文件stream_ctx.yuv_file = fopen(argv[2], "wb");if (!stream_ctx.yuv_file) {av_log(NULL, AV_LOG_ERROR, "无法打开输出文件 '%s'\n", argv[2]);ret = AVERROR(EIO);goto end;}// 查找视频流for (int i = 0; i < input_ctx->nb_streams; i++) {if (input_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {video_stream_idx = i;break;}}if (video_stream_idx < 0) {av_log(NULL, AV_LOG_ERROR, "找不到视频流\n");ret = AVERROR_STREAM_NOT_FOUND;goto end;}// 初始化解码器if ((ret = init_decoder(&stream_ctx.dec_ctx, input_ctx->streams[video_stream_idx])) < 0)goto end;// 打印视频信息av_log(NULL, AV_LOG_INFO, "视频大小: %dx%d\n", stream_ctx.dec_ctx->width,stream_ctx.dec_ctx->height);av_log(NULL, AV_LOG_INFO, "像素格式: %s\n",av_get_pix_fmt_name(stream_ctx.dec_ctx->pix_fmt));// 分配 packet 和 framepacket = av_packet_alloc();frame = av_frame_alloc();if (!packet || !frame) {av_log(NULL, AV_LOG_ERROR, "无法分配内存\n");ret = AVERROR(ENOMEM);goto end;}// 主处理循环while (1) {ret = av_read_frame(input_ctx, packet);if (ret < 0) {if (ret == AVERROR_EOF) {// 发送一个空包,刷新解码器中的帧ret = avcodec_send_packet(stream_ctx.dec_ctx, NULL);} else {av_log(NULL, AV_LOG_ERROR, "读取帧错误: %s\n", av_err2str(ret));break;}} else if (packet->stream_index != video_stream_idx) {av_packet_unref(packet);continue;} else {ret = avcodec_send_packet(stream_ctx.dec_ctx, packet);}if (ret < 0 && ret != AVERROR_EOF) {av_log(NULL, AV_LOG_ERROR, "发送数据包到解码器失败: %s\n", av_err2str(ret));break;}while (1) {ret = avcodec_receive_frame(stream_ctx.dec_ctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {break;} else if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "解码帧错误: %s\n", av_err2str(ret));goto end;}// 写入YUV数据ret = write_yuv_frame(stream_ctx.yuv_file, frame);if (ret < 0) {av_log(NULL, AV_LOG_ERROR, "写入YUV帧失败: %s\n", av_err2str(ret));goto end;}frame_count++;if (frame_count % 100 == 0) {av_log(NULL, AV_LOG_INFO, "已处理 %d 帧\n", frame_count);}av_frame_unref(frame);}if (ret == AVERROR_EOF) {break;}av_packet_unref(packet);}av_log(NULL, AV_LOG_INFO, "解码完成,共处理 %d 帧\n", frame_count);ret = 0;end:// 清理资源if (stream_ctx.yuv_file)fclose(stream_ctx.yuv_file);avcodec_free_context(&stream_ctx.dec_ctx);avformat_close_input(&input_ctx);av_packet_free(&packet);av_frame_free(&frame);return ret < 0 ? 1 : 0;
}
通过 gcc 编译,并运行可执行文件:
# 编译
gcc -g -o h264_to_yuv simple_transcode.c \$(pkg-config --cflags --libs libavcodec libavformat libavutil) \-I/usr/local/include -L/usr/local/lib# 运行
./h264_to_yuv ../code/data/video.h264 output.yuv
3.1 运行可能遇到的问题
在运行代码的时候,如果出现 “无法找到 NVMPI 硬件解码器,尝试使用软解码器” 的打印,可能是因为 FFmpeg 库的配置有问题。
这个问题是由于 FFmpeg 的编译配置导致的,之前下载的 ffmpeg_nvmpi.patch 文件,这是一个为FFmpeg 添加 NVMPI 支持的补丁文件。虽然可以使用 ffmpeg 命令行工具进行 NVMPI 硬件解码,但这是因为系统中安装的 FFmpeg 已经被正确编译并启用了 NVMPI 支持。
而当前的代码是直接使用 FFmpeg 的库进行编程,这需要确保链接的 FFmpeg 库也启用了 NVMPI支持。
解决方法如下:
1. 确保 FFmpeg 库是使用 NVMPI 支持编译的。
# 首先应用NVMPI补丁patch -p1 < ffmpeg_nvmpi.patch# 然后重新配置和编译FFmpeg./configure --enable-nvmpi --enable-shared # 可按需配置 也可用之前的配置makesudo make install
2. 确保系统中安装了 NVMPI 的开发库和头文件。
3. 在编译程序时,需要链接到正确的 FFmpeg 库。
可以通过以下命令来验证 FFmpeg 库是否支持 NVMPI:
ffmpeg -codecs | grep nvmpi
如果看到类似 h264_nvmpi 这样的编解码器,说明 FFmpeg 命令行工具确实支持 NVMPI。
3.2 补丁应用失败的解决方法
这种情况通常意味着:
1. 补丁之前已经被应用过
2. 由于选择反向应用(-R),导致补丁操作变成了移除操作
3. 最后在 nvmpi_enc.c 文件上失败,可能是因为文件内容与预期不符
按以下步骤来修复这个问题:
1. 首先检查当前的文件状态,恢复到原始状态:
root@desktop:~/work/ffmpeg$ git status
root@desktop:~/work/ffmpeg$ git checkout configure libavcodec/Makefile libavcodec/allcodecs.c
2. 清理之前的补丁文件:
root@desktop:~/work/ffmpeg$ rm -f configure.orig libavcodec/*.orig libavcodec/*.rej
3. 检查一下补丁文件的内容:
root@desktop:~/work/ffmpeg$ head -n 5 ffmpeg_nvmpi.patch
4. 重新应用补丁,但这次使用 -f 参数强制应用,并且不要选择反向应用:
root@desktop:~/work/ffmpeg$ patch -p1 -f < ffmpeg_nvmpi.patch
如果提示 nvmpi_enc.c 文件还有问题。这是因为这个文件已经存在,并且内容可能与补丁不完全匹配。 删除已经存在的 nvmpi 相关文件:
root@desktop:~/work/ffmpeg$ rm -f libavcodec/nvmpi_dec.c libavcodec/nvmpi_enc.c
避免补丁再次被反向应用,这次使用 --reject-file=- 选项来忽略拒绝文件,重新应用补丁:
root@desktop:~/work/ffmpeg$ git checkout configure libavcodec/Makefile libavcodec/allcodecs.c && patch -p1 --reject-file=- < ffmpeg_nvmpi.patch
重新配置和编译 FFmpeg,安装编译好的 FFmpeg:
./configure --enable-nvmpi --enable-shared && make -j4
sudo make install
最后再重新编译程序:
gcc simple_transcode.c -o h264_to_yuv $(pkg-config --libs --cflags libavcodec libavformat libavutil) -lnvmpi# 运行可执行文件
./h264_to_yuv ../code/data/video.h264 output.yuv
现在就能够通过代码直接调用硬件解码器解码 H264 文件了。