ffmpeg的tutorial中文版学习笔记(一)

在网上下载了一些pdf版的ffmpeg的tutorial中文版,在学习过程中发现有很多错误,这些错误,或者是文章中的代码中的变量作者没有定义,或者是由于ffmpeg一直在更新,”以时俱进“,而这些资料早已年久失修,一些函数早已更名,或被别的函数替代,学习过程中发现很多问题,故决定做下笔记,做出总结:
          FFMPEG 是一个很好的库,可以用来创建视频应用或者生成特定的工具。FFMPEG 几乎为你把所有的繁重工作都做了,比 如解码、编码、复用和解复用。这使得 多媒体应用程序变得容易编写。它是一个简单的,用C 编写的,快速的并且能够 解码 几乎所有你能用到的格式,当然也包括编码多种格式。
          唯一的问题是它的文档基本上是没有的。有一个单独的指导讲了它的基本原理另 外还有一个使用doxygen 生成的文档。这就是为什么当我决定研究 FFMPEG 来弄 清楚音视频应用程序是如何工作的过程中,我决定把这个过程用文档的形式记录 并且发布出来作为初学指导的原因。
          在FFMPEG 工程中有一个示例的程序叫作ffplay。它是一个用C 编写的利用 ffmpeg 来实现完整视频播放的简单播放器。这个指导将从原来Martin Bohme 写 的一个更新版本的指导开始(我借鉴了一些),基于Fabrice Bellard 的ffplay, 我将从那里开发一个可以使用的视频播放器。在每一个指导中,我将介 绍一个 或者两个新的思想并且讲解我们如何来实现它。每一个指导都会有一个C 源文 件,你可以下载,编译并沿着这条思路来自己做。源文件将向你展示一个真正 的 程序是如何运行,我们如何来调用所有的部件,也将告诉你在这个指导中技术实
现的细节并不重要。当我们结束这个指导的时候,我 们将有一个少于1000 行代 码的可以工作的视频播放器。
          在写播放器的过程中,我们将使用SDL 来输出音频和视频。SDL 是一个优秀的跨 平台的多媒体库,被用在MPEG 播放、模拟器和很多视频游戏中。你将需要下载 并安装SDL 开发库到你的系统中,以便于编译这个指导中的程序。
这篇指导适用于具有相当编程背景的人。至少至少应该懂得C 并且有队列和互斥 量等概念。你应当了解基本的多媒体中的像波形一类的概念,但是你不必知道的 太 多,因为我将在这篇指导中介绍很多这样的概念。
     更新:我修正了在指导7 和8 中的一些代码错误,也添加-lavutil 参数。
指 导1:制作屏幕录像
源代码:tutorial01.c
概要
          电影文件有很多基本的组成部分。首先,文件本身被称为容 器Container,容 器的类型决定了信息被存放在文件中的位置。AVI 和 Quicktime 就是容器的例 子。接着,你有一组流, 例如,你经常有的是一个音频流和一个视频流。(一 个流只是一种想像出来的词语,用来表示一连 串的通过时间来串连的数据元 素)。在流中的数据元素被称为帧Frame。 每个流是由不同的编码器来编码生 成的。编解码器描述了实际的数据是如何被编码Coded 和解码DECoded 的,因此 它的名字叫做CODEC。Divx 和 MP3 就是编解码器的例子。接着从流中被读出来 的叫做包 Packets。包是一段数据,它包含了一段可以被解码成方便我们最后在 应用程序中操作的原始帧的数据。根据我们的目的,每个包包含 了完整的帧或 者对于音频来说是许多格式的完整帧。
     基本上来说,处理视频和音频流是很容易的:
     10 从video.avi 文件中打开视频流video_stream
     20 从视频流中读取包到帧中
     30 如果这个帧还不完整,跳到20
     40 对这个帧进行一些操作
     50 跳回到20
         虽然很多程序可能在对帧进行操作的时候非常的复杂,但是在这个程序中若使用ffmpeg 来处理多种媒体是相当容易的 。因此在这篇指导中,我们将打开一个文件,读 取里面的视频流,而且我们对帧的操作将是把这个帧写到一个PPM 文件中。
打开文件
          首先来看一下我们如何打开一个文件。通过ffmpeg,你必需先初始化这个库: (注意在某些系统中必需 用<ffmpeg/avcodec.h>和<ffmpeg/avformat.h>来替 换)
[cpp]  view plain  copy
 print ?
  1. #include "libavcodec/avcodec.h"  
  2. #include "libavformat/avformat.h"  
  3. #include "libswscale/swscale.h"  
  4. #include <stdio.h>  
  5. void SaveFrame(AVFrame *pFrame,int width,int height,int iFrame);  
  6. int main(int argc,char * argv[])  
  7. {  
  8.     av_register_all();  

          这里注册了所有的文件格式和编解码器的库,所以它们将被自动的使用在被打 开的合适格式的文件上。注意你只需要调用av_register_all()一次,因此我们 在主函数main()中来调用它。如果你喜欢, 也可以只注册特定的格式和编解码 器,但是通常你没有必要这样做。 现在我们可以真正的打开文件:
[cpp]  view plain  copy
 print ?
  1. AVFormatContext *pFormatCtx;  
  2. pFormatCtx=avformat_alloc_context();  
  3. //Open video file  
  4. #ifdef _FFMPEG_0_6_  
  5.      if(av_open_input_file(&pFormatCtx,argv[1],NULL,0,NULL))  
  6. #else  
  7.      if (avformat_open_input(&pFormatCtx,argv[1],NULL,NULL)!=0)  
  8. #endif  
  9.           return -1;//Couldn't open file  
          我们通过第一个参数来获得文件名。这个av_open_input_file函数读取文件的头部并且把信息保存到 我们给的AVFormatContext 结构体中。最后三个参数用来指定特殊的文件格式, 缓冲大小和格式参数,但如果把它们设置为空NULL 或者0,libavformat 将自动 检测这些参数。(av_open_input_file函数现在已被avformat_open_input函数取代)
关于avformat_open_input:
[cpp]  view plain  copy
 print ?
  1. /** 
  2. * Open an input stream and read the header. The codecs are not opened. 
  3. * The stream must be closed with avformat_close_input(). 
  4. * 
  5. * @param ps Pointer to user-supplied AVFormatContext (allocated by avformat_alloc_context). 
  6. *           May be a pointer to NULL, in which case an AVFormatContext is allocated by this 
  7. *           function and written into ps. 
  8. *           Note that a user-supplied AVFormatContext will be freed on failure. 
  9. * @param filename Name of the stream to open. 
  10. * @param fmt If non-NULL, this parameter forces a specific input format. 
  11. *            Otherwise the format is autodetected. 
  12. * @param options  A dictionary filled with AVFormatContext and demuxer-private options. 
  13. *                 On return this parameter will be destroyed and replaced with a dict containing 
  14. *                 options that were not found. May be NULL. 
  15. * 
  16. * @return 0 on success, a negative AVERROR on failure. 
  17. * 
  18. * @note If you want to use custom IO, preallocate the format context and set its pb field. 
  19. */  
  20. int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);  
          这个函数 只是检测了文件的头部,所以接着我们需要检查在文件中的流的信息:
[cpp]  view plain  copy
 print ?
  1. //Retrieve stream information  
  2. #ifdef _FFMPEG_0_6_  
  3.      if (av_find_stream_info(pFormatCtx)<0)  
  4. #else  
  5.      if(avformat_find_stream_info(pFormatCtx,NULL)<0)  
  6. #endif  
  7.           return -1;//Couldn't find stream information  
这个av_find_stream_info函数(已被avformat_find_stream_info函数取代)为pFormatCtx->streams 填充上正确的信息。我们引进一个手工调试的 函数来看一下里面有什么:
[cpp]  view plain  copy
 print ?
  1. //Dump information about file onto standard error  
  2.  av_dump_format(pFormatCtx,0,argv[1],0);  

现在pFormatCtx->streams 仅仅是一组大小为pFormatCtx->nb_streams 的指针, 所以让我们先跳过它直到 我们找到一个视频流。

[cpp]  view plain  copy
 print ?
  1. int i;  
  2.  AVCodecContext *pCodecCtx;  
  3.  //Find the first video stream  
  4.  int videoStream=-1;  
  5.  printf("pFormatCtx->nb_streams=%d\n",pFormatCtx->nb_streams);  
  6.  for (i = 0; i < pFormatCtx->nb_streams; ++i)  
  7.  {  
  8.       if (pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)  
  9.       {  
  10.            videoStream=i;  
  11.            break;  
  12.       }  
  13.  }  
  14.  if (videoStream==-1)  
  15.       return -1;//Didn't find a video stream  
  16.  //Get a pointer to the codec context for the video stream  
  17.  pCodecCtx=pFormatCtx->streams[videoStream]->codec;  
  18.  printf("videoStream=%d\n", videoStream);  
源代码中的pFormatCtx->streams[i]->codec->codec_type类型为:
[cpp]  view plain  copy
 print ?
  1. enum AVMediaType {  
  2.     AVMEDIA_TYPE_UNKNOWN = -1,  ///< Usually treated as AVMEDIA_TYPE_DATA  
  3.     AVMEDIA_TYPE_VIDEO,  
  4.     AVMEDIA_TYPE_AUDIO,  
  5.     AVMEDIA_TYPE_DATA,          ///< Opaque data information usually continuous  
  6.     AVMEDIA_TYPE_SUBTITLE,  
  7.     AVMEDIA_TYPE_ATTACHMENT,    ///< Opaque data information usually sparse  
  8.     AVMEDIA_TYPE_NB  
  9. };  
          流中关于编解码器的信息就是被我们叫做"codec context"(编解码器上下文) 的东西。这里面包含了流中所使用的关于编解码器的所有信息,现在我们有了一 个指向它的指针。但是我们必需要找到真正的 编解码器并且打开它:
[cpp]  view plain  copy
 print ?
  1.      AVCodec *pCodec;     //Find the decoder for the video stream       
  2.      pCodec=avcodec_find_decoder(pCodecCtx->codec_id);       
  3.      if(pCodec==NULL)  
  4.      {  
  5.           fprintf(stderr,"Unsupported codec!\n");  
  6.           return -1; // Codec not found  
  7.      }  
  8.      //Open codec  
  9.      #ifdef _FFMPEG_0_6_  
  10.           if(avcodec_open(pCodecCtx,pCodec)<0)  
  11.      #else  
  12.           if(avcodec_open2(pCodecCtx,pCodec,NULL)<0)  
  13.      #endif  
  14.           return -1;//Could not open codec  
          有些人可能会从旧的指导中记得有两个关于这些代码其它部分:添加 CODEC_FLAG_TRUNCATED 到pCodecCtx->flags 和添 加一个hack 来粗糙的修正帧 率。这两个修正已经不再存在于ffplay.c 中。因此我必须假设它们不再必要。 我们移除了那些代码后还有一个需要指出的不同点:pCodecCtx->time_base 现 在已经保存了帧率的信息。time_base 是一个结构体,它里面有一个分子和分母 (AVRational)。我们使用分数的方式来表示帧率是因为很多编解码器使用非整数 的帧率(例如NTSC 使用29.97fps)。
保存数据
          现在我们需要找到一个地方来保存帧:
[cpp]  view plain  copy
 print ?
  1. AVFrame *pFrame;  
  2. //Allocate video frame  
  3. pFrame=avcodec_alloc_frame();  
          因为我们准备输出保存24 位RGB 色的PPM 文件,我们必需把帧的格式从原来的 转换为RGB。FFMPEG 将为我们做这些转换。在大多数项目中(包括我们的这个) 我们都想把原始的帧转换成一个特定的格式。让我们先为转换来申请一帧的内 存。
[cpp]  view plain  copy
 print ?
  1. //Allocate an AVFrame structure  
  2. AVFrame *pFrameRGB;  
  3. pFrameRGB=avcodec_alloc_frame();  
  4. if (pFrameRGB==NULL)  
  5.      return -1;  
          即使我们申请了一帧的内存,当转换的时候,我们仍然需要一个地方来放置原始 的数据。我们使用avpicture_get_size 来获得我们需要的大小, 然后手工申请 内存空间:
[cpp]  view plain  copy
 print ?
  1. uint8_t *buffer;  
  2. int numBytes;  
  3. //Determine required buffer size and allocate buffer  
  4. numBytes=avpicture_get_size(PIX_FMT_RGB24,pCodecCtx->width,pCodecCtx->height);  
  5. buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));  
av_malloc 是ffmpeg 的malloc,用来实现一个简单的malloc 的包装,这样来保 证内存地址是对齐的(4 字节对齐或者2 字节对齐)。它并不能保证你不被内 存泄漏,重复释放或者其它malloc 的问题所困扰。 现在我们使用avpicture_fill函数把帧和我们新申请到的内存结合。关于 AVPicture 的构成:AVPicture 结构体是AVFrame 结构体的子集――AVFrame 结 构体的开始部分与AVPicture 结构体是一样的。
[cpp]  view plain  copy
 print ?
  1. //Assign appropriate parts of buffer to image planes in pFrameRGB  
  2. //Note that pFrameRGB is an AVFrame,but AVFrame is a superset of AVPicture  
  3. avpicture_fill((AVPicture *)pFrameRGB,buffer,PIX_FMT_RGB24,pCodecCtx->width,pCodecCtx->height);  
其中的avpicture_fill()函数含义:
[cpp]  view plain  copy
 print ?
  1. /** 
  2. * Setup the picture fields based on the specified image parameters 
  3. * and the provided image data buffer. 
  4. * 
  5. * The picture fields are filled in by using the image data buffer 
  6. * pointed to by ptr. 
  7. * 
  8. * If ptr is NULL, the function will fill only the picture linesize 
  9. * array and return the required size for the image buffer. 
  10. * 
  11. * To allocate an image buffer and fill the picture data in one call, 
  12. * use avpicture_alloc(). 
  13. * 
  14. * @param picture       the picture to be filled in 
  15. * @param ptr           buffer where the image data is stored, or NULL 
  16. * @param pix_fmt       the pixel format of the image 
  17. * @param width         the width of the image in pixels 
  18. * @param height        the height of the image in pixels 
  19. * @return the size in bytes required for src, a negative error code 
  20. * in case of failure 
  21. * 
  22. * @see av_image_fill_arrays() 
  23. */  
  24. int avpicture_fill(AVPicture *picture, const uint8_t *ptr, enum AVPixelFormat pix_fmt, int width, int height);  
          最后,我们已经准备好来从流中读取数据了。
读取数据
          我们将要做的是通过读取包来读取整个视频流,然后把它解码成帧,最后转换 格式并保存。
[cpp]  view plain  copy
 print ?
  1. int frameFinished;  
  2. AVPacket packet;  
  3. i=0;  
  4. av_init_packet(&packet);//  
  5. while(av_read_frame(pFormatCtx,&packet)>=0)  
  6. {  
  7.      //printf("packet.stream_index=%d, packet.size=%d\n", packet.stream_index,packet.size);  
  8.      //Is this a packet from the video stream?  
  9.      if (packet.stream_index==videoStream)  
  10.      {  
  11.           //Decode video frame  
  12.           //avcodec_decode_video(pCodecCtx,pFrame,&frameFinished,packet.data,packet.size);  
  13.           avcodec_decode_video2(pCodecCtx,pFrame,&frameFinished,&packet);  
  14.           printf("frameFinished=%d\n",frameFinished );  
  15.           //Did we get a video frame?  
  16.           if (frameFinished)  
  17.           {  
  18.                //Convert the image from its native format to RGB  
  19.                //img_convert((AVPicture*)pFrameRGB,PIX_FMT_RGB24,(AVPicture*)pFrame,pCodecCtx->pix_fmt,pCodecCtx->width,pCodecCtx->height);  
  20.                static struct SwsContext *img_convert_ctx;  
  21.                img_convert_ctx=sws_getContext(pCodecCtx->width,pCodecCtx->height,pCodecCtx->pix_fmt,pCodecCtx->width,pCodecCtx->height,  
  22.                                               PIX_FMT_RGB24,SWS_BICUBIC,NULL,NULL,NULL);  
  23.                if(img_convert_ctx==NULL)  
  24.                {  
  25.                     fprintf(stderr,"Can not initialize the conversion context!\n");  
  26.                     exit(1);  
  27.                }  
  28.                sws_scale(img_convert_ctx,(const uint8_t *const)pFrame->data,pFrame->linesize,0,pCodecCtx->height,  
  29.                          pFrameRGB->data,pFrameRGB->linesize);  
  30.                //Save the frame to disk  
  31.                if (++i<=5)  
  32.                     SaveFrame(pFrameRGB,pCodecCtx->width,pCodecCtx->height,i);  
  33.           }  
  34.      }  
  35.      //Free the packet that was allocated by av_read_frame  
  36.      av_free_packet(&packet);  
  37. }  
          这个循环过程是比较简单的:av_read_frame()读取一个包并且把它保存到 AVPacket 结构体中。注意我们仅仅申请了一个包的结构体――ffmpeg 为我们申 请了内部的数据的内存并通过packet.data 指针来指向它。这些数据可以在后面 通过av_free_packet()函数来释放。函avcodec_decode_video()把包转换为帧。 然而当解码一个包的时候,我们可能没有得到我们需要的关于一帧的完整信息。因此, 当我们得 到一个帧的时候,avcodec_decode_video()为我们设置了帧结束标志 frameFinished。然后我们使用 img_convert()函数来把帧从原始格式 (pCodecCtx->pix_fmt)转换成为RGB 格式(img_convert()函数已被sws_scale()函数取代)。要记住,你可以把一个 AVFrame 结构体的指针转换为AVPicture 结构体的指针。最后我们把帧以及高度,宽度信息 传递给我们的SaveFrame 函数。
 SwsContext:视频分辩率、色彩空间变换时所需要的上下文句柄。
关于sws_scale()函数:
[cpp]  view plain  copy
 print ?
  1. /** 
  2. * Scale the image slice in srcSlice and put the resulting scaled 
  3. * slice in the image in dst. A slice is a sequence of consecutive 
  4. * rows in an image. 
  5. * 
  6. * Slices have to be provided in sequential order, either in 
  7. * top-bottom or bottom-top order. If slices are provided in 
  8. * non-sequential order the behavior of the function is undefined. 
  9. * 
  10. * @param c         the scaling context previously created with 
  11. *                  sws_getContext() 
  12. * @param srcSlice  the array containing the pointers to the planes of 
  13. *                  the source slice 
  14. * @param srcStride the array containing the strides for each plane of 
  15. *                  the source image 
  16. * @param srcSliceY the position in the source image of the slice to 
  17. *                  process, that is the number (counted starting from 
  18. *                  zero) in the image of the first row of the slice 
  19. * @param srcSliceH the height of the source slice, that is the number 
  20. *                  of rows in the slice 
  21. * @param dst       the array containing the pointers to the planes of 
  22. *                  the destination image 
  23. * @param dstStride the array containing the strides for each plane of 
  24. *                  the destination image 
  25. * @return          the height of the output slice 
  26. */  
  27. int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[], const int srcStride[], int srcSliceY, int srcSliceH, uint8_t *const dst[], const int dstStride[]);  
关于包Packets的注释
           从技术上讲一个包可以包含部分或者其它的数据,但是 ffmpeg 的解释器保证了 我们得到的包Packets 包含的要么是完整的要么是多种完整的帧。
          现在我们需要做的是让SaveFrame 函数能把RGB 信息定稿到一个PPM 格式的文件 中。我们将生成一个简单的PPM 格式文件,请相信,它是可以工作 的。
[cpp]  view plain  copy
 print ?
  1. void SaveFrame(AVFrame *pFrame,int width,int height,int iFrame)  
  2. {  
  3.      FILE *pFile;  
  4.      char szFilename[32];  
  5.      int y;  
  6.      //Open file  
  7.      sprintf(szFilename,"frame%d.ppm",iFrame);  
  8.      pFile=fopen(szFilename,"wb");  
  9.      if(pFile==NULL)  
  10.           return;  
  11.      //Write header  
  12.      fprintf(pFile, "P6\n%d %d\n255\n",width,height );  
  13.      //Write pixel data  
  14.      for (y=0;y<height; ++y)  
  15.           fwrite(pFrame->data[0]+y*pFrame->linesize[0],1,width*3,pFile);  
  16.      //Close file  
  17.      fclose(pFile);  
  18. }  
          我们做了一些标准的文件打开动作,然后写入RGB 数据。我们一次向文件写入一 行数据。PPM 格式文件的是一种包含一长串的RGB 数据的文件。如果你了解 HTML 色彩表示的方式,那么它就类似于把每个像素的颜色头对头的展开,就像 #ff0000#ff0000....就表示了了个红色的屏幕。(它被保存成 二进制方式并且 没有分隔符,但是你自己是知道如何分隔的)。文件的头部表示了图像的宽度和 高度以及最大的RGB 值的大小
关于PPM文件:
 ppm是一种简单的图像格式,仅包含格式、图像宽高、bit数等信息和图像数据。
图像数据的保存格式可以用ASCII码,也可用二进制,下面只说说一种ppm格式中比较简单的一种:24位彩色、二进制保存的图像。
文件头+rgb数据:
P6\n
width height\n
255\n
rgbrgb...
其中P6表示ppm的这种格式;\n表示换行符;width和height表示图像的宽高,用空格隔开;255表示每个颜色分量的最大值;rgb数据从上到下,从左到右排放
    文件头由3行文本组成,可由fgets读出
    1)第一行为“P6",表示文件类型
    2)第二行为图像的宽度和高度
    3)第三行为最大的象素值
    接下来是图像数据块。按行顺序存储。每个象素占3个字节,依次为红绿蓝通道,每个通道为1字节整
    数。左上角为坐标原点。
          现在,回顾我们的main()函数。一旦我们开始读取完视频流,我们必需清理一 切:
[cpp]  view plain  copy
 print ?
  1. //Free the RGB image  
  2. av_free(buffer);  
  3. av_free(pFrameRGB);  
  4. //Free the YUV frame  
  5. av_free(pFrame);  
  6. //Close the codec  
  7. avcodec_close(pCodecCtx);  
  8. //Close the video file  
  9. #ifdef _FFMPEG_0_6_  
  10.      av_close_input_file(pFormatCtx);  
  11. #else  
  12.      avformat_close_input(&pFormatCtx);  
  13. #endif  
  14. avformat_free_context(pFormatCtx);  
  15. return 0;  
          你会注意到我们使用av_free 来释放我们使用avcode_alloc_fram 和av_malloc 来分配的内存。
在我Linux系统下编译的命令:
gcc ./tutorial01.c    -o ./tutorial01  -lavutil -lavformat -lavcodec -lswscale -lz  -lm -I /home/Jiakun/ffmpeg_build/include -L /home/Jiakun/ffmpeg_build/lib/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值