OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 18470|回复: 14

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

[复制链接]

230

主题

1950

帖子

10

精华

论坛元老

Rank: 8Rank: 8

积分
4562
金钱
4562
注册时间
2010-12-14
在线时间
32 小时
发表于 2014-12-9 12:37:52 | 显示全部楼层 |阅读模式

第五十章 视频播放器实验

 

[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色和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;                   //标记:是否允许这个流输出?调色板是否变化?

       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;              //实际使用了调色板中的颜色数,压缩格式中不使用

       u32  ClrImportant;              //重要的颜色

}BMP_HEADER;

//颜色表

typedef struct

{

       u8  rgbBlue;                //蓝色的亮度(值范围为0-255)

       u8  rgbGreen;             //绿色的亮度(值范围为0-255)

       u8  rgbRed;                //红色的亮度(值范围为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];      //颜色表

}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”(改用新的调色板)、

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\n", 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错误

       if(avix.SampleRate)//有音频流,才查找

       {

              tbuf+=offset;

              offset=avi_srarch_id(tbuf,size,avix.AudioFLAG);                     //查找音频流标记

              if(offset==0)return AVI_STREAM_ERR;                         //流错误

              tbuf+=offset+4;

              avix.AudioBufSize=*((u16*)tbuf);                                          //得到音频流buf大小.

       }           

       return res;

}

//查找 ID

//buf:待查缓存区

//size:缓存大小

//id:要查找的id,必须是4字节长度

//返回值:0,查找失败,其他:movi ID偏移量

u16 avi_srarch_id(u8* buf,u16 size,u8 *id)

{

       u16 i;

       size-=4;

       for(i=0;i<size;i++)

       {

            if(buf==id[0])

                     if(buf[i+1]==id[1])

                            if(buf[i+2]==id[2])      

                                   if(buf[i+3]==id[3])return i;//找到"id"所在的位置     

       }

       return 0;        

}

//得到stream流信息

//buf:流开始地址(必须是01wb/00wb/01dc/00dc开头)

AVISTATUS avi_get_streaminfo(u8* buf)

{

       avix.StreamID=MAKEWORD(buf+2);        //得到流类型

       avix.StreamSize=MAKEDWORD(buf+4);   //得到流大小

       if(avix.StreamSize%2)avix.StreamSize++;   //奇数加1(avix.StreamSize,必须是偶数)

       if(avix.StreamID==AVI_VIDS_FLAG||avix.StreamID==AVI_AUDS_FLAG)

return AVI_OK;

       return AVI_STREAM_ERR; 

}

这里三个函数,其中avi_ini用于解析AVI文件,获取音视频流数据的详细信息,为后续解码做准备。而avi_srarch_id用于查找某个ID,可以是4个字节长度的ID,比如00dc01wbmovi之类的,在解析数据以及快进快退的时候,有用到。avi_get_streaminfo函数,则是用来获取当前数据流信息,重点是取得流类型和流大小,方便解码和读取下一个数据流。

接下来,我们看mjpeg.c里面的几个函数,代码如下:

//mjpeg 解码初始化

//offx,offy:x,y方向的偏移

//返回值:0,成功;  1,失败

u8 mjpegdec_init(u16 offx,u16 offy)

{

       cinfo=mymalloc(SRAMCCM,sizeof(struct jpeg_decompress_struct));

       jerr=mymalloc(SRAMCCM,sizeof(struct my_error_mgr));

       jmembuf=mymalloc(SRAMCCM,MJPEG_MAX_MALLOC_SIZE);//解码内存池申请

       if(cinfo==0||jerr==0||jmembuf==0){ mjpegdec_free();return 1;}

       //保存图像在x,y方向的偏移量

       imgoffx=offx; imgoffy=offy;

       return 0;

}

//mjpeg结束,释放内存

void mjpegdec_free(void)

{   

       myfree(SRAMCCM,cinfo);

       myfree(SRAMCCM,jerr);

       myfree(SRAMCCM,jmembuf);

}

//解码一副JPEG图片

//buf:jpeg数据流数组   bsize:数组大小

//返回值:0,成功        其他,错误

u8 mjpegdec_decode(u8* buf,u32 bsize)

{

    JSAMPARRAY buffer;        

       if(bsize==0)return 1;

       jpegbuf=buf; jbufsize=bsize;           

       jmempos=0;//MJEPG解码,重新从0开始分配内存

       cinfo->err=jpeg_std_error(&jerr->pub);

       jerr->pub.error_exit = my_error_exit;

       jerr->pub.emit_message = my_emit_message;

       //if(bsize>20*1024)printf("s:%d\r\n",bsize);

       if (setjmp(jerr->setjmp_buffer)) //错误处理

       {

             jpeg_abort_decompress(cinfo);

              jpeg_destroy_decompress(cinfo);

              return 2;

       }

       jpeg_create_decompress(cinfo);

       jpeg_filerw_src_init(cinfo); 

       jpeg_read_header(cinfo, TRUE);

       cinfo->dct_method = JDCT_IFAST;

       cinfo->do_fancy_upsampling = 0; 

       jpeg_start_decompress(cinfo);

       LCD_Set_Window(imgoffx,imgoffy,cinfo->output_width,cinfo->output_height);

       LCD_WriteRAM_Prepare();                   //开始写入GRAM

       while (cinfo->output_scanline < cinfo->output_height)

       {

              jpeg_read_scanlines(cinfo, buffer, 1);

       }

       LCD_Set_Window(0,0,lcddev.width,lcddev.height);//恢复窗口

       jpeg_finish_decompress(cinfo);

       jpeg_destroy_decompress(cinfo); 

       return 0;

}

其中,mjpegdec_init函数,用于初始化jpeg解码 ,主要是申请内存,然后确定视频在液晶上面的偏移(以让视频显示在LCD中央)。mjpegdec_free函数,用于释放内存,解码结束后调用。mjpegdec_decode函数,是解码jpeg的主要函数,通过前面50.1.2节介绍的步骤进行解码,该函数的参数buf指向内存里面的一帧jpeg数据,bsize 就是数据大小。

接下来,我们看videoplayer.c里面video_play_mjpeg函数,代码如下:

//播放一个mjpeg文件

//pname:文件名

//返回值:  KEY0_PRES:下一曲  KEY1_PRES:上一曲

//其他:错误

u8 video_play_mjpeg(u8 *pname)

{  

       u8* framebuf; //视频解码buf      

       u8* pbuf;              //buf指针 

       FIL *favi;

       u8 res=0; u16 offset=0;

       u32  nr;u8 key; u8 i2ssavebuf;   

       i2sbuf[0]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE); //申请音频内存

       i2sbuf[1]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE); //申请音频内存

       i2sbuf[2]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE); //申请音频内存

       i2sbuf[3]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE); //申请音频内存

       framebuf=mymalloc(SRAMIN,AVI_VIDEO_BUF_SIZE); //申请视频buf

       favi=(FIL*)mymalloc(SRAMIN,sizeof(FIL));            //申请favi内存

       memset(i2sbuf[0],0,AVI_AUDIO_BUF_SIZE);

       memset(i2sbuf[1],0,AVI_AUDIO_BUF_SIZE);

       memset(i2sbuf[2],0,AVI_AUDIO_BUF_SIZE);

       memset(i2sbuf[3],0,AVI_AUDIO_BUF_SIZE);

       if(i2sbuf[3]==NULL||framebuf==NULL||favi==NULL) res=0XFF;

       while(res==0)

       {

              res=f_open(favi,(char *)pname,FA_READ);

              if(res==0)

              {

                     pbuf=framebuf;                   

                     res=f_read(favi,pbuf,AVI_VIDEO_BUF_SIZE,&nr);//开始读取      

                     if(res) {printf("fread error:%d\r\n",res);break;}

                     //开始avi解析

                     res=avi_init(pbuf,AVI_VIDEO_BUF_SIZE);      //avi解析

                     if(res){ printf("avi err:%d\r\n",res); break;}

                     video_info_show(&avix);

                     TIM6_Int_Init(avix.SecPerFrame/100-1,8400-1);//10Khz计数频率,1100us

                     offset=avi_srarch_id(pbuf,AVI_VIDEO_BUF_SIZE,"movi");//寻找movi ID  

                     avi_get_streaminfo(pbuf+offset+4);//获取流信息

                     f_lseek(favi,offset+12);         //跳过标志ID,读地址偏移到流数据开始处      

                     res=mjpegdec_init((lcddev.width-avix.Width)/2, 110+(lcddev.height-110-

avix.Height)/2);//初始化JPG解码

//JPG解码初始化

                     if(avix.SampleRate)              //有音频信息,才初始化

                     {

                            WM8978_I2S_Cfg(2,0);       //飞利浦标准,16位数据长度

                                    

I2S2_Init(I2S_Standard_Phillips,I2S_Mode_MasterTx,I2S_CPOL_Low,

I2S_DataFormat_16bextended);     

//飞利浦标准,主机发送,时钟低有效,16位扩展帧

                            I2S2_SampleRate_Set(avix.SampleRate);    //设置采样率

                            I2S2_TX_DMA_Init(i2sbuf[1],i2sbuf[2],avix.AudioBufSize/2); //配置DMA

                            i2s_tx_callback=audio_i2s_dma_callback;    //回调函数I2S_DMA_Callback

                            i2splaybuf=0; i2ssavebuf=0;

                            I2S_Play_Start(); //开启I2S播放

                     }

                    while(1)//播放循环

                     {                                

                            if(avix.StreamID==AVI_VIDS_FLAG)       //视频流

                            {

                                   pbuf=framebuf;

                                   f_read(favi,pbuf,avix.StreamSize+8,&nr);//读整帧+下个数据流ID 

                                   res=mjpegdec_decode(pbuf,avix.StreamSize);

                                   if(res) printf("decode error!\r\n");              

                                   while(frameup==0);//等待时间到达(TIM6的中断里面设置为1)

                                   frameup=0;            //标志清零

                                   frame++;

                            }else //音频流

                            {             

                                   video_time_show(favi,&avix);     //显示当前播放时间

                                   i2ssavebuf++;

                                   if(i2ssavebuf>3)i2ssavebuf=0;

                                   do

                                   {

                                          nr=i2splaybuf;

                                          if(nr)nr--;

                                          else nr=3;

                                   }while(i2ssavebuf==nr);//碰撞等待.

                                   f_read(favi,i2sbuf[i2ssavebuf],avix.StreamSize+8,&nr);//填充i2sbuf      

                                   pbuf=i2sbuf[i2ssavebuf]; 

                            }

                            key=KEY_Scan(0);

                            if(key==KEY0_PRES||key==KEY2_PRES) { res=key; break;}//切换     

                            else if(key==KEY1_PRES||key==WKUP_PRES)

                            {

                                   I2S_Play_Stop();   //关闭音频

                                   video_seek(favi,&avix,framebuf);

                                   pbuf=framebuf;

                                   I2S_Play_Start();   //开启DMA播放

                            }

                            if(avi_get_streaminfo(pbuf+avix.StreamSize))//读取下一帧 流标志

                            {

                                   printf("frame error \r\n");

                                   res=KEY0_PRES;

                                   break;

                            }                                     

                     }

                     I2S_Play_Stop();   //关闭音频

                     TIM6->CR1&=~(1<<0); //关闭定时器6

                     LCD_Set_Window(0,0,lcddev.width,lcddev.height);//恢复窗口

                     mjpegdec_free();    //释放内存

                     f_close(favi);

              }

       }

       myfree(SRAMIN,i2sbuf[0]); myfree(SRAMIN,i2sbuf[1]);

       myfree(SRAMIN,i2sbuf[2]); myfree(SRAMIN,i2sbuf[3]);

       myfree(SRAMIN,framebuf); myfree(SRAMIN,favi);

       return res;

}

该函数用来播放一个avi视频文件(mjpg编码),解码过程就是根据前面我们在50.1.2节最后所介绍的步骤进行,不过在这里,我们的音频播放用了4buf,以提高解码的流畅度。

最后,我们看看主函数代码:

int main(void)

{

 

       NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2

       delay_init(168);  //初始化延时函数

       uart_init(115200);         //初始化串口波特率为115200

      

       LED_Init();                                //初始化LED

       usmart_dev.init(84);             //初始化USMART

      LCD_Init();                                //LCD初始化 

      KEY_Init();                                //按键初始化  

       W25QXX_Init();                         //初始化W25Q128

       WM8978_Init();                          //初始化WM8978 

      

       WM8978_ADDA_Cfg(1,0);         //开启DAC

       WM8978_Input_Cfg(0,0,0);  //关闭输入通道

       WM8978_Output_Cfg(1,0);         //开启DAC输出 

       WM8978_HPvol_Set(40,40);

       WM8978_SPKvol_Set(60);

       TIM3_Int_Init(10000-1,8400-1);//10Khz计数,1秒钟中断一次

      

       my_mem_init(SRAMIN);            //初始化内部内存池

       my_mem_init(SRAMCCM);         //初始化CCM内存池

       exfuns_init();                       //fatfs相关变量申请内存 

      f_mount(fs[0],"0:",1);          //挂载SD 

       POINT_COLOR=RED;     

       while(font_init())                //检查字库

       {        

              LCD_ShowString(30,50,200,16,16,"Font Error!");

              delay_ms(200);                            

              LCD_Fill(30,50,240,66,WHITE);//清除显示          

              delay_ms(200);     

              LED0=!LED0;

       }     

       POINT_COLOR=RED;     

      Show_Str(60,50,200,16,"Explorer STM32开发板",16,0);                                  

       Show_Str(60,70,200,16,"视频播放器实验",16,0);                                      

       Show_Str(60,90,200,16,"正点原子@ALIENTEK",16,0);                                   

       Show_Str(60,110,200,16,"201471",16,0);

       Show_Str(60,130,200,16,"KEY0:NEXT   KEY2REV",16,0);

       Show_Str(60,150,200,16,"KEY_UP:FF   KEY1REW",16,0);

       delay_ms(1500);

       while(1)

       {

              video_play();

       }

}

    该函数代码同上一章的main函数代码几乎一样,十分简单,我们就不再多说了。

最后,因为视频解码需要用到比较多的堆栈,所以需要修改startup_stm32f40_41xxx.s里面的堆栈大小,将原来的0x00000400设置为0x00000800,如下:

Stack_Size      EQU     0x00000800

同时,为了提高速度,我们对编译器进行设置,选择使用-O2优化,从而优化代码,提高速度(但调试效果不好,建议调试时设置为-O0),编译器设置如图50.3.2所示:

50.3.2 编译器优化设置

设置完后,重新编译即可。至此,本实验的软件设计部分结束。

50.4 下载验证

本章,我们例程仅支持MJPG编码的avi格式视频,且音频必须是PCM格式,另外视频分辨率不能大于LCD分辨率。要满足这些要求,现成的avi文件是很难找到的,所以我们需要用软件,将通用视频(任何视频都可以)转换为我们需要的格式,这里我们通过:狸窝全能视频转换器,这款软件来实现(路径:光盘:6,软件资料à软件à视频转换软件à狸窝全能视频转换器.exe)。安装完后,打开,然后进行相关设置,软件设置如图50.4.150.4.2所示:

50.4.1 软件启动界面和设置

50.4.2 高级设置

首先,如图50.4.1所示,点击1处,添加视频,找到你要转换的视频,添加进来。有的视频可能有独立字幕,比如我们打开的这个视频就有,所以在2处选择下字幕(如果没有的,可以忽略此步)。然后在3处,点击▼图标,选择预制方案:AVI-Audio-Video Interleaved(*.avi),即生成.avi文件,然后点击4处的高级设置按钮,进入50.4.2所示的界面,设置详细参数如下:

视频编码器:选择MJPEG。本例程仅支持MJPG视频解码,所以选择这个编码器。

视频尺寸:480x272。这里得根据所用LCD分辨率来选择,我们用480*8004.3寸电容屏模块,所以,这里最大可以设置:480x272PS:如果是2.8屏,最大宽度只能是240)。

比特率:1000。这里设置越大,视频质量越好,解码就越慢(可能会卡),我们设置为1000,可以得到比较好的视频质量,同时也不怎么会卡。

帧率:10。即每秒钟10帧,对于480*272的视频,本例程最高就只能播放10帧左右的视频,如果要想提高帧率,有几个办法:1,降低分辨率;2,降低比特率;3,降低音频采样率。

音频编码器:PCMS16LE。本例程只支持PCM音频,所以选择音频编码器为这个。

采样率:这里设置为11025,即11.025Khz的采样率。这里越高,声音质量越好,不过,转换后的文件就越大,而且视频可能会卡。

其他设置,采用默认的即可。设置完以后,点击确定,即可完成设置。

点击图50.4.15处的文件夹图标,设置转换后视频的输出路径,这里我们设置到了桌面,这样转换后的视频,会保存在桌面。最后,点击图中6处的按钮,即可开始转换了,如图50.4.3所示:

50.4.3 正在转换

       等转换完成后,将转换后的.avi文件,拷贝到SD卡àVIDEO文件夹下,然后插入开发板的SD卡接口,就可以开始测试本章例程了。

在代码编译成功之后,我们下载代码到ALIENTEK探索者STM32F4开发板上,程序先检测字库,然后检测SD卡的VIDEO文件夹,并查找avi视频文件,在找到有效视频文件后,便开始播放视频,如图50.4.4所示:

50.4.4 视频播放中

可以看到,屏幕显示了文件名、索引、声道数、采样率、帧率和播放时间等参数。然后,我们按KEY0/KEY2,可以切换到下一个/上一个视频,按KEY_UP/KEY1,可以快进/快退。

至此,本例程介绍就结束了。本实验,我们在ALIENTEK STM32F4探索者开发板上实现了视频播放,体现了STM32F4强大的处理能力。

附本实验测试结果(视频比特率:1000,音频均为:11025,立体声)

240*160/240*180分辨率,可达30

320*240分辨率,可达20

480*272分辨率,可达10

最后提醒大家,转换的视频分辨率,一定要根据自己的LCD设置,不能超过LCD的尺寸!!否则无法播放(可能只听到声音,看不到图像)。

 

 

 

实验详细手册和源码下载地址:http://www.openedv.com/posts/list/41586.htm 

正点原子探索者STM32F407开发板购买地址http://item.taobao.com/item.htm?id=41855882779

  


 

实验45 视频播放器实验.zip

2.97 MB, 下载次数: 1140

第五十章 视频播放器实验-STM32F4开发指南-正点原子探索者STM32开发板.pdf

1.1 MB, 下载次数: 1495

我是开源电子网?网站管理员,对网站有任何问题,请与我联系!QQ:389063473Email:389063473@qq.com
正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

6

主题

33

帖子

0

精华

初级会员

Rank: 2

积分
121
金钱
121
注册时间
2013-10-24
在线时间
6 小时
发表于 2014-12-11 22:04:23 | 显示全部楼层
看起来很高端啊!!!
回复 支持 反对

使用道具 举报

0

主题

2

帖子

0

精华

新手上路

积分
26
金钱
26
注册时间
2014-11-22
在线时间
1 小时
发表于 2015-5-14 22:06:17 | 显示全部楼层
下载之前顶一下!!!
回复 支持 反对

使用道具 举报

0

主题

2

帖子

0

精华

新手上路

积分
26
金钱
26
注册时间
2014-11-22
在线时间
1 小时
发表于 2015-5-14 22:38:06 | 显示全部楼层
解码如此慢腾腾,是什么不给力???
回复 支持 反对

使用道具 举报

530

主题

11万

帖子

34

精华

管理员

Rank: 12Rank: 12Rank: 12

积分
165371
金钱
165371
注册时间
2010-12-1
在线时间
2110 小时
发表于 2015-5-14 23:01:01 | 显示全部楼层
回复【4楼】twtuge:
---------------------------------
mcu和代码都有,期待你给力的改进下。
我是开源电子网www.openedv.com站长,有关站务问题请与我联系。
正点原子STM32开发板购买店铺http://openedv.taobao.com
正点原子官方微信公众平台,点击这里关注“正点原子”
回复 支持 反对

使用道具 举报

0

主题

1

帖子

0

精华

新手上路

积分
23
金钱
23
注册时间
2015-6-2
在线时间
0 小时
发表于 2015-6-2 11:36:18 | 显示全部楼层
while(frameup==0);//等待时间到达(在TIM6的中断里面设置为1)

                                   frameup=0;            //标志清零
while中的判断条件错了吧.....frameup==1吧
回复 支持 反对

使用道具 举报

13

主题

123

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
242
金钱
242
注册时间
2013-1-6
在线时间
20 小时
发表于 2015-6-2 17:24:24 | 显示全部楼层
原子哥,说句话可不要见怪。感觉在F4上做视频播放就是鸡肋,实际项目中绝对不会去用他干这事,mCU就是控制的作用,一些大数据的处理他还是不行的。一件多搞一些实际中有用的示例,个人见解
回复 支持 反对

使用道具 举报

0

主题

1

帖子

0

精华

新手入门

积分
21
金钱
21
注册时间
2013-11-20
在线时间
0 小时
发表于 2015-8-20 10:01:04 | 显示全部楼层
原子哥,你好,对于这个程序有点疑问哈,请教一下

在优化的libjpeg的jdmerge.c中,原子哥是这样给一个LCD像素点赋值的,
LCD->LCD_RAM = range_limit[y + cblue] >> 3 | (range_limit[y + cgreen] >> 2) << 5 | (range_limit[y + cred] >> 3) << 11;
但是却并没有见到LCD->LCD_RAM地址增加,
在 h2v1_merged_upsample,h2v2_merged_upsample,函数中是写一行LCD数据,
即for (col = cinfo->output_width >> 1; col > 0; col--),
不知道原子哥是怎么处理的??

//LCD地址结构体
typedef struct
{
u16 LCD_REG;
u16 LCD_RAM;
} LCD_TypeDef;
    
#define LCD_BASE        ((u32)(0x6C000000 | 0x0000007E))
#define LCD             ((LCD_TypeDef *) LCD_BASE)
回复 支持 反对

使用道具 举报

22

主题

129

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
324
金钱
324
注册时间
2015-6-23
在线时间
68 小时
发表于 2016-5-4 20:56:17 | 显示全部楼层
我用它来播放:下一站天后320X240时,前面获取播放信息和显示信息都正确,但读取找到“MOVI”后偏移到第一帧视频数据块读取第一个数据块的ID和长度时,avix.StreamID值为0X6463是正确的,但长度avix.StreamSize为0X000006E4,这个值肯定是错的,导致读取第一帧数据和下一帧音频数据的ID和长度时错的,所以播放中断退出,但这个视频用计算机播放是正常的,请教楼主是什么原因?
回复 支持 反对

使用道具 举报

22

主题

129

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
324
金钱
324
注册时间
2015-6-23
在线时间
68 小时
发表于 2016-5-4 22:23:45 | 显示全部楼层
好象只能正常解码第一帧图片,然后就在获取和第一帧图像一起读出来的8个字节ID和长度时,ID不是DC或WB而出错退出
回复 支持 反对

使用道具 举报

22

主题

129

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
324
金钱
324
注册时间
2015-6-23
在线时间
68 小时
发表于 2016-5-5 10:11:06 | 显示全部楼层
程序中我屏蔽了音频部分,其它都没变,修改部分程序如下:
u8 video_play_mjpeg(u8 *pname)
{   
        u8* framebuf;        //视频解码buf         
        u8* pbuf;                //buf指针  
        FIL *favi;
        u8  res=0;
        u16 offset=0;
        u32        nr;
        u8 key;   
    u8 i2ssavebuf;  
        i2sbuf[0]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE);        //申请音频内存
//        i2sbuf[1]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE);        //申请音频内存
//        i2sbuf[2]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE);        //申请音频内存
//        i2sbuf[3]=mymalloc(SRAMIN,AVI_AUDIO_BUF_SIZE);        //申请音频内存
        framebuf=mymalloc(SRAMIN,AVI_VIDEO_BUF_SIZE);        //申请视频buf
        favi=(FIL*)mymalloc(SRAMIN,sizeof(FIL));                //申请favi内存
        memset(i2sbuf[0],0,AVI_AUDIO_BUF_SIZE);
//        memset(i2sbuf[1],0,AVI_AUDIO_BUF_SIZE);
//        memset(i2sbuf[2],0,AVI_AUDIO_BUF_SIZE);
//        memset(i2sbuf[3],0,AVI_AUDIO_BUF_SIZE);
//        if(i2sbuf[3]==NULL||framebuf==NULL||favi==NULL)
        if(i2sbuf[0]==NULL||framebuf==NULL||favi==NULL)
        {
                printf("memory error!\r\n");
                res=0XFF;
        }   
        while(res==0)
        {
                res=f_open(favi,(char *)pname,FA_READ);
                if(res==0)
                {
                        pbuf=framebuf;                       
                        res=f_read(favi,pbuf,AVI_VIDEO_BUF_SIZE,&nr);//开始读取       
                        if(res)
                        {
                                printf("fread error:%d\r\n",res);
                                break;
                        }          
                        //开始avi解析
                        res=avi_init(pbuf,AVI_VIDEO_BUF_SIZE);        //avi解析
                        if(res)
                        {
                                printf("avi err:%d\r\n",res);
                                break;
                        }        
                        video_info_show(&avix);
                        TIM6_Int_Init(avix.SecPerFrame/100-1,8400-1);//10Khz计数频率,加1是100us
                        offset=avi_srarch_id(pbuf,AVI_VIDEO_BUF_SIZE,"movi");//寻找movi ID         
                        avi_get_streaminfo(pbuf+offset+4);                        //获取流信息
                        f_lseek(favi,offset+12);                                        //跳过标志ID,读地址偏移到流数据开始处         
//                        res=mjpegdec_init((lcddev.width-avix.Width)/2,110+(lcddev.height-110-avix.Height)/2);//JPG解码初始化
                        res=mjpegdec_init((lcddev.width-avix.Width)/2,(lcddev.height-avix.Height)/2);//JPG解码初始化
                        if(avix.SampleRate)                                                        //有音频信息,才初始化
                        {
//                                WM8978_I2S_Cfg(2,0);        //飞利浦标准,16位数据长度
//                                I2S2_Init(I2S_Standard_Phillips,I2S_Mode_MasterTx,I2S_CPOL_Low,I2S_DataFormat_16bextended);                //飞利浦标准,主机发送,时钟低电平有效,16位帧长度
//                                I2S2_SampleRate_Set(avix.SampleRate);        //设置采样率
//                                I2S2_TX_DMA_Init(i2sbuf[1],i2sbuf[2],avix.AudioBufSize/2); //配置DMA
//                                i2s_tx_callback=audio_i2s_dma_callback;        //回调函数指向I2S_DMA_Callback
//                                i2splaybuf=0;
//                                i2ssavebuf=0;
//                                I2S_Play_Start(); //开启I2S播放
                        }
                        while(1)//播放循环
                        {                                       
                                if(avix.StreamID==AVI_VIDS_FLAG)        //视频流
                                {
                                        pbuf=framebuf;
                                        f_read(favi,pbuf,avix.StreamSize+8,&nr);                //读入整帧+下一数据流ID信息  
                                        res=mjpegdec_decode(pbuf,avix.StreamSize);
                                        if(res)
                                        {
                                                printf("decode error!\r\n");
                                        }
                                        while(frameup==0);        //等待时间到达(在TIM6的中断里面设置为1)
                                        frameup=0;                        //标志清零
                                        frame++;
                                }else         //音频流
                                {                  
                                        video_time_show(favi,&avix);         //显示当前播放时间
//                                        i2ssavebuf++;
//                                        if(i2ssavebuf>3)i2ssavebuf=0;
//                                        do
//                                        {
//                                                nr=i2splaybuf;
//                                                if(nr)nr--;
//                                                else nr=3;
//                                        }while(i2ssavebuf==nr);//碰撞等待.
//                                        f_read(favi,i2sbuf[i2ssavebuf],avix.StreamSize+8,&nr);//填充i2sbuf         
                                        f_read(favi,i2sbuf[0],avix.StreamSize+8,&nr);//填充i2sbuf         
                                        pbuf=i2sbuf[0];  
                                }
                                key=ReadKeyDown();
                                if(key==KEY1||key==KEY2)//KEY1/KEY2按下,播放下一个/上一个视频
                                {
                                        res=key;
                                        break;
                                }else if(key==KEY3||key==KEY4)
                                {
                                        I2S_Play_Stop();//关闭音频
                                        video_seek(favi,&avix,framebuf);
                                        pbuf=framebuf;
                                        I2S_Play_Start();//开启DMA播放
                                }
                                if(avi_get_streaminfo(pbuf+avix.StreamSize))//读取下一帧 流标志
                                {
                                        printf("frame error \r\n");
                                        res=KEY3;
                                        break;
                                }                                                   
                        }
//                        I2S_Play_Stop();        //关闭音频
                        TIM6->CR1&=~(1<<0); //关闭定时器6
                        LCD_SetDisplayWindow(0,0,lcddev.width,lcddev.height);//恢复窗口
                        mjpegdec_free();        //释放内存
                        f_close(favi);
                }
        }
        myfree(SRAMIN,i2sbuf[0]);
        myfree(SRAMIN,i2sbuf[1]);
        myfree(SRAMIN,i2sbuf[2]);
        myfree(SRAMIN,i2sbuf[3]);
        myfree(SRAMIN,framebuf);
        myfree(SRAMIN,favi);
        return res;
}

监控后发现第一次读60K数据获取AVI信息和第一帧的ID和数据块长度以显示第一帧图像是正常了,但读第一帧时顺带读第二帧的ID和数据长度的数据是错误的,ID不为DC或WB,数据长度也非常巨大,所以第二帧不能正常解码,只能中断退出

@正点原子 , 能帮分析一下原因吗?
回复 支持 反对

使用道具 举报

22

主题

129

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
324
金钱
324
注册时间
2015-6-23
在线时间
68 小时
发表于 2016-5-7 10:21:57 | 显示全部楼层
找到原因了,原来移植的程序里,在JPEG解码时修改了AVIX这个结构体的avix.StreamSize、avix.Width、avix.Height,导致多SD卡文件时指针位置错乱,还有播放是选择按键时窗口位置出错,我自己定义了StreamSize、Width、Height后就可以正常显示了,至于例程中BUG在哪里导致AVIX结构被错误修改还没去查找。
回复 支持 反对

使用道具 举报

0

主题

1

帖子

0

精华

新手入门

积分
4
金钱
4
注册时间
2019-12-13
在线时间
1 小时
发表于 2019-12-13 14:19:38 | 显示全部楼层
]编译错误了
回复 支持 反对

使用道具 举报

0

主题

53

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
354
金钱
354
注册时间
2019-12-7
在线时间
27 小时
发表于 2019-12-20 08:10:38 | 显示全部楼层
下载之前顶一下!!
回复 支持 反对

使用道具 举报

1

主题

7

帖子

0

精华

新手上路

积分
27
金钱
27
注册时间
2017-6-11
在线时间
3 小时
发表于 2021-10-18 00:58:34 | 显示全部楼层
请问一下,显示屏的大小对于显示效果是否有较大影响?是否能支持20寸、30寸或更大的显示屏?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-2-26 19:29

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表