JavaScript
Node.js

Javascriptでライブカメラっぽいアプリケーションを作ったメモ

続きを書きました。最後まで読んで、続きが気になる方はどうぞ。
http://brly.github.io/2018/11/28/zoku-javascript.html

作ったもの

とある「部屋」の様子(動画)を、パソコンやスマートフォンなどからアクセスした時に見えるようにする。
部屋には、何らかカメラのようなものを設置して、端末からはそのカメラが取得したデータを閲覧出来るようにする。

必要なもの

  • ネットを通じてアクセス可能なサーバー
  • ドメイン(出来れば)
  • カメラ(今回はWebカメラ使用)
  • カメラに常時繋がっている端末

簡単な概要

プログラムの書きたい欲が高じて書いたものなので、全体的にテキトー感が漂ってます。

簡素すぎる概要図

webcameraApp.png

各クライアントの端末 -> サーバー

各クライアントの端末から部屋の様子を視聴する方法として、Webサイトを作成し、
そこにブラウザでアクセスしてもらうと、部屋の様子を示す画像/動画が表示される、というようにしました。
なので、端末には(Javascriptが実行できる)ブラウザが入ってれば大丈夫でした。

カメラに常時繋がっている端末 -> サーバー

クライアントがブラウザだけで完結出来るように、カメラに常時繋がっているマシンもブラウザだけで完結します。
概要図では、server.example.comへつながっていますが、そこへ用意したサイトにブラウザでアクセスするだけで
サーバーへ動画像データが送信されるようになっています。

簡素なまとめ

上の話をまとめますと

  • example.com のサイト作り
  • server.example.com のサイト作り
  • server.example.com 経由で送られてくる動画像データを受け取る何らかのプログラム(backend.example.comと命名)

が必要であることが分かるかと思います。
ので、順番に書いていきます。

また、example.comやserver.example.comとかは全部同一マシンに作ったので
全部同じIPに名前解決されるようにしました。

動画の作り方

プログラムの説明に入る前に、これを作った時に採用した動画の作り方を書きます。
http://blogs.yahoo.co.jp/linear_pcm0153/32280752.html
基本動画はパラパラマンガで、フレーム毎の差分だけ持つと圧縮出来るというお話ですね。
なので、例えば完全フレームを1秒毎に用意して10fpsを実現する場合は

  • 完全フレーム1枚
  • 差分フレーム9枚

のデータがあって、あとは1秒以内に差分フレームのデータで0.1s毎に画像を更新し続ければ動画になります。
各クライアント端末はこのデータを受信し、手持ちのブラウザ上で上記を実行することで
部屋の様子となる動画を見ることが出来るようになります。

example.com 作成

Webサイトの作成方法は色々やり方があると思うのですが、作った時は普通にApacheを使って、静的なhtmlとjsファイルを設置しました。
あとはバーチャルドメインを設定しただけです。
example.comの目的は、クライアントが部屋内の様子となる動画(リアルタイム)を見ることです。
そのため、この example.com にアクセスをすると、
動画像データを管理するWebアプリケーションサーバーである backend.example.com と通信をし始め、
そのデータを読み込んでクライアントに表示するようにします。

通信はWebSocketを使います。Ajaxと比べて色々面倒くさいですが、
コネクションを繋ぎっぱなしにして何度もデータを転送する場合には多分こっちのほうが良いかと思われます。

処理フロー

各クライアントはbackendとのWebSocket通信が確立すると、WebSocketから「動画データを送って下さい」というリクエストをbackendへ送信します。
リクエストを受けるとbackendは完全フレームのデータ(DataURL形式)やその他の日時の情報などを含むJSONを文字列形式で送信し、
差分フレームのオブジェクトをMessagePackを使って圧縮してバイナリ形式で送信します。バイナリと文字列を分けたことにあんまり意味はないです。
今思えば、別に全部MessagePackでくるんでバイナリで送受信したほうがシンプルでした。
MessagePackを使っていたのは、やりとりするデータのサイズを抑えて少しくらいネットワーク負荷を抑えようかな、という気持ちからです。

クライアントがフレームデータを受信したら、あとは「動画の作り方」で書いたようなことをします。
動画の表現には、主にcanvasを使いました。
最初にcanvasに完全フレームを読み込ませたら、あとは差分が発生している部分のピクセルを時間毎に逐次更新します。
ピクセルへのアクセスはcanvasのImageDataオブジェクトを取得することで出来ます。
そして、差分フレームによる更新(=動画の再生)が完了したら、再びbackendへリクエストを投げます。この繰り返しです。

10fpsを実現しようとする場合、0.1s毎の繰り返し処理が必要となります。
その繰り返し処理にはsetTimeoutを使いました。

iterate.js
// 完全フレームをcanvasにセットする処理を書く
// .. ここに処理 ..

setTimeout(function(){
  // フレーム更新する処理を書く
  // .. ここに処理 ..

  // まだ、更新すべき差分フレームがある場合は繰り返す 
  if (shouldBeContinued) {
    setTimeout(arguments.callee, 100);
  }
}, 100);

arguments.calleeはどこかでdeprecatedとかいうのを見たのですが使ってました。

server.example.com 作成

このWebサイトはカメラに繋がっているパソコンからアクセスし、カメラから得られるデータをbackendへ送信します。
example.comと同様に、静的なhtmlとjsファイルを置いただけです。

処理フロー

navigator.getUserMediaを使って、Webカメラにアクセスします。
このWebカメラへのアクセスは多分ドライバが当たってないと出来ません。

あとは時間毎に完全フレームのデータと差分フレームのデータを作って、backendに送信します。
上にあるiterate.jsみたいな感じで時間毎にデータを取得して、差分フレームのデータを作ります。
データの送信にはやはりWebSocketを使いました。
そして、あとはこの処理(フレームデータ作成&送信)を繰り返します。

完全フレームの作成は canvas.toDataURL("image/octet-stream") で出来ます。
差分フレームは配列(Uint8Array)を毎フレーム作成し、配列へ一つ前のフレームを保持して
現フレームとの差を調べるようにして作成します。

backend 作成

backendはサーバー的なアプリケーションである必要が有ります。
常に外からの接続(WebSocket)を待っており、接続後のリクエストに応じて行うべき仕事をする必要が有ります。

とはいえ、今回行っていたことはとてもシンプルで、動作内容は大きくふた通りに別れます。

  • 各クライアント端末からの「データを送って下さい」というリクエストに応じて保持データを送信
  • server.example.comにアクセスしているカメラ接続端末から送信されてくるフレームデータの保持

前者は、リクエストが来たらデータを返すだけ、後者は送信されてきたデータを保持するようにするだけとなります。

そして、backend自体を実装するためにはWebSocketサーバーである必要が有ります。
色々な言語で色々なWebSocketサーバーの実装があるかと思いますが、言語はnode.jsで、サーバーの実装は
https://github.com/Worlize/WebSocket-Node
を使いました。

backendでデータを保持する方法ですが、プログラムのメモリ上で済ませていました。
データの読み書きが高速に扱えるようならば、DBなどに書き出せればnode.jsだと
pm2とか使って複数のプロセスに分散させることも出来るので、夢が広がりそうなのですが手を出してません。

書いていなかったのですが、backendのプログラムも同一のサーバーに置いてあります。
apacheは2.4.5くらいのバージョンだとWebSocketのリバースプロキシにも対応していたので、それを使いました。

実行

  1. カメラ接続端末から http://server.example.com にアクセス(動画データがbackendに転送され始める)
  2. 各端末から http://example.com にアクセス(backendから動画データを受信し、canvas上で描画が始まればOK)

運用して感じたこと

動画の圧縮アルゴリズムがアレ

今の所、完全に可逆圧縮で、Wikipediaのデータ圧縮-動画圧縮を見てると非可逆圧縮がイケてるっぽいですね。
javascriptから使えるのはあるんですかね。
あとPeerJSとかで映像送受信とか出来るみたいなので、そうすれば
そもそも中継するサーバー(backend)も必要ないのですが、部屋のネットワークの特性上で
22/80/443とimap/imaps/pop/popsくらいしかポートが空いてなかったので手が出せませんでした。

カメラにつながってる端末からアクセスしてる server.example.com のページ(というかブラウザ)がすぐ死ぬ

開いてるブラウザのタブに「クラッシュしました」と出ます。
とにかく死んではいけないのがこの端末なのですが、最初は1日に3,4回くらい死んでました。
そのうちリファクタリングしつつ、あんまり得意じゃない例外とか入れ始めたら死ににくくなってきました。

今後の展望

  • 動画圧縮改善
  • WebWorker使ってみる
  • DBとかへの書き出しとか