{"id":4878,"date":"2025-05-28T08:00:06","date_gmt":"2025-05-28T15:00:06","guid":{"rendered":"https:\/\/www.cloudacm.com\/?p=4878"},"modified":"2025-05-26T08:12:00","modified_gmt":"2025-05-26T15:12:00","slug":"esp32-cam-video-support","status":"publish","type":"post","link":"https:\/\/www.cloudacm.com\/?p=4878","title":{"rendered":"ESP32-Cam Video Support"},"content":{"rendered":"<p>This post will cover the ESP32 Cam module&#8217;s support for video capture.\u00a0 The module comes with a microSD slot and Omnivision camera interface.\u00a0 Earlier examples demonstrated time lapse video capture, but this post will focus more on real time video.<\/p>\n<p>This topic was found accidentally while researching I2S audio support.\u00a0 This Github repo by <a href=\"https:\/\/github.com\/jameszah\">James Zahary<\/a>, with contribution from <a href=\"https:\/\/github.com\/alkhachatryan\">Alexey Khachatryan<\/a>, provided a realtime video capture process, <a href=\"https:\/\/github.com\/jameszah\/ESP32-CAM-Video-Recorder-junior\">https:\/\/github.com\/jameszah\/ESP32-CAM-Video-Recorder-junior<\/a>.\u00a0 Unlike the more common triggered still or time lapse image captures demonstrated online, this method claimed native video with the ESP32 Cam module.\u00a0 Below is a video demonstrating the method.<\/p>\n<p><iframe loading=\"lazy\" title=\"ESP32 CAM How to Save Movies to SD Card\" width=\"640\" height=\"360\" src=\"https:\/\/www.youtube.com\/embed\/ojLHFpZDZe8?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/p>\n<p>There are several dependencies of the repo that are not needed if the sole purpose is to capture video to the microSD card.\u00a0 In attempts to use the repo, the IDE would fail to compile the code due to these dependencies.\u00a0 Reverse engineering the code would be a labor intensive process.\u00a0 There were other repos, such as <a href=\"https:\/\/github.com\/s60sc\/ESP32-CAM_MJPEG2SD\">https:\/\/github.com\/s60sc\/ESP32-CAM_MJPEG2SD<\/a> by <a href=\"https:\/\/github.com\/s60sc\">s60sc<\/a>.\u00a0 However, the extra features were beyond the scope of simply capturing video.\u00a0 So the question of capturing video to the ESP32 Cam module was posed to AI.\u00a0 There was back and forth with the prompts as to the limitations of the module, what the goal expectations were , and what it reasonably supported.\u00a0 Ultimately, the following code was generated.<\/p>\n<pre>\/*\r\n\r\nESP32-CAM_Video_ver2c\r\n\r\nThis program captures video on the ESP32 Cam module at its maximum FPS\r\nDebugging and log files have been removed to optimize FPS rates\r\nImages are captured with millis values for rate references\r\nImages are batched into 1 minute folders\r\n\r\nFPS drift follows this formula:\r\nfor x in milliseconds\r\ny = 36.54 (x^-0.5) in FPS\r\n\r\nThe code was developed using ChatGPT\r\n\r\nEdited by Patrick Gilfeather May 20, 2025\r\n\r\nThe is Arduino code, with standard setup for ESP32-CAM\r\n- Board AI Thinker ESP32-CAM\r\n- Partition Scheme Huge APP (3MB No OTA)\r\n\r\nHardware used\r\n- Espressif ESP32-Cam module\r\n- Omnivision DCX-OV-5640-E 5MP 180 degree camera module\r\n- SanDisk Ultra 64GB microSD class 10 format FAT (32-bit version)\r\n\r\n*\/\r\n\r\n#include \"esp_camera.h\"\r\n#include \"FS.h\"\r\n#include \"SD_MMC.h\"\r\n\r\n#define PWDN_GPIO_NUM 32\r\n#define RESET_GPIO_NUM -1\r\n#define XCLK_GPIO_NUM 0\r\n#define SIOD_GPIO_NUM 26\r\n#define SIOC_GPIO_NUM 27\r\n#define Y9_GPIO_NUM 35\r\n#define Y8_GPIO_NUM 34\r\n#define Y7_GPIO_NUM 39\r\n#define Y6_GPIO_NUM 36\r\n#define Y5_GPIO_NUM 21\r\n#define Y4_GPIO_NUM 19\r\n#define Y3_GPIO_NUM 18\r\n#define Y2_GPIO_NUM 5\r\n#define VSYNC_GPIO_NUM 25\r\n#define HREF_GPIO_NUM 23\r\n#define PCLK_GPIO_NUM 22\r\n\r\n#define MAX_FRAMES 1000000\r\n#define CAPTURE_WINDOW_MS 300000 \/\/ Optional: 5 minutes capture window\r\n\r\nunsigned long millisOffset = 0; \/\/ Adjusted millis() to avoid overlaps\r\n\r\nunsigned long findMaxTimestamp() {\r\nunsigned long maxTimestamp = 0;\r\n\r\nFile root = SD_MMC.open(\"\/\");\r\nif (!root || !root.isDirectory()) return 0;\r\n\r\nFile folder = root.openNextFile();\r\nwhile (folder) {\r\nif (folder.isDirectory() &amp;&amp; String(folder.name()).startsWith(\"\/min_\")) {\r\nFile file = folder.openNextFile();\r\nwhile (file) {\r\nString fname = String(file.name());\r\nint idx1 = fname.indexOf(\"img_\");\r\nint idx2 = fname.indexOf(\".jpg\");\r\nif (idx1 &gt;= 0 &amp;&amp; idx2 &gt; idx1) {\r\nString tsStr = fname.substring(idx1 + 4, idx2);\r\nunsigned long ts = tsStr.toInt();\r\nif (ts &gt; maxTimestamp) maxTimestamp = ts;\r\n}\r\nfile.close();\r\nfile = folder.openNextFile();\r\n}\r\n}\r\nfolder.close();\r\nfolder = root.openNextFile();\r\n}\r\nreturn maxTimestamp;\r\n}\r\n\r\nvoid setup() {\r\ncamera_config_t config;\r\nconfig.ledc_channel = LEDC_CHANNEL_0;\r\nconfig.ledc_timer = LEDC_TIMER_0;\r\nconfig.pin_d0 = Y2_GPIO_NUM;\r\nconfig.pin_d1 = Y3_GPIO_NUM;\r\nconfig.pin_d2 = Y4_GPIO_NUM;\r\nconfig.pin_d3 = Y5_GPIO_NUM;\r\nconfig.pin_d4 = Y6_GPIO_NUM;\r\nconfig.pin_d5 = Y7_GPIO_NUM;\r\nconfig.pin_d6 = Y8_GPIO_NUM;\r\nconfig.pin_d7 = Y9_GPIO_NUM;\r\nconfig.pin_xclk = XCLK_GPIO_NUM;\r\nconfig.pin_pclk = PCLK_GPIO_NUM;\r\nconfig.pin_vsync = VSYNC_GPIO_NUM;\r\nconfig.pin_href = HREF_GPIO_NUM;\r\nconfig.pin_sscb_sda = SIOD_GPIO_NUM;\r\nconfig.pin_sscb_scl = SIOC_GPIO_NUM;\r\nconfig.pin_pwdn = PWDN_GPIO_NUM;\r\nconfig.pin_reset = RESET_GPIO_NUM;\r\nconfig.xclk_freq_hz = 20000000;\r\nconfig.pixel_format = PIXFORMAT_JPEG;\r\nconfig.frame_size = FRAMESIZE_QVGA;\r\nconfig.jpeg_quality = 12;\r\nconfig.fb_count = 2;\r\n\r\nesp_camera_init(&amp;config);\r\nSD_MMC.begin();\r\n\r\n\/\/ Scan SD card and find the highest timestamp to continue from\r\nunsigned long maxTS = findMaxTimestamp();\r\nmillisOffset = maxTS + 1; \/\/ Start after the latest found timestamp\r\n}\r\n\r\nvoid loop() {\r\nstatic unsigned long frameCount = 0;\r\nif (frameCount &gt;= MAX_FRAMES) return;\r\n\r\nunsigned long now = millisOffset + millis();\r\nunsigned long minuteIndex = now \/ 60000; \/\/ 60000 ms = 1 minute\r\nchar folderPath[32];\r\nsprintf(folderPath, \"\/min_%03lu\", minuteIndex);\r\n\r\nif (!SD_MMC.exists(folderPath)) {\r\nSD_MMC.mkdir(folderPath);\r\n}\r\n\r\ncamera_fb_t * fb = esp_camera_fb_get();\r\nif (!fb) return;\r\n\r\nchar filename[40];\r\nsprintf(filename, \"img_%08lu.jpg\", now);\r\nchar fullPath[64];\r\nsprintf(fullPath, \"%s\/%s\", folderPath, filename);\r\n\r\nFile file = SD_MMC.open(fullPath, FILE_WRITE);\r\nif (file) {\r\nsize_t imgSize = fb-&gt;len;\r\nconst size_t bufferSize = 512;\r\nuint8_t buffer[bufferSize];\r\n\r\nfor (size_t written = 0; written &lt; imgSize; written += bufferSize) {\r\nsize_t chunkSize = (imgSize - written &gt; bufferSize) ? bufferSize : (imgSize - written);\r\nmemset(buffer, 0, chunkSize);\r\nfile.write(buffer, chunkSize);\r\n}\r\n\r\nfile.seek(0);\r\nfile.write(fb-&gt;buf, fb-&gt;len);\r\nfile.close();\r\n}\r\n\r\nesp_camera_fb_return(fb);\r\nframeCount++;\r\n}<\/pre>\n<p>The module captures images and writes them to the microSD media as fast as possible.\u00a0 This isn&#8217;t native video capture, rather a series of images captured in rapid succession that are later assembled into a video with FFMpeg.\u00a0 Images are saved in separate folders for each minute of capture.\u00a0 The following bash script was used to create the video file.<\/p>\n<pre>#!\/bin\/bash\r\n# make_video.sh\r\n\r\n# Directory containing image frames\r\nSEQUENCE=\"000\" # &lt;-- Change to your desired folder\r\nINPUT_DIR=\".\/min_${SEQUENCE}\"\r\nOUTPUT_VIDEO=\"min_${SEQUENCE}.mp4\"\r\nFRAMERATE=7.5 # &lt;-- Set desired FPS for the output video\r\n\r\n# Check if the input directory exists\r\nif [ ! -d \"$INPUT_DIR\" ]; then\r\necho \"Error: Directory $INPUT_DIR not found.\"\r\nexit 1\r\nfi\r\n\r\n# Create video from ordered image sequence\r\nffmpeg -framerate $FRAMERATE -pattern_type glob -i \"${INPUT_DIR}\/img_*.jpg\" \\\r\n-c:v libx264 -pix_fmt yuv420p -crf 23 -preset medium \"$OUTPUT_VIDEO\"\r\n\r\necho \"Video saved to $OUTPUT_VIDEO\"<\/pre>\n<p>Then I would combine all of the videos together with this bash script.<\/p>\n<pre>#!\/bin\/bash\r\n# combine.sh\r\n\r\n# Combine all min_*.mp4 into output.mp4\r\nfor f in min_*.mp4; do echo \"file '$f'\" &gt;&gt; filelist.txt; done\r\nffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4\r\nrm -f filelist.txt<\/pre>\n<p>One thing to note about the image sequence is the increase in time it takes for additional images to be written to the folder.\u00a0 Below is a plot of the time drift of FPS (frames per second) over time.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/05\/Image-Capture-FPS-Decay.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4892 size-full\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/05\/Image-Capture-FPS-Decay.png\" alt=\"\" width=\"992\" height=\"504\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/05\/Image-Capture-FPS-Decay.png 992w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/05\/Image-Capture-FPS-Decay-300x152.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/05\/Image-Capture-FPS-Decay-768x390.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/05\/Image-Capture-FPS-Decay-531x270.png 531w\" sizes=\"auto, (max-width: 992px) 100vw, 992px\" \/><\/a><\/p>\n<p>The plot is based on any number of specific conditions, such as microSD media, camera module, video resolution, and jpeg quality.\u00a0 The following video attempts to compensate for this drift by matching the duration of the video with the actual capture time.<\/p>\n<p><iframe loading=\"lazy\" title=\"ESP32-CAM Video Capture\" src=\"https:\/\/player.vimeo.com\/video\/1087743001?dnt=1&amp;app_id=122963\" width=\"320\" height=\"240\" frameborder=\"0\" allow=\"autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media\"><\/iframe><\/p>\n<p>The video has a slow motion appearance at the beginning and fast forward at the end, less than ideal.\u00a0 Something to consider is clamping the FPS rate with shorter video segments to reduce the likelihood of noticeable drift.<\/p>\n<p>I found another Github repo by <a href=\"https:\/\/github.com\/flaviopuhl\">Fl\u00e1vio Luiz Puhl Jr.<\/a>, that wrote a text overlay on video, <a href=\"https:\/\/github.com\/flaviopuhl\/ESP32-Cam-Text-overlay\">https:\/\/github.com\/flaviopuhl\/ESP32-Cam-Text-overlay<\/a>.\u00a0 So I asked AI if pixel data could be written to the video buffer without any camera module present.\u00a0 To my surprise it could.\u00a0 The following code is a demonstration of how pixel graphics can be drawn to represent data, much like a HUD (Heads Up Display) shows telemetry information for drone FPV operators.<\/p>\n<pre>\/*\r\n\r\nESP32-CAM_Video_ver12\r\n\r\nThis program captures data only to the video frame\r\nThe code was developed using ChatGPT\r\n\r\nEdited by Patrick Gilfeather May 21, 2025\r\n\r\nThe is Arduino code, with standard setup for ESP32-CAM\r\n- Board AI Thinker ESP32-CAM\r\n- Partition Scheme Huge APP (3MB No OTA)\r\n\r\nHardware used\r\n- Espressif ESP32-Cam module\r\n- SanDisk Ultra 64GB microSD class 10 format FAT (32-bit version) \r\n\r\nSerial Debug enabled\r\n\r\n\r\n*\/\r\n\r\n#include \"FS.h\"\r\n#include \"SD_MMC.h\"\r\n#include \"esp_camera.h\"\r\n#include \"img_converters.h\"\r\n\r\n#define FRAME_WIDTH 320\r\n#define FRAME_HEIGHT 240\r\n#define FRAME_INTERVAL_MS 500 \/\/ Frame delay\r\n\r\nuint32_t frameCounter = 0;\r\nunsigned long lastFrameTime = 0;\r\n\r\n\/\/ ------------------- SETUP -------------------\r\nvoid setup() {\r\nSerial.begin(115200);\r\ndelay(1000);\r\n\r\nif (!SD_MMC.begin()) {\r\nSerial.println(\"SD Card Mount Failed\");\r\nreturn;\r\n}\r\n\r\nSerial.println(\"SD Card Initialized. Starting simulated animation...\");\r\n}\r\n\r\n\/\/ ------------------- LOOP -------------------\r\nvoid loop() {\r\nif (millis() - lastFrameTime &lt; FRAME_INTERVAL_MS) return;\r\nlastFrameTime = millis();\r\n\r\nuint16_t* buffer = createBlackFrameRGB565();\r\nif (!buffer) return;\r\n\r\ndrawRotatingCross(buffer, FRAME_WIDTH, FRAME_HEIGHT);\r\ndrawMillisText(buffer, FRAME_WIDTH, FRAME_HEIGHT);\r\n\r\nchar filename[32];\r\nsprintf(filename, \"\/frame_sim_%05lu.jpg\", frameCounter++);\r\nsaveAsJPEG(buffer, FRAME_WIDTH, FRAME_HEIGHT, filename);\r\n\r\nfree(buffer);\r\nSerial.printf(\"Saved: %s\\n\", filename);\r\n}\r\n\r\n\/\/ ------------------- FRAME CREATION -------------------\r\nuint16_t* createBlackFrameRGB565() {\r\nsize_t bufferSize = FRAME_WIDTH * FRAME_HEIGHT * sizeof(uint16_t);\r\nuint16_t* buffer = (uint16_t*)ps_malloc(bufferSize);\r\nif (!buffer) {\r\nSerial.println(\"Failed to allocate frame buffer\");\r\nreturn nullptr;\r\n}\r\nmemset(buffer, 0x00, bufferSize);\r\nreturn buffer;\r\n}\r\n\r\n\/\/ ------------------- OVERLAYS -------------------\r\nvoid drawRotatingCross(uint16_t* buffer, int w, int h) {\r\nint cx = w \/ 2;\r\nint cy = h \/ 2;\r\nint radius = 40;\r\nuint16_t green = 0x07E0;\r\n\r\nfor (int angle = 0; angle &lt; 360; angle++) {\r\nfloat rad = angle * DEG_TO_RAD;\r\nint x = cx + cos(rad) * radius;\r\nint y = cy + sin(rad) * radius;\r\nif (x &gt;= 0 &amp;&amp; y &gt;= 0 &amp;&amp; x &lt; w &amp;&amp; y &lt; h)\r\nbuffer[y * w + x] = green;\r\n}\r\n\r\nfloat angle = (millis() % 60000) * 0.00010472;\r\nint len = radius - 2;\r\n\r\nint x1 = cx + cos(angle) * len;\r\nint y1 = cy + sin(angle) * len;\r\nint x2 = cx - cos(angle) * len;\r\nint y2 = cy - sin(angle) * len;\r\n\r\nint x3 = cx + cos(angle + PI \/ 2) * len;\r\nint y3 = cy + sin(angle + PI \/ 2) * len;\r\nint x4 = cx - cos(angle + PI \/ 2) * len;\r\nint y4 = cy - sin(angle + PI \/ 2) * len;\r\n\r\ndrawLineRGB565(buffer, w, h, x1, y1, x2, y2, green);\r\ndrawLineRGB565(buffer, w, h, x3, y3, x4, y4, green);\r\n}\r\n\r\nvoid drawLineRGB565(uint16_t* pixels, int w, int h, int x0, int y0, int x1, int y1, uint16_t color) {\r\nint dx = abs(x1 - x0), sx = x0 &lt; x1 ? 1 : -1;\r\nint dy = -abs(y1 - y0), sy = y0 &lt; y1 ? 1 : -1;\r\nint err = dx + dy, e2;\r\nwhile (true) {\r\nif (x0 &gt;= 0 &amp;&amp; x0 &lt; w &amp;&amp; y0 &gt;= 0 &amp;&amp; y0 &lt; h)\r\npixels[y0 * w + x0] = color;\r\nif (x0 == x1 &amp;&amp; y0 == y1) break;\r\ne2 = 2 * err;\r\nif (e2 &gt;= dy) { err += dy; x0 += sx; }\r\nif (e2 &lt;= dx) { err += dx; y0 += sy; }\r\n}\r\n}\r\n\r\n\/\/ ------------------- TEXT -------------------\r\n\r\nvoid drawChar(uint16_t* buf, int w, int h, int x, int y, char c, uint16_t color) {\r\nstatic const uint8_t font5x7[][5] = {\r\n\/\/ ASCII 32 to 126 (printable characters)\r\n{0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},{0x14,0x7F,0x14,0x7F,0x14},\r\n{0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},{0x36,0x49,0x55,0x22,0x50},{0x00,0x05,0x03,0x00,0x00},\r\n{0x00,0x1C,0x22,0x41,0x00},{0x00,0x41,0x22,0x1C,0x00},{0x14,0x08,0x3E,0x08,0x14},{0x08,0x08,0x3E,0x08,0x08},\r\n{0x00,0x50,0x30,0x00,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x60,0x60,0x00,0x00},{0x20,0x10,0x08,0x04,0x02},\r\n{0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},{0x42,0x61,0x51,0x49,0x46},{0x21,0x41,0x45,0x4B,0x31},\r\n{0x18,0x14,0x12,0x7F,0x10},{0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x30},{0x01,0x71,0x09,0x05,0x03},\r\n{0x36,0x49,0x49,0x49,0x36},{0x06,0x49,0x49,0x29,0x1E},{0x00,0x36,0x36,0x00,0x00},{0x00,0x56,0x36,0x00,0x00},\r\n{0x08,0x14,0x22,0x41,0x00},{0x14,0x14,0x14,0x14,0x14},{0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x51,0x09,0x06},\r\n{0x32,0x49,0x79,0x41,0x3E},{0x7E,0x11,0x11,0x11,0x7E},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22},\r\n{0x7F,0x41,0x41,0x22,0x1C},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x09,0x01},{0x3E,0x41,0x49,0x49,0x7A},\r\n{0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},{0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41},\r\n{0x7F,0x40,0x40,0x40,0x40},{0x7F,0x02,0x0C,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E},\r\n{0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46},{0x46,0x49,0x49,0x49,0x31},\r\n{0x01,0x01,0x7F,0x01,0x01},{0x3F,0x40,0x40,0x40,0x3F},{0x1F,0x20,0x40,0x20,0x1F},{0x3F,0x40,0x38,0x40,0x3F},\r\n{0x63,0x14,0x08,0x14,0x63},{0x07,0x08,0x70,0x08,0x07},{0x61,0x51,0x49,0x45,0x43},{0x00,0x7F,0x41,0x41,0x00},\r\n{0x02,0x04,0x08,0x10,0x20},{0x00,0x41,0x41,0x7F,0x00},{0x04,0x02,0x01,0x02,0x04},{0x40,0x40,0x40,0x40,0x40},\r\n{0x00,0x01,0x02,0x04,0x00},{0x20,0x54,0x54,0x54,0x78},{0x7F,0x48,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x20},\r\n{0x38,0x44,0x44,0x48,0x7F},{0x38,0x54,0x54,0x54,0x18},{0x08,0x7E,0x09,0x01,0x02},{0x0C,0x52,0x52,0x52,0x3E},\r\n{0x7F,0x08,0x04,0x04,0x78},{0x00,0x44,0x7D,0x40,0x00},{0x20,0x40,0x44,0x3D,0x00},{0x7F,0x10,0x28,0x44,0x00},\r\n{0x00,0x41,0x7F,0x40,0x00},{0x7C,0x04,0x18,0x04,0x78},{0x7C,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38},\r\n{0x7C,0x14,0x14,0x14,0x08},{0x08,0x14,0x14,0x18,0x7C},{0x7C,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x20},\r\n{0x04,0x3F,0x44,0x40,0x20},{0x3C,0x40,0x40,0x20,0x7C},{0x1C,0x20,0x40,0x20,0x1C},{0x3C,0x40,0x30,0x40,0x3C},\r\n{0x44,0x28,0x10,0x28,0x44},{0x0C,0x50,0x50,0x50,0x3C},{0x44,0x64,0x54,0x4C,0x44},\r\n\r\n\/\/ Extra symbols: degree (\u00b0), micro (\u00b5), plus-minus (\u00b1)\r\n{0x06,0x09,0x09,0x06,0x00}, \/\/ \u00b0 (176)\r\n{0x38,0x44,0x44,0x3C,0x40}, \/\/ \u00b5 (181)\r\n{0x10,0x54,0x7C,0x54,0x10} \/\/ \u00b1 (177)\r\n};\r\n\r\nconst uint8_t* chr = nullptr;\r\nif (c &gt;= 32 &amp;&amp; c &lt;= 126)\r\nchr = font5x7[c - 32];\r\nelse if (c == 176) \/\/ \u00b0\r\nchr = font5x7[95];\r\nelse if (c == 181) \/\/ \u00b5\r\nchr = font5x7[96];\r\nelse if (c == 177) \/\/ \u00b1\r\nchr = font5x7[97];\r\nelse\r\nreturn;\r\n\r\nfor (int col = 0; col &lt; 5; col++) {\r\nuint8_t line = chr[col];\r\nfor (int row = 0; row &lt; 7; row++) {\r\nif (line &amp; (1 &lt;&lt; row)) {\r\nint px = x + col;\r\nint py = y + row;\r\nif (px &gt;= 0 &amp;&amp; px &lt; w &amp;&amp; py &gt;= 0 &amp;&amp; py &lt; h)\r\nbuf[py * w + px] = color;\r\n}\r\n}\r\n}\r\n}\r\n\r\n\r\nvoid drawText(uint16_t* buf, int w, int h, int x, int y, const char* text, uint16_t color) {\r\nwhile (*text) {\r\ndrawChar(buf, w, h, x, y, *text++, color);\r\nx += 6;\r\n}\r\n}\r\n\r\nvoid drawMillisText(uint16_t* buf, int w, int h) {\r\nchar label[32];\r\nsprintf(label, \"Time: %lu\", millis());\r\nint len = strlen(label);\r\ndrawText(buf, w, h, w - len * 6 - 5, 5, label, 0x07E0); \/\/ Green\r\n}\r\n\r\n\r\n\/\/ ------------------- JPEG SAVE -------------------\r\nvoid saveAsJPEG(uint16_t* rgb565_buf, int w, int h, const char *path) {\r\nFile file = SD_MMC.open(path, FILE_WRITE);\r\nif (!file) {\r\nSerial.println(\"Failed to open file\");\r\nreturn;\r\n}\r\n\r\n\/\/ Convert RGB565 to RGB888\r\nuint8_t *rgb888_buf = (uint8_t*)ps_malloc(w * h * 3);\r\nif (!rgb888_buf) {\r\nSerial.println(\"RGB888 alloc failed\");\r\nreturn;\r\n}\r\n\r\nfor (int i = 0; i &lt; w * h; i++) {\r\nuint16_t pixel = rgb565_buf[i];\r\nuint8_t r = ((pixel &gt;&gt; 11) &amp; 0x1F) * 255 \/ 31;\r\nuint8_t g = ((pixel &gt;&gt; 5) &amp; 0x3F) * 255 \/ 63;\r\nuint8_t b = (pixel &amp; 0x1F) * 255 \/ 31;\r\n\r\nrgb888_buf[i * 3 + 0] = r;\r\nrgb888_buf[i * 3 + 1] = g;\r\nrgb888_buf[i * 3 + 2] = b;\r\n}\r\n\r\nuint8_t *jpg_buf = NULL;\r\nsize_t jpg_len = 0;\r\n\r\nif (!fmt2jpg(rgb888_buf, w * h * 3, w, h, PIXFORMAT_RGB888, 90, &amp;jpg_buf, &amp;jpg_len)) {\r\nSerial.println(\"JPEG encode failed\");\r\nfree(rgb888_buf);\r\nfile.close();\r\nreturn;\r\n}\r\n\r\nfile.write(jpg_buf, jpg_len);\r\nfile.close();\r\nfree(jpg_buf);\r\nfree(rgb888_buf);\r\n}<\/pre>\n<p>The video was generated from the images captured using the following bash script.<\/p>\n<pre>#!\/bin\/bash\r\n# make_video.sh\r\n\r\n# Directory containing image frames\r\nINPUT_DIR=\".\/microSD\" # &lt;-- Change to your desired folder\r\nOUTPUT_VIDEO=\"ESP32-Cam Telemetry Demo.mp4\"\r\nFRAMERATE=5 # Set desired FPS for the output video\r\n\r\n# Check if the input directory exists\r\nif [ ! -d \"$INPUT_DIR\" ]; then\r\necho \"Error: Directory $INPUT_DIR not found.\"\r\nexit 1\r\nfi\r\n\r\n# Create video from ordered image sequence\r\nffmpeg -framerate $FRAMERATE -pattern_type glob -i \"${INPUT_DIR}\/frame_sim_*.jpg\" \\\r\n-c:v libx264 -pix_fmt yuv420p -crf 23 -preset medium \"$OUTPUT_VIDEO\"\r\n\r\necho \"Video saved to $OUTPUT_VIDEO\"<\/pre>\n<p>Here is the demo video of the telemetry overlay.<\/p>\n<p><iframe loading=\"lazy\" title=\"ESP32-Cam Telemetry Demo\" src=\"https:\/\/player.vimeo.com\/video\/1087748362?dnt=1&amp;app_id=122963\" width=\"320\" height=\"240\" frameborder=\"0\" allow=\"autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media\"><\/iframe><\/p>\n<p>This is likely going to be a topic covered in future posts.\u00a0 Hopefully the experience won&#8217;t be like ordering a side of wheat toast.<\/p>\n<p><iframe loading=\"lazy\" title=\"Hold the Chicken - Five Easy Pieces (3\/8) Movie CLIP (1970) HD\" width=\"640\" height=\"360\" src=\"https:\/\/www.youtube.com\/embed\/hdIXrF34Bz0?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post will cover the ESP32 Cam module&#8217;s support for video capture.\u00a0 The module comes with a microSD slot and Omnivision camera interface.\u00a0 Earlier examples demonstrated time lapse video capture, but this post will focus more on real time video. This topic was found accidentally while researching I2S audio support.\u00a0 This Github repo by James Zahary, with contribution from Alexey Khachatryan, provided a realtime video capture process, https:\/\/github.com\/jameszah\/ESP32-CAM-Video-Recorder-junior.\u00a0 Unlike the more common triggered still or time lapse image captures demonstrated&#8230;<\/p>\n<p class=\"read-more\"><a class=\"btn btn-default\" href=\"https:\/\/www.cloudacm.com\/?p=4878\"> Read More<span class=\"screen-reader-text\">  Read More<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4878","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4878","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4878"}],"version-history":[{"count":25,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4878\/revisions"}],"predecessor-version":[{"id":4905,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4878\/revisions\/4905"}],"wp:attachment":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4878"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4878"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4878"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}