在 Jetson Orin 开发套件上使用 Hardware Encoder / Decoder 构建 FFmpeg

发布于:2025-07-04 ⋅ 阅读:(18) ⋅ 点赞:(0)

 

目录

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 和 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 文件了。


网站公告

今日签到

点亮在社区的每一天
去签到