关注

全网最细万字教学!RK3588使用ffmpeg+rkmpp硬解码+DRM渲染显示MP4视频【纯代码】

1.项目介绍

板子:正点原子RK3588开发板

SDK:正点原子官方提供,正点原子RK3588开发板 — 正点原子资料下载中心 1.0.0 文档

交叉工具编译链:gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu

编程语言:c/c++

cmake: 3.22.1

os:ubuntu22.04(wsl2)

本项目是在PC上搭建交叉编译环境进行编译最后将程序部署在板子端,功能为:应用层使用ffmpeg+rkmpp插件对h.264格式的mp4进行硬解码,然后将得到的AV_PIX_FMT_DRM_PRIME格式帧使用DRM框架进行渲染显示。

题外话,市面上流行的共有三套方案:

(1)ffmpeg使用CPU进行软解,占用cpu资源高,但是稳定;

(2)ffmpeg进行解封装,然后使用瑞芯微提供的mpp框架进行硬解码;

(3)ffmpeg+rkmpp硬解码。

其实2跟3的原理是差不多,只是3把官方的mpp杂糅进了ffmpeg里面。经过实测下来方案三在稳定性上更胜一筹,并且使用流程和ffmpeg一致,基本没有再学习成本。

2.项目文件架构介绍

├───3rd :使用的第三模块,包括mpp、DRM、ffmpeg、x264、opencv、 

	├───install/ :交叉编译生成的头文件和库

	├───source/ :第三模块的源代码

├───example :参考代码,大部分是单独模块的实现和测试

	├──drm-howto/ :DRM的原子操作示例

	├──drm-test/ :DRM的测试

		├─── drm_atomic_ctrc.c :最简单的DRM 原子操作,不使用plane,使单颜色铺满屏幕

		├─── drm_atomic_plane.c:使用plane的DRM原子操作,使单颜色铺满屏幕

		├─── drm_atomic_jpg.c:使用opencv和plane显示一张jpg图片到屏幕上

		├─── drm_test.c  : 不使用原子操作,用最原始的方式(已过时)使用DRM框架显示彩色方块

	├──ffmpeg_player/ :ffmpeg+sdl2实现最简单的播放视频,想了解使用ffmpeg流程的可以查阅

├───source:项目的DRM显示和ffmpeg解码渲染源代码,也是最关键的代码

	├───drmDisplay.cpp:DRM相关的操作和对象

	├───drmDisplay.h

	├───ffmpegPlayer.cpp:ffmpeg相关的操作和对象

	├───ffmpegPlayer.h

├───toolchain:交叉编译工具链放置地点

├───main.cpp:程序的入口函数

...

3.使用说明

由于gitee对个人用户git-lfs有限制不能上传大文件,所以交叉编译工具链无法上传,不过大家的板子不一样,SDK不一样,使用的编译链也不一样,就算上传了用处也不大hhh。所以程序要运用到各自的板子上的话要重新配置交叉编译工具链,使用你们的交叉工具链重新编译对应模块,再编译链接成可执行程序。

(1) 拉取源码

git clone https://gitee.com/freecry039/ffmpeg_rkmpp_drm.git
cd ./ffmpeg_rkmpp_drm

(2) 更新子模块,获取第三方官方库

git submodule update --init --recursive

(3) 重新编译第三方库:

​ 参考第四章节的内容

(4) 创建build目录编译生成对应的执行文件

mkdir ./build
cd build
cmake ..
make -j36

(5)在项目根目录下会出现bin目录,里面会生成main.exe执行程序,/path/to/ffmpeg-rkmpp-drm/bin/main/.exe,将其使用u盘或者frp等拉取到板子上运行即可。

4.交叉编译介绍

嵌入式系统通常由具有特殊硬件架构的处理器(如ARM、MIPS等)组成,而这些处理器的架构和常见的桌面计算机(如x86、x64)有所不同。为了在嵌入式设备上运行程序,我们需要将程序从PC上编译成适用于嵌入式目标平台的代码。这个过程就叫做交叉编译

其实可以简单这样理解,你必须使用你编译sdk时使用的交叉编译工具链去编译目标程序,目标程序才能正常跑在板子上。

4.1 查找对应的交叉编译工具链

厂商一般会提供对应的sdk给用户去编译烧录进板子,那么他们肯定会提供对应的交叉编译工具链的,以正点原子rk3588为例,在官方提供的说明文档中介绍了交叉编译工具链就在prebuilts文件夹下面。
SDK工程目录描述

在找到对应的交叉编译工具链之后直接复制整个交叉编译工具链到项目目录下的toolchain文件夹下面,正点原子的目录如下:

交叉编译工具链所在位置

cp -rf /path/to/toolchain /path/to/ffmpeg_rkmpp_drm/toolchian

然后声明交叉编译工具链的环境变量

export PATH=/path/to/ffmpeg_rkmpp_drm/toolchain/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH

如果声明成功的话在终端输入交叉编译的前缀然后按TAB键会出现提示
交叉编译链声明成功

然后执行会有相对于输出

 aarch64-none-linux-gnu-gcc -v

4.2 交叉编译对应模块

如果不是和本项目一样的环境,那么需要重新编译不同模块。先把项目下对应已经编译好的模块删掉,在项目根目录下执行以下命令

rm -rf ./3rd/install/mpp/* ./3rd/install/ffmpeg/* ./3rd/install/libdrm/* ./3rd/install/opencv/* ./3rd/install/x264/*

因为ffmpeg的交叉编译依赖其他模块,所以要先编译完其它四个模块最后再编译ffmpeg

4.2.1 交叉编译libdrm

进入到libdrm的源码之中

cd ./3rd/source/libdrm-2.4.123/

创建build文件夹

mkdir ./build

创建cross_file.txt文件,用于配置 meson 交叉编译 libdrm 库。这些配置文件指定了交叉编译所需的工具链、目标平台(嵌入式平台)和构建平台(开发平台)的信息。

touch ./cross_file.txt

打开cross_file.txt文件填上以下内容,如果是不同的交叉编译工具链记得把[binaries]下面的对应选项更换成你们的编译链名称,其他照抄即可

[binaries]
c = 'aarch64-none-linux-gnu-gcc'
cpp = 'aarch64-none-linux-gnu-g++'
ar = 'aarch64-none-linux-gnu-ar'
strip = 'aarch64-none-linux-gnu-strip'

[host_machine]
system = 'linux'
cpu_family = 'arm'
cpu = 'armv8'
endian = 'little'

[build_machine]
system = 'linux'
cpu_family = 'x86_64'
cpu = 'x86_64'
endian = 'little'

[project options]
install-test-programs = 'true'

进入之前创建好的build目录下使用 meson 构建系统来配置并构建一个项目

cd ./build
meson --prefix=$(pwd)/../../../install/libdrm/ --cross-file=../cross_file.txt -D exynos=enabled

–prefix 选项指定了项目构建后的安装路径。这里使用了 $(pwd),它是一个 shell 命令,表示当前工作目录的路径(即你运行命令时所在的目录),命令让它指向了ffmpeg_rkmpp_drm/3rd/install/libdrm/

–cross-file 选项告诉 Meson 使用指定的交叉编译配置文件进行构建。…/cross_file.txt 是一个相对路径,指向当前目录的上一级目录中的 cross_file.txt 文件

-D是配置是否编译对应功能的,可加可不加

执行ninja命令进行编译和安装

ninja && ninja install

命令执行完后发现已经生成对应的头文件和库

在这里插入图片描述

也可以使用file命令查看一下对应的.so文件的架构来确认是否交叉编译成功

file libdrm.so.123.0*

在这里插入图片描述

4.2.2 交叉编译mpp

本项目使用的是https://github.com/rockchip-linux/mpp仓库,在编译时使用的日志编号是aaa4c8e9(当时最新),也是项目的子模块里面链接的分支,使用git submodule update --init --recursive可以拉取。

进入到mpp的源码之中

cd ./3rd/source/mpp/

进入对应的build文件夹

cd ./build/linux/aarch64/

按照自己的环境修改make-Makefiles.bash文件,主要是下面标注的两个选项,第一个为交叉编译链的名称,第二个为交叉编译链的路径

mpp_make-Makefiles.bash

修改arm.linux.cross.cmake文件,也是修改为对应的交叉工具链,下面是运行平台,rk3588的话就写aarch64即可

arm.linux.cross.cmake

修改完毕后直接执行脚本

./make-Makefiles.bash

执行成功后会生成makefile对应的文件

在这里插入图片描述

修改cmake_install.cmake,设置make install的安装位置

在这里插入图片描述

执行编译安装命令

make -j36
make install

执行成功后发现./3rd/install/mpp已经生成对应的头文件和库

在这里插入图片描述

也可以使用file命令测试一下,在上文已经介绍过了,就不再多赘述

4.2.3交叉编译 x264

本项目使用的是https://code.videolan.org/videolan/x264.git仓库,在编译时使用的日志编号是da14df55(当时最新),也是项目的子模块里面链接的分支,使用git submodule update --init --recursive可以拉取。

进入到x264的源码之中

cd ./3rd/source/x264

进行编译配置在终端输入

./configure --host=aarch64-none-linux-gnu --prefix=$PWD/../../install/x264/ --disable-asm --disable-opencl --enable-static --enable-shared

–host 选项指定了目标平台(或主机系统)为交叉编译时的目标平台。

–prefix 选项指定了安装路径,即你编译完成后,最终安装的文件将放置的目录。

–disable-asm这个选项禁用 汇编代码(ASM)。汇编代码通常用于针对特定 CPU 架构进行优化,选择不优化。

–disable-opencl,这个选项禁用 OpenCL 支持。OpenCL 是一种用于并行计算的框架,支持在 CPU、GPU 等硬件上运行计算密集型任务。

–enable-static 选项启用 静态库(static library) 的构建,也就是生成.a。

–enable-shared 选项启用 共享库(shared library) 的构建,也就是生成.so。

修改config.mak文件,将对应的命令改为交叉编译工具链的命令

在这里插入图片描述

执行编译命令并且安装

make -j36
make install

执行成功后发现./3rd/install/x264已经生成对应的头文件和库

在这里插入图片描述

也可以使用file命令测试一下,在上文已经介绍过了,就不再多赘述

4.2.4 交叉编译opencv

本项目使用的是https://github.com/opencv/opencv.git仓库,在编译时使用的日志编号是4223495e6c(当时最新),也是项目的子模块里面链接的分支,使用git submodule update --init --recursive可以拉取。

进入到x264的源码之中

cd ./3rd/source/opencv

修改./platforms/linux/aarch64-gnu.toolchain.cmake文件,将其更改为我们的交叉编译链

set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(GCC_COMPILER_VERSION "" CACHE STRING "GCC Compiler version")
set(GNU_MACHINE "aarch64-none-linux-gnu" CACHE STRING "GNU compiler triple")
include("${CMAKE_CURRENT_LIST_DIR}/arm.toolchain.cmake")

在这里插入图片描述

创建build文件夹

mkdir build
cd build

配置cmake

cmake -DCMAKE_TOOLCHAIN_FILE=../platforms/linux/aarch64-gnu.toolchain.cmake .. \
      -DCMAKE_CXX_FLAGS="-march=armv8-a" \
      -DCMAKE_C_FLAGS="-march=armv8-a" \
      -DBUILD_SHARED_LIBS=ON \
      -DWITH_ADE=OFF \
      -DCMAKE_INSTALL_PREFIX=$PWD/../../../install/opencv/

-DCMAKE_TOOLCHAIN_FILE=…/platforms/linux/aarch64-musl.toolchain.cmake
这个选项指定了交叉编译工具链文件。该文件包含了交叉编译环境的配置,如目标平台、编译器路径等。

-DCMAKE_CXX_FLAGS=“-march=armv8-a”
这个选项指定 C++ 编译器的标志,告诉编译器生成针对 ARMv8-A 架构的代码。

-DCMAKE_C_FLAGS=“-march=armv8-a”
这个选项类似于 CMAKE_CXX_FLAGS,它指定了 C 编译器的标志,也告诉编译器生成针对 ARMv8-A 架构的代码。

-DBUILD_SHARED_LIBS=ON
启用构建共享库(动态库)。如果为 OFF,则只会构建静态库。

-DWITH_ADE=OFF
禁用 ADE(Adaptive Dynamic Execution)模块。这通常是为了禁用某些 OpenCV 模块,以减小编译的规模或避免某些特性。

-DCMAKE_INSTALL_PREFIX 指定安装目录

然后直接编译安装

make -j36
make install

执行成功后发现./3rd/install/opencv已经生成对应的头文件和库

在这里插入图片描述

也可以使用file命令测试一下,在上文已经介绍过了,就不再多赘述

4.2.5 交叉编译ffmpeg

本项目使用的是https://github.com/nyanmisaka/ffmpeg-rockchip仓库,在编译时使用的日志编号是b81c3bf(当时最新),日志说明是fixup! lavu: add RKMPP hwcontext,尽量用项目提供的版本或者更新版本,是项目的子模块里面链接的分支,使用git submodule update --init --recursive可以拉取。

因为ffmpeg要用到上面交叉编译的所有库,所以要检查各个库的pkgconfig,如果有问题要修改为正确的目录

(1)首先是libdrm的,进入到./3rd/install/libdrm/lib/pkgconfig目录下,将里面所有pc后缀的文件的第一行prefix全部改为正确安装的目录,经过检查作者的是没有问题的,不用修改,如下:

在这里插入图片描述

(2)mpp的,进入到./3rd/install/mpp/lib/pkgconfig,里面所有pc后缀的文件的第一行prefix全部改为正确安装的目录,作者这个指向了错误的地址prefix=/usr/local,将其修改为安装地址:

在这里插入图片描述

(3)x264的,进入到./3rd/install/x264/lib/pkgconfig,里面所有pc后缀的文件的第一行prefix全部改为正确安装的目录,经过检查,作者的没有问题,不需要修改;

在这里插入图片描述

修改完毕后进入到ffmpeg-rockchip的源码之中

cd ./3rd/source/ffmpeg-rockchip

执行export命令声明pkgconfig的地址,请自行替换安装目录的路径

export PKG_CONFIG_PATH=/home/rain/rk3588/ffmpeg_rkmpp_drm/3rd/install/x264/lib/pkgconfig:/home/rain/rk3588/ffmpeg_rkmpp_drm/3rd/install/mpp/lib/pkgconfig:/home/rain/rk3588/ffmpeg_rkmpp_drm/3rd/install/libdrm/lib/pkgconfig

执行配置,主要是把–cc替换成你们的交叉编译工具链

./configure --enable-version3 \
            --enable-libdrm \
            --enable-rkmpp \
            --enable-libx264 \
            --enable-nonfree \
            --enable-gpl \
            --prefix=$PWD/../../install/ffmpeg/ \
            --target-os=linux \
            --arch=arm64 \
            --cc=aarch64-none-linux-gnu-gcc \
            --enable-cross-compile \
            --disable-x86asm \
            --enable-shared \
            --disable-static

修改./3rd/source/ffmpeg-rockchip/ffbuild/config.mak文件下的STRIP,将其修改为对应交叉编译链的strip

在这里插入图片描述

执行编译命令

make -j36
make install

执行成功后发现./3rd/install/ffmpeg已经生成对应的头文件和库

在这里插入图片描述

也可以使用file命令测试一下,在上文已经介绍过了,就不再多赘述

4.3 打包动态链接到板子端

因为板子本身就有一套已经编译好的动态链接文件.so,但是经过本人调试发现对应的有些库并不支持ffmpeg的rkmpp插件硬解码,为了保证ffmpeg的rkmpp插件能够正确运行和环境的一致性,我们要把我们重新交叉编译的动态链接库替换板子上的 .so动态库

把我们的install库打包成一个压缩文件,在项目根目录执行压缩命令,会在项目根目录下生成一个压缩文ffmpegRkmpp.tar.gz

tar -zcvf ./ffmpegRkmpp.tar.gz ./3rd/install

我们使用u盘或者frp或者其他办法将压缩文件拉取到板子端,接下来的操作全部是在板子端

在板子上运行解压命令

tar -zxvf ./ffmpegRkmpp.tar.gz

先把板子上本来的.so动态链接文件全部删掉

先执行find命令查找本来对应的.so库在哪里,在这里我直接查找ffmpeg的.so的位置,就可以定位到所有的.so的库的位置了

find / -name libavcodec.so

输出如下:

在这里插入图片描述

前面两个是我自己重新编译的库,本身的库位于/usr/lib/libavcodec.so,所以可知所有的模块库都位于/usr/lib,其实这个也是常识

接下来直接参照各个模块的名字把它们全部删掉

// ffmpeg的lib库
rm -rf /usr/lib/libav*
rm -rf /usr/lib/libpostproc*
rm -rf /usr/lib/libswresample*
rm -rf /usr/lib/libswscale*

// libdrm的lib库
rm -rf /usr/lib/libdrm*

// mpp的lib库
rm -rf /usr/lib/librockchip*

// opencv的lib库
rm -rf /usr/lib/libopencv*

// x264的库
rm -rf /usr/lib/libx264*

删除完毕之后就是将我们install里面各个模块的lib内容全部移动到/usr/lib

// ffmpeg的lib库
cp -rf /3rd/install/ffmpeg/lib/libav* /usr/lib
cp -rf /3rd/install/ffmpeg/lib/libpostproc* /usr/lib
cp -rf /3rd/install/ffmpeg/lib/libswresample* /usr/lib
cp -rf /3rd/install/ffmpeg/lib/libswscale* /usr/lib

// libdrm的lib库
cp -rf /3rd/install/libdrm/lib/libdrm* /usr/lib

// mpp的lib库
cp -rf /3rd/install/mpp/lib/librockchip* /usr/lib

// opencv的lib库
cp -rf /3rd/install/opencv/lib/libopencv* /usr/lib

// x264的库
cp -rf /3rd/install/x264/lib/libx264* /usr/lib

把lib库移植成功后就可以测试一下是否所有功能正常了,3rd/install/ffmpeg/bin文件目录下的bin里面有ffmpeg执行程序,直接输入

./ffmpeg -hwaccels

如果看到这样的输出证明已经成功了

在这里插入图片描述

或者执行以下语句测试一下,-i的输入路径要设置为存在的mp4路径

./ffmpeg -hwaccel rkmpp -i ./input.mp4 -c:v h264_rkmpp output_video.mp4
-hwaccel rkmpp:

-hwaccel 是 FFmpeg 的一个选项,用来指定视频解码时使用的硬件加速方法。rkmpp 是 Rockchip 提供的硬件加速解码/编码平台,这意味着 FFmpeg 会使用 Rockchip 的硬件加速模块来解码视频流。

-i 是指定输入文件的参数。这里指定了一个名为 input.mp4 的视频文件,FFmpeg 会从这个文件读取视频内容进行处理。

-c:v 用来指定视频编码器。h264_rkmpp 表示使用 Rockchip 的硬件加速编码器(rkmpp)来进行 H.264 编码。
这意味着解码后的视频流将被压缩成 H.264 格式,并且是通过 Rockchip 的硬件加速来进行编码的。
output_video.mp4:这是输出文件的路径名称。FFmpeg 将处理后的编码视频保存为 output_video.mp4 文件。

执行成功如下:

在这里插入图片描述

5.DRM讲解

5.1DRM基本概念

这一部分有一位大佬的学习路线和讲解非常清晰,各位研究一定要细看DRM (Direct Rendering Manager)_何小龙的博客-CSDN博客

DRM是Linux目前主流的图形显示框架,相比FB架构,DRM更能适应当前日益更新的显示硬件。比如FB原生不支持多层合成,不支持VSYNC,不支持DMA-BUF,不支持异步更新,不支持fence机制等等,而这些功能DRM原生都支持。同时DRM可以统一管理GPU和Display驱动,使得软件架构更为统一,方便管理和维护。

在这里插入图片描述

主要模块
drm系统主要分为三个模块:libdrm,GEM,KMS。

libdrm
libdrm运行在用户空间,是应用程序与内核之间交互的桥梁,其功能主要是填充内核需要的结构并通过ioctl调用传入内核,内核填充后再返回给应用空间。

GEM
GEM(Graphic Execution Manager)主要负责buffer的操作。

KMS
KMS(Kernel Mode Setting)主要负责相关参数的设置(包括分辨率、刷新率、电源状态(休眠唤醒)等)和显示画面的切换(显示buffer的切换,多图层的合成方式,以及每个图层的显示位置)。

基本元素

DRM框架涉及到的元素很多,大致如下:
KMS:CRTCENCODERCONNECTORPLANEFBVBLANKproperty
GEM:DUMBPRIMEfence

在这里插入图片描述

元素说明
CRTC对显示buffer进行扫描,并产生时序信号的硬件模块,通常指Display Controller
ENCODER负责将CRTC输出的timing时序转换成外部设备所需要的信号的模块,如HDMI转换器或DSI Controller
CONNECTOR连接物理显示设备的连接器,如HDMI、DisplayPort、DSI总线,通常和Encoder驱动绑定在一起
PLANE硬件图层,有的Display硬件支持多层合成显示,但所有的Display Controller至少要有1个plane
FBFramebuffer,单个图层的显示内容,唯一一个和硬件无关的基本元素
VBLANK软件和硬件的同步机制,RGB时序中的垂直消影区,软件通常使用硬件VSYNC来实现
property任何你想设置的参数,都可以做成property,是DRM驱动中最灵活、最方便的Mode setting机制
DUMB只支持连续物理内存,基于kernel中通用CMA API实现,多用于小分辨率简单场景
PRIME连续、非连续物理内存都支持,基于DMA-BUF机制,可以实现buffer共享,多用于大内存复杂场景
fencebuffer同步机制,基于内核dma_fence机制实现,用于防止显示内容出现异步问题

其实对于应用而言, 要考虑的最重要的是PLANE、CRTC、CONNECTOR,只要正确配置这三个就能正常显示需要渲染的内容,大致应用流程如下:

framebuffer(显存中的图像数据) -> plane(图层合并、缩放等处理)-> ctrc(将显示数据并转换成encoder可以接受的硬件信号)-> encoder(显示数据转换成不同显示输出接口的硬件信号)-> connector

5.2 modetset调试DRM

这部分是为了找到能够正常使用的CONNECTOR和CRTC,并且调试DRM是否能够正常运行显示。

在前面的成功交叉编译libdrm中的bin目录下有对应的测试工具,我们要用的是其中的modetest,我们连上板子进入到对应的文件目录,如下:

在这里插入图片描述

modetest添加执行权限

chmod a+x modetest

使用的选项如下:

# 查询选项
-c      列出连接器
-e      列出编码器
-f      列出帧缓冲
-p      列出 CRTCs 和平面

# 测试选项
-P <plane_id>@<crtc_id>:<w>x<h>[+<x>+<y>][*<scale>][@<format>]   设置一个平面
-s <connector_id>[,<connector_id>][@<crtc_id>]:<mode>[-<vrefresh>][@<format>]   设置一个显示模式
-C      测试硬件光标
-v      测试垂直同步页面翻转
-w <obj_id>:<prop_name>:<value>   设置属性

# 通用选项
-a      启用原子模式设置
-d      在模式设置后放弃主控权限
-M <module>      指定要使用的驱动程序模块
-D <device>      指定要使用的设

我们直接使用这个命令会列出非常多的参数信息,所以要筛选检查一下使用以下命令获取各组件的id

./modetest -M rockchip | cut -f1 | grep -E ^[0-9A-Z]\|id

输出效果如下:

在这里插入图片描述

可以查阅到Connectors对应的id,然后把modetest里面的信息做一下筛选,按照上面查阅到的Connectors Id填进grep的参数里面

./modetest -M rockchip | grep -E "208|224|238|240|248"

在这里插入图片描述

因为作者板子只连接了DSI-1,所以能用的Connectors id为238,也就是第三个Connectors。

接下来选择一下CRTC,我们按照前面的步骤把CRTC的id填进grep里面做一下筛选

./modetest -M rockchip | grep -E "71|93|115|137"

在这里插入图片描述

可以通过信息看到237解码器对应的CRTC为137,也只有137后面有对应的信息,所以我们要选择的CRTC ID为137,也就是第四个。

找到对应的Connector和Crtc之后就可以用代码来调试一下板子的drm是否能够正常使用,238为Connector id,137为CRTC id,后面为支持的高度和宽度,输入命令之后在DSI屏幕上将会看到闪烁的彩色块。

modetest -M rockchip -s 238@137:1080x1920 -v

在这里插入图片描述

5.3 DRM初始化代码讲解

代码部分为drmDisplay.cpp里面的**int C_DRMDisplay::InitDRMParam()**函数

打开 DRM 设备文件,pDevPath 是设备文件路径(如 /dev/dri/card0),O_RDWR 表示以读写模式打开,O_CLOEXEC 确保文件描述符在执行 exec 系统调用时会自动关闭,如果打开失败,打印错误信息并结束。

iDevfd = open(pDevPath, O_RDWR | O_CLOEXEC);
if (iDevfd < 0) {
    std::cerr << "Faild to open DRM device" << std::endl;
}

调用 drmModeGetResources 获取 DRM 设备的资源信息,包括连接器(connectors)、CRTC(Cathode Ray Tube Controller)、编码器和帧缓冲区等。

pModeRes = drmModeGetResources(iDevfd);

从设备资源信息中选择一个连接器(connector),这里选择的是 connectors[2],即第三个连接器(在5.2已经讲解)。通过 drmModeGetConnector 获取连接器的详细信息。

iConnId = pModeRes->connectors[2];
pModeConn = drmModeGetConnector(iDevfd, iConnId);

将 CRTC ID 设置为设备资源中第四个 CRTC(在5.2已经讲解)。

    iCrtcId = pModeRes->crtcs[3];

设置 DRM 客户端能力 DRM_CLIENT_CAP_UNIVERSAL_PLANES,启用通用平面支持,允许操作图层资源。

  drmSetClientCap(iDevfd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);

调用 drmModeGetPlaneResources 获取所有平面资源(drm_plane),存储在 pModePlaneRes。如果获取失败,打印错误信息并返回错误码。取第一个平面的 ID 存储在 iPlaneId 中。

    pModePlaneRes = drmModeGetPlaneResources(iDevfd);
    if (!pModePlaneRes) {
        fprintf(stderr, "drmModeGetPlaneResources failed: %s\n", strerror(errno));
        return -ENOENT;
    }
    iPlaneId = pModePlaneRes->planes[0];

设置 DRM 客户端能力 DRM_CLIENT_CAP_ATOMIC,启用原子模式操作。

    drmSetClientCap(iDevfd, DRM_CLIENT_CAP_ATOMIC, 1);

获取连接器对象(DRM_MODE_OBJECT_CONNECTOR)的属性。使用 GetPropertyId 函数提取名为 CRTC_ID 的属性 ID。释放属性对象 pProps,避免内存泄漏。

    pProps = drmModeObjectGetProperties(iDevfd, iConnId, DRM_MODE_OBJECT_CONNECTOR);
    iPropertyCrtcId = GetPropertyId(iDevfd, pProps, "CRTC_ID");
    drmModeFreeObjectProperties(pProps);

获取指定 CRTC 对象的属性。提取 ACTIVEMODE_ID 属性的 ID,分别用于设置 CRTC 的激活状态和显示模式。释放属性对象。

    pProps = drmModeObjectGetProperties(iDevfd, iCrtcId, DRM_MODE_OBJECT_CRTC);
    iPropertyActive = GetPropertyId(iDevfd, pProps, "ACTIVE");
    iPropertyModeId = GetPropertyId(iDevfd, pProps, "MODE_ID");
    drmModeFreeObjectProperties(pProps);

为连接器的模式(pModeConn->modes[0])创建属性 Blob,将其标识符存储在 iBlobId。Blob 是一种高效管理属性值的方式。

    drmModeCreatePropertyBlob(iDevfd, &pModeConn->modes[0], sizeof(pModeConn->modes[0]), &iBlobId);

分配原子请求对象 pAtomicReq,用于原子更新。如果分配失败,则打印错误并返回。

    pAtomicReq = drmModeAtomicAlloc();
    if (!pAtomicReq) {
        perror("Failed to allocate atomic request");
        return -1;
    }

通过 drmModeAtomicAddProperty 添加属性到原子请求中:

(1)设置 CRTC 的 ACTIVE 属性为 1(激活)。

(2)设置 CRTC 的 MODE_ID 为模式的 Blob ID。

(3)将连接器的 CRTC_ID 设置为当前 CRTC。

    drmModeAtomicAddProperty(pAtomicReq, iCrtcId, iPropertyActive, 1);
    drmModeAtomicAddProperty(pAtomicReq, iCrtcId, iPropertyModeId, iBlobId);
    drmModeAtomicAddProperty(pAtomicReq, iConnId, iPropertyCrtcId, iCrtcId);

调用 drmModeAtomicCommit 提交原子请求,应用设置。如果成功,将打印信息。

    drmModeAtomicCommit(iDevfd, pAtomicReq, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
    std::cout << "drmModeAtomicCommit SetCrtc" << std::endl;

获取平面的属性。提取平面相关的各种属性 ID,例如帧缓冲区 ID (FB_ID)、CRTC 坐标(CRTC_XCRTC_Y)以及源图像的坐标和尺寸(SRC_XSRC_W 等)。这部分是给更新屏幕数据用的,只要设置好这部分的参数,再原子提交就可以将画面显示到屏幕上。

    pProps = drmModeObjectGetProperties(iDevfd, iPlaneId, DRM_MODE_OBJECT_PLANE);
    iPropertyCrtcId = GetPropertyId(iDevfd, pProps, "CRTC_ID");
    iPropertyfbId = GetPropertyId(iDevfd, pProps, "FB_ID");
    iPropertyCrtcX = GetPropertyId(iDevfd, pProps, "CRTC_X");
    iPropertyCrtcY = GetPropertyId(iDevfd, pProps, "CRTC_Y");
    iPropertyCrtcW = GetPropertyId(iDevfd, pProps, "CRTC_W");
    iPropertyCrtcH = GetPropertyId(iDevfd, pProps, "CRTC_H");
    iPropertySrcX = GetPropertyId(iDevfd, pProps, "SRC_X");
    iPropertySrcY = GetPropertyId(iDevfd, pProps, "SRC_Y");
    iPropertySrcW = GetPropertyId(iDevfd, pProps, "SRC_W");
    iPropertySrcH = GetPropertyId(iDevfd, pProps, "SRC_H");
    drmModeFreeObjectProperties(pProps);

自此drm的初始化已经完成。

6.ffmpeg讲解

这部分代码严格参考了ffmpeg的官方示例代码,位于ffmpeg源码的/doc/examples/hw_decode.c

6.1 ffmpeg初始化

FFmpeg 初始化函数:InitFfmpeg

int C_FfmpegPlayer::InitFfmpeg()

功能:

初始化 FFmpeg,设置输入文件、选择视频流、设置解码器及硬件解码器。


检查硬件设备类型

type = av_hwdevice_find_type_by_name(pHWDevice);
if (type == AV_HWDEVICE_TYPE_NONE) {
	fprintf(stderr, "Device type %s is not supported.\n", pHWDevice);
	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;
}

作用:

  • 根据名称 pHWDevice 查找对应的硬件设备类型。
  • 如果设备类型不支持,打印可用设备类型并返回错误。
  • 使用 av_hwdevice_iterate_types 遍历所有支持的硬件设备类型。

分配 AVPacket

pPacket = av_packet_alloc();
if (!pPacket) {
	fprintf(stderr, "Failed to allocate AVPacket\n");
	return -1;
}

作用:

  • 调用 av_packet_alloc 分配一个 AVPacket 对象,用于存储解码前的数据包(Packet)。
  • 如果分配失败,打印错误信息并返回。

打开输入文件

if (avformat_open_input(&pFmtCtx, pMediaPath, NULL, NULL) != 0) {
	fprintf(stderr, "Cannot open input file '%s'\n", pMediaPath);
	return -1;
}

作用:

  • 调用 avformat_open_input 打开输入媒体文件,返回一个 AVFormatContext 对象(pFmtCtx)。
  • 如果文件无法打开,打印错误信息并返回。

获取流信息

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

作用:

  • 调用 avformat_find_stream_info 分析输入文件中的流信息(如视频、音频流)。
  • 如果失败,打印错误并返回。

输出文件格式信息

av_dump_format(pFmtCtx, 0, pMediaPath, 0);

作用:

  • 调用 av_dump_format 打印输入文件的详细信息(例如流类型、分辨率、编码等)。
  • 有助于调试。

查找视频流

ret = av_find_best_stream(pFmtCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (ret < 0) {
	fprintf(stderr, "Cannot find a video stream in the input file\n");
	return -1;
}
iVideoStream = ret;

作用:

  • 使用 av_find_best_stream 查找输入文件中最佳的视频流。
  • 如果找不到视频流,打印错误信息并返回。

选择解码器

pCodec = avcodec_find_decoder_by_name("h264_rkmpp");
if (pCodec == NULL) {
	std::cout << "Cann't find codec!" << std::endl;
	return -1;
}

作用:

  • 调用 avcodec_find_decoder_by_name 查找指定解码器(此处是硬件加速解码器 "h264_rkmpp")。
  • 如果未找到指定解码器,打印错误并返回。

设置像素格式

stHWPixFmt = AV_PIX_FMT_DRM_PRIME;
std::cout << "hw_pix_fmt: " << stHWPixFmt << std::endl;

作用:

  • 设置硬件解码输出的像素格式,此处为 AV_PIX_FMT_DRM_PRIME,表示 DRM Prime 格式。

分配解码器上下文

if (!(pCodecCtx = avcodec_alloc_context3(pCodec))) {
	printf("avcodec_alloc_context3() failed %d\n", AVERROR(ENOMEM));
	return AVERROR(ENOMEM);
}

作用:

  • 调用 avcodec_alloc_context3 为解码器分配上下文(pCodecCtx)。
  • 如果分配失败,返回内存错误代码。

设置解码器参数

pStream = pFmtCtx->streams[iVideoStream];
iFrameRate = pStream->avg_frame_rate.num / pStream->avg_frame_rate.den;
if (ret = avcodec_parameters_to_context(pCodecCtx, pStream->codecpar) < 0) {
	printf("avcodec_parameters_to_context() failed %d\n", ret);
	return -1;
}

作用:

  • 获取视频流的平均帧率,并设置解码器上下文的参数(例如编解码器类型、比特率等)。
  • 使用 avcodec_parameters_to_context 将流参数复制到解码器上下文。

设置硬件像素格式和初始化硬件解码器

s_stHWPixFmt = stHWPixFmt;
pCodecCtx->get_format = GetHWFormat;

if (InitHWDecoder(pCodecCtx, type) < 0) {
	printf("InitHWDecoder failed \n");
	return -1;
}c

作用:

  • 指定解码器上下文的像素格式获取函数 GetHWFormat
  • 调用 InitHWDecoder 初始化硬件解码器。

打开解码器

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

作用:

  • 调用 avcodec_open2 打开解码器,完成解码器的初始化。

7.播放视频

最后就是常规的解码渲染流程,因为功能只是播放视频并没有搞音频,所以就简单的使用sleep函数进行帧数的调控播放,和常规的音视频播放不同的是是要注意下面这段代码,位于ffmpegPlayer.cpp的DecodeWrite函数中。

// 渲染drm的frame到drm的framebuff中
		if (pFrame->format == HwPixFmt) {
			int DmaBufFd = GetDmaBufFd(pFrame);
			uint32_t Format = GetDRMFrameFormat(pFrame);
			// 初始化 DRM fb
			if (!cDRMDisplay.CheckFramebuffExist(DmaBufFd)) {
				if (cDRMDisplay.InitDRMFb(pFrame, Format))
					goto fail;
			}

			if (DmaBufFd < 0) {
				fprintf(stderr, "Failed to get dma-buf fd\n");
				ret = -1;
				goto fail;
			}
			usleep(frequency * 1000);
			cDRMDisplay.RenderFrameWithDRM(DmaBufFd, pFrame->width, pFrame->height);
		}

因为是调用了硬件解码的接口获取的frame,所以返回的数据是独特的AV_PIX_FMT_DRM_PRIME frames。每一个返回来使用的DRM PRIME fd 都不一致,对应的GEM handles也不一样,所以每一个不同的DRM PRIME fd 都要使用drmModeAddFB2去重新分配一个framebuff,这样才能让每一帧的内容正确显示,一个DRM PRIME fd对应一个framebuff。在作者播放的过程中发现基本是有4个不同的DRM PRIME fd。

所以每次获取frame后都会有判断是否是新的DRM PRIME fd操作,如果是新的就重新分配一个framebuff给它。

之后就将frame的数据直接使用plane的原子操作commit显示在屏幕上,对应的代码位于drmDisplay.cpp的RenderFrameWithDRM中:

int C_DRMDisplay::RenderFrameWithDRM(int iDrmPrimefd, int Width, int Height)
{
	int ret;
	uint32_t handles[4] = {0};

	// 将新的 dma-buf fd 转换为 handle
	ret = drmPrimeFDToHandle(iDevfd, iDrmPrimefd, &handles[0]);
	if (ret) {
		perror("drmPrimeFDToHandle failed");
		return -1;
	}

	ret = drmPrimeFDToHandle(iDevfd, iDrmPrimefd, &handles[1]);
	if (ret) {
		perror("drmPrimeFDToHandle failed");
		return -1;
	}
	uint32_t iFramebufferId;
	for (DRMFramebuffer element : vtDRMFramebuffers) {
        if(element.DrmPrimefd == iDrmPrimefd){
			iFramebufferId = element.FramebufferId;
		}
    }

	// 构造原子请求
	/* atomic plane update */
	// req = drmModeAtomicAlloc();
	drmModeAtomicAddProperty(pAtomicReq, iPlaneId, iPropertyCrtcId, iCrtcId);
	drmModeAtomicAddProperty(pAtomicReq, iPlaneId, iPropertyfbId, iFramebufferId);
	// 显示屏幕的切割
	drmModeAtomicAddProperty(pAtomicReq, iPlaneId, iPropertyCrtcX, 0);
	drmModeAtomicAddProperty(pAtomicReq, iPlaneId, iPropertyCrtcY, 0);
	drmModeAtomicAddProperty(pAtomicReq, iPlaneId, iPropertyCrtcW, Width);
	drmModeAtomicAddProperty(pAtomicReq, iPlaneId, iPropertyCrtcH, Height);
	// framebuff的切割
	drmModeAtomicAddProperty(pAtomicReq, iPlaneId, iPropertySrcX, 0);
	drmModeAtomicAddProperty(pAtomicReq, iPlaneId, iPropertySrcY, 0);
	drmModeAtomicAddProperty(pAtomicReq, iPlaneId, iPropertySrcW, Width << 16);
	drmModeAtomicAddProperty(pAtomicReq, iPlaneId, iPropertySrcH, Height << 16);
	ret = drmModeAtomicCommit(iDevfd, pAtomicReq, 0, NULL);

	if (ret) {
		perror("drmModeAtomicCommit failed");
		return -1;
	}

	// printf("Atomic frame update committed successfully\n");
	return 0;
}

8.演示成果

创建build目录编译生成对应的执行文件

mkdir ./build
cd ./build
cmake ..
make -j36

在项目根目录下会出现bin目录,里面会生成main.exe执行程序,/path/to/ffmpeg-rkmpp-drm/bin/main/.exe,将其使用u盘或者frp等拉取到板子上运行即可。

./main <input_video_path>

如下:

ffmpeg_rkmpp_drm演示

转载自CSDN-专业IT技术社区

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/m0_57724069/article/details/144934724

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--