基于QT和FFmpeg开发流媒体播放器对H.264码流进行解码播放

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 <libavcodec/avcodec.h> //编解码模块
#include <libavformat/avformat.h> //封装模块
#include <libavfilter/avfilter.h> //滤镜模块
#include <libswscale/swscale.h> //视频图像转换计算模块
#include <libavutil/frame.h>
}

#include <QObject>
#include <QMutex>
#include <QImage>

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[]定义输出图像信息
(输出的每个通道数据指针,每个通道行字节数)

  • 构造QImage

构造具有给定宽度、高度和格式的图像,该图像使用现有的内存缓冲区数据 。

数据大小 对于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格式节省了一半的空间。

  • emit GetImage(image)发送信号 一帧图像已经准备好了
     
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 <QThread>
#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 <QWidget>

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

完整工程链接

  • 3
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Qt软件开发是指使用Qt框架进行软件开发的过程。Qt是一种用于开发跨平台软件的工具,提供了丰富的图形界面和功能模块,能够快速开发高质量的软件应用。 基于FFmpeg设计的媒体播放器是一种能够播放RTMP(Real-Time Messaging Protocol)和RTSP(Real-Time Streaming Protocol)媒体协议的软件应用。FFmpeg是一个开源的多媒体框架,能够处理音频和视频编解码、格式转换等多种功能。 在使用Qt进行开发时,首先需要将FFmpeg框架集成到Qt项目中,以便能够使用FFmpeg提供的功能。可以通过静态库、动态库或者源码方式引入FFmpeg。接下来,需要设计并实现媒体播放器的界面,可以使用Qt提供的控件来创建播放器的UI界面,包括播放按钮、进度条、音量控制等。 在播放器的逻辑功能上,需要使用FFmpeg解码视频和音频,并将图像渲染到界面上,同时实现控制功能,如播放、暂停、快进快退等。 对于RTMP协议,需要建立与服务端的连接,并通过RTMP协议发送请求来获取媒体,然后使用FFmpeg进行解码播放。 对于RTSP协议,需要建立与服务器的连接,并通过RTSP协议的SDP描述文件来获取媒体信息,然后使用FFmpeg进行解码播放。 除了基本的播放功能外,还可以增加一些高级功能,如全屏、截图、倍速播放等,来满足不同用户的需求。 总之,基于FFmpeg设计的媒体播放器(支持RTMP和RTSP协议)是Qt软件开发中的一个具体应用场景,通过QtFFmpeg的结合,能够快速开发出功能丰富、稳定可靠的媒体播放器软件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值