1.上位机界面展示
2.QFFmpeg类 解码相关
#ifndef QFFMPEG_H
#define QFFMPEG_H
//必须加以下内容,否则编译不能通过,为了兼容C和C99标准
#ifndef INT64_C
#define INT64_C
#define UINT64_C
#endif
//引入ffmpeg头文件
//由于我们建立的是C++的工程
//编译的时候使用的C++的编译器编译
//而FFMPEG是C的库
//表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约
//因此这里需要加上extern "C"
//否则会提示各种未定义
extern "C"
{
#include //编解码模块
#include //封装模块
#include //滤镜模块
#include //视频图像转换计算模块
#include
}
#include
#include
#include
class QFFmpeg : public QObject
{
Q_OBJECT
private:
QMutex mutex;//互斥锁
//ffmpeg需要用到的变量
AVPicture pAVPicture;//图片数据结构
//就是对容器或者媒体文件层次的抽象, 容器/文件(Container/File):即特定格式的多媒体文件,比如MP4,flv,mov等
AVFormatContext *pAVFormatContext;
//在每一路流中都会描述这路流的编码格式,对编解码器格式以及编解码器的抽象就是AVCodecContext 与 AVCodec。
AVCodecContext *pAVCodecContext;
AVCodec *pAVCodec;//编解码器
AVFrame *pAVFrame;//一般用于存储原始数据 即非压缩数据,例如对视频来说是YUV,RGB
SwsContext *pSwsContext;
AVPacket pAVPacket;//AVPacket通常包含一个压缩的Frame
QString url;
int videoWidth;
int videoHeight;
int videoStreamIndex;
public:
explicit QFFmpeg(QObject *parent = nullptr);//禁止隐式转换
~QFFmpeg();
bool Init();
void Play();
void SetUrl(QString url)
{
this->url = url;
}
QString Url() const
{
return url;
}
int VideoWidth() const
{
return videoWidth;
}
int VideoHeight() const
{
return videoStreamIndex;
}
//自定义信号 只能声明不能定义
signals:
void GetImage(const QImage &image);
public slots:
};
#endif // QFFMPEG_H
构造函数
QFFmpeg::QFFmpeg(QObject *parent) : QObject(parent)
{
videoStreamIndex = -1;
av_register_all();//注册库中所有可用的文件格式和解码器
avformat_network_init();//初始化网络流格式,使用RTSP网络流时必须先执行
pAVFormatContext = avformat_alloc_context();//申请一个AVFormatContext结构的内存,并进行简单初始化
pAVFrame = av_frame_alloc();
}
析构函数
QFFmpeg::~QFFmpeg()
{
avformat_free_context(pAVFormatContext);
av_frame_free(&pAVFrame);
sws_freeContext(pSwsContext);
}
Init()
打开视频流
获取视频流
获取视频流索引
获取视频流的分辨率大小
获取视频流解码器
打开对应解码器
bool QFFmpeg::Init()
{
//1.打开视频流
int result = avformat_open_input(&pAVFormatContext, url.toStdString().c_str(), nullptr, nullptr);
if(result < 0)
{
qDebug() << " 打开视频流失败 ";
return false;
}
//2.获取视频流
//avformat_open_input可能得不到解码所需要的必要信息,需要调用avformat_find_stream_info进一步得到流的信息
result = avformat_find_stream_info(pAVFormatContext, nullptr);
if(result < 0)
{
qDebug() << " 获取视频流信息失败 ";
return false;
}
//3.获取视频流索引
//nb_streams AVFormatContext.streams中的元素数量。
videoStreamIndex = -1;
for(uint i = 0; i < pAVFormatContext->nb_streams; i++)
{
if(pAVFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStreamIndex = i;
break;
}
}
if(videoStreamIndex == -1)
{
qDebug() << " 获取视频流索引失败 ";
return false;
}
//4.获取视频流的分辨率大小
pAVCodecContext = pAVFormatContext->streams[videoStreamIndex]->codec;
videoWidth = pAVCodecContext->width;
videoHeight = pAVCodecContext->height;
avpicture_alloc(&pAVPicture, PIX_FMT_RGB24, videoWidth, videoHeight);
//5.获取视频流解码器
//sws_getContext分配并返回SwsContext。 你需要它来执行使用sws_scale()进行缩放/转换操作。
/*srcW:源图像的宽
srcH:源图像的高
srcFormat:源图像的像素格式
dstW:目标图像的宽
dstH:目标图像的高
dstFormat:目标图像的像素格式
flags:设定图像拉伸使用的算法*/
pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);
pSwsContext = sws_getContext(videoWidth, videoHeight, PIX_FMT_YUV420P, videoWidth, videoHeight, PIX_FMT_RGB24, SWS_BICUBIC, nullptr, nullptr, nullptr);
//6.打开对应解码器
result = avcodec_open2(pAVCodecContext, pAVCodec, nullptr);
if(result < 0)
{
qDebug() << " 打开解码器失败 ";
return false;
}
qDebug() << "初始化视频流成功";
return true;
}
Play()
- av_read_frame 从流中读取数据帧暂存到AVPacket中
- avcodec_decode_video2 从AVPacket中解码数据到AVFrame中
互斥锁保护临界区,实际上是说保护临界区中被多个线程或进程共享的数据。互斥锁保证任何时刻只有一个线程
在执行其中的代码。互斥锁具有以下特点:原子性:把一个互斥锁定义为一个原子操作,这意味着操作系统保证了如果一个线程锁定了互斥锁,则没有其他线程可以在同一时间成功锁定这个互斥量。唯一性:如果一个线程锁定一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量。非繁忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起(不占用CPU资源),直到第一个线程解锁,第二个线程则被唤醒并继续执行,同时锁定这个互斥量
- sws_scale格式转换
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
片是连续的序列图像中的行。
参数struct SwsContext *c,为sws_getContext函数返回值
参数const uint8_t *const srcSlice[], const int srcStride[]定义输入图像信息
(当前处理区域的每个通道数据指针,每个通道行字节数)
stride定义下一行的起始位置。stride和width不一定相同,这是因为:
1.由于数据帧存储的对齐,有可能会向每行后面增加一些填充字节这样 stride = width + N;
2.packet色彩空间下,每个像素几个通道数据混合在一起,例如RGB24,每个像素3字节连续存放,因此下一行的位置需要跳过3*width字节。
srcSlice和srcStride的维数相同,由srcFormat值来。
csp 维数 宽width 跨度stride 高
YUV420 3 w, w/2, w/2 s, s/2, s/2 h, h/2, h/2
YUYV 1 w, w/2, w/2 2s, 0, 0 h, h, h
NV12 2 w, w/2, w/2 s, s, 0 h, h/2
RGB24 1 w, w, w 3s, 0, 0 h, 0, 0
3.参数int srcSliceY, int srcSliceH,定义在输入图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少行。
如果srcSliceY=0,srcSliceH=height,表示一次性处理完整个图像。
4.参数uint8_t *const dst[], const int dstStride[]定义输出图像信息
(输出的每个通道数据指针,每个通道行字节数)
构造具有给定宽度、高度和格式的图像,该图像使用现有的内存缓冲区数据 。
数据大小 对于1个像素的信息存储 RGB格式:R、G、B各占8位,共24位,即3byte
YUV420格式: Y占8位,U、V每4个点共有一个,共8 + 8 / 4 + 8 / 4 = 12位,即3 / 2byte
对于一张图像的数据大小 RGB格式:width * height * 3byte
YUV420格式:width * height * 3 / 2 byte 所以采取YUV420来存储图像数据比RGB格式节省了一半的空间。
void QFFmpeg::Play()
{
int frameFinished = 0;
while(true)
{
if(av_read_frame(pAVFormatContext, &pAVPacket) >=0 )//从h.264裸流取出一帧保存在pAVPacket
{
if(pAVPacket.stream_index == videoStreamIndex)
{
//格式化输出当前时间
qDebug() << "开启解码" << QDateTime::currentDateTime().toString("yyyy-MM-dd HH::mm::ss");
avcodec_decode_video2(pAVCodecContext, pAVFrame, &frameFinished, &pAVPacket);
//ffmpeg中的avcodec_decode_video2()的作用是解码一帧视频数据。
//输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
//如果没有帧可以被解压,得到的frameFinished为零,否则它是非零的。
if(frameFinished)
{
mutex.lock();
sws_scale(pSwsContext, (const uint8_t* const*)pAVFrame->data, pAVFrame->linesize, 0, videoHeight, pAVPicture.data, pAVPicture.linesize);
//发送一帧图像信号
QImage image(pAVPicture.data[0], videoWidth, videoHeight, QImage::Format_RGB888);
//构造具有给定宽度、高度和格式的图像,该图像使用现有的内存缓冲区数据
emit GetImage(image);//发送信号 一帧图像已经准备好了
mutex.unlock();
}
}
}
av_free_packet(&pAVPacket);//释放资源
}
}
3.RtspThread类
任何耗时的操作都不能放在主线程进行,一旦主线程阻塞了,那么体现出来的就是界面卡了。 而我们读取视频和解码视频是一个非常耗时的操作,因此需要另外开辟一个线程来专门做这件事。Qt里面线程的用法:则是写一个类继承QThread, 然后重载其run函数,把耗时的操作全部放入run函数。
#ifndef RTSPTHREAD_H
#define RTSPTHREAD_H
#include
#include "qffmpeg.h"
class RtspThread : public QThread
{
Q_OBJECT
private:
QFFmpeg *ffmpeg;
public:
explicit RtspThread(QObject *parent = nullptr);//禁止隐式转换
/*
任何耗时的操作都不能放在主线程进行,一旦主线程阻塞了,那么体现出来的就是界面卡了。
而我们读取视频和解码视频是一个非常耗时的操作,因此需要另外开辟一个线程来专门做这件事
*/
void run();
void setffmpeg(QFFmpeg *f)
{
ffmpeg = f;
}
signals:
public slots:
};
#endif // RTSPTHREAD_H
#include "rtspthread.h"
RtspThread::RtspThread(QObject *parent):QThread(parent)
{
}
void RtspThread::run()
{
ffmpeg->Play();//发送信号 一帧图像已经准备好了
/*av_read_frame 从流中读取数据帧暂存到AVPacket中
avcodec_decode_video2 从AVPacket中解码数据到AVFrame中
sws_scale格式转换
构造QImage
emit GetImage(image);//发送信号 一帧图像已经准备好了*/
}
4.frmmain类
#ifndef FRMMAIN_H
#define FRMMAIN_H
#include
namespace Ui {
class frmmain;
}
class frmmain : public QWidget
{
Q_OBJECT
public:
explicit frmmain(QWidget *parent = nullptr);
~frmmain();
private slots:
void SetImage(const QImage &image);
//打开
void on_btnOpen_clicked();
//截图
void on_btnGetImage_clicked();
//全选
void on_ckAll_stateChanged(int arg1);
protected:
bool eventFilter(QObject *obj, QEvent *event);
private:
Ui::frmmain *ui;
int tempWidth;
int tempHeight;
bool video1Max;
bool video2Max;
bool video3Max;
bool all;
};
#endif // FRMMAIN_H
on_btnOpen_clicked() 打开
//打开url的槽函数
void frmmain::on_btnOpen_clicked()
{
QFFmpeg *ffmpeg = new QFFmpeg(this);
connect(ffmpeg, SIGNAL(GetImage(QImage)), this, SLOT(SetImage(QImage)));
//发送信号 一帧图像已经准备好了
ffmpeg->SetUrl(ui->txtUrl->text());
if(ffmpeg->Init())
{
RtspThread *rtsp = new RtspThread(this);//创建一个新线程
rtsp->setffmpeg(ffmpeg);
rtsp->start();//开始执行线程run
}
}
on_btnGetImage_clicked() 截图
void frmmain::on_btnGetImage_clicked()
{
ui->labImage->clear();
int index = ui->cboxVideo->currentIndex();//获取通道索引
if(index == 0)
{
if(ui->labVideo1->pixmap() != nullptr)
{
ui->labImage->setPixmap(*ui->labVideo1->pixmap());
}
}
else if(index ==1)
{
if(ui->labVideo2->pixmap() != nullptr)
{
ui->labImage->setPixmap(*ui->labVideo2->pixmap());
}
}
else if(index ==2)
{
if(ui->labVideo2->pixmap() != nullptr)
{
ui->labImage->setPixmap(*ui->labVideo2->pixmap());
}
}
}
on_ckAll_stateChanged() 全选
void frmmain::on_ckAll_stateChanged(int arg1)
{
all = arg1 != 0 ? true : false;
}
SetImage()
QImage 用于图像处理
QPixmap显示图像
void frmmain::SetImage(const QImage &image)
{
if(image.height() > 0)
{
QPixmap pix = QPixmap::fromImage(image.scaled(tempWidth, tempHeight));//格式转换
ui->labVideo1->setPixmap(pix);
if(all)
{
ui->labVideo2->setPixmap(pix);
ui->labVideo3->setPixmap(pix);
}
}
}
eventFilter() 处理用户双击对应通道最大化处理
构造函数
frmmain::frmmain(QWidget *parent) :
QWidget(parent),
ui(new Ui::frmmain)
{
ui->setupUi(this);
tempWidth = 320;
tempHeight = 180;
video1Max = false;
video2Max = false;
video3Max = false;
all = false;
ui->labVideo1->installEventFilter(this);//将事件处理器安装到目标对象
ui->labVideo2->installEventFilter(this);//将事件处理器安装到目标对象
ui->labVideo3->installEventFilter(this);//将事件处理器安装到目标对象
}
//处理用户双击对应通道最大化处理
bool frmmain::eventFilter(QObject *obj, QEvent *event)
{
if(event->type() == QEvent::MouseButtonDblClick)//处理双击事件
{
if(obj == ui->labVideo1)//处理labVideo1的双击事件
{
if(video1Max)//再次双击 labVideo1 labVideo2 labVideo3 labImage 恢复正常大小
{
tempWidth = 320;
tempHeight = 180;
ui->labVideo2->setVisible(true);
ui->labVideo3->setVisible(true);
ui->labImage->setVisible(true);
}
else//首次双击 labVideo1 变大,labVideo2 labVideo3 labImage 关闭
{
tempWidth = 645;
tempHeight = 370;
ui->labVideo2->setVisible(false);
ui->labVideo3->setVisible(false);
ui->labImage->setVisible(false);
}
video1Max = !video1Max; //更新video1Max
}
else if(obj == ui->labVideo2)
{
if(video2Max)
{
tempWidth = 320;
tempHeight = 180;
ui->labVideo2->setVisible(true);
ui->labVideo3->setVisible(true);
ui->labImage->setVisible(true);
}
else
{
tempWidth = 645;
tempHeight = 370;
ui->labVideo2->setVisible(false);
ui->labVideo3->setVisible(false);
ui->labImage->setVisible(false);
}
video2Max = !video2Max;
}
else if(obj == ui->labVideo3)
{
if(video3Max)
{
tempWidth = 320;
tempHeight = 180;
ui->labVideo2->setVisible(true);
ui->labVideo3->setVisible(true);
ui->labImage->setVisible(true);
}
else
{
tempWidth = 645;
tempHeight = 370;
ui->labVideo2->setVisible(false);
ui->labVideo3->setVisible(false);
ui->labImage->setVisible(false);
}
video3Max = !video3Max;
}
}
return QObject::eventFilter(obj, event);
}
析构函数
frmmain::~frmmain()
{
delete ui;
}
完整工程链接