diff --git a/boards/esp32cam_ai_thinker.json b/boards/esp32cam_ai_thinker.json index 7db7f1d..ed12cb8 100644 --- a/boards/esp32cam_ai_thinker.json +++ b/boards/esp32cam_ai_thinker.json @@ -31,7 +31,8 @@ "'-D CAMERA_CONFIG_LEDC_TIMER=LEDC_TIMER_0'", "'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'", "'-D CAMERA_CONFIG_FB_COUNT=2'", - "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'" + "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'", + "'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'" ], "f_cpu": "240000000L", "f_flash": "40000000L", diff --git a/boards/esp32cam_espressif_esp32s2_cam_board.json b/boards/esp32cam_espressif_esp32s2_cam_board.json index 725fa48..dd8b292 100644 --- a/boards/esp32cam_espressif_esp32s2_cam_board.json +++ b/boards/esp32cam_espressif_esp32s2_cam_board.json @@ -1,6 +1,6 @@ { "build": { - "arduino":{ + "arduino": { "ldscript": "esp32s2_out.ld" }, "core": "esp32", @@ -31,7 +31,9 @@ "'-D CAMERA_CONFIG_LEDC_TIMER=LEDC_TIMER_0'", "'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'", "'-D CAMERA_CONFIG_FB_COUNT=2'", - "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'" ], + "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'", + "'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'" + ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", @@ -58,4 +60,4 @@ }, "url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/hw-reference/esp32s2/user-guide-esp-lyrap-cam-v1.1.html", "vendor": "Espressif" -} +} \ No newline at end of file diff --git a/boards/esp32cam_espressif_esp32s2_cam_header.json b/boards/esp32cam_espressif_esp32s2_cam_header.json index 347264f..2c0b347 100644 --- a/boards/esp32cam_espressif_esp32s2_cam_header.json +++ b/boards/esp32cam_espressif_esp32s2_cam_header.json @@ -1,6 +1,6 @@ { "build": { - "arduino":{ + "arduino": { "ldscript": "esp32s2_out.ld" }, "core": "esp32", @@ -31,7 +31,9 @@ "'-D CAMERA_CONFIG_LEDC_TIMER=LEDC_TIMER_0'", "'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'", "'-D CAMERA_CONFIG_FB_COUNT=2'", - "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'" ], + "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'", + "'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'" + ], "f_cpu": "240000000L", "f_flash": "80000000L", "flash_mode": "qio", @@ -58,4 +60,4 @@ }, "url": "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/hw-reference/esp32s2/user-guide-esp-lyrap-cam-v1.1.html", "vendor": "Espressif" -} +} \ No newline at end of file diff --git a/boards/esp32cam_espressif_esp32s3_cam_lcd.json b/boards/esp32cam_espressif_esp32s3_cam_lcd.json index a847749..643053f 100644 --- a/boards/esp32cam_espressif_esp32s3_cam_lcd.json +++ b/boards/esp32cam_espressif_esp32s3_cam_lcd.json @@ -33,7 +33,8 @@ "'-D CAMERA_CONFIG_LEDC_TIMER=LEDC_TIMER_0'", "'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'", "'-D CAMERA_CONFIG_FB_COUNT=2'", - "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'" + "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'", + "'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'" ], "f_cpu": "240000000L", "f_flash": "80000000L", diff --git a/boards/esp32cam_espressif_esp32s3_eye.json b/boards/esp32cam_espressif_esp32s3_eye.json index b67514d..a0a886c 100644 --- a/boards/esp32cam_espressif_esp32s3_eye.json +++ b/boards/esp32cam_espressif_esp32s3_eye.json @@ -33,7 +33,8 @@ "'-D CAMERA_CONFIG_LEDC_TIMER=LEDC_TIMER_0'", "'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'", "'-D CAMERA_CONFIG_FB_COUNT=2'", - "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'" + "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'", + "'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'" ], "f_cpu": "240000000L", "f_flash": "80000000L", diff --git a/boards/esp32cam_espressif_esp_eye.json b/boards/esp32cam_espressif_esp_eye.json index c22f0e1..1c3b0e5 100644 --- a/boards/esp32cam_espressif_esp_eye.json +++ b/boards/esp32cam_espressif_esp_eye.json @@ -1,6 +1,6 @@ { "build": { - "arduino":{ + "arduino": { "ldscript": "esp32_out.ld", "partitions": "huge_app.csv" }, @@ -31,7 +31,8 @@ "'-D CAMERA_CONFIG_LEDC_TIMER=LEDC_TIMER_0'", "'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'", "'-D CAMERA_CONFIG_FB_COUNT=1'", - "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'" + "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'", + "'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'" ], "f_cpu": "240000000L", "f_flash": "40000000L", @@ -62,4 +63,4 @@ }, "url": "https://www.espressif.com/en/products/devkits/esp-eye/overview", "vendor": "Espressif" -} +} \ No newline at end of file diff --git a/boards/esp32cam_freenove_wrover_kit.json b/boards/esp32cam_freenove_wrover_kit.json index 14099ff..8104a4a 100644 --- a/boards/esp32cam_freenove_wrover_kit.json +++ b/boards/esp32cam_freenove_wrover_kit.json @@ -31,7 +31,8 @@ "'-D CAMERA_CONFIG_LEDC_TIMER=LEDC_TIMER_0'", "'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'", "'-D CAMERA_CONFIG_FB_COUNT=2'", - "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'" + "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'", + "'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'" ], "f_cpu": "240000000L", "f_flash": "40000000L", diff --git a/boards/esp32cam_m5stack_camera.json b/boards/esp32cam_m5stack_camera.json index cc62530..11c18fc 100644 --- a/boards/esp32cam_m5stack_camera.json +++ b/boards/esp32cam_m5stack_camera.json @@ -1,6 +1,6 @@ { "build": { - "arduino":{ + "arduino": { "ldscript": "esp32_out.ld", "partitions": "huge_app.csv" }, @@ -28,6 +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 CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'", "'-D GROVE_SDA=13'", "'-D GROVE_SCL=4'" ], @@ -60,4 +61,4 @@ }, "url": "https://docs.m5stack.com/en/unit/m5camera", "vendor": "M5STACK" -} +} \ No newline at end of file diff --git a/boards/esp32cam_m5stack_camera_psram.json b/boards/esp32cam_m5stack_camera_psram.json index 00f47de..f1a9b86 100644 --- a/boards/esp32cam_m5stack_camera_psram.json +++ b/boards/esp32cam_m5stack_camera_psram.json @@ -30,6 +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 CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'", "'-D GROVE_SDA=13'", "'-D GROVE_SCL=4'" ], diff --git a/boards/esp32cam_m5stack_esp32cam.json b/boards/esp32cam_m5stack_esp32cam.json index 8d726dc..1f6f23d 100644 --- a/boards/esp32cam_m5stack_esp32cam.json +++ b/boards/esp32cam_m5stack_esp32cam.json @@ -1,6 +1,6 @@ { "build": { - "arduino":{ + "arduino": { "ldscript": "esp32_out.ld", "partitions": "huge_app.csv" }, @@ -32,6 +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 CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'", "'-D MICROPHONE_GPIO=32'", "'-D GROVE_SDA=13'", "'-D GROVE_SCL=4'" @@ -65,4 +66,4 @@ }, "url": "https://docs.m5stack.com/en/unit/esp32cam", "vendor": "M5STACK" -} +} \ No newline at end of file diff --git a/boards/esp32cam_m5stack_unitcam.json b/boards/esp32cam_m5stack_unitcam.json index b11599e..df7615b 100644 --- a/boards/esp32cam_m5stack_unitcam.json +++ b/boards/esp32cam_m5stack_unitcam.json @@ -1,6 +1,6 @@ { "build": { - "arduino":{ + "arduino": { "ldscript": "esp32_out.ld", "partitions": "huge_app.csv" }, @@ -29,7 +29,8 @@ "'-D CAMERA_CONFIG_LEDC_TIMER=LEDC_TIMER_0'", "'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'", "'-D CAMERA_CONFIG_FB_COUNT=1'", - "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_DRAM'" + "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_DRAM'", + "'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'" ], "f_cpu": "240000000L", "f_flash": "40000000L", @@ -60,4 +61,4 @@ }, "url": "https://docs.m5stack.com/en/unit/unit_cam", "vendor": "M5STACK" -} +} \ No newline at end of file diff --git a/boards/esp32cam_m5stack_unitcams3.json b/boards/esp32cam_m5stack_unitcams3.json index 7926893..c283858 100644 --- a/boards/esp32cam_m5stack_unitcams3.json +++ b/boards/esp32cam_m5stack_unitcams3.json @@ -36,6 +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 CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'", "'-D I2C_MEMS_SDA=48'", "'-D I2C_MEMS_SCL=47'", "'-D TF_CS=9'", @@ -80,4 +81,4 @@ }, "url": "https://docs.m5stack.com/en/unit/Unit-CamS3", "vendor": "M5STACK" -} +} \ No newline at end of file diff --git a/boards/esp32cam_m5stack_wide.json b/boards/esp32cam_m5stack_wide.json index df425dd..ef4c688 100644 --- a/boards/esp32cam_m5stack_wide.json +++ b/boards/esp32cam_m5stack_wide.json @@ -1,6 +1,6 @@ { "build": { - "arduino":{ + "arduino": { "ldscript": "esp32_out.ld", "partitions": "huge_app.csv" }, @@ -29,7 +29,8 @@ "'-D CAMERA_CONFIG_LEDC_TIMER=LEDC_TIMER_0'", "'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'", "'-D CAMERA_CONFIG_FB_COUNT=2'", - "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'" + "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_PSRAM'", + "'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'" ], "f_cpu": "240000000L", "f_flash": "40000000L", @@ -60,4 +61,4 @@ }, "url": "https://shop.m5stack.com/collections/m5-cameras", "vendor": "M5STACK" -} +} \ No newline at end of file diff --git a/boards/esp32cam_seeed_xiao_esp32s3_sense.json b/boards/esp32cam_seeed_xiao_esp32s3_sense.json index fa36d88..30ef0bf 100644 --- a/boards/esp32cam_seeed_xiao_esp32s3_sense.json +++ b/boards/esp32cam_seeed_xiao_esp32s3_sense.json @@ -36,6 +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 CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'", "'-D I2C_MEMS_SDA=41'", "'-D I2C_MEMS_SCL=42'", "'-D TF_CS=21'", @@ -80,4 +81,4 @@ }, "url": "https://www.seeedstudio.com/XIAO-ESP32S3-p-5627.html", "vendor": "Seeed Studio" -} +} \ No newline at end of file diff --git a/boards/esp32cam_ttgo_t_camera.json b/boards/esp32cam_ttgo_t_camera.json index 1f693f3..f903d0d 100644 --- a/boards/esp32cam_ttgo_t_camera.json +++ b/boards/esp32cam_ttgo_t_camera.json @@ -28,6 +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 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'", diff --git a/boards/esp32cam_ttgo_t_journal.json b/boards/esp32cam_ttgo_t_journal.json index 885d5c0..5ec8fd7 100644 --- a/boards/esp32cam_ttgo_t_journal.json +++ b/boards/esp32cam_ttgo_t_journal.json @@ -27,7 +27,8 @@ "'-D CAMERA_CONFIG_LEDC_TIMER=LEDC_TIMER_0'", "'-D CAMERA_CONFIG_LEDC_CHANNEL=LEDC_CHANNEL_0'", "'-D CAMERA_CONFIG_FB_COUNT=1'", - "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_DRAM'" + "'-D CAMERA_CONFIG_FB_LOCATION=CAMERA_FB_IN_DRAM'", + "'-D CAMERA_CONFIG_SCCB_I2C_PORT=I2C_NUM_0'" ], "f_cpu": "240000000L", "f_flash": "40000000L", diff --git a/lib/micro-rtsp-server/include/micro_rtsp_server.h b/lib/micro-rtsp-server/include/micro_rtsp_server.h new file mode 100644 index 0000000..a9492da --- /dev/null +++ b/lib/micro-rtsp-server/include/micro_rtsp_server.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include + +class micro_rtsp_server : public WiFiServer +{ +public: + micro_rtsp_server(micro_rtsp_source *source, unsigned frame_interval = 100, unsigned short port = 554); + ~micro_rtsp_server(); + + void loop(); + + unsigned get_frame_interval() { return frame_interval_; } + unsigned set_frame_interval(unsigned value) { return frame_interval_ = value; } + + size_t clients() { return clients_.size(); } + + class rtsp_client + { + public: + rtsp_client(const WiFiClient &wifi_client); + void handle_request(); + + private: + enum rtsp_command + { + rtsp_command_unknown, + rtsp_command_option, // OPTIONS + rtsp_command_describe, // DESCRIBE + rtsp_command_setup, // SETUP + rtsp_command_play, // PLAY + rtsp_command_teardown // TEARDOWN + }; + + rtsp_command parse_command(const String& request); + unsigned long parse_cseq(const String& request); + bool parse_stream_url(const String& request); + + int parse_client_port(const String& request); + + + WiFiClient wifi_client_; + + bool tcp_transport_; + + String host_url_; + unsigned short host_port_; + String stream_name_; + uint stream_id_; + + unsigned short rtp_port_; + // enum rtsp_state state_; + + String date_header(); + + void handle_option(unsigned long cseq); + void handle_describe(unsigned long cseq, const String& stream_url); + void handle_setup(unsigned long cseq, const String& stream_url); + void handle_play(); + void handle_teardown(); + }; + +private: + micro_rtsp_source *source_; + + unsigned frame_interval_; + unsigned long next_frame_update_; + + unsigned long next_check_client_; + std::list clients_; +}; \ No newline at end of file diff --git a/lib/micro-rtsp-server/include/micro_rtsp_source.h b/lib/micro-rtsp-server/include/micro_rtsp_source.h new file mode 100644 index 0000000..699285c --- /dev/null +++ b/lib/micro-rtsp-server/include/micro_rtsp_source.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class micro_rtsp_source +{ +public: + virtual void update_frame(); + + virtual uint8_t *data() const = 0; + virtual size_t width() const = 0; + virtual size_t height() const = 0; + virtual size_t size() const = 0; +}; diff --git a/lib/micro-rtsp-server/include/micro_rtsp_source_camera.h b/lib/micro-rtsp-server/include/micro_rtsp_source_camera.h new file mode 100644 index 0000000..72b2d6f --- /dev/null +++ b/lib/micro-rtsp-server/include/micro_rtsp_source_camera.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +class micro_rtsp_source_camera : public micro_rtsp_source +{ +public: + micro_rtsp_source_camera(); + virtual ~micro_rtsp_source_camera(); + + esp_err_t initialize(camera_config_t *camera_config); + esp_err_t deinitialize(); + // sensor_t* esp_camera_sensor_get(); + + void update_frame(); + + uint8_t *data() const { return fb->buf; } + size_t width() const { return fb->width; } + size_t height() const { return fb->height; } + size_t size() const { return fb->len; } + +private: + esp_err_t init_result; + camera_fb_t *fb; +}; \ No newline at end of file diff --git a/lib/micro-rtsp-server/library.json b/lib/micro-rtsp-server/library.json new file mode 100644 index 0000000..2ec1def --- /dev/null +++ b/lib/micro-rtsp-server/library.json @@ -0,0 +1,19 @@ +{ + "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": { + "espressif/esp32-camera": "^2.0.4" + } +} \ No newline at end of file diff --git a/lib/micro-rtsp-server/rtsp_server.cpp b/lib/micro-rtsp-server/rtsp_server.cpp new file mode 100644 index 0000000..4308299 --- /dev/null +++ b/lib/micro-rtsp-server/rtsp_server.cpp @@ -0,0 +1,53 @@ +#include "rtsp_server.h" +#include + + +// URI: e.g. rtsp://192.168.178.27:554/mjpeg/1 +rtsp_server::rtsp_server(OV2640 &cam, unsigned long interval, int port /*= 554*/) + : WiFiServer(port), cam_(cam) +{ + log_i("Starting RTSP server"); + _streamer = new OV2640Streamer() + WiFiServer::begin(); + timer_.every(interval, client_handler, this); +} + +size_t rtsp_server::num_connected() +{ + return clients_.size(); +} + +void rtsp_server::doLoop() +{ + timer_.tick(); +} + +rtsp_server::rtsp_client::rtsp_client(const WiFiClient &client, OV2640 &cam) +{ + wifi_client = client; + streamer = std::shared_ptr(new OV2640Streamer(&wifi_client, cam)); + session = std::shared_ptr(new CRtspSession(&wifi_client, streamer.get())); +} + +bool rtsp_server::client_handler(void *arg) +{ + auto self = static_cast(arg); + // Check if a client wants to connect + auto new_client = self->accept(); + if (new_client) + self->clients_.push_back(std::unique_ptr(new rtsp_client(new_client, self->cam_))); + + auto now = millis(); + for (const auto &client : self->clients_) + { + // Handle requests + client->session->handleRequests(0); + // Send the frame. For now return the uptime as time marker, currMs + client->session->broadcastCurrentFrame(now); + } + + self->clients_.remove_if([](std::unique_ptr const &c) + { return c->session->m_stopped; }); + + return true; +} \ No newline at end of file diff --git a/lib/micro-rtsp-server/rtsp_server.h b/lib/micro-rtsp-server/rtsp_server.h new file mode 100644 index 0000000..e287b3a --- /dev/null +++ b/lib/micro-rtsp-server/rtsp_server.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class rtsp_server : public WiFiServer +{ +public: + rtsp_server(OV2640 &cam, unsigned long interval, int port = 554); + + void doLoop(); + + size_t num_connected(); + +private: + struct rtsp_client + { + public: + rtsp_client(const WiFiClient &client, OV2640 &cam); + + WiFiClient wifi_client; + // RTSP session and state + std::shared_ptr session; + }; + + OV2640 cam_; + OV2640Streamer _streamer; + + std::list> clients_; + uintptr_t task_; + Timer<> timer_; + + static bool client_handler(void *); +}; \ No newline at end of file diff --git a/lib/micro-rtsp-server/src/micro_rtsp_server.cpp b/lib/micro-rtsp-server/src/micro_rtsp_server.cpp new file mode 100644 index 0000000..9a46c0c --- /dev/null +++ b/lib/micro-rtsp-server/src/micro_rtsp_server.cpp @@ -0,0 +1,269 @@ +#include +#include +#include + +// Check client connections every 100 milliseconds +#define CHECK_CLIENT_INTERVAL 100 + +micro_rtsp_server::micro_rtsp_server(micro_rtsp_source *source, unsigned frame_interval /*= 100*/, unsigned short port /*= 554*/) +{ + log_i("starting RTSP server"); + + frame_interval_ = frame_interval; + source_ = source; + WiFiServer::begin(); +} + +micro_rtsp_server::~micro_rtsp_server() +{ +} + +void micro_rtsp_server::loop() +{ + auto now = millis(); + + if (next_check_client_ < now) + { + next_check_client_ = now + CHECK_CLIENT_INTERVAL; + + // Check if a client wants to connect + auto client = accept(); + if (client) + clients_.push_back(new rtsp_client(client)); + + // Check for idle clients + // clients_.remove_if([](std::unique_ptr const &c) + // { return c->session->m_stopped; }); + } + + if (next_frame_update_ < now) + { + next_frame_update_ = now + frame_interval_; + + for (const auto &client : clients_) + { + // Handle requests + client->handle_request(); + // Send the frame. For now return the uptime as time marker, currMs + // client->session->broadcastCurrentFrame(now); + } + } +} + +micro_rtsp_server::rtsp_client::rtsp_client(const WiFiClient &wifi_client) +{ + wifi_client_ = wifi_client; + // state_ = rtsp_state_unknown; +} + +micro_rtsp_server::rtsp_client::rtsp_command micro_rtsp_server::rtsp_client::parse_command(const String &request) +{ + log_i("parse_command"); + // Check for RTSP commands: Option, Describe, Setup, Play, Teardown + if (request.startsWith("option")) + return rtsp_command::rtsp_command_option; + + if (request.startsWith("describe")) + return rtsp_command::rtsp_command_describe; + + if (request.startsWith("setup")) + return rtsp_command::rtsp_command_setup; + + if (request.startsWith("play")) + return rtsp_command::rtsp_command_play; + + if (request.startsWith("teardown")) + return rtsp_command::rtsp_command_teardown; + + return rtsp_command::rtsp_command_unknown; +} + +int micro_rtsp_server::rtsp_client::parse_client_port(const String &request) +{ + log_i("parse_client_port"); + auto pos = request.indexOf("client_port="); + if (pos < 0) + { + log_e("client_port not found"); + return 0; + } + + return strtoul(&request.c_str()[pos + 12], nullptr, 10); +} + +unsigned long micro_rtsp_server::rtsp_client::parse_cseq(const String &request) +{ + log_i("parse_cseq"); + auto pos = request.indexOf("cseq: "); + if (pos < 0) + { + log_e("CSeq not found"); + return 0; + } + + return strtoul(&request.c_str()[pos + 6], nullptr, 10); +} + +bool micro_rtsp_server::rtsp_client::parse_stream_url(const String &request) +{ + log_i("parse_host_url"); + // SETUP rtsp://192.168.10.93:1234/mjpeg/1 RTSP/1.0 + auto pos = request.indexOf("rtsp://"); + if (pos < 0) + { + log_e("rtsp:// not found"); + return false; + } + + // Look for : + auto start = pos + 7; + pos = request.indexOf(':', start); + if (pos < 0) + { + log_e("parse stream: no host url/post found (: missing)"); + return false; + } + + // Should be 192.168.10.93 + host_url_ = request.substring(start, pos); + // Should be 1234 + host_port_ = (unsigned short)(&request.c_str()[pos + 1], nullptr, 10); + + start = pos + 1; + pos = request.indexOf('/', start); + if (pos < 0) + { + log_e("parse stream: no host port found (/ missing)"); + return false; + } + + start = pos + 1; + pos = request.indexOf(' ', start); + if (pos < 0) + { + log_e("parse stream: no stream found (' ' missing)"); + return false; + } + + // should be mjpeg/1 + stream_name_ = request.substring(start, pos); + return true; +} + +void micro_rtsp_server::rtsp_client::handle_request() +{ + log_i("handle_request"); + // Read if data available + auto available = wifi_client_.available(); + if (available > 0) + { + auto request = wifi_client_.readString(); + // Make response lowercase + request.toLowerCase(); + + auto command = parse_command(request); + if (command == rtsp_command_unknown) + { + log_w("Invalid RTSP command received: %s", request.c_str()); + return; + } + + unsigned long cseq = parse_cseq(request); + if (cseq == 0) + { + log_w("Invalid sequence: %s", request.c_str()); + return; + } + + switch (command) + { + case rtsp_command_option: + handle_option(cseq); + break; + case rtsp_command_describe: + handle_describe(cseq, request); + break; + case rtsp_command_setup: + handle_setup(cseq, request); + break; + case rtsp_command_play: + handle_play(); + break; + case rtsp_command_teardown: + handle_teardown(); + break; + default: + log_e("unknown rtsp_command"); + assert(false); + } + } +} + +String micro_rtsp_server::rtsp_client::date_header() +{ + char buffer[200]; + auto now = time(nullptr); + strftime(buffer, sizeof(buffer), "Date: %a, %b %d %Y %H:%M:%S GMT", gmtime(&now)); + return buffer; +} + +void micro_rtsp_server::rtsp_client::handle_option(unsigned long cseq) +{ + log_i("handle_option"); + auto response = String("RTSP/1.0 200 OK\r\n") + + String("CSeq: ") + String(cseq) + String("\r\n") + + String("Content-Length: 0\r\n") + + String("Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n") + + String("\r\n"); + wifi_client_.write(response.c_str()); +} + +void micro_rtsp_server::rtsp_client::handle_describe(unsigned long cseq, const String &request) +{ + log_i("handle_describe"); + if (!parse_stream_url(request)) + { + log_w("Unable to parse stream url", request.c_str()); + auto response = String("RTSP/1.0 404 Stream Not Found\r\n") + + String("CSeq: ") + String(cseq) + String("\r\n") + + date_header() + String("\r\n"); + wifi_client_.write(response.c_str()); + return; + } + + if (stream_name_ != "mjpeg/1") + { + log_w("stream %s was requested but is not available", stream_name_.c_str()); + auto response = String("RTSP/1.0 404 Stream Not Found\r\n") + + String("CSeq: ") + String(cseq) + String("\r\n") + + date_header() + + String("\r\n"); + wifi_client_.write(response.c_str()); + return; + } + + auto sdp = String("v=0\r\n") + + String("o=- ") + String(host_port_) + String(" 1 IN IP4 ") + String(rand()) + String("\r\n") + + String("s=\r\n") + + String("t=0 0\r\n") + // start / stop - 0 -> unbounded and permanent session + String("m=video 0 RTP/AVP 26\r\n") + // currently we just handle UDP sessions + String("c=IN IP4 0.0.0.0\r\n"); + + auto response = + String("RTSP/1.0 200 OK\r\n") + + String("CSeq: ") + String(cseq) + String("\r\n") + + date_header() + String("\r\n") + + String("Content-Base: ") + stream_name_ + ("/\r\n") + + String("Content-Type: application/sdp\r\n") + + String("Content-Length: ") + String(sdp.length()) + String("\r\n") + + String("\r\n") + + sdp; + + wifi_client_.write(response.c_str()); +} + +void micro_rtsp_server::rtsp_client::handle_setup(unsigned long cseq, const String &request) +{ + tcp_transport_ = request.indexOf("rtp/avp/tcp") >= 0; + +} \ No newline at end of file diff --git a/lib/micro-rtsp-server/src/micro_rtsp_source_camera.cpp b/lib/micro-rtsp-server/src/micro_rtsp_source_camera.cpp new file mode 100644 index 0000000..f819c41 --- /dev/null +++ b/lib/micro-rtsp-server/src/micro_rtsp_source_camera.cpp @@ -0,0 +1,36 @@ +#include + +#include "micro_rtsp_source_camera.h" + +micro_rtsp_source_camera::micro_rtsp_source_camera() +{ + init_result == ESP_FAIL; +} + +micro_rtsp_source_camera::~micro_rtsp_source_camera() +{ + deinitialize(); +} + +esp_err_t micro_rtsp_source_camera::initialize(camera_config_t *camera_config) +{ + + init_result = esp_camera_init(camera_config); + if (init_result == ESP_OK) + update_frame(); + else + log_e("Camera initialization failed: 0x%x", init_result); +} + +esp_err_t micro_rtsp_source_camera::deinitialize() +{ + return init_result == ESP_OK ? esp_camera_deinit() : ESP_OK; +} + +void micro_rtsp_source_camera::update_frame() +{ + if (fb) + esp_camera_fb_return(fb); + + fb = esp_camera_fb_get(); +} \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 3f722ef..8d604e2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/main.cpp b/src/main.cpp index 9b95f41..832e7b8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,9 @@ #include #include +#include +#include + // HTML files extern const char index_html_min_start[] asm("_binary_html_index_min_html_start"); @@ -48,10 +51,13 @@ auto param_colorbar = iotwebconf::Builder("cb"). // Camera OV2640 cam; +micro_rtsp_source_camera source_camera; // DNS Server DNSServer dnsServer; // RTSP Server std::unique_ptr camera_server; + +micro_rtsp_server micro_rtsp_server(&source_camera, RTSP_PORT); // Web server WebServer web_server(80); @@ -219,7 +225,9 @@ 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()); - constexpr auto i2c_port = I2C_NUM_0; + + // Set frame duration + micro_rtsp_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 @@ -250,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 = 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 source_camera.initialize(&camera_config); + + // return cam.init(camera_config); } void update_camera_settings()