ffmpeg作为一个支持非常多视频、音频格式的开源项目,其应用灰常广泛。今儿在这我们就探讨一下读者对其的理解,其中不泛错误谬误,望各位大大批评指教。这样做的原因主要是官方的文档比较匮乏。经过无数摸索,有一些经验分享。
ffmpeg里有几个重要的概念,熟悉它们以后,事情就变得简单多了。
AVFormatContext代表一个打开的文件或者别的媒体,总之可以说代表数据的来源。视频和音频是按照一定格式存储在文件中的。这种格式仅仅指定如何把视频和音频流区分开来,至于视频如何变成图像,那是解码。平常所说的AVI格式,也就是上面所说的格式,里面视频、音频的编码方式还是可以随意的。
ffmpeg中的AVFormat库可以帮助进行这一“分拆音视频流”的过程;而AVCodec则帮助解码视频。
AVInputFormat *inputFmt; AVFormatContext *fmtCtx = NULL; inputFmt = av_find_input_format("avi"); /* 打开“AVI”的输入格式 */ if (inputFmt == NULL) { /* Error processing */ } if (av_open_input_file(&fmtCtx,"/test/test.avi", inputFmt, 0, NULL) != 0) { /* Error processing */ }
这里为了方便说明,先暂时假设输入的文件是AVI格式的。在很多情况下,我们都不知道文件是什么格式的,FFMPEG提供了探测的方法,这将在下文再提到。
int i, found_stream_index; AVCodecContext *videoDecodeCtx = NULL; for (i=0;i<fmtCtx->nb_streams;i++){ if (fmtCtx->streams[i]->codec->codec_type == AVMEDIA_VIDEO){ videoDecodeCtx = fmtCtx->streams[i]->codec; found_stream_index = i; break; } } if (decodeCtx == NULL) { /* 找不到视频流,错误处理 */ }
当一个AVFormatContext成功打开后,在其结构内就会存着该文件内包含的所有流(视频流、音频流等),每个流(AVStream)都会自动打开一个解码器上下文(AVCodecContext),即提供给解码器的参数,它并非真正的解码器,只是解码器参数!
以上代码中在文件所含的所有流中寻找视频流并得到一个解码器上下文。
AVCodec *videoDecoder; videoDecoder = avcodec_find_encoder(videoDecodeCtx->codec_id); if (videoDecoder == NULL){ /* 找不到解码器,错误处理 */ } if (videoDecoder->capabilities & CODEC_CAP_TRUNCATED){ videoDecodeCtx->flags |= CODEC_FLAG_TRUNCATED; } if (avcodec_open(videoDecodeCtx,videoDecoder) < 0){ /* 打不开解码器,错误处理 */ }
这一步真正地去寻找一个解码器,并使用之前获得的参数打开它。并非任何编码都会被支持,并非任何参数都会被解码器都会被支持,所以一定要进行错误处理哦。
CODEC_CAP_TRUNCATED指明解码器可以支持所谓“碎片输入”,先不管它,等会儿再说。
如果都成功了,那么解码器就成功打开了。接下来就可以开始解码了。
AVPacket pkt; AVFrame *frame = avcodec_alloc_frame(); int got_picture = 0, rc = 0; while (1){ av_init_packet(&pkt); rc = av_read_packet(fmtCtx,&pkt); /* 获取一个包的数据 */ if (rc != 0) break; if (pkt.stream_index != found_stream_index) goto free_pkt_continue; /* 不是所关心的视频流的数据,舍去 */ if ( avcodec_decode_video2 (videoDecodeCtx, frame, &got_picture, &pkt) < 0) { /* 核心函数:解码。错误处理。 */ } if (got_picture) { { /* 处理获得的图像(存储在AVFrame里) */ } av_free(frame); frame = avcodec_alloc_frame(); } free_pkt_continue: av_free_packet(&pkt); }
其实这个过程很简单:
1. 读取数据;
2. 解码。
原始数据是以一个个AVPacket的形式存放的,而解出来的一帧图像存在AVFrame里。一帧图像可以由很多AVPacket组成,所以使用一个got_picture指针来指示是否获得一帧图像。
之前说的是关于CAP_TRUNCATED的问题,就是表明解码器是否支持AVFrame的边界和AVPacket的边界不重合。数据应该可以是零散的,解码器的这个能力很重要,它可以处理来自数据中的任一段(对于网络数据很有用)。
这里说的图像转换,并非类似PNG到JPG的转换,而主要是色彩空间和大小伸缩的转换。例如MPEG4的解码器解出来的图像格式是YUV420P格式,而若让Qt来渲染图像,则需要RGB格式以及任意的大小。
ffmpeg中提供swscale库来提供图像转换支持。现在假设我们在上一步解码出来的数据存放于AVFrame *frame中,我们有:
SwsContext *swsCtx; int dst_width = 320, /* 目标宽度 */ dst_height = 240, /* 目标高度 */ dst_pix_fmt = PIX_FMT_RGB24; /* 目标图像格式 */ AVFrame *convertedFrame = avcodec_alloc_frame(); swsCtx = sws_getContext ( videoDecodeCtx->width, videoDecodeCtx->height, videoDecodeCtx->pix_fmt, dst_width, dst_height, dst_pix_fmt, SWS_FAST_BILINEAR, NULL, NULL, NULL ); if (swsCtx == NULL) { /* 转换上下文初始化失败,错误处理 */ } /* 分配转换需要的内存 */ avpicture_fill ( (AVPicture *)convertedFrame,dst_pix_fmt, dst_width, dst_height); if (sws_scale(swsCtx,frame->data,frame->linesize,0, videoDecodeCtx->height,convertedFrame->data,convertedFrame->linesize) <= 0) { /* 转换失败,错误处理 */ }