Skip to content

Commit

Permalink
asyncweb: MJPEG continuous mode
Browse files Browse the repository at this point in the history
refs #34
  • Loading branch information
yoursunny committed Jan 9, 2025
1 parent e81846a commit d23dbfe
Show file tree
Hide file tree
Showing 6 changed files with 35 additions and 20 deletions.
1 change: 1 addition & 0 deletions examples/AsyncCam/AsyncCam.ino
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ setup() {
Config cfg;
cfg.setPins(pins::AiThinker);
cfg.setResolution(initialResolution);
cfg.setBufferCount(3);
cfg.setJpeg(80);

bool ok = Camera.begin(cfg);
Expand Down
20 changes: 11 additions & 9 deletions examples/AsyncCam/handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ static const char FRONTPAGE[] = R"EOT(
<input type="submit" value="update">
</p></form>
<p id="controls">
<button data-act="mjpeg">show Motion JPEG stream</button>
<button data-act="jpg">show still JPEG image</button>
<button data-act="">hide</button>
<button data-res="cam.jpg">still JPEG</button>
<button data-res="cam.mjpeg">MJPEG</button>
<button data-res="continuous.mjpeg">MJPEG-continuous</button>
<button data-res="">hide</button>
</p>
<div id="display"></div>
<footer>Powered by <a href="https://esp32cam.yoursunny.dev/">esp32cam</a></footer>
Expand Down Expand Up @@ -57,12 +58,8 @@ for (const $ctrl of document.querySelectorAll("#controls button")) {
$img.src = "";
}
const act = evt.target.getAttribute("data-act");
if (act === "") {
$display.innerHTML = "";
} else {
$display.innerHTML = `<img src="/cam.${act}?_=${Math.random()}" alt="camera image">`;
}
const resource = evt.target.getAttribute("data-res");
$display.innerHTML = resource && `<img src="/${resource}?_=${Math.random()}" alt="${resource}">`;
});
}
</script>
Expand Down Expand Up @@ -172,4 +169,9 @@ addRequestHandlers() {

server.on("/cam.jpg", esp32cam::asyncweb::handleStill);
server.on("/cam.mjpeg", esp32cam::asyncweb::handleMjpeg);
server.on("/continuous.mjpeg", HTTP_GET, [](AsyncWebServerRequest* req) {
esp32cam::MjpegConfig cfg;
cfg.minInterval = -1;
req->send(new esp32cam::asyncweb::MjpegResponse(cfg));
});
}
19 changes: 11 additions & 8 deletions src/esp32cam/asyncweb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
namespace esp32cam {
namespace detail {

CaptureTask::CaptureTask(uint32_t queueLength, uint32_t priority, int core) {
CaptureTask::CaptureTask(uint32_t queueLength, uint32_t priority) {
m_queue = xQueueCreate(queueLength, sizeof(Frame*));
if (m_queue == nullptr) {
return;
Expand Down Expand Up @@ -41,10 +41,11 @@ CaptureTask::~CaptureTask() {
}

void
CaptureTask::request() {
if (m_task == nullptr) {
CaptureTask::request(bool continuous) {
if (m_task == nullptr || m_continuous) {
return;
}
m_continuous = continuous;
xTaskNotify(reinterpret_cast<TaskHandle_t>(m_task), 1, eSetValueWithOverwrite);
}

Expand All @@ -62,10 +63,12 @@ void
CaptureTask::run(void* ctx) {
auto self = reinterpret_cast<CaptureTask*>(ctx);
while (true) {
uint32_t value = 0;
xTaskNotifyWait(0, UINT32_MAX, &value, pdMS_TO_TICKS(10000));
if (value == 0) {
continue;
if (!self->m_continuous) {
uint32_t value = 0;
xTaskNotifyWait(0, UINT32_MAX, &value, pdMS_TO_TICKS(10000));
if (value == 0) {
continue;
}
}

auto frame = Camera.capture().release();
Expand Down Expand Up @@ -173,7 +176,7 @@ MjpegResponse::_fillBuffer(uint8_t* buf, size_t buflen) {
// fallthrough
}
case Ctrl::CAPTURE: {
m_task.request();
m_task.request(m_ctrl.cfg.minInterval < 0);
m_ctrl.notifyCapture();
return RESPONSE_TRY_AGAIN;
}
Expand Down
11 changes: 9 additions & 2 deletions src/esp32cam/asyncweb.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ namespace detail {

class CaptureTask {
public:
explicit CaptureTask(uint32_t queueLength, uint32_t priority = 1, int core = 1);
explicit CaptureTask(uint32_t queueLength, uint32_t priority = 1);

~CaptureTask();

explicit operator bool() const {
return m_queue != nullptr && m_task != nullptr;
}

void request();
void request(bool continuous = false);

std::unique_ptr<Frame> retrieve();

Expand All @@ -28,6 +28,7 @@ class CaptureTask {
private:
void* m_queue = nullptr;
void* m_task = nullptr;
bool m_continuous = false;
};

} // namespace detail
Expand Down Expand Up @@ -81,6 +82,12 @@ handleStill(AsyncWebServerRequest* request) {
* different images.
* If task creation fails, respond with HTTP 500 error.
* If image capture fails, the stream is stopped.
*
* Normally, a new frame is captured after the prior frame has been fully sent to the client.
* Setting MjpegConfig::minInternal to -1 enables continuous mode, in which a new frame is
* captured while the prior frame is still being sent to the client. This improves FPS rate
* but also increases video latency. This mode is effective only if there are more than one
* frame buffer created during camera initialization.
*/
class MjpegResponse : public AsyncAbstractResponse {
public:
Expand Down
2 changes: 1 addition & 1 deletion src/esp32cam/mjpeg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ MjpegController::decideAction() {
void
MjpegController::notifyCapture() {
m_nextAction = RETURN;
m_nextCaptureTime = millis() + static_cast<unsigned long>(cfg.minInterval);
m_nextCaptureTime = millis() + static_cast<unsigned long>(std::max(0, cfg.minInterval));
MC_LOG("notifyCapture next=%lu", m_nextCaptureTime);
}

Expand Down
2 changes: 2 additions & 0 deletions src/esp32cam/mjpeg.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ namespace esp32cam {
struct MjpegConfig {
/**
* @brief Minimum interval between frame captures in millis.
*
* Negative value causes @c asyncweb::MjpegResponse to enter continuous mode.
*/
int minInterval = 0;

Expand Down

0 comments on commit d23dbfe

Please sign in to comment.