第五十章 视频播放器实验
[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 {MOD}和
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; //标记:是否允许这个流输出?调 {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字节
,即
BlockID和
BlockSize不算
)
BMP_HEADER
bmiHeader; //位图信息头
AVIRGBQUAD
bmColors[1]; //颜 {MOD}表
}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”(改用新的调 {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*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
", 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错误
一周热门 更多>