ESP32でビットマップ画像ファイルを生成し、ブラウザに連続送信してMotion JPEGならぬMotion BMP動画ストリーミングする実験

ESP32,でビットマップ画像を生成し、Motion,JPEG,でブラウザに動画ストリーミングさせてみた。 ESP32 ( ESP-WROOM-32 )

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

では、最初に紹介した動画のように、先のスケッチよりも、もう少し凝ったアニメーションを紹介します。
ほんのちょっと毛が生えた程度です。

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

/* The MIT License (MIT)
 * License URL: https://opensource.org/licenses/mit-license.php
 * Copyright (c) 2020 Mgo-tec. All rights reserved.
 * 
 * Use Arduino core for the ESP32 stable v1.0.4
 */
#include <WiFi.h>
#include <WiFiClient.h>
#include <utility> //swap関数を使う場合に必要

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;
uint8_t bmp_data_buf[disp_height_pix][max_w_pix_buf] = {};
//----------------------------------
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};
//*********************************************
void setup() {
  Serial.begin(115200);
  Serial.println();
  delay(1000);
  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;
    }
  }
}
//*********************************************
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, 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);
  for(uint16_t i = x0; i <= x1; i++){
    bmp_data_buf[y0][i * 2] = rgb565_lsb;
    bmp_data_buf[y0][i * 2 + 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);
  uint16_t x01 = x0 * 2;
  uint16_t x02 = x0 * 2 + 1;
  if(y0 > y1) std::swap(y0, y1);
  for(uint16_t i = y0; i <= y1; i++){
    bmp_data_buf[i][x01] = rgb565_lsb;
    bmp_data_buf[i][x02] = 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);
  bmp_data_buf[y0][x0 * 2] = rgb565_lsb;
  bmp_data_buf[y0][x0 * 2 + 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 taskHTTP(void *pvParameters){
  connectToWiFi(ssid, password);

  WiFiServer server80(80), server81(81);
  server80.begin();
  server81.begin();
  WiFiClient client80, client81;

  while(true){
    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";

    String html_res_head = "HTTP/1.1 200 OK\r\n";
           html_res_head += "Content-Length: " +String(html_body.length());
           html_res_head += "\r\n";
           html_res_head += "Content-Type: text/html\r\n";
           html_res_head += "Accept-Charset: UTF-8\r\n";
           html_res_head += "\r\n";

    client80 = server80.available();
    if (client80) {
      String req_str = "";
      if (client80.connected()){
        while(client80.available()){
          req_str =client80.readStringUntil('\n');
          if(req_str.indexOf("GET / HTTP/1.1") >= 0) {
            Serial.println(req_str);
            if(!receiveToBlankLine(client80, req_str)) goto exit_1;
            client80.print(html_res_head);
            client80.print(html_body);
            html_res_head = "";
            html_body = "";
            delay(10);
            Serial.println("HTML body send ok!!!!!!!!!");
            while(true){
              client81 = server81.available();
              if(!receiveOnStream(client80, client81, server80)){
                stopClient8081(client80, client81);
                break;
              }
              delay(1);
            }
            req_str = "";      
          }else if(req_str.indexOf("GET /favicon") >= 0){
            if(!receiveToBlankLine(client80, req_str)) break;
            sendFaviconResponse(client80);
            stopClient8081(client80, client81);
            break;
          }
          delay(1);
        }
        delay(1);
      }
    }
exit_1:
    delay(1);
  }
}
//*********************************************
boolean receiveOnStream(WiFiClient &client80, WiFiClient &client81, WiFiServer &server80){
  String req_str = "";
  while(client81.available()){
    req_str =client81.readStringUntil('\n');
    if(req_str.indexOf("/stream") >= 0) {
      Serial.println("----------On Stream");
      Serial.println(req_str);
      if(!receiveToBlankLine(client81, req_str)) break;
      String res_http = "HTTP/1.1 200 OK\r\n";
      res_http += "Access-Control-Allow-Origin: *\r\n";
      res_http += "Content-type: multipart/x-mixed-replace;boundary=--myboundary\r\n";
      res_http += "\r\n";

      client81.print(res_http);
      res_http = "";
      req_str = "";
      delay(10);
      canStartStream = true;
      if(!startStreamMJPEG(client80, client81, server80)){
        return false;
      }
    }
    delay(1);
  }
  return true;
}
//*********************************************
boolean startStreamMJPEG(WiFiClient &client80, WiFiClient &client81, WiFiServer &server80){
  String req_str = "";
  String bound = "--myboundary\r\n";
  String head_bound = "Access-Control-Allow-Origin: *\r\n";
  head_bound += "Content-type: image/bmp\r\n";
  head_bound += "Content-Length: " + String(file_size);
  head_bound += "\r\n";
  head_bound += "\r\n";
  draw_time = millis();

  while(true){
    if(!receiveCtrlRequest(client80, server80)){
      return false;
    }
    if(canStartStream){
      if(canSendImage){
        client81.print(bound);
        client81.print(head_bound);
        streamBmp(client81);
        canSendImage = false;
      }
    }
    delay(1);
  }
  return true;
}
//*********************************************
boolean receiveCtrlRequest(WiFiClient &client80, WiFiServer &server80){
  client80 = server80.available();
  String req_str = "";
  while(client80.available()){
    req_str = client80.readStringUntil('\n');
    if(req_str.indexOf("id=stop_stream") >= 0) {
      if(!receiveToBlankLine(client80, req_str)) break;
      sendCtrlResponse(client80);
      canStartStream = false;
      break;
    }else if(req_str.indexOf("id=re_start_stream") >= 0) {
      if(!receiveToBlankLine(client80, req_str)) break;
      sendCtrlResponse(client80);
      canStartStream = true;
      shouldClear = true;
      draw_time = millis();
      break;
    }else if(req_str.indexOf("id=red") >= 0) {
      if(!receiveToBlankLine(client80, req_str)) break;
      sendCtrlResponse(client80);
      ctrl_red = 0xff, ctrl_green = 0, ctrl_blue = 0;
    }else if(req_str.indexOf("id=green") >= 0) {
      if(!receiveToBlankLine(client80, req_str)) break;
      sendCtrlResponse(client80);
      ctrl_red = 0, ctrl_green = 0xff, ctrl_blue = 0;
    }else if(req_str.indexOf("id=blue") >= 0) {
      if(!receiveToBlankLine(client80, req_str)) break;
      sendCtrlResponse(client80);
      ctrl_red = 0, ctrl_green = 0, ctrl_blue = 0xff;
    }else if(req_str.indexOf("favicon") >= 0) {
      if(!receiveToBlankLine(client80, req_str)) break;
      sendFaviconResponse(client80);
    }else if(req_str.indexOf("id=close_connection") >= 0) {
      if(!receiveToBlankLine(client80, req_str)) break;
      sendCloseResponse(client80);
      shouldClear = true;
      return false;
    }
    delay(1);
  }
  return true;
}
//*********************************************
void sendCtrlResponse(WiFiClient &client80){
  client80.print("HTTP/1.1 200 OK\r\n");
  client80.print("Access-Control-Allow-Origin: *\r\n\r\n");
}
//*********************************************
void sendCloseResponse(WiFiClient &client80){
  client80.print("HTTP/1.1 200 OK");
  client80.print("Connection: close\r\n\r\n");
}
//*********************************************
bool receiveToBlankLine(WiFiClient &client, String &req_str){
  Serial.println(req_str);
  req_str = "";
  uint32_t time_out = millis();
  while(true){
    if(client.available()){
      req_str =client.readStringUntil('\n');
      Serial.println(req_str);
      if(req_str.indexOf("\r") == 0){
        return true;
      }
    }
    if(millis() - time_out > 10000) {
      Serial.println("--------error Time OUT receiveToBlankLine");
      break;
    }
    delay(1);
  }
  return false;
}
//*********************************************
void streamBmp(WiFiClient &client81){
  client81.write(bmp_header, bmp_head_bytes);
  for(int i = 0; i < disp_height_pix; i++){
    client81.write(&bmp_data_buf[i][0], max_w_pix_buf);
  }
  client81.print("\r\n");
  float fps = 1000.0 / (millis() - (float)frame_last_time);
  Serial.printf("%.02lf(fps)\r\n", fps);
  frame_last_time = millis();
}
//*********************************************
void sendFaviconResponse(WiFiClient &client80){
  Serial.println(F("----------Favicon GET Request Received"));
  client80.print("HTTP/1.1 404 Not Found\r\n");
  client80.print("Connection: keep-alive\r\n\r\n");
}
//*********************************************
void stopClient8081(WiFiClient &client80, WiFiClient &client81){
  delay(10);
  client81.flush();
  client81.stop();
  delay(10);
  client80.flush();
  client80.stop();
  delay(10);
  Serial.println("------client80, client81 stop");
}
//*********************************************
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);
}

ブラウザに表示するHTMLのボタンが増えて、ストリーミング中に線や四角形の色を変えられるようにしました。

これで難しかったのは、drawLine関数の斜線の描画ですね。
基本的に点の集合で構成しますが、斜線の傾き角度が小さいと、点が分離してしまいます。
その場合、垂直線や水平直線で描画させて繋げています。
斜線の関数作りは、けっこう頭使うし、疲れますねぇ、、。

では、これをコンパイル書き込み実行してみてください。

色のチェンジは、メッシュ状の斜線の時と、四角形の水平移動の時のみです。
最初に紹介した動画のように表示されればOKです。

フレームレートを上げるには

アニメーションのスムースさ、つまりフレームレートを上げるには、ビットマップ画像では限界があります。

先にも述べたように、今回のビットマップ画像のファイルサイズは
200 x 2 x 148 = 59.2KB
もあります。
これをJPEG画像に圧縮して、10分の1くらいに抑えれば、かなり高速に描画になって、30fpsは可能かと思われますし、もっと大きな画像でもスムースな動画が可能かと思われます。
テレビの画像が約30fpsですから、そこまで実現できれば良いですね。

ただ、JPEG圧縮は私にとってはとても難易度高いので、今後の課題とします。

通信のトラブル解決に便利なツール、Wireshark

Arduino core for the ESP32 で Motion JPEG を実現するために、今回大活躍したのが、フリーのパケット解析ソフト Wireshark です。
これはほんと便利ですよ!

なかなか Motion JPEG の動画ストリーミングが動かず、悩んでいたら、このソフトを使ってパケットを解析したら、ブラウザ側とサーバー側のどちらが悪いのか一発で見抜くことができました。
これが無かったら、自分でMotion JPEGのHTTPコネクション確立は達成できなかったと思います。

このソフトも詳しく紹介したかったのですが、今の自分ではそんな時間は全くありませんでした。
いつか、このソフトの素晴らしさを紹介したいですね。

HTTPコネクションの問題点

このスケッチで遊んでいると、iOS のSafariや他の端末でも交互に表示させたりしたくなります。
そうしたら、うまく動画が表示されない時が度々ありました。
(自分のiPadが古いせいかも知れませんが、、、)
これをAndroid スマホでもたまに症状が出ました。

原因はまだ追究していませんが、どうも元で表示させた端末のコネクション切断が、十分でないような挙動を示します。

そこで、ふと思いました。
先日、こちらの記事にコメント投稿があって、マルチタスクの無限ループに原因があるのではないかという報告がありました。
もしかしたら、それの影響もあるかも知れないと思いました。
今後時間があったら、実験して解明してみたいですね。
Wiresharkでパケット解析すれば、原因が分かるかも知れませんが、今回はその時間が無いので、今後の課題とします。
Arduino core for the ESP32 がバージョンアップしたら、意外と直っちゃうかもね。

実は httpd ライブラリを使うと、もう少しフレームレートが上がる

実はもう実験済みなのですが、Arduino core for the ESP32 のhttpdライブラリを使って、Motion JPEG動画ストリーミングをすると、フレームレートが少し上がります。
これは素晴らしいですね。
やはり、Arduino core の開発チームは凄いなと思いますね。
中身をあまり解明していないのですが、WiFiClientライブラリとはちょっと構造が異なるっぽいです。
これは今後解明して、記事にできたらいいなと思っています。

Httpdで動作させる方法は次回以降記事にしたいと思っています。

編集後記

以上、どうでしたでしょうか。

前回の記事から今まで本業の仕事や家庭の仕事で多忙で、なかなかプログラミングやブログ執筆にとりかかれませんでした。
おかげで、ちょっとずつ貯めていた情報が盛り沢山になってしまい、今回の記事で膨大な情報を記事にする羽目になり、いつものように疲労困ぱい状態になってしまいました。
こういうのはできるだけ避けたいですね。

Motion JPEG ならぬ Motion BMP は、今までにない描画方法で新鮮ですよね。
ブラウザの画面をお絵描き感覚で図形を描いている感じがして面白いですね。
MJPEGは使い古された昔の規格ですが、私の様なアマチュアにはとっても重宝する手法です。
イメージセンサとの相性も抜群ですし、今後の応用に想像が膨らみますね。

今回はビットマップ画像を扱いましたが、やはり高速でスムースな動画を目指すならばJPEGを扱わねばならないことがよーくわかりました。
httpdを使った実験も紹介したいし・・・。
また解明しなければならない課題が山ほど出来てしまいました。

さて、いつディープラーニングを始められるのやら、、、。

ではまた。。。

コメント

  1. 組み込みプログラマ より:

    mgo-tecさん、お久しぶりです。

    ESP32でカメラ画像の送信に興味を持って、調べていたらまたmgo-tecさんのページにたどり着きました。今回もいろいろ読ませて頂きました。ありがとうございます。
    記事を読ませて頂いたお礼に少しだけ気になった事を書かせていただきます。
    (いつもの事ですが参考にならなかったら無視して頂いて結構です。)

    UDPの説明で「これは、UDP規格の下位層で再送設定されているようで、UDPデフォルトの正常な動作のようです。」とありますが、UDPの規格では恐らく再送される事はないと思います。(私が四半世紀以上間違った知識を持っていたのでなければ100%あり得ないのではないかと思います。)
    なぜかと言うと、TCPではデータを送信すると受信側はそのデータを受け取ったという返事(ACK)を送信側に返信します。このACKが一定時間以内に送信側に返らないと、送信側はパケロスが発生したと考えて再送をするという仕組みになっています。
    しかし、UDPではデータを送信したらしっぱなしで、相手に届いたのかどうかは気にしません。なのでUDP層で再送を実施する方法自体がないと思われます。(少なくとも私がLinuxやWindowsでネットワークプログラミングをしていた頃はUDPは再送制御の無いプロトコルという認識でした。)
    又、「UDPはESP32やM5Stackなどの自作IoT機器間のハンドシェイク(コネクション確立)が簡単で実現しやすいのが利点です。ただ、パケットロスは避けられません。」とも書かれていますが、UDPにはそもそもコネクション確立という概念がありません。複数の機器がそれぞれ自分のUDPポートをオープンして、それらのポート間でお互いにデータを投げ込むだけなので、TCPのようなコネクションの確立フェーズはありません。(なので相手のポートがオープンしていなくても送信自体は正常にできてしまいます。)
    パケロスについては、一般的にUDPを使用して高信頼性の通信をする場合は、UDPポートを使用するアプリケーション層で受信時の到達確認の応答や、送信時の一定時間以内の返信が無い場合の再送をシーケンスとして考えて作成します。(送信側はデータ送信後に受信側からの返事を待ち、返事が来たら次のデータを送信する様にし、受信側はデータを受信したら返信をする様にして、TCPがやっている再送制御をアプリケーションが実施する様にします。)
    今回の様なケースでは、上記の様にデータの到達確認制御を入れる様にすれば、受信側の限界に近いパフォーマンスが出せるのではないかと思いました。(今回のケースでは受信側が取りこぼしている確率が高いと思われますので。)

    又、今回のソースを見ていて、ポート80と81のタスクを別々に作成して、コマンド受信側のポート(ポート80でしょうか)のプライオリティを高く設定すれば、ポート81のスレッドで画像の送信をしながらポート80でコマンドを受信した時に即座にコマンドに応答できる様に出来そうな気がしました。(Serverクラスがマルチタスクに対応していればの話になってしまいますが。)

    それでは、失礼します。

    • mgo-tec mgo-tec より:

      組み込みプログラマさん

      たいへんお久しぶりですね。
      2020年に書いたつたない記事をまたまた読んでいただき、ありがとうございます。

      私は現在、諸事情でほとんどESP32を触っておらず、このブログもしばらく放置状態です。

      さてさて、確かにUDPってコネクション確立とか、再送処理とか無いはずですね。
      ただ、うろ覚えですが、この記事を書いた当初は、UDPなのになぜか再送処理っぽい動きをしていたので、そういう想像で記事を書いた記憶があります。
      今思えば、組み込みプログラマさんがおっしゃる通り、Arduino core ESP32がアプリケーション層で何やら処理していそうな気がしますね。

      当時はUDPでの動画送信はどうやってもうまくいかず、Twitterでお世話になった「らびやん」さんから教えてもらい、結局はTCPで送信した方が確実で早くで発熱もしなかったのでした。
      もしかしたら、UDPでもうまくプログラミングすれば、TCPよりも良いかも知れませんね。
      ただ、今は残念ながら再検証する時間が無いのですが…。
      そんなこんなで、いつもいろいろ教えて頂き、ありがとうございます。
      記事も時間がある時に修正を入れたいと思います。
      m(_ _)m

      • 組み込みプログラマ より:

        mgo-tecさん、お返事を頂きましてありがとうございます。
        現在はEPS32を触っていないとの事ですが、せっかくここまで色々と調べたり作ったりしてきたのですから、また面白いプログラムを作ってプログに公開できる様になると良いですね。
        以前にも書きましたが、mgo-tecさんの書いたこのブログは色々な方に大変役立っていると思います。私もESP32を使って何かしようと調べるとよくこのブログの記事がヒットして、拝見させて頂いています。
        ある意味、mgo-tecさんの財産といっても過言ではないくらい素晴らしい内容だと思いますので、これからも運用して頂けると参考にする私たちにとっても大変有難いと思いました。
        私なども、色々と忙しかったりなんだか面倒くさくなってしまったりする事も何度もありましたが、そんな時は少し手を休めて距離を置いたりして、またやりたくなった時に再開する様にしていました。
        mgo-tecさんも、またESP32やその他ガジェトなどを使った面白いプログラムを作れる様な環境になる事を祈っています。
        それでは、失礼します。

        • mgo-tec mgo-tec より:

          組み込みプログラマさん

          とてもありがたいお言葉、感謝感謝です。
          多くの読者に役に立っているのなら嬉しい限りです。
          でも、いろいろと未熟者で、今は反省ばかりですが…。

          ブログ休眠中でも、コメント投稿で問合せがあり、ブログ運営の大変さを今さら感じています。
          今は本業と生活が厳しく、暇が全く無い状態なのです。
          ブログ運営だけで生活できるだけの報酬が出れば、是非ともブログを再開したいんですけどね~…。
          それは無理としても、いつか生活に余裕ができたら再開したいと思っています。

          そんなわけで、また何かお気づきの点がありましたら、コメント頂けると幸いです。
          うれしいコメントありがとうございました~。
          m(_ _)m

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