ESPHome 2026.10.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 uint32_t IMAGE_REQUEST_TIMEOUT = 5000;
17// How often streaming_handler_ reports its throughput.
18static const uint32_t STREAM_STATS_INTERVAL = 5000;
19static const char *const TAG = "esp32_camera_web_server";
20
21#define PART_BOUNDARY "123456789000000000000987654321"
22#define CONTENT_TYPE "image/jpeg"
23#define CONTENT_LENGTH "Content-Length"
24
25static const char *const STREAM_HEADER = "HTTP/1.0 200 OK\r\n"
26 "Access-Control-Allow-Origin: *\r\n"
27 "Connection: close\r\n"
28 "Content-Type: multipart/x-mixed-replace;boundary=" PART_BOUNDARY "\r\n"
29 "\r\n"
30 "--" PART_BOUNDARY "\r\n";
31static const char *const STREAM_ERROR = "Content-Type: text/plain\r\n"
32 "\r\n"
33 "No frames send.\r\n"
34 "--" PART_BOUNDARY "\r\n";
35static const char *const STREAM_PART = "Content-Type: " CONTENT_TYPE "\r\n" CONTENT_LENGTH ": %u\r\n\r\n";
36static const char *const STREAM_BOUNDARY = "\r\n"
37 "--" PART_BOUNDARY "\r\n";
38
40
42
45 this->mark_failed();
46 return;
47 }
48
49 this->semaphore_ = xSemaphoreCreateBinary();
50
51 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
52 config.server_port = this->port_;
53 config.ctrl_port = this->port_;
54 config.max_open_sockets = 1;
55 config.backlog_conn = 2;
56 config.lru_purge_enable = true;
57
58 if (httpd_start(&this->httpd_, &config) != ESP_OK) {
60 return;
61 }
62
63 httpd_uri_t uri = {
64 .uri = "/",
65 .method = HTTP_GET,
66 .handler = [](struct httpd_req *req) { return ((CameraWebServer *) req->user_ctx)->handler_(req); },
67 .user_ctx = this};
68
69 httpd_register_uri_handler(this->httpd_, &uri);
70
72}
73
74void CameraWebServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
75 if (this->running_ && image->was_requested_by(camera::WEB_REQUESTER)) {
76 this->image_ = image;
77 xSemaphoreGive(this->semaphore_);
78 }
79}
80
82 this->running_ = false;
83 this->image_ = nullptr;
84 httpd_stop(this->httpd_);
85 this->httpd_ = nullptr;
86 vSemaphoreDelete(this->semaphore_);
87 this->semaphore_ = nullptr;
88}
89
91 ESP_LOGCONFIG(TAG,
92 "ESP32 Camera Web Server:\n"
93 " Port: %d",
94 this->port_);
95 if (this->mode_ == STREAM) {
96 ESP_LOGCONFIG(TAG, " Mode: stream");
97 } else {
98 ESP_LOGCONFIG(TAG, " Mode: snapshot");
99 }
100
101 if (this->is_failed()) {
102 ESP_LOGE(TAG, " Setup Failed");
103 }
104}
105
107
109 if (!this->running_) {
110 this->image_ = nullptr;
111 }
112}
113
114std::shared_ptr<esphome::camera::CameraImage> CameraWebServer::wait_for_image_() {
115 std::shared_ptr<esphome::camera::CameraImage> image;
116 image.swap(this->image_);
117
118 if (image)
119 return image;
120
121 // Keep waiting until a frame really shows up, rather than trusting a single
122 // take() to mean one is there.
123 //
124 // on_camera_image() gives the semaphore for every frame it accepts, but the
125 // swap above hands frames out without taking it, so as soon as the camera is
126 // faster than this task for one frame the (binary) semaphore is left
127 // signalled by a frame that has already been consumed. The next take() then
128 // returns immediately with nothing to swap in, and the caller reports a lost
129 // frame and closes the stream -- after an arbitrary number of good frames,
130 // which is exactly when the camera happens to fall behind for one iteration.
131 //
132 // running_ is re-checked on every pass so a shutdown or a client that went
133 // away is noticed straight away instead of after the full timeout.
134 const uint32_t start = millis();
135 while (this->running_) {
136 uint32_t elapsed = millis() - start;
137 if (elapsed >= IMAGE_REQUEST_TIMEOUT)
138 break;
139 xSemaphoreTake(this->semaphore_, pdMS_TO_TICKS(IMAGE_REQUEST_TIMEOUT - elapsed));
140 image.swap(this->image_);
141 if (image)
142 break;
143 }
144
145 return image;
146}
147
148esp_err_t CameraWebServer::handler_(struct httpd_req *req) {
149 esp_err_t res = ESP_FAIL;
150
151 this->image_ = nullptr;
152 this->running_ = true;
153
154 switch (this->mode_) {
155 case STREAM:
156 res = this->streaming_handler_(req);
157 break;
158
159 case SNAPSHOT:
160 res = this->snapshot_handler_(req);
161 break;
162 }
163
164 this->running_ = false;
165 this->image_ = nullptr;
166 return res;
167}
168
169static esp_err_t httpd_send_all(httpd_req_t *r, const char *buf, size_t buf_len) {
170 int ret;
171
172 while (buf_len > 0) {
173 ret = httpd_send(r, buf, buf_len);
174 if (ret < 0) {
175 return ESP_FAIL;
176 }
177 buf += ret;
178 buf_len -= ret;
179 }
180 return ESP_OK;
181}
182
183esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
184 esp_err_t res = ESP_OK;
185 char part_buf[64];
186
187 // This manually constructs HTTP response to avoid chunked encoding
188 // which is not supported by some clients
189
190 res = httpd_send_all(req, STREAM_HEADER, strlen(STREAM_HEADER));
191 if (res != ESP_OK) {
192 ESP_LOGW(TAG, "STREAM: failed to set HTTP header");
193 return res;
194 }
195
196 uint32_t frames = 0;
197 // Frame statistics are aggregated over STREAM_STATS_INTERVAL rather than
198 // logged per frame. A line per frame comes out of this (non-main) task tens
199 // of times a second, and formatting and buffering it costs more than the
200 // stream it is reporting on.
201 uint32_t stats_since = millis();
202 uint32_t stats_frames = 0;
203 uint32_t stats_bytes = 0;
204
206
207 while (res == ESP_OK && this->running_) {
208 auto image = this->wait_for_image_();
209
210 if (!image) {
211 // A shutdown is not a lost frame: wait_for_image_() returns empty as soon
212 // as running_ clears, and the loop condition below ends the stream anyway.
213 if (this->running_) {
214 ESP_LOGW(TAG, "STREAM: failed to acquire frame");
215 }
216 res = ESP_FAIL;
217 }
218 if (res == ESP_OK) {
219 size_t hlen = snprintf(part_buf, 64, STREAM_PART, image->get_data_length());
220 res = httpd_send_all(req, part_buf, hlen);
221 }
222 if (res == ESP_OK) {
223 res = httpd_send_all(req, (const char *) image->get_data_buffer(), image->get_data_length());
224 }
225 if (res == ESP_OK) {
226 res = httpd_send_all(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY));
227 }
228 if (res == ESP_OK) {
229 frames++;
230 stats_frames++;
231 stats_bytes += image->get_data_length();
232 uint32_t elapsed = millis() - stats_since;
233 if (elapsed >= STREAM_STATS_INTERVAL) {
234 ESP_LOGD(TAG, "MJPG: %.1ffps, %" PRIu32 "B/frame (%" PRIu32 " frames)", stats_frames * 1000.0f / elapsed,
235 stats_bytes / stats_frames, stats_frames);
236 stats_since = millis();
237 stats_frames = 0;
238 stats_bytes = 0;
239 }
240 }
241 }
242
243 // Report whatever did not fill a whole interval, so a stream that only ran for
244 // a second or two still says what it managed rather than nothing at all.
245 if (stats_frames > 0) {
246 uint32_t elapsed = millis() - stats_since;
247 if (elapsed == 0)
248 elapsed = 1;
249 ESP_LOGD(TAG, "MJPG: %.1ffps, %" PRIu32 "B/frame (%" PRIu32 " frames)", stats_frames * 1000.0f / elapsed,
250 stats_bytes / stats_frames, stats_frames);
251 }
252
253 if (!frames) {
254 res = httpd_send_all(req, STREAM_ERROR, strlen(STREAM_ERROR));
255 }
256
258
259 ESP_LOGI(TAG, "STREAM: closed. Frames: %" PRIu32, frames);
260
261 return res;
262}
263
264esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) {
265 esp_err_t res = ESP_OK;
266
268
269 auto image = this->wait_for_image_();
270
271 if (!image) {
272 ESP_LOGW(TAG, "SNAPSHOT: failed to acquire frame");
273 httpd_resp_send_500(req);
274 res = ESP_FAIL;
275 return res;
276 }
277
278 res = httpd_resp_set_type(req, CONTENT_TYPE);
279 if (res != ESP_OK) {
280 ESP_LOGW(TAG, "SNAPSHOT: failed to set HTTP response type");
281 return res;
282 }
283
284 httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
285
286 if (res == ESP_OK) {
287 res = httpd_resp_send(req, (const char *) image->get_data_buffer(), image->get_data_length());
288 }
289 return res;
290}
291
292} // namespace esphome::esp32_camera_web_server
293
294#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_()
int ret
constexpr float LATE
For components that should be initialized at the very end of the setup process.
Definition component.h:59
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
static void uint32_t