ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ESP32-CAM实现AVI视频录制:从JPEG流到独立视频文件的完整方案

ESP32-CAM实现AVI视频录制:从JPEG流到独立视频文件的完整方案 1. 项目缘起从JPEG到AVIESP32-CAM的视频录制之路如果你玩过ESP32-CAM大概率是从那个经典的“网络摄像头”例程开始的。插上USB转TTL刷入Arduino IDE里的CameraWebServer示例打开浏览器一个实时视频流就出来了。方便是方便但有个问题一直让我觉得不够“爽”它只能一帧一帧地传JPEG图片没法直接录制成一个完整的视频文件。这意味着你想做个安防监控、延时摄影或者简单的视频日志都得在电脑端开个软件不停地抓图、合成既麻烦又占资源。最近在折腾一个需要本地存储视频片段的小项目这个痛点又被放大了。于是我开始琢磨能不能让ESP32-CAM自己把拍到的画面直接存成一个标准的视频文件比如AVI格式然后扔到SD卡里这样设备就真正独立了断电重启、远程控制都方便。搜了一圈发现网上关于ESP32-CAM输出AVI文件的完整方案并不多要么是只言片语要么是依赖复杂的RTOS和外部库对新手不太友好。所以我决定自己趟一条路出来把从原理到代码再到踩过的坑完整地梳理一遍。这个项目的核心目标很明确让ESP32-CAM在无需电脑持续干预的情况下将摄像头捕获的图像序列实时编码并封装成标准的AVI视频文件直接保存到其自带的MicroSD卡中。听起来像是把一台迷你DV机塞进了这个比硬币大不了多少的模块里。接下来我会带你一步步拆解其中的技术关节并分享一个经过实测可用的Arduino实现方案。2. AVI格式解析为什么是它以及它怎么装下视频在动手写代码之前我们得先搞清楚要生成的目标——AVI文件。AVI是Audio Video Interleave的缩写直译过来就是“音频视频交错”是微软早年推出的一种多媒体容器格式。对于ESP32-CAM这个场景我们只关心视频不涉及音频所以事情可以简化不少。选择AVI而不是更现代的MP4或MKV主要基于几个现实的考虑。首先是复杂度。AVI的文件结构相对简单、直白它没有MP4那种复杂的“盒子”结构其本质就是一个RIFF文件里面按照特定的“块”来组织数据。这对于资源极其有限的微控制器来说意味着我们可以用更少的代码和内存来完成文件的封装写入。其次是兼容性。AVI是一个老而弥坚的格式几乎所有的视频播放器、操作系统都能原生支持省去了我们额外处理解码器的麻烦。最后是流式写入的友好性。AVI可以在文件写入的过程中逐步构建索引这对于我们需要一边采集图像、一边写入SD卡的场景非常合适我们不需要在录制结束前就知道文件的总帧数或总大小。一个最简单的AVI文件结构可以这样理解RIFF头部这是一个总括性的块声明这是一个RIFF文件类型是‘AVI ’。AVI主列表里面包含两个重要的子列表。hdrl列表存放文件的“元数据”。最关键的是avih块它定义了整个AVI流的全局信息比如总帧数录制开始时可以填0、帧率、数据流数量等。还有一个strl列表用于定义视频流的具体参数核心是strh和strf块。strh说明这是视频流并指定其编码格式对我们来说就是未压缩的RGB或YUV或者Motion JPEGstrf则存放更具体的格式信息如图像宽度、高度、位深。movi列表这是文件的主体所有视频帧数据都放在这里。每一帧数据都被包裹在一个00db或00dc这样的标签块里。索引块通常位于文件末尾是一个idx1块。它记录了每一帧数据在movi列表中的具体位置偏移量和大小。有了这个索引播放器才能快速随机访问任意一帧而不是从头开始线性搜索。这个索引可以在录制完成后一次性写入。对于ESP32-CAM我们通常输出的是JPEG压缩后的图像。在AVI中每一帧JPEG数据可以直接作为一个“视频帧”存入movi列表。这种格式被称为Motion JPEG AVI本质上就是一系列JPEG图片按顺序播放。它的优点是编码简单摄像头硬件直接输出JPEG缺点是文件体积比使用帧间压缩的H.264等格式要大得多。但在存储卡容量廉价、且ESP32-CAM处理能力有限的前提下MJPEG AVI是一个务实且高效的选择。3. 硬件与核心库选型搭建可录制的ESP32-CAM要让想法落地硬件是基础。你需要一块ESP32-CAM开发板。市面上最常见的那种集成了ESP32-S芯片、OV2640摄像头模组、一个MicroSD卡槽、一个LED闪光灯和几个GPIO排针。请注意有些廉价版本可能省略了SD卡槽务必确认你手头的板子有。此外你还需要一个USB转TTL串口模块用于烧录程序以及一根MicroSD卡建议Class10以上容量无需太大32GB足够。在软件层面我们主要依赖Arduino核心库和几个关键的第三方库Arduino core for ESP32这是基础提供了对ESP32芯片的基本驱动。ESP32 Camera Driver这是重中之重。它由乐鑫官方维护提供了初始化OV2640/OV7670等摄像头、配置分辨率、质量、获取图像帧的底层API。我们项目里所有的图像数据都来源于此。SD库Arduino自带的SD卡库用于文件的读写操作。ESP32的Arduino核心对其有良好的支持。这里有一个关键点我们不需要复杂的视频编码库。因为OV2640摄像头模组内部有一个JPEG编码器它可以直接输出压缩好的JPEG图像数据。我们的任务不是去编码视频流而是将这些现成的JPEG帧按照AVI的格式要求“打包”成一个文件。这大大降低了项目的难度。整个系统的数据流可以这样概括ESP32启动 - 初始化摄像头 - 初始化SD卡 - 创建AVI文件并写入头部 - 进入循环从摄像头获取一帧JPEG数据 - 将这帧数据作为AVI的一个00dc块写入文件 - 更新内存中的帧索引信息 - 循环直到达到预定帧数或收到停止信号 - 停止采集 - 将帧索引写入文件尾部 - 关闭文件。这个过程完全在ESP32-CAM上独立运行。4. 实战代码拆解手把手构建AVI写入器理论说得再多不如一行代码来得实在。下面我将分模块解析实现AVI录制的核心代码。请注意为了清晰和篇幅这里展示的是关键逻辑片段完整的、可编译的代码需要你根据这些逻辑去组织和填充。4.1 初始化与AVI文件头创建一切从初始化开始。在setup()函数中我们需要依次初始化串口、摄像头和SD卡。#include “esp_camera.h” #include “FS.h” #include “SD_MMC.h” // 摄像头引脚定义根据你的ESP32-CAM型号调整 #define PWDN_GPIO_NUM 32 #define RESET_GPIO_NUM -1 #define XCLK_GPIO_NUM 0 #define SIOD_GPIO_NUM 26 #define SIOC_GPIO_NUM 27 #define Y9_GPIO_NUM 35 #define Y8_GPIO_NUM 34 #define Y7_GPIO_NUM 39 #define Y6_GPIO_NUM 36 #define Y5_GPIO_NUM 21 #define Y4_GPIO_NUM 19 #define Y3_GPIO_NUM 18 #define Y2_GPIO_NUM 5 #define VSYNC_GPIO_NUM 25 #define HREF_GPIO_NUM 23 #define PCLK_GPIO_NUM 22 File aviFile; uint32_t frameCount 0; uint32_t moviListStart 0; // 记录movi列表开始的位置 uint32_t idx1Position 0; // 记录索引块开始的位置 uint8_t idx1Data[16 * 1024]; // 假设最大帧数每帧索引占16字节 uint32_t idx1Size 0; void setup() { Serial.begin(115200); // 1. 初始化摄像头 camera_config_t config; config.ledc_channel LEDC_CHANNEL_0; config.ledc_timer LEDC_TIMER_0; config.pin_d0 Y2_GPIO_NUM; // ... 填充所有引脚定义 config.pin_xclk XCLK_GPIO_NUM; config.pin_sscb_sda SIOD_GPIO_NUM; config.pin_sscb_scl SIOC_GPIO_NUM; config.pin_pwdn PWDN_GPIO_NUM; config.pin_reset RESET_GPIO_NUM; config.xclk_freq_hz 20000000; config.pixel_format PIXFORMAT_JPEG; // 关键直接获取JPEG格式 config.frame_size FRAMESIZE_SVGA; // 例如800x600 config.jpeg_quality 12; // 0-63数值越小质量越高 config.fb_count 2; esp_err_t err esp_camera_init(config); if (err ! ESP_OK) { Serial.printf(“摄像头初始化失败 0x%x”, err); return; } // 2. 初始化SD卡使用1线模式兼容性更好 if(!SD_MMC.begin(“/sdcard”, true)) { // 第二个参数true表示1线模式 Serial.println(“SD卡挂载失败”); return; } uint8_t cardType SD_MMC.cardType(); if(cardType CARD_NONE){ Serial.println(“未插入SD卡”); return; } // 3. 创建AVI文件并写入头部 createAVIHeader(“/sdcard/test.avi”); }createAVIHeader函数是核心之一它负责在SD卡上创建文件并写入AVI的RIFF头部、hdrl列表等元数据信息。由于我们在录制开始时还不知道总帧数avih块中的相关字段可以先填0最后再回来更新。同时我们需要精确记录movi列表开始的文件位置因为后续所有的帧数据都要写在这个列表里。void createAVIHeader(const char* filename) { aviFile SD_MMC.open(filename, FILE_WRITE); if(!aviFile){ Serial.println(“创建文件失败”); return; } // 写入RIFF头部 writeChunk(“RIFF”, 0); // 大小先写0最后再修正 aviFile.write(“AVI “, 4); // 写入hdrl列表 writeChunk(“LIST”, 0); // 大小先写0 aviFile.write(“hdrl”, 4); // 写入avih块 (AVI主头部) // … 这里需要计算并写入avih结构体数据包含帧率、总帧数(暂0)、数据流数量等 // 写入视频流信息(strl列表) writeChunk(“LIST”, 0); aviFile.write(“strl”, 4); // 写入strh块 (流头部) // … 写入视频流类型、编码格式(‘MJPG’)、帧率等信息 // 写入strf块 (流格式) // … 写入BITMAPINFOHEADER结构包含宽度、高度、位深、压缩类型(‘MJPG’)等 // 结束hdrl列表的LIST块并回写其正确大小 // … // 记录movi列表的开始位置并写入LIST头 moviListStart aviFile.position(); writeChunk(“LIST”, 0); // 大小先写0 aviFile.write(“movi”, 4); // 暂时不关闭文件准备接收帧数据 }writeChunk是一个辅助函数用于写入一个标准的RIFF块4字节块标识 4字节块大小 数据。4.2 实时帧捕获与写入循环初始化完成后进入loop()函数或者由一个单独的录制任务函数负责循环抓取帧并写入。void loop() { // 假设通过一个按钮或串口命令触发录制 if (startRecording) { recordFrames(300); // 例如录制300帧 startRecording false; } } void recordFrames(uint32_t maxFrames) { camera_fb_t * fb NULL; for(frameCount 0; frameCount maxFrames; frameCount){ // 1. 从摄像头获取一帧JPEG数据 fb esp_camera_fb_get(); if(!fb){ Serial.println(“获取摄像头帧失败”); break; } // 2. 将帧数据作为’00dc’块写入movi列表 // ‘00dc’表示未压缩的视频帧数据块 aviFile.write(“00dc”, 4); uint32_t frameSize fb-len; aviFile.write((uint8_t*)frameSize, 4); // 写入块大小 aviFile.write(fb-buf, fb-len); // 写入JPEG数据 // 如果数据长度不是2字节对齐需要补一个0RIFF规范 if(frameSize % 2 ! 0){ aviFile.write((uint8_t)0); frameSize; } // 3. 在内存中记录索引信息 // idx1块每条记录16字节’00dc’, 标志(0x0000), 偏移量, 大小 uint32_t offset aviFile.position() – frameSize – 8; // 计算该帧’00dc’块在文件中的起始位置 memcpy(idx1Data[idx1Size], “00dc”, 4); uint32_t flags 0x0000; memcpy(idx1Data[idx1Size4], flags, 4); memcpy(idx1Data[idx1Size8], offset, 4); memcpy(idx1Data[idx1Size12], fb-len, 4); // 注意这里是原始fb-len不是对齐后的 idx1Size 16; // 4. 释放帧缓冲区 esp_camera_fb_return(fb); // 可选每录制N帧打印一次进度或控制LED闪烁 if(frameCount % 30 0){ Serial.printf(“已录制 %d 帧n”, frameCount); } delay(40); // 根据帧率控制延时例如25fps对应40ms } // 录制结束完成AVI文件 finalizeAVIFile(); }4.3 文件收尾与索引写入录制循环结束后我们必须正确地“封口”这个AVI文件。这包括完成movi列表回写到movi列表LIST块的大小。写入idx1索引块将内存中记录的索引数据写入文件。完成RIFF头部回写整个RIFF文件的总大小。关闭文件。void finalizeAVIFile() { if(!aviFile) return; // 1. 结束movi列表的LIST块 uint32_t moviListEnd aviFile.position(); uint32_t moviListSize moviListEnd – moviListStart; aviFile.seek(moviListStart 4); // 跳回到LIST块的大小字段位置 aviFile.write((uint8_t*)moviListSize, 4); // 写入正确的大小 aviFile.seek(moviListEnd); // 回到文件末尾 // 2. 写入idx1索引块 writeChunk(“idx1”, idx1Size); aviFile.write(idx1Data, idx1Size); // 3. 修正整个RIFF文件的大小 uint32_t fileSize aviFile.size(); aviFile.seek(4); // 跳转到RIFF块的大小字段位置 uint32_t riffSize fileSize – 8; // RIFF大小 文件总大小 – 8 (因为’RIFF’和大小字段本身不算) aviFile.write((uint8_t*)riffSize, 4); // 4. 修正avih块中的总帧数等信息需要提前记录avih块的位置 // aviFile.seek(avihPos …); // 跳转到总帧数字段 // aviFile.write((uint8_t*)frameCount, 4); // 5. 关闭文件 aviFile.close(); Serial.printf(“录制完成共 %d 帧文件大小%d 字节n”, frameCount, fileSize); }至此一个完整的、由ESP32-CAM独立生成的AVI视频文件就静静地躺在你的SD卡里了。你可以把卡拔出来插到电脑上用任何播放器打开它。5. 关键参数调优与性能瓶颈分析代码能跑通只是第一步要录得稳、录得好还需要理解并调整几个关键参数。这些参数直接影响了视频的流畅度、画质和系统稳定性。1. 图像分辨率与JPEG质量在camera_config_t中frame_size和jpeg_quality是一对需要权衡的参数。frame_size从FRAMESIZE_QQVGA(160x120) 到FRAMESIZE_UXGA(1600x1200) 可选。分辨率越高单帧JPEG数据量越大。对于持续录制建议从FRAMESIZE_SVGA(800x600) 或FRAMESIZE_XGA(1024x768) 开始。更高的分辨率如UXGA在连续抓帧时极易导致摄像头缓冲区溢出或SD卡写入跟不上。jpeg_quality范围0-630最好63最差。质量越高单帧体积越大。经过实测设置在10-15之间能在画质和体积间取得很好的平衡。低于10体积增长非常明显高于20画质劣化开始能被肉眼察觉。2. 帧率控制ESP32-CAM的帧率并非直接设置而是由loop()中每次抓帧后的delay()以及摄像头本身的性能共同决定。esp_camera_fb_get()这个函数是阻塞的它会等待摄像头传感器准备好一帧数据。OV2640在不同分辨率下的最大帧率是有限的。例如在SVGA分辨率下理论最高帧率约25-30fps。我们的delay(40)目标是实现约25fps。实际上由于SD卡写入速度、JPEG编码时间等因素实际稳定帧率往往会低于理论值。你需要通过串口打印每帧的耗时来测算实际帧率。3. SD卡写入速度这是最大的性能瓶颈。ESP32-CAM通常使用SPI模式连接SD卡写入速度有限。一帧800x600、质量12的JPEG图片大小可能在30-50KB。以25fps计算数据吞吐量要求达到750KB/s ~ 1.25MB/s。这已经接近甚至超过了某些低速SD卡在SPI模式下的稳定写入能力。注意务必使用Class10或UHS-I及以上速度等级的MicroSD卡。低速卡会导致写入队列堵塞轻则丢帧、视频卡顿重则导致文件系统错误、录制中断。可以在代码中加入检查如果aviFile.write()耗时过长可以考虑主动跳帧丢弃当前帧来保证录制不中断。4. 内存与缓冲区config.fb_count设置了帧缓冲区的数量。默认是1但建议设置为2。双缓冲区允许摄像头在填充一个缓冲区时CPU可以处理另一个缓冲区中的数据写入SD卡形成流水线能小幅提升性能。但缓冲区会占用PSRAM如果你的模块没有外接PSRAM使用高分辨率时单缓冲区都可能内存不足。一个典型的性能瓶颈排查链路是录制一段视频发现实际帧率远低于预期 - 在代码中打印每次esp_camera_fb_get()和aviFile.write()的耗时 - 如果fb_get耗时很长可能是摄像头分辨率设置过高或传感器问题如果write耗时很长问题很可能出在SD卡速度上 - 尝试降低分辨率、JPEG质量或更换更高速的SD卡 - 如果写入依然慢可以尝试在SD_MMC.begin()时使用4线模式如果硬件支持这能显著提升速度但有些ESP32-CAM模块的SD卡接线只支持1线模式。6. 避坑指南从文件损坏到帧率不稳在实际操作中我遇到了不少坑这里集中分享一下希望你能绕过去。坑一AVI文件无法播放或只有第一帧现象生成的.avi文件在电脑上打开有的播放器报错有的只显示第一张图就卡住。排查首先用十六进制编辑器如HxD打开文件对照AVI格式规范检查。最常见的原因是块大小字段写错了。RIFF格式规定块大小字段记录的是“数据”部分的大小不包括块标识符和大小字段本身的8个字节。但在写入00dc块时大小字段应该是JPEG数据的实际长度。而在idx1索引中记录的大小是原始JPEG数据的长度即fb-len而不是对齐后的长度。如果这里混淆了索引指向的位置就会错位播放器自然找不到正确的帧数据。解决仔细核对代码中所有write块大小的部分。确保00dc块的大小是fb-len而idx1里记录的大小也是fb-len。文件末尾的填充字节为了2字节对齐不参与大小计算。坑二录制一段时间后程序崩溃或重启现象录制几十秒后ESP32自动重启串口提示“Guru Meditation Error”或“Task watchdog got triggered”。排查这通常是看门狗超时或堆内存耗尽引起的。esp_camera_fb_get()和SD_MMC.write()都是可能耗时的操作如果它们在主循环中阻塞太久看门狗就会触发复位。解决启用看门狗喂狗在loop()或录制循环中定期调用delay(0)或yield()这会让ESP32有机会执行后台任务包括喂狗。优化SD卡写入确保使用aviFile.write(fb-buf, fb-len)进行批量写入而不是单字节写入。检查SD卡速度和接线。增加任务优先级如果使用了FreeRTOS任务来负责录制可以适当提高该任务的优先级但注意不要阻塞低优先级任务太久。监控内存在串口输出ESP32的剩余堆内存观察在录制过程中是否持续下降这可能意味着存在内存泄漏如帧缓冲区未释放。坑三实际帧率远低于设定值现象代码里设了delay(33)想实现30fps但实际只有10fps。排查如前所述瓶颈可能在摄像头、SD卡或CPU。添加调试代码测量一帧的总处理时间fb_getwrite 其他逻辑。解决分而治之单独测试摄像头最大帧率。写一个只抓图、不存SD卡的程序看能达到多快。压力测试SD卡写一个程序不断向SD卡写入固定大小的数据块测试其持续写入速度。降低负载如果目标是流畅录制适当降低分辨率是最有效的方法。从UXGA降到SVGA帧率可能会有质的提升。跳帧策略如果某一帧的处理时间过长可以主动丢弃它直接获取下一帧以维持大致的时间节奏。但这会导致视频轻微不连贯。坑四SD卡突然无法识别或文件丢失现象之前好好的某次上电后提示“SD卡挂载失败”或录制好的文件不见了。排查SPI总线对电源噪声和接线非常敏感。劣质的USB线或电源适配器可能导致供电不稳在写入时引发文件系统错误。解决加强供电尝试使用外部5V电源直接给ESP32-CAM供电而非通过USB转TTL模块供电。检查接线确保SD卡座的引脚CLK, CMD, D0, D1, D2, D3接触良好尤其是使用排线连接时。文件系统恢复将SD卡插入电脑使用磁盘检查工具修复。在代码中可以考虑在初始化SD卡前先调用SD_MMC.end()再begin()进行软复位。7. 进阶思路让视频录制更实用基础功能实现后我们可以在此基础上做一些扩展让这个项目更贴近实际应用。1. 运动检测触发录制单纯的持续录制既费电又占存储。可以结合ESP32-CAM的像素数据实现简单的运动检测。一种轻量级的方法是定期获取一帧低分辨率的灰度图使用PIXFORMAT_GRAYSCALE与上一帧进行比较计算像素差异的绝对值之和。如果超过阈值则触发高分辨率JPEG的录制一段时间。这样SD卡里保存的就都是“有事件发生”的有效视频片段。2. 循环录制与文件管理存储空间有限需要自动管理。可以设计一个简单的循环缓冲区逻辑设置一个最大文件数量或总存储空间上限。当录制新文件时检查SD卡空间或文件数如果超过限制则自动删除最旧的文件。这需要用到SD库的文件列表遍历和删除功能。3. 通过网络传输视频片段虽然本文聚焦本地AVI录制但ESP32-CAM本身具备Wi-Fi功能。一个混合模式是平时以低帧率或运动检测模式录制到SD卡当网络连接可用时例如连接到家庭Wi-Fi可以将录制好的AVI文件通过HTTP或FTP上传到NAS或云服务器。这需要处理大文件分块上传、断点续传等问题复杂度较高但实现了本地备份与云端同步的结合。4. 优化AVI索引写入方式当前方案是在内存中累积所有索引最后一次性写入。如果录制帧数非常多比如上万帧可能会耗尽内存。一个改进方案是将索引也实时写入文件但放在一个临时位置。录制结束后再将这个临时索引块移动到标准的idx1位置并修正文件头。这避免了内存消耗但增加了文件操作的复杂性。实现ESP32-CAM的AVI视频输出是一个将硬件特性、文件格式和嵌入式资源管理紧密结合的过程。它没有用到高深的编码算法但非常考验对细节的把握和对系统资源的理解。从最初的只能输出JPEG流到如今能生成标准的、可在任何设备上播放的AVI文件这个小模块的实用性得到了巨大的提升。
返回列表