教程原地址为:http://dranger.com/ffmpeg/tutorial01.html,本人只是做了翻译和部分接口更新工作,保证其能正常工作。需要做一些预备工作

:::java
sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libavdevice-dev libavfilter-dev libavutil-dev libpostproc-dev 

首先我们需要了解视频文件的一些基本概念,视频文件本身被称作容器,例如avi或者是quicktime,容器的类型确定了文件的信息。然后,容器里装的东西叫流(stream),通常包括视频流和音频流(“流”的意思其实就是“随着时间推移的一段连续的数据元素”)。流中的数据元素叫做“帧”。每个流由不同的编解码器来编码,编解码器定义了数据如何编码(COded)和解码(DECoded),所以叫做编解码器(CODEC)。编解码器的例子有Divx和mp3。包(Packets),是从流中读取的,通过解码器解包,得到原始的帧,我们便可以对这些数据进行播放等的处理。对于我们来说,每个包包含完整的帧,或者多个音频帧。

在初级的水平,处理音视频流是非常简单的:

  • 从video.avi中获得视频流
  • 从视频流中解包得到帧
  • 如果帧不完整,重复第2步
  • 对帧进行相关操作
  • 重复第2步

用ffmpeg来处理多媒体就像上面的步骤那么简单,即使你的第4步可能很复杂。所以在本教程,我们先打开一个视频,读取视频流,获得帧,然后第4步是把帧数据存储为PPM文件。

##打开文件

我们先来看一下怎么打开一个视频文件,首先把头文件包含进来

:::c
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
...
int main(int argc, char *argv[]){
  av_register_all();

av_register_all只需要调用一次,他会注册所有可用的文件格式和编解码库,当文件被打开时他们将自动匹配相应的编解码库。如果你愿意,可以之注册个别的文件格式和编解码库。

现在我们真的要打开一个文件了

:::c
AVFormatContext *pFormatCtx;

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

我们从传入的第一个参数获得文件路径,这个函数会读取文件头信息,并把信息保存在我们存入的pFormatCtx结构体当中。这个函数后面三个参数分别是指定文件格式,缓存大小,和格式化选项,当我们设置为NULL或0时,libavformat会自动完成这些工作。

这个函数仅仅是获得了头信息,下一步我们要得到流信息

:::c
if(av_find_steam_info(pFormatCtx)<0)
  return -1

这个函数填充了pFormatCtx->streams流信息,

我们可以通过dump_format把信息打印出来

:::c
dump_format(pFormatCtx, 0, argv[1], 0);

pFromatCtx->streams只是大小为pFormateCtx->nb_streams的一系列的点,我们要从中得到视频流

:::c
int i;
AVCodecContext *pCodecCtx;
// Find the first video stream
videoStream=-1;
for(i=0; i<pformatctx->nb_streams; i++)
  if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO) 
  {
    videoStream=i;
    break;
  }   
if(videoStream==-1)
  return -1; // Didn't find a video stream</pformatctx-></pre>

// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;

pCodecCtx包含了这个流在用的编解码的所有信息,但我们仍需要通过他获得特定的解码器然后打开他。

:::c
AVCodec *pCodec;
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
  fprintf(stderr, "Unsupported codec!\n");
  return -1; // Codec not found
}
// Open codec
if(avcodec_open(pCodecCtx, pCodec)<0)
  return -1; // Could not open codec

##存储数据

现在我们需要一个地方来存储一帧

:::c
AVFrame *pFrame;
pFrame=avcodec_alloc_frame();

我们计划存储的PPM文件,其存储的数据是24位RGB,我们需要把得到的一帧从本地格式转换为RGB,ffmpeg可以帮我们完成这个工作。在很多工程里,我们都希望把原始帧转换到特定格式。现在就让我们来完成这个工作吧。

:::c
AVFrame *pFrameRGB;
pFrameRGB=avcodec_alloc_frame();
if(pFrameRGB==NULL)
  return -1;

即使我们分配了帧空间,我们仍然需要空间来存放转换时的raw数据,我们用avpicture_get_size来得到我们需要的空间,然后手动分配。

:::c
uint8_t *buffer;
int numBytes;
numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
                pCodecCtx->height);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

av_malloc是ffmpeg简单封装的一个分配函数,意在确保内存地址的对齐等,它不会保护你的内存泄漏,二次释放,或其他malloc问题。

现在,我们使用avpicture_fill来关联我们新分配的缓冲区的帧。AVPicture结构体是AVFrame结构体的一个子集,开始的AVFrame是和AVPicture相同的。

:::c
// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
       pCodecCtx->width, pCodecCtx->height);

我们准备读取流了!

##读取数据

我们要做的是通过包来读取整个视频流,然后解码到帧当中,一但一帧完成了,我们将转换并保存它。这里跟教程的接口调用有不一样的地方。

:::c
int frameFinished;
AVPacket packet;

i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
  // Is this a packet from the video stream?
  if(packet.stream_index==videoStream) {
    // Decode video frame
//    avcodec_decode_video(pCodecCtx, pFrame, &frameFinished, 
//           packet.data, packet.size);
      int result;
      avcodec_decode_video2(pCodecCtx,pFrame,&frameFinished,
              &packet);

// Did we get a video frame?
    if(frameFinished) {
  //  Convert the image from its native format to RGB
//    img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, 
//                  (AVPicture*)pFrame, pCodecCtx->pix_fmt,pCodecCtx->width, 
//                    pCodecCtx->height);

img_convert_ctx = sws_getContext(pCodecCtx->width,
                                      pCodecCtx->height,
                                      pCodecCtx->pix_fmt,
                                      pCodecCtx->width,
                                      pCodecCtx->height,
                                      PIX_FMT_RGB24,
                                      SWS_BICUBIC,NULL,
                                      NULL,NULL);
    result = sws_scale(img_convert_ctx,
              (const uint8_t* const*)pFrame->data,
              pFrame->linesize,
              0,
              pCodecCtx->height,
              pFrameRGB->data,
              pFrameRGB->linesize);
    printf("get result is %d~~~~~~~~~\n",result);
    // Save the frame to disk
    printf("i is %d \n",i);
    if(++i<=5)
      SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height,
            i);
      }
    }

// Free the packet that was allocated by av_read_frame
   av_free_packet(&packet);
}

现在我们需要做的事情就是写SaveFrame函数来保存数据到PPM文件。

:::c
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
  FILE *pFile;
  char szFilename[32];
  int  y;</pre>

printf("start sws_scale\n");
  // Open file
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile=fopen(szFilename, "wb");
  if(pFile==NULL)
  {
    printf("pFile is null");
    return;
  }

// Write header
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);

// Write pixel data
  for(y=0; y<height; y++)
    fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);

// Close file
  fclose(pFile);
}

我们做了一些标准文件打开,然后写RGB数据,我们一次写一行文件,PPM文件就是简单地把RGB信息保存为一长串。头部记录着宽和高,和RGB的最大尺寸。

现在回到main函数,读完视频流后,我们需要释放一切

:::c
// Free the RGB image
  av_free(buffer);
  av_free(pFrameRGB);

// Free the YUV frame
  av_free(pFrame);

// Close the codec
  avcodec_close(pCodecCtx);

// Close the video file
  av_close_input_file(pFormatCtx);

return 0;

这些就是全部代码来,现在你需要linux系统编译和运行

:::java
gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lswscale -lz

得到tutorial01,执行以下语句可得到同级目录下的5个PPM文件

:::java
./tutorial01 hello.mp4 

需要源码可留言。