ESP32-P4 MJPEG视频播放器开发实战:从摄像头到SD卡的完整解决方案 项目背景
本文记录了在ESP32-P4开发板(配ST7703 LCD屏幕)上,将摄像头视频采集改为SD卡MJPEG视频播放的完整开发过程。整个过程历经多次技术选型和问题排查,最终实现了稳定的24fps多视频轮播系统。
开发环境:
芯片:ESP32-P4
屏幕:ST7703 MIPI-DSI (720x720)
ESP-IDF:v5.5.1
视频格式:MJPEG (480x480 @ 24fps)
第一阶段:技术选型与初步实现 1.1 文件格式选择
初始方案:AVI容器 + MJPEG编码
最初选择了AVI容器格式,理由如下:
成熟的格式,有现成的解析库
包含完整的元数据(分辨率、帧率等)
可以直接从已有AVI文件读取
遇到的第一个问题:AVI文件解析
实现了基于内存搜索的AVI解析器:
// 搜索"movi"标识定位数据区
uint32_t movi_offset = search_fourcc(header_buf, read_size, "movi");
// 逐帧读取00dc chunk
while (fread(chunk_header, 1, 8, fp) == 8) {
if (chunk_id == 0x63643030) { // "00dc"
// 读取JPEG帧数据
fread(jpeg_data, 1, chunk_size, fp);
}
}
这部分基本顺利,能正确提取JPEG帧数据。
1.2 JPEG硬件解码器集成
ESP32-P4内置硬件JPEG解码器,理论性能很高。按照官方文档配置:
// 创建解码器引擎
jpeg_decode_engine_cfg_t decode_eng_cfg = {
.intr_priority = 0,
.timeout_ms = 40,
};
ESP_ERROR_CHECK(jpeg_new_decoder_engine(&decode_eng_cfg, &decoder_handle));
// 分配输入/输出缓冲区
jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
};
output_buf = jpeg_alloc_decoder_mem(width * height * 3, &rx_mem_cfg, &size);
第二阶段:问题爆发 - 解码失败与色块 2.1 现象描述
运行后出现以下问题:
每帧都超时:ESP_ERR_TIMEOUT
输出数据全0:即使out_size正确,但buffer内容是全0
屏幕显示规则色块/网格:绿色、紫色、粉色相间的马赛克
关键日志:
E (6392) jpeg.decoder: jpeg_decoder_process timeout
I (6392) video_player: Decoded frame #1 output data:
I (6392) video_player: 00 00 00 00 00 00 00 00 00 00 00 00 ...
W (6392) video_player: JPEG decode timeout but data complete (out:691200 bytes)
2.2 问题排查过程
猜测1:输入JPEG数据有问题?
验证JPEG数据完整性:
// 检查JPEG头尾标记
if (jpeg_data[0] == 0xFF && jpeg_data[1] == 0xD8 &&
jpeg_data[size-2] == 0xFF && jpeg_data[size-1] == 0xD9) {
ESP_LOGI(TAG, "✓ JPEG frame is complete");
}
结果:✅ JPEG数据完整正确
猜测2:RGB字节序不对?
尝试切换 JPEG_DEC_RGB_ELEMENT_ORDER_BGR 和 RGB。 结果:❌ 无效,仍然是色块
猜测3:YUV色彩空间转换问题?
添加YUV到RGB转换配置:
.conv_std = JPEG_YUV_RGB_CONV_STD_BT601,
结果:❌ 无效
猜测4:Cache一致性问题?
这是问题的核心!尝试了多种Cache同步方案:
// 输入:CPU写入后,刷新到内存
esp_cache_msync(input_buf, size, ESP_CACHE_MSYNC_FLAG_DIR_C2M);
// 输出:DMA写入后,失效CPU cache
esp_cache_msync(output_buf, size, ESP_CACHE_MSYNC_FLAG_DIR_M2C);
结果:各种对齐错误,数据仍然全0
2.3 对比测试:单张照片 vs 视频
关键发现:
✅ 单张JPEG照片能正常解码显示
❌ AVI视频每帧都失败
对比代码发现:
照片测试:不调用任何Cache同步,却能正常工作
视频播放:添加了各种Cache同步,反而失败
结论:问题不在Cache同步本身,而在AVI容器格式的连续解码上。
第三阶段:转折点 - 切换到纯MJPEG格式 3.1 发现参考代码
找到乐鑫官方的MJPEG播放示例,使用的是纯MJPEG格式(不是AVI容器):
纯MJPEG格式:
[FF D8 ... FF D9][FF D8 ... FF D9][FF D8 ... FF D9]...
JPEG帧1 JPEG帧2 JPEG帧3
AVI容器格式:
[AVI Header][LIST movi]
[00dc][size][JPEG数据]
[00dc][size][JPEG数据]
3.2 视频格式转换
使用FFmpeg转换:
# 错误的方式(强制YUV422p)
ffmpeg -i input.avi -pix_fmt yuvj422p -f mjpeg output.mjpeg # ❌
# 正确的方式(让FFmpeg自动选择)
ffmpeg -i input.mp4 -q:v 3 -f mjpeg output.mjpeg # ✅
关键差异:
yuvj422p:某些YUV变体,ESP32-P4可能不完全兼容
自动选择:通常是yuv420p,标准格式,完全兼容
3.3 集成参考代码
复制官方的esp_mjpeg_decode组件:
typedef struct {
FILE *input;
uint8_t *mjpeg_buf;
uint8_t *output_buf;
jpeg_decoder_handle_t decoder_engine;
int16_t w, h;
// ...
} esp_mjpeg_decode_t;
// 读取一帧
esp_mjpeg_decode_read_mjpeg_buf(&mjpeg);
// 解码
esp_mjpeg_decode_jpg(&mjpeg);
// 显示
esp_lcd_panel_draw_bitmap(..., esp_mjpeg_decode_get_out_buf(&mjpeg));
结果:✅ 立即成功!视频正常播放,无超时,无色块!
第四阶段:性能优化 4.1 初始性能
使用纯MJPEG格式后:
帧率:16-18 FPS
瓶颈分析:
JPEG解码:~40ms
SD卡读取:~2ms
LCD刷新:~18ms
总计:~60ms = 16.7 FPS
4.2 关键优化:启用DMA2D
发现参考代码的LCD配置有一个关键参数:
esp_lcd_dpi_panel_config_t dpi_config = {
// ...
.flags.use_dma2d = true, // ★ 关键!
};
效果:帧率从 16fps 飙升到 70-82 FPS!
原理:
不启用DMA2D:CPU逐字节复制像素数据到LCD
启用DMA2D:硬件DMA直接传输,CPU只需触发
4.3 Cache配置优化
对比参考代码的sdkconfig,发现关键差异:
# 你的配置(失败时) CONFIG_CACHE_L2_CACHE_128KB=y CONFIG_CACHE_L2_CACHE_LINE_64B=y # 参考代码(成功) CONFIG_CACHE_L2_CACHE_256KB=y CONFIG_CACHE_L2_CACHE_LINE_128B=y
更大的Cache和Cache Line能提升DMA传输的稳定性。
4.4 SD卡速度优化
发现:不同SD卡速度差异巨大!
旧卡(SDSC):40 MHz → 16-18 fps
新卡(SDHC):52 MHz → 70-82 fps
教训:硬件性能对整体体验影响巨大,不要忽视SD卡的选择。
第五阶段:帧率精确控制 5.1 问题
全速播放是70-82 FPS,但源视频是24 FPS。如何精确控制到24fps?
失败的尝试1:固定延迟
vTaskDelay(pdMS_TO_TICKS(41)); // 固定延迟41ms
// 结果:18-19 FPS(太慢)
// 原因:FreeRTOS tick粒度问题,延迟不精确
失败的尝试2:动态延迟
elapsed_time = 实际处理时间;
delay = target_time - elapsed_time;
vTaskDelay(pdMS_TO_TICKS(delay));
// 结果:仍然18-19 FPS
// 原因:累积误差,每帧处理时间不同
5.2 成功的方案:固定时间间隔法
核心思想:基于绝对时间而非相对延迟
int64_t next_frame_time_us = esp_timer_get_time(); // 初始时间
int64_t frame_interval_us = 1000000 / 24; // 41667微秒
while (read_frame()) {
// 等待到预定时间
int64_t now = esp_timer_get_time();
int64_t wait_us = next_frame_time_us - now;
if (wait_us > 1000) {
vTaskDelay(pdMS_TO_TICKS(wait_us / 1000));
}
// 解码并显示
decode_and_display();
// 更新下一帧时间(累加,不是重新计算)
next_frame_time_us += frame_interval_us;
}
效果:帧率精确控制在 23.9-24.1 FPS,误差 < 0.5%
优点:
消除累积误差
自动补偿慢帧
基于高精度定时器(微秒级)
核心技术要点总结 1. 文件格式选择
格式 优点 缺点 推荐度
AVI容器 包含元数据 解析复杂,Cache问题 ⭐⭐
纯MJPEG 简单高效 无元数据 ⭐⭐⭐⭐⭐
转换命令:
ffmpeg -i video.mp4 -vf "scale=480:480" -r 24 -q:v 3 -f mjpeg video.mjpeg
注意:
✅ 使用 -f mjpeg 输出纯MJPEG
✅ 让FFmpeg自动选择色彩空间(通常是yuv420p)
❌ 不要强制 -pix_fmt yuvj422p(可能不兼容)
2. 内存分配
正确方式:
// 输入和输出都使用 jpeg_alloc_decoder_mem
jpeg_decode_memory_alloc_cfg_t tx_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_INPUT_BUFFER,
};
input_buf = jpeg_alloc_decoder_mem(jpeg_size, &tx_mem_cfg, &alloc_size);
jpeg_decode_memory_alloc_cfg_t rx_mem_cfg = {
.buffer_direction = JPEG_DEC_ALLOC_OUTPUT_BUFFER,
};
output_buf = jpeg_alloc_decoder_mem(w * h * bpp, &rx_mem_cfg, &alloc_size);
错误方式:
// ❌ 使用普通 heap_caps_malloc
input_buf = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_DMA);
// 可能导致DMA访问问题
3. Cache同步
关键结论:jpeg_alloc_decoder_mem 返回的内存是DMA-coherent的,不需要手动Cache同步!
如果你添加了 esp_cache_msync,反而可能导致问题:
C2M(Cache to Memory):会覆盖DMA写入的数据
M2C(Memory to Cache):可能有对齐错误
正确做法:什么都不做,让库自动处理。
4. LCD加速
必须启用DMA2D:
esp_lcd_dpi_panel_config_t dpi_config = {
// ...
.flags.use_dma2d = true, // ★ 关键配置
};
效果:帧率从16fps → 70+fps
5. 帧率控制
固定时间间隔法:
next_frame_time += frame_interval; // 基于绝对时间
wait_until(next_frame_time); // 等待到这个时间点
decode_and_display(); // 然后立即处理
优于动态延迟法(delay = target - elapsed)。