首页 > 其他分享 >ffmpeg 之 sdl

ffmpeg 之 sdl

时间:2023-08-16 11:25:29浏览次数:45  
标签:pCodecCtx ffmpeg packet SDL av sdl pFrame NULL

使用ffmpeg解码视频渲染到sdl窗口

前言

使用ffmpeg解码视频并渲染视频到窗口,网上是有不少例子的,但是大部分例子的细节都不是很完善,比如资源释放、flush解码缓存、多线程优化等都没有。特别是想要快速搭建一个demo时,总是要重新编写不少代码,比较不方便,所以在这里提供一个完善的例子,可以直接拷贝拿来使用。

一、ffmpeg解码

ffmpeg解码的流程是比较经典且通用的,基本上是文件、网络流、摄像头都是一模一样的流程。

1、打开输入流

首先需要打开输入流,输入流可以是文件、rtmp、rtsp、http等。

AVFormatContext* pFormatCtx = NULL;
const char* input="test.mp4";
//打开输入流
avformat_open_input(&pFormatCtx, input, NULL, NULL) ;
//查找输入流信息
avformat_find_stream_info(pFormatCtx, NULL) ; 

2、查找视频流

因为是渲染视频,所以需要找到输入流中的视频流。通过遍历判断codec_type 为AVMEDIA_TYPE_VIDEO值的视频流。视频流有可能有多个的,这里我们取第一个。

//视频流的下标
int	 videoindex = -1;
for (unsigned i = 0; i < pFormatCtx->nb_streams; i++)
	if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
		videoindex = i;
		break;
	}

3、打开解码器

通过输入流的信息获取到解码器参数然后查找到响应解码器,最后打开解码器即可。

AVCodecContext* pCodecCtx = NULL;
const AVCodec* pCodec = NULL;
//初始化解码上下文
pCodecCtx=avcodec_alloc_context3(NULL);
//获取解码参数
avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoindex]->codecpar) 
//查找解码器
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
//打开解码器
avcodec_open2(pCodecCtx, pCodec, &opts)

打开解码器时可以使用多线程参数优化解码速度。

AVDictionary* opts = NULL;
//使用多线程解码
if (!av_dict_get(opts, "threads", NULL, 0))
	av_dict_set(&opts, "threads", "auto", 0);

4、解码

解码的流程就是读取输入流的包,对包进行解码,获取解码后的帧。

AVPacket packet;
AVFrame* pFrame = av_frame_alloc();
//读取包
while (av_read_frame(pFormatCtx, &packet) == 0)
{   //发送包
	avcodec_send_packet(pCodecCtx, &packet);
	//接收帧
	while (avcodec_receive_frame(pCodecCtx, pFrame) == 0)
	{
	   //取得解码后的帧pFrame
	   
	   av_frame_unref(pFrame);
	}
	av_packet_unref(&packet);
}

解码有个细节是需要注意的,即当av_read_frame到文件尾结束后,需要再次调用avcodec_send_packet传入NULL或者空包flush出里面的缓存帧。下面是完善的解码流程

while (1)
{
	int gotPacket = av_read_frame(pFormatCtx, &packet) == 0;
	if (!gotPacket || packet.stream_index == videoindex)
		//!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) {
          //取得解码后的帧pFrame
          
          av_frame_unref(pFrame);
		}
	}
	av_packet_unref(&packet);
	if (!gotPacket)
		break;
}

5、重采样

当遇到像素格式或分辨率与输出目标不一致时,就需要进行重采样了,重采样通常放在解码循环中。

struct SwsContext* swsContext = NULL;
enum AVPixelFormat forceFormat = AV_PIX_FMT_YUV420P;
uint8_t* outBuffer = NULL;
uint8_t* dst_data[4];
int dst_linesize[4];
if (forceFormat != pCodecCtx->pix_fmt)
{
  swsContext = sws_getCachedContext(swsContext, pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, forceFormat, SWS_FAST_BILINEAR, NULL, NULL, NULL);
  if (!outBuffer)
	  outBuffer = av_malloc(av_image_get_buffer_size(forceFormat, pCodecCtx->width, pCodecCtx->height, 64));
  av_image_fill_arrays(dst_data, dst_linesize, outBuffer, forceFormat, pCodecCtx->width, pCodecCtx->height, 1);
  sws_scale(swsContext, pFrame->data, pFrame->linesize, 0, pFrame->height, dst_data, dst_linesize) ;
}

6、销毁资源

使用完成后需要释放资源。

if (pFrame)
	av_frame_free(&pFrame);
if (pCodecCtx)
{
	avcodec_close(pCodecCtx);
	avcodec_free_context(&pCodecCtx);
}
if (pFormatCtx)
	avformat_close_input(&pFormatCtx);
if (pFormatCtx)
	avformat_free_context(pFormatCtx);
if (swsContext)
	sws_freeContext(swsContext);
av_dict_free(&opts);
if (outBuffer)
	av_free(outBuffer);

二、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_CreateWindow即可,需要指定窗口标题、位置大小、以及一些标记,如下面示例是窗口gl窗口。

//创建窗口
SDL_Window* screen = SDL_CreateWindow("video play window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,screen_w, screen_h,SDL_WINDOW_OPENGL);

3、创建纹理

先创建窗口的渲染器然后通过渲染器创建后台纹理,纹理的大小与视频大小一致。另外需要指定纹理的像素格式,下列示例的SDL_PIXELFORMAT_IYUV与ffmpeg的AV_PIX_FMT_YUV420P对应

sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
//创建和视频大小一样的纹理
sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width, pCodecCtx->height)

4、渲染

渲染的时候需要指定窗口区域以及视频区域,然后将视频数据更新到后台纹理,后台纹理数据再转换到前台纹理,然后在进行显示。下面是渲染yuv420p的示例

//窗口区域
SDL_Rect sdlRect;
sdlRect.x = 0;
sdlRect.y = 0;
sdlRect.w = screen_w;
sdlRect.h = screen_h;
//视频区域
SDL_Rect sdlRect2;
sdlRect2.x = 0;
sdlRect2.y = 0;
sdlRect2.w = pCodecCtx->width;
sdlRect2.h = pCodecCtx->height;
//渲染到sdl窗口
SDL_RenderClear(sdlRenderer);
SDL_UpdateYUVTexture(sdlTexture, &sdlRect2, dst_data[0], dst_linesize[0], dst_data[1], dst_linesize[1], dst_data[2], dst_linesize[2]);
SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);
SDL_RenderPresent(sdlRenderer);

5、销毁资源

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

if (sdlTexture)
	SDL_DestroyTexture(sdlTexture);
if (sdlRenderer)
	SDL_DestroyRenderer(sdlRenderer);
if (screen)
	SDL_DestroyWindow(screen);
SDL_Quit();

三、完整代码

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

代码
 #include <stdio.h>
#include <SDL.h>
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#undef main
int main(int argc, char** argv) {
	const char* input = "D:\\FFmpeg\\test.mp4";
	enum AVPixelFormat forceFormat = AV_PIX_FMT_YUV420P;
	AVFormatContext* pFormatCtx = NULL;
	AVCodecContext* pCodecCtx = NULL;
	const AVCodec* pCodec = NULL;
	AVDictionary* opts = NULL;
	AVPacket packet;
	AVFrame* pFrame = NULL;
	struct SwsContext* swsContext = NULL;
	uint8_t* outBuffer = NULL;
	int	 videoindex = -1;
	int exitFlag = 0;
	int isLoop = 1;
	double framerate;
	int screen_w = 640, screen_h = 360;
	SDL_Renderer* sdlRenderer = NULL;
	SDL_Texture* sdlTexture = NULL;
	//初始化SDL
	if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
		printf("Could not initialize SDL - %s\n", SDL_GetError());
		return -1;
	}
	//创建窗口
	SDL_Window* screen = SDL_CreateWindow("video play window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
		screen_w, screen_h,
		SDL_WINDOW_OPENGL);
	if (!screen) {
		printf("SDL: could not create window - exiting:%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_VIDEO) {
			videoindex = i;
			break;
		}
	if (videoindex == -1) {
		printf("Didn't find a video 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[videoindex]->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->width == 0 || pCodecCtx->height == 0)
	{
		printf("Invalid video size.\n");
		goto end;
	}
	if (pCodecCtx->pix_fmt == AV_PIX_FMT_NONE)
	{
		printf("Unknown pix foramt.\n");
		goto end;
	}
	pFrame = av_frame_alloc();
	framerate = (double)pFormatCtx->streams[videoindex]->avg_frame_rate.num / pFormatCtx->streams[videoindex]->avg_frame_rate.den;
start:
	while (!exitFlag)
	{
		//读取包
		int gotPacket = av_read_frame(pFormatCtx, &packet) == 0;
		if (!gotPacket || packet.stream_index == videoindex)
			//!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* dst_data[4];
				int dst_linesize[4];
				if (forceFormat != pCodecCtx->pix_fmt)
					//重采样-格式转换
				{
					swsContext = sws_getCachedContext(swsContext, pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, forceFormat, SWS_FAST_BILINEAR, NULL, NULL, NULL);
					if (!outBuffer)
						outBuffer =(uint8_t*) av_malloc(av_image_get_buffer_size(forceFormat, pCodecCtx->width, pCodecCtx->height, 64));
					av_image_fill_arrays(dst_data, dst_linesize, outBuffer, forceFormat, pCodecCtx->width, pCodecCtx->height, 1);
					if (sws_scale(swsContext, pFrame->data, pFrame->linesize, 0, pFrame->height, dst_data, dst_linesize) < 0)
					{
						printf("Call sws_scale error.\n");
						av_frame_unref(pFrame);
						av_packet_unref(&packet);
						goto end;
					}
				}
				else
				{
					memcpy(dst_data, pFrame->data, sizeof(uint8_t*) * 4);
					memcpy(dst_linesize, pFrame->linesize, sizeof(int) * 4);
				}

				if (!sdlRenderer)
					//初始化sdl纹理
				{
					sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
					if (!sdlRenderer)
					{
						printf("Create sdl renderer error.\n");
						av_frame_unref(pFrame);
						av_packet_unref(&packet);
						goto end;
					}
					//创建和视频大小一样的纹理
					sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width, pCodecCtx->height);
					if (!sdlTexture)
					{
						printf("Create sdl texture error.\n");
						av_frame_unref(pFrame);
						av_packet_unref(&packet);
						goto end;
					}
				}
				//窗口区域
				SDL_Rect sdlRect;
				sdlRect.x = 0;
				sdlRect.y = 0;
				sdlRect.w = screen_w;
				sdlRect.h = screen_h;
				//视频区域
				SDL_Rect sdlRect2;
				sdlRect2.x = 0;
				sdlRect2.y = 0;
				sdlRect2.w = pCodecCtx->width;
				sdlRect2.h = pCodecCtx->height;
				//渲染到sdl窗口
				SDL_RenderClear(sdlRenderer);
				SDL_UpdateYUVTexture(sdlTexture, &sdlRect2, dst_data[0], dst_linesize[0], dst_data[1], dst_linesize[1], dst_data[2], dst_linesize[2]);
				SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, &sdlRect);
				SDL_RenderPresent(sdlRenderer);
				SDL_Delay(1000 / framerate);
				av_frame_unref(pFrame);
				//轮询窗口事件
				SDL_Event sdl_event;
				if (SDL_PollEvent(&sdl_event))
					exitFlag = sdl_event.type == SDL_WINDOWEVENT && sdl_event.window.event == SDL_WINDOWEVENT_CLOSE;
			}
		}
		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 (pFrame)
		av_frame_free(&pFrame);
	if (pCodecCtx)
	{
		avcodec_close(pCodecCtx);
		avcodec_free_context(&pCodecCtx);
	}
	if (pFormatCtx)
		avformat_close_input(&pFormatCtx);
	if (pFormatCtx)
		avformat_free_context(pFormatCtx);
	if (swsContext)
		sws_freeContext(swsContext);
	av_dict_free(&opts);
	if (outBuffer)
		av_free(outBuffer);
	if (sdlTexture)
		SDL_DestroyTexture(sdlTexture);
	if (sdlRenderer)
		SDL_DestroyRenderer(sdlRenderer);
	if (screen)
		SDL_DestroyWindow(screen);
	SDL_Quit();
	return 0;
}

 

 

 

使用ffmpeg解码音频sdl(push)播放

前言

使用ffmpeg解码音频并使用sdl播放,网上还是有一些例子的,大多都不是特别完善,比如打开音频设备、音频重采样、使用push的方式播放音频等,都是有不少细节需要注意处理。尤其是使用push的方式播放音频,流程很简单完全可以使用单线程实现,但是队列数据长度比较难控制控制。而且有时想要快速搭建一个demo时,总是要重新编写不少代码,比较不方便,所以在这里提供一个完善的例子,可以直接拷贝拿来使用。

一、ffmpeg解码

ffmpeg解码的流程是比较经典且通用的,基本上是文件、网络流、本地设备都是一模一样的流程。

1、打开输入流

首先需要打开输入流,输入流可以是文件、rtmp、rtsp、http等。

AVFormatContext* pFormatCtx = NULL;
const char* input="test.mp4";
//打开输入流
avformat_open_input(&pFormatCtx, input, NULL, NULL) ;
//查找输入流信息
avformat_find_stream_info(pFormatCtx, NULL) ; 

2、查找音频流

因为是音频播放,所以需要找到输入流中的音频流。通过遍历判断codec_type 为AVMEDIA_TYPE_AUDIO值的流。音频流有可能有多个的,这里我们取第一个。

//视频流的下标
int	 audioindex = -1;
for (unsigned i = 0; i < pFormatCtx->nb_streams; i++)
	if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
		audioindex = i;
		break;
	}

3、打开解码器

通过输入流的信息获取到解码器参数然后查找到响应解码器,最后打开解码器即可。

AVCodecContext* pCodecCtx = NULL;
const AVCodec* pCodec = NULL;
//初始化解码上下文
pCodecCtx=avcodec_alloc_context3(NULL);
//获取解码参数
avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[audioindex]->codecpar) 
//查找解码器
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
//打开解码器
avcodec_open2(pCodecCtx, pCodec, &opts)

打开解码器时可以使用多线程参数优化解码速度。

AVDictionary* opts = NULL;
//使用多线程解码
if (!av_dict_get(opts, "threads", NULL, 0))
	av_dict_set(&opts, "threads", "auto", 0);

4、解码

解码的流程就是读取输入流的包,对包进行解码,获取解码后的帧。

AVPacket packet;
AVFrame* pFrame = av_frame_alloc();
//读取包
while (av_read_frame(pFormatCtx, &packet) == 0)
{   //发送包
	avcodec_send_packet(pCodecCtx, &packet);
	//接收帧
	while (avcodec_receive_frame(pCodecCtx, pFrame) == 0)
	{
	   //取得解码后的帧pFrame
	   
	   av_frame_unref(pFrame);
	}
	av_packet_unref(&packet);
}

解码有个细节是需要注意的,即当av_read_frame到文件尾结束后,需要再次调用avcodec_send_packet传入NULL或者空包flush出里面的缓存帧。下面是完善的解码流程

while (1)
{
	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) {
          //取得解码后的帧pFrame
          
          av_frame_unref(pFrame);
		}
	}
	av_packet_unref(&packet);
	if (!gotPacket)
		break;
}

5、重采样

当遇到音频格式或采样率、声道数与输出目标不一致时,就需要进行重采样了,重采样通常放在解码循环中。

struct SwrContext* swr_ctx = NULL;
enum AVSampleFormatforceFormat = AV_SAMPLE_FMT_FLT;
uint8_t* data;
size_t dataSize;
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, pCodecCtx->channel_layout, pCodecCtx->sample_fmt, pCodecCtx->sample_
		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);
}

6、销毁资源

使用完成后需要释放资源

//销毁资源
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播放

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设置可能不生效,不利于push的方式播放。

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 = NULL;
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、播放(push)

我们采用push的方式播放,即调用SDL_QueueAudio,将音频数据写入sdl内部维护的队列中,sdl会按照一定的频率读取队列数据并写入带音频设备。

SDL_QueueAudio(audioId, data, dataSize);

4、销毁资源

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

if (audioId >= 2)
	SDL_CloseAudioDevice(audioId);
SDL_Quit();

 

三、队列长度控制

使用push(SDL_QueueAudio)的方式播放音频,通常会遇到一个问题:应该以什么频率往队列写入多少数据?如何保持队列长度稳定,且不会因为数据过少导致声音卡顿。通用以定量的方式是不可行的,基本都会出现数据量少卡顿或队列长度不断增长。这时候我们需要能够动态的控制队列长度,数据少了就写入快一些,数据过多就写入慢一些。

1、问题

写入过快或者慢都会出现问题。

(1)、写入较快的情况

写入过快时队列长度不受控制的增长,如果播放时间足够长就会导致out of memory。

(2)、写入较慢的情况

写入过慢则会导致队列数据不足,sdl会自动补充静音包,呈现出来的结果就是播放的声音断断续续的。

2、 解决方法

(1)、使用pid

比较简单的动态控制算法就是pid了,我们只需要根据当前队列的长度计算出需要调整的延时,即能够控制队列长度:(示例)

//目标队列长度
double targetSize;
//当前队列长度
int size;
error_p = targetSize - size;
error_i += error_p;
error_d = error_p - error_dp;
error_dp = error_p;
size = (kp * error_p + ki * error_i + kd * error_d);
//将targetSize - size转换成时长就是延时。
double delay;

效果预览:
目标队列长度是49152bytes,基本在可控范围内波动

四、完整代码

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

代码
 
#include <stdio.h>
#include <SDL.h>
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#undef main
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;
	SDL_AudioSpec wanted_spec, spec;
	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 || av_get_channel_layout_nb_channels(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 = NULL;
	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();
	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;
				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);
				}
				else
				{
					data = pFrame->data[0];
					dataSize = av_samples_get_buffer_size(pFrame->linesize, pFrame->channels, pFrame->nb_samples, forceFormat, 0);
				}
				//写入数据
				SDL_QueueAudio(audioId, data, dataSize);
				//延时,按照数据长度,-1是防止写入过慢卡顿
				SDL_Delay((dataSize) * 1000.0 / (spec.freq * av_get_bytes_per_sample(forceFormat) * spec.channels) - 1);
				int size = SDL_GetQueuedAudioSize(audioId);
				printf("queue size:%dbytes\n", size);
			}
		}
		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 (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);
	if (audioId >= 2)
		SDL_CloseAudioDevice(audioId);
	SDL_Quit();
	return 0;
}

总结

以上就是今天要讲的内容,总的来说,使用ffmpeg解码音频sdl播放流程是基本与视频一致的,而且使用push的方式,相对与pull的方式,不需要使用额外的队列以及条件变量做访问控制。但是音频队列数据长度的控制也是一个难点,虽然本文使用pid达到了目的,但长度还是存在动态波动,需要继续调参或者调

 

标签:pCodecCtx,ffmpeg,packet,SDL,av,sdl,pFrame,NULL
From: https://www.cnblogs.com/fxw1/p/17633538.html

相关文章

  • 记录 FFmpeg开发常用功能封装
    说明记录下个人在开发中使用到的FFmpeg常用功能,避免相同功能代码的重复编写,使用时直接复制提升效率。由于音视频处理的场景众多,无法编写完全通用的方法接口,可能需根据实际场景进行一定的修改,本文章中的代码也将持续更新优化。代码这里提供ffmpegheader.h,ffmpegheader.cpp。配......
  • 使用FFmpeg进行yuv420转rgba
    讲解一下将获取到视频数据,进行rgb转码,并且进行相应的缩放操作//存放解码过后的数据unsignedchar*decode_data;intdecode_size=0;/***解码AVFrame中的yuv420数据并且转换为rgba数据**@paramframe需要解码的帧结构*@paramsrc_width需要转换的帧宽度*......
  • FFmpeg3.2 msvc+msys 源码编译
    材料FFmpeg3.2源码x264x265fdk-aac注意:由于FFmpeg源码的版本太久,采用的第三方库是最新的,因此需要做调整基本操作编译64位FFmpeg程序Windows开始菜单->VisualStudio2022->x64NativeToolsCommandPromptforVS2019编译32位FFmpeg程序Windows开始菜单->VisualStudio2......
  • 跨平台xamarin.Android 开发之 :适配各架构(X86_64 、 X86、arm64-v8a、 armeabi-v7a )
    此代码的编写花费了脑细胞:在每次编码开启编码器到只需要一次编码器的开启优化前提:编译好FFMpeg的各平台的动态库基本上Android X86_64、X86、arm64-v8a、armeabi-v7a采用FFmpeg编码的方式基本一直。差异是内存分配和取指有所不同,如果分配不对,直接闪退。先看看通用的编码......
  • 跨平台xamarin.Android 开发之 :适配各架构(X86_64 、 X86、arm64-v8a、 armeabi-v7a )
    此代码的编写花费了脑细胞:在每次解码开启解码器到只需要一次解码器的开启优化前提:编译好FFMpeg的各平台的动态库Windows、Android(X86_64、X86、arm64-v8a、armeabi-v7a)解码相对编码要简单一些,因为不涉及到AVFrame取指转换解码包括:创建解码器、解码、释放解码器us......
  • 跨平台xamarin.Android 开发之 :适配各架构(X86_64 、 X86、arm64-v8a、 armeabi-v7a
    从事Windows,项目探索预研跨平台开发,对Android只知道有X86_64、X86、arm64-v8a、  armeabi-v7a这么个东西其他空白。编译入手采用Xamarin.Android开发。通过摸索。在Xamarin.Android中使用FFmpeg编解码,需要获取源码编译成对应Android架构的so动态库,如何编译不在此处讨论,稍......
  • 视频获取缩略图使用ffmpeg插件
      stringmp4URL=Server.MapPath("~/Upload/")+"33.mp4";stringOutURL=Server.MapPath("~/Upload/")+DateTime.Now.ToString("yyyyMMddHHmmssfff")+".png";ffmpeg(mp4URL,OutURL,3); ......
  • 关于FFmpeg释放 AVFormatContext*解码上下文的一些问题
    关于FFmpeg释放AVFormatContext*解码上下文的一些问题FFmpeg的一些常用函数用途结构体释放解码上下文FFmpeg的一些常用函数用途av_register_all()注册所有组件。avformat_open_input()打开输入视频文件。avformat_find_stream_info()获取视频文件信息。avcodec_find_d......
  • ffmpeg使用avformat_close_input()函数释放结构体时崩溃的问题
    先看一下我调试时,发现程序崩溃的代码位置  //这是我的程序释放流上下文时的操作 if(m_pAvFormatContext) { //释放视频解码器上下文 if(m_iVideoStreamIndex>=0) avcodec_free_context(&m_pVideoDecodeContext);//此处是发生崩溃......
  • codeblocks 配置SDL2、SDL2_image,找不到SDL2/SDL.h SDL.h SDL_image.h
    codeblocks配置SDL2、SDL2_image下载https://github.com/libsdl-org/SDL/releases/https://github.com/libsdl-org/SDL_image/releases1.解压将SDL2_image-devel-2.6.3-mingw.zip里面x86_64-w64-mingw32的lib、bin、include对应文件解压到SDL2-devel-2.28.2-mingw.zip\SDL2-......