2010年11月5日金曜日

WebSocket Jpeg Streaming

以前、井上さん(@makoto_inoue)、遠藤さん(@MiCHiLU)主催の#wsbofに出た際、何となく見せたWebSocket Jpeg Streamingのソースをやっと公開しました。遅くなりすぎて、とってもごめんなさい。

まぁ、時期的に旬を逃したのですが、もうすぐSPEC-03が出るのと、丁度hybiでchunk/streamingに関するメールが飛んでいるので、そのあたりを少し。

WebSocketを利用されたことがある方ならご存じの通り、今現在、WebSocketで利用できるフレームフォーマットは、0x00と0xffで囲まれた、UTF-8のSTRINGだけです。

さて、WebSocketを利用して皆さん何を送受信したいと思うでしょうか?
うん、大抵の場合、JSONですよね。(XML…聞かなかったことにします)

例えば、こんなメッセージを送りたいとしましょう。
{"data": "Hello WebSocket!"}

この場合、現在のSPEC-76に準拠した各種WebSocketのServer側実装は、ほぼ全てが以下のようにフレームを構成してメッセージを送信しています。
0x00, '{"data": "Hello WebSocket!"}', 0xff

すると、そのメッセージを受けたブラウザでは、0x00, 0xffをはぎ取り、websocket.onmessage functionへのevent messageでは、'{"data": "Hello WebSocket!"}'だけを受け取ることが可能です。

つまり、javascript codeとしては、
var ws = new WebSocket("ws://hoge.com/fuga");
ws.onmessage = function(msg) {
    var obj = JSON.parse(msg.data);
}
とすることによって、obj.data == "Hello WebSocket!"を得られるということになります。

さて、ここで長い長いデータを送信したい、と思った場合、どうなるでしょうか?
0x00, '{"data": "1Mbytes data"}', 0xff

こういった場合、ブラウザは0xffが出現するまで、いつ終わるともわからないSTRINGをずっと保持しておかなくてはいけなくなります。(もしかすると0xffは永遠に来ないかもしれません)
Server側はもう少し楽できるのですが、今現在公開されているwebsocket modの殆どは、client側と同様に全てのデータフレームを構築してから送信しているように見受けられます。(違ってたらごめんなさい)

それを避けるためには通常、長い長いデータを適当なサイズで区切って送受信する事になります。
ここでは簡単にするために、サーバ側から'Hello WebSocket!'を区切って送信してみましょう。
0x00, '{"data": "Hel', 0xff
0x00, 'lo WebSocket!"}', 0xff
さて、この場合、clientであるjavascript側はちょっと困ったことになりますよね。
ws.onmessage = function(msg) {
    var obj = JSON.parse(msg.data); // exception
}

当たり前ですが、callbackで受け取ったmessageはJSON stringではないので、Exceptionが発生します。
これをどうにかしないといけないよね、と言う問題点が指摘され、フレームフォーマット再編の一因になった、と記憶しています。
さて、ここまでを覚えておいていただいて。WebSocket Jpeg Streamingの説明に入ります。

サーバ側
http://d.hatena.ne.jp/os0x/20100724/1280003367

ここで発表した通りなのですが、
https://docs.google.com/present/view?id=dfn9bp3w_06bh6spg8

のスライド14番、15番目を参照ください。
送りつけているJSON Stringは、
'{"fps": daemonで取得したfps, "image": "base64 encoded jpeg image"}'
となっており、上記のJSON Stringがブラウザには、
0x00, '{"fps": daemonで取得したfps, "im', 0xff
0x00'age": "base64 encoded jpeg image"}'0xff
などのように、chunkとして送付されてきます。

(誤解があるといけないので記述しておきますが、jpeg_streamer.cではchunkにしていません。mod_websocket for lighty側で勝手にchunkにされます…と言うか、しています。)

クライアント側
構成は、以下のようになっています。













肝は、websocket_chunk.js。

先程記述したように、
0x00, '{"data": "Hel', 0xff
0x00, 'lo WebSocket!"}', 0xff

のように送信されたメッセージは、WebSocket onmessage callback function内で単純にJSON.parseはできません。
その為、websocket_chunk.jsでは、JSON stringとして正しいメッセージを受けるまで、bufferingしています。(適当に作ったので、JSON stringだけです={}の数を数えてるだけです。arrayはご勘弁)
websocket_chunk.jsの中で、JSON stringとして正しいと判断した場合、JSON.parseをし、JSON objとして指定されたfunctionにそのJSON objを引数としてcallbackをかけます。
また、今回のデモのようなstreamingの場合、chunkとして送信されてくるdataでは常にws.onmessage callbackが発生します。
その場合、websocket_chunk.jsをmain thread(上の例で行くと、player.jsの中)で実行すると、main threadにcontext switchが引っ切り無しに起こる…と、思います。(ブラウザの実装を詳しく知らないので予想ですけど)
というわけで、その部分をworkerに叩きこんでます。(なお、現在は、chrome/chromiumおよびsafariでしか、WebSocket in Workerが動きません。ですので、Firefox4βなどで動作させたい方は、websocket_chunkをmain threadで動作させるよう変更していただけば、動作します)

最後に、worker内で、適当な数のJSON objをbuffering、JSON objのarrayとしてmain threadへpostMessage、と。
こんな流れになっています。

SPEC-03を用いれば解決するのかもしれませんが、それでもJavaScriptとbackend Server間でメッセージフォーマットを規定する必要はどうしても出てきます。その際の助けになれば、幸いです。

ソースコード一式は、https://github.com/nori0428/websocket_streamingから取得可能ですので、MBPやAirをお持ちの方、Linux+USB Camなどの環境をお持ちの方で、試してみようかなと思われる奇特な方はお試しください。
なお、これはテストとか全くしてませんので、悪しからず。

ではでは。

2 件のコメント:

  1. 追記として
    個人的に、WebSocketを用いた下り方向のstreamingに関しては、技術的な興味以上では余り効果的な利用方法は思いつきません。
    しかしながら、上り方向特に、Google TalkなどのApplicationで動画像、音声などの上り方向に関しては非常に効果的な使い方が出来るようになるかと思います。

    device/capture APIが実装され、PCおよびモバイル端末からのストリーミングをWebSocketを利用してUpload、MUXした結果を<video><audio>などで受け取れるようになると、幸せですね:)

    返信削除
  2. opencv for cocoaがmake出来ない方へ

    https://gist.github.com/665343

    返信削除