与解码相关的主要代码在上一篇博客中已经作了介绍,本篇咱们会先讨论一下如何控制解码速度再提供一个我我的的封装思路。最后回归到界面设计环节重点看一下如何保证播放器界面在缩放和拖动的过程当中保证视频画面的宽高比例。git
1、解码速度算法
播放器播放媒体文件的时候播放进度须要咱们本身控制。基本的控制方法有两种:设计模式
媒体文件在编码的时候,正常状况下视频数据和音频输出是交替写入的。换句话说,解码每一帧视频数据伴随须要播放的音频数据也应该被解码。因此,方案一的实现就比较简单和直接。可是在有些状况下也可能会出现音视频编码不一样步的问题,大部分状况是视频提早于音频。万一遇到这样的状况,若是须要让咱们的播放器带有必定纠错功能就必须采用第二种方案。方案二的设计思路是当遇到音频数据时正常播放,遇到视频数据时先缓冲起来,再根据pts参数同步。ide
方案一函数
QTime t; QIODevice ioDevice; t.restart(); AVPacket *pkt = readPacket(); if (pkt->stream_index == videoIndex) { // 当前为视频帧,计算视频播放每帧的间隔时间(1000/fps) - 解码消耗的时间(毫秒) = 实际解码间隔时间interval codecPacket(pkt); int el = t.elapsed(); int interval = 1000 / fps - el > 0 ? 1000 / fps - el : 1; QThread::msleep(interval); } else if (pkt->stream_index == audioIndex) { // 当前为音频帧,直接让Qt的音频播放器播放 codecPacket(pkt); char data[10000] = { 0 }; int len = toPCM(data); ioDevice->write(data, len); }
方案二编码
AVPacket *pkt = readPacket(); if (pkt->stream_index == audioIndex) { codecPacket(pkt); char data[AUDIO_IODEVICE_WRITE_SIZE] = { 0 }; int len = toPCM(data); ioDevice->write(data, len); } else if (pkt->stream_index == videoIndex) { videoPacketList.push_back(pkt); } while (videoPacketList.size() > 0 && videoPts < audioPts) { AVPacket *pkt = videoPacketList.front(); videoPacketList.pop_front(); codecPacket(pkt); }
这个方案遇到的另一个问题是咱们如何获取videoPts和audioPts这两个值。我我的的解决思路是在解码环节进行,即,每次对pkt进行一次解码就根据pkt的stream_index值分别记录解码后的AVFrame的pts。不过音频的pts和视频的pts不能直接比较。咱们还须要根据各自的AVRational作一次换算。算法以下:spa
AVRational r; frame->pts * (double)r.num / (double)r.den;
2、封装思路讨论线程
代码封装实际是一个见仁见智的工做,可能不一样的人对代码结构的理解不一样,实现的封装方式也会存在差别。包括咱们的解决方案到底针对哪些需求也会按照不一样的思路作封装。在这里插一句题外话,你们认为程序开发究竟是一种什么样的工做性质?是仅仅为了实现客户的需求吗?若是你只能理解到这一层,那恐怕还远远不够!客户需求只能算是抛给你的一个问题,而你反馈给客户的应该是一套合理的解决方案。从这个观点出发咱们进行再抽象,程序开发应该是一种从问题空间到解空间的映射。既然如此,咱们就不能将本身的工做仅仅停留在功能实现这个层面,咱们还应该提供更好的解决思路——最佳实践。设计
基本上,若是咱们只须要设计一个简单的播放器。大概须要三个模块的支持:代理
界面模块(av_player):包括了界面的样式和基础互动功能
解码模块(Decoder):这个部分主要经过对FFmpeg的功能二次封装,并对外提供接口支持
播放器模块(PlayerWidget):负责界面和解码模块的链接,界面中嵌入播放器模块,视频显示和音频播放都由播放器模块独立负责。
下面看一下我设计的解码模块对外提供的接口:Decoder.h
class Decoder : protected QThread { public: Decoder(); virtual ~Decoder(); bool open(const char *filename); void close(); // 从文件中读取一个压缩报文 AVPacket* readPacket(); // 解码报文并释放空间,返回值为当前解码报文的pts时间(毫秒) int codecPacket(AVPacket* pkt); // 将解码帧Frame转码为RGB或PCM int toRGB(char *outData, int outWidth, int outHeight); int toPCM(char *outData); int durationMsec; // 文件时长 int fps; // 视频FPS int srcWidth; // 视频宽度 int srcHeight; // 视频高度 int videoIndex; // 视频通道 int audioIndex; // 音频通道 int sampleRate; // 音频采样率 int channels; // 声道 int sampleSize; // 样本位数 bool endFlag; // 线程结束标志 bool pauseFlag; // 线程暂停标志 // 记录当前的音视频所处在的pts时间戳(毫秒) int videoPts; int audioPts; // 记录音视频的编解码格式 int sampleFmt; int pixFmt; /************************************************************************/ /* default: CD音质(16bit 44100Hz stereo) */ /************************************************************************/ int dstSampleRate = 44100; // 采样率 int dstSampleSize = 16; // 采样大小 int dstChannels = 2; // 通道数 // 线程启动的代理方法 void start(); // 音频输出 QAudioOutput *audioOutput = NULL; protected: void run(); private: QMutex mtx; AVFormatContext *pFormatCtx = NULL; SwsContext *videoSwsCtx = NULL; AVFrame *yuv = NULL; SwrContext *audioSwrCtx = NULL; AVFrame *pcm = NULL; QIODevice *ioDevice = NULL; std::list<AVPacket*> videoPacketList; AVInputTypeEnum avType = AVInputTypeEnum::NOTYPE; QString fileName; };
乍一看很复杂,咱们稍微理一下思路。首先Decoder继承了QThread,并重写了start()方法。重写的好处是,在对调用者彻底透明的状况下,咱们能够在这个函数中作一些初始化工做。在设计模式中,它数据代理模式。其余方法介绍:
播放器模块:PlayerWidget.h
class PlayerWidget : public QOpenGLWidget { public: PlayerWidget(Decoder *dec, QWidget *parent, int interval); virtual ~PlayerWidget(); /************************************************************************/ /* default: 720p 25fps */ /************************************************************************/ int videoWidth = 720; int videoHeight = 480; int m_interval = 40; /************************************************************************/ /* default: CD音质(16bit 44100Hz stereo) */ /************************************************************************/ int sampleRate = 44100; // 采样率 int sampleSize = 16; // 采样大小 int channels = 2; // 通道数 protected: void timerEvent(QTimerEvent *e); void paintEvent(QPaintEvent *e); private: Decoder *decoder = NULL; QAudioOutput *out; QIODevice *io; };
这个模块继承自QOpenGLWidget,并包含了QAudioOutput。这两个Qt类分别表明了视频播放和音频播放。
界面模块:在这个模块中有一个重要的工做就是当咱们在播放视频的时候放大和缩小播放器窗口如何保证视频画面依然保持正确的宽高比,为此我写了一个静态函数:
struct AspectRatio { double width; double height; }; static AspectRatio* fitRatio(int outWidth, int outHeight, int inWidth, int inHeight) { double r1 = ((double)outWidth / (double)outHeight); double r2 = ((double)inWidth / (double)inHeight); AspectRatio *ar = new AspectRatio; if (r1 > r2) { int newWidth = (double)(outHeight * inWidth) / (double)inHeight; ar->width = newWidth; ar->height = outHeight; return ar; } else { int newHeight = (double)(inHeight * outWidth) / (double)inWidth; ar->width = outWidth; ar->height = newHeight; return ar; } }
最后附上我本身设计的播放器界面
项目源码:https://gitee.com/learnhow/ffmpeg_studio/tree/master/_64bit/src/av_player