利用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博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/397866.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python爬虫——爬取bilibili中的视频

爬取bilibili中的视频 本次爬取&#xff0c;还是运用的是requests方法 首先进入bilibili官网中&#xff0c;选取你想要爬取的视频&#xff0c;进入视频播放页面&#xff0c;按F12&#xff0c;将网络中的名称栏向上拉找到第一个并点击&#xff0c;可以在标头中&#xff0c;找到…

Ps:通过 RGB 值计算 HSB 值

在 Photoshop 中&#xff0c;HSB&#xff08;色相、饱和度和明度&#xff09;仅作为表达颜色的一种方式而存在&#xff0c;并不是一种颜色模式。 色相/饱和度命令就是基于色彩三要素进行调色的常用命令。 还有一个与 HSB 相关的滤镜&#xff1a;HSB/HSL 滤镜&#xff0c;用于实…

什么是交互测试?

最近有接触到一个有趣的名词&#xff1a;交互测试。 在对这个名词进行解释之前&#xff0c;我先去特意请教了一个产品经理朋友&#xff0c;问下交互的概念。于是知道了我们的行业里面还有很多个有趣的职位&#xff1a;交互设计师、UE、UI、前端、设计.....等等等等这些&#x…

C语言——查漏补缺

前言 本篇博客主要记录一些C语言的遗漏点&#xff0c;完成查漏补缺的工作&#xff0c;如果读者感兴趣&#xff0c;可以看看下面的内容。都是一些小点&#xff0c;下面进入正文部分。 1. 字符汇聚 编写代码&#xff0c;演示多个字符从两端移动&#xff0c;向中间汇聚 #inclu…

Linux:多线程(三.POSIX信号量、生产消费模型、线程池、其他常见的锁)

上次讲解了&#xff1a;Linux&#xff1a;多线程&#xff08;二.理解pthread_t、线程互斥与同步、基于阻塞队列的生产消费模型&#xff09; 文章目录 1.POSIX信号量1.1引入1.2回顾加深理解信号量1.3信号量的操作接口 2.基于循环队列的生产消费模型2.1循环队列2.2整个项目 3.线程…

网络协议七 应用层 HTTP 协议

应用层常见的协议 HTTP协议 一. 如何查看我们的http 协议全部的内容有哪些呢&#xff1f; 一种合理的方法是 通过 wireshark 软件&#xff0c;找到想要查看的HTTP --->追踪流--->HTTP流 来查看 结果如下&#xff1a;红色部分 为 发送给服务器的&#xff0c;蓝色部分为服…

40【源码】数据可视化:基于 Echarts + Python 动态实时大屏 - 无线网络大数据平台

数据可视化大屏的出现&#xff0c;掀起一番又一番的浪潮&#xff0c;众多企业主纷纷想要打造属于自己的“酷炫吊炸天”的霸道总裁大屏驾驶舱。 之前有小伙伴们建议我出一些视频课程来学习Echarts&#xff0c;这样可以更快上手&#xff0c;所以我就追星赶月的录制了《Echarts -…

为什么在职场上大家都在装,别人才会觉得你很强

在职场中&#xff0c;有时候会发现那些看似强大的人并不一定是真的强&#xff0c;而是他们懂得如何装出来。 上班就如甄嬛传里的宫斗&#xff0c;懂得“装”是一种智慧和生存技能。为什么在职场要会装&#xff1f;别人才会觉得你很强&#xff1f; 1、装冷脸形象没坏处 在职场…

C语言 | Leetcode C语言题解之第327题区间和的个数

题目&#xff1a; 题解&#xff1a; int countRangeSumRecursive(long long* sum, int lower, int upper, int left, int right) {if (left right) {return 0;} else {int mid (left right) / 2;int n1 countRangeSumRecursive(sum, lower, upper, left, mid);int n2 cou…

中国自动驾驶出租车冲击网约车市场

近年来&#xff0c;中国的自动驾驶技术迅速发展&#xff0c;对传统网约车市场构成了越来越大的冲击。随着科技巨头百度旗下的萝卜快跑等公司加速推广无人驾驶出租车&#xff0c;这一趋势引发了广泛的讨论和担忧。 自动驾驶技术的迅猛发展 中国自动驾驶行业正处于快速发展阶段&…

企业数字化转型解决方案

企业数字化转型解决方案旨在通过系统化的方法和先进技术&#xff0c;帮助企业在数字时代实现全面的业务升级和优化。首先&#xff0c;解决方案包括构建和部署强大的数字基础设施&#xff0c;如云计算平台、大数据分析系统和物联网设备&#xff0c;以支持企业的业务运营和数据处…

一个人活成一个团队:python的django项目devops实战

文章目录 一、需求规划二、代码管理三、创建流水线1、配置流水线源 四、自动测试五、自动构建六、自动部署七、总结 对于开发团队来说提高软件交付的速度和质量是一个永恒的话题&#xff0c;对于个人开发者来说同样如此。作为一个码农&#xff0c;一定会有几个自己私有的小项目…

Mysql 脚本转换为drawio ER 脚本

Navicat 导出数据库脚本 通过代码转换脚本 import java.io.BufferedReader; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern;/*** SQL 脚本转换为 drawio ER 脚本*/ pu…

【C++指南】函数重载:多态性的基石

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《C指南》 期待您的关注 目录 引言 一、函数重载的概念 二、函数重载的原理 三、函数重载的应用场景 四、函数重载的规则 五…

使用 Vue 官方脚手架初始化 Vue3 项目

Vite 官网&#xff1a;https://cn.vitejs.dev/ Vue 官网&#xff1a;https://vuejs.org/ Vue 官方文档&#xff1a;https://cn.vuejs.org/guide/introduction.html Element Plus 官网&#xff1a;https://element-plus.org/ Tailwind CSS 官网&#xff1a;https://tailwindcss.…

Xilinx课程,就这么水灵灵地上线了~

如果你想了解&#xff1a; 如何利用精通流水线&#xff08;Pipeline&#xff09;技术&#xff0c;让电路设计效率倍增&#xff1f; 如何掌握利用性能基线指导设计流程的方法&#xff1f; 如何理解集成电路设计中的UltraFast Design Methodology Implementation设计方法学中的…

100 Exercises To Learn Rust 挑战!准备篇

公司内部的学习会非常活跃&#xff01;我也参与了Rust学习会&#xff0c;并且一直在研究rustlings。最近&#xff0c;我发现了一个类似于rustlings的新教程网站&#xff1a;Welcome - 100 Exercises To Learn Rust。 rustlings是基于Rust的权威官方文档《The Rust Programming…

docker技术中docker-compose与harbor技术

docker-composeharbor docker网络概念 当大规模使用docker时&#xff0c;容器间通信就成了一个问题。 docker支持的四种网络模式在run时指定 host模式 --nethost 容器和宿主机共享一个网络命名空间 container模式 --net{容器id} 多个容器共享一个网络 none模式 --netnone …

【深度学习】TTS,CosyVoice,推理部署的代码原理讲解分享

文章目录 demo代码加载配置文件speech_tokenizer_v1.onnx(只在zero_shot的时候使用)campplus.onnx(只为了提取说话人音色embedding)`campplus_model` 的作用代码解析具体过程解析总结示意图CosyVoiceFrontEndCosyVoiceModel推理过程总体推理过程推理速度很慢: https://git…

基于Python爬虫+机器学习的长沙市租房价格预测研究

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…