ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
camera_web_server.cpp
Go to the documentation of this file.
1#ifdef USE_ESP32
2
3#include "camera_web_server.h"
5#include "esphome/core/hal.h"
7#include "esphome/core/log.h"
8#include "esphome/core/util.h"
9
10#include <cstdlib>
11#include <esp_http_server.h>
12#include <utility>
13
15
16static const int IMAGE_REQUEST_TIMEOUT = 5000;
17static const char *const TAG = "esp32_camera_web_server";
18
19#define PART_BOUNDARY "123456789000000000000987654321"
20#define CONTENT_TYPE "image/jpeg"
21#define CONTENT_LENGTH "Content-Length"
22
23static const char *const STREAM_HEADER = "HTTP/1.0 200 OK\r\n"
24 "Access-Control-Allow-Origin: *\r\n"
25 "Connection: close\r\n"
26 "Content-Type: multipart/x-mixed-replace;boundary=" PART_BOUNDARY "\r\n"
27 "\r\n"
28 "--" PART_BOUNDARY "\r\n";
29static const char *const STREAM_ERROR = "Content-Type: text/plain\r\n"
30 "\r\n"
31 "No frames send.\r\n"
32 "--" PART_BOUNDARY "\r\n";
33static const char *const STREAM_PART = "Content-Type: " CONTENT_TYPE "\r\n" CONTENT_LENGTH ": %u\r\n\r\n";
34static const char *const STREAM_BOUNDARY = "\r\n"
35 "--" PART_BOUNDARY "\r\n";
36
38
40
43 this->mark_failed();
44 return;
45 }
46
47 this->semaphore_ = xSemaphoreCreateBinary();
48
49 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
50 config.server_port = this->port_;
51 config.ctrl_port = this->port_;
52 config.max_open_sockets = 1;
53 config.backlog_conn = 2;
54 config.lru_purge_enable = true;
55
56 if (httpd_start(&this->httpd_, &config) != ESP_OK) {
58 return;
59 }
60
61 httpd_uri_t uri = {
62 .uri = "/",
63 .method = HTTP_GET,
64 .handler = [](struct httpd_req *req) { return ((CameraWebServer *) req->user_ctx)->handler_(req); },
65 .user_ctx = this};
66
67 httpd_register_uri_handler(this->httpd_, &uri);
68
70}
71
72void CameraWebServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
73 if (this->running_ && image->was_requested_by(camera::WEB_REQUESTER)) {
74 this->image_ = image;
75 xSemaphoreGive(this->semaphore_);
76 }
77}
78
80 this->running_ = false;
81 this->image_ = nullptr;
82 httpd_stop(this->httpd_);
83 this->httpd_ = nullptr;
84 vSemaphoreDelete(this->semaphore_);
85 this->semaphore_ = nullptr;
86}
87
89 ESP_LOGCONFIG(TAG,
90 "ESP32 Camera Web Server:\n"
91 " Port: %d",
92 this->port_);
93 if (this->mode_ == STREAM) {
94 ESP_LOGCONFIG(TAG, " Mode: stream");
95 } else {
96 ESP_LOGCONFIG(TAG, " Mode: snapshot");
97 }
98
99 if (this->is_failed()) {
100 ESP_LOGE(TAG, " Setup Failed");
101 }
102}
103
105
107 if (!this->running_) {
108 this->image_ = nullptr;
109 }
110}
111
112std::shared_ptr<esphome::camera::CameraImage> CameraWebServer::wait_for_image_() {
113 std::shared_ptr<esphome::camera::CameraImage> image;
114 image.swap(this->image_);
115
116 if (!image) {
117 // retry as we might still be fetching image
118 xSemaphoreTake(this->semaphore_, IMAGE_REQUEST_TIMEOUT / portTICK_PERIOD_MS);
119 image.swap(this->image_);
120 }
121
122 return image;
123}
124
125esp_err_t CameraWebServer::handler_(struct httpd_req *req) {
126 esp_err_t res = ESP_FAIL;
127
128 this->image_ = nullptr;
129 this->running_ = true;
130
131 switch (this->mode_) {
132 case STREAM:
133 res = this->streaming_handler_(req);
134 break;
135
136 case SNAPSHOT:
137 res = this->snapshot_handler_(req);
138 break;
139 }
140
141 this->running_ = false;
142 this->image_ = nullptr;
143 return res;
144}
145
146static esp_err_t httpd_send_all(httpd_req_t *r, const char *buf, size_t buf_len) {
147 int ret;
148
149 while (buf_len > 0) {
150 ret = httpd_send(r, buf, buf_len);
151 if (ret < 0) {
152 return ESP_FAIL;
153 }
154 buf += ret;
155 buf_len -= ret;
156 }
157 return ESP_OK;
158}
159
160esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
161 esp_err_t res = ESP_OK;
162 char part_buf[64];
163
164 // This manually constructs HTTP response to avoid chunked encoding
165 // which is not supported by some clients
166
167 res = httpd_send_all(req, STREAM_HEADER, strlen(STREAM_HEADER));
168 if (res != ESP_OK) {
169 ESP_LOGW(TAG, "STREAM: failed to set HTTP header");
170 return res;
171 }
172
173 uint32_t last_frame = millis();
174 uint32_t frames = 0;
175
177
178 while (res == ESP_OK && this->running_) {
179 auto image = this->wait_for_image_();
180
181 if (!image) {
182 ESP_LOGW(TAG, "STREAM: failed to acquire frame");
183 res = ESP_FAIL;
184 }
185 if (res == ESP_OK) {
186 size_t hlen = snprintf(part_buf, 64, STREAM_PART, image->get_data_length());
187 res = httpd_send_all(req, part_buf, hlen);
188 }
189 if (res == ESP_OK) {
190 res = httpd_send_all(req, (const char *) image->get_data_buffer(), image->get_data_length());
191 }
192 if (res == ESP_OK) {
193 res = httpd_send_all(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY));
194 }
195 if (res == ESP_OK) {
196 frames++;
197 int64_t frame_time = millis() - last_frame;
198 last_frame = millis();
199
200 ESP_LOGD(TAG, "MJPG: %" PRIu32 "B %" PRIu32 "ms (%.1ffps)", (uint32_t) image->get_data_length(),
201 (uint32_t) frame_time, 1000.0 / (uint32_t) frame_time);
202 }
203 }
204
205 if (!frames) {
206 res = httpd_send_all(req, STREAM_ERROR, strlen(STREAM_ERROR));
207 }
208
210
211 ESP_LOGI(TAG, "STREAM: closed. Frames: %" PRIu32, frames);
212
213 return res;
214}
215
216esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) {
217 esp_err_t res = ESP_OK;
218
220
221 auto image = this->wait_for_image_();
222
223 if (!image) {
224 ESP_LOGW(TAG, "SNAPSHOT: failed to acquire frame");
225 httpd_resp_send_500(req);
226 res = ESP_FAIL;
227 return res;
228 }
229
230 res = httpd_resp_set_type(req, CONTENT_TYPE);
231 if (res != ESP_OK) {
232 ESP_LOGW(TAG, "SNAPSHOT: failed to set HTTP response type");
233 return res;
234 }
235
236 httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
237
238 if (res == ESP_OK) {
239 res = httpd_resp_send(req, (const char *) image->get_data_buffer(), image->get_data_length());
240 }
241 return res;
242}
243
244} // namespace esphome::esp32_camera_web_server
245
246#endif // USE_ESP32
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:272
virtual void start_stream(CameraRequester requester)=0
virtual void stop_stream(CameraRequester requester)=0
virtual void add_listener(CameraListener *listener)=0
Add a listener to receive camera events.
virtual void request_image(CameraRequester requester)=0
static Camera * instance()
The singleton instance of the camera implementation.
Definition camera.cpp:18
void on_camera_image(const std::shared_ptr< camera::CameraImage > &image) override
CameraListener interface.
std::shared_ptr< camera::CameraImage > image_
std::shared_ptr< camera::CameraImage > wait_for_image_()
constexpr float LATE
For components that should be initialized at the very end of the setup process.
Definition component.h:57
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t