You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
}

额外检查项

  1. SD卡兼容性:使用FAT32格式的SD卡,容量建议在4GB-32GB之间,避免UHS高速卡(部分ESP32-CAM对其兼容性差)。
  2. 硬件连接:确认SD卡模块与ESP32-CAM的连线无误,AI-Thinker模块的SD_MMC引脚无需额外配置(代码已适配)。
  3. 串口调试:通过串口打印的帧大小,确认相机正常输出JPEG数据。若帧大小为0,检查相机初始化状态或调整CAMERA_QUALITY参数。
  4. 文件权限:确保SD卡未开启写保护,ESP32-CAM具备写入文件的权限。

内容的提问来源于stack exchange,提问作者Mr.Programmer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 00:50:55