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.pswp.cn/news/696784.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/696784.shtml
英文地址,請注明出處:http://en.pswp.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…

找出作弊的人

文章目錄 題目描述輸入描述輸出描述樣例1解釋:樣例2代碼 題目描述 公司組織了一次考試,現在考試結果出來了&#xff0c;想看一下有沒人存在作弊行為,但是員工太多了,需要先對員工進行一次過濾,再進一步確定是否存在作弊行為。 過濾的規則為:找到分差最小的員工ID對(p1,p2)列表…

【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 集群的構建模塊、…

流動資金貸款管理辦法

流動資金貸款管理辦法 (2024年1月30日國家金融監督管理總局令2024年第2號公布 自2024年7月1日起施行) 第一章 總 則 第一條 為規范銀行業金融機構流動資金貸款業務經營行為&#xff0c;加強流動資金貸款審慎經營管理&#xff0c;促進流動資金貸款業務健康發展&#xff0c;依…

【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…

【大模型】finetune 百川2

使用官網例子finetune百川2&#xff0c;微調腳本如下 模型為baichuan_chat_13B_v1 export CUDA_VISIBLE_DEVICES1 hostfile"" deepspeed --hostfile$hostfile baichuan_fineturn/fine-tune/fine-tune.py \--report_to "none" \--data_path "baichu…

2.22號qt

1.使用信號和槽實現多個界面跳轉 1.1準備兩個界面 1.2第一個界面準備signal 1.3第二個界面準備slot 1.4將第一個界面的信號和槽進行連接 2.qss登錄界面升級優化 2.1概念 Qss是Qt程序界面中用來設置控件的背景圖片、大小、字體顏色、字體類型、按鈕狀態變化等屬性&#xff…

【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;備受人們的喜愛。…

jax可微分編程的筆記

jax可微分編程的筆記 1.1.1 求導的基本概念 所謂的導數是一個從集合F到自身的映射. 有時,我們也將一個從函數到函數的映射稱為一個操作, 這里的操作在物理學中也被稱作一個算符. 但在計算機中的操作符相當于數學中的一個函數,而非從 函數到函數的映射. 應該指出的是,如果我們…

vue小記——this(2)

在Vue的方法中使用普通函數作為回調函數&#xff0c;那么在該回調函數中&#xff0c;this將不會指向Vue實例&#xff0c;而是指向全局對象&#xff08;在瀏覽器中是window&#xff09;。 錯誤 &#xff1a; export default { data() { return { message: Hello Vue! }; …

npm 包發布

name publish必填項&#xff08;version,nameverson構成唯一標識&#xff09;&#xff0c;唯一&#xff0c;所以publish前驗證庫里是否存在該名稱&#xff0c;方式npm info xxx npm ERR! 404 cy_test is not in the npm registry.可以使用。規則&#xff1a;不能以.或者_開頭…