Skip to content

Commit 9426f3f

Browse files
easytarget15498th
andauthored
Merge pull request easytarget#195 from 15498th/framerate_limit
* 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. * Add max fps control in full index page for ovh2640. * Add max fps control for ov3660 index page. * Update API documentation. * Use better name for framerate limit variables. * Change UI to show the same values of framerate control as used in command interface. * 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. * Fix delay before first frame by sending first request boundary before entering the loop. Co-authored-by: 15498th <user@localhost> Co-authored-by: Owen Carter <[email protected]>
2 parents d064e88 + cfe81b0 commit 9426f3f

File tree

7 files changed

+69
-7
lines changed

7 files changed

+69
-7
lines changed

API.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Call `/control?var=<key>&val=<val>` with a settings key and value to set camera
2626
```
2727
lamp - Lamp value in percent; integer, 0 - 100 (-1 = disabled)
2828
framesize - See below
29+
min_frame_time - Minimal frame duration in ms, used to limit max FPS. Must be positive integer
2930
quality - 10 to 63 (ov3660: 4 to 10)
3031
contrast - -2 to 2 (ov3660: -3 to 3)
3132
brightness - -2 to 2 (ov3660: -3 to 3)

app_httpd.cpp

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ extern int8_t streamCount;
5252
extern unsigned long streamsServed;
5353
extern unsigned long imagesServed;
5454
extern int myRotation;
55+
extern int minFrameTime;
5556
extern int lampVal;
5657
extern bool autoLamp;
5758
extern bool filesystem;
@@ -249,6 +250,10 @@ static esp_err_t stream_handler(httpd_req_t *req){
249250

250251
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
251252

253+
if(res == ESP_OK){
254+
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
255+
}
256+
252257
while(true){
253258
fb = esp_camera_fb_get();
254259
if (!fb) {
@@ -263,16 +268,16 @@ static esp_err_t stream_handler(httpd_req_t *req){
263268
_jpg_buf = fb->buf;
264269
}
265270
}
266-
if(res == ESP_OK){
267-
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
268-
}
269271
if(res == ESP_OK){
270272
size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
271273
res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
272274
}
273275
if(res == ESP_OK){
274276
res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
275277
}
278+
if(res == ESP_OK){
279+
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
280+
}
276281
if(fb){
277282
esp_camera_fb_return(fb);
278283
fb = NULL;
@@ -287,13 +292,16 @@ static esp_err_t stream_handler(httpd_req_t *req){
287292
break;
288293
}
289294
int64_t frame_time = esp_timer_get_time() - last_frame;
290-
last_frame = esp_timer_get_time();;
291295
frame_time /= 1000;
296+
int32_t frame_delay = (minFrameTime > frame_time) ? minFrameTime - frame_time : 0;
297+
delay(frame_delay);
298+
292299
if (debugData) {
293-
Serial.printf("MJPG: %uB %ums (%.1ffps)\r\n",
300+
Serial.printf("MJPG: %uB %ums, delay: %ums, framerate (%.1ffps)\r\n",
294301
(uint32_t)(_jpg_buf_len),
295-
(uint32_t)frame_time, 1000.0 / (uint32_t)frame_time);
302+
(uint32_t)frame_time, frame_delay, 1000.0 / (uint32_t)(frame_time + frame_delay));
296303
}
304+
last_frame = esp_timer_get_time();
297305
}
298306

299307
streamsServed++;
@@ -369,6 +377,7 @@ static esp_err_t cmd_handler(httpd_req_t *req){
369377
else if(!strcmp(variable, "wb_mode")) res = s->set_wb_mode(s, val);
370378
else if(!strcmp(variable, "ae_level")) res = s->set_ae_level(s, val);
371379
else if(!strcmp(variable, "rotate")) myRotation = val;
380+
else if(!strcmp(variable, "min_frame_time")) minFrameTime = val;
372381
else if(!strcmp(variable, "autolamp") && (lampVal != -1)) {
373382
autoLamp = val;
374383
if (autoLamp) {
@@ -425,6 +434,7 @@ static esp_err_t status_handler(httpd_req_t *req){
425434
*p++ = '{';
426435
p+=sprintf(p, "\"lamp\":%d,", lampVal);
427436
p+=sprintf(p, "\"autolamp\":%d,", autoLamp);
437+
p+=sprintf(p, "\"min_frame_time\":%d,", minFrameTime);
428438
p+=sprintf(p, "\"framesize\":%u,", s->status.framesize);
429439
p+=sprintf(p, "\"quality\":%u,", s->status.quality);
430440
p+=sprintf(p, "\"xclk\":%u,", xclk);

esp32-cam-webserver.ino

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ char myVer[] PROGMEM = __DATE__ " @ " __TIME__;
150150
#endif
151151
int myRotation = CAM_ROTATION;
152152

153+
// minimal frame duration in ms, effectively 1/maxFPS
154+
#if !defined(MIN_FRAME_TIME)
155+
#define MIN_FRAME_TIME 0
156+
#endif
157+
int minFrameTime = MIN_FRAME_TIME;
158+
153159
// Illumination LAMP and status LED
154160
#if defined(LAMP_DISABLE)
155161
int lampVal = -1; // lamp is disabled in config

index_ov2640.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,20 @@ const uint8_t index_ov2640_html[] = R"=====(<!doctype html>
244244
<label class="slider" for="colorbar"></label>
245245
</div>
246246
</div>
247+
<div class="input-group" id="min_frame_time-group">
248+
<label for="min_frame_time">Frame Duration Limit</label>
249+
<select id="min_frame_time" class="default-action">
250+
<option value="3333">3333ms (0.3fps)</option>
251+
<option value="2000">2000ms (0.5fps)</option>
252+
<option value="1000">1000ms (1fps)</option>
253+
<option value="500">500ms (2fps)</option>
254+
<option value="333">333ms (3fps)</option>
255+
<option value="200">200ms (5fps)</option>
256+
<option value="100">100ms (10fps)</option>
257+
<option value="50">50ms (20fps)</option>
258+
<option value="0" selected="selected">Disabled</option>
259+
</select>
260+
</div>
247261
<div class="input-group" id="preferences-group">
248262
<label for="prefs" style="line-height: 2em;">Preferences</label>
249263
<button id="reboot" title="Reboot the camera module">Reboot</button>
@@ -304,6 +318,7 @@ const uint8_t index_ov2640_html[] = R"=====(<!doctype html>
304318
const savePrefsButton = document.getElementById('save_prefs')
305319
const clearPrefsButton = document.getElementById('clear_prefs')
306320
const rebootButton = document.getElementById('reboot')
321+
const minFrameTime = document.getElementById('min_frame_time')
307322

308323
const hide = el => {
309324
el.classList.add('hidden')
@@ -367,6 +382,8 @@ const uint8_t index_ov2640_html[] = R"=====(<!doctype html>
367382
} else if(el.id === "rotate"){
368383
rotate.value = value;
369384
applyRotation();
385+
} else if(el.id === "min_frame_time"){
386+
min_frame_time.value = value;
370387
} else if(el.id === "stream_url"){
371388
streamURL = value;
372389
viewerURL = value + 'view';
@@ -571,6 +588,9 @@ const uint8_t index_ov2640_html[] = R"=====(<!doctype html>
571588
updateConfig(framesize)
572589
}
573590

591+
minFrameTime.onchange = () => {
592+
updateConfig(minFrameTime)
593+
574594
xclk.onchange = () => {
575595
console.log("xclk:" , xclk);
576596
updateConfig(xclk)

index_ov3660.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,20 @@ const uint8_t index_ov3660_html[] = R"=====(<!doctype html>
258258
<label class="slider" for="colorbar"></label>
259259
</div>
260260
</div>
261+
<div class="input-group" id="min_frame_time-group">
262+
<label for="min_frame_time">Frame Duration Limit</label>
263+
<select id="min_frame_time" class="default-action">
264+
<option value="3333">3333ms (0.3fps)</option>
265+
<option value="2000">2000ms (0.5fps)</option>
266+
<option value="1000">1000ms (1fps)</option>
267+
<option value="500">500ms (2fps)</option>
268+
<option value="333">333ms (3fps)</option>
269+
<option value="200">200ms (5fps)</option>
270+
<option value="100">100ms (10fps)</option>
271+
<option value="50">50ms (20fps)</option>
272+
<option value="0" selected="selected">Disabled</option>
273+
</select>
274+
</div>
261275
<div class="input-group" id="preferences-group">
262276
<label for="prefs" style="line-height: 2em;">Preferences</label>
263277
<button id="reboot" title="Reboot the camera module">Reboot</button>
@@ -318,6 +332,7 @@ const uint8_t index_ov3660_html[] = R"=====(<!doctype html>
318332
const savePrefsButton = document.getElementById('save_prefs')
319333
const clearPrefsButton = document.getElementById('clear_prefs')
320334
const rebootButton = document.getElementById('reboot')
335+
const minFrameTime = document.getElementById('min_frame_time')
321336

322337
const hide = el => {
323338
el.classList.add('hidden')
@@ -379,6 +394,8 @@ const uint8_t index_ov3660_html[] = R"=====(<!doctype html>
379394
} else if(el.id === "rotate"){
380395
rotate.value = value;
381396
applyRotation();
397+
} else if(el.id === "min_frame_time"){
398+
min_frame_time.value = value;
382399
} else if(el.id === "stream_url"){
383400
streamURL = value;
384401
viewerURL = value + 'view';
@@ -580,6 +597,9 @@ const uint8_t index_ov3660_html[] = R"=====(<!doctype html>
580597
updateConfig(framesize)
581598
}
582599

600+
minFrameTime.onchange = () => {
601+
updateConfig(minFrameTime)
602+
583603
xclk.onchange = () => {
584604
console.log("xclk:" , xclk);
585605
updateConfig(xclk)

myconfig.sample.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ struct station stationList[] = {{"ssid1", "pass1", true},
146146
// Browser Rotation (one of: -90,0,90, default 0)
147147
// #define CAM_ROTATION 0
148148

149+
// Minimal frame duration in ms, used to limit max FPS
150+
// max_fps = 1000/min_frame_time
151+
// #define MIN_FRAME_TIME 500
149152

150153
/*
151154
* Additional Features

storage.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ void loadPrefs(fs::FS &fs){
9090
sensor_t * s = esp_camera_sensor_get();
9191
// process all the settings
9292
lampVal = jsonExtract(prefs, "lamp").toInt();
93-
if (jsonExtract(prefs, "autolamp").toInt() == 0) autoLamp = false; else autoLamp = true;
93+
if (jsonExtract(prefs, "autolamp").toInt() == 0) autoLamp = false; else autoLamp = true;
94+
minFrameTime = jsonExtract(prefs, "min_frame_time").toInt();
9495
s->set_framesize(s, (framesize_t)jsonExtract(prefs, "framesize").toInt());
9596
s->set_quality(s, jsonExtract(prefs, "quality").toInt());
9697
int xclkPref = jsonExtract(prefs, "xclk").toInt();
@@ -140,6 +141,7 @@ void savePrefs(fs::FS &fs){
140141
*p++ = '{';
141142
p+=sprintf(p, "\"lamp\":%i,", lampVal);
142143
p+=sprintf(p, "\"autolamp\":%u,", autoLamp);
144+
p+=sprintf(p, "\"min_frame_time\":%d,", minFrameTime);
143145
p+=sprintf(p, "\"framesize\":%u,", s->status.framesize);
144146
p+=sprintf(p, "\"quality\":%u,", s->status.quality);
145147
p+=sprintf(p, "\"xclk\":%u,", xclk);

0 commit comments

Comments
 (0)