├── src ├── OV2640.h └── OV2640.cpp ├── README.md ├── LICENSE.txt ├── camera_pins.h └── esp32_camera_mjpeg_multiclient.ino /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32 MJPEG Multiclient Streaming Server 2 | 3 | This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM or ESP-EYE modules. 4 | 5 | This is tested to work with **VLC** and **Blynk** video widget. 6 | 7 | 8 | 9 | **This version uses FreeRTOS tasks to enable streaming to up to 10 connected clients** 10 | 11 | 12 | 13 | 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/) 14 | 15 | Full story: https://www.hackster.io/anatoli-arkhipenko/multi-client-mjpeg-streaming-from-esp32-47768f 16 | 17 | ------ 18 | 19 | ##### Other repositories that may be of interest 20 | 21 | ###### ESP32 MJPEG streaming server servicing a single client: 22 | 23 | https://github.com/arkhipenko/esp32-cam-mjpeg 24 | 25 | 26 | 27 | ###### ESP32 MJPEG streaming server servicing multiple clients (FreeRTOS based): 28 | 29 | https://github.com/arkhipenko/esp32-cam-mjpeg-multiclient 30 | 31 | 32 | 33 | ###### ESP32 MJPEG streaming server servicing multiple clients (FreeRTOS based) with the latest camera drivers from espressif. 34 | 35 | https://github.com/arkhipenko/esp32-mjpeg-multiclient-espcam-drivers 36 | 37 | 38 | 39 | ###### Cooperative multitasking library: 40 | 41 | https://github.com/arkhipenko/TaskScheduler 42 | 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /esp32_camera_mjpeg_multiclient.ino: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM 4 | and ESP-EYE modules. 5 | This is tested to work with VLC and Blynk video widget and can support up to 10 6 | simultaneously connected streaming clients. 7 | Simultaneous streaming is implemented with FreeRTOS tasks. 8 | 9 | Inspired by and based on this Instructable: $9 RTSP Video Streamer Using the ESP32-CAM Board 10 | (https://www.instructables.com/id/9-RTSP-Video-Streamer-Using-the-ESP32-CAM-Board/) 11 | 12 | Board: AI-Thinker ESP32-CAM or ESP-EYE 13 | Compile as: 14 | ESP32 Dev Module 15 | CPU Freq: 240 16 | Flash Freq: 80 17 | Flash mode: QIO 18 | Flash Size: 4Mb 19 | Patrition: Minimal SPIFFS 20 | PSRAM: Enabled 21 | */ 22 | 23 | // ESP32 has two cores: APPlication core and PROcess core (the one that runs ESP32 SDK stack) 24 | #define APP_CPU 1 25 | #define PRO_CPU 0 26 | 27 | #include "src/OV2640.h" 28 | #include 29 | #include 30 | #include 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | // Select camera model 38 | //#define CAMERA_MODEL_WROVER_KIT 39 | #define CAMERA_MODEL_ESP_EYE 40 | //#define CAMERA_MODEL_M5STACK_PSRAM 41 | //#define CAMERA_MODEL_M5STACK_WIDE 42 | //#define CAMERA_MODEL_AI_THINKER 43 | 44 | #include "camera_pins.h" 45 | 46 | /* 47 | Next one is an include with wifi credentials. 48 | This is what you need to do: 49 | 50 | 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"). 51 | 2. Place the following text in the file: 52 | #define SSID1 "replace with your wifi ssid" 53 | #define PWD1 "replace your wifi password" 54 | 3. Save. 55 | 56 | Should work then 57 | */ 58 | #include "home_wifi_multi.h" 59 | 60 | OV2640 cam; 61 | 62 | WebServer server(80); 63 | 64 | // ===== rtos task handles ========================= 65 | // Streaming is implemented with 3 tasks: 66 | TaskHandle_t tMjpeg; // handles client connections to the webserver 67 | TaskHandle_t tCam; // handles getting picture frames from the camera and storing them locally 68 | TaskHandle_t tStream; // actually streaming frames to all connected clients 69 | 70 | // frameSync semaphore is used to prevent streaming buffer as it is replaced with the next frame 71 | SemaphoreHandle_t frameSync = NULL; 72 | 73 | // Queue stores currently connected clients to whom we are streaming 74 | QueueHandle_t streamingClients; 75 | 76 | // We will try to achieve 25 FPS frame rate 77 | const int FPS = 14; 78 | 79 | // We will handle web client requests every 50 ms (20 Hz) 80 | const int WSINTERVAL = 100; 81 | 82 | 83 | // ======== Server Connection Handler Task ========================== 84 | void mjpegCB(void* pvParameters) { 85 | TickType_t xLastWakeTime; 86 | const TickType_t xFrequency = pdMS_TO_TICKS(WSINTERVAL); 87 | 88 | // Creating frame synchronization semaphore and initializing it 89 | frameSync = xSemaphoreCreateBinary(); 90 | xSemaphoreGive( frameSync ); 91 | 92 | // Creating a queue to track all connected clients 93 | streamingClients = xQueueCreate( 10, sizeof(WiFiClient*) ); 94 | 95 | //=== setup section ================== 96 | 97 | // Creating RTOS task for grabbing frames from the camera 98 | xTaskCreatePinnedToCore( 99 | camCB, // callback 100 | "cam", // name 101 | 4096, // stacj size 102 | NULL, // parameters 103 | 2, // priority 104 | &tCam, // RTOS task handle 105 | APP_CPU); // core 106 | 107 | // Creating task to push the stream to all connected clients 108 | xTaskCreatePinnedToCore( 109 | streamCB, 110 | "strmCB", 111 | 4 * 1024, 112 | NULL, //(void*) handler, 113 | 2, 114 | &tStream, 115 | APP_CPU); 116 | 117 | // Registering webserver handling routines 118 | server.on("/mjpeg/1", HTTP_GET, handleJPGSstream); 119 | server.on("/jpg", HTTP_GET, handleJPG); 120 | server.onNotFound(handleNotFound); 121 | 122 | // Starting webserver 123 | server.begin(); 124 | 125 | //=== loop() section =================== 126 | xLastWakeTime = xTaskGetTickCount(); 127 | for (;;) { 128 | server.handleClient(); 129 | 130 | // After every server client handling request, we let other tasks run and then pause 131 | taskYIELD(); 132 | vTaskDelayUntil(&xLastWakeTime, xFrequency); 133 | } 134 | } 135 | 136 | 137 | // Commonly used variables: 138 | volatile size_t camSize; // size of the current frame, byte 139 | volatile char* camBuf; // pointer to the current frame 140 | 141 | 142 | // ==== RTOS task to grab frames from the camera ========================= 143 | void camCB(void* pvParameters) { 144 | 145 | TickType_t xLastWakeTime; 146 | 147 | // A running interval associated with currently desired frame rate 148 | const TickType_t xFrequency = pdMS_TO_TICKS(1000 / FPS); 149 | 150 | // Mutex for the critical section of swithing the active frames around 151 | portMUX_TYPE xSemaphore = portMUX_INITIALIZER_UNLOCKED; 152 | 153 | // Pointers to the 2 frames, their respective sizes and index of the current frame 154 | char* fbs[2] = { NULL, NULL }; 155 | size_t fSize[2] = { 0, 0 }; 156 | int ifb = 0; 157 | 158 | //=== loop() section =================== 159 | xLastWakeTime = xTaskGetTickCount(); 160 | 161 | for (;;) { 162 | 163 | // Grab a frame from the camera and query its size 164 | cam.run(); 165 | size_t s = cam.getSize(); 166 | 167 | // If frame size is more that we have previously allocated - request 125% of the current frame space 168 | if (s > fSize[ifb]) { 169 | fSize[ifb] = s * 4 / 3; 170 | fbs[ifb] = allocateMemory(fbs[ifb], fSize[ifb]); 171 | } 172 | 173 | // Copy current frame into local buffer 174 | char* b = (char*) cam.getfb(); 175 | memcpy(fbs[ifb], b, s); 176 | 177 | // Let other tasks run and wait until the end of the current frame rate interval (if any time left) 178 | taskYIELD(); 179 | vTaskDelayUntil(&xLastWakeTime, xFrequency); 180 | 181 | // Only switch frames around if no frame is currently being streamed to a client 182 | // Wait on a semaphore until client operation completes 183 | xSemaphoreTake( frameSync, portMAX_DELAY ); 184 | 185 | // Do not allow interrupts while switching the current frame 186 | portENTER_CRITICAL(&xSemaphore); 187 | camBuf = fbs[ifb]; 188 | camSize = s; 189 | ifb++; 190 | ifb &= 1; // this should produce 1, 0, 1, 0, 1 ... sequence 191 | portEXIT_CRITICAL(&xSemaphore); 192 | 193 | // Let anyone waiting for a frame know that the frame is ready 194 | xSemaphoreGive( frameSync ); 195 | 196 | // Technically only needed once: let the streaming task know that we have at least one frame 197 | // and it could start sending frames to the clients, if any 198 | xTaskNotifyGive( tStream ); 199 | 200 | // Immediately let other (streaming) tasks run 201 | taskYIELD(); 202 | 203 | // If streaming task has suspended itself (no active clients to stream to) 204 | // there is no need to grab frames from the camera. We can save some juice 205 | // by suspedning the tasks 206 | if ( eTaskGetState( tStream ) == eSuspended ) { 207 | vTaskSuspend(NULL); // passing NULL means "suspend yourself" 208 | } 209 | } 210 | } 211 | 212 | 213 | // ==== Memory allocator that takes advantage of PSRAM if present ======================= 214 | char* allocateMemory(char* aPtr, size_t aSize) { 215 | 216 | // Since current buffer is too smal, free it 217 | if (aPtr != NULL) free(aPtr); 218 | 219 | 220 | size_t freeHeap = ESP.getFreeHeap(); 221 | char* ptr = NULL; 222 | 223 | // If memory requested is more than 2/3 of the currently free heap, try PSRAM immediately 224 | if ( aSize > freeHeap * 2 / 3 ) { 225 | if ( psramFound() && ESP.getFreePsram() > aSize ) { 226 | ptr = (char*) ps_malloc(aSize); 227 | } 228 | } 229 | else { 230 | // Enough free heap - let's try allocating fast RAM as a buffer 231 | ptr = (char*) malloc(aSize); 232 | 233 | // If allocation on the heap failed, let's give PSRAM one more chance: 234 | if ( ptr == NULL && psramFound() && ESP.getFreePsram() > aSize) { 235 | ptr = (char*) ps_malloc(aSize); 236 | } 237 | } 238 | 239 | // Finally, if the memory pointer is NULL, we were not able to allocate any memory, and that is a terminal condition. 240 | if (ptr == NULL) { 241 | ESP.restart(); 242 | } 243 | return ptr; 244 | } 245 | 246 | 247 | // ==== STREAMING ====================================================== 248 | const char HEADER[] = "HTTP/1.1 200 OK\r\n" \ 249 | "Access-Control-Allow-Origin: *\r\n" \ 250 | "Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n"; 251 | const char BOUNDARY[] = "\r\n--123456789000000000000987654321\r\n"; 252 | const char CTNTTYPE[] = "Content-Type: image/jpeg\r\nContent-Length: "; 253 | const int hdrLen = strlen(HEADER); 254 | const int bdrLen = strlen(BOUNDARY); 255 | const int cntLen = strlen(CTNTTYPE); 256 | 257 | 258 | // ==== Handle connection request from clients =============================== 259 | void handleJPGSstream(void) 260 | { 261 | // Can only acommodate 10 clients. The limit is a default for WiFi connections 262 | if ( !uxQueueSpacesAvailable(streamingClients) ) return; 263 | 264 | 265 | // Create a new WiFi Client object to keep track of this one 266 | WiFiClient* client = new WiFiClient(); 267 | *client = server.client(); 268 | 269 | // Immediately send this client a header 270 | client->write(HEADER, hdrLen); 271 | client->write(BOUNDARY, bdrLen); 272 | 273 | // Push the client to the streaming queue 274 | xQueueSend(streamingClients, (void *) &client, 0); 275 | 276 | // Wake up streaming tasks, if they were previously suspended: 277 | if ( eTaskGetState( tCam ) == eSuspended ) vTaskResume( tCam ); 278 | if ( eTaskGetState( tStream ) == eSuspended ) vTaskResume( tStream ); 279 | } 280 | 281 | 282 | // ==== Actually stream content to all connected clients ======================== 283 | void streamCB(void * pvParameters) { 284 | char buf[16]; 285 | TickType_t xLastWakeTime; 286 | TickType_t xFrequency; 287 | 288 | // Wait until the first frame is captured and there is something to send 289 | // to clients 290 | ulTaskNotifyTake( pdTRUE, /* Clear the notification value before exiting. */ 291 | portMAX_DELAY ); /* Block indefinitely. */ 292 | 293 | xLastWakeTime = xTaskGetTickCount(); 294 | for (;;) { 295 | // Default assumption we are running according to the FPS 296 | xFrequency = pdMS_TO_TICKS(1000 / FPS); 297 | 298 | // Only bother to send anything if there is someone watching 299 | UBaseType_t activeClients = uxQueueMessagesWaiting(streamingClients); 300 | if ( activeClients ) { 301 | // Adjust the period to the number of connected clients 302 | xFrequency /= activeClients; 303 | 304 | // Since we are sending the same frame to everyone, 305 | // pop a client from the the front of the queue 306 | WiFiClient *client; 307 | xQueueReceive (streamingClients, (void*) &client, 0); 308 | 309 | // Check if this client is still connected. 310 | 311 | if (!client->connected()) { 312 | // delete this client reference if s/he has disconnected 313 | // and don't put it back on the queue anymore. Bye! 314 | delete client; 315 | } 316 | else { 317 | 318 | // Ok. This is an actively connected client. 319 | // Let's grab a semaphore to prevent frame changes while we 320 | // are serving this frame 321 | xSemaphoreTake( frameSync, portMAX_DELAY ); 322 | 323 | client->write(CTNTTYPE, cntLen); 324 | sprintf(buf, "%d\r\n\r\n", camSize); 325 | client->write(buf, strlen(buf)); 326 | client->write((char*) camBuf, (size_t)camSize); 327 | client->write(BOUNDARY, bdrLen); 328 | 329 | // Since this client is still connected, push it to the end 330 | // of the queue for further processing 331 | xQueueSend(streamingClients, (void *) &client, 0); 332 | 333 | // The frame has been served. Release the semaphore and let other tasks run. 334 | // If there is a frame switch ready, it will happen now in between frames 335 | xSemaphoreGive( frameSync ); 336 | taskYIELD(); 337 | } 338 | } 339 | else { 340 | // Since there are no connected clients, there is no reason to waste battery running 341 | vTaskSuspend(NULL); 342 | } 343 | // Let other tasks run after serving every client 344 | taskYIELD(); 345 | vTaskDelayUntil(&xLastWakeTime, xFrequency); 346 | } 347 | } 348 | 349 | 350 | 351 | const char JHEADER[] = "HTTP/1.1 200 OK\r\n" \ 352 | "Content-disposition: inline; filename=capture.jpg\r\n" \ 353 | "Content-type: image/jpeg\r\n\r\n"; 354 | const int jhdLen = strlen(JHEADER); 355 | 356 | // ==== Serve up one JPEG frame ============================================= 357 | void handleJPG(void) 358 | { 359 | WiFiClient client = server.client(); 360 | 361 | if (!client.connected()) return; 362 | cam.run(); 363 | client.write(JHEADER, jhdLen); 364 | client.write((char*)cam.getfb(), cam.getSize()); 365 | } 366 | 367 | 368 | // ==== Handle invalid URL requests ============================================ 369 | void handleNotFound() 370 | { 371 | String message = "Server is running!\n\n"; 372 | message += "URI: "; 373 | message += server.uri(); 374 | message += "\nMethod: "; 375 | message += (server.method() == HTTP_GET) ? "GET" : "POST"; 376 | message += "\nArguments: "; 377 | message += server.args(); 378 | message += "\n"; 379 | server.send(200, "text / plain", message); 380 | } 381 | 382 | 383 | 384 | // ==== SETUP method ================================================================== 385 | void setup() 386 | { 387 | 388 | // Setup Serial connection: 389 | Serial.begin(115200); 390 | delay(1000); // wait for a second to let Serial connect 391 | 392 | 393 | // Configure the camera 394 | camera_config_t config; 395 | config.ledc_channel = LEDC_CHANNEL_0; 396 | config.ledc_timer = LEDC_TIMER_0; 397 | config.pin_d0 = Y2_GPIO_NUM; 398 | config.pin_d1 = Y3_GPIO_NUM; 399 | config.pin_d2 = Y4_GPIO_NUM; 400 | config.pin_d3 = Y5_GPIO_NUM; 401 | config.pin_d4 = Y6_GPIO_NUM; 402 | config.pin_d5 = Y7_GPIO_NUM; 403 | config.pin_d6 = Y8_GPIO_NUM; 404 | config.pin_d7 = Y9_GPIO_NUM; 405 | config.pin_xclk = XCLK_GPIO_NUM; 406 | config.pin_pclk = PCLK_GPIO_NUM; 407 | config.pin_vsync = VSYNC_GPIO_NUM; 408 | config.pin_href = HREF_GPIO_NUM; 409 | config.pin_sscb_sda = SIOD_GPIO_NUM; 410 | config.pin_sscb_scl = SIOC_GPIO_NUM; 411 | config.pin_pwdn = PWDN_GPIO_NUM; 412 | config.pin_reset = RESET_GPIO_NUM; 413 | config.xclk_freq_hz = 20000000; 414 | config.pixel_format = PIXFORMAT_JPEG; 415 | 416 | // Frame parameters: pick one 417 | // config.frame_size = FRAMESIZE_UXGA; 418 | // config.frame_size = FRAMESIZE_SVGA; 419 | // config.frame_size = FRAMESIZE_QVGA; 420 | config.frame_size = FRAMESIZE_VGA; 421 | config.jpeg_quality = 12; 422 | config.fb_count = 2; 423 | 424 | #if defined(CAMERA_MODEL_ESP_EYE) 425 | pinMode(13, INPUT_PULLUP); 426 | pinMode(14, INPUT_PULLUP); 427 | #endif 428 | 429 | if (cam.init(config) != ESP_OK) { 430 | Serial.println("Error initializing the camera"); 431 | delay(10000); 432 | ESP.restart(); 433 | } 434 | 435 | 436 | // Configure and connect to WiFi 437 | IPAddress ip; 438 | 439 | WiFi.mode(WIFI_STA); 440 | WiFi.begin(SSID1, PWD1); 441 | Serial.print("Connecting to WiFi"); 442 | while (WiFi.status() != WL_CONNECTED) 443 | { 444 | delay(500); 445 | Serial.print(F(".")); 446 | } 447 | ip = WiFi.localIP(); 448 | Serial.println(F("WiFi connected")); 449 | Serial.println(""); 450 | Serial.print("Stream Link: http://"); 451 | Serial.print(ip); 452 | Serial.println("/mjpeg/1"); 453 | 454 | 455 | // Start mainstreaming RTOS task 456 | xTaskCreatePinnedToCore( 457 | mjpegCB, 458 | "mjpeg", 459 | 4 * 1024, 460 | NULL, 461 | 2, 462 | &tMjpeg, 463 | APP_CPU); 464 | } 465 | 466 | 467 | void loop() { 468 | vTaskDelay(1000); 469 | } 470 | --------------------------------------------------------------------------------