275.ラズパイ無双[17_WebSocketストリーミング1]
初回:2022/9/21
Raspberry Pi (ラズベリーパイ、通称"ラズパイ")で何か作ってみようという新シリーズです。『WebSocket』ベースの画像配信サーバーを構築していきたいとおもいます。
P子「やっと、始まるのね。書く書くサギかと思ってたわ」※1
前回も書きましたが、gevent、geventwebsocket ベースの画像ストリーミングサーバーを構築していきます。まずは、元ネタの解説から始めたいと思います。
例によって、サンプルプログラムは、下記に用意しました。
≪サンプル≫
https://osdn.net/downloads/users/39/39276/No275.zip/
1.gevent、geventwebsocket
前回に、ちらっと説明しましたが、もう少し詳しく述べたいと思います。
≪参考1≫
https://doitu.info/blog/5aabc92d31e68500964d9255
PythonとBottleでWebSocketサーバ/クライアントを実装
≪参考2≫
https://symfoware.blog.fc2.com/blog-entry-2426.html
BottleフレームワークでWebsocket通信を行う
今回のWebSocketのサンプルの元ネタ記事です。
≪参考1≫では、Bottleによる『WebSocketサーバの実装サンプル』に手を加えて、ブロードキャストするサンプルになっています。サーバー側の実装が非常に参考になりました。
クライアント側に関しては、vue.js を使用されています。この、vue.js では、変数の取り出しに、{{変数}}みたいな記述を行うので、bottleのテンプレートと相性が悪いです。
そこで、クライアント側は、純粋な jQuery で記述されている≪参考2≫を使う事にしました。
【main2.py】と、views/main2.html が、上記の参考1,2をマージして作成したベースとなるソースです。
変更箇所としては、webSocketの接続先となるIPアドレスを、テンプレートで指定できるようにしました。localhost では、起動しているパソコンでしか、アクセスできませんし、毎回、サーバーのIPアドレスを手書きで修正するのは邪魔くさいので。
あと、while True: のループで、複数クライアントに値を返す際、time.sleep(3) すると、制御が明け渡されないので、別のクライアントからのアクセスが受け付けられません。
その場合は、gevent の sleep を使えば、制御が移るので、複数クライアントからのリクエストを受け付ける事が可能になります。
クライアント側【main2.html】で言うと、ws.send(""); を、ws.onopen = function(evt)の中で行っています。これは、WebSocketsの接続に時間がかかるため、それ以前に sendするとエラーになるためです。参考までに、当初、
setTimeout(function(){ ws.send(""); },5000);
でタイミングを計っていましたが、別の解説記事で、ws.onopen してから、send すべしと書かれていました。
2.バイナリデータ
【main3.py】は、【main2.py】で、テスト的に導入したカウントアップの処理を、ジェネレータで作成してみたサンプルです。
【main4.py】は、カウントアップのジェネレータの代わりに、画像を返すジェネレータにしてみました。
画像は、cv2.imencode('.jpg', frame) で取得します。それを、文字列として、
ascimg = base64.b64encode(img).decode('ascii')
に変換して送信します。
受信側【main4.html】では、指定の タグに、
$('#player').attr("src", "data:image/jpg;base64,"+evt.data);
で渡します。
バイナリの画像データをBase64でテキストに変換して、Web上でBase64で受け取るのは、何となく非効率な気がします。
WebSocket には、バイナリでやり取りできる機能があり、
ws.binaryType = 'arraybuffer';
を指定することで、型付き配列(TypedArray)のバイナリデータを受信することができます。
【main4B.py】ascimg の代わりに、
bytimg = img.tobytes()
を送信します。クライアント側【main4B.html】では、
var blob = new Blob([evt.data], { type: "image/jpeg" });で
var url = window.URL || window.webkitURL;
$('#player').attr("src", url.createObjectURL(blob));
で画像を表示します。
≪参考3≫
https://qiita.com/Yarimizu14/items/f56123c738f12ad1844a
AjaxでバイナリのJPEG画像データを受け取って表示する
3.JSON送受信
【main5.py】は、JSON形式でクライアントからデータを送信し、受信したデータを加工して、JSON形式で応答するサンプルです。
サーバー側で、websocket.receive() で受け取った、body 文字列を、
message = json.loads(body)
でJSON形式に戻します。
クライアント【main5.html】に返す場合は、
var msg = JSON.parse(evt.data);
var text = "(" + new Date(msg.date) + ") " + msg.text ;
$('#result').append('<li>' + text + '</li>');
という感じで、JSON.parse でJavaScriptのオブジェクトに戻します。
一連の JSON形式でのやり取りができれば、各種情報を、リアルタイムでブラウザに返すことが可能です。
【main6.py】では、ascii 形式の画像データをJSONで渡すことで、画像と一緒に各種データをクライアントにリアルタイムで戻すサンプルになっています。
4.まとめ
ざっくり流しすぎですが、上記に書いていること以外に、色々とテストしています。geventwebsocket に依存した問題なのか、WebSocket全般の問題なのか判りませんが、根本的に課題は同じだと思いますので、他のモジュールを使用する場合でも、類似の課題にぶち当たる可能性があると思います。
そういう意味では、この記事でも参考になるかもしれません。
P子「参考にならない可能性もあるわね」
ほな、さいなら
======= <<注釈>>=======
※1 P子「やっと、始まるのね。書く書くサギかと思ってたわ」
P子とは、私があこがれているツンデレPythonの仮想女性の心の声です。