M5CameraとngrokでMotion JPEG動画を遠隔ストリーミングする実験

ngrokとM5Cameraで動画ストリーミング M5Stack

今回は久々にスゴイです。
ngrokを使って、M5Cameraの映像を遠隔地でストリーミング配信する実験をしてみました。
なんと、手持ちの4GスマホのブラウザからM5Cameraの動画ストリーミングができたんです。

スポンサーリンク

ngrokは今回、Windows10パソコンにインストールして使いましたが、おそらく、Macパソコンやラズパイでも使えると思います。ラズパイでは近々試してみる予定です。
とにかく以下の動画をご覧ください。(音無しです)

やったね!
4GスマホでM5Cameraのストリーミング再生できたということは、個人的にはかなりスゴイことだと思いました。

画角は160 x 120 pixelで、フレームレートは25fps出ています。
しかもSSL通信のhttpsアクセスですよ!
SSL通信はngrokに全て任せれば良いので、WiFiClientSecureライブラリは使わずに済みました。

今回、シングルタスクで動かしているので、露出変更の度に動画が一時停止してしまいますが、個人的には十分許容範囲だと思っています。
そして、ポイントは、動画ストリーミングとボタン制御のポート番号が同じ80番で実現できたことです。
無料版ngrokでは、ポートが1つしか使えないという制限のため、それで実現できたというのは素晴らしいですね。

これで、世界のどこへ行ってもM5Cameraのストリーミングが可能になりました。
ただ、パソコンかラズパイなどでngrokを動かしたままにしなければいけません。

え?
それなら、カメラ付きのラズパイの方がもっと簡単じゃね?

たぶん、皆さんそう思われるでしょう。
事実、私自身もそう思い、Twitterでつぶやいていました。
ラズパイZEROを使えばパソコンいらないし、もっとシンプルに構成できますよね。
でも、それを言ってしまったらオシマイです。
いいんです!自己満足で…。

ただ、実は、今回の実験目的は、Google Colaboratory上でM5Cameraの映像をストリーミングさせながらリアルタイム画像処理することでした。あくまでその途中成果なのです。
それを実現させるためにはngrokで遠隔ストリーミングできなければならなかったというわけです。

そして、既に実証済みですが、Google Colab上でM5Camera映像のリアルタイム画像処理はできました。
その方法は次回紹介したいと思いますが、まずはngrokとブラウザでM5Cameraストリーミングをする方法を紹介したいと思います。
HTTP 431エラーで長い間悩まされ、悪戦苦闘した経緯も述べたいと思います。

なお、プログラミングについては基本的に素人ですので、誤り等ありましたら、コメント投稿でご連絡いただけると助かります。

    【目次】

  1. 事前準備
  2. Arduino core ESP32のhttpd関数使用で、431 Request Header Fields Too Largeエラーが出てアクセスできない原因
  3. ngrokとの連携では、httpdをあきらめ、WiFiClientを使う
  4. コンパイル書き込み実行、そして動作確認
  5. まとめ

事前準備

使ったもの

M5Camera

Espressif Systems ESP32-WROVER搭載、イメージセンサOV2640搭載のM5Stack社製WiFi&Bluetoothマイコンモジュールです。
スイッチサイエンスさんで購入できます。

https://www.switch-science.com/catalog/5207/

なお、以前、当ブログでM5Cameraをレビューした記事がありますので、参考にしてみて下さい。

M5Camera をレビューしてみた。分解したり、Arduino IDE でスマホに映したりする実験

Wi-Fiルータ環境

M5CameraはWiFi STAモードで動作させるので、別途Wi-Fiルータ環境が必要です。
事前にM5Cameraが接続できるようにファイアウォールやMACアドレスフィルタリングを見直しておきます。

パソコンおよびUSBケーブル

ここではWindows10 パソコンを使いました。
M5Cameraと接続するUSBケーブルは信頼性の高い、良質なUSBケーブルを使います。

スマートフォン

今回は手持ちの4G回線Android 10 スマホと、Wi-FiのみのiPad (iOS12.5.3)を使って実験しました。

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

Arduino IDE のバージョンは以下で動作確認しています。
ver 1.8.13
ver 1.8.15

Arduino core for the ESP32 は以下のバージョンで動作確認しています。
stable 1.0.6

Arduino core for the ESP32のインストール方法は以下の記事を参照してください。

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

ngrokのインストール

ngrokは、トンネリングでローカルサーバーを全世界にWeb公開できるツールです。
Windows、MacOS、Linux、FreeBSDで使うことができます。

今回使用したものは、Windows10 64bit用で、バージョンは以下です。
ver 2.3.40

インストール方法は前回の以下の記事を参照してください。
リージョン(地域)はJPにし、Basic認証を設定しておきます。

ngrokインストール方法と簡単な使い方

Arduino core ESP32のhttpd関数使用で、431 Request Header Fields Too Largeエラーが出てアクセスできない原因

これは私が長い間ハマった事です。

M5CameraやESP32-CAMでは、サンプルスケッチのCameraWebServerを参考にして、httpd関連関数を使うことが多いと思います。
ただ、それでngrokにアクセスすると、HTTP 431エラーとなって、アクセスできないのです。

ブラウザ Google Chrome 91 では以下のエラーが出ました。
Header fields are too long for server to interpret.

また、ngrokコマンドプロンプトでは以下のエラーが表示されました。
431 Request Header Fields Too Large.

これは、クライアントからのリクエストヘッダサイズが大きすぎるという意味です。

あれ?
ESP32側はサーバーだから、自分のプログラミングが原因ではなさそうだし、これはブラウザからGETリクエストした時のヘッダのバイト数が多すぎるということなので、ブラウザ側の問題になってしまいます。
おかしいな~、と思いました。

では、HTTP 431エラーについて、MDNの以下のドキュメントを参照してみます。
https://developer.mozilla.org/ja/docs/Web/HTTP/Status/431

そこに書いてあることをそれぞれ検証してみました。

ブラウザのCookieを全削除しても431エラーは消えなかった

先ほどのMDNドキュメントによれば、まず、ブラウザで保存してあるCookieデータが多すぎるとリクエストサイズが大きくなり、エラーが出る場合があるという記述がありました。
しかし、自分のブラウザのCookieを全て削除してみたところ、解決できませんでした。
後ほどリクエストヘッダデータを紹介していますが、そこでもCookieによる多量のリクエストヘッダは見受けられませんでした。
ネット検索でも多くはこの類の情報でしたが、今回の場合は的外れでした。

URLを短くしても431エラーは変わらず

もう一つの対策方法として、リクエストするURLの長さを短くするという方法がありました。
今まで、私はBasic認証をURLに含めてリクエストしていたので、そのせいかと思いました。
しかし、Basic認証を外して、最小限のURLにしてみても、431エラーは消えませんでした。
これも的外れでした。

Basic認証とSSL通信を外した普通のHTTPアクセスの場合はOKだった、けど…

ngrokを使う上で最低限のセキュリティ対策として、Basic認証をセットして、SSLのhttps通信することだと思います。
それでアクセスできないと意味がありません。

では、その場合のGoogle ChromeのGETリクエストヘッダ情報を見てみます。

下図の様に、Google Chromeのデベロッパーツールの「Network」タブで、Request Headersを開いてみます。

(図01)

デベロッパーツールでリクエストヘッダを見る方法

Google ChromeのデベロッパーツールでHTTPリクエストヘッダの内容を見る方法。

すると、こんな感じでした。

Request Header
:authority: xxxxxxxxxxxx.jp.ngrok.io
:method: GET
:path: /
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
accept-encoding: gzip, deflate, br
accept-language: ja,en-US;q=0.9,en;q=0.8,da;q=0.7,af;q=0.6
authorization: Basic xxxxxxxxxxxxxxxxxxxxxxxxx
cache-control: max-age=0
dnt: 1
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="90", "Chromium";v="90"
sec-ch-ua-mobile: ?0
sec-fetch-dest: document
sec-fetch-mode: navigate
sec-fetch-site: none
sec-fetch-user: ?1
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4472.77 Safari/537.36

先ほども述べたように、Cookieによる長文のリクエスト項目はありませんね。
このヘッダサイズはそんなに長いとは思いませんが、一体どういうことなのでしょうか???

ならば、SSLのhttpsで、Basic認証無しのURLを作って、リクエストしてみます。
すると、こうなりました。

Request Header
:authority: xxxxxxxxxxxx.jp.ngrok.io
:method: GET
:path: /
:scheme: https
accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
accept-encoding: gzip, deflate, br
accept-language: ja,en-US;q=0.9,en;q=0.8,da;q=0.7,af;q=0.6
dnt: 1
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="90", "Chromium";v="90"
sec-ch-ua-mobile: ?0
sec-fetch-dest: document
sec-fetch-mode: navigate
sec-fetch-site: none
sec-fetch-user: ?1
upgrade-insecure-requests: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4472.77 Safari/537.36

これでも、やはり431エラーが出ました。
先ほどのBasic認証有りのリクエストヘッダと比較すると、authorization項目が無いだけで、ヘッダサイズはあまり変化ありません。

ならば、SSLも外して、単なるhttpでリクエストしてみます。
こうなりました。

Request Header
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8,da;q=0.7,af;q=0.6
Connection: keep-alive
DNT: 1
Host: xxxxxxxxxxxx.jp.ngrok.io
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4472.77 Safari/537.36

どうですか?
ヘッダサイズはかなり減りましたね。
secure関連がゴソッと無くなりました。
現に、これでアクセスすると、httpd関数群を使ってもエラーが出ませんでした。

ということで、ブラウザからのリクエストヘッダサイズが大きかったことが原因のようです。
(実は、後で述べていますが、httpd関数のリクエストヘッダの最大サイズは512byte設定でした。)

ただ、これではセキュリティ全く無しで、世界中の誰でも簡単にアクセスできて、とても危険な状態です。
それではとても使えないので、他の方法を探ってみます。

因みに余談ですが、ngrokを通さずに、ローカルネットワークでアクセスする場合は431エラー出ないので、その場合のリクエストヘッダも調べてみると、こうなりました。

GET / HTTP/1.1
Host: 192.168.0.10
Connection: keep-alive
DNT: 1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: ja,en-US;q=0.9,en;q=0.8,da;q=0.7,af;q=0.6

約390文字でした。
ということで、431エラーはヘッダサイズが原因っぽいですね。

Firefox と iOS(12.5.3) は431エラー出なかった

実は、Firefoxブラウザ(ver 88.0.1)およびiOS(12.5.3)では431エラーが出ませんでした。
httpd関数を使っても問題無く表示されました。

なら、Google Chromeと何が異なるのかを検証してみます。

まず、Google ChromeからESP32に届くリクエストヘッダを見てみると、こんな感じでした。

GET / HTTP/1.1
Host: xxxxxxxxxxxx.jp.ngrok.io
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4472.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8,da;q=0.7,af;q=0.6
Authorization: Basic xxxxxxxxxxxxxxxxxxxxxxxxx
Cache-Control: max-age=0
Dnt: 1
Sec-Ch-Ua: " Not;A Brand";v="99", "Google Chrome";v="90", "Chromium";v="90"
Sec-Ch-Ua-Mobile: ?0
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
X-Forwarded-For: xxx.xxx.xxx.xxx
X-Forwarded-Proto: https

この文字数を数えてみると、約750文字ありました。
となると、後で述べるようにhttpd関数のデフォルト設定値512文字を超えてしまうわけです。

では、Firefoxでアクセスしてみると、リクエストヘッダはこうなりました。

GET / HTTP/1.1
Host: xxxxxxxxxxxx.jp.ngrok.io
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.7,en;q=0.3
Authorization: Basic xxxxxxxxxxxxxxxxxxxxxxxxx
Te: trailers
Upgrade-Insecure-Requests: 1
X-Forwarded-For: xxx.xxx.xxx.xxx
X-Forwarded-Proto: https

文字数は約430文字で、512文字を下回りました。
と、いうことで、Firefoxでアクセスすると、GETリクエストのヘッダサイズが少ないのでhttpd関数を使っても正常に表示されるという訳だと思われます。

ちなみに、iOS(12.5.3)ではこんな感じです。

GET / HTTP/1.1
Host: xxxxxxxxxxxx.jp.ngrok.io
User-Agent: Mozilla/5.0 (iPad; CPU OS 12_5_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Mobile/15E148 Safari/604.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: br, gzip, deflate
Accept-Language: ja-jp
Authorization: Basic xxxxxxxxxxxxxxxxxxxxxxxxx
X-Forwarded-For: xxx.xxx.xxx.xxx
X-Forwarded-Proto: https

約390文字で、Firefoxより少なかったです。
よって、FirefoxとiOSでは問題無く表示されるというわけです。

でも、やはりGoogle ChromeやEdgeで表示させたいですね。
ならばArduino core ESP32のhttpdライブラリ関数を使うにはどうしたらよいのかを次で探ってみます。

sdkconfig の CONFIG_HTTPD_MAX_REQ_HDR_LEN 設定を変えても431エラーは消えない

いろいろとネットの情報を漁っていたら、GitHubのArduino core ESP32のissueにちょっとしたヒントらしきものがありました。

Arduino core ESP32内に以下の2つのファイルがあります。
sdkconfig
sdkconfig.h

このファイルに以下のような定義があります。
CONFIG_HTTPD_MAX_REQ_HDR_LEN=512

これを、1024に変更すれば良いという記述がありました。

しかし、実際に1024あるいは4096に変更して、Arduino IDEを再起動してコンパイル書き込みし、ngrokへアクセスしてみましたが、エラーは無くなりませんでした。
結局この方法も的外れでした。

それに、sdkconfigファイルはArduino core ESP32をアップデートすると上書きされてしまうので、私個人的にそれを変更したくありません。

ならば、Arduinoスケッチ上で、

#undef CONFIG_HTTPD_MAX_REQ_HDR_LEN
#define CONFIG_HTTPD_MAX_REQ_HDR_LEN 4096

とし、更に

config.max_uri_handlers   = 64;
config.max_resp_headers   = 64;

と再定義して試してみました。
しかし、変わらず431エラーになってしまい、いずれもうまくいきませんでした。
結局どれも的外れでした。

おそらく、先ほどのGitHub issueにあるように、ESP-IDFCONFIG_HTTPD_MAX_REQ_HDR_LENを再定義してビルドし直さないと、うまくいかないのかも知れません。
ということで、別の対策を探ることにしました。

結局、私はhttpd関数は使わないことに決めました。
(ただし、これは現在のバージョンの場合のお話で、今後更新されたらエラーが出なくなるかも知れませんね。)

実は、この次のページで紹介しているように、従来のWiFiClientクラスを使えば、問題無く使えることが分ったんです。

コメント

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