#创作者激励# 基于树莓派设计的音视频播放器(从0开始) 原创 精华

DS小龙哥
发布于 2023-3-17 09:16
浏览
2收藏

一、前言

【1】功能总结

选择树莓派4B设计一款家庭影院系统,可以播放本地视频、网络视频直播、游戏直播、娱乐直播、本地音乐、网络音乐,当做FM网络收音机。 软件采用Qt设计、播放器引擎采用ffmpeg。 当前的硬件选择的是树莓派4B,烧写官方系统,完成最终的开发。

本篇文章主要从树莓派开箱体验、系统烧写、远程登录、Qt开发环境搭建、FFMPEG相关库编译、播放器软件设计几个部分介绍。 在文章还分析了ffmpeg解码原理,渲染原理等等。

(1)播放器效果:播放游戏直播

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(2)播放器效果:播放本地视频

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(3)在线收音机:FM

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

【2】项目背景

随着经济的发展,近几年,家庭影院已开始慢慢的进入寻常百姓家里,伴随着科技的不断进步引领着智能家居的发展,家庭影院作为智能家居其中的一部分,已然逐步成为众多家庭用户的时尚新选择。

随着人们消费观念的改变,电视节目和影视资源的丰富多彩,再到现在网络技术的先进,网络资源已经可以给人以无限的可能,以前家庭影院的用户大多数是影视剧院、高端商业展示区和高端的别墅住宅用户,而影院在人们的心中更是成为了一种奢侈品,在大多数人眼里家庭影院的安装位置必须是单独的影音室,这给用户形成了一个没有适合的住房面积约等于不能在家庭里搭建影院的误区。但现在不一样了,只要有15平方以上封闭式或者开放式的空间,就算在客厅或者是卧室也可以安装家庭影院,影院不再是以往人们眼中的奢侈品,而是数码消费品。现在观看电影、电视节目非常简单,可以直接从网上下载,而且现在很多的视频网站都有高清电影专区,专门提供高品质的影视资源,这就极大地方便了人们享受足不出户,就能观看大片的乐趣。

随着科技的发展和进步,智能产品的完善和稳定,未来家庭影院已经更趋向智能化、一体化、网络化和平面化。智能化不仅让操作变得更简单而且功能更强大;一体化让设备变得便捷;网络化让资源实现更丰富多彩;平面化让效果变得更加的逼真。

树莓派它是一款基于arm的小型电脑主板,它是以SD卡做为内存硬盘,树莓派的卡片主板周围有一到四个USB接口和一个10/100 以太网接口(A型没有网口),网线,鼠标,键盘都可以连接树莓派上,同时树莓派还拥有视频模拟信号的电视输出接口和HDMI高清视频输出接口,以上的这些部件全部整合在一张仅比信用卡稍大的主板上,而且树莓派具备了所有PC的基本功能只需接通电视机和键盘,就能执行如电子表格、文字处理、玩游戏、播放高清视频和音乐等诸多功能。与我们常见的51单片机这类的嵌入式微控制器相比,不仅可以完成相同的IO引脚控制,它还能运行有相应的操作系统,

树莓派它可以完成更复杂的任务管理与调度,能够支持更上层应用的开发,为开发者们提供了更广阔的应用空间。比如说树莓派的开发语言的选择不仅仅只限于C语言,其还可以连接底层硬件与上层的应用,可以实现物联网的云控制和云管理,大家也可以忽略树莓派自带的IO控制,使用树莓派搭建小型的网络服务器,做一些小型的测试开发和服务。

与一般的PC计算机平台相比,树莓派可以提供的IO引脚,能够直接控制其他底层硬件的功能,这是一般PC计算机做不到的,当然,树莓派体积小,成本低。基于树莓派的以上优点可以说它是建造家庭影院的首选设备。

【3】 工作原理

本设计中,家庭影院-视频播放器的工作原理:本地视频/网络视频----FFMPEG解码得到原始音频帧和视频帧—QT界面绘制图像----SDL控制声卡播放声音。

视频播放基本处理流程大致包括以下几个阶段:

(1)解协议

从原始的流媒体协议数据中删除信令数据,只保留音视频数据,如采用RTMP协议传输的数据,经过解协议后输出flv格式的数据。

(2)解封装

分离音频和视频压缩编码数据,常见的封装格式mp4,mkv,rmvb,flv,avi这些格式。从而将已经压缩编码的视频、音频数据放到一起。例如FLV格式的数据经过解封装后输出H.264编码的视频码流和AAC编码的音频码流。

(3)解码

视频,音频压缩编码数据,还原成非压缩的视频,音频原始数据,音频的压缩编码标准包括AAC,MP3,AC-3等,视频压缩编码标准包含H.264,MPEG2,VC-1等经过解码得到非压缩的视频颜色数据如YUV420P,RGB和非压缩的音频数据如PCM等。

(4)音视频同步

将同步解码出来的音频和视频数据分别送至系统声卡和显卡播放。

【4】设计思路

根据本设计的设计要求,我们需要做的是将系统划分,第一区域为视频源获取,第二区域为音频视频解码转换,第三区域为图像渲染显示、音频播放。

在本次设计中,最大的难点在于软件处理音频视频数据。涉及到各种视频、音频的转码、音频视频的同步等。

底层的解码库使用FFMPEG,FFMPEG是一个集成了各种编解码器的库,可以说是一个全能型的工具,从视频采集、视频编码到视频传输(包括RTP、RTCP、RTMP、RTSP等等协议)都可以直接使用FFMPEG来完成,更重要的一点FFMPEG是跨平台的,Windows、Linux、Aandroid、IOS这些主流系统通吃。

在本次设计中,主要以树莓派为终端,解码视频进行播放,操作系统使用移植性和兼容性强的Linux操作系统,视频视频解码库采用跨平台的开源库:ffmpeg、SDL。

界面采用QT设计,可以实现本地视频播放、网络流媒体视频播放,树莓派的图像数据使用HDMI接口输出,可以接任意支持HDMI接口的显示屏进行显示,方便便捷。

二、树莓派4B环境搭建

【1】硬件环境介绍

当前购买的树莓派开发板是4B型号,2GB内存,就买了一个主板,不带其他任何配件。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

树莓派是什么?Raspberry Pi(中文名为“树莓派”,简写为RPi,或者RasPi/RPi)是为学生计算机编程教育而设计,只有信用卡大小的卡片式电脑,其系统基于Linux。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

【2】资料下载

第一步,先将树莓派4B需要使用的资料下载下来。

【3】准备需要的配件

(1)准备一张至少32G的TFT卡,用来烧写系统。

(2)准备一个读卡器,方便插入TFT卡,好方便插入到电脑上拷贝系统

(3)树莓派主板一个

(4)一根网线(方便插路由器上与树莓派连接)

(5)一根type-C的电源线。用自己Android手机的数据线就行,拿手机充电器供电。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

【4】准备烧写系统

(1)安装镜像烧写工具

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(2)格式化SD卡

将TFT卡通过读卡器插入到电脑上,将TFT卡格式化。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(3)烧写系统

**接下来准备烧写的系统是这一个系统:**将系统解压出来。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

然后打开刚才安装好的镜像烧写工具,在软件中选择需要安装的 img(镜像)文件,“Device”下选择SD的盘符,然后选择“Write”,然后就开始安装系统了,根据你的SD速度,安装过程有快有慢。

注意:从网盘下载下来的镜像如果没有解压就先解压,释放出img文件。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

下面是烧写的流程:

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

点击YES,开始烧写。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

烧写过程中:

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

安装结束后会弹出完成对话框,说明安装就完成了,如果不成功,需要关闭防火墙一类的软件,重新插入SD进行安装。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

需要注意的是,安装完,windows系统下看到SD只有74MB了,这是正常现象,因为linux下的磁盘分区win下是看不到的。 烧录成功后windows系统可能会因为无法识别分区而提示格式化分区,此时**千万不要格式化!不要格式化!不要格式化!**点击取消,然后弹出内存卡,插入到树莓派上。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

至此,树莓派烧写成功。

【5】启动系统

(1)树莓派供电

由于我买的树莓派开发板不带电源线,就采用Android手机的充电线供电。 使用Type-C供电时,要求电源头的参数要求,电压是5V,电流是3A。

我的充电器是小米的120W有线快充,刚好满足要求。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(2)启动树莓派(以Type-C供电示例)

烧写完后把MicroSD卡直接插入树莓派的MicroSD卡插槽,如果有显示器就连接显示器,有DHMI线机也可以连接外接的显示器,有鼠标、键盘都可以插上去,就可以进入树莓派系统了。

但是,我这块板子就一个主板,什么都没有。就拿网线将树莓派的网口与路由器连接。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

上电之后,开发板的指示灯会闪烁,说明已经启动。

(3)查看开发板的IP地址

现在板子没屏幕,想要连接板子,只能通过SSH远程登录的方式,当前烧写的这个系统默认开机就启动了SSH,所以只要知道开发板的IP地址就可以远程登录进去。

**如何知道树莓派板子的IP地址?**方法很多,最简单是直接登录路由器的后台界面查看连接进入的设备。

我使用的小米路由器,登录后台,看到了树莓派的IP地址。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(4)SSH方式登录开发板

打开SSH远程登录工具:PuTTY_0.67.0.0.exe

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

输入IP地址和端口号,点击open。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

正常情况下,就登录成功了。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

接下来看看联网情况。 ping一下百度测试互联网是否畅通,因为接下来要在线安装软件包。

ping www.baidu.com

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

可以看到网络没有问题。

提示: 按下 Ctrl + C 可以终止命令行。 这算是Linux基础。

【6】windows远程登录桌面

为了方便图形化方式开发,可以使用windows系统通过远程桌面登录树莓派,就可以看到界面了,不过需要先安装工具。

(1)安装xdrp

在树莓派的命令行终端输入命令:

sudo apt-get install xrdp

按下回车之后,会弹出确认窗口。输入 y之后,按下回车,继续安装。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

安装完毕:

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(2)打开windows远程桌面

在windows电脑上打开运行命令的窗口,输入mstsc来打开远程桌面。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

打开远程桌面的窗口:

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(3)连接树莓派远程桌面

打开远程桌面后,输入树莓派开发板的IP地址,点击连接。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

如果弹出窗口,就选择

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

接下来就进入到树莓派开发板的远程桌面的登录窗口了。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

输入后点击OK按钮登录。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

正常情况下,就顺利的进入树莓派的桌面了。接下来就可以进行远程桌面开发了。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

【7】扩展树莓派SD卡可用空间

树莓派系统默认启动时,树莓派默认没有把整个存储空间拓展到整张卡中,如果需要使用整个SD卡,这时候可以通过人为的把存储空间拓展到整张卡上。

(1)查看内存使用情况

打开命令行终端,输入df -h 命令。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(2)扩展内存

<1> 打开树莓派命令行终端输入:

pi@raspberrypi:~ $ sudo raspi-config

<2> 在弹出的命令行里选择Advanced Options

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

<3> 选择第一个选项。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

<4> 点击确定

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

<5> 点击右边的Finish按钮保存退出。

确定之后,关闭界面,系统会自动重启,重启之后,使用df命令查看是否扩展成功(我这里插的是32G的SD卡)。

可以看到,我的系统已经扩展成功了,目前可以内存空间是19G。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

【8】树莓派连接WIFI

(1)配置需要连接的WIFI

点击右上角的数据连接图标,打开WIFI列表,点击想要的WIFI进行连接。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

连接成功后的效果:

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(2)通过WIFI的IP地址登录远程桌面

在路由器的后台可以看到,目前树莓派连入了两个IP地址。接下来把网线拔掉,使用WIFI无线也可以直接连接无线桌面,这样就不用插网线了。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

三、部署Qt开发环境

【1】安装Qt相关工具

打开命令行终端,依次输入以下命令。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

1. pi@raspberrypi:~ $ sudo apt-get update
2. pi@raspberrypi:~ $ sudo apt-get install qt5-default
3. pi@raspberrypi:~ $ sudo apt-get install qtcreator
4. pi@raspberrypi:~ $ sudo apt-get install qtmultimedia5-dev
5. pi@raspberrypi:~ $ sudo apt-get install libqt5serialport5-dev

命令运行中:

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

现场环境:

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

Qt软件安装成功之后,在树莓派界面的左上角的编程菜单中可以看到Qt的软件图标:

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

点击一下Qt Creator图标就可以启动Qt软件。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

打开选项页面,查看默认的编译器套件是否配置OK。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

【2】新建Qt工程测试环境

环境搭建好之后,打开Qt软件新建一个工程,测试环境是否OK。

(1)新建工程

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(2)编译代码运行

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(3)运行成功

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

【3】拷贝代码到树莓派

当前已经有代码在windows下开发好,可以通过U盘方式直接拷贝到树莓派上运行。

(1)将代码拷贝到U盘里。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(2)将U盘插到树莓派上

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

树莓派识别到U盘:

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

(3)运行代码

打开现有的工程。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

播放器运行效果:

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

【4】准备FFMPEG相关文件

先下载以下的文件。如果不做视频播放器,SDL可以不用。

FFmpeg官网下载地址: http://www.ffmpeg.org/download.html

X264下载地址: https://www.videolan.org/developers/x264.html

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

【5】开始编译FFMPEG相关库

按下键盘组合键: Ctrl+alt+t 进入到系统终端。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

进入Downloads目录下。

#创作者激励# 基于树莓派设计的音视频播放器(从0开始)-鸿蒙开发者社区

解压命令: tar xvf <要解压的文件名称> 把这个4个文件都全部解压

(1)先安装需要的插件库:

sudo apt-get install libomxil-bellagio-dev

(2)X264库在树莓派上配置安装的方法:

mv /home/pi/Downloads/x264-master.tar.bz2 ./
tar xvf x264-master.tar.bz2 
cd x264-master/
./configure --prefix=$PWD/_install  --enable-shared
make && make install
sudo cp _install/include /usr/ -rf
sudo cp _install/lib /usr/ -rf

(3)配置ffmpeg:

[wbyq@wbyq ffmpeg-4.2.2]$ ./configure --enable-shared --prefix=$PWD/_install --enable-gpl --enable-libx264 --enable-omx-rpi --enable-mmal --enable-hwaccel=h264_mmal --enable-decoder=h264_mmal --enable-encoder=h264_omx --enable-omx

(4)编译并安装ffmpeg:

[wbyq@wbyq ffmpeg-4.2.2]$ make && make install

【6】ffmpeg解码代码

GPU硬解视频帧的核心代码:

int VideoDecodThread::StartPlay()
{
    is_started = false;
    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;


    /*音频相关--------------------------开始*/

    AVFrame *PCM_pFrame = nullptr;

    int audio_stream_index = -1;
    AVCodec  *audio_pCodec= nullptr;

    #define MAX_AUDIO_FRAME_SIZE  192000

    //设置音频转码后输出相关参数
    //采样的布局方式
    uint64_t out_channel_layout = AV_CH_LAYOUT_MONO;
    //采样个数
    int out_nb_samples = 1024;
    //采样格式
    enum AVSampleFormat  sample_fmt = AV_SAMPLE_FMT_S16;
    //采样率
    int out_sample_rate = 44100;
    //通道数
    int out_channels;
    int audio_buffer_size;
    uint8_t *audio_buffer;
    int64_t in_channel_layout;
    struct SwrContext *audio_convert_ctx;

    /*音频相关--------------------------结束*/

/*
Hardware acceleration methods:
cuda
dxva2
qsv
d3d11va
qsv
cuvid
*/
    //1. 根据名称查找解码器的类型
    type = av_hwdevice_find_type_by_name(m_HardwareName.data());
    if (type == AV_HWDEVICE_TYPE_NONE)
    {
        fprintf(stderr, "Device type %s is not supported.\n",m_HardwareName.data());
        fprintf(stderr, "可用设备类型:");
        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. 打开多媒体流,并且获取一些信息
    if (avformat_open_input(&input_ctx,m_MediaFile, NULL, NULL) != 0)
    {
        fprintf(stderr, "无法打开输入文件 '%s'\n",m_MediaFile);
        return -1;
    }

    //3. 读取媒体文件的数据包以获取流信息
    if (avformat_find_stream_info(input_ctx, NULL) < 0)
    {
        fprintf(stderr, "找不到输入流信息.\n");
        return -1;
    }

    LogSend(tr("媒体中流的数量: %1\n").arg(input_ctx->nb_streams));
    for(int i = 0; i < input_ctx->nb_streams; ++i)
    {
        const AVStream* stream = input_ctx->streams[i];
        if(stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            audio_stream_index = i;
            //查找解码器
            audio_pCodec=avcodec_find_decoder(stream->codecpar->codec_id);
            //打开解码器
            int err = avcodec_open2(stream->codec,audio_pCodec, nullptr);
            if(err!=0)
            {
                LogSend(tr("音频解码器打开失败.\n"));
                return 0;
            }
            else
            {
                //初始化音频解码相关的参数
                PCM_pFrame = av_frame_alloc();// 存放解码后PCM数据的缓冲区

                //创建packet,用于存储解码前音频的数据
                //packet = (AVPacket *)malloc(sizeof(AVPacket));
                //av_init_packet(packet);

                //通道数
                out_channels = av_get_channel_layout_nb_channels(out_channel_layout);
                //创建buffer
                audio_buffer_size = av_samples_get_buffer_size(nullptr, out_channels, out_nb_samples, sample_fmt, 1);
                //注意要用av_malloc
                audio_buffer = (uint8_t *)av_malloc(MAX_AUDIO_FRAME_SIZE * 2);

                in_channel_layout = av_get_default_channel_layout(input_ctx->streams[audio_stream_index]->codec->channels);
                //打开转码器
                audio_convert_ctx = swr_alloc();
                //设置转码参数
                audio_convert_ctx = swr_alloc_set_opts(audio_convert_ctx, out_channel_layout, sample_fmt, out_sample_rate, \
                                                 in_channel_layout, input_ctx->streams[audio_stream_index]->codec->sample_fmt, input_ctx->streams[audio_stream_index]->codec->sample_rate, 0, nullptr);
                //初始化转码器
                swr_init(audio_convert_ctx);
                qDebug()<<"音频流配置初始化完成...";

                //启动音频播放子线程
                audio_queue_data.clear_queue();
                m_AudioPlayThread.m_run=1;
                m_AudioPlayThread.start();
            }
            break;
        }
    }



    /* 查找视频流信息*/
    ret = av_find_best_stream(input_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0);
    if (ret < 0)
    {
        fprintf(stderr, "在输入文件中找不到视频流\n");
        return -1;
    }
    video_stream = ret;

    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;
        }
    }

    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;

    //得到视频帧的宽高
    video_width=video->codecpar->width;
    video_height=video->codecpar->height;
    LogSend(tr("视频帧的尺寸(以像素为单位): (宽X高)%1x%2 像素格式: %3\n").arg(
        video->codecpar->width).arg(video->codecpar->height).arg(video->codecpar->format));


    decoder_ctx->get_format  = get_hw_format;

    if (hw_decoder_init(decoder_ctx, type) < 0)
    {
        qDebug()<<"硬件加速-解码器初始化失败...";
        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;
    }

    //计算图像所占字节大小
    int numBytes = avpicture_get_size(AV_PIX_FMT_NV12,video_width,video_height);
    out_buffer_NV12 = (uint8_t*)av_malloc(numBytes * sizeof(uint8_t));


    qDebug()<<"format_ctx->duration:"<<input_ctx->duration;

    /* 实际解码并转储原始数据 */
    //表示视频加载成功
    while(1)
    {
        m_AudioPlayThread.m_run=m_run;
        if(m_run==0)
        {
            break;
        }
        if(m_run == 2)
        {
            msleep(100); //暂停播放
            continue;
        }

        if (is_CurrentSeekPos)
        {
            is_CurrentSeekPos = 0;
            //偏移到指定位置再开始解码    AVSEEK_FLAG_BACKWARD 向后找最近的关键帧
            av_seek_frame(input_ctx, -1, m_n64CurrentSeekPos* AV_TIME_BASE, AVSEEK_FLAG_BACKWARD);
            qDebug()<<"跳转的位置:"<<m_n64CurrentSeekPos;
        }

        if ((ret = av_read_frame(input_ctx, &packet)) < 0)
            break;


        //如果当前一包数据是视频帧
        if (video_stream == packet.stream_index)
        {
            //等待音频同步,当音频队列里的数据包小于5的时候再继续解码
            while(audio_queue_data.get_queue_cnt()>5)
            {
                msleep(10);
            }

            //计算程序运行时间方法
            QTime timedebuge;//声明一个时钟对象
            timedebuge.start();//开始计时
            ret = decode_write(decoder_ctx, &packet);
            qDebug()<<"处理一帧总耗时:"<<timedebuge.elapsed()<<"ms";//输出计时

            //当前时间
           video_clock = av_q2d(input_ctx->streams[video_stream]->time_base) * packet.pts;

           qDebug()<<"pkt.pts:"<<packet.pts<<"video_clock:"<<video_clock;
           //时间信号
           sig_getCurrentTime(video_clock, input_ctx->duration *1.0 / AV_TIME_BASE);

        }
        //如果是音频包
        else if(packet.stream_index == audio_stream_index)
        {
            if(audio_stream_index==-1)continue;

           //2. 发送帧
            if (avcodec_send_packet(input_ctx->streams[audio_stream_index]->codec,&packet) != 0)
            {
                av_packet_unref(&packet);//不成功就释放这个pkt
                continue;
            }

            //3. 解码帧
            if (avcodec_receive_frame(input_ctx->streams[audio_stream_index]->codec, PCM_pFrame) != 0)
            {
                av_packet_unref(&packet);//不成功就释放这个pkt
                continue;
            }

            //转码
            swr_convert(audio_convert_ctx, &audio_buffer, MAX_AUDIO_FRAME_SIZE, (const uint8_t **)PCM_pFrame->data, PCM_pFrame->nb_samples);

            //保存数据到队列
            struct AudioData audio_data;
            audio_data.audio_buffer=(uint8_t *)malloc(audio_buffer_size);
            audio_data.audio_buffer_size=audio_buffer_size;
            memcpy(audio_data.audio_buffer,audio_buffer,audio_buffer_size);
            audio_queue_data.write_queue(audio_data);

        }

       //释放
       av_packet_unref(&packet);
    }

    /* flush the decoder */
    packet.data = NULL;
    packet.size = 0;
    ret = decode_write(decoder_ctx, &packet);
    av_packet_unref(&packet);

    avcodec_free_context(&decoder_ctx);
    avformat_close_input(&input_ctx);
    av_buffer_unref(&hw_device_ctx);

    if(out_buffer_NV12)av_free(out_buffer_NV12);

    if(audio_stream_index!=-1)
    {
        av_free(audio_buffer);
        swr_free(&audio_convert_ctx);
        av_frame_free(&PCM_pFrame);
        m_AudioPlayThread.m_run=0;
        m_AudioPlayThread.quit();
        m_AudioPlayThread.wait();
    }

    LogSend("视频音频解码播放器的线程退出成功.\n");
    return 0;
}

软解视频帧的核心代码:

int ffmpeg_dec()
{
    ffmpeg_laliu_run_flag=true;
    int video_width=0;
    int video_height=0;

    // Allocate an AVFormatContext
    AVFormatContext* format_ctx = avformat_alloc_context();
    format_ctx->interrupt_callback.callback = interrupt_cb; //--------注册回调函数

    // 打开rtsp:打开输入流并读取标题。 编解码器未打开
    QByteArray url =m_rtmp_addr.toUtf8();// "rtmp://193.112.142.152:8888/live/abcd";

    //打印ffmpge的版本
    LogSend(QString("FFMPEG版本: %1\n").arg(av_version_info()));

    LogSend(QString("拉流地址: %1\n").arg(m_rtmp_addr));
    int ret = -1;

    LogSend(QString("正在打开输入流并读取头信息.\n"));

    ret = avformat_open_input(&format_ctx, url.data(), nullptr, nullptr);
    if(ret != 0)
    {
        LogSend(QString("无法打开网址: %1, return value: %2 \n").arg(url.data()).arg(ret));

        qDebug()<<"m_rtmp_addr:"<<m_rtmp_addr;
        qDebug()<<"url:"<<url;
        qDebug()<<"rtmp_buff:"<<url.data();
        return -1;
    }

    LogSend(QString("正在读取媒体文件的数据包以获取流信息.\n"));

    // 读取媒体文件的数据包以获取流信息
    ret = avformat_find_stream_info(format_ctx, nullptr);
    if(ret < 0)
    {
        LogSend(tr("无法获取流信息: %1\n").arg(ret));
        return -1;
    }

    AVCodec  *video_pCodec;
    AVCodec  *audio_pCodec;
    // audio/video stream index
    int video_stream_index = -1;
    int audio_stream_index = -1;

    LogSend(tr("视频中流的数量: %1\n").arg(format_ctx->nb_streams));
    for(int i = 0; i < format_ctx->nb_streams; ++i)
    {
        const AVStream* stream = format_ctx->streams[i];
        LogSend(tr("编码数据的类型: %1\n").arg(stream->codecpar->codec_id));
        if(stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            //查找解码器
            video_pCodec=avcodec_find_decoder(stream->codecpar->codec_id);
            //打开解码器
            int err = avcodec_open2(stream->codec,video_pCodec, NULL);
            if(err!=0)
            {
                  LogSend(tr("H264解码器打开失败.\n"));
                  return 0;
            }
            video_stream_index = i;
            //得到视频帧的宽高
            video_width=stream->codecpar->width;
            video_height=stream->codecpar->height;

            LogSend(tr("视频帧的尺寸(以像素为单位): (宽X高)%1x%2 像素格式: %3\n").arg(
                stream->codecpar->width).arg(stream->codecpar->height).arg(stream->codecpar->format));
        }
        else if(stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            audio_stream_index = i;
            qDebug()<<tr("音频样本格式: %1").arg(stream->codecpar->format);
            //查找解码器
            audio_pCodec=avcodec_find_decoder(stream->codecpar->codec_id);
            //打开解码器
            int err = avcodec_open2(stream->codec,audio_pCodec, nullptr);
            if(err!=0)
            {
                  LogSend(tr("AAC解码器打开失败.\n"));
                  return 0;
            }
        }
    }

    //初始化解码相关的参数
    AVFrame *yuv420p_pFrame = nullptr;
    AVFrame *PCM_pFrame = nullptr;
    AVPacket *packet;
    uint8_t *buffer;
    struct SwrContext *convert_ctx;
    int buffer_size;

    if (video_stream_index == -1)
    {
         LogSend("没有检测到视频流.\n");
         return -1;
    }
    else
    {
          yuv420p_pFrame = av_frame_alloc();// 存放解码后YUV数据的缓冲区
    }

    //申请存放yuv420p数据的空间
    yuv420p_data=new unsigned char[video_width*video_height*3/2];
    //申请存放rgb24数据的空间
    rgb24_data=new unsigned char[video_width*video_height*3];
    int y_size=video_width*video_height;

    AVPacket pkt;
    int re;

    LogSend("开始读取数据包...\n");
    while(ffmpeg_laliu_run_flag)
    {
        //读取一帧数据
        ret=av_read_frame(format_ctx, &pkt);
        if(ret < 0)
        {
            continue;
        }

        //得到视频包
        if(pkt.stream_index == video_stream_index)
        {
            //解码视频 frame
             re = avcodec_send_packet(format_ctx->streams[video_stream_index]->codec,&pkt);//发送视频帧
             if (re != 0)
             {
                 av_packet_unref(&pkt);//不成功就释放这个pkt
                 continue;
             }
             re = avcodec_receive_frame(format_ctx->streams[video_stream_index]->codec, yuv420p_pFrame);//接受后对视频帧进行解码
             if (re != 0)
             {
                 av_packet_unref(&pkt);//不成功就释放这个pkt
                 continue;
             }

            //将YUV数据拷贝到缓冲区
            memcpy(yuv420p_data,(const void *)yuv420p_pFrame->data[0],y_size);
            memcpy(yuv420p_data+y_size,(const void *)yuv420p_pFrame->data[1],y_size/4);
            memcpy(yuv420p_data+y_size+y_size/4,(const void *)yuv420p_pFrame->data[2],y_size/4);
            //将yuv420p转为RGB24格式
            YUV420P_to_RGB24(yuv420p_data,rgb24_data,video_width,video_height);
            //加载图片数据
            QImage image(rgb24_data,video_width,video_height,QImage::Format_RGB888);
            VideoDataOutput(image); //发送信号
        }
        av_packet_unref(&pkt);
    }

    avformat_close_input(&format_ctx);//释放解封装器的空间,以防空间被快速消耗完
    avformat_free_context(format_ctx);


    return 0;
}

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
分类
从0开始开发视频播放器系列代码Qt+ffmpeg.zip 3.98M 29次下载
已于2023-3-17 10:23:20修改
4
收藏 2
回复
举报
2条回复
按时间正序
/
按时间倒序
喝一大口可乐
喝一大口可乐

用树莓派播放直播的操作太厉害了

回复
2023-3-17 17:47:45
麻辣香锅配馒头
麻辣香锅配馒头

想看看直播的视频效果

回复
2023-3-20 14:25:27
回复
    相关推荐