自定义播放器系列

第一章 视频渲染第二章 音频(push)播放第三章 音频(pull)播放(本章)第四章 实现时钟同步第五章 实现通用时钟同步第六章 实现播放器

前言

我们上一章讲了,ffmpeg解码SDL push的方式播放音频,调用流程简单,但是实现起来还是有点难度的。接下来讲的就是使用pull的方式播放音频,pull的方式即是使用回调的方式播放音频,在打开SDL音频设备的时候传入一个回调方法,SDL内部会按照一定频率回调这个方法,我们在回调方法中往音频缓冲写数据就能够播放声音了。

一、FFmpeg解码

ffmpeg解码部分与《使用ffmpeg解码音频sdl(push)播放》一致,这里就不再赘述。

二、sdl播放1、初始化sdl

使用sdl前需要在最开始初始化sdl,全局只需要初始化一次即可。

if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) { printf("Could not initialize SDL - %s\n", SDL_GetError()); return -1; }

2、打开音频设备

建议使用SDL_OpenAudioDevice打开设备,使用SDL_OpenAudio的话samples设置可能不生效,比较不灵活一点。pull的方式播放其实就是采用回调的方式播放,我们给下面代码的wanted_spec.callback赋值即可。

SDL_AudioSpec wanted_spec, spec; int audioId = 0; //打开设备 wanted_spec.channels = av_get_channel_layout_nb_channels(pCodecCtx->channel_layout); wanted_spec.freq = pCodecCtx->sample_rate; wanted_spec.format = AUDIO_F32SYS; wanted_spec.silence = 0; wanted_spec.samples = FFMAX(512, 2 << av_log2(wanted_spec.freq / 30)); //注册回调方法 wanted_spec.callback = audioCallback; wanted_spec.userdata = NULL; audioId = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, 1); if (audioId < 2) { printf("Open audio device error!\n"); goto end; } //开启播放 SDL_PauseAudioDevice(audioId, 0);

3、播放(pull)

我们采用pull的方式播放,即注册回调方法,sdl会按照一定的频率触发回调,我们只需往回调参数的缓存指针写入音频数据即可。

//音频设备读取数据回调方法 void audioCallback(void* userdata, Uint8* stream, int len) { //TODO:从音频队列中读取数据到设备缓冲 };

4、销毁资源

使用完成后需要销毁资源,如下所示,SDL_Quit并不是必要的,通常是程序退出才需要调用,这个时候调不调已经无所谓了。

if (audioId >= 2) { SDL_PauseAudioDevice(audioId, 1); SDL_CloseAudioDevice(audioId); } SDL_Quit();

三、使用AVAudioFifo

由于是采用回调的方式播放,必然需要一个队列往里面写入解码的数据,音频设备回调时再将数据读出。我们直接使用ffmpeg提供的音频队列即可。

1、初始化

AVAudioFifo是基于采样数的,所以初始化的时候需要设置音频格式以及队列长度采样数。直接使用音频设备的参数,队列长度要稍微长一点。

//使用音频队列 AVAudioFifo *fifo = av_audio_fifo_alloc(forceFormat, spec.channels, spec.samples * 10);

2、写入数据

我们需要解码处往队列里写入数据

//解码后的音频数据 uint8_t* data; //音频数据的采样数 size_t samples; if (av_audio_fifo_space(fifo) >= samples) { av_audio_fifo_write(fifo, (void**)&data, samples); }

3、读取数据

在音频设备回调中读取队列中的音频数据

void audioCallback(void* userdata, Uint8* stream, int len) { //从音频队列中读取数据到设备缓冲 av_audio_fifo_read(fifo, (void**)&stream, spec.samples); };

4、销毁资源

if (fifo) { av_audio_fifo_free(fifo); }

相关学习资料推荐,点击下方链接免费报名,先码住不迷路~】

音视频免费学习地址:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发

【免费分享】音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C ,linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击788280672加群免费领取~

使用ffmpeg分离rtmp音频(使用ffmpeg解码音频sdlpull播放)(1)

四、完整代码1、代码

将上述代码合并起来形成一个完整的音频解码播放流程:示例的sdk版本:ffmpeg 4.3、sdl2windows、linux都可以正常运行

#include <stdio.h> #include <SDL.h> #include "libavformat/avformat.h" #include "libavcodec/avcodec.h" #include "libswscale/swscale.h" #include "libswresample/swresample.h" #include "libavutil/audio_fifo.h" #undef main AVAudioFifo* fifo = NULL; SDL_AudioSpec wanted_spec, spec; SDL_mutex* mtx = NULL; static void audioCallback(void* userdata, Uint8* stream, int len) { //由于AVAudioFifo非线程安全,且是子线程触发此回调,所以需要加锁 SDL_LockMutex(mtx); //读取队列中的音频数据 av_audio_fifo_read(fifo, (void**)&stream, spec.samples); SDL_UnlockMutex(mtx); }; int main(int argc, char** argv) { const char* input = "test_music.wav"; enum AVSampleFormat forceFormat; AVFormatContext* pFormatCtx = NULL; AVCodecContext* pCodecCtx = NULL; const AVCodec* pCodec = NULL; AVDictionary* opts = NULL; AVPacket packet; AVFrame* pFrame = NULL; struct SwrContext* swr_ctx = NULL; uint8_t* outBuffer = NULL; int audioindex = -1; int exitFlag = 0; int isLoop = 1; double framerate; int audioId = 0; memset(&packet, 0, sizeof(AVPacket)); //初始化SDL if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) { printf("Could not initialize SDL - %s\n", SDL_GetError()); return -1; } //打开输入流 if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) { printf("Couldn't open input stream.\n"); goto end; } //查找输入流信息 if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { printf("Couldn't find stream information.\n"); goto end; } //获取音频流 for (unsigned i = 0; i < pFormatCtx->nb_streams; i ) if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { audioindex = i; break; } if (audioindex == -1) { printf("Didn't find a audio stream.\n"); goto end; } //创建解码上下文 pCodecCtx = avcodec_alloc_context3(NULL); if (pCodecCtx == NULL) { printf("Could not allocate AVCodecContext\n"); goto end; } //获取解码器 if (avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[audioindex]->codecpar) < 0) { printf("Could not init AVCodecContext\n"); goto end; } pCodec = avcodec_find_decoder(pCodecCtx->codec_id); if (pCodec == NULL) { printf("Codec not found.\n"); goto end; } //使用多线程解码 if (!av_dict_get(opts, "threads", NULL, 0)) av_dict_set(&opts, "threads", "auto", 0); //打开解码器 if (avcodec_open2(pCodecCtx, pCodec, &opts) < 0) { printf("Could not open codec.\n"); goto end; } if (pCodecCtx->sample_fmt == AV_SAMPLE_FMT_NONE) { printf("Unknown sample foramt.\n"); goto end; } if (pCodecCtx->sample_rate <= 0 || pFormatCtx->streams[audioindex]->codecpar->channels <= 0) { printf("Invalid sample rate or channel count!\n"); goto end; } //打开设备 wanted_spec.channels = pFormatCtx->streams[audioindex]->codecpar->channels; wanted_spec.freq = pCodecCtx->sample_rate; wanted_spec.format = AUDIO_F32SYS; wanted_spec.silence = 0; wanted_spec.samples = FFMAX(512, 2 << av_log2(wanted_spec.freq / 30)); wanted_spec.callback = audioCallback; wanted_spec.userdata = NULL; audioId = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, 1); if (audioId < 2) { printf("Open audio device error!\n"); goto end; } switch (spec.format) { case AUDIO_S16SYS: forceFormat = AV_SAMPLE_FMT_S16; break; case AUDIO_S32SYS: forceFormat = AV_SAMPLE_FMT_S32; break; case AUDIO_F32SYS: forceFormat = AV_SAMPLE_FMT_FLT; break; default: printf("audio device format was not surported!\n"); goto end; break; } pFrame = av_frame_alloc(); if (!pFrame) { printf("alloc frame failed!\n"); goto end; } //使用音频队列 fifo = av_audio_fifo_alloc(forceFormat, spec.channels, spec.samples * 10); if (!fifo) { printf("alloc fifo failed!\n"); goto end; } mtx = SDL_CreateMutex(); if (!mtx) { printf("alloc mutex failed!\n"); goto end; } SDL_PauseAudioDevice(audioId, 0); start: while (!exitFlag) { //读取包 int gotPacket = av_read_frame(pFormatCtx, &packet) == 0; if (!gotPacket || packet.stream_index == audioindex) //!gotPacket:未获取到packet需要将解码器的缓存flush,所以还需要进一次解码流程。 { //发送包 if (avcodec_send_packet(pCodecCtx, &packet) < 0) { printf("Decode error.\n"); av_packet_unref(&packet); goto end; } //接收解码的帧 while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) { uint8_t* data; size_t dataSize; size_t samples; if (forceFormat != pCodecCtx->sample_fmt || spec.freq != pFrame->sample_rate || spec.channels != pFrame->channels) //重采样 { //计算输入采样数 int out_count = (int64_t)pFrame->nb_samples * spec.freq / pFrame->sample_rate 256; //计算输出数据大小 int out_size = av_samples_get_buffer_size(NULL, spec.channels, out_count, forceFormat, 0); //输入数据指针 const uint8_t** in = (const uint8_t**)pFrame->extended_data; //输出缓冲区指针 uint8_t** out = &outBuffer; int len2 = 0; if (out_size < 0) { av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n"); goto end; } if (!swr_ctx) //初始化重采样对象 { swr_ctx = swr_alloc_set_opts(NULL, av_get_default_channel_layout(spec.channels), forceFormat, spec.freq, av_get_default_channel_layout(pFormatCtx->streams[audioindex]->codecpar->channels) , pCodecCtx->sample_fmt, pCodecCtx->sample_rate, 0, NULL); if (!swr_ctx || swr_init(swr_ctx) < 0) { av_log(NULL, AV_LOG_ERROR, "swr_alloc_set_opts() failed\n"); goto end; } } if (!outBuffer) //申请输出缓冲区 { outBuffer = (uint8_t*)av_mallocz(out_size); } //执行重采样 len2 = swr_convert(swr_ctx, out, out_count, in, pFrame->nb_samples); if (len2 < 0) { av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n"); goto end; } //取得输出数据 data = outBuffer; //输出数据长度 dataSize = av_samples_get_buffer_size(0, spec.channels, len2, forceFormat, 1); samples = len2; } else { data = pFrame->data[0]; dataSize = av_samples_get_buffer_size(pFrame->linesize, pFrame->channels, pFrame->nb_samples, forceFormat, 0); samples = pFrame->nb_samples; } //写入队列 while (1) { SDL_LockMutex(mtx); if (av_audio_fifo_space(fifo) >= samples) { av_audio_fifo_write(fifo, (void**)&data, samples); SDL_UnlockMutex(mtx); break; } SDL_UnlockMutex(mtx); //队列可用空间不足则延时等待 SDL_Delay((dataSize) * 1000.0 / (spec.freq * av_get_bytes_per_sample(forceFormat) * spec.channels) - 1); } } } av_packet_unref(&packet); if (!gotPacket) { //循环播放时flush出缓存帧后需要调用此方法才能重新解码。 avcodec_flush_buffers(pCodecCtx); break; } } if (!exitFlag) { if (isLoop) { //定位到起点 if (avformat_seek_file(pFormatCtx, -1, 0, 0, 0, AVSEEK_FLAG_FRAME) >= 0) { goto start; } } } end: //销毁资源 if (fifo) { av_audio_fifo_free(fifo); } if (audioId >= 2) { SDL_PauseAudioDevice(audioId, 1); SDL_CloseAudioDevice(audioId); } if (mtx) { SDL_DestroyMutex(mtx); } if (pFrame) { if (pFrame->format != -1) { av_frame_unref(pFrame); } av_frame_free(&pFrame); } if (packet.data) { av_packet_unref(&packet); } if (pCodecCtx) { avcodec_close(pCodecCtx); avcodec_free_context(&pCodecCtx); } if (pFormatCtx) avformat_close_input(&pFormatCtx); if (pFormatCtx) avformat_free_context(pFormatCtx); swr_free(&swr_ctx); av_dict_free(&opts); if (outBuffer) av_free(outBuffer); SDL_Quit(); return 0; }

总结

以上就是今天要讲的内容,使用pull的方式播发音频比push简单很多,而且更加灵活可以继续实现更复杂的功能比如多路音频合并,使用push则难以实现。我们唯一要注意的就是保证线程安全,对队列的读写也非常简单,长度控制直接通过延时就可以做到。

原文 使用ffmpeg解码音频sdl(pull)播放_CodeOfCC的博客-CSDN博客

,