mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 08:28:12 +01:00
Add esp32_camera_web_server:
to expose mjpg/jpg images (#2237)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
d6717c0032
commit
fb57ab0add
8 changed files with 328 additions and 2 deletions
|
@ -52,6 +52,7 @@ esphome/components/dsmr/* @glmnet @zuidwijk
|
||||||
esphome/components/esp32/* @esphome/core
|
esphome/components/esp32/* @esphome/core
|
||||||
esphome/components/esp32_ble/* @jesserockz
|
esphome/components/esp32_ble/* @jesserockz
|
||||||
esphome/components/esp32_ble_server/* @jesserockz
|
esphome/components/esp32_ble_server/* @jesserockz
|
||||||
|
esphome/components/esp32_camera_web_server/* @ayufan
|
||||||
esphome/components/esp32_improv/* @jesserockz
|
esphome/components/esp32_improv/* @jesserockz
|
||||||
esphome/components/esp8266/* @esphome/core
|
esphome/components/esp8266/* @esphome/core
|
||||||
esphome/components/exposure_notifications/* @OttoWinter
|
esphome/components/exposure_notifications/* @OttoWinter
|
||||||
|
|
|
@ -77,7 +77,7 @@ void APIServer::setup() {
|
||||||
this->last_connected_ = millis();
|
this->last_connected_ = millis();
|
||||||
|
|
||||||
#ifdef USE_ESP32_CAMERA
|
#ifdef USE_ESP32_CAMERA
|
||||||
if (esp32_camera::global_esp32_camera != nullptr) {
|
if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) {
|
||||||
esp32_camera::global_esp32_camera->add_image_callback(
|
esp32_camera::global_esp32_camera->add_image_callback(
|
||||||
[this](const std::shared_ptr<esp32_camera::CameraImage> &image) {
|
[this](const std::shared_ptr<esp32_camera::CameraImage> &image) {
|
||||||
for (auto &c : this->clients_)
|
for (auto &c : this->clients_)
|
||||||
|
|
|
@ -17,7 +17,7 @@ from esphome.core import CORE
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||||
from esphome.cpp_helpers import setup_entity
|
from esphome.cpp_helpers import setup_entity
|
||||||
|
|
||||||
DEPENDENCIES = ["esp32", "api"]
|
DEPENDENCIES = ["esp32"]
|
||||||
|
|
||||||
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
|
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
|
||||||
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
|
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
|
||||||
|
|
|
@ -45,6 +45,7 @@ void ESP32Camera::dump_config() {
|
||||||
auto conf = this->config_;
|
auto conf = this->config_;
|
||||||
ESP_LOGCONFIG(TAG, "ESP32 Camera:");
|
ESP_LOGCONFIG(TAG, "ESP32 Camera:");
|
||||||
ESP_LOGCONFIG(TAG, " Name: %s", this->name_.c_str());
|
ESP_LOGCONFIG(TAG, " Name: %s", this->name_.c_str());
|
||||||
|
ESP_LOGCONFIG(TAG, " Internal: %s", YESNO(this->internal_));
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
ESP_LOGCONFIG(TAG, " Board Has PSRAM: %s", YESNO(psramFound()));
|
ESP_LOGCONFIG(TAG, " Board Has PSRAM: %s", YESNO(psramFound()));
|
||||||
#endif // USE_ARDUINO
|
#endif // USE_ARDUINO
|
||||||
|
|
28
esphome/components/esp32_camera_web_server/__init__.py
Normal file
28
esphome/components/esp32_camera_web_server/__init__.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.const import CONF_ID, CONF_PORT, CONF_MODE
|
||||||
|
|
||||||
|
CODEOWNERS = ["@ayufan"]
|
||||||
|
DEPENDENCIES = ["esp32_camera"]
|
||||||
|
MULTI_CONF = True
|
||||||
|
|
||||||
|
esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server")
|
||||||
|
CameraWebServer = esp32_camera_web_server_ns.class_("CameraWebServer", cg.Component)
|
||||||
|
Mode = esp32_camera_web_server_ns.enum("Mode")
|
||||||
|
|
||||||
|
MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT}
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(CameraWebServer),
|
||||||
|
cv.Required(CONF_PORT): cv.port,
|
||||||
|
cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
|
||||||
|
},
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
server = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
cg.add(server.set_port(config[CONF_PORT]))
|
||||||
|
cg.add(server.set_mode(config[CONF_MODE]))
|
||||||
|
await cg.register_component(server, config)
|
239
esphome/components/esp32_camera_web_server/camera_web_server.cpp
Normal file
239
esphome/components/esp32_camera_web_server/camera_web_server.cpp
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#include "camera_web_server.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/util.h"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <esp_http_server.h>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace esp32_camera_web_server {
|
||||||
|
|
||||||
|
static const int IMAGE_REQUEST_TIMEOUT = 2000;
|
||||||
|
static const char *const TAG = "esp32_camera_web_server";
|
||||||
|
|
||||||
|
#define PART_BOUNDARY "123456789000000000000987654321"
|
||||||
|
#define CONTENT_TYPE "image/jpeg"
|
||||||
|
#define CONTENT_LENGTH "Content-Length"
|
||||||
|
|
||||||
|
static const char *const STREAM_HEADER =
|
||||||
|
"HTTP/1.1 200\r\nAccess-Control-Allow-Origin: *\r\nContent-Type: multipart/x-mixed-replace;boundary=" PART_BOUNDARY
|
||||||
|
"\r\n";
|
||||||
|
static const char *const STREAM_500 = "HTTP/1.1 500\r\nContent-Type: text/plain\r\n\r\nNo frames send.\r\n";
|
||||||
|
static const char *const STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
|
||||||
|
static const char *const STREAM_PART = "Content-Type: " CONTENT_TYPE "\r\n" CONTENT_LENGTH ": %u\r\n\r\n";
|
||||||
|
|
||||||
|
CameraWebServer::CameraWebServer() {}
|
||||||
|
|
||||||
|
CameraWebServer::~CameraWebServer() {}
|
||||||
|
|
||||||
|
void CameraWebServer::setup() {
|
||||||
|
if (!esp32_camera::global_esp32_camera || esp32_camera::global_esp32_camera->is_failed()) {
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->semaphore_ = xSemaphoreCreateBinary();
|
||||||
|
|
||||||
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
|
config.server_port = this->port_;
|
||||||
|
config.ctrl_port = this->port_;
|
||||||
|
config.max_open_sockets = 1;
|
||||||
|
config.backlog_conn = 2;
|
||||||
|
|
||||||
|
if (httpd_start(&this->httpd_, &config) != ESP_OK) {
|
||||||
|
mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_uri_t uri = {
|
||||||
|
.uri = "/",
|
||||||
|
.method = HTTP_GET,
|
||||||
|
.handler = [](struct httpd_req *req) { return ((CameraWebServer *) req->user_ctx)->handler_(req); },
|
||||||
|
.user_ctx = this};
|
||||||
|
|
||||||
|
httpd_register_uri_handler(this->httpd_, &uri);
|
||||||
|
|
||||||
|
esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr<esp32_camera::CameraImage> image) {
|
||||||
|
if (this->running_) {
|
||||||
|
this->image_ = std::move(image);
|
||||||
|
xSemaphoreGive(this->semaphore_);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void CameraWebServer::on_shutdown() {
|
||||||
|
this->running_ = false;
|
||||||
|
this->image_ = nullptr;
|
||||||
|
httpd_stop(this->httpd_);
|
||||||
|
this->httpd_ = nullptr;
|
||||||
|
vSemaphoreDelete(this->semaphore_);
|
||||||
|
this->semaphore_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CameraWebServer::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "ESP32 Camera Web Server:");
|
||||||
|
ESP_LOGCONFIG(TAG, " Port: %d", this->port_);
|
||||||
|
if (this->mode_ == STREAM)
|
||||||
|
ESP_LOGCONFIG(TAG, " Mode: stream");
|
||||||
|
else
|
||||||
|
ESP_LOGCONFIG(TAG, " Mode: snapshot");
|
||||||
|
|
||||||
|
if (this->is_failed()) {
|
||||||
|
ESP_LOGE(TAG, " Setup Failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float CameraWebServer::get_setup_priority() const { return setup_priority::LATE; }
|
||||||
|
|
||||||
|
void CameraWebServer::loop() {
|
||||||
|
if (!this->running_) {
|
||||||
|
this->image_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<esphome::esp32_camera::CameraImage> CameraWebServer::wait_for_image_() {
|
||||||
|
std::shared_ptr<esphome::esp32_camera::CameraImage> image;
|
||||||
|
image.swap(this->image_);
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
// retry as we might still be fetching image
|
||||||
|
xSemaphoreTake(this->semaphore_, IMAGE_REQUEST_TIMEOUT / portTICK_PERIOD_MS);
|
||||||
|
image.swap(this->image_);
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t CameraWebServer::handler_(struct httpd_req *req) {
|
||||||
|
esp_err_t res = ESP_FAIL;
|
||||||
|
|
||||||
|
this->image_ = nullptr;
|
||||||
|
this->running_ = true;
|
||||||
|
|
||||||
|
switch (this->mode_) {
|
||||||
|
case STREAM:
|
||||||
|
res = this->streaming_handler_(req);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SNAPSHOT:
|
||||||
|
res = this->snapshot_handler_(req);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->running_ = false;
|
||||||
|
this->image_ = nullptr;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t httpd_send_all(httpd_req_t *r, const char *buf, size_t buf_len) {
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
while (buf_len > 0) {
|
||||||
|
ret = httpd_send(r, buf, buf_len);
|
||||||
|
if (ret < 0) {
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
buf += ret;
|
||||||
|
buf_len -= ret;
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
|
||||||
|
esp_err_t res = ESP_OK;
|
||||||
|
char part_buf[64];
|
||||||
|
|
||||||
|
// This manually constructs HTTP response to avoid chunked encoding
|
||||||
|
// which is not supported by some clients
|
||||||
|
|
||||||
|
res = httpd_send_all(req, STREAM_HEADER, strlen(STREAM_HEADER));
|
||||||
|
if (res != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "STREAM: failed to set HTTP header");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t last_frame = millis();
|
||||||
|
uint32_t frames = 0;
|
||||||
|
|
||||||
|
while (res == ESP_OK && this->running_) {
|
||||||
|
if (esp32_camera::global_esp32_camera != nullptr) {
|
||||||
|
esp32_camera::global_esp32_camera->request_stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto image = this->wait_for_image_();
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
ESP_LOGW(TAG, "STREAM: failed to acquire frame");
|
||||||
|
res = ESP_FAIL;
|
||||||
|
}
|
||||||
|
if (res == ESP_OK) {
|
||||||
|
res = httpd_send_all(req, STREAM_BOUNDARY, strlen(STREAM_BOUNDARY));
|
||||||
|
}
|
||||||
|
if (res == ESP_OK) {
|
||||||
|
size_t hlen = snprintf(part_buf, 64, STREAM_PART, image->get_data_length());
|
||||||
|
res = httpd_send_all(req, part_buf, hlen);
|
||||||
|
}
|
||||||
|
if (res == ESP_OK) {
|
||||||
|
res = httpd_send_all(req, (const char *) image->get_data_buffer(), image->get_data_length());
|
||||||
|
}
|
||||||
|
if (res == ESP_OK) {
|
||||||
|
frames++;
|
||||||
|
int64_t frame_time = millis() - last_frame;
|
||||||
|
last_frame = millis();
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "MJPG: %uB %ums (%.1ffps)", (uint32_t) image->get_data_length(), (uint32_t) frame_time,
|
||||||
|
1000.0 / (uint32_t) frame_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frames) {
|
||||||
|
res = httpd_send_all(req, STREAM_500, strlen(STREAM_500));
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "STREAM: closed. Frames: %u", frames);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) {
|
||||||
|
esp_err_t res = ESP_OK;
|
||||||
|
|
||||||
|
if (esp32_camera::global_esp32_camera != nullptr) {
|
||||||
|
esp32_camera::global_esp32_camera->request_image();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto image = this->wait_for_image_();
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
ESP_LOGW(TAG, "SNAPSHOT: failed to acquire frame");
|
||||||
|
httpd_resp_send_500(req);
|
||||||
|
res = ESP_FAIL;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
res = httpd_resp_set_type(req, CONTENT_TYPE);
|
||||||
|
if (res != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "SNAPSHOT: failed to set HTTP response type");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg");
|
||||||
|
|
||||||
|
if (res == ESP_OK) {
|
||||||
|
res = httpd_resp_set_hdr(req, CONTENT_LENGTH, esphome::to_string(image->get_data_length()).c_str());
|
||||||
|
}
|
||||||
|
if (res == ESP_OK) {
|
||||||
|
res = httpd_resp_send(req, (const char *) image->get_data_buffer(), image->get_data_length());
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esp32_camera_web_server
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ESP32
|
|
@ -0,0 +1,51 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
|
||||||
|
#include "esphome/components/esp32_camera/esp32_camera.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/preferences.h"
|
||||||
|
|
||||||
|
struct httpd_req;
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace esp32_camera_web_server {
|
||||||
|
|
||||||
|
enum Mode { STREAM, SNAPSHOT };
|
||||||
|
|
||||||
|
class CameraWebServer : public Component {
|
||||||
|
public:
|
||||||
|
CameraWebServer();
|
||||||
|
~CameraWebServer();
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
void on_shutdown() override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override;
|
||||||
|
void set_port(uint16_t port) { this->port_ = port; }
|
||||||
|
void set_mode(Mode mode) { this->mode_ = mode; }
|
||||||
|
void loop() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::shared_ptr<esphome::esp32_camera::CameraImage> wait_for_image_();
|
||||||
|
esp_err_t handler_(struct httpd_req *req);
|
||||||
|
esp_err_t streaming_handler_(struct httpd_req *req);
|
||||||
|
esp_err_t snapshot_handler_(struct httpd_req *req);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
uint16_t port_{0};
|
||||||
|
void *httpd_{nullptr};
|
||||||
|
SemaphoreHandle_t semaphore_;
|
||||||
|
std::shared_ptr<esphome::esp32_camera::CameraImage> image_;
|
||||||
|
bool running_{false};
|
||||||
|
Mode mode_{STREAM};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esp32_camera_web_server
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ESP32
|
|
@ -481,6 +481,12 @@ esp32_camera:
|
||||||
resolution: 640x480
|
resolution: 640x480
|
||||||
jpeg_quality: 10
|
jpeg_quality: 10
|
||||||
|
|
||||||
|
esp32_camera_web_server:
|
||||||
|
- port: 8080
|
||||||
|
mode: stream
|
||||||
|
- port: 8081
|
||||||
|
mode: snapshot
|
||||||
|
|
||||||
external_components:
|
external_components:
|
||||||
- source: github://esphome/esphome@dev
|
- source: github://esphome/esphome@dev
|
||||||
refresh: 1d
|
refresh: 1d
|
||||||
|
|
Loading…
Reference in a new issue