19 Commits

Author SHA1 Message Date
Rene Zeldenthuis
12332fd7eb Work in progress 2024-11-04 00:46:58 +01:00
Rene Zeldenthuis
4dc36fb1e8 Work in progress 2024-11-02 21:45:07 +01:00
Rene Zeldenthuis
4b50b52ae0 WIP 2024-04-02 01:16:17 +02:00
Rene Zeldenthuis
b8abcdcdee WIP 2024-04-02 00:27:54 +02:00
Rene Zeldenthuis
5117f0d21a WIP 2024-04-01 19:58:44 +02:00
Rene Zeldenthuis
f59fb8bb9a WIP 2024-04-01 17:57:37 +02:00
Rene Zeldenthuis
c599657882 WIP 2024-03-30 22:31:33 +01:00
Rene Zeldenthuis
3e688ddcfb Fix 2024-03-19 14:00:12 +01:00
Rene Zeldenthuis
c2dcd8b3d4 Merge branch 'develop' into 106-create-new-rtsp-server 2024-03-19 13:58:50 +01:00
Rene Zeldenthuis
86ae9af574 WIP 2024-03-19 13:45:22 +01:00
Rene Zeldenthuis
2eb7a58e1c Work in progress 2024-02-18 12:47:17 +01:00
Rene Zeldenthuis
6119020e7d Work in progress 2024-02-17 10:12:23 +01:00
Rene Zeldenthuis
152c068f68 Fixes 2024-02-15 22:33:42 +01:00
Rene Zeldenthuis
f05932b896 Unittests in progress 2024-02-15 21:59:34 +01:00
Rene Zeldenthuis
4fbba6fc96 Unit tests in progress 2024-02-15 21:59:08 +01:00
Rene Zeldenthuis
4fb0f61e8e Merge branch 'develop' into 106-create-new-rtsp-server 2024-02-15 21:27:15 +01:00
Rene Zeldenthuis
03582b83cb WIP 2024-02-13 19:15:27 +01:00
Rene Zeldenthuis
26d1af2f28 Work in progress 2024-02-13 01:23:40 +01:00
Rene Zeldenthuis
97fc4ceb33 WIP 2024-02-11 00:42:55 +01:00
38 changed files with 1306 additions and 47 deletions

View File

@@ -97,7 +97,7 @@ There are a lot of boards available that are all called ESP32-CAM.
However, there are differences in CPU (type/speed/cores), how the camera is connected, presence of PSRAM or not...
To select the right board use the table below and use the configuration that is listed below for your board:
| Board | Image | CPU | SRAM | Flash | PSRAM | Camera | | Site |
| Board | Image | CPU | SRAM | Flash | PSRAM | Camera | Extras | Manufacturer site |
|--- |--- |--- |--- |--- | --- |--- |--- |--- |
| Espressif ESP32-Wrover CAM | ![img](assets/boards/esp32-wrover-cam.jpg) | ESP32 | 520KB | 4Mb | 4MB | OV2640 | | |
| AI-Thinker ESP32-CAM | ![img](assets/boards/ai-thinker-esp32-cam-ipex.jpg) ![img](assets/boards/ai-thinker-esp32-cam.jpg) | ESP32-S | 520KB | 4Mb | 4MB | OV2640 | | [https://docs.ai-thinker.com/esp32-cam](https://docs.ai-thinker.com/esp32-cam) |

View File

@@ -32,7 +32,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'"
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'"
],
"f_cpu": "240000000L",
"f_flash": "40000000L",

View File

@@ -32,7 +32,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'"
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",

View File

@@ -32,7 +32,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'"
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",

View File

@@ -34,7 +34,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'"
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",

View File

@@ -34,7 +34,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'"
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'"
],
"f_cpu": "240000000L",
"f_flash": "80000000L",

View File

@@ -1,6 +1,6 @@
{
"build": {
"arduino":{
"arduino": {
"ldscript": "esp32_out.ld",
"partitions": "huge_app.csv"
},
@@ -32,7 +32,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=1'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'"
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'"
],
"f_cpu": "240000000L",
"f_flash": "40000000L",

View File

@@ -32,7 +32,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'"
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'"
],
"f_cpu": "240000000L",
"f_flash": "40000000L",

View File

@@ -1,6 +1,6 @@
{
"build": {
"arduino":{
"arduino": {
"ldscript": "esp32_out.ld",
"partitions": "huge_app.csv"
},
@@ -28,7 +28,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=1'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_DRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'",
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'",
"'-D GROVE_SDA=13'",
"'-D GROVE_SCL=4'"
],

View File

@@ -30,7 +30,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'",
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'",
"'-D GROVE_SDA=13'",
"'-D GROVE_SCL=4'"
],

View File

@@ -1,6 +1,6 @@
{
"build": {
"arduino":{
"arduino": {
"ldscript": "esp32_out.ld",
"partitions": "huge_app.csv"
},
@@ -32,7 +32,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'",
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'",
"'-D MICROPHONE_GPIO=32'",
"'-D GROVE_SDA=13'",
"'-D GROVE_SCL=4'"

View File

@@ -1,6 +1,6 @@
{
"build": {
"arduino":{
"arduino": {
"ldscript": "esp32_out.ld",
"partitions": "huge_app.csv"
},
@@ -30,7 +30,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=1'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_DRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'"
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'"
],
"f_cpu": "240000000L",
"f_flash": "40000000L",

View File

@@ -36,7 +36,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_DRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'",
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'",
"'-D I2C_MEMS_SDA=17'",
"'-D I2C_MEMS_SCL=41'",
"'-D TF_CS=9'",

View File

@@ -1,6 +1,6 @@
{
"build": {
"arduino":{
"arduino": {
"ldscript": "esp32_out.ld",
"partitions": "huge_app.csv"
},
@@ -30,7 +30,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'"
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'"
],
"f_cpu": "240000000L",
"f_flash": "40000000L",

View File

@@ -36,7 +36,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=2'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'",
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'",
"'-D I2C_MEMS_SDA=41'",
"'-D I2C_MEMS_SCL=42'",
"'-D TF_CS=21'",

View File

@@ -28,7 +28,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=1'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_DRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'",
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'",
"'-D LCD_SSD1306_PIN_SDA=21'",
"'-D LCD_SSD1306_PIN_SCL=22'",
"'-D BUTTON_RIGHT_PIN=34'",

View File

@@ -28,7 +28,7 @@
"'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'",
"'-D CAMERA_CONFIG_FB_COUNT=1'",
"'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_DRAM'",
"'-D SCCB_I2C_PORT=I2C_NUM_0'"
"'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'"
],
"f_cpu": "240000000L",
"f_flash": "40000000L",

1
dotnet_riscv Submodule

Submodule dotnet_riscv added at 70e3cb657b

View File

@@ -9,6 +9,12 @@
#define OTA_PASSWORD "ESP32CAM-RTSP"
// Time servers
#define NTP_SERVER_1 "nl.pool.ntp.org"
#define NTP_SERVER_2 "europe.pool.ntp.org"
#define NTP_SERVER_3 "time.nist.gov"
#define NTP_SERVERS NTP_SERVER_1, NTP_SERVER_2, NTP_SERVER_3
#define RTSP_PORT 554
#define DEFAULT_FRAME_DURATION 200

View File

@@ -0,0 +1,18 @@
#include <stddef.h>
#include "jpg_section.h"
class jpg
{
public:
bool decode(const uint8_t *jpg, size_t size);
const jpg_section_dqt_t *quantization_table_luminance_;
const jpg_section_dqt_t *quantization_table_chrominance_;
const uint8_t *jpeg_data_start;
const uint8_t *jpeg_data_end;
private:
static const jpg_section_t *find_jpg_section(const uint8_t **ptr, const uint8_t *end, jpg_section_t::jpg_section_flag flag);
};

View File

@@ -0,0 +1,107 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
// http://www.ietf.org/rfc/rfc2345.txt Each table is an array of 64 values given in zig-zag order, identical to the format used in a JFIF DQT marker segment.
constexpr size_t jpeg_quantization_table_length = 64;
typedef struct __attribute__((packed))
{
enum jpg_section_flag : uint8_t
{
DATA = 0x00,
SOF0 = 0xc0,
SOF1 = 0xc1,
SOF2 = 0xc2,
SOF3 = 0xc3,
DHT = 0xc4,
SOF5 = 0xc5,
SOF6 = 0xc6,
SOF7 = 0xc7,
JPG = 0xc8,
SOF9 = 0xc9,
SOF10 = 0xca,
SOF11 = 0xcb,
DAC = 0xcc,
SOF13 = 0xcd,
SOF14 = 0xce,
SOF15 = 0xcf,
RST0 = 0xd0,
RST1 = 0xd1,
RST2 = 0xd2,
RST3 = 0xd3,
RST4 = 0xd4,
RST5 = 0xd5,
RST6 = 0xd6,
RST7 = 0xd7,
SOI = 0xd8,
EOI = 0xd9,
SOS = 0xda,
DQT = 0xdb,
DNL = 0xdc,
DRI = 0xdd,
DHP = 0xde,
EXP = 0xdf,
APP0 = 0xe0,
APP1 = 0xe1,
APP2 = 0xe2,
APP3 = 0xe3,
APP4 = 0xe4,
APP5 = 0xe5,
APP6 = 0xe6,
APP7 = 0xe7,
APP8 = 0xe8,
APP9 = 0xe9,
APP10 = 0xea,
APP11 = 0xeb,
APP12 = 0xec,
APP13 = 0xed,
APP14 = 0xee,
APP15 = 0xef,
JPG0 = 0xf0,
JPG1 = 0xf1,
JPG2 = 0xf2,
JPG3 = 0xf3,
JPG4 = 0xf4,
JPG5 = 0xf5,
JPG6 = 0xf6,
JPG7 = 0xf7,
JPG8 = 0xf8,
JPG9 = 0xf9,
COM = 0xfe,
JPG10 = 0xfa,
JPG11 = 0xfb,
JPG12 = 0xfc,
JPG13 = 0xfd
};
const uint8_t framing; // 0xff
const jpg_section_flag flag;
const uint8_t length_msb;
const uint8_t length_lsb;
const uint8_t data[];
static bool is_valid_flag(const jpg_section_flag flag);
static const char *flag_name(const jpg_section_flag flag);
uint16_t data_length() const;
uint16_t section_length() const;
} jpg_section_t;
typedef struct __attribute__((packed)) // 0xffe0
{
char identifier[5] = {'J', 'F', 'I', 'F', 0}; // JFIF identifier, zero-terminated
uint8_t version_major = 1;
uint8_t version_minor = 1; // JFIF version 1.1
uint8_t density_units = 0; // no density units specified
uint16_t density_hor = 1;
uint16_t density_ver = 1; // density: 1 pixel "per pixel" horizontally and vertically
uint8_t thumbnail_hor = 0;
uint8_t thumbnail_ver = 0; // no thumbnail (size 0 x 0)
} jpg_section_app0_t;
typedef struct __attribute__((packed)) // 0xffdb
{
uint8_t id; // 0= quantLuminance, 1= quantChrominance
uint8_t data[jpeg_quantization_table_length];
} jpg_section_dqt_t;

View File

@@ -0,0 +1,14 @@
{
"name": "micro-jpg",
"version": "1.0.0",
"description": "JPEG library",
"keywords": "",
"repository": {
"type": "git",
"url": "https://github.com/rzeldent/"
},
"build": {
"srcDir": "src/",
"includeDir": "include/"
}
}

111
lib/micro-jpg/src/jpg.cpp Normal file
View File

@@ -0,0 +1,111 @@
#include <esp32-hal-log.h>
#include "jpg.h"
const jpg_section_t *jpg::find_jpg_section(const uint8_t **ptr, const uint8_t *end, jpg_section_t::jpg_section_flag flag)
{
log_d("find_jpeg_section 0x%02x (%s)", flag, jpg_section_t::flag_name(flag));
while (*ptr < end)
{
// flag, len MSB, len LSB
auto section = reinterpret_cast<const jpg_section_t *>((*ptr));
if (section->framing != 0xff)
{
log_e("Expected framing 0xff but found: 0x%02x", section->framing);
break;
}
if (!jpg_section_t::is_valid_flag(section->flag))
{
log_d("Unknown section 0x%02x", flag);
return nullptr;
}
// Advance pointer section has a length, so not SOI (0xd8) and EOI (0xd9)
*ptr += section->section_length();
if (section->flag == flag)
{
log_d("Found section 0x%02x (%s), %d bytes", flag, jpg_section_t::flag_name(section->flag), section->section_length());
return section;
}
log_d("Skipping section: 0x%02x (%s), %d bytes", section->flag, jpg_section_t::flag_name(section->flag), section->section_length());
}
// Not found
return nullptr;
}
// See https://create.stephan-brumme.com/toojpeg/
bool jpg::decode(const uint8_t *data, size_t size)
{
log_d("decode_jpeg");
// Look for start jpeg file (0xd8)
auto ptr = data;
auto end = ptr + size;
// Check for SOI (start of image) 0xff, 0xd8
if (!find_jpg_section(&ptr, end, jpg_section_t::jpg_section_flag::SOI))
{
log_e("No valid start of image marker found");
return false;
}
// First quantization table (Luminance - black & white images)
const jpg_section_t *quantization_table_section;
if (!(quantization_table_section = find_jpg_section(&ptr, end, jpg_section_t::jpg_section_flag::DQT)))
{
log_e("No quantization_table_luminance section found");
return false;
}
if (quantization_table_section->data_length() != sizeof(jpg_section_dqt_t))
{
log_w("Invalid length of quantization_table_luminance section. Expected %d but read %d", sizeof(jpg_section_dqt_t), quantization_table_section->data_length());
return false;
}
quantization_table_luminance_ = reinterpret_cast<const jpg_section_dqt_t *>(quantization_table_section->data);
// Second quantization table (Chrominance - color images)
if (!(quantization_table_section = find_jpg_section(&ptr, end, jpg_section_t::jpg_section_flag::DQT)))
{
log_w("No quantization_table_chrominance section found");
return false;
}
if (quantization_table_section->data_length() != sizeof(jpg_section_dqt_t))
{
log_w("Invalid length of quantization_table_chrominance section. Expected %d but read %d", sizeof(jpg_section_dqt_t), quantization_table_section->data_length());
return false;
}
quantization_table_chrominance_ = reinterpret_cast<const jpg_section_dqt_t *>(quantization_table_section->data);
// Start of scan
if (!find_jpg_section(&ptr, end, jpg_section_t::jpg_section_flag::SOS))
{
log_e("No start of scan section found");
return false;
}
// Start of the data sections
jpeg_data_start = ptr;
log_d("Skipping over data sections");
// Scan over all the sections. 0xff followed by not zero, is a new section
while (ptr < end - 1 && (ptr[0] != 0xff || ptr[1] == 0))
ptr++;
// Check if marker is an end of image marker
if (!find_jpg_section(&ptr, end, jpg_section_t::jpg_section_flag::EOI))
{
log_e("No end of image marker found");
return false;
}
jpeg_data_end = ptr;
log_d("Total jpeg data: %d bytes", jpeg_data_end - jpeg_data_start);
return true;
}

View File

@@ -0,0 +1,154 @@
#include "jpg_section.h"
uint16_t jpg_section_t::data_length() const
{
return (length_msb << 8) + length_lsb - sizeof(jpg_section_t::length_msb)- sizeof(jpg_section_t::length_lsb);
}
uint16_t jpg_section_t::section_length() const
{
return flag == SOI || flag == EOI ? sizeof(jpg_section_t::framing) + sizeof(jpg_section_t::flag) : sizeof(jpg_section_t::framing) + sizeof(jpg_section_t::flag) + (length_msb << 8) + length_lsb;
}
bool jpg_section_t::is_valid_flag(const jpg_section_flag flag)
{
return flag >= SOF0 && flag <= COM;
}
// from: https://www.disktuna.com/list-of-jpeg-markers/
const char *jpg_section_t::flag_name(const jpg_section_flag flag)
{
switch (flag)
{
case DATA:
return "DATA"; // DATA
case SOF0:
return "SOF0"; // Start of Frame 0 Baseline DCT
case SOF1:
return "SOF1"; // Start of Frame 1 Extended Sequential DCT
case SOF2:
return "SOF2"; // Start of Frame 2 Progressive DCT
case SOF3:
return "SOF3"; // Start of Frame 3 Lossless (sequential)
case DHT:
return "DHT"; // Define Huffman Table
case SOF5:
return "SOF5"; // Start of Frame 5 Differential sequential DCT
case SOF6:
return "SOF6"; // Start of Frame 6 Differential progressive DCT
case SOF7:
return "SOF7"; // Start of Frame 7 Differential lossless (sequential)
case JPG:
return "JPG"; // JPEG Extensions
case SOF9:
return "SOF9"; // Start of Frame 9 Extended sequential DCT, Arithmetic coding
case SOF10:
return "SOF10"; // Start of Frame 10 Progressive DCT, Arithmetic coding
case SOF11:
return "SOF11"; // Start of Frame 11 Lossless (sequential), Arithmetic coding
case DAC:
return "DAC"; // Define Arithmetic Coding
case SOF13:
return "SOF13"; // Start of Frame 13 Differential sequential DCT, Arithmetic coding
case SOF14:
return "SOF14"; // Start of Frame 14 Differential progressive DCT, Arithmetic coding
case SOF15:
return "SOF15"; // Start of Frame 15 Differential lossless (sequential), Arithmetic coding
case RST0:
return "RST0"; // Restart Marker 0
case RST1:
return "RST1"; // Restart Marker 1
case RST2:
return "RST2"; // Restart Marker 2
case RST3:
return "RST3"; // Restart Marker 3
case RST4:
return "RST4"; // Restart Marker 4
case RST5:
return "RST5"; // Restart Marker 5
case RST6:
return "RST6"; // Restart Marker 6
case RST7:
return "RST7"; // Restart Marker 7
case SOI:
return "SOI"; // Start of Image
case EOI:
return "EOI"; // End of Image
case SOS:
return "SOS"; // Start of Scan
case DQT:
return "DQT"; // Define Quantization Table
case DNL:
return "DNL"; // Define Number of Lines (Not common)
case DRI:
return "DRI"; // Define Restart Interval
case DHP:
return "DHP"; // Define Hierarchical Progression (Not common)
case EXP:
return "EXP"; // Expand Reference Component (Not common)
case APP0:
return "APP0"; // Application Segment 0 JFIF JFIF JPEG image, AVI1 Motion JPEG (MJPG)
case APP1:
return "APP1"; // Application Segment 1 EXIF Metadata, TIFF IFD format, JPEG Thumbnail (160×120) Adobe XMP
case APP2:
return "APP2"; // Application Segment 2 ICC color profile, FlashPix
case APP3:
return "APP3"; // Application Segment 3 (Not common) JPS Tag for Stereoscopic JPEG images
case APP4:
return "APP4"; // Application Segment 4 (Not common)
case APP5:
return "APP5"; // Application Segment 5 (Not common)
case APP6:
return "APP6"; // Application Segment 6 (Not common) NITF Lossles profile
case APP7:
return "APP7"; // Application Segment 7 (Not common)
case APP8:
return "APP8"; // Application Segment 8 (Not common)
case APP9:
return "APP9"; // Application Segment 9 (Not common)
case APP10:
return "APP10"; // Application Segment 10 PhoTags (Not common) ActiveObject (multimedia messages / captions)
case APP11:
return "APP11"; // Application Segment 11 (Not common) HELIOS JPEG Resources (OPI Postscript)
case APP12:
return "APP12"; // Application Segment 12 Picture Info (older digicams), Photoshop Save for Web: Ducky
case APP13:
return "APP13"; // Application Segment 13 Photoshop Save As: IRB, 8BIM, IPTC
case APP14:
return "APP14"; // Application Segment 14 (Not common)
case APP15:
return "APP15"; // Application Segment 15 (Not common)
case JPG0:
return "JPG0"; // JPEG Extension 0
case JPG1:
return "JPG1"; // JPEG Extension 1
case JPG2:
return "JPG2"; // JPEG Extension 2
case JPG3:
return "JPG3"; // JPEG Extension 3
case JPG4:
return "JPG4"; // JPEG Extension 4
case JPG5:
return "JPG5"; // JPEG Extension 5
case JPG6:
return "JPG6"; // JPEG Extension 6
case JPG7:
return "JPG7"; // SOF48 JPEG Extension 7 JPEG-LS Lossless JPEG
case JPG8:
return "JPG8"; // LSE JPEG Extension 8 JPEG-LS Extension Lossless JPEG Extension Parameters
case JPG9:
return "JPG9"; // JPEG Extension 9 (Not common)
case JPG10:
return "JPG10"; // JPEG Extension 10 (Not common)
case JPG11:
return "JPG11"; // JPEG Extension 11 (Not common)
case JPG12:
return "JPG12"; // JPEG Extension 12 (Not common)
case JPG13:
return "JPG13"; // JPEG Extension 13 (Not common)
case COM:
return "COM"; // Comment
}
return "Unknown";
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <micro_rtsp_source.h>
#include <esp_camera.h>
class micro_rtsp_camera : public micro_rtsp_source
{
public:
micro_rtsp_camera();
virtual ~micro_rtsp_camera();
esp_err_t initialize(camera_config_t *camera_config);
esp_err_t deinitialize();
virtual void update_frame();
virtual uint8_t *data() const { return fb_->buf; }
virtual size_t width() const { return fb_->width; }
virtual size_t height() const { return fb_->height; }
virtual size_t size() const { return fb_->len; }
private:
esp_err_t init_result_;
camera_fb_t *fb_;
};

View File

@@ -0,0 +1,56 @@
#pragma once
#include <map>
#include <string>
class micro_rtsp_requests
{
public:
std::string process_request(const std::string& request);
bool active() const { return stream_active_; }
private:
// enum rtsp_command
// {
// rtsp_command_unknown,
// rtsp_command_options, // OPTIONS
// rtsp_command_describe, // DESCRIBE
// rtsp_command_setup, // SETUP
// rtsp_command_play, // PLAY
// rtsp_command_teardown // TEARDOWN
// };
static const std::string available_stream_name_;
//rtsp_command parse_command(const std::string &request);
//static bool parse_cseq(const std::string &line, unsigned long &cseq);
bool parse_client_port(const std::string &request);
//bool parse_stream_url(const std::string &request);
//static std::string date_header();
static std::string handle_rtsp_error(unsigned long cseq, unsigned short code, const std::string &message);
static std::string handle_options(unsigned long cseq);
static std::string handle_describe(unsigned long cseq, const std::string &request);
std::string handle_setup(unsigned long cseq, const std::map<std::string, std::string> &request);
std::string handle_play(unsigned long cseq);
std::string handle_teardown(unsigned long cseq);
//unsigned long cseq_;
// std::string host_url_;
// unsigned short host_port_;
// std::string stream_name_;
bool tcp_transport_;
unsigned short start_client_port_;
unsigned short end_client_port_;
unsigned short rtp_streamer_port_;
unsigned short rtcp_streamer_port_;
unsigned long rtsp_session_id_;
bool stream_active_;
bool stream_stopped_;
};

View File

@@ -0,0 +1,45 @@
#pragma once
#include <Arduino.h>
#include <WiFiServer.h>
#include <string>
#include <list>
#include "micro_rtsp_camera.h"
#include "micro_rtsp_requests.h"
#include "micro_rtsp_streamer.h"
class micro_rtsp_server : WiFiServer
{
public:
micro_rtsp_server(micro_rtsp_source &source);
~micro_rtsp_server();
void begin(unsigned short port = 554);
void end();
unsigned get_frame_interval() const { return frame_interval_; }
unsigned set_frame_interval(unsigned value) { return frame_interval_ = value; }
void loop();
size_t clients() const { return clients_.size(); }
class rtsp_client : public WiFiClient, public micro_rtsp_requests
{
public:
rtsp_client(const WiFiClient &client);
~rtsp_client();
void handle_request();
};
private:
micro_rtsp_source &source_;
unsigned frame_interval_;
unsigned long next_frame_update_;
unsigned long next_check_client_;
micro_rtsp_streamer streamer_;
std::list<rtsp_client> clients_;
};

View File

@@ -0,0 +1,16 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
// Interface for a video source
class micro_rtsp_source
{
public:
virtual void update_frame() = 0;
virtual uint8_t *data() const = 0;
virtual size_t width() const = 0;
virtual size_t height() const = 0;
virtual size_t size() const = 0;
};

View File

@@ -0,0 +1,43 @@
#pragma once
#include <jpg_section.h>
#include <micro_rtsp_camera.h> // Add this line to include the definition of micro_rtsp_camera
#include <micro_rtsp_structs.h>
// https://en.wikipedia.org/wiki/Maximum_transmission_unit
constexpr size_t max_wifi_mtu = 2304;
// Payload JPG - https://www.ietf.org/rfc/rfc1890.txt
constexpr uint8_t RTP_PAYLOAD_JPG = 26;
// One of the types below will be returned, the jpeg_packet_with_quantization_t for the first packet, then the jpeg_packet_t
typedef struct __attribute__((packed))
{
rtp_over_tcp_hdr_t rtp_over_tcp_hdr;
rtp_hdr_t rtp_hdr;
jpeg_hdr_t jpeg_hdr;
jpeg_hdr_qtable_t jpeg_hdr_qtable;
uint8_t quantization_table_luminance[jpeg_quantization_table_length];
uint8_t quantization_table_chrominance[jpeg_quantization_table_length];
uint8_t jpeg_data[];
} jpeg_packet_with_quantization_t;
typedef struct __attribute__((packed))
{
rtp_over_tcp_hdr_t rtp_over_tcp_hdr;
rtp_hdr_t rtp_hdr;
jpeg_hdr_t jpeg_hdr;
uint8_t jpeg_data[];
} jpeg_packet_t;
class micro_rtsp_streamer
{
public:
micro_rtsp_streamer(const micro_rtsp_source& source);
rtp_over_tcp_hdr_t *create_jpg_packet(const uint8_t *jpg_scan, const uint8_t *jpg_scan_end, uint8_t **jpg_offset, const uint32_t timestamp, const uint8_t *quantization_table_luminance, const uint8_t *quantization_table_chrominance);
private:
const micro_rtsp_source& source_;
uint32_t ssrc_;
uint16_t sequence_number_;
};

View File

@@ -0,0 +1,51 @@
#pragma once
#include <stdint.h>
// https://www.ietf.org/rfc/rfc2326#section-10.12
typedef struct __attribute__((packed))
{
char magic = '$'; // Magic encapsulation ASCII dollar sign (24 hexadecimal)
uint8_t channel; // Channel identifier
uint16_t length; // Network order
} rtp_over_tcp_hdr_t;
// RTP data header - http://www.ietf.org/rfc/rfc3550.txt
typedef struct __attribute__((packed))
{
uint16_t version : 2; // protocol version
uint16_t padding : 1; // padding flag
uint16_t extension : 1; // header extension flag
uint16_t cc : 4; // CSRC count
uint16_t marker : 1; // marker bit
uint16_t pt : 7; // payload type
uint16_t seq : 16; // sequence number
uint32_t ts; // timestamp
uint32_t ssrc; // synchronization source
} rtp_hdr_t;
// https://datatracker.ietf.org/doc/html/rfc2435
typedef struct __attribute__((packed))
{
uint32_t tspec : 8; // type-specific field
uint32_t off : 24; // fragment byte offset
uint8_t type; // id of jpeg decoder params
uint8_t q; // Q values 0-127 indicate the quantization tables. JPEG types 0 and 1 (and their corresponding types 64 and 65)
uint8_t width; // frame width in 8 pixel blocks
uint8_t height; // frame height in 8 pixel blocks
} jpeg_hdr_t;
typedef struct __attribute__((packed))
{
uint16_t dri;
uint16_t f : 1;
uint16_t l : 1;
uint16_t count : 14;
} jpeg_hdr_rst_t;
typedef struct __attribute__((packed))
{
uint8_t mbz;
uint8_t precision;
uint16_t length;
} jpeg_hdr_qtable_t;

View File

@@ -0,0 +1,20 @@
{
"name": "micro-rtsp-streamer",
"version": "1.0.0",
"description": "RTSP Server",
"keywords": "",
"repository": {
"type": "git",
"url": "https://github.com/rzeldent/micro-rtsp-streamer"
},
"build": {
"srcDir": "src/",
"includeDir": "include/"
},
"dependencies": {
"micro-jpg": "^1.0.0",
"espressif/esp32-camera": "^2.0.4"
}
}

View File

@@ -0,0 +1,39 @@
#include <esp32-hal-log.h>
#include "micro_rtsp_camera.h"
micro_rtsp_camera::micro_rtsp_camera()
{
init_result_ == ESP_FAIL;
}
micro_rtsp_camera::~micro_rtsp_camera()
{
deinitialize();
}
esp_err_t micro_rtsp_camera::initialize(camera_config_t *camera_config)
{
log_v("camera_config={.pin_pwdn:%u,.pin_reset:%u,.pin_xclk:%u,.pin_sccb_sda:%u,.pin_sccb_scl:%u,.pin_d7:%u,.pin_d6:%u,.pin_d5:%u,.pin_d4:%u,.pin_d3:%u,.pin_d2:%u,.pin_d1:%u,.pin_d0:%u,.pin_vsync:%u,.pin_href:%u,.pin_pclk:%u,.xclk_freq_hz:%d,.ledc_timer:%u,ledc_channel:%u,.pixel_format:%d,.frame_size:%d,.jpeg_quality:%d,.fb_count:%d,.fb_location%d,.grab_mode:%d,sccb_i2c_port:%d}", camera_config->pin_pwdn, camera_config->pin_reset, camera_config->pin_xclk, camera_config->pin_sccb_sda, camera_config->pin_sccb_scl, camera_config->pin_d7, camera_config->pin_d6, camera_config->pin_d5, camera_config->pin_d4, camera_config->pin_d3, camera_config->pin_d2, camera_config->pin_d1, camera_config->pin_d0, camera_config->pin_vsync, camera_config->pin_href, camera_config->pin_pclk, camera_config->xclk_freq_hz, camera_config->ledc_timer, camera_config->ledc_channel, camera_config->pixel_format, camera_config->frame_size, camera_config->jpeg_quality, camera_config->fb_count, camera_config->fb_location, camera_config->grab_mode, camera_config->sccb_i2c_port);
init_result_ = esp_camera_init(camera_config);
if (init_result_ == ESP_OK)
update_frame();
else
log_e("Camera initialization failed: 0x%02x", init_result_);
return init_result_;
}
esp_err_t micro_rtsp_camera::deinitialize()
{
return init_result_ == ESP_OK ? esp_camera_deinit() : ESP_OK;
}
void micro_rtsp_camera::update_frame()
{
if (fb_)
esp_camera_fb_return(fb_);
fb_ = esp_camera_fb_get();
}

View File

@@ -0,0 +1,217 @@
#include <esp32-hal-log.h>
#include <iomanip>
#include <unordered_map>
#include <regex>
#include "micro_rtsp_requests.h"
// https://datatracker.ietf.org/doc/html/rfc2326
const std::string micro_rtsp_requests::available_stream_name_ = "/mjpeg/1";
bool micro_rtsp_requests::parse_client_port(const std::string &request)
{
log_v("request: %s", request.c_str());
std::regex regex("client_port=([0-9]+)", std::regex_constants::icase);
std::smatch match;
if (!std::regex_match(request, match, regex))
{
log_e("client_port not found");
return false;
}
start_client_port_ = std::stoi(match[1].str());
return true;
}
std::string micro_rtsp_requests::handle_rtsp_error(unsigned long cseq, unsigned short code, const std::string &message)
{
log_e("code: %d, message: %s", code, message.c_str());
auto now = time(nullptr);
std::ostringstream oss;
oss << "RTSP/1.0 " << code << " " << message << "\r\n"
<< "CSeq: " << cseq << "\r\n"
<< std::put_time(std::gmtime(&now), "Date: %a, %b %d %Y %H:%M:%S GMT") << "\r\n";
return oss.str();
}
// OPTIONS rtsp://192.168.178.247:554/mjpeg/1 RTSP/1.0
// CSeq: 2
// User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)
std::string micro_rtsp_requests::handle_options(unsigned long cseq)
{
auto now = time(nullptr);
std::ostringstream oss;
oss << "RTSP/1.0 200 OK\r\n"
<< "CSeq: " << cseq << "\r\n"
<< std::put_time(std::gmtime(&now), "Date: %a, %b %d %Y %H:%M:%S GMT") << "\r\n"
<< "Content-Length: 0\r\n"
<< "Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n"
<< "\r\n";
return oss.str();
}
// DESCRIBE rtsp://192.168.178.247:554/mjpeg/1 RTSP/1.0
// CSeq: 3
// User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)
// Accept: application/sdp
std::string micro_rtsp_requests::handle_describe(unsigned long cseq, const std::string &request)
{
// Parse the url
static const std::regex regex_url("rtsp:\\/\\/([^\\/:]+)(?::(\\d+))?(\\/.*)?\\s+RTSP\\/1\\.0", std::regex_constants::icase);
std::smatch match;
if (!std::regex_search(request, match, regex_url))
return handle_rtsp_error(cseq, 400, "Invalid URL");
auto host = match[1].str();
auto port = match[2].str().length() > 0 ? std::stoi(match[2].str()) : 554;
auto path = match[3].str();
log_i("host: %s, port: %d, path: %s", host.c_str(), port, path.c_str());
if (path != available_stream_name_)
return handle_rtsp_error(cseq, 404, "Stream Not Found");
std::ostringstream osbody;
osbody << "v=0\r\n"
<< "o=- " << std::rand() << " 1 IN IP4 " << host << "\r\n"
<< "s=\r\n"
<< "t=0 0\r\n" // start / stop - 0 -> unbounded and permanent session
<< "m=video 0 RTP/AVP 26\r\n" // currently we just handle UDP sessions
<< "c=IN IP4 0.0.0.0\r\n";
auto body = osbody.str();
auto now = time(nullptr);
std::ostringstream oss;
oss << "RTSP/1.0 200 OK\r\n"
<< "CSeq: " << cseq << "\r\n"
<< std::put_time(std::gmtime(&now), "Date: %a, %b %d %Y %H:%M:%S GMT") << "\r\n"
<< "Content-Base: rtsp://" << host << ":" << port << path << "/" << "\r\n"
<< "Content-Type: application/sdp\r\n"
<< "Content-Length: " << body.size() << "\r\n"
<< "\r\n"
<< body;
return oss.str();
}
// SETUP rtsp://192.168.178.247:554/mjpeg/1 RTSP/1.0
// CSeq: 0
// Transport: RTP/AVP;unicast;client_port=9058-9059
std::string micro_rtsp_requests::handle_setup(unsigned long cseq, const std::map<std::string, std::string> &lines)
{
log_v("request: %s", request.c_str());
auto it = lines.find("Transport");
if (it == lines.end())
return handle_rtsp_error(cseq, 400, "No Transport Header Found");
static const std::regex regex_transport("\\s+RTP\\/AVP(\\/TCP)?;unicast;client_port=(\\d+)-(\\d+)", std::regex_constants::icase);
std::smatch match;
if (!std::regex_search(it->second, match, regex_transport))
return handle_rtsp_error(cseq, 400, "Could Not Parse Transport");
tcp_transport_ = match[1].str().length() > 0;
start_client_port_ = std::stoi(match[2].str());
end_client_port_ = std::stoi(match[3].str());
log_i("tcp_transport: %d, start_client_port: %d, end_client_port: %d", tcp_transport_, start_client_port_, end_client_port_);
std::ostringstream ostransport;
if (tcp_transport_)
ostransport << "RTP/AVP/TCP;unicast;interleaved=0-1";
else
ostransport << "RTP/AVP;unicast;destination=127.0.0.1;source=127.0.0.1;client_port=" << start_client_port_ << "-" << end_client_port_ + 1 << ";server_port=" << rtp_streamer_port_ << "-" << rtp_streamer_port_/*rtcp_streamer_port_*/;
auto now = time(nullptr);
std::ostringstream oss;
oss << "RTSP/1.0 200 OK\r\n"
<< "CSeq: " << cseq << "\r\n"
<< std::put_time(std::gmtime(&now), "Date: %a, %b %d %Y %H:%M:%S GMT") << "\r\n"
<< "Transport: " << ostransport.str() << "\r\n"
<< "Session: " << rtsp_session_id_<< "\r\n";
return oss.str();
}
std::string micro_rtsp_requests::handle_play(unsigned long cseq)
{
log_v("request: %s", request.c_str());
stream_active_ = true;
auto now = time(nullptr);
std::ostringstream oss;
oss << "RTSP/1.0 200 OK\r\n"
<< "CSeq: " << cseq << "\r\n"
<< std::put_time(std::gmtime(&now), "Date: %a, %b %d %Y %H:%M:%S GMT") << "\r\n"
<< "Range: npt=0.000-\r\n"
<< "Session: " << rtsp_session_id_ << "\r\n"
<< "RTP-Info: url=rtsp://127.0.0.1:8554" << available_stream_name_ << "/track1" << "\r\n"
<< "\r\n";
return oss.str();
}
std::string micro_rtsp_requests::handle_teardown(unsigned long cseq)
{
log_v("request: %s", request.c_str());
stream_stopped_ = true;
auto now = time(nullptr);
std::ostringstream oss;
oss << "RTSP/1.0 200 OK\r\n"
<< "CSeq: " << cseq << "\r\n"
<< std::put_time(std::gmtime(&now), "Date: %a, %b %d %Y %H:%M:%S GMT") << "\r\n"
<< "\r\n";
return oss.str();
}
// Parse a request e.g.
// Request: OPTIONS rtsp://192.168.178.247:554/mjpeg/1 RTSP/1.0
// CSeq: 2
// User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)
std::string micro_rtsp_requests::process_request(const std::string &request)
{
log_v("request: %s", request.c_str());
std::stringstream ss(request);
// Get the request line
std::string request_line;
if (!std::getline(ss, request_line))
return handle_rtsp_error(0, 400, "No Request Found");
// Create a map with headers
std::string line;
std::map<std::string, std::string> headers;
std::size_t pos;
while (std::getline(ss, line))
{
if ((pos = line.find(':')) != std::string::npos)
headers[line.substr(0, pos)] = line.substr(pos + 1);
// else
// log_e("No : found for header: %s", line.c_str());
}
log_i("request_line: %s", request_line.c_str());
for (const auto &header : headers)
log_i("header: %s: %s", header.first.c_str(), header.second.c_str());
// Check for CSeq
const auto cseq_it = headers.find("CSeq");
if (cseq_it == headers.end())
return handle_rtsp_error(0, 400, "No Sequence Found");
auto cseq = std::stoul(cseq_it->second);
if (request_line.find("OPTIONS") == 0)
return handle_options(cseq);
if (request_line.find("DESCRIBE") == 0)
return handle_describe(cseq, request_line);
if (request_line.find("SETUP") == 0)
return handle_setup(cseq, headers);
if (request_line.find("PLAY") == 0)
return handle_play(cseq);
if (request_line.find("TEARDOWN") == 0)
return handle_teardown(cseq);
return handle_rtsp_error(cseq, 400, "Unknown Command or malformed request");
}

View File

@@ -0,0 +1,108 @@
#include <micro_rtsp_server.h>
#include <jpg.h>
#include <vector>
#include <memory>
// Check client connections every 100 milliseconds
#define CHECK_CLIENT_INTERVAL 10
micro_rtsp_server::micro_rtsp_server(micro_rtsp_source &source)
: source_(source), streamer_(source)
{
}
micro_rtsp_server::~micro_rtsp_server()
{
end();
}
void micro_rtsp_server::begin(unsigned short port /*= 554*/)
{
WiFiServer::begin(port);
}
void micro_rtsp_server::end()
{
WiFiServer::end();
}
void micro_rtsp_server::loop()
{
auto now = millis();
if (next_check_client_ < now)
{
log_v("Check for new client");
next_check_client_ = now + CHECK_CLIENT_INTERVAL;
// Check if a client wants to connect
auto client = accept();
if (client)
clients_.push_back(rtsp_client(client));
// Check for idle clients
clients_.remove_if([](rtsp_client &c)
{ return !c.connected(); });
for (auto client : clients_)
client.handle_request();
}
if (next_frame_update_ < now)
{
log_v("Stream frame t=%d", next_frame_update_);
next_frame_update_ = now + frame_interval_;
auto ts = time(nullptr);
// Get next jpg frame
source_.update_frame();
// Decode to get quantitation- and scan data
jpg jpg;
auto jpg_data = source_.data();
auto jpg_size = source_.size();
assert(jpg.decode(jpg_data, jpg_size));
auto jpg_scan_current = (uint8_t *)jpg.jpeg_data_start;
while (jpg_scan_current < jpg.jpeg_data_end)
{
auto packet = streamer_.create_jpg_packet(jpg.jpeg_data_start, jpg.jpeg_data_end, &jpg_scan_current, ts, jpg.quantization_table_luminance_->data, jpg.quantization_table_chrominance_->data);
for (auto client : clients_)
{
log_i("Stream frame to client: 0x%08x", client);
// RTP over TCP encapsulates in a $
client.write((const uint8_t *)packet, packet->length + sizeof(rtp_over_tcp_hdr_t));
// TODO: UDP
}
free(packet);
}
}
}
micro_rtsp_server::rtsp_client::rtsp_client(const WiFiClient &wifi_client)
: WiFiClient(wifi_client)
{
}
micro_rtsp_server::rtsp_client::~rtsp_client()
{
stop();
}
void micro_rtsp_server::rtsp_client::handle_request()
{
// Read if data available
auto bytes_available = available();
if (bytes_available > 0)
{
std::string request(bytes_available, '\0');
if (read((uint8_t *)&request[0], bytes_available) == bytes_available)
{
request.resize(bytes_available);
log_i("Request: %s", request.c_str());
auto response = process_request(request);
log_i("Response: %s", response.c_str());
println(response.c_str());
println();
}
}
}

View File

@@ -0,0 +1,82 @@
#include <stddef.h>
#include <memory.h>
#include <esp32-hal-log.h>
#include "micro_rtsp_streamer.h"
#include "esp_random.h"
micro_rtsp_streamer::micro_rtsp_streamer(const micro_rtsp_source &source)
: source_(source)
{
// Random number
ssrc_ = esp_random();
sequence_number_ = 0;
}
rtp_over_tcp_hdr_t *micro_rtsp_streamer::create_jpg_packet(const uint8_t *jpg_scan, const uint8_t *jpg_scan_end, uint8_t **jpg_offset, const uint32_t timestamp, const uint8_t *quantization_table_luminance, const uint8_t *quantization_table_chrominance)
{
log_v("jpg_scan:0x%08x, jpg_scan_end:0x%08x, jpg_offset:0x%08x, timestamp:%d, quantization_table_luminance:0x%08x, quantization_table_chrominance:0x%08x", jpg_scan, jpg_scan_end, jpg_offset, timestamp, quantization_table_luminance, quantization_table_chrominance);
// The MTU of wireless networks is 2,312 bytes. This size includes the packet headers.
const auto isFirstFragment = jpg_scan == *jpg_offset;
const auto include_quantization_tables = isFirstFragment && quantization_table_luminance != nullptr && quantization_table_chrominance != nullptr;
// Quantization tables musty be included in the first packet
const auto headers_size = include_quantization_tables ? sizeof(jpeg_packet_with_quantization_t) : sizeof(jpeg_packet_t);
const auto payload_size = max_wifi_mtu - headers_size;
const auto jpg_bytes_left = jpg_scan_end - *jpg_offset;
const bool isLastFragment = jpg_bytes_left <= payload_size;
const auto jpg_bytes = isLastFragment ? jpg_bytes_left : payload_size;
const uint16_t packet_size = headers_size + jpg_bytes;
const auto packet = static_cast<jpeg_packet_t *>(calloc(1, packet_size));
// 4 bytes RTP over TCP header
packet->rtp_over_tcp_hdr.channel = 0;
packet->rtp_over_tcp_hdr.length = packet_size;
log_v("rtp_over_tcp_hdr_t={.magic=%c,.channel=%u,.length=%u}", packet->rtp_over_tcp_hdr.magic, packet->rtp_over_tcp_hdr.channel, packet->rtp_over_tcp_hdr.length);
// 12 bytes RTP header
packet->rtp_hdr.version = 2;
packet->rtp_hdr.marker = isLastFragment;
packet->rtp_hdr.pt = RTP_PAYLOAD_JPG;
packet->rtp_hdr.seq = sequence_number_;
packet->rtp_hdr.ts = timestamp;
packet->rtp_hdr.ssrc = ssrc_;
log_v("rtp_hdr={.version:%u,.padding:%u,.extension:%u,.cc:%u,.marker:%u,.pt:%u,.seq:%u,.ts:%u,.ssrc:%u}", packet->rtp_hdr.version, packet->rtp_hdr.padding, packet->rtp_hdr.extension, packet->rtp_hdr.cc, packet->rtp_hdr.marker, packet->rtp_hdr.pt, packet->rtp_hdr.seq, packet->rtp_hdr.ts, packet->rtp_hdr.ssrc);
// 8 bytes JPEG payload header
packet->jpeg_hdr.tspec = 0; // type-specific field
packet->jpeg_hdr.off = (uint32_t)(*jpg_offset - jpg_scan); // fragment byte offset (24 bits in jpg)
packet->jpeg_hdr.type = 0; // id of jpeg decoder params
packet->jpeg_hdr.q = (uint8_t)(include_quantization_tables ? 0x80 : 0x5e); // quantization factor (or table id) 5eh=94d
packet->jpeg_hdr.width = (uint8_t)(source_.width() >> 3); // frame width in 8 pixel blocks
packet->jpeg_hdr.height = (uint8_t)(source_.height() >> 3); // frame height in 8 pixel blocks
log_v("jpeg_hdr={.tspec:%u,.off:0x%6x,.type:0x2%x,.q:%u,.width:%u.height:%u}", packet->jpeg_hdr.tspec, packet->jpeg_hdr.off, packet->jpeg_hdr.type, packet->jpeg_hdr.q, packet->jpeg_hdr.width, packet->jpeg_hdr.height);
// Only in first packet of the frame
if (include_quantization_tables)
{
auto packet_with_quantization = reinterpret_cast<jpeg_packet_with_quantization_t *>(packet);
packet_with_quantization->jpeg_hdr_qtable.mbz = 0;
packet_with_quantization->jpeg_hdr_qtable.precision = 0; // 8 bit precision
packet_with_quantization->jpeg_hdr_qtable.length = jpeg_quantization_table_length + jpeg_quantization_table_length;
log_v("jpeg_hdr_qtable={.mbz:%u,.precision:%u,.length:%u}", packet_with_quantization->jpeg_hdr_qtable.mbz, packet_with_quantization->jpeg_hdr_qtable.precision, packet_with_quantization->jpeg_hdr_qtable.length);
memcpy(packet_with_quantization->quantization_table_luminance, quantization_table_luminance, jpeg_quantization_table_length);
memcpy(packet_with_quantization->quantization_table_chrominance, quantization_table_chrominance, jpeg_quantization_table_length);
// Copy JPG data
memcpy(packet_with_quantization->jpeg_data, *jpg_offset, jpg_bytes);
}
else
{
// Copy JPG data
memcpy(packet->jpeg_data, *jpg_offset, jpg_bytes);
}
// Update JPG offset
*jpg_offset += jpg_bytes;
// Update sequence number
sequence_number_++;
return (rtp_over_tcp_hdr_t *)packet;
}

View File

@@ -10,7 +10,7 @@
###############################################################################
[platformio]
#default_envs = esp32cam_ai_thinker
default_envs = esp32cam_ai_thinker
#default_envs = esp32cam_espressif_esp_eye
#default_envs = esp32cam_espressif_esp32s2_cam_board
#default_envs = esp32cam_espressif_esp32s2_cam_header
@@ -31,23 +31,25 @@
[env]
platform = espressif32
framework = arduino
test_framework = unity
#upload_protocol = espota
#upload_port = 192.168.178.223
#upload_flags = --auth='ESP32CAM-RTSP'
# Partition scheme for OTA
board_build.partitions = min_spiffs.csv
#board_build.partitions = max_spiffs.csv
monitor_speed = 115200
monitor_rts = 0
monitor_dtr = 0
monitor_filters = log2file, time, default, esp32_exception_decoder
#monitor_filters = log2file, time, default, esp32_exception_decoder
monitor_filters = esp32_exception_decoder
build_flags =
-Ofast
-D 'BOARD_NAME="${this.board}"'
-D 'CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE'
-D 'CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_INFO'
-D 'IOTWEBCONF_PASSWORD_LEN=64'
board_build.embed_txtfiles =
@@ -56,7 +58,8 @@ board_build.embed_txtfiles =
lib_deps =
prampec/IotWebConf@^3.2.1
geeksville/Micro-RTSP@^0.1.6
rzeldent/micro-moustache@^1.0.1
rzeldent/micro-moustache
rzeldent/micro-timezonedb
[env:esp32cam_ai_thinker]
board = esp32cam_ai_thinker

View File

@@ -16,6 +16,9 @@
#include <moustache.h>
#include <settings.h>
#include <micro_rtsp_camera.h>
#include <micro_rtsp_server.h>
// HTML files
extern const char index_html_min_start[] asm("_binary_html_index_min_html_start");
@@ -47,11 +50,15 @@ auto param_dcw = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("dcw").labe
auto param_colorbar = iotwebconf::Builder<iotwebconf::CheckboxTParameter>("cb").label("Colorbar").defaultValue(DEFAULT_COLORBAR).build();
// Camera
OV2640 cam;
// OV2640 cam;
// DNS Server
DNSServer dnsServer;
// RTSP Server
std::unique_ptr<rtsp_server> camera_server;
// std::unique_ptr<rtsp_server> camera_server;
micro_rtsp_camera camera;
micro_rtsp_server server(camera);
// Web server
WebServer web_server(80);
@@ -100,7 +107,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()) : "RTSP server disabled"},
{"NumRTSPSessions", String(server.clients())},
// Network
{"HostName", hostname},
{"MacAddress", WiFi.macAddress()},
@@ -162,10 +169,10 @@ void handle_snapshot()
// Remove old images stored in the frame buffer
auto frame_buffers = CAMERA_CONFIG_FB_COUNT;
while (frame_buffers--)
cam.run();
camera.update_frame();
auto fb_len = cam.getSize();
auto fb = (const char *)cam.getfb();
auto fb_len = camera.size();
auto fb = camera.data();
if (fb == nullptr)
{
web_server.send(404, "text/plain", "Unable to obtain frame buffer from the camera");
@@ -175,7 +182,7 @@ void handle_snapshot()
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);
web_server.sendContent((const char *)fb, fb_len);
}
#define STREAM_CONTENT_BOUNDARY "123456789000000000000987654321"
@@ -197,11 +204,11 @@ void handle_stream()
while (client.connected())
{
client.write("\r\n--" STREAM_CONTENT_BOUNDARY "\r\n");
cam.run();
camera.update_frame();
client.write("Content-Type: image/jpeg\r\nContent-Length: ");
sprintf(size_buf, "%d\r\n\r\n", cam.getSize());
sprintf(size_buf, "%d\r\n\r\n", camera.size());
client.write(size_buf);
client.write(cam.getfb(), cam.getSize());
client.write(camera.data(), camera.size());
}
log_v("client disconnected");
@@ -218,7 +225,11 @@ esp_err_t initialize_camera()
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());
const camera_config_t camera_config = {
// Set frame duration
server.set_frame_interval(param_frame_duration.value());
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
@@ -247,10 +258,12 @@ esp_err_t initialize_camera()
#if CONFIG_CAMERA_CONVERTER_ENABLED
conv_mode = CONV_DISABLE, // RGB<->YUV Conversion mode
#endif
.sccb_i2c_port = SCCB_I2C_PORT // If pin_sccb_sda is -1, use the already configured I2C bus by number
.sccb_i2c_port = CAMERA_CONFIG_SCCB_I2C_PORT // If pin_sccb_sda is -1, use the already configured I2C bus by number
};
return cam.init(camera_config);
return camera.initialize(&camera_config);
// return cam.init(camera_config);
}
void update_camera_settings()
@@ -289,7 +302,8 @@ void update_camera_settings()
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));
server.begin(RTSP_PORT);
// Add RTSP service to mDNS
// HTTP is already set by iotWebConf
MDNS.addService("rtsp", "tcp", RTSP_PORT);
@@ -374,21 +388,24 @@ void setup()
#endif
iotWebConf.init();
// Set the time servers
configTime(0, 0, NTP_SERVERS);
// Try to initialize 3 times
for (auto i = 0; i < 3; i++)
{
log_i("Initializing camera...");
camera_init_result = initialize_camera();
if (camera_init_result == ESP_OK)
{
update_camera_settings();
break;
}
esp_camera_deinit();
log_e("Failed to initialize camera. Error: 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());
log_e("Failed to initialize camera. Error: 0x%04x. 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());
delay(500);
}
update_camera_settings();
// Set up required URL handlers on the web server
web_server.on("/", HTTP_GET, handle_root);
web_server.on("/config", []
@@ -406,6 +423,5 @@ void loop()
{
iotWebConf.doLoop();
if (camera_server)
camera_server->doLoop();
server.loop();
}

127
test/test_main.cpp Normal file

File diff suppressed because one or more lines are too long