第五十章 视频播放器实验
[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格式视频播放,我们先来简单介绍一下AVI和libjpeg。
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文件结构方式,RIFF(Resource Interchange
File Format,资源互换文件格式)是微软定义的一种用于管理WINDOWS环境中多媒体数据的文件格式,波形音频WAVE,MIDI和数字视频AVI都采用这种格式存储。构造RIFF文件的基本单元叫做数据块(Chunk),每个数据块包含3个部分,
1、4字节的数据块标记(或者叫做数据块的ID)
2、数据块的大小
3、数据
整个RIFF文件可以看成一个数据块,其数据块ID为RIFF,称为RIFF块。一个RIFF文件中只允许存在一个RIFF块。RIFF块中包含一系列的子块,其中有一种子块的ID为"LIST",称为LIST块,LIST块中可以再包含一系列的子块,但除了LIST块外的其他所有的子块都不能再包含子块。
RIFF和LIST块分别比普通的数据块多一个被称为形式类型(Form
Type)和列表类型(List Type)的数据域,其组成如下:
1、4字节的数据块标记(Chunk ID)
2、数据块的大小
3、4字节的形式类型或者列表类型(ID)
4、数据
下面我们看看AVI文件的结构。AVI文件是目前使用的最复杂的RIFF文件,它能同时存储同步表现的音频视频数据。AVI的RIFF块的形式类型(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块(结构体都有把BlockID和BlockSize包含进来,下同)的定义如下:
//avih 子块信息
typedef struct
{
u32
BlockID; //块标志:avih==0X61766968
u32
BlockSize; //块大小(不包含最初的8字节,即BlockID和BlockSize不算)
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子列表,至少包括一个strh(Stream Header)块和一个strf(Stream Format)块,还有一个可选的strn(Stream Name)块(未列出)。注意:strl子列表出现的顺序与媒体流的编号(比如:00dc,前面的00,即媒体流编号00)是对应的,比如第一个strl子列表说明的是第一个流(Stream 0),假设是视频流,则表征视频数据块的四字符码为“00dc”,第二个strl子列表说明的是第二个流(Stream 1),假设是音频流,则表征音频数据块的四字符码为“01dw”,以此类推。
先看strh子块,该块用于说明这个流的头信息,定义如下:
//strh 流头子块信息(strh∈strl)
typedef struct
{
u32
BlockID; //块标志:strh==0X73747268
u32
BlockSize; //块大小(不包含最初的8字节,即BlockID和BlockSize不算)
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字节,即BlockID和BlockSize不算)
BMP_HEADER
bmiHeader; //位图信息头
AVIRGBQUAD
bmColors[1]; //颜色表
}STRF_BMPHEADER;
这里有3个结构体,strf子块完整内容即:STRF_BMPHEADER结构体,不过对我们有用的信息,都存放在BMP_HEADER结构体里面,本结构体对视频数据的解码起决定性的作用,它告诉我们视频的分辨率(Width和Height),以及视频所用的编码器(Compression),因此它决定了视频的解码。本章例程仅支持解码视频分辨率小于屏幕分辨率,且编解码器必须是MJPG的视频格式。
如果strh子块是音频数据流(StreamType=“auds”),则strf子块的内容定义如下:
//对于strh,如果是音频流,strf(流格式)使STRF_WAVHEADER块
typedef struct
{
u32
BlockID; //块标志,strf==0X73747266
u32
BlockSize; //块大小(不包含最初的8字节,即BlockID和BlockSize不算)
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文件的主体部分。音视频数据块交错的嵌入在“movi”LIST块里面,通过标准类型码进行区分,标准类型码有如下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*240的JPG图片,用TJPGD来解码,得120多ms,而用libjpeg,则只需要50ms左右即可完成解码,明显速度上libjpeg要快不少,使得解码视频成为可能。实际上,经过我们优化后的libjpeg,使用STM32F4,在不超频的情况下,可以流畅播放480*272@10帧的MJPG视频(带音频)。
篇幅所限,关于libjpeg的移植,我们这里就不介绍了,请大家参考光盘源码。关于libjpeg的移植和使用,其实在下载的libjpeg源码里面,就有很多介绍,大家重点可以看:readme.txt、filelist.txt、install.txt和libjpeg.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数据的详细信息,也保存着解码之后输出数据的详细信息。一般情况下,每次调用libjpeg库API的时候都需要把这个变量作为第一个参数传入。另外用户也可以通过修改该变量来修改libjpeg行为,比如输出数据格式,libjpeg库可用的最大内存等等。
不过,在STM32F4里面使用,可不能按以示例代码这么来定义cinfo和jerr结构体,因为单片机堆栈有限,cinfo和jerr都比较大(均超过400字节),很容易出现堆栈溢出的情况。在开发板源码,使用的是全局变量,而且用的是指针,通过内存管理分配。
接下来,开始看解码步骤,第一步是分配,并初始化解码对象结构体。这里做了两件事:1,错误管理,2,初始化解码对象。首先,错误管理使用setjmp和longjmp机制(不懂请百度)来实现类似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_buffer和skip_input_data,前者用于填充数据给libjpeg,后者用于跳过一定字节的数据。这两个函数请看本例程源码(在mjpeg.c里面)。
第三步,读取文件参数。通过jpeg_read_header函数实现,该函数将读取JPEG的很多参数,必须在解码前调用。
第四步,设置解码参数,示例代码没有做任何设置(使用默认值)。本章代码则做了设置,如下:
cinfo->dct_method
= JDCT_IFAST;
cinfo->do_fancy_upsampling
= 0;
这里,我们设置了使用快速整型DCT和do_fancy_upsampling的值为假(0),以提高解码速度。
第五步,开始解码。示例代码首先调用jpeg_start_decompress函数,然后计算样本输出buffer大小,并为其申请内存,为后续读取解码后的数据做准备。不过本章例程,我们为了提高速度,没有做这些处理了,我们直接修改底层函数:h2v1_merged_upsample和h2v2_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_upsample和h2v2_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.c。libjepg的使用,我们就介绍到这里。
最后,我们看看要实现avi视频文件的播放,主要有哪些步骤,如下:
1)初始化各外设
要解码视频,相关外设肯定要先初始化好,比如:SDIO(驱动SD卡用)、I2S、DMA、WM8978、LCD和按键等。这些具体初始化过程,在前面的例程都有介绍,大同小异,这里就不再细说了。
2)读取AVI文件,并解析
要解码,得先读取avi文件,按50.1.1节的介绍,读取出音视频关键信息,音频参数:编码方式、采样率、位数和音频流类型码(01wb/00wb)等;视频参数:编码方式、帧间隔、图片尺寸和视频流类型码(00dc/01dc)等;共同的:数据流起始地址。有了这些参数,我们便可以初始化音视频解码,为后续解码做好准备。
3)根据解析结果,设置相关参数
根据第2步解析的结果,设置I2S的音频采样率和位数,同时要让视频显示在LCD中间区域,得根据图片尺寸,设置LCD开窗时x,y方向的偏移量。
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
这些前面都已介绍过。本实验,大家需要准备1个SD卡和一个耳机(或喇叭),分别插入SD卡接口和耳机接口(喇叭接P1接口),然后下载本实验就可以看视频了!
50.3 软件设计
打开本章实验工程目录可以看到,我们在工程根目录新建MJPEG文件夹,在该文件夹里面新建了JPEG文件夹,存放libjpeg v9a的相关代码,同时,在MJPEG文件夹里面新建了avi.c、avi.h、mjpeg.c和mjpeg.h四个文件。然后,工程里面,新建了MJPEG分组,将需要用到的相关.c文件添加到该分组下面,并将MJPEG和JPEG两个文件夹加入头文件包含路径。
我们还在APP文件夹下面新建了videoplayer.c和videoplayer.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,比如00dc,01wb,movi之类的,在解析数据以及快进快退的时候,有用到。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计数频率,加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解码
//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节最后所介绍的步骤进行,不过在这里,我们的音频播放用了4个buf,以提高解码的流畅度。
最后,我们看看主函数代码:
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,"2014年7月1日",16,0);
Show_Str(60,130,200,16,"KEY0:NEXT KEY2 REV",16,0);
Show_Str(60,150,200,16,"KEY_UP:FF KEY1:REW",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.1和50.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*800的4.3寸电容屏模块,所以,这里最大可以设置:480x272。PS:如果是2.8屏,最大宽度只能是240)。
比特率:1000。这里设置越大,视频质量越好,解码就越慢(可能会卡),我们设置为1000,可以得到比较好的视频质量,同时也不怎么会卡。
帧率:10。即每秒钟10帧,对于480*272的视频,本例程最高就只能播放10帧左右的视频,如果要想提高帧率,有几个办法:1,降低分辨率;2,降低比特率;3,降低音频采样率。
音频编码器:PCMS16LE。本例程只支持PCM音频,所以选择音频编码器为这个。
采样率:这里设置为11025,即11.025Khz的采样率。这里越高,声音质量越好,不过,转换后的文件就越大,而且视频可能会卡。
其他设置,采用默认的即可。设置完以后,点击确定,即可完成设置。
点击图50.4.1的5处的文件夹图标,设置转换后视频的输出路径,这里我们设置到了桌面,这样转换后的视频,会保存在桌面。最后,点击图中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的尺寸!!否则无法播放(可能只听到声音,看不到图像)。
正点原子探索者STM32F407开发板购买地址:http://item.taobao.com/item.htm?id=41855882779
|