【正点原子探索者STM32F407开发板例程连载+教学】第50章 视频播放器实验

2019-07-20 22:26发布

第五十章 视频播放器实验

  [mw_shl_code=c,true]1.硬件平台:正点原子探索者STM32F407开发板 2.软件平台:MDK5.1 3.固件库版本:V1.4.0[/mw_shl_code]
STM32F4的处理能力,不仅可以软解码音频,还可以用来播放视频!本章,我们将使用探索者STM32F4开发板来播放AVI视频,本章我们将实现一个简单的视频播放器,实现AVI视频播放。本章分为如下几个部: 50.1 AVI&libjpeg简介 50.2 硬件设计 50.3 软件设计 50.4 下载验证  

50.1 AVI&libjpeg简介  

本章,我们使用libjpeg(由IJG提供),来实现MJPG编码的AVI格式视频播放,我们先来简单介绍一下AVIlibjpeg

50.1.1 AVI简介

AVI是音频视频交错(Audio Video Interleaved)的英文缩写,它是微软开发的一种符合RIFF文件规范的数字音频与视频文件格式,原先用于Microsoft Video for Windows (简称VFW)环境,现在已被多数操作系统直接支持。 AVI格式允许视频和音频交错在一起同步播放,支持256 {MOD}和RLE压缩,但AVI文件并未限定压缩标准,AVI仅仅是一个容器,用不同压缩算法生成的AVI文件,必须使用相应的解压缩算法才能播放出来。比如本章,我们使用的AVI,其音频数据采用16位线性PCM格式(未压缩),而视频数据,则采用MJPG编码方式。 在介绍AVI文件前,我们要先来看看RIFF文件结构。AVI文件采用的是RIFF文件结构方式,RIFFResource Interchange File Format,资源互换文件格式)是微软定义的一种用于管理WINDOWS环境中多媒体数据的文件格式,波形音频WAVEMIDI和数字视频AVI都采用这种格式存储。构造RIFF文件的基本单元叫做数据块(Chunk),每个数据块包含3个部分, 14字节的数据块标记(或者叫做数据块的ID 2、数据块的大小 3、数据 整个RIFF文件可以看成一个数据块,其数据块IDRIFF,称为RIFF块。一个RIFF文件中只允许存在一个RIFF块。RIFF块中包含一系列的子块,其中有一种子块的ID"LIST",称为LIST块,LIST块中可以再包含一系列的子块,但除了LIST块外的其他所有的子块都不能再包含子块。 RIFFLIST块分别比普通的数据块多一个被称为形式类型(Form Type)和列表类型(List Type)的数据域,其组成如下: 14字节的数据块标记(Chunk ID 2、数据块的大小 34字节的形式类型或者列表类型(ID 4、数据 下面我们看看AVI文件的结构。AVI文件是目前使用的最复杂的RIFF文件,它能同时存储同步表现的音频视频数据。AVIRIFF块的形式类型(Form Type)是AVI,它一般包含3个子块,如下所述: 1、信息块,一个ID"hdrl"LIST块,定义AVI文件的数据格式。 2、数据块,一个ID "movi"LIST块,包含AVI的音视频序列数据。 3、索引块,ID"idxl"的子块,定义"movi"LIST块的索引数据,是可选块(不一定有)。 接下来,我们详细介绍下AVI文件的各子块构造,AVI文件的结构如图50.1.1.1所示:
50.1.1.1 AVI文件结构图 从上图可以看出(注意‘AVI ’,是带了一个空格的),AVI文件,由:信息块(HeaderList)、数据块(MovieList)和索引块(Index Chunk)等三部分组成,下面,我们分别介绍这几个部分。 1,信息块(HeaderList 信息块,即ID为“hdrl”的LIST块,它包含文件的通用信息,定义数据格式,所用的压缩算法等参数等。hdrl块还包括了一系列的字块,首先是:avih块,用于记录AVI的全局信息,比如数据流的数量,视频图像的宽度和高度等信息,avih块(结构体都有把BlockIDBlockSize包含进来,下同)的定义如下: //avih 子块信息 typedef struct {            u32 BlockID;                //块标志:avih==0X61766968        u32 BlockSize;              //块大小(不包含最初的8字节,BlockIDBlockSize不算)        u32 SecPerFrame;         //视频帧间隔时间(单位为us)        u32 MaxByteSec;         //最大数据传输率,字节/        u32 PaddingGranularity; //数据填充的粒度        u32 Flags;                    //AVI文件的全局标记,比如是否含有索引块等        u32 TotalFrame;            //文件总帧数        u32 InitFrames;           //为交互格式指定初始帧数(非交互格式应该指定为0        u32 Streams;                 //包含的数据流种类个数,通常为2        u32 RefBufSize;            //建议读取本文件的缓存大小(应能容纳最大的块)        u32 Width;                   //图像宽        u32 Height;                  //图像高        u32 Reserved[4];           //保留 }AVIH_HEADER; 这里有很多我们要用到的信息,比如SecPerFrame,通过该参数,我们可以知道每秒钟的帧率,也就知道了每秒钟需要解码多少帧图片,才能正常播放。TotalFrame告诉我们整个视频有多少帧,结合SecPerFrame参数,就可以很方便计算整个视频的时间了。Streams告诉我们数据流的种类数,一般是2,即包含视频数据流和音频数据流。avih块之后,是一个或者多个strl子列表,文件中有多少种数据流(即前面的Streams),就有多少个strl子列表。每个strl子列表,至少包括一个strhStream Header)块和一个strfStream Format)块,还有一个可选的strnStream Name)块(未列出)。注意:strl子列表出现的顺序与媒体流的编号(比如:00dc,前面的00,即媒体流编号00)是对应的,比如第一个strl子列表说明的是第一个流(Stream 0),假设是视频流,则表征视频数据块的四字符码为“00dc”,第二个strl子列表说明的是第二个流(Stream 1),假设是音频流,则表征音频数据块的四字符码为“01dw”,以此类推。 先看strh子块,该块用于说明这个流的头信息,定义如下: //strh 流头子块信息(strhstrl) typedef struct {            u32 BlockID;                //块标志:strh==0X73747268        u32 BlockSize;              //块大小(不包含最初的8字节,BlockIDBlockSize不算)        u32 StreamType;           //数据流种类,vids(0X73646976):视频;auds(0X73647561):音频        u32 Handler;                 //指定流的处理者,对于音视频来说即解码器,MJPG/H264.        u32 Flags;                   //标记:是否允许这个流输出?调 {MOD}板是否变化?        u16 Priority;                 //流的优先级(当有多个同类型的流时优先级最高的为默认流)        u16 Language;                     //音频的语言代号        u32 InitFrames;           //为交互格式指定初始帧数        u32 Scale;                    //数据量, 视频每桢的大小或者音频的采样大小        u32 Rate;                     //Scale/Rate=每秒采样数        u32 Start;                            //数据流开始播放的位置,单位为Scale        u32 Length;                  //数据流的数据量,单位为Scale       u32 RefBufSize;          //建议使用的缓冲区大小     u32 Quality;                 //解压缩质量参数,值越大,质量越好        u32 SampleSize;           //音频的样本大小        struct                           //视频帧所占的矩形        {                                      short Left;               short Top;               short Right;               short Bottom;        }Frame;                       }STRH_HEADER; 这里面,对我们最有用的即StreamType Handler这两个参数了,StreamType用于告诉我们此strl描述的是音频流(“auds”),还是视频流(“vids”)。而Handler则告诉我们所使用的解码器,比如MJPG/H264等(实际以strf块为准)。 然后是strf子块,不过strf字块,需要根据strh字块的类型而定。     如果strh子块是视频数据流(StreamType=vids”),则strf子块的内容定义如下: //BMP结构体 typedef struct {        u32  BmpSize;             //bmp结构体大小,包含(BmpSize在内)       long Width;                  //图像宽        long Height;                 //图像高        u16  Planes;                //平面数,必须为1        u16  BitCount;             //像素位数,0X0018表示24        u32  Compression;       //压缩类型,比如:MJPG/H264        u32  SizeImage;           //图像大小        long XpixPerMeter;       //水平分辨率        long YpixPerMeter;              //垂直分辨率        u32  ClrUsed;              //实际使用了调 {MOD}板中的颜 {MOD}数,压缩格式中不使用        u32  ClrImportant;              //重要的颜 {MOD} }BMP_HEADER; //颜 {MOD}表 typedef struct {        u8  rgbBlue;                //蓝 {MOD}的亮度(值范围为0-255)        u8  rgbGreen;             //绿 {MOD}的亮度(值范围为0-255)        u8  rgbRed;                //红 {MOD}的亮度(值范围为0-255)        u8  rgbReserved;          //保留,必须为0 }AVIRGBQUAD; //对于strh,如果是视频流,strf(流格式)使STRH_BMPHEADER typedef struct {        u32 BlockID;                //块标志,strf==0X73747266        u32 BlockSize;              //块大小(不包含最初的8字节,BlockIDBlockSize不算)        BMP_HEADER bmiHeader;       //位图信息头        AVIRGBQUAD bmColors[1];      //颜 {MOD}表 }STRF_BMPHEADER;  这里有3个结构体,strf子块完整内容即:STRF_BMPHEADER结构体,不过对我们有用的信息,都存放在BMP_HEADER结构体里面,本结构体对视频数据的解码起决定性的作用,它告诉我们视频的分辨率(WidthHeight),以及视频所用的编码器(Compression),因此它决定了视频的解码。本章例程仅支持解码视频分辨率小于屏幕分辨率,且编解码器必须是MJPG的视频格式。   如果strh子块是音频数据流(StreamType=“auds”),则strf子块的内容定义如下: //对于strh,如果是音频流,strf(流格式)使STRF_WAVHEADER typedef struct {        u32 BlockID;                //块标志,strf==0X73747266        u32 BlockSize;              //块大小(不包含最初的8字节,BlockIDBlockSize不算)      u16 FormatTag;             //格式标志:0X0001=PCM,0X0055=MP3...        u16 Channels;              //声道数,一般为2,表示立体声        u32 SampleRate;          //音频采样率        u32 BaudRate;            //波特率        u16 BlockAlign;           //数据块对齐标志        u16 Size;                      //该结构大小 }STRF_WAVHEADER; 本结构体对音频数据解码起决定性的作用,他告诉我们音频信号的编码方式(FormatTag)、声道数(Channels)和采样率(SampleRate)等重要信息。本章例程仅支持PCM格式(FormatTag=0X0001)的音频数据解码。 2,数据块(MovieList 信息块,即ID为“movi”LIST块,它包含AVI的音视频序列数据,是这个AVI文件的主体部分。音视频数据块交错的嵌入在“moviLIST块里面,通过标准类型码进行区分,标准类型码有如下4种: 1,“##db”(非压缩视频帧)、 2,“##dc”(压缩视频帧)、 3,“##pc”(改用新的调 {MOD}板)、 4,“##wb”(音频帧)。 其中##是编号,得根据我们的数据流顺序来确定,也就是前面的strl块。比如,如果第一个strl块是视频数据,那么对于压缩的视频帧,标准类型码就是:00dc。第二个strl块是音频数据,那么对于音频帧,标准类型码就是:01wb 紧跟着标准类型码的是4个字节的数据长度(不包含类型码和长度参数本身,也就是总长度必须要加8才对),该长度必须是偶数,如果读到为奇数,则加1即可。我们读数据的时候,一般一次性要读完一个标准类型码所表征的数据,方便解码。 3,索引块(Index Chunk     最后,紧跟在‘hdrl’列表和‘movi’列表之后的,就是AVI文件可选的索引块。这个索引块为AVI文件中每一个媒体数据块进行索引,并且记录它们在文件中的偏移(可能相对于‘movi’列表,也可能相对于AVI文件开头)。本章我们用不到索引块,这里就不详细介绍了。     关于AVI文件,我们就介绍到这,有兴趣的朋友,可以再看看光盘:6,软件资料àAVI学习资料 里面的相关文档。

50.1.2 libjpeg简介

libjpeg是一个完全用C语言编写的库,包含了被广泛使用的JPEG解码、JPEG编码和其他的JPEG功能的实现。这个库由IJG组织(Independent JPEG Group(独立JPEG小组))提供,并维护。libjpeg,目前最新版本为v9a,可以在:http://www.ijg.org 这个网站下载到。 libjpeg具有稳定、兼容性强和解码速度较快等优点。        本章,我们使用libjpeg v9a来实现MJPG数据流的解码,MJPG数据流,其实就是一张张的JPEG图片拼起来的图片视频流,只要能快速解码JPEG图片,就可以实现视频播放。 前面的图片显示实验我们使用了TJPGD来做JPEG解码,大家可能会问,为什么不直接用TJPGD来解码呢?原因就是TJPGD的特点就是:占用资源少,但是解码速度慢。在STM32F4上,同样一张320*240JPG图片,用TJPGD来解码,得120ms,而用libjpeg,则只需要50ms左右即可完成解码,明显速度上libjpeg要快不少,使得解码视频成为可能。实际上,经过我们优化后的libjpeg,使用STM32F4,在不超频的情况下,可以流畅播放480*272@10帧的MJPG视频(带音频)。 篇幅所限,关于libjpeg的移植,我们这里就不介绍了,请大家参考光盘源码。关于libjpeg的移植和使用,其实在下载的libjpeg源码里面,就有很多介绍,大家重点可以看:readme.txtfilelist.txtinstall.txtlibjpeg.txt等。 本节我们主要讲解一下如何使用libjpeg来实现一个jpeg图片的解码,这个在libjpeg源码里面:example.c,这个文件里面有简单的示范代码,在libjpeg.txt里面也有相关内容介绍。这里我们简要的给大家介绍一下,example.c里面的标准解码流程如下(示例代码): //错误结构体 struct my_error_mgr {       struct jpeg_error_mgr pub;    // jpeg_error_mgr结构体,里面有很多错误处理函数       jmp_buf setjmp_buffer;        //返回给函数调用者 }; typedef struct my_error_mgr * my_error_ptr; //JPEG解码错误处理函数 METHODDEF(void) my_error_exit (j_common_ptr cinfo) {       my_error_ptr myerr = (my_error_ptr) cinfo->err; //指向cinfo->err       (*cinfo->err->output_message) (cinfo);              //显示错误信息       longjmp(myerr->setjmp_buffer, 1);             //跳转到setjmp } //JPEG解码函数 GLOBAL(int) read_JPEG_file (char * filename) {       struct jpeg_decompress_struct cinfo;       struct my_error_mgr jerr;      //错误处理结构体       FILE * infile;                      //输入源文件       JSAMPARRAY buffer;         //输出缓存       int row_stride;              /* physical row width in output buffer */       if ((infile = fopen(filename, "rb")) == NULL)//尝试打开文件       {            fprintf(stderr, "can't open %s ", filename);            return 0;       }        //第一步,设置错误管理,初始化JPEG解码对象        cinfo.err = jpeg_std_error(&jerr.pub);         //建立JPEG错误处理流程       jerr.pub.error_exit = my_error_exit;            //处理函数指向my_error_exit       if (setjmp(jerr.setjmp_buffer)) //建立my_error_exit函数使用的返回上下文,当其他地方 //调用longjmp函数时,可以返回到这里进行错误处理 {            jpeg_destroy_decompress(&cinfo);//释放解码对象资源            fclose(infile);//关闭文件            return 0;       }       jpeg_create_decompress(&cinfo);//初始化解码对象cinfo        //第二步,指定数据源(比如一个文件)        jpeg_stdio_src(&cinfo, infile);        //第三步,读取文件参数(通过jpeg_read_header函数)         (void) jpeg_read_header(&cinfo, TRUE);//可以忽略此返回值        //第四步,设置解码参数(这里使用jpeg_read_header确定的默认参数),故无处理。        //第五步,开始解码 (void) jpeg_start_decompress(&cinfo);//还是忽略返回值        //在读取数据之前,可以做一些处理,比如设定LCD窗口,设定LCD起始坐标等       row_stride = cinfo.output_width * cinfo.output_components;//确定一样有多少个样本     //确保buffer至少可以保存一行的样本数据,为其申请内存     buffer = (*cinfo.mem->alloc_sarray)    ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);        //第六步,循环读取数据 while (cinfo.output_scanline < cinfo.output_height)//每次读一样,直到读完整个文件 {            (void) jpeg_read_scanlines(&cinfo, buffer, 1);     //解码一行数据         put_scanline_someplace(buffer[0], row_stride);   //将解码后的数据输出到某处       }       //第七步,结束解码 (void) jpeg_finish_decompress(&cinfo);//结束解码,忽略返回值        //第八步,释放解码对象资源          jpeg_destroy_decompress(&cinfo);//释放解码时申请的资源(大把内存)       fclose(infile);  //关闭文件       return 1;         //结束 } 以上代码,将一个jpeg解码分成了8个步骤,我们结合本例程代码简单讲解下这几个步骤。不过,我们先来看一下一个很重要的结构体数据类型:struct jpeg_decompress_struct,定义成cinfo变量,该变量保存着jpeg数据的详细信息,也保存着解码之后输出数据的详细信息。一般情况下,每次调用libjpegAPI的时候都需要把这个变量作为第一个参数传入。另外用户也可以通过修改该变量来修改libjpeg行为,比如输出数据格式,libjpeg库可用的最大内存等等。 不过,在STM32F4里面使用,可不能按以示例代码这么来定义cinfojerr结构体,因为单片机堆栈有限,cinfojerr都比较大(均超过400字节),很容易出现堆栈溢出的情况。在开发板源码,使用的是全局变量,而且用的是指针,通过内存管理分配。        接下来,开始看解码步骤,第一步是分配,并初始化解码对象结构体。这里做了两件事:1,错误管理,2,初始化解码对象。首先,错误管理使用setjmplongjmp机制(不懂请百度)来实现类似C++的异常处理功能,外部代码可以调用longjmp来跳转到setjmp位置,执行错误管理(释放内存,关闭文件等)。这里注册了一个my_error_exit函数,来执行错误退出处理,在本例程代码,还实现了一个函数:my_emit_message,输出警告信息,方便调试代码。然后,初始化解码对象cinfo,就是通过jpeg_create_decompress函数实现。        第二步,指定数据源。示例代码用的是jpeg_stdio_src函数。本章代码,我们用另外一个函数实现: //初始化jpeg解码数据源 static void jpeg_filerw_src_init(j_decompress_ptr cinfo) {     if (cinfo->src == NULL)     /* first time for this JPEG object? */     {         cinfo->src = (struct jpeg_source_mgr *) (*cinfo->mem->alloc_small)((j_common_ptr)  cinfo, JPOOL_PERMANENT,sizeof(struct jpeg_source_mgr));     }     cinfo->src->init_source = init_source;     cinfo->src->fill_input_buffer = fill_input_buffer;     cinfo->src->skip_input_data = skip_input_data;     cinfo->src->resync_to_restart = jpeg_resync_to_restart; /* use default method */     cinfo->src->term_source = term_source;     cinfo->src->bytes_in_buffer = 0; /* forces fill_input_buffer on first read */     cinfo->src->next_input_byte = NULL; /* until buffer loaded */ }            该函数里面,设置了cinfo->src的各个函数指针,用于获取外部数据。这里面重点是两个函数:fill_input_bufferskip_input_data,前者用于填充数据给libjpeg,后者用于跳过一定字节的数据。这两个函数请看本例程源码(在mjpeg.c里面)。        第三步,读取文件参数。通过jpeg_read_header函数实现,该函数将读取JPEG的很多参数,必须在解码前调用。        第四步,设置解码参数,示例代码没有做任何设置(使用默认值)。本章代码则做了设置,如下:        cinfo->dct_method = JDCT_IFAST;        cinfo->do_fancy_upsampling = 0;         这里,我们设置了使用快速整型DCTdo_fancy_upsampling的值为假(0),以提高解码速度。        第五步,开始解码。示例代码首先调用jpeg_start_decompress函数,然后计算样本输出buffer大小,并为其申请内存,为后续读取解码后的数据做准备。不过本章例程,我们为了提高速度,没有做这些处理了,我们直接修改底层函数:h2v1_merged_upsampleh2v2_merged_upsample(在jdmerge.c里面),将输出的RGB数据直接转换成RGB565,送给LCD。然后,为了正确的输出到LCD,我们在jpeg_start_decompress函数之后,加入如下代码:        LCD_Set_Window(imgoffx,imgoffy,cinfo->output_width,cinfo->output_height);        LCD_WriteRAM_Prepare();                   //开始写入GRAM        这两个函数,先设置好开窗大小(即jpeg图片尺寸),然后就发送准备写入GRAM指令。后续解码的时候,直接就在h2v1_merged_upsampleh2v2_merged_upsample里面丢数据给LCD,实现jpeg解码输出到LCD        第六步,循环读取数据。通过jpeg_read_scanlines函数,循环解码并读取jpeg图片数据,实现jpeg解码。示例代码通过put_scanline_someplace函数,输出到某个地方(如lcd,文件等),本章例程则直接解码的时候就输出到LCD了,所以仅剩jpeg_read_scanlines函数,循环调用即可实现jpegàLCD的操作。        第七步,解码结束。解码完成后,通过jpeg_finish_decompress函数,结束jpeg解码。        第八步,释放解码对象资源。在所有操作完成后,通过jpeg_destroy_decompress,释放解码过程中用到的资源(比如释放内存)。        这样,我们就完成了一张jpeg图片的解码。上面,我们简要列出了本章例程与example.c的异同,详细的代码,请大家参考光盘本例程源码mjpeg.clibjepg的使用,我们就介绍到这里。 最后,我们看看要实现avi视频文件的播放,主要有哪些步骤,如下: 1)初始化各外设 要解码视频,相关外设肯定要先初始化好,比如:SDIO(驱动SD卡用)、I2SDMAWM8978LCD和按键等。这些具体初始化过程,在前面的例程都有介绍,大同小异,这里就不再细说了。 2)读取AVI文件,并解析       要解码,得先读取avi文件,按50.1.1节的介绍,读取出音视频关键信息,音频参数:编码方式、采样率、位数和音频流类型码(01wb/00wb)等;视频参数:编码方式、帧间隔、图片尺寸和视频流类型码(00dc/01dc)等;共同的:数据流起始地址。有了这些参数,我们便可以初始化音视频解码,为后续解码做好准备。 3)根据解析结果,设置相关参数        根据第2步解析的结果,设置I2S的音频采样率和位数,同时要让视频显示在LCD中间区域,得根据图片尺寸,设置LCD开窗时xy方向的偏移量。 4)读取数据流,开始解码        前面三步完成,就可以正式开始播放视频了。读取视频流数据(movi块),根据类型码,执行音频/视频解码。对于音频数据(01wb/00wb),本例程只支持未压缩的PCM数据,所以,直接填充到DMA缓冲区即可,由DMA循环发送给WM8978,播放音频。对于视频数据(00dc/01dc),本例程只支持MJPG,通过libjpeg解码,所以将视频数据按前面所说的几个步骤解码即可。然后,利用定时器来控制帧间隔,以正常速度播放视频,从而实现音视频解码。 5)解码完成,释放资源        最后在文件读取完后(或者出错了),需要释放申请的内存、恢复LCD窗口、关闭定时器、停止I2S播放音乐和关闭文件等一系列操作,等待下一次解码。   

50.2 硬件设计

本章实验功能简介:开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,则开始播放SD卡VIDEO文件夹里面的视频(.avi格式)。注意:1,在SD卡根目录必须建立一个VIDEO文件夹,并存放AVI视频(仅支持MJPG视频,音频必须是PCM,且视频分辨率必须小于等于屏幕分辨率)在里面。2,我们所需要的视频,可以通过:狸窝全能视频转换器,转换后得到,具体步骤后续会讲到(50.4节)。 视频播放时,LCD上还会显示视频名字、当前视频编号、总视频数、声道数、音频采样率、帧率、播放时间和总时间等信息。KEY0用于选择下一个视频,KEY2用于选择上一个视频,KEY_UP可以快进,KEY1可以快退。DS0还是用于指示程序运行状态(仅字库错误时)。 本实验用到的资源如下: 1)  指示灯DS0 2)  4个按键(KEY_UP/KEY0/KEY1/KEY2 3)  串口 4)  TFTLCD模块 5)  SD 6)  SPI FLASH 7)  WM8978 8)  I2S2 这些前面都已介绍过。本实验,大家需要准备1SD卡和一个耳机(或喇叭),分别插入SD卡接口和耳机接口(喇叭接P1接口),然后下载本实验就可以看视频了!

50.3 软件设计

打开本章实验工程目录可以看到,我们在工程根目录新建MJPEG文件夹,在该文件夹里面新建了JPEG文件夹,存放libjpeg v9a的相关代码,同时,在MJPEG文件夹里面新建了avi.cavi.hmjpeg.cmjpeg.h四个文件。然后,工程里面,新建了MJPEG分组,将需要用到的相关.c文件添加到该分组下面,并将MJPEGJPEG两个文件夹加入头文件包含路径。 我们还在APP文件夹下面新建了videoplayer.cvideoplayer.h两个文件,然后将videoplayer.c加入到工程的APP组下。        整个工程代码有点多,我们看看本实验新添加进来的代码,有哪些,如图50.3.1所示:  50.3.1 本实验新增代码 可见,本工程新增的代码是比较多的,主要是libjpeg需要的文件挺多的。这里我们挑一部分重要代码给大家讲解。 首先是avi.c里面的几个函数,代码如下: AVI_INFO avix;                                //avi文件相关信息 u8*const AVI_VIDS_FLAG_TBL[2]={"00dc","01dc"};//视频编码标志字符串,00dc/01dc u8*const AVI_AUDS_FLAG_TBL[2]={"00wb","01wb"};//音频编码标志字符串,00wb/01wb //avi解码初始化 //buf:输入缓冲区 //size:缓冲区大小 //返回值:AVI_OK,avi文件解析成功 //         其他,错误代码 AVISTATUS avi_init(u8 *buf,u16 size)              {        u16 offset; u8 *tbuf;             AVISTATUS res=AVI_OK;        AVI_HEADER *aviheader; LIST_HEADER *listheader;           AVIH_HEADER *avihheader;STRH_HEADER *strhheader;         STRF_BMPHEADER *bmpheader;STRF_WAVHEADER *wavheader;        tbuf=buf;        aviheader=(AVI_HEADER*)buf;        if(aviheader->RiffID!=AVI_RIFF_ID)return AVI_RIFF_ERR;          //RIFF ID错误        if(aviheader->AviID!=AVI_AVI_ID)return AVI_AVI_ERR;                     //AVI ID错误        buf+=sizeof(AVI_HEADER);                                                               //偏移        listheader=(LIST_HEADER*)(buf);                                               if(listheader->ListID!=AVI_LIST_ID)return AVI_LIST_ERR;           //LIST ID错误        if(listheader->ListType!=AVI_HDRL_ID)return AVI_HDRL_ERR;    //HDRL ID错误        buf+=sizeof(LIST_HEADER);                                                      //偏移        avihheader=(AVIH_HEADER*)(buf);        if(avihheader->BlockID!=AVI_AVIH_ID)return AVI_AVIH_ERR;    //AVIH ID错误        avix.SecPerFrame=avihheader->SecPerFrame;                                //得到帧间隔时间        avix.TotalFrame=avihheader->TotalFrame;                                      //得到总帧数         buf+=avihheader->BlockSize+8;                                                    //偏移        listheader=(LIST_HEADER*)(buf);        if(listheader->ListID!=AVI_LIST_ID)return AVI_LIST_ERR;           //LIST ID错误        if(listheader->ListType!=AVI_STRL_ID)return AVI_STRL_ERR;      //STRL ID错误         strhheader=(STRH_HEADER*)(buf+12);        if(strhheader->BlockID!=AVI_STRH_ID)return AVI_STRH_ERR;    //STRH ID错误       if(strhheader->StreamType==AVI_VIDS_STREAM)                       ///视频帧在前        {               if(strhheader->Handler!=AVI_FORMAT_MJPG)return AVI_FORMAT_ERR;//不支持               avix.VideoFLAG=(u8*)AVI_VIDS_FLAG_TBL[0];   //视频流标记  "00dc"               avix.AudioFLAG=(u8*)AVI_AUDS_FLAG_TBL[1];  //音频流标记  "01wb"               bmpheader=(STRF_BMPHEADER*)(buf+12+strhheader->BlockSize+8);//strf               if(bmpheader->BlockID!=AVI_STRF_ID)return AVI_STRF_ERR;//STRF ID错误                avix.Width=bmpheader->bmiHeader.Width;               avix.Height=bmpheader->bmiHeader.Height;               buf+=listheader->BlockSize+8;                          //偏移               listheader=(LIST_HEADER*)(buf);               if(listheader->ListID!=AVI_LIST_ID)//是不含有音频帧的视频文件               {                      avix.SampleRate=0;                                   //音频采样率                      avix.Channels=0;                                       //音频通道数                      avix.AudioType=0;                                    //音频格式               }else               {                                        if(listheader->ListType!=AVI_STRL_ID)return AVI_STRL_ERR;//STRL ID错误                        strhheader=(STRH_HEADER*)(buf+12);                      if(strhheader->BlockID!=AVI_STRH_ID)return AVI_STRH_ERR;//STRH错误                      if(strhheader->StreamType!=AVI_AUDS_STREAM) return AVI_FORMAT_ERR;//格式错误                      wavheader=(STRF_WAVHEADER*)(buf+12+strhheader->BlockSize+8);//strf                      if(wavheader->BlockID!=AVI_STRF_ID)return AVI_STRF_ERR;//STRF 错误                        avix.SampleRate=wavheader->SampleRate; //音频采样率                      avix.Channels=wavheader->Channels;         //音频通道数                      avix.AudioType=wavheader->FormatTag;    //音频格式               }        }else if(strhheader->StreamType==AVI_AUDS_STREAM)              //音频帧在前        {               avix.VideoFLAG=(u8*)AVI_VIDS_FLAG_TBL[1];          //视频流标记  "01dc"               avix.AudioFLAG=(u8*)AVI_AUDS_FLAG_TBL[0];         //音频流标记  "00wb"               wavheader=(STRF_WAVHEADER*)(buf+12+strhheader->BlockSize+8);//strf               if(wavheader->BlockID!=AVI_STRF_ID)return AVI_STRF_ERR;    //STRF ID错误               avix.SampleRate=wavheader->SampleRate;                                    //音频采样率               avix.Channels=wavheader->Channels;                                            //音频通道数               avix.AudioType=wavheader->FormatTag;                                       //音频格式               buf+=listheader->BlockSize+8;                                                      //偏移               listheader=(LIST_HEADER*)(buf);               if(listheader->ListID!=AVI_LIST_ID)return AVI_LIST_ERR;           //LIST ID错误               if(listheader->ListType!=AVI_STRL_ID)return AVI_STRL_ERR;      //STRL ID错误                 strhheader=(STRH_HEADER*)(buf+12);               if(strhheader->BlockID!=AVI_STRH_ID)return AVI_STRH_ERR;    //STRH ID错误               if(strhheader->StreamType!=AVI_VIDS_STREAM)return AVI_FORMAT_ERR;          bmpheader=(STRF_BMPHEADER*)(buf+12+strhheader->BlockSize+8);//strf               if(bmpheader->BlockID!=AVI_STRF_ID)return AVI_STRF_ERR;    //STRF ID错误                if(bmpheader->bmiHeader.Compression!=AVI_FORMAT_MJPG) return AVI_FORMAT_ERR;//格式错误                avix.Width=bmpheader->bmiHeader.Width;               avix.Height=bmpheader->bmiHeader.Height;            }        offset=avi_srarch_id(tbuf,size,"movi");                                          //查找movi ID        if(offset==0)return AVI_MOVI_ERR;                                     //MOVI ID错误     
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。