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

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

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

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で共有してやるという感じだった。
今後は多対多でライブチャットできるようにしたい。