STM32之视频播放器(AVI&JPEG)
一、AVI简介AVI (Audio Video Interleaved) 是微软开发的一种符合RIFF文件规范的数字音视频交错文件格式 。AVI格式允许视频和音频交错在一起同步播放,支持256色和RLE压缩,但AVI文件并未限定压缩标准,AVI仅仅是一个容器,用不同压缩算法(比如:H.264/MPEG4/MJPEG等)生成的AVI文件,必须使用相应的解压缩算法才能播放出来。比如本章,我们使用的AV
一、AVI简介
AVI (Audio Video Interleaved) 是微软开发的一种符合RIFF文件规范的数字音视频交错文件格式 。AVI格式允许视频和音频交错在一起同步播放,支持256色和RLE压缩,但AVI文件并未限定压缩标准,AVI仅仅是一个容器,用不同压缩算法(比如:H.264/MPEG4/MJPEG等)生成的AVI文件,必须使用相应的解压缩算法才能播放出来。比如本章,我们使用的AVI,其音频数据采用16位线性PCM格式(未压缩),而视频数据,则采用MJPEG编码方式。AVI采用的是RIFF格式编码。
二、RIFF格式简介
RIFF(Resource Interchange File Format,资源互换文件格式)是微软定义的一种用于管理WINDOWS环境中多媒体数据的文件格式,波形音频WAVE,MIDI和数字视频AVI都采用这种格式存储。构造RIFF文件的基本单元叫做数据块(Chunk),每个数据块包含3个部分:
1、数据块标记(或者叫做数据块的ID, 4字节)
2、数据块的大小(4字节)
3、数据
整个RIFF文件可以看成一个数据块,其数据块ID为"RIFF" ,称为RIFF块。一个RIFF文件中只有一个RIFF块。RIFF块中包含一系列的子块,其中有一种子块的ID为"LIST",称为LIST块,LIST块中可以再包含一系列的子块,但除了LIST块外的其他所有子块都不能再包含子块。
RIFF和LIST块分别比普通的数据块多一个被称为形式类型(Form Type)和列表类型(List Type)的数据域,其组成如下:
- 1、数据块标记(Chunk ID,4字节)
- 2、数据块的大小(4字节)
- 3、形式类型或者列表类型(ID, 4字节)
- 4、数据
AVI文件是目前使用的最复杂的RIFF文件,它能同时存储同步表现的音频视频数据。AVI的RIFF块的形式类型(Form Type)是AVI ,它一般包含3个子块,如下所述: - 1、信息块,一个ID为"hdrl"的LIST块,定义AVI文件的数据格式。
- 2、数据块,一个ID为 "movi"的LIST块,包含AVI的音视频序列数据。
- 3、索引块,ID为"idxl"的子块,定义"movi"LIST块的索引数据,是可选块。
LIST块信息定义(不含数据域):
typedef struct
{
u32 ListID; //ListID=='LIST'==0X4c495354
u32 BlockSize; //块大小(不包含最初的8字节,也ListID和BlockSize不计算在内)
u32 ListType; //LIST子块类型:hdrl(信息块)/movi(数据块)
}LIST_HEADER;
三、AVI文件结构
AVI文件是由:信息块(HeaderList)、数据块(MovieList)和索引块(Index Chunk)等三部分组成。
①信息块(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;
在avih块之后,有一个或者多个strl子列表,文件中有多少种数据流(即前面的Streams),就有多少个strl子列表。每个strl子列表,至少包括一个strh(StreamHeader)块和一个strf(Stream Format)块,另有strn(Stream Name)块(可选,不一定有)。
注意:strl子列表出现的顺序与媒体流的编号是对应的(比如:00dc,前面的00,即媒体流编号00)。比如第一个strl子列表说明的是第一个流(Stream 0),假设是视频流,则表征视频数据块的四字符码为“00dc”,第二个strl子列表说明的是第二个流(Stream 1),假设是音频流,则表征音频数据块的四字符码为“01wb”,以此类推。
②strh子块。该块用于说明这个流的头信息,strh定义如下侧代码所示:
其中,对我们最有用的即StreamType 和Handler这两个参数。StreamType用于表示数据流类型;Handler则告诉我们所使用的解码器,比如MJPG/H264等。
//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;
注意音频流,Handler的值为0x01,由strf确定音频格式。
③strf子块。strf子块,需要根据strh子块的类型而定。
如果strh子块是视频数据流(StreamType=“vids”),则strf子块定义如下方代码所示:视频流的strf包含2个结构,最有用的就是BMP_HEADER结构体。它告诉视频分辨率及其所用编码器等重要信息。
//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;
如果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)等重要信息。
④数据块(MovieList),即ID为“movi”的LIST块,它包含AVI的音视频序列数据,是这个AVI文件的主体部分。音视频数据块交错的嵌入在“movi” LIST块里面,通过标准类型码进行区分,标准类型码有如下4种:
1,“##db”(非压缩视频帧)
2,“##dc”(压缩视频帧)
3,“##pc”(改用新的调色板)
4,“##wb”(音频帧)
其中##是编号,得根据我们的数据流顺序来确定,也就是前面的strl块。比如,如果第一个strl块是视频数据,那么对于视频帧,标准类型码就是:00dc。第二个strl块是音频数据,那么对于音频帧,标准类型码就是:01wb。紧跟着标准类型码的是4个字节的数据长度(不包含类型码和长度参数本身),该长度必须是偶数,如果读到为奇数,则加1即可。我们读数据的时候,一般一次性要读完一个标准类型码所表征的数据,方便解码。
⑤索引块(Index Chunk)。最后,紧跟在‘hdrl’列表和‘movi’列表之后的,就是AVI文件可选的索引块。这个索引块为AVI文件中每一个媒体数据块进行索引,并且记录它们在文件中的偏移(可能相对于‘movi’列表,也可能相对于AVI文件开头)。
四、JPEG简介
jpeg格式是目前网络上最流行的图像格式,一般简称为jpg格式,是可以把图像文件压缩到最小的格式。jpeg格式的图片在获得极高的压缩率的同时能展现十分丰富生动的图像。由于体积小,因此非常适合应用与互联网,可减少图像的传输时间,也普遍应用于需要连续色调的图像。
JPEG图片格式组成部分:SOI(文件头)+APP0(图像识别信息)+ DQT(定义量化表)+ SOF0(图像基本信息)+ DHT(定义Huffman表) + DRI(定义重新开始间隔)+ SOS(扫描行开始)+ EOI(文件尾)。
五、libjpeg解析JPEG
libjpeg是一个完全用C语言编写的库,包含了被广泛使用的JPEG解码、JPEG编码和其他的JPEG功能的实现。这个库由IJG组织(Independent JPEG Group,即独立JPEG小组)提供,并维护。libjpeg,目前最新版本为v9a,可以在:http://www.ijg.org 这个网站下载到。 libjpeg具有稳定、兼容性强和解码速度较快等优点。
解码步骤
①分配,并初始化解码对象结构体(cinfo)。
这里做了两件事:1,错误管理,2,初始化解码对象。首先,错误管理使用setjmp和longjmp机制来实现类似C++的异常处理功能,外部代码可以调用longjmp来跳转到setjmp位置,执行错误管理(释放内存,关闭文件等)。这里注册了my_error_exit函数,来执行错误退出处理,另外,使用:my_emit_message函数,来输出警告信息,方便调试代码。然后,通过调用jpeg_create_decompress函数实现初始化解码对象结构体:cinfo。
②指定数据源。
示例代码(指: example.c里面的JPEG解码参考代码,下同)用的是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用于填充数据给libjpeg
skip_input_data用于跳过一定字节的数据
③读取文件参数。
这个步骤,通过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显示窗口大小
LCD_WriteRAM_Prepare(); //开始写入GRAM
解码时,直接在h2v1_merged_upsample和h2v2_merged_upsample里面丢数据给LCD,实现jpeg解码输出到LCD。
⑥循环读取数据。
通过jpeg_read_scanlines函数,循环解码并读取jpeg图片数据,实现jpeg解码。示例代码通过put_scanline_someplace函数,输出到某个地方(如lcd,文件等)。
⑦释放解码对象资源。
在所有操作完成后,通过jpeg_destroy_decompress,释放解码过程中用到的资源(比如释放内存)。
六、STM32视频播放步骤
1)初始化各外设
要解码视频,相关外设肯定要先初始化好,比如:SDIO(驱动SD卡用)、I2S、DMA、WM8978、LCD和按键等。这些具体初始化过程,在前面的例程都有介绍,大同小异,这里就不再细说了。
2)读取AVI文件,并解析
要解码,得先读取avi文件,按之前的介绍,读取出音视频关键信息,音频参数:编码方式、采样率、位数和音频流类型码(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播放音乐和关闭文件等一系列操作,等待下一次解码 。
STM32视频播放就讲解到这里啦!!!
更多推荐
所有评论(0)