前面大概地介绍了图像格式和不同格式之间的转换、h264的一些知识和FFmpeg编码和解码的基本流程。现在分别为编码和解码给出一个例子。例子是使用参考output_example.cpp和前面介绍中说到的老外写的一个例子。不说那么多了,上代码。
编码:
#ifdef __cplusplus #define __STDC_CONSTANT_MACROS #ifdef _STDINT_H #undef _STDINT_H #endif # include <stdint.h> #endif extern "C" { #include<libavcodec/avcodec.h> #include<libavformat/avformat.h> #include<libswscale/swscale.h> } #include <iostream> #include<stdio.h> #include<string.h> #include<stdlib.h> using namespace std; int g_width = 352; int g_height = 288; int g_video_outbuff_size; uint8_t* g_video_outbuff = NULL; AVPixelFormat g_pix_fmt = AV_PIX_FMT_YUV420P; //init Video Stream and return it AVStream* getVideoStream(AVFormatContext* fmt_ctx) { AVStream* stream = NULL; stream = avformat_new_stream(fmt_ctx, NULL); if( stream == NULL) { fprintf(stderr, "new stream fail\n"); exit(1); } AVCodecContext* codec_ctx = stream->codec; codec_ctx->codec_id = fmt_ctx->oformat->video_codec; codec_ctx->codec_type = AVMEDIA_TYPE_VIDEO; codec_ctx->bit_rate = 400000; codec_ctx->gop_size = 3; codec_ctx->pix_fmt = g_pix_fmt; codec_ctx->width = g_width; codec_ctx->height = g_height; codec_ctx->time_base.num = 1; codec_ctx->time_base.den = 25; codec_ctx->me_range = 16; codec_ctx->max_qdiff = 4; codec_ctx->qmin = 10; codec_ctx->qmax = 51; codec_ctx->qcompress = 0.6; if( codec_ctx->codec_id == CODEC_ID_MPEG2VIDEO ) codec_ctx->max_b_frames = 2; if( codec_ctx->codec_id == CODEC_ID_MPEG1VIDEO) codec_ctx->mb_decision = 2; // some formats want stream headers to be separate if(!strcmp(fmt_ctx->oformat->name, "mp4") || !strcmp(fmt_ctx->oformat->name, "mov") || !strcmp(fmt_ctx->oformat->name, "3gp") ) { codec_ctx->flags |= CODEC_FLAG_GLOBAL_HEADER; } return stream; } void initEncoder(AVStream* stream) { AVCodecContext* codec_ctx = stream->codec; AVCodec* encoder = avcodec_find_encoder(codec_ctx->codec_id); if( encoder == NULL ) { fprintf(stderr, "cann't find the encoder\n"); exit(1); } if( avcodec_open2(codec_ctx, encoder, NULL) < 0 ) { fprintf(stderr, "could not open video codec\n"); exit(1); } } AVFrame* getAVFrame() { int size = avpicture_get_size(g_pix_fmt, g_width, g_height); uint8_t* buff = (uint8_t*)av_malloc(size); if( buff == NULL) { fprintf(stderr, "av malloc fail\n"); exit(1); } AVFrame* frame = av_frame_alloc(); if( frame == NULL) { fprintf(stderr, "alloc frame fail\n"); exit(1); } avpicture_fill((AVPicture*)frame, buff, g_pix_fmt, g_width, g_height); return frame; } void writeFrame(AVFormatContext* fmt_ctx, AVStream* stream, AVFrame* frame) { int ret, out_size; AVPacket packet; AVCodecContext* codec_ctx = stream->codec; if (fmt_ctx->oformat->flags & AVFMT_RAWPICTURE) { /* raw video case. The API will change slightly in the near futur for that */ av_init_packet(&packet); packet.flags |= AV_PKT_FLAG_KEY; packet.stream_index= stream->index; packet.data= (uint8_t *)frame; packet.size= sizeof(AVPicture); ret = av_write_frame(fmt_ctx, &packet); } else { /* encode the image */ out_size = avcodec_encode_video(codec_ctx, g_video_outbuff, g_video_outbuff_size, frame); /* if zero size, it means the image was buffered */ if (out_size > 0) { av_init_packet(&packet); if(codec_ctx->coded_frame->key_frame) packet.flags |= AV_PKT_FLAG_KEY; packet.stream_index= stream->index; packet.data= g_video_outbuff; // not the video_outbuf_size, note! packet.size= out_size; /* write the compressed frame in the media file */ ret = av_write_frame(fmt_ctx, &packet); } else { ret = 0; } } if (ret != 0) { fprintf(stderr, "Error while writing video frame\n"); exit(1); } } int main(int argc, char** argv) { const char* input_file = argc < 2 ? "flower_cif.yuv" : argv[1]; FILE* fin = fopen(input_file, "rb"); if( fin == NULL ) { fprintf(stderr, "cann't open the yuv file"); return -1; } const char* output_filename = argc < 3 ? "flower.mpeg" : argv[2]; g_width = 352; g_height = 288; av_register_all(); //正如其函数名,该函数就是在猜。根据文件名的后缀猜应该使用什么编码 //当然,也可能找不到对应后缀的编码方法。此时返回NULL。 AVOutputFormat* output_fmt = av_guess_format(NULL, output_filename, NULL); if( output_fmt == NULL ) { fprintf(stderr, "Couldn't deduce output format from file extension: using MPEG.\n"); output_fmt = av_guess_format("mpeg", NULL, NULL); } if( output_fmt == NULL ) { fprintf(stderr, "Could not find suitable output format"); return -1; } //负责申请一个AVFormatContext结构的内存,并进行简单初始化 //需要使用avformat_free_context()来释放 //avformat_free_context()可以用来释放该结构里的所有东西以及该结构本身 AVFormatContext* fmt_ctx = avformat_alloc_context(); if( fmt_ctx == NULL ) { fprintf(stderr, "Memory error"); return -1; } //把要使用的编码器复制给format_ctx fmt_ctx->oformat = output_fmt; strncpy(fmt_ctx->filename, output_filename, sizeof(fmt_ctx->filename)); //http://www.ffmpeg.org/doxygen/1.0/group__lavc__core.html //can see #define CodecID AVCodecID if( output_fmt->video_codec == AV_CODEC_ID_NONE) return -1; AVStream* stream = getVideoStream(fmt_ctx); av_dump_format(fmt_ctx, 0, output_filename, 1); initEncoder(stream); if( avio_open(&fmt_ctx->pb, output_filename, AVIO_FLAG_WRITE) < 0) { fprintf(stderr, "cann't open the output file\n"); return -1; } if (!(fmt_ctx->oformat->flags & AVFMT_RAWPICTURE)) { /* allocate output buffer */ /* XXX: API change will be done */ /* buffers passed into lav* can be allocated any way you prefer, as long as they're aligned enough for the architecture, and they're freed appropriately (such as using av_free for buffers allocated with av_malloc) */ g_video_outbuff_size = 200000; g_video_outbuff = (uint8_t *)av_malloc(g_video_outbuff_size); } AVFrame* frame = getAVFrame(); if( avformat_write_header(fmt_ctx, NULL) < 0 ) { fprintf(stderr, "cann't write the file head\n"); return -1; } int yuv_frame_size = avpicture_get_size(g_pix_fmt, g_width, g_height); int ret = 0; while( 1 ) { ret = fread(frame->data[0], 1, yuv_frame_size, fin); if( ret != yuv_frame_size ) { fprintf(stderr, "%d don't read enough data\n", ret); break; } writeFrame(fmt_ctx, stream, frame); } av_write_trailer(fmt_ctx); avio_close(fmt_ctx->pb); av_free(fmt_ctx); return 0; }在getVideoStream函数中,可以设置视频的比特率。对其进行修改可以发现:
mp4格式的文件比mpg有更大的压缩比。当比特率取400000(40Kbps)时,mpg文件就已经比较清晰了,但对应的mp4文件却很模糊。同时可以看到两者的文件大小也差了10多倍。如果把比特率调为40000000,那么得到的mp4文件才比较清晰,当然此时的mp4文件已经增大了10多倍,比刚才400000比特率下的mpg文件还要大。
解码:这部分基本和老外写的例子是一样的,我加入了一些注释,并保存到bmp图片格式中。
#ifdef __cplusplus #define __STDC_CONSTANT_MACROS #ifdef _STDINT_H #undef _STDINT_H #endif # include <stdint.h> #endif extern "C" { #include<libavcodec/avcodec.h> #include<libavformat/avformat.h> #include<libavutil/log.h> #include<libswscale/swscale.h> } #include <iostream> #include<stdio.h> using namespace std; #include <windows.h> bool saveAsBitmap(AVFrame *pFrameRGB, int width, int height, int iFrame) { FILE *pFile = NULL; BITMAPFILEHEADER bmpheader; BITMAPINFO bmpinfo; char fileName[32]; int bpp = 24; // open file sprintf(fileName, "frame%d.bmp", iFrame); pFile = fopen(fileName, "wb"); if (!pFile) return false; bmpheader.bfType = ('M' <<8)|'B'; bmpheader.bfReserved1 = 0; bmpheader.bfReserved2 = 0; bmpheader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); bmpheader.bfSize = bmpheader.bfOffBits + width*height*bpp/8; bmpinfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmpinfo.bmiHeader.biWidth = width; bmpinfo.bmiHeader.biHeight = -height; //reverse the image bmpinfo.bmiHeader.biPlanes = 1; bmpinfo.bmiHeader.biBitCount = bpp; bmpinfo.bmiHeader.biCompression = BI_RGB; bmpinfo.bmiHeader.biSizeImage = 0; bmpinfo.bmiHeader.biXPelsPerMeter = 100; bmpinfo.bmiHeader.biYPelsPerMeter = 100; bmpinfo.bmiHeader.biClrUsed = 0; bmpinfo.bmiHeader.biClrImportant = 0; fwrite(&bmpheader, sizeof(BITMAPFILEHEADER), 1, pFile); fwrite(&bmpinfo.bmiHeader, sizeof(BITMAPINFOHEADER), 1, pFile); uint8_t *buffer = pFrameRGB->data[0]; for (int h=0; h<height; h++) { for (int w=0; w<width; w++) { fwrite(buffer+2, 1, 1, pFile); fwrite(buffer+1, 1, 1, pFile); fwrite(buffer, 1, 1, pFile); buffer += 3; } } fclose(pFile); return true; } int main(int argc, char** argv) { //This registers all available file formats and codecs with //the library so they will be used automatically //when a file with the corresponding format/codec is opened. //Note that you only need to call av_register_all() once, av_register_all(); char err_msg[50]; int ret; const char* filename = argc > 1 ? argv[1] : "19.mp4"; //在使用前,一定要初始化,有网友就是因为没有初始化而被坑 //avformat_open_input其内部实现为: //int avformat_open_input(AVFormatContext **ps, const char *filename, // AVInputFormat *fmt, AVDictionary **options) // { // AVFormatContext *s = *ps; // int ret = 0; // AVDictionary *tmp = NULL; // ID3v2ExtraMeta *id3v2_extra_meta = NULL; // if (!s && !(s = avformat_alloc_context())) // return AVERROR(ENOMEM); // .... // } AVFormatContext* format_ctx = NULL; //我用的是2.2.1版本,没有av_open_input_file函数了,可以到 //http://ffmpeg.zeranoe.com/builds/win32/dev/ //下载低版本的ffmpeg。 //第三个参数用来指定视频的格式,第四个参数是格式选项。设置为NULL即可。 //libavformat可以自动获取。 if( (ret = avformat_open_input(&format_ctx, filename, NULL, NULL)) < 0 ) { //如果是gcc编译的话,用av_err2str是没有问题的,但用C++编译就有问题。 //查看av_err2str是一个宏。其实现用了一个临近数组吧。 //fun( (char[34]){0} );像这种形式,c语言支持这种形式, //但C++不支持。所以,不能用av_err2str。取而代之用av_strerror av_strerror(ret, err_msg, sizeof(err_msg) ); fprintf(stderr, "Cann't open the file: %s", err_msg); return -1; } //通过读取媒体文件的中的包来获取媒体文件中的流信息 //也就是把媒体文件中的音视频流等信息读出来,保存在容器中,以便解码时使用 if( (ret = avformat_find_stream_info(format_ctx, NULL)) < 0 ) { av_strerror(ret, err_msg, sizeof(err_msg) ); fprintf(stderr, "cann't find stream info: %s", err_msg); return -1; } //这个函数将填充format_ctx->streams成员。 //注意,并不是用filename来填充. //streams的声明是 AVStream * streams[MAX_STREAMS] //最后一个参数输出是否为output av_dump_format(format_ctx, 0, filename, 0); //此时,streams成员数组指向了一系列的帧,帧数由nb_streams成员指明 int video_stream = -1; int i; for(i = 0; i < format_ctx->nb_streams; ++i) { //查找第一个视频帧 if( format_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO ) { video_stream = i; break; } } if( video_stream == -1 ) { fputs("cann't find the video frame", stderr); return -1; } //stream里面关于编解码器的信息 被称为 编解码器上下文。 //其包含了stream会使用的所有编解码器信息。现在用codec_ctx指向其 AVCodecContext* codec_ctx = format_ctx->streams[video_stream]->codec; //下面来获取编解码器,并打开之 //查找解码器之前,必须先调用av_register_all注册所有支持的解码器 //查找成功返回解码器指针,否则返回NULL //音视频解码器保存在一个链表中,查找过程中,函数从头到尾遍历链表,通过比较解码器的ID来查找 AVCodec* decoder = avcodec_find_decoder(codec_ctx->codec_id); if( decoder == NULL ) { fputs("Unsupported codec!\n", stderr); return -1; } //AVDictionary* option_dict = NULL; //使用给定的AVCodec初始化AVCodecContext if( avcodec_open2(codec_ctx, decoder, NULL) < 0) { fputs("Could not open codec\n", stderr); return -1; } //获取存放一个帧的空间 AVFrame* frame = av_frame_alloc(); //av_molloc只是malloc函数的简单包装, //其保证分配的地址都是经过对齐的。当我们不再使用的时候, //必须要用av_free来释放内存 AVFrame* frame_rgb = av_frame_alloc(); if( frame == NULL || frame_rgb == NULL) { fputs("alloc frame place fail", stderr); return -1; } //虽然有了申请存放帧的空间,但还需要申请一个空间来存放帧对应的原始数据, //因为我们转换的时候需要用到。 //get the size we need int byte_num = avpicture_get_size(PIX_FMT_RGB24, codec_ctx->width, codec_ctx->height); uint8_t* buffer = (uint8_t*)av_malloc(byte_num * sizeof(uint8_t)); //用avpicture_fill函数把一个帧和一个buffer, 格式、大小关联起来 // avpicture_fill((AVPicture*)frame_rgb, buffer, PIX_FMT_RGB24, codec_ctx->width, codec_ctx->height); SwsContext *sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt, codec_ctx->width, codec_ctx->height, PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL); int yuv420p_bytes = avpicture_get_size(AV_PIX_FMT_YUV420P, codec_ctx->width, codec_ctx->height); uint8_t* yuv420p_buff = (uint8_t*)av_malloc(yuv420p_bytes); AVFrame* yuv420p_frame = av_frame_alloc(); avpicture_fill((AVPicture*)yuv420p_frame, yuv420p_buff, AV_PIX_FMT_YUV420P, codec_ctx->width, codec_ctx->height); SwsContext* yuv_sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt, codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL); //之前的结构体都是声明为指针,内存由ffmpeg分配,而这个是变量。 //估计是因为这个包的空间肯定会重复使用,没必要要求ffmpeg库分配。 AVPacket packet; int frame_finished; i = 0; //从视频中读取一个包,并保存到packet中 while( av_read_frame(format_ctx, &packet) >= 0 ) { //前面我们已经获取了第一个视频帧的下标,video_stream保存了该下标 if( packet.stream_index == video_stream ) { //对视频帧进行解码. //如果有帧被解压,frame_finished将被赋予非0值,否则为0 //数据将从packet中解压到frame中。 //在前面的avcodec_open2函数中,codec_ctx获取了解码器。现在将用这个解码器来解码 avcodec_decode_video2(codec_ctx, frame, &frame_finished, &packet); if( frame_finished ) { //Convert the image from its native format to RGB sws_scale(sws_ctx, frame->data, frame->linesize, 0, codec_ctx->height, frame_rgb->data, frame_rgb->linesize); sws_scale(yuv_sws_ctx, frame->data, frame->linesize, 0, codec_ctx->height, yuv420p_frame->data, yuv420p_frame->linesize); fwrite(yuv420p_frame->data[0], 1, yuv420p_bytes, fout); if( ++i <= 5 ) { saveAsBitmap(frame_rgb, codec_ctx->width, codec_ctx->height, i); } } } //需要注意的是:我们只是定义了一个packet 结构体。 //而结构体有一个data指针,我们定义这个变量时, //并没有申请一个内存并让data指向之。这个工作有ffmpeg去完成。 //所以需要调用av_free_packet av_free_packet(&packet); } av_free(frame); av_free(frame_rgb); av_free(buffer); avcodec_close(codec_ctx); avformat_close_input(&format_ctx); sws_freeContext(sws_ctx); return 0; }