当前位置: 首页 > news >正文

利用QT和FFmpeg实现一个简单的视频播放器

          在当今的多媒体世界中,视频播放已成为不可或缺的一部分。从简单的媒体播放器到复杂的视频编辑软件,视频解码和显示技术无处不在。本示例使用Qt和FFmpeg构建一个简单的视频播放器。利用ffmpeg解码视频,通过QWidget渲染解码后的图像,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。特点: 采用软件解码(CPU)、只解码图像数据,主要是演示了ffmpeg的基本使用流程,如何通过ffmpeg完成视频解码,转换图像像素格式,最后完成图像渲染。视频解码采用独立子线程,解码后将得到的图像数据通过信号槽发方式传递给UI界面进行渲染。

一、 环境介绍    

1、QT版本: QT5.12.6

2、编译器:  MSVC2017 64

3、ffmpeg版本: 6.1.1

4、SDL2 音频播放所需

5、完整工程下载地址(下载即可编译运行): https://download.csdn.net/download/u012959478/89626950

二、实现功能
  • 使用ffmpeg音视频库软解码实现视频播放器
  • 支持打开多种本地视频文件(如mp4,mov,avi等)
  • 支持视频匀速播放
  • 采用QPainter进行图像显示,支持自适应窗口缩放
  • 视频播放支持实时开始,暂停,继续播放
  • 采用模块化编程,视频解码,线程控制,图像显示各功能分离,低耦合
  • 多线程编程
三、实现思路  

该视频播放器的主要运行三条线程,需要两条队列:

线程1(音视频数据分离):使用FFMPEG分解视频文件,将视频数据存入到视频队列中,将音频数据存入到音频队列中。

线程2(视频解码):从视频队列中获取一包视频数据,通过FFMPEG解码该包视频数据,解码后再将视频转换为RGB数据,最后通过QT的画图显示将视频画面显示出来。

线程3(音频解码):实际该线程由SDL新建,它是通过回调的方式来从音频队列中获取音频数据,由SDL解码后再进行声音的播放。

四、示例代码  
 condmutex.h
#ifndef CONDMUTEX_H
#define CONDMUTEX_H#include "SDL.h"class CondMutex {
public:CondMutex();~CondMutex();void lock();void unlock();void signal();void broadcast();void wait();private:/** 互斥锁 */SDL_mutex *_mutex = nullptr;/** 条件变量 */SDL_cond *_cond = nullptr;
};#endif // CONDMUTEX_H
condmutex.cpp 
#include "condmutex.h"CondMutex::CondMutex() {// 创建互斥锁_mutex = SDL_CreateMutex();// 创建条件变量_cond = SDL_CreateCond();
}CondMutex::~CondMutex() {SDL_DestroyMutex(_mutex);SDL_DestroyCond(_cond);
}void CondMutex::lock() {SDL_LockMutex(_mutex);
}void CondMutex::unlock() {SDL_UnlockMutex(_mutex);
}void CondMutex::signal() {SDL_CondSignal(_cond);
}void CondMutex::broadcast() {SDL_CondBroadcast(_cond);
}void CondMutex::wait() {SDL_CondWait(_cond, _mutex);
}
videoslider.h 
#ifndef VIDEOSLIDER_H
#define VIDEOSLIDER_H#include <QSlider>class VideoSlider : public QSlider {Q_OBJECT
public:explicit VideoSlider(QWidget *parent = nullptr);signals:void clicked(VideoSlider *slider);private:void mousePressEvent(QMouseEvent *ev) override;
};#endif // VIDEOSLIDER_H
videoslider.cpp 
#include "videoslider.h"
#include <QMouseEvent>
#include <QStyle>VideoSlider::VideoSlider(QWidget *parent) : QSlider(parent) {}void VideoSlider::mousePressEvent(QMouseEvent *ev) {// 根据点击位置的x值,计算出对应的valueint value = QStyle::sliderValueFromPosition(minimum(),maximum(),ev->pos().x(),width());setValue(value);QSlider::mousePressEvent(ev);// 发出信号emit clicked(this);
}
videowidget.h
#ifndef VIDEOWIDGET_H
#define VIDEOWIDGET_H#include <QWidget>
#include <QImage>
#include "videoplayer.h"/*** 显示(渲染)视频*/
class VideoWidget : public QWidget {Q_OBJECT
public:explicit VideoWidget(QWidget *parent = nullptr);~VideoWidget();public slots:void onPlayerFrameDecoded(VideoPlayer *player, uint8_t *data, VideoPlayer::VideoSwsSpec &spec);void onPlayerStateChanged(VideoPlayer *player);private:QImage *_image = nullptr;QRect _rect;void paintEvent(QPaintEvent *event) override;void freeImage();
};#endif // VIDEOWIDGET_H
videowidget.cpp 
#include "videowidget.h"
#include <QPainter>VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {// 设置背景色setAttribute(Qt::WA_StyledBackground);setStyleSheet("background: black");
}VideoWidget::~VideoWidget() {freeImage();
}void VideoWidget::onPlayerStateChanged(VideoPlayer *player) {if (player->getState() != VideoPlayer::Stopped) return;freeImage();update();
}void VideoWidget::onPlayerFrameDecoded(VideoPlayer *player,uint8_t *data, VideoPlayer::VideoSwsSpec &spec) {if (player->getState() == VideoPlayer::Stopped) return;// 释放之前的图片freeImage();// 创建新的图片if (data != nullptr) {_image = new QImage((uchar *) data,spec.width, spec.height,QImage::Format_RGB888);// 计算最终的尺寸// 组件的尺寸int w = width();int h = height();// 计算rectint dx = 0;int dy = 0;int dw = spec.width;int dh = spec.height;// 计算目标尺寸if (dw > w || dh > h) { // 缩放if (dw * h > w * dh) { // 视频的宽高比 > 播放器的宽高比dh = w * dh / dw;dw = w;} else {dw = h * dw / dh;dh = h;}}// 居中dx = (w - dw) >> 1;dy = (h - dh) >> 1;_rect = QRect(dx, dy, dw, dh);}update();//触发paintEvent方法
}void VideoWidget::paintEvent(QPaintEvent *event) {if (!_image) return;// 将图片绘制到当前组件上QPainter(this).drawImage(_rect, *_image);
}void VideoWidget::freeImage() {if (_image) {av_free(_image->bits());delete _image;_image = nullptr;}
}
 videoplayer.h
#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H#include <QObject>
#include <QDebug>
#include <list>
#include "condmutex.h"extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
}#define ERROR_BUF \char errbuf[1024]; \av_strerror(ret, errbuf, sizeof (errbuf));#define CODE(func,code) \if (ret < 0) { \ERROR_BUF; \qDebug() << #func << "error" << errbuf; \code; \}#define END(func) CODE(func,fataError(); return;)
#define RET(func) CODE(func, return ret;)
#define CONTINUE(func) CODE(func, continue;)
#define BREAK(func) CODE(func, break;)/*** 预处理视频数据(不负责显示、渲染视频)*/
class VideoPlayer : public QObject {Q_OBJECT
public:// 状态typedef enum {Stopped = 0,Playing,Paused} State;// 音量typedef enum {Min = 0,Max = 100} Volumn;// 视频frame参数typedef struct {int width;int height;AVPixelFormat pixFmt;int size;} VideoSwsSpec;explicit VideoPlayer(QObject *parent = nullptr);~VideoPlayer();/** 播放 */void play();/** 暂停 */void pause();/** 停止 */void stop();/** 是否正在播放中 */bool isPlaying();/** 获取当前的状态 */State getState();/** 设置文件名 */void setFilename(QString &filename);/** 获取总时长(单位是妙,1秒=1000毫秒=1000000微妙)*/int getDuration();/** 当前的播放时刻(单位是秒) */int getTime();/** 设置当前的播放时刻(单位是秒) */void setTime(int seekTime);/** 设置音量 */void setVolumn(int volumn);int getVolumn();/** 设置静音 */void setMute(bool mute);bool isMute();signals:void stateChanged(VideoPlayer *player);void timeChanged(VideoPlayer *player);void initFinished(VideoPlayer *player);void playFailed(VideoPlayer *player);void frameDecoded(VideoPlayer *player,uint8_t *data,VideoSwsSpec &spec);private:/******** 音频相关 ********/typedef struct {int sampleRate;AVSampleFormat sampleFmt;int chLayout;int chs;int bytesPerSampleFrame;} AudioSwrSpec;/** 解码上下文 */AVCodecContext *_aDecodeCtx = nullptr;/** 流 */AVStream *_aStream = nullptr;/** 存放音频包的列表 */std::list<AVPacket> _aPktList;/** 音频包列表的锁 */CondMutex _aMutex;/** 音频重采样上下文 */SwrContext *_aSwrCtx = nullptr;/** 音频重采样输入\输出参数 */AudioSwrSpec _aSwrInSpec;AudioSwrSpec _aSwrOutSpec;/** 音频重采样输入\输出frame */AVFrame *_aSwrInFrame = nullptr;AVFrame *_aSwrOutFrame = nullptr;/** 音频重采样输出PCM的索引(从哪个位置开始取出PCM数据填充到SDL的音频缓冲区) */int _aSwrOutIdx = 0;/** 音频重采样输出PCM的大小 */int _aSwrOutSize = 0;/** 音量 */int _volumn = Max;/** 静音 */bool _mute = false;/** 音频时钟,当前音频包对应的时间值 */double _aTime = 0;/** 是否有音频流 */bool _hasAudio = false;/** 音频资源是否可以释放 */bool _aCanFree = false;/** 外面设置的当前播放时刻(用于完成seek功能) */int _aSeekTime = -1;/** 初始化音频信息 */int initAudioInfo();/** 初始化SDL */int initSDL();/** 添加数据包到音频包列表中 */void addAudioPkt(AVPacket &pkt);/** 清空音频包列表 */void clearAudioPktList();/** SDL填充缓冲区的回调函数 */static void sdlAudioCallbackFunc(void *userdata, Uint8 *stream, int len);/** SDL填充缓冲区的回调函数 */void sdlAudioCallback(Uint8 *stream, int len);/** 音频解码 */int decodeAudio();/** 初始化音频重采样 */int initSwr();/******** 视频相关 ********//** 解码上下文 */AVCodecContext *_vDecodeCtx = nullptr;/** 流 */AVStream *_vStream = nullptr;/** 像素格式转换的输入\输出frame */AVFrame *_vSwsInFrame = nullptr, *_vSwsOutFrame = nullptr;/** 像素格式转换的上下文 */SwsContext *_vSwsCtx = nullptr;/** 像素格式转换的输出frame的参数 */VideoSwsSpec _vSwsOutSpec;/** 存放视频包的列表 */std::list<AVPacket> _vPktList;/** 视频包列表的锁 */CondMutex _vMutex;/** 视频时钟,当前视频包对应的时间值 */double _vTime = 0;/** 是否有视频流 */bool _hasVideo = false;/** 视频资源是否可以释放 */bool _vCanFree = false;/** 外面设置的当前播放时刻(用于完成seek功能) */int _vSeekTime = -1;/** 初始化视频信息 */int initVideoInfo();/** 初始化视频像素格式转换 */int initSws();/** 添加数据包到视频包列表中 */void addVideoPkt(AVPacket &pkt);/** 清空视频包列表 */void clearVideoPktList();/** 解码视频 */void decodeVideo();/******** 其他 ********//** 当前的状态 */State _state = Stopped;/** fmtCtx是否可以释放 */bool _fmtCtxCanFree = false;/** 文件名 */QString _filename;// 解封装上下文AVFormatContext *_fmtCtx = nullptr;/** 外面设置的当前播放时刻(用于完成seek功能) */int _seekTime = -1;/** 初始化解码器和解码上下文 */int initDecoder(AVCodecContext **decodeCtx,AVStream **stream,AVMediaType type);/** 改变状态 */void setState(State state);/** 读取文件数据 */void readFile();/** 释放资源 */void free();void freeAudio();void freeVideo();/** 严重错误 */void fataError();
};#endif // VIDEOPLAYER_H
videoplayer.cpp
#include "videoplayer.h"
#include <thread>#define AUDIO_MAX_PKT_SIZE 1000
#define VIDEO_MAX_PKT_SIZE 500VideoPlayer::VideoPlayer(QObject *parent) : QObject(parent) {// 初始化Audio子系统if (SDL_Init(SDL_INIT_AUDIO)) {// 返回值不是0,就代表失败qDebug() << "SDL_Init error" << SDL_GetError();emit playFailed(this);return;}
}VideoPlayer::~VideoPlayer() {// 不再对外发送消息disconnect();stop();SDL_Quit();
}void VideoPlayer::play() {if (_state == Playing) return;// 状态可能是:暂停、停止、正常完毕if(_state == Stopped){// 开始线程:读取文件std::thread([this](){readFile();}).detach();// detach 等到readFile方法执行完,这个线程就会销毁}else{setState(Playing);}
}void VideoPlayer::pause() {if (_state != Playing) return;// 状态可能是:正在播放setState(Paused);
}void VideoPlayer::stop() {if (_state == Stopped) return;// 状态可能是:正在播放、暂停、正常完毕// 改变状态_state = Stopped;// 释放资源free();// 通知外界emit stateChanged(this);
}bool VideoPlayer::isPlaying() {return _state == Playing;
}VideoPlayer::State VideoPlayer::getState() {return _state;
}void VideoPlayer::setFilename(QString &filename) {_filename = filename;
}int VideoPlayer::getDuration(){return _fmtCtx ? round(_fmtCtx->duration * av_q2d(AV_TIME_BASE_Q)) : 0;
}int VideoPlayer::getTime(){return round(_aTime);
}void VideoPlayer::setVolumn(int volumn){_volumn = volumn;
}void VideoPlayer::setTime(int seekTime){_seekTime = seekTime;
}int VideoPlayer::getVolumn(){return _volumn;
}void VideoPlayer::setMute(bool mute) {_mute = mute;
}bool VideoPlayer::isMute() {return _mute;
}void VideoPlayer::readFile(){   int ret = 0;// 创建解封装上下文、打开文件ret = avformat_open_input(&_fmtCtx,_filename.toUtf8().data(),nullptr,nullptr);END(avformat_open_input);// 检索流信息ret = avformat_find_stream_info(_fmtCtx,nullptr);END(avformat_find_stream_info);// 打印流信息到控制台av_dump_format(_fmtCtx,0,_filename.toUtf8().data(),0);fflush(stderr);// 初始化音频信息_hasAudio = initAudioInfo() >= 0;// 初始化视频信息_hasVideo = initVideoInfo() >= 0;if (!_hasAudio && !_hasVideo) {emit playFailed(this);free();return;}// 到此为止,初始化完毕emit initFinished(this);// 改变状态setState(Playing);// 音频解码子线程:开始工作SDL_PauseAudio(0);// 开启新的线程去解码视频数据std::thread([this](){decodeVideo();}).detach();// 从输入文件中读取数据AVPacket pkt;while (_state != Stopped) {// 处理seek操作if (_seekTime >= 0) {int streamIdx;if (_hasAudio) { // 优先使用音频流索引streamIdx = _aStream->index;} else {streamIdx = _vStream->index;}// 现实时间 -> 时间戳AVRational timeBase = _fmtCtx->streams[streamIdx]->time_base;int64_t ts = _seekTime / av_q2d(timeBase);//           ret = av_seek_frame(_fmtCtx, streamIdx, ts, AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME);ret = avformat_seek_file(_fmtCtx, streamIdx, INT64_MIN, ts, INT64_MAX, 0);if(ret < 0){// seek失败qDebug() << "seek失败" << _seekTime << ts << streamIdx;_seekTime = -1;}else{// seek成功qDebug() << "seek成功" << _seekTime << ts << streamIdx;// 清空之前读取的数据包clearAudioPktList();clearVideoPktList();_vSeekTime = _seekTime;_aSeekTime = _seekTime;_seekTime = -1;// 恢复时钟_aTime = 0;_vTime = 0;}}int vSize = _vPktList.size();int aSize = _aPktList.size();if (vSize >= VIDEO_MAX_PKT_SIZE || aSize >= AUDIO_MAX_PKT_SIZE) {SDL_Delay(1);continue;}ret = av_read_frame(_fmtCtx, &pkt);if (ret == 0) {if (pkt.stream_index == _aStream->index) { // 读取到的是音频数据addAudioPkt(pkt);} else if (pkt.stream_index == _vStream->index) { // 读取到的是视频数据addVideoPkt(pkt);}else{// 如果不是音频、视频流,直接释放av_packet_unref(&pkt);}} else if (ret == AVERROR_EOF) { // 读到了文件的尾部//           break;// seek的时候不能用breakif(vSize == 0 && aSize ==0){// 说明文件正常播放完毕_fmtCtxCanFree = true;break;}} else {ERROR_BUF;qDebug() << "av_read_frame error" << errbuf;continue;}}if (_fmtCtxCanFree) { // 文件正常播放完毕stop();} else {// 标记一下:_fmtCtx可以释放了_fmtCtxCanFree = true;}
}int VideoPlayer::initDecoder(AVCodecContext **decodeCtx,AVStream **stream,AVMediaType type) {// 根据type寻找最合适的流信息// 返回值是流索引int ret = av_find_best_stream(_fmtCtx, type, -1, -1, nullptr, 0);RET(av_find_best_stream);// 检验流int streamIdx = ret;*stream = _fmtCtx->streams[streamIdx];if (!*stream) {qDebug() << "stream is empty";return -1;}// 为当前流找到合适的解码器const AVCodec *decoder = avcodec_find_decoder((*stream)->codecpar->codec_id);if (!decoder) {qDebug() << "decoder not found" << (*stream)->codecpar->codec_id;return -1;}// 初始化解码上下文*decodeCtx = avcodec_alloc_context3(decoder);if (!decodeCtx) {qDebug() << "avcodec_alloc_context3 error";return -1;}// 从流中拷贝参数到解码上下文中ret = avcodec_parameters_to_context(*decodeCtx, (*stream)->codecpar);RET(avcodec_parameters_to_context);// 打开解码器ret = avcodec_open2(*decodeCtx, decoder, nullptr);RET(avcodec_open2);return 0;
}void VideoPlayer::setState(State state) {if (state == _state) return;_state = state;emit stateChanged(this);
}void VideoPlayer::free(){while (_hasAudio && !_aCanFree);while (_hasVideo && !_vCanFree);while (!_fmtCtxCanFree);avformat_close_input(&_fmtCtx);_fmtCtxCanFree = false;_seekTime = -1;freeAudio();freeVideo();
}void VideoPlayer::fataError(){setState(Stopped);free();emit playFailed(this);
}
 videoplayer_audio.cpp
#include "videoplayer.h"// 初始化音频信息
int VideoPlayer::initAudioInfo() {int ret = initDecoder(&_aDecodeCtx,&_aStream,AVMEDIA_TYPE_AUDIO);RET(initDecoder);// 初始化音频重采样ret = initSwr();RET(initSwr);// 初始化SDLret = initSDL();RET(initSDL);return 0;
}int VideoPlayer::initSwr() {// 重采样输入参数_aSwrInSpec.sampleFmt = _aDecodeCtx->sample_fmt;_aSwrInSpec.sampleRate = _aDecodeCtx->sample_rate;_aSwrInSpec.chLayout = _aDecodeCtx->channel_layout;_aSwrInSpec.chs = _aDecodeCtx->channels;// 重采样输出参数_aSwrOutSpec.sampleFmt = AV_SAMPLE_FMT_S16;_aSwrOutSpec.sampleRate = 44100;_aSwrOutSpec.chLayout = AV_CH_LAYOUT_STEREO;_aSwrOutSpec.chs = av_get_channel_layout_nb_channels(_aSwrOutSpec.chLayout);_aSwrOutSpec.bytesPerSampleFrame = _aSwrOutSpec.chs * av_get_bytes_per_sample(_aSwrOutSpec.sampleFmt);// 创建重采样上下文_aSwrCtx = swr_alloc_set_opts(nullptr,// 输出参数_aSwrOutSpec.chLayout,_aSwrOutSpec.sampleFmt,_aSwrOutSpec.sampleRate,// 输入参数_aSwrInSpec.chLayout,_aSwrInSpec.sampleFmt,_aSwrInSpec.sampleRate,0, nullptr);if (!_aSwrCtx) {qDebug() << "swr_alloc_set_opts error";return -1;}// 初始化重采样上下文int ret = swr_init(_aSwrCtx);RET(swr_init);// 初始化重采样的输入frame_aSwrInFrame = av_frame_alloc();if (!_aSwrInFrame) {qDebug() << "av_frame_alloc error";return -1;}// 初始化重采样的输出frame_aSwrOutFrame = av_frame_alloc();if (!_aSwrOutFrame) {qDebug() << "av_frame_alloc error";return -1;}// 初始化重采样的输出frame的data[0]空间ret = av_samples_alloc(_aSwrOutFrame->data,_aSwrOutFrame->linesize,_aSwrOutSpec.chs,4096, _aSwrOutSpec.sampleFmt, 1);RET(av_samples_alloc);return 0;
}void VideoPlayer::freeAudio(){_aSwrOutIdx = 0;_aSwrOutSize =0;_aTime = 0;_aCanFree = false;_aSeekTime = -1;clearAudioPktList();avcodec_free_context(&_aDecodeCtx);swr_free(&_aSwrCtx);av_frame_free(&_aSwrInFrame);if(_aSwrOutFrame){av_freep(&_aSwrOutFrame->data[0]);// 因手动创建了data[0]的空间av_frame_free(&_aSwrOutFrame);}// 停止播放SDL_PauseAudio(1);SDL_CloseAudio();
}void VideoPlayer::sdlAudioCallbackFunc(void *userdata, uint8_t *stream, int len){VideoPlayer *player = (VideoPlayer *)userdata;player->sdlAudioCallback(stream,len);
}int VideoPlayer::initSDL(){// 音频参数SDL_AudioSpec spec;// 采样率spec.freq = _aSwrOutSpec.sampleRate;// 采样格式(s16le)spec.format = AUDIO_S16LSB;// 声道数spec.channels = _aSwrOutSpec.chs;// 音频缓冲区的样本数量(这个值必须是2的幂)spec.samples = 512;// 回调spec.callback = sdlAudioCallbackFunc;// 传递给回调的参数spec.userdata = this;// 打开音频设备if (SDL_OpenAudio(&spec, nullptr)) {qDebug() << "SDL_OpenAudio error" << SDL_GetError();return -1;}return 0;
}void VideoPlayer::addAudioPkt(AVPacket &pkt){_aMutex.lock();_aPktList.push_back(pkt);_aMutex.signal();_aMutex.unlock();
}void VideoPlayer::clearAudioPktList(){_aMutex.lock();for(AVPacket &pkt : _aPktList){av_packet_unref(&pkt);}_aPktList.clear();_aMutex.unlock();
}void VideoPlayer::sdlAudioCallback(Uint8 *stream, int len){// 清零(静音)SDL_memset(stream, 0, len);// len:SDL音频缓冲区剩余的大小(还未填充的大小)while (len > 0) {if (_state == Paused) break;if (_state == Stopped) {_aCanFree = true;break;}// 说明当前PCM的数据已经全部拷贝到SDL的音频缓冲区了// 需要解码下一个pkt,获取新的PCM数据if (_aSwrOutIdx >= _aSwrOutSize) {// 全新PCM的大小_aSwrOutSize = decodeAudio();// 索引清0_aSwrOutIdx = 0;// 没有解码出PCM数据,那就静音处理if (_aSwrOutSize <= 0) {// 假定PCM的大小_aSwrOutSize = 1024;// 给PCM填充0(静音)memset(_aSwrOutFrame->data[0], 0, _aSwrOutSize);}}// 本次需要填充到stream中的PCM数据大小int fillLen = _aSwrOutSize - _aSwrOutIdx;fillLen = std::min(fillLen, len);// 获取当前音量int volumn = _mute ? 0 : ((_volumn * 1.0 / Max) * SDL_MIX_MAXVOLUME);// 填充SDL缓冲区SDL_MixAudio(stream,_aSwrOutFrame->data[0] + _aSwrOutIdx,fillLen, volumn);// 移动偏移量len -= fillLen;stream += fillLen;_aSwrOutIdx += fillLen;}
}/*** @brief VideoPlayer::decodeAudio* @return 解码出来的pcm大小*/
int VideoPlayer::decodeAudio(){// 加锁_aMutex.lock();if (_aPktList.empty() || _state == Stopped) {_aMutex.unlock();return 0;}// 取出头部的数据包AVPacket pkt = _aPktList.front();// 从头部中删除_aPktList.pop_front();// 解锁_aMutex.unlock();// 保存音频时钟if (pkt.pts != AV_NOPTS_VALUE) {_aTime = av_q2d(_aStream->time_base) *pkt.pts;// 通知外界:播放时间点发生了改变emit timeChanged(this);}// 如果是视频,不能在这个位置判断(不能提前释放pkt,不然会导致B帧、P帧解码失败,画面撕裂)// 发现音频的时间是早于seekTime的,直接丢弃if (_aSeekTime >= 0) {if (_aTime < _aSeekTime) {// 释放pktav_packet_unref(&pkt);return 0;} else {_aSeekTime = -1;}}// 发送压缩数据到解码器int ret = avcodec_send_packet(_aDecodeCtx, &pkt);// 释放pktav_packet_unref(&pkt);RET(avcodec_send_packet);// 获取解码后的数据ret = avcodec_receive_frame(_aDecodeCtx, _aSwrInFrame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {return 0;} else RET(avcodec_receive_frame);// 重采样输出的样本数int outSamples = av_rescale_rnd(_aSwrOutSpec.sampleRate,_aSwrInFrame->nb_samples,_aSwrInSpec.sampleRate, AV_ROUND_UP);// 由于解码出来的PCM。跟SDL要求的PCM格式可能不一致,需要进行重采样ret = swr_convert(_aSwrCtx,_aSwrOutFrame->data,outSamples,(const uint8_t **) _aSwrInFrame->data,_aSwrInFrame->nb_samples);RET(swr_convert);return ret * _aSwrOutSpec.bytesPerSampleFrame;
}
videoplayer_video.cpp
#include "videoplayer.h"
#include <thread>
extern "C" {
#include <libavutil/imgutils.h>
}// 初始化视频信息
int VideoPlayer::initVideoInfo() {int ret = initDecoder(&_vDecodeCtx,&_vStream,AVMEDIA_TYPE_VIDEO);RET(initDecoder);// 初始化像素格式转换ret = initSws();RET(initSws);return 0;
}int VideoPlayer::initSws(){int inW = _vDecodeCtx->width;int inH = _vDecodeCtx->height;// 输出frame的参数_vSwsOutSpec.width = inW >> 4 << 4;// 先除以16在乘以16,保证是16的倍数_vSwsOutSpec.height = inH >> 4 << 4;_vSwsOutSpec.pixFmt = AV_PIX_FMT_RGB24;_vSwsOutSpec.size = av_image_get_buffer_size(_vSwsOutSpec.pixFmt,_vSwsOutSpec.width,_vSwsOutSpec.height, 1);// 初始化像素格式转换的上下文_vSwsCtx = sws_getContext(inW,inH,_vDecodeCtx->pix_fmt,_vSwsOutSpec.width,_vSwsOutSpec.height,_vSwsOutSpec.pixFmt,SWS_BILINEAR, nullptr, nullptr, nullptr);if (!_vSwsCtx) {qDebug() << "sws_getContext error";return -1;}// 初始化像素格式转换的输入frame_vSwsInFrame = av_frame_alloc();if (!_vSwsInFrame) {qDebug() << "av_frame_alloc error";return -1;}// 初始化像素格式转换的输出frame_vSwsOutFrame = av_frame_alloc();if (!_vSwsOutFrame) {qDebug() << "av_frame_alloc error";return -1;}// _vSwsOutFrame的data[0]指向的内存空间int ret = av_image_alloc(_vSwsOutFrame->data,_vSwsOutFrame->linesize,_vSwsOutSpec.width,_vSwsOutSpec.height,_vSwsOutSpec.pixFmt,1);RET(av_image_alloc);return 0;
}void VideoPlayer::addVideoPkt(AVPacket &pkt){_vMutex.lock();_vPktList.push_back(pkt);_vMutex.signal();_vMutex.unlock();
}void VideoPlayer::clearVideoPktList(){_vMutex.lock();for(AVPacket &pkt : _vPktList){av_packet_unref(&pkt);}_vPktList.clear();_vMutex.unlock();
}void VideoPlayer::freeVideo(){clearVideoPktList();avcodec_free_context(&_vDecodeCtx);av_frame_free(&_vSwsInFrame);if (_vSwsOutFrame) {av_freep(&_vSwsOutFrame->data[0]);av_frame_free(&_vSwsOutFrame);}sws_freeContext(_vSwsCtx);_vSwsCtx = nullptr;_vStream = nullptr;_vTime = 0;_vCanFree = false;_vSeekTime = -1;
}void VideoPlayer::decodeVideo(){while (true) {// 如果是暂停,并且没有Seek操作if (_state == Paused && _vSeekTime == -1) {continue;}if (_state == Stopped) {_vCanFree = true;break;}_vMutex.lock();if(_vPktList.empty()){_vMutex.unlock();continue;}// 取出头部的视频包AVPacket pkt = _vPktList.front();_vPktList.pop_front();_vMutex.unlock();// 视频时钟if (pkt.dts != AV_NOPTS_VALUE) {_vTime = av_q2d(_vStream->time_base) * pkt.dts;}// 发送压缩数据到解码器int ret = avcodec_send_packet(_vDecodeCtx, &pkt);// 释放pktav_packet_unref(&pkt);CONTINUE(avcodec_send_packet);while (true) {ret = avcodec_receive_frame(_vDecodeCtx, _vSwsInFrame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {break;} else BREAK(avcodec_receive_frame);// 一定要在解码成功后,再进行下面的判断// 发现视频的时间是早于seekTime的,直接丢弃if(_vSeekTime >= 0){if (_vTime < _vSeekTime) {continue;// 丢掉} else {_vSeekTime = -1;}}// 像素格式的转换sws_scale(_vSwsCtx,_vSwsInFrame->data, _vSwsInFrame->linesize,0, _vDecodeCtx->height,_vSwsOutFrame->data, _vSwsOutFrame->linesize);if(_hasAudio){// 有音频// 如果视频包过早被解码出来,那就需要等待对应的音频时钟到达while (_vTime > _aTime && _state == Playing) {SDL_Delay(1);}}uint8_t *data = (uint8_t *)av_malloc(_vSwsOutSpec.size);memcpy(data, _vSwsOutFrame->data[0], _vSwsOutSpec.size);// 发出信号emit frameDecoded(this,data,_vSwsOutSpec);qDebug()<< "渲染了一帧"<< _vTime << _aTime;}}
}
 界面设计mainwindow.ui

mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include "videoplayer.h"
#include "videoslider.h"QT_BEGIN_NAMESPACE
namespace Ui {class MainWindow;
}
QT_END_NAMESPACEclass MainWindow : public QMainWindow {Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:void onPlayerStateChanged(VideoPlayer *player);void onPlayerTimeChanged(VideoPlayer *player);void onPlayerInitFinished(VideoPlayer *player);void onPlayerPlayFailed(VideoPlayer *player);void onSliderClicked(VideoSlider *slider);void on_stopBtn_clicked();void on_openFileBtn_clicked();void on_currentSlider_valueChanged(int value);void on_volumnSlider_valueChanged(int value);void on_playBtn_clicked();void on_muteBtn_clicked();private:Ui::MainWindow *ui;VideoPlayer *_player;QString getTimeText(int value);
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include <QMessageBox>#define FILEPATH "../test/"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow) {ui->setupUi(this);// 注册信号的参数类型,保证能够发出信号qRegisterMetaType<VideoPlayer::VideoSwsSpec>("VideoSwsSpec&");// 创建播放器_player = new VideoPlayer();connect(_player, &VideoPlayer::stateChanged,this, &MainWindow::onPlayerStateChanged);connect(_player, &VideoPlayer::timeChanged,this, &MainWindow::onPlayerTimeChanged);connect(_player, &VideoPlayer::initFinished,this, &MainWindow::onPlayerInitFinished);connect(_player, &VideoPlayer::playFailed,this, &MainWindow::onPlayerPlayFailed);connect(_player, &VideoPlayer::frameDecoded,ui->videoWidget, &VideoWidget::onPlayerFrameDecoded);connect(_player, &VideoPlayer::stateChanged,ui->videoWidget, &VideoWidget::onPlayerStateChanged);// 监听时间滑块的点击connect(ui->currentSlider, &VideoSlider::clicked,this, &MainWindow::onSliderClicked);// 设置音量滑块的范围ui->volumnSlider->setRange(VideoPlayer::Volumn::Min,VideoPlayer::Volumn::Max);ui->volumnSlider->setValue(ui->volumnSlider->maximum() >> 2);
}MainWindow::~MainWindow() {delete ui;delete _player;
}void MainWindow::onSliderClicked(VideoSlider *slider) {_player->setTime(slider->value());
}void MainWindow::onPlayerPlayFailed(VideoPlayer *player) {QMessageBox::critical(nullptr,"提示","播放失败");
}void MainWindow::onPlayerTimeChanged(VideoPlayer *player) {ui->currentSlider->setValue(player->getTime());
}void MainWindow::onPlayerInitFinished(VideoPlayer *player) {int duration = player->getDuration();qDebug()<< duration;// 设置一些slider的范围ui->currentSlider->setRange(0,duration);// 设置label的文字ui->durationLabel->setText(getTimeText(duration));
}/*** onPlayerStateChanged方法的发射虽然在子线程中执行(VideoPlayer::readFile()),* 但是此方法是在主线程执行,因为它的connect是在主线程执行的*/
void MainWindow::onPlayerStateChanged(VideoPlayer *player) {VideoPlayer::State state = player->getState();if (state == VideoPlayer::Playing) {ui->playBtn->setText("暂停");} else {ui->playBtn->setText("播放");}if (state == VideoPlayer::Stopped) {ui->playBtn->setEnabled(false);ui->stopBtn->setEnabled(false);ui->currentSlider->setEnabled(false);ui->volumnSlider->setEnabled(false);ui->muteBtn->setEnabled(false);ui->durationLabel->setText(getTimeText(0));ui->currentSlider->setValue(0);// 显示打开文件的页面ui->playWidget->setCurrentWidget(ui->openFilePage);} else {ui->playBtn->setEnabled(true);ui->stopBtn->setEnabled(true);ui->currentSlider->setEnabled(true);ui->volumnSlider->setEnabled(true);ui->muteBtn->setEnabled(true);// 显示播放视频的页面ui->playWidget->setCurrentWidget(ui->videoPage);}
}void MainWindow::on_stopBtn_clicked() {_player->stop();
}void MainWindow::on_openFileBtn_clicked() {QString filename = QFileDialog::getOpenFileName(nullptr,"选择多媒体文件",FILEPATH,"多媒体文件 (*.mp4 *.avi *.mkv *.mp3 *.aac)");qDebug() << "打开文件" << filename;if (filename.isEmpty()) return;// 开始播放打开的文件_player->setFilename(filename);_player->play();
}void MainWindow::on_currentSlider_valueChanged(int value) {ui->currentLabel->setText(getTimeText(value));
}void MainWindow::on_volumnSlider_valueChanged(int value) {ui->volumnLabel->setText(QString("%1").arg(value));_player->setVolumn(value);
}void MainWindow::on_playBtn_clicked() {VideoPlayer::State state = _player->getState();if (state == VideoPlayer::Playing) {_player->pause();} else {_player->play();}
}QString MainWindow::getTimeText(int value){QString h = QString("0%1").arg(value / 3600).right(2);QString m = QString("0%1").arg((value / 60) % 60).right(2);QString s = QString("0%1").arg(value % 60).right(2);return  QString("%1:%2:%3").arg(h).arg(m).arg(s);
}void MainWindow::on_muteBtn_clicked()
{if (_player->isMute()) {_player->setMute(false);ui->muteBtn->setText("静音");} else {_player->setMute(true);ui->muteBtn->setText("开音");}
}

        通过以上的实现,我们就可以得到一个简单的录音软件,它可以利用QT实现录音,使用ffmpeg进行音频重采样,并使用fdk-aac进行编码。这个录音软件不仅简单易用,可以帮助我们记录和存储语音信息,是一个非常实用的工具。

五、运行效果

​​​​​​​

        谢谢您的阅读。希望本文能对您有所帮助,并且给您带来了一些新的观点和思考。如果您有任何问题或意见,请随时与我联系。再次感谢您的支持!

 六、相关文章

Windosw下Visual Studio2022编译FFmpeg(支持x264、x265、fdk-acc)-CSDN博客

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • android视频播放,(一)MediaPlayer视频播放示例
  • 微调LLama 3.1——七月论文审稿GPT第5.5版:拿早期paper-review数据集微调LLama 3.1
  • Docker的卸载|安装|启动|停止|重启
  • vue防止鼠标左键拖动选中页面的元素
  • Elasticsearch 文档修改:全量更新与增量更新
  • C语言 | Leetcode C语言题解之第329题矩阵中的最长递增路径
  • LeetCode Pow(x, n)
  • HTTP的场景实践
  • 学习嵌入式的第十六天----结构体 共用体
  • C# winform 三层架构增删改查,(删除篇)
  • Python面试宝典第32题:课程表
  • cmake+ninja交叉编译android下的静态库
  • Elasticsearch 基本搜索
  • AI大模型开发——2.深度学习基础(1)
  • HCIP第七章(BGP拓展知识)
  • 《深入 React 技术栈》
  • 【干货分享】SpringCloud微服务架构分布式组件如何共享session对象
  • co.js - 让异步代码同步化
  • Dubbo 整合 Pinpoint 做分布式服务请求跟踪
  • gcc介绍及安装
  • HTTP--网络协议分层,http历史(二)
  • JAVA SE 6 GC调优笔记
  • SpringBoot几种定时任务的实现方式
  • Vue 2.3、2.4 知识点小结
  • 包装类对象
  • 利用jquery编写加法运算验证码
  • 前端
  • 王永庆:技术创新改变教育未来
  • 写给高年级小学生看的《Bash 指南》
  • 国内开源镜像站点
  • 进程与线程(三)——进程/线程间通信
  • ​​​​​​​ubuntu16.04 fastreid训练过程
  • ​LeetCode解法汇总2696. 删除子串后的字符串最小长度
  • ​云纳万物 · 数皆有言|2021 七牛云战略发布会启幕,邀您赴约
  • # Redis 入门到精通(九)-- 主从复制(1)
  • # 安徽锐锋科技IDMS系统简介
  • # 日期待t_最值得等的SUV奥迪Q9:空间比MPV还大,或搭4.0T,香
  • #我与Java虚拟机的故事#连载08:书读百遍其义自见
  • #我与Java虚拟机的故事#连载09:面试大厂逃不过的JVM
  • $Django python中使用redis, django中使用(封装了),redis开启事务(管道)
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (2)nginx 安装、启停
  • (2/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (2015)JS ES6 必知的十个 特性
  • (3)llvm ir转换过程
  • (c语言版)滑动窗口 给定一个字符串,只包含字母和数字,按要求找出字符串中的最长(连续)子串的长度
  • (顶刊)一个基于分类代理模型的超多目标优化算法
  • (读书笔记)Javascript高级程序设计---ECMAScript基础
  • (二) Windows 下 Sublime Text 3 安装离线插件 Anaconda
  • (附源码)springboot家庭财务分析系统 毕业设计641323
  • (附源码)计算机毕业设计SSM基于健身房管理系统
  • (全注解开发)学习Spring-MVC的第三天
  • (十七)Flask之大型项目目录结构示例【二扣蓝图】
  • (转)程序员技术练级攻略
  • **PHP二维数组遍历时同时赋值