- Moved html from SPIFF to code

- Added script to process html
- Added WRover and MStack camera's
- Automatic detection PSRAM
- Added setting for # frame buffers
- Feedback of camera initialization value
- Added connected feedback
This commit is contained in:
Rene Zeldenthuis
2022-09-12 23:45:04 +02:00
parent 291a620715
commit 8174b1e5d2
12 changed files with 281 additions and 121 deletions

View File

@@ -3,7 +3,7 @@
![status badge](https://github.com/rzeldent/esp32cam-rtsp/actions/workflows/main.yml/badge.svg?event=push)
Simple [RTSP](https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol) server.
Easy configuration through the web interface. Stable.
Easy configuration through the web interface.
Flashing this software on a esp32cam module will make it a **RTSP streaming camera** server.
This allows CCTV systems and applications like **VLC** to connect directly to the ESP32CAM camera stream.
@@ -24,6 +24,7 @@ This software provides a **configuration web server**, that can be used to:
- Select the board type,
- Select the image size,
- Select the frame rate,
- Select number of frame buffers
- Select the JPEG quality
The software provides contains also a mDNS server to be easily discoverable on the local network.
@@ -80,17 +81,15 @@ There are to flavours to do this; using the command line or the graphical interf
### Using the command line
First the source code and SPIFF partition (data) has to be compiled. Type:
First the source code has to be compiled. Type:
```
pio run
pio run buildfs
```
When finished, the SPIFF partition and software have to be uploaded.
When finished, firmware has to be uploaded.
Make sure the ESP32-CAM is in download mode (see previous section) and type:
```
pio run -t upload
pio run -t uploadfs
```
When done remove the jumper when using a FTDI adapter or press the reset button on the ESP32-CAM.
@@ -101,11 +100,9 @@ To monitor the output, start a terminal using:
### Using Visual studio
Open the project in a new window. Run the following tasks using the ```Terminal -> Run Task``` or CTRL+ALT+T command in the menu. Make sure the ESP32-CAM is in download mode during the uploads.
Open the project in a new window. Run the following tasks using the ```Terminal -> Run Task``` or CTRL+ALT+T command in the menu (or use the icons below on the toolbar). Make sure the ESP32-CAM is in download mode during the uploads.
- PlatformIO: Build Filesystem Image (esp32cam)
- PlatformIO: Build (esp32cam)
- PlatformIO: Upload Filesystem Image (esp32cam)
- PlatformIO: Upload (esp32cam)
To monitor the behavior run the task, run:
@@ -113,7 +110,7 @@ To monitor the behavior run the task, run:
## Setting up the ESP32CAM-RTSP
After the programming of the ESP32, there is no configuration present. This needs to be added.
To connect initially to the device open the WiFi connections and select the WiFi network / accesspoint called **ESP32CAM-RTSP**.
To connect initially to the device open the WiFi connections and select the WiFi network / access point called **ESP32CAM-RTSP**.
Initially there is no password present.
After connecting, the browser should automatically open the status page.
@@ -151,6 +148,14 @@ 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!**
## Issues observed
### Power
Make sure the power is 5 volts and stable.
### PSRAM
Some esp32cam modules have additional ram on the board. This allows to use this ram as frame buffer.
## Credits
esp32cam-ready depends on PlatformIO, Bootstap5 and Micro-RTSP by Kevin Hester.

View File

@@ -1,82 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="bootstrap.min.css">
<title>{{AppTitle}} v{{AppVersion}}</title>
</head>
<body>
<div class="container">
<h1>{{ThingName}}</h1>
<hr>
{{#ConfigChanged}}
<div class="alert alert-danger" role="alert">
<p>The configuration has been changed. It is recommended to restart the device</p>
<p>This can be done by pressing the restart button below</p>
<button type="button" class="btn btn-danger" onclick="location.href='restart'">Restart</button>
</div>
{{/ConfigChanged}}
<div class="card bg-light mb-3">
<h5 class="card-header">ESP32</h5>
<div class="card-body">
<p>CPU model: {{ChipModel}}</p>
<p>CPU speed: {{CpuFreqMHz}}Mhz</p>
<p>Mac address: {{MacAddress}}</p>
<p>IPv4 address: {{IpV4}}</p>
<p>IPv6 address: {{IpV6}}</p>
</ul>
</div>
</div>
<div class="card bg-light mb-3">
<h5 class="card-header">Settings</h5>
<div class="card-body">
<p>Camera type: {{CameraType}}</p>
<p>Frame size: {{FrameSize}}</p>
<p>Frame rate: {{FrameDuration}} ms ({{FrameFrequency}} f/s)</p>
<p>JPEG quality: {{JpegQuality}} (0-100)</p>
{{#CameraInitialized}}
<div class="alert alert-success" role="alert">
Camera was initialized successfully!
</div>
{{/CameraInitialized}}
{{^CameraInitialized}}
<div class="alert alert-danger" role="alert">
Failed to initialize the camera! No streaming possible. Please change the camera settings and
restart
</div>
{{/CameraInitialized}}
</div>
</div>
<div class="card bg-light mb-3">
<h5 class="card-header">Diagnostics</h5>
<div class="card-body">
<p>Uptime: {{Uptime}}</p>
<p>Free heap: {{FreeHeap}}b</p>
<p>Max free block: {{MaxAllocHeap}}b</p>
</div>
</div>
<div class="card bg-light mb-3">
<h5 class="card-header">Camera stream</h5>
<div class="card-body">
The camera stream can be found at the following location:
<a
href="rtsp://{{ThingName}}.local:{{RtspPort}}/mjpeg/1">rtsp://{{ThingName}}.local:{{RtspPort}}/mjpeg/1</a>
</div>
</div>
<button type="button" class="btn btn-lg btn-warning" onclick="location.href='config'">Change
configuration</button>
</div>
</body>
</html>

4
generate_html.ps1 Normal file
View File

@@ -0,0 +1,4 @@
. python3 -m pip install --upgrade pip setuptools wheel
. python3 -m pip install htmlmin
. python3 ./html_to_cpp.py ./html ./include/html_data.h

5
generate_html.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
python3 -m pip install --upgrade pip setuptools wheel
python3 -m pip install htmlmin
python3 ./html_to_cpp.py ./html ./include/html_data.h

112
html/index.html Normal file
View File

@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="bootstrap.min.css">
<title>{{AppTitle}} v{{AppVersion}}</title>
</head>
<body>
<div class="container">
<h1>{{ThingName}}</h1>
<hr>
{{#ConfigChanged}}
<div class="alert alert-danger" role="alert">
<p>The configuration has been changed.</p>
<p>It is recommended to restart the device.</p>
<button type="button" class="btn btn-danger" onclick="location.href='restart'">Restart</button>
</div>
{{/ConfigChanged}}
<div class="card bg-light mb-3">
<h5 class="card-header">ESP32</h5>
<div class="card-body">
<ul>
<li>CPU model: {{ChipModel}}</li>
<li>CPU speed: {{CpuFreqMHz}}Mhz</li>
<li>Mac address: {{MacAddress}}</li>
<li>IPv4 address: {{IpV4}}</li>
<li>IPv6 address: {{IpV6}}</li>
</ul>
{{#NetworkState.ApMode}}
<div class="alert alert-warning" role="alert">
<p>Not connected to an access point. Please configure!</p>
</div>
{{/NetworkState.ApMode}}
{{#NetworkState.OnLine}}
<div class="alert alert-success" role="alert">
<p>Connected to the access point</p>
</div>
{{/NetworkState.OnLine}}
</ul>
</div>
</div>
<div class="card bg-light mb-3">
<h5 class="card-header">Settings</h5>
<div class="card-body">
<ul>
<li>Camera type: {{CameraType}}</li>
<li>Frame rate: {{FrameDuration}} ms ({{FrameFrequency}} f/s)</li>
<li>Frame size: {{FrameSize}}</li>
<li>Frame buffer location: {{FrameBufferLocation}}</li>
<li>Frame buffers: {{FrameBuffers}}</li>
<li>
<div class="d-flex justify-content-start">
JPEG quality:
</div>
<div class="d-flex justify-content-end">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 25%;"
aria-valuenow="{{JpegQuality}}" aria-valuemin="0" aria-valuemax="100">
{{JpegQuality}}
</div>
</div>
</div>
</li>
</ul>
{{#CameraInitialized}}
<div class="alert alert-success" role="alert">
<p>Camera was initialized successfully!</p>
</div>
{{/CameraInitialized}}
{{^CameraInitialized}}
<div class="alert alert-danger" role="alert">
<p>Failed to initialize the camera!</p>
<p>Result: {{CameraInitResultText}} ({{CameraInitResult}})</p>
<p>Please check hardware or correct the camera settings and restart.</p>
</div>
{{/CameraInitialized}}
</div>
</div>
<div class="card bg-light mb-3">
<h5 class="card-header">Diagnostics</h5>
<div class="card-body">
<ul>
<li>Uptime: {{Uptime}}</li>
<li>Free heap: {{FreeHeap}}b</li>
<li>Max free block: {{MaxAllocHeap}}b</li>
</ul>
</div>
</div>
<div class="card bg-light mb-3">
<h5 class="card-header">Camera stream</h5>
<div class="card-body">
</p>The camera stream can be found at the following location:</p>
<a
href="rtsp://{{ThingName}}.local:{{RtspPort}}/mjpeg/1">rtsp://{{ThingName}}.local:{{RtspPort}}/mjpeg/1</a>
</div>
</div>
<button type="button" class="btn btn-warning" onclick="location.href='config'">Settings</button>
</div>
</body>
</html>

View File

@@ -5,6 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="bootstrap.min.css">
<meta http-equiv="refresh" content="10;url=/">
<title>{{AppTitle}} v{{AppVersion}}</title>
</head>
@@ -14,7 +15,7 @@
<hr>
<div class="jumbotron bg-light">
<h1 class="display-4">Restart</h1>
<p class="lead">The device is currently restarting. Please stand by...</p>
<p class="lead">The device is restarting.</p>
<hr class="my-4">
<p>In some cases, the device requires a hard reset (power cycle).</p>
<div class="d-flex justify-content-center">

46
html_to_cpp.py Normal file
View File

@@ -0,0 +1,46 @@
# pip install Pillow
import os
import sys
import htmlmin
if (len(sys.argv) <= 2):
print('Usage: html_to_cpp.py <input_dir> <file.h>')
sys.exit(1)
input_dir = sys.argv[1]
file_h = sys.argv[2]
file_names = os.listdir(input_dir)
file_names = filter(lambda x: x[0] != '.' and os.path.isfile(os.path.join(input_dir, x)), file_names)
file_names = sorted(file_names)
output_file = open(file_h, 'w')
output_file.write('//*******************************************************************************\n')
output_file.write('// HTML import\n')
output_file.write('// Machine generated file\n')
output_file.write('// ******************************************************************************\n')
output_file.write('\n')
for file_name in file_names:
print(f'Processing: {file_name}... ')
file_path = os.path.join(input_dir, file_name)
file_data_name = f'file_data_{file_name}'.replace('.', '_')
file = open(file_path, 'r')
html = file.read()
file.close()
html_mimified = htmlmin.minify(html, remove_empty_space=True)
output_file.write('\n')
output_file.write('constexpr char ' + file_data_name + '[] = "')
# escape "
html_mimified_escaped = html_mimified.replace('"', '\\"')
output_file.write(html_mimified_escaped)
output_file.write('";\n')
output_file.close()
print('Done.')

View File

@@ -86,11 +86,63 @@ constexpr camera_config_t esp32cam_ttgo_t_settings = {
.jpeg_quality = 12,
.fb_count = 2};
constexpr camera_config_t esp32cam_m5stack_settings = {
.pin_pwdn = -1,
.pin_reset = 15,
.pin_xclk = 27,
.pin_sscb_sda = 25,
.pin_sscb_scl = 23,
.pin_d7 = 19,
.pin_d6 = 36,
.pin_d5 = 18,
.pin_d4 = 39,
.pin_d3 = 5,
.pin_d2 = 34,
.pin_d1 = 35,
.pin_d0 = 32,
.pin_vsync = 22,
.pin_href = 26,
.pin_pclk = 21,
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_SVGA,
.jpeg_quality = 12,
.fb_count = 2};
constexpr camera_config_t esp32cam_wrover_kit_settings = {
.pin_pwdn = -1,
.pin_reset = -1,
.pin_xclk = 21,
.pin_sscb_sda = 26,
.pin_sscb_scl = 27,
.pin_d7 = 35,
.pin_d6 = 34,
.pin_d5 = 39,
.pin_d4 = 36,
.pin_d3 = 19,
.pin_d2 = 18,
.pin_d1 = 5,
.pin_d0 = 4,
.pin_vsync = 25,
.pin_href = 23,
.pin_pclk = 22,
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_SVGA,
.jpeg_quality = 12,
.fb_count = 2};
constexpr const camera_config_entry_t camera_configs[] = {
{"ESP32CAM", esp32cam_settings},
{"AI THINKER", esp32cam_aithinker_settings},
{"TTGO T-CAM", esp32cam_ttgo_t_settings}};
{"TTGO T-CAM", esp32cam_ttgo_t_settings},
{"M5 STACK", esp32cam_m5stack_settings},
{"WROVER KIT", esp32cam_wrover_kit_settings}};
const camera_config_t lookup_camera_config(const char *pin)
{

11
include/html_data.h Normal file

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,7 @@
#define RTSP_PORT 554
#define DEFAULT_CAMERA_CONFIG "AI THINKER"
#define DEFAULT_FRAMEDURATION "20"
#define DEFAULT_FRAMESIZE "SVGA (800x600)"
#define DEFAULT_FRAME_DURATION "20"
#define DEFAULT_FRAME_BUFFERS "2"
#define DEFAULT_FRAME_SIZE "SVGA (800x600)"
#define DEFAULT_JPEG_QUALITY "12"

View File

@@ -9,19 +9,21 @@
#include <camera_config.h>
#include <format_duration.h>
#include <format_si.h>
#include <SPIFFS.h>
#include <template_render.h>
#include <html_data.h>
#include <settings.h>
char camera_config_val[sizeof(camera_config_entry)];
char frame_duration_val[6];
char frame_size_val[sizeof(frame_size_entry_t)];
char frame_buffers_val[3];
char jpeg_quality_val[4];
auto config_group_stream_settings = iotwebconf::ParameterGroup("settings", "Streaming settings");
auto config_camera_config = iotwebconf::SelectParameter("Camera config", "config", camera_config_val, sizeof(camera_config_val), (const char *)camera_configs, (const char *)camera_configs, sizeof(camera_configs) / sizeof(camera_configs[0]), sizeof(camera_configs[0]), DEFAULT_CAMERA_CONFIG);
auto config_frame_rate = iotwebconf::NumberParameter("Frame duration (ms)", "fd", frame_duration_val, sizeof(frame_duration_val), DEFAULT_FRAMEDURATION, nullptr, "min=\"10\"");
auto config_frame_size = iotwebconf::SelectParameter("Frame size", "fs", frame_size_val, sizeof(frame_size_val), (const char *)frame_sizes, (const char *)frame_sizes, sizeof(frame_sizes) / sizeof(frame_sizes[0]), sizeof(frame_sizes[0]), DEFAULT_FRAMESIZE);
auto config_frame_rate = iotwebconf::NumberParameter("Frame duration (ms)", "fd", frame_duration_val, sizeof(frame_duration_val), DEFAULT_FRAME_DURATION, nullptr, "min=\"10\"");
auto config_frame_size = iotwebconf::SelectParameter("Frame size", "fs", frame_size_val, sizeof(frame_size_val), (const char *)frame_sizes, (const char *)frame_sizes, sizeof(frame_sizes) / sizeof(frame_sizes[0]), sizeof(frame_sizes[0]), DEFAULT_FRAME_SIZE);
auto config_frame_buffers = iotwebconf::NumberParameter("Frame buffers", "fb", frame_buffers_val, sizeof(frame_buffers_val), DEFAULT_FRAME_BUFFERS, nullptr, "min=\"1\" max=\"16\"");
auto config_jpg_quality = iotwebconf::NumberParameter("JPEG quality", "q", jpeg_quality_val, sizeof(jpeg_quality_val), DEFAULT_JPEG_QUALITY, nullptr, "min=\"1\" max=\"100\"");
// Camera
@@ -36,16 +38,14 @@ IotWebConf iotWebConf(WIFI_SSID, &dnsServer, &web_server, WIFI_PASSWORD, CONFIG_
// Keep track of config changes. This will allow a reset of the device
bool config_changed = false;
// Check if camera is initialized
bool camera_initialized = false;
// Camera initialization result
esp_err_t camera_init_result;
void stream_file(const char *spiffs_file, const char *mime_type)
void stream_text_file(const char *contents, const char *mime_type)
{
// Cache for 86400 seconds (one day)
web_server.sendHeader("Cache-Control", "max-age=86400");
auto file = SPIFFS.open(spiffs_file);
web_server.streamFile(file, mime_type);
file.close();
web_server.send(200, mime_type, contents);
}
void handle_root()
@@ -68,18 +68,22 @@ void handle_root()
{"FrameSize", frame_size_val},
{"FrameDuration", frame_duration_val},
{"FrameFrequency", String(1000.0 / atol(frame_duration_val), 1)},
{"FrameBufferLocation", psramFound() ? "PSRAM" : "DRAM)"},
{"FrameBuffers", frame_buffers_val},
{"JpegQuality", jpeg_quality_val},
{"Uptime", String(format_duration(millis() / 1000))},
{"FreeHeap", format_si(ESP.getFreeHeap())},
{"MaxAllocHeap", format_si(ESP.getMaxAllocHeap())},
{"RtspPort", String(RTSP_PORT)},
{"ConfigChanged", String(config_changed)},
{"CameraInitialized", String(camera_initialized)}};
{"NetworkState.ApMode", String(iotWebConf.getState() == iotwebconf::NetworkState::ApMode)},
{"NetworkState.OnLine", String(iotWebConf.getState() == iotwebconf::NetworkState::OnLine)},
{"CameraInitialized", String(camera_init_result == ESP_OK)},
{"CameraInitResult", "0x" + String(camera_init_result, 16)},
{"CameraInitResultText", esp_err_to_name(camera_init_result)}};
web_server.sendHeader("Cache-Control", "no-cache");
auto file = SPIFFS.open("/index.html");
auto html = template_render(file.readString(), substitutions);
file.close();
auto html = template_render(file_data_index_html, substitutions);
web_server.send(200, "text/html", html);
}
@@ -100,12 +104,10 @@ void handle_restart()
{"ThingName", iotWebConf.getThingName()}};
web_server.sendHeader("Cache-Control", "no-cache");
auto file = SPIFFS.open("/restart.html");
auto html = template_render(file.readString(), substitutions);
file.close();
auto html = template_render(file_data_restart_html, substitutions);
web_server.send(200, "text/html", html);
log_v("Restarting... Press refresh to connect again");
sleep(1000);
sleep(100);
ESP.restart();
}
@@ -115,29 +117,34 @@ void on_config_saved()
config_changed = true;
}
bool initialize_camera()
esp_err_t initialize_camera()
{
log_v("initialize_camera");
log_i("Camera config: %s", camera_config_val);
auto camera_config = lookup_camera_config(camera_config_val);
log_i("Frame size: %s", frame_size_val);
auto frame_size = lookup_frame_size(frame_size_val);
log_i("Frame buffers: %s", frame_buffers_val);
auto frame_buffers = atoi(frame_buffers_val);
log_i("JPEG quality: %s", jpeg_quality_val);
auto jpeg_quality = atoi(jpeg_quality_val);
log_i("Frame rate: %s ms", frame_duration_val);
camera_config.frame_size = frame_size;
camera_config.fb_count = frame_buffers;
camera_config.fb_location = psramFound() ? CAMERA_FB_IN_PSRAM : CAMERA_FB_IN_DRAM;
camera_config.jpeg_quality = jpeg_quality;
return cam.init(camera_config) == ESP_OK;
return cam.init(camera_config);
}
void start_rtsp_server()
{
log_v("start_rtsp_server");
camera_initialized = initialize_camera();
if (!camera_initialized)
camera_init_result = initialize_camera();
if (camera_init_result != ESP_OK)
{
log_e("Failed to initialize camera. Type: %s, frame size: %s, frame rate: %s ms, jpeg quality: %s", camera_config_val, frame_size_val, frame_duration_val, jpeg_quality_val);
log_e("Failed to initialize camera: 0x%0xd. Type: %s, frame size: %s, frame buffers: %s, frame rate: %s ms, jpeg quality: %s", camera_init_result, camera_config_val, frame_size_val, frame_buffers_val, frame_duration_val, jpeg_quality_val);
return;
}
@@ -171,12 +178,10 @@ void setup()
log_i("Free heap: %d bytes", ESP.getFreeHeap());
log_i("Starting " APP_TITLE "...");
if (!SPIFFS.begin())
log_e("Error while mounting SPIFFS. Please upload the filesystem");
config_group_stream_settings.addItem(&config_camera_config);
config_group_stream_settings.addItem(&config_frame_rate);
config_group_stream_settings.addItem(&config_frame_size);
config_group_stream_settings.addItem(&config_frame_buffers);
config_group_stream_settings.addItem(&config_jpg_quality);
iotWebConf.addParameterGroup(&config_group_stream_settings);
iotWebConf.getApTimeoutParameter()->visible = true;
@@ -192,7 +197,7 @@ void setup()
// bootstrap
web_server.on("/bootstrap.min.css", HTTP_GET, []()
{ stream_file("/bootstrap.min.css", "text/css"); });
{ stream_text_file(file_data_bootstrap_min_css, "text/css"); });
web_server.onNotFound([]()
{ iotWebConf.handleNotFound(); });