aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorToby Vincent <tobyv@tobyvin.dev>2024-01-07 14:54:30 -0600
committerToby Vincent <tobyv@tobyvin.dev>2024-01-07 14:54:30 -0600
commitd9d7b361c63233f2904a1e0bb1be2c36f2722243 (patch)
tree9d85aa6cb5084a92bd8776543cc957756f1d71a6
Initial commit
-rw-r--r--.gitignore6
-rw-r--r--LICENSE.txt27
-rw-r--r--README.md42
l---------compile_commands.json1
-rw-r--r--extra_script.py16
-rw-r--r--platformio.ini18
-rw-r--r--src/OV2640.cpp193
-rw-r--r--src/OV2640.h43
-rw-r--r--src/camera_pins.h99
-rw-r--r--src/main.cpp423
10 files changed, 868 insertions, 0 deletions
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 <Arduino.h>
+#include <pgmspace.h>
+#include <stdio.h>
+#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 <OV2640.h>
+#include <WebServer.h>
+#include <WiFi.h>
+#include <WiFiClient.h>
+
+#include <driver/rtc_io.h>
+#include <esp_bt.h>
+#include <esp_sleep.h>
+#include <esp_wifi.h>
+
+#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); }