ESP32-CAM录制视频生成0KB空AVI文件问题求助
ESP32-CAM录制AVI文件体积0KB问题解决
问题根源
你的代码中writeAVIHeader、writeFrame、finalizeAVIFile三个核心函数为空实现,没有向SD卡文件写入任何实际数据,这是生成0KB无效文件的直接原因。AVI文件需要完整的文件头、帧数据和索引信息才能被播放器识别,缺失这些内容会导致文件无法被解析。
完整解决方案
以下是补全关键函数后的完整代码,适配AI-Thinker ESP32-CAM硬件配置,实现MJPEG格式的AVI视频录制:
#include "esp_camera.h" #include "FS.h" #include "SD_MMC.h" #include "EEPROM.h" // Pin definitions for AI-Thinker ESP32-CAM #define LED_PIN 33 // Camera settings #define CAMERA_FRAME_SIZE FRAMESIZE_QVGA #define CAMERA_QUALITY 10 #define FRAME_RATE 30 // 目标帧率 // SD card settings #define SD_CS_PIN 5 // AVI格式常量 #define AVI_HEADER_SIZE 544 #define FRAME_CHUNK_SIZE 8 typedef struct { char riff[4]; // "RIFF" uint32_t fileSize; // 文件总大小 - 8 char avi[4]; // "AVI " char list[4]; // "LIST" uint32_t listSize; // hdrl块大小 char hdrl[4]; // "hdrl" char avih[4]; // "avih" uint32_t avihSize; // avih块大小 (56) uint32_t usPerFrame; // 每帧时长(微秒) uint32_t maxBytesPerSec; // 最大每秒字节数 uint32_t padding; // 对齐方式(0) uint32_t flags; // 视频标志位 uint32_t totalFrames; // 总帧数 uint32_t initialFrames; // 初始帧数(0) uint32_t streams; // 流数量(1) uint32_t suggestedBufferSize; // 建议缓冲区大小 uint32_t width; // 视频宽度 uint32_t height; // 视频高度 uint32_t reserved[4]; // 保留字段 char list2[4]; // "LIST" uint32_t list2Size; // strl块大小 char strl[4]; // "strl" char strh[4]; // "strh" uint32_t strhSize; // strh块大小 (56) char fccType[4]; // "vids" char fccHandler[4]; // "MJPG" uint32_t flags; // 流标志位 uint16_t priority; // 优先级(0) uint16_t language; // 语言(0) uint32_t initialFrames; // 初始帧数(0) uint32_t scale; // 时间刻度(1) uint32_t rate; // 帧率(FRAME_RATE) uint32_t start; // 起始时间(0) uint32_t length; // 总帧数 uint32_t suggestedBufferSize; // 建议缓冲区大小 uint32_t quality; // 质量(0) uint32_t sampleSize; // 采样大小(0) int16_t frameLeft; // 帧左偏移(0) int16_t frameTop; // 帧上偏移(0) int16_t frameRight; // 帧右偏移(宽度) int16_t frameBottom; // 帧下偏移(高度) char strf[4]; // "strf" uint32_t strfSize; // strf块大小(40) uint32_t biSize; // BITMAPINFOHEADER大小(40) uint32_t biWidth; // 宽度 uint32_t biHeight; // 高度 uint16_t biPlanes; // 位面数(1) uint16_t biBitCount; // 位深度(24) char biCompression[4];// "MJPG" uint32_t biSizeImage; // 单帧最大字节数 uint32_t biXPelsPerMeter; // 水平分辨率(0) uint32_t biYPelsPerMeter; // 垂直分辨率(0) uint32_t biClrUsed; // 颜色数(0) uint32_t biClrImportant; // 重要颜色数(0) char list3[4]; // "LIST" uint32_t list3Size; // movi块大小(后续更新) char movi[4]; // "movi" } AVIHeader; void setup() { Serial.begin(115200); delay(1000); // Initialize the camera camera_config_t config; config.ledc_channel = LEDC_CHANNEL_0; config.ledc_timer = LEDC_TIMER_0; config.pin_d0 = 5; config.pin_d1 = 18; config.pin_d2 = 19; config.pin_d3 = 21; config.pin_d4 = 36; config.pin_d5 = 39; config.pin_d6 = 34; config.pin_d7 = 35; config.pin_xclk = 0; config.pin_pclk = 22; config.pin_vsync = 25; config.pin_href = 23; config.pin_sscb_sda = 26; config.pin_sscb_scl = 27; config.pin_pwdn = 32; config.pin_reset = -1; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG; config.frame_size = CAMERA_FRAME_SIZE; config.jpeg_quality = CAMERA_QUALITY; config.fb_count = 2; // Camera init esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x", err); return; } // Initialize SD card if (!SD_MMC.begin()) { Serial.println("SD Card Mount Failed"); return; } uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024); Serial.printf("SD Card Size: %lluMB\n", cardSize); // Initialize LED pin pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); } void loop() { unsigned long startTime = millis(); // Start time File videoFile = SD_MMC.open("/video.avi", FILE_WRITE); if (!videoFile) { Serial.println("Failed to open file for writing"); return; } int totalFrames = (20000 * FRAME_RATE) / 1000; // 计算20秒的总帧数 writeAVIHeader(videoFile, totalFrames); int recordedFrames = 0; while (millis() - startTime < 20000) { // Record for 20 seconds camera_fb_t *fb = esp_camera_fb_get(); if (!fb) { Serial.println("Camera capture failed"); videoFile.close(); return; } // 打印帧大小,确认相机正常输出数据 Serial.printf("Frame size: %d bytes\n", fb->len); // Write the frame data to the AVI file if (!writeFrame(videoFile, fb)) { Serial.println("Failed to write frame"); esp_camera_fb_return(fb); videoFile.close(); return; } recordedFrames++; esp_camera_fb_return(fb); digitalWrite(LED_PIN, HIGH); delay(50); digitalWrite(LED_PIN, LOW); // 计算剩余延时,保证帧率稳定 unsigned long frameTime = millis() - startTime; unsigned long expectedFrameTime = (recordedFrames * 1000) / FRAME_RATE; if (expectedFrameTime > frameTime) { delay(expectedFrameTime - frameTime); } } // Finalize AVI file if (!finalizeAVIFile(videoFile, recordedFrames)) { Serial.println("Failed to finalize AVI file"); } videoFile.close(); Serial.printf("Recording complete. Total frames recorded: %d\n", recordedFrames); while (true); // Stop further execution } // Function to write the AVI header void writeAVIHeader(File &videoFile, int frameCount) { AVIHeader header = {0}; // RIFF块 memcpy(header.riff, "RIFF", 4); memcpy(header.avi, "AVI ", 4); // hdrl LIST块 memcpy(header.list, "LIST", 4); header.listSize = 48 + 8 + 56 + 8 + 56 + 8 + 40; // hdrl块总大小 memcpy(header.hdrl, "hdrl", 4); // avih子块 memcpy(header.avih, "avih", 4); header.avihSize = 56; header.usPerFrame = 1000000 / FRAME_RATE; header.maxBytesPerSec = (320 * 240 * 3 * FRAME_RATE) / 2; // 预估MJPEG码率 header.flags = 0x10; // AVIF_HASINDEX header.totalFrames = frameCount; header.streams = 1; header.suggestedBufferSize = 320 * 240 * 3; header.width = 320; // QVGA宽度 header.height = 240; // QVGA高度 // strl LIST块 memcpy(header.list2, "LIST", 4); header.list2Size = 8 + 56 + 8 + 40; // strl块总大小 memcpy(header.strl, "strl", 4); // strh子块 memcpy(header.strh, "strh", 4); header.strhSize = 56; memcpy(header.fccType, "vids", 4); memcpy(header.fccHandler, "MJPG", 4); header.scale = 1; header.rate = FRAME_RATE; header.length = frameCount; header.frameRight = 320; header.frameBottom = 240; // strf子块 memcpy(header.strf, "strf", 4); header.strfSize = 40; header.biSize = 40; header.biWidth = 320; header.biHeight = 240; header.biPlanes = 1; header.biBitCount = 24; memcpy(header.biCompression, "MJPG", 4); header.biSizeImage = 320 * 240 * 3; // movi LIST块(初始大小设为0,后续更新) memcpy(header.list3, "LIST", 4); header.list3Size = 0; memcpy(header.movi, "movi", 4); // 写入头数据 videoFile.write((uint8_t*)&header, sizeof(AVIHeader)); } // Function to write a frame to the AVI file bool writeFrame(File &videoFile, camera_fb_t *fb) { // 帧块标识 char chunkId[] = "00db"; videoFile.write((uint8_t*)chunkId, 4); // 帧数据大小(小端模式) uint32_t frameSize = fb->len; videoFile.write((uint8_t*)&frameSize, 4); // 写入JPEG帧数据 return videoFile.write(fb->buf, fb->len) == fb->len; } // Function to finalize the AVI file (e.g., writing index) bool finalizeAVIFile(File &videoFile, int recordedFrames) { uint32_t fileSize = videoFile.size(); // 更新RIFF块的文件大小 videoFile.seek(4); uint32_t riffFileSize = fileSize - 8; if (videoFile.write((uint8_t*)&riffFileSize, 4) != 4) return false; // 更新avih块的总帧数 videoFile.seek(4 + 4 + 4 + 4 + 4 + 4); // 定位到avih的totalFrames字段 if (videoFile.write((uint8_t*)&recordedFrames, 4) != 4) return false; // 更新strh块的总帧数 videoFile.seek(4 + 4 + 4 + 4 + 4 + 4 + 56 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4); // 定位到strh的length字段 if (videoFile.write((uint8_t*)&recordedFrames, 4) != 4) return false; // 更新movi LIST块的大小 uint32_t moviSize = fileSize - AVI_HEADER_SIZE; videoFile.seek(AVI_HEADER_SIZE - 8); // 定位到list3Size字段 if (videoFile.write((uint8_t*)&moviSize, 4) != 4) return false; // 写入索引块(适配部分播放器) videoFile.seek(fileSize); char idx1[] = "idx1"; videoFile.write((uint8_t*)idx1, 4); uint32_t idxSize = recordedFrames * 16; videoFile.write((uint8_t*)&idxSize, 4); uint32_t offset = AVI_HEADER_SIZE; for (int i = 0; i < recordedFrames; i++) { char chunkId[] = "00db"; videoFile.write((uint8_t*)chunkId, 4); uint32_t flags = 0x10; // AVIIF_KEYFRAME videoFile.write((uint8_t*)&flags, 4); videoFile.write((uint8_t*)&offset, 4); // 记录当前帧大小 uint32_t frameSize = videoFile.size() - offset - 8; videoFile.write((uint8_t*)&frameSize, 4); offset += 8 + frameSize; } return true; }
额外检查项
- SD卡兼容性:使用FAT32格式的SD卡,容量建议在4GB-32GB之间,避免UHS高速卡(部分ESP32-CAM对其兼容性差)。
- 硬件连接:确认SD卡模块与ESP32-CAM的连线无误,AI-Thinker模块的SD_MMC引脚无需额外配置(代码已适配)。
- 串口调试:通过串口打印的帧大小,确认相机正常输出JPEG数据。若帧大小为0,检查相机初始化状态或调整
CAMERA_QUALITY参数。 - 文件权限:确保SD卡未开启写保护,ESP32-CAM具备写入文件的权限。
内容的提问来源于stack exchange,提问作者Mr.Programmer
相关产品推荐
相关产品推荐

