ESPHome 2026.11.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;
19ESPHOME_LOG_TAG(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(LOG_STR("Camera not available"));
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) {
59 this->mark_failed(LOG_STR("Failed to start the web server"));
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
103
105 if (!this->running_) {
106 this->image_ = nullptr;
107 }
108}
109
110std::shared_ptr<esphome::camera::CameraImage> CameraWebServer::wait_for_image_() {
111 std::shared_ptr<esphome::camera::CameraImage> image;
112 image.swap(this->image_);
113
114 if (image)
115 return image;
116
117 // Keep waiting until a frame really shows up, rather than trusting a single
118 // take() to mean one is there.
119 //
120 // on_camera_image() gives the semaphore for every frame it accepts, but the
121 // swap above hands frames out without taking it, so as soon as the camera is
122 // faster than this task for one frame the (binary) semaphore is left
123 // signalled by a frame that has already been consumed. The next take() then
124 // returns immediately with nothing to swap in, and the caller reports a lost
125 // frame and closes the stream -- after an arbitrary number of good frames,
126 // which is exactly when the camera happens to fall behind for one iteration.
127 //
128 // running_ is re-checked on every pass so a shutdown or a client that went
129 // away is noticed straight away instead of after the full timeout.
130 const uint32_t start = millis();
131 while (this->running_) {
132 uint32_t elapsed = millis() - start;
133 if (elapsed >= IMAGE_REQUEST_TIMEOUT)
134 break;
135 xSemaphoreTake(this->semaphore_, pdMS_TO_TICKS(IMAGE_REQUEST_TIMEOUT - elapsed));
136 image.swap(this->image_);
137 if (image)
138 break;
139 }
140
141 return image;
142}
143
144esp_err_t CameraWebServer::handler_(struct httpd_req *req) {
145 esp_err_t res = ESP_FAIL;
146
147 this->image_ = nullptr;
148 this->running_ = true;
149
150 switch (this->mode_) {
151 case STREAM:
152 res = this->streaming_handler_(req);
153 break;
154
155 case SNAPSHOT:
156 res = this->snapshot_handler_(req);
157 break;
158 }
159
160 this->running_ = false;
161 this->image_ = nullptr;
162 return res;
163}
164
165static esp_err_t httpd_send_all(httpd_req_t *r, const char *buf, size_t buf_len) {
166 int ret;
167
168 while (buf_len > 0) {
169 ret = httpd_send(r, buf, buf_len);
170 if (ret < 0) {
171 return ESP_FAIL;
172 }
173 buf += ret;
174 buf_len -= ret;
175 }
176 return ESP_OK;
177}
178
179esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
180 esp_err_t res = ESP_OK;
181 char part_buf[64];
182
183 // This manually constructs HTTP response to avoid chunked encoding
184 // which is not supported by some clients
185
186 res = httpd_send_all(req, STREAM_HEADER, strlen(STREAM_HEADER));
187 if (res != ESP_OK) {
188 ESP_LOGW(TAG, "STREAM: failed to set HTTP header");
189 return res;
190 }
191
192 uint32_t frames = 0;
193 // Frame statistics are aggregated over STREAM_STATS_INTERVAL rather than
194 // logged per frame. A line per frame comes out of this (non-main) task tens
195 // of times a second, and formatting and buffering it costs more than the
196 // stream it is reporting on.
197 uint32_t stats_since = millis();
198 uint32_t stats_frames = 0;
199 uint32_t stats_bytes = 0;
200
202
203 while (res == ESP_OK && this->running_) {
204 auto image = this->wait_for_image_();
205
206 if (!image) {
207 // A shutdown is not a lost frame: wait_for_image_() returns empty as soon
208 // as running_ clears, and the loop condition below ends the stream anyway.
209 if (this->running_) {
210 ESP_LOGW(TAG, "STREAM: failed to acquire frame");
211 }
212 res = ESP_FAIL;
213 }
214 if (res == ESP_OK) {
215 size_t hlen = snprintf(part_buf, 64, STREAM_PART, image->get_data_length());
216 res = httpd_send_all(req, part_buf, hlen);
217 }
218 if (res == ESP_OK) {
219 res = httpd_send_all(req, (const char *) image->get_data_buffer(), image->get_data_length());
220 }
221 if (res == ESP_OK) {
222 res = httpd_send_all(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY));
223 }
224 if (res == ESP_OK) {
225 frames++;
226 stats_frames++;
227 stats_bytes += image->get_data_length();
228 uint32_t elapsed = millis() - stats_since;
229 if (elapsed >= STREAM_STATS_INTERVAL) {
230 ESP_LOGD(TAG, "MJPG: %.1ffps, %" PRIu32 "B/frame (%" PRIu32 " frames)", stats_frames * 1000.0f / elapsed,
231 stats_bytes / stats_frames, stats_frames);
232 stats_since = millis();
233 stats_frames = 0;
234 stats_bytes = 0;
235 }
236 }
237 }
238
239 // Report whatever did not fill a whole interval, so a stream that only ran for
240 // a second or two still says what it managed rather than nothing at all.
241 if (stats_frames > 0) {
242 uint32_t elapsed = millis() - stats_since;
243 if (elapsed == 0)
244 elapsed = 1;
245 ESP_LOGD(TAG, "MJPG: %.1ffps, %" PRIu32 "B/frame (%" PRIu32 " frames)", stats_frames * 1000.0f / elapsed,
246 stats_bytes / stats_frames, stats_frames);
247 }
248
249 if (!frames) {
250 res = httpd_send_all(req, STREAM_ERROR, strlen(STREAM_ERROR));
251 }
252
254
255 ESP_LOGI(TAG, "STREAM: closed. Frames: %" PRIu32, frames);
256
257 return res;
258}
259
260esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) {
261 esp_err_t res = ESP_OK;
262
264
265 auto image = this->wait_for_image_();
266
267 if (!image) {
268 ESP_LOGW(TAG, "SNAPSHOT: failed to acquire frame");
269 httpd_resp_send_500(req);
270 res = ESP_FAIL;
271 return res;
272 }
273
274 res = httpd_resp_set_type(req, CONTENT_TYPE);
275 if (res != ESP_OK) {
276 ESP_LOGW(TAG, "SNAPSHOT: failed to set HTTP response type");
277 return res;
278 }
279
280 httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
281
282 if (res == ESP_OK) {
283 res = httpd_resp_send(req, (const char *) image->get_data_buffer(), image->get_data_length());
284 }
285 return res;
286}
287
288} // namespace esphome::esp32_camera_web_server
289
290#endif // USE_ESP32
void mark_failed()
Mark this component as failed.
bool is_failed() const
Definition component.h:274
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
ESPHOME_LOG_TAG(TAG, "esp32_camera_web_server")
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:25
static void uint32_t