8 Commits
1.0.4 ... 1.0.5

Author SHA1 Message Date
rzeldent
730d941988 Merge pull request #54 from rzeldent/feature/http_stream
Added JPEG Motion streaming
2023-03-25 22:26:11 +01:00
Rene Zeldenthuis
2fdb589bfd MD update 2023-03-25 22:24:11 +01:00
Rene Zeldenthuis
e69ff1fbe8 MD changes 2023-03-25 22:19:27 +01:00
Rene Zeldenthuis
064f0e9ac9 Updated MD 2023-03-25 22:13:17 +01:00
Rene Zeldenthuis
7ceb878a59 Merge branch 'develop' into feature/http_stream 2023-03-25 22:01:07 +01:00
Rene Zeldenthuis
19b8f77d57 Added JPEG Motion streaming 2023-03-25 21:39:11 +01:00
rzeldent
5dac9876fb Merge pull request #53 from rzeldent/feature/psram
Added setting for PSRAM and Buffers
2023-03-25 21:35:42 +01:00
Rene Zeldenthuis
1b06941ee8 Added setting for PSRAM and Buffers 2023-03-25 20:33:59 +01:00
5 changed files with 183 additions and 65 deletions

View File

@@ -1,14 +1,26 @@
# ESP32CAM-RTSP
# ESP32CAM-RTSP :video_camera:
[![Platform IO CI](https://github.com/rzeldent/esp32cam-rtsp/actions/workflows/main.yml/badge.svg)](https://github.com/rzeldent/esp32cam-rtsp/actions/workflows/main.yml)
Simple [RTSP](https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol) server.
Easy configuration through the web interface.
Simple [RTSP](https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol), [HTTP JPEG Streamer](https://en.wikipedia.org/wiki/Motion_JPEG) and image server with configuration through the web interface.
Flashing this software on a ESP32CAM module will make it a **RTSP streaming camera** server.
The RTSP protocol is an industry standard and allows many CCTV systems and applications (like for example [VLC](https://www.videolan.org/vlc/)) to connect directly to the ESP32CAM camera stream.
It is also possible to stream directly to a server using [ffmpeg](https://ffmpeg.org).
This makes the module a camera server allowing recording and the stream can be stored on a disk and replayed later.
Flashing this software on a ESP32CAM module will make it a **RTSP streaming camera** server, a **HTTP Motion JPEG streamer** and a **HTTP image server**.
Supported protocols
- :white_check_mark: RTSP
The RTSP protocol is an industry standard and allows many CCTV systems and applications (like for example [VLC](https://www.videolan.org/vlc/)) to connect directly to the ESP32CAM camera stream.
It is also possible to stream directly to a server using [ffmpeg](https://ffmpeg.org).
This makes the module a camera server allowing recording and the stream can be stored on a disk and replayed later.
The URL is rtsp://<ip address>:554/mjpeg/1
- :white_check_mark: HTTP Motion JPEG
The HTTP JPEG streamer makes it possible to watch the camera stream directly in your browser.
The URL is http://<ip address>/stream
- :white_check_mark: HTTP image
The HTTP Image returns an HTTP JPEG image of the camera.
The URL is http://<ip address>/snapshot
This software supports the following ESP32-CAM (and alike) modules:
@@ -20,7 +32,7 @@ This software supports the following ESP32-CAM (and alike) modules:
![ESP32CAM module](assets/ESP32-CAM.jpg)
This software provides a **configuration web server**, that can be used to:
The software provides a **configuration web server**, that can be used to:
- Provide information about the state of the device, wifi connection and camera,
- Set the WiFi parameters,
@@ -30,6 +42,8 @@ This software provides a **configuration web server**, that can be used to:
- Select the image size,
- Select the frame rate,
- Select the JPEG quality
- Enable the use of the PSRAM
- Set the number of frame buffers
- Configure the camera options:
- Brightness
- Contrast
@@ -185,12 +199,21 @@ In case changes have been made to the configuration, this is shown and the possi
Clicking on the ```change configuration``` button will open the configuration. It is possible that a password dialog is shown before entering.
If this happens, for the user enter 'admin' and for the password the value that has been configured as the Access Point password.
## Connecting to the RTSP stream
## Connecting to the RTSP stream :video_camera:
RTSP stream is available at: [rtsp://esp32cam-rtsp.local:554/mjpeg/1](rtsp://esp32cam-rtsp.local:554/mjpeg/1).
This link can be opened with for example [VLC](https://www.videolan.org/vlc/).
:warning: **Please be aware that there is no password present on the stream!**
## Connecting to the JPEG motion server :video_camera:
The JPEG motion server server is available using a normal web browser at: [http://esp32cam-rtsp.local:/stream](http://esp32cam-rtsp.local/stream).
## Connecting to the image server :camera:
The image server server is available using a normal web browser at: [http://esp32cam-rtsp.local:/snapshot](http://esp32cam-rtsp.local/snapshot).
:bangbang: **Please be aware that there is no password present!**.
Everybody with access to the device can see the streams or images! Beware of :trollface:!
## API
@@ -237,30 +260,66 @@ If no v parameter is present, it will be set to the value of the flash LED inten
- When finished configuring for the first time and the access point is entered, disconnect from the wireless network provided by the device.
This should reset the device and connect to the access point.
Resetting is also a good alternative...
- There are modules that have no or faulty PSRAM (despite advertised as such).
This can be the case if the camera fails to initialize.
It might help to disable the use of the PSRAM and reduce the buffers and the screen size.
### Power
Make sure the power is 5 volts and stable, although the ESP32 is a 3.3V module, this voltage is created on the board.
If not stable, it has been reported that restarts occur when starting up (probably when power is required for WiFi).
The software disables the brown out protection so there is some margin in the voltage.
Some people suggest to add a capacitor over the 5V input to stabilize the voltage.
### PSRAM
### PSRAM / Buffers / JPEG quality
Some esp32cam modules have additional ram on the board. This allows to use this ram as frame buffer.
Detecting and using this special RAM is handled automatically.
The availability of PSRAM can be seen in the HTML status overview.
### Camera modules
Not all the boards are equipped with PSRAM:
| Board | PSRAM |
|--- |--- |
| ESP32CAM | Yes |
| ESP32CAM (USB-C) | No |
| AI THINKER | Yes |
| TTGO T-CAM | No |
| M5 STACK| | No |
| WROVER KIT | Yes |
Depending on the image resolution, framerate and quality, the PSRAM must be enabled and/or the number of frame buffers increased to keep up with the data generated by the sensor.
There are (a lot of?) boards around with faulty PSRAM. If the camera fails to initialize, this might be a reason. See on [Reddit](https://www.reddit.com/r/esp32/comments/z2hyns/i_have_a_faulty_psram_on_my_esp32cam_what_should/).
In this case disable the use of PSRAM in the configuration and do not use camera modes that require PSRAM,
For the setting JPEG quality, a lower number means higher quality.
Be aware that a very high quality (low number) can cause the ESP32 cam to crash or return no image.
The default settings are:
- No PSRAM
- SVGA (800x600)
- 1 frame buffer
- JPEG quality 12
- With PSRAM
- UXGA (1600x1200)
- 2 frame buffers
- JPEG quality 10
### Camera module
Be careful when connecting the camera module.
Make sure it is connected the right way around (Camera pointing away from the board) and the ribbon cable inserted to the end before locking it.
## Credits
esp32cam-ready depends on PlatformIO, Bootstrap5 and Micro-RTSP by Kevin Hester.
esp32cam-rtsp depends on PlatformIO, Bootstrap 5 and Micro-RTSP by Kevin Hester.
## Change history
- March 2023
- Added options to set PSRAM / Frame buffers
- Added JPEG Motion streaming
- Feb 2023
- Added additional settings for camera configuration
- Nov 2022

View File

@@ -26,6 +26,10 @@
<div class="card bg-light mb-3">
<h5 class="card-header">ESP32</h5>
<div class="card-body">
<div class="row">
<div class="col-4">SDK Version:</div>
<div class="col-8">{{SDKVersion}}</div>
</div>
<div class="row">
<div class="col-4">CPU model:</div>
<div class="col-8">{{ChipModel}} rev. {{ChipRevision}}</div>
@@ -125,12 +129,12 @@
</div>
{{#NetworkState.ApMode}}
<div class="mt-4 alert alert-warning" role="alert">
<p>Not connected to an access point. Consider configuring the access point.</p>
<h4 class="text-center">Not connected to an access point.<br>Consider configuring the access point.</h4>
</div>
{{/NetworkState.ApMode}}
{{#NetworkState.OnLine}}
<div class="mt-4 alert alert-success" role="alert">
<p>Connected to the access point</p>
<h4 class="text-center">Connected to the access point</h4>
</div>
{{/NetworkState.OnLine}}
</div>
@@ -148,18 +152,18 @@
<div class="col-4">Frame size:</div>
<div class="col-8">{{FrameSize}}</div>
</div>
<div class="row">
<div class="col-4">Frame buffer location:</div>
<div class="col-8">{{FrameBufferLocation}}</div>
</div>
<div class="row">
<div class="col-4">Frame buffers:</div>
<div class="col-8">{{FrameBuffers}}</div>
</div>
<div class="row">
<div class="col-4">JPEG quality:</div>
<div class="col-8">{{JpegQuality}} [1-100]</div>
</div>
<div class="row">
<div class="col-4">Enable PSRAM:</div>
<div class="col-8">{{#EnablePSRAM}}Enabled{{/EnablePSRAM}}{{^EnablePSRAM}}Disabled{{/EnablePSRAM}}</div>
</div>
<div class="row">
<div class="col-4">Number of frame buffers:</div>
<div class="col-8">{{FrameBuffers}}</div>
</div>
<div class="row">
<div class="col-4">Brightness:</div>
<div class="col-8">{{Brightness}} [-2,2]</div>
@@ -178,11 +182,11 @@
</div>
<div class="row">
<div class="col-4">White balance:</div>
<div class="col-8">{{WhiteBal}}</div>
<div class="col-8">{{#WhiteBal}}Auto{{/WhiteBal}}{{^WhiteBal}}Manual{{/WhiteBal}}</div>
</div>
<div class="row">
<div class="col-4">AWB gain:</div>
<div class="col-8">{{AwbGain}}</div>
<div class="col-8">{{#AwbGain}}Auto{{/AwbGain}}{{^AwbGain}}Manual{{/AwbGain}}</div>
</div>
<div class="row">
<div class="col-4">WB mode:</div>
@@ -190,11 +194,12 @@
</div>
<div class="row">
<div class="col-4">Exposure control:</div>
<div class="col-8">{{ExposureCtrl}}</div>
<div class="col-8">
{{#ExposureCtrl}}Auto{{/ExposureCtrl}}{{^ExposureCtrl}}Manual{{/ExposureCtrl}}</div>
</div>
<div class="row">
<div class="col-4">Auto exposure control (dsp):</div>
<div class="col-8">{{Aec2}}</div>
<div class="col-8">{{#Aec2}}Enabled{{/Aec2}}{{^Aec2}}Disabled{{/Aec2}}</div>
</div>
<div class="row">
<div class="col-4">Auto Exposure level:</div>
@@ -206,7 +211,7 @@
</div>
<div class="row">
<div class="col-4">Gain control:</div>
<div class="col-8">{{GainCtrl}}</div>
<div class="col-8">{{#GainCtrl}}Auto{{/GainCtrl}}{{^GainCtrl}}Manual{{/GainCtrl}}</div>
</div>
<div class="row">
<div class="col-4">AGC gain:</div>
@@ -218,48 +223,47 @@
</div>
<div class="row">
<div class="col-4">Black pixel correct:</div>
<div class="col-8">{{Bpc}}</div>
<div class="col-8">{{#Bpc}}Auto{{/Bpc}}{{^Bpc}}Manual{{/Bpc}}</div>
</div>
<div class="row">
<div class="col-4">White pixel correct:</div>
<div class="col-8">{{Wpc}}</div>
<div class="col-8">{{#Wpc}}Auto{{/Wpc}}{{^Wpc}}Manual{{/Wpc}}</div>
</div>
<div class="row">
<div class="col-4">Gamma correct:</div>
<div class="col-8">{{RawGma}}</div>
<div class="col-8">{{#RawGma}}Enabled{{/RawGma}}{{^RawGma}}Disabled{{/RawGma}}</div>
</div>
<div class="row">
<div class="col-4">Lens correction:</div>
<div class="col-8">{{Lenc}}</div>
<div class="col-8">{{#Lenc}}Enabled{{/Lenc}}{{^Lenc}}Disabled{{/Lenc}}</div>
</div>
<div class="row">
<div class="col-4">Horizontal mirror:</div>
<div class="col-8">{{HMirror}}</div>
<div class="col-8">{{#HMirror}}Mirrored{{/HMirror}}{{^HMirror}}Normal{{/HMirror}}</div>
</div>
<div class="row">
<div class="col-4">Vertical flip:</div>
<div class="col-8">{{VFlip}}</div>
<div class="col-8">{{#VFlip}}Flipped{{/VFlip}}{{^VFlip}}Normal{{/VFlip}}</div>
</div>
<div class="row">
<div class="col-4">Downsize enable:</div>
<div class="col-8">{{Dcw}}</div>
<div class="col-8">{{#Dcw}}Enabled{{/Dcw}}{{^Dcw}}Disabled{{/Dcw}}</div>
</div>
<div class="row">
<div class="col-4">Color bar:</div>
<div class="col-8">{{ColorBar}}</div>
<div class="col-8">{{#ColorBar}}Enabled{{/ColorBar}}{{^ColorBar}}Camera{{/ColorBar}}</div>
</div>
{{#CameraInitialized}}
<div class="mt-4 alert alert-success" role="alert">
<p>Camera was initialized successfully!</p>
<h4 class="text-center">Camera was initialized successfully!</h4>
</div>
{{/CameraInitialized}}
{{^CameraInitialized}}
<div class="mt-4 alert alert-danger" role="alert">
<p>Failed to initialize the camera!</p>
<p>Result: {{CameraInitResultText}}</p>
<h4 class="text-center">Failed to initialize the camera!</h4>
<p>Result: {{CameraInitResult}} ({{CameraInitResultText}})</p>
<p>Please check hardware or correct the camera settings and restart.</p>
<button type="button" class="btn btn-danger"
onclick="location.href='restart'">Restart</button>
<button type="button" class="btn btn-danger" onclick="location.href='restart'">Restart</button>
</div>
{{/CameraInitialized}}
</div>
@@ -273,28 +277,27 @@
<div class="card-body">
<div class="row">
<span>
The camera RTSP stream can be found at:
<a href="rtsp://{{IpV4}}:{{RtspPort}}/mjpeg/1">rtsp://{{IpV4}}:{{RtspPort}}/mjpeg/1</a>
RTSP camera stream: <a href="rtsp://{{IpV4}}:{{RtspPort}}/mjpeg/1">rtsp://{{IpV4}}:{{RtspPort}}/mjpeg/1</a>
</span>
</div>
<div class="row">
<span>
A snapshot of the camera can be found at:
<a href="http://{{IpV4}}/snapshot">http://{{IpV4}}/snapshot</a>
JPEG Motion stream: <a href="http://{{IpV4}}/stream" target="_blank">http://{{IpV4}}/stream</a>
</span>
</div>
<div class="row">
<span>
The intensity of the flash led (0-255) can be controlled using:
<a href="http://{{IpV4}}/flash?v=0">http://{{IpV4}}/flash?v=value</a>.
Authentication is required and if no value is present, the configuration value is used.
Snapshot of the camera: <a href="http://{{IpV4}}/snapshot " target="_blank">http://{{IpV4}}/snapshot</a>
</span>
</div>
<div class="row">
<span>
Restarting the camera can be done using:
<a href="http://{{IpV4}}/restart">http://{{IpV4}}/restart</a>.
Authentication is required.
Intensity of the flash led (0-255): <a href="http://{{IpV4}}/flash?v=0">http://{{IpV4}}/flash?v=0</a>. Authentication is required.
</span>
</div>
<div class="row">
<span>
Restart the camera: <a href="http://{{IpV4}}/restart">http://{{IpV4}}/restart</a>. Authentication is required.
</span>
</div>
</div>

File diff suppressed because one or more lines are too long

View File

@@ -5,15 +5,17 @@
#define WIFI_SSID "ESP32CAM-RTSP"
#define WIFI_PASSWORD nullptr
#define CONFIG_VERSION "1.4"
#define CONFIG_VERSION "1.5"
#define OTA_PASSWORD "ESP32CAM-RTSP"
#define RTSP_PORT 554
#define DEFAULT_CAMERA_CONFIG "AI THINKER"
#define DEFAULT_ENABLE_PSRAM psramFound()
#define DEFAULT_BUFFERS (psramFound() ? 2 : 1)
#define DEFAULT_FRAME_DURATION 100
#define DEFAULT_FRAME_SIZE (psramFound() ? "UXGA (1600x1200)" : "VGA (640x480)")
#define DEFAULT_FRAME_SIZE (psramFound() ? "UXGA (1600x1200)" : "SVGA (800x600)")
#define DEFAULT_JPEG_QUALITY (psramFound() ? 10 : 12)
#define DEFAULT_BRIGHTNESS 0

View File

@@ -25,6 +25,8 @@ 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_enable_psram = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("eps").label("Enable PSRAM if available").defaultValue(DEFAULT_ENABLE_PSRAM).build();
auto param_frame_buffers = iotwebconf::Builder<iotwebconf::IntTParameter<int>>("fb").label("Buffers").defaultValue(DEFAULT_BUFFERS).min(1).max(4).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();
@@ -98,6 +100,7 @@ void handle_root()
{"AppTitle", APP_TITLE},
{"AppVersion", APP_VERSION},
{"ThingName", iotWebConf.getThingName()},
{"SDKVersion", ESP.getSdkVersion()},
{"ChipModel", ESP.getChipModel()},
{"ChipRevision", String(ESP.getChipRevision())},
{"CpuFreqMHz", String(ESP.getCpuFreqMHz())},
@@ -109,7 +112,7 @@ void handle_root()
{"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()) : "N/A"},
{"NumRTSPSessions", camera_server != nullptr ? String(camera_server->num_connected()) : "RTSP server disabled"},
// Network
{"HostName", hostname},
{"MacAddress", WiFi.macAddress()},
@@ -125,10 +128,11 @@ void handle_root()
{"FrameSize", String(param_frame_size.value())},
{"FrameDuration", String(param_frame_duration.value())},
{"FrameFrequency", String(1000.0 / param_frame_duration.value(), 1)},
{"FrameBufferLocation", psramFound() ? "PSRAM" : "DRAM)"},
{"FrameBuffers", String(psramFound() ? 2 : 1)},
{"JpegQuality", String(param_jpg_quality.value())},
{"EnablePSRAM", String(param_enable_psram.value())},
{"FrameBuffers", String(param_frame_buffers.value())},
{"CameraInitialized", String(camera_init_result == ESP_OK)},
{"CameraInitResult", String(camera_init_result)},
{"CameraInitResultText", esp_err_to_name(camera_init_result)},
// Settings
{"Brightness", String(param_brightness.value())},
@@ -194,7 +198,7 @@ void handle_snapshot()
return;
}
// Remove old images stored in the framebuffer
// Remove old images stored in the frame buffer
auto frame_buffers = psramFound() ? 2 : 1;
while (frame_buffers--)
cam.run();
@@ -213,6 +217,36 @@ void handle_snapshot()
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");
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");
}
void handle_flash()
{
log_v("handle_flash");
@@ -243,24 +277,33 @@ esp_err_t initialize_camera()
{
log_v("initialize_camera");
log_i("Camera config: %s", param_board.value());
auto camera_config = lookup_camera_config(param_board.value());
auto camera_config_template = lookup_camera_config(param_board.value());
// Copy the settings
camera_config_t camera_config;
memset(&camera_config, 0, sizeof(camera_config_t));
memcpy(&camera_config, &camera_config_template, sizeof(camera_config_t));
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());
log_i("Frame duration: %d ms", param_frame_duration.value());
camera_config.frame_size = frame_size;
camera_config.jpeg_quality = param_jpg_quality.value();
if (psramFound())
camera_config.grab_mode = CAMERA_GRAB_LATEST;
log_i("Enable PSRAM: %d", param_enable_psram.value());
log_i("Frame buffers: %d", param_frame_buffers.value());
camera_config.fb_count = param_frame_buffers.value();
if (param_enable_psram.value() && psramFound())
{
camera_config.fb_count = 2;
camera_config.fb_location = CAMERA_FB_IN_PSRAM;
log_i("PSRAM enabled!");
}
else
{
camera_config.fb_count = 1;
camera_config.fb_location = CAMERA_FB_IN_DRAM;
log_i("PSRAM disabled");
}
return cam.init(camera_config);
}
@@ -314,9 +357,11 @@ void on_connected()
analogWrite(LED_FLASH, param_led_intensity.value());
// Start (OTA) Over The Air programming when connected
ArduinoOTA.begin();
// Start the RTSP Server if initializef
// 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()
@@ -349,14 +394,20 @@ void setup()
log_i("CPU Freq: %d Mhz", getCpuFrequencyMhz());
log_i("Free heap: %d bytes", ESP.getFreeHeap());
log_i("SDK version: %s", ESP.getSdkVersion());
log_i("Starting " APP_TITLE "...");
if (psramFound())
psramInit();
param_group_board.addItem(&param_board);
iotWebConf.addParameterGroup(&param_group_board);
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_enable_psram);
param_group_camera.addItem(&param_frame_buffers);
param_group_camera.addItem(&param_brightness);
param_group_camera.addItem(&param_contrast);
param_group_camera.addItem(&param_saturation);
@@ -402,6 +453,9 @@ void setup()
web_server.on("/restart", HTTP_GET, handle_restart);
// Camera snapshot
web_server.on("/snapshot", HTTP_GET, handle_snapshot);
// Camera stream
web_server.on("/stream", HTTP_GET, handle_stream);
// Camera flash light
web_server.on("/flash", HTTP_GET, handle_flash);