From 158575affe1545ea893a24b59693bd3efd3652a9 Mon Sep 17 00:00:00 2001 From: 15498th Date: Fri, 17 Dec 2021 00:14:44 +0300 Subject: [PATCH 1/8] Add framerate limit, print delay in debug dump. Add global variable and #define in config to set default. Add new parameter in /control handler and preferences. --- app_httpd.cpp | 12 +++++++++--- esp32-cam-webserver.ino | 6 ++++++ myconfig.sample.h | 3 +++ storage.cpp | 3 +++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/app_httpd.cpp b/app_httpd.cpp index 5b4c8bd..8280983 100644 --- a/app_httpd.cpp +++ b/app_httpd.cpp @@ -52,6 +52,7 @@ extern int8_t streamCount; extern unsigned long streamsServed; extern unsigned long imagesServed; extern int myRotation; +extern int framerateLimit; extern int lampVal; extern bool autoLamp; extern bool filesystem; @@ -274,13 +275,16 @@ static esp_err_t stream_handler(httpd_req_t *req){ break; } int64_t frame_time = esp_timer_get_time() - last_frame; - last_frame = esp_timer_get_time();; frame_time /= 1000; + int32_t frame_delay = (framerateLimit > frame_time) ? framerateLimit - frame_time : 0; + delay(frame_delay); + if (debugData) { - Serial.printf("MJPG: %uB %ums (%.1ffps)\r\n", + Serial.printf("MJPG: %uB %ums, delay: %ums, framerate (%.1ffps)\r\n", (uint32_t)(_jpg_buf_len), - (uint32_t)frame_time, 1000.0 / (uint32_t)frame_time); + (uint32_t)frame_time, frame_delay, 1000.0 / (uint32_t)(frame_time + frame_delay)); } + last_frame = esp_timer_get_time(); } streamsServed++; @@ -355,6 +359,7 @@ static esp_err_t cmd_handler(httpd_req_t *req){ else if(!strcmp(variable, "wb_mode")) res = s->set_wb_mode(s, val); else if(!strcmp(variable, "ae_level")) res = s->set_ae_level(s, val); else if(!strcmp(variable, "rotate")) myRotation = val; + else if(!strcmp(variable, "framerate_limit")) framerateLimit = val; else if(!strcmp(variable, "autolamp") && (lampVal != -1)) { autoLamp = val; if (autoLamp) { @@ -410,6 +415,7 @@ static esp_err_t status_handler(httpd_req_t *req){ *p++ = '{'; p+=sprintf(p, "\"lamp\":%d,", lampVal); p+=sprintf(p, "\"autolamp\":%d,", autoLamp); + p+=sprintf(p, "\"framerate_limit\":%d,", framerateLimit); p+=sprintf(p, "\"framesize\":%u,", s->status.framesize); p+=sprintf(p, "\"quality\":%u,", s->status.quality); p+=sprintf(p, "\"brightness\":%d,", s->status.brightness); diff --git a/esp32-cam-webserver.ino b/esp32-cam-webserver.ino index 507030d..50a5dc2 100644 --- a/esp32-cam-webserver.ino +++ b/esp32-cam-webserver.ino @@ -147,6 +147,12 @@ char myVer[] PROGMEM = __DATE__ " @ " __TIME__; #endif int myRotation = CAM_ROTATION; +// minimal frame duration in ms, effectively 1/maxFPS +#if !defined(FRAMERATE_LIMIT) + #define FRAMERATE_LIMIT 0 +#endif +int framerateLimit = FRAMERATE_LIMIT; + // Illumination LAMP and status LED #if defined(LAMP_DISABLE) int lampVal = -1; // lamp is disabled in config diff --git a/myconfig.sample.h b/myconfig.sample.h index 215f9ec..f40dcdb 100644 --- a/myconfig.sample.h +++ b/myconfig.sample.h @@ -143,6 +143,9 @@ struct station stationList[] = {{"ssid1", "pass1", true}, // Browser Rotation (one of: -90,0,90, default 0) // #define CAM_ROTATION 0 +// Minimal frame duration in ms, used to limit max FPS +// max_fps = 1000/framerate_limit +// #define FRAMERATE_LIMIT 500 /* * Additional Features diff --git a/storage.cpp b/storage.cpp index 8ebc7fa..c463c97 100644 --- a/storage.cpp +++ b/storage.cpp @@ -7,6 +7,7 @@ extern void flashLED(int flashtime); extern int myRotation; // Rotation extern int lampVal; // The current Lamp value extern int autoLamp; // Automatic lamp mode +extern int framerateLimit; // Minimal frame duration /* * Useful utility when debugging... @@ -90,6 +91,7 @@ void loadPrefs(fs::FS &fs){ // process all the settings lampVal = jsonExtract(prefs, "lamp").toInt(); autoLamp = jsonExtract(prefs, "autolamp").toInt(); + framerateLimit = jsonExtract(prefs, "framerate_limit").toInt(); s->set_framesize(s, (framesize_t)jsonExtract(prefs, "framesize").toInt()); s->set_quality(s, jsonExtract(prefs, "quality").toInt()); s->set_brightness(s, jsonExtract(prefs, "brightness").toInt()); @@ -136,6 +138,7 @@ void savePrefs(fs::FS &fs){ *p++ = '{'; p+=sprintf(p, "\"lamp\":%i,", lampVal); p+=sprintf(p, "\"autolamp\":%u,", autoLamp); + p+=sprintf(p, "\"framerate_limit\":%d,", framerateLimit); p+=sprintf(p, "\"framesize\":%u,", s->status.framesize); p+=sprintf(p, "\"quality\":%u,", s->status.quality); p+=sprintf(p, "\"brightness\":%d,", s->status.brightness); From 1cda0e2ef71ae46fa9c0768c07b644c10c914020 Mon Sep 17 00:00:00 2001 From: 15498th Date: Fri, 17 Dec 2021 00:15:01 +0300 Subject: [PATCH 2/8] Add max fps control in full index page for ovh2640. --- index_ov2640.h | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/index_ov2640.h b/index_ov2640.h index f923a21..49a4980 100644 --- a/index_ov2640.h +++ b/index_ov2640.h @@ -237,6 +237,20 @@ const uint8_t index_ov2640_html[] = R"=====( +
+ + +
@@ -296,6 +310,7 @@ const uint8_t index_ov2640_html[] = R"=====( const savePrefsButton = document.getElementById('save_prefs') const clearPrefsButton = document.getElementById('clear_prefs') const rebootButton = document.getElementById('reboot') + const framerateLimit = document.getElementById('framerate_limit') const hide = el => { el.classList.add('hidden') @@ -359,6 +374,8 @@ const uint8_t index_ov2640_html[] = R"=====( } else if(el.id === "rotate"){ rotate.value = value; applyRotation(); + } else if(el.id === "framerate_limit"){ + framerate_limit.value = value; } else if(el.id === "stream_url"){ streamURL = value; viewerURL = value + 'view'; @@ -562,6 +579,10 @@ const uint8_t index_ov2640_html[] = R"=====( updateConfig(framesize) } + framerateLimit.onchange = () => { + updateConfig(framerateLimit) + } + swapButton.onclick = () => { window.open('/?view=simple','_self'); } From a63bcc35c81a9421f094a8336805edbdda5e1a31 Mon Sep 17 00:00:00 2001 From: 15498th Date: Fri, 17 Dec 2021 00:15:05 +0300 Subject: [PATCH 3/8] Add max fps control for ov3660 index page. --- index_ov3660.h | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/index_ov3660.h b/index_ov3660.h index eb7ca74..ef33baa 100644 --- a/index_ov3660.h +++ b/index_ov3660.h @@ -251,6 +251,20 @@ const uint8_t index_ov3660_html[] = R"=====(
+
+ + +
@@ -310,6 +324,7 @@ const uint8_t index_ov3660_html[] = R"=====( const savePrefsButton = document.getElementById('save_prefs') const clearPrefsButton = document.getElementById('clear_prefs') const rebootButton = document.getElementById('reboot') + const framerateLimit = document.getElementById('framerate_limit') const hide = el => { el.classList.add('hidden') @@ -371,6 +386,8 @@ const uint8_t index_ov3660_html[] = R"=====( } else if(el.id === "rotate"){ rotate.value = value; applyRotation(); + } else if(el.id === "framerate_limit"){ + framerate_limit.value = value; } else if(el.id === "stream_url"){ streamURL = value; viewerURL = value + 'view'; @@ -571,6 +588,10 @@ const uint8_t index_ov3660_html[] = R"=====( updateConfig(framesize) } + framerateLimit.onchange = () => { + updateConfig(framerateLimit) + } + swapButton.onclick = () => { window.open('/?view=simple','_self'); } From 69888bd6d81b86aa7145054e03797a498d618869 Mon Sep 17 00:00:00 2001 From: 15498th Date: Sat, 18 Dec 2021 14:07:23 +0300 Subject: [PATCH 4/8] Update API documentation. --- API.md | 1 + 1 file changed, 1 insertion(+) diff --git a/API.md b/API.md index 8e0be09..9a434f2 100644 --- a/API.md +++ b/API.md @@ -26,6 +26,7 @@ Call `/control?var=&val=` with a settings key and value to set camera ``` lamp - Lamp value in percent; integer, 0 - 100 (-1 = disabled) framesize - See below +framerate_limit - Minimal frame duration in ms, used to limit max FPS. Must be positive integer quality - 10 to 63 (ov3660: 4 to 10) contrast - -2 to 2 (ov3660: -3 to 3) brightness - -2 to 2 (ov3660: -3 to 3) From 721e825daaaca67c274fd185ad1daf2862b09f39 Mon Sep 17 00:00:00 2001 From: 15498th Date: Sat, 18 Dec 2021 14:29:43 +0300 Subject: [PATCH 5/8] Use better name for framerate limit variables. --- API.md | 2 +- app_httpd.cpp | 8 ++++---- esp32-cam-webserver.ino | 6 +++--- index_ov2640.h | 16 ++++++++-------- index_ov3660.h | 16 ++++++++-------- myconfig.sample.h | 4 ++-- storage.cpp | 6 +++--- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/API.md b/API.md index 9a434f2..f7d1640 100644 --- a/API.md +++ b/API.md @@ -26,7 +26,7 @@ Call `/control?var=&val=` with a settings key and value to set camera ``` lamp - Lamp value in percent; integer, 0 - 100 (-1 = disabled) framesize - See below -framerate_limit - Minimal frame duration in ms, used to limit max FPS. Must be positive integer +min_frame_time - Minimal frame duration in ms, used to limit max FPS. Must be positive integer quality - 10 to 63 (ov3660: 4 to 10) contrast - -2 to 2 (ov3660: -3 to 3) brightness - -2 to 2 (ov3660: -3 to 3) diff --git a/app_httpd.cpp b/app_httpd.cpp index 8280983..2026c4b 100644 --- a/app_httpd.cpp +++ b/app_httpd.cpp @@ -52,7 +52,7 @@ extern int8_t streamCount; extern unsigned long streamsServed; extern unsigned long imagesServed; extern int myRotation; -extern int framerateLimit; +extern int minFrameTime; extern int lampVal; extern bool autoLamp; extern bool filesystem; @@ -276,7 +276,7 @@ static esp_err_t stream_handler(httpd_req_t *req){ } int64_t frame_time = esp_timer_get_time() - last_frame; frame_time /= 1000; - int32_t frame_delay = (framerateLimit > frame_time) ? framerateLimit - frame_time : 0; + int32_t frame_delay = (minFrameTime > frame_time) ? minFrameTime - frame_time : 0; delay(frame_delay); if (debugData) { @@ -359,7 +359,7 @@ static esp_err_t cmd_handler(httpd_req_t *req){ else if(!strcmp(variable, "wb_mode")) res = s->set_wb_mode(s, val); else if(!strcmp(variable, "ae_level")) res = s->set_ae_level(s, val); else if(!strcmp(variable, "rotate")) myRotation = val; - else if(!strcmp(variable, "framerate_limit")) framerateLimit = val; + else if(!strcmp(variable, "min_frame_time")) minFrameTime = val; else if(!strcmp(variable, "autolamp") && (lampVal != -1)) { autoLamp = val; if (autoLamp) { @@ -415,7 +415,7 @@ static esp_err_t status_handler(httpd_req_t *req){ *p++ = '{'; p+=sprintf(p, "\"lamp\":%d,", lampVal); p+=sprintf(p, "\"autolamp\":%d,", autoLamp); - p+=sprintf(p, "\"framerate_limit\":%d,", framerateLimit); + p+=sprintf(p, "\"min_frame_time\":%d,", minFrameTime); p+=sprintf(p, "\"framesize\":%u,", s->status.framesize); p+=sprintf(p, "\"quality\":%u,", s->status.quality); p+=sprintf(p, "\"brightness\":%d,", s->status.brightness); diff --git a/esp32-cam-webserver.ino b/esp32-cam-webserver.ino index 50a5dc2..06a0eb8 100644 --- a/esp32-cam-webserver.ino +++ b/esp32-cam-webserver.ino @@ -148,10 +148,10 @@ char myVer[] PROGMEM = __DATE__ " @ " __TIME__; int myRotation = CAM_ROTATION; // minimal frame duration in ms, effectively 1/maxFPS -#if !defined(FRAMERATE_LIMIT) - #define FRAMERATE_LIMIT 0 +#if !defined(MIN_FRAME_TIME) + #define MIN_FRAME_TIME 0 #endif -int framerateLimit = FRAMERATE_LIMIT; +int minFrameTime = MIN_FRAME_TIME; // Illumination LAMP and status LED #if defined(LAMP_DISABLE) diff --git a/index_ov2640.h b/index_ov2640.h index 49a4980..1195f70 100644 --- a/index_ov2640.h +++ b/index_ov2640.h @@ -237,9 +237,9 @@ const uint8_t index_ov2640_html[] = R"=====(
-
- - @@ -310,7 +310,7 @@ const uint8_t index_ov2640_html[] = R"=====( const savePrefsButton = document.getElementById('save_prefs') const clearPrefsButton = document.getElementById('clear_prefs') const rebootButton = document.getElementById('reboot') - const framerateLimit = document.getElementById('framerate_limit') + const minFrameTime = document.getElementById('min_frame_time') const hide = el => { el.classList.add('hidden') @@ -374,8 +374,8 @@ const uint8_t index_ov2640_html[] = R"=====( } else if(el.id === "rotate"){ rotate.value = value; applyRotation(); - } else if(el.id === "framerate_limit"){ - framerate_limit.value = value; + } else if(el.id === "min_frame_time"){ + min_frame_time.value = value; } else if(el.id === "stream_url"){ streamURL = value; viewerURL = value + 'view'; @@ -579,8 +579,8 @@ const uint8_t index_ov2640_html[] = R"=====( updateConfig(framesize) } - framerateLimit.onchange = () => { - updateConfig(framerateLimit) + minFrameTime.onchange = () => { + updateConfig(minFrameTime) } swapButton.onclick = () => { diff --git a/index_ov3660.h b/index_ov3660.h index ef33baa..e28ceaa 100644 --- a/index_ov3660.h +++ b/index_ov3660.h @@ -251,9 +251,9 @@ const uint8_t index_ov3660_html[] = R"=====(
-
- - @@ -324,7 +324,7 @@ const uint8_t index_ov3660_html[] = R"=====( const savePrefsButton = document.getElementById('save_prefs') const clearPrefsButton = document.getElementById('clear_prefs') const rebootButton = document.getElementById('reboot') - const framerateLimit = document.getElementById('framerate_limit') + const minFrameTime = document.getElementById('min_frame_time') const hide = el => { el.classList.add('hidden') @@ -386,8 +386,8 @@ const uint8_t index_ov3660_html[] = R"=====( } else if(el.id === "rotate"){ rotate.value = value; applyRotation(); - } else if(el.id === "framerate_limit"){ - framerate_limit.value = value; + } else if(el.id === "min_frame_time"){ + min_frame_time.value = value; } else if(el.id === "stream_url"){ streamURL = value; viewerURL = value + 'view'; @@ -588,8 +588,8 @@ const uint8_t index_ov3660_html[] = R"=====( updateConfig(framesize) } - framerateLimit.onchange = () => { - updateConfig(framerateLimit) + minFrameTime.onchange = () => { + updateConfig(minFrameTime) } swapButton.onclick = () => { diff --git a/myconfig.sample.h b/myconfig.sample.h index f40dcdb..5958b62 100644 --- a/myconfig.sample.h +++ b/myconfig.sample.h @@ -144,8 +144,8 @@ struct station stationList[] = {{"ssid1", "pass1", true}, // #define CAM_ROTATION 0 // Minimal frame duration in ms, used to limit max FPS -// max_fps = 1000/framerate_limit -// #define FRAMERATE_LIMIT 500 +// max_fps = 1000/min_frame_time +// #define MIN_FRAME_TIME 500 /* * Additional Features diff --git a/storage.cpp b/storage.cpp index c463c97..1e202f1 100644 --- a/storage.cpp +++ b/storage.cpp @@ -7,7 +7,7 @@ extern void flashLED(int flashtime); extern int myRotation; // Rotation extern int lampVal; // The current Lamp value extern int autoLamp; // Automatic lamp mode -extern int framerateLimit; // Minimal frame duration +extern int minFrameTime; // Minimal frame duration /* * Useful utility when debugging... @@ -91,7 +91,7 @@ void loadPrefs(fs::FS &fs){ // process all the settings lampVal = jsonExtract(prefs, "lamp").toInt(); autoLamp = jsonExtract(prefs, "autolamp").toInt(); - framerateLimit = jsonExtract(prefs, "framerate_limit").toInt(); + minFrameTime = jsonExtract(prefs, "min_frame_time").toInt(); s->set_framesize(s, (framesize_t)jsonExtract(prefs, "framesize").toInt()); s->set_quality(s, jsonExtract(prefs, "quality").toInt()); s->set_brightness(s, jsonExtract(prefs, "brightness").toInt()); @@ -138,7 +138,7 @@ void savePrefs(fs::FS &fs){ *p++ = '{'; p+=sprintf(p, "\"lamp\":%i,", lampVal); p+=sprintf(p, "\"autolamp\":%u,", autoLamp); - p+=sprintf(p, "\"framerate_limit\":%d,", framerateLimit); + p+=sprintf(p, "\"min_frame_time\":%d,", minFrameTime); p+=sprintf(p, "\"framesize\":%u,", s->status.framesize); p+=sprintf(p, "\"quality\":%u,", s->status.quality); p+=sprintf(p, "\"brightness\":%d,", s->status.brightness); From fdd920910e361149da470fff6c90cf66d0c41117 Mon Sep 17 00:00:00 2001 From: 15498th Date: Sat, 18 Dec 2021 14:39:52 +0300 Subject: [PATCH 6/8] Change UI to show the same values of framerate control as used in command interface. --- index_ov2640.h | 18 +++++++++--------- index_ov3660.h | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/index_ov2640.h b/index_ov2640.h index 1195f70..2af2846 100644 --- a/index_ov2640.h +++ b/index_ov2640.h @@ -238,16 +238,16 @@ const uint8_t index_ov2640_html[] = R"=====(
- +
diff --git a/index_ov3660.h b/index_ov3660.h index e28ceaa..d7eab9b 100644 --- a/index_ov3660.h +++ b/index_ov3660.h @@ -252,16 +252,16 @@ const uint8_t index_ov3660_html[] = R"=====(
- +
From 1b836c5479f3ed3e13408b4f502f7cf770752a86 Mon Sep 17 00:00:00 2001 From: 15498th Date: Sun, 19 Dec 2021 00:08:16 +0300 Subject: [PATCH 7/8] Apply framerate limiting delay after serving content boundary. Putting delay after finishing serving the frame causes image to appear in stream with a lag. The reason for this appears to be that browser relies on next frame content boundary in order to determine the end of image. That means that in order for browser to show frame right after receiving it, server needs to send next frame content boundary right after image itself, and delay to limit framerate should be applied after content boundary, not before it. According to https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html sending first frame without content boundary will likely cause its content to be ignored, which seems to be acceptable price for not treating first iteration of the loop as special case. --- app_httpd.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app_httpd.cpp b/app_httpd.cpp index 2026c4b..36654bb 100644 --- a/app_httpd.cpp +++ b/app_httpd.cpp @@ -251,9 +251,6 @@ static esp_err_t stream_handler(httpd_req_t *req){ _jpg_buf = fb->buf; } } - if(res == ESP_OK){ - res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY)); - } if(res == ESP_OK){ size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len); res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen); @@ -261,6 +258,9 @@ static esp_err_t stream_handler(httpd_req_t *req){ if(res == ESP_OK){ res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len); } + if(res == ESP_OK){ + res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY)); + } if(fb){ esp_camera_fb_return(fb); fb = NULL; From 20f4c24dae61f178d3c7da8f7349774ad5f98aa2 Mon Sep 17 00:00:00 2001 From: 15498th Date: Sun, 19 Dec 2021 02:12:10 +0300 Subject: [PATCH 8/8] Fix delay before first frame by sending first request boundary before entering the loop. --- app_httpd.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app_httpd.cpp b/app_httpd.cpp index 36654bb..665de3a 100644 --- a/app_httpd.cpp +++ b/app_httpd.cpp @@ -237,6 +237,10 @@ static esp_err_t stream_handler(httpd_req_t *req){ httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + if(res == ESP_OK){ + res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY)); + } + while(true){ fb = esp_camera_fb_get(); if (!fb) {