ESP32 WROVER CAMで複数ライブカメラ

ESP32-CAMでもテストしましたがなんだか不安定なのでESP32 WROVERでのテストにしました。LAN内に複数カメラを設置してWIFIでそれぞれの画像を監視するものです。例えば家の中の部屋にいくつか設置したり、家の中から外に向けて設置したりして画像を確認するものです。各部屋のURLはHTMLのリンクでつなげます。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806156/ea0bf24a-aeb0-9828-b462-db5725aa0532.png">

# wifi,固定IPなどの設定
* wifiのIDなどを指定する
* thread処理したら不安定だったのでesp_http_server.hを使い単一接続とする
* DHCPでなく固定IPとなるように設定する
```c++
/*
 * @file  wrover_webserver-nothread.ino
 * @brief ESP32-WROVER カメラを使いLAN内で監視カメラ構築
 * @details HTTPのマルチパートストリームを用いて複数のWROVERをWEBリンクで繋ぐ。
 *          10秒間だけWEBストリーミング画像を映す。(負荷低減の為)
 *          DHCPアドレスを固定IPに指定する。
 */
#include "esp_camera.h"
#include <WiFi.h>
#include "esp_http_server.h"

// 変更箇所①
const char* ssid = "XXXXXXXXX";  // 接続するWiFiのSSID
const char* password = "XXXXXXXXX";  // 接続するWiFiのパスワード

// 変更箇所②
const IPAddress ip(192,168,0,13);
const IPAddress gateway(192,168,0,1);
const IPAddress subnet(255,255,255,0);
```
# HTTPのマルチパートストリーム
```c++
//リアルタイムにストリーミング画像を表示するためには、HTTPのマルチパートストリームを利用。
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=frame";
static const char* _STREAM_BOUNDARY = "\r\n--frame\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

// 変更箇所③
static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ESP32 Camera</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; }
        img { width: 320px; height: 240px; }
        .link { margin: 10px; display: block; }
    </style>
</head>
<body>
    <h1>部屋A</h1>
    <img src="/stream" id="camera-stream">
    <h2>他カメラ</h2>
    <a href="http://192.168.0.12" class="link">リビング</a>
    <a href="http://192.168.0.13" class="link">部屋A/a>
</body>
</html>
)rawliteral";
```

# ストリームを処理するハンドラ
* 15秒間だけストリーミングするようにする(負荷軽減)
```c++

static esp_err_t stream_handler(httpd_req_t *req) {
    camera_fb_t *fb = NULL;
    esp_err_t res = ESP_OK;
    size_t _jpg_buf_len = 0;
    uint8_t *_jpg_buf = NULL;
    char part_buf[64];

    res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
    if (res != ESP_OK) {
        return res;
    }

    unsigned long startTime = millis();  

    while (millis() - startTime < 15000) {  // 15秒間ストリーミング時
        fb = esp_camera_fb_get();
        if (!fb) {
            Serial.println("Camera capture failed");
            res = ESP_FAIL;
        } else {
            if (fb->format != PIXFORMAT_JPEG) {
                bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
                esp_camera_fb_return(fb);
                fb = NULL;
                if (!jpeg_converted) {
                    Serial.println("JPEG compression failed");
                    res = ESP_FAIL;
                }
            } else {
                _jpg_buf_len = fb->len;
                _jpg_buf = fb->buf;
            }
        }

        if (res == ESP_OK) {
            size_t hlen = snprintf(part_buf, 64, _STREAM_PART, _jpg_buf_len);
            res = httpd_resp_send_chunk(req, part_buf, hlen);
        }
        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;
            _jpg_buf = NULL;
        } else if (_jpg_buf) {
            free(_jpg_buf);
            _jpg_buf = NULL;
        }

        if (res != ESP_OK) {
            break;
        }


    }

    // ストリーミング終了後、残りのチャンクを送信して終了 15秒間ストリーミング時
    httpd_resp_send_chunk(req, NULL, 0);

    return res;
}

```

# カメラサーバーを開始する関数
```c++

void start_camera_server() {
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();

    httpd_uri_t index_uri = {
        .uri       = "/",
        .method    = HTTP_GET,
        .handler   = index_handler,
        .user_ctx  = NULL
    };

    httpd_uri_t stream_uri = {
        .uri       = "/stream",
        .method    = HTTP_GET,
        .handler   = stream_handler,
        .user_ctx  = NULL
    };

    if (httpd_start(&server, &config) == ESP_OK) {
        httpd_register_uri_handler(server, &index_uri);
        httpd_register_uri_handler(server, &stream_uri);
    }
}
```

# その他の設定
* カメラモデルに基づくピン配置の設定
if defined(CAMERA_MODEL_WROVER_KIT)
* カメラの設定
camera_config_t config
上記はいつもと同じように設定します。
* 電源はPCからとらずAC電源からUSB接続します。特にESP32-CAMでするときはで電圧降下しました。
* その他安定させるために色々策を施しています。

# テスト結果
* 画像下のリンクURLをクリックすると各カメラへ移動します。

```plantuml
@startuml
<style>
root {
  Padding 10
  Margin 10
}
header {
  FontColor black
  FontSize 50
}
</style>
scale 300 width
center header 図1 リビングからのライブカメラ
title <img:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806156/6cb11b1f-7034-b629-cafd-60713e097cb4.png>
@enduml
```
<br>

```plantuml
@startuml
<style>
root {
  Padding 10
  Margin 10
}
header {
  FontColor black
  FontSize 50
}
</style>
scale 300 width
center header 図2 部屋からのライブカメラ
title <img:https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806156/caf5bce8-b4ed-db2b-7490-2091f5f08306.jpeg>
@enduml
```