FFMEPEG+SDL2 多路音频采集播放

最近在做播放器软件,接触到FFMEPEG的采集部分,中间也走了很多弯路,记录下,也给后来者插个眼

首先,初始化FFMEPEG和SDL,我这里使用的方法是视频数据上抛到接口,音频数据则通过SDL直接播放

FFMEPEG及SDL初始化

	int initFFmpeg() {
		//初始化设备号
		if (!pAVFormatContext) {
			pAVFormatContext = avformat_alloc_context();	//申请一个AVFormatContext结构的内存,并进行简单初始化
		}

		pAVFormatContext->interrupt_callback.callback = decode_interrupt_cb;
		pAVFormatContext->interrupt_callback.opaque = this;

		if (!pAVFrame) {
			pAVFrame = av_frame_alloc();
		}

		AVDictionary *avdic = NULL;
		//av_dict_set(&avdic, "bufsize", "2000k", 0);
		if (rtspTcp_) {
			av_dict_set(&avdic, "rtsp_transport", "tcp", 0);
		}

		lasttime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();

		//打开视频流
		int result = avformat_open_input(&pAVFormatContext, url_.c_str(), NULL, &avdic);
		if (avdic) {
			av_dict_free(&avdic);
			avdic = NULL;
		}
		if (result < 0) {
			//"打开视频流失败";
			return -1;
		}

		//获取视频流信息
		result = avformat_find_stream_info(pAVFormatContext, NULL);
		if (result < 0) {
			//"获取视频流信息失败";
			return -2;
		}

		av_dump_format(pAVFormatContext, 0, url_.c_str(), 0);

		//获取视频流索引
		videoStreamIndex = -1;
		for (int i = 0; i < pAVFormatContext->nb_streams; i++) {
			if (pAVFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
				videoStreamIndex = i;
				break;
			}
		}

		if (videoStreamIndex == -1) {
			//"获取视频流索引失败";
			return -3;
		}

		//获取视频流的分辨率大小
		pAVCodecContext = avcodec_alloc_context3(NULL);
		if (!pAVCodecContext)
		{
			return -4;
		}
		result = avcodec_parameters_to_context(pAVCodecContext, pAVFormatContext->streams[videoStreamIndex]->codecpar);
		if (result < 0) {
			return -5;
		}
		videoWidth = pAVCodecContext->width;
		videoHeight = pAVCodecContext->height;

		mtx_.lock();
		frame_.width = videoWidth;
		frame_.height = videoHeight;
		frame_.fmt = VideoFormat::VIDEO_FMT_I420;
		mtx_.unlock();

		//获取视频流解码器
		pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);

		//打开对应解码器
		result = avcodec_open2(pAVCodecContext, pAVCodec, NULL);
		if (result < 0) {
			//打开解码器失败
			return -6;
		}

		audioStreamIndex = -1;
		for (int i = 0; i < pAVFormatContext->nb_streams; i++) {
			if (pAVFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
				audioStreamIndex = i;
				break;
			}
		}

		//将音频流信息拷贝到新的AVCodecContext
		AVCodecContext* pCodecCtxOrg = nullptr;
		pCodecCtxOrg = pAVFormatContext->streams[audioStreamIndex]->codec; // codec context
		// 找到audio stream的 decoder
		pAuCodec = avcodec_find_decoder(pCodecCtxOrg->codec_id);
		if (!pAuCodec)
		{
			cout << "Unsupported codec!" << endl;
			return -1;
		}
		// 不直接使用从AVFormatContext得到的CodecContext,要复制一个
		pAUCodecContext = avcodec_alloc_context3(pAuCodec);
		if (avcodec_copy_context(pAUCodecContext, pCodecCtxOrg) != 0)
		{
			cout << "Could not copy codec context!" << endl;
			return -1;
		}
		result = avcodec_open2(pAUCodecContext, pAuCodec, NULL);
		if (result < 0) {
			//打开解码器失败
			return -6;
		}

		//SDL初始化
		if (SDL_Init(SDL_INIT_AUDIO))
		{
			return -1;
		}
		//重采样SwrContext初始化
		resampler = swr_alloc_set_opts(NULL,
			pAUCodecContext->channel_layout,
			AV_SAMPLE_FMT_S16,
			44100,
			pAUCodecContext->channel_layout,
			pAUCodecContext->sample_fmt,
			pAUCodecContext->sample_rate,
			0,
			NULL);
		swr_init(resampler);
		//打开设备,使用的时SDL_OpenAudioDevice而不是SDL_OpenAudio.为了后续的音频设备采样播出
		//其中SDL_AudioSpec的回调callback设置为空,由系统自动采集音频数据并播出,而不是自己处理
		SDL_AudioSpec want, have;
		SDL_zero(want);
		SDL_zero(have);
		want.freq = 44100;
		want.channels = pAUCodecContext->channels;
		want.format = AUDIO_S16SYS;
		audioDeviceNum = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
		SDL_PauseAudioDevice(audioDeviceNum, 0);

		return 0;
	}

 其中包含了FFMEPEG对音频和视频数据的初始化内容,这部分与其他文章并无不同,就略过了

SDL初始化,我只是用了音频部分,需要注意的点,主要实在打开音频设备这一步上,

在SDL2中,共提供了两套用于播放音频数据的API:

SDL_AudioSpec打开音频设备数据播放数据填充
第一套callback = mCallbackSDL_OpenAudioSDL_PauseAudioSDL_AudioCallback
第二套callback = NULLSDL_OpenAudioDeviceSDL_PauseAudioDeviceSDL_QueueAudio

两套API,同时使用时混音处理会出现问题,无法正常播放,若有信心解决,可混用,否则不建议

区别:

单路流时,没有显著区别,均可正常使用,其中第一套需手动完成回调部分,实现数据传入音频设备,写入时,需调用SDL_MixAudio

多路流时,SDL_OpenAudio只能打开一次,再次调用无条件返回错误,所以除第一次打开外,后续流仍需调用SDL_OpenAudioDevice,且同时存在时,SDL_PauseAudio和SDL_CloseAudio调用会对其他对应的音频设备造成影响。

数据填充:

我用的是第二套方案,以实现多路音频流播放,如需第一套实现,请自行查找

void loop() {
		std::unique_lock<mutex> lck(loopMtx_);
		running_ = true;	//开始运行
		int result = initFFmpeg();
		if (result < 0) {
			//初始化失败
			running_ = false;
			return;
		}
		int ret = 0;
		AVFrame* frame = av_frame_alloc();
		AVFrame* audioframe = av_frame_alloc();
		while (running_)
		{
			uint64_t ts = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
			if (av_read_frame(pAVFormatContext, &avPacket) >= 0) {
				if (avPacket.stream_index == videoStreamIndex) 
                {
                    //视频部分忽略
				}
				else if (avPacket.stream_index == audioStreamIndex)
				{
					AVStream* stream = pAVFormatContext->streams[avPacket.stream_index];
					if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
						ret = avcodec_send_packet(pAUCodecContext, &avPacket);
						while (ret >= 0) {
							ret = avcodec_receive_frame(pAUCodecContext, frame);
							if (ret >= 0) {
								int dst_samples = frame->channels * av_rescale_rnd(
									swr_get_delay(resampler, frame->sample_rate)
									+ frame->nb_samples,
									44100,
									frame->sample_rate,
									AV_ROUND_UP);
								uint8_t* audiobuf = NULL;
								ret = av_samples_alloc(&audiobuf,
									NULL,
									1,
									dst_samples,
									AV_SAMPLE_FMT_S16,
									1);
								dst_samples = frame->channels * swr_convert(
									resampler,
									&audiobuf,
									dst_samples,
									(const uint8_t**)frame->data,
									frame->nb_samples);
								ret = av_samples_fill_arrays(audioframe->data,
									audioframe->linesize,
									audiobuf,
									1,
									dst_samples,
									AV_SAMPLE_FMT_S16,
									1);
								SDL_QueueAudio(audioDeviceNum,
									audioframe->data[0],
									audioframe->linesize[0]);
							}
						}
					}
				}
			}
			av_packet_unref(&avPacket);//释放资源,否则内存会一直上升
		}
	}
};

 通过FFMEPEG获取音频数据frame,在通过swr重采样生成新的audioframe ,最后用SDL_QueueAudio填充,直接缓存进播放设备。

备注:

        懒得去扒重复代码,要复用的同学可以先参考下其他的实现方案,修改初始化和读取部分的代码

总结:

国内能找到的SDL文档,主要是采用的第一套方法,单路流时使用正常,无非就是手动实现回调时繁琐了些。但是多路流的播放相关的资料很少,我也是看到一些线索后在谷歌上找到了相关的开发资料。实验发现可行,实现上简单了很多,多路音频播放正常。

记录下,也为在学习SDL 的同学多留点可用资料

参考地址:SDL2:第五个程序:播放pcm数据_SuperLi-CSDN博客_sdl_queueaudio

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Qt和SDL是两种不同的程序库,均可用于实现音频播放。下面分别介绍一下在这两种程序库中实现音频播放的方法: Qt通过QMediaPlayer类来实现音频播放。QMediaPlayer类是Qt Multimedia模块的一部分,它可以播放各种媒体文件,包括音频和视频。在使用QMediaPlayer类时,需要先创建一个QMediaPlayer对象,然后调用setMedia方法设置音频文件的路径,最后调用play方法来开始播放音频文件。例如,以下代码可以播放一个MP3文件: ```c++ #include <QtMultimedia/QMediaPlayer> int main(int argc, char *argv[]) { QMediaPlayer player; player.setMedia(QUrl::fromLocalFile("/path/to/file.mp3")); player.play(); return 0; } ``` SDL也提供了音频播放功能。具体来说,可以通过SDL音频子系统来实现。在使用SDL音频子系统时,需要先调用SDL_Init函数初始化音频子系统,然后设置音频参数,注册回调函数,最后在回调函数中处理音频数据。以下是一个简单的示例代码,可以播放WAV文件: ```c #include <SDL2/SDL.h> // 回调函数,用于处理音频数据 void audio_callback(void *userdata, Uint8 *stream, int len) { static Uint32 pos = 0; // 当前播放位置 SDL_memcpy(stream, userdata + pos, len); pos += len; } int main(int argc, char *argv[]) { SDL_Init(SDL_INIT_AUDIO); SDL_AudioSpec wav_spec; Uint8 *wav_data; Uint32 wav_length; // 加载WAV文件 SDL_LoadWAV("/path/to/file.wav", &wav_spec, &wav_data, &wav_length); // 设置音频参数 SDL_AudioSpec want, have; SDL_memset(&want, 0, sizeof(want)); want.freq = wav_spec.freq; want.format = AUDIO_S16SYS; want.channels = wav_spec.channels; want.samples = 1024; want.callback = audio_callback; want.userdata = wav_data; // 打开音频设备并开始播放 SDL_OpenAudio(&want, &have); SDL_PauseAudio(0); // 等待音频播放结束 SDL_Delay(wav_length/wav_spec.freq*1000); // 关闭音频设备和释放资源 SDL_CloseAudio(); SDL_FreeWAV(wav_data); return 0; } ``` 综上所述,无论是Qt还是SDL都可以实现音频播放。要根据具体情况选择适合自己的程序库和方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值