Files
esp32cam-rtsp/src/main.cpp
Rene Zeldenthuis 545a824b37 Removed OTA
2024-02-05 00:55:44 +01:00

407 lines
19 KiB
C++

#include <Arduino.h>
#include <esp_wifi.h>
#include <soc/rtc_cntl_reg.h>
#include <driver/i2c.h>
#include <IotWebConf.h>
#include <IotWebConfTParameter.h>
#include <OV2640.h>
#include <ESPmDNS.h>
#include <rtsp_server.h>
#include <lookup_camera_effect.h>
#include <lookup_camera_frame_size.h>
#include <lookup_camera_gainceiling.h>
#include <lookup_camera_wb_mode.h>
#include <format_duration.h>
#include <format_number.h>
#include <moustache.h>
#include <settings.h>
// HTML files
extern const char index_html_min_start[] asm("_binary_html_index_min_html_start");
auto param_group_camera = iotwebconf::ParameterGroup("camera", "Camera settings");
auto param_frame_duration = iotwebconf::Builder<iotwebconf::UIntTParameter<unsigned long>>("fd").label("Frame duration (ms)").defaultValue(DEFAULT_FRAME_DURATION).min(10).build();
auto param_frame_size = iotwebconf::Builder<iotwebconf::SelectTParameter<sizeof(frame_sizes[0])>>("fs").label("Frame size").optionValues((const char *)&frame_sizes).optionNames((const char *)&frame_sizes).optionCount(sizeof(frame_sizes) / sizeof(frame_sizes[0])).nameLength(sizeof(frame_sizes[0])).defaultValue(DEFAULT_FRAME_SIZE).build();
auto param_jpg_quality = iotwebconf::Builder<iotwebconf::UIntTParameter<byte>>("q").label("JPG quality").defaultValue(DEFAULT_JPEG_QUALITY).min(1).max(100).build();
auto param_brightness = iotwebconf::Builder<iotwebconf::IntTParameter<int>>("b").label("Brightness").defaultValue(DEFAULT_BRIGHTNESS).min(-2).max(2).build();
auto param_contrast = iotwebconf::Builder<iotwebconf::IntTParameter<int>>("c").label("Contrast").defaultValue(DEFAULT_CONTRAST).min(-2).max(2).build();
auto param_saturation = iotwebconf::Builder<iotwebconf::IntTParameter<int>>("s").label("Saturation").defaultValue(DEFAULT_SATURATION).min(-2).max(2).build();
auto param_special_effect = iotwebconf::Builder<iotwebconf::SelectTParameter<sizeof(camera_effects[0])>>("e").label("Effect").optionValues((const char *)&camera_effects).optionNames((const char *)&camera_effects).optionCount(sizeof(camera_effects) / sizeof(camera_effects[0])).nameLength(sizeof(camera_effects[0])).defaultValue(DEFAULT_EFFECT).build();
auto param_whitebal = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("wb").label("White balance").defaultValue(DEFAULT_WHITE_BALANCE).build();
auto param_awb_gain = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("awbg").label("Automatic white balance gain").defaultValue(DEFAULT_WHITE_BALANCE_GAIN).build();
auto param_wb_mode = iotwebconf::Builder<iotwebconf::SelectTParameter<sizeof(camera_wb_modes[0])>>("wbm").label("White balance mode").optionValues((const char *)&camera_wb_modes).optionNames((const char *)&camera_wb_modes).optionCount(sizeof(camera_wb_modes) / sizeof(camera_wb_modes[0])).nameLength(sizeof(camera_wb_modes[0])).defaultValue(DEFAULT_WHITE_BALANCE_MODE).build();
auto param_exposure_ctrl = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("ec").label("Exposure control").defaultValue(DEFAULT_EXPOSURE_CONTROL).build();
auto param_aec2 = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("aec2").label("Auto exposure (dsp)").defaultValue(DEFAULT_AEC2).build();
auto param_ae_level = iotwebconf::Builder<iotwebconf::IntTParameter<int>>("ael").label("Auto Exposure level").defaultValue(DEFAULT_AE_LEVEL).min(-2).max(2).build();
auto param_aec_value = iotwebconf::Builder<iotwebconf::IntTParameter<int>>("aecv").label("Manual exposure value").defaultValue(DEFAULT_AEC_VALUE).min(9).max(1200).build();
auto param_gain_ctrl = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("gc").label("Gain control").defaultValue(DEFAULT_GAIN_CONTROL).build();
auto param_agc_gain = iotwebconf::Builder<iotwebconf::IntTParameter<int>>("agcg").label("AGC gain").defaultValue(DEFAULT_AGC_GAIN).min(0).max(30).build();
auto param_gain_ceiling = iotwebconf::Builder<iotwebconf::SelectTParameter<sizeof(camera_gain_ceilings[0])>>("gcl").label("Auto Gain ceiling").optionValues((const char *)&camera_gain_ceilings).optionNames((const char *)&camera_gain_ceilings).optionCount(sizeof(camera_gain_ceilings) / sizeof(camera_gain_ceilings[0])).nameLength(sizeof(camera_gain_ceilings[0])).defaultValue(DEFAULT_GAIN_CEILING).build();
auto param_bpc = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("bpc").label("Black pixel correct").defaultValue(DEFAULT_BPC).build();
auto param_wpc = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("wpc").label("White pixel correct").defaultValue(DEFAULT_WPC).build();
auto param_raw_gma = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("rg").label("Gamma correct").defaultValue(DEFAULT_RAW_GAMMA).build();
auto param_lenc = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("lenc").label("Lens correction").defaultValue(DEFAULT_LENC).build();
auto param_hmirror = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("hm").label("Horizontal mirror").defaultValue(DEFAULT_HORIZONTAL_MIRROR).build();
auto param_vflip = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("vm").label("Vertical mirror").defaultValue(DEFAULT_VERTICAL_MIRROR).build();
auto param_dcw = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("dcw").label("Downsize enable").defaultValue(DEFAULT_DCW).build();
auto param_colorbar = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("cb").label("Colorbar").defaultValue(DEFAULT_COLORBAR).build();
// Camera
OV2640 cam;
// DNS Server
DNSServer dnsServer;
// RTSP Server
std::unique_ptr<rtsp_server> camera_server;
// Web server
WebServer web_server(80);
auto thingName = String(WIFI_SSID) + "-" + String(ESP.getEfuseMac(), 16);
IotWebConf iotWebConf(thingName.c_str(), &dnsServer, &web_server, WIFI_PASSWORD, CONFIG_VERSION);
// Camera initialization result
esp_err_t camera_init_result;
void handle_root()
{
log_v("Handle root");
// Let IotWebConf test and handle captive portal requests.
if (iotWebConf.handleCaptivePortal())
return;
// Format hostname
auto hostname = "esp32-" + WiFi.macAddress() + ".local";
hostname.replace(":", "");
hostname.toLowerCase();
// Wifi Modes
const char *wifi_modes[] = {"NULL", "STA", "AP", "STA+AP"};
auto ipv4 = WiFi.getMode() == WIFI_MODE_AP ? WiFi.softAPIP() : WiFi.localIP();
auto ipv6 = WiFi.getMode() == WIFI_MODE_AP ? WiFi.softAPIPv6() : WiFi.localIPv6();
auto initResult = esp_err_to_name(camera_init_result);
if (initResult == nullptr)
initResult = "Unknown reason";
moustache_variable_t substitutions[] = {
// Version / CPU
{"AppTitle", APP_TITLE},
{"AppVersion", APP_VERSION},
{"BoardType", BOARD_NAME},
{"ThingName", iotWebConf.getThingName()},
{"SDKVersion", ESP.getSdkVersion()},
{"ChipModel", ESP.getChipModel()},
{"ChipRevision", String(ESP.getChipRevision())},
{"CpuFreqMHz", String(ESP.getCpuFreqMHz())},
{"CpuCores", String(ESP.getChipCores())},
{"FlashSize", format_memory(ESP.getFlashChipSize(), 0)},
{"HeapSize", format_memory(ESP.getHeapSize())},
{"PsRamSize", format_memory(ESP.getPsramSize(), 0)},
// Diagnostics
{"Uptime", String(format_duration(millis() / 1000))},
{"FreeHeap", format_memory(ESP.getFreeHeap())},
{"MaxAllocHeap", format_memory(ESP.getMaxAllocHeap())},
{"NumRTSPSessions", camera_server != nullptr ? String(camera_server->num_connected()) : "RTSP server disabled"},
// Network
{"HostName", hostname},
{"MacAddress", WiFi.macAddress()},
{"AccessPoint", WiFi.SSID()},
{"SignalStrength", String(WiFi.RSSI())},
{"WifiMode", wifi_modes[WiFi.getMode()]},
{"IPv4", ipv4.toString()},
{"IPv6", ipv6.toString()},
{"NetworkState.ApMode", String(iotWebConf.getState() == iotwebconf::NetworkState::ApMode)},
{"NetworkState.OnLine", String(iotWebConf.getState() == iotwebconf::NetworkState::OnLine)},
// Camera
{"FrameSize", String(param_frame_size.value())},
{"FrameDuration", String(param_frame_duration.value())},
{"FrameFrequency", String(1000.0 / param_frame_duration.value(), 1)},
{"JpegQuality", String(param_jpg_quality.value())},
{"CameraInitialized", String(camera_init_result == ESP_OK)},
{"CameraInitResult", String(camera_init_result)},
{"CameraInitResultText", initResult},
// Settings
{"Brightness", String(param_brightness.value())},
{"Contrast", String(param_contrast.value())},
{"Saturation", String(param_saturation.value())},
{"SpecialEffect", String(param_special_effect.value())},
{"WhiteBal", String(param_whitebal.value())},
{"AwbGain", String(param_awb_gain.value())},
{"WbMode", String(param_wb_mode.value())},
{"ExposureCtrl", String(param_exposure_ctrl.value())},
{"Aec2", String(param_aec2.value())},
{"AeLevel", String(param_ae_level.value())},
{"AecValue", String(param_aec_value.value())},
{"GainCtrl", String(param_gain_ctrl.value())},
{"AgcGain", String(param_agc_gain.value())},
{"GainCeiling", String(param_gain_ceiling.value())},
{"Bpc", String(param_bpc.value())},
{"Wpc", String(param_wpc.value())},
{"RawGma", String(param_raw_gma.value())},
{"Lenc", String(param_lenc.value())},
{"HMirror", String(param_hmirror.value())},
{"VFlip", String(param_vflip.value())},
{"Dcw", String(param_dcw.value())},
{"ColorBar", String(param_colorbar.value())},
// RTSP
{"RtspPort", String(RTSP_PORT)}};
web_server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
auto html = moustache_render(index_html_min_start, substitutions);
web_server.send(200, "text/html", html);
}
void handle_snapshot()
{
log_v("handle_snapshot");
if (camera_init_result != ESP_OK)
{
web_server.send(404, "text/plain", "Camera is not initialized");
return;
}
// Remove old images stored in the frame buffer
auto frame_buffers = CAMERA_CONFIG_FB_COUNT;
while (frame_buffers--)
cam.run();
auto fb_len = cam.getSize();
auto fb = (const char *)cam.getfb();
if (fb == nullptr)
{
web_server.send(404, "text/plain", "Unable to obtain frame buffer from the camera");
return;
}
web_server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
web_server.setContentLength(fb_len);
web_server.send(200, "image/jpeg", "");
web_server.sendContent(fb, fb_len);
}
#define STREAM_CONTENT_BOUNDARY "123456789000000000000987654321"
void handle_stream()
{
log_v("handle_stream");
if (camera_init_result != ESP_OK)
{
web_server.send(404, "text/plain", "Camera is not initialized");
return;
}
log_v("starting streaming");
// Blocks further handling of HTTP server until stopped
char size_buf[12];
auto client = web_server.client();
client.write("HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Type: multipart/x-mixed-replace; boundary=" STREAM_CONTENT_BOUNDARY "\r\n");
while (client.connected())
{
client.write("\r\n--" STREAM_CONTENT_BOUNDARY "\r\n");
cam.run();
client.write("Content-Type: image/jpeg\r\nContent-Length: ");
sprintf(size_buf, "%d\r\n\r\n", cam.getSize());
client.write(size_buf);
client.write(cam.getfb(), cam.getSize());
}
log_v("client disconnected");
client.stop();
log_v("stopped streaming");
}
esp_err_t initialize_camera()
{
log_v("initialize_camera");
constexpr auto pixformat = PIXFORMAT_JPEG;
log_i("Frame size: %s", param_frame_size.value());
auto frame_size = lookup_frame_size(param_frame_size.value());
log_i("JPEG quality: %d", param_jpg_quality.value());
auto jpeg_quality = param_jpg_quality.value();
log_i("Frame duration: %d ms", param_frame_duration.value());
constexpr auto i2c_port = I2C_NUM_0;
camera_config_t camera_config = {
.pin_pwdn = CAMERA_CONFIG_PIN_PWDN, // GPIO pin for camera power down line
.pin_reset = CAMERA_CONFIG_PIN_RESET, // GPIO pin for camera reset line
.pin_xclk = CAMERA_CONFIG_PIN_XCLK, // GPIO pin for camera XCLK line
.pin_sccb_sda = CAMERA_CONFIG_PIN_SCCB_SDA, // GPIO pin for camera SDA line
.pin_sccb_scl = CAMERA_CONFIG_PIN_SCCB_SCL, // GPIO pin for camera SCL line
.pin_d7 = CAMERA_CONFIG_PIN_Y9, // GPIO pin for camera D7 line
.pin_d6 = CAMERA_CONFIG_PIN_Y8, // GPIO pin for camera D6 line
.pin_d5 = CAMERA_CONFIG_PIN_Y7, // GPIO pin for camera D5 line
.pin_d4 = CAMERA_CONFIG_PIN_Y6, // GPIO pin for camera D4 line
.pin_d3 = CAMERA_CONFIG_PIN_Y5, // GPIO pin for camera D3 line
.pin_d2 = CAMERA_CONFIG_PIN_Y4, // GPIO pin for camera D2 line
.pin_d1 = CAMERA_CONFIG_PIN_Y3, // GPIO pin for camera D1 line
.pin_d0 = CAMERA_CONFIG_PIN_Y2, // GPIO pin for camera D0 line
.pin_vsync = CAMERA_CONFIG_PIN_VSYNC, // GPIO pin for camera VSYNC line
.pin_href = CAMERA_CONFIG_PIN_HREF, // GPIO pin for camera HREF line
.pin_pclk = CAMERA_CONFIG_PIN_PCLK, // GPIO pin for camera PCLK line
.xclk_freq_hz = CAMERA_CONFIG_CLK_FREQ_HZ, // Frequency of XCLK signal, in Hz. EXPERIMENTAL: Set to 16MHz on ESP32-S2 or ESP32-S3 to enable EDMA mode
.ledc_timer = CAMERA_CONFIG_LEDC_TIMER, // LEDC timer to be used for generating XCLK
.ledc_channel = CAMERA_CONFIG_LEDC_CHANNEL, // LEDC channel to be used for generating XCLK
.pixel_format = pixformat, // Format of the pixel data: PIXFORMAT_ + YUV422|GRAYSCALE|RGB565|JPEG
.frame_size = frame_size, // Size of the output image: FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
.jpeg_quality = jpeg_quality, // Quality of JPEG output. 0-63 lower means higher quality
.fb_count = CAMERA_CONFIG_FB_COUNT, // Number of frame buffers to be allocated. If more than one, then each frame will be acquired (double speed)
.fb_location = CAMERA_CONFIG_FB_LOCATION, // The location where the frame buffer will be allocated
.grab_mode = CAMERA_GRAB_LATEST, // When buffers should be filled
#if CONFIG_CAMERA_CONVERTER_ENABLED
conv_mode = CONV_DISABLE, // RGB<->YUV Conversion mode
#endif
.sccb_i2c_port = i2c_port // If pin_sccb_sda is -1, use the already configured I2C bus by number
};
return cam.init(camera_config);
}
void update_camera_settings()
{
auto camera = esp_camera_sensor_get();
if (camera == nullptr)
{
log_e("Unable to get camera sensor");
return;
}
camera->set_brightness(camera, param_brightness.value());
camera->set_contrast(camera, param_contrast.value());
camera->set_saturation(camera, param_saturation.value());
camera->set_special_effect(camera, lookup_camera_effect(param_special_effect.value()));
camera->set_whitebal(camera, param_whitebal.value());
camera->set_awb_gain(camera, param_awb_gain.value());
camera->set_wb_mode(camera, lookup_camera_wb_mode(param_wb_mode.value()));
camera->set_exposure_ctrl(camera, param_exposure_ctrl.value());
camera->set_aec2(camera, param_aec2.value());
camera->set_ae_level(camera, param_ae_level.value());
camera->set_aec_value(camera, param_aec_value.value());
camera->set_gain_ctrl(camera, param_gain_ctrl.value());
camera->set_agc_gain(camera, param_agc_gain.value());
camera->set_gainceiling(camera, lookup_camera_gainceiling(param_gain_ceiling.value()));
camera->set_bpc(camera, param_bpc.value());
camera->set_wpc(camera, param_wpc.value());
camera->set_raw_gma(camera, param_raw_gma.value());
camera->set_lenc(camera, param_lenc.value());
camera->set_hmirror(camera, param_hmirror.value());
camera->set_vflip(camera, param_vflip.value());
camera->set_dcw(camera, param_dcw.value());
camera->set_colorbar(camera, param_colorbar.value());
}
void start_rtsp_server()
{
log_v("start_rtsp_server");
camera_server = std::unique_ptr<rtsp_server>(new rtsp_server(cam, param_frame_duration.value(), RTSP_PORT));
// Add RTSP service to mDNS
// HTTP is already set by iotWebConf
MDNS.addService("rtsp", "tcp", 554);
}
void on_connected()
{
log_v("on_connected");
// Start the RTSP Server if initialized
if (camera_init_result == ESP_OK)
start_rtsp_server();
else
log_e("Not starting RTSP server: camera not initialized");
}
void on_config_saved()
{
log_v("on_config_saved");
update_camera_settings();
}
void setup()
{
// Disable brownout
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
// LED_BUILTIN (GPIO33) has inverted logic false => LED on
#ifdef LED_BUILTIN
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, false);
#endif
// ESP32S2 has no serial port
#ifndef ARDUINO_USB_CDC_ON_BOOT
Serial.begin(115200);
Serial.setDebugOutput(true);
#endif
log_i("CPU Freq: %d Mhz, %d core(s)", getCpuFrequencyMhz(), ESP.getChipCores());
log_i("Free heap: %d bytes", ESP.getFreeHeap());
log_i("SDK version: %s", ESP.getSdkVersion());
log_i("Board: %s", BOARD_NAME);
log_i("Starting " APP_TITLE "...");
if (CAMERA_CONFIG_FB_LOCATION == CAMERA_FB_IN_PSRAM)
{
if (!psramInit())
log_e("Failed to initialize PSRAM");
}
param_group_camera.addItem(&param_frame_duration);
param_group_camera.addItem(&param_frame_size);
param_group_camera.addItem(&param_jpg_quality);
param_group_camera.addItem(&param_brightness);
param_group_camera.addItem(&param_contrast);
param_group_camera.addItem(&param_saturation);
param_group_camera.addItem(&param_special_effect);
param_group_camera.addItem(&param_whitebal);
param_group_camera.addItem(&param_awb_gain);
param_group_camera.addItem(&param_wb_mode);
param_group_camera.addItem(&param_exposure_ctrl);
param_group_camera.addItem(&param_aec2);
param_group_camera.addItem(&param_ae_level);
param_group_camera.addItem(&param_aec_value);
param_group_camera.addItem(&param_gain_ctrl);
param_group_camera.addItem(&param_agc_gain);
param_group_camera.addItem(&param_gain_ceiling);
param_group_camera.addItem(&param_bpc);
param_group_camera.addItem(&param_wpc);
param_group_camera.addItem(&param_raw_gma);
param_group_camera.addItem(&param_lenc);
param_group_camera.addItem(&param_hmirror);
param_group_camera.addItem(&param_vflip);
param_group_camera.addItem(&param_dcw);
param_group_camera.addItem(&param_colorbar);
iotWebConf.addParameterGroup(&param_group_camera);
iotWebConf.getApTimeoutParameter()->visible = true;
iotWebConf.setConfigSavedCallback(on_config_saved);
iotWebConf.setWifiConnectionCallback(on_connected);
#ifdef LED_BUILTIN
iotWebConf.setStatusPin(LED_BUILTIN, LOW);
#endif
iotWebConf.init();
camera_init_result = initialize_camera();
if (camera_init_result == ESP_OK)
update_camera_settings();
else
log_e("Failed to initialize camera: 0x%0x. Frame size: %s, frame rate: %d ms, jpeg quality: %d", camera_init_result, param_frame_size.value(), param_frame_duration.value(), param_jpg_quality.value());
// Set up required URL handlers on the web server
web_server.on("/", HTTP_GET, handle_root);
web_server.on("/config", []
{ iotWebConf.handleConfig(); });
// Camera snapshot
web_server.on("/snapshot", HTTP_GET, handle_snapshot);
// Camera stream
web_server.on("/stream", HTTP_GET, handle_stream);
web_server.onNotFound([]()
{ iotWebConf.handleNotFound(); });
}
void loop()
{
iotWebConf.doLoop();
if (camera_server)
camera_server->doLoop();
sleep(0);
}