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)。