From aed616c0dbd33e92935f1ea4f316d1958c116882 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 11:45:36 +0300 Subject: [PATCH 01/21] Added webpage for viewing cam stream and more --- esp32_camera_mjpeg_multiclient.ino | 790 +++++++++++++++++++++++------ 1 file changed, 627 insertions(+), 163 deletions(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index 9a998d3..027871b 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -1,5 +1,4 @@ /* - This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM and ESP-EYE modules. This is tested to work with VLC and Blynk video widget and can support up to 10 @@ -18,67 +17,237 @@ Flash Size: 4Mb Patrition: Minimal SPIFFS PSRAM: Enabled + +# Modifications on this build: +# web interface added for viewing, configuring camera settings and wifi settings +# simple ftp server to upload the webserver html files to internal SPIFFS + ( edit the FtpServerKey.h file and modify to #define DEFAULT_STORAGE_TYPE_ESP32 STORAGE_SPIFFS ) +# arduino OTA for firmware upload +# some performance tweaks +# use of external wifi antena is highly recommended for the esp32cam board +# set "Events Run On: core 0" and "Arduino Run On: core 0" +# used the board flash on gpio 4 +# used the board led on gpio 33 +# added external led on gpio 2 +# added a button to enter AP mode and configure Wifi credentials wich are then saved to SPIFFS and loaded on boot along with camera settings + (button is connected to gpio 13 and 14) */ // ESP32 has two cores: APPlication core and PROcess core (the one that runs ESP32 SDK stack) #define APP_CPU 1 #define PRO_CPU 0 -#include "src/OV2640.h" +#include "OV2640.h" #include #include #include - -#include -#include -#include -#include +#include +#include +#include +#include +#include // Select camera model //#define CAMERA_MODEL_WROVER_KIT -#define CAMERA_MODEL_ESP_EYE +//#define CAMERA_MODEL_ESP_EYE //#define CAMERA_MODEL_M5STACK_PSRAM //#define CAMERA_MODEL_M5STACK_WIDE -//#define CAMERA_MODEL_AI_THINKER - +#define CAMERA_MODEL_AI_THINKER #include "camera_pins.h" -/* - Next one is an include with wifi credentials. - This is what you need to do: +#define FLASH_PIN 4 // Define the GPIO pin for the flash LED +#define LED_PIN 33 // Define the GPIO pin for the internal LED +#define RED_LED_PIN 2 // Define the GPIO pin for the external RED LED +#define BUTTON_PIN_INPUT 13 // GPIO 13 +#define BUTTON_PIN_OUTPUT 14 // GPIO 14 - 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"). - 2. Place the following text in the file: - #define SSID1 "replace with your wifi ssid" - #define PWD1 "replace your wifi password" - 3. Save. +unsigned long lastTime = 0; +unsigned long timerDelay = 50; +int led_state = LOW; +bool led_flag = false; - Should work then -*/ -#include "home_wifi_multi.h" +String ssid; +String password; +String hostname; OV2640 cam; WebServer server(80); +FtpServer ftpSrv; // ===== 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 - +TaskHandle_t tMjpeg = NULL; // handles client connections to the webserver +//TaskHandle_t tCam; // handles getting picture frames from the camera and storing them locally +TaskHandle_t camTaskHandle = NULL; +//TaskHandle_t tStream; // actually streaming frames to all connected clients +TaskHandle_t streamTaskHandle = NULL; // 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 14 FPS frame rate +int FPS = 14; // Default FPS value +// We will handle web client requests every 10 ms (100 Hz) +const int WSINTERVAL = 10; + +// Function to handle camera settings +void handleCameraSettings() { + if (server.hasArg("plain") == false) { // Check if body received + server.send(400, "application/json", "{\"status\":\"Invalid Request\"}"); + return; + } -// We will try to achieve 25 FPS frame rate -const int FPS = 14; + String body = server.arg("plain"); + StaticJsonDocument<200> doc; + DeserializationError error = deserializeJson(doc, body); -// We will handle web client requests every 50 ms (20 Hz) -const int WSINTERVAL = 100; + if (error) { + server.send(400, "application/json", "{\"status\":\"Invalid JSON\"}"); + return; + } + + int quality = doc["quality"]; + int brightness = doc["brightness"]; + int contrast = doc["contrast"]; + int saturation = doc["saturation"]; + int specialEffect = doc["specialEffect"]; + int whiteBalance = doc["whiteBalance"]; + int awbGain = doc["awbGain"]; + int wbMode = doc["wbMode"]; + int hmirror = doc["hmirror"]; + int vflip = doc["vflip"]; + int colorbar = doc["colorbar"]; + int gammaCorrection = doc["gammaCorrection"]; + int aec2 = doc["aec2"]; + int aeLevel = doc["aeLevel"]; + int aecValue = doc["aecValue"]; + int exposureControl = doc["exposureControl"]; + int gainControl = doc["gainControl"]; + int agcGain = doc["agcGain"]; + int dcw = doc["dcw"]; + int fps = doc["fps"]; // Retrieve the FPS setting + int led = doc["led"]; // Retrieve the led setting + String resolution = doc["resolution"]; + // Apply camera settings + sensor_t* s = esp_camera_sensor_get(); + if (resolution == "QVGA") { + s->set_framesize(s, FRAMESIZE_QVGA); + } else if (resolution == "VGA") { + s->set_framesize(s, FRAMESIZE_VGA); + } else if (resolution == "SVGA") { + s->set_framesize(s, FRAMESIZE_SVGA); + } else if (resolution == "XGA") { + s->set_framesize(s, FRAMESIZE_XGA); + } else if (resolution == "SXGA") { + s->set_framesize(s, FRAMESIZE_SXGA); + } else if (resolution == "UXGA") { + s->set_framesize(s, FRAMESIZE_UXGA); + } + s->set_quality(s, quality); + s->set_brightness(s, brightness); + s->set_contrast(s, contrast); + s->set_saturation(s, saturation); + s->set_special_effect(s, specialEffect); + s->set_whitebal(s, whiteBalance); // Auto White Balance + s->set_awb_gain(s, awbGain); // AWB Gain + s->set_wb_mode(s, wbMode); // White Balance Mode + s->set_hmirror(s, hmirror); // Horizontal Mirror + s->set_vflip(s, vflip); // Vertical Flip + s->set_colorbar(s, colorbar); // Color Bar + s->set_raw_gma(s, gammaCorrection); // RAW Gamma Correction + s->set_aec2(s, aec2); // AEC2 + s->set_ae_level(s, aeLevel); // AE Level + s->set_aec_value(s, aecValue); // AEC Value + s->set_exposure_ctrl(s, exposureControl); // Exposure Control + s->set_gain_ctrl(s, gainControl); // Gain Control + s->set_agc_gain(s, agcGain); // AGC Gain + s->set_dcw(s, dcw); // Downsize Mode + FPS = fps; // Update the global FPS variable + if (led == 1) { + digitalWrite(FLASH_PIN, HIGH); + } else { + digitalWrite(FLASH_PIN, LOW); + } + // Save settings to SPIFFS + saveSettings(body); + // Send response + server.send(200, "application/json", "{\"status\":\"Settings applied\"}"); +} +// Apply settings on reboot +void applySettings(const String& settings) { + // Parse the settings and apply them to the camera + StaticJsonDocument<200> doc; + DeserializationError error = deserializeJson(doc, settings); + + if (error) { + Serial.println("Failed to parse settings"); + return; + } + int quality = doc["quality"]; + int brightness = doc["brightness"]; + int contrast = doc["contrast"]; + int saturation = doc["saturation"]; + int specialEffect = doc["specialEffect"]; + int whiteBalance = doc["whiteBalance"]; + int awbGain = doc["awbGain"]; + int wbMode = doc["wbMode"]; + int hmirror = doc["hmirror"]; + int vflip = doc["vflip"]; + int colorbar = doc["colorbar"]; + int gammaCorrection = doc["gammaCorrection"]; + int aec2 = doc["aec2"]; + int aeLevel = doc["aeLevel"]; + int aecValue = doc["aecValue"]; + int exposureControl = doc["exposureControl"]; + int gainControl = doc["gainControl"]; + int agcGain = doc["agcGain"]; + int dcw = doc["dcw"]; + int fps = doc["fps"]; // Retrieve the FPS setting + int led = doc["led"]; // Retrieve the led setting + String resolution = doc["resolution"]; + // Apply camera settings + sensor_t* s = esp_camera_sensor_get(); + if (resolution == "QVGA") { + s->set_framesize(s, FRAMESIZE_QVGA); + } else if (resolution == "VGA") { + s->set_framesize(s, FRAMESIZE_VGA); + } else if (resolution == "SVGA") { + s->set_framesize(s, FRAMESIZE_SVGA); + } else if (resolution == "XGA") { + s->set_framesize(s, FRAMESIZE_XGA); + } else if (resolution == "SXGA") { + s->set_framesize(s, FRAMESIZE_SXGA); + } else if (resolution == "UXGA") { + s->set_framesize(s, FRAMESIZE_UXGA); + } + s->set_quality(s, quality); + s->set_brightness(s, brightness); + s->set_contrast(s, contrast); + s->set_saturation(s, saturation); + s->set_special_effect(s, specialEffect); + s->set_whitebal(s, whiteBalance); // Auto White Balance + s->set_awb_gain(s, awbGain); // AWB Gain + s->set_wb_mode(s, wbMode); // White Balance Mode + s->set_hmirror(s, hmirror); // Horizontal Mirror + s->set_vflip(s, vflip); // Vertical Flip + s->set_colorbar(s, colorbar); // Color Bar + s->set_raw_gma(s, gammaCorrection); // RAW Gamma Correction + s->set_aec2(s, aec2); // AEC2 + s->set_ae_level(s, aeLevel); // AE Level + s->set_aec_value(s, aecValue); // AEC Value + s->set_exposure_ctrl(s, exposureControl); // Exposure Control + s->set_gain_ctrl(s, gainControl); // Gain Control + s->set_agc_gain(s, agcGain); // AGC Gain + s->set_dcw(s, dcw); // Downsize Mode + FPS = fps; // Update the global FPS variable + if (led == 1) { + digitalWrite(FLASH_PIN, HIGH); + } else { + digitalWrite(FLASH_PIN, LOW); + } +} // ======== Server Connection Handler Task ========================== void mjpegCB(void* pvParameters) { @@ -87,101 +256,143 @@ void mjpegCB(void* pvParameters) { // Creating frame synchronization semaphore and initializing it frameSync = xSemaphoreCreateBinary(); - xSemaphoreGive( frameSync ); + xSemaphoreGive(frameSync); // Creating a queue to track all connected clients - streamingClients = xQueueCreate( 10, sizeof(WiFiClient*) ); + 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 + camCB, // callback + "cam", // name + 6 * 1024, // stack size + NULL, // parameters + 6, // priority + &camTaskHandle, // 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); + 6 * 1024, + NULL, // parameters + 6, // priority + &streamTaskHandle, // RTOS task handle + PRO_CPU); // core // Registering webserver handling routines - server.on("/mjpeg/1", HTTP_GET, handleJPGSstream); + server.on("/stream", HTTP_GET, handleJPGSstream); server.on("/jpg", HTTP_GET, handleJPG); + server.on("/", HTTP_GET, []() { + File file = SPIFFS.open("/index.html", "r"); + server.streamFile(file, "text/html"); + file.close(); + }); + server.on("/cam_settings", HTTP_GET, []() { + File file = SPIFFS.open("/cam_settings.html", "r"); + server.streamFile(file, "text/html"); + file.close(); + }); + server.on("/wifi_settings", HTTP_GET, []() { + File file = SPIFFS.open("/wifi_settings.html", "r"); + server.streamFile(file, "text/html"); + file.close(); + }); + server.on("/getSettings", HTTP_GET, []() { + String settings = loadSettings(); + server.send(200, "application/json", settings); + }); + server.on("/saveSettings", HTTP_POST, []() { + if (server.hasArg("plain")) { + String body = server.arg("plain"); + File file = SPIFFS.open("/wifi_settings.json", FILE_WRITE); + if (!file) { + server.send(500, "application/json", "{\"message\":\"Failed to save settings\"}"); + return; + } + file.print(body); + file.close(); + server.send(200, "application/json", "{\"message\":\"Settings saved successfully\"}"); + } + }); + server.on("/reboot", HTTP_POST, []() { + server.send(200, "application/json", "{\"message\":\"Rebooting...\"}"); + // Delete the ap_mode.flag file + if (SPIFFS.exists("/ap_mode.flag")) { + SPIFFS.remove("/ap_mode.flag"); + } + delay(1000); + ESP.restart(); + }); + server.on("/rebootAp", HTTP_POST, []() { + File file = SPIFFS.open("/ap_mode.flag", FILE_WRITE); + if (!file) { + server.send(500, "application/json", "{\"message\":\"Failed to set AP mode flag\"}"); + return; + } + file.close(); + server.send(200, "application/json", "{\"message\":\"Rebooting into AP mode...\"}"); + delay(1000); + ESP.restart(); + }); + // Serve static files + server.serveStatic("/cam_styles.css", SPIFFS, "/cam_styles.css"); + server.serveStatic("/cam_script.js", SPIFFS, "/cam_script.js"); + server.serveStatic("/wifi_styles.css", SPIFFS, "/wifi_styles.css"); + server.serveStatic("/wifi_script.js", SPIFFS, "/wifi_script.js"); + server.serveStatic("/index_script.js", SPIFFS, "/index_script.js"); + server.on("/settings", HTTP_POST, handleCameraSettings); 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); } } - // Commonly used variables: -volatile size_t camSize; // size of the current frame, byte -volatile char* camBuf; // pointer to the current frame - +volatile size_t camSize; // size of the current frame, byte +volatile char* camBuf; // pointer to the current frame // ==== 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(); + 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 ); - + xSemaphoreTake(frameSync, portMAX_DELAY); // Do not allow interrupts while switching the current frame portENTER_CRITICAL(&xSemaphore); camBuf = fbs[ifb]; @@ -189,53 +400,42 @@ void camCB(void* pvParameters) { 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 ); - + 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 ); - + xTaskNotifyGive(streamTaskHandle); // 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 ) { + if (eTaskGetState(streamTaskHandle) == eSuspended) { vTaskSuspend(NULL); // passing NULL means "suspend yourself" } } } - // ==== 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); + if (aSize > freeHeap * 2 / 3) { + if (psramFound() && ESP.getFreePsram() > aSize) { + ptr = (char*)ps_malloc(aSize); } - } - else { + } else { // Enough free heap - let's try allocating fast RAM as a buffer - ptr = (char*) malloc(aSize); + 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); + 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(); @@ -243,10 +443,9 @@ char* allocateMemory(char* aPtr, size_t aSize) { return ptr; } - // ==== STREAMING ====================================================== -const char HEADER[] = "HTTP/1.1 200 OK\r\n" \ - "Access-Control-Allow-Origin: *\r\n" \ +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: "; @@ -256,55 +455,47 @@ const int cntLen = strlen(CTNTTYPE); // ==== Handle connection request from clients =============================== -void handleJPGSstream(void) -{ +void handleJPGSstream(void) { // Can only acommodate 10 clients. The limit is a default for WiFi connections - if ( !uxQueueSpacesAvailable(streamingClients) ) return; - - + 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); - + 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 ); + if (eTaskGetState(camTaskHandle) == eSuspended) vTaskResume(camTaskHandle); + if (eTaskGetState(streamTaskHandle) == eSuspended) vTaskResume(streamTaskHandle); } // ==== Actually stream content to all connected clients ======================== -void streamCB(void * pvParameters) { +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. */ - + 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 ) { + 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); + WiFiClient* client; + xQueueReceive(streamingClients, (void*)&client, 0); // Check if this client is still connected. @@ -312,31 +503,32 @@ void streamCB(void * pvParameters) { // delete this client reference if s/he has disconnected // and don't put it back on the queue anymore. Bye! delete client; - } - else { - + led_flag = false; + } else { + led_flag = true; // 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 ); + xSemaphoreTake(frameSync, portMAX_DELAY); + //client->write(HEADER, hdrLen); + //client->write(BOUNDARY, bdrLen); 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((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); + 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 ); + xSemaphoreGive(frameSync); taskYIELD(); } - } - else { + } else { // Since there are no connected clients, there is no reason to waste battery running vTaskSuspend(NULL); } @@ -346,29 +538,26 @@ void streamCB(void * pvParameters) { } } - - -const char JHEADER[] = "HTTP/1.1 200 OK\r\n" \ - "Content-disposition: inline; filename=capture.jpg\r\n" \ +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) -{ +void handleJPG(void) { WiFiClient client = server.client(); if (!client.connected()) return; + digitalWrite(FLASH_PIN, HIGH); // flash on for capture jpg cam.run(); client.write(JHEADER, jhdLen); client.write((char*)cam.getfb(), cam.getSize()); + digitalWrite(FLASH_PIN, LOW); } - - // ==== Handle invalid URL requests ============================================ -void handleNotFound() -{ +void handleNotFound() { String message = "Server is running!\n\n"; + message += "input address is wrong!\n\n"; message += "URI: "; message += server.uri(); message += "\nMethod: "; @@ -378,18 +567,33 @@ void handleNotFound() message += "\n"; server.send(200, "text / plain", message); } +// ==== SETUP method ================================================================== +void setup() { + // Setup Serial connection: + Serial.begin(115200); + delay(1000); // wait for a second to let Serial connect + // Initialize LED pins + pinMode(FLASH_PIN, OUTPUT); + pinMode(LED_PIN, OUTPUT); + pinMode(RED_LED_PIN, OUTPUT); + digitalWrite(LED_PIN, HIGH); // internal led pin is inverted + pinMode(BUTTON_PIN_INPUT, INPUT_PULLUP); // Set GPIO 13 as input + digitalWrite(BUTTON_PIN_INPUT, HIGH); // Initialize GPIO 13 as high -// ==== SETUP method ================================================================== -void setup() -{ + pinMode(BUTTON_PIN_OUTPUT, OUTPUT); // Set GPIO 14 as output + digitalWrite(BUTTON_PIN_OUTPUT, LOW); // Initialize GPIO 14 as LOW - // Setup Serial connection: - Serial.begin(115200); - delay(1000); // wait for a second to let Serial connect + // Attach the interrupt to pin 13, triggering on FALLING (HIGH to LOW transition) + attachInterrupt(digitalPinToInterrupt(BUTTON_PIN_INPUT), handleButtonInterrupt, FALLING); + // Initialize SPIFFS + if (!SPIFFS.begin(true)) { + Serial.println("An error has occurred while mounting SPIFFS"); + return; + } // Configure the camera camera_config_t config; config.ledc_channel = LEDC_CHANNEL_0; @@ -412,58 +616,318 @@ void setup() config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG; - + config.fb_location = CAMERA_FB_IN_PSRAM; + //config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; /*!< Fills buffers when they are empty. Less resources but first 'fb_count' frames might be old */ + config.grab_mode = CAMERA_GRAB_LATEST; /*!< Except when 1 frame buffer is used, queue will always contain the last 'fb_count' frames */ // 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.frame_size = FRAMESIZE_UXGA; + config.jpeg_quality = 20; 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(); } + sensor_t* s = esp_camera_sensor_get(); + s->set_brightness(s, 0); // -2 to 2 + s->set_contrast(s, 0); // -2 to 2 + s->set_saturation(s, 0); // -2 to 2 + //s->set_sharpness(s, 2); // -2 to 2 //unsuported + s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia) + s->set_whitebal(s, 1); // 0 = disable , 1 = enable + s->set_awb_gain(s, 1); // 0 = disable , 1 = enable + s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home) + s->set_exposure_ctrl(s, 1); // 0 = disable , 1 = enable + s->set_aec2(s, 1); // 0 = disable , 1 = enable + s->set_ae_level(s, 0); // -2 to 2 + s->set_aec_value(s, 600); // 0 to 1200 + s->set_gain_ctrl(s, 1); // 0 = disable , 1 = enable + s->set_agc_gain(s, 15); // 0 to 30 + s->set_gainceiling(s, (gainceiling_t)0); // 0 to 6 + s->set_bpc(s, 1); // 0 = disable , 1 = enable + s->set_wpc(s, 1); // 0 = disable , 1 = enable + s->set_raw_gma(s, 1); // 0 = disable , 1 = enable + s->set_lenc(s, 1); // 0 = disable , 1 = enable + s->set_hmirror(s, 1); // 0 = disable , 1 = enable + s->set_vflip(s, 1); // 0 = disable , 1 = enable + s->set_dcw(s, 1); // 0 = disable , 1 = enable + s->set_colorbar(s, 0); // 0 = disable , 1 = enable + //s->set_denoise(s, 2); //unsuported + delay(1000); + + createDefault_cam_Settings(); // Create default cam settings if they don't exist + // Load and apply camera settings on startup + String settings = loadSettings(); + if (settings != "") { + applySettings(settings); + } + // Configure and connect to WiFi + createDefault_wifi_Settings(); // Create default wifi settings if they don't exist + // Check if we should boot into AP mode + if (shouldBootAPMode()) { + setupAPMode(); + } else { + load_wifi_Settings(); + wifi_Connect(); + } + // Start mainstreaming RTOS task + xTaskCreatePinnedToCore( + mjpegCB, + "mjpeg", + 4 * 1024, + NULL, + 6, + &tMjpeg, + PRO_CPU); + + ftpSrv.begin("user", "pasw"); //username, password for ftp. set ports in ESP8266FtpServer.h (default 21, 50009 for PASV) + Serial.println("FTP Server Ready"); + ota_setup(); +} + +volatile bool buttonPressed = false; // Flag to indicate button press + +// ISR to set the flag +void handleButtonInterrupt() { + buttonPressed = true; // Set the flag when the interrupt triggers +} + +void loop() { + unsigned long currentMillis = millis(); + // Handle LED blinking logic, signal if there are any clients watching + if (((currentMillis - lastTime) >= timerDelay) && (led_flag)) { + lastTime = currentMillis; + led_state = !led_state; // Toggle LED state + digitalWrite(LED_PIN, led_state); + digitalWrite(RED_LED_PIN, led_state); + } else if (!led_flag && led_state == HIGH) { + led_state = LOW; + digitalWrite(LED_PIN, HIGH); + digitalWrite(RED_LED_PIN, LOW); + } + + if (buttonPressed) { + buttonPressed = false; // Clear the flag + File file = SPIFFS.open("/ap_mode.flag", FILE_WRITE); + if (!file) { + Serial.println("Failed to create file"); + return; + } + file.close(); + delay(1000); + ESP.restart(); + } + ArduinoOTA.handle(); // Check for OTA updates + ftpSrv.handleFTP(); + taskYIELD(); + vTaskDelay(pdMS_TO_TICKS(1)); +} - // Configure and connect to WiFi - IPAddress ip; +void setupAPMode() { + WiFi.softAP("ESP32CAM-AP", "12345678"); + // Set custom IP address + IPAddress local_IP(192, 168, 0, 1); // Change to your desired IP address + IPAddress gateway(192, 168, 0, 1); // Change to your desired gateway address + IPAddress subnet(255, 255, 255, 0); // Subnet mask + WiFi.softAPConfig(local_IP, gateway, subnet); + + IPAddress IP = WiFi.softAPIP(); + Serial.print("AP IP address: "); + Serial.println(IP); +} + +// Function to save settings to SPIFFS +void saveSettings(const String& settings) { + File file = SPIFFS.open("/cam_settings.json", FILE_WRITE); + if (!file) { + Serial.println("Failed to open file for writing"); + return; + } + file.print(settings); + file.close(); + Serial.println("Cam Settings saved to SPIFFS"); +} + +// Function to load settings from SPIFFS +String loadSettings() { + File file = SPIFFS.open("/cam_settings.json"); + if (!file) { + Serial.println("Failed to open file for reading"); + return ""; + } + + String settings = file.readString(); + file.close(); + Serial.println("Cam Settings loaded from SPIFFS"); + return settings; +} + +void createDefault_wifi_Settings() { + if (!SPIFFS.exists("/wifi_settings.json")) { + File file = SPIFFS.open("/wifi_settings.json", FILE_WRITE); + if (!file) { + Serial.println("Failed to create wifi settings file"); + return; + } + DynamicJsonDocument doc(1024); + doc["ssid"] = "free-wifi"; + doc["password"] = "default"; + doc["hostname"] = "esp32cam"; + serializeJson(doc, file); + file.close(); + Serial.println("Default wifi settings file created"); + } +} +void createDefault_cam_Settings() { + if (!SPIFFS.exists("/cam_settings.json")) { + File file = SPIFFS.open("/cam_settings.json", FILE_WRITE); + if (!file) { + Serial.println("Failed to create cam settings file"); + return; + } + DynamicJsonDocument doc(1024); + doc["quality"] = "20"; + doc["fps"] = "14"; + doc["brightness"] = "0"; + doc["contrast"] = "0"; + doc["saturation"] = "0"; + doc["specialEffect"] = "0"; + doc["whiteBalance"] = "1"; + doc["awbGain"] = "1"; + doc["wbMode"] = "0"; + doc["hmirror"] = "0"; + doc["vflip"] = "0"; + doc["colorbar"] = "0"; + doc["gammaCorrection"] = "1"; + doc["aec2"] = "1"; + doc["aeLevel"] = "0"; + doc["aecValue"] = "600"; + doc["exposureControl"] = "1"; + doc["gainControl"] = "1"; + doc["agcGain"] = "15"; + doc["dcw"] = "1"; + doc["led"] = "0"; + doc["resolution"] = "VGA"; + serializeJson(doc, file); + file.close(); + Serial.println("Default cam settings file created"); + } +} + +void wifi_Connect() { + IPAddress ip; WiFi.mode(WIFI_STA); - WiFi.begin(SSID1, PWD1); + WiFi.setTxPower(WIFI_POWER_19_5dBm); + WiFi.setSleep(false); + WiFi.setHostname(hostname.c_str()); + WiFi.begin(ssid.c_str(), password.c_str()); Serial.print("Connecting to WiFi"); - while (WiFi.status() != WL_CONNECTED) - { + 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"); - + Serial.println(ip); +} - // Start mainstreaming RTOS task - xTaskCreatePinnedToCore( - mjpegCB, - "mjpeg", - 4 * 1024, - NULL, - 2, - &tMjpeg, - APP_CPU); +bool shouldBootAPMode() { + if (SPIFFS.exists("/ap_mode.flag")) { + return true; + } else { + return false; + } } +void load_wifi_Settings() { + if (!SPIFFS.exists("/wifi_settings.json")) { + Serial.println("wifi_settings.json file does not exist"); + return; + } -void loop() { - vTaskDelay(1000); + File file = SPIFFS.open("/wifi_settings.json", FILE_READ); + if (!file) { + Serial.println("Failed to open wifi_settings.json file for reading"); + return; + } + + size_t size = file.size(); + if (size == 0) { + Serial.println("wifi_settings.json file is empty"); + file.close(); + return; + } + + std::unique_ptr buf(new char[size + 1]); + file.readBytes(buf.get(), size); + buf[size] = '\0'; // Null-terminate the buffer + file.close(); + Serial.println("wifi_settings.json content:"); + Serial.println(buf.get()); + + DynamicJsonDocument doc(1024); + DeserializationError error = deserializeJson(doc, buf.get()); + if (error) { + Serial.print("Failed to parse JSON: "); + Serial.println(error.f_str()); + return; + } + ssid = doc["ssid"].as(); + password = doc["password"].as(); + hostname = doc["hostname"].as(); + + Serial.println("WiFi settings loaded from SPIFFS"); + Serial.print("SSID: "); + Serial.println(ssid); + Serial.print("Password: "); + Serial.println(password); + Serial.print("Hostname: "); + Serial.println(hostname); +} + +// OTA setup +void ota_setup() { + ArduinoOTA.setHostname("ESPCAM_OTA"); + ArduinoOTA.setPassword("OTA-pasw"); // Set a strong password for OTA updates + ArduinoOTA.onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) { + type = "sketch"; + } else { // U_SPIFFS + type = "filesystem"; + } + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + Serial.println("Start updating " + type); + }); + ArduinoOTA.onEnd([]() { + Serial.println("\nEnd"); + delay(30000); + ESP.restart(); + }); + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }); + ArduinoOTA.onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) { + Serial.println("Auth Failed"); + } else if (error == OTA_BEGIN_ERROR) { + Serial.println("Begin Failed"); + } else if (error == OTA_CONNECT_ERROR) { + Serial.println("Connect Failed"); + } else if (error == OTA_RECEIVE_ERROR) { + Serial.println("Receive Failed"); + } else if (error == OTA_END_ERROR) { + Serial.println("End Failed"); + } + }); + ArduinoOTA.begin(); + Serial.println("OTA updates Ready"); } From 5f96bfd458bebe69135c464e5bb235e0b32cc1e9 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 11:48:56 +0300 Subject: [PATCH 02/21] Added webpage for viewing cam stream and more --- esp32_camera_mjpeg_multiclient.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index 027871b..ae5afb9 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -29,7 +29,7 @@ # used the board flash on gpio 4 # used the board led on gpio 33 # added external led on gpio 2 -# added a button to enter AP mode and configure Wifi credentials wich are then saved to SPIFFS and loaded on boot along with camera settings +# added a button to enter AP mode and configure Wifi credentials which are then saved to SPIFFS and loaded on boot along with camera settings (button is connected to gpio 13 and 14) */ From 0b029d966811ee9e95c09838d6e2a0888cde74e3 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 11:51:13 +0300 Subject: [PATCH 03/21] Update OV2640.h --- src/OV2640.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/OV2640.h b/src/OV2640.h index b9b5706..0f1d6de 100644 --- a/src/OV2640.h +++ b/src/OV2640.h @@ -2,10 +2,10 @@ #define OV2640_H_ #include -#include -#include -#include "esp_log.h" -#include "esp_attr.h" +//#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; From 79b77dd3ed24f4b180ee87f71a3420189f3de9cb Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 11:53:54 +0300 Subject: [PATCH 04/21] Create index.html --- html/index.html | 84 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 html/index.html diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..5187907 --- /dev/null +++ b/html/index.html @@ -0,0 +1,84 @@ + + + + + + + ESP32 Camera Server + + + +

ESP32 Camera Server

+
+ +
+
+

Live stream from your ESP32 camera module. Below you can find links to configure your camera and Wi-Fi settings.

+
+ + + + From e242dbddd9b65ed3b34eb2c9029309d8ca866b09 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 11:56:06 +0300 Subject: [PATCH 05/21] Add files via upload --- html/cam_script.js | 195 ++++++++++++++++++++++++++++++++++++++++ html/cam_settings.html | 173 +++++++++++++++++++++++++++++++++++ html/cam_styles.css | 88 ++++++++++++++++++ html/index_script.js | 86 ++++++++++++++++++ html/wifi_script.js | 55 ++++++++++++ html/wifi_settings.html | 35 ++++++++ html/wifi_styles.css | 40 +++++++++ 7 files changed, 672 insertions(+) create mode 100644 html/cam_script.js create mode 100644 html/cam_settings.html create mode 100644 html/cam_styles.css create mode 100644 html/index_script.js create mode 100644 html/wifi_script.js create mode 100644 html/wifi_settings.html create mode 100644 html/wifi_styles.css diff --git a/html/cam_script.js b/html/cam_script.js new file mode 100644 index 0000000..2895ce9 --- /dev/null +++ b/html/cam_script.js @@ -0,0 +1,195 @@ +var baseHost = document.location.origin; +var streamUrl = `${baseHost}/stream`; +var jpgUrl = `${baseHost}/jpg`; +const view = document.getElementById('videoStream'); +const viewContainer = document.getElementById('streamContainer'); +const toggleStreamButton = document.getElementById('toggleStreamButton'); +const getJpgButton = document.getElementById('getJpgButton'); + +const startStream = () => { + view.src = streamUrl; + viewContainer.style.display = 'flex'; + toggleStreamButton.innerHTML = 'Stop Video Stream'; + getJpgButton.disabled = true; // Disable "Save Picture" button +}; + +const stopStream = () => { + // Remove the event listener to prevent unintended downloads + view.onload = null; + view.src = ''; + viewContainer.style.display = 'none'; + toggleStreamButton.innerHTML = 'Start Video Stream'; + getJpgButton.disabled = false; // Enable "Save Picture" button +}; + +const getJpg = async () => { + try { + const response = await fetch(jpgUrl); + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + + view.src = blobUrl; // Display the image + viewContainer.style.display = 'flex'; + + setTimeout(() => { + downloadJpg(blobUrl); // Download the same image + }, 500); + } catch (error) { + console.error('Error fetching image:', error); + } +}; + +const downloadJpg = (url) => { + fetch(url) + .then(response => response.blob()) + .then(blob => { + const link = document.createElement('a'); + const blobUrl = URL.createObjectURL(blob); + + // Get the current timestamp + const now = new Date(); + const timestamp = now.toISOString().replace(/[:.-]/g, ''); // Format the timestamp + + // Append the timestamp to the filename + link.href = blobUrl; + link.download = `image_${timestamp}.jpg`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // Revoke the Blob URL + URL.revokeObjectURL(blobUrl); + }) + .catch(error => console.error('Error downloading the image:', error)); +}; + +toggleStreamButton.addEventListener('click', () => { + if (viewContainer.style.display === 'none' || view.src !== streamUrl) { + startStream(); + } else { + stopStream(); + } +}); + +getJpgButton.addEventListener('click', () => { + getJpg(); +}); + +document.addEventListener('DOMContentLoaded', () => { + fetch('/getSettings') + .then(response => response.json()) + .then(settings => { + document.getElementById('quality').value = settings.quality; + document.getElementById('brightness').value = settings.brightness; + document.getElementById('contrast').value = settings.contrast; + document.getElementById('saturation').value = settings.saturation; + document.getElementById('resolution').value = settings.resolution; + document.getElementById('specialEffect').value = settings.specialEffect; + document.getElementById('whiteBalance').value = settings.whiteBalance; + document.getElementById('awbGain').value = settings.awbGain; + document.getElementById('wbMode').value = settings.wbMode; + document.getElementById('hmirror').value = settings.hmirror; + document.getElementById('vflip').value = settings.vflip; + document.getElementById('colorbar').value = settings.colorbar; + document.getElementById('gammaCorrection').value = settings.gammaCorrection; + document.getElementById('exposureControl').value = settings.exposureControl; + document.getElementById('aec2').value = settings.aec2; + document.getElementById('aeLevel').value = settings.aeLevel; + document.getElementById('aecValue').value = settings.aecValue; + document.getElementById('gainControl').value = settings.gainControl; + document.getElementById('agcGain').value = settings.agcGain; + document.getElementById('dcw').value = settings.dcw; + document.getElementById('led').value = settings.led; + document.getElementById('fps').value = settings.fps; + }) + .catch(error => { + console.error('Error loading settings:', error); + }); +}); + +document.getElementById('resetDefaultsButton').addEventListener('click', () => { + // Resetting the values to their defaults + document.getElementById('quality').value = '20'; + document.getElementById('fps').value = '14'; + document.getElementById('brightness').value = '0'; + document.getElementById('contrast').value = '0'; + document.getElementById('saturation').value = '0'; + document.getElementById('resolution').value = 'SVGA'; + document.getElementById('specialEffect').value = '0'; + document.getElementById('whiteBalance').value = '1'; + document.getElementById('awbGain').value = '1'; + document.getElementById('wbMode').value = '0'; + document.getElementById('hmirror').value = '0'; + document.getElementById('vflip').value = '0'; + document.getElementById('colorbar').value = '0'; + document.getElementById('gammaCorrection').value = '1'; + document.getElementById('exposureControl').value = '1'; + document.getElementById('aec2').value = '1'; + document.getElementById('aeLevel').value = '0'; + document.getElementById('aecValue').value = '10'; + document.getElementById('gainControl').value = '1'; + document.getElementById('agcGain').value = '15'; + document.getElementById('dcw').value = '1'; + document.getElementById('led').value = '0'; + + // Trigger the "Save Settings" button click + document.getElementById('saveSettingsButton').click(); +}); + + +/* ... existing code ... */ + +const saveButton = document.getElementById('saveSettingsButton'); + +const infoBar = document.getElementById('infoBar'); +const showMessage = (message) => { + infoBar.innerHTML = message; + infoBar.style.display = 'block'; + setTimeout(() => { + infoBar.style.display = 'none'; + }, 3000); +}; + +saveButton.addEventListener('click', () => { + const settings = { + quality: document.getElementById('quality').value, + brightness: document.getElementById('brightness').value, + contrast: document.getElementById('contrast').value, + saturation: document.getElementById('saturation').value, + resolution: document.getElementById('resolution').value, + specialEffect: document.getElementById('specialEffect').value, + whiteBalance: document.getElementById('whiteBalance').value, + awbGain: document.getElementById('awbGain').value, + wbMode: document.getElementById('wbMode').value, + hmirror: document.getElementById('hmirror').value, + vflip: document.getElementById('vflip').value, + colorbar: document.getElementById('colorbar').value, + gammaCorrection: document.getElementById('gammaCorrection').value, + exposureControl: document.getElementById('exposureControl').value, + aec2: document.getElementById('aec2').value, + aeLevel: document.getElementById('aeLevel').value, + aecValue: document.getElementById('aecValue').value, + gainControl: document.getElementById('gainControl').value, + agcGain: document.getElementById('agcGain').value, + dcw: document.getElementById('dcw').value, + fps: document.getElementById('fps').value, + led: document.getElementById('led').value + }; + + fetch('/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(settings) + }) + .then(response => response.json()) + .then(data => { + console.log('Settings saved:', data); + showMessage('Settings applied successfully'); + }) + .catch((error) => { + console.error('Error:', error); + showMessage('Error applying settings'); + }); +}); diff --git a/html/cam_settings.html b/html/cam_settings.html new file mode 100644 index 0000000..36b6db0 --- /dev/null +++ b/html/cam_settings.html @@ -0,0 +1,173 @@ + + + + ESP32-CAM Settings + + + +
+

Camera Settings

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ + + diff --git a/html/cam_styles.css b/html/cam_styles.css new file mode 100644 index 0000000..1ca9024 --- /dev/null +++ b/html/cam_styles.css @@ -0,0 +1,88 @@ +body { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; + height: 100vh; + background-color: #000000; + margin: 0; + font-family: Arial, sans-serif; + color: white; +} +.settings { + flex: 0 0 auto; + display: flex; + flex-direction: column; + align-items: flex-end; + background-color: #333; + padding: 2px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + margin-right: 5px; +} +h1 { + color: #fff; + font-size: 16px; +} +label { + margin: 2px; + font-size: 14px; + color: #fff; +} +input, select { + margin: 2px; + padding: 2px; + font-size: 12px; + width: 120px; + box-sizing: border-box; +} +.button-row { + display: flex; + flex-direction: row; + justify-content: flex-end; + width: 100%; +} +button { + margin: 2px; + padding: 5px 10px; + font-size: 14px; + background-color: #007bff; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; +} +button:hover { + background-color: #0056b3; +} +.stream { + flex: 1 1 auto; + display: flex; + justify-content: center; + align-items: center; + background-color: #000000; + border-radius: 10px; + padding: 10px; +} +iframe { + width: 100%; + height: 100%; + border: none; +} +.info-bar { + position: fixed; + bottom: 0; + width: 100%; + padding: 10px; + background-color: #007bff; + color: white; + text-align: center; + font-size: 14px; + display: none; /* Initially hidden */ +} +button:disabled { + background-color: #ccc; /* Light gray background */ + color: #666; /* Darker gray text */ + cursor: not-allowed; /* Show 'not allowed' cursor */ + opacity: 0.6; /* Slightly faded */ +} diff --git a/html/index_script.js b/html/index_script.js new file mode 100644 index 0000000..25f5a6b --- /dev/null +++ b/html/index_script.js @@ -0,0 +1,86 @@ +document.addEventListener('DOMContentLoaded', () => { + const baseHost = document.location.origin; + const streamUrl = `${baseHost}/stream`; + + const videoContainer = document.getElementById('videoContainer'); // The container for the video + const mjpegStreamImg = document.createElement('img'); // Dynamically create an element + + // Set the class for the element + mjpegStreamImg.className = 'mjpeg-stream'; + mjpegStreamImg.src = streamUrl; + + // Set dimensions to scale responsively while maintaining aspect ratio + const aspectRatio = 640 / 480; // VGA aspect ratio (width / height) + + // Adjust dynamically based on the container's width + function adjustStreamSize() { + const containerWidth = videoContainer.offsetWidth; + const calculatedHeight = containerWidth / aspectRatio; // Maintain aspect ratio + + mjpegStreamImg.style.width = `${containerWidth}px`; + mjpegStreamImg.style.height = `${calculatedHeight}px`; + videoContainer.style.height = `${calculatedHeight}px`; + } + + // Initial adjustment + adjustStreamSize(); + + // Re-adjust on window resize + window.addEventListener('resize', adjustStreamSize); + + // Add the element to the video container + videoContainer.appendChild(mjpegStreamImg); + + // Create the date and time element + const dateTimeOverlay = document.createElement('div'); + dateTimeOverlay.className = 'date-time-overlay'; + videoContainer.appendChild(dateTimeOverlay); + + // Update the date and time dynamically + setInterval(() => { + const now = new Date(); + const formattedTime = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const formattedDate = now.toLocaleDateString(); + dateTimeOverlay.innerText = `${formattedDate} ${formattedTime}`; + }, 1000); + + // Create the text element + const overlayText = document.createElement('div'); + overlayText.className = 'overlay-text'; + overlayText.innerText = 'ESP32-CAM live video stream'; // Replace with your desired text + videoContainer.appendChild(overlayText); + + // Add CSS styles for the overlays + const style = document.createElement('style'); + style.innerHTML = ` + #videoContainer { + position: relative; /* Make container the positioning context */ + max-width: 100%; /* Ensure container doesn't exceed screen width */ + margin: 0 auto; /* Center the video container */ + max-width: 800px; /* Set a maximum width for desktop browsers */ + } + .overlay-text { + position: absolute; + top: 10px; + left: 10px; + color: white; + background-color: rgba(0, 0, 0, 0.5); + padding: 5px 10px; + font-size: 16px; + font-family: Arial, sans-serif; + border-radius: 5px; + } + .date-time-overlay { + position: absolute; + bottom: 10px; + right: 10px; + color: white; + background-color: rgba(0, 0, 0, 0.5); + padding: 5px 10px; + font-size: 12px; + font-family: Arial, sans-serif; + border-radius: 5px; + } + `; + document.head.appendChild(style); +}); diff --git a/html/wifi_script.js b/html/wifi_script.js new file mode 100644 index 0000000..c771a24 --- /dev/null +++ b/html/wifi_script.js @@ -0,0 +1,55 @@ +function areFieldsFilled() { + const ssid = document.getElementById('ssid').value.trim(); + const password = document.getElementById('password').value.trim(); + const hostname = document.getElementById('hostname').value.trim(); + + return ssid && password && hostname; +} + +document.getElementById('saveButton').addEventListener('click', () => { + const ssid = document.getElementById('ssid').value.trim(); + const password = document.getElementById('password').value.trim(); + const hostname = document.getElementById('hostname').value.trim(); + + if (!ssid || !password || !hostname) { + alert('Please fill out all fields.'); + return; + } + + const settings = { + ssid, + password, + hostname + }; + + fetch('/saveSettings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(settings) + }).then(response => response.json()) + .then(data => alert(data.message)); +}); + +document.getElementById('rebootButton').addEventListener('click', () => { + if (!areFieldsFilled()) { + alert('Please fill out all fields before rebooting.'); + return; + } + + fetch('/reboot', { + method: 'POST' + }).then(response => response.json()) + .then(data => alert(data.message)); +}); + +document.getElementById('rebootApButton').addEventListener('click', () => { + fetch('/rebootAp', { + method: 'POST' + }).then(response => response.json()) + .then(data => { + document.getElementById('apInfo').style.display = 'block'; + alert(data.message); + }); +}); diff --git a/html/wifi_settings.html b/html/wifi_settings.html new file mode 100644 index 0000000..34cbdc2 --- /dev/null +++ b/html/wifi_settings.html @@ -0,0 +1,35 @@ + + + + ESP32 Settings + + + +

Wi-Fi Settings

+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ + + + diff --git a/html/wifi_styles.css b/html/wifi_styles.css new file mode 100644 index 0000000..9383575 --- /dev/null +++ b/html/wifi_styles.css @@ -0,0 +1,40 @@ +body { + font-family: Arial, sans-serif; + background-color: #f0f0f0; + margin: 0; + padding: 20px; +} + +h1 { + color: #333; +} + +label { + display: block; + margin-top: 10px; +} + +input[type="text"], +input[type="password"] { + width: 100%; + padding: 8px; + margin-top: 5px; + box-sizing: border-box; +} + +.button-row { + margin-top: 20px; +} + +button { + padding: 10px 20px; + background-color: #4CAF50; + color: white; + border: none; + cursor: pointer; + margin-right: 10px; +} + +button:hover { + background-color: #45a049; +} From c9d5995510ebe9f8047b75534f85492c7e03ca86 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 12:00:57 +0300 Subject: [PATCH 06/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 850b941..45a2d11 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ 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** - +Working example of this fork you can check out [here](http://vlaha-41.go.ro:50800). 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/) From 18eaf6a6e75cd45ad68399c07ceb57fb033f017c Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 12:11:31 +0300 Subject: [PATCH 07/21] Added webpage for viewing cam stream and more --- esp32_camera_mjpeg_multiclient.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index ae5afb9..de07685 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -37,7 +37,7 @@ #define APP_CPU 1 #define PRO_CPU 0 -#include "OV2640.h" +#include "src/OV2640.h" #include #include #include From 59babc5f3c13e683129d2a771d9d354f330cce68 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 12:14:55 +0300 Subject: [PATCH 08/21] Added webpage for viewing cam stream and more --- esp32_camera_mjpeg_multiclient.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index de07685..2e6e9a9 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -30,7 +30,7 @@ # used the board led on gpio 33 # added external led on gpio 2 # added a button to enter AP mode and configure Wifi credentials which are then saved to SPIFFS and loaded on boot along with camera settings - (button is connected to gpio 13 and 14) + (button is connected though a 220 ohm series rezistor from gpio 13 to 14) */ // ESP32 has two cores: APPlication core and PROcess core (the one that runs ESP32 SDK stack) From b03916d2868b6ab7cdbf03e621511e46ba52897d Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 12:18:17 +0300 Subject: [PATCH 09/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 45a2d11..cae946b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # ESP32 MJPEG Multiclient Streaming Server +# Working example of this fork you can check out [here](http://vlaha-41.go.ro:50800). This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM or ESP-EYE modules. @@ -8,7 +9,6 @@ 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** -Working example of this fork you can check out [here](http://vlaha-41.go.ro:50800). 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/) From 1596f4c6b54baacea8b25a75039735b4c098418a Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 15:37:07 +0300 Subject: [PATCH 10/21] Update: webpage for viewing cam stream and more --- esp32_camera_mjpeg_multiclient.ino | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index 2e6e9a9..e3bd39f 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -36,31 +36,29 @@ // ESP32 has two cores: APPlication core and PROcess core (the one that runs ESP32 SDK stack) #define APP_CPU 1 #define PRO_CPU 0 - -#include "src/OV2640.h" -#include -#include -#include -#include -#include -#include -#include -#include - // Select camera model //#define CAMERA_MODEL_WROVER_KIT //#define CAMERA_MODEL_ESP_EYE //#define CAMERA_MODEL_M5STACK_PSRAM //#define CAMERA_MODEL_M5STACK_WIDE #define CAMERA_MODEL_AI_THINKER -#include "camera_pins.h" - #define FLASH_PIN 4 // Define the GPIO pin for the flash LED #define LED_PIN 33 // Define the GPIO pin for the internal LED #define RED_LED_PIN 2 // Define the GPIO pin for the external RED LED #define BUTTON_PIN_INPUT 13 // GPIO 13 #define BUTTON_PIN_OUTPUT 14 // GPIO 14 +#include "src/OV2640.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "camera_pins.h" + unsigned long lastTime = 0; unsigned long timerDelay = 50; int led_state = LOW; @@ -625,7 +623,7 @@ void setup() { // config.frame_size = FRAMESIZE_QVGA; config.frame_size = FRAMESIZE_UXGA; config.jpeg_quality = 20; - config.fb_count = 2; + config.fb_count = 1; if (cam.init(config) != ESP_OK) { Serial.println("Error initializing the camera"); From a3794ef27ae5b1ab0b435c834dac8e2ab6f97274 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 18:33:20 +0300 Subject: [PATCH 11/21] Update esp32_camera_mjpeg_multiclient.ino -added camera deinitialization before OTA updates to prevent incorect settings after restart --- esp32_camera_mjpeg_multiclient.ino | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index e3bd39f..c56d0ef 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -26,6 +26,7 @@ # some performance tweaks # use of external wifi antena is highly recommended for the esp32cam board # set "Events Run On: core 0" and "Arduino Run On: core 0" +# set "Erase All Flash Before Sketch Upload: Disabled" to prevent SPIFFS deletion # used the board flash on gpio 4 # used the board led on gpio 33 # added external led on gpio 2 @@ -97,7 +98,7 @@ void handleCameraSettings() { } String body = server.arg("plain"); - StaticJsonDocument<200> doc; + DynamicJsonDocument doc(1024); DeserializationError error = deserializeJson(doc, body); if (error) { @@ -176,7 +177,7 @@ void handleCameraSettings() { // Apply settings on reboot void applySettings(const String& settings) { // Parse the settings and apply them to the camera - StaticJsonDocument<200> doc; + DynamicJsonDocument doc(1024); DeserializationError error = deserializeJson(doc, settings); if (error) { @@ -895,6 +896,12 @@ void ota_setup() { ArduinoOTA.setHostname("ESPCAM_OTA"); ArduinoOTA.setPassword("OTA-pasw"); // Set a strong password for OTA updates ArduinoOTA.onStart([]() { + // Deinitialize camera + if (cam.deinit() != ESP_OK) { + Serial.println("Error deinitializing the camera!"); + }else { + Serial.println("Deinitializing the camera!"); + } String type; if (ArduinoOTA.getCommand() == U_FLASH) { type = "sketch"; From 56c187d1dba80eb64f27b5f6ebb9ec119dfae554 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 18:35:02 +0300 Subject: [PATCH 12/21] Update OV2640.h -added deinit function --- src/OV2640.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/OV2640.h b/src/OV2640.h index 0f1d6de..ff289fb 100644 --- a/src/OV2640.h +++ b/src/OV2640.h @@ -19,6 +19,7 @@ class OV2640 ~OV2640(){ }; esp_err_t init(camera_config_t config); + esp_err_t deinit(void); void run(void); size_t getSize(void); uint8_t *getfb(void); From 8915e6660ea30e93b047bfa22fd3376be00cfea5 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 6 Apr 2025 18:36:29 +0300 Subject: [PATCH 13/21] Update OV2640.cpp -added cam.deinit(); --- src/OV2640.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/OV2640.cpp b/src/OV2640.cpp index 02d04d5..d77ee20 100644 --- a/src/OV2640.cpp +++ b/src/OV2640.cpp @@ -191,3 +191,16 @@ esp_err_t OV2640::init(camera_config_t config) return ESP_OK; } + +esp_err_t OV2640::deinit() { + + esp_err_t err = esp_camera_deinit(); + + if (err != ESP_OK) { + printf("Camera deinit failed with error 0x%x", err); + return err; + } + // ESP_ERROR_CHECK(gpio_install_isr_service(0)); + + return ESP_OK; +} From 110400db97bbdd8cd71f169d8377a4ca95650635 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Mon, 7 Apr 2025 12:47:30 +0300 Subject: [PATCH 14/21] Update esp32_camera_mjpeg_multiclient.ino --- esp32_camera_mjpeg_multiclient.ino | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index c56d0ef..1165aa2 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -709,6 +709,14 @@ void loop() { digitalWrite(RED_LED_PIN, LOW); } + button_check(); // Check for button press and if pressed reboot into AP mode + ArduinoOTA.handle(); // Check for OTA updates + ftpSrv.handleFTP(); + taskYIELD(); + vTaskDelay(pdMS_TO_TICKS(1)); +} + +void button_check() { if (buttonPressed) { buttonPressed = false; // Clear the flag File file = SPIFFS.open("/ap_mode.flag", FILE_WRITE); @@ -720,11 +728,6 @@ void loop() { delay(1000); ESP.restart(); } - - ArduinoOTA.handle(); // Check for OTA updates - ftpSrv.handleFTP(); - taskYIELD(); - vTaskDelay(pdMS_TO_TICKS(1)); } void setupAPMode() { @@ -827,9 +830,11 @@ void wifi_Connect() { WiFi.setHostname(hostname.c_str()); WiFi.begin(ssid.c_str(), password.c_str()); Serial.print("Connecting to WiFi"); + buttonPressed = false; while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print(F(".")); + button_check(); } ip = WiFi.localIP(); Serial.println(F("WiFi connected")); @@ -893,7 +898,7 @@ void load_wifi_Settings() { // OTA setup void ota_setup() { - ArduinoOTA.setHostname("ESPCAM_OTA"); + ArduinoOTA.setHostname(hostname.c_str()); ArduinoOTA.setPassword("OTA-pasw"); // Set a strong password for OTA updates ArduinoOTA.onStart([]() { // Deinitialize camera From 9ad44a49fff8a6496e999dac2f032cffa7420336 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Tue, 8 Apr 2025 19:11:17 +0300 Subject: [PATCH 15/21] Update esp32_camera_mjpeg_multiclient.ino --- esp32_camera_mjpeg_multiclient.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index 1165aa2..be547a2 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -25,7 +25,7 @@ # arduino OTA for firmware upload # some performance tweaks # use of external wifi antena is highly recommended for the esp32cam board -# set "Events Run On: core 0" and "Arduino Run On: core 0" +# set "Events Run On: core 0" and "Arduino Run On: core 1" # set "Erase All Flash Before Sketch Upload: Disabled" to prevent SPIFFS deletion # used the board flash on gpio 4 # used the board led on gpio 33 From 6c9fb5b9e8cb6f0cdc71c463f24f19ffca1fb06b Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Wed, 9 Apr 2025 14:26:32 +0300 Subject: [PATCH 16/21] Update esp32_camera_mjpeg_multiclient.ino --- esp32_camera_mjpeg_multiclient.ino | 32 +++++++++++++++++++----------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index be547a2..d7e638d 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -322,7 +322,8 @@ void mjpegCB(void* pvParameters) { if (SPIFFS.exists("/ap_mode.flag")) { SPIFFS.remove("/ap_mode.flag"); } - delay(1000); + // Deinitialize camera + camDeinit(); ESP.restart(); }); server.on("/rebootAp", HTTP_POST, []() { @@ -333,7 +334,8 @@ void mjpegCB(void* pvParameters) { } file.close(); server.send(200, "application/json", "{\"message\":\"Rebooting into AP mode...\"}"); - delay(1000); + // Deinitialize camera + camDeinit(); ESP.restart(); }); // Serve static files @@ -709,13 +711,22 @@ void loop() { digitalWrite(RED_LED_PIN, LOW); } - button_check(); // Check for button press and if pressed reboot into AP mode - ArduinoOTA.handle(); // Check for OTA updates - ftpSrv.handleFTP(); + button_check(); // Check for button press and if pressed reboot into AP mode + ArduinoOTA.handle(); // Check for OTA updates + ftpSrv.handleFTP(); // Handle FTP server taskYIELD(); vTaskDelay(pdMS_TO_TICKS(1)); } +void camDeinit() { + // Deinitialize camera + if (cam.deinit() != ESP_OK) { + Serial.println("Error deinitializing the camera!"); + } else { + Serial.println("Deinitializing the camera!"); + } +} + void button_check() { if (buttonPressed) { buttonPressed = false; // Clear the flag @@ -725,7 +736,8 @@ void button_check() { return; } file.close(); - delay(1000); + // Deinitialize camera + camDeinit(); ESP.restart(); } } @@ -901,12 +913,8 @@ void ota_setup() { ArduinoOTA.setHostname(hostname.c_str()); ArduinoOTA.setPassword("OTA-pasw"); // Set a strong password for OTA updates ArduinoOTA.onStart([]() { - // Deinitialize camera - if (cam.deinit() != ESP_OK) { - Serial.println("Error deinitializing the camera!"); - }else { - Serial.println("Deinitializing the camera!"); - } + // Deinitialize camera + camDeinit(); String type; if (ArduinoOTA.getCommand() == U_FLASH) { type = "sketch"; From e60f1bb38b6a082f27bf4230ec05add3f1336dbf Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Wed, 9 Apr 2025 15:18:20 +0300 Subject: [PATCH 17/21] Update esp32_camera_mjpeg_multiclient.ino --- esp32_camera_mjpeg_multiclient.ino | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index d7e638d..6624c3b 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -83,6 +83,8 @@ TaskHandle_t camTaskHandle = NULL; TaskHandle_t streamTaskHandle = NULL; // frameSync semaphore is used to prevent streaming buffer as it is replaced with the next frame SemaphoreHandle_t frameSync = NULL; +// camSync semaphore is used to sync access to the camera +SemaphoreHandle_t camSync = NULL; // Queue stores currently connected clients to whom we are streaming QueueHandle_t streamingClients; // We will try to achieve 14 FPS frame rate @@ -256,6 +258,9 @@ void mjpegCB(void* pvParameters) { // Creating frame synchronization semaphore and initializing it frameSync = xSemaphoreCreateBinary(); xSemaphoreGive(frameSync); + // Creating camera synchronization semaphore and initializing it + camSync = xSemaphoreCreateBinary(); + xSemaphoreGive(camSync); // Creating a queue to track all connected clients streamingClients = xQueueCreate(10, sizeof(WiFiClient*)); @@ -378,6 +383,8 @@ void camCB(void* pvParameters) { xLastWakeTime = xTaskGetTickCount(); for (;;) { // Grab a frame from the camera and query its size + // lock access to the camera until we've read the frame + xSemaphoreTake(camSync, portMAX_DELAY); cam.run(); size_t s = cam.getSize(); // If frame size is more that we have previously allocated - request 125% of the current frame space @@ -388,6 +395,7 @@ void camCB(void* pvParameters) { // Copy current frame into local buffer char* b = (char*)cam.getfb(); memcpy(fbs[ifb], b, s); + xSemaphoreGive(camSync); // Let other tasks run and wait until the end of the current frame rate interval (if any time left) taskYIELD(); vTaskDelayUntil(&xLastWakeTime, xFrequency); @@ -499,7 +507,6 @@ void streamCB(void* pvParameters) { 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! @@ -512,8 +519,6 @@ void streamCB(void* pvParameters) { // are serving this frame xSemaphoreTake(frameSync, portMAX_DELAY); - //client->write(HEADER, hdrLen); - //client->write(BOUNDARY, bdrLen); client->write(CTNTTYPE, cntLen); sprintf(buf, "%d\r\n\r\n", camSize); client->write(buf, strlen(buf)); @@ -550,9 +555,11 @@ void handleJPG(void) { if (!client.connected()) return; digitalWrite(FLASH_PIN, HIGH); // flash on for capture jpg + xSemaphoreTake(camSync, portMAX_DELAY); cam.run(); client.write(JHEADER, jhdLen); client.write((char*)cam.getfb(), cam.getSize()); + xSemaphoreGive(camSync); digitalWrite(FLASH_PIN, LOW); } // ==== Handle invalid URL requests ============================================ From 12883967f053f10f46886dbfb87127dfa9e08a6b Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Wed, 9 Apr 2025 15:26:32 +0300 Subject: [PATCH 18/21] Update cam_script.js --- html/cam_script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/html/cam_script.js b/html/cam_script.js index 2895ce9..a216c33 100644 --- a/html/cam_script.js +++ b/html/cam_script.js @@ -10,7 +10,7 @@ const startStream = () => { view.src = streamUrl; viewContainer.style.display = 'flex'; toggleStreamButton.innerHTML = 'Stop Video Stream'; - getJpgButton.disabled = true; // Disable "Save Picture" button + // getJpgButton.disabled = true; // Disable "Save Picture" button }; const stopStream = () => { @@ -19,7 +19,7 @@ const stopStream = () => { view.src = ''; viewContainer.style.display = 'none'; toggleStreamButton.innerHTML = 'Start Video Stream'; - getJpgButton.disabled = false; // Enable "Save Picture" button + // getJpgButton.disabled = false; // Enable "Save Picture" button }; const getJpg = async () => { From 34a5c0ec683481b613c759da35f0d6c2b39653b0 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sun, 18 May 2025 10:18:52 +0300 Subject: [PATCH 19/21] Update : added key debounce 50ms key debounce to prevent unintentional behavior --- esp32_camera_mjpeg_multiclient.ino | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/esp32_camera_mjpeg_multiclient.ino b/esp32_camera_mjpeg_multiclient.ino index 6624c3b..604e90a 100644 --- a/esp32_camera_mjpeg_multiclient.ino +++ b/esp32_camera_mjpeg_multiclient.ino @@ -25,13 +25,13 @@ # arduino OTA for firmware upload # some performance tweaks # use of external wifi antena is highly recommended for the esp32cam board -# set "Events Run On: core 0" and "Arduino Run On: core 1" +# set "Events Run On: core 0" and "Arduino Run On: core 0" # set "Erase All Flash Before Sketch Upload: Disabled" to prevent SPIFFS deletion # used the board flash on gpio 4 # used the board led on gpio 33 # added external led on gpio 2 # added a button to enter AP mode and configure Wifi credentials which are then saved to SPIFFS and loaded on boot along with camera settings - (button is connected though a 220 ohm series rezistor from gpio 13 to 14) + (button is connected through a 220 ohm series rezistor from gpio 13 to 14) */ // ESP32 has two cores: APPlication core and PROcess core (the one that runs ESP32 SDK stack) @@ -59,6 +59,7 @@ #include #include #include "camera_pins.h" +#include unsigned long lastTime = 0; unsigned long timerDelay = 50; @@ -70,7 +71,7 @@ String password; String hostname; OV2640 cam; - +Ticker debounceTicker; WebServer server(80); FtpServer ftpSrv; @@ -633,7 +634,7 @@ void setup() { // config.frame_size = FRAMESIZE_QVGA; config.frame_size = FRAMESIZE_UXGA; config.jpeg_quality = 20; - config.fb_count = 1; + config.fb_count = 2; if (cam.init(config) != ESP_OK) { Serial.println("Error initializing the camera"); @@ -697,11 +698,10 @@ void setup() { ota_setup(); } -volatile bool buttonPressed = false; // Flag to indicate button press - -// ISR to set the flag +// ISR to start debounce timer void handleButtonInterrupt() { - buttonPressed = true; // Set the flag when the interrupt triggers + // Start the debounceTicker + debounceTicker.attach_ms(50, button_check); // check button state after 50ms debouncing time } void loop() { @@ -717,8 +717,7 @@ void loop() { digitalWrite(LED_PIN, HIGH); digitalWrite(RED_LED_PIN, LOW); } - - button_check(); // Check for button press and if pressed reboot into AP mode + ArduinoOTA.handle(); // Check for OTA updates ftpSrv.handleFTP(); // Handle FTP server taskYIELD(); @@ -734,9 +733,12 @@ void camDeinit() { } } +// Check for button press and if pressed reboot into AP mode void button_check() { - if (buttonPressed) { - buttonPressed = false; // Clear the flag + + debounceTicker.detach(); // Stops the debounce ticker + + if (digitalRead(BUTTON_PIN_INPUT) == LOW) { File file = SPIFFS.open("/ap_mode.flag", FILE_WRITE); if (!file) { Serial.println("Failed to create file"); @@ -849,11 +851,9 @@ void wifi_Connect() { WiFi.setHostname(hostname.c_str()); WiFi.begin(ssid.c_str(), password.c_str()); Serial.print("Connecting to WiFi"); - buttonPressed = false; while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print(F(".")); - button_check(); } ip = WiFi.localIP(); Serial.println(F("WiFi connected")); From 3bf9374daddcb6de185ec24f7c76abd298423cbd Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Sat, 24 May 2025 09:30:14 +0300 Subject: [PATCH 20/21] Update index.html --- html/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/html/index.html b/html/index.html index 5187907..719dde1 100644 --- a/html/index.html +++ b/html/index.html @@ -1,7 +1,6 @@ - ESP32 Camera Server From b1c6becb4b020e5d4a17356c0fad6914834fe9c0 Mon Sep 17 00:00:00 2001 From: Janos Raul Szabo Date: Thu, 31 Jul 2025 11:31:11 +0300 Subject: [PATCH 21/21] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cae946b..2c22639 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ESP32 MJPEG Multiclient Streaming Server -# Working example of this fork you can check out [here](http://vlaha-41.go.ro:50800). +# Working example of this fork you can check out [here](http://sha256-mining.go.ro:50800). This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM or ESP-EYE modules.