1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-336836-1-1.html 4)对正点原子STM32感兴趣的同学可以加群讨论:879133275
第五十一章 视频播放器实验本章我们将继续学习使用STM32H750自带的硬件JPEG编解码器,用于播放AVI视频(MJPEG编码),实现一个简单的视频播放器。 本章分为如下几个小节: 51.1 AVI简介 51.2 硬件设计 51.3 程序设计 51.4 下载验证
51.1 AVI简介 本章,我们使用STM32H7的硬件JPEG解码器,来实现MJPG编码的AVI格式视频播放,硬件JPEG解码器前面的实验已经介绍过了。接下来给大家简单介绍一下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文件的结构如图51.1.1所示:
图51.1.1 AVI文件结构图 从上图可以看出(注意‘AVI ’,是带了一个空格的),AVI文件,由:信息块(HeaderList)、数据块(MovieList)和索引块(Index Chunk)等三部分组成,下面,我们分别介绍这几个部分。 1、信息块(HeaderList) 信息块,即ID为“hdrl”的LIST块,它包含文件的通用信息,定义数据格式,所用的压缩算法等参数等。hdrl块还包括了一系列的字块,首先是:avih块,用于记录AVI的全局信息,比如数据流的数量,视频图像的宽度和高度等信息,avih块(结构体都有把BlockID和BlockSize包含进来,下同)的定义如下:
/* avih 子块信息 */
typedef struct
{
uint32_t BlockID; /* 块标志:avih==0X61766968 */
uint32_t BlockSize;/*块大小(不包含最初8字节,也就是BlockID和BlockSize不计算在内*/
uint32_t SecPerFrame; /* 视频帧间隔时间(单位为us) */
uint32_t MaxByteSec; /* 最大数据传输率,字节/秒 */
uint32_t PaddingGranularity; /* 数据填充的粒度 */
uint32_t Flags; /* AVI文件的全局标记,比如是否含有索引块等 */
uint32_t TotalFrame; /* 文件总帧数 */
uint32_t InitFrames; /* 为交互格式指定初始帧数(非交互格式应该指定为0)*/
uint32_t Streams; /* 包含的数据流种类个数,通常为2 */
uint32_t RefBufSize;/* 建议读取本文件的缓存大小(应能容纳最大的块)默认可能是1M字节*/
uint32_t Width; /* 图像宽 */
uint32_t Height; /* 图像高 */
uint32_t 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
{
uint32_t BlockID; /* 块标志:strh==0X73747268 */
/* 块大小(不包含最初的8字节,也就是BlockID和BlockSize不计算在内) */
uint32_t BlockSize;
uint32_t StreamType;/*数据流种类,vids(0X73646976):视频;auds(0X73647561):音频*/
uint32_t Handler; /*指定流的处理者,对于音视频来说就是解码器,比如MJPG/H264之类的*/
uint32_t Flags; /* 标记:是否允许这个流输出?调色板是否变化? */
uint16_t Priority; /* 流的优先级(当有多个相同类型的流时优先级最高的为默认流) */
uint16_t Language; /* 音频的语言代号 */
uint32_t InitFrames; /* 为交互格式指定初始帧数 */
uint32_t Scale; /* 数据量, 视频每桢的大小或者音频的采样大小 */
uint32_t Rate; /* Scale/Rate=每秒采样数 */
uint32_t Start; /* 数据流开始播放的位置,单位为Scale */
uint32_t Length; /* 数据流的数据量,单位为Scale */
uint32_t RefBufSize; /* 建议使用的缓冲区大小 */
uint32_t Quality; /* 解压缩质量参数,值越大,质量越好 */
uint32_t 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
{
uint32_t BmpSize; /* bmp结构体大小,包含(BmpSize在内) */
long Width; /* 图像宽 */
long Height; /* 图像高 */
uint16_t Planes; /* 平面数,必须为1 */
uint16_t BitCount; /* 像素位数,0X0018表示24位 */
uint32_t Compression; /* 压缩类型,比如:MJPG/H264等 */
uint32_t SizeImage; /* 图像大小 */
long XpixPerMeter; /* 水平分辨率 */
long YpixPerMeter; /* 垂直分辨率 */
uint32_t ClrUsed; /* 实际使用了调色板中的颜色数,压缩格式中不使用 */
uint32_t ClrImportant; /* 重要的颜色 */
} BMP_HEADER;
/* 颜色表 */
typedef struct
{
uint8_t rgbBlue; /* 蓝色的亮度(值范围为0-255) */
uint8_t rgbGreen; /* 绿色的亮度(值范围为0-255) */
uint8_t rgbRed; /* 红色的亮度(值范围为0-255) */
uint8_t rgbReserved; /* 保留,必须为0 */
} AVIRGBQUAD;
/* 对于strh,如果是视频流,strf(流格式)使STRH_BMPHEADER块 */
typedef struct
{
uint32_t BlockID; /* 块标志,strf==0X73747266 */
uint32_t 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(流格式)使STRH_WAVHEADER块 */
typedef struct
{
uint32_t BlockID; /* 块标志,strf==0X73747266 */
uint32_t BlockSize; /* 块大小(不包含最初的8字节,也就是BlockID
和本BlockSize不计算在内) */
uint16_t FormatTag; /* 格式标志:0X0001=PCM,0X0055=MP3 */
uint16_t Channels; /* 声道数,一般为2,表示立体声 */
uint32_t SampleRate; /* 音频采样率 */
uint32_t BaudRate; /* 波特率 */
uint16_t BlockAlign; /* 数据块对齐标志 */
uint16_t 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学习资料 里面的相关文档。 最后,我们看看要实现avi视频文件的播放,主要有哪些步骤,如下: 1)初始化各外设 要解码视频,相关外设肯定要先初始化好,比如:SDMMC(驱动SD卡用)、SAI、DMA、LCD和按键等。这些具体初始化过程,在前面的例程都有介绍,大同小异,这里就不再细说了。 2)读取AVI文件,并解析 要解码,得先读取avi文件,读取出音视频关键信息,音频参数:编码方式、采样率、位数和音频流类型码(01wb/00wb)等;视频参数:编码方式、帧间隔、图片尺寸和视频流类型码(00dc/01dc)等;共同的:数据流起始地址。有了这些参数,我们便可以初始化音视频解码,为后续解码做好准备。 3)根据解析结果,设置相关参数 根据第2步解析的结果,设置SAI的音频采样率和位数,同时要让视频显示在LCD中间区域,得根据图片尺寸,设置LCD开窗时x,y方向的偏移量。 4)读取数据流,开始解码 前面三步完成,就可以正式开始播放视频了。读取视频流数据(movi块),根据类型码,执行音频/视频解码。对于音频数据(01wb/00wb),本例程只支持未压缩的PCM数据,所以,直接填充到DMA缓冲区即可,由DMA循环发送给ES8388,播放音频。对于视频数据(00dc/01dc),本例程只支持MJPG,通过硬件JPEG解码,硬件JPEG解码流程详见第五十章。然后,利用定时器来控制帧间隔,以正常速度播放视频,从而实现音视频解码。 5)解码完成,释放资源 最后在文件读取完后(或者出错了),需要释放申请的内存、恢复LCD窗口、关闭定时器、停止SAI播放音乐和关闭文件等一系列操作,等待下一次解码。 51.2 硬件设计
- 例程功能 1、本实验开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,则开始播放SD卡VIDEO文件夹里面的视频(.avi格式)。 注意:自备SD卡一张,并在SD卡根目录建立一个VIDEO文件夹,存放AVI视频(仅支持MJPG视频,音频必须是PCM,且视频分辨率必须小于等于屏幕分辨率)在里面。例程所需视频,可以通过:狸窝全能视频转换器,转换后得到,具体步骤见。 视频播放时,LCD上会显示视频名字、当前视频编号、总视频数、声道数、音频采样率、帧率、播放时间和总时间等信息。KEY0用于选择下一个视频,KEY1用于选择上一个视频,KEY_UP可以快进,KEY1可以快退。 2、LED0闪烁,提示程序运行。
- 硬件资源 1)RGB灯 RED :LED0 - PB4 GREEN :LED1 - PE6 2)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面) 3)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动) 4)独立按键 :KEY0 - PA1、KEY1 - PA15、WK_UP - PA0 5)SD卡,通过SDMMC1(SDMMC_D0D4(PC8PC11), SDMMC_SCK(PC12),SDMMC_CMD(PD2))连接 6)norflash(QSPI FLASH芯片,连接在QSPI上) 7)硬件JPEG解码内核(STM32H750自带) 8)定时器6、7 51.3 程序设计 51.3.1 程序流程图
图50.3.1.1 照相机实验程序流程图 51.3.2 程序解析
- MJPEG驱动代码 这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MJPEG驱动源码包括四个文件:avi.c、avi.h、mjpeg.c和mjpeg.h。 avi.h头文件在51.1小节部分讲过,具体请看源码。下面来看到avi.c文件,这里总共有三个函数都很重要,首先介绍AVI解码初始化函数,该函数定义如下:
/* avi文件相关信息 */
AVI_INFO avix;
/* 视频编码标志字符串,00dc/01dc */
uint8_t *const AVI_VIDS_FLAG_TBL[2] = {"00dc", "01dc"};
/* 音频编码标志字符串,00wb/01wb */
uint8_t *const AVI_AUDS_FLAG_TBL[2] = {"00wb", "01wb"};
/**
* @brief AVI解码初始化
* @param buf : 输入缓冲区
* @param size : 缓冲区大小
* @retval 执行结果
* @arg AVI_OK, AVI文件解析成功
* @arg 其他 , 错误代码
*/
AVISTATUS avi_init(uint8_t *buf, uint32_t size)
{
uint16_t offset;
uint8_t *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) /* 视频帧在前 */
{
/* 非MJPG视频流,不支持 */
if (strhheader->Handler != AVI_FORMAT_MJPG)return AVI_FORMAT_ERR;
avix.VideoFLAG = (uint8_t *)AVI_VIDS_FLAG_TBL[0]; /* 视频流标记 "00dc" */
avix.AudioFLAG = (uint8_t *)AVI_AUDS_FLAG_TBL[1]; /* 音频流标记 "01wb" */
/* strf */
bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);
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
{
/* STRL ID错误 */
if (listheader->ListType != AVI_STRL_ID)return AVI_STRL_ERR;
strhheader = (STRH_HEADER *)(buf + 12);
/* STRH ID错误 */
if (strhheader->BlockID != AVI_STRH_ID)return AVI_STRH_ERR;
/* 格式错误 */
if (strhheader->StreamType != AVI_AUDS_STREAM)return AVI_FORMAT_ERR;
/* strf */
wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize + 8);
/* STRF ID错误 */
if (wavheader->BlockID != AVI_STRF_ID)return AVI_STRF_ERR;
avix.SampleRate = wavheader->SampleRate; /* 音频采样率 */
avix.Channels = wavheader->Channels; /* 音频通道数 */
avix.AudioType = wavheader->FormatTag; /* 音频格式 */
}
}
else if (strhheader->StreamType == AVI_AUDS_STREAM) /* 音频帧在前 */
{
avix.VideoFLAG = (uint8_t *)AVI_VIDS_FLAG_TBL[1]; /* 视频流标记 "01dc" */
avix.AudioFLAG = (uint8_t *)AVI_AUDS_FLAG_TBL[0]; /* 音频流标记 "00wb" */
/* strf */
wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize + 8);
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错误*/
/* STRL ID错误 */
if (listheader->ListType != AVI_STRL_ID)return AVI_STRL_ERR;
strhheader = (STRH_HEADER *)(buf + 12);
/* STRH ID错误 */
if (strhheader->BlockID != AVI_STRH_ID)return AVI_STRH_ERR;
/* 格式错误 */
if (strhheader->StreamType != AVI_VIDS_STREAM)return AVI_FORMAT_ERR;
/* strf */
bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);
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 = *((uint16_t *)tbuf); /* 得到音频流buf大小 */
}
printf("avi init ok\r\n");
printf("avix.SecPerFrame:%d\r\n", avix.SecPerFrame);
printf("avix.TotalFrame:%d\r\n", avix.TotalFrame);
printf("avix.Width:%d\r\n", avix.Width);
printf("avix.Height:%d\r\n", avix.Height);
printf("avix.AudioType:%d\r\n", avix.AudioType);
printf("avix.SampleRate:%d\r\n", avix.SampleRate);
printf("avix.Channels:%d\r\n", avix.Channels);
printf("avix.AudioBufSize:%d\r\n", avix.AudioBufSize);
printf("avix.VideoFLAG:%s\r\n", avix.VideoFLAG);
printf("avix.AudioFLAG:%s\r\n", avix.AudioFLAG);
return res;
}
该函数用于解析AVI文件,获取音视频流数据的详细信息,为后续解码做准备。 接下来介绍的是查找 ID函数,其定义如下:
/**
* @brief 查找 ID
* @param buf : 输入缓冲区
* @param size : 缓冲区大小
* @param id : 要查找的id, 必须是4字节长度
* @retval 执行结果
* @arg 0 , 没找到
* @arg 其他 , movi ID偏移量
*/
uint32_t avi_srarch_id(uint8_t *buf, uint32_t size, uint8_t *id)
{
uint32_t i;
uint32_t idsize = 0;
size -= 4;
for (i = 0; i 0X10)return i; /* 找到"id"所在的位置 */
}
}
return 0;
}
该函数用于查找某个ID,可以是4个字节长度的ID,比如00dc,01wb,movi之类的,在解析数据以及快进快退的时候,有用到。 接下来介绍的是得到stream流信息函数,其定义如下:
/**
* @brief 得到stream流信息
* @param buf : 流开始地址(必须是01wb/00wb/01dc/00dc开头)
* @retval 执行结果
* @arg AVI_OK, AVI文件解析成功
* @arg 其他 , 错误代码
*/
AVISTATUS avi_get_streaminfo(uint8_t *buf)
{
avix.StreamID = MAKEWORD(buf + 2); /* 得到流类型 */
avix.StreamSize = MAKEDWORD(buf + 4); /* 得到流大小 */
if (avix.StreamSize > AVI_MAX_FRAME_SIZE) /* 帧大小太大了,直接返回错误 */
{
printf("FRAME SIZE OVER:%d\r\n", avix.StreamSize);
avix.StreamSize = 0;
return AVI_STREAM_ERR;
}
/* 奇数加1(avix.StreamSize,必须是偶数) */
if (avix.StreamSize % 2)avix.StreamSize++;
if (avix.StreamID == AVI_VIDS_FLAG || avix.StreamID == AVI_AUDS_FLAG)
return AVI_OK;
return AVI_STREAM_ERR;
}
该函数用来获取当前数据流信息,重点是取得流类型和流大小,方便解码和读取下一个数据流。
mjpeg.h文件只有一些函数和变量声明,接下来,介绍mjpeg.c里面的几个函数,首先是初始化MJPEG函数,其定义如下:
/**
* @brief 初始化MJPEG
* @param offx : 显示图像在LCD上x方向的偏移量
* @param offy : 显示图像在LCD上y方向的偏移量
* @param width : 显示图像的宽度
* @param height : 显示图像的高度
* @retval 执行结果
* @arg 0 , 成功
* @arg 其他 , 失败
*/
uint8_t mjpeg_init(uint16_t offx, uint16_t offy, uint32_t width,
uint32_t height)
{
uint8_t i;
uint8_t res;
res = mjpeg_jpeg_core_init(&mjpeg); /* 初始化JPEG内核,不申请IN BUF */
if (res)return 1;
for (i = 0; i = mjpeg.Conf.ImageHeight)break;
}
/* out暂停,且当前writebuf已经为空了,则恢复out输出 */
else if (mjpeg.outdma_pause == 1 &&
mjpeg.outbuf[mjpeg.outbuf_write_ptr].sta == 0)
{
/* 继续下一次DMA传输 */
jpeg_out_dma_resume((uint32_t) mjpeg.outbuf[mjpeg.outbuf_write_ptr].buf, mjpeg.yuvblk_size);
mjpeg.outdma_pause = 0;
}
if (mjpeg.state == JPEG_STATE_ERROR) /* 解码出错,直接退出 */
{
break;
}
if (mjpeg.state == JPEG_STATE_FINISHED) /* 解码结束了,检查是否异常结束 */
{
if (mjpeg.yuvblk_curheight (mjpeg.yuvblk_curheight + 16))
{
mjpeg.state = JPEG_STATE_ERROR; /* 标记错误 */
printf("early finished!\r\n");
break;
}
}
}
if (g_mjpeg_fileover) /* 文件读完了,及时退出,防止死循环 */
{
timecnt++;
if (mjpeg.state == JPEG_STATE_NOHEADER)break; /* 解码JPEG头失败了 */
if (timecnt > 0X3FFFF)break; /* 超时退出 */
}
}
myfree(SRAM12, p_rgb565buf); /* 释放内存 */
return 0;
}
该函数是解码jpeg的主要函数,解码步骤参见第四十九章相关内容。解码后利用DMA2D将YUV转换成RGB565数据,存放在p_rgb565buf(MCU屏/RGB竖屏)/RGB屏显存(RGB横屏)里面,然后通过mjpeg_fill_color函数,将RGB565数据显示到LCD屏幕上。 2. APP驱动代码 这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。APP驱动源码包括两个文件:videoplayer.c和videoplayer.h。 videoplayer.h头文件有两个宏定义和函数声明,具体请看源码。下面来看到videoplayer.c文件中,播放一个MJPEG文件函数,其定义如下:
/**
* @brief 播放一个MJPEG文件
* @param pname : 文件名
* @retval 执行结果
* @arg KEY0_PRES , 下一曲
* @arg KEY1_PRES , 上一曲
* @arg 其他 , 错误
*/
uint8_t video_play_mjpeg(uint8_t *pname)
{
uint8_t *framebuf; /* 视频解码buf */
uint8_t *pbuf; /* buf指针 */
FIL *favi;
uint8_t res = 0;
uint32_t offset = 0;
uint32_t nr;
uint8_t key;
psaibuf = mymalloc(SRAM4, AVI_AUDIO_BUF_SIZE); /* 申请音频内存 */
framebuf = mymalloc(SRAMIN, AVI_VIDEO_BUF_SIZE); /* 申请视频buf */
favi = (FIL *)mymalloc(SRAM12, sizeof(FIL)); /* 申请favi内存 */
memset(psaibuf, 0, AVI_AUDIO_BUF_SIZE);
if (!psaibuf || !framebuf || !favi)
{
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 || avix.Width > lcddev.width)
{
printf("avi err:%d\r\n", res);
res = KEY0_PRES;
break;
}
video_info_show(&avix);
/* 10Khz计数频率,加1是100us */
btim_tim7_int_init(avix.SecPerFrame / 100 - 1, 24000 - 1);
/* 寻找movi ID */
offset = avi_srarch_id(pbuf, AVI_VIDEO_BUF_SIZE, "movi");
avi_get_streaminfo(pbuf + offset + 4); /* 获取流信息 */
f_lseek(favi, offset + 12); /* 跳过标志ID,读地址偏移到流数据开始处 */
if (lcddev.height fptr - AVI_VIDEO_BUF_SIZE) + offset + 8);
}
else
{
printf("g_frame error \r\n");
res = KEY0_PRES;
break;
}
}
}
TIM7->CR1 &= ~(1
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?