学习FFmpeg API – 解码视频

ffmpeg是编解码的利器,用了好久,之前看过dranger 的教程,很是精彩,受益颇多,是学习ffmpeg api很好的材料。惋惜的是其针对的ffmpeg版本已经比较老了,而ffmpeg的更新又很快,有些API已经彻底换掉了,致使dranger教程中的 代码已经没法编译,正好最近须要使用ffmpeg,因而就利用dranger的教程和代码,本身边学边记录,因而也就有了这个所谓的 New FFmpeg Tutorial,但愿对学习ffmpeg的人有所帮助。php

Tutorial 1: Decoding video frames

source code:videoframe.chtml

视频播放过程

首先简单介绍如下视频文件的相关知识。咱们平时看到的视频文件有许多格式,好比 avi, mkv, rmvb, mov, mp4等等,这些被称为容器Container), 不一样的容器格式规定了其中音视频数据的组织方式(也包括其余数据,好比字幕等)。容器中通常会封装有视频和音频轨,也称为视频流(stream)和音频 流,播放视频文件的第一步就是根据视频文件的格式,解析(demux)出其中封装的视频流、音频流以及字幕(若是有的话),解析的数据读到包 (packet)中,每一个包里保存的是视频帧(frame)或音频帧,而后分别对视频帧和音频帧调用相应的解码器(decoder)进行解码,好比使用 H.264编码的视频和MP3编码的音频,会相应的调用H.264解码器和MP3解码器,解码以后获得的就是原始的图像(YUV or RGB)和声音(PCM)数据,而后根据同步好的时间将图像显示到屏幕上,将声音输出到声卡,最终就是咱们看到的视频。git

FFmpeg的API就是根据这个过程设计的,所以使用FFmpeg来处理视频文件的方法很是直观简单。下面就一步一步介绍从视频文件中解码出图片的过程。api

声明变量

首先定义整个过程当中须要使用到的变量:缓存

int main(int argc, const char *argv[])
{
  AVFormatContext *pFormatCtx = NULL;
  int             i, videoStream;
  AVCodecContext  *pCodecCtx;
  AVCodec         *pCodec;
  AVFrame         *pFrame;
  AVFrame         *pFrameRGB;
  AVPacket        packet;
  int             frameFinished;
  int             numBytes;
  uint8_t         *buffer;

  • AVFormatContext:保存须要读入的文件的格式信息,好比流的个数以及流数据等数据结构

  • AVCodecCotext:保存了相应流的详细编码信息,好比视频的宽、高,编码类型等。ide

  • pCodec:真正的编解码器,其中有编解码须要调用的函数函数

  • AVFrame:用于保存数据帧的数据结构,这里的两个帧分别是保存颜色转换先后的两帧图像学习

  • AVPacket:解析文件时会将音/视频帧读入到packet中ui

打开文件

接下来咱们打开一个视频文件。

av_register_all();

av_register_all 定义在 libavformat 里,调用它用以注册全部支持的文件格式以及编解码器,从其实现代码里能够看到它会调用 avcodec_register_all,所以以后就能够用全部ffmpeg支持的codec了。

if( avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0 )
    return -1;

使用新的API avformat_open_input来打开一个文件,第一个参数是一个AVFormatContext指针变量的地址,它会根据打开的文件信息填充AVFormatContext,须要注意的是,此处的pFormatContext必须为NULL或由avformat_alloc_context分配获得,这也是上一节中将其初始化为NULL的缘由,不然此函数调用会出问题。第二个参数是打开的文件名,经过argv[1]指定,也就是命令行的第一个参数。后两个参数分别用于指定特定的输入格式(AVInputFormat)以及指定文件打开额外参数的AVDictionary结构,这里均留做NULL。

if( avformat_find_stream_info(pFormatCtx, NULL ) < 0 )
    return -1;
 
  av_dump_format(pFormatCtx, -1, argv[1], 0);

avformat_open_input函数只是读文件头,并不会填充流信息,所以咱们须要接下来调用avformat_find_stream_info获取文件中的流信息,此函数会读取packet,并肯定文件中全部的流信息,设置pFormatCtx->streams指向文件中的流,但此函数并不会改变文件指针,读取的packet会给后面的解码进行处理。
最后调用一个帮助函数av_dump_format,输出文件的信息,也就是咱们在使用ffmpeg时能看到的文件详细信息。第二个参数指定输出哪条流的信息,-1表示给ffmpeg本身选择。最后一个参数用于指定dump的是否是输出文件,咱们dump的是输入文件,所以必定要是0。

如今 pFormatCtx->streams 中已经有全部流了,所以如今咱们遍历它找到第一条视频流:

  videoStream = -1;
  for( i = 0; i < pFormatCtx->nb_streams; i++ )
    if( pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
      videoStream = i;
      break;
    }
 
  if( videoStream == -1 )
    return -1;

codec_type 的宏定义已经由之前的 CODEC_TYPE_VIDEO 改成 AVMEDIA_TYPE_VIDEO 了。接下来咱们经过这条 video stream 的编解码信息打开相应的解码器:

  pCodecCtx = pFormatCtx->streams[videoStream]->codec;
 
  pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
  if( pCodec == NULL )
    return -1;
 
  if( avcodec_open2(pCodecCtx, pCodec, NULL) < 0 )
    return -1;

分配图像缓存

接下来咱们准备给即将解码的图片分配内存空间。

pFrame = avcodec_alloc_frame();
  if( pFrame == NULL )
    return -1;
 
  pFrameRGB = avcodec_alloc_frame();
  if( pFrameRGB == NULL )
    return -1;

调用 avcodec_alloc_frame 分配帧,由于最后咱们会将图像写成 24-bits RGB 的 PPM 文件,所以这里须要两个 AVFrame,pFrame用于存储解码后的数据,pFrameRGB用于存储转换后的数据:

  numBytes = avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
				pCodecCtx->height);

这里调用 avpicture_get_size,根据 pCodecCtx 中原始图像的宽高计算 RGB24 格式的图像须要占用的空间大小,这是为了以后给 pFrameRGB 分配空间:

buffer = av_malloc(numBytes);
 
  avpicture_fill( (AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
		  pCodecCtx->width, pCodecCtx->height);

接着上面的,首先是用 av_malloc 分配上面计算大小的内存空间,而后调用 avpicture_fill 将 pFrameRGB 跟 buffer 指向的内存关联起来。

获取图像

OK,一切准备好就能够开始从文件中读取视频帧并解码获得图像了。

  i = 0;
  while( av_read_frame(pFormatCtx, &packet) >= 0 ) {
    if( packet.stream_index == videoStream ) {
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
 
      if( frameFinished ) {
	struct SwsContext *img_convert_ctx = NULL;
	img_convert_ctx = 
	  sws_getCachedContext(img_convert_ctx, pCodecCtx->width,
			       pCodecCtx->height, pCodecCtx->pix_fmt,
			       pCodecCtx->width, pCodecCtx->height,
			       PIX_FMT_RGB24, SWS_BICUBIC,
			       NULL, NULL, NULL);
	if( !img_convert_ctx ) {
	  fprintf(stderr, "Cannot initialize sws conversion context\n");
	  exit(1);
	}
	sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data,
		  pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
		  pFrameRGB->linesize);
	if( i++ < 50 )
	  SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);
      }
    }
    av_free_packet(&packet);
  }

av_read_frame 从文件中读取一个packet,对于视频来讲一个packet里面包含一帧图像数据,音频可能包含多个帧(当音频帧长度固定时),读到这一帧后,若是是视频帧,则使用 avcodec_decode_video2 对packet中的帧进行解码,有时候解码器并不能从一个packet中解码获得一帧图像数据(好比在须要其余参考帧的状况下),所以会设置 frameFinished,若是已经获得下一帧图像则设置 frameFinished 非零,不然为零。因此这里咱们判断 frameFinished 是否为零来肯定 pFrame 中是否已经获得解码的图像。注意在每次处理完后须要调用 av_free_packet 释放读取的packet。

解码获得图像后,颇有可能不是咱们想要的 RGB24 格式,所以须要使用 swscale 来作转换,调用 sws_getCachedContext 获得转换上下文,使用 sws_scale 将图形从解码后的格式转换为 RGB24,最后将前50帧写人 ppm 文件。最后释放图像以及关闭文件:

 av_free(buffer);
  av_free(pFrameRGB);
  av_free(pFrame);
  avcodec_close(pCodecCtx);
  avformat_close_input(&pFormatCtx);
 
  return 0;
}
 
static void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame)
{
  FILE *pFile;
  char szFilename[32];
  int y;
 
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile = fopen(szFilename, "wb");
  if( !pFile )
    return;
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);
 
  for( y = 0; y < height; y++ )
    fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);
 
  fclose(pFile);
}
相关文章
相关标签/搜索