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