STM32之视频播放器(AVI&JPEG)

一、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视频播放就讲解到这里啦!!!

  • 5
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

留小乙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值