欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

FFmpeg 解码音视频实例及碰到的问题记录(二)

程序员文章站 2022-04-24 14:33:43
...

最近项目开发中需要使用FFmpeg进行音视频的解码,在使用过程中遇到了一些问题对其进行记录。

FFmpeg版本:FFmpeg3.4.1 下载地址如下:https://download.csdn.net/download/lifei092/10670992

1、音频解码:

  1. av_register_all();   遍历注册所有的组件,包括各种编解码器、解复用器等等;
  2. AVFormatContext *pFormatCtx = avformat_alloc_context();  主要存储视音频封装格式中包含的信息,此变量非常重要,几乎贯穿了解码过程的始终,此后主要位pFormatCtx变量分配内存;
  3. avformat_open_input(&pFormatCtx, pInput_test, NULL, NULL); 打开输入视频文件,读取文件头信息,将文件头信息读取到pFormatCtx 结构体内,为后续解码做准备;
  4. avformat_find_stream_info(pFormatCtx, NULL); 获取文件流信息,主要根据pFormatCtx结构体内的已有信息对pFormatCtx结构体内流字段赋值流信息,即,可看作进一步为pFormatCtx结构体进行赋值;
  5. AVCodecParameters *pAudioCodecCtx= pFormatCtx->streams[a_stream_idx]->codecpar; 提取pFormatCtx结构体内的对应流编码器信息
  6. AVCodec *pAudioCodec= avcodec_find_decoder(pAudioCodecCtx->codec_id);  根据编解码的编码id号查找对应的音频解码器信息
  7. AVCodecContext *pAudioEnc = avcodec_alloc_context3(pAudioCodec);  根据初始化AVCodecContext,只是分配,还没打开
  8. avcodec_parameters_to_context(pAudioEnc, pFormatCtx->streams[a_stream_idx]->codecpar);  填充CodecContext信息,此步骤对于解码音频十分重要,若无此步骤,则无法进行正常的音频解码
  9. avcodec_open2(pAudioEnc , pAudioCodec, NULL);  打开解码器
  10. av_read_frame(pFormatCtx, packet);  从pFormatCtx结构体内流字段读取视频文件压缩流到packet变量内
  11. avcodec_send_packet(pAudioEnc , packet); 将packet内文件压缩流导入到对应解码器内
  12. avcodec_receive_frame(pAudioEnc , pFrame); 从解码器pAudioEnc 内解码压缩流到pFrame变量内(存储解压缩后的音视频数据)
  13. pFrame结构体内的 uint8_t *data[AV_NUM_DATA_POINTERS];  成员内存储了音视频流解码后的数据,可对其进行相应操作(可保存可进行图像处理等等)。

以下是示例代码(由于从项目中扣取出来,大致流程无问题,未进行验证可能会有一些小问题,现先记录后续再进行验证):

#include <windows.h>
#include <stdint.h>
#include <iostream>

extern "C"
{
#include "libavcodec/avcodec.h"   
#include "libavformat/avformat.h"  
#include "libavutil/channel_layout.h"  
#include "libavutil/common.h"  
#include "libavutil/imgutils.h"  
#include "libswscale/swscale.h"   
#include "libavutil/imgutils.h"      
#include "libavutil/opt.h"         
#include "libavutil/mathematics.h"      
#include "libavutil/samplefmt.h"   
};

#define ES_STREAM_VIDEO 1
#define ES_STREAM_AUDIO 2

#pragma comment(lib, "FFmpeg/lib/avcodec.lib")  
#pragma comment(lib, "FFmpeg/lib/avformat.lib")  
#pragma comment(lib, "FFmpeg/lib/avdevice.lib")  
#pragma comment(lib, "FFmpeg/lib/avfilter.lib")  
#pragma comment(lib, "FFmpeg/lib/avutil.lib")   
#pragma comment(lib, "FFmpeg/lib/swresample.lib")  
#pragma comment(lib, "FFmpeg/lib/swscale.lib")  

typedef struct AVMediaPacket
{
	BYTE* m_data[TL_NUM_DATA_POINTERS];
	int m_linesize[TL_NUM_DATA_POINTERS];
	BYTE* m_pBuf;
	int m_buf_size;
	int m_max_size;
	int m_cur_size;//ES包使用
	int m_packet_type;//0ES包 1Frame

	//enum AVPixelFormat for video frames
	//enum AVSampleFormat for audio
	int m_pixel_format;
	int m_channel_count;
	LONGLONG m_channel_layout;
	int m_nb_samples;  // 单个声道音频样本个数
	int m_sample_rate; // 音频采样率

	int m_width;
	int m_height;
	/**
	* The content of the picture is interlaced.
	* - encoding: Set by user.
	* - decoding: Set by libavcodec. (default 0)
	*/
	int m_interlaced_frame;//0逐行帧,1隔行帧
	/**
	* If the content is interlaced, is top field displayed first.
	* - encoding: Set by user.
	* - decoding: Set by libavcodec.
	*/
	int m_top_field_first;//0偶底场优先,1上奇场优先
	int m_pict_type;

	int m_es_stream_type;//是视频帧还是音频帧

	LONGLONG m_pos;
	LONGLONG m_origin_size;//原始ES包的字节数
	LONGLONG m_dts;//ES包使用
	LONGLONG m_pts;
	LONGLONG m_origin_ts;
	LONGLONG m_sys_pts;//转换成系统时间的pts(毫秒)
	LONGLONG m_duration;//帧时长
	LONGLONG m_sys_duration;//转换成系统时间的长度(毫秒)

	int m_align;//数据的对齐方式

	int m_scale_mod;//视频帧用到的变换方式(该帧是经过该变换方式得来的)

	int m_stream_index;//同AVPacket的stream_index
	int m_flags;//同AVPacket的flags
}AVMediaPacket;


/********** 查询FFmpeg支持的编解码器 ************/
void CheckEncoderDecoder()
{
	char *info = (char *)malloc(40000);
	memset(info, 0, 40000);

	AVCodec *c_temp = av_codec_next(NULL);

	while (c_temp != NULL)
	{
		if (c_temp->decode != NULL)
		{
			strcat(info, "[Decode]");
		}
		else
		{
			strcat(info, "[Encode]");
		}
		switch (c_temp->type)
		{
		case AVMEDIA_TYPE_VIDEO:
			strcat(info, "[Video]");
			break;
		case AVMEDIA_TYPE_AUDIO:
			strcat(info, "[Audeo]");
			break;
		default:
			strcat(info, "[Other]");
			break;
		}
		sprintf(info, "%s %10s\n", info, c_temp->name);
		c_temp = c_temp->next;
	}
	puts(info);
	free(info);
}

void InitMediaPacket(AVMediaPacket* pFrame,int nPacketType, int nEsStreamType)
{
	for (int i = 0; i < TL_NUM_DATA_POINTERS; i++)
	{
		pFrame->m_data[i] = NULL;   // 音视频数据
		pFrame->m_linesize[i] = 0;  // 每行数据的size
	}
	pFrame->m_pBuf = NULL;  // 音视频数据指针

	pFrame->m_buf_size = 0;
	pFrame->m_max_size = 0;
	pFrame->m_cur_size = 0;//ES包使用
	pFrame->m_packet_type = nPacketType;//0ES包 1Frame

	//enum AVPixelFormat for video frames
	//enum AVSampleFormat for audio
	pFrame->m_pixel_format = AV_PIX_FMT_NONE; 
	pFrame->m_channel_count = -1;
	pFrame->m_channel_layout = -1;
	pFrame->m_nb_samples = -1;  // 单个声道音频样本个数
	pFrame->m_sample_rate = -1; // 音频采样率

	pFrame->m_width = -1;
	pFrame->m_height = -1;
	/**
	* The content of the picture is interlaced.
	* - encoding: Set by user.
	* - decoding: Set by libavcodec. (default 0)
	*/
	pFrame->m_interlaced_frame = -1;//0逐行帧,1隔行帧
	/**
	* If the content is interlaced, is top field displayed first.
	* - encoding: Set by user.
	* - decoding: Set by libavcodec.
	*/
	pFrame->m_top_field_first = -1;//0偶底场优先,1上奇场优先
	pFrame->m_pict_type = -1;

	pFrame->m_es_stream_type = nEsStreamType;//是视频帧还是音频帧

	pFrame->m_pos = -1;
	pFrame->m_origin_size = -1;//原始ES包的字节数
	pFrame->m_dts = -1;//ES包使用
	pFrame->m_pts = -1;
	pFrame->m_origin_ts = -1;
	pFrame->m_sys_pts = -1;//转换成系统时间的pts(毫秒)
	pFrame->m_duration = -1;//帧时长
	pFrame->m_sys_duration = -1;//转换成系统时间的长度(毫秒)

	pFrame->m_align = -1;//数据的对齐方式

	pFrame->m_scale_mod = -1;//视频帧用到的变换方式(该帧是经过该变换方式得来的)

	pFrame->m_stream_index = -1;//同AVPacket的stream_index
	pFrame->m_flags = -1;//同AVPacket的flags
}

BOOL MallocMediaFrameBuf(AVMediaPacket* pFrame, int nBufLen)
{
	if (nBufLen <= 0)
		return FALSE;

	for (int i = 0; i < TL_NUM_DATA_POINTERS; i++)
	{
		pFrame->m_data[i] = NULL;   // 音视频数据
		pFrame->m_linesize[i] = 0;  // 每行数据的size
	}

	pFrame->m_pBuf = (BYTE*)malloc(nBufLen*sizeof(BYTE));

	pFrame->m_buf_size = nBufLen;

	if (pFrame->m_pBuf)
		return TRUE;
	else
		return FALSE;
}

void ReleaseMediaPacket(AVMediaPacket* pFrame)
{
	if (pFrame->m_pBuf)
	{
		free(pFrame->m_pBuf);
		pFrame->m_pBuf = NULL;
	}
}



int main()
{
	const char *pInput_test = "E:\\C++\\VideoExaminationSystem\\彩条\\田野风测试1.gxf";
	FILE *fout = fopen("E://test.yuv", "wb+");
	AVMediaPacket src_AudioFrame;

	//1.注册所有组件
	av_register_all();

	CheckEncoderDecoder();  // 查询此FFmpeg版本支持的编解码器

	//封装格式上下文,统领全局的结构体,保存了视频文件封装格式的相关信息
	AVFormatContext *pFormatCtx = avformat_alloc_context();  //主要存储视音频封装格式中包含的信息

	//2.打开输入视频文件,读取文件头信息
	if (avformat_open_input(&pFormatCtx, pInput_test, NULL, NULL) != 0)
	{
		printf("%s", "无法打开输入视频文件");
		return -1;
	}

	//3.获取视频文件信息
	if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
	{
		printf("%s", "无法获取视频文件信息");
		return -1;
	}

	//获取视频流的索引位置
	//遍历所有类型的流(音频流、视频流、字幕流),找到视频流
	int a_stream_idx = -1;  // 音频流
	
	//number of streams
	for (int i = 0; i < pFormatCtx->nb_streams; i++)
	{
		//流的类型
		if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
		{
			a_stream_idx = i;  // 记录音频流索引
			break;
		}
	}

	if (a_stream_idx == -1 && (AUDIO_DETECT_ONLY == nDetectFlag || AV_DETECT == nDetectFlag))
	{
		printf("%s", "找不到音频流\n");
		return -1;
	}

	//只有知道视频的编码方式,才能够根据编码方式去找到解码器
	//获取视频流中的编解码上下文
	AVCodecParameters *pAudioCodecCtx = NULL;
	AVCodec *pAudioCodec = NULL;
	AVCodecContext *pAudioEnc = NULL;
	AVPacket *packet = NULL;
	AVFrame *pFrame = NULL;

	int src_audio_buf_size = -1;
	int nb_planes = -1;
		
	pAudioCodecCtx = pFormatCtx->streams[a_stream_idx]->codecpar;
	//4.根据编解码上下文中的编码id查找对应的解码
	pAudioCodec = avcodec_find_decoder(pAudioCodecCtx->codec_id);
	if (pAudioCodec == NULL)
	{
		printf("%s", "找不到音频解码器\n");
		return -1;
	}
	pAudioEnc = avcodec_alloc_context3(pAudioCodec);  // 初始化AVCodecContext,只是分配,还没打开

	/****************** START --> 此步骤非常重要: 不进行设置无法进行正常解码 ****************/
	// 填充CodecContext信息
	if (avcodec_parameters_to_context(pAudioEnc, pFormatCtx->streams[a_stream_idx]->codecpar) < 0)
	{
		printf("Failed to copy codec parameters to decoder context!\n");
		return -1;
	}
	/****************** END --> 此步骤非常重要: 不进行设置无法进行正常解码 ****************/

	//5.打开解码器
	if (avcodec_open2(pAudioEnc, pAudioCodec, NULL)<0)
	{
		printf("%s", "音频解码器无法打开\n");
		return -1;
	}

	//输出音频信息
	printf("音频采样率:%d\n", pAudioCodecCtx->sample_rate);
	//printf("音频帧尺寸:%d\n", pAudioCodecCtx->frame_size);
	printf("音频的通道:%d\n", pAudioCodecCtx->channels);
	printf("音频解码器的名称:%s\n", pAudioCodec->name);
	std::cout << std::endl;

	src_AudioFrame.m_pixel_format = pAudioCodecCtx->format; 
	src_AudioFrame.m_channel_count = pAudioCodecCtx->channels;
	src_AudioFrame.m_sample_rate = pAudioCodecCtx->sample_rate;
	//src_AudioFrame.m_nb_samples = pAudioCodecCtx->frame_size;

	nb_planes = av_sample_fmt_is_planar((AVSampleFormat)pAudioCodecCtx->format) ? pAudioCodecCtx->channels : 1; //如果是 planar类型,需要分配一个指针数组,每个元素指向一个声道

	src_audio_buf_size = AVCODEC_MAX_AUDIO_FRAME_SIZE * 4;
	if (!MallocMediaFrameBuf(&src_AudioFrame, src_audio_buf_size))
	{
		return NULL;
	}

	// 根据音频格式填充结构体相关字段
	//av_samples_fill_arrays(src_AudioFrame.m_data, src_AudioFrame.m_linesize, src_AudioFrame.m_pBuf, src_AudioFrame.m_channel_count, src_AudioFrame.m_nb_samples, (AVSampleFormat)src_AudioFrame.m_pixel_format, 1);
		

	//准备读取
	//缓冲区,开辟空间,AVPacket用于存储一帧一帧的压缩数据(H264)
	packet = (AVPacket*)av_malloc(sizeof(AVPacket));

	//内存分配, AVFrame用于存储解码后的像素数据(YUV)
	pFrame = av_frame_alloc();

	int ret = -1;
	int audio_frame_count = 0;

	//6.一帧一帧的读取压缩数据
	while (av_read_frame(pFormatCtx, packet) >= 0)
	{
		//只要音频压缩数据(根据流的索引位置判断)
		if (packet->stream_index == a_stream_idx && (AUDIO_DETECT_ONLY == nDetectFlag || AV_DETECT == nDetectFlag))
		{
			//7.解码一帧视频压缩数据,得到视频像素数据
			ret = avcodec_send_packet(pAudioEnc, packet);

			if (ret < 0)
			{
				printf("%s\n", "音频解码错误");
				return -1;
			}

			ret = avcodec_receive_frame(pAudioEnc, pFrame);

			src_AudioFrame.m_nb_samples = pFrame->nb_samples;
			// 根据音频格式填充结构体相关字段
			av_samples_fill_arrays(src_AudioFrame.m_data, src_AudioFrame.m_linesize, src_AudioFrame.m_pBuf, src_AudioFrame.m_channel_count, src_AudioFrame.m_nb_samples, (AVSampleFormat)src_AudioFrame.m_pixel_format, 1);

			if (!ret)  // 0表示解码成功
			{
				for (int j = 0; j < nb_planes; j++)
				{
					memcpy(src_AudioFrame.m_pBuf, pFrame->data[j], pFrame->linesize[0] * sizeof(BYTE));
					fwrite(src_AudioFrame.m_data[j], pFrame->width*pFrame->height/(radio*radio), 1, fout);
				}
				
				audio_frame_count++;
				printf("Decode Audio Frame Number : %d\n", audio_frame_count);
			}
		}

		//释放资源
		av_packet_unref(packet);
	}

	// Free the YUV frame
	av_frame_free(&pFrame);

	// Close the codecs
	avcodec_close(pAudioEnc);

	// Close the video file
	avformat_close_input(&pFormatCtx);

	ReleaseMediaPacket(&src_AudioFrame);

	std::cout << "I: Finish Video Scale." << std::endl;

	system("pause");
	return 0;
}

在调试过程中遇到了以下问题:

  1. 对于音频解码过程中,需注意音频数据是否为planar格式,若为planar格式则解码后的音频数据在AVFrame结构体中多个通道中,而若不为planar格式,则解码后的音频数据存放在AVFrame结构体中的data[0]中。判断是否为planar格式通过以下方式  nb_planes = av_sample_fmt_is_planar((AVSampleFormat)pAudioCodecCtx->format) ? pAudioCodecCtx->channels : 1; //如果是 planar类型,需要分配一个指针数组,每个元素指向一个声道
  2. 通常在解码视频时,通过avcodec_open2()函数后即可获取frame_size(数据帧大小);而音频解码过程中,发现通过avcodec_open2()函数后获取frame_size = 0;  这显然是有问题的,在调试过程中也确实遇到了无法解码音频数据的情况。解决方法为:通过avcodec_receive_frame(pAudioEnc, pFrame);函数之后,获取frame_size(数据帧大小),即src_AudioFrame.m_nb_samples = pFrame->nb_samples; 再根据音频格式填充结构体相关字段av_samples_fill_arrays();