ESP32でhttpdライブラリを使うとMotion JPEG(BMP)動画ストリーミングが速い!

ESP32,でhttpdライブラリを使うと、Motion,JPEGストリーミングが高速で動作する。 ESP32 ( ESP-WROOM-32 )

こんばんは。
世間がこんな状況で、私も少し時間が取れるようになってきたので、この早いタイミングでまた記事をアップできました。

今回も前回記事に引き続いて動画ストリーミングの Motion JPEG およびBMP を扱いますが、今回はArduino core for the ESP32 に予め備わっている httpd 関数を使ってブラウザとコネクション確立して、動画ストリーミングしてみました。

そしたら、何と!
前回のストリーミングより3倍速くなったんです。

スポンサーリンク

前回記事ではArduinoの基本的な関数を使ったことにより、ブラウザとのコール&レスポンスの仕組みが勉強できたんですが、いかんせん通信速度が遅かったんです。
ですが、今回、httpd関数を使ったことにより、旧型Android スマホでは、
5fps → 約18fps
Windows 10 パソコンで、
7fps → 約20fps
となったんです。
3倍ですよ、3倍!!!

それも、圧縮していない 50KB のビットマップ画像ですよ。
ここまで速度が出れば十分ですね。

とにかくまずは旧型Android 8.0 スマホの動画を見てみて下さい。

どうでしょう?
前回記事の自作プログラミングよりか圧倒的に速いですよね。
ボタン操作はちょっとタイムラグがありますが、それでも前回よりダントツに速いです。

では、次はWindows 10 パソコンを使うとこうなりました。

これは速いですね。
サクサクです。
ボタン操作のレスポンスもメチャメチャ高速です。

では、前回の動画と今回の動画を分かり易く比較してみました。

これを見て分かる通り、httpdライブラリが圧倒して速いですね。
スゴイと思いませんか? この速さ。

今回はESP32で生成したビットマップ(BMP)画像だけで試していますが、もし、JPEG画像で通信したならば、これよりさらに数倍の速度が出ると思います。
しかももっと大きい画像でも動画はスムースだと思います。

前回記事では、私は何とかしてこの速さを実現しようといろいろ解明に取り組んだのですが、自分の実力ではとても無理でした。
httpd関連関数のソースコードを読み解いてみたんですが、最終的にはコンパイルされたアセンブラファイルでブラックBOX化されていて、解明を断念しました。

httpdライブラリについては、ネットで情報があるとおり、Linux等にあるライブラリらしく、Apacheなどのサーバープログラミングに使われているようです。
ですから、オープンソースなんだと思います。
Arduino core for the ESP32 の基幹であるFreeRTOSにもhttpdライブラリがあるようです。たぶん。。。
FreeRTOSライブラリの解明も断念せざるを得ませんでした。
ちょっと悔しいのですが、仕方ありません。
じゃぁ、このhttpdの圧倒的な通信の速さにあやかってしまいましょう。

ということで、素直にArduino core for the ESP32に既存のhttpdライブラリを使ったMotion JPEGならぬMotion BMP動画ストリーミングのプログラミング実験を紹介します。

因みに何度も申し上げておりますが、私はプログラミングについては素人です。
誤りや勘違い等、何かありましたらコメント投稿でご連絡ください。

    【目次】

  1. 使うもの
  2. Arduino core for the ESP32 のインストール
  3. Motion JPEG 対応ブラウザ
  4. httpd Motion JPEG(BMP) スケッチ例 その1(四角形を表示するだけの簡単なもの)
  5. コンパイル書き込み実行
  6. httpd Motion JPEG(BMP) スケッチ例 その2(ちょっと凝ったアニメーション)
  7. 編集後記

使うもの

ESP32 ( ESP32-WROOM-32 )開発ボード又はM5Stack

日本で安心して使える技適取得済みのESP32-WROOM-32開発ボードや、M5Stack を使って下さい。
今回の実験では、スイッチサイエンスさんの ESPr Developer 32 を使いました。
それについては以下の記事を参照してください。
ESPr Developer 32 ( スイッチサイエンス製 ) を使ってみました

ESPr Developer 32
スイッチサイエンス(Switch Science)

(追記)
M5Stack Basicは、この記事を書いた当時より格段にバージョンアップしております。
以下のスイッチサイエンスさんの公式サイトをご参照ください。
https://www.switch-science.com/collections/%E5%85%A8%E5%95%86%E5%93%81/products/9010

M5Stack FIRE
スイッチサイエンス

WiFi環境

事前にESP32が通信できるように、ファイアウォール設定やMACアドレスフィルタリング設定を済ませておいてください。
ESP32のMACアドレスの確認は以下の記事を参照してください。

ESP32-WROOM-32 チップ・メモリ・MACアドレス情報取得方法

手軽に部屋でWiFi環境を構築できる、以下のようなホテルルーターでも良いと思います。

パソコン又はスマホ等

できるだけ高速CPU、高速WiFiの最新パソコンおよびスマホを推奨します。

Arduino core for the ESP32 のインストール

Arduino IDE はver 1.8.12 で動作確認しています。
Arduino core for the ESP32 は、stable ver 1.0.4 で動作確認しています。
Arduino core for the ESP32 のインストール方法は以下の記事を参照してください。

Arduino core for the ESP32 のインストール方法

Motion JPEG 対応ブラウザ

Motion JPEG が対応しているブラウザは、EdgeとIE以外OKです。
詳しくは前回記事を参照してください。

httpd Motion JPEG(BMP) スケッチ例 その1
(四角形を表示するだけの簡単なもの)

では、Arduino core for the ESP32 にある httpd ライブラリを使って、WiFi STAモード(外部ルーターを介すモード)で、Motion JPEG(BMP) 動画ストリーミング通信するスケッチ(プログラミング)を紹介します。

まずは、お試しとして、四角形を表示するだけのスケッチです。
ちゃんとボタンスイッチの双方向通信もできます。

(※SSIDやパスワードは、ESP32が第三者の手に渡った場合、ソフトウェアによって情報を抜き取られる場合があります。
このスケッチによるいかなるトラブルも当方では責任を負えませんのでご了承ください。)

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

/* The MIT License (MIT)
 * License URL: https://opensource.org/licenses/mit-license.php
 * Copyright (c) 2020 Mgo-tec. All rights reserved.
 * Modify app_httpd.cpp(Arduino core for the ESP32 v1.0.4).
 * Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
 * app_httpd.cpp - Licensed under the Apache License, Version 2.0
 *     http://www.apache.org/licenses/LICENSE-2.0
 */
#include <WiFi.h>
#include <utility> //swap関数を使う場合に必要
#include <esp_http_server.h> //httpd関数を使う場合必要

const char* ssid = "xxxxxxxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxxxxxxx"; //ご自分のルーターのパスワードに書き換えてください
//----------------------------------
const uint16_t disp_width_pix = 200, disp_height_pix = 148;
const uint16_t max_x = disp_width_pix - 1;
const uint16_t max_y = disp_height_pix - 1;
const uint16_t max_w_pix_buf = disp_width_pix * 2;
//----------------------------------
boolean canStartStream = false;
boolean canSendImage = false;
boolean shouldClear = true;
uint32_t frame_last_time = 0; //for display FPS
uint16_t y_old = 0;
uint16_t draw_line_count = 0;
int16_t draw_rect_count = 0;
uint32_t draw_time = 0;
int8_t moving_width_pix = 3;
uint8_t ctrl_red = 0, ctrl_green = 0xff, ctrl_blue = 0xff;
//------Initialize bitmap data------
const uint16_t data_size = disp_width_pix * 2 * disp_height_pix;
const uint8_t data_size_lsb = (uint8_t)(0x00ff & data_size);
const uint8_t data_size_msb = (uint8_t)(data_size >> 8); 
const uint8_t bmp_head_bytes = 66;
const uint16_t file_size = bmp_head_bytes + data_size;
const uint8_t file_size_lsb = (uint8_t)(0x00ff & file_size);
const uint8_t file_size_msb = (uint8_t)(file_size >> 8);
const uint8_t info_header_size = 0x28; //情報ヘッダサイズは常に40byte = 0x28byte
const uint8_t bits_per_pixel = 16; //色ビット数=16bit(0x10)
const uint8_t compression = 3; //色ビット数が16bitの場合、マスクを設定するので3にする。
const uint8_t red_mask[2] =   {0b11111000, 0b00000000};
const uint8_t green_mask[2] = {0b00000111, 0b11100000};
const uint8_t blue_mask[2] =  {0b00000000, 0b00011111}; 
//※Bitmap file headerは全てリトルエンディアン
const uint8_t bmp_header[bmp_head_bytes]=
    {0x42, 0x4D,
     file_size_lsb, file_size_msb, 0, 0,
     0, 0, 0, 0,
     bmp_head_bytes, 0, 0, 0,
     info_header_size, 0, 0, 0,
     disp_width_pix, 0, 0, 0,
     disp_height_pix, 0, 0, 0,
     1, 0, bits_per_pixel, 0,
     compression, 0, 0, 0,
     data_size_lsb, data_size_msb, 0, 0,
     0,0,0,0,
     0,0,0,0,
     0,0,0,0,
     0,0,0,0,
     red_mask[1], red_mask[0], 0, 0,
     green_mask[1], green_mask[0], 0, 0,
     blue_mask[1], blue_mask[0], 0, 0};
//----------------------------------
uint8_t bmp_data_buf[bmp_head_bytes + disp_height_pix * max_w_pix_buf] = {};
httpd_handle_t stream_httpd = NULL;
httpd_handle_t control_httpd = NULL;
//*********************************************
void setup() {
  Serial.begin(115200);
  Serial.println();
  delay(1000);

  memcpy(bmp_data_buf, bmp_header, 66); //BMPファイル配列にヘッダ情報を書き込む
  
  TaskHandle_t taskHTTP_handl;
  if (!xTaskCreatePinnedToCore(&taskHTTP, "taskHTTP", 9216, NULL, 24, &taskHTTP_handl, 0)) {
    Serial.println("Failed to create taskHTTP");
  }
  while(!canStartStream){
    delay(1);
  }
}

void loop() {
  if(shouldClear){
    clearAll();
    shouldClear = false;
  }
  if(canStartStream){
    if(!canSendImage){
      uint16_t x0, y0, x1, y1;
      if(changeDrawCount(draw_time, 0, 2000)){
        //0~2秒までは赤色四角形表示
        x0 = 0, y0 = 0, x1 = 50, y1 = 50;
        drawRectangleFill(x0, y0, x1, y1, 0xff, 0x00, 0x00);
      }
      if(changeDrawCount(draw_time, 2000, 4000)){
        //2~4秒までは緑色四角形表示
        x0 = 75, y0 = 49, x1 = 125, y1 = 99;
        drawRectangleFill(x0, y0, x1, y1, 0x00, 0xff, 0x00);
      }
      if(changeDrawCount(draw_time, 4000, 6000)){
        //4~6秒までは青色四角形表示
        x0 = 149, y0 = 97, x1 = max_x, y1 = max_y;
        drawRectangleFill(x0, y0, x1, y1, 0x00, 0x00, 0xff);
      }
      if(changeDrawCount(draw_time, 6000, 6500)){
        //6.0秒から6.5秒までは画面消去
        clearAll();
        draw_time = millis();
      }
      canSendImage = true;
    }
  }
}
//*********************************************
void taskHTTP(void *pvParameters){
  connectToWiFi(ssid, password);
  startHttpd();
  while(true){
    delay(1);
  }
}
//****************************************
void startHttpd(){
  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 cmd_uri = {
    .uri       = "/command",
    .method    = HTTP_GET,
    .handler   = cmd_handler,
    .user_ctx  = NULL
  };

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

  if (httpd_start(&control_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(control_httpd, &index_uri);
    httpd_register_uri_handler(control_httpd, &cmd_uri);
  }

  config.server_port += 1;
  config.ctrl_port += 1;

  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &stream_uri);
  }
}
//****************************************
static esp_err_t stream_handler(httpd_req_t *req){
  canStartStream = true;
  esp_err_t res = ESP_OK;
  char * part_buf[64];

  static const char* stream_part = "Content-Type: image/bmp\r\nContent-Length: %u\r\n\r\n";
  #define PART_BOUNDARY "myboundary"
  static const char* stream_content_type = "multipart/x-mixed-replace;boundary=--" PART_BOUNDARY;
  static const char* stream_boundary = "\r\n--" PART_BOUNDARY "\r\n";

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

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

  draw_time = millis();
  while(true){
    if(canSendImage){
      if(res == ESP_OK){
        size_t hlen = snprintf((char *)part_buf, 64, stream_part, file_size);
        res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
      }
      if(res == ESP_OK){
        res = httpd_resp_send_chunk(req, (const char *)bmp_data_buf, file_size);
        canSendImage = false;
        float fps = 1000.0 / (millis() - (float)frame_last_time);
        Serial.printf("%.02lf(fps)\r\n", fps);
        frame_last_time = millis();
      }
      if(res == ESP_OK){
        res = httpd_resp_send_chunk(req, stream_boundary, strlen(stream_boundary));
      }
      if(res != ESP_OK){
        break;
      }
    }
    delay(1);
  }
  return res;
}
//****************************************
static esp_err_t index_handler(httpd_req_t *req){
  String html_body = "<!DOCTYPE html>\r\n";
          html_body += "<html><head></head><body>\r\n";
          html_body += "<img id='pic_place' width='200' height='148' style='border-style:solid; transform:scale(1, -1);'>\r\n";
          html_body += "<div>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='startStream()'>ON Stream</button><br>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='changeControl(\"re_start_stream\",1)'>Re-Start Stream</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeControl(\"stop_stream\",1)'>Stop Stream</button></p>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeControl(\"close_connection\",1)'>Close Connection</button><br>\r\n";
          html_body += "</div>\r\n";
          html_body += "<script>\r\n";
          html_body += "var base_url = document.location.origin;\r\n";
          html_body += "var url_stream = base_url + ':81';\r\n";
          html_body += "function startStream() {\r\n";
          html_body += "var pic = document.getElementById('pic_place');\r\n";
          html_body += "pic.src = url_stream+'/stream';};\r\n";
          html_body += "function changeControl(id_txt, value_txt){\r\n";
          html_body += "var new_url = base_url+'/command?id=';\r\n";
          html_body += "new_url += id_txt + '&';\r\n";
          html_body += "new_url += 'value=' + value_txt;\r\n";
          html_body += "fetch(new_url)\r\n";
          html_body += ".then((response) => {\r\n";
          html_body += "if(response.ok){return response.text();} \r\n";
          html_body += "else {throw new Error();}})\r\n";
          html_body += ".then((text) => console.log(text))\r\n";
          html_body += ".catch((error) => console.log(error));};\r\n";
          html_body += "</script></body></html>\r\n";

  httpd_resp_set_type(req, "text/html");
  httpd_resp_set_hdr(req, "Accept-Charset", "UTF-8");
  return httpd_resp_send(req, html_body.c_str(), html_body.length());
}
//******************************************
static esp_err_t cmd_handler(httpd_req_t *req){
  char*  buf;
  size_t buf_len;
  char id_txt[32] = {0,};
  char value_txt[32] = {0,};

  buf_len = httpd_req_get_url_query_len(req) + 1;
  if (buf_len > 1) {
    buf = (char*)malloc(buf_len);
    if(!buf){
      httpd_resp_send_500(req);
      return ESP_FAIL;
    }
    if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
      Serial.println(buf);
      if (httpd_query_key_value(buf, "id", id_txt, sizeof(id_txt)) == ESP_OK &&
        httpd_query_key_value(buf, "value", value_txt, sizeof(value_txt)) == ESP_OK) {
      } else {
        free(buf);
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
    } else {
      Serial.println(buf);
      free(buf);
      httpd_resp_send_404(req);
      return ESP_FAIL;
    }
    free(buf);
  } else {
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }

  uint8_t val = atoi(value_txt);
  int res = 0;
  if(!strcmp(id_txt, "re_start_stream")) {
    canStartStream = true;
    shouldClear = true;
    draw_time = millis();
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "stop_stream")) {
    canStartStream = false;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "red")) {
    ctrl_red = 0xff, ctrl_green = 0, ctrl_blue = 0;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "green")) {
    ctrl_red = 0, ctrl_green = 0xff, ctrl_blue = 0;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "blue")) {
    ctrl_red = 0, ctrl_green = 0, ctrl_blue = 0xff;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "close_connection")) {
    shouldClear = true;
    httpd_resp_set_hdr(req, "Connection", "Close");
    httpd_resp_send(req, NULL, 0);
    Serial.println("※このボタンは現在機能しません");
    delay(10);
    return ESP_OK;
  }else {
    res = -1;
  }

  if(res){
    return httpd_resp_send_500(req);
  }

  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  return httpd_resp_send(req, NULL, 0);
}
//*********************************************
boolean changeDrawCount(uint32_t now_time, uint32_t start_time, uint32_t stop_time){
  if((millis() - now_time > start_time) && (millis() - now_time < stop_time)){
    return true;
  }
  return false;
}
//*********************************************
void clearAll(){
  memset(bmp_data_buf + bmp_head_bytes, 0, disp_height_pix * max_w_pix_buf);
}
//*********************************************
void drawRectangleFill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(red, green, blue, rgb565_msb, rgb565_lsb);
  for(int i = x0; i <= x1; i++){
    drawVerticalLine565(i, y0, y1, rgb565_msb, rgb565_lsb);
  }
}
//*********************************************
void drawVerticalLine565(uint16_t x0, uint16_t y0, uint16_t y1, uint8_t rgb565_msb, uint8_t rgb565_lsb){
  judgeMaxPixel(x0, max_x);
  judgeMaxPixel(y0, max_y);
  judgeMaxPixel(y1, max_y);
  if(y0 > y1) std::swap(y0, y1);
  uint16_t p = 0;
  for(uint16_t i = y0; i <= y1; i++){
    p = bmp_head_bytes + i * max_w_pix_buf + x0 * 2;
    bmp_data_buf[p] = rgb565_lsb;
    bmp_data_buf[p + 1] = rgb565_msb;
  }
}
//*********************************************
void convertRGB888toRGB565(uint8_t red888, uint8_t green888, uint8_t blue888, uint8_t &rgb565_msb, uint8_t &rgb565_lsb){
  //RGB888をRGB565へ変換するには、下位ビットを削除するだけでOK。
  uint8_t red565 = red888 & 0b11111000;
  uint8_t green565 = green888 & 0b11111100;
  uint8_t blue565 = blue888 & 0b11111000;
  rgb565_msb = red565 | (green565 >> 5);
  rgb565_lsb = (green565 << 3) | (blue565 >> 3);
}
//*********************************************
void judgeMaxPixel(uint16_t &pix, uint16_t max_pix){
  if(pix > max_pix){
    Serial.printf("Over Max pix = %d\r\n", pix);
    pix = max_pix;
  }
}
//*********************************************
void connectToWiFi(const char * ssid, const char * pwd){
  Serial.println("Connecting to WiFi network: " + String(ssid));
  WiFi.disconnect(true, true);
  delay(1000);
  WiFi.begin(ssid, password);
  Serial.println("Waiting for WIFI connection...");
  while ( WiFi.status() != WL_CONNECTED ) {
    delay(500);
    Serial.print(".");
  }
  IPAddress myIP = WiFi.localIP();
  Serial.println("WiFi connected!");
  Serial.print("My IP address: ");
  Serial.println(myIP);
  delay(1000);
}

【分かる範囲でザッと解説】

Arduino core for the ESP32 のhttpd関連サンプルスケッチは、CameraWebSereverにあります。
その、app_httpd.cppファイルを改変しました。

ビットマップファイルの生成は、前回記事と基本的に同じですが、65行目を見て分かる通り、前回ではbmp_data_bufが2次元配列だったのを、今回は1次元配列にしています。
なぜかというと、188行目にストリーミング送信する際に使う
httpd_resp_send_chunk
という関数があって、それは1次元配列のポインタを扱っているからです。

あとは、ブラウザとのリクエスト&レスポンスのやり取りは前回記事同じなので、それを理解しながらhttpd関連関数でプログラミングしてくと、比較的分かり易いかと思います。
よって、前回記事と重複しているところは省いて説明します。

●11行目:
esp_http_server.h というヘッダファイルは、httpd関連関数を使う時に必要です。

●65行目:
先ほども述べたように、今回のビットマップファイル生成用の配列バッファは1次元配列に収めます。
すると、2次元配列よりもポインタ計算が面倒になります。

●69-116行目:
ESP32のCPU core 1 のsetup関数とメインloop関数です。
74行目で、ビットマップファイルのヘッダ部分をbmp_data_buf に代入しておきます。
77行目にあるように、httpd関連のWiFi通信は、マルチタスクで CPU core 0 で実行させます。
メインloopでは、bmp_data_buf に四角形を描画させています。

●118-124行:
ここが、ESP32 のマルチタスク CPU core 0 で動作させているhttpdの無限ループです。
これ、ちょっと不思議に思いませんか?
たぶん、私の勝手な想像ですが、httpdライブラリは既にマルチタスク化や割り込み処理されていて、126-161行を1回実行するだけで、裏でループ処理が動いているのではないかと思います。
httpdライブラリを深く探れなかったので、単なる想像です。

●126-161行:
まずは、それぞれの構造体の初期化を行います。
httpd_config_t 型構造体の config には、ポート番号等の通信関係の環境変数を初期化しています。
httpd_uri_t 型構造体の index_uri は、ESP32サーバーのルートへのGETリクエスト関する変数です。
cmd_uri は、ブラウザからボタンスイッチなどで制御コマンドGETリクエストが来た場合に関する変数です。
stream_uri は、ブラウザの「ON Stream」ボタンを押してGETリクエストが来た場合の構造体です。

150-153行目でポート番号80番のhttpdサーバーが稼働し始めます。
155-160行で、ポート番号を81番にして、ストリーミング用のポート81番が稼働します。

●163-204行:
ここで、ブラウザの「ON Stream」ボタンを押して、ポート番号81番でGETリクエストをESP32が受信したら、Motion JPEG動画ストリーミング用のレスポンスヘッダを返します。
168-171行のchar型の文字列連結方法は、今回私は初めて知りました。
Arduino のString関数のように連結したい場合、defineのプリプロセッサを使えば出来るんですね。
今後使えるかも知れませんね。

そして、前回記事で説明したように、Motion JPEG動画ストリーミングには、Content-Typeに
multipart/x-mixed-replace;boundary=
を使います。
また、178行目にあるように、ストリーミングしながらコマンド双方向通信を行うために、
Access-Control-Allow-Origin: *
もレスポンスヘッダに含めます。

181-202行で実際に Motion JPEG(BMP)動画ストリーミング送信しています。
184行で、boundary文字列とレスポンスヘッダを送り、
188行の httpd_resp_send_chunk という関数でビットマップデータを送っています。
ただ、前回記事の手法と異なるのは、チャンク(chunk)という手法を使っているようです。
チャンクというのは、データを分割して送りますが、その個々の分割データの前にデータのバイト数をテキスト数値で送り、その後に分割データを送ります。
それを繰り返して、最後のデータには’0’というテキストデータを送って、改行文字列を送ります。
詳しくは以下のサイトを参照ください。
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Transfer-Encoding

ただ、私が前回記事のスケッチでチャンク化して実験しましたが、チャンク化した方が速度が遅くなってしまったので止めました。
チャンク化が速いのか? というのはちょっと疑問です。

●206-237行:
ここでは、最初にブラウザからESP32サーバーのルートにGETリクエストが来た場合にレスポンスする関数です。
ここでHTMLを送信しています。

●239-309行:
ここでは、ポート番号81番で動画ストリーミング中に、ポート番号80番でブラウザから制御コマンドGETリクエストが来た場合の処理です。
たぶん、これは割り込みで制御されているんだろうと思いますが、定かではありません。
GETリクエスト中の文字列をid値とvalue値で分類して、ESP32の動作を決めています。
ただ、292-298行目にあるように、「Close Connection」ボタンが押されても、コネクションがクローズできません。
ここは私なりにいろいろ実験したのですが、クライアントとの切断ができませんでした。
つまり、途中で他の端末からのコントロールに切り替えることができないのです。
これは今後の課題とします。
分かる人がいたら教えてください。

以上です。
あとは前回記事のスケッチと同じなので、割愛します。

コンパイル書き込み実行

では、コンパイル書き込み実行させてみてください。
シリアルモニターを115200bpsで起動すると、前回記事よりもフレームレートがほぼ3倍になっていることが分かると思います。
私のWiFi環境では、シリアルモニターに下図のように表示されました。
前回記事と比べると3倍になっていますね。

httpd Motion JPEG(BMP) スケッチ例 その2
(ちょっと凝ったアニメーション)

では次に、前回記事と同様にちょっと凝ったアニメーションのスケッチ例を紹介します。
線画を加えて、四角形移動中にボタン操作で色を変えることができます。
このスケッチは前節のスケッチに毛が生えた程度のものなので、前回記事同様、説明を割愛します。

(※SSIDやパスワードは、ESP32が第三者の手に渡った場合、ソフトウェアによって情報を抜き取られる場合があります。
このスケッチによるいかなるトラブルも当方では責任を負えませんのでご了承ください。)

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

/* The MIT License (MIT)
 * License URL: https://opensource.org/licenses/mit-license.php
 * Copyright (c) 2020 Mgo-tec. All rights reserved.
 * Modify app_httpd.cpp(Arduino core for the ESP32 v1.0.4).
 * Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
 * app_httpd.cpp - Licensed under the Apache License, Version 2.0
 *     http://www.apache.org/licenses/LICENSE-2.0
 */
#include <WiFi.h>
#include <utility> //swap関数を使う場合に必要
#include <esp_http_server.h> //httpd関数を使う場合必要

const char* ssid = "xxxxxxxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxxxxxxx"; //ご自分のルーターのパスワードに書き換えてください
//----------------------------------
const uint16_t disp_width_pix = 200, disp_height_pix = 148;
const uint16_t max_x = disp_width_pix - 1;
const uint16_t max_y = disp_height_pix - 1;
const uint16_t max_w_pix_buf = disp_width_pix * 2;
//----------------------------------
boolean canStartStream = false;
boolean canSendImage = false;
boolean shouldClear = true;
uint32_t frame_last_time = 0; //for display FPS
uint16_t y_old = 0;
uint16_t draw_line_count = 0;
int16_t draw_rect_count = 0;
uint32_t draw_time = 0;
int8_t moving_width_pix = 3;
uint8_t ctrl_red = 0, ctrl_green = 0xff, ctrl_blue = 0xff;
//------Initialize bitmap data------
const uint16_t data_size = disp_width_pix * 2 * disp_height_pix;
const uint8_t data_size_lsb = (uint8_t)(0x00ff & data_size);
const uint8_t data_size_msb = (uint8_t)(data_size >> 8); 
const uint8_t bmp_head_bytes = 66;
const uint16_t file_size = bmp_head_bytes + data_size;
const uint8_t file_size_lsb = (uint8_t)(0x00ff & file_size);
const uint8_t file_size_msb = (uint8_t)(file_size >> 8);
const uint8_t info_header_size = 0x28; //情報ヘッダサイズは常に40byte = 0x28byte
const uint8_t bits_per_pixel = 16; //色ビット数=16bit(0x10)
const uint8_t compression = 3; //色ビット数が16bitの場合、マスクを設定するので3にする。
const uint8_t red_mask[2] =   {0b11111000, 0b00000000};
const uint8_t green_mask[2] = {0b00000111, 0b11100000};
const uint8_t blue_mask[2] =  {0b00000000, 0b00011111}; 
//※Bitmap file headerは全てリトルエンディアン
const uint8_t bmp_header[bmp_head_bytes]=
    {0x42, 0x4D,
     file_size_lsb, file_size_msb, 0, 0,
     0, 0, 0, 0,
     bmp_head_bytes, 0, 0, 0,
     info_header_size, 0, 0, 0,
     disp_width_pix, 0, 0, 0,
     disp_height_pix, 0, 0, 0,
     1, 0, bits_per_pixel, 0,
     compression, 0, 0, 0,
     data_size_lsb, data_size_msb, 0, 0,
     0,0,0,0,
     0,0,0,0,
     0,0,0,0,
     0,0,0,0,
     red_mask[1], red_mask[0], 0, 0,
     green_mask[1], green_mask[0], 0, 0,
     blue_mask[1], blue_mask[0], 0, 0};
//----------------------------------
uint8_t bmp_data_buf[bmp_head_bytes + disp_height_pix * max_w_pix_buf] = {};
httpd_handle_t stream_httpd = NULL;
httpd_handle_t control_httpd = NULL;
//*********************************************
void setup() {
  Serial.begin(115200);
  Serial.println();
  delay(1000);

  memcpy(bmp_data_buf, bmp_header, 66); //BMPファイル配列にヘッダ情報を書き込む
  
  TaskHandle_t taskHTTP_handl;
  if (!xTaskCreatePinnedToCore(&taskHTTP, "taskHTTP", 9216, NULL, 24, &taskHTTP_handl, 0)) {
    Serial.println("Failed to create taskHTTP");
  }
  while(!canStartStream){
    delay(1);
  }
}

void loop() {
  if(shouldClear){
    clearAll();
    shouldClear = false;
  }
  if(canStartStream){
    if(!canSendImage){
      if(changeDrawCount(draw_time, 0, 3000)){
        drawRectangleLine(0, 0, max_x, max_y, 0xff, 0xff, 0xff);
        drawLine(0, 0, max_x, max_y, 0xff, 0xff, 0xff);
        drawLine(max_x, 0, 0, max_y, 0xff, 0xff, 0xff);
        drawVerticalLine(100, max_y, 0, 0xff, 0, 0);
        drawHorizontalLine(0, 74, max_x, 0xff, 0xff, 0);
      }
      if(changeDrawCount(draw_time, 3000, 3500)){
        clearAll();
      }
      if(changeDrawCount(draw_time, 3500, 6000)){
        uint8_t bar_w = 25;
        drawRectangleFill(0, 0, bar_w - 1, max_y, 0xff, 0xff, 0xff);
        drawRectangleFill(bar_w, 0, bar_w * 2 - 1, max_y, 0xff, 0x00, 0x00);
        drawRectangleFill(bar_w * 2, 0, bar_w * 3 - 1, max_y, 0x00, 0xff, 0x00);
        drawRectangleFill(bar_w * 3, 0, bar_w * 4 - 1, max_y, 0x00, 0x00, 0xff);
        drawRectangleFill(bar_w * 4, 0, bar_w * 5 - 1, max_y, 0xff, 0xff, 0x00);
        drawRectangleFill(bar_w * 5, 0, bar_w * 6 - 1, max_y, 0x00, 0xff, 0xff);
        drawRectangleFill(bar_w * 6, 0, bar_w * 7 - 1, max_y, 0xff, 0x00, 0xff);
        drawRectangleFill(bar_w * 7, 0, bar_w * 8 - 1, max_y, 0x80, 0x80, 0x80);
      }
      if(changeDrawCount(draw_time, 6000, 6500)){
        clearAll();
      }
      if(changeDrawCount(draw_time, 6500, 16000)){
        drawLine(0, draw_line_count * 7, draw_line_count * 9, max_y, 0xff, 0xff, 0xff);
        drawLine(draw_line_count * 9, 0, max_x, draw_line_count * 7, ctrl_red, ctrl_green, ctrl_blue);
        draw_line_count++;
        if(draw_line_count >= 22){
          draw_line_count = 0;
        }
      }
      if(changeDrawCount(draw_time, 16000, 16500)){
        draw_line_count = 0;
        clearAll();
      }
      if(changeDrawCount(draw_time, 16500, 35000)){
        uint8_t rect_width = 20;
        uint8_t x0 = draw_rect_count;
        uint8_t x1 = draw_rect_count + rect_width;
        uint8_t y0 = 75;
        uint8_t y1 = y0 + rect_width;
        drawRectangleFill(x0, y0, x1, y1, ctrl_red, ctrl_green, ctrl_blue);
        if(moving_width_pix < 0){
          if(x1 != (max_x)){
            drawRectangleFill(x1, y0, x1 - moving_width_pix, y1, 0x00, 0x00, 0x00);
          }
        }else{
          if(draw_rect_count >= moving_width_pix){
            drawRectangleFill(x0 - moving_width_pix, y0, x0 - 1, y1, 0x00, 0x00, 0x00);
          }
        }
        draw_rect_count += moving_width_pix;
        if(draw_rect_count >= (max_x -rect_width)){
          moving_width_pix = -3;
        }else if(draw_rect_count <= 0){
          moving_width_pix = 3;
        }
      }
      if(changeDrawCount(draw_time, 35000, 35500)){
        draw_rect_count = 0;
        clearAll();
        draw_time = millis();
      }
      canSendImage = true;
    }
  }
}
//*********************************************
void taskHTTP(void *pvParameters){
  connectToWiFi(ssid, password);
  startHttpd();
  while(true){
    delay(1);
  }
}
//****************************************
void startHttpd(){
  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 cmd_uri = {
    .uri       = "/command",
    .method    = HTTP_GET,
    .handler   = cmd_handler,
    .user_ctx  = NULL
  };

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

  if (httpd_start(&control_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(control_httpd, &index_uri);
    httpd_register_uri_handler(control_httpd, &cmd_uri);
  }

  config.server_port += 1;
  config.ctrl_port += 1;

  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &stream_uri);
  }
}
//****************************************
static esp_err_t stream_handler(httpd_req_t *req){
  canStartStream = true;
  esp_err_t res = ESP_OK;
  char * part_buf[64];

  static const char* stream_part = "Content-Type: image/bmp\r\nContent-Length: %u\r\n\r\n";
  #define PART_BOUNDARY "myboundary"
  static const char* stream_content_type = "multipart/x-mixed-replace;boundary=--" PART_BOUNDARY;
  static const char* stream_boundary = "\r\n--" PART_BOUNDARY "\r\n";

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

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

  draw_time = millis();
  while(true){
    if(canSendImage){
      if(res == ESP_OK){
        size_t hlen = snprintf((char *)part_buf, 64, stream_part, file_size);
        res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
      }
      if(res == ESP_OK){
        res = httpd_resp_send_chunk(req, (const char *)bmp_data_buf, file_size);
        canSendImage = false;
        float fps = 1000.0 / (millis() - (float)frame_last_time);
        Serial.printf("%.02lf(fps)\r\n", fps);
        frame_last_time = millis();
      }
      if(res == ESP_OK){
        res = httpd_resp_send_chunk(req, stream_boundary, strlen(stream_boundary));
      }
      if(res != ESP_OK){
        break;
      }
    }
    delay(1);
  }
  return res;
}
//****************************************
static esp_err_t index_handler(httpd_req_t *req){
  String html_body = "<!DOCTYPE html>\r\n";
          html_body += "<html><head></head><body>\r\n";
          html_body += "<img id='pic_place' width='200' height='148' style='border-style:solid; transform:scale(1, -1);'>\r\n";
          html_body += "<div>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='startStream()'>ON Stream</button><br>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='changeControl(\"red\",1)'>RED</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeControl(\"green\",1)'>GREEN</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeControl(\"blue\",1)'>BLUE</button></p>\r\n";
          html_body += "<p><button style='border-radius:25px;' onclick='changeControl(\"re_start_stream\",1)'>Re-Start Stream</button>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeControl(\"stop_stream\",1)'>Stop Stream</button></p>\r\n";
          html_body += "<button style='border-radius:25px;' onclick='changeControl(\"close_connection\",1)'>Close Connection</button><br>\r\n";
          html_body += "</div>\r\n";
          html_body += "<script>\r\n";
          html_body += "var base_url = document.location.origin;\r\n";
          html_body += "var url_stream = base_url + ':81';\r\n";
          html_body += "function startStream() {\r\n";
          html_body += "var pic = document.getElementById('pic_place');\r\n";
          html_body += "pic.src = url_stream+'/stream';};\r\n";
          html_body += "function changeControl(id_txt, value_txt){\r\n";
          html_body += "var new_url = base_url+'/command?id=';\r\n";
          html_body += "new_url += id_txt + '&';\r\n";
          html_body += "new_url += 'value=' + value_txt;\r\n";
          html_body += "fetch(new_url)\r\n";
          html_body += ".then((response) => {\r\n";
          html_body += "if(response.ok){return response.text();} \r\n";
          html_body += "else {throw new Error();}})\r\n";
          html_body += ".then((text) => console.log(text))\r\n";
          html_body += ".catch((error) => console.log(error));};\r\n";
          html_body += "</script></body></html>\r\n";

  httpd_resp_set_type(req, "text/html");
  httpd_resp_set_hdr(req, "Accept-Charset", "UTF-8");
  return httpd_resp_send(req, html_body.c_str(), html_body.length());
}
//******************************************
static esp_err_t cmd_handler(httpd_req_t *req){
  char*  buf;
  size_t buf_len;
  char id_txt[32] = {0,};
  char value_txt[32] = {0,};

  buf_len = httpd_req_get_url_query_len(req) + 1;
  if (buf_len > 1) {
    buf = (char*)malloc(buf_len);
    if(!buf){
      httpd_resp_send_500(req);
      return ESP_FAIL;
    }
    if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
      Serial.println(buf);
      if (httpd_query_key_value(buf, "id", id_txt, sizeof(id_txt)) == ESP_OK &&
        httpd_query_key_value(buf, "value", value_txt, sizeof(value_txt)) == ESP_OK) {
      } else {
        free(buf);
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
    } else {
      Serial.println(buf);
      free(buf);
      httpd_resp_send_404(req);
      return ESP_FAIL;
    }
    free(buf);
  } else {
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }

  uint8_t val = atoi(value_txt);
  int res = 0;
  if(!strcmp(id_txt, "re_start_stream")) {
    canStartStream = true;
    shouldClear = true;
    draw_time = millis();
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "stop_stream")) {
    canStartStream = false;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "red")) {
    ctrl_red = 0xff, ctrl_green = 0, ctrl_blue = 0;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "green")) {
    ctrl_red = 0, ctrl_green = 0xff, ctrl_blue = 0;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "blue")) {
    ctrl_red = 0, ctrl_green = 0, ctrl_blue = 0xff;
    Serial.printf("%s = %d\r\n", id_txt, val);
  }else if(!strcmp(id_txt, "close_connection")) {
    shouldClear = true;
    httpd_resp_set_hdr(req, "Connection", "Close");
    httpd_resp_send(req, NULL, 0);
    Serial.println("※このボタンは現在機能しません");
    delay(100);
    return ESP_OK;
  }else {
    res = -1;
  }

  if(res){
    return httpd_resp_send_500(req);
  }

  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  return httpd_resp_send(req, NULL, 0);
}
//*********************************************
boolean changeDrawCount(uint32_t now_time, uint32_t start_time, uint32_t stop_time){
  if((millis() - now_time > start_time) && (millis() - now_time < stop_time)){
    return true;
  }
  return false;
}
//*********************************************
void clearAll(){
  memset(bmp_data_buf + bmp_head_bytes, 0, disp_height_pix * max_w_pix_buf);
}
//*********************************************
void drawRectangleFill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(red, green, blue, rgb565_msb, rgb565_lsb);
  for(int i = x0; i <= x1; i++){
    drawVerticalLine565(i, y0, y1, rgb565_msb, rgb565_lsb);
  }
}
//*********************************************
void drawHorizontalLine(uint16_t x0, uint16_t y0, uint16_t x1, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(red, green, blue, rgb565_msb, rgb565_lsb);
  drawHorizontalLine565(x0, y0, x1, rgb565_msb, rgb565_lsb);
}
//*********************************************
void drawHorizontalLine565(uint16_t x0, uint16_t y0, uint16_t x1, uint8_t rgb565_msb, uint8_t rgb565_lsb){
  judgeMaxPixel(x0, max_x);
  judgeMaxPixel(x1, max_x);
  judgeMaxPixel(y0, max_y);
  uint16_t p = 0;
  for(uint16_t i = x0; i <= x1; i++){
    p = bmp_head_bytes + y0 * max_w_pix_buf + i * 2;
    bmp_data_buf[p] = rgb565_lsb;
    bmp_data_buf[p + 1] = rgb565_msb;
  }
}
//*********************************************
void drawVerticalLine(uint16_t x0, uint16_t y0, uint16_t y1, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(red, green, blue, rgb565_msb, rgb565_lsb);
  drawVerticalLine565(x0, y0, y1, rgb565_msb, rgb565_lsb);
}
//*********************************************
void drawVerticalLine565(uint16_t x0, uint16_t y0, uint16_t y1, uint8_t rgb565_msb, uint8_t rgb565_lsb){
  judgeMaxPixel(x0, max_x);
  judgeMaxPixel(y0, max_y);
  judgeMaxPixel(y1, max_y);
  if(y0 > y1) std::swap(y0, y1);
  uint16_t p = 0;
  for(uint16_t i = y0; i <= y1; i++){
    p = bmp_head_bytes + i * max_w_pix_buf + x0 * 2;
    bmp_data_buf[p] = rgb565_lsb;
    bmp_data_buf[p + 1] = rgb565_msb;
  }
}
//*********************************************
void drawRectangleLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(red, green, blue, rgb565_msb, rgb565_lsb);
  drawHorizontalLine565(x0, y0, x1, rgb565_msb, rgb565_lsb);
  drawVerticalLine565(x0, y0, y1, rgb565_msb, rgb565_lsb);
  drawHorizontalLine565(x0, y1, x1, rgb565_msb, rgb565_lsb);
  drawVerticalLine565(x1, y0, y1, rgb565_msb, rgb565_lsb);
}
//*********************************************
void drawPixel(uint16_t x0, uint16_t y0, uint8_t red, uint8_t green, uint8_t blue){
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(red, green, blue, rgb565_msb, rgb565_lsb);
  drawPixel565(x0, y0, rgb565_msb, rgb565_lsb);
}
//*********************************************
void drawPixel565(uint16_t x0, uint16_t y0, uint8_t rgb565_msb, uint8_t rgb565_lsb){
  judgeMaxPixel(x0, max_x);
  judgeMaxPixel(y0, max_y);
  uint16_t p = bmp_head_bytes + y0 * max_w_pix_buf + x0 * 2;
  bmp_data_buf[p] = rgb565_lsb;
  bmp_data_buf[p + 1] = rgb565_msb;
}
//*********************************************
void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t red, uint8_t green, uint8_t blue){
  judgeMaxPixel(x0, max_x);
  judgeMaxPixel(x1, max_x);
  judgeMaxPixel(y0, max_y);
  judgeMaxPixel(y1, max_y);
  uint8_t rgb565_msb = 0, rgb565_lsb = 0;
  convertRGB888toRGB565(red, green, blue, rgb565_msb, rgb565_lsb);
  if(x1 == x0) {
    drawVerticalLine565(x0, y0, y1, rgb565_msb, rgb565_lsb);
    return;
  }
  if(y1 == y0) {
    drawHorizontalLine565(x0, y0, x1, rgb565_msb, rgb565_lsb);
    return;
  }
  double sita = atan2((double)(y1 - y0), (double)(x1 - x0));
  int16_t y_new;
  int i = x0;
  while(true){
    y_new = (uint16_t)round((double)(i - x0) * tan(sita) + y0);
    if(y_new >= disp_height_pix) y_new = max_y;
    if(y_new - y_old > 1){
      drawVerticalLine565(i, y_old, y_new, rgb565_msb, rgb565_lsb);
    }else{
      drawPixel565(i, y_new, rgb565_msb, rgb565_lsb);
    }
    y_old = y_new;
    if(x1 > x0){
      i++;
      if(i >= disp_width_pix) break;
    }else{
      i--;
      if(i < 0) break;
    }
  }
}
//*********************************************
void convertRGB888toRGB565(uint8_t red888, uint8_t green888, uint8_t blue888, uint8_t &rgb565_msb, uint8_t &rgb565_lsb){
  //RGB888をRGB565へ変換するには、下位ビットを削除するだけでOK。
  uint8_t red565 = red888 & 0b11111000;
  uint8_t green565 = green888 & 0b11111100;
  uint8_t blue565 = blue888 & 0b11111000;
  rgb565_msb = red565 | (green565 >> 5);
  rgb565_lsb = (green565 << 3) | (blue565 >> 3);
}
//*********************************************
void judgeMaxPixel(uint16_t &pix, uint16_t max_pix){
  if(pix > max_pix){
    Serial.printf("Over Max pix = %d\r\n", pix);
    pix = max_pix;
  }
}
//*********************************************
void connectToWiFi(const char * ssid, const char * pwd){
  Serial.println("Connecting to WiFi network: " + String(ssid));
  WiFi.disconnect(true, true);
  delay(1000);
  WiFi.begin(ssid, password);
  Serial.println("Waiting for WIFI connection...");
  while ( WiFi.status() != WL_CONNECTED ) {
    delay(500);
    Serial.print(".");
  }
  IPAddress myIP = WiFi.localIP();
  Serial.println("WiFi connected!");
  Serial.print("My IP address: ");
  Serial.println(myIP);
  delay(1000);
}

では、これもコンパイル書き込みしてみて下さい。
これも前節のスケッチと速度は変わらずに前回記事の3倍の速度が出ていると思います。

あとは、最初に紹介した動画のように前回記事と比較すると、httpdライブラリの凄さがわかると思います。
スゴイっすね。
httpdに完敗です。。。

編集後記

いやぁ~、、、やっぱArduino core の開発チームの方々はスゴイっす!!!
httpdの方が3倍も速いなんて驚きです。
私もいつかプログラミング速度を追求して、WiFiストリーミングの速度をここまで詰められるようになりたいと思いました。
でも、それをするにはあまりにも時間と労力がかかりすぎます。
そうなると、結局は既存のライブラリを使って、次の開発に進んだ方が得策かも知れませんね。

では、次回はイメージセンサからの映像をMotion JPEG(BMP)で飛ばしてみたいと思います。

ではまた。。。

Amazon.co.jp 当ブログのおすすめ

スイッチサイエンス ESPr Developer 32 Type-C SSCI-063647
スイッチサイエンス
¥2,420(2024/04/25 03:04時点)
ZEROPLUS ロジックアナライザ LAP-C(16032)
ZEROPLUS
¥22,504(2024/04/24 13:13時点)
Excelでわかるディープラーニング超入門
技術評論社
¥2,068(2024/04/24 21:40時点)

コメント

タイトルとURLをコピーしました