├── LICENSE.txt ├── README.md ├── camera_pins.h ├── esp32_camera_mjpeg.ino └── src ├── OV2640.cpp └── OV2640.h /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2020, Anatoli Arkhipenko. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 21 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 22 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 23 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 24 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 25 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32 MJPEG Streaming Server 2 | 3 | This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM and ESP-EYE modules. 4 | 5 | This is tested to work with **VLC** and **Blynk** video widget. 6 | 7 | 8 | 9 | Inspired by and based on this Instructable: [$9 RTSP Video Streamer Using the ESP32-CAM Board](https://www.instructables.com/id/9-RTSP-Video-Streamer-Using-the-ESP32-CAM-Board/) 10 | 11 | Full story: https://www.hackster.io/anatoli-arkhipenko/multi-client-mjpeg-streaming-from-esp32-47768f 12 | 13 | 14 | ------ 15 | 16 | ##### Other repositories that may be of interest 17 | 18 | ###### ESP32 MJPEG streaming server servicing a single client: 19 | 20 | https://github.com/arkhipenko/esp32-cam-mjpeg 21 | 22 | 23 | 24 | ###### ESP32 MJPEG streaming server servicing multiple clients (FreeRTOS based): 25 | 26 | https://github.com/arkhipenko/esp32-cam-mjpeg-multiclient 27 | 28 | 29 | 30 | ###### ESP32 MJPEG streaming server servicing multiple clients (FreeRTOS based) with the latest camera drivers from espressif. 31 | 32 | https://github.com/arkhipenko/esp32-mjpeg-multiclient-espcam-drivers 33 | 34 | 35 | 36 | ###### Cooperative multitasking library: 37 | 38 | https://github.com/arkhipenko/TaskScheduler 39 | 40 | -------------------------------------------------------------------------------- /camera_pins.h: -------------------------------------------------------------------------------- 1 | 2 | #if defined(CAMERA_MODEL_WROVER_KIT) 3 | #define PWDN_GPIO_NUM -1 4 | #define RESET_GPIO_NUM -1 5 | #define XCLK_GPIO_NUM 21 6 | #define SIOD_GPIO_NUM 26 7 | #define SIOC_GPIO_NUM 27 8 | 9 | #define Y9_GPIO_NUM 35 10 | #define Y8_GPIO_NUM 34 11 | #define Y7_GPIO_NUM 39 12 | #define Y6_GPIO_NUM 36 13 | #define Y5_GPIO_NUM 19 14 | #define Y4_GPIO_NUM 18 15 | #define Y3_GPIO_NUM 5 16 | #define Y2_GPIO_NUM 4 17 | #define VSYNC_GPIO_NUM 25 18 | #define HREF_GPIO_NUM 23 19 | #define PCLK_GPIO_NUM 22 20 | 21 | #elif defined(CAMERA_MODEL_ESP_EYE) 22 | #define PWDN_GPIO_NUM -1 23 | #define RESET_GPIO_NUM -1 24 | #define XCLK_GPIO_NUM 4 25 | #define SIOD_GPIO_NUM 18 26 | #define SIOC_GPIO_NUM 23 27 | 28 | #define Y9_GPIO_NUM 36 29 | #define Y8_GPIO_NUM 37 30 | #define Y7_GPIO_NUM 38 31 | #define Y6_GPIO_NUM 39 32 | #define Y5_GPIO_NUM 35 33 | #define Y4_GPIO_NUM 14 34 | #define Y3_GPIO_NUM 13 35 | #define Y2_GPIO_NUM 34 36 | #define VSYNC_GPIO_NUM 5 37 | #define HREF_GPIO_NUM 27 38 | #define PCLK_GPIO_NUM 25 39 | 40 | #elif defined(CAMERA_MODEL_M5STACK_PSRAM) 41 | #define PWDN_GPIO_NUM -1 42 | #define RESET_GPIO_NUM 15 43 | #define XCLK_GPIO_NUM 27 44 | #define SIOD_GPIO_NUM 25 45 | #define SIOC_GPIO_NUM 23 46 | 47 | #define Y9_GPIO_NUM 19 48 | #define Y8_GPIO_NUM 36 49 | #define Y7_GPIO_NUM 18 50 | #define Y6_GPIO_NUM 39 51 | #define Y5_GPIO_NUM 5 52 | #define Y4_GPIO_NUM 34 53 | #define Y3_GPIO_NUM 35 54 | #define Y2_GPIO_NUM 32 55 | #define VSYNC_GPIO_NUM 22 56 | #define HREF_GPIO_NUM 26 57 | #define PCLK_GPIO_NUM 21 58 | 59 | #elif defined(CAMERA_MODEL_M5STACK_WIDE) 60 | #define PWDN_GPIO_NUM -1 61 | #define RESET_GPIO_NUM 15 62 | #define XCLK_GPIO_NUM 27 63 | #define SIOD_GPIO_NUM 22 64 | #define SIOC_GPIO_NUM 23 65 | 66 | #define Y9_GPIO_NUM 19 67 | #define Y8_GPIO_NUM 36 68 | #define Y7_GPIO_NUM 18 69 | #define Y6_GPIO_NUM 39 70 | #define Y5_GPIO_NUM 5 71 | #define Y4_GPIO_NUM 34 72 | #define Y3_GPIO_NUM 35 73 | #define Y2_GPIO_NUM 32 74 | #define VSYNC_GPIO_NUM 25 75 | #define HREF_GPIO_NUM 26 76 | #define PCLK_GPIO_NUM 21 77 | 78 | #elif defined(CAMERA_MODEL_AI_THINKER) 79 | #define PWDN_GPIO_NUM 32 80 | #define RESET_GPIO_NUM -1 81 | #define XCLK_GPIO_NUM 0 82 | #define SIOD_GPIO_NUM 26 83 | #define SIOC_GPIO_NUM 27 84 | 85 | #define Y9_GPIO_NUM 35 86 | #define Y8_GPIO_NUM 34 87 | #define Y7_GPIO_NUM 39 88 | #define Y6_GPIO_NUM 36 89 | #define Y5_GPIO_NUM 21 90 | #define Y4_GPIO_NUM 19 91 | #define Y3_GPIO_NUM 18 92 | #define Y2_GPIO_NUM 5 93 | #define VSYNC_GPIO_NUM 25 94 | #define HREF_GPIO_NUM 23 95 | #define PCLK_GPIO_NUM 22 96 | 97 | #else 98 | #error "Camera model not selected" 99 | #endif 100 | -------------------------------------------------------------------------------- /esp32_camera_mjpeg.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM and 4 | ESP32-EYE modules. 5 | This is tested to work with VLC and Blynk video widget. 6 | 7 | Inspired by and based on this Instructable: $9 RTSP Video Streamer Using the ESP32-CAM Board 8 | (https://www.instructables.com/id/9-RTSP-Video-Streamer-Using-the-ESP32-CAM-Board/) 9 | 10 | Board: AI-Thinker ESP32-CAM 11 | 12 | */ 13 | 14 | #include "src/OV2640.h" 15 | #include 16 | #include 17 | #include 18 | 19 | // Select camera model 20 | //#define CAMERA_MODEL_WROVER_KIT 21 | #define CAMERA_MODEL_ESP_EYE 22 | //#define CAMERA_MODEL_M5STACK_PSRAM 23 | //#define CAMERA_MODEL_M5STACK_WIDE 24 | //#define CAMERA_MODEL_AI_THINKER 25 | 26 | #include "camera_pins.h" 27 | 28 | /* 29 | Next one is an include with wifi credentials. 30 | This is what you need to do: 31 | 32 | 1. Create a file called "home_wifi_multi.h" in the same folder OR under a separate subfolder of the "libraries" folder of Arduino IDE. (You are creating a "fake" library really - I called it "MySettings"). 33 | 2. Place the following text in the file: 34 | #define SSID1 "replace with your wifi ssid" 35 | #define PWD1 "replace your wifi password" 36 | 3. Save. 37 | 38 | Should work then 39 | */ 40 | #include "home_wifi_multi.h" 41 | 42 | OV2640 cam; 43 | 44 | WebServer server(80); 45 | 46 | const char HEADER[] = "HTTP/1.1 200 OK\r\n" \ 47 | "Access-Control-Allow-Origin: *\r\n" \ 48 | "Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n"; 49 | const char BOUNDARY[] = "\r\n--123456789000000000000987654321\r\n"; 50 | const char CTNTTYPE[] = "Content-Type: image/jpeg\r\nContent-Length: "; 51 | const int hdrLen = strlen(HEADER); 52 | const int bdrLen = strlen(BOUNDARY); 53 | const int cntLen = strlen(CTNTTYPE); 54 | 55 | void handle_jpg_stream(void) 56 | { 57 | char buf[32]; 58 | int s; 59 | 60 | WiFiClient client = server.client(); 61 | 62 | client.write(HEADER, hdrLen); 63 | client.write(BOUNDARY, bdrLen); 64 | 65 | while (true) 66 | { 67 | if (!client.connected()) break; 68 | cam.run(); 69 | s = cam.getSize(); 70 | client.write(CTNTTYPE, cntLen); 71 | sprintf( buf, "%d\r\n\r\n", s ); 72 | client.write(buf, strlen(buf)); 73 | client.write((char *)cam.getfb(), s); 74 | client.write(BOUNDARY, bdrLen); 75 | } 76 | } 77 | 78 | const char JHEADER[] = "HTTP/1.1 200 OK\r\n" \ 79 | "Content-disposition: inline; filename=capture.jpg\r\n" \ 80 | "Content-type: image/jpeg\r\n\r\n"; 81 | const int jhdLen = strlen(JHEADER); 82 | 83 | void handle_jpg(void) 84 | { 85 | WiFiClient client = server.client(); 86 | 87 | cam.run(); 88 | if (!client.connected()) return; 89 | 90 | client.write(JHEADER, jhdLen); 91 | client.write((char *)cam.getfb(), cam.getSize()); 92 | } 93 | 94 | void handleNotFound() 95 | { 96 | String message = "Server is running!\n\n"; 97 | message += "URI: "; 98 | message += server.uri(); 99 | message += "\nMethod: "; 100 | message += (server.method() == HTTP_GET) ? "GET" : "POST"; 101 | message += "\nArguments: "; 102 | message += server.args(); 103 | message += "\n"; 104 | server.send(200, "text / plain", message); 105 | } 106 | 107 | void setup() 108 | { 109 | 110 | Serial.begin(115200); 111 | //while (!Serial); //wait for serial connection. 112 | 113 | camera_config_t config; 114 | config.ledc_channel = LEDC_CHANNEL_0; 115 | config.ledc_timer = LEDC_TIMER_0; 116 | config.pin_d0 = Y2_GPIO_NUM; 117 | config.pin_d1 = Y3_GPIO_NUM; 118 | config.pin_d2 = Y4_GPIO_NUM; 119 | config.pin_d3 = Y5_GPIO_NUM; 120 | config.pin_d4 = Y6_GPIO_NUM; 121 | config.pin_d5 = Y7_GPIO_NUM; 122 | config.pin_d6 = Y8_GPIO_NUM; 123 | config.pin_d7 = Y9_GPIO_NUM; 124 | config.pin_xclk = XCLK_GPIO_NUM; 125 | config.pin_pclk = PCLK_GPIO_NUM; 126 | config.pin_vsync = VSYNC_GPIO_NUM; 127 | config.pin_href = HREF_GPIO_NUM; 128 | config.pin_sscb_sda = SIOD_GPIO_NUM; 129 | config.pin_sscb_scl = SIOC_GPIO_NUM; 130 | config.pin_pwdn = PWDN_GPIO_NUM; 131 | config.pin_reset = RESET_GPIO_NUM; 132 | config.xclk_freq_hz = 20000000; 133 | config.pixel_format = PIXFORMAT_JPEG; 134 | 135 | // Frame parameters 136 | // config.frame_size = FRAMESIZE_UXGA; 137 | config.frame_size = FRAMESIZE_QVGA; 138 | config.jpeg_quality = 12; 139 | config.fb_count = 2; 140 | 141 | #if defined(CAMERA_MODEL_ESP_EYE) 142 | pinMode(13, INPUT_PULLUP); 143 | pinMode(14, INPUT_PULLUP); 144 | #endif 145 | 146 | cam.init(config); 147 | 148 | IPAddress ip; 149 | 150 | WiFi.mode(WIFI_STA); 151 | WiFi.begin(SSID1, PWD1); 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 | Serial.print("Stream Link: http://"); 162 | Serial.print(ip); 163 | Serial.println("/mjpeg/1"); 164 | server.on("/mjpeg/1", HTTP_GET, handle_jpg_stream); 165 | server.on("/jpg", HTTP_GET, handle_jpg); 166 | server.onNotFound(handleNotFound); 167 | server.begin(); 168 | } 169 | 170 | void loop() 171 | { 172 | server.handleClient(); 173 | } 174 | -------------------------------------------------------------------------------- /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, // FIXME: on the TTGO T-Journal I think this is GPIO 0 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 | void OV2640::run(void) 105 | { 106 | if (fb) 107 | //return the frame buffer back to the driver for reuse 108 | esp_camera_fb_return(fb); 109 | 110 | fb = esp_camera_fb_get(); 111 | } 112 | 113 | void OV2640::runIfNeeded(void) 114 | { 115 | if (!fb) 116 | run(); 117 | } 118 | 119 | int OV2640::getWidth(void) 120 | { 121 | runIfNeeded(); 122 | return fb->width; 123 | } 124 | 125 | int OV2640::getHeight(void) 126 | { 127 | runIfNeeded(); 128 | return fb->height; 129 | } 130 | 131 | size_t OV2640::getSize(void) 132 | { 133 | runIfNeeded(); 134 | if (!fb) 135 | return 0; // FIXME - this shouldn't be possible but apparently the new cam board returns null sometimes? 136 | return fb->len; 137 | } 138 | 139 | uint8_t *OV2640::getfb(void) 140 | { 141 | runIfNeeded(); 142 | if (!fb) 143 | return NULL; // FIXME - this shouldn't be possible but apparently the new cam board returns null sometimes? 144 | 145 | return fb->buf; 146 | } 147 | 148 | framesize_t OV2640::getFrameSize(void) 149 | { 150 | return _cam_config.frame_size; 151 | } 152 | 153 | void OV2640::setFrameSize(framesize_t size) 154 | { 155 | _cam_config.frame_size = size; 156 | } 157 | 158 | pixformat_t OV2640::getPixelFormat(void) 159 | { 160 | return _cam_config.pixel_format; 161 | } 162 | 163 | void OV2640::setPixelFormat(pixformat_t format) 164 | { 165 | switch (format) 166 | { 167 | case PIXFORMAT_RGB565: 168 | case PIXFORMAT_YUV422: 169 | case PIXFORMAT_GRAYSCALE: 170 | case PIXFORMAT_JPEG: 171 | _cam_config.pixel_format = format; 172 | break; 173 | default: 174 | _cam_config.pixel_format = PIXFORMAT_GRAYSCALE; 175 | break; 176 | } 177 | } 178 | 179 | esp_err_t OV2640::init(camera_config_t config) 180 | { 181 | memset(&_cam_config, 0, sizeof(_cam_config)); 182 | memcpy(&_cam_config, &config, sizeof(config)); 183 | 184 | esp_err_t err = esp_camera_init(&_cam_config); 185 | if (err != ESP_OK) 186 | { 187 | printf("Camera probe failed with error 0x%x", err); 188 | return err; 189 | } 190 | // ESP_ERROR_CHECK(gpio_install_isr_service(0)); 191 | 192 | return ESP_OK; 193 | } 194 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------