WebSocketを使ってなんちゃってビデオチャットを作ってみた その2

はじめに

前回の続き。Webカメラのキャプチャ速度が遅いのでActionScriptをいじりました。ついでにJavaScriptで簡単なリアルタイム画像処理をする機能も実装しました。以下、その様子を録画した動画です。画像処理の様子はフルスクリーンで再生しないと分かりにくいです(;・∀・)

この動画は、Webカメラの映像をそのまま描画したり、白黒化や赤・緑・青色のみを抽出する画像処理の様子を録画したものです。前回よりもキャプチャ速度が数倍早くなっていることが確認できると思います。ですが、今回はWebSocketサーバとの通信は行っていません。

プラグインの改造

Webカメラの操作ができるようになるjQueryプラグインですが、キャプチャ速度が遅いのでソースコードを見てみました。以下は、そのコードの一部です。

  • jscam.as(214〜234行目)
214 	private static function _stream():Void {
215 
216 		buffer.draw(_root.video);
217 
218 		if (null != stream) {
219 			clearInterval(stream);
220 		}
221 
222 
223 		for (var i = 0; i < 240; ++i) {
224 
225 			var row = "";
226 			for (var j=0; j < 320; ++j) {
227 				row+= buffer.getPixel(j, i);
228 				row+= ";";
229 			}
230 			ExternalInterface.call("webcam.onSave", row);
231 		}
232 
233 		stream = setInterval(_stream, 10);
234 	}

この _stream() 関数は、ディスプレイ機器が映像を描画するときの動きに似ています。216行目でカメラが写している画像を取り出し、223〜231行目でカメラ画像の走査線を1本づつJavaScriptのメソッド(webcam.onSave)に渡しています。つまり、1フレームの画像データ(320×240px)を240回に分けてActionScriptからJavaScriptに渡しています。
この部分がボトルネックになっていると考えたので、1回で1フレームの画像データをActionScriptからJavaScriptに渡すように改造することにしました。

プラグインの公式ページに、

Recompile the Flash binary

If you've made changes to the code or did just adjust the size of the video in the XML specification file, you can easily recompile the swf file from Linux console with the provided Makefile. You are required to install the two open source projects swfmill and mtasc that can be easily installed using apt-get under Debian/Ubuntu:

apt-get install swfmill mtasc
vim src/jscam.xml
make

と書かれていたので、swfmillとmtascをインストールしてmakeしました。MacUbuntuのどちらでもコンパイルに成功しましたが、私の環境では生成されたswfは動きませんでした(ヽ'ω`)
私はActionScriptに関してはHelloWorldに毛が生えた程度の知識しかないので、なぜ動かないのか皆目見当もつきません。なので、プラグインの改造は諦めて、ネット上にあるサンプルコードを参考にしながら最低限の機能を実装したswfをゼロから作ることにしました。

Webカメラの映像をJavaScriptに渡すActionScript

上記のページのサンプルコードとプラグインのコードを参考にしながら以下のプログラムを書きました。

  • MyJSCam.as
package {
    import flash.display.Sprite;
    import flash.display.BitmapData;
    import flash.media.Camera;
    import flash.media.Video;
    import flash.external.ExternalInterface;

    [SWF(backgroundColor=0xFFFFFF, width="320", height="240")]
    public class MyJSCam extends Sprite {
        private var camera:Camera;
        private var video:Video;
        private var bmd:BitmapData;
        private var pixelsData:Array;
        
        function MyJSCam () {
            camera = Camera.getCamera();

            if (camera != null) {
                camera.setMode(320, 240, 24);  // width, height, fps
                video = new Video(camera.width, camera.height);
                video.attachCamera(camera);
                bmd = new BitmapData(camera.width, camera.height, false, 0xffffff);
                pixelsData = [];
                for (var x:uint=0; x<320; ++x) {
                    pixelsData[x] = new Array(240);
                }
                // swfの領域でカメラ映像を表示するとき必要な処理
                //addChild(video);   
                //video.x = 0;
                //video.y = 0;
            }

            ExternalInterface.addCallback("capture", capture);
        }

        private function capture ():Array {
            bmd.draw(video);

            for (var x:uint=0; x<320; ++x) {
                for (var y:uint=0; y<240; ++y) {
                    pixelsData[x][y] = bmd.getPixel(x, y);
                }
            }

            return pixelsData;
        }
    }
}

コンパイルにはmxmlcを使いました。

 % mxmlc MyJSCam.as

もし何度もコンパイルするなら、rlwrapとfcshを使うと速くて楽になりますよ。

 % rlwrap fcsh
Adobe Flex Compiler SHell (fcsh)
Version 4.1.0 build 16076
Copyright (c) 2004-2007 Adobe Systems, Inc. All rights reserved.

(fcsh) mxmlc MyJSCam.as
fcsh : コンパイルターゲット ID として 1 を割り当てました
設定ファイル "/usr/local/flex_sdk_4.1/frameworks/flex-config.xml" をロードしています
/Library/WebServer/Documents/webcamTmp2/MyJSCam.swf (992 バイト)
(fcsh) compile 1
設定ファイル "/usr/local/flex_sdk_4.1/frameworks/flex-config.xml" をロードしています
再コンパイル: /Library/WebServer/Documents/webcamTmp2/MyJSCam.as
理由: ソースファイルまたは含まれているファイルのいずれかが更新されました。
変更されたファイル : 1 影響を受けるファイル : 0
/Library/WebServer/Documents/webcamTmp2/MyJSCam.swf (992 バイト)

MyJSCam.swfの実行例

コンパイルして生成したMyJSCam.swfと同じ階層に以下のHTMLファイルを置いて下さい。

  • index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <script type="text/javascript">
            function init () {
                webcamera = document["MyJSCam"];
                cnvsObj = document.getElementById("cnvs");
                cntxtObj = cnvsObj.getContext("2d");
                cntxtObj.clearRect(0, 0, 320, 240);
                cameraImg = cntxtObj.getImageData(0, 0, 320, 240);
                captureFPS = createFPS();
                imageMode = "";
                interval = setInterval(captureLoop, 1000);
            }

            function createFPS () {
                var oldTime = new Date();
                return function () {
                    var newTime = new Date();
                    var ps = 1000 / (newTime - oldTime);
                    oldTime = newTime;

                    return ps;
                }
            }

            function captureLoop () {
                if (interval) clearInterval(interval);

                imageData = webcamera.capture();
                document.getElementById("captureFPS").innerHTML = Math.round(captureFPS()*100)/100;
                var pos = 0;
                for (var y=0; y<240; ++y) {
                    for (var x=0; x<320; ++x) {
                        var pixel = imageData[x][y];
                        var r = (pixel >> 16) & 0xFF;
                        var g = (pixel >> 8) & 0xFF;
                        var b = pixel & 0xFF;
                        if (imageMode) {  // FULL_COLOR モード以外のとき
                            var gray = Math.round((r + g + b) / 3);
                            switch (imageMode) {
                            case "GRAY_SCALE":  // 白黒にする
                                r = g = b = gray;
                                break;
                            case "RED":    // 赤色を抽出する
                                r = (r > 50 && g*1.5 < r && b*1.5 < r) ? r : gray;
                                g = b = gray;
                                break;
                            case "GREEN":  // 緑色を抽出する
                                g = (g > 50 && r*1.5 < g && b*1.5 < g) ? g : gray;
                                r = b = gray;
                                break;
                            case "BLUE":   // 青色を抽出する 
                                b = (b > 50 && r*1.5 < b && g*1.5 < b) ? b : gray;
                                g = r = gray;
                                break;
                            }
                        }
                        cameraImg.data[pos + 0] = r;
                        cameraImg.data[pos + 1] = g;
                        cameraImg.data[pos + 2] = b;
                        cameraImg.data[pos + 3] = 0xFF;
                        pos += 4;
                    }
                }
                cntxtObj.putImageData(cameraImg, 0, 0);

                interval = setInterval(captureLoop, 0);
            }
        </script>
    </head>
    <body onload="init()">
        <embed src="MyJSCam.swf" name="MyJSCam" allowScriptAccess="always" width="0" height="0" />
        <span id="captureFPS">captureFPS</span>
        <div><canvas id="cnvs" width="320" height="240"></canvas></div>
        <input name="mode" type="radio" value="" checked onclick="imageMode=this.value"/>FULL_COLOR
        <input name="mode" type="radio" value="GRAY_SCALE" onclick="imageMode=this.value"/>GRAY_SCALE
        <input name="mode" type="radio" value="RED" onclick="imageMode=this.value"/>RED
        <input name="mode" type="radio" value="GREEN" onclick="imageMode=this.value"/>GREEN
        <input name="mode" type="radio" value="BLUE" onclick="imageMode=this.value"/>BLUE
    </body>
</html>

この例では、MyJSCam.swfのcapture()メソッドを呼び出してカメラ画像データ(320×240px)を取得し、HTML5canvasを使ってWebブラウザ上に描画します。描画するとき、モードが選択されていれば画像処理(白黒にする、赤・緑・青色の抽出)を行います。

実行するときは前回と同様に、Flashのプライバシー設定でカメラへのアクセスを許可してから行ってください。

おわりに

今回作成したMyJSCam.swfを用いることでWebカメラのキャプチャ速度が向上しました。前回のに比べるとだいぶ滑らかになりましたが、上記の実行例はWebSocketサーバとの通信をまったく行っていません。なので、次回はMyJSCam.swfを前回のプログラムに適用してみようと思います。

本当は以下のライブラリを使って、石仮面の画像をリアルタイムに顔の上に重ねる族長(オサ)モードの実装をしたかったんですが、優先順位を考えて今回は諦めました(´・ω・`)ショボーン