├── .gitignore ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── 3D ├── Case+Stand.PNG ├── ESP32-CAM-Back.stl ├── ESP32-CAM-Case.f3z ├── ESP32-CAM-Foot.stl └── ESP32-CAM-Front.stl ├── ArduinoIDE └── Micro-RTSP.zip ├── Docs ├── 20200309_131750.mp4 ├── Breadboard-s.jpg ├── Breadboard.jpg ├── ESP32-CAM.avi ├── ESP32-CAM.mkv ├── ESP32-CAM.mp4 ├── VLC-Windows-1.PNG ├── VLC-Windows.PNG └── esp32-cam-development-board.jpg ├── HW ├── ESP32-CAM-Breadboard.fzz └── ESP32-CAM-schematic.png ├── README.md ├── customparts.csv ├── lib └── Micro-RTSP │ ├── .gitignore │ ├── .library.json │ ├── LICENSE │ ├── README.md │ ├── TODO.md │ ├── examples │ ├── .gitignore │ ├── ESP32-devcam.ino │ ├── platformio.ini │ └── wifikeys_template.h │ ├── library.json │ ├── library.properties │ ├── src │ ├── CRtspSession.cpp │ ├── CRtspSession.h │ ├── CStreamer.cpp │ ├── CStreamer.h │ ├── JPEGSamples.cpp │ ├── JPEGSamples.h │ ├── OV2640.cpp │ ├── OV2640.h │ ├── OV2640Streamer.cpp │ ├── OV2640Streamer.h │ ├── SimStreamer.cpp │ ├── SimStreamer.h │ ├── platglue-esp32.h │ ├── platglue-posix.h │ └── platglue.h │ └── test │ ├── Makefile │ ├── README.md │ ├── RTSPTestServer.cpp │ ├── devvlc.sh │ ├── rfccode.cpp │ └── runvlc.sh ├── platformio.ini └── src ├── main.cpp ├── main.h ├── ota.cpp ├── rtsp.cpp ├── src.ino ├── webstream.cpp └── wifikeys.h /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < https://docs.platformio.org/page/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < https://docs.platformio.org/page/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < https://docs.platformio.org/page/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choose one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | # language: python 28 | # python: 29 | # - "2.7" 30 | # 31 | # sudo: false 32 | # cache: 33 | # directories: 34 | # - "~/.platformio" 35 | # 36 | # install: 37 | # - pip install -U platformio 38 | # - platformio update 39 | # 40 | # script: 41 | # - platformio run 42 | 43 | 44 | # 45 | # Template #2: The project is intended to be used as a library with examples. 46 | # 47 | 48 | # language: python 49 | # python: 50 | # - "2.7" 51 | # 52 | # sudo: false 53 | # cache: 54 | # directories: 55 | # - "~/.platformio" 56 | # 57 | # env: 58 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 59 | # - PLATFORMIO_CI_SRC=examples/file.ino 60 | # - PLATFORMIO_CI_SRC=path/to/test/directory 61 | # 62 | # install: 63 | # - pip install -U platformio 64 | # - platformio update 65 | # 66 | # script: 67 | # - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N 68 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "functional": "cpp" 4 | } 5 | } -------------------------------------------------------------------------------- /3D/Case+Stand.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/3D/Case+Stand.PNG -------------------------------------------------------------------------------- /3D/ESP32-CAM-Back.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/3D/ESP32-CAM-Back.stl -------------------------------------------------------------------------------- /3D/ESP32-CAM-Case.f3z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/3D/ESP32-CAM-Case.f3z -------------------------------------------------------------------------------- /3D/ESP32-CAM-Foot.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/3D/ESP32-CAM-Foot.stl -------------------------------------------------------------------------------- /3D/ESP32-CAM-Front.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/3D/ESP32-CAM-Front.stl -------------------------------------------------------------------------------- /ArduinoIDE/Micro-RTSP.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/ArduinoIDE/Micro-RTSP.zip -------------------------------------------------------------------------------- /Docs/20200309_131750.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/Docs/20200309_131750.mp4 -------------------------------------------------------------------------------- /Docs/Breadboard-s.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/Docs/Breadboard-s.jpg -------------------------------------------------------------------------------- /Docs/Breadboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/Docs/Breadboard.jpg -------------------------------------------------------------------------------- /Docs/ESP32-CAM.avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/Docs/ESP32-CAM.avi -------------------------------------------------------------------------------- /Docs/ESP32-CAM.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/Docs/ESP32-CAM.mkv -------------------------------------------------------------------------------- /Docs/ESP32-CAM.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/Docs/ESP32-CAM.mp4 -------------------------------------------------------------------------------- /Docs/VLC-Windows-1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/Docs/VLC-Windows-1.PNG -------------------------------------------------------------------------------- /Docs/VLC-Windows.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/Docs/VLC-Windows.PNG -------------------------------------------------------------------------------- /Docs/esp32-cam-development-board.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/Docs/esp32-cam-development-board.jpg -------------------------------------------------------------------------------- /HW/ESP32-CAM-Breadboard.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/HW/ESP32-CAM-Breadboard.fzz -------------------------------------------------------------------------------- /HW/ESP32-CAM-schematic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circuitrocks/ESP32-RTSP/16e826e33a00661cd89eff55f53d37121ee8999d/HW/ESP32-CAM-schematic.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32-CAM RTSP stream 2 | 3 | This repository is for the [Circuitrocks@Learn](https://learn.circuit.rocks/?p=2245) blog how to stream a video from the ESP32-CAM to a RTSP client or web browser. 4 | 5 | The source code for both ArduinoIDE and PlatformIO is in the _**src**_ subfolder. 6 | 7 | ## Compiling with PlatformIO 8 | Just open the folder containing the files with PlatformIO and adjust your settings in platformio.ini. Required libraries are either automatically installed (OneButton) or in the lib folder (Micro-RTSP). 9 | 10 | ## Compiling with ArduinoIDE 11 | Install the required libraries as explained in the [blog](https://learn.circuit.rocks/?p=2245). 12 | Open the file **`src.ino`** in the _**src**_ folder. Don't be surprised that the file is empty. It is only there for compatibility with ArduinoIDE. 13 | 14 | -------------------------------------------------------------------------------- /customparts.csv: -------------------------------------------------------------------------------- 1 | # Name, Type, SubType, Offset, Size, Flags 2 | nvs, data, nvs, 0x9000, 0x5000, 3 | otadata, data, ota, 0xe000, 0x2000, 4 | app0, app, ota_0, 0x10000, 0x1E0000, 5 | app1, app, ota_1, 0x1F0000,0x1E0000, 6 | spiffs, data, spiffs, 0x3F0000,0x10000, -------------------------------------------------------------------------------- /lib/Micro-RTSP/.gitignore: -------------------------------------------------------------------------------- 1 | testserver 2 | octo.jpg 3 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/.library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Micro-RTSP", 3 | "keywords": [ 4 | "esp32", 5 | "camera", 6 | "esp32-cam", 7 | "rtsp" 8 | ], 9 | "description": "A small/efficient RTSP server for ESP32 and other micros", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/geeksville/Micro-RTSP.git" 13 | }, 14 | "authors": [ 15 | { 16 | "email": "kevinh@geeksville.com", 17 | "url": "https://github.com/geeksville", 18 | "maintainer": true, 19 | "name": "Kevin Hester" 20 | } 21 | ], 22 | "version": "0.1.6", 23 | "frameworks": [ 24 | "arduino" 25 | ], 26 | "platforms": [ 27 | "atmelavr", 28 | "atmelsam", 29 | "espressif32", 30 | "espressif8266", 31 | "intel_arc32", 32 | "microchippic32", 33 | "nordicnrf51", 34 | "nordicnrf52", 35 | "ststm32", 36 | "ststm8", 37 | "teensy", 38 | "timsp430" 39 | ], 40 | "id": 6071 41 | } -------------------------------------------------------------------------------- /lib/Micro-RTSP/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 S. Kevin Hester-Chow, kevinh@geeksville.com (MIT License) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/README.md: -------------------------------------------------------------------------------- 1 | # Micro-RTSP 2 | 3 | This is a small library which can be used to serve up RTSP streams from 4 | resource constrained MCUs. It lets you trivially make a $10 open source 5 | RTSP video stream camera. 6 | 7 | # Usage 8 | 9 | This library works for ESP32/arduino targets but also for most any posixish platform. 10 | 11 | ## Example arduino/ESP32 usage 12 | 13 | This library will work standalone, but it is _super_ easy to use if your app is platform.io based. 14 | Just "pio lib install Micro-RTSP" to pull the latest version from their library server. If you want to use the OV2640 15 | camera support you'll need to be targeting the espressif32 platform in your project. 16 | 17 | See the [example platform.io app](/examples). It should build and run on virtually any of the $10 18 | ESP32-CAM boards (such as M5CAM). The relevant bit of the code is included below. In short: 19 | 1. Listen for a TCP connection on the RTSP port with accept() 20 | 2. When a connection comes in, create a CRtspSession and OV2640Streamer camera streamer objects. 21 | 3. While the connection remains, call session->handleRequests(0) to handle any incoming client requests. 22 | 4. Every 100ms or so call session->broadcastCurrentFrame() to send new frames to any clients. 23 | 24 | ``` 25 | void loop() 26 | { 27 | uint32_t msecPerFrame = 100; 28 | static uint32_t lastimage = millis(); 29 | 30 | // If we have an active client connection, just service that until gone 31 | // (FIXME - support multiple simultaneous clients) 32 | if(session) { 33 | session->handleRequests(0); // we don't use a timeout here, 34 | // instead we send only if we have new enough frames 35 | 36 | uint32_t now = millis(); 37 | if(now > lastimage + msecPerFrame || now < lastimage) { // handle clock rollover 38 | session->broadcastCurrentFrame(now); 39 | lastimage = now; 40 | 41 | // check if we are overrunning our max frame rate 42 | now = millis(); 43 | if(now > lastimage + msecPerFrame) 44 | printf("warning exceeding max frame rate of %d ms\n", now - lastimage); 45 | } 46 | 47 | if(session->m_stopped) { 48 | delete session; 49 | delete streamer; 50 | session = NULL; 51 | streamer = NULL; 52 | } 53 | } 54 | else { 55 | client = rtspServer.accept(); 56 | 57 | if(client) { 58 | //streamer = new SimStreamer(&client, true); // our streamer for UDP/TCP based RTP transport 59 | streamer = new OV2640Streamer(&client, cam); // our streamer for UDP/TCP based RTP transport 60 | 61 | session = new CRtspSession(&client, streamer); // our threads RTSP session and state 62 | } 63 | } 64 | } 65 | ``` 66 | ## Example posix/linux usage 67 | 68 | There is a small standalone example [here](/test/RTSPTestServer.cpp). You can build it by following [these](/test/README.md) directions. The usage of the two key classes (CRtspSession and SimStreamer) are very similar to to the ESP32 usage. 69 | 70 | ## Supporting new camera devices 71 | 72 | Supporting new camera devices is quite simple. See OV2640Streamer for an example and implement streamImage() 73 | by reading a frame from your camera. 74 | 75 | # Structure and design notes 76 | 77 | # Issues and sending pull requests 78 | 79 | Please report issues and send pull requests. I'll happily reply. ;-) 80 | 81 | # Credits 82 | 83 | The server code was initially based on a great 2013 [tutorial](https://www.medialan.de/usecase0001.html) by Medialan. 84 | 85 | # License 86 | 87 | Copyright 2018 S. Kevin Hester-Chow, kevinh@geeksville.com (MIT License) 88 | 89 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 90 | 91 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 92 | 93 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 94 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/TODO.md: -------------------------------------------------------------------------------- 1 | * add instructions for example app 2 | * push RTSP streams to other servers ( https://github.com/ant-media/Ant-Media-Server/wiki/Getting-Started ) 3 | * make stack larger so that the various scratch buffers (currently in bss) can be shared 4 | * cleanup code to a less ugly unified coding standard 5 | * support multiple simultaneous clients on the device 6 | * make octocat test image work again (by changing encoding type from 1 to 0 (422 vs 420)) 7 | 8 | DONE: 9 | * serve real jpegs (use correct quantization & huffman tables) 10 | * test that both TCP and UDP clients work 11 | * change framerate to something slow 12 | * test remote access 13 | * select a licence and put license into github 14 | * find cause of new mystery pause when starting up in sim mode 15 | * split sim code from real code via inheritence 16 | * use device camera 17 | * package the ESP32-CAM stuff as a library so I can depend on it 18 | * package as a library https://docs.platformio.org/en/latest/librarymanager/creating.html#library-creating-examples 19 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/examples/.gitignore: -------------------------------------------------------------------------------- 1 | wifikeys.h 2 | .pioenvs 3 | .piolibdeps 4 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/examples/ESP32-devcam.ino: -------------------------------------------------------------------------------- 1 | #include "OV2640.h" 2 | #include 3 | #include 4 | #include 5 | 6 | #include "SimStreamer.h" 7 | #include "OV2640Streamer.h" 8 | #include "CRtspSession.h" 9 | 10 | #define ENABLE_OLED //if want use oled ,turn on thi macro 11 | // #define SOFTAP_MODE // If you want to run our own softap turn this on 12 | #define ENABLE_WEBSERVER 13 | #define ENABLE_RTSPSERVER 14 | 15 | #ifdef ENABLE_OLED 16 | #include "SSD1306.h" 17 | #define OLED_ADDRESS 0x3c 18 | #define I2C_SDA 14 19 | #define I2C_SCL 13 20 | SSD1306Wire display(OLED_ADDRESS, I2C_SDA, I2C_SCL, GEOMETRY_128_32); 21 | bool hasDisplay; // we probe for the device at runtime 22 | #endif 23 | 24 | OV2640 cam; 25 | 26 | #ifdef ENABLE_WEBSERVER 27 | WebServer server(80); 28 | #endif 29 | 30 | #ifdef ENABLE_RTSPSERVER 31 | WiFiServer rtspServer(8554); 32 | #endif 33 | 34 | 35 | #ifdef SOFTAP_MODE 36 | IPAddress apIP = IPAddress(192, 168, 1, 1); 37 | #else 38 | #include "wifikeys.h" 39 | #endif 40 | 41 | #ifdef ENABLE_WEBSERVER 42 | void handle_jpg_stream(void) 43 | { 44 | WiFiClient client = server.client(); 45 | String response = "HTTP/1.1 200 OK\r\n"; 46 | response += "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n"; 47 | server.sendContent(response); 48 | 49 | while (1) 50 | { 51 | cam.run(); 52 | if (!client.connected()) 53 | break; 54 | response = "--frame\r\n"; 55 | response += "Content-Type: image/jpeg\r\n\r\n"; 56 | server.sendContent(response); 57 | 58 | client.write((char *)cam.getfb(), cam.getSize()); 59 | server.sendContent("\r\n"); 60 | if (!client.connected()) 61 | break; 62 | } 63 | } 64 | 65 | void handle_jpg(void) 66 | { 67 | WiFiClient client = server.client(); 68 | 69 | cam.run(); 70 | if (!client.connected()) 71 | { 72 | return; 73 | } 74 | String response = "HTTP/1.1 200 OK\r\n"; 75 | response += "Content-disposition: inline; filename=capture.jpg\r\n"; 76 | response += "Content-type: image/jpeg\r\n\r\n"; 77 | server.sendContent(response); 78 | client.write((char *)cam.getfb(), cam.getSize()); 79 | } 80 | 81 | void handleNotFound() 82 | { 83 | String message = "Server is running!\n\n"; 84 | message += "URI: "; 85 | message += server.uri(); 86 | message += "\nMethod: "; 87 | message += (server.method() == HTTP_GET) ? "GET" : "POST"; 88 | message += "\nArguments: "; 89 | message += server.args(); 90 | message += "\n"; 91 | server.send(200, "text/plain", message); 92 | } 93 | #endif 94 | 95 | void lcdMessage(String msg) 96 | { 97 | #ifdef ENABLE_OLED 98 | if(hasDisplay) { 99 | display.clear(); 100 | display.drawString(128 / 2, 32 / 2, msg); 101 | display.display(); 102 | } 103 | #endif 104 | } 105 | 106 | void setup() 107 | { 108 | #ifdef ENABLE_OLED 109 | hasDisplay = display.init(); 110 | if(hasDisplay) { 111 | display.flipScreenVertically(); 112 | display.setFont(ArialMT_Plain_16); 113 | display.setTextAlignment(TEXT_ALIGN_CENTER); 114 | } 115 | #endif 116 | lcdMessage("booting"); 117 | 118 | Serial.begin(115200); 119 | while (!Serial) 120 | { 121 | ; 122 | } 123 | cam.init(esp32cam_config); 124 | 125 | IPAddress ip; 126 | 127 | 128 | #ifdef SOFTAP_MODE 129 | const char *hostname = "devcam"; 130 | // WiFi.hostname(hostname); // FIXME - find out why undefined 131 | lcdMessage("starting softAP"); 132 | WiFi.mode(WIFI_AP); 133 | WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); 134 | bool result = WiFi.softAP(hostname, "12345678", 1, 0); 135 | if (!result) 136 | { 137 | Serial.println("AP Config failed."); 138 | return; 139 | } 140 | else 141 | { 142 | Serial.println("AP Config Success."); 143 | Serial.print("AP MAC: "); 144 | Serial.println(WiFi.softAPmacAddress()); 145 | 146 | ip = WiFi.softAPIP(); 147 | } 148 | #else 149 | lcdMessage(String("join ") + ssid); 150 | WiFi.mode(WIFI_STA); 151 | WiFi.begin(ssid, password); 152 | while (WiFi.status() != WL_CONNECTED) 153 | { 154 | delay(500); 155 | Serial.print(F(".")); 156 | } 157 | ip = WiFi.localIP(); 158 | Serial.println(F("WiFi connected")); 159 | Serial.println(""); 160 | Serial.println(ip); 161 | #endif 162 | 163 | lcdMessage(ip.toString()); 164 | 165 | #ifdef ENABLE_WEBSERVER 166 | server.on("/", HTTP_GET, handle_jpg_stream); 167 | server.on("/jpg", HTTP_GET, handle_jpg); 168 | server.onNotFound(handleNotFound); 169 | server.begin(); 170 | #endif 171 | 172 | #ifdef ENABLE_RTSPSERVER 173 | rtspServer.begin(); 174 | #endif 175 | } 176 | 177 | CStreamer *streamer; 178 | CRtspSession *session; 179 | WiFiClient client; // FIXME, support multiple clients 180 | 181 | void loop() 182 | { 183 | #ifdef ENABLE_WEBSERVER 184 | server.handleClient(); 185 | #endif 186 | 187 | #ifdef ENABLE_RTSPSERVER 188 | uint32_t msecPerFrame = 100; 189 | static uint32_t lastimage = millis(); 190 | 191 | // If we have an active client connection, just service that until gone 192 | // (FIXME - support multiple simultaneous clients) 193 | if(session) { 194 | session->handleRequests(0); // we don't use a timeout here, 195 | // instead we send only if we have new enough frames 196 | 197 | uint32_t now = millis(); 198 | if(now > lastimage + msecPerFrame || now < lastimage) { // handle clock rollover 199 | session->broadcastCurrentFrame(now); 200 | lastimage = now; 201 | 202 | // check if we are overrunning our max frame rate 203 | now = millis(); 204 | if(now > lastimage + msecPerFrame) 205 | printf("warning exceeding max frame rate of %d ms\n", now - lastimage); 206 | } 207 | 208 | if(session->m_stopped) { 209 | delete session; 210 | delete streamer; 211 | session = NULL; 212 | streamer = NULL; 213 | } 214 | } 215 | else { 216 | client = rtspServer.accept(); 217 | 218 | if(client) { 219 | //streamer = new SimStreamer(&client, true); // our streamer for UDP/TCP based RTP transport 220 | streamer = new OV2640Streamer(&client, cam); // our streamer for UDP/TCP based RTP transport 221 | 222 | session = new CRtspSession(&client, streamer); // our threads RTSP session and state 223 | } 224 | } 225 | #endif 226 | } 227 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/examples/platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:m5stack-core-esp32] 12 | platform = espressif32@>=1.6.0 13 | board = m5stack-core-esp32 14 | framework = arduino 15 | lib_deps = Micro-RTSP 16 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/examples/wifikeys_template.h: -------------------------------------------------------------------------------- 1 | // copy this file to wifikeys.h and edit 2 | const char *ssid = "YOURNETHERE"; // Put your SSID here 3 | const char *password = "YOURPASSWORDHERE"; // Put your PASSWORD here 4 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Micro-RTSP", 3 | "keywords": "esp32, camera, esp32-cam, rtsp", 4 | "description": "A small/efficient RTSP server for ESP32 and other micros", 5 | "repository": 6 | { 7 | "type": "git", 8 | "url": "https://github.com/geeksville/Micro-RTSP.git" 9 | }, 10 | "authors": 11 | [ 12 | { 13 | "name": "Kevin Hester", 14 | "email": "kevinh@geeksville.com", 15 | "url": "https://github.com/geeksville", 16 | "maintainer": true 17 | } 18 | ], 19 | "version": "0.1.6", 20 | "frameworks": "arduino", 21 | "platforms": "*" 22 | } 23 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/library.properties: -------------------------------------------------------------------------------- 1 | name=Micro-RTSP 2 | version=0.1.6 3 | author=Kevin Hester 4 | maintainer=Kevin Hester 5 | sentence=Mikro RTSP server for mikros 6 | paragraph=A small/efficient RTSP server for ESP32 and other micros 7 | category=Data Storage 8 | url=https://github.com/geeksville/Micro-RTSP.git 9 | architectures=* 10 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/CRtspSession.cpp: -------------------------------------------------------------------------------- 1 | #include "CRtspSession.h" 2 | #include 3 | #include 4 | 5 | CRtspSession::CRtspSession(SOCKET aRtspClient, CStreamer * aStreamer) : m_RtspClient(aRtspClient),m_Streamer(aStreamer) 6 | { 7 | printf("Creating RTSP session\n"); 8 | Init(); 9 | 10 | m_RtspSessionID = getRandom(); // create a session ID 11 | m_RtspSessionID |= 0x80000000; 12 | m_StreamID = -1; 13 | m_ClientRTPPort = 0; 14 | m_ClientRTCPPort = 0; 15 | m_TcpTransport = false; 16 | m_streaming = false; 17 | m_stopped = false; 18 | }; 19 | 20 | CRtspSession::~CRtspSession() 21 | { 22 | closesocket(m_RtspClient); 23 | }; 24 | 25 | void CRtspSession::Init() 26 | { 27 | m_RtspCmdType = RTSP_UNKNOWN; 28 | memset(m_URLPreSuffix, 0x00, sizeof(m_URLPreSuffix)); 29 | memset(m_URLSuffix, 0x00, sizeof(m_URLSuffix)); 30 | memset(m_CSeq, 0x00, sizeof(m_CSeq)); 31 | memset(m_URLHostPort, 0x00, sizeof(m_URLHostPort)); 32 | m_ContentLength = 0; 33 | }; 34 | 35 | bool CRtspSession::ParseRtspRequest(char const * aRequest, unsigned aRequestSize) 36 | { 37 | char CmdName[RTSP_PARAM_STRING_MAX]; 38 | static char CurRequest[RTSP_BUFFER_SIZE]; // Note: we assume single threaded, this large buf we keep off of the tiny stack 39 | unsigned CurRequestSize; 40 | 41 | Init(); 42 | CurRequestSize = aRequestSize; 43 | memcpy(CurRequest,aRequest,aRequestSize); 44 | 45 | // check whether the request contains information about the RTP/RTCP UDP client ports (SETUP command) 46 | char * ClientPortPtr; 47 | char * TmpPtr; 48 | static char CP[1024]; 49 | char * pCP; 50 | 51 | ClientPortPtr = strstr(CurRequest,"client_port"); 52 | if (ClientPortPtr != nullptr) 53 | { 54 | TmpPtr = strstr(ClientPortPtr,"\r\n"); 55 | if (TmpPtr != nullptr) 56 | { 57 | TmpPtr[0] = 0x00; 58 | strcpy(CP,ClientPortPtr); 59 | pCP = strstr(CP,"="); 60 | if (pCP != nullptr) 61 | { 62 | pCP++; 63 | strcpy(CP,pCP); 64 | pCP = strstr(CP,"-"); 65 | if (pCP != nullptr) 66 | { 67 | pCP[0] = 0x00; 68 | m_ClientRTPPort = atoi(CP); 69 | m_ClientRTCPPort = m_ClientRTPPort + 1; 70 | }; 71 | }; 72 | }; 73 | }; 74 | 75 | // Read everything up to the first space as the command name 76 | bool parseSucceeded = false; 77 | unsigned i; 78 | for (i = 0; i < sizeof(CmdName)-1 && i < CurRequestSize; ++i) 79 | { 80 | char c = CurRequest[i]; 81 | if (c == ' ' || c == '\t') 82 | { 83 | parseSucceeded = true; 84 | break; 85 | } 86 | CmdName[i] = c; 87 | } 88 | CmdName[i] = '\0'; 89 | if (!parseSucceeded) { 90 | printf("failed to parse RTSP\n"); 91 | return false; 92 | } 93 | 94 | printf("RTSP received %s\n", CmdName); 95 | 96 | // find out the command type 97 | if (strstr(CmdName,"OPTIONS") != nullptr) m_RtspCmdType = RTSP_OPTIONS; else 98 | if (strstr(CmdName,"DESCRIBE") != nullptr) m_RtspCmdType = RTSP_DESCRIBE; else 99 | if (strstr(CmdName,"SETUP") != nullptr) m_RtspCmdType = RTSP_SETUP; else 100 | if (strstr(CmdName,"PLAY") != nullptr) m_RtspCmdType = RTSP_PLAY; else 101 | if (strstr(CmdName,"TEARDOWN") != nullptr) m_RtspCmdType = RTSP_TEARDOWN; 102 | 103 | // check whether the request contains transport information (UDP or TCP) 104 | if (m_RtspCmdType == RTSP_SETUP) 105 | { 106 | TmpPtr = strstr(CurRequest,"RTP/AVP/TCP"); 107 | if (TmpPtr != nullptr) m_TcpTransport = true; else m_TcpTransport = false; 108 | }; 109 | 110 | // Skip over the prefix of any "rtsp://" or "rtsp:/" URL that follows: 111 | unsigned j = i+1; 112 | while (j < CurRequestSize && (CurRequest[j] == ' ' || CurRequest[j] == '\t')) ++j; // skip over any additional white space 113 | for (; (int)j < (int)(CurRequestSize-8); ++j) 114 | { 115 | if ((CurRequest[j] == 'r' || CurRequest[j] == 'R') && 116 | (CurRequest[j+1] == 't' || CurRequest[j+1] == 'T') && 117 | (CurRequest[j+2] == 's' || CurRequest[j+2] == 'S') && 118 | (CurRequest[j+3] == 'p' || CurRequest[j+3] == 'P') && 119 | CurRequest[j+4] == ':' && CurRequest[j+5] == '/') 120 | { 121 | j += 6; 122 | if (CurRequest[j] == '/') 123 | { // This is a "rtsp://" URL; skip over the host:port part that follows: 124 | ++j; 125 | unsigned uidx = 0; 126 | while (j < CurRequestSize && CurRequest[j] != '/' && CurRequest[j] != ' ' && uidx < sizeof(m_URLHostPort) - 1) 127 | { // extract the host:port part of the URL here 128 | m_URLHostPort[uidx] = CurRequest[j]; 129 | uidx++; 130 | ++j; 131 | }; 132 | } 133 | else --j; 134 | i = j; 135 | break; 136 | } 137 | } 138 | 139 | // Look for the URL suffix (before the following "RTSP/"): 140 | parseSucceeded = false; 141 | for (unsigned k = i+1; (int)k < (int)(CurRequestSize-5); ++k) 142 | { 143 | if (CurRequest[k] == 'R' && CurRequest[k+1] == 'T' && 144 | CurRequest[k+2] == 'S' && CurRequest[k+3] == 'P' && 145 | CurRequest[k+4] == '/') 146 | { 147 | while (--k >= i && CurRequest[k] == ' ') {} 148 | unsigned k1 = k; 149 | while (k1 > i && CurRequest[k1] != '/') --k1; 150 | if (k - k1 + 1 > sizeof(m_URLSuffix)) return false; 151 | unsigned n = 0, k2 = k1+1; 152 | 153 | while (k2 <= k) m_URLSuffix[n++] = CurRequest[k2++]; 154 | m_URLSuffix[n] = '\0'; 155 | 156 | if (k1 - i > sizeof(m_URLPreSuffix)) return false; 157 | n = 0; k2 = i + 1; 158 | while (k2 <= k1 - 1) m_URLPreSuffix[n++] = CurRequest[k2++]; 159 | m_URLPreSuffix[n] = '\0'; 160 | i = k + 7; 161 | parseSucceeded = true; 162 | break; 163 | } 164 | } 165 | if (!parseSucceeded) return false; 166 | 167 | // Look for "CSeq:", skip whitespace, then read everything up to the next \r or \n as 'CSeq': 168 | parseSucceeded = false; 169 | for (j = i; (int)j < (int)(CurRequestSize-5); ++j) 170 | { 171 | if (CurRequest[j] == 'C' && CurRequest[j+1] == 'S' && 172 | CurRequest[j+2] == 'e' && CurRequest[j+3] == 'q' && 173 | CurRequest[j+4] == ':') 174 | { 175 | j += 5; 176 | while (j < CurRequestSize && (CurRequest[j] == ' ' || CurRequest[j] == '\t')) ++j; 177 | unsigned n; 178 | for (n = 0; n < sizeof(m_CSeq)-1 && j < CurRequestSize; ++n,++j) 179 | { 180 | char c = CurRequest[j]; 181 | if (c == '\r' || c == '\n') 182 | { 183 | parseSucceeded = true; 184 | break; 185 | } 186 | m_CSeq[n] = c; 187 | } 188 | m_CSeq[n] = '\0'; 189 | break; 190 | } 191 | } 192 | if (!parseSucceeded) return false; 193 | 194 | // Also: Look for "Content-Length:" (optional) 195 | for (j = i; (int)j < (int)(CurRequestSize-15); ++j) 196 | { 197 | if (CurRequest[j] == 'C' && CurRequest[j+1] == 'o' && 198 | CurRequest[j+2] == 'n' && CurRequest[j+3] == 't' && 199 | CurRequest[j+4] == 'e' && CurRequest[j+5] == 'n' && 200 | CurRequest[j+6] == 't' && CurRequest[j+7] == '-' && 201 | (CurRequest[j+8] == 'L' || CurRequest[j+8] == 'l') && 202 | CurRequest[j+9] == 'e' && CurRequest[j+10] == 'n' && 203 | CurRequest[j+11] == 'g' && CurRequest[j+12] == 't' && 204 | CurRequest[j+13] == 'h' && CurRequest[j+14] == ':') 205 | { 206 | j += 15; 207 | while (j < CurRequestSize && (CurRequest[j] == ' ' || CurRequest[j] == '\t')) ++j; 208 | unsigned num; 209 | if (sscanf(&CurRequest[j], "%u", &num) == 1) m_ContentLength = num; 210 | } 211 | } 212 | return true; 213 | }; 214 | 215 | RTSP_CMD_TYPES CRtspSession::Handle_RtspRequest(char const * aRequest, unsigned aRequestSize) 216 | { 217 | if (ParseRtspRequest(aRequest,aRequestSize)) 218 | { 219 | switch (m_RtspCmdType) 220 | { 221 | case RTSP_OPTIONS: { Handle_RtspOPTION(); break; }; 222 | case RTSP_DESCRIBE: { Handle_RtspDESCRIBE(); break; }; 223 | case RTSP_SETUP: { Handle_RtspSETUP(); break; }; 224 | case RTSP_PLAY: { Handle_RtspPLAY(); break; }; 225 | default: {}; 226 | }; 227 | }; 228 | return m_RtspCmdType; 229 | }; 230 | 231 | void CRtspSession::Handle_RtspOPTION() 232 | { 233 | static char Response[1024]; // Note: we assume single threaded, this large buf we keep off of the tiny stack 234 | 235 | snprintf(Response,sizeof(Response), 236 | "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" 237 | "Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n\r\n",m_CSeq); 238 | 239 | socketsend(m_RtspClient,Response,strlen(Response)); 240 | } 241 | 242 | void CRtspSession::Handle_RtspDESCRIBE() 243 | { 244 | static char Response[1024]; // Note: we assume single threaded, this large buf we keep off of the tiny stack 245 | static char SDPBuf[1024]; 246 | static char URLBuf[1024]; 247 | 248 | // check whether we know a stream with the URL which is requested 249 | m_StreamID = -1; // invalid URL 250 | if ((strcmp(m_URLPreSuffix,"mjpeg") == 0) && (strcmp(m_URLSuffix,"1") == 0)) m_StreamID = 0; else 251 | if ((strcmp(m_URLPreSuffix,"mjpeg") == 0) && (strcmp(m_URLSuffix,"2") == 0)) m_StreamID = 1; 252 | if (m_StreamID == -1) 253 | { // Stream not available 254 | snprintf(Response,sizeof(Response), 255 | "RTSP/1.0 404 Stream Not Found\r\nCSeq: %s\r\n%s\r\n", 256 | m_CSeq, 257 | DateHeader()); 258 | 259 | socketsend(m_RtspClient,Response,strlen(Response)); 260 | return; 261 | }; 262 | 263 | // simulate DESCRIBE server response 264 | static char OBuf[256]; 265 | char * ColonPtr; 266 | strcpy(OBuf,m_URLHostPort); 267 | ColonPtr = strstr(OBuf,":"); 268 | if (ColonPtr != nullptr) ColonPtr[0] = 0x00; 269 | 270 | snprintf(SDPBuf,sizeof(SDPBuf), 271 | "v=0\r\n" 272 | "o=- %d 1 IN IP4 %s\r\n" 273 | "s=\r\n" 274 | "t=0 0\r\n" // start / stop - 0 -> unbounded and permanent session 275 | "m=video 0 RTP/AVP 26\r\n" // currently we just handle UDP sessions 276 | // "a=x-dimensions: 640,480\r\n" 277 | "c=IN IP4 0.0.0.0\r\n", 278 | rand(), 279 | OBuf); 280 | char StreamName[64]; 281 | switch (m_StreamID) 282 | { 283 | case 0: strcpy(StreamName,"mjpeg/1"); break; 284 | case 1: strcpy(StreamName,"mjpeg/2"); break; 285 | }; 286 | snprintf(URLBuf,sizeof(URLBuf), 287 | "rtsp://%s/%s", 288 | m_URLHostPort, 289 | StreamName); 290 | snprintf(Response,sizeof(Response), 291 | "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" 292 | "%s\r\n" 293 | "Content-Base: %s/\r\n" 294 | "Content-Type: application/sdp\r\n" 295 | "Content-Length: %d\r\n\r\n" 296 | "%s", 297 | m_CSeq, 298 | DateHeader(), 299 | URLBuf, 300 | (int) strlen(SDPBuf), 301 | SDPBuf); 302 | 303 | socketsend(m_RtspClient,Response,strlen(Response)); 304 | } 305 | 306 | void CRtspSession::Handle_RtspSETUP() 307 | { 308 | static char Response[1024]; 309 | static char Transport[255]; 310 | 311 | // init RTP streamer transport type (UDP or TCP) and ports for UDP transport 312 | m_Streamer->InitTransport(m_ClientRTPPort,m_ClientRTCPPort,m_TcpTransport); 313 | 314 | // simulate SETUP server response 315 | if (m_TcpTransport) 316 | snprintf(Transport,sizeof(Transport),"RTP/AVP/TCP;unicast;interleaved=0-1"); 317 | else 318 | snprintf(Transport,sizeof(Transport), 319 | "RTP/AVP;unicast;destination=127.0.0.1;source=127.0.0.1;client_port=%i-%i;server_port=%i-%i", 320 | m_ClientRTPPort, 321 | m_ClientRTCPPort, 322 | m_Streamer->GetRtpServerPort(), 323 | m_Streamer->GetRtcpServerPort()); 324 | snprintf(Response,sizeof(Response), 325 | "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" 326 | "%s\r\n" 327 | "Transport: %s\r\n" 328 | "Session: %i\r\n\r\n", 329 | m_CSeq, 330 | DateHeader(), 331 | Transport, 332 | m_RtspSessionID); 333 | 334 | socketsend(m_RtspClient,Response,strlen(Response)); 335 | } 336 | 337 | void CRtspSession::Handle_RtspPLAY() 338 | { 339 | static char Response[1024]; 340 | 341 | // simulate SETUP server response 342 | snprintf(Response,sizeof(Response), 343 | "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" 344 | "%s\r\n" 345 | "Range: npt=0.000-\r\n" 346 | "Session: %i\r\n" 347 | "RTP-Info: url=rtsp://127.0.0.1:8554/mjpeg/1/track1\r\n\r\n", 348 | m_CSeq, 349 | DateHeader(), 350 | m_RtspSessionID); 351 | 352 | socketsend(m_RtspClient,Response,strlen(Response)); 353 | } 354 | 355 | char const * CRtspSession::DateHeader() 356 | { 357 | static char buf[200]; 358 | time_t tt = time(NULL); 359 | strftime(buf, sizeof buf, "Date: %a, %b %d %Y %H:%M:%S GMT", gmtime(&tt)); 360 | return buf; 361 | } 362 | 363 | int CRtspSession::GetStreamID() 364 | { 365 | return m_StreamID; 366 | }; 367 | 368 | 369 | 370 | /** 371 | Read from our socket, parsing commands as possible. 372 | */ 373 | bool CRtspSession::handleRequests(uint32_t readTimeoutMs) 374 | { 375 | if(m_stopped) 376 | return false; // Already closed down 377 | 378 | static char RecvBuf[RTSP_BUFFER_SIZE]; // Note: we assume single threaded, this large buf we keep off of the tiny stack 379 | 380 | memset(RecvBuf,0x00,sizeof(RecvBuf)); 381 | int res = socketread(m_RtspClient,RecvBuf,sizeof(RecvBuf), readTimeoutMs); 382 | if(res > 0) { 383 | // we filter away everything which seems not to be an RTSP command: O-ption, D-escribe, S-etup, P-lay, T-eardown 384 | if ((RecvBuf[0] == 'O') || (RecvBuf[0] == 'D') || (RecvBuf[0] == 'S') || (RecvBuf[0] == 'P') || (RecvBuf[0] == 'T')) 385 | { 386 | RTSP_CMD_TYPES C = Handle_RtspRequest(RecvBuf,res); 387 | if (C == RTSP_PLAY) 388 | m_streaming = true; 389 | else if (C == RTSP_TEARDOWN) 390 | m_stopped = true; 391 | } 392 | return true; 393 | } 394 | else if(res == 0) { 395 | printf("client closed socket, exiting\n"); 396 | m_stopped = true; 397 | return true; 398 | } 399 | else { 400 | // Timeout on read 401 | 402 | return false; 403 | } 404 | } 405 | 406 | void CRtspSession::broadcastCurrentFrame(uint32_t curMsec) { 407 | // Send a frame 408 | if (m_streaming && !m_stopped) { 409 | // printf("serving a frame\n"); 410 | m_Streamer->streamImage(curMsec); 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/CRtspSession.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CStreamer.h" 4 | #include "platglue.h" 5 | 6 | // supported command types 7 | enum RTSP_CMD_TYPES 8 | { 9 | RTSP_OPTIONS, 10 | RTSP_DESCRIBE, 11 | RTSP_SETUP, 12 | RTSP_PLAY, 13 | RTSP_TEARDOWN, 14 | RTSP_UNKNOWN 15 | }; 16 | 17 | #define RTSP_BUFFER_SIZE 10000 // for incoming requests, and outgoing responses 18 | #define RTSP_PARAM_STRING_MAX 200 19 | #define MAX_HOSTNAME_LEN 256 20 | 21 | class CRtspSession 22 | { 23 | public: 24 | CRtspSession(SOCKET aRtspClient, CStreamer * aStreamer); 25 | ~CRtspSession(); 26 | 27 | RTSP_CMD_TYPES Handle_RtspRequest(char const * aRequest, unsigned aRequestSize); 28 | int GetStreamID(); 29 | 30 | /** 31 | Read from our socket, parsing commands as possible. 32 | 33 | return false if the read timed out 34 | */ 35 | bool handleRequests(uint32_t readTimeoutMs); 36 | 37 | /** 38 | broadcast a current frame 39 | */ 40 | void broadcastCurrentFrame(uint32_t curMsec); 41 | 42 | bool m_streaming; 43 | bool m_stopped; 44 | 45 | private: 46 | void Init(); 47 | bool ParseRtspRequest(char const * aRequest, unsigned aRequestSize); 48 | char const * DateHeader(); 49 | 50 | // RTSP request command handlers 51 | void Handle_RtspOPTION(); 52 | void Handle_RtspDESCRIBE(); 53 | void Handle_RtspSETUP(); 54 | void Handle_RtspPLAY(); 55 | 56 | // global session state parameters 57 | int m_RtspSessionID; 58 | SOCKET m_RtspClient; // RTSP socket of that session 59 | int m_StreamID; // number of simulated stream of that session 60 | IPPORT m_ClientRTPPort; // client port for UDP based RTP transport 61 | IPPORT m_ClientRTCPPort; // client port for UDP based RTCP transport 62 | bool m_TcpTransport; // if Tcp based streaming was activated 63 | CStreamer * m_Streamer; // the UDP or TCP streamer of that session 64 | 65 | // parameters of the last received RTSP request 66 | 67 | RTSP_CMD_TYPES m_RtspCmdType; // command type (if any) of the current request 68 | char m_URLPreSuffix[RTSP_PARAM_STRING_MAX]; // stream name pre suffix 69 | char m_URLSuffix[RTSP_PARAM_STRING_MAX]; // stream name suffix 70 | char m_CSeq[RTSP_PARAM_STRING_MAX]; // RTSP command sequence number 71 | char m_URLHostPort[MAX_HOSTNAME_LEN]; // host:port part of the URL 72 | unsigned m_ContentLength; // SDP string size 73 | }; 74 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/CStreamer.cpp: -------------------------------------------------------------------------------- 1 | #include "CStreamer.h" 2 | 3 | #include 4 | 5 | CStreamer::CStreamer(SOCKET aClient, u_short width, u_short height) : m_Client(aClient) 6 | { 7 | printf("Creating TSP streamer\n"); 8 | m_RtpServerPort = 0; 9 | m_RtcpServerPort = 0; 10 | m_RtpClientPort = 0; 11 | m_RtcpClientPort = 0; 12 | 13 | m_SequenceNumber = 0; 14 | m_Timestamp = 0; 15 | m_SendIdx = 0; 16 | m_TCPTransport = false; 17 | 18 | m_RtpSocket = NULLSOCKET; 19 | m_RtcpSocket = NULLSOCKET; 20 | 21 | m_width = width; 22 | m_height = height; 23 | m_prevMsec = 0; 24 | }; 25 | 26 | CStreamer::~CStreamer() 27 | { 28 | udpsocketclose(m_RtpSocket); 29 | udpsocketclose(m_RtcpSocket); 30 | }; 31 | 32 | int CStreamer::SendRtpPacket(unsigned const char * jpeg, int jpegLen, int fragmentOffset, BufPtr quant0tbl, BufPtr quant1tbl) 33 | { 34 | #define KRtpHeaderSize 12 // size of the RTP header 35 | #define KJpegHeaderSize 8 // size of the special JPEG payload header 36 | 37 | #define MAX_FRAGMENT_SIZE 1100 // FIXME, pick more carefully 38 | int fragmentLen = MAX_FRAGMENT_SIZE; 39 | if(fragmentLen + fragmentOffset > jpegLen) // Shrink last fragment if needed 40 | fragmentLen = jpegLen - fragmentOffset; 41 | 42 | bool isLastFragment = (fragmentOffset + fragmentLen) == jpegLen; 43 | 44 | // Do we have custom quant tables? If so include them per RFC 45 | 46 | bool includeQuantTbl = quant0tbl && quant1tbl && fragmentOffset == 0; 47 | uint8_t q = includeQuantTbl ? 128 : 0x5e; 48 | 49 | static char RtpBuf[2048]; // Note: we assume single threaded, this large buf we keep off of the tiny stack 50 | int RtpPacketSize = fragmentLen + KRtpHeaderSize + KJpegHeaderSize + (includeQuantTbl ? (4 + 64 * 2) : 0); 51 | 52 | memset(RtpBuf,0x00,sizeof(RtpBuf)); 53 | // Prepare the first 4 byte of the packet. This is the Rtp over Rtsp header in case of TCP based transport 54 | RtpBuf[0] = '$'; // magic number 55 | RtpBuf[1] = 0; // number of multiplexed subchannel on RTPS connection - here the RTP channel 56 | RtpBuf[2] = (RtpPacketSize & 0x0000FF00) >> 8; 57 | RtpBuf[3] = (RtpPacketSize & 0x000000FF); 58 | // Prepare the 12 byte RTP header 59 | RtpBuf[4] = 0x80; // RTP version 60 | RtpBuf[5] = 0x1a | (isLastFragment ? 0x80 : 0x00); // JPEG payload (26) and marker bit 61 | RtpBuf[7] = m_SequenceNumber & 0x0FF; // each packet is counted with a sequence counter 62 | RtpBuf[6] = m_SequenceNumber >> 8; 63 | RtpBuf[8] = (m_Timestamp & 0xFF000000) >> 24; // each image gets a timestamp 64 | RtpBuf[9] = (m_Timestamp & 0x00FF0000) >> 16; 65 | RtpBuf[10] = (m_Timestamp & 0x0000FF00) >> 8; 66 | RtpBuf[11] = (m_Timestamp & 0x000000FF); 67 | RtpBuf[12] = 0x13; // 4 byte SSRC (sychronization source identifier) 68 | RtpBuf[13] = 0xf9; // we just an arbitrary number here to keep it simple 69 | RtpBuf[14] = 0x7e; 70 | RtpBuf[15] = 0x67; 71 | 72 | // Prepare the 8 byte payload JPEG header 73 | RtpBuf[16] = 0x00; // type specific 74 | RtpBuf[17] = (fragmentOffset & 0x00FF0000) >> 16; // 3 byte fragmentation offset for fragmented images 75 | RtpBuf[18] = (fragmentOffset & 0x0000FF00) >> 8; 76 | RtpBuf[19] = (fragmentOffset & 0x000000FF); 77 | 78 | /* These sampling factors indicate that the chrominance components of 79 | type 0 video is downsampled horizontally by 2 (often called 4:2:2) 80 | while the chrominance components of type 1 video are downsampled both 81 | horizontally and vertically by 2 (often called 4:2:0). */ 82 | RtpBuf[20] = 0x00; // type (fixme might be wrong for camera data) https://tools.ietf.org/html/rfc2435 83 | RtpBuf[21] = q; // quality scale factor was 0x5e 84 | RtpBuf[22] = m_width / 8; // width / 8 85 | RtpBuf[23] = m_height / 8; // height / 8 86 | 87 | int headerLen = 24; // Inlcuding jpeg header but not qant table header 88 | if(includeQuantTbl) { // we need a quant header - but only in first packet of the frame 89 | //printf("inserting quanttbl\n"); 90 | RtpBuf[24] = 0; // MBZ 91 | RtpBuf[25] = 0; // 8 bit precision 92 | RtpBuf[26] = 0; // MSB of lentgh 93 | 94 | int numQantBytes = 64; // Two 64 byte tables 95 | RtpBuf[27] = 2 * numQantBytes; // LSB of length 96 | 97 | headerLen += 4; 98 | 99 | memcpy(RtpBuf + headerLen, quant0tbl, numQantBytes); 100 | headerLen += numQantBytes; 101 | 102 | memcpy(RtpBuf + headerLen, quant1tbl, numQantBytes); 103 | headerLen += numQantBytes; 104 | } 105 | // printf("Sending timestamp %d, seq %d, fragoff %d, fraglen %d, jpegLen %d\n", m_Timestamp, m_SequenceNumber, fragmentOffset, fragmentLen, jpegLen); 106 | 107 | // append the JPEG scan data to the RTP buffer 108 | memcpy(RtpBuf + headerLen,jpeg + fragmentOffset, fragmentLen); 109 | fragmentOffset += fragmentLen; 110 | 111 | m_SequenceNumber++; // prepare the packet counter for the next packet 112 | 113 | IPADDRESS otherip; 114 | IPPORT otherport; 115 | socketpeeraddr(m_Client, &otherip, &otherport); 116 | 117 | // RTP marker bit must be set on last fragment 118 | if (m_TCPTransport) // RTP over RTSP - we send the buffer + 4 byte additional header 119 | socketsend(m_Client,RtpBuf,RtpPacketSize + 4); 120 | else // UDP - we send just the buffer by skipping the 4 byte RTP over RTSP header 121 | udpsocketsend(m_RtpSocket,&RtpBuf[4],RtpPacketSize, otherip, m_RtpClientPort); 122 | 123 | return isLastFragment ? 0 : fragmentOffset; 124 | }; 125 | 126 | void CStreamer::InitTransport(u_short aRtpPort, u_short aRtcpPort, bool TCP) 127 | { 128 | m_RtpClientPort = aRtpPort; 129 | m_RtcpClientPort = aRtcpPort; 130 | m_TCPTransport = TCP; 131 | 132 | if (!m_TCPTransport) 133 | { // allocate port pairs for RTP/RTCP ports in UDP transport mode 134 | for (u_short P = 6970; P < 0xFFFE; P += 2) 135 | { 136 | m_RtpSocket = udpsocketcreate(P); 137 | if (m_RtpSocket) 138 | { // Rtp socket was bound successfully. Lets try to bind the consecutive Rtsp socket 139 | m_RtcpSocket = udpsocketcreate(P + 1); 140 | if (m_RtcpSocket) 141 | { 142 | m_RtpServerPort = P; 143 | m_RtcpServerPort = P+1; 144 | break; 145 | } 146 | else 147 | { 148 | udpsocketclose(m_RtpSocket); 149 | udpsocketclose(m_RtcpSocket); 150 | }; 151 | } 152 | }; 153 | }; 154 | }; 155 | 156 | u_short CStreamer::GetRtpServerPort() 157 | { 158 | return m_RtpServerPort; 159 | }; 160 | 161 | u_short CStreamer::GetRtcpServerPort() 162 | { 163 | return m_RtcpServerPort; 164 | }; 165 | 166 | void CStreamer::streamFrame(unsigned const char *data, uint32_t dataLen, uint32_t curMsec) 167 | { 168 | if(m_prevMsec == 0) // first frame init our timestamp 169 | m_prevMsec = curMsec; 170 | 171 | // compute deltat (being careful to handle clock rollover with a little lie) 172 | uint32_t deltams = (curMsec >= m_prevMsec) ? curMsec - m_prevMsec : 100; 173 | m_prevMsec = curMsec; 174 | 175 | // locate quant tables if possible 176 | BufPtr qtable0, qtable1; 177 | 178 | if(!decodeJPEGfile(&data, &dataLen, &qtable0, &qtable1)) { 179 | printf("can't decode jpeg data\n"); 180 | return; 181 | } 182 | 183 | int offset = 0; 184 | do { 185 | offset = SendRtpPacket(data, dataLen, offset, qtable0, qtable1); 186 | delay(15); 187 | } while(offset != 0); 188 | 189 | // Increment ONLY after a full frame 190 | uint32_t units = 90000; // Hz per RFC 2435 191 | m_Timestamp += (units * deltams / 1000); // fixed timestamp increment for a frame rate of 25fps 192 | 193 | m_SendIdx++; 194 | if (m_SendIdx > 1) m_SendIdx = 0; 195 | }; 196 | 197 | #include 198 | 199 | // search for a particular JPEG marker, moves *start to just after that marker 200 | // This function fixes up the provided start ptr to point to the 201 | // actual JPEG stream data and returns the number of bytes skipped 202 | // APP0 e0 203 | // DQT db 204 | // DQT db 205 | // DHT c4 206 | // DHT c4 207 | // DHT c4 208 | // DHT c4 209 | // SOF0 c0 baseline (not progressive) 3 color 0x01 Y, 0x21 2h1v, 0x00 tbl0 210 | // - 0x02 Cb, 0x11 1h1v, 0x01 tbl1 - 0x03 Cr, 0x11 1h1v, 0x01 tbl1 211 | // therefore 4:2:2, with two separate quant tables (0 and 1) 212 | // SOS da 213 | // EOI d9 (no need to strip data after this RFC says client will discard) 214 | bool findJPEGheader(BufPtr *start, uint32_t *len, uint8_t marker) { 215 | // per https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format 216 | unsigned const char *bytes = *start; 217 | 218 | // kinda skanky, will break if unlucky and the headers inxlucde 0xffda 219 | // might fall off array if jpeg is invalid 220 | // FIXME - return false instead 221 | while(bytes - *start < *len) { 222 | uint8_t framing = *bytes++; // better be 0xff 223 | if(framing != 0xff) { 224 | printf("malformed jpeg, framing=%x\n", framing); 225 | return false; 226 | } 227 | uint8_t typecode = *bytes++; 228 | if(typecode == marker) { 229 | unsigned skipped = bytes - *start; 230 | //printf("found marker 0x%x, skipped %d\n", marker, skipped); 231 | 232 | *start = bytes; 233 | 234 | // shrink len for the bytes we just skipped 235 | *len -= skipped; 236 | 237 | return true; 238 | } 239 | else { 240 | // not the section we were looking for, skip the entire section 241 | switch(typecode) { 242 | case 0xd8: // start of image 243 | { 244 | break; // no data to skip 245 | } 246 | case 0xe0: // app0 247 | case 0xdb: // dqt 248 | case 0xc4: // dht 249 | case 0xc0: // sof0 250 | case 0xda: // sos 251 | { 252 | // standard format section with 2 bytes for len. skip that many bytes 253 | uint32_t len = bytes[0] * 256 + bytes[1]; 254 | //printf("skipping section 0x%x, %d bytes\n", typecode, len); 255 | bytes += len; 256 | break; 257 | } 258 | default: 259 | printf("unexpected jpeg typecode 0x%x\n", typecode); 260 | break; 261 | } 262 | } 263 | } 264 | 265 | printf("failed to find jpeg marker 0x%x", marker); 266 | return false; 267 | } 268 | 269 | // the scan data uses byte stuffing to guarantee anything that starts with 0xff 270 | // followed by something not zero, is a new section. Look for that marker and return the ptr 271 | // pointing there 272 | void skipScanBytes(BufPtr *start) { 273 | BufPtr bytes = *start; 274 | 275 | while(true) { // FIXME, check against length 276 | while(*bytes++ != 0xff); 277 | if(*bytes++ != 0) { 278 | *start = bytes - 2; // back up to the 0xff marker we just found 279 | return; 280 | } 281 | } 282 | } 283 | void nextJpegBlock(BufPtr *bytes) { 284 | uint32_t len = (*bytes)[0] * 256 + (*bytes)[1]; 285 | //printf("going to next jpeg block %d bytes\n", len); 286 | *bytes += len; 287 | } 288 | 289 | // When JPEG is stored as a file it is wrapped in a container 290 | // This function fixes up the provided start ptr to point to the 291 | // actual JPEG stream data and returns the number of bytes skipped 292 | bool decodeJPEGfile(BufPtr *start, uint32_t *len, BufPtr *qtable0, BufPtr *qtable1) { 293 | // per https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format 294 | unsigned const char *bytes = *start; 295 | 296 | if(!findJPEGheader(&bytes, len, 0xd8)) // better at least look like a jpeg file 297 | return false; // FAILED! 298 | 299 | // Look for quant tables if they are present 300 | *qtable0 = NULL; 301 | *qtable1 = NULL; 302 | BufPtr quantstart = *start; 303 | uint32_t quantlen = *len; 304 | if(!findJPEGheader(&quantstart, &quantlen, 0xdb)) { 305 | printf("error can't find quant table 0\n"); 306 | } 307 | else { 308 | // printf("found quant table %x\n", quantstart[2]); 309 | 310 | *qtable0 = quantstart + 3; // 3 bytes of header skipped 311 | nextJpegBlock(&quantstart); 312 | if(!findJPEGheader(&quantstart, &quantlen, 0xdb)) { 313 | printf("error can't find quant table 1\n"); 314 | } 315 | else { 316 | // printf("found quant table %x\n", quantstart[2]); 317 | } 318 | *qtable1 = quantstart + 3; 319 | nextJpegBlock(&quantstart); 320 | } 321 | 322 | if(!findJPEGheader(start, len, 0xda)) 323 | return false; // FAILED! 324 | 325 | // Skip the header bytes of the SOS marker FIXME why doesn't this work? 326 | uint32_t soslen = (*start)[0] * 256 + (*start)[1]; 327 | *start += soslen; 328 | *len -= soslen; 329 | 330 | // start scanning the data portion of the scan to find the end marker 331 | BufPtr endmarkerptr = *start; 332 | uint32_t endlen = *len; 333 | 334 | skipScanBytes(&endmarkerptr); 335 | if(!findJPEGheader(&endmarkerptr, &endlen, 0xd9)) 336 | return false; // FAILED! 337 | 338 | // endlen must now be the # of bytes between the start of our scan and 339 | // the end marker, tell the caller to ignore bytes afterwards 340 | *len = endmarkerptr - *start; 341 | 342 | return true; 343 | } 344 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/CStreamer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "platglue.h" 4 | 5 | typedef unsigned const char *BufPtr; 6 | 7 | class CStreamer 8 | { 9 | public: 10 | CStreamer(SOCKET aClient, u_short width, u_short height); 11 | virtual ~CStreamer(); 12 | 13 | void InitTransport(u_short aRtpPort, u_short aRtcpPort, bool TCP); 14 | u_short GetRtpServerPort(); 15 | u_short GetRtcpServerPort(); 16 | 17 | virtual void streamImage(uint32_t curMsec) = 0; // send a new image to the client 18 | protected: 19 | 20 | void streamFrame(unsigned const char *data, uint32_t dataLen, uint32_t curMsec); 21 | 22 | private: 23 | int SendRtpPacket(unsigned const char *jpeg, int jpegLen, int fragmentOffset, BufPtr quant0tbl = NULL, BufPtr quant1tbl = NULL);// returns new fragmentOffset or 0 if finished with frame 24 | 25 | UDPSOCKET m_RtpSocket; // RTP socket for streaming RTP packets to client 26 | UDPSOCKET m_RtcpSocket; // RTCP socket for sending/receiving RTCP packages 27 | 28 | uint16_t m_RtpClientPort; // RTP receiver port on client (in host byte order!) 29 | uint16_t m_RtcpClientPort; // RTCP receiver port on client (in host byte order!) 30 | IPPORT m_RtpServerPort; // RTP sender port on server 31 | IPPORT m_RtcpServerPort; // RTCP sender port on server 32 | 33 | u_short m_SequenceNumber; 34 | uint32_t m_Timestamp; 35 | int m_SendIdx; 36 | bool m_TCPTransport; 37 | SOCKET m_Client; 38 | uint32_t m_prevMsec; 39 | 40 | u_short m_width; // image data info 41 | u_short m_height; 42 | }; 43 | 44 | 45 | 46 | // When JPEG is stored as a file it is wrapped in a container 47 | // This function fixes up the provided start ptr to point to the 48 | // actual JPEG stream data and returns the number of bytes skipped 49 | // returns true if the file seems to be valid jpeg 50 | // If quant tables can be found they will be stored in qtable0/1 51 | bool decodeJPEGfile(BufPtr *start, uint32_t *len, BufPtr *qtable0, BufPtr *qtable1); 52 | bool findJPEGheader(BufPtr *start, uint32_t *len, uint8_t marker); 53 | 54 | // Given a jpeg ptr pointing to a pair of length bytes, advance the pointer to 55 | // the next 0xff marker byte 56 | void nextJpegBlock(BufPtr *start); 57 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/JPEGSamples.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifndef ARDUINO_ARCH_ESP32 4 | #define INCLUDE_SIMDATA 5 | #endif 6 | 7 | #ifdef INCLUDE_SIMDATA 8 | extern unsigned const char capture_jpg[]; 9 | extern unsigned const char octo_jpg[]; 10 | extern unsigned int octo_jpg_len, capture_jpg_len; 11 | #endif 12 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/OV2640.cpp: -------------------------------------------------------------------------------- 1 | #include "OV2640.h" 2 | 3 | #define TAG "OV2640" 4 | 5 | // definitions appropriate for the ESP32-CAM devboard (and most clones) 6 | camera_config_t esp32cam_config { 7 | 8 | .pin_pwdn = -1, 9 | .pin_reset = 15, 10 | 11 | .pin_xclk = 27, 12 | 13 | .pin_sscb_sda = 25, 14 | .pin_sscb_scl = 23, 15 | 16 | .pin_d7 = 19, 17 | .pin_d6 = 36, 18 | .pin_d5 = 18, 19 | .pin_d4 = 39, 20 | .pin_d3 = 5, 21 | .pin_d2 = 34, 22 | .pin_d1 = 35, 23 | .pin_d0 = 17, 24 | .pin_vsync = 22, 25 | .pin_href = 26, 26 | .pin_pclk = 21, 27 | .xclk_freq_hz = 20000000, 28 | .ledc_timer = LEDC_TIMER_0, 29 | .ledc_channel = LEDC_CHANNEL_0, 30 | .pixel_format = PIXFORMAT_JPEG, 31 | // .frame_size = FRAMESIZE_UXGA, // needs 234K of framebuffer space 32 | // .frame_size = FRAMESIZE_SXGA, // needs 160K for framebuffer 33 | // .frame_size = FRAMESIZE_XGA, // needs 96K or even smaller FRAMESIZE_SVGA - can work if using only 1 fb 34 | .frame_size = FRAMESIZE_SVGA, 35 | .jpeg_quality = 12, //0-63 lower numbers are higher quality 36 | .fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg 37 | }; 38 | 39 | camera_config_t esp32cam_aithinker_config { 40 | 41 | .pin_pwdn = 32, 42 | .pin_reset = -1, 43 | 44 | .pin_xclk = 0, 45 | 46 | .pin_sscb_sda = 26, 47 | .pin_sscb_scl = 27, 48 | 49 | // Note: LED GPIO is apparently 4 not sure where that goes 50 | // per https://github.com/donny681/ESP32_CAMERA_QR/blob/e4ef44549876457cd841f33a0892c82a71f35358/main/led.c 51 | .pin_d7 = 35, 52 | .pin_d6 = 34, 53 | .pin_d5 = 39, 54 | .pin_d4 = 36, 55 | .pin_d3 = 21, 56 | .pin_d2 = 19, 57 | .pin_d1 = 18, 58 | .pin_d0 = 5, 59 | .pin_vsync = 25, 60 | .pin_href = 23, 61 | .pin_pclk = 22, 62 | .xclk_freq_hz = 20000000, 63 | .ledc_timer = LEDC_TIMER_1, 64 | .ledc_channel = LEDC_CHANNEL_1, 65 | .pixel_format = PIXFORMAT_JPEG, 66 | // .frame_size = FRAMESIZE_UXGA, // needs 234K of framebuffer space 67 | // .frame_size = FRAMESIZE_SXGA, // needs 160K for framebuffer 68 | // .frame_size = FRAMESIZE_XGA, // needs 96K or even smaller FRAMESIZE_SVGA - can work if using only 1 fb 69 | .frame_size = FRAMESIZE_SVGA, 70 | .jpeg_quality = 12, //0-63 lower numbers are higher quality 71 | .fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg 72 | }; 73 | 74 | camera_config_t esp32cam_ttgo_t_config { 75 | 76 | .pin_pwdn = 26, 77 | .pin_reset = -1, 78 | 79 | .pin_xclk = 32, 80 | 81 | .pin_sscb_sda = 13, 82 | .pin_sscb_scl = 12, 83 | 84 | .pin_d7 = 39, 85 | .pin_d6 = 36, 86 | .pin_d5 = 23, 87 | .pin_d4 = 18, 88 | .pin_d3 = 15, 89 | .pin_d2 = 4, 90 | .pin_d1 = 14, 91 | .pin_d0 = 5, 92 | .pin_vsync = 27, 93 | .pin_href = 25, 94 | .pin_pclk = 19, 95 | .xclk_freq_hz = 20000000, 96 | .ledc_timer = LEDC_TIMER_0, 97 | .ledc_channel = LEDC_CHANNEL_0, 98 | .pixel_format = PIXFORMAT_JPEG, 99 | .frame_size = FRAMESIZE_SVGA, 100 | .jpeg_quality = 12, //0-63 lower numbers are higher quality 101 | .fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg 102 | }; 103 | 104 | 105 | 106 | 107 | void OV2640::run(void) 108 | { 109 | if(fb) 110 | //return the frame buffer back to the driver for reuse 111 | esp_camera_fb_return(fb); 112 | 113 | fb = esp_camera_fb_get(); 114 | } 115 | 116 | void OV2640::runIfNeeded(void) 117 | { 118 | if(!fb) 119 | run(); 120 | } 121 | 122 | int OV2640::getWidth(void) 123 | { 124 | runIfNeeded(); 125 | return fb->width; 126 | } 127 | 128 | int OV2640::getHeight(void) 129 | { 130 | runIfNeeded(); 131 | return fb->height; 132 | } 133 | 134 | size_t OV2640::getSize(void) 135 | { 136 | runIfNeeded(); 137 | return fb->len; 138 | } 139 | 140 | uint8_t *OV2640::getfb(void) 141 | { 142 | runIfNeeded(); 143 | return fb->buf; 144 | } 145 | 146 | framesize_t OV2640::getFrameSize(void) 147 | { 148 | return _cam_config.frame_size; 149 | } 150 | 151 | void OV2640::setFrameSize(framesize_t size) 152 | { 153 | _cam_config.frame_size = size; 154 | } 155 | 156 | pixformat_t OV2640::getPixelFormat(void) 157 | { 158 | return _cam_config.pixel_format; 159 | } 160 | 161 | void OV2640::setPixelFormat(pixformat_t format) 162 | { 163 | switch (format) 164 | { 165 | case PIXFORMAT_RGB565: 166 | case PIXFORMAT_YUV422: 167 | case PIXFORMAT_GRAYSCALE: 168 | case PIXFORMAT_JPEG: 169 | _cam_config.pixel_format = format; 170 | break; 171 | default: 172 | _cam_config.pixel_format = PIXFORMAT_GRAYSCALE; 173 | break; 174 | } 175 | } 176 | 177 | 178 | esp_err_t OV2640::init(camera_config_t config) 179 | { 180 | memset(&_cam_config, 0, sizeof(_cam_config)); 181 | memcpy(&_cam_config, &config, sizeof(config)); 182 | 183 | esp_err_t err = esp_camera_init(&_cam_config); 184 | if (err != ESP_OK) 185 | { 186 | printf("Camera probe failed with error 0x%x", err); 187 | return err; 188 | } 189 | // ESP_ERROR_CHECK(gpio_install_isr_service(0)); 190 | 191 | return ESP_OK; 192 | } 193 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/OV2640.h: -------------------------------------------------------------------------------- 1 | #ifndef OV2640_H_ 2 | #define OV2640_H_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include "esp_log.h" 8 | #include "esp_attr.h" 9 | #include "esp_camera.h" 10 | 11 | extern camera_config_t esp32cam_config, esp32cam_aithinker_config, esp32cam_ttgo_t_config; 12 | 13 | class OV2640 14 | { 15 | public: 16 | OV2640(){ 17 | fb = NULL; 18 | }; 19 | ~OV2640(){ 20 | }; 21 | esp_err_t init(camera_config_t config); 22 | void run(void); 23 | size_t getSize(void); 24 | uint8_t *getfb(void); 25 | int getWidth(void); 26 | int getHeight(void); 27 | framesize_t getFrameSize(void); 28 | pixformat_t getPixelFormat(void); 29 | 30 | void setFrameSize(framesize_t size); 31 | void setPixelFormat(pixformat_t format); 32 | 33 | private: 34 | void runIfNeeded(); // grab a frame if we don't already have one 35 | 36 | // camera_framesize_t _frame_size; 37 | // camera_pixelformat_t _pixel_format; 38 | camera_config_t _cam_config; 39 | 40 | camera_fb_t *fb; 41 | }; 42 | 43 | #endif //OV2640_H_ 44 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/OV2640Streamer.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "OV2640Streamer.h" 3 | #include 4 | 5 | 6 | 7 | OV2640Streamer::OV2640Streamer(SOCKET aClient, OV2640 &cam) : CStreamer(aClient, cam.getWidth(), cam.getHeight()), m_cam(cam) 8 | { 9 | printf("Created streamer width=%d, height=%d\n", cam.getWidth(), cam.getHeight()); 10 | } 11 | 12 | void OV2640Streamer::streamImage(uint32_t curMsec) 13 | { 14 | m_cam.run();// queue up a read for next time 15 | 16 | BufPtr bytes = m_cam.getfb(); 17 | streamFrame(bytes, m_cam.getSize(), curMsec); 18 | } 19 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/OV2640Streamer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CStreamer.h" 4 | #include "OV2640.h" 5 | 6 | class OV2640Streamer : public CStreamer 7 | { 8 | bool m_showBig; 9 | OV2640 &m_cam; 10 | 11 | public: 12 | OV2640Streamer(SOCKET aClient, OV2640 &cam); 13 | 14 | virtual void streamImage(uint32_t curMsec); 15 | }; 16 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/SimStreamer.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "SimStreamer.h" 3 | #include "JPEGSamples.h" 4 | 5 | 6 | #ifdef INCLUDE_SIMDATA 7 | SimStreamer::SimStreamer(SOCKET aClient, bool showBig) : CStreamer(aClient, showBig ? 800 : 640, showBig ? 600 : 480) 8 | { 9 | m_showBig = showBig; 10 | } 11 | 12 | void SimStreamer::streamImage(uint32_t curMsec) 13 | { 14 | if(m_showBig) { 15 | BufPtr bytes = capture_jpg; 16 | uint32_t len = capture_jpg_len; 17 | 18 | streamFrame(bytes, len, curMsec); 19 | } 20 | else { 21 | BufPtr bytes = octo_jpg; 22 | uint32_t len = octo_jpg_len; 23 | 24 | streamFrame(bytes, len, curMsec); 25 | } 26 | } 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/SimStreamer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "JPEGSamples.h" 4 | #include "CStreamer.h" 5 | 6 | #ifdef INCLUDE_SIMDATA 7 | class SimStreamer : public CStreamer 8 | { 9 | bool m_showBig; 10 | public: 11 | SimStreamer(SOCKET aClient, bool showBig); 12 | 13 | virtual void streamImage(uint32_t curMsec); 14 | }; 15 | #endif 16 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/platglue-esp32.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | //#include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | 17 | typedef WiFiClient *SOCKET; 18 | typedef WiFiUDP *UDPSOCKET; 19 | typedef IPAddress IPADDRESS; // On linux use uint32_t in network byte order (per getpeername) 20 | typedef uint16_t IPPORT; // on linux use network byte order 21 | 22 | #define NULLSOCKET NULL 23 | 24 | inline void closesocket(SOCKET s) { 25 | printf("closing TCP socket\n"); 26 | 27 | if(s) { 28 | s->stop(); 29 | // delete s; TDP WiFiClients are never on the heap in arduino land? 30 | } 31 | } 32 | 33 | #define getRandom() random(65536) 34 | 35 | inline void socketpeeraddr(SOCKET s, IPADDRESS *addr, IPPORT *port) { 36 | *addr = s->remoteIP(); 37 | *port = s->remotePort(); 38 | } 39 | 40 | inline void udpsocketclose(UDPSOCKET s) { 41 | printf("closing UDP socket\n"); 42 | if(s) { 43 | s->stop(); 44 | delete s; 45 | } 46 | } 47 | 48 | inline UDPSOCKET udpsocketcreate(unsigned short portNum) 49 | { 50 | UDPSOCKET s = new WiFiUDP(); 51 | 52 | if(!s->begin(portNum)) { 53 | printf("Can't bind port %d\n", portNum); 54 | delete s; 55 | return NULL; 56 | } 57 | 58 | return s; 59 | } 60 | 61 | // TCP sending 62 | inline ssize_t socketsend(SOCKET sockfd, const void *buf, size_t len) 63 | { 64 | return sockfd->write((uint8_t *) buf, len); 65 | } 66 | 67 | inline ssize_t udpsocketsend(UDPSOCKET sockfd, const void *buf, size_t len, 68 | IPADDRESS destaddr, IPPORT destport) 69 | { 70 | sockfd->beginPacket(destaddr, destport); 71 | sockfd->write((const uint8_t *) buf, len); 72 | if(!sockfd->endPacket()) 73 | printf("error sending udp packet\n"); 74 | 75 | return len; 76 | } 77 | 78 | /** 79 | Read from a socket with a timeout. 80 | 81 | Return 0=socket was closed by client, -1=timeout, >0 number of bytes read 82 | */ 83 | inline int socketread(SOCKET sock, char *buf, size_t buflen, int timeoutmsec) 84 | { 85 | if(!sock->connected()) { 86 | printf("client has closed the socket\n"); 87 | return 0; 88 | } 89 | 90 | int numAvail = sock->available(); 91 | if(numAvail == 0 && timeoutmsec != 0) { 92 | // sleep and hope for more 93 | delay(timeoutmsec); 94 | numAvail = sock->available(); 95 | } 96 | 97 | if(numAvail == 0) { 98 | // printf("timeout on read\n"); 99 | return -1; 100 | } 101 | else { 102 | // int numRead = sock->readBytesUntil('\n', buf, buflen); 103 | int numRead = sock->readBytes(buf, buflen); 104 | // printf("bytes avail %d, read %d: %s", numAvail, numRead, buf); 105 | return numRead; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/platglue-posix.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | typedef int SOCKET; 14 | typedef int UDPSOCKET; 15 | typedef uint32_t IPADDRESS; // On linux use uint32_t in network byte order (per getpeername) 16 | typedef uint16_t IPPORT; // on linux use network byte order 17 | 18 | #define NULLSOCKET 0 19 | 20 | inline void closesocket(SOCKET s) { 21 | close(s); 22 | } 23 | 24 | #define getRandom() rand() 25 | 26 | inline void socketpeeraddr(SOCKET s, IPADDRESS *addr, IPPORT *port) { 27 | 28 | sockaddr_in r; 29 | socklen_t len = sizeof(r); 30 | if(getpeername(s,(struct sockaddr*)&r,&len) < 0) { 31 | printf("getpeername failed\n"); 32 | *addr = 0; 33 | *port = 0; 34 | } 35 | else { 36 | //htons 37 | 38 | *port = r.sin_port; 39 | *addr = r.sin_addr.s_addr; 40 | } 41 | } 42 | 43 | inline void udpsocketclose(UDPSOCKET s) { 44 | close(s); 45 | } 46 | 47 | inline UDPSOCKET udpsocketcreate(unsigned short portNum) 48 | { 49 | sockaddr_in addr; 50 | 51 | addr.sin_family = AF_INET; 52 | addr.sin_addr.s_addr = INADDR_ANY; 53 | 54 | int s = socket(AF_INET, SOCK_DGRAM, 0); 55 | addr.sin_port = htons(portNum); 56 | if (bind(s,(sockaddr*)&addr,sizeof(addr)) != 0) { 57 | printf("Error, can't bind\n"); 58 | close(s); 59 | s = 0; 60 | } 61 | 62 | return s; 63 | } 64 | 65 | // TCP sending 66 | inline ssize_t socketsend(SOCKET sockfd, const void *buf, size_t len) 67 | { 68 | // printf("TCP send\n"); 69 | return send(sockfd, buf, len, 0); 70 | } 71 | 72 | inline ssize_t udpsocketsend(UDPSOCKET sockfd, const void *buf, size_t len, 73 | IPADDRESS destaddr, uint16_t destport) 74 | { 75 | sockaddr_in addr; 76 | 77 | addr.sin_family = AF_INET; 78 | addr.sin_addr.s_addr = destaddr; 79 | addr.sin_port = htons(destport); 80 | //printf("UDP send to 0x%0x:%0x\n", destaddr, destport); 81 | 82 | return sendto(sockfd, buf, len, 0, (sockaddr *) &addr, sizeof(addr)); 83 | } 84 | 85 | /** 86 | Read from a socket with a timeout. 87 | 88 | Return 0=socket was closed by client, -1=timeout, >0 number of bytes read 89 | */ 90 | inline int socketread(SOCKET sock, char *buf, size_t buflen, int timeoutmsec) 91 | { 92 | // Use a timeout on our socket read to instead serve frames 93 | struct timeval tv; 94 | tv.tv_sec = 0; 95 | tv.tv_usec = timeoutmsec * 1000; // send a new frame ever 96 | setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); 97 | 98 | int res = recv(sock,buf,buflen,0); 99 | if(res > 0) { 100 | return res; 101 | } 102 | else if(res == 0) { 103 | return 0; // client dropped connection 104 | } 105 | else { 106 | if (errno == EWOULDBLOCK || errno == EAGAIN) 107 | return -1; 108 | else 109 | return 0; // unknown error, just claim client dropped it 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/src/platglue.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef ARDUINO_ARCH_ESP32 4 | #include "platglue-esp32.h" 5 | #else 6 | #include "platglue-posix.h" 7 | #endif 8 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/test/Makefile: -------------------------------------------------------------------------------- 1 | 2 | SRCS = ../src/CRtspSession.cpp ../src/CStreamer.cpp ../src/JPEGSamples.cpp ../src/SimStreamer.cpp 3 | 4 | run: *.cpp ../src/* 5 | skill testerver 6 | g++ -o testserver -I ../src -I . *.cpp $(SRCS) 7 | ./testserver 8 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/test/README.md: -------------------------------------------------------------------------------- 1 | # Testserver 2 | 3 | This is a standalone Linux test application to allow development of this 4 | library without going through the slow process of always testing on the ESP32. 5 | Almost all of the code is the same - only platglue-posix.h differs from 6 | platglue-esp32.h (thus serving as a crude HAL). 7 | 8 | RESPTestServer.cpp also serves as a small example of how this library could 9 | be used on Poxix systems. 10 | 11 | # Usage 12 | 13 | Run "make" to build and run the server. Run "runvlc.sh" to fire up a VLC client 14 | that talks to that server. If all is working you should see a static image 15 | of my office that I captured using a ESP32-CAM. 16 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/test/RTSPTestServer.cpp: -------------------------------------------------------------------------------- 1 | #include "platglue.h" 2 | 3 | #include "SimStreamer.h" 4 | #include "CRtspSession.h" 5 | #include "JPEGSamples.h" 6 | #include 7 | #include 8 | 9 | 10 | 11 | void workerThread(SOCKET s) 12 | { 13 | SimStreamer streamer(s, true); // our streamer for UDP/TCP based RTP transport 14 | 15 | CRtspSession rtsp(s, &streamer); // our threads RTSP session and state 16 | 17 | while (!rtsp.m_stopped) 18 | { 19 | uint32_t timeout = 400; 20 | if(!rtsp.handleRequests(timeout)) { 21 | struct timeval now; 22 | gettimeofday(&now, NULL); // crufty msecish timer 23 | uint32_t msec = now.tv_sec * 1000 + now.tv_usec / 1000; 24 | rtsp.broadcastCurrentFrame(msec); 25 | } 26 | } 27 | } 28 | 29 | int main() 30 | { 31 | SOCKET MasterSocket; // our masterSocket(socket that listens for RTSP client connections) 32 | SOCKET ClientSocket; // RTSP socket to handle an client 33 | sockaddr_in ServerAddr; // server address parameters 34 | sockaddr_in ClientAddr; // address parameters of a new RTSP client 35 | socklen_t ClientAddrLen = sizeof(ClientAddr); 36 | 37 | printf("running RTSP server\n"); 38 | 39 | ServerAddr.sin_family = AF_INET; 40 | ServerAddr.sin_addr.s_addr = INADDR_ANY; 41 | ServerAddr.sin_port = htons(8554); // listen on RTSP port 8554 42 | MasterSocket = socket(AF_INET,SOCK_STREAM,0); 43 | 44 | int enable = 1; 45 | if (setsockopt(MasterSocket, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) { 46 | printf("setsockopt(SO_REUSEADDR) failed"); 47 | return 0; 48 | } 49 | 50 | // bind our master socket to the RTSP port and listen for a client connection 51 | if (bind(MasterSocket,(sockaddr*)&ServerAddr,sizeof(ServerAddr)) != 0) { 52 | printf("error can't bind port errno=%d\n", errno); 53 | 54 | return 0; 55 | } 56 | if (listen(MasterSocket,5) != 0) return 0; 57 | 58 | while (true) 59 | { // loop forever to accept client connections 60 | ClientSocket = accept(MasterSocket,(struct sockaddr*)&ClientAddr,&ClientAddrLen); 61 | printf("Client connected. Client address: %s\r\n",inet_ntoa(ClientAddr.sin_addr)); 62 | if(fork() == 0) 63 | workerThread(ClientSocket); 64 | } 65 | 66 | closesocket(MasterSocket); 67 | 68 | return 0; 69 | } 70 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/test/devvlc.sh: -------------------------------------------------------------------------------- 1 | # for testing 2 | vlc -v rtsp://192.168.86.215:8554/mjpeg/1 3 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/test/rfccode.cpp: -------------------------------------------------------------------------------- 1 | #include "platglue.h" 2 | 3 | #include "SimStreamer.h" 4 | #include "CRtspSession.h" 5 | #include "JPEGSamples.h" 6 | 7 | 8 | // From RFC2435 generates standard quantization tables 9 | 10 | /* 11 | * Table K.1 from JPEG spec. 12 | */ 13 | static const int jpeg_luma_quantizer[64] = { 14 | 16, 11, 10, 16, 24, 40, 51, 61, 15 | 12, 12, 14, 19, 26, 58, 60, 55, 16 | 14, 13, 16, 24, 40, 57, 69, 56, 17 | 14, 17, 22, 29, 51, 87, 80, 62, 18 | 18, 22, 37, 56, 68, 109, 103, 77, 19 | 24, 35, 55, 64, 81, 104, 113, 92, 20 | 49, 64, 78, 87, 103, 121, 120, 101, 21 | 72, 92, 95, 98, 112, 100, 103, 99 22 | }; 23 | 24 | /* 25 | * Table K.2 from JPEG spec. 26 | */ 27 | static const int jpeg_chroma_quantizer[64] = { 28 | 17, 18, 24, 47, 99, 99, 99, 99, 29 | 18, 21, 26, 66, 99, 99, 99, 99, 30 | 24, 26, 56, 99, 99, 99, 99, 99, 31 | 47, 66, 99, 99, 99, 99, 99, 99, 32 | 99, 99, 99, 99, 99, 99, 99, 99, 33 | 99, 99, 99, 99, 99, 99, 99, 99, 34 | 99, 99, 99, 99, 99, 99, 99, 99, 35 | 99, 99, 99, 99, 99, 99, 99, 99 36 | }; 37 | 38 | /* 39 | * Call MakeTables with the Q factor and two u_char[64] return arrays 40 | */ 41 | void 42 | MakeTables(int q, u_char *lqt, u_char *cqt) 43 | { 44 | int i; 45 | int factor = q; 46 | 47 | if (q < 1) factor = 1; 48 | if (q > 99) factor = 99; 49 | if (q < 50) 50 | q = 5000 / factor; 51 | else 52 | q = 200 - factor*2; 53 | 54 | for (i=0; i < 64; i++) { 55 | int lq = (jpeg_luma_quantizer[i] * q + 50) / 100; 56 | int cq = (jpeg_chroma_quantizer[i] * q + 50) / 100; 57 | 58 | /* Limit the quantizers to 1 <= q <= 255 */ 59 | if (lq < 1) lq = 1; 60 | else if (lq > 255) lq = 255; 61 | lqt[i] = lq; 62 | 63 | if (cq < 1) cq = 1; 64 | else if (cq > 255) cq = 255; 65 | cqt[i] = cq; 66 | } 67 | } 68 | 69 | 70 | 71 | // analyze an imge from our camera to find which quant table it is using... 72 | // Used to see if our camera is spitting out standard RTP tables (it isn't) 73 | 74 | // So we have to use Q of 255 to indicate that each frame has unique quant tables 75 | // use 0 for precision in the qant header, 64 for length 76 | void findCameraQuant() 77 | { 78 | BufPtr bytes = capture_jpg; 79 | uint32_t len = capture_jpg_len; 80 | 81 | if(!findJPEGheader(&bytes, &len, 0xdb)) { 82 | printf("error can't find quant table 0\n"); 83 | return; 84 | } 85 | else { 86 | printf("found quant table %x (len %d)\n", bytes[2], bytes[1]); 87 | } 88 | BufPtr qtable0 = bytes + 3; // 3 bytes of header skipped 89 | 90 | nextJpegBlock(&bytes); 91 | if(!findJPEGheader(&bytes, &len, 0xdb)) { 92 | printf("error can't find quant table 1\n"); 93 | return; 94 | } 95 | else { 96 | printf("found quant table %x\n", bytes[2]); 97 | } 98 | BufPtr qtable1 = bytes + 3; 99 | 100 | nextJpegBlock(&bytes); 101 | 102 | for(int q = 0; q < 128; q++) { 103 | uint8_t lqt[64], cqt[64]; 104 | MakeTables(q, lqt, cqt); 105 | 106 | if(memcmp(qtable0, lqt, sizeof(lqt)) == 0 && memcmp(qtable1, cqt, sizeof(cqt)) == 0) { 107 | printf("Found matching quant table %d\n", q); 108 | } 109 | } 110 | printf("No matching quant table found!\n"); 111 | } 112 | -------------------------------------------------------------------------------- /lib/Micro-RTSP/test/runvlc.sh: -------------------------------------------------------------------------------- 1 | # for testing 2 | vlc -v rtsp://127.0.0.1:8554/mjpeg/1 3 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp32cam] 12 | platform = https://github.com/platformio/platform-espressif32.git ;espressif32 13 | board = esp32cam 14 | framework = arduino 15 | board_build.partitions = customparts.csv 16 | ; upload_port = COM3 17 | upload_port = 192.168.0.109 18 | build_flags = 19 | ; -DBOARD_HAS_PSRAM=TRUE 20 | -mfix-esp32-psram-cache-issue 21 | lib_deps = 22 | ; Micro-RTSP 23 | ; https://github.com/beegee-tokyo/Micro-RTSP.git 24 | OneButton 25 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "main.h" 2 | 3 | /** Put your WiFi credentials into this file **/ 4 | #include "wifikeys.h" 5 | 6 | /** Camera class */ 7 | OV2640 cam; 8 | 9 | /** GPIO for OTA request button */ 10 | int otaButton = 12; 11 | /** Button class */ 12 | OneButton pushBt(otaButton, true, true); 13 | 14 | /** Function declarations */ 15 | void enableOTA(void); 16 | void resetDevice(void); 17 | 18 | /** 19 | * Called once after reboot/powerup 20 | */ 21 | void setup() 22 | { 23 | // Start the serial connection 24 | Serial.begin(115200); 25 | 26 | Serial.println("\n\n##################################"); 27 | Serial.printf("Internal Total heap %d, internal Free Heap %d\n", ESP.getHeapSize(), ESP.getFreeHeap()); 28 | Serial.printf("SPIRam Total heap %d, SPIRam Free Heap %d\n", ESP.getPsramSize(), ESP.getFreePsram()); 29 | Serial.printf("ChipRevision %d, Cpu Freq %d, SDK Version %s\n", ESP.getChipRevision(), ESP.getCpuFreqMHz(), ESP.getSdkVersion()); 30 | Serial.printf("Flash Size %d, Flash Speed %d\n", ESP.getFlashChipSize(), ESP.getFlashChipSpeed()); 31 | Serial.println("##################################\n\n"); 32 | 33 | // Initialize the ESP32 CAM, here we use the AIthinker ESP32 CAM 34 | delay(100); 35 | cam.init(esp32cam_aithinker_config); 36 | delay(100); 37 | 38 | // Connect the WiFi 39 | WiFi.mode(WIFI_STA); 40 | WiFi.begin(ssid, password); 41 | while (WiFi.status() != WL_CONNECTED) 42 | { 43 | delay(500); 44 | Serial.print("."); 45 | } 46 | 47 | // Print information how to contact the camera server 48 | IPAddress ip = WiFi.localIP(); 49 | Serial.print("\nWiFi connected with IP "); 50 | Serial.println(ip); 51 | #ifdef ENABLE_RTSPSERVER 52 | Serial.print("Stream Link: rtsp://"); 53 | Serial.print(ip); 54 | Serial.println(":8554/mjpeg/1\n"); 55 | #endif 56 | #ifdef ENABLE_WEBSERVER 57 | Serial.print("Browser Stream Link: http://"); 58 | Serial.print(ip); 59 | Serial.println("\n"); 60 | Serial.print("Browser Single Picture Link: http//"); 61 | Serial.print(ip); 62 | Serial.println("/jpg\n"); 63 | #endif 64 | #ifdef ENABLE_WEBSERVER 65 | // Initialize the HTTP web stream server 66 | initWebStream(); 67 | #endif 68 | 69 | #ifdef ENABLE_RTSPSERVER 70 | // Initialize the RTSP stream server 71 | initRTSP(); 72 | #endif 73 | 74 | // Attach the button functions 75 | pushBt.attachClick(enableOTA); 76 | pushBt.attachDoubleClick(resetDevice); 77 | } 78 | 79 | void loop() 80 | { 81 | // Check the button 82 | pushBt.tick(); 83 | 84 | if (otaStarted) 85 | { 86 | ArduinoOTA.handle(); 87 | } 88 | //Nothing else to do here 89 | delay(100); 90 | } 91 | 92 | /** 93 | * Handle button single click 94 | */ 95 | void enableOTA(void) 96 | { 97 | // If OTA is not enabled 98 | if (!otaStarted) 99 | { 100 | // Stop the camera servers 101 | #ifdef ENABLE_WEBSERVER 102 | stopWebStream(); 103 | #endif 104 | #ifdef ENABLE_RTSPSERVER 105 | stopRTSP(); 106 | #endif 107 | delay(100); 108 | Serial.println("OTA enabled"); 109 | // Start the OTA server 110 | startOTA(); 111 | otaStarted = true; 112 | } 113 | else 114 | { 115 | // If OTA was enabled 116 | otaStarted = false; 117 | // Stop the OTA server 118 | stopOTA(); 119 | // Restart the camera servers 120 | #ifdef ENABLE_WEBSERVER 121 | initWebStream(); 122 | #endif 123 | #ifdef ENABLE_RTSPSERVER 124 | initRTSP(); 125 | #endif 126 | } 127 | } 128 | 129 | /** 130 | * Handle button double click 131 | */ 132 | void resetDevice(void) 133 | { 134 | delay(100); 135 | WiFi.disconnect(); 136 | esp_restart(); 137 | } -------------------------------------------------------------------------------- /src/main.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // WiFi stuff 4 | #include 5 | #include 6 | #include 7 | 8 | // OTA stuff 9 | #include 10 | #include 11 | #include 12 | 13 | // Camera stuff 14 | #include "OV2640.h" 15 | #include "OV2640Streamer.h" 16 | #include "CRtspSession.h" 17 | 18 | // Button stuff 19 | #include 20 | 21 | // Select which of the servers are active 22 | // Select only one or the streaming will be very slow! 23 | // #define ENABLE_WEBSERVER 24 | #define ENABLE_RTSPSERVER 25 | 26 | // Camera class 27 | extern OV2640 cam; 28 | 29 | // RTSP stuff 30 | void initRTSP(void); 31 | void stopRTSP(void); 32 | 33 | // Web server stuff 34 | void initWebStream(void); 35 | void stopWebStream(void); 36 | void handleWebServer(void); 37 | 38 | // OTA stuff 39 | void startOTA(void); 40 | void stopOTA(void); 41 | extern boolean otaStarted; -------------------------------------------------------------------------------- /src/ota.cpp: -------------------------------------------------------------------------------- 1 | #include "main.h" 2 | 3 | /** Flag if OTA is enabled */ 4 | boolean otaStarted = false; 5 | 6 | /** Limit the progress output on serial */ 7 | unsigned int lastProgress = 0; 8 | 9 | /** 10 | * Initialize OTA server 11 | * and start waiting for OTA requests 12 | */ 13 | void startOTA(void) 14 | { 15 | ArduinoOTA 16 | // OTA request received 17 | .onStart([]() { 18 | String type; 19 | if (ArduinoOTA.getCommand() == U_FLASH) 20 | type = "sketch"; 21 | else // U_SPIFFS 22 | type = "filesystem"; 23 | 24 | // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() 25 | Serial.println("Start updating " + type); 26 | lastProgress = 0; 27 | otaStarted = true; 28 | }) 29 | .onEnd([]() { 30 | // OTA is finished 31 | Serial.println("\nEnd"); 32 | }) 33 | .onProgress([](unsigned int progress, unsigned int total) { 34 | // Status report during OTA 35 | if ((lastProgress == 0) || ((progress / (total / 100)) >= lastProgress + 5)) 36 | { 37 | Serial.printf("Progress: %u%%\r", (progress / (total / 100))); 38 | lastProgress = (progress / (total / 100)); 39 | if (lastProgress == 0) 40 | { 41 | lastProgress = 1; 42 | } 43 | } 44 | }) 45 | .onError([](ota_error_t error) { 46 | // Error occured during OTA, report it 47 | Serial.printf("Error[%u]: ", error); 48 | if (error == OTA_AUTH_ERROR) 49 | Serial.println("Auth Failed"); 50 | else if (error == OTA_BEGIN_ERROR) 51 | Serial.println("Begin Failed"); 52 | else if (error == OTA_CONNECT_ERROR) 53 | Serial.println("Connect Failed"); 54 | else if (error == OTA_RECEIVE_ERROR) 55 | Serial.println("Receive Failed"); 56 | else if (error == OTA_END_ERROR) 57 | Serial.println("End Failed"); 58 | }); 59 | 60 | // Enable MDNS so device can be seen 61 | ArduinoOTA.setMdnsEnabled(true); 62 | 63 | // Create a unique name 64 | // IPAddress ip = WiFi.localIP(); 65 | // String hostName = "ESP32-CAM" + ip.toString(); 66 | char hostName[] = "ESP32-CAM"; 67 | Serial.printf("Device is advertising as ESP32-CAM\n"); 68 | // Set the MDNS advertising name 69 | ArduinoOTA.setHostname(hostName); 70 | // Start the OTA server 71 | ArduinoOTA.begin(); 72 | } 73 | 74 | /** 75 | * Stop the OTA server 76 | */ 77 | void stopOTA(void) 78 | { 79 | ArduinoOTA.end(); 80 | } -------------------------------------------------------------------------------- /src/rtsp.cpp: -------------------------------------------------------------------------------- 1 | #include "main.h" 2 | #ifdef ENABLE_RTSPSERVER 3 | 4 | // Use this URL to connect the RTSP stream, replace the IP address with the address of your device 5 | // rtsp://192.168.0.109:8554/mjpeg/1 6 | 7 | /** Forward dedclaration of the task handling RTSP */ 8 | void rtspTask(void *pvParameters); 9 | 10 | /** Task handle of the RTSP task */ 11 | TaskHandle_t rtspTaskHandler; 12 | 13 | /** WiFi server for RTSP */ 14 | WiFiServer rtspServer(8554); 15 | 16 | /** Stream for the camera video */ 17 | CStreamer *streamer = NULL; 18 | /** Session to handle the RTSP communication */ 19 | CRtspSession *session = NULL; 20 | /** Client to handle the RTSP connection */ 21 | WiFiClient rtspClient; 22 | /** Flag from main loop to stop the RTSP server */ 23 | boolean stopRTSPtask = false; 24 | 25 | /** 26 | * Starts the task that handles RTSP streaming 27 | */ 28 | void initRTSP(void) 29 | { 30 | // Create the task for the RTSP server 31 | xTaskCreate(rtspTask, "RTSP", 4096, NULL, 1, &rtspTaskHandler); 32 | 33 | // Check the results 34 | if (rtspTaskHandler == NULL) 35 | { 36 | Serial.println("Create RTSP task failed"); 37 | } 38 | else 39 | { 40 | Serial.println("RTSP task up and running"); 41 | } 42 | } 43 | 44 | /** 45 | * Called to stop the RTSP task, needed for OTA 46 | * to avoid OTA timeout error 47 | */ 48 | void stopRTSP(void) 49 | { 50 | stopRTSPtask = true; 51 | } 52 | 53 | /** 54 | * The task that handles RTSP connections 55 | * Starts the RTSP server 56 | * Handles requests in an endless loop 57 | * until a stop request is received because OTA 58 | * starts 59 | */ 60 | void rtspTask(void *pvParameters) 61 | { 62 | uint32_t msecPerFrame = 50; 63 | static uint32_t lastimage = millis(); 64 | 65 | // rtspServer.setNoDelay(true); 66 | rtspServer.setTimeout(1); 67 | rtspServer.begin(); 68 | 69 | while (1) 70 | { 71 | // If we have an active client connection, just service that until gone 72 | if (session) 73 | { 74 | session->handleRequests(0); // we don't use a timeout here, 75 | // instead we send only if we have new enough frames 76 | 77 | uint32_t now = millis(); 78 | if (now > lastimage + msecPerFrame || now < lastimage) 79 | { // handle clock rollover 80 | session->broadcastCurrentFrame(now); 81 | lastimage = now; 82 | } 83 | 84 | // Handle disconnection from RTSP client 85 | if (session->m_stopped) 86 | { 87 | Serial.println("RTSP client closed connection"); 88 | delete session; 89 | delete streamer; 90 | session = NULL; 91 | streamer = NULL; 92 | } 93 | } 94 | else 95 | { 96 | rtspClient = rtspServer.accept(); 97 | // Handle connection request from RTSP client 98 | if (rtspClient) 99 | { 100 | Serial.println("RTSP client started connection"); 101 | streamer = new OV2640Streamer(&rtspClient, cam); // our streamer for UDP/TCP based RTP transport 102 | 103 | session = new CRtspSession(&rtspClient, streamer); // our threads RTSP session and state 104 | delay(100); 105 | } 106 | } 107 | if (stopRTSPtask) 108 | { 109 | // User requested RTSP server stop 110 | if (rtspClient) 111 | { 112 | Serial.println("Shut down RTSP server because OTA starts"); 113 | delete session; 114 | delete streamer; 115 | session = NULL; 116 | streamer = NULL; 117 | } 118 | // Delete this task 119 | vTaskDelete(NULL); 120 | } 121 | delay(10); 122 | } 123 | } 124 | #endif 125 | -------------------------------------------------------------------------------- /src/src.ino: -------------------------------------------------------------------------------- 1 | // Only here so we can use ArduinoIDE -------------------------------------------------------------------------------- /src/webstream.cpp: -------------------------------------------------------------------------------- 1 | #include "main.h" 2 | 3 | /** Web server class */ 4 | WebServer server(80); 5 | 6 | /** Forward dedclaration of the task handling browser requests */ 7 | void webTask(void *pvParameters); 8 | 9 | /** Task handle of the web task */ 10 | TaskHandle_t webTaskHandler; 11 | 12 | /** Flag to stop the web server */ 13 | boolean stopWeb = false; 14 | 15 | /** Web request function forward declarations */ 16 | void handle_jpg_stream(void); 17 | void handle_jpg(void); 18 | void handleNotFound(); 19 | 20 | /** 21 | * Initialize the web stream server by starting the handler task 22 | */ 23 | void initWebStream(void) 24 | { 25 | #ifdef ENABLE_WEBSERVER 26 | // Create the task for the web server 27 | xTaskCreate(webTask, "WEB", 4096, NULL, 1, &webTaskHandler); 28 | 29 | if (webTaskHandler == NULL) 30 | { 31 | Serial.println("Create Webstream task failed"); 32 | } 33 | else 34 | { 35 | Serial.println("Webstream task up and running"); 36 | } 37 | #endif 38 | } 39 | 40 | /** 41 | * Called to stop the web server task, needed for OTA 42 | * to avoid OTA timeout error 43 | */ 44 | void stopWebStream(void) 45 | { 46 | stopWeb = true; 47 | } 48 | 49 | /** 50 | * The task that handles web server connections 51 | * Starts the web server 52 | * Handles requests in an endless loop 53 | * until a stop request is received because OTA 54 | * starts 55 | */ 56 | void webTask(void *pvParameters) 57 | { 58 | // Set the function to handle stream requests 59 | server.on("/", HTTP_GET, handle_jpg_stream); 60 | // Set the function to handle single picture requests 61 | server.on("/jpg", HTTP_GET, handle_jpg); 62 | // Set the function to handle other requests 63 | server.onNotFound(handleNotFound); 64 | // Start the web server 65 | server.begin(); 66 | 67 | while (1) 68 | { 69 | #ifdef ENABLE_WEBSERVER 70 | // Check if the server has clients 71 | server.handleClient(); 72 | #endif 73 | } 74 | if (stopWeb) 75 | { 76 | // User requested web server stop 77 | server.close(); 78 | // Delete this task 79 | vTaskDelete(NULL); 80 | } 81 | delay(100); 82 | } 83 | 84 | #ifdef ENABLE_WEBSERVER 85 | /** 86 | * Handle web stream requests 87 | * Gives a first response to prepare the streaming 88 | * Then runs in a loop to update the web content 89 | * every time a new frame is available 90 | */ 91 | void handle_jpg_stream(void) 92 | { 93 | WiFiClient thisClient = server.client(); 94 | String response = "HTTP/1.1 200 OK\r\n"; 95 | response += "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n"; 96 | server.sendContent(response); 97 | 98 | while (1) 99 | { 100 | cam.run(); 101 | if (!thisClient.connected()) 102 | { 103 | break; 104 | } 105 | response = "--frame\r\n"; 106 | response += "Content-Type: image/jpeg\r\n\r\n"; 107 | server.sendContent(response); 108 | 109 | thisClient.write((char *)cam.getfb(), cam.getSize()); 110 | server.sendContent("\r\n"); 111 | delay(150); 112 | } 113 | } 114 | 115 | /** 116 | * Handle single picture requests 117 | * Gets the latest picture from the camera 118 | * and sends it to the web client 119 | */ 120 | void handle_jpg(void) 121 | { 122 | WiFiClient thisClient = server.client(); 123 | 124 | cam.run(); 125 | if (!thisClient.connected()) 126 | { 127 | return; 128 | } 129 | String response = "HTTP/1.1 200 OK\r\n"; 130 | response += "Content-disposition: inline; filename=capture.jpg\r\n"; 131 | response += "Content-type: image/jpeg\r\n\r\n"; 132 | server.sendContent(response); 133 | thisClient.write((char *)cam.getfb(), cam.getSize()); 134 | } 135 | 136 | /** 137 | * Handle any other request from the web client 138 | */ 139 | void handleNotFound() 140 | { 141 | IPAddress ip = WiFi.localIP(); 142 | String message = "Stream Link: rtsp://"; 143 | message += ip.toString(); 144 | message += ":8554/mjpeg/1\n"; 145 | message += "Browser Stream Link: http://"; 146 | message += ip.toString(); 147 | message += "\n"; 148 | message += "Browser Single Picture Link: http//"; 149 | message += ip.toString(); 150 | message += "/jpg\n"; 151 | message += "\n"; 152 | server.send(200, "text/plain", message); 153 | } 154 | #endif 155 | -------------------------------------------------------------------------------- /src/wifikeys.h: -------------------------------------------------------------------------------- 1 | #include 2 | // Change YOUR_AP_NAME and YOUR_AP_PASSWORD to your WiFi credentials 3 | const char *ssid = "YOUR_AP_NAME"; // Put your SSID here 4 | const char *password = "YOUR_AP_PASSWORD"; // Put your PASSWORD here --------------------------------------------------------------------------------