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>来替 换)
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include <stdio.h>
void SaveFrame(AVFrame *pFrame,int width,int height,int iFrame);
int main(int argc,char * argv[])
{
    av_register_all();

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

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

 int i;
	 AVCodecContext *pCodecCtx;
	 //Find the first video stream
	 int videoStream=-1;
	 printf("pFormatCtx->nb_streams=%d\n",pFormatCtx->nb_streams);
	 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;//Didn't find a video stream
	 //Get a pointer to the codec context for the video stream
	 pCodecCtx=pFormatCtx->streams[videoStream]->codec;
	 printf("videoStream=%d\n", videoStream);
源代码中的pFormatCtx->streams[i]->codec->codec_type类型为:
enum AVMediaType {
	AVMEDIA_TYPE_UNKNOWN = -1,  ///< Usually treated as AVMEDIA_TYPE_DATA
	AVMEDIA_TYPE_VIDEO,
	AVMEDIA_TYPE_AUDIO,
	AVMEDIA_TYPE_DATA,          ///< Opaque data information usually continuous
	AVMEDIA_TYPE_SUBTITLE,
	AVMEDIA_TYPE_ATTACHMENT,    ///< Opaque data information usually sparse
	AVMEDIA_TYPE_NB
};
          流中关于编解码器的信息就是被我们叫做"codec context"(编解码器上下文) 的东西。这里面包含了流中所使用的关于编解码器的所有信息,现在我们有了一 个指向的指针。但是我们必需要找到真正的 编解码器并且打开它:
     AVCodec *pCodec;     //Find the decoder for the video stream     
     pCodec=avcodec_find_decoder(pCodecCtx->codec_id);     
     if(pCodec==NULL)
     {
          fprintf(stderr,"Unsupported codec!\n");
          return -1; // Codec not found
     }
     //Open codec
     #ifdef _FFMPEG_0_6_
          if(avcodec_open(pCodecCtx,pCodec)<0)
     #else
          if(avcodec_open2(pCodecCtx,pCodec,NULL)<0)
     #endif
          return -1;//Could not open codec

          有些人可能会从旧的指导中记得有两个关于这些代码其它部分:添加 CODEC_FLAG_TRUNCATED 到pCodecCtx->flags 和添 加一个hack 来粗糙的修正帧 率。这两个修正已经不存在于ffplay.c 中。因此我必须假设它们不再必要。 我们移除了那些代码后还有一个需要指出的不同点:pCodecCtx->time_base 现 在已经保存了帧率的信息。time_base 是一个结构体,它里面有一个分子和分母 (AVRational)。我们使用分数的方式来表示帧率是因为很多编解码器使用非整数 的帧率(例如NTSC 使用29.97fps)。
保存数据
          现在我们需要找到一个地方来保存帧:
     AVFrame *pFrame;
     //Allocate video frame
     pFrame=avcodec_alloc_frame();
          因为我们准备输出保存24 位RGB 色的PPM 文件,我们必需把帧的格式从原来的 转换为RGB。FFMPEG 将为我们做这些转换。在大多数项目中包括我们的这个 我们都想把原始的帧转换成一个特定的格式。让我们先为转换来申请一帧的内 存。
     //Allocate an AVFrame structure
     AVFrame *pFrameRGB;
     pFrameRGB=avcodec_alloc_frame();
     if (pFrameRGB==NULL)
          return -1;
          即使我们申请了一帧的内存,当转换的时候,我们仍然需要一个地方来放置原始 的数据。我们使用avpicture_get_size 来获得我们需要的大小, 然后手工申请 内存空间:
     uint8_t *buffer;
     int numBytes;
     //Determine required buffer size and allocate buffer
     numBytes=avpicture_get_size(PIX_FMT_RGB24,pCodecCtx->width,pCodecCtx->height);
     buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
av_malloc 是ffmpeg 的malloc,用来实现一个简单的malloc 的包装,这样来保 证内存地址是对齐的(4 字节对齐或者2 字节对齐)。它并不能保你不被内 存泄漏重复释放或者其它malloc 的问题所困扰。 现在我们使用avpicture_fill函数把帧和我们新申请到的内存结合。关于 AVPicture 的成:AVPicture 结构体是AVFrame 结构体的子集――AVFrame 结 构体的开始部分与AVPicture 结构体是一样的。
//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);
其中的avpicture_fill()函数含义:
/**
* Setup the picture fields based on the specified image parameters
* and the provided image data buffer.
*
* The picture fields are filled in by using the image data buffer
* pointed to by ptr.
*
* If ptr is NULL, the function will fill only the picture linesize
* array and return the required size for the image buffer.
*
* To allocate an image buffer and fill the picture data in one call,
* use avpicture_alloc().
*
* @param picture       the picture to be filled in
* @param ptr           buffer where the image data is stored, or NULL
* @param pix_fmt       the pixel format of the image
* @param width         the width of the image in pixels
* @param height        the height of the image in pixels
* @return the size in bytes required for src, a negative error code
* in case of failure
*
* @see av_image_fill_arrays()
*/
int avpicture_fill(AVPicture *picture, const uint8_t *ptr, enum AVPixelFormat pix_fmt, int width, int height);
          最后,我们已经准备好来从流中读取数据了。
读取数据
          我们将要做的是通过读取包来读取整个视频流,然后把它解码成帧,最后转换 格式并保存。
     int frameFinished;
     AVPacket packet;
     i=0;
     av_init_packet(&packet);//
     while(av_read_frame(pFormatCtx,&packet)>=0)
     {
          //printf("packet.stream_index=%d, packet.size=%d\n", packet.stream_index,packet.size);
          //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);
               avcodec_decode_video2(pCodecCtx,pFrame,&frameFinished,&packet);
               printf("frameFinished=%d\n",frameFinished );
               //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);
                    static struct SwsContext *img_convert_ctx;
                    img_convert_ctx=sws_getContext(pCodecCtx->width,pCodecCtx->height,pCodecCtx->pix_fmt,pCodecCtx->width,pCodecCtx->height,
                                                   PIX_FMT_RGB24,SWS_BICUBIC,NULL,NULL,NULL);
                    if(img_convert_ctx==NULL)
                    {
                         fprintf(stderr,"Can not initialize the 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);
                    //Save the frame to disk
                    if (++i<=5)
                         SaveFrame(pFrameRGB,pCodecCtx->width,pCodecCtx->height,i);
               }
          }
          //Free the packet that was allocated by av_read_frame
          av_free_packet(&packet);
     }
          这个循环过程是比较简单的: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()函数:
/**
* Scale the image slice in srcSlice and put the resulting scaled
* slice in the image in dst. A slice is a sequence of consecutive
* rows in an image.
*
* Slices have to be provided in sequential order, either in
* top-bottom or bottom-top order. If slices are provided in
* non-sequential order the behavior of the function is undefined.
*
* @param c         the scaling context previously created with
*                  sws_getContext()
* @param srcSlice  the array containing the pointers to the planes of
*                  the source slice
* @param srcStride the array containing the strides for each plane of
*                  the source image
* @param srcSliceY the position in the source image of the slice to
*                  process, that is the number (counted starting from
*                  zero) in the image of the first row of the slice
* @param srcSliceH the height of the source slice, that is the number
*                  of rows in the slice
* @param dst       the array containing the pointers to the planes of
*                  the destination image
* @param dstStride the array containing the strides for each plane of
*                  the destination image
* @return          the height of the output slice
*/
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 格式文件,请相信,它是可以工作 的。
void SaveFrame(AVFrame *pFrame,int width,int height,int iFrame)
{
     FILE *pFile;
     char szFilename[32];
     int y;
     //Open file
     sprintf(szFilename,"frame%d.ppm",iFrame);
     pFile=fopen(szFilename,"wb");
     if(pFile==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 数据的文件。如果你了解 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()函数。一旦我们开始读取完视频流,我们必需清理一 切:
     //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
     #ifdef _FFMPEG_0_6_
          av_close_input_file(pFormatCtx);
     #else
          avformat_close_input(&pFormatCtx);
     #endif
     avformat_free_context(pFormatCtx);
     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/
源代码:
见这里的github
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值