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:
と書かれていたので、swfmillとmtascをインストールしてmakeしました。MacとUbuntuのどちらでもコンパイルに成功しましたが、私の環境では生成された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 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)を取得し、HTML5のcanvasを使ってWebブラウザ上に描画します。描画するとき、モードが選択されていれば画像処理(白黒にする、赤・緑・青色の抽出)を行います。
実行するときは前回と同様に、Flashのプライバシー設定でカメラへのアクセスを許可してから行ってください。