gates1deの備忘録

主に開発でひっかかったこと, ハマったことを記録します

jQuery Mobileでビデオチャット

前回の記事に引き続き, WebRTCを利用したアプリということでビデオチャットを例に色々と学習しています.
今回は, AndroidアプリでWebRTC実現できないかなーと思って, ブラウザをそのままAndroidアプリに貼り付ける「WebView」というものを使って実装してみよう! …としてみましたが, どうやらAndroid4.4(現時点で最新)ですらWebViewのブラウザ(Androidの既定ブラウザ)がgetUserMedia(カメラやマイクを使った動画・音声の取得)に対応していないため諦めました.
でもやっぱりユーザビリティを考えると, スマートフォンからWebアプリにアクセスするのは面倒かつ見た目が非常に良くないので, jQuery Mobileでネイティブライクなものを作ってみようと思いつきました.
まぁそれでもWebアプリにアクセスすることは変わりないので, お気に入りとかに入れたアプリへのリンクを開かなきゃいけないのは面倒だと思ったので, Androidアプリから直接ブラウザでアプリを開いて, その後アプリを終了させるという無駄なことをしでかしました.
手間が減って少し楽になるってのもあるけど, Google Cloud Message(Push通知)とかも使えちゃうのでまぁ無駄でもないかなとプラスに考えてます笑
前置きが長くなりましたが, そんな感じでやっていきましょう.

前回のビデオチャットの編集

前回はcssとかも使っていないし, ビデオチャットを使う人にとっていらない部分も見えていたので, まだパソコンで見るならまだしも, スマホでアクセスされるとちょいと厳しいものがありました.
ということで前回のvideoChat.htmlを以下のように編集してみましょう(新しくファイルとして作って頂いて構いません).

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>jQuery Mobile</title>
<link rel="stylesheet"
  href="http://code.jquery.com/mobile/1.4.0/jquery.mobile-1.4.0.min.css" />
<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="http://code.jquery.com/mobile/1.4.0/jquery.mobile-1.4.0.min.js">
</script>
</head>
<body>

<!--ページ領域-->
<div data-role="page" data-title="jQuery Mobile Video Chat">

  <!--ヘッダー領域-->
  <div data-role="header">
    <h1>jQuery Mobile Video Chat</h1>
  </div>

  <div role="main" class="ui-content">
		<div data-role="controlgroup" data-type="horizontal" data-inline="true" data-mini="true">
			<button class="ui-btn" onclick="startVideo();">Video ON</button>
			<button class="ui-btn" onclick="stopVideo();">Video OFF</button>
		  <button class="ui-btn" onclick="connect();">Connect</button>
		  <button class="ui-btn" onclick="exit();">Exit</button>
		</div>  
		<br />
		<div data-role="content">
			<video id="local_video" autoplay style="width: 300px; height: 225px; border: 1px solid black;"></video>
			<video id="remote_video" autoplay style="width: 300px; height: 225px; border: 1px solid red;"></video>
		</div>
	</div>
  <div data-role="footer">
    <h3>Copyright 2014-201X, Yyy.Xxxxx</h3>
  </div>

</div>
<script src="/socket.io/socket.io.js"></script>
  
<script>
  var localVideo = document.getElementById('local_video');
  var remoteVideo = document.getElementById('remote_video');
  var localStream = null;
  var peerConnection = null;
  var peerStarted = false;
  var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':true , 'OfferToReceiveVideo':true }};
  navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
  
  var socketReady = false;
  var socket = io.connect('/');

	socket.on('connect', onOpened)
        .on('message', onMessage);

  function onOpened(event) {
    console.log('socket opened.');
    socketReady = true;
  }

  function onMessage(event) {
    if (event.type === 'offer') {
      console.log("Received offer, set offer, sending answer....")
      onOffer(event);	  
    } else if (event.type === 'answer' && peerStarted) {
      console.log('Received answer, settinng answer SDP');
	  onAnswer(event);
    } else if (event.type === 'candidate' && peerStarted) {
      console.log('Received ICE candidate...');
	  onCandidate(event);
    } else if (event.type === 'user dissconnected' && peerStarted) {
      console.log("disconnected");
      stop();
    }
  }
  
	var SendSDPValue;
	var SendICEValue;
	var ReceiveSDPValue;
	var ReceiveICEValue;
	var SendCandidateText;
	var CR = String.fromCharCode(13);
  
  function onSDP() {
    var text = SendSDPValue;
		var event = JSON.parse(text);
		if (peerConnection) {
		  onAnswer(event);
		}
		else {
		  onOffer(event);
		}

		ReceiveSDPValue ="";
  }  
  
  function onICE() {
		var text = SendCandidateText;
		var event = JSON.parse(text);
		onCandidate(event);
		ReceiveICEValue ="";
  }
  
  
  function onOffer(event) {
    console.log("Received offer...")
		console.log(event);
    setOffer(event);
		sendAnswer(event);
		peerStarted = true;
  }
  
  function onAnswer(event) {
    console.log("Received Answer...")
		console.log(event);
		setAnswer(event);
  }
  
  function onCandidate(event) {
    var candidate = new RTCIceCandidate({sdpMLineIndex:event.sdpMLineIndex, sdpMid:event.sdpMid, candidate:event.candidate});
    console.log("Received Candidate...")
		console.log(candidate);
    peerConnection.addIceCandidate(candidate);
  }

  function sendSDP(sdp) {
    var text = JSON.stringify(sdp);
		console.log("---sending sdp text ---");
		console.log(text);
		SendSDPValue = text;

		socket.json.send(sdp);
  }
  
  function sendCandidate(candidate) {
		SendCandidateText = JSON.stringify(candidate);
		console.log("---sending candidate text ---");
		console.log(SendCandidateText);
		socket.json.send(candidate);
  }
  
  function startVideo() {
		navigator.getUserMedia({video: true, audio: true},
			function (stream) {
				localStream = stream;
	      localVideo.src = window.webkitURL.createObjectURL(stream);
				localVideo.play();
		 // localVideo.volume = 0;
			},
			function (error) {
				console.error('An error occurred: [CODE ' + error.code + ']');
				return;
			}
		);
	}

  function stopVideo() {
    localVideo.src = "";
    localStream.stop();
  }

  function prepareNewConnection() {
    var pc_config = {"iceServers":[
     {"url":"stun:220.110.149.251:3478"},
     {"url":"turn:220.110.149.251:3478", "username":"push", "credential":"push0077"}
    ]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create peerConnection, exception: " + e.message);
    }

    peer.onicecandidate = function (event) {
      if (event.candidate) {
        console.log(event.candidate);
        sendCandidate({type: "candidate", 
                          sdpMLineIndex: event.candidate.sdpMLineIndex,
                          sdpMid: event.candidate.sdpMid,
                          candidate: event.candidate.candidate}
		);
      } else {
        console.log("End of candidates. ------------------- phase=" + event.eventPhase);
      }
    };

    console.log('Adding local stream...');
    peer.addStream(localStream);

    peer.addEventListener("addstream", onRemoteStreamAdded, false);
    peer.addEventListener("removestream", onRemoteStreamRemoved, false)

    function onRemoteStreamAdded(event) {
      console.log("Added remote stream");
      remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
    }

    function onRemoteStreamRemoved(event) {
      console.log("Remove remote stream");
      remoteVideo.src = "";
    }

    return peer;
  }

  function sendOffer() {
    peerConnection = prepareNewConnection();
    peerConnection.createOffer(function (sessionDescription) {
      peerConnection.setLocalDescription(sessionDescription);
      console.log("Sending: SDP");
      console.log(sessionDescription);
      sendSDP(sessionDescription);
    }, function () {
      console.log("Create Offer failed");
    }, mediaConstraints);
  }

  function setOffer(event) {
    if (peerConnection) {
	  console.error('peerConnection alreay exist!');
	}
    peerConnection = prepareNewConnection();
    peerConnection.setRemoteDescription(new RTCSessionDescription(event));
  }
  
  function sendAnswer(event) {
    console.log('sending Answer. Creating remote session description...' );
	if (! peerConnection) {
	  console.error('peerConnection NOT exist!');
	  return;
	}

    peerConnection.createAnswer(function (sessionDescription) {
      peerConnection.setLocalDescription(sessionDescription);
      console.log("Sending: SDP");
      console.log(sessionDescription);
      sendSDP(sessionDescription);
		}, function () { 
			console.log("Create Answer failed");
    }, mediaConstraints);
  }

  function setAnswer(event) {
    if (! peerConnection) {
	  console.error('peerConnection NOT exist!');
	  return;
	}
	peerConnection.setRemoteDescription(new RTCSessionDescription(event));
  }
  
  function connect() {
		if (!peerStarted && localStream && socketReady) { 
			sendOffer();
      peerStarted = true;
    } else {
      alert("接続エラー: 相手がいないか, ビデオが起動していないか, インターネット接続に問題があります.");
    }
  }

  function exit() {
    console.log("Exit.");
    stop();
  }

  function stop() {
    peerConnection.close();
    peerConnection = null;
    peerStarted = false;
  }
</script>
</body>
</html>

300行くらいのコードですが, 前回と比べて200行くらい減らしました.
( ×jQuery Mobileだからコードが減った : いらないものを消した)

特にインストールするライブラリとかなくて, jQuery Mobileのスタイルシートへのリンクを指定してあげるだけで簡単に使えるのが魅力的ですね.
それでは簡単でしたが以上です.
Say good-bye!