Freenove ESP32-S3-WROOM CAMボードのカメラを使って、MJPEG(Motion JPEG)によりブラウザ上で表示を更新します。
HTTP上でMotionJPEGの配信
multipart/x-mixed-replaceというのは、Netscape社が提唱した、サーバからのプッシュ配信の為のもので、クライアント側は、新たなpartが送られてきたら、表示中のものを破棄して新しいPartを表示する、という仕様になっています。
各JPEG画像の区切りの判断は、通常のmultipart同様、Content-Typeに記載したboundary文字列で判断します。イメージとして、いくつか省略していますが、最初に2行がレスポンスヘッダで、その後に–myboundaryから始まる各文章(ブロック)がJPEG画像一枚一枚に対応します。
HTTP/1.1 200 OK content-type: multipart/x-mixed-replace; boundary=myboundary --myboundary Content-Length:74429 Content-Type:image/jpeg (JPEG画像) --myboundary Content-Length:74429 Content-Type:image/jpeg (JPEG画像)
このContentTypeが処理できるブラウザは、ChromeとFireFoxで、IEとEdgeでは処理できません。※AndroidのChromeでも処理できました。
MJPEGスケッチの作成
MJPEGスケッチ「esp32s3_mjpeg.ino」を作成します。
esp32s3_mjpeg.ino
/**********************************************************************
Filename : Camera and SDcard
Description : Use the onboard buttons to take photo and save them to an SD card.
Auther : www.freenove.com
Modification: 2022/11/02
**********************************************************************/
#include "esp_camera.h"
#define CAMERA_MODEL_ESP32S3_EYE
#include "camera_pins.h"
#include "ws2812.h"
#include "sd_read_write.h"
//*************
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#define BUTTON_PIN 0
//*************
const char* ssid = "aterm-5459b0-g"; // 各自のWifi環境のSSIDを指定する
const char* password = "618fcf2fb4b02"; // 各自のWifi環境のPasswordを指定する
// Webサーバーのインスタンスを作成
WebServer server(80); // ポート番号は80を使用
hw_timer_t* timer = NULL;
uint64_t time_ms;
uint64_t current_time;
// MJPEGストリームのヘッダ
const char* STREAM_HEADER =
"HTTP/1.1 200 OK\r\n"
"Content-Type: multipart/x-mixed-replace; boundary=frame\r\n"
"\r\n";
// JPEGフレームのヘッダ
const char* FRAME_HEADER =
"--frame\r\n"
"Content-Type: image/jpeg\r\n"
"Content-Length: %d\r\n"
"\r\n";
// ルートエンドポイント
const char* root_html = R"rawliteral(
<p>It might take more than 5 seconds to capture a photo.</p>
<div><img src="stream" ></div>
)rawliteral";
// JPEGストリームハンドル
int count = 0;
void handleJPGStream() {
WiFiClient client = server.client();
if (!client.connected()) {
Serial.println("Client disconnected");
return;
}
camera_fb_t* fb = NULL;
// MJPEGストリームのヘッダーを送信
current_time = timerReadMillis(timer);
client.print(STREAM_HEADER);
time_ms = timerReadMillis(timer) - current_time;
Serial.print("TSTREAM_HEADER (ms): ");
Serial.println(time_ms);
while (client.connected()) {
// Serial.println(count);
count = count + 1;
// カメラのフレームをキャプチャ
current_time = timerReadMillis(timer);
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
return;
}
time_ms = timerReadMillis(timer) - current_time;
Serial.print("sp_camera_fb_get (ms): ");
Serial.println(time_ms);
// JPEGフレームのヘッダーを送信
current_time = timerReadMillis(timer);
client.printf(FRAME_HEADER, fb->len);
client.write(fb->buf, fb->len); // JPEGデータを送信
time_ms = timerReadMillis(timer) - current_time;
Serial.print("FRAME_HEADER+write (ms): ");
Serial.println(time_ms);
// フレームバッファを開放
current_time = timerReadMillis(timer);
esp_camera_fb_return(fb);
time_ms = timerReadMillis(timer) - current_time;
Serial.print("esp_camera_fb_return (ms): ");
Serial.println(time_ms);
// 10 FPS: delay(100)
// 20 FPS: delay(50)
// 30 FPS: delay(33)
// delay(33); // 次のフレームまで待機(FPS調整)
ws2812SetColor(count & 3);
}
}
void setup() {
Serial.begin(115200);
Serial.setDebugOutput(false);
Serial.println();
pinMode(BUTTON_PIN, INPUT_PULLUP);
ws2812Init();
sdmmcInit();
//removeDir(SD_MMC, "/camera");
createDir(SD_MMC, "/camera");
listDir(SD_MMC, "/camera", 0);
if (cameraSetup() == 1) {
ws2812SetColor(2);
} else {
ws2812SetColor(1);
return;
}
//***********************************
Serial.setDebugOutput(true);
// PSRAMのサイズを確認
if (ESP.getPsramSize()) {
Serial.println("PSRAM is present.");
Serial.print("PSRAM size: ");
Serial.println(ESP.getPsramSize());
} else {
Serial.println("PSRAM is not present.");
}
// Wi-Fi接続
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
Serial.println("Connected to WiFi");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP()); // IPアドレスを表示
// ルートエンドポイントでホームページを表示
server.on("/", HTTP_GET, []() {
server.send(200, "text/html", root_html);
});
// /stream エンドポイントで映像をストリーム
server.on("/stream", HTTP_GET, handleJPGStream);
// サーバーを開始
server.begin();
Serial.println("Web server started!");
timer = timerBegin(1000000); // タイマーハンドルを取得
}
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) {
delay(20);
if (digitalRead(BUTTON_PIN) == LOW) {
ws2812SetColor(3);
while (digitalRead(BUTTON_PIN) == LOW)
;
camera_fb_t* fb = NULL;
fb = esp_camera_fb_get();
if (fb != NULL) {
int photo_index = readFileNum(SD_MMC, "/camera");
if (photo_index != -1) {
String path = "/camera/" + String(photo_index) + ".jpg";
writejpg(SD_MMC, path.c_str(), fb->buf, fb->len);
}
esp_camera_fb_return(fb);
} else {
Serial.println("Camera capture failed.");
}
ws2812SetColor(2);
}
}
//*******************************
server.handleClient();
}
int cameraSetup(void) {
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 10000000;
config.frame_size = FRAMESIZE_VGA;
config.pixel_format = PIXFORMAT_JPEG; // for streaming
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
// if PSRAM IC present, init with UXGA resolution and higher JPEG quality
// for larger pre-allocated frame buffer.
if (psramFound()) {
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// Limit the frame size when PSRAM is not available
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}
// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return 0;
}
sensor_t* s = esp_camera_sensor_get();
// initial sensors are flipped vertically and colors are a bit saturated
s->set_vflip(s, 1); // flip it back
s->set_brightness(s, 1); // up the brightness just a bit
s->set_saturation(s, 0); // lower the saturation
Serial.println("Camera configuration complete!");
return 1;
}
MJPEGスケッチの実行
作成したMJPEGスケッチ「esp32s3_mjpeg.ino」を実行します。パソコンのブラウザから「http://192.168.10.108/」にアクセスします。シリアルモニタに次のように時刻が表示されます。表示更新間隔がばらついていることが分かります。
撮影されている動画の時計の針の動きにばらつきがあり、1秒以下で更新されていない場合もあることが分かります。
