From d9d7b361c63233f2904a1e0bb1be2c36f2722243 Mon Sep 17 00:00:00 2001 From: Toby Vincent Date: Sun, 7 Jan 2024 14:54:30 -0600 Subject: Initial commit --- .gitignore | 6 + LICENSE.txt | 27 ++++ README.md | 42 +++++ compile_commands.json | 1 + extra_script.py | 16 ++ platformio.ini | 18 +++ src/OV2640.cpp | 193 +++++++++++++++++++++++ src/OV2640.h | 43 +++++ src/camera_pins.h | 99 ++++++++++++ src/main.cpp | 423 ++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 868 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 120000 compile_commands.json create mode 100644 extra_script.py create mode 100644 platformio.ini create mode 100644 src/OV2640.cpp create mode 100644 src/OV2640.h create mode 100644 src/camera_pins.h create mode 100644 src/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9cef18 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.pio +.clang_complete +.gcc-flags.json +.ccls +.cache +.env diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..2a87d2b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2015-2020, Anatoli Arkhipenko. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..850b941 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# ESP32 MJPEG Multiclient Streaming Server + +This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM or ESP-EYE modules. + +This is tested to work with **VLC** and **Blynk** video widget. + + + +**This version uses FreeRTOS tasks to enable streaming to up to 10 connected clients** + + + +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/) + +Full story: https://www.hackster.io/anatoli-arkhipenko/multi-client-mjpeg-streaming-from-esp32-47768f + +------ + +##### Other repositories that may be of interest + +###### ESP32 MJPEG streaming server servicing a single client: + +https://github.com/arkhipenko/esp32-cam-mjpeg + + + +###### ESP32 MJPEG streaming server servicing multiple clients (FreeRTOS based): + +https://github.com/arkhipenko/esp32-cam-mjpeg-multiclient + + + +###### ESP32 MJPEG streaming server servicing multiple clients (FreeRTOS based) with the latest camera drivers from espressif. + +https://github.com/arkhipenko/esp32-mjpeg-multiclient-espcam-drivers + + + +###### Cooperative multitasking library: + +https://github.com/arkhipenko/TaskScheduler + diff --git a/compile_commands.json b/compile_commands.json new file mode 120000 index 0000000..18959ab --- /dev/null +++ b/compile_commands.json @@ -0,0 +1 @@ +.pio/build/esp32cam/compile_commands.json \ No newline at end of file diff --git a/extra_script.py b/extra_script.py new file mode 100644 index 0000000..6fcc5ec --- /dev/null +++ b/extra_script.py @@ -0,0 +1,16 @@ +import os + +Import("env") + +with open(".env", "r") as f: + envs = [] + for line in f.readlines(): + k, v = line.strip().split("=") + envs.append('-D{}=\\"{}\\"'.format(k, v)) + env.Append(BUILD_FLAGS=envs) + +# include toolchain paths +env.Replace(COMPILATIONDB_INCLUDE_TOOLCHAIN=True) + +# override compilation DB path +env.Replace(COMPILATIONDB_PATH=os.path.join("$BUILD_DIR", "compile_commands.json")) diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..23e577c --- /dev/null +++ b/platformio.ini @@ -0,0 +1,18 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32cam] +platform = espressif32 +board = esp32cam +framework = arduino +extra_scripts = pre:extra_script.py +monitor_speed = 115200 +upload_speed = 921600 +lib_deps = espressif/esp32-camera@^2.0.4 diff --git a/src/OV2640.cpp b/src/OV2640.cpp new file mode 100644 index 0000000..02d04d5 --- /dev/null +++ b/src/OV2640.cpp @@ -0,0 +1,193 @@ +#include "OV2640.h" + +#define TAG "OV2640" + +// definitions appropriate for the ESP32-CAM devboard (and most clones) +camera_config_t esp32cam_config{ + + .pin_pwdn = -1, // FIXME: on the TTGO T-Journal I think this is GPIO 0 + .pin_reset = 15, + + .pin_xclk = 27, + + .pin_sscb_sda = 25, + .pin_sscb_scl = 23, + + .pin_d7 = 19, + .pin_d6 = 36, + .pin_d5 = 18, + .pin_d4 = 39, + .pin_d3 = 5, + .pin_d2 = 34, + .pin_d1 = 35, + .pin_d0 = 17, + .pin_vsync = 22, + .pin_href = 26, + .pin_pclk = 21, + .xclk_freq_hz = 20000000, + .ledc_timer = LEDC_TIMER_0, + .ledc_channel = LEDC_CHANNEL_0, + .pixel_format = PIXFORMAT_JPEG, + // .frame_size = FRAMESIZE_UXGA, // needs 234K of framebuffer space + // .frame_size = FRAMESIZE_SXGA, // needs 160K for framebuffer + // .frame_size = FRAMESIZE_XGA, // needs 96K or even smaller FRAMESIZE_SVGA - can work if using only 1 fb + .frame_size = FRAMESIZE_SVGA, + .jpeg_quality = 12, //0-63 lower numbers are higher quality + .fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg +}; + +camera_config_t esp32cam_aithinker_config{ + + .pin_pwdn = 32, + .pin_reset = -1, + + .pin_xclk = 0, + + .pin_sscb_sda = 26, + .pin_sscb_scl = 27, + + // Note: LED GPIO is apparently 4 not sure where that goes + // per https://github.com/donny681/ESP32_CAMERA_QR/blob/e4ef44549876457cd841f33a0892c82a71f35358/main/led.c + .pin_d7 = 35, + .pin_d6 = 34, + .pin_d5 = 39, + .pin_d4 = 36, + .pin_d3 = 21, + .pin_d2 = 19, + .pin_d1 = 18, + .pin_d0 = 5, + .pin_vsync = 25, + .pin_href = 23, + .pin_pclk = 22, + .xclk_freq_hz = 20000000, + .ledc_timer = LEDC_TIMER_1, + .ledc_channel = LEDC_CHANNEL_1, + .pixel_format = PIXFORMAT_JPEG, + // .frame_size = FRAMESIZE_UXGA, // needs 234K of framebuffer space + // .frame_size = FRAMESIZE_SXGA, // needs 160K for framebuffer + // .frame_size = FRAMESIZE_XGA, // needs 96K or even smaller FRAMESIZE_SVGA - can work if using only 1 fb + .frame_size = FRAMESIZE_SVGA, + .jpeg_quality = 12, //0-63 lower numbers are higher quality + .fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg +}; + +camera_config_t esp32cam_ttgo_t_config{ + + .pin_pwdn = 26, + .pin_reset = -1, + + .pin_xclk = 32, + + .pin_sscb_sda = 13, + .pin_sscb_scl = 12, + + .pin_d7 = 39, + .pin_d6 = 36, + .pin_d5 = 23, + .pin_d4 = 18, + .pin_d3 = 15, + .pin_d2 = 4, + .pin_d1 = 14, + .pin_d0 = 5, + .pin_vsync = 27, + .pin_href = 25, + .pin_pclk = 19, + .xclk_freq_hz = 20000000, + .ledc_timer = LEDC_TIMER_0, + .ledc_channel = LEDC_CHANNEL_0, + .pixel_format = PIXFORMAT_JPEG, + .frame_size = FRAMESIZE_SVGA, + .jpeg_quality = 12, //0-63 lower numbers are higher quality + .fb_count = 2 // if more than one i2s runs in continous mode. Use only with jpeg +}; + +void OV2640::run(void) +{ + if (fb) + //return the frame buffer back to the driver for reuse + esp_camera_fb_return(fb); + + fb = esp_camera_fb_get(); +} + +void OV2640::runIfNeeded(void) +{ + if (!fb) + run(); +} + +int OV2640::getWidth(void) +{ + runIfNeeded(); + return fb->width; +} + +int OV2640::getHeight(void) +{ + runIfNeeded(); + return fb->height; +} + +size_t OV2640::getSize(void) +{ + runIfNeeded(); + if (!fb) + return 0; // FIXME - this shouldn't be possible but apparently the new cam board returns null sometimes? + return fb->len; +} + +uint8_t *OV2640::getfb(void) +{ + runIfNeeded(); + if (!fb) + return NULL; // FIXME - this shouldn't be possible but apparently the new cam board returns null sometimes? + + return fb->buf; +} + +framesize_t OV2640::getFrameSize(void) +{ + return _cam_config.frame_size; +} + +void OV2640::setFrameSize(framesize_t size) +{ + _cam_config.frame_size = size; +} + +pixformat_t OV2640::getPixelFormat(void) +{ + return _cam_config.pixel_format; +} + +void OV2640::setPixelFormat(pixformat_t format) +{ + switch (format) + { + case PIXFORMAT_RGB565: + case PIXFORMAT_YUV422: + case PIXFORMAT_GRAYSCALE: + case PIXFORMAT_JPEG: + _cam_config.pixel_format = format; + break; + default: + _cam_config.pixel_format = PIXFORMAT_GRAYSCALE; + break; + } +} + +esp_err_t OV2640::init(camera_config_t config) +{ + memset(&_cam_config, 0, sizeof(_cam_config)); + memcpy(&_cam_config, &config, sizeof(config)); + + esp_err_t err = esp_camera_init(&_cam_config); + if (err != ESP_OK) + { + printf("Camera probe failed with error 0x%x", err); + return err; + } + // ESP_ERROR_CHECK(gpio_install_isr_service(0)); + + return ESP_OK; +} diff --git a/src/OV2640.h b/src/OV2640.h new file mode 100644 index 0000000..b9b5706 --- /dev/null +++ b/src/OV2640.h @@ -0,0 +1,43 @@ +#ifndef OV2640_H_ +#define OV2640_H_ + +#include +#include +#include +#include "esp_log.h" +#include "esp_attr.h" +#include "esp_camera.h" + +extern camera_config_t esp32cam_config, esp32cam_aithinker_config, esp32cam_ttgo_t_config; + +class OV2640 +{ +public: + OV2640(){ + fb = NULL; + }; + ~OV2640(){ + }; + esp_err_t init(camera_config_t config); + void run(void); + size_t getSize(void); + uint8_t *getfb(void); + int getWidth(void); + int getHeight(void); + framesize_t getFrameSize(void); + pixformat_t getPixelFormat(void); + + void setFrameSize(framesize_t size); + void setPixelFormat(pixformat_t format); + +private: + void runIfNeeded(); // grab a frame if we don't already have one + + // camera_framesize_t _frame_size; + // camera_pixelformat_t _pixel_format; + camera_config_t _cam_config; + + camera_fb_t *fb; +}; + +#endif //OV2640_H_ diff --git a/src/camera_pins.h b/src/camera_pins.h new file mode 100644 index 0000000..7855722 --- /dev/null +++ b/src/camera_pins.h @@ -0,0 +1,99 @@ + +#if defined(CAMERA_MODEL_WROVER_KIT) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 21 +#define SIOD_GPIO_NUM 26 +#define SIOC_GPIO_NUM 27 + +#define Y9_GPIO_NUM 35 +#define Y8_GPIO_NUM 34 +#define Y7_GPIO_NUM 39 +#define Y6_GPIO_NUM 36 +#define Y5_GPIO_NUM 19 +#define Y4_GPIO_NUM 18 +#define Y3_GPIO_NUM 5 +#define Y2_GPIO_NUM 4 +#define VSYNC_GPIO_NUM 25 +#define HREF_GPIO_NUM 23 +#define PCLK_GPIO_NUM 22 + +#elif defined(CAMERA_MODEL_ESP_EYE) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 4 +#define SIOD_GPIO_NUM 18 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 36 +#define Y8_GPIO_NUM 37 +#define Y7_GPIO_NUM 38 +#define Y6_GPIO_NUM 39 +#define Y5_GPIO_NUM 35 +#define Y4_GPIO_NUM 14 +#define Y3_GPIO_NUM 13 +#define Y2_GPIO_NUM 34 +#define VSYNC_GPIO_NUM 5 +#define HREF_GPIO_NUM 27 +#define PCLK_GPIO_NUM 25 + +#elif defined(CAMERA_MODEL_M5STACK_PSRAM) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM 15 +#define XCLK_GPIO_NUM 27 +#define SIOD_GPIO_NUM 25 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 19 +#define Y8_GPIO_NUM 36 +#define Y7_GPIO_NUM 18 +#define Y6_GPIO_NUM 39 +#define Y5_GPIO_NUM 5 +#define Y4_GPIO_NUM 34 +#define Y3_GPIO_NUM 35 +#define Y2_GPIO_NUM 32 +#define VSYNC_GPIO_NUM 22 +#define HREF_GPIO_NUM 26 +#define PCLK_GPIO_NUM 21 + +#elif defined(CAMERA_MODEL_M5STACK_WIDE) +#define PWDN_GPIO_NUM -1 +#define RESET_GPIO_NUM 15 +#define XCLK_GPIO_NUM 27 +#define SIOD_GPIO_NUM 22 +#define SIOC_GPIO_NUM 23 + +#define Y9_GPIO_NUM 19 +#define Y8_GPIO_NUM 36 +#define Y7_GPIO_NUM 18 +#define Y6_GPIO_NUM 39 +#define Y5_GPIO_NUM 5 +#define Y4_GPIO_NUM 34 +#define Y3_GPIO_NUM 35 +#define Y2_GPIO_NUM 32 +#define VSYNC_GPIO_NUM 25 +#define HREF_GPIO_NUM 26 +#define PCLK_GPIO_NUM 21 + +#elif defined(CAMERA_MODEL_AI_THINKER) +#define PWDN_GPIO_NUM 32 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 0 +#define SIOD_GPIO_NUM 26 +#define SIOC_GPIO_NUM 27 + +#define Y9_GPIO_NUM 35 +#define Y8_GPIO_NUM 34 +#define Y7_GPIO_NUM 39 +#define Y6_GPIO_NUM 36 +#define Y5_GPIO_NUM 21 +#define Y4_GPIO_NUM 19 +#define Y3_GPIO_NUM 18 +#define Y2_GPIO_NUM 5 +#define VSYNC_GPIO_NUM 25 +#define HREF_GPIO_NUM 23 +#define PCLK_GPIO_NUM 22 + +#else +#error "Camera model not selected" +#endif diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..e15bffa --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,423 @@ +#define APP_CPU 1 +#define PRO_CPU 0 + +#include +#include +#include +#include + +#include +#include +#include +#include + +#define PWDN_GPIO_NUM 32 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 0 +#define SIOD_GPIO_NUM 26 +#define SIOC_GPIO_NUM 27 + +#define Y9_GPIO_NUM 35 +#define Y8_GPIO_NUM 34 +#define Y7_GPIO_NUM 39 +#define Y6_GPIO_NUM 36 +#define Y5_GPIO_NUM 21 +#define Y4_GPIO_NUM 19 +#define Y3_GPIO_NUM 18 +#define Y2_GPIO_NUM 5 +#define VSYNC_GPIO_NUM 25 +#define HREF_GPIO_NUM 23 +#define PCLK_GPIO_NUM 22 + +OV2640 cam; + +WebServer server(80); + +// ===== rtos task handles ========================= +// Streaming is implemented with 3 tasks: +TaskHandle_t tMjpeg; // handles client connections to the webserver +TaskHandle_t tCam; // handles getting picture frames from the camera and storing + // them locally +TaskHandle_t tStream; // actually streaming frames to all connected clients + +// frameSync semaphore is used to prevent streaming buffer as it is replaced +// with the next frame +SemaphoreHandle_t frameSync = NULL; + +// Queue stores currently connected clients to whom we are streaming +QueueHandle_t streamingClients; + +// We will try to achieve 25 FPS frame rate +const int FPS = 14; + +// We will handle web client requests every 50 ms (20 Hz) +const int WSINTERVAL = 100; + +// Commonly used variables: +volatile size_t camSize; // size of the current frame, byte +volatile char *camBuf; // pointer to the current frame + +// ==== Memory allocator that takes advantage of PSRAM if present +// ======================= +char *allocateMemory(char *aPtr, size_t aSize) { + + // Since current buffer is too smal, free it + if (aPtr != NULL) + free(aPtr); + + size_t freeHeap = ESP.getFreeHeap(); + char *ptr = NULL; + + // If memory requested is more than 2/3 of the currently free heap, try PSRAM + // immediately + if (aSize > freeHeap * 2 / 3) { + if (psramFound() && ESP.getFreePsram() > aSize) { + ptr = (char *)ps_malloc(aSize); + } + } else { + // Enough free heap - let's try allocating fast RAM as a buffer + ptr = (char *)malloc(aSize); + + // If allocation on the heap failed, let's give PSRAM one more chance: + if (ptr == NULL && psramFound() && ESP.getFreePsram() > aSize) { + ptr = (char *)ps_malloc(aSize); + } + } + + // Finally, if the memory pointer is NULL, we were not able to allocate any + // memory, and that is a terminal condition. + if (ptr == NULL) { + ESP.restart(); + } + return ptr; +} + +// ==== RTOS task to grab frames from the camera ========================= +void camCB(void *pvParameters) { + + TickType_t xLastWakeTime; + + // A running interval associated with currently desired frame rate + const TickType_t xFrequency = pdMS_TO_TICKS(1000 / FPS); + + // Mutex for the critical section of swithing the active frames around + portMUX_TYPE xSemaphore = portMUX_INITIALIZER_UNLOCKED; + + // Pointers to the 2 frames, their respective sizes and index of the current + // frame + char *fbs[2] = {NULL, NULL}; + size_t fSize[2] = {0, 0}; + int ifb = 0; + + //=== loop() section =================== + xLastWakeTime = xTaskGetTickCount(); + + for (;;) { + + // Grab a frame from the camera and query its size + cam.run(); + size_t s = cam.getSize(); + + // If frame size is more that we have previously allocated - request 125% + // of the current frame space + if (s > fSize[ifb]) { + fSize[ifb] = s * 4 / 3; + fbs[ifb] = allocateMemory(fbs[ifb], fSize[ifb]); + } + + // Copy current frame into local buffer + char *b = (char *)cam.getfb(); + memcpy(fbs[ifb], b, s); + + // Let other tasks run and wait until the end of the current frame rate + // interval (if any time left) + taskYIELD(); + vTaskDelayUntil(&xLastWakeTime, xFrequency); + + // Only switch frames around if no frame is currently being streamed to a + // client Wait on a semaphore until client operation completes + xSemaphoreTake(frameSync, portMAX_DELAY); + + // Do not allow interrupts while switching the current frame + portENTER_CRITICAL(&xSemaphore); + camBuf = fbs[ifb]; + camSize = s; + ifb++; + ifb &= 1; // this should produce 1, 0, 1, 0, 1 ... sequence + portEXIT_CRITICAL(&xSemaphore); + + // Let anyone waiting for a frame know that the frame is ready + xSemaphoreGive(frameSync); + + // Technically only needed once: let the streaming task know that we have + // at least one frame and it could start sending frames to the clients, if + // any + xTaskNotifyGive(tStream); + + // Immediately let other (streaming) tasks run + taskYIELD(); + + // If streaming task has suspended itself (no active clients to stream to) + // there is no need to grab frames from the camera. We can save some juice + // by suspedning the tasks + if (eTaskGetState(tStream) == eSuspended) { + vTaskSuspend(NULL); // passing NULL means "suspend yourself" + } + } +} + +// ==== STREAMING ====================================================== +const char HEADER[] = "HTTP/1.1 200 OK\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Content-Type: multipart/x-mixed-replace; " + "boundary=123456789000000000000987654321\r\n"; +const char BOUNDARY[] = "\r\n--123456789000000000000987654321\r\n"; +const char CTNTTYPE[] = "Content-Type: image/jpeg\r\nContent-Length: "; +const int hdrLen = strlen(HEADER); +const int bdrLen = strlen(BOUNDARY); +const int cntLen = strlen(CTNTTYPE); + +// ==== Handle connection request from clients =============================== +void handleJPGSstream(void) { + // Can only acommodate 10 clients. The limit is a default for WiFi + // connections + if (!uxQueueSpacesAvailable(streamingClients)) + return; + + // Create a new WiFi Client object to keep track of this one + WiFiClient *client = new WiFiClient(); + *client = server.client(); + + // Immediately send this client a header + client->write(HEADER, hdrLen); + client->write(BOUNDARY, bdrLen); + + // Push the client to the streaming queue + xQueueSend(streamingClients, (void *)&client, 0); + + // Wake up streaming tasks, if they were previously suspended: + if (eTaskGetState(tCam) == eSuspended) + vTaskResume(tCam); + if (eTaskGetState(tStream) == eSuspended) + vTaskResume(tStream); +} + +// ==== Actually stream content to all connected clients +// ======================== +void streamCB(void *pvParameters) { + char buf[16]; + TickType_t xLastWakeTime; + TickType_t xFrequency; + + // Wait until the first frame is captured and there is something to send + // to clients + ulTaskNotifyTake(pdTRUE, /* Clear the notification value before exiting. */ + portMAX_DELAY); /* Block indefinitely. */ + + xLastWakeTime = xTaskGetTickCount(); + for (;;) { + // Default assumption we are running according to the FPS + xFrequency = pdMS_TO_TICKS(1000 / FPS); + + // Only bother to send anything if there is someone watching + UBaseType_t activeClients = uxQueueMessagesWaiting(streamingClients); + if (activeClients) { + // Adjust the period to the number of connected clients + xFrequency /= activeClients; + + // Since we are sending the same frame to everyone, + // pop a client from the the front of the queue + WiFiClient *client; + xQueueReceive(streamingClients, (void *)&client, 0); + + // Check if this client is still connected. + + if (!client->connected()) { + // delete this client reference if s/he has disconnected + // and don't put it back on the queue anymore. Bye! + delete client; + } else { + + // Ok. This is an actively connected client. + // Let's grab a semaphore to prevent frame changes while we + // are serving this frame + xSemaphoreTake(frameSync, portMAX_DELAY); + + client->write(CTNTTYPE, cntLen); + sprintf(buf, "%d\r\n\r\n", camSize); + client->write(buf, strlen(buf)); + client->write((char *)camBuf, (size_t)camSize); + client->write(BOUNDARY, bdrLen); + + // Since this client is still connected, push it to the end + // of the queue for further processing + xQueueSend(streamingClients, (void *)&client, 0); + + // The frame has been served. Release the semaphore and let other tasks + // run. If there is a frame switch ready, it will happen now in between + // frames + xSemaphoreGive(frameSync); + taskYIELD(); + } + } else { + // Since there are no connected clients, there is no reason to waste + // battery running + vTaskSuspend(NULL); + } + // Let other tasks run after serving every client + taskYIELD(); + vTaskDelayUntil(&xLastWakeTime, xFrequency); + } +} + +const char JHEADER[] = "HTTP/1.1 200 OK\r\n" + "Content-disposition: inline; filename=capture.jpg\r\n" + "Content-type: image/jpeg\r\n\r\n"; +const int jhdLen = strlen(JHEADER); + +// ==== Serve up one JPEG frame ============================================= +void handleJPG(void) { + WiFiClient client = server.client(); + + if (!client.connected()) + return; + cam.run(); + client.write(JHEADER, jhdLen); + client.write((char *)cam.getfb(), cam.getSize()); +} + +// ==== Handle invalid URL requests ============================================ +void handleNotFound() { + String message = "Server is running!\n\n"; + message += "URI: "; + message += server.uri(); + message += "\nMethod: "; + message += (server.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server.args(); + message += "\n"; + server.send(200, "text / plain", message); +} + +// ======== Server Connection Handler Task ========================== +void mjpegCB(void *pvParameters) { + TickType_t xLastWakeTime; + const TickType_t xFrequency = pdMS_TO_TICKS(WSINTERVAL); + + // Creating frame synchronization semaphore and initializing it + frameSync = xSemaphoreCreateBinary(); + xSemaphoreGive(frameSync); + + // Creating a queue to track all connected clients + streamingClients = xQueueCreate(10, sizeof(WiFiClient *)); + + //=== setup section ================== + + // Creating RTOS task for grabbing frames from the camera + xTaskCreatePinnedToCore(camCB, // callback + "cam", // name + 4096, // stacj size + NULL, // parameters + 2, // priority + &tCam, // RTOS task handle + APP_CPU); // core + + // Creating task to push the stream to all connected clients + xTaskCreatePinnedToCore(streamCB, "strmCB", 4 * 1024, + NULL, //(void*) handler, + 2, &tStream, APP_CPU); + + // Registering webserver handling routines + server.on("/mjpeg/1", HTTP_GET, handleJPGSstream); + server.on("/jpg", HTTP_GET, handleJPG); + server.onNotFound(handleNotFound); + + // Starting webserver + server.begin(); + + //=== loop() section =================== + xLastWakeTime = xTaskGetTickCount(); + for (;;) { + server.handleClient(); + + // After every server client handling request, we let other tasks run and + // then pause + taskYIELD(); + vTaskDelayUntil(&xLastWakeTime, xFrequency); + } +} + +// ==== SETUP method +// ================================================================== +void setup() { + + // Setup Serial connection: + Serial.begin(115200); + delay(1000); // wait for a second to let Serial connect + + // Configure the camera + camera_config_t config; + config.ledc_channel = LEDC_CHANNEL_0; + config.ledc_timer = LEDC_TIMER_0; + config.pin_d0 = Y2_GPIO_NUM; + config.pin_d1 = Y3_GPIO_NUM; + config.pin_d2 = Y4_GPIO_NUM; + config.pin_d3 = Y5_GPIO_NUM; + config.pin_d4 = Y6_GPIO_NUM; + config.pin_d5 = Y7_GPIO_NUM; + config.pin_d6 = Y8_GPIO_NUM; + config.pin_d7 = Y9_GPIO_NUM; + config.pin_xclk = XCLK_GPIO_NUM; + config.pin_pclk = PCLK_GPIO_NUM; + config.pin_vsync = VSYNC_GPIO_NUM; + config.pin_href = HREF_GPIO_NUM; + config.pin_sscb_sda = SIOD_GPIO_NUM; + config.pin_sscb_scl = SIOC_GPIO_NUM; + config.pin_pwdn = PWDN_GPIO_NUM; + config.pin_reset = RESET_GPIO_NUM; + config.xclk_freq_hz = 20000000; + config.pixel_format = PIXFORMAT_JPEG; + + // Frame parameters: pick one + // config.frame_size = FRAMESIZE_UXGA; + // config.frame_size = FRAMESIZE_SVGA; + // config.frame_size = FRAMESIZE_QVGA; + config.frame_size = FRAMESIZE_VGA; + config.jpeg_quality = 12; + config.fb_count = 2; + +#if defined(CAMERA_MODEL_ESP_EYE) + pinMode(13, INPUT_PULLUP); + pinMode(14, INPUT_PULLUP); +#endif + + if (cam.init(config) != ESP_OK) { + Serial.println("Error initializing the camera"); + delay(10000); + ESP.restart(); + } + + // Configure and connect to WiFi + IPAddress ip; + + WiFi.mode(WIFI_STA); + WiFi.begin(WIFI_SSID, WIFI_PASS); + + Serial.print("Connecting to WiFi"); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print(F(".")); + } + ip = WiFi.localIP(); + Serial.println(F("WiFi connected")); + Serial.println(""); + Serial.print("Stream Link: http://"); + Serial.print(ip); + Serial.println("/mjpeg/1"); + + // Start mainstreaming RTOS task + xTaskCreatePinnedToCore(mjpegCB, "mjpeg", 4 * 1024, NULL, 2, &tMjpeg, + APP_CPU); +} + +void loop() { vTaskDelay(1000); } -- cgit v1.2.3-70-g09d2