Qt + FFmpeg 视频播放器

一、 环境搭建

1. 下载

QT:window 5.7.0版本
FFmpeg: ffmpeg-20200522-38490cb-win32-dev
注意:这里下载 32位dev版本,要和编译器对应(我的mingw是32位的)

2. 加载库

在FFmpeg的解压目录里

  1. include/ 的头文件拷贝到自己的项目的工程路径下
  2. lib/ 拷贝所需要的库到自己的项目里

#这里是我所用到的库和 pro里的配置

//avcodec-58.dll
//avdevice-58.dll
//avfilter-7.dll
//avformat-58.dll
//avutil-56.dll
//potproc-55.dll
//swresample-3.dll
//swscale-5.dll

INCLUDEPATH += $$PWD/include/

LIBS += -L$$PWD/lib -lavutil-56 -lavformat-58 -lavcodec-58 -lavdevice-58 -lavfilter-7 -lpostproc-55 -lswresample-3 -lswscale-5

#到这里配置就完成了

二、实战演练

1. 功能介绍

源码链接: https://github.com/autocatfuuustudy/note/tree/master/QT/ffmpeg

  1. 音视频播放
  2. 调节音量,跳转进度
  3. 支持弹幕功能

实际效果图

4分屏视频播放
在这里插入图片描述

2. 音视频操作流程图

FFmpeg操作流程图AFFmepg.h

[AFFmpeg类] 音视频操作功能封装
操作流程:

  1. open();
    打开音视频文件
    读取音视频流
    读取音视频的解码器
    初始化音视频的缓存区
    获取音视频的时长,时基
  2. readVideo(); readAudio();
    解码音视频流
  3. getFrame(); getAudio();
    读取解码后的音视频流数据
  4. seek();
    跳转音视频流
  5. getDeviation();
    计算当前音视频帧的时间差
    ===========================================
    在open()成功之后开始两个定时器分别执行
    <1> readVideo() getFrame() [视频]
    <2> readAudio() getAudio() [音频]
    即可。
#ifndef AFFMPEG_H
#define AFFMPEG_H

#ifdef __cplusplus
extern "C"{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include "libswresample/swresample.h"
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <pthread.h>
}
#endif

#include "afilter.h"

class AFFmpeg
{
public:
    AFFmpeg();
    ~AFFmpeg();
    bool open(const char* filepath,bool isaudio = true);//打开文件
    int readVideo();                                    //读取视频
    int readAudio();                                    //读取音频
    int seek(float pos);                                //跳转
    AVFrame* getFrame(){ return pVideoFrameRGB;}        //读取图像
    int getAudio(char **buf);                           //读取音频
    int getDuration(){ return duration;}                //获取总时长
    double getCDuration(){ return cduration;}           //获取当前时长
    int getFPS() { return fps.num;}                     //获取帧率
    double getDeviation();                              //获取当前视频和音频的时差
    void setAFilterOpen() { isAFilteropen = !isAFilteropen; } //弹幕开关
private:
    //视频相关
    AVFormatContext     *pFormatContext;    //文件内容相关信息

    AVCodecContext      *pVideoCodecContext;//编码信息
    AVCodec             *pVideoCodec;       //解码器
    AVFrame             *pVideoFrame;       //一帧视频 源
    AVFrame             *pVideoFrameRGB;    //一帧视频 RGB格式
    AVRational          fps;                //帧率
    struct SwsContext   *pSwsContext;       //转换格式用的结构体
    unsigned char       *out_buffer;        //数据流初始化用的
    int                 videoindex;         //视频流索引
    AVPacket           *pVCPacket;          //当前视频解码包
    AVRational          Vrational;          //视频时间基准

    //音频相关
    AVFormatContext     *pAFormatContext;   //文件内容相关信息

    AVCodecContext      *pAudioCodecContext;//编码信息
    AVCodec             *pAudioCodec;       //解码器
    AVFrame             *pAudioFrame;       //一帧音频
    struct SwrContext   *pSwrContext;       //转换格式用的结构体
    uint8_t             *pAudioBuffer;      //音频输出数据
    int                 ABufLen;            //pAudioBuffer的长度
    int                 audioindex;         //音频流索引
    AVPacket           *pACPacket;          //当前音频解码包
    AVRational          Arational;          //音频时间基准

    //字幕相关   加弹幕不是很可靠  20条的时候CPU占用太高了 大概是方法不对吧
    AFilter             *pAFilter;          //字幕类
    AVFrame             *pFilterFrame;      //输出帧
    bool                isAFilteropen;      //弹幕开关
    int                 speed;              //弹幕移动速度

    //==================================================
    bool                isopen;             //打开标志
    bool                isstart;            //开始解码标志
    bool                isaudio;            //开启音频

    int                 duration;           //时长
    double              cduration;          //当前时长
};

#endif // AFFMPEG_H

3. 弹幕功能

原本是用FFmpeg的avfilter模块来实现的,但是太卡了 【不是说FFmpeg 这功能做的不行 是因为我没找对方法吧。功能我也保留了 封装在【AFilter类】中

=================================================================
让我们来看看另外一种方法吧

  1. 使用xml存储弹幕信息 【参考了B站的弹幕】
    PS: 无非就是一些增删查改的操作啦
  2. 使用 QPainter 绘制 【在展示窗体里实现】

实现思路:

  1. 每秒更新一次数据 读取当前一秒内的所有弹幕
    PS:用double类型去更新太不准了 而且更新太频繁还是不要吧
  2. 每帧视频更新时更新弹幕的显示坐标 达到滚动的效果
    PS: 滚动步进可以根据实际显示宽度自行计算
  3. 删除 第 n 秒前的弹幕数据 滚动完毕的数据清楚掉
    PS: n 值可以自行确定
  4. 跳转视频的时候 清空原来的数据再重新获取
#ifndef DANMUKU_H
#define DANMUKU_H

#include <QFile>
#include <QXmlStreamReader>
#include <QMultiHash>

struct DanMuData{
    double index;     //时间戳索引
    int arg[4];
//    int type;       //弹幕类型 固定位置:滚动 0:1
//    int time;       //滚动弹幕实时位置
//    int x;          //X轴
//    int y;          //Y轴
    QStringList list;
//    int color;      //颜色 rgb值
//    char text[256]; //内容
    DanMuData *next;
};

class  DanMuKu
{
public:
    DanMuKu();
    ~DanMuKu();
    void open();                            //打开文件
    void init(DanMuData **data);            //初始化结构体
    void read(int timestamp);               //读取数据
    void insert(DanMuData *data);           //插入数据
    int insertFile(DanMuData data);         //插入数据
    void update(double time,int step=1);    //更新数据
    void del(double timestamp);             //删除数据
    void clear();                           //清空数据
    void show();                            //打印数据
    DanMuData* get(){ return head->next;}   //获取数据
private:
    QFile *file;                            //弹幕文件
    DanMuData *head;                        //数据
};

#endif // DANMUKU_H

4. 展示窗体

我把QT播放音频的功能封装 和展示窗体封装再同一个文件了 【XAudio类】
流程:

  1. 开启两定时器 去解码音视频
    #一个在绘制界面
    #一个在写入音频数据
  2. 做了一个悬浮窗体来控制相关操作
  3. 这里写死音视频文件路径了,具体要漂亮的实现就自己去做吧 【在mainwindow.cpp里】

关键代码

【QT 播放音频】#####
//记得在pro文件里加上 QT += multimedia 
class XAudio
{
public:
    explicit XAudio();
    bool start();       //开启
    void play();        //播放
    void pause();       //暂停
    bool write(const char *data,int len);                       //写入数据
    int getfreebytes(){return pOutput->bytesFree();}            //获取数据剩余空间大小
    void setVolume(double volume){ pOutput->setVolume(volume);} //调节音量
    double getVolume(){ return pOutput->volume();}              //获取音量
    ~XAudio();
private:
    QAudioOutput *pOutput;  //输出
    QAudioFormat fmt;       //音频参数
    QMutex mutex;
    QIODevice *pOut;
    bool isPlay;            //启动标志
};

XAudio::XAudio()
{
    fmt.setSampleRate(44100);   //采样率
    fmt.setSampleSize(16);      //样本大小
    fmt.setSampleType(QAudioFormat::UnSignedInt);   //数据类型
    fmt.setChannelCount(2);     //通道数
    fmt.setCodec("audio/pcm");  //解码格式
    fmt.setByteOrder(QAudioFormat::LittleEndian);   //端序

    pOutput = new QAudioOutput(fmt);
    pOutput->setVolume(1);
    pOut = NULL;
    isPlay = false;
    start();
}

XAudio::~XAudio()
{
    delete pOutput;
}

bool XAudio::start()
{
    if(!isPlay)
    {
        pOut = pOutput->start();
        isPlay = true;
        return true;
    }
    return false;
}

void XAudio::play()
{
    if(!pOut && !isPlay)
    {
        isPlay = true;
        mutex.lock();
        pOutput->resume();
        mutex.unlock();
    }
}

void XAudio::pause()
{
    if(!pOut && isPlay)
    {
        isPlay = false;
        mutex.lock();
        pOutput->suspend();
        mutex.unlock();
    }
}
【 绘制 弹幕 和 视频 】
void TestWidget::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);
    QPainter painter(this);

    if(!img.isNull())
        painter.drawImage(0,0,img.scaled(size()));
    if(isDanMu)
        DrawDanMu(&painter);
}

void TestWidget::DrawDanMu(QPainter *painter)
{
    DanMuData *p = dMuKu->get();
    if(!p)
        return;

    QPen pen;
    QFont f; f.setPixelSize(22);
    painter->setFont(f);
    while(1) {
        if(!p || p->index > CDuration)
            break;
        int color = p->list.at(0).toInt(NULL,16);
        pen.setColor(QColor(color & 0xFF,((color >> 8) & 0xFF),(color >> 16)));
        painter->setPen(pen);
        if(p->arg[0] == 0) {
            painter->drawText(p->arg[2],p->arg[3],p->list.at(1));
        } else {
            //根据比例画弹幕
            int X = (_DEM_ - p->arg[1]) * width() / _DEM_;
            //int X =  p->arg[1] * width() / _DEM_;
            painter->drawText(X,p->arg[3],p->list.at(1));
        }
        p = p->next;
    }
}
【 读取音视频数据 】
void TestWidget::openvideo()
{
    if(!isOpen)
    {
        m = new AFFmpeg;
        //if(m->open("D:/Picture/avi/test.mp4"))
        if(m->open(path.toLocal8Bit()))
        {
            dMuKu->open();
            timer->start(1000/m->getFPS());
            Atimer->start(10);
            isOpen = true;
        }
        else
            qDebug() << "can't open.";
    }
}

void TestWidget::videoshow()
{
    if(isSPressed)
        return;

    int POs = m->readVideo();

    if(POs > 0)
    {
        AVFrame *f = m->getFrame();
        if(f)
        {
            img = QImage((uchar*)f->data[0],f->width,f->height,QImage::Format_RGB32);
            CDuration = m->getCDuration();
            if((int)CDuration > timestamp - 1) {
                dMuKu->read((int)CDuration);
                dMuKu->del(CDuration - _DANMU_TIME_);
                timestamp++;
            }
            dMuKu->update(CDuration);

            int minter = m->getDuration() / 60;
            int sec = m->getDuration() % 60;
            int cminter = CDuration / 60;
            int csec = (int)CDuration % 60;
            QString strtime = QString("%1:%2/%3:%4").arg(cminter).arg(csec,2,10,QLatin1Char('0')).arg(minter).arg(sec,2,10,QLatin1Char('0'));
            Timelab->setText(strtime);
            //qDebug() << m->getCDuration() << m->getDuration();
            CurTimeSlider->setValue(CDuration/m->getDuration() * 1000);
            update();
        }
    }
    else if(POs == -2)
    {
        qDebug() << "stop";
        timer->stop();
        Atimer->stop();
        isOpen = false;
        isPause = true;
        delete m;
    }
}

void TestWidget::audioplay()
{ 
    if(isSPressed)
        return;

    char *buf = NULL;
    int len = m->getAudio(&buf);
    if(len >= 0 && pAudio->getfreebytes() >= len)
    {
        //int size =
        m->readAudio();
        //qDebug() <<  len << freeByte <<  size;
        pAudio->write(buf,len);
    }
}

5. 写在最后

     本作品只是兴趣使然的产物,从零开始花费了大概一个月的时间,说做的多么牛逼那是没有的。但对于初学
 者来说当作入门资料还是可以的。如果你想要入音视频这块行业,想要更多地了解,可以去看看雷霄骅的博客。
 关于代码里有啥疑惑的可以留言【佛系回复】或者 联系本人QQ :673315140在项目过程中,遇到的问题还蛮多
 的,但是并没记录下来,已经不知从何记起了【蛋疼(ˉ▽ˉ;)...】 

猜你喜欢

转载自blog.csdn.net/qq_40714324/article/details/106911260