linux下ffmpeg调用GPU硬件解码(VDPAU/VAAPI)保存文件

本文讲解在linux下面,如何通过ffmpeg调用GPU硬件解码,并保存解码完的yuv文件。
其实,ffmpeg自带的例子hw_decode.c这个文件,就已经能满足要求了,因此,本文就尝试讲解以下hw_decode这个例子。hw_decode.c可以调用VDPAU硬件解码,也可以调用VAAPI硬件解码,下面依次讲解如何进行操作。

下载hw_decode.c文件

我是从网上直接下载ffmpeg源码,下载地址如下:https://ffmpeg.org/releases/ffmpeg-4.2.9.tar.bz2
我这里下载的是4.2.9的源码,然后解压缩之后,在ffmpeg-4.2.9/doc/examples/hw_decode.c路径,就保存了我们需要的hw_decode.c文件。

搭建开发环境

搭建开发环境分2种,一种是直接使用系统自带的软件源里面的软件包进行开发,另外一种就是自己重新编译ffmpeg并进行开发,这两种选一种就可以了。推荐使用软件源的软件包进行开发,因为相对简单一些。下面分别讲解如何操作。

使用软件源的软件包进行开发

需要安装的依赖项如下,我这里是deb系列安装方式。

sudo apt install libvdpau-dev libva-dev ffmpeg libavcodec-dev libavformat-dev libavutil-dev

编译, cd 到ffmpeg-4.2.9/doc/examples目录,执行如下命令

gcc hw_decode.c -lavcodec -lavutil -lavformat -o hw_decode

就可以得到hw_decode这个可执行文件。

自己编译ffmpeg进行开发

自己编译ffmpeg,首先要下载ffmpeg源码,下载地址如下:https://ffmpeg.org/releases/ffmpeg-4.2.9.tar.bz2。
然后解压缩,cd ffmpeg-4.2.9,然后进行configure配置,如果你想使用VDPAU解码,那么configure命令如下

./configure --enable-shared --enable-vdpau

如果你想使用vaapi解码,那么configure命令如下

./configure --enable-shared --enable-vaapi

如果你vdpau和vaapi都想使用,那么进行如下configure。

./configure --enable-shared --enable-vdpau --enable-vaapi

然后,这里可能会遇到问题,可能就是没有安装vdpau开发包,或者没有安装vaapi开发包导致的,输入如下命令安装就可以了。

sudo apt install libvdpau-dev libva-dev 

然后再进行configure操作。
之后,再进行如下操作:

make -j8 
make examples 
sudo make install

其中,make -j8是使用8线程进行ffmpeg编译。
make examples,就是把ffmpeg所有的例子都编译,这样在ffmpeg-4.2.9/doc/exmaples目录,就会生成hw_decode这个可执行文件。
sudo make install,会将ffmpeg的动态库安装到/usr/local/lib下面,可执行文件安装到/usr/local/bin下面,头文件安装到/usr/local/include目录下面。

运行hw_decode例子

cd 到生成hw_decode的目录,如果使用vdpau解码,那么执行如下命令,你需要将第2个参数的视频路径,替换成你的视频路径。

./hw_decode vdpau ~/视频/210329_06B_Bali_1080p_013.mp4  ./out.yuv

如果使用vaapi解码,那么需要使用如下命令:

./hw_decode vaapi ~/视频/210329_06B_Bali_1080p_013.mp4  ./out.yuv

同样,需要将第2个参数替换成你的视频路径。
有的显卡,需要添加环境变量LIBVA_DRIVER_NAME。比如景嘉微JM9系列显卡,需要使用如下命令:

LIBVA_DRIVER_NAME=jmgpu ./hw_decode vaapi ~/视频/210329_06B_Bali_1080p_013.mp4  ./out.yuv

检验out.yuv结果

ffplay -pix_fmt nv12 -s 1920x1080 out.yuv

如上所示,使用ffmpeg自带的播放器ffplay,然后-pix_fmt 指定yuv格式, -s指定分辨率,然后播放。
在这里插入图片描述

hw_decode例子源码讲解

下面开始讲解代码,从main函数开始讲解。

int main(int argc, char *argv[])
{AVFormatContext *input_ctx = NULL;int video_stream, ret;AVStream *video = NULL;AVCodecContext *decoder_ctx = NULL;AVCodec *decoder = NULL;AVPacket packet;enum AVHWDeviceType type;int i;if (argc < 4) {fprintf(stderr, "Usage: %s <device type> <input file> <output file>\n", argv[0]);return -1;}

刚开始的一段,全是变量声明和定义,这些变量都是后面用的到的。然后if (argc < 4)这个判断,是用来判断使用方式的,下面的使用方式,正好是4个argc,第一个./hw_decode是程序名字,第2个参数vaapi表示使用的解码接口,第3个参数是视频路径,第4个参数是输出yuv路径。

./hw_decode vaapi ~/视频/210329_06B_Bali_1080p_013.mp4  ./out.yuv

如果argc < 4,那么提示使用方式,然后返回-1,程序结束。

    type = av_hwdevice_find_type_by_name(argv[1]);if (type == AV_HWDEVICE_TYPE_NONE) {fprintf(stderr, "Device type %s is not supported.\n", argv[1]);fprintf(stderr, "Available device types:");while((type = av_hwdevice_iterate_types(type)) != AV_HWDEVICE_TYPE_NONE)fprintf(stderr, " %s", av_hwdevice_get_type_name(type));fprintf(stderr, "\n");return -1;}

接下来,就是去寻找第2个参数对应的硬件解码类型,argv[1]就对应我们解码程序的参数"vdpau",或者"vaapi",如果找到了,就保存在变量type中,如果没找到,就通过一个while循环把支持的硬件类型列举,并打印出来,然后return -1程序退出。

    /* open the input file */if (avformat_open_input(&input_ctx, argv[2], NULL, NULL) != 0) {fprintf(stderr, "Cannot open input file '%s'\n", argv[2]);return -1;}

接下来,avformat_open_input,就是打开输入文件,在我这里,对应的就是打开“~/视频/210329_06B_Bali_1080p_013.mp4”这个文件,argv[2]就是输入视频路径,如果失败了,就返回-1,否则继续。

    if (avformat_find_stream_info(input_ctx, NULL) < 0) {fprintf(stderr, "Cannot find input stream information.\n");return -1;}

然后,查找视频文件里面的码流信息,一般就是找这个视频里面,有几个视频流,有几个音频流,如果没有找到因视频信息,就加一条错误打印,然后返回-1.

    /* find the video stream information */ret = av_find_best_stream(input_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0);if (ret < 0) {fprintf(stderr, "Cannot find a video stream in the input file\n");return -1;}video_stream = ret;

接下来,查找AVMEDIA_TYPE_VIDEO,也就是查找视频流信息,并将视频流的索引号,保存在video_stream中。

    for (i = 0;; i++) {const AVCodecHWConfig *config = avcodec_get_hw_config(decoder, i);if (!config) {fprintf(stderr, "Decoder %s does not support device type %s.\n",decoder->name, av_hwdevice_get_type_name(type));return -1;}if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&config->device_type == type) {hw_pix_fmt = config->pix_fmt;break;}}

接下来,就是通过一个循环,查找能支持的硬件格式对应的pix_fmt,比如我这里使用vaapi,那么通过AV_HWDEVICE_TYPE_VAAPI找到了pix_fmt为AV_PIX_FMT_VAAPI_VLD。
在这里插入图片描述

    if (!(decoder_ctx = avcodec_alloc_context3(decoder)))return AVERROR(ENOMEM);video = input_ctx->streams[video_stream];if (avcodec_parameters_to_context(decoder_ctx, video->codecpar) < 0)return -1;decoder_ctx->get_format  = get_hw_format;

继续,分配一个解码上下文 decoder_ctx,然后根据视频码流信息,填充decoder_ctx里面内容。
并将get_hw_format这个函数地址,给到decoder_ctx->get_format中,这样后续解码器解码时会调用这个get_fomat函数指针来对格式进行判断。

    if (hw_decoder_init(decoder_ctx, type) < 0)return -1;

初始化完了解码上下文,再初始化硬件解码器。

    if ((ret = avcodec_open2(decoder_ctx, decoder, NULL)) < 0) {fprintf(stderr, "Failed to open codec for stream #%u\n", video_stream);return -1;}

打开解码器。

    /* open the file to dump raw data */output_file = fopen(argv[3], "w+");

打开输出文件,这个argv[3],就对应我们命令行里面的out.yuv,就是打开这个文件,方便后面写入使用。

    /* actual decoding and dump the raw data */while (ret >= 0) {if ((ret = av_read_frame(input_ctx, &packet)) < 0)break;if (video_stream == packet.stream_index)ret = decode_write(decoder_ctx, &packet);av_packet_unref(&packet);}

重点戏来了,就是这个while循环,av_read_frame读取一帧数据,保存在packet中,然后判断以下这个packet的stream_index是不是video_stream,如果是视频数据,就调用decode_write,否则就什么也不做,处理完之后,调用av_packet_unref取消packet的引用。看来重点就在这个decode_write函数里面。

static int decode_write(AVCodecContext *avctx, AVPacket *packet)
{AVFrame *frame = NULL, *sw_frame = NULL;AVFrame *tmp_frame = NULL;uint8_t *buffer = NULL;int size;int ret = 0;ret = avcodec_send_packet(avctx, packet);if (ret < 0) {fprintf(stderr, "Error during decoding\n");return ret;}

decode_write拿到packet数据,调用avcodec_send_packet,将packet发送给解码器。

while (1) {if (!(frame = av_frame_alloc()) || !(sw_frame = av_frame_alloc())) {fprintf(stderr, "Can not alloc frame\n");ret = AVERROR(ENOMEM);goto fail;}ret = avcodec_receive_frame(avctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {av_frame_free(&frame);av_frame_free(&sw_frame);return 0;} else if (ret < 0) {fprintf(stderr, "Error while decoding\n");goto fail;}if (frame->format == hw_pix_fmt) {/* retrieve data from GPU to CPU */if ((ret = av_hwframe_transfer_data(sw_frame, frame, 0)) < 0) {fprintf(stderr, "Error transferring the data to system memory\n");goto fail;}tmp_frame = sw_frame;} elsetmp_frame = frame;size = av_image_get_buffer_size(tmp_frame->format, tmp_frame->width,tmp_frame->height, 1);buffer = av_malloc(size);if (!buffer) {fprintf(stderr, "Can not alloc buffer\n");ret = AVERROR(ENOMEM);goto fail;}ret = av_image_copy_to_buffer(buffer, size,(const uint8_t * const *)tmp_frame->data,(const int *)tmp_frame->linesize, tmp_frame->format,tmp_frame->width, tmp_frame->height, 1);if (ret < 0) {fprintf(stderr, "Can not copy image to buffer\n");goto fail;}if ((ret = fwrite(buffer, 1, size, output_file)) < 0) {fprintf(stderr, "Failed to dump raw data.\n");goto fail;}fail:av_frame_free(&frame);av_frame_free(&sw_frame);av_freep(&buffer);if (ret < 0)return ret;}

然后一个大的while循环,这里其实就是让解码器去解码,如果解码得到数据,就将数据从GPU显存拷贝到CPU内存,然后再写入out.yuv文件中。下面分开讲解。

    while (1) {if (!(frame = av_frame_alloc()) || !(sw_frame = av_frame_alloc())) {fprintf(stderr, "Can not alloc frame\n");ret = AVERROR(ENOMEM);goto fail;}

while的开始,分配了2个frame,第一个frame,是用来保存GPU解码完毕的数据,这个数据位于显存。第2个sw_frame是用来保存内存数据,用来将GPU显存的yuv数据拷贝到内存用的。

        ret = avcodec_receive_frame(avctx, frame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {av_frame_free(&frame);av_frame_free(&sw_frame);return 0;} else if (ret < 0) {fprintf(stderr, "Error while decoding\n");goto fail;}

avcode_receive_frame,用来接受解码器传过来的frame数据,也就是如果解码器解码完了,会得到一个解码完毕的AVFrame数据,这个数据就保存在frame中。如果返回值为EAGAIN或者AVERROR_EOF,说明之前的packet并没有解码得到一个完整的AVFrame数据,因此需要把前面分配的2个frame和sw_frame都释放掉,然后返回0,说明这一个packet处理完毕了。如果ret 是其他值 < 0,说明解码出错了,goto fail。fail标签后面再说。

        if (frame->format == hw_pix_fmt) {/* retrieve data from GPU to CPU */if ((ret = av_hwframe_transfer_data(sw_frame, frame, 0)) < 0) {fprintf(stderr, "Error transferring the data to system memory\n");goto fail;}tmp_frame = sw_frame;} elsetmp_frame = frame;

否则,我们解码得到了一帧数据,判断一下,这一帧数据的格式,如果格式正好是hw_pix_fmt,那么调用av_hwframe_transfer_data,将frame里面的GPU数据,传输到sw_frame里面,tmp_frame正好等于sw_frame。如果不是hw_pix_fmt,那么tmp_frame就是frame。这个执行完之后,tmp_frame里面保存的就是内存数据了。

        size = av_image_get_buffer_size(tmp_frame->format, tmp_frame->width,tmp_frame->height, 1);buffer = av_malloc(size);if (!buffer) {fprintf(stderr, "Can not alloc buffer\n");ret = AVERROR(ENOMEM);goto fail;}ret = av_image_copy_to_buffer(buffer, size,(const uint8_t * const *)tmp_frame->data,(const int *)tmp_frame->linesize, tmp_frame->format,tmp_frame->width, tmp_frame->height, 1);if (ret < 0) {fprintf(stderr, "Can not copy image to buffer\n");goto fail;}

接下来,判断tmp_frame的数据大小,分配一个size大小的buffer,将tmp_frame的数据,搬到buffer中。

        if ((ret = fwrite(buffer, 1, size, output_file)) < 0) {fprintf(stderr, "Failed to dump raw data.\n");goto fail;}

然后将buffer中的数据,写入到output_file中,也就是写入到out.yuv中。

    fail:av_frame_free(&frame);av_frame_free(&sw_frame);av_freep(&buffer);if (ret < 0)return ret;}
}

如果失败了,释放frame, sw_frame, buffer内容,并且如果ret <0, 返回ret。

    /* actual decoding and dump the raw data */while (ret >= 0) {if ((ret = av_read_frame(input_ctx, &packet)) < 0)break;if (video_stream == packet.stream_index)ret = decode_write(decoder_ctx, &packet);av_packet_unref(&packet);}/* flush the decoder */packet.data = NULL;packet.size = 0;ret = decode_write(decoder_ctx, &packet);av_packet_unref(&packet);if (output_file)fclose(output_file);avcodec_free_context(&decoder_ctx);avformat_close_input(&input_ctx);av_buffer_unref(&hw_device_ctx);return 0;
}

然后一直循环av_read_frame,解码写文件,直到av_read_frame < 0,也就是把整个输入文件都处理完了,这个while循环结束。
接下来,还设置了一个packet.data = NULL, 调用了一次decode_write,就是告诉解码器,我没有数据了,你里面如果还缓存一些数据,都给我输出出来吧。

最后就是关闭输出文件,释放解码器上下文,关闭输出,释放硬件设备上下文。至此, hw_decode解析完毕。

常见问题

  1. 为什么硬件解码这么慢,CPU占用率也很高?
    答: 之所以这么慢,CPU占用率高,是因为有2个操作,1个操作是需要将数据从GPU显存拷贝到CPU内存,另外1个操作是需要写文件。如果你屏蔽av_hwframe_transfer_data及之后的操作,这里对应代码107行到139行,那么速度将会特别快。
    2. 为什么运行vaapi时提示找不到vaapi device。
    答:可能原因是没有安装 vaapi驱动,或者没有指定LIBVA_DRIVER_NAME这个环境变量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/696784.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

watchpoint

前言 内存被踩&#xff0c;通过 watchpoint 找到真凶 实例 以 smsc911x 网卡驱动为基体&#xff0c;进行实验&#xff0c;和网卡本身功能无关&#xff0c; 每执行一次 ifconfig eth0 up&#xff0c;就会调用一次 smsc911x_open()&#xff0c;我在这里设计了一段代码&#xf…

数学知识(四)(容斥原理、博弈论)

一、容斥原理 容斥原理公式 一共加或者减的式子个数 &#xff08;一&#xff09;利用容斥原理解决求能被质数整除的数的个数 890计算能被整除的数的个数 因为一共有2^n-1种选法&#xff0c;可以用位运算的方式枚举&#xff0c;对于得到的每一种选法&#xff0c;根据存在的数…

六、回归与聚类算法 - 逻辑回归与二分类

线性回归欠拟合与过拟合线性回归的改进 - 岭回归分类算法&#xff1a;逻辑回归模型保存与加载无监督学习&#xff1a;K-means算法 1、应用场景 2、原理 2.1 输入 2.2 激活函数 3、损失以及优化 3.1 损失 3.2 优化 4、逻辑回归API 5、分类的评估方法 5.1 精确率和召回率 5.2…

【Spring】IoC容器 控制反转 与 DI依赖注入 配置类实现版本 第四期

文章目录 基于 配置类 方式管理 Bean一、 配置类和扫描注解二、Bean定义组件三、高级特性&#xff1a;Bean注解细节四、高级特性&#xff1a;Import扩展五、基于注解配置类方式整合三层架构组件总结 基于 配置类 方式管理 Bean Spring 完全注解配置&#xff08;Fully Annotatio…

Kotlin学习 6

1.接口 interface Movable {var maxSpeed: Intvar wheels: Intfun move(movable: Movable): String}class Car(var name: String, override var wheels: Int 4, _maxSpeed: Int) : Movable {override var maxSpeed: Int _maxSpeedget() fieldset(value) {field value}overr…

C语言读取 ini 配置文件,修改/添加键值对

C语言读取 ini 配置文件&#xff0c;修改/添加键值对 C语言读取 ini 配置文件&#xff0c;对section中的键值对进行修改/添加&#xff0c;如果section不存在&#xff0c;则在末尾将新的section/key/value 添加进去。 一、了解什么是INI文件&#xff1f; ini 文件是Initializ…

【大数据】Flink 之部署篇

Flink 之部署篇 1.概述和参考架构2.可重复的资源清理3.部署模式3.1 Application 模式3.2 Per-Job 模式&#xff08;已废弃&#xff09;3.3 Session 模式 Flink 是一个多用途框架&#xff0c;支持多种不同的混合部署方案。下面&#xff0c;我们将简要介绍 Flink 集群的构建模块、…

【html学习笔记】3.表单元素

1.文本框 1.1 语法 <input type "text">表示文本框。且只能写一行 1.2 属性 使用属性size 设置文本框大小 <input type"text" size"10">2. 使用属性value 来设置文本框的默认文字 <input type"text" size"…

Vue状态管理库-Pinia

一、Pinia是什么&#xff1f; Pinia 是 Vue 的专属状态管理库&#xff0c;它允许支持跨组件或页面共享状态&#xff0c;即共享数据&#xff0c;他的初始设计目的是设计一个支持组合式API的 Vue 状态管理库&#xff08;因为vue3一个很大的改变就是组合式API&#xff09;,当然这…

PFA三角烧瓶实验室PFA锥形瓶本底纯净耐腐蚀性强

PFA三角烧瓶外观呈平底圆锥状&#xff0c;下阔上狭&#xff0c;有一圆柱形颈部&#xff0c;上方有一较颈部阔的开口&#xff0c;可用塞子封闭。PFA三角烧瓶也称PFA锥形瓶&#xff0c;PFA反应瓶&#xff0c;PFA三角烧瓶、PFA依氏烧瓶、PFA锥形烧瓶&#xff0c;PFA鄂伦麦尔瓶等。…

普中51单片机学习(串口通信)

串口通信 原理 计算机通信是将计算机技术和通信技术的相结合&#xff0c;完成计算机与外部设备或计算机与计算机之间的信息交换 。可以分为两大类&#xff1a;并行通信与串行通信。并行通信通常是将数据字节的各位用多条数据线同时进行传送 。控制简单、传输速度快&#xff1…

【Python】Python实现串口通信(Python+Stm32)

&#x1f389;欢迎来到Python专栏~Python实现串口通信 ☆* o(≧▽≦)o *☆嗨~我是小夏与酒&#x1f379; ✨博客主页&#xff1a;小夏与酒的博客 &#x1f388;该系列文章专栏&#xff1a;Python学习专栏 文章作者技术和水平有限&#xff0c;如果文中出现错误&#xff0c;希望…

springboot208基于springboot物流管理系统

基于spring boot物流管理系统设计与实现 摘 要 社会发展日新月异&#xff0c;用计算机应用实现数据管理功能已经算是很完善的了&#xff0c;但是随着移动互联网的到来&#xff0c;处理信息不再受制于地理位置的限制&#xff0c;处理信息及时高效&#xff0c;备受人们的喜爱。…

maven工程打包引入本地jar包

1、通过maven生成本地区仓库包 mvn install:install-file --settings D:\lkx\download\apache-maven-3.6.3\conf\settings.xml -Dfileaspose-cad-21.8.jar -DartifactIdaspose-cad -DgroupIdsystem.core -Dversion21.8 -Dpackagingjar -DgeneratePomtrue # --settings&#xf…

进程线程间的通信:2024/2/22

作业1&#xff1a;代码实现线程互斥机制 代码&#xff1a; #include <myhead.h>//临界资源 int num10;//创建一个互斥锁 pthread_mutex_t mutex;//任务一 void *task1(void *arg) {//获取锁资源pthread_mutex_lock(&mutex);num123;sleep(3);printf("task1:num…

PacketSender-用于发送/接收 TCP、UDP、SSL、HTTP 的网络实用程序

PacketSender-用于发送/接收 TCP、UDP、SSL、HTTP 的网络实用程序 PacketSender是一款开源的用于发送/接收 TCP、UDP、SSL、HTTP 的网络实用程序&#xff0c;作者为dannagle。 其官网地址为&#xff1a;https://packetsender.com/&#xff0c;Github源代码地址&#xff1a;htt…

SQL Server —— While语句循环

一&#xff1a;简介 while 循环是有条件的循环控制语句。满足条件后&#xff0c;再执行循环体中的SQL语句。 while: break, 如果有多条语句可以在while后面添加begin-end。关于while的语法 while(条件) -- begin -- 语句1 -- 语句2 -- break 根据情况是否添加break -- end 二…

leetcode日记(32)字符串相乘

做了很久很久……真的太繁琐了&#xff01;&#xff01; class Solution { public:string multiply(string num1, string num2) {string s;string str;if (num1 "0" || num2 "0") return "0";for(int inum2.size()-1;i>0;i--){int c2num2[…

Qt:tabWidget控件

一、tabWidget用来做什么 tabWidget控件用来进行不同控件页面的跳转&#xff0c; 二、控件的一些函数功能 添加一个页面&#xff0c;返回index int addTab(QWidget *widget, const QString &); int addTab(QWidget *widget, const QIcon& icon, const QString &…

pytest教程-11-初识fixture

领取资料&#xff0c;咨询答疑&#xff0c;请➕wei: June__Go 上一小节我们学习了使用allure生成html测试报告的方法&#xff0c;本小节我们讲解一下pytest fixture测试夹具的使用方法。 前言 在做自动化的过程中&#xff0c;编写用例时候需要用到用例的前置和用例的后置&a…