一、前言
【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需要使用的资料下载下来。
【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。

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

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

可以看到网络没有问题。
提示: 按下 Ctrl + C
可以终止命令行。 这算是Linux基础。
【6】windows远程登录桌面
为了方便图形化方式开发,可以使用windows系统通过远程桌面登录树莓派,就可以看到界面了,不过需要先安装工具。
(1)安装xdrp
在树莓派的命令行终端输入命令:
按下回车之后,会弹出确认窗口。输入 y
之后,按下回车,继续安装。

安装完毕:

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

打开远程桌面的窗口:

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

如果弹出窗口,就选择是
。

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

输入后点击OK
按钮登录。

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

【7】扩展树莓派SD卡可用空间
树莓派系统默认启动时,树莓派默认没有把整个存储空间拓展到整张卡中,如果需要使用整个SD卡,这时候可以通过人为的把存储空间拓展到整张卡上。
(1)查看内存使用情况
打开命令行终端,输入df -h
命令。

(2)扩展内存
<1> 打开树莓派命令行终端输入:
<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相关工具
打开命令行终端,依次输入以下命令。

命令运行中:

现场环境:

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)先安装需要的插件库:
(2)X264库在树莓派上配置安装的方法:
(3)配置ffmpeg:
(4)编译并安装ffmpeg:
【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;
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;
}
if (avformat_open_input(&input_ctx,m_MediaFile, NULL, NULL) != 0)
{
fprintf(stderr, "无法打开输入文件 '%s'\n",m_MediaFile);
return -1;
}
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();
out_channels = av_get_channel_layout_nb_channels(out_channel_layout);
audio_buffer_size = av_samples_get_buffer_size(nullptr, out_channels, out_nb_samples, sample_fmt, 1);
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;
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)
{
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;
if (avcodec_send_packet(input_ctx->streams[audio_stream_index]->codec,&packet) != 0)
{
av_packet_unref(&packet);
continue;
}
if (avcodec_receive_frame(input_ctx->streams[audio_stream_index]->codec, PCM_pFrame) != 0)
{
av_packet_unref(&packet);
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);
}
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;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
软解视频帧的核心代码:
用树莓派播放直播的操作太厉害了
想看看直播的视频效果