目录
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 和 frame
packet = 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 # 可按需配置 也可用之前的配置
make
sudo 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 文件了。