ESP32など

電子工作してます

ESP32-CAM / M5Stack Timer CameraでのWebサーバー画像ストリーミング

サンプルの CameraWebServer.ino / web_cam.ino が難しかったので、ストリーミング(Motion JPEG)の部分だけ切り出した。

ピン配列はM5Stack Timer Cameraを使用する場合の設定。

メインスケッチ
#include <WiFi.h>
#include <esp_camera.h>
#include <esp_http_server.h>

#include "html_text.h" // HTMLファイルの内容

const char *ssid     = "******";
const char *password = "******";

#define PART_BOUNDARY "123456789000000000000987654321"
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: image/jpeg\r\nContent-Length: %u\r\n\r\n";

void setup() {
    Serial.begin(115200);

    // カメラ用パラメータ
    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer   = LEDC_TIMER_0;
    config.pin_d0       = 32;
    config.pin_d1       = 35;
    config.pin_d2       = 34;
    config.pin_d3       = 5;
    config.pin_d4       = 39;
    config.pin_d5       = 18;
    config.pin_d6       = 36;
    config.pin_d7       = 19;
    config.pin_xclk     = 27;
    config.pin_pclk     = 21;
    config.pin_vsync    = 22;
    config.pin_href     = 26;
    config.pin_sccb_sda = 25;
    config.pin_sccb_scl = 23;
    config.pin_pwdn     = -1;
    config.pin_reset    = 15;
    config.xclk_freq_hz = 20000000;
    config.pixel_format = PIXFORMAT_JPEG;
    config.frame_size   = FRAMESIZE_QVGA; // 320x240
    config.jpeg_quality = 10;
    config.fb_count     = 2; // CAMERA_GRAB_LATEST設定時は2以上必要
    config.grab_mode    = CAMERA_GRAB_LATEST; // 最後のフレームバッファだけを取得

    // カメラ開始
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) {
        Serial.printf("Camera init failed with error 0x%x", err);
        return;
    }

    // WiFi開始
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println(WiFi.localIP());

    // Webサーバー開始
    start_MJPEG_server();
}

void loop() {}

// Webサーバー開始
void start_MJPEG_server(){
    httpd_handle_t camera_httpd = NULL;
    httpd_handle_t stream_httpd = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();

    // ドキュメントルート用
    httpd_uri_t index_uri = {
        .uri       = "/",
        .method    = HTTP_GET,
        .handler   = index_handler,
        .user_ctx  = NULL
    };

    // MotionJPEG用
    httpd_uri_t stream_uri = {
        .uri       = "/stream",
        .method    = HTTP_GET,
        .handler   = stream_handler,
        .user_ctx  = NULL
    };
    
    // ドキュメントルート用
    Serial.printf("Starting web server on port: '%d'\n", config.server_port);
    if (httpd_start(&camera_httpd, &config) == ESP_OK) {
        httpd_register_uri_handler(camera_httpd, &index_uri);
    }

    // MotionJPEG用
    config.server_port += 1;
    config.ctrl_port += 1;
    Serial.printf("Starting stream server on port: '%d'\n", config.server_port);
    if (httpd_start(&stream_httpd, &config) == ESP_OK) {
        httpd_register_uri_handler(stream_httpd, &stream_uri);
    }
}

// ブラウザで、ドキュメントルートが表示された際の処理(HTMLを返す)
static esp_err_t index_handler(httpd_req_t *req){
  httpd_resp_set_type(req, "text/html");
  httpd_resp_set_hdr(req, "Accept-Charset", "UTF-8");
  return httpd_resp_send(req, HTML_TEXT.c_str(), HTML_TEXT.length());
}

// ブラウザで、Motion JPEG(<img>)が表示された際の処理(閉じるまでループで撮影画像送信)
static esp_err_t stream_handler(httpd_req_t *req){
  
    camera_fb_t *fb = NULL;
    esp_err_t res = ESP_OK;
    char * part_buf[64];

    // HTTPヘッダー送信(ストリーミング全体)
    res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
    if(res != ESP_OK){
        return res;
    }

    while(true){
      
        // 撮影
        fb = esp_camera_fb_get();
        if (!fb) {
            Serial.println("Camera capture failed");
            res = ESP_FAIL;
        }

        // HTTPヘッダー送信(jpeg1つ分)
        if(res == ESP_OK){
            size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, fb->len);
            res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
        }

        // jpegデータ送信
        if(res == ESP_OK){
            res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len);
        }

        // boundary(データ区切り文字列)送信
        if(res == ESP_OK){
            res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
        }
        
        // フレームバッファ開放
        if (fb) {
            esp_camera_fb_return(fb);
        }

        // 接続解除 もしくは エラー時に抜ける
        if(res != ESP_OK){
            break;
        }
    }
    return res;
}

表示用HTMLファイルのテキスト情報

HTMLだけ切り出して別ファイルに保管。

Arduino IDEでraw文字列(生文字列リテラル / Raw string literals)使えるの初めて知った。助かる。

static const String HTML_TEXT = R"(
<!DOCTYPE html>
<html>
  <head>
      <script>
      window.onload = function() {
      // imgのsrcに「http://192.168.x.x:81/stream」のurlを設定
          var id_stream = document.getElementById("img_stream");
          id_stream.src = document.location.origin + ":81/stream";
      }
      </script>
  </head>
  <body>
      <img id="img_stream" width="320" height="240">
  </body>
</html>
)";