OpenEdv-开源电子网

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

《ESP32-P4开发指南— V1.0》第四十五章 视频播放器实验

[复制链接]

1262

主题

1276

帖子

2

精华

超级版主

Rank: 8Rank: 8

积分
5418
金钱
5418
注册时间
2019-5-8
在线时间
1404 小时
发表于 2026-2-10 09:28:16 | 显示全部楼层 |阅读模式
本帖最后由 正点原子运营 于 2026-2-10 09:28 编辑

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

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

2)章节摘自【正点原子】ESP32-P4开发指南— V1.0

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

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

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

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


2.jpg

3.png

在图片解码章节中了解到,ESP32-P4自带了硬件JPEG解码器,我们完全可以用来播放视频!本章,我们将使用ESP32-P4的硬件JPEG解码器来实现播放AVI视频,本章我们将实现一个简单的视频播放器,实现AVI视频播放。
本章分为如下几个小节:
45.1 AVI简介
45.2 硬件设计
45.3 程序设计
45.4 下载验证


45.1 AVI简介
AVI是音频视频交错(Audio Video Interleaved)的英文缩写,它是微软开发的一种符合RIFF文件规范的数字音频与视频文件格式,原先用于Microsoft Video for Windows(简称VFW)环境,现在已被多数操作系统直接支持。
AVI格式允许视频和音频交错在一起同步播放,支持256色和RLE压缩,但AVI文件并未限定压缩标准,AVI仅仅是一个容器,用不同压缩算法生成的AVI文件,必须使用相应的解压缩算法才能播放出来。比如本章,我们使用的AVI,其音频数据采用16位线性PCM格式(未压缩),而视频数据,则采用MJPG编码方式。大家可使用AVI解析工具对AVI文件进行解析,解析完成后,我们就可以得到该文件的AVI信息结构,如下图所示。

第四十五章 视频播放器实验529.png
图45.1.1 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文件的结构如下图所示。


第四十五章 视频播放器实验1335.png
图57.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_t PaddingGranularity;        /* 数据填充的粒度 */
  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_t BlockID;               /* 块标志:strh==0X73747268 */
  5. /* 块大小(不包含最初的8字节,也就是BlockID和BlockSize不计算在内) */
  6. uint32_t BlockSize;
  7.     uint32_t StreamType;/*数据流种类,vids(0X73646976):视频;auds(0X73647561):音频*/
  8.     uint32_t Handler;   /*指定流的处理者,对于音视频来说就是解码器,比如MJPG/H264之类的*/
  9.     uint32_t Flags;                 /* 标记:是否允许这个流输出?调色板是否变化? */
  10.     uint16_t Priority;             /* 流的优先级(当有多个相同类型的流时优先级最高的为默认流) */
  11.     uint16_t Language;                     /* 音频的语言代号 */
  12.     uint32_t InitFrames;                   /* 为交互格式指定初始帧数 */
  13.     uint32_t Scale;                 /* 数据量, 视频每帧的大小或者音频的采样大小 */
  14.     uint32_t Rate;                  /* Scale/Rate=每秒采样数 */
  15.     uint32_t Start;                 /* 数据流开始播放的位置,单位为Scale */
  16.     uint32_t Length;                /* 数据流的数据量,单位为Scale */
  17.     uint32_t RefBufSize;                   /* 建议使用的缓冲区大小 */
  18.     uint32_t Quality;                      /* 解压缩质量参数,值越大,质量越好 */
  19.     uint32_t SampleSize;                  /* 音频的样本大小 */
  20.     struct                           /* 视频帧所占的矩形 */
  21.     {
  22.         short Left;
  23.         short Top;
  24.         short Right;
  25.         short Bottom;
  26.     } Frame;
  27. } STRH_HEADER;
复制代码
这里面,对我们最有用的即StreamType 和Handler这两个参数了,StreamType用于告诉我们此strl描述的是音频流(“auds”),还是视频流(“vids”)。而Handler则告诉我们所使用的解码器,比如MJPG/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;          /* 压缩类型,比如:MJPG/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_t BlockSize;             /* 块大小(不包含最初的8字节,也就是BlockID
  29. 和本BlockSize不计算在内) */
  30.     BMP_HEADER bmiHeader;           /* 位图信息头 */
  31.     AVIRGBQUAD bmColors[1];                /* 颜色表 */
  32. } STRF_BMPHEADER;
复制代码
这里有3个结构体,strf子块完整内容即:STRF_BMPHEADER结构体,不过对我们有用的信息,都存放在BMP_HEADER结构体里面,本结构体对视频数据的解码起决定性的作用,它告诉我们视频的分辨率(Width和Height),以及视频所用的编码器(Compression),因此它决定了视频的解码。本章例程仅支持解码视频分辨率小于屏幕分辨率,且编解码器必须是MJPG的视频格式。
如果strh子块是音频数据流(StreamType=“auds”),则strf子块的内容定义如下:
  1. /* 对于strh,如果是音频流,strf(流格式)使STRH_WAVHEADER块 */
  2. typedef struct
  3. {
  4.     uint32_t BlockID;               /* 块标志,strf==0X73747266 */
  5. uint32_t BlockSize;             /* 块大小(不包含最初的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,软件资料AVI学习资料 里面的相关文档。

45.2 硬件设计

45.2.1 程序功能
1,本实验开机后,先初始化各外设,然后检测字库是否存在,如果检测无问题,则开始播放TF卡VIDEO文件夹里面的视频(.avi格式)。
注意:自备TF卡一张,并在TF卡根目录建立一个VIDEO文件夹,存放AVI视频(仅支持MJPG视频,音频必须是PCM,且视频分辨率必须小于等于屏幕分辨率)在里面。例程所需视频,可以通过:狸窝全能视频转换器,转换后得到,具体步骤下面45.4章节内容。
视频播放时,LCD上会显示视频名字、当前视频编号、总视频数、声道数、音频采样率、帧率、播放时间和总时间等信息。KEY0用于选择下一个视频,KEY1用于选择上一个视频,KEY2可以快进。
2,LED0闪烁,提示程序运行。

45.2.2 硬件资源
1)LED灯
        LED        0                - IO51
2)ES8388音频CODEC芯片,通过I2S驱动
3)I2S音频接口
        I2S_BCK_IO                - IO47
        I2S_WS_IO                - IO48
        I2S_DO_IO                - IO49
        I2S_DI_IO                - IO50
        I2S_MCK_IO                - IO46
4)XL9555
        IIC_INT                - IO36
        IIC_SDA                - IO33
        IIC_SCL                - IO32
        EXIO_8                - KEY0
        EXIO_9                - KEY1
        EXIO_10                - KEY2
5)RGBLCD/MIPILCD(引脚太多,不罗列出来)
6) SPIFFS
7)SD卡
        CMD                  -  IO44
        CLK                -  IO43
        D0                      -  IO39
        D1                      -  IO40
        D2                     -  IO41
        D3                      -  IO42

45.3 程序设计

45.3.1 程序流程图

第四十五章 视频播放器实验7527.png
图45.3.1.1 视频播放器实验程序流程图

45.3.2 程序解析
1,MJPEG驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MJPEG驱动源码包括四个文件:avi.c、avi.h、mjpeg.c和mjpeg.h。  
avi.h头文件在45.1小节部分讲过 ,具体请看源码。下面来看到avi.c文件,这里总共有三个函数都很重要,首先介绍AVI解码初始化函数,该函数定义如下:
  1. /* avi文件相关信息 */
  2. AVI_INFO g_avix;
  3. /* 视频编码标志字符串,00dc/01dc */                                      
  4. char *const AVI_VIDS_FLAG_TBL[2] = {"00dc", "01dc"};
  5. /* 音频编码标志字符串,00wb/01wb */  
  6. char *const AVI_AUDS_FLAG_TBL[2] = {"00wb", "01wb"};  

  7. /**
  8. * @brief            AVI解码初始化
  9. * [url=home.php?mod=space&uid=271674]@param[/url]            buf  : 输入缓冲区
  10. * @param            size : 缓冲区大小
  11. * @retval            执行结果
  12. *   @arg             AVI_OK, AVI文件解析成功
  13. *   @arg                  其他  , 错误代码
  14. */
  15. AVISTATUS avi_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)return AVI_RIFF_ERR; /* RIFF ID错误 */
  29.     if (aviheader->AviID != AVI_AVI_ID)return AVI_AVI_ERR;    /* AVI ID错误 */
  30.     buf += sizeof(AVI_HEADER);                                                                  /* 偏移 */
  31.     listheader = (LIST_HEADER *)(buf);
  32.     if (listheader->ListID != AVI_LIST_ID)return AVI_LIST_ERR;/* LIST ID错误 */
  33.     if (listheader->ListType!=AVI_HDRL_ID)return AVI_HDRL_ERR;/* HDRL ID错误*/
  34.     buf += sizeof(LIST_HEADER);                                                                 /* 偏移 */
  35.     avihheader = (AVIH_HEADER *)(buf);
  36.     if (avihheader->BlockID!= AVI_AVIH_ID)return AVI_AVIH_ERR;/* AVIH ID错误 */
  37.     g_avix.SecPerFrame = avihheader->SecPerFrame;                                /* 得到帧间隔时间 */
  38.     g_avix.TotalFrame = avihheader->TotalFrame;                                  /* 得到总帧数 */
  39.     buf += avihheader->BlockSize + 8;                                            /* 偏移 */
  40.     listheader = (LIST_HEADER *)(buf);
  41.     if (listheader->ListID != AVI_LIST_ID)return AVI_LIST_ERR;        /* LIST ID错误 */
  42.     if (listheader->ListType!= AVI_STRL_ID)return AVI_STRL_ERR;/* STRL ID错误*/
  43.     strhheader = (STRH_HEADER *)(buf + 12);
  44.     if (strhheader->BlockID!= AVI_STRH_ID)return AVI_STRH_ERR;        /* STRH ID错误 */
  45.     if (strhheader->StreamType == AVI_VIDS_STREAM)                  /* 视频帧在前 */
  46. {
  47. /* 非MJPG视频流,不支持 */
  48.         if (strhheader->Handler != AVI_FORMAT_MJPG)return AVI_FORMAT_ERR;   
  49.         g_avix.VideoFLAG=(uint8_t *)AVI_VIDS_FLAG_TBL[0];/* 视频流标记  "00dc" */
  50.         g_avix.AudioFLAG=(uint8_t *)AVI_AUDS_FLAG_TBL[1];/* 音频流标记  "01wb" */
  51. /* strf */
  52.         bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);   
  53.         if(bmpheader->BlockID!= AVI_STRF_ID)return AVI_STRF_ERR;/*STRF ID错误*/
  54.         g_avix.Width = bmpheader->bmiHeader.Width;
  55.         g_avix.Height = bmpheader->bmiHeader.Height;
  56.         buf += listheader->BlockSize + 8;                   /* 偏移 */
  57.         listheader = (LIST_HEADER *)(buf);
  58.         if (listheader->ListID != AVI_LIST_ID)          /* 是不含有音频帧的视频文件 */
  59.         {
  60.             g_avix.SampleRate = 0;            /* 音频采样率 */
  61.             g_avix.Channels = 0;              /* 音频通道数 */
  62.             g_avix.AudioType = 0;             /* 音频格式 */
  63.         }
  64.         else
  65.         {
  66. /* STRL ID错误 */
  67.             if (listheader->ListType != AVI_STRL_ID)return AVI_STRL_ERR;
  68.             strhheader = (STRH_HEADER *)(buf + 12);
  69. /* STRH ID错误 */
  70.             if (strhheader->BlockID != AVI_STRH_ID)return AVI_STRH_ERR;     
  71. /* 格式错误 */
  72.             if (strhheader->StreamType != AVI_AUDS_STREAM)
  73.                                 return AVI_FORMAT_ERR;   
  74. /* strf */
  75.             wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize+8);   
  76. /* STRF ID错误 */
  77.             if (wavheader->BlockID != AVI_STRF_ID)return AVI_STRF_ERR;  
  78.             g_avix.SampleRate = wavheader->SampleRate;   /* 音频采样率 */
  79.             g_avix.Channels = wavheader->Channels;               /* 音频通道数 */
  80.             g_avix.AudioType = wavheader->FormatTag;             /* 音频格式 */
  81.         }
  82.     }
  83.     else if (strhheader->StreamType == AVI_AUDS_STREAM)        /* 音频帧在前 */
  84.     {
  85.        g_avix.VideoFLAG = (uint8_t *)AVI_VIDS_FLAG_TBL[1];
  86. /* 视频流标记 "01dc" */
  87.        g_avix.AudioFLAG = (uint8_t *)AVI_AUDS_FLAG_TBL[0];
  88. /* 音频流标记 "00wb" */
  89. /* strf */
  90.        wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize + 8);   
  91.        if (wavheader->BlockID != AVI_STRF_ID)return AVI_STRF_ERR;
  92. /* STRF ID错误 */
  93.        g_avix.SampleRate = wavheader->SampleRate;                /* 音频采样率 */
  94.        g_avix.Channels = wavheader->Channels;                    /* 音频通道数 */
  95.        g_avix.AudioType = wavheader->FormatTag;                 /* 音频格式 */
  96.        buf += listheader->BlockSize + 8;                       /* 偏移 */
  97.        listheader = (LIST_HEADER *)(buf);
  98.        if (listheader->ListID != AVI_LIST_ID)return AVI_LIST_ERR;
  99.        /* LIST ID错误 */
  100.        /* STRL ID错误 */
  101.        if (listheader->ListType != AVI_STRL_ID)return AVI_STRL_ERR;
  102.        strhheader = (STRH_HEADER *)(buf + 12);                /* STRH ID错误 */
  103.        if (strhheader->BlockID != AVI_STRH_ID)return AVI_STRH_ERR;/* 格式错误 */
  104.        if (strhheader->StreamType != AVI_VIDS_STREAM)return AVI_FORMAT_ERR;
  105.        /* strf */   
  106.        bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);   
  107.        if (bmpheader->BlockID != AVI_STRF_ID)return AVI_STRF_ERR;
  108. /* STRF ID错误 */
  109.        if (bmpheader->bmiHeader.Compression != AVI_FORMAT_MJPG
  110.        return AVI_FORMAT_ERR;                             /* 格式错误 */
  111.        g_avix.Width = bmpheader->bmiHeader.Width;
  112.        g_avix.Height = bmpheader->bmiHeader.Height;
  113.     }
  114.     offset = avi_srarch_id(tbuf, size, (uint8_t*)"movi");        /* 查找movi ID */
  115.     if (offset == 0)return AVI_MOVI_ERR;                             /* MOVI ID错误 */
  116.     if (g_avix.SampleRate)                                      /* 有音频流,才查找 */
  117.     {
  118.         tbuf += offset;
  119.         offset = avi_srarch_id(tbuf, size, g_avix.AudioFLAG); /* 查找音频流标记 */
  120.         if (offset == 0)return AVI_STREAM_ERR;                              /* 流错误 */
  121.         tbuf += offset + 4;
  122.         g_avix.AudioBufSize = *((uint16_t *)tbuf);  /* 得到音频流buf大小 */
  123.     }
  124.     printf("avi init ok\r\n");
  125.     printf("g_avix.SecPerFrame:%d\r\n", g_avix.SecPerFrame);
  126.     printf("g_avix.TotalFrame:%d\r\n", g_avix.TotalFrame);
  127.     printf("g_avix.Width:%d\r\n", g_avix.Width);
  128.     printf("g_avix.Height:%d\r\n", g_avix.Height);
  129.     printf("g_avix.AudioType:%d\r\n", g_avix.AudioType);
  130.     printf("g_avix.SampleRate:%d\r\n", g_avix.SampleRate);
  131.     printf("g_avix.Channels:%d\r\n", g_avix.Channels);
  132.     printf("g_avix.AudioBufSize:%d\r\n", g_avix.AudioBufSize);
  133.     printf("g_avix.VideoFLAG:%s\r\n", g_avix.VideoFLAG);
  134.     printf("g_avix.AudioFLAG:%s\r\n", g_avix.AudioFLAG);
  135.     return res;
  136. }
复制代码
该函数用于解析AVI文件,获取音视频流数据的详细信息,为后续解码做准备。接下来介绍的是查找 ID函数,其定义如下:
  1. /**
  2. * @brief              查找 ID
  3. * @param              buf  : 输入缓冲区
  4. * @param             size : 缓冲区大小
  5. * @param             id   : 要查找的id, 必须是4字节长度
  6. * @retval            执行结果
  7. *   @arg            0     , 没找到
  8. *   @arg            其他  , movi ID偏移量
  9. */
  10. uint32_t avi_srarch_id(uint8_t *buf, uint32_t size, char *id)
  11. {
  12.     uint32_t i;
  13.     uint32_t idsize = 0;
  14.     size -= 4;
  15.     for (i = 0; i < size; i++)
  16.     {
  17.         if ((buf[i] == id[0]) &&
  18.             (buf[i + 1] == id[1]) &&
  19.             (buf[i + 2] == id[2]) &&
  20.             (buf[i + 3] == id[3]))
  21.         {
  22. /* 得到帧大小,必须大于16字节,才返回,否则不是有效数据 */
  23.             idsize = MAKEDWORD(buf + i + 4);   
  24.             if (idsize > 0X10)return i;         /* 找到"id"所在的位置 */
  25.         }
  26.     }
  27.     return 0;
  28. }
复制代码
该函数用于查找某个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. AVISTATUS avi_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.         g_avix.StreamSize = 0;
  15.         return AVI_STREAM_ERR;
  16.     }
  17. /* 奇数加1(g_avix.StreamSize,必须是偶数) */
  18.     if (g_avix.StreamSize % 2)g_avix.StreamSize++;  
  19. if (g_avix.StreamID == AVI_VIDS_FLAG || g_avix.StreamID == AVI_AUDS_FLAG)
  20. return AVI_OK;
  21.     return AVI_STREAM_ERR;
  22. }
复制代码
该函数用来获取当前数据流信息,重点是取得流类型和流大小,方便解码和读取下一个数据流。接下来,我们来看看mjpeg.c/.h文件。mjpeg.h文件只有一些函数和变量声明,我们重点介绍.c文件下的哪几个重要的函数,首先是初始化MJPEG解码数据源的函数,其定义如下:
  1. /**
  2. * @brief            初始化JPEG解码器
  3. * @param             width: 显示图像的宽度
  4. * @param             height: 显示图像的高度
  5. * @param            malloc_size: 申请到BUF大小
  6. * @retval           JPEG解码器申请到内存的地址
  7. */
  8. uint8_t *mjpegdec_init(uint32_t width, uint32_t height, uint32_t *malloc_size)
  9. {
  10.     jpeg_decode_engine_cfg_t decode_eng_cfg = {     /* JPEG解码器引擎配置 */
  11.         .intr_priority = 0,                         /* 中断优先级,0选择默认 */
  12.         .timeout_ms    = -1,                        /* 超时时间 */
  13. };
  14. /* 安装JPEG解码器驱动 */
  15.     ESP_ERROR_CHECK(jpeg_new_decoder_engine(&decode_eng_cfg, &jpgd_handle));   

  16.     jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = {   /* JPEG解码器内存申请配置 */
  17.         .buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,   /* 图像输出BUF */
  18.     };

  19. return (uint8_t *)jpeg_alloc_decoder_mem(width * height * 2 * 4,
  20. &rx_mem_cfg, (size_t *)malloc_size);   /* 分配缓冲区 */
  21. }
复制代码
该函数主要用于新建JPEG解码引擎,然后为解码数据申请4倍屏幕分辨率大小。下面介绍的是MJPEG释放所有申请的内存函数,其定义如下:
  1. /**
  2. * @brief             卸载JPEG解码器并释放内存
  3. * @param            buf: JPEG解码器申请的内存
  4. * @retval            无
  5. */
  6. void mjpegdec_free(uint8_t *buf)
  7. {
  8.     heap_caps_free(buf);         /* 释放JPEG解码器申请的内存 */
  9.     ESP_ERROR_CHECK(jpeg_del_decoder_engine(jpgd_handle));/* 卸载JPEG解码器引擎 */
  10. }
复制代码
该函数用于释放内存,并关闭JPEG解码引擎。接下来,笔者将介绍的是解码一副JPEG图片函数,其定义如下:
  1. /**
  2. * @brief             解码一副JPEG图片
  3. * @param              inbuf: jpeg数据流数组
  4. * @param              inbuf_size: jpeg数据流大小
  5. * @param             outbuf: jpeg解码器申请的内存缓冲区
  6. * @param             outbuf_size: jpeg解码器申请到内存大小
  7. * @param             width: 显示图像的宽度
  8. * @param             height: 显示图像的高度  
  9. * @retval            0,成功; 1,错误帧/解码错误
  10. */
  11. uint8_t mjpegdec_decode( uint8_t *inbuf, uint32_t inbuf_size,  uint8_t *outbuf,
  12. uint32_t outbuf_size, uint32_t width, uint32_t height)
  13. {
  14.     if (inbuf_size == 0)    /* 帧错误,跳过解码 */
  15.     {
  16.         return 1;
  17.     }

  18.     static uint32_t out_size = 0;
  19. ESP_ERROR_CHECK(jpeg_decoder_process(jpgd_handle, &decode_cfg_rgb,
  20. inbuf, inbuf_size, outbuf, outbuf_size, &out_size));     /* 解码JPEG图片 */
  21.     if (out_size == 0)      /* 解码长度错误 */
  22.     {
  23.         return 1;
  24.     }

  25.     /* 计算居中绘制的起始坐标 */
  26.     int x_offset = (lcddev.width - width) / 2;
  27.     int y_offset = (lcddev.height  - height) / 2;

  28.     /* 确保坐标合法性 */
  29.     x_offset = x_offset < 0 ? 0 : x_offset;
  30.     y_offset = y_offset < 0 ? 0 : y_offset;

  31.     /* LCD draw */
  32. esp_lcd_panel_draw_bitmap(lcddev.lcd_panel_handle, x_offset, y_offset,
  33. width + x_offset, height + y_offset + 1, (uint8_t *)outbuf);

  34.     return 0;
  35. }
复制代码
该函数主要用于把MJPEG图像数据进行JPEG解码,然后将其转换成RGB565数据格式,最后计算起始位置,让视频播放至LCD正中间位置。

2,APP驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。APP驱动源码包括两个文件:videoplayer.c和videoplayer.h。  
videoplayer.h头文件有两个宏定义和函数声明,具体请看源码。下面来看到videoplayer.c文件中,播放一个MJPEG文件函数,其定义如下:

  1. /**
  2. * @brief             播放MJPEG视频
  3. * @param             pname: 视频文件名
  4. * @retval            执行结果
  5. *                    KEY0_PRES: 下一个视频
  6. *                    KEY2_PRES: 上一个视频
  7. *                    其他值   : 错误代码
  8. */
  9. static uint8_t video_play_mjpeg(uint8_t *pname)
  10. {
  11.     uint8_t *framebuf;                      /* 视频解码buf */
  12.     uint8_t *pbuf;                          /* buf指针 */
  13.     uint8_t res = 0;
  14.     uint16_t offset;
  15.     uint32_t nr;
  16.     uint8_t key;
  17.     FIL *favi;

  18.     void *audiobuf[2];              /* 音频解码buf */
  19.     void *audio_buffer = NULL;

  20.     es8388_adda_cfg(1, 0);          /* 打开DAC,关闭ADC */
  21.     es8388_input_cfg(0);            /* 录音关闭 */
  22.     es8388_output_cfg(1, 1);        /* 喇叭通道和耳机通道打开 */
  23.     es8388_hpvol_set(20);           /* 设置喇叭 */
  24.     es8388_spkvol_set(20);          /* 设置耳机 */
  25.     vTaskDelay(pdMS_TO_TICKS(20));

  26.     framebuf =  heap_caps_malloc(AVI_VIDEO_BUF_SIZE, MALLOC_CAP_DMA);
  27.     audiobuf[0] = heap_caps_malloc(AVI_AUDIO_BUF_SIZE, MALLOC_CAP_SPIRAM);
  28.     audiobuf[1] = heap_caps_malloc(AVI_AUDIO_BUF_SIZE, MALLOC_CAP_SPIRAM);
  29.     audio_buffer = audiobuf[0];

  30. favi = (FIL *)malloc(sizeof(FIL));         /* 申请favi内存 */
  31. /* 只要最后这个视频buf申请失败, 前面的申请失不失败都不重要, 总之就是失败了 */
  32. if ((framebuf == NULL) || (favi == NULL) || (audiobuf[0] == NULL)
  33. || (audiobuf[1] == NULL))     
  34.     {
  35.         ESP_LOGI("videoplay", "memory error!");
  36.         res = 0xFF;
  37.     }

  38.     memset(framebuf,    0, AVI_VIDEO_BUF_SIZE);
  39.     memset(audiobuf[0], 0, AVI_AUDIO_BUF_SIZE);
  40.     memset(audiobuf[1], 0, AVI_AUDIO_BUF_SIZE);

  41.     while (res == 0)
  42.     {
  43.         res = (uint8_t)f_open(favi, (const TCHAR *)pname, FA_READ);            
  44.         if (res == 0)
  45.         {
  46.             pbuf = framebuf;
  47.             res = (uint8_t)f_read(favi, pbuf, AVI_VIDEO_BUF_SIZE, (UINT *)&nr);  
  48.             if (res != 0)
  49.             {
  50.                 ESP_LOGI(videoplay_tag, "fread error:%d", res);
  51.                 break;
  52.             }
  53.             
  54.             res = avi_init(pbuf, AVI_VIDEO_BUF_SIZE);   /* AVI解析 */
  55.             if (res != 0)
  56.             {
  57.                 ESP_LOGI(videoplay_tag, "avi error:%d", res);
  58.                 res = KEY0_PRES;
  59.                 break;
  60.             }
  61. /* 显示当前视频文件的相关信息(g_avix结构体在avi.c文件定义) */
  62.             video_info_show(&g_avix);               
  63. /* 初始化ESP_TIMER,用于等待帧间隔 */
  64.             frame_timer_init(g_avix.SecPerFrame);   

  65.             offset = avi_srarch_id(pbuf, AVI_VIDEO_BUF_SIZE, "movi");   
  66.             avi_get_streaminfo(pbuf + offset + 4);                     
  67.             f_lseek(favi, offset + 12); /* 跳过标志ID,读地址偏移到流数据开始处 */
  68.             
  69.             jpeg_rx_buf = mjpegdec_init(g_avix.Width, g_avix.Height,
  70. &jpeg_alloc_size);     /* MJPEG初始化 */

  71.             if (g_avix.SampleRate)              /* 有音频信息,才初始化 */
  72.             {
  73.                 myi2s_init();                   /* 初始化i2s */  
  74.                 vTaskDelay(pdMS_TO_TICKS(50));  /* 适当延时 */
  75.                 es8388_i2s_cfg(0, 3);           /* 飞利浦标准,16位数据长度 */
  76. /* 设置采样率和数据位宽 */
  77.                 i2s_set_samplerate_bits_sample(g_avix.SampleRate,
  78. I2S_BITS_PER_SAMPLE_16BIT);   
  79.                 i2s_trx_start();                /* I2S TRX启动 */
  80.             }

  81.             while (1)   /* 循环播放文件内容 */
  82.             {
  83.                 if (g_avix.StreamID == AVI_VIDS_FLAG)         /* 视频流 dc */
  84.                 {
  85.                     pbuf = framebuf;
  86.                     f_read(favi, pbuf, g_avix.StreamSize + 8, (UINT *)&nr);
  87.                     /* 解码一副JPEG图片 */
  88.                     res = mjpegdec_decode(pbuf, g_avix.StreamSize, jpeg_rx_buf,
  89. jpeg_alloc_size, g_avix.Width, g_avix.Height);  
  90.                     if (res != 0)
  91.                     {
  92.                         ESP_LOGI(videoplay_tag, "illegal frame decode error!");
  93.                     }
  94. /* 等待时间到达(在mytimer.c的中断里面设置为1) */
  95.                     while (g_frameup == 0);   
  96.                     g_frameup = 0;         /* 等待播放时间到达 */
  97.                     g_frame++;
  98.                 }
  99.                 else        /* wb 音频 */
  100.                 {
  101.                     audio_buffer = audiobuf[0] == audio_buffer ?
  102. audiobuf[1] : audiobuf[0];     /* 使用双缓冲 */

  103.                     if (g_avix.Width < lcddev.width)        /* 满屏不显示 */
  104.                     {
  105.                         video_time_show(favi, &g_avix);     /* 显示当前播放时间 */
  106.                     }

  107.                     f_read(favi, audio_buffer, g_avix.StreamSize+8,(UINT *)&nr);
  108.                     pbuf = audio_buffer;
  109.                     i2s_tx_write(audio_buffer, g_avix.StreamSize);              
  110.                 }

  111.                 key = xl9555_key_scan(0);
  112.                
  113.                 if (key == KEY0_PRES || key == KEY2_PRES)  
  114.                 {
  115.                     res = key;
  116.                     break;
  117.                 }
  118.                 else if (key == KEY1_PRES || key_scan(0))
  119.                 {
  120.                     i2s_trx_stop();     /* 关闭音频 */
  121.                     video_seek(favi, &g_avix, framebuf);
  122.                     pbuf = framebuf;
  123.                     i2s_trx_start();    /* 开启音频播放 */
  124.                 }
  125.                 if (avi_get_streaminfo(pbuf + g_avix.StreamSize) != 0)
  126.                 {
  127.                     ESP_LOGI(videoplay_tag, "g_frame error");
  128.                     res = KEY0_PRES;
  129.                     break;
  130.                 }
  131.             }
  132.             f_close(favi);                  /* 关闭文件 */
  133.             i2s_trx_stop();                 /* 关闭音频 */
  134.             i2s_deinit();                   /* I2S恢复到默认 */
  135.             mjpegdec_free(jpeg_rx_buf);     /* 卸载硬件JPEG驱动并释放内存 */
  136.             frame_timer_stop();             /* 停止定时器工作 */  
  137.         }
  138.     }
  139.     free(framebuf);
  140.     free(audiobuf[0]);
  141.     free(audiobuf[1]);
  142.     free(favi);
  143.     return res;
  144. }
复制代码
该函数用来播放一个avi视频文件(mjpg编码),首先我们对AVI文件进行解析,判断该文件是否是AVI文件,然后读取该文件的图像数据,接着将图像数据传输至硬件JPEG解码器进行解码,最后得到显示在LCD屏幕上。

3,CMakeLists.txt文件
本例程的功能实现主要依靠VIDEO驱动。要在main函数中,成功调用VIDEO文件中的内容,就得需要修改main文件夹下的CMakeLists.txt文件,修改如下:

  1. idf_component_register(
  2.     SRC_DIRS
  3.         "."
  4.         "APP"
  5.         "APP/AUDIO"
  6.         "APP/VIDEO"
  7.     INCLUDE_DIRS
  8.         "."
  9.         "APP"
  10.         "APP/AUDIO"
  11.         "APP/VIDEO"
  12.         )
复制代码

4,main.c代码
在main.c里面编写如下代码。

  1. void app_main(void)
  2. {
  3.     esp_err_t ret;
  4.     uint8_t key;

  5.     ret = nvs_flash_init();         /* 初始化NVS */

  6.     if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
  7.     {
  8.         ESP_ERROR_CHECK(nvs_flash_erase());
  9.         ESP_ERROR_CHECK(nvs_flash_init());
  10.     }

  11.     led_init();                         /* LED初始化 */
  12.     key_init();                         /* KEY初始化 */
  13.     myiic_init();                       /* MYIIC初始化 */
  14.     xl9555_init();                      /* XL9555初始化 */
  15.     lcd_init();                         /* LCD屏初始化 */

  16.     es8388_init();                      /* ES8388初始化 */
  17.     xl9555_pin_write(SPK_EN_IO, 0);     /* 打开喇叭 */

  18.     while (sdmmc_init())            /* 检测不到SD卡 */
  19.     {
  20.         lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
  21.         vTaskDelay(pdMS_TO_TICKS(200));
  22.         lcd_fill(30, 110, 239, 126, WHITE);
  23.         vTaskDelay(pdMS_TO_TICKS(200));
  24.     }

  25.     ret = exfuns_init();            /* 为fatfs相关变量申请内存 */

  26.     while (fonts_init())            /* 检查字库 */
  27.     {
  28.         lcd_clear(WHITE);
  29.         lcd_show_string(30, 30, 200, 16, 16, "ESP32-P4", RED);
  30.         
  31.         key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);  /* 更新字库 */

  32.         while (key)         /* 更新失败 */
  33.         {
  34.             lcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
  35.             vTaskDelay(pdMS_TO_TICKS(200));
  36.             lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
  37.             vTaskDelay(pdMS_TO_TICKS(200));
  38.         }

  39.         lcd_show_string(30, 50, 200, 16, 16, "Font Update Success!   ", RED);
  40.         vTaskDelay(pdMS_TO_TICKS(1000));
  41.         lcd_clear(WHITE);
  42.     }

  43.     text_show_string(30, 50,  200, 16, "正点原子ESP32-P4开发板",16,0, RED);
  44.     text_show_string(30, 70,  200, 16, "视频播放器 实验", 16, 0, RED);
  45.     text_show_string(30, 90,  200, 16, "正点原子@ALIENTEK", 16, 0, RED);
  46.     text_show_string(30, 110, 200, 16, "KEY0:NEXT KEY2:PREV", 16, 0, RED);
  47.     text_show_string(30, 130, 200, 16, "BOOT:FF   KEY1:REW ", 16, 0, RED);

  48. vTaskDelay(pdMS_TO_TICKS(1000));

  49.     frame_rate_timer();     /* 定时器初始化,定时时间为1秒,主要用于获取帧率 */

  50.     while (1)
  51.     {
  52.         video_play();
  53.     }
  54. }
复制代码
该函数就相对简单了,在初始化各个外设后,通过 video_play 函数,开始音频播放,到这里本实验的代码基本就编写完成了。

45.4 下载验证
本章,我们例程仅支持MJPG编码的avi格式视频,且音频必须是PCM格式,另外视频分辨率不能大于LCD分辨率。要满足这些要求,现成的avi文件是很难找到的,所以我们需要用软件,将通用视频(任何视频都可以)转换为我们需要的格式,这里我们通过:狸窝全能视频转换器,这款软件来实现(路径:光盘:6,软件资料软件视频转换软件狸窝全能视频转换器.exe)。安装完后,打开,然后进行相关设置,软件设置如图45.4.1和45.4.2所示:

第四十五章 视频播放器实验26448.png
图45.4.1 软件启动界面和设置

第四十五章 视频播放器实验26468.png
图45.4.2 高级设置

首先,如图45.4.1所示,点击1处,添加视频,找到你要转换的视频,添加进来。有的视频可能有独立字幕,比如我们打开的这个视频就有,所以在2处选择下字幕(如果没有的,可以忽略此步)。然后在3处,点击▼图标,选择预制方案:AVI-Audio-Video Interleaved(*.avi),即生成.avi文件,然后点击4处的高级设置按钮,进入45.4.2所示的界面,设置详细参数如下:
视频编码器:选择MJPEG。本例程仅支持MJPG视频解码,所以选择这个编码器。
视频尺寸:480x272。这里得根据所用LCD分辨率来选择,假设我们用800*480的4.3寸电容屏模块,则这里最大可以设置:272x480。PS:如果是2.8屏,最大宽度只能是240)。
比特率:1000。这里设置越大,视频质量越好,解码就越慢(可能会卡),我们设置为1000,可以得到比较好的视频质量,同时也不怎么会卡。
帧率:10。即每秒钟10帧。对于480*272的视频,本例程最高能播放30帧左右的视频,如果要想提高帧率,有几个办法:1,降低分辨率;2,降低比特率;3,降低音频采样率。
音频编码器:PCMS16LE。本例程只支持PCM音频,所以选择音频编码器为这个。
采样率:这里设置为11025,即11.025Khz的采样率。这里越高,声音质量越好,不过,转换后的文件就越大,而且视频可能会卡。
其他设置,采用默认的即可。设置完以后,点击确定,即可完成设置。
点击图45.4.1的5处文件夹图标,设置转换后视频的输出路径,这里设置到了桌面,这样转换后的视频,会保存在桌面。最后,点击图中6处的按钮,即可开始转换了,如下图所示:


第四十五章 视频播放器实验27187.png
图45.4.3 正在转换

等转换完成后,将转换后的.avi文件,拷贝到SD卡→VIDEO文件夹下,然后插入开发板的SD卡接口,就可以开始测试本章例程了。
将程序下载到开发板后,程序先检测字库,只有字库已经更新才可以继续执行后面的程序。字库已更新,就可以看到LCD首先显示一些实验相关的信息,如下图所示。


第四十五章 视频播放器实验27342.png
图45.4.4显示实验相关信息

显示了上图的信息后,检测SD卡的VIDEO文件夹,并查找avi视频文件,在找到有效视频文件后,便开始播放视频,如下图所示。

第四十五章 视频播放器实验27422.png
图45.4.5 视频播放中

可以看到,屏幕显示了文件名、索引、声道数、采样率、帧率和播放时间等参数。然后,我们按KEY0/KEY1,可以切换到下一个/上一个视频,按KEY2,可以快进。
至此,本例程介绍就结束了。本实验,我们在开发板上实现了视频播放,体现了ESP32-P4强大的处理能力。

回复

使用道具 举报

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

本版积分规则


关闭

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

正点原子公众号

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

GMT+8, 2026-2-18 13:21

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

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