一、前言
【1】功能总结
选择树莓派 4B 设计一款家庭影院系统,可以播放本地视频、网络视频直播、游戏直播、娱乐直播、本地音乐、网络音乐,当做 FM 网络收音机。 软件采用 Qt 设计、播放器引擎采用 ffmpeg。 当前的硬件选择的是树莓派 4B,烧写官方系统,完成最终的开发。
本篇文章主要从树莓派开箱体验、系统烧写、远程登录、Qt 开发环境搭建、FFMPEG 相关库编译、播放器软件设计几个部分介绍。 在文章还分析了 ffmpeg 解码原理,渲染原理等等。
(1)播放器效果:播放游戏直播
(2)播放器效果:播放本地视频
(3)在线收音机:FM
【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 内存,就买了一个主板,不带其他任何配件。
树莓派是什么?Raspberry Pi(中文名为“树莓派”,简写为 RPi,或者 RasPi/RPi)是为学生计算机编程教育而设计,只有信用卡大小的卡片式电脑,其系统基于 Linux。
【2】资料下载
第一步,先将树莓派 4B 需要使用的资料下载下来。
4B 资料 链接:https://pan.baidu.com/s/1GoMgDz1tUWLxHR6-z3LqCw提取码:vfpe
【3】准备需要的配件
(1)准备一张至少 32G 的 TFT 卡,用来烧写系统。
(2)准备一个读卡器,方便插入 TFT 卡,好方便插入到电脑上拷贝系统
(3)树莓派主板一个
(4)一根网线(方便插路由器上与树莓派连接)
(5)一根 type-C 的电源线。用自己 Android 手机的数据线就行,拿手机充电器供电。
【4】准备烧写系统
(1)安装镜像烧写工具
(2)格式化 SD 卡
将 TFT 卡通过读卡器插入到电脑上,将 TFT 卡格式化。
(3)烧写系统
**接下来准备烧写的系统是这一个系统:**将系统解压出来。
然后打开刚才安装好的镜像烧写工具,在软件中选择需要安装的 img(镜像)文件,“Device”下选择 SD 的盘符,然后选择“Write”,然后就开始安装系统了,根据你的 SD 速度,安装过程有快有慢。
注意:从网盘下载下来的镜像如果没有解压就先解压,释放出 img 文件。
下面是烧写的流程:
点击YES
,开始烧写。
烧写过程中:
安装结束后会弹出完成对话框,说明安装就完成了,如果不成功,需要关闭防火墙一类的软件,重新插入 SD 进行安装。
需要注意的是,安装完,windows 系统下看到 SD 只有 74MB 了,这是正常现象,因为 linux 下的磁盘分区 win 下是看不到的。 烧录成功后 windows 系统可能会因为无法识别分区而提示格式化分区,此时**千万不要格式化!不要格式化!不要格式化!**点击取消,然后弹出内存卡,插入到树莓派上。
至此,树莓派烧写成功。
【5】启动系统
(1)树莓派供电
由于我买的树莓派开发板不带电源线,就采用 Android 手机的充电线供电。 使用 Type-C 供电时,要求电源头的参数要求,电压是 5V,电流是 3A。
我的充电器是小米的 120W 有线快充,刚好满足要求。
(2)启动树莓派(以 Type-C 供电示例)
烧写完后把 MicroSD 卡直接插入树莓派的 MicroSD 卡插槽,如果有显示器就连接显示器,有 DHMI 线机也可以连接外接的显示器,有鼠标、键盘都可以插上去,就可以进入树莓派系统了。
但是,我这块板子就一个主板,什么都没有。就拿网线将树莓派的网口与路由器连接。
上电之后,开发板的指示灯会闪烁,说明已经启动。
(3)查看开发板的 IP 地址
现在板子没屏幕,想要连接板子,只能通过 SSH 远程登录的方式,当前烧写的这个系统默认开机就启动了 SSH,所以只要知道开发板的 IP 地址就可以远程登录进去。
**如何知道树莓派板子的 IP 地址?**方法很多,最简单是直接登录路由器的后台界面查看连接进入的设备。
我使用的小米路由器,登录后台,看到了树莓派的 IP 地址。
(4)SSH 方式登录开发板
当前烧写系统的登录账号和密码如下:
打开 SSH 远程登录工具:PuTTY_0.67.0.0.exe
。
输入 IP 地址和端口号,点击 open。
然后输入账号和密码。
输入用户名 pi
按下回车,然后再输入密码 yahboom
。 注意:Linux 下为了保护隐私,输入密码是不可见的,你只需要正常输入,按下回车键确定 即可。
正常情况下,就登录成功了。
接下来看看联网情况。 ping 一下百度测试互联网是否畅通,因为接下来要在线安装软件包。
可以看到网络没有问题。
提示: 按下 Ctrl + C
可以终止命令行。 这算是 Linux 基础。
【6】windows 远程登录桌面
为了方便图形化方式开发,可以使用 windows 系统通过远程桌面登录树莓派,就可以看到界面了,不过需要先安装工具。
(1)安装 xdrp
在树莓派的命令行终端输入命令:
sudo apt-get install xrdp
复制代码
按下回车之后,会弹出确认窗口。输入 y
之后,按下回车,继续安装。
安装完毕:
(2)打开 windows 远程桌面
在 windows 电脑上打开运行命令的窗口,输入mstsc
来打开远程桌面。
打开远程桌面的窗口:
(3)连接树莓派远程桌面
打开远程桌面后,输入树莓派开发板的 IP 地址,点击连接。
如果弹出窗口,就选择是
。
接下来就进入到树莓派开发板的远程桌面的登录窗口了。
接下来输入面账号和密码。
输入后点击OK
按钮登录。
正常情况下,就顺利的进入树莓派的桌面了。接下来就可以进行远程桌面开发了。
【7】扩展树莓派 SD 卡可用空间
树莓派系统默认启动时,树莓派默认没有把整个存储空间拓展到整张卡中,如果需要使用整个 SD 卡,这时候可以通过人为的把存储空间拓展到整张卡上。
(1)查看内存使用情况
打开命令行终端,输入df -h
命令。
(2)扩展内存
<1> 打开树莓派命令行终端输入:
pi@raspberrypi:~ $ sudo raspi-config
复制代码
<2> 在弹出的命令行里选择Advanced Options
<3> 选择第一个选项。
<4> 点击确定
<5> 点击右边的Finish
按钮保存退出。
确定之后,关闭界面,系统会自动重启,重启之后,使用 df 命令查看是否扩展成功(我这里插的是 32G 的 SD 卡)。
可以看到,我的系统已经扩展成功了,目前可以内存空间是 19G。
【8】树莓派连接 WIFI
(1)配置需要连接的 WIFI
点击右上角的数据连接图标,打开 WIFI 列表,点击想要的 WIFI 进行连接。
输入密码:
连接成功后的效果:
(2)通过 WIFI 的 IP 地址登录远程桌面
在路由器的后台可以看到,目前树莓派连入了两个 IP 地址。接下来把网线拔掉,使用 WIFI 无线也可以直接连接无线桌面,这样就不用插网线了。
三、部署 Qt 开发环境
【1】安装 Qt 相关工具
打开命令行终端,依次输入以下命令。
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
复制代码
命令运行中:
现场环境:
Qt 软件安装成功之后,在树莓派界面的左上角的编程菜单中可以看到 Qt 的软件图标:
点击一下Qt Creator
图标就可以启动 Qt 软件。
打开选项
页面,查看默认的编译器套件是否配置 OK。
【2】新建 Qt 工程测试环境
环境搭建好之后,打开 Qt 软件新建一个工程,测试环境是否 OK。
(1)新建工程
(2)编译代码运行
(3)运行成功
【3】拷贝代码到树莓派
当前已经有代码在 windows 下开发好,可以通过 U 盘方式直接拷贝到树莓派上运行。
(1)将代码拷贝到 U 盘里。
(2)将 U 盘插到树莓派上
树莓派识别到 U 盘:
(3)运行代码
打开现有的工程。
播放器运行效果:
【4】准备 FFMPEG 相关文件
先下载以下的文件。如果不做视频播放器,SDL 可以不用。
FFmpeg 官网下载地址: http://www.ffmpeg.org/download.html
X264 下载地址: https://www.videolan.org/developers/x264.html
【5】开始编译 FFMPEG 相关库
按下键盘组合键: Ctrl+alt+t 进入到系统终端。
进入 Downloads 目录下。
解压命令: 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 解码代码
播放器项目下载地址:
https://download.csdn.net/download/xiaolong1126626497/87153858
https://download.csdn.net/download/xiaolong1126626497/87451034
https://download.csdn.net/download/xiaolong1126626497/87451115
https://download.csdn.net/download/xiaolong1126626497/87451067
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;
}
复制代码
评论