FFMPEG视频解码

1.背景介绍

学习FFMPEG有段时间了,FFMPEG对通用的视频编解码做了统一接口处理的抽象,比如在解码处理时,无须关心其具体的编解码格式,仅需关心其pixfmt即可。
FFMPEG使用时需要关心下面这些核心的结构体。

AVFormatContext    // 封视频格装上下文,是处理编封装功能的结构体
AVCodecContext     // 解码器上下文,是编解码功能的结构体,存储了gop/definition/pixfmt/profile/bit_rate等参数
AVCodec            // 存储编解码器信息的结构体
AVFrame            // 存储解码后的视音频数据,包括yuv/pcm/宏块数据/运动矢量等信息
AVPacket           // 存储编码后的数据
SwsContext         // 格式转换
AVStream           // 存储每一个视频/音频流信息的结构体

2.解码

下面代码完成如下功能:

  • 视频解码,包括带透明度的webm解码;
  • scaler缩放,输出yuv420;
#include 
#include "math.h"
#include 
#include 
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "stdint.h"

#ifndef   UINT64_C

#define   UINT64_C(value)__CONCAT(value,ULL)

#endif

#ifdef __cplusplus
extern "C" 
{
#endif
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
	#include 
    #include 
#ifdef __cplusplus
}
#endif

int ffmpeg_video_dec(char* path_in, char* path_yuv_out)
{
	AVFormatContext*	AFCtx_p;		// 解封装上下文,是解封装功能的结构体
	AVCodecContext*		ACCtx_p;		// 解码器上下文,是编解码功能的结构体,存储了gop/definition/pixfmt/profile/bit_rate等参数
	AVCodec*			codec_p;		// 存储编解码器信息的结构体
	AVFrame*			pFrame;			// 存储解码后的视音频数据,包括yuv/pcm/宏块数据/运动矢量等信息
	AVFrame*			pFrameyuv;
	AVPacket*			packet;
	struct SwsContext*	img_convert_ctx;
	AVStream*			stream;			// 存储每一个视频/音频流信息的结构体

	int videoindex = -1;
	int y_size;

	FILE* fp_yuv = fopen(path_yuv_out, "w+");

	av_register_all(); 					// 初始化FFMPEG,注册所有模块,调用了这个才能正常使用编码器和解码器

	// Allocate an AVFormatContext.
    AFCtx_p = avformat_alloc_context();	// 创建AFCtx_p

	avformat_network_init();			// 初始化网络库

	// 打开stream文件并且read header,将文件信息存入解封装上下文
	int open_stream_ret = avformat_open_input(&AFCtx_p, path_in, NULL, NULL);
	if (open_stream_ret != 0)
	{
		fprintf(stderr, "open failed[%d].\n", open_stream_ret);
		return -1;
	}

	// 获取视频流信息
	int find_stream_ret = avformat_find_stream_info(AFCtx_p, NULL);
	if (find_stream_ret < 0)
	{
		fprintf(stderr, "stream find failed[%d].\n", find_stream_ret);
		return -1;
	}

	// dump调试信息
	av_dump_format(AFCtx_p, 0, path_in, 0);

	// 获取metadata字典
	AVDictionaryEntry* entry = nullptr;

	// webm透明通道标识
	int alpha_flag = 0;

	//打开视频并且获取了视频流,设置视频索引默认值
	for (int i = 0; i < AFCtx_p->nb_streams; i++)
	{
		if (AFCtx_p->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
		{
			fprintf(stderr, "nb_streams[%d], videoindex[%d].\n", AFCtx_p->nb_streams, i);
			videoindex = i;
		}
		stream = AFCtx_p->streams[i];

		while ((entry = av_dict_get(AFCtx_p->streams[i]->metadata, "", entry, AV_DICT_IGNORE_SUFFIX))) {
			if((!strcmp(entry->key, "ALPHA_MODE")) && (!strcmp(entry->value, "1"))) {
				alpha_flag = 1;
				fprintf(stderr, "webm alpha.\n");
			} else {
				alpha_flag = 0;
			}
			fprintf(stdout, "key: %s, value: %s\n", entry->key, entry->value);
		}

		fprintf(stderr, "stream[%d], num[%d], den[%d], frame_num[%ld].\n", i, stream->avg_frame_rate.num, stream->avg_frame_rate.den, stream->nb_frames);
	}

	// 如果没有找到视频索引,说明不是一个视频文件
	if (videoindex == -1)
	{
		fprintf(stderr, "not a video.\n");
		return -1;
	}

	// 分配解码器上下文空间
	ACCtx_p = avcodec_alloc_context3(NULL);

	// 从封装上下文中获取编解码器上下文信息
	int codec_get_ret = avcodec_parameters_to_context(ACCtx_p, AFCtx_p->streams[videoindex]->codecpar);
	if (codec_get_ret < 0)
	{
		fprintf(stderr, "copy stream failed[%d].\n", codec_get_ret);
		return -1;
	}

	// 查找解码器
	fprintf(stderr, "codec_id[%d].\n", ACCtx_p->codec_id);
    fprintf(stderr, "timebase[%d/%d].\n", ACCtx_p->pkt_timebase.num, ACCtx_p->pkt_timebase.den);
	if(1 == alpha_flag) {
		if(AV_CODEC_ID_VP8 == ACCtx_p->codec_id) {
			codec_p = avcodec_find_decoder_by_name("libvpx");			// vp8-alpha
		} else if(AV_CODEC_ID_VP9 == ACCtx_p->codec_id) {
			codec_p = avcodec_find_decoder_by_name("libvpx-vp9");		// vp9-alpha
		} else {
			codec_p = avcodec_find_decoder(ACCtx_p->codec_id);			// 不带alpha
		}
	} else {
        // codec_p = avcodec_find_decoder_by_name("h264_cuvid");
		codec_p = avcodec_find_decoder(ACCtx_p->codec_id);				// 不带alpha
	}

	if (!codec_p)
	{
		fprintf(stderr, "find decoder error.\n");
		return -1;
	}
	// 打开解码器
	int open_codec_ret = avcodec_open2(ACCtx_p, codec_p, NULL);
	if (open_codec_ret != 0)
	{
		fprintf(stderr, "open codec failed[%d].\n", open_codec_ret);
		return -1;
	}

	// 分配AVPacket/AVFrame
	packet = av_packet_alloc();
	pFrame = av_frame_alloc();
	pFrameyuv = av_frame_alloc();
	// 获取转换后YUV数据的大小
	int dst_width = ACCtx_p->width / 2 * 2;
	int dst_height = ACCtx_p->height / 2 * 2;
	int video_size = dst_width * dst_height;
	uint8_t* buf = NULL;

	// 裁剪图像
	fprintf(stderr, "src_width[%d], src_height[%d], dst_width[%d], dst_height[%d], pix_fmt[%d].\n", 
		ACCtx_p->width, ACCtx_p->height, dst_width, dst_height, ACCtx_p->pix_fmt);
	
	// 根据格式分配scale操作相关的上下文SwsContext
	if((AV_PIX_FMT_YUV420P == ACCtx_p->pix_fmt && alpha_flag == 1) || AV_PIX_FMT_RGBA == ACCtx_p->pix_fmt || AV_PIX_FMT_ARGB == ACCtx_p->pix_fmt || AV_PIX_FMT_YUVA444P12LE == ACCtx_p->pix_fmt || AV_PIX_FMT_PAL8 == ACCtx_p->pix_fmt) {
		fprintf(stderr, "this is rgba/yuva data, pixfmt[%d].\n", ACCtx_p->pix_fmt);

		// 这种格式libvpx解码时会转换为yuva420
		if((AV_PIX_FMT_YUV420P == ACCtx_p->pix_fmt && alpha_flag == 1)) {
			img_convert_ctx = sws_getContext(ACCtx_p->width, ACCtx_p->height, AV_PIX_FMT_YUVA420P,
				dst_width, dst_height, AV_PIX_FMT_YUVA420P, SWS_BICUBIC, NULL, NULL, NULL);
		} else {
			img_convert_ctx = sws_getContext(ACCtx_p->width, ACCtx_p->height, ACCtx_p->pix_fmt,
				dst_width, dst_height, AV_PIX_FMT_YUVA420P, SWS_BICUBIC, NULL, NULL, NULL);
		}

		video_size = av_image_get_buffer_size(AV_PIX_FMT_YUVA420P, dst_width, dst_height, 1);
		buf = (uint8_t*)av_malloc(video_size);
		av_image_fill_arrays(pFrameyuv->data, pFrameyuv->linesize,
			buf, AV_PIX_FMT_YUVA420P, dst_width, dst_height, 1);
		
	} else {
		fprintf(stderr, "this is rgb/yuv data, pixfmt[%d].\n", ACCtx_p->pix_fmt);
		img_convert_ctx = sws_getContext(ACCtx_p->width, ACCtx_p->height, ACCtx_p->pix_fmt,
			dst_width, dst_height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);

		video_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, dst_width, dst_height, 1);
		buf = (uint8_t*)av_malloc(video_size);
		av_image_fill_arrays(pFrameyuv->data, pFrameyuv->linesize,
			buf, AV_PIX_FMT_YUV420P, dst_width, dst_height, 1);
	}

	if (!img_convert_ctx)
	{
		fprintf(stderr, "get swscale context failed.\n");
		return -1;
	}

	// 循环读取帧数据并转换写入
	while (av_read_frame(AFCtx_p, packet) >= 0)
	{
		if (packet->stream_index == videoindex)
		{
			if (avcodec_send_packet(ACCtx_p, packet) != 0)
			{
				fprintf(stderr, "send video stream packet failed.\n");
				return -1;
			}
			if (avcodec_receive_frame(ACCtx_p, pFrame) != 0)
			{
				fprintf(stderr, "receive video frame failed, status = %d.\n", avcodec_receive_frame(ACCtx_p, pFrame));
				continue;
			}

			fprintf(stderr, "decoding frame %d.\n", ACCtx_p->frame_number);

			sws_scale(img_convert_ctx, pFrame->data, pFrame->linesize,
				0, ACCtx_p->height, pFrameyuv->data, pFrameyuv->linesize);

			// 视频编辑即对pFrameyuv->data进行编辑
			y_size = dst_width * dst_height;
			fwrite(pFrameyuv->data[0], 1, y_size, fp_yuv);			//Y
			fwrite(pFrameyuv->data[1], 1, y_size/4, fp_yuv);		//U
			fwrite(pFrameyuv->data[2], 1, y_size/4, fp_yuv);		//V
			if(AV_PIX_FMT_YUVA420P == ACCtx_p->pix_fmt || AV_PIX_FMT_RGBA == ACCtx_p->pix_fmt || AV_PIX_FMT_ARGB == ACCtx_p->pix_fmt || AV_PIX_FMT_YUVA444P12LE == ACCtx_p->pix_fmt || AV_PIX_FMT_PAL8 == ACCtx_p->pix_fmt) {
				fwrite(pFrameyuv->data[3], 1, y_size, fp_yuv);		//A
			}

			fflush(fp_yuv);
		}
	}
	fclose(fp_yuv);

	av_free(buf);
	av_frame_free(&pFrame);
	av_frame_free(&pFrameyuv);
	av_packet_free(&packet);
	sws_freeContext(img_convert_ctx);
	avcodec_free_context(&ACCtx_p);
	avformat_close_input(&AFCtx_p);
	avformat_free_context(AFCtx_p);
	return 1;
}

// g++ test_gpu_dec.cpp -L /usr/local/lib -lavcodec -lavformat -lavutil -lswscale -I /usr/local/include -o test_dec
// ./test_enc xxx.h264 xxx.yuv 10
int main(int argc, char** argv)
{
    if(argc < 3)
        fprintf(stderr, "input argc not enough.\n");
    
    for(int i = 0; i < atoi(argv[3]); i++) {
        fprintf(stderr, "i[%d].\n", i);
	    ffmpeg_video_dec(argv[1], argv[2]);
    }
    return 0;
}

3.编译及运行

服务端使用g++编译,生成可执行文件,运行时,指定输入的h264文件、输出的yuv文件以及运行次数,这里运行10次,至此,解码部分测试完成。

g++ test_gpu_dec.cpp -L /usr/local/lib -lavcodec -lavformat -lavutil -lswscale -I /usr/local/include -o test_dec
./test_enc xxx.h264 xxx.yuv 10   # 调用10次

你可能感兴趣的:(图像处理)