图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驱动文件。
1,MJPEG驱动
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MJPEG驱动源码包括四个文件:avi.c、avi.h、mjpeg.c和mjpeg.h。
avi.h头文件在43.1小节部分讲过,具体请看源码。下面来看到avi.c文件,这里总共有三个函数都很重要,首先介绍AVI解码初始化函数,该函数定义如下:
/* avi文件相关信息 */
AVI_INFO g_avix;
/* 视频编码标志字符串,00dc/01dc */
char *const AVI_VIDS_FLAG_TBL[2] = {"00dc", "01dc"};
/* 音频编码标志字符串,00wb/01wb */
char *const AVI_AUDS_FLAG_TBL[2] = {"00wb", "01wb"};
/**
* @brief avi解码初始化
* @param buf : 输入缓冲区
* @param size : 缓冲区大小
* @retval res
* @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错误 */
}
g_avix.SecPerFrame = avihheader->SecPerFrame; /* 得到帧间隔时间 */
g_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) /* 视频帧在前 */
{
if (strhheader->Handler != AVI_FORMAT_MJPG)
{
return AVI_FORMAT_ERR; /* 非MJPG视频流,不支持 */
}
g_avix.VideoFLAG = AVI_VIDS_FLAG_TBL[0]; /* 视频流标记 "00dc" */
g_avix.AudioFLAG = AVI_AUDS_FLAG_TBL[1]; /* 音频流标记 "01wb" */
bmpheader = (STRF_BMPHEADER *)(buf + 12 + strhheader->BlockSize + 8);
if (bmpheader->BlockID != AVI_STRF_ID)
{
return AVI_STRF_ERR; /* STRF ID错误 */
}
g_avix.Width = bmpheader->bmiHeader.Width;
g_avix.Height = bmpheader->bmiHeader.Height;
buf += listheader->BlockSize + 8; /* 偏移 */
listheader = (LIST_HEADER *)(buf);
if (listheader->ListID != AVI_LIST_ID) /* 是不含有音频帧的视频文件 */
{
g_avix.SampleRate = 0; /* 音频采样率 */
g_avix.Channels = 0; /* 音频通道数 */
g_avix.AudioType = 0; /* 音频格式 */
}
else
{
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_AUDS_STREAM)
{
return AVI_FORMAT_ERR; /* 格式错误 */
}
wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize+8);
if (wavheader->BlockID != AVI_STRF_ID)
{
return AVI_STRF_ERR; /* STRF ID错误 */
}
g_avix.SampleRate = wavheader->SampleRate; /* 音频采样率 */
g_avix.Channels = wavheader->Channels; /* 音频通道数 */
g_avix.AudioType = wavheader->FormatTag; /* 音频格式 */
}
}
else if (strhheader->StreamType == AVI_AUDS_STREAM)/* 音频帧在前 */
{
g_avix.VideoFLAG = AVI_VIDS_FLAG_TBL[1]; /* 视频流标记 "01dc" */
g_avix.AudioFLAG = AVI_AUDS_FLAG_TBL[0]; /* 音频流标记 "00wb" */
wavheader = (STRF_WAVHEADER *)(buf + 12 + strhheader->BlockSize + 8);
if (wavheader->BlockID != AVI_STRF_ID)
{
return AVI_STRF_ERR; /* STRF ID错误 */
}
g_avix.SampleRate = wavheader->SampleRate; /* 音频采样率 */
g_avix.Channels = wavheader->Channels; /* 音频通道数 */
g_avix.AudioType = wavheader->FormatTag; /* 音频格式 */
buf += listheader->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)
{
return AVI_FORMAT_ERR; /* 格式错误 */
}
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; /* 格式错误 */
}
g_avix.Width = bmpheader->bmiHeader.Width;
g_avix.Height = bmpheader->bmiHeader.Height;
}
offset = avi_srarch_id(tbuf, size, "movi"); /* 查找movi ID */
if (offset == 0)
{
return AVI_MOVI_ERR; /* MOVI ID错误 */
}
if (g_avix.SampleRate) /* 有音频流,才查找 */
{
tbuf += offset;
offset = avi_srarch_id(tbuf, size, g_avix.AudioFLAG); /* 查找音频流标记 */
if (offset == 0)
{
return AVI_STREAM_ERR; /* 流错误 */
}
tbuf += offset + 4;
g_avix.AudioBufSize = *((uint16_t *)tbuf); /* 得到音频流buf大小 */
}
printf("avi init ok\r\n");
printf("g_avix.SecPerFrame:%ld\r\n", g_avix.SecPerFrame);
printf("g_avix.TotalFrame:%ld\r\n", g_avix.TotalFrame);
printf("g_avix.Width:%ld\r\n", g_avix.Width);
printf("g_avix.Height:%ld\r\n", g_avix.Height);
printf("g_avix.AudioType:%d\r\n", g_avix.AudioType);
printf("g_avix.SampleRate:%ld\r\n", g_avix.SampleRate);
printf("g_avix.Channels:%d\r\n", g_avix.Channels);
printf("g_avix.AudioBufSize:%d\r\n", g_avix.AudioBufSize);
printf("g_avix.VideoFLAG:%s\r\n", g_avix.VideoFLAG);
printf("g_avix.AudioFLAG:%s\r\n", g_avix.AudioFLAG);
return res;
}
该函数用于解析AVI文件,获取音视频流数据的详细信息,为后续解码做准备。
接下来介绍的是查找 ID函数,其定义如下:
/**
* @brief 查找 ID
* @param buf : 待查缓存区
* @param size : 缓存大小
* @param id : 要查找的id,必须是4字节长度
* @retval 0,接收应答失败
* 其他:movi ID偏移量
*/
uint16_t avi_srarch_id(uint8_t *buf, uint32_t size, char *id)
{
uint32_t i;
uint32_t idsize = 0;
size -= 4;
for (i = 0; i < size; i++)
{
if ((buf == id[0]) &&
(buf[i + 1] == id[1]) &&
(buf[i + 2] == id[2]) &&
(buf[i + 3] == id[3]))
{
idsize = MAKEDWORD(buf + i + 4);
/* 得到帧大小,必须大于16字节,才返回,否则不是有效数据 */
if (idsize > 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)
{
g_avix.StreamID = MAKEWORD(buf + 2); /* 得到流类型 */
g_avix.StreamSize = MAKEDWORD(buf + 4); /* 得到流大小 */
if (g_avix.StreamSize > AVI_MAX_FRAME_SIZE) /* 帧大小太大了,直接返回错误 */
{
printf("FRAME SIZE OVER:%d\r\n", g_avix.StreamSize);
g_avix.StreamSize = 0;
return AVI_STREAM_ERR;
}
if (g_avix.StreamSize % 2)
{
g_avix.StreamSize++; /* 奇数加1(g_avix.StreamSize,必须是偶数) */
}
if (g_avix.StreamID == AVI_VIDS_FLAG || g_avix.StreamID == AVI_AUDS_FLAG)
{
return AVI_OK;
}
return AVI_STREAM_ERR;
}
该函数用来获取当前数据流信息,重点是取得流类型和流大小,方便解码和读取下一个数据流。
mjpeg.h文件只有一些函数和变量声明,接下来,介绍mjpeg.c里面的几个函数,首先是初始化MJPEG解码数据源的函数,其定义如下:
/**
* @brief mjpeg 解码初始化
* @param offx,offy:x,y方向的偏移
* @retval 0,成功; 1,失败
*/
char mjpegdec_init(uint16_t offx, uint16_t offy)
{
cinfo = (struct jpeg_decompress_struct *)
malloc(sizeof(struct jpeg_decompress_struct));
jerr = (struct my_error_mgr *)malloc(sizeof(struct my_error_mgr));
if (cinfo == NULL || jerr == NULL)
{
printf("[E][mjpeg.cpp] mjpegdec_init():
malloc failed to apply for memory\r\n");
mjpegdec_free();
return -1;
}
/* 保存图像在x,y方向的偏移量 */
imgoffx = offx;
imgoffy = offy;
return 0;
}
该函数用于初始化jpeg解码,主要是申请内存,然后确定视频在液晶上面的偏移(让视频显示在SPILCD中央)。
下面介绍的是MJPEG释放所有申请的内存函数,其定义如下:
/**
* @brief mjpeg结束,释放内存
* @param 无
* @retval 无
*/
void mjpegdec_free(void)
{
free(cinfo);
free(jerr);
}
该函数用于释放内存,解码结束后调用。
下面介绍的是解码一副JPEG图片函数,其定义如下:
/**
* @brief 解码一副JPEG图片
* @param buf: jpeg数据流数组
* @param bsize: 数组大小
* @retval 0,成功; 1,失败
*/
uint8_t mjpegdec_decode(uint8_t* buf, uint32_t bsize)
{
JSAMPARRAY buffer;
if (bsize == 0) return 1;
int row_stride = 0;
int j = 0; /* 记录当前解码的行数 */
int lineR = 0; /* 每一行R分量的起始位置 */
cinfo->err = jpeg_std_error(&jerr->pub);
jerr->pub.error_exit = my_error_exit;
jerr->pub.emit_message = my_emit_message;
cinfo->out_color_space = JCS_RGB;
if (setjmp(jerr->setjmp_buffer)) /* 错误处理 */
{
jpeg_abort_decompress(cinfo);
jpeg_destroy_decompress(cinfo);
return 2;
}
jpeg_create_decompress(cinfo);
jpeg_mem_src(cinfo, buf, bsize); /* 测试正常 */
jpeg_read_header(cinfo, TRUE);
jpeg_start_decompress(cinfo);
row_stride = cinfo->output_width * cinfo->output_components;
/* 计算buffer大小并申请相应空间 */
buffer = (*cinfo->mem->alloc_sarray)
((j_common_ptr)cinfo, JPOOL_IMAGE, row_stride, 1);
while (cinfo->output_scanline < cinfo->output_height)
{
int i = 0;
jpeg_read_scanlines(cinfo, buffer, 1);
unsigned short tmp_color565;
/* 为上述图像数据赋值 */
for (int k = 0; k < Windows_Width * 2; k += 2)
{
tmp_color565 = rgb565(buffer[0],
buffer[0][i + 1],
buffer[0][i + 2]);
lcd_buf[lineR + k] = (tmp_color565 & 0xFF00) >> 8;
lcd_buf[lineR + k + 1] = tmp_color565 & 0x00FF;
i += 3;
}
j++;
lineR = j * Windows_Width * 2;
}
lcd_set_window(imgoffx,
imgoffy - 30,
imgoffx + cinfo->output_width - 1,
imgoffy - 30 + cinfo->output_height - 1);
taskENTER_CRITICAL(&my_spinlock);
/* 例如:96*96*2/1536 = 12;分12次发送RGB数据 */
for(int x = 0;
x < (cinfo->output_width * cinfo->output_height * 2 / LCD_BUF_SIZE);
x++)
{
/* &lcd_buf[j * LCD_BUF_SIZE] 偏移地址发送数据 */
lcd_write_data(&lcd_buf[x * LCD_BUF_SIZE] , LCD_BUF_SIZE);
}
taskEXIT_CRITICAL(&my_spinlock);
lcd_set_window(0, 0, lcd_self.width, lcd_self.height); /* 恢复窗口 */
jpeg_finish_decompress(cinfo);
jpeg_destroy_decompress(cinfo);
return 0;
}
该函数是解码jpeg的主要函数,通过前面43.1.2节介绍过的步骤进行解码,该函数的参数buf指向内存里面的一帧jpeg数据,bsize是数据大小。
2,APP驱动
videoplayer.h头文件有两个宏定义和函数声明,具体请看源码。下面来看到videoplayer.c文件中,播放一个MJPEG文件函数,其定义如下:
/**
* @brief 播放MJPEG视频
* @param pname: 视频文件名
* @retval 按键键值
* KEY2_PRES: 上一个视频
* KEY0_PRES: 下一个视频
* 其他值 : 错误代码
*/
static uint8_t video_play_mjpeg(uint8_t *pname)
{
uint8_t *framebuf;
uint8_t *pbuf;
uint8_t res = 0;
uint16_t offset;
uint32_t nr;
uint8_t key;
FIL *favi;
/* 申请内存 */
framebuf = (uint8_t *)malloc(AVI_VIDEO_BUF_SIZE);
favi = (FIL *)malloc(sizeof(FIL));
if ((framebuf == NULL) || (favi == NULL))
{
printf("memory error!\r\n");
res = 0xFF;
}
memset(framebuf, 0, AVI_VIDEO_BUF_SIZE);
while (res == 0)
{
/* 打开文件 */
res = (uint8_t)f_open(favi, (const TCHAR *)pname, FA_READ);
if (res == 0)
{
pbuf = framebuf;
/* 开始读取 */
res = (uint8_t)f_read(favi, pbuf, AVI_VIDEO_BUF_SIZE, (UINT*)&nr);
if (res != 0)
{
printf("fread error:%d\r\n", res);
break;
}
/* AVI解析 */
res = avi_init(pbuf, AVI_VIDEO_BUF_SIZE);
if (res != 0)
{
printf("avi error:%d\r\n", res);
break;
}
video_info_show(&g_avix);
esptim_int_init(g_avix.SecPerFrame / 1000, 1000);
/* 寻找movi ID */
offset = avi_srarch_id(pbuf, AVI_VIDEO_BUF_SIZE, "movi");
/* 获取流信息 */
avi_get_streaminfo(pbuf + offset + 4);
/* 跳过标志ID,读地址偏移到流数据开始处 */
f_lseek(favi, offset + 12);
/* 初始化JPG解码 */
res = mjpegdec_init((lcd_self.width - g_avix.Width) / 2,
110+(lcd_self.height-110-g_avix.Height)/2);
/* 定义图像的宽高 */
Windows_Width = g_avix.Width;
Windows_Height = g_avix.Height;
/* 有音频信息,才初始化 */
if (g_avix.SampleRate)
{
printf("g_avix.SampleRate:%ld\r\n",g_avix.SampleRate);
/* 飞利浦标准,16位数据长度 */
es8388_sai_cfg(0, 3);
/* 设置采样率 */
i2s_set_samplerate_bits_sample(g_avix.SampleRate,
I2S_BITS_PER_SAMPLE_16BIT);
i2s_start(I2S_NUM);
}
while (1)
{
/* 视频流 */
if (g_avix.StreamID == AVI_VIDS_FLAG)
{
pbuf = framebuf;
/* 读取整帧+下一帧数据流ID信息 */
f_read(favi, pbuf, g_avix.StreamSize + 8, (UINT*)&nr);
res = mjpegdec_decode(pbuf, g_avix.StreamSize);
if (res != 0)
{
printf("decode error!\r\n");
}
/* 等待播放时间到达 */
while (frameup == 0);
frameup = 0;
}
else
{
/* 显示当前播放时间 */
video_time_show(favi, &g_avix);
/* 填充psaibuf */
f_read(favi, framebuf, g_avix.StreamSize + 8, &nr);
pbuf = framebuf;
/* 数据转换+发送给DAC */
i2s_tx_write(framebuf, g_avix.StreamSize);
}
key = xl9555_key_scan(0);
/* KEY0/KEY2按下,播放下一个/上一个视频 */
if (key == KEY0_PRES || key == KEY2_PRES)
{
res = key;
break;
}
else if (key == KEY1_PRES || key == KEY3_PRES)
{
/* 关闭音频 */
i2s_stop(I2S_NUM);
video_seek(favi, &g_avix, framebuf);
pbuf = framebuf;
/* 开启DMA播放 */
i2s_start(I2S_NUM);
}
/* 读取下一帧流标志 */
if (avi_get_streaminfo(pbuf + g_avix.StreamSize) != 0)
{
printf("g_frame error\r\n");
res = KEY0_PRES;
break;
}
}
i2s_stop(I2S_NUM);
esp_timer_stop(esp_tim_handle);
/* 恢复窗口 */
lcd_set_window(0, 0, lcd_self.width, lcd_self.height);
/* 释放内存 */
mjpegdec_free();
/* 关闭文件 */
f_close(favi);
}
}
i2s_zero_dma_buffer(I2S_NUM);
free(framebuf);
free(favi);
return res;
}
该函数用来播放一个avi视频文件(MJPEG编码),解码过程就是根据前面我们在43.1节最后所介绍的步骤进行。其他代码,我们就不介绍了,请大家参考本例程源码。
43.3.4 CMakeLists.txt文件
打开本实验BSP下的CMakeLists.txt文件,其内容如下所示:
set(src_dirs
IIC
LCD
LED
SDIO
SPI
XL9555
ESPTIM
ES8388
I2S)
set(include_dirs
IIC
LCD
LED
SDIO
SPI
XL9555
ESPTIM
ES8388
I2S)
set(requires
driver
fatfs
esp_timer)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
上述的红色ESPTIM驱动以及esp_timer依赖库需要由开发者自行添加,以确保视频播放驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了视频播放驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。
打开本实验main文件下的CMakeLists.txt文件,其内容如下所示:
idf_component_register(
SRC_DIRS
"."
"app"
INCLUDE_DIRS
"."
"app")
上述的红色app驱动需要由开发者自行添加,在此便不做赘述了。
43.3.5 实验应用代码
打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。
i2c_obj_t i2c0_master;
/**
* @brief 程序入口
* @param 无
* @retval 无
*/
void app_main(void)
{
esp_err_t ret = 0;
uint8_t key = 0;
/* 初始化NVS */
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
/* 初始化LED */
led_init();
/* 初始化IIC0 */
i2c0_master = iic_init(I2C_NUM_0);
/* 初始化SPI */
spi2_init();
/* 初始化IO扩展芯片 */
xl9555_init(i2c0_master);
/* 初始化LCD */
lcd_init();
/* ES8388初始化 */
es8388_init(i2c0_master);
/* 开启DAC关闭ADC */
es8388_adda_cfg(1, 0);
es8388_input_cfg(0);
/* DAC选择通道输出 */
es8388_output_cfg(1, 1);
/* 设置耳机音量 */
es8388_hpvol_set(20);
/* 设置喇叭音量 */
es8388_spkvol_set(20);
/* I2S初始化 */
i2s_init();
vTaskDelay(1000);
/* 打开喇叭 */
xl9555_pin_write(SPK_EN_IO,0);
/* 检测不到SD卡 */
while (sd_spi_init())
{
lcd_show_string(30, 110, 200, 16, 16, "SD Card Error!", RED);
vTaskDelay(500);
lcd_show_string(30, 130, 200, 16, 16, "Please Check! ", RED);
vTaskDelay(500);
}
/* 检查字库 */
while (fonts_init())
{
/* 清屏 */
lcd_clear(WHITE);
lcd_show_string(30, 30, 200, 16, 16, "ESP32-S3", RED);
/* 更新字库 */
key = fonts_update_font(30, 50, 16, (uint8_t *)"0:", RED);
/* 更新失败 */
while (key)
{
lcd_show_string(30, 50, 200, 16, 16, "Font Update Failed!", RED);
vTaskDelay(200);
lcd_fill(20, 50, 200 + 20, 90 + 16, WHITE);
vTaskDelay(200);
}
lcd_show_string(30, 50, 200, 16, 16, "Font Update Success! ", RED);
vTaskDelay(1500);
/* 清屏 */
lcd_clear(WHITE);
}
/* 为fatfs相关变量申请内存 */
ret = exfuns_init();
text_show_string(30, 30, 200, 16, "正点原子ESP32开发板", 16, 0, RED);
text_show_string(30, 50, 200, 16, "视频播放器实验", 16, 0, RED);
text_show_string(30, 70, 200, 16, "正点原子@ALIENTEK", 16, 0, RED);
text_show_string(30, 90, 200, 16, "KEY0:NEXT KEY2:PREV ", 16, 0, RED);
text_show_string(30, 110, 200, 16, "KEY_UP:FF KEY1:REW", 16, 0, RED);
/* 实验信息显示延时 */
vTaskDelay(500);
while (1)
{
video_play();
}
}
main函数只是经过一系列的外设初始化后,检查字库是否已经更新,然后显示实验的信息,就通过调用video_play函数,执行视频播放的程序了。
43.4 下载验证
本章,我们例程仅支持MJPEG编码的avi格式视频,且音频必须是PCM格式,另外视频分辨率不能大于LCD分辨率。要满足这些要求,现成的avi文件是很难找到的,所以我们需要用软件,将通用视频(任何视频都可以)转换为我们需要的格式,这里我们通过:狸窝全能视频转换器,这款软件来实现(路径:光盘:6,软件资料à1,软件à7,其他软件.zipà视频转换软件à狸窝全能视频转换器.exe)。安装完后,打开,然后进行相关设置,软件设置如图43.4.1和43.4.2所示:
图43.4.1 软件启动界面和设置
图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所示:
图43.4.3 正在转换
等转换完成后,将转换后的.avi文件,拷贝到SD卡àVIDEO文件夹下,然后插入开发板的SD卡接口,就可以开始测试本章例程了。
将程序下载到开发板后,程序先检测字库,只有字库已经更新才可以继续执行后面的程序。字库已更新,就可以看到LCD首先显示一些实验相关的信息,如图43.4.4所示:
图43.4.4显示实验相关信息
显示了上图的信息后,检测SD卡的VIDEO文件夹,并查找avi视频文件,在找到有效视频文件后,便开始播放视频,如图43.4.5所示:
图43.4.5 视频播放中
可以看到,屏幕显示了文件名、索引、声道数、采样率、帧率和播放时间等参数。然后,我们按KEY0/KEY2,可以切换到下一个/上一个视频,按KEY_UP/KEY1,可以快进/快退。
至此,本例程介绍就结束了。本实验,我们在开发板上实现了视频播放,体现了DNESP32S3强大的处理能力。
附本实验测试结果(视频比特率:1000,音频均为:110250,立体声)
对 240*160/240*180分辨率,可达30帧
对 320*240分辨率,可达20帧
对 480*272分辨率,可达10帧
最后提醒大家,转换的视频分辨率,一定要根据自己的SPILCD设置,不能超过SPILCD的尺寸!!否则无法播放(可能只听到声音,看不到图像)。