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 <iostream>
#include "math.h"
#include <sys/syscall.h>
#include <sys/types.h>
#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 <libavutil/frame.h>
    #include <libavutil/mem.h>
    #include <libavcodec/avcodec.h>
    #include <libavutil/log.h>
    #include <libavutil/mathematics.h>
    #include <libavformat/avformat.h>
    #include <libswscale/swscale.h>
	#include <libavutil/imgutils.h>
    #include <libavutil/time.h>
#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次
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章