当前位置:   article > 正文

第十章:ffmpeg和QT开发播放器之音频库使用_qt+ffmpeg 音画同步

qt+ffmpeg 音画同步

写在前面:

    这部分对应视频课程中的5-1~5-8。实现了播放器的音频播放功能。

1、QT音频库介绍

       前面的课程教了如何播放视频,但对音频没有处理。

       需要播放音频的话,要生成一个对象QAudioOutput *out同时要包含它的头文件

       #include <QAudioOutput>,如何设定音频的参数?那么就需要在设置一个QAudioFormat fmt;通过它来指定音频输出的格式。

       fmt.setSampleRate(48000)    :表示一秒钟采集48000个音频数据,越大效果越好。
       fmt.setSampleSize(16)               :表示每个声音数据的大小(4、8、16)位,也就是样本大小。
       fmt.setChannelCount(2)            :表示双声道。

       fmt.setCodec("audio/pcm")       :表示音频格式,这里的pcm格式是未压缩普通格式。

       调用out = new QAudioOutput(fmt);创建输出流。打开音频输出之后,

       之后启动,调用start函数,回返回一个QIODevice 的接口。QIODevice *ad out->start();

       通过QIODevice 接口就可以向里面写入东西播放音频了ad->write(***);

2、封装音频播放类

                      

       首先将构造函数放进protected中,这样做的目的是音频只有一个播放类,所以使用protected,这样就不能随便创建XAudioPlay();构造函数了。只能由内部自己创建(单件模式)

       使用一种方法,隐藏一些信息,把类分装完成之后,使得调用接口的人不知道这个函数是如何实现的,这样将来在换其他方式音频播放时候,调用者就不需要在关注这些变化。所以将这些函数定义成纯虚函数,在继承类中实现这些接口。

       这样的话,我们就将CXAudioPlay做成了继承类。这样将来实现的方法就放在了继承类里面。CXAudioPlay则是我们的接口类就不用实现这些函数功能了。

       如果没有完全实现纯虚函数,就会变成一个抽象类,抽象类是不能实例化的,也就是不能实现对象。

  1. class XAudioPlay
  2. {
  3. public:
  4. XAudioPlay *Get();
  5. virtual bool Start() = 0;
  6. virtual void Play(bool isplay) = 0;
  7. virtual bool Write(const char *data, int datasize) = 0;
  8. virtual ~XAudioPlay();
  9. protected:
  10. XAudioPlay();
  11. };
  12. class CXAudioPlay : public XAudioPlay
  13. {
  14. public:
  15. bool Start()
  16. {
  17. return true;
  18. }
  19. void Play(bool isplay)
  20. {
  21. }
  22. bool Write(const char *data, int datasize)
  23. {
  24. return true;
  25. }
  26. };

3、实现音频播放的启动和停止接口

       启动的实现,流程大致是设置一个QAudioFormat参数,然后传入参数并start,start之后会返回一个QIODevice。
       每次重新打开一个视频的时候,需要stop,清除output。

  1. void Stop()
  2. {
  3. if (output)
  4. {
  5. output->stop();
  6. delete output;
  7. output = NULL;
  8. }
  9. }

4、实现音频播放的暂停

  1. void Play(bool isplay)
  2. {
  3. if (!output) return;
  4. if (isplay)
  5. {
  6. output->resume();//恢复播放
  7. }
  8. else
  9. {
  10. output->suspend();//暂停
  11. }
  12. }

5、实现音频播放缓冲接口的实现

       write函数的作用,由文件当中读取出音频数据来,在放到QAudio进行播放,但是播放是从缓冲区里面去读的,所以要判断缓冲区的大小。

  1. int GetFree()
  2. {
  3. if (!output) return 0;
  4. return output->bytesFree();//获取是否有足够的空间
  5. }
  6. bool Write(const char *data, int datasize)
  7. {
  8. if (io)
  9. io->write(data, datasize);
  10. return true;
  11. }

6、ffmpeg音频解码器打开

       对音频进行打开解码器,进行解码,重采样三步操作。

  1. if (enc->codec_type == AVMEDIA_TYPE_AUDIO)//然后是音频流
  2. {
  3. audioStream = i;
  4. AVCodec *codec = avcodec_find_decoder(enc->codec_id); //判断系统有没有这个音频解码器
  5. if (avcodec_open2(enc, codec, NULL) < 0)//打开失败
  6. {
  7. mutex.unlock();
  8. return false;
  9. }
  10. this->sampleRate = enc->sample_rate;
  11. this->channel = enc->channels;
  12. switch (enc->sample_fmt)
  13. {
  14. case AV_SAMPLE_FMT_S16:
  15. this->sampleSize = 16;
  16. break;
  17. case AV_SAMPLE_FMT_S32:
  18. this->sampleSize = 32;
  19. break;
  20. default:
  21. break;
  22. }
  23. printf("audio sample rate:%d sample size %d sample channel %d\n",
  24. this->sampleRate, this->sampleSize, this->channel);
  25. }

7、音频解码

       在ffmpeg老版本中的视频解码音频解码是用不同函数的,新版的则是使用同一个函数。但是这里会存在一个问题,视频和音频的解码后的数据存放在哪里?肯定不能存放在同个地方。因为视频和音频是两个不同线程,而且要同步播放,用同块内存肯定要出问题的。所以要给音频增加一个内存。

       AVFrame *pcm = NULL;

       加入音频之后还带来一个问题,我们在控制播放进度的时候,以前是以视频为准的 ,控制视频的播放速度,但音频则不行,会导致声音失真。那么之前的pts是存放视频的,现在就要改成存放音频的pts。同时改写之前的AVFrame *Decode(const AVPacket *pkt);函数让它直接返回pts

8、音频重采样

       音频解码之后可以直接播放,但更多时候,解码出来的音频格式并不一定满足当前机器播放的要求。这里需要重采样。例如32位改成16位的或者是通道数2改1.

       引用头文件#include <libswresample/swresample.h>

       再引用重采样的库文件#pragma comment(lib,"swresample.lib")。

       音频解码完之后,如果同段声音放两次会出现问题,所以要保证解码函数和重采样函数在同个线程就好了。

       SwrContext *aCtx = NULL; //创建音频转码器的容器
  1. struct SwrContext *swr_alloc_set_opts //设置属性
  2. (struct SwrContext *s, //
  3. int64_t out_ch_layout, //表示输出这个通道类型,比如2.1声道、5.1声道这种
  4. enum AVSampleFormat out_sample_fmt, //表示输出的采样大小,
  5. int out_sample_rate, //表示输出的采样率
  6. int64_t in_ch_layout, //表示输入的通道类型
  7. enum AVSampleFormat in_sample_fmt, //表示输入的采样大小
  8. int in_sample_rate, //表示输入的采样率
  9. int log_offset, //表示偏移
  10. void *log_ctx //暂时不了解
  11. );

       ffmpeg的音频处理申请空间函数struct SwrContext *swr_alloc(void);;
       ffmpeg的音频处理释放空间函数void swr_free(struct SwrContext **s);;

  1. int swr_convert( //设置输出空间
  2. struct SwrContext *s, //打开的重采样设置
  3. uint8_t **out, //输出的地方
  4. int out_count, //输出的采样数量的大小,要保证足够大
  5. const uint8_t **in , //输入的数据
  6. int in_count //输入的数据大小
  7. );
  8. //返回值:每一个通道的样本数量,如果小于0就是失败了。

       外部调用该函数的地方是想知道重采样了多少字节使用av_samples_get_buffer_size函数实现

  1. int av_samples_get_buffer_size(
  2. int *linesize,
  3. int nb_channels, //样本通道
  4. int nb_samples, //样本数量
  5. enum AVSampleFormat sample_fmt, //样本的类型
  6. int align //
  7. );
  1. int XFFmpeg::ToPCM(char *out)
  2. {
  3. mutex.lock();
  4. if (!ic || !pcm || !out)
  5. {
  6. mutex.unlock();
  7. return 0;
  8. }
  9. AVCodecContext *ctx = ic->streams[audioStream]->codec;
  10. if (aCtx == NULL)
  11. {
  12. aCtx = swr_alloc();
  13. swr_alloc_set_opts(aCtx, ctx->channel_layout, //设置属性
  14. AV_SAMPLE_FMT_S16,
  15. ctx->sample_rate, ctx->channels,
  16. ctx->sample_fmt,
  17. ctx->sample_rate,
  18. 0,0);
  19. swr_init(aCtx); //初始化
  20. }
  21. uint8_t *data[1];
  22. data[0] = (uint8_t *)out;
  23. int len = swr_convert(aCtx, data, 10000,
  24. (const uint8_t **)pcm->data,pcm->nb_samples);//设置输出空间
  25. if (len <= 0)
  26. {
  27. mutex.unlock();
  28. return 0;
  29. }
  30. int outsize = av_samples_get_buffer_size(NULL,ctx->channels, pcm->nb_samples,
  31. AV_SAMPLE_FMT_S16,0);
  32. mutex.unlock();
  33. return outsize;
  34. }

8、完成音视频播放

       首先记得对音频处理函数做互斥。

       音频读取速率也需要控制,也不能够一直读取音频数据来解码。

       音频缓冲区:音频在播放的时候会不断从缓冲区里面读取,读一帧在删除一帧,如果空间满的时候,就没办法写入了,这样就会造成声音的失真。

  1. int free = XAudioPlay::Get()->GetFree();//读取音频缓冲区的剩余空间
  2. if (free < 10000) //空间不足的话 就不要往缓冲区里面写数据了。
  3. {
  4. msleep(1);
  5. continue;
  6. }
  7. if (pkt.stream_index == XFFmpeg::Get()->audioStream)//如果是音频流,
  8. {
  9. XFFmpeg::Get()->Decode(&pkt);//解码音频,Decode内部做了音视频的判断
  10. av_packet_unref(&pkt);//释放空间
  11. int len = XFFmpeg::Get()->ToPCM(acm_out);//重采样
  12. XAudioPlay::Get()->Write(acm_out,len); //播放音频
  13. continue;
  14. }

9、通过多线程和缓冲队列实现音频同步

       目前音视频播放都挺正常的,但是如果播放网络流视频,比如rtsp、rtmp这类网络流的,就很有可能造成不同步现象,

       解决方法:现将视频缓存起来,判断视频和音频是否同步,不同步就等一下再播放。

       使用std的list来缓存视频。

       using namespace std;

       static list<AVPacket> videos;

       这里不能直接拿音频和视频的pts做比较,因为他们两个的timebase不同,所以必须都转换成毫秒才能进行对比。
  1. videos.push_back(pkt); //入栈
  2. videos.front();//出栈
  1. while (videos.size() > 0)
  2. {
  3. AVPacket pack = videos.front();//出栈
  4. int pts = XFFmpeg::Get()->GetPts(&pack);//获取视频的毫秒数
  5. if (pts > apts) //当视频播放进度小于音频的时候才解码
  6. {
  7. break;
  8. }
  9. XFFmpeg::Get()->Decode(&pack);
  10. av_packet_unref(&pack);//释放空间
  11. videos.pop_front();
  12. }





声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号