読者です 読者をやめる 読者になる 読者になる

あんまり見ないでください

プログラミング・技術関連,アイディア,気づいたことなどを低レベルで垂れ流す場所.

プロキシ環境下でRobolectricを導入する

AndroidアプリのユニットテストでContextやandroid.jarのクラスを使ったメソッドのテストを書くときに、Robolectricを使えばAndroidTestよりも高速でテストできるという情報を得たので導入してみました。この記事では、プロキシ環境下でRobolectricを導入する手順を説明します。

目標

Robolectricのサンプル
robolectric/robolectric-samples · GitHub
ユニットテストをプロキシ環境下で実施する。

環境

導入手順

サンプルプロジェクトをインポート

robolectric/robolectric-samples · GitHub

でサンプルプロジェクトのZipファイルをダウンロードします(もちろん、gitでクローンしてもいいです)。ダウンロードしてきたら、AndroidStudioでプロジェクトをインポートします。

ユニットテストを実行できるようにする

  1. Run -> Edit Configurations... で、Run/Debug Configurationsの設定画面を出す。
  2. 左上の「+」ボタンを押すとAdd New Configurationというのが出てくるので、JUnitを選択する。
  3. Nameはなんでもよい
  4. Use class path of mod... は、テストしたいapiのもの(android-api-21とか)。
  5. Test KindはAll in packageでcomを指定すれば、パッケージがcomで始まるクラスが全てテストできる。(All in Directoryとかでもいいかもしれない)

プロキシ環境下でない場合は、ここでRunボタン(再生ボタンみたいなやつ)の左のセレクトボックスで追加したJunitのConfigurationを選択して実行すれば、動作するはずです。しかし、プロキシ環境下の場合はMavenリポジトリからライブラリをダウンロードするときに上手くいきません。

~/.m2/settings.xml を追加する

Mavenのプロキシ設定は、

<ユーザディレクトリ>/.m2/settings.xml

に記述します。.m2ディレクトリがない場合はコマンドラインのmkdirで作成するなどしてください。settings.xmlに以下のような記述をします。

<proxies>
  <proxy>
    <active>true</active>
    <protocol>http</protocol>
    <host>proxy.somewhere.com</host>
    <port>8080</port>
    <username>proxyuser</username>
    <password>somepassword</password>
    <nonProxyHosts>www.google.com|*.somewhere.com</nonProxyHosts>
  </proxy>
</proxies>

自分のプロキシ環境のホスト名やポート番号、自分のユーザ名とパスワードを記述してください。
参考:setting.xml解説

テストを実行する

  1. Build Variants(Android Studioの左サイドの下の方)をクリックし、Test ArtifactのセレクトボックスをUnit Testに変更
  2. Runボタン(再生ボタンみたいなやつ)の左のセレクトボックスで追加したJunitのConfigurationを選択して実行

Mavenリポジトリからライブラリをダウンロードする際に、[WARNING]が出るが気にしない。ダウンロードに時間がかかるが初回だけなので気にしない。

説明は以上です。

おまけ:自分のプロジェクトにRobolectricを導入する

自分のプロジェクトに導入する際は、

dependencies {
    testCompile "junit:junit:4.10"
    testCompile "org.robolectric:robolectric:3.0"
}

をappのbuild.gradeに記述して上記の導入手順を実施すれば、Contextやandroid.jarのクラスを使えるようになる。

感想

gitのプロキシ設定、Android Studioのプロキシ設定をして、もうプロキシ設定はいいやと思っていたところに、この問題が出てきたので「うげぇぇ!」と思いました。あと、Mavenリポジトリからライブラリをダウンロードする際、どうやら何かのアクセスでタイムアウトになり[WARNING]を出してからsettings.xmlを読んでプロキシ経由のアクセスをしている(ように見える)ので、ダウンロードに時間がかかっているのではと思いました。どうにかなりませんかね。

WebRTCのgetUserMediaで取得した映像をいじってみた その②【魚眼レンズ風に加工する】

前回の記事では、getUserMediaで取得した映像の画素値を変更して、映像を加工していた。前回は、加工前の画素の位置と加工後の画素の位置は同じだったが、今回は加工前の画素を射影して、魚眼レンズ風の映像を作る。

射影の方法

魚眼レンズ風にするにはどうすればよいかを以下の図を用いて説明する。

f:id:artak:20140812172932p:plain

図の説明

上図は、D だけ離れた平面の被写体を魚眼レンズで映している状況を表したものである。被写体の中心(魚眼レンズから被写体に垂線を引いたときの交点)から、L だけ離れた点から魚眼レンズに入射する光の入射角を θ とする。

射影の式の導出

魚眼レンズの射影方式はいくつかあるが、今回は(wikipedia さんによると)ほとんどの魚眼レンズで採用されているという、等距離射影方式を採用した。等距離射影方式とは、光の入射角と射影先の中心からの距離が比例するように投影する方法である。
つまり、魚眼レンズの投影先の中心からの距離を I 、比例定数を r とすると、

I = rθ

の関係が成り立っている。ここで被写体の中心を原点として、そこから L だけ離れた点の座標を (x, y) とすると、投影先の座標 (X, Y) は、

X = x * I / L = r*x*θ / L
= r*x*arctan(L / D) / L
= r*x*arctan(sqrt(x^2 + y^2) / D) / sqrt(x^2 + y^2)

Y = y * I / L
= r*y*arctan(sqrt(x^2 + y^2) / D) / sqrt(x^2 + y^2)

と表すことができる。

ソースコード

テンプレートは前回と一緒。

gyogan.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="gyogan.js"></script>
<title>Gyogan</title>
</head>
<body>
<video id="video"></video>
<canvas id="display_canvas"></canvas>
</body>
</html>

gyogan.js

navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ;
window.URL = window.URL || window.webkitURL ;

function initialize() {
	navigator.getUserMedia(
		{audio: true, video: true},
		function(stream) {
			var video = document.getElementById('video');
			video.src = URL.createObjectURL(stream);
			video.play();
			renderStart();
		},
		function(error) {
			console.error(error);
		}
	);
}

function renderStart() {
	var video = document.getElementById('video');
	var buffer = document.createElement('canvas');
	var display = document.getElementById('display_canvas');
	var bufferContext = buffer.getContext('2d');
	var displayContext = display.getContext('2d');
	var render = function() {
		requestAnimationFrame(render);
		var width = video.videoWidth;
		var height = video.videoHeight;
		if (width == 0 || height == 0) {return;}
		buffer.width = display.width = width;
		buffer.height = display.height = height;
		bufferContext.drawImage(video, 0, 0);
		
		var src = bufferContext.getImageData(0, 0, width, height);
		var dest = bufferContext.createImageData(buffer.width, buffer.height);
		
		var r = 80;
		var d = 80;
		
		for (var src_row = 0; src_row < height; src_row++) {
			for (var src_col = 0; src_col < width; src_col++) {
				// 原点を画像の中心にもっていく
				var src_x = src_col - width/2;
				var src_y = height/2 - src_row;
				
				// 等距離射影
				var l = Math.sqrt(Math.pow(src_x, 2) + Math.pow(src_y, 2));
				var dest_x = Math.floor(r * src_x * Math.atan(l/d) / l);
				var dest_y = Math.floor(r * src_y * Math.atan(l/d) / l);
				
				// 原点を左上に戻す
				var dest_col = width/2 + dest_x;
				var dest_row = height/2 - dest_y;
				
				var src_address = (src_row*width + src_col)*4;
				var dest_address = (dest_row*width + dest_col)*4;
				
				dest.data[dest_address + 0] = src.data[src_address + 0];
				dest.data[dest_address + 1] = src.data[src_address + 1];
				dest.data[dest_address + 2] = src.data[src_address + 2];
				dest.data[dest_address + 3] = 255;
			}
		}
		
		displayContext.putImageData(dest, 0, 0);
	};
	render();
}

window.addEventListener('load', initialize);

動作確認

上記のソースコードを動かしてみた。

加工前のカメラの映像
f:id:artak:20140812171153p:plain

加工後のカメラの映像
f:id:artak:20140812171234p:plain

魚眼っぽくなっているのが分かる。r と D を変更することで、射影のされ方が変わる。D を小さくすれば、加工後の映像がより丸みを帯びるようになるが、r よりも小さくなると以下のように画像に白い線が入ってしまう。

f:id:artak:20140812172337p:plain

WebRTCのgetUserMediaで取得した映像をいじってみた

WebRTCのgetUserMediaで取得した自分のカメラ映像の画素を取得し、いろいろいじってみた。

テンプレート

まず、いろいろいじるためのテンプレートを載せる。

template.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="template.js"></script>
<title>Title</title>
</head>
<body>
<video id="video"></video>
<canvas id="display_canvas"></canvas>
</body>
</html>

template.js

navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ;
window.URL = window.URL || window.webkitURL ;

function initialize() {
	navigator.getUserMedia(
		{audio: true, video: true},
		function(stream) {
			var video = document.getElementById('video');
			video.src = URL.createObjectURL(stream);
			video.play();
			renderStart();
		},
		function(error) {
			console.error(error);
		}
	);
}

function renderStart() {
	var video = document.getElementById('video');
	var buffer = document.createElement('canvas');
	var display = document.getElementById('display_canvas');
	var bufferContext = buffer.getContext('2d');
	var displayContext = display.getContext('2d');
	
	var render = function() {
		requestAnimationFrame(render);
		var width = video.videoWidth;
		var height = video.videoHeight;
		if (width == 0 || height == 0) {return;}
		buffer.width = display.width = width;
		buffer.height = display.height = height;
		bufferContext.drawImage(video, 0, 0);
		
		var src = bufferContext.getImageData(0, 0, width, height); // カメラ画像のデータ
		var dest = bufferContext.createImageData(buffer.width, buffer.height); // 空のデータ(サイズはカメラ画像と一緒)
		
		/* ##############################
		    ここで処理を行う
		############################## */

		displayContext.putImageData(dest, 0, 0);
	};
	render();
}

window.addEventListener('load', initialize);

src.data は、
src.data = { [画素(0)のR,G,B,alpha], [画素(1)のR,G,B,alpha], ... , [画素(width*height-1)のR,G,B,alpha] }
というふうにカメラ画像のRGB値とalpha値が入った長さ width*height*4 の配列になっている。(見やすくするために [ 画素(1)のR,G,B,alpha ] と表記しているが、実際は一次元配列。)
この元データを使って、dest.data に加工後の画素値を入れていく。

色調反転

まずは、単純に色調反転をしてみる。

ソースコード(処理と、その前後だけ)

var src = bufferContext.getImageData(0, 0, width, height); // カメラ画像のデータ
var dest = bufferContext.createImageData(buffer.width, buffer.height); // 空のデータ(サイズはカメラ画像と一緒)

for (var i = 0; i < dest.data.length; i += 4) {
	dest.data[i + 0] = 255 - src.data[i + 0]; // Red
	dest.data[i + 1] = 255 - src.data[i + 1]; // Green
	dest.data[i + 2] = 255 - src.data[i + 2]; // Blue
	dest.data[i + 3] = 255;                     // Alpha
}

displayContext.putImageData(dest, 0, 0);

こんな感じになる。不健康ですね。

f:id:artak:20140811192949p:plain

エッジ検出

http://www.html5.jp/canvas/ref/method/getImageData.html
にエッジ検出のサンプルがあったので、使ってみた。

ソースコード(処理と、その前後だけ)

var src = bufferContext.getImageData(0, 0, width, height); // カメラ画像のデータ
var dest = bufferContext.createImageData(buffer.width, buffer.height); // 空のデータ(サイズはカメラ画像と一緒)

for (var y = 1; y < height-1; y += 1) {
	for (var x = 1; x < width-1; x += 1) {
		for (var c = 0; c < 3; c += 1) {
			var i = (y*width + x)*4 + c;
			dest.data[i] = 127 + -src.data[i - width*4 - 4] -   src.data[i - width*4] - src.data[i - width*4 + 4] +
							-src.data[i - 4]       + 8*src.data[i]       - src.data[i + 4] +
							-src.data[i + width*4 - 4] -   src.data[i + width*4] - src.data[i + width*4 + 4];
		}
		dest.data[(y*width + x)*4 + 3] = 255; // Alpha
	}
}

displayContext.putImageData(dest, 0, 0);

こんな感じになる。動くと楽しい。

f:id:artak:20140811193953p:plain

おまけ

RGB値が 128 より小さい場合は 0 に、128 以上の場合は 255 に、という処理をしてみた。

ソースコード(処理と、その前後だけ)

var src = bufferContext.getImageData(0, 0, width, height); // カメラ画像のデータ
var dest = bufferContext.createImageData(buffer.width, buffer.height); // 空のデータ(サイズはカメラ画像と一緒)

for (var i = 0; i < dest.data.length; i += 4) {
	for (var c = 0; c < 3; c ++) {
		if (src.data[i+c] < 128) {
			dest.data[i+c] = 0;
		} else {
			dest.data[i+c] = 255;
		}
	}
	dest.data[i+3] = 255; // Alpha
}

displayContext.putImageData(dest, 0, 0);

こんな感じになる。なんかかっこいい。

f:id:artak:20140811194828p:plain

感想

カメラの画素を取得していじるのはOpenCVでやったことがあるが、その時は結構面倒なイメージだった。しかし、今回はJavaScriptのWebAPIを使うことで非常に楽に出来たので驚いた。

WebRTCで多人数のビデオチャット

前回の記事では、WebRTCで1対1のビデオチャットを作ってみた。今回は、多対多でビデオチャットを実現したいと思う。今回も、とにかく動かすことを目標にする。

参考サイト

シグナリングサーバーを応用! 「WebRTCを使って複数人で話してみよう」

このサイトで、相手のIDをキーとしたpeerConectionの連想配列を作れば良さそうだということと、自分より先に入室しているクライアントの情報を知る必要があることがわかった。他にもチャットルームの実装について書かれているので、また参考にするかも。

環境

前回と一緒。

ソースコード

サーバー側

これも前回と一緒で、WebRTCを仕組みから実装までやってみるGitHubにアップされているサーバのコードを使っている。

クライアント側
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title></title>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript">
$(document).ready(function(){
	// セレクター
	var myVideo = $('video#myVideo');
	var messageBox = $('div#messageBox');
	var index = 1;
	
	// websocket
	var websocket = new WebSocket('ws://localhost:8124/');
	
	// peer
	var server = [{"url": "stun:stun.l.google.com:19302"}];
	var peer = [];
	var streamIsAdded = false;
	
	var myStream = null;
	
	// ユーザIDをランダム生成
	var my_uuid = UUID();
	var userName = window.prompt("ユーザー名を入力してください", "");
	
	function init(){
		// 自分のカメラを利用する
		getUserMedia();
		
		// websocketの実装
		websocket.onopen = function() { onOpen(); };
		websocket.onmessage = function(event) { onMessage(event); };
		
		// ページを離れるとき
		$(window).bind("beforeunload", function() {
			// 退室情報を送信
			websocket.send(JSON.stringify({
				type: 'defect',
				uuid: my_uuid,
				userName : userName
			}));
			// websocketを切断
			websocket.close(1000,"page unload");
		});
	}
	
	function getUserMedia(){
		navigator.webkitGetUserMedia(
			{ audio: true, video: true },
			function(stream) {
				// 自分のカメラ映像を表示
				myVideo.attr({
					src : window.webkitURL.createObjectURL(stream)
				});
				myVideo.animate({ 
					width: "300px",
					height: "225px",
				}, 1000 );
				
				myStream = stream;
				streamIsAdded = true;
				
				for(x in peer){
					peer[x].addStream(myStream);
					messageBox.append('Add myStream ID: '+ x + '<br>');
				}
			},
			function(err) {
				console.log(err.name + ': ' + err.message);
			}
		);
	}
	
	// peerを追加
	function addPeer(id){
		messageBox.append('addPeer<br>');
		peer[id] = new webkitRTCPeerConnection({"iceServers": server});
		if(myStream != null){
			peer[id].addStream(myStream);
			messageBox.append('Add myStream ID: '+ id + '<br>');
		}
		peer[id].onicecandidate = function(event) { onCandidate(event, id); };
		peer[id].onaddstream = function(stream) { onAddStream(stream); };
	}
	
	// 自分のcandidateデータの処理
	function onCandidate(event, id){
		if ( Object.keys(event.candidate).length > 0 ) {
			websocket.send(JSON.stringify({
				type : 'candidate',
				candidate : event.candidate,
				from : my_uuid,
				to : id
			}));
		}
	}
	
	// 相手のカメラ映像が届いた時の処理
	function onAddStream(stream){
		$('video:eq('+ index +')').attr({
			src : window.webkitURL.createObjectURL(stream.stream)
		});
		$('video:eq('+ index +')').animate({ 
			width: "300px",
			height: "225px",
		}, 1000 );
		index++;
	}
	
	// websocket接続完了時の処理
	function onOpen(){
		// 入室情報を送信
		websocket.send(JSON.stringify({
			type: 'join',
			uuid: my_uuid,
			userName : userName
		}));
	}
	
	// websocketメッセージ受信イベントを処理
	function onMessage(event){
		// 受信したメッセージを復元
		var data = JSON.parse(event.data);
		
		// pushされたメッセージを解釈し、要素を生成する
		if (data.type === 'join') {
			if(data.uuid == my_uuid){
				messageBox.append(data.userName+ 'のID: '+ data.uuid + '<br>');
			}else{
				messageBox.append(data.userName + 'が入室しました <input type="button" id="'+ data.uuid +'" value="Send offer"><br>');
				$('#' + data.uuid).click(function(){
					if(streamIsAdded){
						var to_uuid = $(this).attr('id');
						var to_userName = data.userName;
						messageBox.append(to_userName + 'に接続申請をしました。<br>');
						createOffer(to_uuid);
					}else{
						messageBox.append('カメラを有効にしてください。<br>');
					}
				});
				addPeer(data.uuid);
				websocket.send(JSON.stringify({
					type: 'responce',
					uuid: my_uuid,
					userName : userName
				}));
			}
		} else if (data.type === 'responce') {
			if(data.uuid != my_uuid){
				addPeer(data.uuid);
			}
		} else if (data.type === 'defect') {
			messageBox.append(data.userName + 'が退室しました<br>');
			$('#' + data.uuid).remove();
		} else if (data.type === 'sdp') {
			if(data.to == my_uuid){ // 自分にSDPが送られてきた
				var sdp = new RTCSessionDescription(data.sdp);
				if(sdp.type == "offer"){ // offerをもらった
					messageBox.append(data.userName + 'から接続申請がありました。 <input type="button" id="'+ data.from +'" value="許可する"><br>');
					$('input#' + data.from).click(function(){
						if(streamIsAdded){
							var from_userName = data.userName;
							messageBox.append(from_userName + 'に接続許可をしました。<br>');
							
							var from_uuid = $(this).attr('id');
							// リモートに相手のsdpをセット
							peer[from_uuid].setRemoteDescription(sdp, function() {
								// 送り主にanswerを返す
								createAnswer(from_uuid);
							});
						}else{
							messageBox.append('カメラを有効にしてください。<br>');
						}
					});
					
				}else if(sdp.type == "answer"){ // answerが返ってきた
					messageBox.append(data.userName + 'が接続を許可しました。<br>');
					// リモートに相手のsdpをセット
					peer[data.from].setRemoteDescription(sdp, function() {
						messageBox.append('Session connection completed!!<br>');
					});
				}
			}
		} else if (data.type == 'candidate') {
			if(data.to == my_uuid){
				var candidate = new RTCIceCandidate(data.candidate);
				peer[data.from].addIceCandidate(candidate);
			}else if(data.from == my_uuid){
				var candidate = new RTCIceCandidate(data.candidate);
				peer[data.to].addIceCandidate(candidate);
			}
		}
	}
	
	function createOffer(to_uuid){
		peer[to_uuid].createOffer(function(sdp) {
			// ローカルに自分のsdpをセット
			peer[to_uuid].setLocalDescription(sdp, function() {
				// セット完了したら、相手に自分のSDPを送る
				websocket.send(JSON.stringify({
					type : 'sdp',
					sdp : sdp,
					from : my_uuid,
					to : to_uuid,
					userName : userName
				}));
			});
		});
	}
	
	function createAnswer(to_uuid){
		peer[to_uuid].createAnswer(function(sdp) {
			// この引数のSDPは自分用!
			peer[to_uuid].setLocalDescription(sdp, function() {
				// セット完了したら、相手に自分のAnswerSDPを送る
				websocket.send(JSON.stringify({
					type : 'sdp',
					sdp : sdp,
					from : my_uuid,
					to : to_uuid,
					userName : userName
				}));
			});
		});
	}
	
	function UUID() {
		var uuid = [
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1)
		].join("");
		return uuid;
	}
	
	init();
});
</script>
</head>

<body>
<table border="1" width="920" height="235">
	<tr id="display">
		<td>
			<video id="myVideo" width="60" height="45" autoplay="1" muted></video>
			<video width="60" height="45" autoplay="1"></video>
			<video width="60" height="45" autoplay="1"></video>
			<video width="60" height="45" autoplay="1"></video>
			<video width="60" height="45" autoplay="1"></video>
			<video width="60" height="45" autoplay="1"></video>
		</td>
	</tr>
</table><br>
<div id="messageBox"></div>
</body>
</html>
主な変更点
  • 他クライアント入室情報の横にオファー送信ボタンを表示。オファー受信時にアンサー送信ボタンを表示。
  • peerが連想配列になった。addPeer(id) で追加。
  • 他クライアントが入室した(joinを受信した)時に、自分がいることを知らせる(responceを送信)
  • id無しのvideoタグを追加した。

他にも余計な機能をつけてしまったので、コードがより一層ゴチャゴチャしている。

動かし方

ローカルでしか試していない。

① ターミナルでindex.jsのあるディレクトリに移動し、

node index.js

でWebSocketサーバを起動する。

Chromeでクライアント側のHTMLファイルを開く。ユーザー名が聞かれるので入力する。

③ もう一つクライアントを開くと、先に開いているクライアントに入室情報と「Send offer」ボタンが表示される。カメラを有効にしていると、オファーを送ることができる。

f:id:artak:20140717032140p:plain

④ オファーを受け取ったクライアントに、「許可する」ボタンが表示される。カメラを有効にして「許可する」ボタンを押すと接続が完了する。

f:id:artak:20140717032205p:plain

③と④を繰り返せば、最大6人までチャットができる。ソースコードのvideoタグを増やせば、人数を増やせる。

f:id:artak:20140717032218p:plain

感想

多人数のpeer接続の管理自体は思ったよりも簡単だった。しかし、videoタグに他クライアントのidをつけてonAddStream()時に区別しようとしたところ、なぜか上手く動かずにハマってしまった。代わりにvideoタグのセレクタに:eq(index)フィルタ追加し、相手との接続が完了するたびにindexを増やすという方法で解決した。また、candidateデータは相手にだけ送ればいいと思っていたが、自分のonIceCandidate()で発生したデータもaddIceCandidate()しないといけないということがわかった。

WebRTCについて学ぶ

手っ取り早くWebRTCを使ってみたいならPeerJSやSimpleWebRTCを使えばいいと思うが、今回は勉強ということで、ライブラリを使わずにライブチャットを作ってみた。

参考サイト

WebSocket関係

「Node.jsとWebSocket.IOでチャットアプリを作る」
http://mawatari.jp/archives/make-a-chat-application-in-node-js-and-websocket-io

webRTC関係

「WebRTCを仕組みから実装までやってみる」
http://blog.wnotes.net/blog/article/webrtc-beginning

「WebRTCことはじめ」
http://www.gcgate.jp/engineerblog/2014/01/07/194/

目標

とにかく動くものを作ることが目標。

環境

HTTPサーバ:Apache
websocketサーバ:Node.js
クライアント:jqueryを利用
ブラウザ:Chrome バージョン 35.0.1916.153

ソースコード

サーバー側

「WebRTCを仕組みから実装までやってみる」
http://blog.wnotes.net/blog/article/webrtc-beginning
のindex.jsをそのまま使った。受信したデータを全てのクライアントに送信する処理を行う。

クライアント側

クライアント側のコードは以下。

<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>ライブチャット</title>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript">
$(document).ready(function(){
	// セレクター
	var myVideo = $('video#myVideo');
	var remoteVideo = $('video#remoteVideo');
	var textBox = $('input#textBox');
	var sendOfferBtn = $('input#sendOffer');
	var messageBox = $('div');
	
	// websocket
	var websocket = new WebSocket('ws://localhost:8124/');
	
	// peer
	var server = [{"url": "stun:stun.l.google.com:19302"}];
	var peer = new webkitRTCPeerConnection({"iceServers": server});
	
	// ユーザIDをランダム生成
	var my_uuid = UUID();
	
	function init(){
		// 自分のカメラを利用する
		getUserMedia();
		
		// オファーボタンのクリックイベント
		sendOfferBtn.click(function(){
			var to_uuid = textBox.val();
			messageBox.append('Send offer to: ' + to_uuid + '<br>');
			createOffer(to_uuid);
		});
		
		// peerの実装
		peer.onicecandidate = function(event) { onCandidate(event); };
		peer.onaddstream = function(stream) { onAddStream(stream); };
		
		// websocketの実装
		websocket.onopen = function() { onOpen(); };
		websocket.onmessage = function(event) { onMessage(event); };
		
		// ページを離れるときにwebsocketを切断しておく。
		$(window).bind("beforeunload", function() {
			websocket.close(1000,"通常切断");
		});
	}
	
	function getUserMedia(){
		navigator.webkitGetUserMedia(
			{ audio: true, video: true },
			function(stream) {
				// 自分のカメラ映像を表示
				myVideo.attr({
					src : window.webkitURL.createObjectURL(stream)
				});
				
				// 自分のpeerにカメラストリームを接続させる
				peer.addStream(stream)
			},
			function(err) {
				console.log(err.name + ': ' + err.message);
			}
		);
	}
	
	// 自分のcandidateデータの処理
	function onCandidate(event){
		messageBox.append('onCandidate<br>');
		if ( Object.keys(event.candidate).length > 0 ) {
			websocket.send(JSON.stringify({
				type : 'candidate',
				candidate : event.candidate
			}));
		}
	}
	
	// 相手のカメラ映像が届いた時の処理
	function onAddStream(stream){
		messageBox.append('onAddstream<br>');
		remoteVideo.attr({
			src : window.webkitURL.createObjectURL(stream.stream)
		});
	}
	
	// websocket接続完了時の処理
	function onOpen(){
		textBox.focus();
		// 入室情報を送信
		websocket.send(JSON.stringify({
			type: 'join',
			uuid: my_uuid
		}));
	}
	
	// websocketメッセージ受信イベントを処理
	function onMessage(event){
		var data = JSON.parse(event.data);
		
		if (data.type === 'join') {
			messageBox.append('ID: ' + data.uuid + 'が入室しました<br>');
		} else if (data.type === 'sdp') {
			if(data.to == my_uuid){ // 自分にSDPが送られてきた
				var sdp = new RTCSessionDescription(data.sdp);
				
				if(sdp.type == "offer"){ // offerをもらった
					messageBox.append('Offerd from ID: '+ data.from + '<br>');
					// リモートに相手のsdpをセット
					peer.setRemoteDescription(sdp, function() {
						// 送り主にanswerを返す
						createAnswer(data.from);
					});
				}else if(sdp.type == "answer"){ // answerが返ってきた
					messageBox.append('Answerd from ID: '+ data.from + '<br>');
					// リモートに相手のsdpをセット
					peer.setRemoteDescription(sdp, function() {
						messageBox.append('Session connection completed!!<br>');
					});
				}
			}
		} else if (data.type == 'candidate') {
			var candidate = new RTCIceCandidate(data.candidate);
			peer.addIceCandidate(candidate);
		}
	}
	
	function createOffer(to_uuid){
		peer.createOffer(function(sdp) {
			// ローカルに自分のsdpをセット
			peer.setLocalDescription(sdp, function() {
				// セット完了したら、相手に自分のSDPを送る
				websocket.send(JSON.stringify({
					type : 'sdp',
					sdp : sdp,
					from : my_uuid,
					to : to_uuid
				}));
			});
		});
	}
	
	function createAnswer(to_uuid){
		peer.createAnswer(function(sdp) {
			// この引数のSDPは自分用!
			peer.setLocalDescription(sdp, function() {
				// セット完了したら、相手に自分のAnswerSDPを送る
				websocket.send(JSON.stringify({
					type : 'sdp',
					sdp : sdp,
					from : my_uuid,
					to : to_uuid
				}));
			});
		});
	}
	
	function UUID() {
		var uuid = [
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1),
			(((1+Math.random())*0x10000)|0).toString(16).substring(1)
		].join("");
		return uuid;
	}
	
	init();
});
</script>
</head>

<body>
<video id="myVideo" width="400" height="300" autoplay="1" muted></video>
<video id="remoteVideo" width="400" height="300" autoplay></video><br>
<input type="text" id="textBox">
<input type="button" id="sendOffer" value="Send offer">
<div></div>
</body>
</html>

動かし方

ローカルで動かす方法を書く。

① ターミナルでindex.jsのあるディレクトリに移動し、

node index.js

でWebSocketサーバを起動する。

ChromeでクライアントのHTMLファイルを2つ開く。
カメラのアクセスを許可するか聞かれるので、2つとも許可しておく。クライアントにはランダムでIDが割り振られ、webSocket接続時に入室情報として送信する。先に開いた方のクライアントには、自分の入室情報と後に開いた相手の入室情報が表示されているはずなので、相手のIDをコピーして、テキストボックスに貼付ける。

f:id:artak:20140715220548p:plain

③ 「Send offer」ボタンを押すと、接続が開始する。

f:id:artak:20140715220604p:plain

リモートサーバーで動かす場合は、WebSocketのアドレスだけ変えれば動くはず。

感想

参考サイトを見ながら、Call対象の特定、オファーの送受信、アンサーの送受信部分、candidateデータの共有、という感じで一つ一つ作っていったので、なんとなく雰囲気はつかめた気がする。基本的にpeer接続に関するメソッドを呼び出したときに出てくるSDPやCandidateといったデータをWebSocketで共有してやるという感じだった。
今後は多対多でライブチャットできるようにしたい。

木の直径を求めるアルゴリズムの証明

非負の距離(重み)を持つ無向の木について,最も遠い頂点間の距離(最遠頂点間距離)を木の直径という.この直径を求めるアルゴリズムは意外と簡単だが,参考サイトの証明ではすっきりできなかったので,自分なりに証明を考えてみる.

参考サイト:http://www.prefield.com/algorithm/graph/tree_diameter.html

アルゴリズムの説明>
適当な頂点sを選び,sからの最遠頂点uを探索する.次にuからの最遠頂点vを探索する.このとき,(u,v)は木の最遠頂点対となっており,木の直径はuとvの距離と等しい.

<自分なりの証明>
uが少なくとも一つの最遠頂点対に含まれることを証明する.基本的な方針は参考サイトと同じ.サイトで理解できなかったところを自分なりに考える.

s:任意の頂点
u:sからの最遠頂点
(x,y):とある最遠頂点対
t:sからuに向かう経路でuとxが分かれる点
d(a,b):点aと点bの距離.d(a,b) = d(b,a) >= 0,d(a,a) = 0

図で表すとこんな感じ

yの位置ごとに場合分けして,d(x,y) = d(u,y) または d(x,y) = d(x,u) を示す.

(1)yがs-t間
yがs-t経路上の頂点,またはs-t経路上の頂点から出ている場合.

d(x,y) = d(y,t) + d(t,x) >= d(y,u) = d(y,t) + d(t,u)より
d(t,x) >= d(t,u)
また,d(s,u) = d(s,t) + d(t,u) >= d(s,x) = d(s,t) + d(t,x)より
d(t,u) >= d(t,x)
よって,d(t,x) = d(t,u)となるので,
d(x,y) = d(y,t) + d(t,x) = d(y,t) + d(t,u) = d(u,y)

(2)yがt-x間
yがt-x経路上の頂点,またはt-x経路上の頂点から出ている場合.

tからxに向かう経路でxとyが分かれる点をt'(yがt-x経路上にある場合はt'=y)とすると,
d(x,y) = d(y,t') + d(t',x) >= d(u,y) = d(y,t') + d(t',u)より
d(t',x) >= d(t,t') + d(t,u) (= d(t',u)) …①
また,d(s,u) = d(s,t) + d(t,u) >= d(s,x) = d(s,t) + d(t,x)より
d(t,u) >= d(t,t') + d(t',x) (= d(t,x)) …②
①式と②式の両辺を足し合わせて整理すると,
d(t,t') <= 0
距離は非負より,d(t,t') = 0
よって①式は
d(t',x) >= d(t,u)
②式は
d(t,u) >= d(t',x)
となるので
d(t',x) = d(t,u) = d(t,t') + d(t,u) = d(t',u)
以上より
d(x,y) = d(y,t') + d(t',x) = d(y,t') + d(t',u) = d(u,y)

(3)yがt-u間
yがt-u経路上の頂点,またはt-u経路上の頂点から出ている場合.

tからuに向かう経路でuとyが分かれる点をt'とすると,
d(x,y) = d(x,t') + d(t',y) >= d(x,u) = d(x,t') + d(t',u)より
d(t',y) >= d(t',u)
また,d(s,u) = d(s,t') + d(t',u) >= d(s,y) = d(s,t') + d(t',y)より
d(t',u) >= d(t',y)
よって,d(t',y) = d(t',u)となるので,
d(x,y) = d(x,t') + d(t',y) = d(x,t') + d(t',u) = d(x,u)

以上より,uは最遠頂点対に含まれるので,uの最遠点をvとすると(u,v)は最遠頂点対となる.

<感想>
図を見せながらやれば,割と簡単に他人に説明できそうだが,言葉で説明するのは結構めんどくさい.もっとエレガントな証明があるはずだが,自分的にはすっきりしたので良しとする.

何か変なところや,わからんところがあったら,コメントお願いします.

LeapMotionのサンプルプログラムで遊んだ

<サンプルプログラムを動かしてみる>
手やモノの動きを検知するデバイスLeapMotionを研究室の先生が購入してきたので,とりあえず,javaのサンプルプログラムを動かしてみる.

参考サイト http://blogkamiuma.blogspot.jp/2013/08/leapmotion-eclipsejava.html

SDKの入手
https://developer.leapmotion.com/
から,SDKをダウンロード.適当なフォルダで解凍.

②プロジェクト作成
Eclipseに適当なプロジェクトを作成する.

③サンプルプログラムのコピー
/LeapSDK/samples/Sample.java をプロジェクトのsrcにコピー

④ビルド・パスの設定1
ビルド・パスの構成 -> 外部jar追加
から,/LeapSDK/lib/LeapJava.jar をライブラリに追加

⑤ビルド・パスの設定2
追加した LeapJava.jar について
ネイティブ・ライブラリーのロケーション -> 編集
から,LeapJava.dll のあるフォルダを指定
僕の場合は /LeapSDK/lib/x64(32ビットの場合はx86

f:id:artak:20140217214435p:plain

⑥実行!
サンプルプログラムを実行すると,認識している手や指,ジェスチャに関する情報がコンソールに出力される.なるほど,この情報を使って何かをするわけですか.


<じゃんけんの手を表示する>
指の数に応じて,じゃんけんの手を表示するプログラムを作ってみた.
~やったこと~
・適当にじゃんけんの手の画像を拾い,gu.jpg,tyoki.jpg,pa.jpgと名付ける.
・AppFrameクラスを追加.
・SampleListenerクラスのonInitメソッドとonFrameメソッドをいじる.

いじった&追加したコードは以下.

class SampleListener extends Listener {
    AppFrame f;
    
    public void onInit(Controller controller) {
        System.out.println("Initialized");
        f = new AppFrame();
        f.addWindowListener(new WindowAdapter(){
            public void windowClosing(WindowEvent e){
                System.exit(0);
            }
        });   
        f.setVisible(true);
    }
    
    
    public void onFrame(Controller controller) {
        Frame frame = controller.frame();
        System.out.println(frame.fingers().count());
        f.setFingerNum(frame.fingers().count());
        f.repaint();
    }
}


import java.awt.*;

public class AppFrame extends Frame {
	private Image gu;
	private Image tyoki;
	private Image pa;
	private int fingerNum;
	
	public void paint(Graphics g){
		switch(fingerNum){
		case 0:
		    g.drawImage(gu, 20, 40, 320, 320, this);
		    break;
		case 2:
		    g.drawImage(tyoki, 20, 40, 320, 320, this);
		    break;
		case 5:
		    g.drawImage(pa, 20, 40, 320, 320, this);
		    break;
		default:
		    g.clearRect(0, 0, 360, 380);
		    break;
		}
	}
	
	AppFrame(){
		setSize(360, 380);
		setLayout(null);

		gu = getToolkit().getImage("gu.jpg");
		tyoki = getToolkit().getImage("tyoki.jpg");
		pa = getToolkit().getImage("pa.jpg");
	}
	
	public void setFingerNum(int fingerNum){
	    this.fingerNum = fingerNum;
	}
}

結果,ちゃんと表示してくれた.ただし,指の数だけで判定しているので,指が2本と認識されればチョキが表示される.ちなみに,先輩が人形の足をLeapMotionにかざしたときもチョキの画像が表示された.