こんばんは。
約2か月ぶりの記事更新です。
今回はちょっとスゴイですよ!
ESP32開発ボードM5StacでArduinoプログラミングしてビットマップ(BMP)ファイルを生成し、Google ChromeやFireFoxおよびSafari等のブラウザに連続送信して、動画ストリーミングを実現してみる実験です。
今回は初の試みで Motion JPEG (MJPEG) という手法を使いました。
しかも、動画ストリーミングしながら、ブラウザからボタン操作ができる双方向通信が実現できました。
今まで私が過去に扱ってきたストリーミング通信の WebSocket や Server-Sent Events あるいは UDP などの、どれとも異なります。
自分的にはめちゃめちゃ新鮮な手法です。
(といっても、MJPEG自体はかなり昔からある手法だったみたいです)
ちょっと注意ですが、MJPEGはMPEGとは異なりますよ~~!
以前、私は同じだと思っていましたが、全然違っていました。
今さら知った次第です。
Motion JPEG方式はTwitterで、@s1tnkさんから参考サイトを教えて頂きました。
教えて頂かなかったら、たぶん延々と分からなかったかも知れません。
感謝感謝でございます。
ありがとうございました~!!!
m(_ _)m
今回は、MJPEGを使ったと言っても、JPEG画像ではなく、BMP画像を使いました。
ネット上ではビットマップ(BMP)画像を使った Motion JPEGの例が殆どありませんでしたが、やってみたら難無く動作しました。
難無く動作したとは言っても、既存のHTTPClientやWebServer、httpdライブラリは使わず、基本的なWiFiやWiFiClientライブラリだけを使って動作させました。
これにはほんと苦労しました。
とにかく、まずは四角形を表示させるだけのシンプルなMotion JPEGストリーミングの以下の動画を見てください。
これは、200 x 148 pixel のビットマップ画像をESP32で生成して、WiFiで連続してスマホに送信しています。
外部WiFiルーターを介す、STAモードです。
これでも一応パラパラ漫画風アニメーションなんです。
ストリーミング中でもボタンコントロールができていて、双方向通信できていますね。
パッと見、面白くない動作ですが、これを実現するまでにほんと苦労しましたよ。
では、続いて、もうちょっと凝ったアニメーションを作ってみた以下の動画をご覧ください。
どうでしょうか。
まるで、スマホのブラウザにマイコン直結の液晶ディスプレイのような感覚で表示させることができました。
これはパッと見ではHTMLのCanvasに描いたように見えますが、ビットマップ画像のアニメーションなんですヨ!!!
メッシュ状の斜線と四角形ムービーのところでは、スマホのボタンスイッチで色をリアルタイムで変えられています。
このスマホは旧型Androidなので、フレームレートは 5fps 程度しか出ていません。
ビットマップ画像の容量が大きいので、仕方ありません。
なので、ボタン操作もちょっとタイムラグがあります。
では、続いてWindows 10パソコンのGoogle Chromeで動作させた動画をご覧ください。
この場合は、フレームレートが 7fps 出ました。
やはり、受信側機器の処理能力に左右されますね。
でも、UDPデータグラムに比べてパケットロスがありません。
受信側のパソコンやスマホの方がESP32より格段に処理能力が高いので当たり前と言えば当たり前ですが、やはりこれはTCP/IP通信の優れたところですね。
らびやんさんのおっしゃる通りでした。
しかも、WiFiルーターの能力によって、かなり距離が離れていてもストリーミングできて、素晴らしいと思いました。
ということで、アマチュア素人の自分なりの目線でMotion JPEGならぬ Motion BMP を紹介していこうと思います。
- なぜMotion JPEG (MJPEG)を使うことにしたのか
- 各ブラウザの動作状況
- 他のストリーミング(およびデータグラム)通信との違い
- ビットマップ(BMP)ファイルの作り方
- Arduino core ESP32 で Motion JPEG ストリーミングを実現するザッとした流れ
- 双方向通信の方法(Motion JPEGで動画ストリーミング送信しながらブラウザからのリクエストを受信する)
- 使ったもの
- Arduino core for the ESP32 のインストール
- スケッチ例(四角形を表示するだけのシンプルなMotion JPEGストリーミング用)
- HTMLおよびJavaScript ソースコード
- コンパイル書き込み実行
- スケッチ例(ちょっと凝ったMotion JPEGアニメーション)
- フレームレートを上げるには
- 通信のトラブル解決に便利なツール、Wireshark
- HTTPコネクションの問題点
- 実は httpdライブラリを使うと、もう少しフレームレートが上がる
- 編集後記
【目次】
なぜMotion JPEG (MJPEG)を使うことにしたのか
なぜ、今、私がMotion JPEGを使うことにしたのかと言うと、去年からESP32やM5Stackとイメージセンサ OV2640を搭載したM5Cameraとのストリーミング(データグラム)通信の実験をしていて、UDP通信だとパケットロスしたり、なかなかフレームレートが上げられなかったりして、謎が多かったんです。
UDPはパケットロスすることは分っていたのですが、高速通信できるはずなのに、もうちょっと何とかなりそうなのに変だなぁ、、、と思っていました。
そこでTwitterでつぶやいていたところ、いつもお世話になっているらびやんさんが、TCP/IPの方がパケットロスが無くて充分速度が出るよ、とおっしゃっていたので私も試してみることにしました。
そう言えば、以前、このブログのこちらの記事で、Arduino core for the ESP32のサンプルスケッチCameraWebServerを使った実験をしました。
そうしたら、スマホのブラウザに表示されたカメラ画像ストリーミングがメチャメチャ高速で、驚くほどリアルタイム性が高かったんです。
画角を800 x 600 pixelくらいの大画面にしても、20fpsくらい速度が出ていて、スゴかったんです。
ということは、プログラミング次第でここまで高速化が実現できて、パケットロスも無くせるわけですね。
そこで、サンプルスケッチのCameraWebServerを読み解いてみると、ブラウザとの通信はhttpdライブラリ関数を使っていました。
ただ、このhttpdの関数群を見ても、ブラウザとのコールアンドレスポンスが謎すぎて、良く解りませんでした。
それに、ブラウザに出力するHTMLタグは、GZIP圧縮されたバイナリファイルをヘッダに組み込んでいて、
「なんじゃこりゃ!?」
状態でした。
アマチュアの私にとっては敷居が高すぎるっっ!!! と思いました。
そんなこんなでしたが、ブラウザのソースコードビュー画面を見れば、HTMLソースコードが読み取ることができました。
さらに解明を進めていると、今度は<img>タグだけで動画ストリーミングが実現できている謎にぶち当たりました。
動画形式ファイルと言えば、MPEG形式を想像しますよね。
私は動画に関しては全くのド素人で、仕組みも全く分からないのですが、パソコンで動画を楽しむ場合、多くがMPEG形式でした。
<img>タグと言えば、JPEGやBMPなどの静止画だけの表示のはずなのに、なぜ・・・?
動画は<video>タグのはずでは・・・?
謎、謎、謎、、、でした。
そもそも、OV2640などの旧式イメージセンサは、MPEG動画出力はありません。
JPEGやビットマップ画像、YUVなどのフォーマットです。
JPEGやビットマップ画像設定の場合は、完成された1枚の画像(1フレーム)を順次高速で生成し、パラパラ漫画のようにESP32などのマイコンにDMA(Direct Memory Access)で送って来ます。
(これについては過去の以下の記事を参照してください。
esp32-cameraライブラリを読み解く ~OV2640, SCCB, DMA, I2S 編~)
この1枚1枚の画像をブラウザに一方的に送り付けていて、なぜ動画ストリーミングになるのか、不思議でなりませんでした。
そんな中、先にも述べたように、この謎をTwitterでつぶやいていたら、@s1tnkさんからとっても有益な情報をいただきました。
これはMotion MPEGという手法で、以下の記事を参考にすれとよいですよ! ということでした。
なんと!
衝撃的!!!
ネットで調べてもなかなか分からなかったのですが、この記事で一発理解できました。
HTTPレスポンスヘッダにほんの少しのテキストファイルを含めるだけで、JPEG動画ストリーミングが実現できてしまうとのことです。
HTTP通信を理解されている人ならば、この記事を見れば一発で理解できると思います。
WebSocketの時のようにハンドシェイクに苦労していたのはいったい何だったのか、、、と思いましたね。
たったこれだけでストリーミングできるなら、早く言ってよ!、、、って思っちゃいました。
そんなこんなでしたが、改めて、@s1tnkさん、およびQiita記事の@Yukio-Ichikawaさん、情報ありがとうございました。
m(_ _)m
というわけで、Moton JPEG ( MJPEG )を使ってみることにしたわけです。
後で方法を詳しく説明しますが、これは便利ですよ!
ブラウザをまるで液晶ディスプレイのように画像出力できるんです。
特に、イメージセンサから受け取った画像をパラパラ漫画のように連続送信して動画ストリーミングする場合、最適な手段だと思います。
MJPEG自体は古い昔の規格ですが、自作IoTや電子工作界隈では、まだまだ現役で使えそうですよ!
各ブラウザの動作状況
私が2020/3月時点で動作確認して分かったのは、こんな感じです。
(2021/4月時点で、Microsoft Edgeは動作OKでした。)
Windows 10 Google Chrome | OK |
---|---|
Android 8.0 Google Chrome | OK |
Windows 10 FireFox | OK |
iOS (12.4.5) Safari | OK |
Windows 10 Edge | OK |
Windows 10 Internet Explorer | × |
iOSのSafariについては、ブラウザの反応がイマイチでした。
ただ、私の手持ちiPadが古いせいかもしれません。
他のストリーミング(およびデータグラム)通信との違い
私は過去に3種類くらいWiFiでESP32とストリーミング(およびデータグラム)通信を実験してきました。
以下の
WebSocket
Server-Sent Events
UDP
などです。
プロトコル | コネクション確立 | データ送受信 | パケットロス | バイナリ送受信 | 対応ブラウザ |
---|---|---|---|---|---|
WebSocket | 超複雑 | 双方向 | あまり無い | OK | ほぼOK |
Server-Sent Events | 比較的簡単 | 一方向 | あまり無い | ×(テキストのみ) | IE以外OK |
UDP | 簡単 | 双方向可 | 多い | OK | 無理 |
Motion JPEG | 比較的簡単 | ほぼ双方向 | あまり無い | OK | IE以外OK |
Motion JPEG (MJPEG)
Motion JPEG ( MJPEG )は、先ほどもチラッと紹介したとおり、JPEG画像やビットマップ(BMP)画像のパラパラ漫画風の動画ストリーミングです。
後で詳しく紹介しますが、Web上のHTMLの<img>タグや<iframe>タグ内に画像を一方的に送信し続けることで実現します。
ブラウザとのコネクション確立(ハンドシェイク)は通常のHTTP通信ハンドシェイクのヘッダ情報に一行増やすだけで実現できて、JPEGやビットマップ画像の動画ストリーミングをするにはとっても手軽です。
ただ、ストリーミングにはTCP/IP および HTTPの規格に沿うため、一度に送るパケット通信量がデバイスによって決められています。
Arduino core for ESP32 の場合、1460 byteで定義されています。
なので、適度に画像を分割して送信するプログラミングが必要になります。
ビットマップ(BMP)画像でストリーミングする場合、JPEG画像に比べて数倍~数十倍の容量になるので、当然フレームレートも下がります。
そして、ボタン操作をして動画を停止したりするリアルタイム双方向通信は、プログラミングがちょっと難しいです。
後で詳しく紹介しますが、制御コントロール用のテキスト通信のポート番号と、動画ストリーミング用のポート番号は別にするという風にしなければうまくできませんでした。
WebSocket
WebSocketは、完全双方向リアルタイム通信ですが、ブラウザとのコネクション確立はかなり面倒で複雑です。
過去のこちらの記事ではコネクション確立(ハンドシェイク)方法を、こちらの記事ではデータ送受信方法を紹介しています。
ESP32とブラウザとのWebSocket通信はできましたが、ESP32同士やM5Stack同士は実現できていません。
私自身も素人ながらライブラリを作ったりしましたが、どこかのプロが作った既存ライブラリに頼った方が賢明だと思います。
そして、JPEG画像やビットマップ画像を送信する場合はバイナリデータを扱いますので、WebSocketでも可能なことは可能です。
でも、私はまだバイナリデータの送受信は試しておらず、テキストデータのみのままです。
Server-Sent Events
Server-Sent Events については、テキストデータの場合のみで、しかも一方向通信です。
一方向のテキストデータのストリーミングに限って言えば、WebSocketに比べてとても簡単で素晴らしいシステムだと思います。
ビットマップ画像などのバイナリデータを扱うことは工夫すれば出来ないことは無いかもしれませんが、特殊文字の条件分けが複雑で、とても非効率なプログラミングになるので、やめた方が良いですね。
UDP
UDPについては、ストリーミングという用語ではなく、正確にはデータグラムというそうです。
UDPの最大の欠点は、ブラウザとの通信が現時点では出来ないことです。
アプリ経由なら可能ですが、個人的にはやはりブラウザと通信したいですね。
UDPはESP32やM5Stackなどの自作IoT機器間のハンドシェイク(コネクション確立)が簡単で実現しやすいのが利点です。
ただ、パケットロスは避けられません。
パケットロスを防いで、できるだけ高速受信するためには、送信側の能力を圧倒的に上回る受信側の処理速度が必要ですね。
ESP32やM5Stack同士のUDP通信では、パケットロスを防ぐならば送信側の速度を遅くするしか無いようです。
それに、Arduino core ESP32 のWiFiデフォルト設定では、ロスしたパケットが再送されているらしく、そのせいで通信トラフィックを圧迫してしまいます。
これは、UDP規格の下位層で再送設定されているようで、UDPデフォルトの正常な動作のようです。
そのせいで、送信側の電力を多く消耗するのも厄介です。
その場合、トラフィックを監視しながら、送信間隔をコントロールするなどの処理が必要なのかも知れません。
では、次の節ではビットマップファイルの作り方を紹介します。
コメント
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クラスがマルチタスクに対応していればの話になってしまいますが。)
それでは、失礼します。
組み込みプログラマさん
たいへんお久しぶりですね。
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やその他ガジェトなどを使った面白いプログラムを作れる様な環境になる事を祈っています。
それでは、失礼します。
組み込みプログラマさん
とてもありがたいお言葉、感謝感謝です。
多くの読者に役に立っているのなら嬉しい限りです。
でも、いろいろと未熟者で、今は反省ばかりですが…。
ブログ休眠中でも、コメント投稿で問合せがあり、ブログ運営の大変さを今さら感じています。
今は本業と生活が厳しく、暇が全く無い状態なのです。
ブログ運営だけで生活できるだけの報酬が出れば、是非ともブログを再開したいんですけどね~…。
それは無理としても、いつか生活に余裕ができたら再開したいと思っています。
そんなわけで、また何かお気づきの点がありましたら、コメント頂けると幸いです。
うれしいコメントありがとうございました~。
m(_ _)m