在当今的多媒体世界中,视频播放已成为不可或缺的一部分。从简单的媒体播放器到复杂的视频编辑软件,视频解码和显示技术无处不在。本示例使用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值,计算出对应的value
int 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();
// 计算rect
int 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 500
VideoPlayer::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的时候不能用break
if(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);
// 初始化SDL
ret = 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) {
// 释放pkt
av_packet_unref(&pkt);
return 0;
} else {
_aSeekTime = -1;
}
}
// 发送压缩数据到解码器
int ret = avcodec_send_packet(_aDecodeCtx, &pkt);
// 释放pkt
av_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);
// 释放pkt
av_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_NAMESPACE
class MainWindow : public QMainWindow {
Q_OBJECT
public:
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博客
标签:播放器,QT,int,void,VideoPlayer,player,ui,include,FFmpeg From: https://blog.csdn.net/u012959478/article/details/141086028