Rewrite the streaming back to http while keeping AsyncWebServer in charge of API, remove OTA update time limit (#19)

This commit is contained in:
Zdzislaw Goik 2022-07-13 23:43:56 +02:00 committed by GitHub
parent 08bdb6d378
commit 78ac54bff8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 210 additions and 298 deletions

View File

@ -7,6 +7,7 @@
#include "cameraHandler.h"
extern char *MDSNTrackerName;
extern int STREAM_SERVER_PORT;
extern int CONTROL_SERVER_PORT;
extern OpenIris::LEDManager ledManager;
extern OpenIris::StateManager stateManager;

View File

@ -6,8 +6,7 @@ namespace OpenIris
class OTA
{
private:
unsigned long bootTimestamp = 0;
bool isOtaEnabled = true;
bool isOTAEnabled = false;
public:
void SetupOTA(const char *OTAPassword, uint16_t OTAServerPort)
@ -20,6 +19,7 @@ namespace OpenIris
return;
}
ArduinoOTA.setPort(OTAServerPort);
isOTAEnabled = true;
ArduinoOTA
.onStart([]()
@ -44,20 +44,12 @@ namespace OpenIris
log_i("Starting up basic OTA server");
log_i("OTA will be live for 30s, after which it will be disabled until restart");
ArduinoOTA.begin();
bootTimestamp = millis();
}
void HandleOTAUpdate()
{
if (isOtaEnabled)
if (isOTAEnabled)
{
if (bootTimestamp + 30000 < millis())
{
// we're disabling ota after first 30sec so that nothing bad happens during playtime
isOtaEnabled = false;
log_i("From now on, OTA is disabled");
return;
}
ArduinoOTA.handle();
}
}

View File

@ -0,0 +1,22 @@
#pragma once
#define PART_BOUNDARY "123456789000000000000987654321"
#include <Arduino.h>
#include "esp_camera.h"
#include "esp_http_server.h"
namespace OpenIris
{
namespace StreamHelpers
{
esp_err_t stream(httpd_req_t *req);
}
class StreamServer
{
private:
httpd_handle_t camera_stream = nullptr;
public:
int startStreamServer();
};
}

View File

@ -1,163 +0,0 @@
#pragma once
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "esp_camera.h"
#define PART_BOUNDARY "123456789000000000000987654321"
namespace OpenIris
{
namespace WebServerHelpers
{
static const char *STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char *STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char *STREAM_PART = "Content-Type: %s\r\nContent-Length: %u\r\n\r\n";
static const char *JPG_CONTENT_TYPE = "image/jpeg";
}
typedef struct
{
camera_fb_t *fb;
size_t index;
} camera_frame_t;
class AsyncJpegStreamResponse : public AsyncAbstractResponse
{
private:
camera_frame_t _frame;
size_t _index;
size_t _jpg_buf_len;
uint8_t *_jpg_buf;
long lastAsyncRequest;
public:
AsyncJpegStreamResponse()
{
_callback = nullptr;
_code = 200;
_contentLength = 0;
_contentType = OpenIris::WebServerHelpers::STREAM_CONTENT_TYPE;
_sendContentLength = false;
_chunked = true;
_index = 0;
_jpg_buf_len = 0;
_jpg_buf = NULL;
lastAsyncRequest = 0;
memset(&_frame, 0, sizeof(camera_frame_t));
}
~AsyncJpegStreamResponse()
{
if (_frame.fb)
{
if (_frame.fb->format != PIXFORMAT_JPEG)
{
free(_jpg_buf);
}
esp_camera_fb_return(_frame.fb);
}
}
bool _sourceValid() const
{
return true;
}
virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override
{
size_t ret = _content(buf, maxLen, _index);
if (ret != RESPONSE_TRY_AGAIN)
{
_index += ret;
}
return ret;
}
size_t _content(uint8_t *buffer, size_t maxLen, size_t index)
{
if (!_frame.fb || _frame.index == _jpg_buf_len)
{
if (index && _frame.fb)
{
long end = millis();
int fp = (end - lastAsyncRequest);
log_d("Size: %uKB, Time: %ums (%ifps)\n", _jpg_buf_len / 1024, fp, 1000 / fp);
lastAsyncRequest = end;
if (_frame.fb->format != PIXFORMAT_JPEG)
{
free(_jpg_buf);
}
esp_camera_fb_return(_frame.fb);
_frame.fb = NULL;
_jpg_buf_len = 0;
_jpg_buf = NULL;
}
if (maxLen < (strlen(OpenIris::WebServerHelpers::STREAM_BOUNDARY) + strlen(OpenIris::WebServerHelpers::STREAM_PART) + strlen(OpenIris::WebServerHelpers::JPG_CONTENT_TYPE) + 8))
{
// log_w("Not enough space for headers");
return RESPONSE_TRY_AGAIN;
}
// get frame
_frame.index = 0;
_frame.fb = esp_camera_fb_get();
if (_frame.fb == NULL)
{
log_e("Camera frame failed");
return 0;
}
if (_frame.fb->format != PIXFORMAT_JPEG)
{
unsigned long st = millis();
bool jpeg_converted = frame2jpg(_frame.fb, 40, &_jpg_buf, &_jpg_buf_len);
if (!jpeg_converted)
{
log_e("JPEG compression failed");
esp_camera_fb_return(_frame.fb);
_frame.fb = NULL;
_jpg_buf_len = 0;
_jpg_buf = NULL;
return 0;
}
log_i("JPEG: %lums, %uB", millis() - st, _jpg_buf_len);
}
else
{
_jpg_buf_len = _frame.fb->len;
_jpg_buf = _frame.fb->buf;
}
// send boundary
size_t blen = 0;
if (index)
{
blen = strlen(OpenIris::WebServerHelpers::STREAM_BOUNDARY);
memcpy(buffer, OpenIris::WebServerHelpers::STREAM_BOUNDARY, blen);
buffer += blen;
}
// send header
size_t hlen = sprintf((char *)buffer, OpenIris::WebServerHelpers::STREAM_PART, OpenIris::WebServerHelpers::JPG_CONTENT_TYPE, _jpg_buf_len);
buffer += hlen;
// send frame
hlen = maxLen - hlen - blen;
if (hlen > _jpg_buf_len)
{
maxLen -= hlen - _jpg_buf_len;
hlen = _jpg_buf_len;
}
memcpy(buffer, _jpg_buf, hlen);
_frame.index += hlen;
return maxLen;
}
size_t available = _jpg_buf_len - _frame.index;
if (maxLen > available)
{
maxLen = available;
}
memcpy(buffer, _jpg_buf + _frame.index, maxLen);
_frame.index += maxLen;
return maxLen;
}
};
}

View File

@ -1,20 +0,0 @@
#pragma once
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "esp_camera.h"
namespace OpenIris
{
class HTTPDHandler
{
private:
void command_handler(AsyncWebServerRequest *request);
void stream_handler(AsyncWebServerRequest *request);
void roi_crop_command_handler(AsyncWebServerRequest *request);
AsyncWebServer *server;
public:
HTTPDHandler();
void startStreamServer();
};
}

View File

@ -0,0 +1,21 @@
#pragma once
#define WEBSERVER_H
#define HTTP_ANY 0b01111111
#define HTTP_GET 0b00000001
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
namespace OpenIris
{
class APIServer
{
private:
void command_handler(AsyncWebServerRequest *request);
AsyncWebServer *server;
public:
APIServer();
void startAPIServer();
};
}

View File

@ -22,9 +22,9 @@ int OpenIris::CameraHandler::setupCamera()
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000; //10000000 stable,
//16500000 optimal,
//20000000 max fps
config.xclk_freq_hz = 20000000; // 10000000 stable,
// 16500000 optimal,
// 20000000 max fps
config.pixel_format = PIXFORMAT_JPEG;
if (psramFound())
@ -52,7 +52,7 @@ int OpenIris::CameraHandler::setupCamera()
camera_sensor->set_contrast(camera_sensor, 2); // -2 to 2
camera_sensor->set_saturation(camera_sensor, -2); // -2 to 2
camera_sensor->set_whitebal(camera_sensor, 1); // 0 = disable , 1 = enable
camera_sensor->set_awb_gain(camera_sensor, 1); // 0 = disable , 1 = enable
camera_sensor->set_awb_gain(camera_sensor, 1); // 0 = disable , 1 = enable
camera_sensor->set_wb_mode(camera_sensor, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home)
camera_sensor->set_exposure_ctrl(camera_sensor, 1); // 0 = disable , 1 = enable
camera_sensor->set_aec2(camera_sensor, 0); // 0 = disable , 1 = enable

View File

@ -5,29 +5,34 @@
#include "MDNSManager.h"
#include "cameraHandler.h"
#include "LEDManager.h"
#include "webServer/webserverHandler.h"
#include "streamServer.h"
#include "webserverHandler.h"
#include "OTA.h"
#include "StateManager.h"
char *MDSNTrackerName = "OpenIrisTracker";
const char *MDSNTrackerName = "OpenIrisTracker";
int STREAM_SERVER_PORT = 80;
int CONTROL_SERVER_PORT = 81;
auto ota = OpenIris::OTA();
auto ledManager = OpenIris::LEDManager(33);
auto cameraHandler = OpenIris::CameraHandler();
auto stateManager = OpenIris::StateManager();
auto httpdHandler = OpenIris::HTTPDHandler();
auto apiServer = OpenIris::APIServer();
auto streamServer = OpenIris::StreamServer();
void setup()
{
Serial.begin(115200);
Serial.setDebugOutput(true);
Serial.println();
ledManager.setupLED();
cameraHandler.setupCamera();
OpenIris::WiFiHandler::setupWifi(ssid, password);
OpenIris::MDNSHandler::setupMDNS();
httpdHandler.startStreamServer();
apiServer.startAPIServer();
streamServer.startStreamServer();
ledManager.on();
ota.SetupOTA(OTAPassword, OTAServerPort);

109
ESP/src/streamServer.cpp Normal file
View File

@ -0,0 +1,109 @@
#include "streamServer.h"
#include "GlobalVars.h"
constexpr static char *STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
constexpr static char *STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
constexpr static char *STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\nX-Timestamp: %d.%06d\r\n\r\n";
esp_err_t OpenIris::StreamHelpers::stream(httpd_req_t *req)
{
long last_request_time = 0;
camera_fb_t *fb = NULL;
struct timeval _timestamp;
esp_err_t res = ESP_OK;
size_t _jpg_buf_len = 0;
uint8_t *_jpg_buf = NULL;
char *part_buf[128];
static int64_t last_frame = 0;
if (!last_frame)
last_frame = esp_timer_get_time();
res = httpd_resp_set_type(req, STREAM_CONTENT_TYPE);
if (res != ESP_OK)
return res;
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin; Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n", "*");
httpd_resp_set_hdr(req, "X-Framerate", "60");
while (true)
{
fb = esp_camera_fb_get();
if (!fb)
{
log_e("Camera capture failed");
res = ESP_FAIL;
}
else
{
_timestamp.tv_sec = fb->timestamp.tv_sec;
_timestamp.tv_usec = fb->timestamp.tv_usec;
_jpg_buf_len = fb->len;
_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, 128, STREAM_PART, _jpg_buf_len, _timestamp.tv_sec, _timestamp.tv_usec);
res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
}
if (res == ESP_OK)
res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
if (fb)
{
esp_camera_fb_return(fb);
fb = NULL;
_jpg_buf = NULL;
}
else if (_jpg_buf)
{
free(_jpg_buf);
_jpg_buf = NULL;
}
if (res != ESP_OK)
break;
long request_end = millis();
long latency = (request_end - last_request_time);
last_request_time = request_end;
log_d("Size: %uKB, Time: %ums (%ifps)\n", _jpg_buf_len / 1024, latency, 1000 / latency);
}
last_frame = 0;
return res;
}
int OpenIris::StreamServer::startStreamServer()
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_uri_handlers = 1;
config.server_port = STREAM_SERVER_PORT;
config.ctrl_port = STREAM_SERVER_PORT;
httpd_uri_t stream_page = {
.uri = "/",
.method = HTTP_GET,
.handler = &OpenIris::StreamHelpers::stream,
.user_ctx = nullptr};
int status = httpd_start(&camera_stream, &config);
if (status != ESP_OK)
return -1;
else
{
httpd_register_uri_handler(camera_stream, &stream_page);
log_d("Stream server initialized");
return 0;
}
}

View File

@ -1,94 +0,0 @@
#include "GlobalVars.h"
#include "webServer/asyncJPGResponse.h"
#include "webServer/webserverHandler.h"
OpenIris::HTTPDHandler::HTTPDHandler()
{
this->server = new AsyncWebServer(STREAM_SERVER_PORT);
}
void OpenIris::HTTPDHandler::startStreamServer()
{
this->server->on(
"/",
HTTP_GET,
std::bind(&OpenIris::HTTPDHandler::stream_handler, this, std::placeholders::_1));
this->server->on(
"/control",
HTTP_GET,
std::bind(&OpenIris::HTTPDHandler::command_handler, this, std::placeholders::_1));
this->server->on(
"/crop-roi",
HTTP_GET,
std::bind(&OpenIris::HTTPDHandler::roi_crop_command_handler, this, std::placeholders::_1));
log_d("Initializing web server");
this->server->begin();
}
void OpenIris::HTTPDHandler::roi_crop_command_handler(AsyncWebServerRequest *request)
{
int offsetX = 0;
int offsetY = 0;
int outputX = 0;
int outputY = 0;
if (request->hasParam("offsetX"))
{
AsyncWebParameter *offsetX_param = request->getParam("offsetX");
offsetX = atoi(offsetX_param->value().c_str());
}
if (request->hasParam("offsetY"))
{
AsyncWebParameter *offsetY_param = request->getParam("offsetY");
offsetY = atoi(offsetY_param->value().c_str());
}
if (request->hasParam("outputX"))
{
AsyncWebParameter *outputX_param = request->getParam("outputX");
outputX = atoi(outputX_param->value().c_str());
}
if (request->hasParam("outputY"))
{
AsyncWebParameter *outputY_param = request->getParam("outputY");
outputY = atoi(outputY_param->value().c_str());
}
if (offsetX != 0 && offsetY != 0 && outputX != 0 && outputY != 0)
cameraHandler.setVieWindow(offsetX, offsetY, outputX, outputY);
request->send(200);
}
void OpenIris::HTTPDHandler::command_handler(AsyncWebServerRequest *request)
{
if (request->hasParam("framesize"))
{
AsyncWebParameter *framesize_param = request->getParam("framesize");
cameraHandler.setCameraResolution((framesize_t)atoi(framesize_param->value().c_str()));
}
if (request->hasParam("hmirror"))
{
AsyncWebParameter *hmirror_param = request->getParam("hmirror");
cameraHandler.setHFlip(atoi(hmirror_param->value().c_str()));
}
if (request->hasParam("vflip"))
{
AsyncWebParameter *vflip_param = request->getParam("vflip");
cameraHandler.setVFlip(atoi(vflip_param->value().c_str()));
}
request->send(200);
}
void OpenIris::HTTPDHandler::stream_handler(AsyncWebServerRequest *request)
{
OpenIris::AsyncJpegStreamResponse *response = new OpenIris::AsyncJpegStreamResponse();
if (!response)
{
request->send(501);
return;
}
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}

View File

@ -0,0 +1,39 @@
#include "GlobalVars.h"
#include "webserverHandler.h"
OpenIris::APIServer::APIServer()
{
this->server = new AsyncWebServer(CONTROL_SERVER_PORT);
}
void OpenIris::APIServer::startAPIServer()
{
this->server->on(
"/control",
HTTP_GET,
std::bind(&OpenIris::APIServer::command_handler, this, std::placeholders::_1));
log_d("Initializing web server");
this->server->begin();
}
void OpenIris::APIServer::command_handler(AsyncWebServerRequest *request)
{
if (request->hasParam("framesize"))
{
AsyncWebParameter *framesize_param = request->getParam("framesize");
cameraHandler.setCameraResolution((framesize_t)atoi(framesize_param->value().c_str()));
}
if (request->hasParam("hmirror"))
{
AsyncWebParameter *hmirror_param = request->getParam("hmirror");
cameraHandler.setHFlip(atoi(hmirror_param->value().c_str()));
}
if (request->hasParam("vflip"))
{
AsyncWebParameter *vflip_param = request->getParam("vflip");
cameraHandler.setVFlip(atoi(vflip_param->value().c_str()));
}
request->send(200);
}

View File

@ -27,7 +27,7 @@ void OpenIris::WiFiHandler::setupWifi(const char *ssid, const char *password)
delay(1600);
log_i("\n\rWiFi connected\n\r");
log_i("ESP will be streaming under 'http://%s:80/\r\n", WiFi.localIP().toString().c_str());
log_i("ESP will be accepting commands under 'http://%s:80/control\r\n", WiFi.localIP().toString().c_str());
log_i("ESP will be accepting commands under 'http://%s:81/control\r\n", WiFi.localIP().toString().c_str());
}
else
{