32_音视频播放器_SDL播放
一、简介
接着上节的音频解码,使用SDL播放音频。
通过上节程序运行打印发现这些音频信息明显不符合SDL的,所以我们需要进行重采样
二、音频重采样
这里我们可以参考之前的《12_采样格式&音频重采样》来实现现在的重采样。
2.1 引入头文件
extern "C" { #include <libswresample/swresample.h> }
还需要在pro
文件中引入swresample
库
LIBS += -L $${FFMPEG_HOME}/lib \ -lavcodec \ -lavformat \ -lavutil \ -lswresample
2.2 定义重采样相关属性
/******** 音频相关 ********/ typedef struct { int sampleRate; AVSampleFormat sampleFmt; int chLayout; int chs; int bytesPerSampleFrame; } AudioSwrSpec; /** 音频重采样上下文 */ SwrContext *_aSwrCtx = nullptr; /** 音频重采样输入\输出参数 */ AudioSwrSpec _aSwrInSpec, _aSwrOutSpec; /** 音频重采样输入\输出frame */ AVFrame *_aSwrInFrame = nullptr, *_aSwrOutFrame = nullptr; /** 音频重采样输出PCM的索引(从哪个位置开始取出PCM数据填充到SDL的音频缓冲区) */ int _aSwrOutIdx = 0; /** 音频重采样输出PCM的大小 */ int _aSwrOutSize = 0; /** 初始化音频重采样 */ int initSwr();
2.3初始化重采样
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; } return 0; }
在initAudioInfo
方法中调用initSwr
方法
int VideoPlayer::initAudioInfo() { int ret = initDecoder(&_aDecodeCtx,&_aStream,AVMEDIA_TYPE_AUDIO); RET(initDecoder); // 初始化音频重采样 ret = initSwr(); RET(initSwr); // 初始化SDL ret = initSDL(); RET(initSDL); return 0; }
2.4 重采样
上面进行了重采样的初始化后,现在我们可以在解码出来的PCM进行重采样
int VideoPlayer::decodeAudio(){ ...... // 重采样输出的样本数 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; }
swr_convert
函数的参数解释:
int swr_convert(struct SwrContext *s, uint8_t **out, int out_count, const uint8_t **in , int in_count);
- 参数1:重采样上下文
- 参数2:输出到什么地方,这里我们希望输出到
_aSwrOutFrame->data
,到时候可以直接通过_aSwrOutFrame->data[0]
拿到它指向的PCM数据,如果是Planar格式data[0]
指向第一个声道,data[1]
指向下一个声道,但是这里最终重采样出来的数据是非Planar,是s16的所以这里可以直接通data[0]
拿到PCM数据
- 参数3:希望输出多少个样本,
outSamples = outSampleRate * inSamples / inSampleRate
,可以直接使用ffmpeg的av_rescale_rnd
函数得到。 - 参数4:输入数据,可以使用
_aSwrInFrame->data
- 参数5:重采样输入数据里面包含多少个样本,可以使用
_aSwrInFrame->nb_samples
,这个值不一定是固定的, - 返回值:真正转换成功的样本,也就是每一个声道的样本数。
如果此时你运行代码会出现内存错误,这是因为重采样时,_aSwrOutFrame->data
的data[0]
未分配空间,所以需要在初始化重采样的地方给data[0]
分配空间
// 初始化重采样的输出frame的data[0]空间 ret = av_samples_alloc(_aSwrOutFrame->data, _aSwrOutFrame->linesize, _aSwrOutSpec.chs, 4096, _aSwrOutSpec.sampleFmt, 1); RET(av_samples_alloc);
三、SDL播放
上面实现了重采样,那么现在我们需要把重采样的数据填充到回调函数sdlAudioCallback
的stream
里面。
void VideoPlayer::sdlAudioCallback(Uint8 *stream, int len){ // 清零(静音) SDL_memset(stream, 0, len); // len:SDL音频缓冲区剩余的大小(还未填充的大小) while (len > 0) { if (_state == Stopped) 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); // 填充SDL缓冲区 SDL_MixAudio(stream, _aSwrOutFrame->data[0] + _aSwrOutIdx, fillLen, SDL_MIX_MAXVOLUME); // 移动偏移量 len -= fillLen; stream += fillLen; _aSwrOutIdx += fillLen; } }
SDL_MixAudio
函数解释:
extern DECLSPEC void SDLCALL SDL_MixAudio(Uint8 * dst, const Uint8 * src, Uint32 len, int volume);
- 参数1:填充的目的地
- 参数2:数据的源头,就是PCM从那个地方开始
- 参数3:填充数据的长度,需要填充多少数据
- 参数4:音量大小
也就是把src这个位置开始的多少个数据len填入到dst里面去
各个字段解释:
_aSwrOutSize
:表面这次重采样PCm的大小
fillLen = _aSwrOutSize - _aSwrOutIdx
:需要填充到stream
中的PCM数据大小,减去_aSwrOutIdx
主要是用于一次采样PCM大小大于了stream
的缓冲区。
len -= fillLen
:SDL音频缓冲区剩余的大小(还未填充的大小)
stream += fillLen
:跳过刚刚已经填充的大小
_aSwrOutIdx += fillLen
:跳过刚刚已经填充的大小
如果_aSwrOutIdx >= _aSwrOutSize
说明PCM所有内容都已经拷贝到stream里面了,此时的PCM数据已经没有利用价值了,这个时候就得解码下一个pkt
,获取新的PCM数据,此时_aSwrOutIdx
就需要清零。如果没有解码出PCM数据,那就静音处理(_aSwrOutSize = 1024
是经验值)。
四、停止功能
首先需要修改videoplayer.cpp
中play
方法,在读取文件时判断当前状态释放时停止状态。
void VideoPlayer::play() { if (_state == Playing) return; // 状态可能是:暂停、停止、正常完毕 if(_state == Stopped){ // 开始线程:读取文件 std::thread([this](){ readFile(); }).detach();// detach 等到readFile方法执行完,这个线程就会销毁 setState(Playing); } }
在videoplayer.h
新增释放资源的方法
/** 释放资源 */ void free(); void freeAudio(); void freeVideo();
在videoplayer.cpp
中释放公共的一些资源
void VideoPlayer::free(){ avformat_close_input(&_fmtCtx); freeAudio(); freeVideo(); }
现在主要是释放音频相关的资源
void VideoPlayer::freeAudio(){ _aSwrOutIdx = 0; _aSwrOutSize =0; 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(); }
在解码音频的方法decodeAudio
中,还需要判断状态释放为停止状态,因为,一执行此方法就加锁了,就会再次阻塞等待,等到后终于可以拿到锁了,但是在我们等待期间有可能就被我们关掉了,此时就会出现问题,因此这里还需要在判断一下状态_state == Stopped
int VideoPlayer::decodeAudio(){ // 加锁 _aMutex->lock(); if (_aPktList->empty() || _state == Stopped) { _aMutex->unlock(); return 0; } ...... }
在videoplayer.cpp
的读取文件的while
循环中也要判断释放为停止状态
while (true) { if(_state == Stopped) break; AVPacket pkt; 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{ continue; } }
我们之前分装好的END
的宏函数最后是goto
去是释放资源,现在我们直接调用free
方法就可以了
#define END(func) \ if (ret < 0) { \ ERROR_BUF; \ qDebug() << #func << "error" << errbuf; \ setState(Stopped); \ emit playFailed(this); \ free(); \ return; \ }
// 初始化音频信息 bool hasAudio = initAudioInfo() >= 0; // 初始化视频信息 bool hasVideo = initVideoInfo() >= 0; if (!hasAudio && !hasVideo) { emit playFailed(this); free(); return; }
五、处理读完音频包的情况
while (_state != Stopped) { AVPacket pkt; 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 if (ret == AVERROR_EOF) { // 读到了文件的尾部 qDebug() << "已经读取到文件尾部"; break; } else { ERROR_BUF; qDebug() << "av_read_frame error" << errbuf; continue; } }
六、实现调节音量
七、实现静音功能
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!