OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 17|回复: 0

《ESP32-S3使用指南—IDF版 V1.6》第四十三章视频播放器实验

[复制链接]

1175

主题

1187

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
5071
金钱
5071
注册时间
2019-5-8
在线时间
1267 小时
发表于 3 小时前 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2025-10-21 10:14 编辑

第四十三章视频播放器实验

1)实验平台:正点原子DNESP32S3开发板

2)章节摘自【正点原子】ESP32-S3使用指南—IDF版 V1.6

3)购买链接:https://detail.tmall.com/item.htm?&id=768499342659

4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32S3.html

5)正点原子官方B站:https://space.bilibili.com/394620890

6)正点原子DNESP32S3开发板技术交流群:132780729

155537c2odj87vz1z9vj6l.jpg

155537nfqovl2gg9faaol9.png

DNESP32S3的处理能力,不仅可以软解码音频,还可以用来播放视频!本章,我们将使用DNESP32S3开发板来播放AVI视频,本章我们将实现一个简单的视频播放器。
本章分为如下几个小节:
43.1 ES8388录音简介
43.2 硬件设计
43.3 程序设计
43.4 下载验证

43.1 AVI&libjpeg简介
本章,我们将使用libjepg(由IJG提供),来实现MJPEG编码的AVI格式视频播放,我们先来简单介绍一下AVI和libjpeg。
43.1.1 AVI简介
AVI是音频视频交错(Audio Video Interleaved)的英文缩写,它是微软开发的一种符合RIFF文件规范的数字音频与视频文件格式,原先用于Microsoft Video forWindows (简称VFW)环境,现在已被多数操作系统直接支持。
AVI格式允许视频和音频交错在一起同步播放,支持256色和RLE压缩,但AVI文件并未限定压缩标准,AVI仅仅是一个容器,用不同压缩算法生成的AVI文件,必须使用相应的解压缩算法才能播放出来。比如本章,我们使用的AVI,其音频数据采用16位线性PCM格式(未压缩),而视频数据,则采用MJPEG编码方式。
在介绍AVI文件前,我们要先来看看RIFF文件结构。AVI文件采用的是RIFF文件结构方式,RIFF(Resource Interchange File Format,资源互换文件格式)是微软定义的一种用于管理WINDOWS环境中多媒体数据的文件格式,波形音频WAVE,MIDI和数字视频AVI都采用这种格式存储。构造RIFF文件的基本单元叫做数据块(Chunk),每个数据块包含3个部分:
l  4字节的数据块标记(或者叫做数据块的ID)
l  数据块的大小
l  数据
整个RIFF文件可以看成一个数据块,其数据块ID为RIFF,称为RIFF块。一个RIFF文件中只允许存在一个RIFF块。RIFF块中包含一系列的子块,其中有一种子块的ID为"LIST",称为LIST块,LIST块中可以再包含一系列的子块,但除了LIST块外的其他所有的子块都不能再包含子块。
RIFF和LIST块分别比普通的数据块多一个被称为形式类型(Form Type)和列表类型(List Type)的数据域,其组成如下:
l  4字节的数据块标记(Chunk ID)
l  数据块的大小
l  4字节的形式类型或者列表类型(ID)
l  数据
下面我们看看AVI文件的结构。AVI文件是目前使用的最复杂的RIFF文件,它能同时存储同步表现的音频视频数据。AVI的RIFF块的形式类型(Form Type)是AVI,它一般包含3个子块,如下所述:
l  信息块,一个ID为"hdrl"的LIST块,定义AVI文件的数据格式。
l  数据块,一个ID为 "movi"的LIST块,包含AVI的音视频序列数据。
l  索引块,ID为"idxl"的子块,定义"movi"LIST块的索引数据,是可选块(不一定有)。
接下来,我们详细介绍下AVI文件的各子块构造,AVI文件的结构如图43.1.1所示:        
image002.png
图43.1.1 AVI文件结构图
从上图可以看出(注意‘AVI ’,是带了一个空格的),AVI文件,由:信息块(HeaderList)、数据块(MovieList)和索引块(Index Chunk)等三部分组成,下面,我们分别介绍这几个部分。
1,信息块(HeaderList
信息块,即ID为“hdrl”的LIST块,它包含文件的通用信息,定义数据格式,所用的压缩算法等参数等。hdrl块还包括了一系列的子块,首先是:avih块,用于记录AVI的全局信息,比如数据流的数量,视频图像的宽度和高度等信息,avih块(结构体都有把BlockID和BlockSize包含进来,下同)的定义如下:
  1. /* avih 子块信息 */
  2. typedef struct
  3. {
  4.     uint32_t BlockID;                 /* 块标志:avih==0X61766968 */
  5.     uint32_t BlockSize; /* 块大小(不包含最初8字节,也就是BlockID和BlockSize不在内*/
  6.     uint32_t SecPerFrame;            /* 视频帧间隔时间(单位为us) */
  7.     uint32_t MaxByteSec;             /* 最大数据传输率,字节/秒 */
  8.     uint32_tPaddingGranularity;    /* 数据填充的粒度 */
  9.     uint32_t Flags;                   /* AVI文件的全局标记,比如是否含有索引块等 */
  10.     uint32_t TotalFrame;                 /* 文件总帧数 */
  11.     uint32_t InitFrames;             /* 为交互格式指定初始帧数(非交互格式应该指定为0)*/
  12.     uint32_t Streams;                 /* 包含的数据流种类个数,通常为2 */
  13.     uint32_t RefBufSize;/* 建议读取本文件的缓存大小(应能容纳最大的块)默认是1M字节*/
  14.     uint32_t Width;                   /* 图像宽 */
  15.     uint32_t Height;                  /* 图像高 */
  16.     uint32_t Reserved[4];                /* 保留 */
  17. }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子块,该块用于说明这个流的头信息,定义如下:
  1. /* strh 流头子块信息(strh∈strl) */
  2. typedef struct
  3. {
  4. uint32_tBlockID;        /* 块标志:strh==0X73747268*/
  5. /* 块大小(不包含最初的8字节,也就是BlockID和BlockSize不计算在内) */
  6. uint32_tBlockSize;
  7. uint32_tStreamType;
  8. /* 数据流种类,vids(0X73646976):视频;auds(0X73647561):音频 */
  9. uint32_t Handler;  
  10. /* 指定流的处理者,对于音视频来说就是解码器,比如MJPEG/H264之类的 */
  11.     uint32_t Flags;          /* 标记:是否允许这个流输出?调色板是否变化? */
  12.     uint16_t Priority;       /* 流的优先级(当有多个相同类型的流时优先级最高的为默认流)*/
  13.     uint16_t Language;       /* 音频的语言代号 */
  14.     uint32_t InitFrames;    /* 为交互格式指定初始帧数 */
  15.     uint32_t Scale;          /* 数据量, 视频每帧的大小或者音频的采样大小 */
  16.     uint32_t Rate;           /*Scale/Rate=每秒采样数 */
  17.     uint32_t Start;          /* 数据流开始播放的位置,单位为Scale */
  18.     uint32_t Length;         /* 数据流的数据量,单位为Scale */
  19.     uint32_t RefBufSize;    /* 建议使用的缓冲区大小 */
  20.     uint32_t Quality;        /* 解压缩质量参数,值越大,质量越好 */
  21.     uint32_t SampleSize;    /* 音频的样本大小 */
  22.     struct                     /* 视频帧所占的矩形 */
  23.     {
  24.         short Left;
  25.         short Top;
  26.         short Right;
  27.         shortBottom;
  28.     } Frame;
  29. }STRH_HEADER;
复制代码
这里面,对我们最有用的即StreamType 和Handler这两个参数了,StreamType用于告诉我们此strl描述的是音频流(“auds”),还是视频流(“vids”)。而Handler则告诉我们所使用的解码器,比如MJPEG/H264等(实际以strf块为准)。
然后是strf子块,不过strf子块,需要根据strh子块的类型而定。
如果strh子块是视频数据流(StreamType=“vids”),则strf子块的内容定义如下:
  1. /* BMP结构体 */
  2. typedef struct
  3. {
  4.     uint32_t BmpSize;         /*bmp结构体大小,包含(BmpSize在内) */
  5.     long Width;                /* 图像宽 */
  6.     long Height;               /* 图像高 */
  7.     uint16_t  Planes;         /* 平面数,必须为1 */
  8.     uint16_t  BitCount;       /* 像素位数,0X0018表示24位 */
  9.     uint32_t  Compression;   /* 压缩类型,比如:MJPEG/H264等 */
  10.     uint32_t  SizeImage;     /* 图像大小 */
  11.     long XpixPerMeter;       /* 水平分辨率 */
  12.     long YpixPerMeter;       /* 垂直分辨率 */
  13.     uint32_t  ClrUsed;       /* 实际使用了调色板中的颜色数,压缩格式中不使用 */
  14.     uint32_t  ClrImportant;     /* 重要的颜色 */
  15. }BMP_HEADER;

  16. /* 颜色表 */
  17. typedef struct
  18. {
  19.     uint8_t  rgbBlue;        /* 蓝色的亮度(值范围为0-255) */
  20.     uint8_t  rgbGreen;       /* 绿色的亮度(值范围为0-255) */
  21.     uint8_t  rgbRed;          /* 红色的亮度(值范围为0-255) */
  22.     uint8_t  rgbReserved;        /* 保留,必须为0 */
  23. }AVIRGBQUAD;

  24. /* 对于strh,如果是视频流,strf(流格式)使STRH_BMPHEADER块 */
  25. typedef struct
  26. {
  27.     uint32_t BlockID;         /* 块标志,strf==0X73747266 */
  28. uint32_tBlockSize;      /* 块大小(不包含最初的8字节,也就是BlockID
  29. 和本BlockSize不计算在内) */
  30.     BMP_HEADER bmiHeader;    /* 位图信息头 */
  31.     AVIRGBQUAD bmColors[1];     /* 颜色表 */
  32. }STRF_BMPHEADER;
复制代码

这里有3个结构体,strf子块完整内容即:STRF_BMPHEADER结构体,不过对我们有用的信息,都存放在BMP_HEADER结构体里面,本结构体对视频数据的解码起决定性的作用,它告诉我们视频的分辨率(Width和Height),以及视频所用的编码器(Compression),因此它决定了视频的解码。本章例程仅支持解码视频分辨率小于屏幕分辨率,且编解码器必须是MJPEG的视频格式。
如果strh子块是音频数据流(StreamType=“auds”),则strf子块的内容定义如下:
  1. /* 对于strh,如果是音频流,strf(流格式)使STRH_WAVHEADER块 */
  2. typedef struct
  3. {
  4.     uint32_t BlockID;        /* 块标志,strf==0X73747266 */
  5. uint32_tBlockSize;      /* 块大小(不包含最初的8字节,也就是BlockID
  6. 和本BlockSize不计算在内) */
  7.     uint16_t FormatTag;      /* 格式标志:0X0001=PCM,0X0055=MP3 */
  8.     uint16_t Channels;       /* 声道数,一般为2,表示立体声 */
  9.     uint32_t SampleRate;     /* 音频采样率 */
  10.     uint32_t BaudRate;       /* 波特率 */
  11.     uint16_t BlockAlign;        /* 数据块对齐标志 */
  12.     uint16_t Size;            /* 该结构大小 */
  13. }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,软件资料à3,AVI学习资料.zip里面的相关文档。  
43.1.2 libjpeg简介
libjpeg是一个完全用C语言编写的库,包含了广泛使用的JPEG解码、JPEG编码和其他的JPEG功能的实现。这个IJG库由组织(Independent JPEG Group(独立JPEG小组))提供并维护。libjepg,目前最新版本为v9f,可以在https://www.ijg.org/files/这个网站下载。libjpeg具有稳定、兼容性强和解码速度较快等优点。
本章,我们使用libjpeg来实现MJPEG数据流的解码,MJPEG数据流,其实就是一张张的JPEG图片拼起来的图片视频流,只要能快速解码JPEG图片,就可以实现视频播放。
前面的图片显示实验我们使用了TJPGD实现JPEG解码,大家可能会问,为什么不直接用TJPGD来解码呢?因为TJPG的特点是:占用资源少,但是解码速度慢。在DNESP32S3上,同样一张320*240的JPG图片,用TJPGD来解码,需要120多毫秒,而用libjpeg,则只需要50ms左右即可完成解码,libjpeg的解码速度明显比TJPGD快了不少,使得解码视频成为可能。实际上,经过优化后的libjpeg,使用DNESP32S3不超频的情况下,可以流畅播放480*272@ 10帧的MJPEG视频(带音频)。
关于libjpeg的移植和使用,在下载的libjpeg源码里面还有很多介绍,可以重点看:readme.txt、filelist.txt、install.txt和libjpeg.txt等,也可以参考光盘源码进行移植与使用。
我们主要讲解一下如何使用libjpeg来实现一个jpeg图片的解码,这个在libjpeg源码里面:example.c,这个文件里面有简单的示例,在libjpeg.txt里面也有相关内容的介绍。我们主要简要介绍一下example.c里面标准解码流程(示例代码):
  1. structmy_error_mgr
  2. {
  3.     struct jpeg_error_mgrpub;   /* 公共”字段 */
  4.     jmp_buf setjmp_buffer;       /* 用于返回 */
  5. };

  6. typedef structmy_error_mgr * my_error_ptr;

  7. /**
  8. * @brief   JPEG解码错误处理函数
  9. * @param   无
  10. * @retval  无
  11. */

  12. METHODDEF(void)my_error_exit (j_common_ptr cinfo)
  13. {
  14.     my_error_ptr myerr = (my_error_ptr) cinfo->err;     /* 指向cinfo->err */
  15.     (*cinfo->err->output_message) (cinfo);               /* 显示错误信息 */
  16.     longjmp(myerr->setjmp_buffer, 1);                  /* 跳转到setjmp处 */
  17. }

  18. /**
  19. * @brief   JPEG解码函数
  20. * @param   filename : 解码文件
  21. * @retval  无
  22. */
  23. GLOBAL(int)read_JPEG_file(char* filename)
  24. {
  25.     structjpeg_decompress_struct cinfo;
  26.     struct my_error_mgrjerr;       /* 错误处理结构体 */
  27.     FILE *infile;                /* 输入源文件 */
  28.     JSAMPARRAY buffer;           /* 输出缓存 */
  29.     int row_stride;               /*physical row width in output buffer */

  30.     if ((infile= fopen(filename, "rb")) == NULL) /* 尝试打开文件 */
  31.     {
  32.         fprintf(stderr, "can'topen %s\n", filename);
  33.         return 0;
  34.     }

  35.     /* 第一步,设置错误管理,初始化JPEG解码对象 */
  36.     cinfo.err =jpeg_std_error(&jerr.pub);  /* 建立JPEG错误处理流程 */
  37.     jerr.pub.error_exit= my_error_exit;    /* 处理函数指向 my_error_exit */
  38.     /*
  39.      * 建立my_error_exit函数使用的返回上下文,当其他地方
  40.      * 调用longjmp函数时,可以返回到这里进行错误处理
  41.      */
  42.     if (setjmp(jerr.setjmp_buffer))
  43.     {
  44.         jpeg_destroy_decompress(&cinfo);   /* 释放解码对象资源 */
  45.         fclose(infile);                      /* 关闭文件 */
  46.         return 0;
  47.     }
  48.     jpeg_create_decompress(&cinfo);     /* 初始化解码对象cinfo */
  49.    
  50.     /* 第二步,指定数据源(比如一个文件) */
  51.     jpeg_stdio_src(&cinfo,infile);
  52.    
  53.     /* 第三步,读取文件参数(通过jpeg_read_header函数) */
  54.     (void)jpeg_read_header(&cinfo, TRUE);
  55.    
  56.     /* 第四步,设置解码参数(这里使用jpeg_read_header确认的默认参数),故无处理 */
  57.    
  58.     /* 第五步,开始解码 */
  59.     (void)jpeg_start_decompress(&cinfo);
  60.    
  61.     /* 在读取数据之前,可以做一些处理,比如设定LCD窗口,设定LCD起始坐标等 */
  62.    
  63.     /* 确定一样有多少样本 */
  64.     row_stride = cinfo.output_width* cinfo.output_components;
  65.     /* 确保buffer至少可以保存一行的数据样本,并为其申请内存 */
  66.     buffer = (*cinfo.mem->alloc_sarray)
  67.         ((j_common_ptr) &cinfo,JPOOL_IMAGE, row_stride, 1);

  68.     /* 第六步,循环读取数据 */
  69.    
  70.     /* 每次读取一样,直到读完整个文件 */      
  71.     while (cinfo.output_scanline< cinfo.output_height)
  72.     {
  73.        (void)jpeg_read_scanlines(&cinfo,buffer, 1);/* 解码一行数据 */
  74.         put_scanline_someplace(buffer[0],row_stride);/* 将解码后的数据输出到某处 */
  75.     }

  76.     /* 第七步,结束解码 */
  77.     (void)jpeg_finish_decompress(&cinfo);

  78.     /* 第八步,释放解码对象资源 */
  79.     jpeg_destroy_decompress(&cinfo);  /* 释放解码申请的资源(SRAM内存) */
  80.     fclose(infile);                     /* 关闭文件 */
  81.     return 1;
  82. }
复制代码

以上代码,将一个jpeg解码分成了8个步骤,我们结合本例程代码简单讲解下这几个步骤。我们先来看一下一个很重要的结构体数据类型:structjpeg_decompress_struct,定义成cinfo结构体变量,该变量保存着jpeg数据的详细信息,也保存着解码之后输出数据的详细信息。一般情况下,每次调用libjpeg库API的时候都需要把这个变量作为第一个参数传入。另外,用户也可以通过修改这个变量来修改libjpeg行为,比如输出数据格式,libjpeg库可用的最大内存等。
因为DNESP32S3堆栈有限,不能按照示例来定义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函数。这个函数的作用是指定数据源,我们用另外一个函数实现:
  1. /**
  2. * @brief   设置JPEG解压缩的源数据
  3. * @param   cinfo : 结构体指针
  4.               Inbuffer :内存位置和大小所储存在结构体中的指针
  5.               Insize : 数据大小
  6. * @retval  无
  7. */
  8. GLOBAL(void)
  9. jpeg_mem_src (j_decompress_ptrcinfo,
  10.                  const unsigned char *inbuffer,
  11.               size_t insize)
  12. {
  13.   struct jpeg_source_mgr * src;

  14.   if (inbuffer== NULL || insize == 0)/* 将空输入视为致命错误*/
  15.     ERREXIT(cinfo,JERR_INPUT_EMPTY);

  16.   /* 源对象是永久性的,
  17.      因此可以通过只在第一个之前调用jpeg_mem_src从同一个缓冲区读取一系列JPEG图像 */
  18.   if (cinfo->src == NULL) { /*first time for this JPEG object? */
  19.     cinfo->src = (structjpeg_source_mgr *) (*cinfo->mem->alloc_small)
  20.       ((j_common_ptr) cinfo,JPOOL_PERMANENT, SIZEOF(struct jpeg_source_mgr));
  21.   }
  22.   src = cinfo->src;
  23.   src->init_source =init_mem_source;
  24.   src->fill_input_buffer= fill_mem_input_buffer;
  25.   src->skip_input_data =skip_input_data;
  26.   src->resync_to_restart= jpeg_resync_to_restart; /* 使用默认方法 */
  27.   src->term_source =term_source;
  28.   src->bytes_in_buffer =insize;
  29.   src->next_input_byte = (constJOCTET *) inbuffer;
  30. }
复制代码
这里面重点是两个函数:fill_mem_input_buffer和skip_input_data,前者用于从内存填充数据给libjpeg,后者用于跳过一定字节的数据。这两个函数请看本例程源码(在mjpeg.c里面)。
第三步,读取文件参数。通过jpeg_read_header函数实现,该函数将读取JPEG的各个参数,必须在解码之前调用。
第四步,开始解码。示例代码首先调用jpeg_start_decompress函数,然后计算样本输出buffer大小,并为其申请内存,为后续读取解码后的数据做准备。
第五步,循环读取数据。通过jpeg_read_scanlines函数,循环解码并读取jpeg图片数据,实现jpeg解码。
第六步,解码结束。解码完成后,通过jepg_finish_decompress函数,结束jpeg解码。
第七步,释放解码对象资源。在所有操作完成后,通过jpeg_destroy_decompress,释放解码过程中用到的资源(比如释放内存)。
这样,我们就完成了一张jpeg图片的解码。详细的代码,请大家参考光盘本例程源码mjpeg.c。
最后,我们看看要实现avi视频文件的播放,主要有哪些步骤,如下:
l  初始化各外设
要解码视频,相关外设肯定要先初始化好,比如:SDIO(驱动SD卡用)、SAI、DMA、ES8388、LCD按键等。这些具体初始化过程,在前面的例程都有介绍,大同小异,这里就不再细说了。
l  读取AVI文件,并解析
要解码,得先读取avi文件,按43.1.1节的介绍,读取出音视频关键信息,音频参数:编码方式、采样率、位数和音频流类型码(01wb/00wb)等;视频参数:编码方式、帧间隔、图片尺寸和视频流类型码(00dc/01dc)等;共同的:数据流起始地址。有了这些参数,我们便可以初始化音视频解码,为后续解码做好准备。
l  根据解析结果,设置相关参数
根据第2步解析的结果,设置I&#178;S的音频采样率和位数,同时要让视频显示在LCD中间区域,得根据图片尺寸,设置LCD开窗时x,y方向的偏移量。
l  读取数据流,开始解码
前面三步完成,就可以正式开始播放视频了。读取视频流数据(movi块),根据类型码,执行音频/视频解码。对于音频数据(01wb/00wb),本例程只支持未压缩的PCM数据。对于视频数据(00dc/01dc),本例程只支持MJPEG,通过libjpeg解码,所以将视频数据按前面所说的几个步骤解码即可。然后,利用定时器来控制帧间隔,以正常速度播放视频,从而实现音视频解码。
l  解码完成,释放资源
最后在文件读取完后(或者出错了),需要释放申请的内存、恢复LCD窗口、关闭定时器、停止SAI播放音乐和关闭文件等一系列操作,等待下一次解码。

43.2 硬件设计
43.2.1例程功能
1、本实验开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,则开始播放SD卡VIDEO文件夹里面的视频(.avi格式)。
注意:自备SD卡一张,并在SD卡根目录建立一个VIDEO文件夹,存放AVI视频(仅支持MJPEG视频,音频必须是PCM,且视频分辨率必须小于等于屏幕分辨率)在里面。例程所需视频,可以通过:狸窝全能视频转换器,转换后得到,具体步骤见<<DNESP32S3开发指南>>)。
视频播放时,LCD上会显示视频名字、当前视频编号、总视频数、声道数、音频采样率、帧率、播放时间和总时间等信息。KEY0用于选择下一个视频,KEY1用于选择上一个视频,KEY_UP可以快进,KEY1可以快退。
2、LED闪烁,提示程序运行。
43.2.2硬件资源
1. LED灯
LED -IO0
2.独立按键
KEY0(XL9555) - IO1_7
KEY1(XL9555) - IO1_6
KEY2(XL9555) - IO1_5
KEY3(XL9555) - IO1_4
3. XL9555
IIC_SDA-IO41
IIC_SCL-IO42
4. SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在P5端口,使用跳线帽将IO_SET和LCD_DC相连)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
5. SD
CS-IO2
SCK-IO12
MOSI-IO11
MISO-IO13
6. ES8388音频CODEC芯片(IIC端口0)
IIC_SDA-IO41
IIC_SCL-IO42
I2S_BCK_IO-IO46
I2S_WS_IO-IO9
I2S_DO_IO-IO10
I2S_DI_IO-IO14
IS2_MCLK_IO-IO3
7. 开发板板载的咪头或自备麦克风输入
8. 喇叭或耳机
9. MJPEG解码库
10. 高分辨率定时器(ESP定时器)
43.2.3原理图
本实验相关的原理图同上一章节。

43.3 程序设计
43.3.1 程序流程图
程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。下面看看本实验的程序流程图:
image004.png
图43.3.1.1录音实验程序流程图
43.3.2 视频播放实验函数解析
本章实验所使用ESP32-S3的API函数在第四十一章节已经讲述过了,在此不再赘述。
43.3.3 视频播放实验驱动解析
在IDF版的32_videoplayer例程中,作者在32_videoplayer\components\BSP路径下新增了一个ESPTIM文件夹,分别用于存放esptim.c、esptim.h这两个文件。同时,在32_videoplayer\components路径下新增了MJPEG驱动文件。
1MJPEG驱动
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MJPEG驱动源码包括四个文件:avi.c、avi.h、mjpeg.c和mjpeg.h。
avi.h头文件在43.1小节部分讲过,具体请看源码。下面来看到avi.c文件,这里总共有三个函数都很重要,首先介绍AVI解码初始化函数,该函数定义如下:
  1. /* avi文件相关信息 */
  2. AVI_INFO g_avix;
  3. /* 视频编码标志字符串,00dc/01dc */
  4. char *constAVI_VIDS_FLAG_TBL[2] = {"00dc", "01dc"};
  5. /* 音频编码标志字符串,00wb/01wb */
  6. char *constAVI_AUDS_FLAG_TBL[2] = {"00wb", "01wb"};

  7. /**
  8. * @brief      avi解码初始化
  9. * @param      buf  : 输入缓冲区
  10. * @param      size : 缓冲区大小
  11. * @retval     res
  12. * @arg         OK,avi文件解析成功
  13. * @arg         其他,错误代码
  14. */
  15. AVISTATUSavi_init(uint8_t *buf, uint32_t size)
  16. {
  17.     uint16_t offset;
  18.     uint8_t *tbuf;
  19.     AVISTATUS res =AVI_OK;
  20.     AVI_HEADER *aviheader;
  21.     LIST_HEADER *listheader;
  22.     AVIH_HEADER *avihheader;
  23.     STRH_HEADER *strhheader;

  24.     STRF_BMPHEADER *bmpheader;
  25.     STRF_WAVHEADER *wavheader;

  26.     tbuf = buf;
  27.     aviheader = (AVI_HEADER*)buf;
  28.     if (aviheader->RiffID!= AVI_RIFF_ID)
  29.     {
  30.         returnAVI_RIFF_ERR;                          /* RIFF ID错误 */
  31.     }

  32.     if (aviheader->AviID !=AVI_AVI_ID)
  33.     {
  34.         returnAVI_AVI_ERR;                           /* AVI ID错误 */
  35.     }

  36.     buf += sizeof(AVI_HEADER);                       /* 偏移 */
  37.     listheader = (LIST_HEADER*)(buf);
  38.     if (listheader->ListID!= AVI_LIST_ID)
  39.     {
  40.         returnAVI_LIST_ERR;                          /* LIST ID错误 */
  41.     }

  42.     if (listheader->ListType!= AVI_HDRL_ID)
  43.     {
  44.         returnAVI_HDRL_ERR;                          /* HDRL ID错误 */
  45.     }

  46.     buf += sizeof(LIST_HEADER);                      /* 偏移 */
  47.     avihheader = (AVIH_HEADER*)(buf);
  48.     if (avihheader->BlockID!= AVI_AVIH_ID)
  49.     {
  50.         returnAVI_AVIH_ERR;                          /* AVIH ID错误 */
  51.     }

  52.     g_avix.SecPerFrame= avihheader->SecPerFrame;      /* 得到帧间隔时间 */
  53.     g_avix.TotalFrame= avihheader->TotalFrame;      /* 得到总帧数 */
  54.     buf +=avihheader->BlockSize + 8;                 /* 偏移 */
  55.     listheader = (LIST_HEADER*)(buf);
  56.     if (listheader->ListID!= AVI_LIST_ID)
  57.     {
  58.         returnAVI_LIST_ERR;                          /* LIST ID错误 */
  59.     }

  60.     if (listheader->ListType!= AVI_STRL_ID)
  61.     {
  62.         returnAVI_STRL_ERR;                          /* STRL ID错误 */
  63.     }

  64.     strhheader = (STRH_HEADER*)(buf + 12);
  65.     if (strhheader->BlockID!= AVI_STRH_ID)
  66.     {
  67.         returnAVI_STRH_ERR;                          /*STRH ID错误 */
  68.     }

  69.     if (strhheader->StreamType== AVI_VIDS_STREAM)     /* 视频帧在前 */
  70.     {
  71.         if (strhheader->Handler!= AVI_FORMAT_MJPG)
  72.         {
  73.             returnAVI_FORMAT_ERR;                   /* 非MJPG视频流,不支持 */
  74.         }

  75.         g_avix.VideoFLAG= AVI_VIDS_FLAG_TBL[0];     /* 视频流标记 "00dc" */
  76.         g_avix.AudioFLAG= AVI_AUDS_FLAG_TBL[1];     /* 音频流标记 "01wb" */
  77.         bmpheader = (STRF_BMPHEADER*)(buf + 12 +strhheader->BlockSize + 8);   
  78.         if (bmpheader->BlockID!= AVI_STRF_ID)
  79.         {
  80.             returnAVI_STRF_ERR;                      /* STRF ID错误 */
  81.         }

  82.         g_avix.Width =bmpheader->bmiHeader.Width;
  83.         g_avix.Height= bmpheader->bmiHeader.Height;
  84.         buf +=listheader->BlockSize + 8;            /* 偏移 */
  85.         listheader = (LIST_HEADER*)(buf);
  86.         if (listheader->ListID!= AVI_LIST_ID)      /* 是不含有音频帧的视频文件 */
  87.         {
  88.             g_avix.SampleRate= 0;                     /* 音频采样率 */
  89.             g_avix.Channels= 0;                       /* 音频通道数 */
  90.             g_avix.AudioType= 0;                      /* 音频格式 */

  91.         }
  92.         else
  93.         {
  94.             if (listheader->ListType!= AVI_STRL_ID)
  95.             {
  96.                 returnAVI_STRL_ERR;    /* STRL ID错误 */
  97.             }

  98.             strhheader = (STRH_HEADER*)(buf + 12);
  99.             if (strhheader->BlockID!= AVI_STRH_ID)
  100.             {
  101.                 returnAVI_STRH_ERR;    /* STRH ID错误 */
  102.             }

  103.             if (strhheader->StreamType!= AVI_AUDS_STREAM)
  104.             {
  105.                 returnAVI_FORMAT_ERR;  /* 格式错误 */
  106.             }

  107.             wavheader = (STRF_WAVHEADER*)(buf + 12 +strhheader->BlockSize+8);   
  108.             if (wavheader->BlockID!= AVI_STRF_ID)
  109.             {
  110.                 returnAVI_STRF_ERR;    /* STRF ID错误 */
  111.             }

  112.             g_avix.SampleRate= wavheader->SampleRate;       /* 音频采样率 */
  113.             g_avix.Channels= wavheader->Channels;       /* 音频通道数 */
  114.             g_avix.AudioType= wavheader->FormatTag;      /* 音频格式 */
  115.         }
  116.     }
  117.     else if (strhheader->StreamType== AVI_AUDS_STREAM)/* 音频帧在前 */
  118.     {
  119.         g_avix.VideoFLAG= AVI_VIDS_FLAG_TBL[1];         /* 视频流标记 "01dc" */
  120.         g_avix.AudioFLAG= AVI_AUDS_FLAG_TBL[0];         /* 音频流标记 "00wb" */
  121.         wavheader = (STRF_WAVHEADER*)(buf + 12 +strhheader->BlockSize + 8);
  122.         if (wavheader->BlockID!= AVI_STRF_ID)
  123.         {
  124.             returnAVI_STRF_ERR;                          /*STRF ID错误 */
  125.         }

  126.         g_avix.SampleRate= wavheader->SampleRate;           /* 音频采样率 */
  127.         g_avix.Channels= wavheader->Channels;            /* 音频通道数 */
  128.         g_avix.AudioType= wavheader->FormatTag;         /* 音频格式 */
  129.         buf +=listheader->BlockSize + 8;                  /* 偏移 */
  130.         listheader = (LIST_HEADER*)(buf);
  131.         if (listheader->ListID!= AVI_LIST_ID)
  132.         {
  133.             returnAVI_LIST_ERR;                          /* LIST ID错误 */
  134.         }

  135.         if (listheader->ListType!= AVI_STRL_ID)
  136.         {
  137.             returnAVI_STRL_ERR;    /* STRL ID错误 */
  138.         }

  139.         strhheader = (STRH_HEADER*)(buf + 12);
  140.         if (strhheader->BlockID!= AVI_STRH_ID)
  141.         {
  142.             returnAVI_STRH_ERR;    /* STRH ID错误 */
  143.         }

  144.         if (strhheader->StreamType!= AVI_VIDS_STREAM)
  145.         {
  146.             returnAVI_FORMAT_ERR;  /* 格式错误 */
  147.         }

  148.         bmpheader = (STRF_BMPHEADER*)(buf + 12 +strhheader->BlockSize + 8);   
  149.         if (bmpheader->BlockID!= AVI_STRF_ID)
  150.         {
  151.             returnAVI_STRF_ERR;    /* STRF ID错误 */
  152.         }

  153.         if (bmpheader->bmiHeader.Compression!= AVI_FORMAT_MJPG)
  154.         {
  155.             returnAVI_FORMAT_ERR;  /* 格式错误 */
  156.         }

  157.         g_avix.Width =bmpheader->bmiHeader.Width;
  158.         g_avix.Height= bmpheader->bmiHeader.Height;
  159.     }

  160.     offset =avi_srarch_id(tbuf, size, "movi");     /* 查找moviID */
  161.     if (offset== 0)
  162.     {
  163.         returnAVI_MOVI_ERR;        /*MOVI ID错误 */
  164.     }

  165.     if (g_avix.SampleRate)          /* 有音频流,才查找 */
  166.     {
  167.         tbuf +=offset;
  168.         offset =avi_srarch_id(tbuf, size,g_avix.AudioFLAG);   /* 查找音频流标记 */
  169.         if (offset== 0)
  170.         {
  171.             returnAVI_STREAM_ERR;  /* 流错误 */
  172.         }
  173.         tbuf +=offset + 4;
  174.         g_avix.AudioBufSize= *((uint16_t *)tbuf);  /* 得到音频流buf大小 */
  175.     }

  176.     printf("aviinit ok\r\n");
  177.     printf("g_avix.SecPerFrame:%ld\r\n",g_avix.SecPerFrame);
  178.     printf("g_avix.TotalFrame:%ld\r\n",g_avix.TotalFrame);
  179.     printf("g_avix.Width:%ld\r\n",g_avix.Width);
  180.     printf("g_avix.Height:%ld\r\n",g_avix.Height);
  181.     printf("g_avix.AudioType:%d\r\n",g_avix.AudioType);
  182.     printf("g_avix.SampleRate:%ld\r\n",g_avix.SampleRate);
  183.     printf("g_avix.Channels:%d\r\n",g_avix.Channels);
  184.     printf("g_avix.AudioBufSize:%d\r\n",g_avix.AudioBufSize);
  185.     printf("g_avix.VideoFLAG:%s\r\n",g_avix.VideoFLAG);
  186.     printf("g_avix.AudioFLAG:%s\r\n",g_avix.AudioFLAG);

  187.     return res;
  188. }
复制代码

该函数用于解析AVI文件,获取音视频流数据的详细信息,为后续解码做准备。
接下来介绍的是查找 ID函数,其定义如下:
  1. /**
  2. * @brief      查找 ID
  3. * @param      buf  : 待查缓存区
  4. * @param      size : 缓存大小
  5. * @param      id   : 要查找的id,必须是4字节长度
  6. * @retval     0,接收应答失败
  7. *              其他:movi ID偏移量
  8. */
  9. uint16_tavi_srarch_id(uint8_t *buf,uint32_t size, char *id)
  10. {
  11. uint32_ti;
  12. uint32_tidsize = 0;
  13.     size -= 4;
  14.     for (i = 0; i < size; i++)
  15.     {
  16.         if ((buf == id[0])&&
  17.             (buf[i + 1] == id[1])&&
  18.             (buf[i + 2] == id[2])&&
  19.             (buf[i + 3] == id[3]))   
  20.                 {
  21.                     idsize = MAKEDWORD(buf + i + 4);   
  22. /* 得到帧大小,必须大于16字节,才返回,否则不是有效数据 */
  23.                     if (idsize > 0X10)return i; /* 找到"id"所在的位置 */
  24.               }
  25.             }
  26.          }
  27.     }
  28.     return 0;
  29. }
复制代码

该函数用于查找某个ID,可以是4个字节长度的ID,比如00dc,01wb,movi之类的,在解析数据以及快进快退的时候,有用到。
接下来介绍的是得到stream流信息函数,其定义如下:
  1. /**
  2. * @brief      得到stream流信息
  3. * @param      buf  : 流开始地址(必须是01wb/00wb/01dc/00dc开头)
  4. * @retval     执行结果
  5. *  @arg       AVI_OK, AVI文件解析成功
  6. *  @arg       其他  , 错误代码
  7. */
  8. AVISTATUSavi_get_streaminfo(uint8_t *buf)
  9. {
  10.     g_avix.StreamID= MAKEWORD(buf + 2);        /* 得到流类型 */
  11.     g_avix.StreamSize= MAKEDWORD(buf + 4);         /* 得到流大小 */
  12. if (g_avix.StreamSize > AVI_MAX_FRAME_SIZE)    /* 帧大小太大了,直接返回错误 */
  13.     {
  14.         printf("FRAME SIZEOVER:%d\r\n", g_avix.StreamSize);
  15.         g_avix.StreamSize = 0;
  16.         returnAVI_STREAM_ERR;
  17.     }
  18.     if (g_avix.StreamSize% 2)
  19.     {
  20.         g_avix.StreamSize++;    /* 奇数加1(g_avix.StreamSize,必须是偶数) */
  21.     }
  22.     if (g_avix.StreamID== AVI_VIDS_FLAG || g_avix.StreamID== AVI_AUDS_FLAG)
  23.     {
  24.         returnAVI_OK;
  25.     }
  26.     return AVI_STREAM_ERR;
  27. }
复制代码

该函数用来获取当前数据流信息,重点是取得流类型和流大小,方便解码和读取下一个数据流。
mjpeg.h文件只有一些函数和变量声明,接下来,介绍mjpeg.c里面的几个函数,首先是初始化MJPEG解码数据源的函数,其定义如下:
  1. /**
  2. * @brief      mjpeg 解码初始化
  3. * @param      offx,offy:x,y方向的偏移
  4. * @retval     0,成功; 1,失败
  5. */
  6. charmjpegdec_init(uint16_t offx, uint16_t offy)
  7. {
  8. cinfo = (structjpeg_decompress_struct *)
  9. malloc(sizeof(structjpeg_decompress_struct));
  10.     jerr = (structmy_error_mgr *)malloc(sizeof(structmy_error_mgr));
  11.     if (cinfo == NULL || jerr == NULL)
  12.     {
  13.         printf("[E][mjpeg.cpp]mjpegdec_init():
  14. malloc failed to apply for memory\r\n");
  15.         mjpegdec_free();
  16.         return -1;
  17.     }
  18.     /* 保存图像在x,y方向的偏移量 */
  19.     imgoffx = offx;
  20.     imgoffy = offy;
  21.     return 0;
  22. }
复制代码

该函数用于初始化jpeg解码,主要是申请内存,然后确定视频在液晶上面的偏移(让视频显示在SPILCD中央)。
下面介绍的是MJPEG释放所有申请的内存函数,其定义如下:
  1. /**
  2. * @brief      mjpeg结束,释放内存
  3. * @param      无
  4. * @retval     无
  5. */
  6. voidmjpegdec_free(void)
  7. {
  8.     free(cinfo);
  9.     free(jerr);
  10. }
复制代码

该函数用于释放内存,解码结束后调用。
下面介绍的是解码一副JPEG图片函数,其定义如下:
  1. /**
  2. * @brief      解码一副JPEG图片
  3. * @param      buf: jpeg数据流数组
  4. * @param      bsize: 数组大小
  5. * @retval     0,成功; 1,失败
  6. */
  7. uint8_tmjpegdec_decode(uint8_t* buf, uint32_t bsize)
  8. {
  9.     JSAMPARRAY buffer;
  10.     if (bsize == 0) return 1;
  11.     int row_stride = 0;
  12.     int j = 0;                             /* 记录当前解码的行数 */
  13.     int lineR = 0;                         /* 每一行R分量的起始位置 */
  14.     cinfo->err =jpeg_std_error(&jerr->pub);
  15.     jerr->pub.error_exit= my_error_exit;
  16.     jerr->pub.emit_message= my_emit_message;
  17.     cinfo->out_color_space= JCS_RGB;
  18.     if (setjmp(jerr->setjmp_buffer))    /* 错误处理 */
  19.     {
  20.         jpeg_abort_decompress(cinfo);
  21.         jpeg_destroy_decompress(cinfo);
  22.         return 2;
  23.     }
  24.     jpeg_create_decompress(cinfo);
  25.     jpeg_mem_src(cinfo, buf, bsize);    /* 测试正常 */
  26.     jpeg_read_header(cinfo, TRUE);
  27.     jpeg_start_decompress(cinfo);
  28.     row_stride = cinfo->output_width* cinfo->output_components;
  29.     /* 计算buffer大小并申请相应空间 */
  30.     buffer = (*cinfo->mem->alloc_sarray)
  31.         ((j_common_ptr)cinfo,JPOOL_IMAGE, row_stride, 1);
  32.    
  33.     while (cinfo->output_scanline< cinfo->output_height)
  34.     {
  35.         int i = 0;
  36.         jpeg_read_scanlines(cinfo,buffer, 1);
  37.         unsigned shorttmp_color565;
  38.         /* 为上述图像数据赋值 */
  39.         for (int k = 0; k <Windows_Width * 2; k += 2)
  40.         {
  41.             tmp_color565 =rgb565(buffer[0],
  42.                                       buffer[0][i + 1],
  43.                                       buffer[0][i + 2]);
  44.                                     
  45.             lcd_buf[lineR + k] = (tmp_color565& 0xFF00) >> 8;
  46.             lcd_buf[lineR + k + 1] =  tmp_color565 & 0x00FF;
  47.             i += 3;
  48.         }
  49.         
  50.         j++;
  51.         lineR = j *Windows_Width * 2;
  52.     }
  53.     lcd_set_window(imgoffx,
  54.                       imgoffy - 30,
  55.                       imgoffx + cinfo->output_width- 1,
  56.                       imgoffy - 30 + cinfo->output_height- 1);
  57.     taskENTER_CRITICAL(&my_spinlock);
  58.     /* 例如:96*96*2/1536= 12;分12次发送RGB数据 */
  59.     for(int x = 0;
  60.             x < (cinfo->output_width* cinfo->output_height * 2 /LCD_BUF_SIZE);
  61.             x++)
  62.     {
  63.         /*&lcd_buf[j * LCD_BUF_SIZE] 偏移地址发送数据 */
  64.         lcd_write_data(&lcd_buf[x *LCD_BUF_SIZE] , LCD_BUF_SIZE);
  65.     }
  66.    
  67.     taskEXIT_CRITICAL(&my_spinlock);
  68.     lcd_set_window(0, 0,lcd_self.width, lcd_self.height);  /* 恢复窗口 */
  69.     jpeg_finish_decompress(cinfo);
  70.     jpeg_destroy_decompress(cinfo);
  71.     return 0;
  72. }
复制代码

该函数是解码jpeg的主要函数,通过前面43.1.2节介绍过的步骤进行解码,该函数的参数buf指向内存里面的一帧jpeg数据,bsize是数据大小。
2APP驱动
videoplayer.h头文件有两个宏定义和函数声明,具体请看源码。下面来看到videoplayer.c文件中,播放一个MJPEG文件函数,其定义如下:
  1. /**
  2. * @brief      播放MJPEG视频
  3. * @param      pname: 视频文件名
  4. * @retval     按键键值
  5. *             KEY2_PRES: 上一个视频
  6. *             KEY0_PRES: 下一个视频
  7. *             其他值   : 错误代码
  8. */
  9. static uint8_tvideo_play_mjpeg(uint8_t *pname)
  10. {
  11.     uint8_t *framebuf;
  12.     uint8_t *pbuf;
  13.     uint8_t res = 0;
  14.     uint16_t offset;
  15.     uint32_t nr;
  16.     uint8_t key;
  17.     FIL *favi;
  18.    
  19.     /* 申请内存 */
  20.     framebuf = (uint8_t *)malloc(AVI_VIDEO_BUF_SIZE);
  21.     favi = (FIL *)malloc(sizeof(FIL));
  22.     if ((framebuf== NULL) || (favi == NULL))
  23.     {
  24.         printf("memoryerror!\r\n");
  25.         res = 0xFF;
  26.     }
  27.     memset(framebuf, 0,AVI_VIDEO_BUF_SIZE);
  28.    
  29.     while (res == 0)
  30.     {
  31.         /* 打开文件 */
  32.         res = (uint8_t)f_open(favi, (const TCHAR*)pname, FA_READ);
  33.         
  34.         if (res == 0)
  35.         {
  36.             pbuf =framebuf;
  37.             
  38.             /* 开始读取 */
  39.             res = (uint8_t)f_read(favi, pbuf,AVI_VIDEO_BUF_SIZE, (UINT*)&nr);
  40.             
  41.             if (res != 0)
  42.             {
  43.                 printf("freaderror:%d\r\n", res);
  44.                 break;
  45.             }
  46.             
  47.             /* AVI解析 */
  48.             res =avi_init(pbuf, AVI_VIDEO_BUF_SIZE);
  49.             
  50.             if (res != 0)
  51.             {
  52.                 printf("avierror:%d\r\n", res);
  53.                 break;
  54.             }
  55.             
  56.             video_info_show(&g_avix);
  57.             esptim_int_init(g_avix.SecPerFrame/ 1000, 1000);
  58.             
  59.             /* 寻找movi ID */
  60.             offset =avi_srarch_id(pbuf, AVI_VIDEO_BUF_SIZE, "movi");
  61.             
  62.             /* 获取流信息 */
  63.             avi_get_streaminfo(pbuf +offset + 4);
  64.             
  65.             /* 跳过标志ID,读地址偏移到流数据开始处 */
  66.             f_lseek(favi,offset + 12);
  67.             
  68.             /* 初始化JPG解码 */
  69.             res =mjpegdec_init((lcd_self.width -g_avix.Width) / 2,
  70.                                     110+(lcd_self.height-110-g_avix.Height)/2);
  71.             
  72.             /* 定义图像的宽高 */
  73.             Windows_Width =g_avix.Width;
  74.             Windows_Height =g_avix.Height;
  75.             /* 有音频信息,才初始化 */
  76.             if (g_avix.SampleRate)
  77.             {
  78.                 printf("g_avix.SampleRate:%ld\r\n",g_avix.SampleRate);
  79.                
  80.                 /* 飞利浦标准,16位数据长度 */
  81.                 es8388_sai_cfg(0, 3);
  82.                
  83.                 /* 设置采样率 */
  84.                 i2s_set_samplerate_bits_sample(g_avix.SampleRate,
  85.                                               I2S_BITS_PER_SAMPLE_16BIT);
  86.                                                 i2s_start(I2S_NUM);
  87.             }
  88.             while (1)
  89.             {
  90.                
  91.                 /* 视频流 */
  92.                 if (g_avix.StreamID== AVI_VIDS_FLAG)
  93.                 {
  94.                     pbuf =framebuf;
  95.                     
  96.                     /* 读取整帧+下一帧数据流ID信息 */
  97.                     f_read(favi, pbuf,g_avix.StreamSize + 8, (UINT*)&nr);
  98.                     res =mjpegdec_decode(pbuf, g_avix.StreamSize);
  99.                     
  100.                     if (res != 0)
  101.                     {
  102.                         printf("decodeerror!\r\n");
  103.                     }
  104.                     /* 等待播放时间到达 */
  105.                     while (frameup== 0);
  106.                     frameup = 0;
  107.                 }
  108.                 else
  109.                 {
  110.                     /* 显示当前播放时间 */
  111.                     video_time_show(favi, &g_avix);
  112.                     
  113.                     /* 填充psaibuf */
  114.                     f_read(favi,framebuf, g_avix.StreamSize + 8, &nr);
  115.                     pbuf =framebuf;
  116.                     
  117.                     /* 数据转换+发送给DAC */
  118.                     i2s_tx_write(framebuf,g_avix.StreamSize);
  119.                 }
  120.                
  121.                 key =xl9555_key_scan(0);
  122.                 /* KEY0/KEY2按下,播放下一个/上一个视频 */
  123.                 if (key ==KEY0_PRES || key == KEY2_PRES)
  124.                 {
  125.                     res = key;
  126.                     break;
  127.                 }
  128.                 else if (key ==KEY1_PRES || key == KEY3_PRES)
  129.                 {
  130.                     /* 关闭音频 */
  131.                     i2s_stop(I2S_NUM);
  132.                     video_seek(favi, &g_avix,framebuf);
  133.                     pbuf =framebuf;
  134.                     
  135.                     /* 开启DMA播放 */
  136.                     i2s_start(I2S_NUM);
  137.                 }
  138.                 /* 读取下一帧流标志 */
  139.                 if (avi_get_streaminfo(pbuf +g_avix.StreamSize) != 0)
  140.                 {
  141.                     printf("g_frameerror\r\n");
  142.                     res =KEY0_PRES;
  143.                     break;
  144.                 }
  145.             }
  146.             i2s_stop(I2S_NUM);
  147.             esp_timer_stop(esp_tim_handle);
  148.             
  149.             /* 恢复窗口 */
  150.             lcd_set_window(0, 0,lcd_self.width, lcd_self.height);
  151.             
  152.             /* 释放内存 */
  153.             mjpegdec_free();
  154.             
  155.             /* 关闭文件 */
  156.             f_close(favi);
  157.         }
  158.     }
  159.     i2s_zero_dma_buffer(I2S_NUM);
  160.     free(framebuf);
  161.     free(favi);
  162.    
  163.     return res;
  164. }
复制代码

该函数用来播放一个avi视频文件(MJPEG编码),解码过程就是根据前面我们在43.1节最后所介绍的步骤进行。其他代码,我们就不介绍了,请大家参考本例程源码。
43.3.4 CMakeLists.txt文件
打开本实验BSP下的CMakeLists.txt文件,其内容如下所示:
  1. set(src_dirs
  2.             IIC
  3.             LCD
  4.             LED
  5.             SDIO
  6.             SPI
  7.             XL9555
  8.             ESPTIM
  9.             ES8388
  10.             I2S)
  11. set(include_dirs
  12.             IIC
  13.             LCD
  14.             LED
  15.             SDIO
  16.             SPI
  17.             XL9555
  18.             ESPTIM
  19.             ES8388
  20.             I2S)
  21. set(requires
  22.             driver
  23.             fatfs
  24.             esp_timer)
  25. idf_component_register(SRC_DIRS${src_dirs}
  26. INCLUDE_DIRS ${include_dirs}REQUIRES ${requires})
  27. component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
复制代码

上述的红色ESPTIM驱动以及esp_timer依赖库需要由开发者自行添加,以确保视频播放驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了视频播放驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。
打开本实验main文件下的CMakeLists.txt文件,其内容如下所示:
  1. idf_component_register(
  2.     SRC_DIRS
  3.         "."
  4.         "app"
  5.     INCLUDE_DIRS
  6.         "."
  7.         "app")
复制代码

上述的红色app驱动需要由开发者自行添加,在此便不做赘述了。
43.3.5 实验应用代码
打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。
  1. i2c_obj_ti2c0_master;
  2. /**
  3. * @brief      程序入口
  4. * @param      无
  5. * @retval     无
  6. */
  7. voidapp_main(void)
  8. {
  9.     esp_err_t ret = 0;
  10.     uint8_t key = 0;
  11.     /* 初始化NVS*/
  12.     ret =nvs_flash_init();
  13. if (ret ==ESP_ERR_NVS_NO_FREE_PAGES ||
  14. ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
  15.     {
  16.         ESP_ERROR_CHECK(nvs_flash_erase());
  17.         ret =nvs_flash_init();
  18.     }
  19.     /* 初始化LED*/
  20.     led_init();
  21.    
  22.     /* 初始化IIC0*/
  23.     i2c0_master =iic_init(I2C_NUM_0);
  24.    
  25.     /* 初始化SPI*/
  26.     spi2_init();
  27.    
  28.     /* 初始化IO扩展芯片 */  
  29.     xl9555_init(i2c0_master);
  30.    
  31.     /* 初始化LCD*/
  32.     lcd_init();
  33.     /* ES8388初始化 */
  34.     es8388_init(i2c0_master);
  35.    
  36.     /* 开启DAC关闭ADC */
  37.     es8388_adda_cfg(1, 0);
  38.     es8388_input_cfg(0);
  39.    
  40.     /* DAC选择通道输出 */
  41.     es8388_output_cfg(1, 1);
  42.    
  43.     /* 设置耳机音量 */
  44.     es8388_hpvol_set(20);
  45.    
  46.     /* 设置喇叭音量 */
  47.     es8388_spkvol_set(20);
  48.    
  49.     /* I2S初始化 */
  50.     i2s_init();
  51.     vTaskDelay(1000);
  52.    
  53.     /* 打开喇叭 */
  54.     xl9555_pin_write(SPK_EN_IO,0);
  55.     /* 检测不到SD卡 */
  56.     while (sd_spi_init())
  57.     {
  58.         lcd_show_string(30, 110, 200, 16, 16, "SDCard Error!", RED);
  59.         vTaskDelay(500);
  60.         lcd_show_string(30, 130, 200, 16, 16, "PleaseCheck! ", RED);
  61.         vTaskDelay(500);
  62.     }
  63.     /* 检查字库 */
  64.     while (fonts_init())
  65.     {
  66.         /* 清屏 */
  67.         lcd_clear(WHITE);
  68.         lcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", RED);
  69.         
  70.         /* 更新字库 */
  71.         key =fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);
  72.    
  73.         /* 更新失败 */
  74.         while (key)
  75.         {
  76.             lcd_show_string(30, 50, 200, 16, 16, "FontUpdate Failed!", RED);
  77.             vTaskDelay(200);
  78.             lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
  79.             vTaskDelay(200);
  80.         }
  81.         lcd_show_string(30, 50, 200, 16, 16, "FontUpdate Success!   ", RED);
  82.         vTaskDelay(1500);
  83.         
  84.         /* 清屏 */
  85.         lcd_clear(WHITE);
  86.     }
  87.     /* 为fatfs相关变量申请内存 */
  88.     ret =exfuns_init();
  89.     text_show_string(30, 30, 200, 16, "正点原子ESP32开发板", 16, 0, RED);
  90.     text_show_string(30, 50, 200, 16, "视频播放器实验", 16, 0, RED);
  91.     text_show_string(30, 70, 200, 16, "正点原子@ALIENTEK", 16, 0, RED);
  92.     text_show_string(30, 90, 200, 16, "KEY0:NEXTKEY2:PREV ", 16, 0, RED);
  93.     text_show_string(30, 110, 200, 16, "KEY_UP:FF   KEY1:REW", 16, 0, RED);
  94.    
  95.     /* 实验信息显示延时 */
  96.     vTaskDelay(500);
  97.     while (1)
  98.     {
  99.         video_play();
  100.     }
  101. }
复制代码

main函数只是经过一系列的外设初始化后,检查字库是否已经更新,然后显示实验的信息,就通过调用video_play函数,执行视频播放的程序了。
43.4 下载验证
本章,我们例程仅支持MJPEG编码的avi格式视频,且音频必须是PCM格式,另外视频分辨率不能大于LCD分辨率。要满足这些要求,现成的avi文件是很难找到的,所以我们需要用软件,将通用视频(任何视频都可以)转换为我们需要的格式,这里我们通过:狸窝全能视频转换器,这款软件来实现(路径:光盘:6,软件资料à1,软件à7,其他软件.zipà视频转换软件à狸窝全能视频转换器.exe)。安装完后,打开,然后进行相关设置,软件设置如图43.4.1和43.4.2所示:
image005.png
图43.4.1 软件启动界面和设置
image007.png
图43.4.2 高级设置
首先,如图43.4.1所示,点击1处,添加视频,找到你要转换的视频,添加进来。有的视频可能有独立字幕,比如我们打开的这个视频就有,所以在2处选择下字幕(如果没有的,可以忽略此步)。然后在3处,点击▼图标,选择预制方案:AVI-Audio-Video Interleaved(*.avi),即生成.avi文件,然后点击4处的高级设置按钮,进入43.4.2所示的界面,设置详细参数如下:
视频编码器:选择MJPEG。本例程仅支持MJPEG视频解码,所以选择这个编码器。
视频尺寸:480x272。这里得根据所用LCD分辨率来选择,假设我们用800*480的4.3寸电容屏模块,则这里最大可以设置:480x272。PS:如果是2.8屏,最大宽度只能是240)。
比特率:1000。这里设置越大,视频质量越好,解码就越慢(可能会卡),我们设置为1000,可以得到比较好的视频质量,同时也不怎么会卡。
帧率:10。即每秒钟10帧。对于480*272的视频,本例程最高能播放30帧左右的视频,如果要想提高帧率,有几个办法:1,降低分辨率;2,降低比特率;3,降低音频采样率。
音频编码器:PCMS16LE。本例程只支持PCM音频,所以选择音频编码器为这个。
采样率:这里设置为110250,即11.025Khz的采样率。这里越高,声音质量越好,不过,转换后的文件就越大,而且视频可能会卡。
其他设置,采用默认的即可。设置完以后,点击确定,即可完成设置。
点击图43.4.1的5处的文件夹图标,设置转换后视频的输出路径,这里我们设置到了桌面,这样转换后的视频,会保存在桌面。最后,点击图中6处的按钮,即可开始转换了,如图43.4.3所示:
image009.png
图43.4.3 正在转换
等转换完成后,将转换后的.avi文件,拷贝到SD卡àVIDEO文件夹下,然后插入开发板的SD卡接口,就可以开始测试本章例程了。
将程序下载到开发板后,程序先检测字库,只有字库已经更新才可以继续执行后面的程序。字库已更新,就可以看到LCD首先显示一些实验相关的信息,如图43.4.4所示:
image011.png
图43.4.4显示实验相关信息
显示了上图的信息后,检测SD卡的VIDEO文件夹,并查找avi视频文件,在找到有效视频文件后,便开始播放视频,如图43.4.5所示:
image013.png
图43.4.5 视频播放中
可以看到,屏幕显示了文件名、索引、声道数、采样率、帧率和播放时间等参数。然后,我们按KEY0/KEY2,可以切换到下一个/上一个视频,按KEY_UP/KEY1,可以快进/快退。
至此,本例程介绍就结束了。本实验,我们在开发板上实现了视频播放,体现了DNESP32S3强大的处理能力。
附本实验测试结果(视频比特率:1000,音频均为:110250,立体声)
对 240*160/240*180分辨率,可达30帧
对 320*240分辨率,可达20帧
对 480*272分辨率,可达10帧
最后提醒大家,转换的视频分辨率,一定要根据自己的SPILCD设置,不能超过SPILCD的尺寸!!否则无法播放(可能只听到声音,看不到图像)。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


关闭

原子哥极力推荐上一条 /1 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-10-21 13:18

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表