H.264 Codec for ESP32-S3
Loading...
Searching...
No Matches
H264Encoder.h
Go to the documentation of this file.
1#pragma once
2
3/**
4 * @file H264Encoder.h
5 * @brief Header-only helper to capture frames from ESP32 camera and encode to
6 * H.264
7 * @author esp_h264 library
8 */
9
10#include <Arduino.h>
11#include <esp_log.h>
12
13#include <vector>
14
15#include "H264Config.h"
16#include "esp_camera.h"
17#include "h264/esp_h264_enc_single.h"
18#include "h264/esp_h264_enc_single_sw.h"
19#include "h264/esp_h264_types.h"
20#include "h264/h264_color_convert.h"
21
22namespace esp_h264 {
23
24/**
25 * @class H264Encoder
26 * @brief Template class for capturing frames from ESP32 camera and encoding to
27 * H.264
28 *
29 * H264Encoder is a lightweight, header-only class that wraps camera
30 * initialization, pixel format conversions (RGB565/YUV422 → I420), H.264
31 * encoder lifecycle, and streaming to any Arduino Print implementation.
32 *
33 * The class is templated on an allocator type to allow customization of memory
34 * allocation strategy (e.g., PSRAM vs regular heap).
35 *
36 * @tparam Alloc Memory allocator type for internal buffers (defaults to
37 * PSRAMAllocatorH264)
38 *
39 * @note This class intentionally keeps implementation in the header for easy
40 * integration into Arduino sketches without separate .cpp files
41 * @note The pixel format conversions are straightforward and not highly
42 * optimized
43 * @note Requires ESP32 with camera module and sufficient memory for frame
44 * buffers
45 *
46 * Example usage:
47 * @code
48 * #include "H264Encoder.h"
49 * #include "UDPPrint.h"
50 *
51 * H264EncoderPSRAM streamer; // Uses PSRAM allocation (simpler than
52 * H264Encoder<>) UDPPrint udpOut;
53 *
54 * void setup() {
55 * auto cfg = streamer.defaultConfig();
56 * cfg.ssid = "MyWiFi";
57 * cfg.password = "MyPassword";
58 * cfg.width = 640;
59 * cfg.height = 480;
60 * cfg.fps = 15;
61 *
62 * udpOut.begin("192.168.1.100", 5000);
63 * if (!streamer.begin(cfg)) {
64 * Serial.println("Failed to start camera streamer");
65 * }
66 * }
67 *
68 * void loop() {
69 * streamer.captureH264(udpOut); // Captures, encodes, and streams
70 * udpOut.flush();
71 * }
72 * @endcode
73 */
74template <typename Alloc = H264_DEFAULT_ALLOCATOR>
76 public:
77 /** @brief Logging tag for ESP32 log system */
78 static constexpr const char* TAG = "H264Encoder";
79
80 /**
81 * @struct Config
82 * @brief Configuration structure for camera, WiFi, and encoder settings
83 *
84 * Contains all parameters needed to configure the camera module, WiFi
85 * connection, H.264 encoder, and pin assignments for ESP32 camera boards.
86 */
87 struct Config {
88 int width = 640; ///< Frame width in pixels
89 int height = 480; ///< Frame height in pixels
90 int fps = 15; ///< Target frames per second
92 400 * 1024; ///< Output buffer size for encoded H.264 data (bytes)
93
94 int gop = -1; ///< Group of Pictures (default: fps)
95 int bitrate = -1; ///< Bitrate in bps (default: width*height*fps*30/100)
96 int qp_min = -1; ///< Minimum quantizer parameter (default: 28)
97 int qp_max = -1; ///< Maximum quantizer parameter (default: 30)
98
99 bool use_camera = false; ///< Internal flag to track camera state
100 pixformat_t pixel_format = PIXFORMAT_YUV422; ///< Pixel format for camera capture
101
102 int pwdn_pin = -1; ///< Camera PWDN pin
103 int reset_pin = -1; ///< Camera RESET pin
104 int xclk_pin = -1; ///< Camera XCLK pin
105 int siod_pin = -1; ///< Camera SIOD pin
106 int sioc_pin = -1; ///< Camera SIOC pin
107 int y2_pin = -1; ///< Camera Y2 pin
108 int y3_pin = -1; ///< Camera Y3 pin
109 int y4_pin = -1; ///< Camera Y4 pin
110 int y5_pin = -1; ///< Camera Y5 pin
111 int y6_pin = -1; ///< Camera Y6 pin
112 int y7_pin = -1; ///< Camera Y7 pin
113 int y8_pin = -1; ///< Camera Y8 pin
114 int y9_pin = -1; ///< Camera Y9 pin
115 int vsync_pin = -1; ///< Camera VSYNC pin
116 int href_pin = -1; ///< Camera HREF pin
117 int pclk_pin = -1; ///< Camera PCLK pin
118 };
119
120 /**
121 * @brief Create a default configuration with reasonable defaults
122 *
123 * Returns a Config structure populated with default values suitable for
124 * ESP32-S3-EYE development boards. WiFi credentials are set to nullptr,
125 * requiring manual configuration or external WiFi management.
126 *
127 * @return Config Default configuration structure
128 *
129 * @note WiFi SSID and password are nullptr - set them manually or manage WiFi
130 * externally
131 * @note Pin assignments match the ESP32-S3-EYE development board layout
132 */
134 ESP_LOGD(TAG, "defaultConfig");
135 Config cfg;
136 cfg.width = 640;
137 cfg.height = 480;
138 cfg.fps = 15;
139 cfg.outBufferSize = 0;
140 cfg.pixel_format = PIXFORMAT_YUV422;
141 return cfg;
142 }
143
144 /**
145 * @brief Create a default configuration based on camera preset name
146 *
147 * Searches for a predefined camera preset that contains the specified name
148 * and returns a Config structure with the corresponding pin mappings and
149 * default settings. If no matching preset is found, returns the generic
150 * default configuration.
151 *
152 * @param camera Name of the camera preset
153 * @return Config Configuration structure for the specified camera preset
154 */
155 Config defaultConfig(const char* camera) {
156 ESP_LOGI(TAG, "defaultConfig for camera preset: %s", camera);
157 for (const auto& mapping : kFallbackMappings) {
158 if (strstr(mapping.name, camera) != nullptr) {
159 Config cfg = defaultConfig();
160 applyPinMapping(cfg, mapping);
161 ESP_LOGI(TAG, "Using default config for camera preset: %s", camera);
162 return cfg;
163 }
164 }
165 ESP_LOGW(TAG, "No preset found for camera: %s, using generic defaults",
166 camera);
167 return defaultConfig();
168 }
169
170 /**
171 * @brief Default constructor
172 *
173 * Creates an uninitialized H264Encoder. Call begin() to initialize
174 * WiFi, camera, and encoder resources.
175 */
176 H264Encoder() { ESP_LOGD(TAG, "H264Encoder"); }
177
178 /**
179 * @brief Destructor
180 *
181 * Automatically releases camera, encoder, and buffer resources by calling
182 * end().
183 *
184 * @note Safe to destroy without calling end() explicitly
185 */
187 ESP_LOGD(TAG, "~H264Encoder");
188 end();
189 }
190
191 /**
192 * @brief Initialize WiFi, camera and H.264 encoder
193 *
194 * Configures and initializes all subsystems needed for camera streaming:
195 * 1. WiFi connection (if credentials provided)
196 * 2. Camera module with specified pins and resolution
197 * 3. H.264 software encoder with specified bitrate and GOP
198 * 4. Internal frame buffers using the configured allocator
199 *
200 * @param cfg Configuration structure (passed by value to allow temporary
201 * objects)
202 * @return true if all initialization succeeded, false on any failure
203 *
204 * @note If WiFi credentials are nullptr, WiFi initialization is skipped
205 * @note Camera is configured for RGB565 pixel format internally
206 * @note Frame buffers are allocated using the template allocator (Alloc)
207 * @note On failure, any partially initialized resources are cleaned up
208 */
209 bool begin(Config cfg) {
210 ESP_LOGD(TAG, "begin");
211 sensor = nullptr;
212 // copy the provided config into the stored config_
213 cfg_ = cfg;
214 // init encoder
215 if (!initEncoder()) return false;
216 // init camera if requested
217 if (cfg_.use_camera) {
218 if (!initCamera()) return false;
219 }
220 return initData();
221 }
222
223 /**
224 * @brief Capture a single frame and write raw I420 data to buffer
225 *
226 * Captures one frame from the camera, converts it from the native format
227 * (RGB565 or YUV422) to I420 (YUV420 planar), and writes the raw pixel
228 * data to the provided buffer.
229 *
230 * @param dst Destination buffer for I420 pixel data
231 * @param dst_len Size of destination buffer in bytes
232 * @return true if frame capture and conversion succeeded, false otherwise
233 *
234 * @note Buffer must be at least width × height × 1.5 bytes for I420 data
235 * @note This is a lower-level method; most users should use captureH264()
236 * instead
237 * @note Caller is responsible for ensuring buffer is large enough
238 */
239 bool captureFrame(uint8_t* dst, size_t dst_len) {
240 ESP_LOGD(TAG, "captureFrame");
241 if (!dst) return false;
242 return captureFrameI420(dst, dst_len);
243 }
244
245 /**
246 * @brief Encode raw YUV422 frame data to H.264 and write to Print stream
247 *
248 * Converts YUV422 data to I420, encodes it, and writes to the Print stream.
249 * @param raw_data Pointer to YUV422 pixel data buffer
250 * @param raw_len Size of YUV422 data in bytes
251 * @param out Print stream to receive encoded H.264 data
252 * @return true if encoding and writing succeeded, false otherwise
253 */
254 bool encodeYUV422(const uint8_t* raw_data, size_t raw_len, Print& out) {
255 ESP_LOGD(TAG, "encodeYUV422: %u bytes", (unsigned)raw_len);
256 if (!enc_handle_) {
257 ESP_LOGE(TAG, "Invalid encoder handle in encodeYUV422");
258 return false;
259 }
260 if (!raw_data || raw_len == 0) {
261 ESP_LOGE(TAG,
262 "No data provided to encodeYUV422 (raw_data=%p, raw_len=%u)",
263 raw_data, (unsigned)raw_len);
264 return false;
265 }
266 if (in_buf_.empty()) {
267 ESP_LOGE(TAG, "Input buffer is empty in encodeYUV422");
268 return false;
269 }
270 if (out_buf_.empty()) {
271 ESP_LOGE(TAG, "Output buffer is empty in encodeYUV422");
272 return false;
273 }
274 // Convert YUV422 to I420 using color conversion utility
275 // Assume width and height from config
276 size_t expected = (size_t)cfg_.width * (size_t)cfg_.height * 2;
277 if (raw_len < expected) {
278 ESP_LOGE(
279 TAG,
280 "Insufficient data for encodeYUV422: expected %u bytes, got %u bytes",
281 (unsigned)expected, (unsigned)raw_len);
282 return false;
283 }
284 // Use optimized color conversion functions from h264_color_convert.h
285#ifdef HAVE_ESP32S3
286 yuyv2iyuv_esp32s3((uint32_t)cfg_.height, (uint32_t)cfg_.width,
287 (uint8_t*)raw_data, in_buf_.data());
288#else
289 yuyv2iyuv((uint32_t)cfg_.height, (uint32_t)cfg_.width, (uint8_t*)raw_data,
290 in_buf_.data());
291#endif
292 // Now encode the I420 data
293 return encode(in_buf_.data(), in_buf_.size(), out);
294 }
295
296 /**
297 * @brief Encode raw RGB565 frame data to H.264 and write to Print stream
298 *
299 * Converts RGB565 data to I420, encodes it, and writes to the Print stream.
300 * @param raw_data Pointer to RGB565 pixel data buffer
301 * @param raw_len Size of RGB565 data in bytes
302 * @param out Print stream to receive encoded H.264 data
303 * @return true if encoding and writing succeeded, false otherwise
304 */
305 bool encodeRGB565(const uint8_t* raw_data, size_t raw_len, Print& out) {
306 ESP_LOGD(TAG, "encodeRGB565: %u bytes", (unsigned)raw_len);
307 if (!enc_handle_) {
308 ESP_LOGE(TAG, "Invalid encoder handle in encodeRGB565");
309 return false;
310 }
311 if (!raw_data || raw_len == 0) {
312 ESP_LOGE(TAG,
313 "No data provided to encodeRGB565 (raw_data=%p, raw_len=%u)",
314 raw_data, (unsigned)raw_len);
315 return false;
316 }
317 if (in_buf_.empty()) {
318 ESP_LOGE(TAG, "Input buffer is empty in encodeRGB565");
319 return false;
320 }
321 if (out_buf_.empty()) {
322 ESP_LOGE(TAG, "Output buffer is empty in encodeRGB565");
323 return false;
324 }
325 size_t expected = (size_t)cfg_.width * (size_t)cfg_.height * 2;
326 if (raw_len < expected) {
327 ESP_LOGE(
328 TAG,
329 "Insufficient data for encodeRGB565: expected %u bytes, got %u bytes",
330 (unsigned)expected, (unsigned)raw_len);
331 return false;
332 }
333 size_t expected_i420 = (size_t)cfg_.width * (size_t)cfg_.height * 3 / 2;
334 if (in_buf_.size() < expected_i420) {
335 ESP_LOGE(TAG,
336 "Input buffer too small for I420: need %u bytes, got %u bytes",
337 (unsigned)expected_i420, (unsigned)in_buf_.size());
338 return false;
339 }
340 ESP_LOGI(TAG,
341 "Converting RGB565 to I420 for encoding: input %u bytes, output "
342 "buffer %u bytes",
343 (unsigned)raw_len, (unsigned)in_buf_.size());
344
345 rgb565_to_i420(raw_data, in_buf_.data(), cfg_.width, cfg_.height);
346 return encode(in_buf_.data(), expected_i420, out);
347 }
348
349 /**
350 * @brief Encode raw I420 frame data to H.264 and write to Print stream
351 *
352 * Takes raw I420 pixel data, encodes it using the H.264 software encoder,
353 * and writes the resulting NAL units to the provided Print stream.
354 * Handles partial writes with retry logic.
355 *
356 * @param raw_data Pointer to I420 pixel data buffer
357 * @param raw_len Size of I420 data in bytes
358 * @param out Print stream to receive encoded H.264 data
359 * @return true if encoding and writing succeeded, false otherwise
360 *
361 * @note Input data must be valid I420 format with correct dimensions
362 * @note Implements retry logic for partial Print::write() operations
363 * @note This is a lower-level method; most users should use captureH264()
364 * instead
365 */
366 bool encode(const uint8_t* raw_data, size_t raw_len, Print& out) {
367 ESP_LOGD(TAG, "encode: %u bytes", (unsigned)raw_len);
368 if (!enc_handle_) {
369 ESP_LOGE(TAG, "Invalid encoder handle in encode()");
370 return false;
371 }
372 if (!raw_data || raw_len == 0) {
373 ESP_LOGE(TAG, "Invalid input data for encode(): raw_data=%p, raw_len=%u",
374 raw_data, (unsigned)raw_len);
375 return false;
376 }
377 if (out_buf_.empty()) {
378 ESP_LOGE(TAG, "Output buffer is empty in encode()");
379 return false;
380 }
381
382 esp_h264_enc_in_frame_t in_frame{};
383 esp_h264_enc_out_frame_t out_frame{};
384 in_frame.raw_data.buffer = (uint8_t*)raw_data;
385 in_frame.raw_data.len = raw_len;
386 out_frame.raw_data.buffer = out_buf_.data();
387 out_frame.raw_data.len = out_buf_.size();
388
389 ESP_LOGD(
390 TAG,
391 "Starting encoding: input size=%u bytes, output buffer size=%u bytes",
392 (unsigned)raw_len, (unsigned)out_buf_.size());
393
394 esp_h264_err_t r = esp_h264_enc_process(enc_handle_, &in_frame, &out_frame);
395 if (r != ESP_H264_ERR_OK) {
396 ESP_LOGE(TAG, "Encoding failed with error code: %d", r);
397 return false;
398 }
399
400 size_t written = 0;
401 if (out_frame.length > 0) {
402 const uint8_t* buf = out_frame.raw_data.buffer;
403 size_t to_write = out_frame.length;
404 const int max_retries = 5;
405 int attempts = 0;
406 while (written < to_write && attempts < max_retries) {
407 size_t n = out.write(buf + written, to_write - written);
408 if (n > 0) {
409 written += n;
410 } else {
411 // if nothing was written, wait a bit before retrying to avoid busy
412 // loop
413 delay(5);
414 }
415 ++attempts;
416 }
417 if (written < to_write) {
418 ESP_LOGW(TAG, "Partial write: expected %u bytes, wrote %u bytes",
419 (unsigned)to_write, (unsigned)written);
420 return false;
421 }
422 }
423 ESP_LOGI(TAG, "Encoded frame: i420=%u bytes, h264=%u bytes",
424 (unsigned)in_frame.raw_data.len, (unsigned)out_frame.length);
425
426 return (r == ESP_H264_ERR_OK);
427 }
428
429 /**
430 * @brief Capture frame, encode to H.264, and stream with frame rate control
431 *
432 * High-level method that performs the complete streaming pipeline:
433 * 1. Captures one frame from the camera
434 * 2. Converts pixel format to I420 if needed
435 * 3. Encodes frame using H.264 software encoder
436 * 4. Writes encoded data to the provided Print stream
437 * 5. Enforces configured frame rate by delaying remaining time
438 *
439 * This method measures processing time and only delays for the remainder
440 * of the target frame interval, ensuring consistent frame rate regardless
441 * of processing time variations.
442 *
443 * @param out Print stream to receive encoded H.264 NAL units
444 * @return true if capture, encoding, and streaming succeeded, false otherwise
445 *
446 * @note Frame rate is controlled by Config::fps setting
447 * @note Processing time is subtracted from frame interval delay
448 * @note This is the main method most applications should use
449 * @note On failure, no delay is applied (to avoid blocking on errors)
450 */
451 bool captureH264(Print& out) {
452 ESP_LOGD(TAG, "captureH264");
453 // basic guards
454 if (in_buf_.empty()) return false;
455 if (!enc_handle_) return false;
456
457 // compute target frame interval in ms
458 unsigned long frame_ms = (cfg_.fps > 0) ? (1000u / (unsigned)cfg_.fps) : 0u;
459 unsigned long start = millis();
460
461 // capture into the input buffer
462 if (!captureFrameI420(in_buf_.data(), in_buf_.size())) {
463 return false;
464 }
465
466 // encode from the input buffer to the output buffer, writing to `out`
467 if (!encode(in_buf_.data(), in_buf_.size(), out)) {
468 return false;
469 }
470
471 // enforce configured frame rate: delay only remaining time after processing
472 if (frame_ms > 0) {
473 unsigned long elapsed = millis() - start;
474 if (elapsed < frame_ms) delay(frame_ms - elapsed);
475 }
476
477 return true;
478 }
479
480 /**
481 * @brief Cleanup and release camera resources
482 *
483 * Deinitializes the ESP32 camera driver and releases all allocated
484 * resources. Should be called when streaming is complete or when
485 * switching camera configurations.
486 *
487 * After calling end(), the camera becomes available for other
488 * applications or for reconfiguration with different settings.
489 *
490 * @note Safe to call multiple times
491 * @note Required before calling begin() with different Config
492 * @note Automatically called by destructor
493 */
494 void end() {
495 ESP_LOGD(TAG, "end");
496 sensor = nullptr;
497 if (enc_handle_) {
498 esp_h264_enc_close(enc_handle_);
499 esp_h264_enc_del(enc_handle_);
500 enc_handle_ = nullptr;
501 }
502 if (!in_buf_.empty()) {
503 in_buf_.clear();
504 }
505 if (!out_buf_.empty()) {
506 out_buf_.clear();
507 }
508 }
509
510 sensor_t* getSensor() { return sensor; }
511
512 private:
513 struct PinMapping {
514 const char* name;
515 int pwdn_pin;
516 int reset_pin;
517 int xclk_pin;
518 int siod_pin;
519 int sioc_pin;
520 int y2_pin;
521 int y3_pin;
522 int y4_pin;
523 int y5_pin;
524 int y6_pin;
525 int y7_pin;
526 int y8_pin;
527 int y9_pin;
528 int vsync_pin;
529 int href_pin;
530 int pclk_pin;
531 };
532 static constexpr PinMapping kFallbackMappings[] = {
533 {"ESP32-S3-N16R8-CAM", -1, -1, 15, 4, 5, 11, 9, 8, 10, 12, 18, 17, 16, 6,
534 7, 13},
535 {"ESP32-S3-CAM", 38, -1, 15, 4, 5, 11, 9, 8, 10, 12, 18, 17, 16, 6, 7,
536 13},
537 {"ESP32-S3-GOOUUU", -1, -1, 15, 4, 5, 11, 9, 8, 10, 12, 18, 17, 16, 6, 7,
538 13},
539 {"LILYGO-TCAM-PLUS-S3-V1.2", 4, -1, 7, 1, 2, 12, 14, 11, 9, 8, 6, 15, 13,
540 3, 5, 10},
541 {"LILYGO-TCAM-PLUS-S3-V1.0", -1, 3, 7, 1, 2, 12, 14, 11, 13, 8, 6, 15, 9,
542 4, 5, 10},
543 {"DFROBOT-FIREBEETLE2", -1, -1, 15, 4, 5, 11, 9, 8, 10, 12, 18, 17, 16, 6,
544 7, 13},
545 {"ESP32-S3-12K", 2, 21, 38, 4, 11, 6, 5, 8, 10, 12, 18, 17, 39, 13, 14,
546 7},
547 {"ESP32-S3-XIAO", -1, -1, 10, 40, 39, 15, 17, 18, 16, 14, 12, 11, 48, 38,
548 47, 13},
549 {"ESP32-S3-CAM-TS", -1, -1, 33, 37, 36, 7, 5, 4, 6, 8, 42, 48, 47, 35, 34,
550 41},
551 {"GENERIC_S3_CAM", -1, -1, 40, 17, 18, 39, 41, 42, 12, 3, 14, 47, 13, 21,
552 38, 11},
553 {"ESP32-S3-KORVO", -1, -1, 40, 17, 18, 13, 47, 14, 3, 12, 42, 41, 39, 21,
554 38, 11},
555 };
556
557 Config cfg_;
558 esp_h264_enc_handle_t enc_handle_ = nullptr;
559 std::vector<uint8_t, Alloc> in_buf_;
560 std::vector<uint8_t, Alloc> out_buf_;
561 sensor_t* sensor = nullptr;
562
563 /**
564 * @brief Initialize ESP32 camera with Config pin mappings and settings
565 * @return true if camera initialization successful and buffers allocated,
566 * false on error
567 * @note Allocates I420 conversion buffer (width×height×1.5 bytes) and output
568 * buffer using configured allocator
569 * @note Automatically selects framesize based on Config::width and
570 * Config::height
571 * @note Sets RGB565 pixel format for color conversion compatibility
572 */
573 bool initCamera() {
574 ESP_LOGD(TAG, "initCamera");
575 // choose framesize from requested width/height; default to VGA
576 auto pick_frame_size = [](int w, int h) -> framesize_t {
577 if (w == 160 && h == 120) return FRAMESIZE_QQVGA;
578 if (w == 320 && h == 240) return FRAMESIZE_QVGA;
579 if (w == 640 && h == 480) return FRAMESIZE_VGA;
580 if (w == 800 && h == 600) return FRAMESIZE_SVGA;
581 if (w == 1024 && h == 768) return FRAMESIZE_XGA;
582 // fallback
583 ESP_LOGI(TAG, "Unsupported resolution %dx%d, defaulting to VGA", w, h);
584 return FRAMESIZE_VGA;
585 };
586 framesize_t fs = pick_frame_size(cfg_.width, cfg_.height);
587 if (!tryInitCamera(fs, cfg_, "current-config")) {
588 for (const auto& mapping : kFallbackMappings) {
589 Config fallback = cfg_;
590 applyPinMapping(fallback, mapping);
591 if (samePins(cfg_, fallback)) {
592 continue;
593 }
594 if (tryInitCamera(fs, fallback, mapping.name)) {
595 cfg_ = fallback;
596 break;
597 }
598 }
599
600 if (!cameraInitialized()) {
601 ESP_LOGE(TAG,
602 "esp_camera_init failed for all known ESP32-S3 camera pin "
603 "mappings");
604 ESP_LOGE(
605 TAG,
606 "Likely causes: unsupported camera sensor in esp_camera, "
607 "custom/non-standard ESP32-S3 board pinout, or incorrect wiring");
608 return false;
609 }
610 }
611 ESP_LOGD(TAG, "Camera initialized successfully");
612
613 sensor = esp_camera_sensor_get();
614 if (sensor) {
615 ESP_LOGI(TAG, "PID: 0x%02X", sensor->id.PID);
616 sensor->set_framesize(sensor, fs);
617 }
618
619 return true;
620 }
621
622 /**
623 * @brief Initialize internal buffers
624 */
625 bool initData() {
626 // allocate buffers sized for cfg_.width/height
627 size_t in_len = (size_t)cfg_.width * (size_t)cfg_.height * 3 / 2;
628 size_t out_len = cfg_.outBufferSize > 0 ? cfg_.outBufferSize : in_len;
629
630 try {
631 in_buf_.clear();
632 out_buf_.clear();
633 in_buf_.resize(in_len);
634 out_buf_.resize(out_len);
635 } catch (const std::bad_alloc& e) {
636 ESP_LOGE(TAG, "Failed to allocate buffers: %s", e.what());
637 in_buf_.clear();
638 out_buf_.clear();
639 return false;
640 }
641 return true;
642 }
643
644 void applyPinMapping(Config& config, const PinMapping& mapping) const {
645 config.pwdn_pin = mapping.pwdn_pin;
646 config.reset_pin = mapping.reset_pin;
647 config.xclk_pin = mapping.xclk_pin;
648 config.siod_pin = mapping.siod_pin;
649 config.sioc_pin = mapping.sioc_pin;
650 config.y2_pin = mapping.y2_pin;
651 config.y3_pin = mapping.y3_pin;
652 config.y4_pin = mapping.y4_pin;
653 config.y5_pin = mapping.y5_pin;
654 config.y6_pin = mapping.y6_pin;
655 config.y7_pin = mapping.y7_pin;
656 config.y8_pin = mapping.y8_pin;
657 config.y9_pin = mapping.y9_pin;
658 config.vsync_pin = mapping.vsync_pin;
659 config.href_pin = mapping.href_pin;
660 config.pclk_pin = mapping.pclk_pin;
661 }
662
663 bool cameraInitialized() const { return esp_camera_sensor_get() != nullptr; }
664
665 static constexpr unsigned long kCameraRetryDelayMs = 80;
666
667 bool tryInitCamera(framesize_t fs, const Config& camera_cfg,
668 const char* preset_name = nullptr) {
669 if (camera_cfg.y2_pin == -1 || camera_cfg.y3_pin == -1 ||
670 camera_cfg.y4_pin == -1 || camera_cfg.y5_pin == -1 ||
671 camera_cfg.y6_pin == -1 || camera_cfg.y7_pin == -1 ||
672 camera_cfg.y8_pin == -1 || camera_cfg.y9_pin == -1 ||
673 camera_cfg.xclk_pin == -1 || camera_cfg.pclk_pin == -1 ||
674 camera_cfg.vsync_pin == -1 || camera_cfg.href_pin == -1 ||
675 camera_cfg.siod_pin == -1 || camera_cfg.sioc_pin == -1) {
676 return false;
677 }
678 if (preset_name && preset_name[0] != '\0') {
679 ESP_LOGI(TAG, "Trying camera preset: %s", preset_name);
680 }
681 ESP_LOGI(
682 TAG,
683 "Trying camera pins: pwdn=%d reset=%d xclk=%d siod=%d sioc=%d y2=%d "
684 "y3=%d y4=%d y5=%d y6=%d y7=%d y8=%d y9=%d vsync=%d href=%d pclk=%d",
685 camera_cfg.pwdn_pin, camera_cfg.reset_pin, camera_cfg.xclk_pin,
686 camera_cfg.siod_pin, camera_cfg.sioc_pin, camera_cfg.y2_pin,
687 camera_cfg.y3_pin, camera_cfg.y4_pin, camera_cfg.y5_pin,
688 camera_cfg.y6_pin, camera_cfg.y7_pin, camera_cfg.y8_pin,
689 camera_cfg.y9_pin, camera_cfg.vsync_pin, camera_cfg.href_pin,
690 camera_cfg.pclk_pin);
691
692 camera_config_t config{};
693 config.ledc_channel = LEDC_CHANNEL_0;
694 config.ledc_timer = LEDC_TIMER_0;
695 config.pin_d0 = camera_cfg.y2_pin;
696 config.pin_d1 = camera_cfg.y3_pin;
697 config.pin_d2 = camera_cfg.y4_pin;
698 config.pin_d3 = camera_cfg.y5_pin;
699 config.pin_d4 = camera_cfg.y6_pin;
700 config.pin_d5 = camera_cfg.y7_pin;
701 config.pin_d6 = camera_cfg.y8_pin;
702 config.pin_d7 = camera_cfg.y9_pin;
703 config.pin_xclk = camera_cfg.xclk_pin;
704 config.pin_pclk = camera_cfg.pclk_pin;
705 config.pin_vsync = camera_cfg.vsync_pin;
706 config.pin_href = camera_cfg.href_pin;
707 config.pin_sccb_sda = camera_cfg.siod_pin;
708 config.pin_sccb_scl = camera_cfg.sioc_pin;
709 config.pin_pwdn = camera_cfg.pwdn_pin;
710 config.pin_reset = camera_cfg.reset_pin;
711 config.xclk_freq_hz = 20000000;
712 config.frame_size = fs;
713 config.pixel_format = cfg_.pixel_format;
714 config.fb_count = 1;
715 // config.fb_location = CAMERA_FB_IN_DRAM;
716 config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
717 esp_err_t err = esp_camera_init(&config);
718 if (err == ESP_OK) {
719 sensor = esp_camera_sensor_get();
720 ESP_LOGI(TAG, "PID: 0x%02X", sensor->id.PID);
721 return true;
722 }
723
724 ESP_LOGW(TAG, "esp_camera_init '%s' failed with error 0x%x",
725 preset_name ? preset_name : "unknown", (unsigned)err);
726 esp_camera_deinit();
727 delay(kCameraRetryDelayMs);
728 return false;
729 }
730
731 bool samePins(const Config& lhs, const Config& rhs) const {
732 return lhs.pwdn_pin == rhs.pwdn_pin && lhs.reset_pin == rhs.reset_pin &&
733 lhs.xclk_pin == rhs.xclk_pin && lhs.siod_pin == rhs.siod_pin &&
734 lhs.sioc_pin == rhs.sioc_pin && lhs.y2_pin == rhs.y2_pin &&
735 lhs.y3_pin == rhs.y3_pin && lhs.y4_pin == rhs.y4_pin &&
736 lhs.y5_pin == rhs.y5_pin && lhs.y6_pin == rhs.y6_pin &&
737 lhs.y7_pin == rhs.y7_pin && lhs.y8_pin == rhs.y8_pin &&
738 lhs.y9_pin == rhs.y9_pin && lhs.vsync_pin == rhs.vsync_pin &&
739 lhs.href_pin == rhs.href_pin && lhs.pclk_pin == rhs.pclk_pin;
740 }
741
742 /**
743 * @brief Initialize H.264 software encoder with Config settings
744 * @return true if encoder creation and opening successful, false on error
745 * @note Automatically calculates bitrate as width×height×fps÷20
746 * @note Sets GOP (Group of Pictures) to 2×fps for balanced
747 * compression/quality
748 * @note Uses esp_h264 software encoder (not hardware acceleration)
749 */
750 bool initEncoder() {
751 ESP_LOGD(TAG, "initEncoder");
752
753 esp_h264_enc_cfg_sw_t enc_cfg{};
754 enc_cfg.res.width = (uint16_t)cfg_.width;
755 enc_cfg.res.height = (uint16_t)cfg_.height;
756 enc_cfg.fps = (uint8_t)cfg_.fps;
757 enc_cfg.gop = (cfg_.gop > 0) ? (uint8_t)cfg_.gop : (uint8_t)cfg_.fps;
759 enc_cfg.rc.bitrate =
760 (cfg_.bitrate > 0)
761 ? (uint32_t)cfg_.bitrate
762 : (uint32_t)(cfg_.width * cfg_.height * cfg_.fps * 30 / 100);
763 enc_cfg.rc.qp_min = (cfg_.qp_min >= 0) ? (uint8_t)cfg_.qp_min : 28;
764 enc_cfg.rc.qp_max = (cfg_.qp_max >= 0) ? (uint8_t)cfg_.qp_max : 30;
765
766 ESP_LOGI(TAG,
767 "Encoder settings: width=%u, height=%u, fps=%u, gop=%u, "
768 "pic_type=0x%08X, bitrate=%u, qp_min=%u, qp_max=%u",
769 (unsigned)enc_cfg.res.width, (unsigned)enc_cfg.res.height,
770 (unsigned)enc_cfg.fps, (unsigned)enc_cfg.gop,
771 (unsigned)enc_cfg.pic_type, (unsigned)enc_cfg.rc.bitrate,
772 (unsigned)enc_cfg.rc.qp_min, (unsigned)enc_cfg.rc.qp_max);
773
774 esp_h264_err_t res = esp_h264_enc_sw_new(&enc_cfg, &enc_handle_);
775 if (res != ESP_H264_ERR_OK || !enc_handle_) {
776 ESP_LOGE(TAG, "esp_h264_enc_sw_new failed: %d", (int)res);
777 return false;
778 }
779 res = esp_h264_enc_open(enc_handle_);
780 if (res != ESP_H264_ERR_OK) {
781 ESP_LOGE(TAG, "esp_h264_enc_open failed: %d", (int)res);
782 return false;
783 }
784 return true;
785 }
786
787 /**
788 * @brief Capture one frame and convert to I420 (YUV420 planar) format
789 *
790 * Captures frame from ESP32 camera and converts from the camera's native
791 * pixel format to I420 format required by H.264 encoder. Supports RGB565
792 * and YUV422 input formats with automatic format detection.
793 *
794 * @param dst Destination buffer for I420 data (must be width×height×1.5
795 * bytes)
796 * @param dst_len Size of destination buffer in bytes
797 * @return true if frame captured and converted successfully, false on error
798 *
799 * @note I420 format: Y plane (width×height), U plane (width/2×height/2), V
800 * plane (width/2×height/2)
801 * @note Automatically returns camera framebuffer after conversion
802 * @note Logs error for unsupported pixel formats
803 */
804 bool captureFrameI420(uint8_t* dst, size_t dst_len) {
805 ESP_LOGD(TAG, "captureFrameI420");
806 if (!dst) return false;
807 camera_fb_t* fb = esp_camera_fb_get();
808 if (!fb) return false;
809
810 int w = fb->width;
811 int h = fb->height;
812 size_t expected = (size_t)w * (size_t)h * 3 / 2;
813 if (dst_len < expected) {
814 ESP_LOGE(TAG, "Destination buffer too small: need %u got %u",
815 (unsigned)expected, (unsigned)dst_len);
816 esp_camera_fb_return(fb);
817 return false;
818 }
819
820 bool res = false;
821 switch (fb->format) {
822 case PIXFORMAT_RGB565:
823 res = captureFrameI420_RGB565(dst, dst_len, fb, w, h);
824 break;
825 case PIXFORMAT_YUV422:
826 res = captureFrameI420_YUV422(dst, dst_len, fb, w, h);
827 break;
828 default:
829 ESP_LOGE(TAG, "Unsupported camera pixel format: %d", fb->format);
830 res = false;
831 esp_camera_fb_return(fb);
832 break;
833 }
834
835 return res;
836 }
837
838 /**
839 * @brief Convert raw RGB565 buffer to I420 (YUV420 planar) format
840 * @param src Pointer to RGB565 input buffer
841 * @param dst Pointer to I420 output buffer
842 * @param w Frame width in pixels
843 * @param h Frame height in pixels
844 */
845 void rgb565_to_i420(const uint8_t* src, uint8_t* dst, int w, int h) {
846 // --- Y plane ---
847 for (int y = 0; y < h; ++y) {
848 for (int x = 0; x < w; ++x) {
849 int i = (y * w + x) * 2;
850 // little endien
851 uint16_t val = src[i] | (src[i + 1] << 8);
852 // big endian
853 //uint16_t val = (src[i] << 8) | src[i + 1];
854
855 // Better RGB expansion
856 uint8_t r = ((val >> 11) & 0x1F);
857 uint8_t g = ((val >> 5) & 0x3F);
858 uint8_t b = (val & 0x1F);
859
860 r = (r << 3) | (r >> 2);
861 g = (g << 2) | (g >> 4);
862 b = (b << 3) | (b >> 2);
863
864 int yv = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
865 dst[y * w + x] = (uint8_t)std::clamp(yv, 0, 255);
866 }
867 }
868
869 uint8_t* uplane = dst + w * h;
870 uint8_t* vplane = uplane + (w / 2) * (h / 2);
871
872 // uint8_t* vplane = dst + w * h;
873 // uint8_t* uplane = vplane + (w / 2) * (h / 2);
874
875 // --- UV planes (4:2:0 subsampling) ---
876 for (int y = 0; y < h; y += 2) {
877 for (int x = 0; x < w; x += 2) {
878 int sumU = 0, sumV = 0;
879
880 for (int dy = 0; dy < 2; ++dy) {
881 for (int dx = 0; dx < 2; ++dx) {
882 int i = ((y + dy) * w + (x + dx)) * 2;
883 uint16_t val = src[i] | (src[i + 1] << 8);
884
885 uint8_t r = ((val >> 11) & 0x1F);
886 uint8_t g = ((val >> 5) & 0x3F);
887 uint8_t b = (val & 0x1F);
888
889 r = (r << 3) | (r >> 2);
890 g = (g << 2) | (g >> 4);
891 b = (b << 3) | (b >> 2);
892
893 int u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
894 int v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
895
896 sumU += u;
897 sumV += v;
898 }
899 }
900
901 int ux = x / 2;
902 int uy = y / 2;
903 int pos = uy * (w / 2) + ux;
904
905 // Clamp AFTER averaging
906 uplane[pos] = (uint8_t)std::clamp(sumU / 4, 0, 255);
907 vplane[pos] = (uint8_t)std::clamp(sumV / 4, 0, 255);
908 }
909 }
910 }
911
912 /**
913 * @brief Convert RGB565 framebuffer to I420 format
914 *
915 * Converts ESP32 camera RGB565 framebuffer to I420 (YUV420 planar) format.
916 * Performs RGB to YUV color space conversion with proper scaling and
917 * subsampling.
918 *
919 * @param dst Destination I420 buffer
920 * @param dst_len Size of destination buffer (unused, validated by caller)
921 * @param fb Camera framebuffer containing RGB565 data
922 * @param w Frame width in pixels
923 * @param h Frame height in pixels
924 * @return true on successful conversion, false on error
925 *
926 * @note Y plane: full resolution luminance
927 * @note U/V planes: 2×2 subsampled chrominance (quarter resolution)
928 * @note Automatically releases framebuffer after conversion
929 */
930 bool captureFrameI420_RGB565(uint8_t* dst, size_t /*dst_len*/,
931 camera_fb_t* fb, int w, int h) {
932 ESP_LOGD(TAG, "captureFrameI420_RGB565");
933 rgb565_to_i420(fb->buf, dst, w, h);
934 esp_camera_fb_return(fb);
935 return true;
936 }
937
938 /**
939 * @brief Convert YUV422/YUYV framebuffer to I420 format using optimized
940 * functions
941 *
942 * Converts ESP32 camera YUV422 framebuffer to I420 (YUV420 planar) format
943 * using the optimized color conversion functions from h264_color_convert.h.
944 * On ESP32-S3, uses assembly-optimized yuyv2iyuv_esp32s3(), otherwise
945 * uses the standard yuyv2iyuv() function.
946 *
947 * @param dst Destination I420 buffer
948 * @param dst_len Size of destination buffer (unused, validated by caller)
949 * @param fb Camera framebuffer containing YUV422/YUYV data
950 * @param w Frame width in pixels
951 * @param h Frame height in pixels
952 * @return true on successful conversion, false on error
953 *
954 * @note YUV422 format: Y0 U0 Y1 V0 (4 bytes for 2 pixels)
955 * @note I420 format: Y plane + U plane + V plane (separate planes)
956 * @note Uses hardware-optimized assembly on ESP32-S3 when available
957 * @note Automatically releases framebuffer after conversion
958 */
959 bool captureFrameI420_YUV422(uint8_t* dst, size_t /*dst_len*/,
960 camera_fb_t* fb, int w, int h) {
961 ESP_LOGD(TAG, "captureFrameI422_YUV422");
962 uint8_t* p = fb->buf;
963
964 // Use optimized color conversion functions from h264_color_convert.h
965#ifdef HAVE_ESP32S3
966 // Use assembly-optimized version on ESP32-S3
967 yuyv2iyuv_esp32s3((uint32_t)h, (uint32_t)w, p, dst);
968#else
969 // Use standard optimized version on other platforms
970 yuyv2iyuv((uint32_t)h, (uint32_t)w, p, dst);
971#endif
972
973 esp_camera_fb_return(fb);
974 return true;
975 }
976};
977
978/**
979 * @brief Type alias for H264Encoder using default PSRAM allocation
980 *
981 * Provides a simpler way to declare H264Encoder instances without specifying
982 * the allocator template parameter. Uses PSRAMAllocatorH264<uint8_t> by
983 * default.
984 *
985 * Example usage:
986 * @code
987 * H264EncoderPSRAM encoder; // Instead of
988 * H264Encoder<PSRAMAllocatorH264<uint8_t>>
989 * @endcode
990 */
991using H264EncoderPSRAM = H264Encoder<PSRAMAllocatorH264<uint8_t>>;
992
993/**
994 * @brief Type alias for H264Encoder using internal RAM allocation
995 *
996 * Provides a simpler way to declare H264Encoder instances that use fast
997 * internal RAM instead of external PSRAM.
998 *
999 * Example usage:
1000 * @code
1001 * H264EncoderRAM encoder; // Instead of
1002 * H264Encoder<RAMAllocatorH264<uint8_t>>
1003 * @endcode
1004 */
1005using H264EncoderRAM = H264Encoder<RAMAllocatorH264<uint8_t>>;
1006
1007} // namespace esp_h264
1008
1009#ifdef ARDUINO
1010using namespace esp_h264;
1011#endif
Template class for capturing frames from ESP32 camera and encoding to H.264.
Definition: H264Encoder.h:75
bool captureH264(Print &out)
Capture frame, encode to H.264, and stream with frame rate control.
Definition: H264Encoder.h:451
H264Encoder()
Default constructor.
Definition: H264Encoder.h:176
bool encodeYUV422(const uint8_t *raw_data, size_t raw_len, Print &out)
Encode raw YUV422 frame data to H.264 and write to Print stream.
Definition: H264Encoder.h:254
sensor_t * getSensor()
Definition: H264Encoder.h:510
bool encodeRGB565(const uint8_t *raw_data, size_t raw_len, Print &out)
Encode raw RGB565 frame data to H.264 and write to Print stream.
Definition: H264Encoder.h:305
bool begin(Config cfg)
Initialize WiFi, camera and H.264 encoder.
Definition: H264Encoder.h:209
void end()
Cleanup and release camera resources.
Definition: H264Encoder.h:494
bool captureFrame(uint8_t *dst, size_t dst_len)
Capture a single frame and write raw I420 data to buffer.
Definition: H264Encoder.h:239
Config defaultConfig()
Create a default configuration with reasonable defaults.
Definition: H264Encoder.h:133
Config defaultConfig(const char *camera)
Create a default configuration based on camera preset name.
Definition: H264Encoder.h:155
static constexpr const char * TAG
Logging tag for ESP32 log system.
Definition: H264Encoder.h:78
~H264Encoder()
Destructor.
Definition: H264Encoder.h:186
bool encode(const uint8_t *raw_data, size_t raw_len, Print &out)
Encode raw I420 frame data to H.264 and write to Print stream.
Definition: H264Encoder.h:366
esp_h264_err_t esp_h264_enc_close(esp_h264_enc_handle_t enc)
This function closes the H.264 encoder instance specified by enc
Definition: esp_h264_enc_single.c:28
esp_h264_err_t esp_h264_enc_open(esp_h264_enc_handle_t enc)
This function opens an H.264 encoder in single stream.
Definition: esp_h264_enc_single.c:12
esp_h264_err_t esp_h264_enc_process(esp_h264_enc_handle_t enc, esp_h264_enc_in_frame_t *in_frame, esp_h264_enc_out_frame_t *out_frame)
This function performs single encoding of H.264 video frames. To encode one image using an image enco...
Definition: esp_h264_enc_single.c:19
esp_h264_err_t esp_h264_enc_del(esp_h264_enc_handle_t enc)
This function is used to delete an H.264 encoder.
Definition: esp_h264_enc_single.c:35
struct esp_h264_enc_if * esp_h264_enc_handle_t
H.264 single stream encoder handle.
Definition: esp_h264_enc_single.h:19
esp_h264_err_t esp_h264_enc_sw_new(const esp_h264_enc_cfg_sw_t *cfg, esp_h264_enc_handle_t *out_enc)
This function is used to create a new instance of the esp_h264_enc_t data structure,...
Definition: esp_h264_enc_single_sw.c:183
esp_h264_enc_cfg_t esp_h264_enc_cfg_sw_t
Configuration type for software-based H.264 encoder.
Definition: esp_h264_enc_single_sw.h:24
@ ESP_H264_RAW_FMT_I420
Definition: esp_h264_types.h:61
@ ESP_H264_ERR_OK
Definition: esp_h264_types.h:24
void yuyv2iyuv(uint32_t height, uint32_t width, uint8_t *in, uint8_t *out)
Convert YUYV data to I420 data.
Definition: h264_color_convert.c:9
Definition: H264Decoder.h:31
Configuration structure for camera, WiFi, and encoder settings.
Definition: H264Encoder.h:87
int qp_min
Minimum quantizer parameter (default: 28)
Definition: H264Encoder.h:96
size_t outBufferSize
Output buffer size for encoded H.264 data (bytes)
Definition: H264Encoder.h:91
int y8_pin
Camera Y8 pin.
Definition: H264Encoder.h:113
int width
Frame width in pixels.
Definition: H264Encoder.h:88
int reset_pin
Camera RESET pin.
Definition: H264Encoder.h:103
int y4_pin
Camera Y4 pin.
Definition: H264Encoder.h:109
int pwdn_pin
Camera PWDN pin.
Definition: H264Encoder.h:102
int fps
Target frames per second.
Definition: H264Encoder.h:90
int y2_pin
Camera Y2 pin.
Definition: H264Encoder.h:107
pixformat_t pixel_format
Pixel format for camera capture.
Definition: H264Encoder.h:100
int y9_pin
Camera Y9 pin.
Definition: H264Encoder.h:114
int qp_max
Maximum quantizer parameter (default: 30)
Definition: H264Encoder.h:97
int siod_pin
Camera SIOD pin.
Definition: H264Encoder.h:105
int xclk_pin
Camera XCLK pin.
Definition: H264Encoder.h:104
int vsync_pin
Camera VSYNC pin.
Definition: H264Encoder.h:115
int y5_pin
Camera Y5 pin.
Definition: H264Encoder.h:110
int bitrate
Bitrate in bps (default: width*height*fps*30/100)
Definition: H264Encoder.h:95
int pclk_pin
Camera PCLK pin.
Definition: H264Encoder.h:117
int y3_pin
Camera Y3 pin.
Definition: H264Encoder.h:108
int sioc_pin
Camera SIOC pin.
Definition: H264Encoder.h:106
int height
Frame height in pixels.
Definition: H264Encoder.h:89
bool use_camera
Internal flag to track camera state.
Definition: H264Encoder.h:99
int href_pin
Camera HREF pin.
Definition: H264Encoder.h:116
int y7_pin
Camera Y7 pin.
Definition: H264Encoder.h:112
int y6_pin
Camera Y6 pin.
Definition: H264Encoder.h:111
int gop
Group of Pictures (default: fps)
Definition: H264Encoder.h:94
esp_h264_resolution_t res
Definition: esp_h264_types.h:160
uint8_t fps
Definition: esp_h264_types.h:155
esp_h264_raw_format_t pic_type
Definition: esp_h264_types.h:151
uint8_t gop
Definition: esp_h264_types.h:152
esp_h264_enc_rc_t rc
Definition: esp_h264_types.h:161
esp_h264_pkt_t raw_data
Definition: esp_h264_types.h:187
esp_h264_pkt_t raw_data
Definition: esp_h264_types.h:169
uint32_t length
Definition: esp_h264_types.h:170
uint32_t bitrate
Definition: esp_h264_types.h:136
uint8_t qp_max
Definition: esp_h264_types.h:142
uint8_t qp_min
Definition: esp_h264_types.h:140
uint8_t * buffer
Definition: esp_h264_types.h:100
uint32_t len
Definition: esp_h264_types.h:101
uint16_t height
Definition: esp_h264_types.h:119
uint16_t width
Definition: esp_h264_types.h:110