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を使った実験も紹介したいし・・・。
また解明しなければならない課題が山ほど出来てしまいました。

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

ではまた。。。

コメント

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