<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:series="http://organizeseries.com/"
	>

<channel>
	<title>WebRTCを使ってみよう！ &#8211; HTML5Experts.jp</title>
	<atom:link href="/series/webrtc-beginner/feed/" rel="self" type="application/rss+xml" />
	<link>https://html5experts.jp</link>
	<description>日本に、もっとエキスパートを。</description>
	<lastBuildDate>Sat, 07 Jul 2018 03:14:05 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.7.19</generator>
	<item>
		<title>WebRTCで録画する！MediaRecoderを使ってみよう</title>
		<link>/mganeko/12475/</link>
		<pubDate>Mon, 16 Feb 2015 01:44:06 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=12475</guid>
		<description><![CDATA[連載： WebRTCを使ってみよう！ (7)こんにちは！がねこまさしです。今回はWebRTCの録画機能を使って、ブラウザ(Firefox)で録画してみましょう。 Media Recorder API WebRTCでの録画...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc-beginner/" class="series-158" title="WebRTCを使ってみよう！" data-wpel-link="internal">WebRTCを使ってみよう！</a> (7)</div><p>こんにちは！がねこまさしです。今回はWebRTCの録画機能を使って、ブラウザ(Firefox)で録画してみましょう。</p>

<h2>Media Recorder API</h2>

<p>WebRTCでの録画機能はについては、MediaRecorder APIとしてこちらで検討が行われています。</p>

<p><a href="http://www.w3.org/TR/mediastream-recording/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">MediaStream Recording</a></p>

<p>Firefoxではすでに実装が始まっていますので、実際に使ってみることができます。</p>

<h2>早速録画してみる</h2>

<p>まずはいつものようにgetUserMedia()で localStreamを取得します。それを使って録画するのは、このようになります。簡単ですね!
</p><pre class="crayon-plain-tag">var localStream; // getUserMedia()で取得したstreamをセットしておく
var recorder =  null;
function startRecording() {
 recorder = new MediaRecorder(localStream);
 recorder.ondataavailable = function(evt) {
  // 録画が終了したタイミングで呼び出される
 }

 // 録画開始
 recorder.start();
}

// 録画停止
function stopRecording() {
 recorder.stop();
}</pre><p> 
私が試したところ、60分程度は問題なく録画できました。メモリ使用量が増加することもないので、ディスクのどこかにキャッシュされているのだと思います。（場所は発見できませんでした）</p>

<h2>再生するには</h2>

<p>録画したものを再生するには、先ほどのrecorder.ondataavaliable()で準備をしておく必要があります。Blobから、URLを生成しておきます。</p>

<p></p><pre class="crayon-plain-tag">var blobUrl = null;

 recorder.ondataavailable = function(evt) {
  var videoBlob = new Blob([evt.data], { type: evt.data.type });
  blobUrl = window.URL.createObjectURL(videoBlob);
 }</pre><p></p>

<p>あとは、videoタグで再生すればOKです。</p>

<p></p><pre class="crayon-plain-tag">var playbackVideo =  document.getElementById('playback_video');
function playRecorded() {
 playbackVideo.src = blobUrl;
 playbackVideo.onended = function() {
  playbackVideo.pause();
  playbackVideo.src = "";
 };

 playbackVideo.play();
}</pre><p></p>

<p>録画を停止しないとondataavaliable()のイベントが発生しないので、再生はできません。残念ながらいわゆる「追いかけ再生」はできないようです。</p>

<h2>保存するには</h2>

<p>せっかく録画した映像をファイルに保存するにはどうすればよいのでしょうか？　残念ながらJavaScriptでファイルに直接書き込むAPIは今はFirefoxにはなさそうです。&lt;a&gt;タグを使って、ユーザにファイルを指定して保存してもらいます。</p>

<p></p><pre class="crayon-plain-tag">var anchor = document.getElementById('downloadlink');

 recorder.ondataavailable = function(e) {
  var videoBlob = new Blob([e.data], { type: e.data.type });
  blobUrl = window.URL.createObjectURL(videoBlob);

  anchor.download = 'recorded.webm';
  anchor.href = blobUrl;
 }</pre><p></p>

<p>録画されたファイルはwebm形式になります。ビデオコーデックはVP8、オーディオコーデックはVorbis Audioでした。</p>

<h2>終わりに</h2>

<p>MediaRecorderを使えば、映像を簡単に記録することができます。サーバにポストして保存すれば、映像共有サイトも作れそうです。まだFirefoxでしか利用できませんが、今後の普及が楽しみですね。<br />
※<a href="https://lab.infocom.co.jp/demo/webrtc-recorder.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">こちらにデモページ</a>を用意しました。</p>

<h2>全体のソース</h2>

<p></p><pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
 &lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8" /&gt;
 &lt;title&gt;recording Firefox&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
 &lt;h2&gt;MediaRecorder Demo for Firefox&lt;/h2&gt;
 &lt;button id="start_button" onclick="startVideo()"&gt;StartVideo&lt;/button&gt;
 &lt;button id="stop_button" onclick="stopVideo()"&gt;StopVideo&lt;/button&gt;
  
 &lt;button id="start_record_button" onclick="startRecording()"&gt;StartRecord&lt;/button&gt;
 &lt;button id="stop_record_button" onclick="stopRecording()"&gt;StopRecord&lt;/button&gt;
  
 &lt;button id="play_button" onclick="playRecorded()"&gt;Play&lt;/button&gt;
 &lt;a href="#" id="downloadlink" class="download"&gt;Download&lt;/a&gt;

 &lt;br /&gt;
 &lt;video id="local_video" width="320px" height="240px" autoplay="1" style="border: 1px solid;"&gt;&lt;/video&gt;
 &lt;video id="playback_video" width="320px" height="240px" autoplay="1" style="border: 1px solid;"&gt;&lt;/video&gt;

&lt;/body&gt;
&lt;script&gt;
navigator.getUserMedia  = navigator.getUserMedia    || navigator.webkitGetUserMedia ||
                           navigator.mozGetUserMedia || navigator.msGetUserMedia;

var localVideo =  document.getElementById('local_video');
var playbackVideo =  document.getElementById('playback_video');
var anchor = document.getElementById('downloadlink');
var localStream = null;
var recorder =  null;
var blobUrl = null;


function startRecording() {
 if (! localStream) {
  console.warn("no stream");
  return;
 }
 if (recorder) {
  console.warn("recorder already exist");
  return;
 }

 recorder = new MediaRecorder(localStream);
 recorder.ondataavailable = function(evt) {
  console.log("data available, start playback");
  var videoBlob = new Blob([evt.data], { type: evt.data.type });
  blobUrl = window.URL.createObjectURL(videoBlob);
  playbackVideo.src = blobUrl;
  playbackVideo.onended = function() {
   playbackVideo.pause();
   playbackVideo.src = "";
  };

  anchor.download = 'recorded.webm';
  anchor.href = blobUrl;

  playbackVideo.play();
 }

 recorder.start();
 console.log("start recording");
}

function stopRecording() {
 if (recorder) {
  recorder.stop();
  console.log("stop recording");
 }
}

function playRecorded() {
 if (blobUrl) {
  playbackVideo.src = blobUrl;
  playbackVideo.onended = function() {
   playbackVideo.pause();
   playbackVideo.src = "";
  };

  playbackVideo.play();
 }
}

// Request the usermedia
function startVideo() {
 navigator.getUserMedia({video: true, audio: true}, showMedia, errCallback);
}
 
function showMedia(stream) {
 localStream  = stream;
 localVideo.src = window.URL.createObjectURL(stream);
}

var errCallback = function(e) {
 console.log('media error', e);
};
 

function stopVideo() {
 if (localStream) {
  localVideo.pause();
  localVideo.src = "";

  localStream.stop();
  localStream = null;
 }
}

&lt;/script&gt;
&lt;/html&gt;</pre><p></p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTCを使ってみよう！]]></series:name>
	</item>
		<item>
		<title>WebRTC落穂拾い：初心者がつまずきやすいポイントをフォロー</title>
		<link>/mganeko/6956/</link>
		<pubDate>Mon, 26 May 2014 00:00:49 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=6956</guid>
		<description><![CDATA[連載： WebRTCを使ってみよう！ (6)こんにちは、がねこまさしです。以前WebRTCに関する連載を書かせていただきましたが、今回はそのフォロー記事を書きたいと思います。 4月に記事をベースにしたハンズオンを行ったり...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc-beginner/" class="series-158" title="WebRTCを使ってみよう！" data-wpel-link="internal">WebRTCを使ってみよう！</a> (6)</div><p>こんにちは、がねこまさしです。以前<a href="https://html5experts.jp/series/webrtc-beginner/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">WebRTCに関する連載</a>を書かせていただきましたが、今回はそのフォロー記事を書きたいと思います。</p>

<p>4月に記事をベースにしたハンズオンを行ったり、別のイベントで参加者の方とお話しさせていただく機会がありました。すると、みなさんいろいろな部分で引っ掛かってしまうケースが多いことが分かりました。私の記事の説明不足も多いので、以下のつまずきやすいポイントについて、この機会に改めて補足させていただきます。</p>

<ul>
<li>カメラがつながらない</li>
<li>手動シグナリングがうまくいかない</li>
<li>シグナリングサーバーでつながらない</li>
<li>Firefoxでも使いたい</li>
</ul>

<h2>カメラがつながらない</h2>

<p>第1回の<a href="https://html5experts.jp/mganeko/5098/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">HTML5でWebRTCを使ってみよう！「カメラを使ってみよう」編</a>ではまずカメラにアクセスします。ところが、カメラにつながらないケースが時々発生しているようです。</p>

<h3>ブラウザが違うケース</h3>

<p>記事に書いたソースコードは、Chrome用になっていますので、Chromeでお試しください。FirefoxもWebRTCに対応していますが、JavaScriptの書き方がちょっと違います。<br />
※Chrome/Firefox両方に対応させる書き方もあります。この記事の最後でご説明します。<br /></p>

<h3>権限の制限に引っ掛かっている</h3>

<p>ChromeでWebRTCを使ってカメラにアクセスするには、HTML/JavaScriptをローカルディスクから読み込む(file://～)ではなく、Webサーバーから読み込まなくてはなりません(http://～、https://～)。お手数ですが、ApacheやnginxといったWebサーバー、あるいはpython,ruby,node.js,go等を使ってテスト用サーバーを動かしてください。HTMLファイルは、Webサーバー側に配置してくださいね。<br /></p>

<h4>Node.jsを使った簡易Webサーバー</h4>

<p>例として簡易WebサーバーをNode.jsで作った場合のサンプルをこちらに掲載します。※HTML5エキスパートの方のソースを一部拝借させていただきました。
</p><pre class="crayon-plain-tag">var port = 9000;
var html = require('fs').readFileSync(__dirname + '/hand.html'); // 任意のHTMLファイル
var server = require('http').createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html', 'Content-Length': html.length});
  res.end(html);
}).listen(port);</pre><p> 
こちらを例えば http.js として保存し、htmlファイルも（この例ではhand.html）同じディレクトリに保存してください。<br />
次に、コマンドプロンプト/ターミナルから、 
</p><pre class="crayon-plain-tag">node http.js</pre><p> 
のように簡易Webサーバーを起動してください。ブラウザからは http://localhost:9000/ にアクセスしてくださいね。</p>

<h3>うっかり「拒否」してしまった</h3>

<p>ブラウザがカメラやマイクにアクセスする許可を求めてきた際に、うっかり「拒否」してしまった場合、カメラの映像は取得できません。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/05/ask_camera.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/05/ask_camera-300x25.png" alt="ask_camera" width="300" height="25" class="alignnone size-medium wp-image-6962" srcset="/wp-content/uploads/2014/05/ask_camera-300x25.png 300w, /wp-content/uploads/2014/05/ask_camera-207x17.png 207w, /wp-content/uploads/2014/05/ask_camera.png 454w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /><br />
そんな場合は、アドレスバーの右端に「カメラに×印」が表示されます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/05/camera_refuse.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/05/camera_refuse.png" alt="camera_refuse" width="143" height="78" class="alignnone size-full wp-image-6963" /></a><br /><br />
ここで「カメラに×印」のアイコンをクリックすると、カメラのアクセス許可状態を変更するメニューが現れます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/05/camera_access.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/05/camera_access-300x152.png" alt="camera_access" width="300" height="152" class="alignnone size-medium wp-image-6966" srcset="/wp-content/uploads/2014/05/camera_access-300x152.png 300w, /wp-content/uploads/2014/05/camera_access-207x105.png 207w, /wp-content/uploads/2014/05/camera_access.png 430w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
「カメラやマイクへのアクセスを要求しているか確認する」を選択し、[完了]をクリックすると、リロードを促されます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/05/camera_reload.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/05/camera_reload-300x23.png" alt="camera_reload" width="300" height="23" class="alignnone size-medium wp-image-6967" srcset="/wp-content/uploads/2014/05/camera_reload-300x23.png 300w, /wp-content/uploads/2014/05/camera_reload-207x16.png 207w, /wp-content/uploads/2014/05/camera_reload.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
[再読み込み]ボタンをクリックして、再度挑戦してみてください。</p>

<h3>なぜかカメラを認識しないケース</h3>

<p>そもそもブラウザがカメラを認識していないことがあります。確実ではありませんが、次の手順で認識してくれることもあります。</p>

<ol>
<li>USB接続の場合、カメラを抜き差しする → 再挑戦</li>
<li>ブラウザを終了し、再度起動する → 再挑戦</li>
<li>OSを再起動 → 再挑戦</li>
</ol>

<p>USBの抜き差しが一番可能性が高く、後ろに行くにしたがって、成功する可能性は下がる印象です。不思議です。</p>

<h2>手動シグナリングが上手く行かない</h2>

<p>第2回の<a href="https://html5experts.jp/mganeko/5181/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">「WebRTCに触ってみたいエンジニア必見！手動でWebRTC通信をつなげてみよう」</a>では、SDPとICEを手動でコピー＆ペーストしてPeer-to-Peer通信を確立しました。ところが実際にやってみると、うまくいかないケースが多々ありました。実感としては約半数のケースでうまくいかず、ただ黒い画面が表示されるだけでした。</p>

<p>正確な原因はよく分かっていませんが、コピーする範囲（選択した範囲）が1文字でもずれていると、正しく接続できないことは確かです。そのため、対象の文字列を自動で選択するようにちょっとだけ改良しました。<a /="" data-wpel-link="internal">記事</a>の最後に「手動シグナリングの改良版ソース(2014年4月21日追加）」として改良したソースを載せていますので、うまくいかなかった人もこちらで再チャレンジしてみてください。
<a href="https://html5experts.jp/wp-content/uploads/2014/05/sdp_auto_select.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/05/sdp_auto_select-300x141.png" alt="sdp_auto_select" width="300" height="141" class="alignnone size-medium wp-image-6973" srcset="/wp-content/uploads/2014/05/sdp_auto_select-300x141.png 300w, /wp-content/uploads/2014/05/sdp_auto_select-207x97.png 207w, /wp-content/uploads/2014/05/sdp_auto_select.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><a href="https://html5experts.jp/wp-content/uploads/2014/05/ice_auto_select.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/05/ice_auto_select-300x225.png" alt="ice_auto_select" width="300" height="225" class="alignnone size-medium wp-image-6974" srcset="/wp-content/uploads/2014/05/ice_auto_select-300x225.png 300w, /wp-content/uploads/2014/05/ice_auto_select-207x155.png 207w, /wp-content/uploads/2014/05/ice_auto_select.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /><br />
また、この機会に同一LAN上の別のマシン間でも手動シグナリングを試しました。テキストファイル経由でSDP/ICEを交換して、無事通信することができました。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/05/handshake_2pc.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/05/handshake_2pc-300x211.png" alt="handshake_2pc" width="300" height="211" class="alignnone size-medium wp-image-6977" srcset="/wp-content/uploads/2014/05/handshake_2pc-300x211.png 300w, /wp-content/uploads/2014/05/handshake_2pc-207x145.png 207w, /wp-content/uploads/2014/05/handshake_2pc.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h2>シグナリングサーバーでつながらない</h2>

<p>第3回の<a href="https://html5experts.jp/mganeko/5349/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">「WebRTC初心者でも簡単にできる！Node.jsで仲介（シグナリング）を作ってみよう」</a>では、Node.jsを使ってシグナリングサーバーを作りました。こちらもnode.jsをよく触っている方に、勘違いさせてしまったようです。</p>

<p>Node.jsでもexpressなどのフレームワークを使って、Webアプリケーションを作ることが多いです。なので今回もシグナリングサーバーでWebサーバーも兼ねていて、HTMLもそこから取得できると思われた方がいたようです。第3回の記事では、Node.jsでWebSocketを使ったシグナリングサーバーのみ作成しています。お手数ですが、Webサーバーは別途用意してくださいね。<br />
もちろん先ほどと同じく、Node.jsで簡易Webサーバーを作ることも可能ですので、お好みでどうぞ。</p>

<h2>Firefoxでも使いたい</h2>

<p>FirefoxでもWebRTCが使えますが、私が書いた記事のソースはChromeにしか対応していません。Chrome/Firefox共通で使える様にクロスブラウザ対応にしてみましょう。</p>

<p>クロスブラウザ対応する書き方はいろいろありますが、今回はGoogleで用意している<a href="https://webrtc.googlecode.com/svn/trunk/samples/js/base/adapter.js" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">adapter.js</a>を使ってみます。題材には、第3回の<a href="https://html5experts.jp/mganeko/5349/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">シグナリングサーバーを使った1対1の通信</a>を用います。<br />
<br />
adapter.jsを使うには、まずHTMLの最初でJavaScriptを読み込みます。</p>

<p></p><pre class="crayon-plain-tag">&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;WebRTC 1 to 1 signaling&lt;/title&gt;  
  &lt;script src="https://webrtc.googlecode.com/svn/trunk/samples/js/base/adapter.js"&gt;&lt;/script&gt;
&lt;/head&gt;</pre><p></p>

<p>次に、ブラウザ固有のクラス、メソッドを置き換えます。</p>

<ul>
    <li>navigator.webkitGetUserMedia() &#8212;&gt; getUserMedia()</li>
    <li>video.src = window.webkitURL.createObjectURL(stream)  &#8212;&gt; attachMediaStream(video, stream)</li>
    <li>webkitRTCPeerConnection &#8211;&gt; RTCPeerConnection</li>
</ul>

<p>具体的には、次の箇所になります。</p>

<p></p><pre class="crayon-plain-tag">function startVideo() {
    //navigator.webkitGetUserMedia({video: true, audio: false},
    getUserMedia({video: true, audio: false},

    function (stream) { // success
      localStream = stream;
      //localVideo.src = window.webkitURL.createObjectURL(stream);
      attachMediaStream(localVideo, stream);
      localVideo.play();
      localVideo.volume = 0;
    },

    function (error) { // error
      console.error('An error occurred: [CODE ' + error.code + ']');
      return;
    }
    );
  }</pre><p></p>

<p></p><pre class="crayon-plain-tag">function prepareNewConnection() {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      //peer = new webkitRTCPeerConnection(pc_config);
      peer = new RTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create peerConnection, exception: " + e.message);
    }</pre><p></p>

<p></p><pre class="crayon-plain-tag">function onRemoteStreamAdded(event) {
      console.log("Added remote stream");
      //remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
      attachMediaStream(remoteVideo, event.stream)
    }</pre><p> 
すると、このようにChromeとFirefox間で通信ができるようになります。
<a href="https://html5experts.jp/wp-content/uploads/2014/05/adapter_crossbrowser.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/05/adapter_crossbrowser-300x158.png" alt="adapter_crossbrowser" width="300" height="158" class="alignnone size-medium wp-image-6984" srcset="/wp-content/uploads/2014/05/adapter_crossbrowser-300x158.png 300w, /wp-content/uploads/2014/05/adapter_crossbrowser-207x109.png 207w, /wp-content/uploads/2014/05/adapter_crossbrowser.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h2>おわりに</h2>

<p>ありがたいことに実際にWebRTCの記事を試してくださった方々の言葉を聞くチャンスがあり、そこで出てきた疑問に答えるために、今回の記事を書かせていただきました。貴重なフィードバックをいただいた方々にお礼申し上げます。ありがとうございました。</p>

<p>一連の記事についてでも、そうでなくても、WebRTCに関する質問やご意見があれば、どんどんお寄せください。私の分かる範囲でまた補足記事を書かせていただきます。よろしくお願いします。</p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTCを使ってみよう！]]></series:name>
	</item>
		<item>
		<title>壁を越えろ！WebRTCでNAT/Firewallを越えて通信しよう</title>
		<link>/mganeko/5554/</link>
		<pubDate>Tue, 11 Mar 2014 01:00:31 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[STUN]]></category>
		<category><![CDATA[TURN]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=5554</guid>
		<description><![CDATA[連載： WebRTCを使ってみよう！ (5)こんにちは！がねこまさしです。前回は複数人の同時通話まで実現しました。社内で使うには十分なレベルです。 しかし本格的な企業ユースとなると、まだまだ障害があります。会社と家、自社...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc-beginner/" class="series-158" title="WebRTCを使ってみよう！" data-wpel-link="internal">WebRTCを使ってみよう！</a> (5)</div><p>こんにちは！がねこまさしです。<a href="https://html5experts.jp/mganeko/5438/" title="シグナリングサーバーを応用！ 「WebRTCを使って複数人で話してみよう」" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">前回は複数人の同時通話まで実現</a>しました。社内で使うには十分なレベルです。<br />
しかし本格的な企業ユースとなると、まだまだ障害があります。会社と家、自社と別の会社さんなど、実際に通信しようとするとNATやFirewallといった壁が立ちはだかります。</p>

<h2>NATを越えよう</h2>

<h3>NATの役割は</h3>

<p><a href="http://ja.wikipedia.org/wiki/ネットワーク%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E5%A4%89%E6%8F%9B" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">NAT(+IPマスカレード)</a>は企業だけでなく、一般家庭でも使われています。ブロードバンドルーターやWiFiルーターでは、1つのグローバルIPアドレスを、複数のPCやデバイスで共有することができます。このとき、NATには2つの役割があります。</p>

<ul>
    <li>インターネットにつながったグローバルなIPアドレスと、家庭内/社内のローカルなネットワークでのIPアドレスの変換</li>
    <li>複数のPC/デバイスが同時に通信できるように、ポートマッピングによるポート変換</li>
</ul>

<p>WebRTCでNAT越しに通信すること考えてみましょう。</p>

<h4>ブラウザが知っている情報</h4>

<ul>
    <li>ローカルネットワークのIPアドレス:A</li>
    <li>自分が使う（動的に割り振った)UDPポート:A/UDP</li>
</ul>

<h4>ブラウザでは分からない情報</h4>

<ul>
    <li>グローバルIPアドレス:A&#8217;</li>
    <li>NATによってマッピングされた、外部に向けたUDPポート:A&#8217;/UDP</li>
</ul>

<p>Peer-to-Peer通信を行うには、シグナリング処理でお互いに（ローカルネットワーク内の情報ではなく）インターネット側から見た情報を通知する必要があります。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/03/webrtc_nat_0.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/03/webrtc_nat_0-300x204.png" alt="webrtc_nat_0" width="300" height="204" class="alignnone size-medium wp-image-5559" srcset="/wp-content/uploads/2014/03/webrtc_nat_0-300x204.png 300w, /wp-content/uploads/2014/03/webrtc_nat_0-207x141.png 207w, /wp-content/uploads/2014/03/webrtc_nat_0.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
ブラウザーが、インターネット側から見た情報を知るための仕組みが、STUNになります。</p>

<h3>STUNの仕組みは</h3>

<p>STUNの仕組みは意外とシンプルです。インターネット側にいる誰か（STUNサーバー）に、自分（ブラウザ）がどう見えるか教えてもらうだけです。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/03/webrtc_nat_1.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/03/webrtc_nat_1-300x213.png" alt="webrtc_nat_1" width="300" height="213" class="alignnone size-medium wp-image-5562" srcset="/wp-content/uploads/2014/03/webrtc_nat_1-300x213.png 300w, /wp-content/uploads/2014/03/webrtc_nat_1-207x147.png 207w, /wp-content/uploads/2014/03/webrtc_nat_1.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
STUNは元々WebRTCのために作られた仕組みではなく、より汎用的なUDP通信の補助として生まれました。VoIPやネットワークゲームの世界でも使われているようです。
<br /><br />
自分を外側から見た情報が分かったら、それをシグナリングサーバー経由で通信相手に渡します。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/03/webrtc_nat_2.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/03/webrtc_nat_2-300x213.png" alt="webrtc_nat_2" width="300" height="213" class="alignnone size-medium wp-image-5565" srcset="/wp-content/uploads/2014/03/webrtc_nat_2-300x213.png 300w, /wp-content/uploads/2014/03/webrtc_nat_2-207x147.png 207w, /wp-content/uploads/2014/03/webrtc_nat_2.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
お互いの情報が伝わったら、そこを目掛けて通信を行います。間にNATが挟まりますが、ポートを直接マッピングしているのであくまでもPeer-to-Peerです。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/03/webrtc_nat_3.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/03/webrtc_nat_3-300x215.png" alt="webrtc_nat_3" width="300" height="215" class="alignnone size-medium wp-image-5566" srcset="/wp-content/uploads/2014/03/webrtc_nat_3-300x215.png 300w, /wp-content/uploads/2014/03/webrtc_nat_3-207x148.png 207w, /wp-content/uploads/2014/03/webrtc_nat_3.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<h3>STUNサーバーを動かそう</h3>

<p>それでは実際にSTUNサーバーを動かしてみましょう。Linuxで動作するオープンソースのものがあるので、そちらを使います。<br />
<a href="https://code.google.com/p/rfc5766-turn-server/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer"><strong>rfc5766-turn-server</strong></a><br />
このサーバはSTUNだけでなく、後程説明するTURNにも対応しています。<br />
DownloadページからOSに合わせたgzファイルをダウンロードし、INSTALL手順に従ってビルド、インストールしてください。参考までに以前私が実施した手順を載せておきます。※ちょっと古いバージョンです。最新のものを取得してインストールしてください。
</p><pre class="crayon-plain-tag">$ wget https://rfc5766-turn-server.googlecode.com/files/turnserver-3.2.1.4-CentOS6-x86_64.tar.gz
$ tar zxvf turnserver-3.2.1.4-CentOS6-x86_64.tar.gz
$ cd turnserver-3.2.1.4
$ ./install.sh</pre><p> 
CentOSの場合、以下の場所に導入されました。
</p><pre class="crayon-plain-tag">バイナリ &rarr;  /usr/bin/turnserver 
設定ファイル &rarr;  /etc/turnserver/*.conf</pre><p></p>

<p>次に設定ファイル( /etc/turnserver/turnserver.conf )を見てみましょう。ポイントとなるのは次の箇所です。
</p><pre class="crayon-plain-tag"># STUN/TURNサーバーの接続待ポート番号。デフォルトは3478です。
# コメントアウトを外して数値を指定すれば、任意のポートに変更できます。 
#
# TURN listener port for UDP and TCP (Default: 3478).
# Note: actually, TLS &amp;amp; DTLS sessions can connect to the
# &quot;plain&quot; TCP &amp;amp; UDP port(s), too - if allowed by configuration.
#
#listening-port=3478</pre><p>
TCPとUDPの両方で待ち受けできますが、STUNはUDPのみが有効なので注意が必要です。※つまりUDPが通らない環境ではSTUNは使えません。</p>

<p></p><pre class="crayon-plain-tag"># このサーバーはデフォルトでSTUN/TURNの両方をサポートしています。
# STUNのみ使いたい場合は、stun-only のコメントアウトを外します。
#
# Option to suppress TURN functionality, only STUN requests will be processed.
# Run as STUN server only, all TURN requests will be ignored.
# By default, this option is NOT set.
#
#stun-only</pre><p>
STUNはPeer-to-Peer通信が始まればサーバーのCPU負荷、ネットワーク負荷はかかりません。それに対して後述するTURNでは特にネットワーク負荷がかかります。サーバーを借りていてネットワーク通信量で課金されるようなケースでは、stun-onlyを設定してTURNは無効にしておいた方が良いかもしれません。<br /><br />
設定がすんだらSTUNサーバーを起動してみましょう。画面にエラーが出なければ無事に起動成功です。エラーが出る場合は turnserver.conf を確認してみてください。
</p><pre class="crayon-plain-tag">/usr/bin/turnserver -o -v -c /etc/turnserver/turnserver.conf</pre><p></p>

<h3>クライアントのソースを修正しよう</h3>

<p>ここまでで準備したSTUNサーバーを使うように、クライアント側のソースを修正しましょう。<a href="https://html5experts.jp/mganeko/5438/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">前回</a>のソースの一部を変更します。<br />
STUNサーバーが stun.yourdomain.com で、デフォルトのポート3478で動いていると仮定します。その情報をPeerConnectionに教えてあげます。
</p><pre class="crayon-plain-tag">function prepareNewConnection(id) {
  var pc_config = {"iceServers":[ {"url":"stun:stun.yourdomain.com:3478"} ]};
  var peer = null;
  try {
    peer = new webkitRTCPeerConnection(pc_config);
  } catch (e) {
    console.log("Failed to create PeerConnection, exception: " + e.message);
  }

  //...省略...
}</pre><p> 
さあ、これで接続を試してください。上手く行けば、自宅と友人の家で通信が可能になっているはずです。</p>

<h3>STUNでNATを越えられないとき</h3>

<p>NATにはグローバルIPアドレスを共有するだけでなく、セキュリティ対策としての役割もあります。内部の端末を隠したり、通信できるポートを制限したり、一種の簡易Firewallとして利用されているケースもあります。その場合はFirewallの場合と同じく、次に説明するTURNを利用する必要があります。<br />
また、NATの構造によっては、接続先によって（今回の場合、STUNサーバーとPeer-to-Peerの通信相手）別のポートが割り当てられる Symmetric NAT という物があります。この場合もSTUNの仕組みでは通信することができません。やはりTURNの出番ということになります。</p>

<h2>Firewallを越えよう</h2>

<p>一般家庭のようにブロードバンドルーターなどでNATがある環境では、STUNを使えば通信が可能になります。次は一般的な企業で使えるようにしましょう。<br />
企業ではFirewallが設置されているケースがほとんどです。その場合、外部と通信できるポートも制限されます。STUNではUDPポートは動的に割り振られるままなので、Firewallにとても大きな穴を空けないと通信ができません。きっとセキュリティ管理者に怒られてしまいます。こんなケースに対応するのが、TURNの仕組みです。TURNもWebRTCのために生まれたのではなく、VoIPやネットワークゲームの世界で使われていたものです。</p>

<h3>TURNの仕組みは</h3>

<p>TURNを使った通信では、TURNサーバが実際のストリームデータを受け渡す間に入ります。すべてのパケットをTURNサーバーがリレーすることになり、もはやPeer-to-Peer通信ではなくなります。この際TURNサーバーでは動画のエンコーディングは行わないので、CPU負荷よりもネットワーク負荷が高くなりやすいです。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/03/webrtc_fw_1.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/03/webrtc_fw_1-300x222.png" alt="webrtc_fw_1" width="300" height="222" class="alignnone size-medium wp-image-5593" srcset="/wp-content/uploads/2014/03/webrtc_fw_1-300x222.png 300w, /wp-content/uploads/2014/03/webrtc_fw_1-207x153.png 207w, /wp-content/uploads/2014/03/webrtc_fw_1.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h3>Firewallに穴を空けよう</h3>

<p>TURNを使うには、Firewallに1つ穴を空ける必要があります。標準では3478/UDPを使うので、そのポートが通過可能になるように設定して（してもらって）ください。<br />
もう1つ、シグナリングサーバーと通信するための穴も空ける必要があります。例えば9000番を使うのであれば、9000/TCPも同様に通過可能になるように設定して（してもらって）ください。<br />
会社間で通信するのは、両方の会社でFirewallに穴を空ける必要があります。<br />
※これを読んで「結局Firewallをいじるのかよー」とがっかりした人もいますよね？　Firewallをいじらない方法もあるので、最後までお楽しみに。</p>

<h3>TURNサーバーを動かそう</h3>

<p>TURNサーバーは先ほどSTUNサーバーとしてインストールした<a href="https://code.google.com/p/rfc5766-turn-server/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer"><strong>rfc5766-turn-server</strong></a><br />がそのまま使えます。/etc/turnserver/turnserver.conf の設定を変更してTURNとして使える様にします。
</p><pre class="crayon-plain-tag"># STUN/TURNサーバーの接続待ポート番号。デフォルトは3478です。
# コメントアウトを外して数値を指定すれば、任意のポートに変更できます。 
#
# TURN listener port for UDP and TCP (Default: 3478).
# Note: actually, TLS &amp;amp; DTLS sessions can connect to the
# &quot;plain&quot; TCP &amp;amp; UDP port(s), too - if allowed by configuration.
#
#listening-port=3478</pre><p>
デフォルトのポートは3478ですが、自由に設定できます。<br />
</p><pre class="crayon-plain-tag"># UDP/IPの他に、rfc5766-turn-serverではTLS/DTLSでの通信も可能です（デフォルトで有効になっています）
# 残念ながらブラウザー側の実装がまだ不十分なので、この機能は無効にしておく（コメントアウトを外す）ことをお勧めします

# Uncomment if no TLS client listener is desired.
# By default TLS client listener is always started.
#
no-tls

# Uncomment if no DTLS client listener is desired.
# By default DTLS client listener is always started.
#
no-dtls</pre><p>
TLS/DTLSは今回は使わない設定にしておきます。また先ほどのSTUNを動かす際に stun-only を設定した場合は、再びコメントアウトしてTURNも使える様にして下さい。
</p><pre class="crayon-plain-tag"># Option to suppress TURN functionality, only STUN requests will be processed.
# Run as STUN server only, all TURN requests will be ignored.
# By default, this option is NOT set.
#
#stun-only</pre><p></p>

<p><br />
そして、WebRTCでTURNを使う際のキモはこちらの設定です。
</p><pre class="crayon-plain-tag"># 認証の方法を選択します。次の3種類があります。
#   no-auth
#   st-cred-mech (short-term credential mechanism) 
#   lt-cred-mech (long-term credential mechanism) 
# WebRTCでは lt-cred-mechを使用する必要がありますので、その行のコメントアウトを外します。

# Uncomment to use long-term credential mechanism.
# By default no credentials mechanism is used (any user allowed).
# This option can be used with either flat file user database or
# PostgreSQL DB or MySQL DB or Redis DB for user keys storage.
#
lt-cred-mech

# 同時に、realmも設定が必要になります。お忘れなく。
#
# Realm for long-term credentials mechanism and for TURN REST API.
#
realm=turn.yourdomain.com</pre><p>
なかなか lt-cred-mech が必須だとは分からず、とても長い間悩んでしまいました。lt-cred-mech で使用するアカウント情報はPostgreSQL, MySQL, Redisなどで管理できますが、今回はシンプルにファイル管理にします。
</p><pre class="crayon-plain-tag"># ユーザーアカウントを定義するファイル名を指定します。 
#
# 'Dynamic' user accounts database file name.
# Only users for long-term mechanism can be stored in a flat file,
# short-term mechanism will not work with option, the short-term
# mechanism required PostgreSQL or MySQL or Redis database.
# 'Dynamic' long-term user accounts are dynamically checked by the turnserver process,
# so that they can be changed while the turnserver is running.
#
# Default file name is turnuserdb.conf.
#
userdb=/etc/turnserver/turnuserdb.conf</pre><p></p>

<p>/etc/turnserver/turnuserdb.conf を使うことにしたので、その内容も変更します。
</p><pre class="crayon-plain-tag">#This file can be used as user accounts storage for long-term credentials mechanism.
#
#username1:key1
#username2:key2
# OR:
#username1:password1
#username2:password2
yourid:yourpassword</pre><p>
例としてユーザID: yourid 、パスワード: yourpassword　としました。　※実際にはもっと強度の高いパスワードにしてくださいね。<br />
<br />
これで turnserverを再起動すれば、TURNでの通信が有効になります。
</p><pre class="crayon-plain-tag">/usr/bin/turnserver -o -v -c /etc/turnserver/turnserver.conf</pre><p>
※ちなみに turnserverを安全に停止させる手段が分かりません。仕方がないので kill で殺しています&#8230;。</p>

<h3>クライアントのソースを修正しよう</h3>

<p>ここまでで準備したTURNサーバーを使うように、クライアント側のソースを修正しましょう。STUNで修正した部分と同じ個所になります。
STUN/TURNサーバーが turn.yourdomain.com で、デフォルトのポート3478で動いていると仮定します。その情報をPeerConnectionに教えてあげます。（STUNとTURNの両方を候補にすることができます）
</p><pre class="crayon-plain-tag">function prepareNewConnection(id) {
  var pc_config = {"iceServers":[
   {"url":"stun:turn.yourdomain.com:3478"},
   {"url":"turn:turn.yourdomain.com:3478", "username":"yourid", "credential":"yourpassword"}
  ]};
  var peer = null;
  try {
    peer = new webkitRTCPeerConnection(pc_config);
  } catch (e) {
    console.log("Failed to create PeerConnection, exception: " + e.message);
  }

  //...省略...
}</pre><p>
さあ、これで接続を試してください。上手く行けば、会社と自宅、あるいは会社と友人の会社で通信が可能になります。</p>

<h2>Firewallはそのままで</h2>

<p>実際の企業ではセキュリティ上の制約や手続き上の問題で、Firewallに穴を空けるのが大変なことも多々あります。お客様の会社だったらなおさらですよね。そんな時のために、TURN over TCP という規格があり、rfc5766-turn-server と Chrome の両方ともサポートしてます。これを使えば、Firewallはそのままで、通信が可能になります。</p>

<h3>TURNサーバを設定し直そう</h3>

<p></p><pre class="crayon-plain-tag"># 一つのポート番号で、UDPとTCPの両方を待ち受けすることができます。
# HTTPと同じ、80番を設定します。
#
# TURN listener port for UDP and TCP (Default: 3478).
# Note: actually, TLS &amp;amp; DTLS sessions can connect to the
# &quot;plain&quot; TCP &amp;amp; UDP port(s), too - if allowed by configuration.
#
listening-port=80</pre><p>
設定が終わったら、turnserverを再起動してください。</p>

<h3>クライアントのソースを修正しよう</h3>

<p></p><pre class="crayon-plain-tag">function prepareNewConnection(id) {
  var pc_config = {"iceServers":[
   {"url":"stun:turn.yourdomain.com:80"},
   {"url":"turn:turn.yourdomain.com:80?transport=udp", "username":"yourid", "credential":"yourpassword"},
   {"url":"turn:turn.yourdomain.com:80?transport=tcp", "username":"yourid", "credential":"yourpassword"}
  ]};
  var peer = null;
  try {
    peer = new webkitRTCPeerConnection(pc_config);
  } catch (e) {
    console.log("Failed to create PeerConnection, exception: " + e.message);
  }

  //...省略...
}</pre><p>
ここでは省略しますが、シグナリングサーバーも 80/TCP で動かす必要があります。サーバー側のNode.jsのポート番号と、クライアント側のsocket.ioのつなぎ先のポート番号を80番に変更してください。※Webサーバー、シグナリングサーバー、TURNサーバーの3つをすべて80/TCPで動かすので、サーバーを3つ別々に立てる必要があります。頑張ってください。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/03/webrtc_fw_80.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/03/webrtc_fw_80-300x226.png" alt="webrtc_fw_80" width="300" height="226" class="alignnone size-medium wp-image-5601" srcset="/wp-content/uploads/2014/03/webrtc_fw_80-300x226.png 300w, /wp-content/uploads/2014/03/webrtc_fw_80-207x156.png 207w, /wp-content/uploads/2014/03/webrtc_fw_80.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
さあ、これで最後です。ブラウザをリロードしてください。きっと壁を越えて対話ができることと思います。</p>

<h2>最後に</h2>

<p>これまで全5回、WebRTCの使いかたを説明してきました。開発者向けにコードを一から書いてきましたが、世の中には便利なライブラリやサービスも数多くあります。</p>

<ul>
    <li><a href="https://html5experts.jp/yusuke-naka/3693/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">WebRTC開発者向けライブラリ「PeerJS」はこうして作られた</a></li>
<li><a href="https://html5experts.jp/yusuke-naka/1130/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">WebRTCで注目された海外企業のサービス19本一挙公開</a></li>
</ul>

<p>こちらを利用するのもありですね。日本でWebRTCが盛り上がって、企業ユースでも認知されるのを期待しています。<br />
おつきあいいただき、どうもありがとうございました。<br />
<img src="/wp-content/uploads/2014/03/thankyou-300x216.png" alt="thankyou" width="200" class="alignnone size-medium wp-image-5611" srcset="/wp-content/uploads/2014/03/thankyou-300x216.png 300w, /wp-content/uploads/2014/03/thankyou-207x149.png 207w, /wp-content/uploads/2014/03/thankyou.png 559w" sizes="(max-width: 300px) 100vw, 300px" /></p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTCを使ってみよう！]]></series:name>
	</item>
		<item>
		<title>シグナリングサーバーを応用！ 「WebRTCを使って複数人で話してみよう」</title>
		<link>/mganeko/5438/</link>
		<pubDate>Tue, 04 Mar 2014 01:00:10 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[Node.js]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=5438</guid>
		<description><![CDATA[連載： WebRTCを使ってみよう！ (4)こんにちは！ 前回はシグナリングサーバーを動かして、WebRTCでPeer-to-Peer通信をつなぐ処理を作りました。最後に書いた通り、前回の実装ではサーバーあたり2人だけし...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc-beginner/" class="series-158" title="WebRTCを使ってみよう！" data-wpel-link="internal">WebRTCを使ってみよう！</a> (4)</div><p>こんにちは！ <a href="https://html5experts.jp/mganeko/5349/" title="WebRTC初心者でも簡単にできる！Node.jsで仲介（シグナリング）を作ってみよう" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">前回はシグナリングサーバーを動かして</a>、WebRTCでPeer-to-Peer通信をつなぐ処理を作りました。最後に書いた通り、前回の実装ではサーバーあたり2人だけしか同時に通知できません。今回はこれをもっと実用的にしていきましょう。
※今回もNode学園祭2013で発表した内容と共通の部分が多いです。<a href="http://www.slideshare.net/mganeko/2013-web-rtcnode" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">その時の資料</a>も併せてご参照ください。</p>

<p>※こちらの記事は2014年に書かれました。<a href="https://html5experts.jp/mganeko/20112/" target="_blank" data-wpel-link="internal">2016年8月のアップデート記事</a>がありますので、そちらもご参照ください。</p>

<h2>複数会議室を作ろう</h2>

<p>前回作ったのは、いわばカップル1組限定サイトのシングルテナントアプリでした（左）。これを複数組が共存できる、マルチテナント（複数会議室）のアプリに改造します（右）。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/rtc11_multiroom.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/rtc11_multiroom-300x199.png" alt="rtc11_multiroom" width="300" height="199" class="alignnone size-medium wp-image-5439" srcset="/wp-content/uploads/2014/02/rtc11_multiroom-300x199.png 300w, /wp-content/uploads/2014/02/rtc11_multiroom-207x137.png 207w, /wp-content/uploads/2014/02/rtc11_multiroom.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
複数組が共存できない理由は、シグナリングの通信が同じシグナリングサーバーに接続している全員に飛んでしまうからです。これを混線しないように分離してあげる必要があります。シグナリングサーバーで利用しているsocket.ioでは、これを簡単に実現できるroom機能があります。</p>

<ul>
    <li>roomに入室する&#8230; socket.join()</li>
    <li>roomから退室する&#8230; socket.leave()</li>
    <li>room内だけにメッセージを送る&#8230; socket.broadcast.to.emit()</li>
</ul>

<p>まずクライアント側を一部手直しします。
</p><pre class="crayon-plain-tag">function onOpened(evt) {
    console.log('socket opened.');
    socketReady = true;

    var roomname = getRoomName(); // 会議室名を取得する
    socket.emit('enter', roomname);
}</pre><p> 
ソケット接続が確立したら、シグナリングサーバーに対して入室要求(enter)を送っています。ここでgetRoomName()はアプリケーション側で実装する部分で、何らかの方法で会議室名を取得して返します。
手抜きなサンプルとしてはこんな感じでしょうか。URLの?以降をそのまま切り出して返しています。</p>

<p></p><pre class="crayon-plain-tag">function getRoomName() { // たとえば、 URLに  ?roomname  とする
  var url = document.location.href;
  var args = url.split('?');
  if (args.length &gt; 1) {
    var room = args[1];
    if (room != "") {
      return room;
    }
  }
  return "_defaultroom";
}</pre><p></p>

<p>ついでにもう少し直しましょう。実は前回までのサンプルではカメラしかアクセスしていません。マイクはアクセスしていないので、声が聞こえません。今回はマイクも取得するように一カ所だけ修正します。※webkitGetUserMedia()の引数を変更</p>

<p></p><pre class="crayon-plain-tag">// start local video
  function startVideo() {
    navigator.webkitGetUserMedia({video: true, audio: true},  // &lt;--- audio: true に変更
      function (stream) { // success
        localStream = stream;
        localVideo.src = window.webkitURL.createObjectURL(stream);
        localVideo.play();
        localVideo.volume = 0;
      },
      function (error) { // error
        console.error('An error occurred: [CODE ' + error.code + ']');
        return;
      }
    );
  }</pre><p> 
※同一PC上で複数の2つのウィンドウ/タブを開いて通信する場合、ハウリングしやすいので音量を絞るか、ヘッドフォンを利用してください。</p>

<p>今度はシグナリングサーバー側も修正しましょう。クライアントからの入室要求(enter)に対応するのと、会議室内だけに通信する部分です。</p>

<p></p><pre class="crayon-plain-tag">// 入室
socket.on('enter', function(roomname) {
    socket.set('roomname', roomname);
    socket.join(roomname);
});

socket.on('message', function(message) {
  emitMessage('message', message);
});

socket.on('disconnect', function() {
  emitMessage('user disconnected');
});

// 会議室名が指定されていたら、室内だけに通知
function emitMessage(type, message) {
  var roomname;
  socket.get('roomname', function(err, _room) {  roomname = _room;  });

  if (roomname) {  socket.broadcast.to(roomname).emit(type, message);   }
  else {   socket.broadcast.emit(type, message);   }
}</pre><p> 
シグナリングサーバーを起動しなおして、HTMLをリロードすれば、複数会議室に対応したマルチテナントアプリの完成です。
URLの後ろに ?room1 や ?room2 などのように会議室名を指定すれば、 その部屋の人と通信できます。
<br /></p>

<h2>複数人で通信してみたい</h2>

<p>次は2人だけでなく、複数人で同時に話せるようにしてみたいと思います。こんな感じです。
<a href="https://html5experts.jp/wp-content/uploads/2014/02/rtc_nn.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/rtc_nn-300x241.png" alt="rtc_nn" width="300" height="241" class="alignnone size-medium wp-image-5446" srcset="/wp-content/uploads/2014/02/rtc_nn-300x241.png 300w, /wp-content/uploads/2014/02/rtc_nn-207x166.png 207w, /wp-content/uploads/2014/02/rtc_nn.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<h3>複数のPeer-to-Peer通信を扱うには</h3>

<p>複数人と通信するには、クライアント側（ブラウザ側）に相手の数だけPeerConnectionが必要です。それを管理するための便宜上のクラスを作ります。
通信状況や、相手のID(socket.ioが割り振る）を保持します。 
</p><pre class="crayon-plain-tag">var MAX_CONNECTION_COUNT = 3;
var connections = {}; // Connection hash
function Connection() { // Connection Class
  var self = this;
  var id = "";  // socket.id of partner
  var peerconnection = null; // RTCPeerConnection instance
  var established = false; // is Already Established
  var iceReady = false;
}

function getConnection(id) {
  var con = null;
  con = connections[id];
  return con;
}

function addConnection(id, connection) {
  connections[id] = connection;
}</pre><p> 
ついでに、複数のConnectionを格納する配列と、それを管理する関数群も用意します。ここに挙げた2つ以外に、getConnectionCount(), isConnectPossible(), deleteConnection(id), などなど。（詳細は最後に全ソースを掲載します）
<br /></p>

<h3>シグナリングを手直し</h3>

<p>シグナリングの流れも手直しが必要です。
今までのシグナリングでは、最初にOffer SDPを送る際に同じ部屋の全員に送っていました(broadcast)。すると全員からAnswer SDPが返ってきてしまうので、情報が衝突してしまいます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/sdp_nn_corrupt.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/sdp_nn_corrupt-300x200.png" alt="sdp_nn_corrupt" width="300" height="200" class="alignnone size-medium wp-image-5453" srcset="/wp-content/uploads/2014/02/sdp_nn_corrupt-300x200.png 300w, /wp-content/uploads/2014/02/sdp_nn_corrupt-207x138.png 207w, /wp-content/uploads/2014/02/sdp_nn_corrupt.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
そこで、まず部屋に誰が居るかを確認し(call-response)、一人ずつ個別にOffer-Answerのやり取りをする必要があります。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/sdp_call_response.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/sdp_call_response-300x225.png" alt="sdp_call_response" width="300" height="225" class="alignnone size-medium wp-image-5456" srcset="/wp-content/uploads/2014/02/sdp_call_response-300x225.png 300w, /wp-content/uploads/2014/02/sdp_call_response-207x155.png 207w, /wp-content/uploads/2014/02/sdp_call_response.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
では、クライアント側のソースを直していきましょう。</p>

<p></p><pre class="crayon-plain-tag">function call() {
  if (! isLocalStreamStarted()) return;
  socket.json.send({type: "call"});
}

function onMessage(evt) {
  var id = evt.from;
  var target = evt.sendto;
  var conn = getConnection(id);

  if (evt.type === 'call') {
    if (! isLocalStreamStarted()) return;
    if (conn) return;  // already connected

    if (isConnectPossible()) {
      socket.json.send({type: "response", sendto: id });
    }
    else {   console.warn('max connections. so ignore call');     }
  }
  else if (evt.type === 'response') {
    sendOffer(id);
    return;
  }
}</pre><p> 
call()で全員にbroadcastし、受け取った側はonMessage()の中でcallを受け取ると、responseを相手を特定して送り返します。発信側はreponseを受け取ると、その相手に対してoffer SDPを送っています。 sendOffer()の中身もちょっと変わります。
</p><pre class="crayon-plain-tag">function sendOffer(id) {
  var conn = getConnection(id); // &lt;--- すでに作成済のコネクションを探す
  if (!conn) {
    conn = prepareNewConnection(id);
  }

  conn.peerconnection.createOffer(function (sessionDescription) { // in case of success
    conn.iceReady = true;
    conn.peerconnection.setLocalDescription(sessionDescription);
    sessionDescription.sendto = conn.id; // &lt;--- 送る相手を指定する
    sendSDP(sessionDescription);
  }, function () { // in case of error
    console.log(&quot;Create Offer failed&quot;);
  }, mediaConstraints);
  conn.iceReady = true;
}</pre><p> 
同様に、sendAnswer()もちょっと変えます。複数のコネクションに対応するのと、送る相手を指定するのが変更点です。</p>

<p></p><pre class="crayon-plain-tag">function sendAnswer(evt) {
  console.log('sending Answer. Creating remote session description...' );
  var id = evt.from;
  var conn = getConnection(id); // &lt;--- すでに作成済のコネクションを探す
  if (! conn) {
    console.error('peerConnection not exist!');
    return
  }

  conn.peerconnection.createAnswer(function (sessionDescription) { 
    // in case of success
    conn.iceReady = true;
    conn.peerconnection.setLocalDescription(sessionDescription);
    sessionDescription.sendto = id; // &lt;--- 送る相手を指定する
    sendSDP(sessionDescription);
  }, function () { // in case of error
    console.log(&quot;Create Answer failed&quot;);
  }, mediaConstraints);
  conn.iceReady = true;
}</pre><p> 
<br />
さらに、SDPを覚える処理も複数セッションに対応させます。</p>

<p></p><pre class="crayon-plain-tag">function setOffer(evt) {
  var id = evt.from;
  var conn = getConnection(id);
  if (! conn) {
    conn = prepareNewConnection(id);
    conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt));
  }
  else {
    console.error('peerConnection alreay exist!');
  }
}

function setAnswer(evt) {
  var id = evt.from;
  var conn = getConnection(id);
  if (! conn) {
    console.error('peerConnection not exist!');
    return
  }
  conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt));
}</pre><p> 
<br />
引き続きConnectionを生成する処理も修正します。今まではPeerConnectionを直接返していましたが、今回はConnectionのインスタンスを生成し、そこにPeerConnectionを保持させます。また、Candidateの送信時にも相手先を指定します。
</p><pre class="crayon-plain-tag">function prepareNewConnection(id) {
  var pc_config = {"iceServers":[]};
  var peer = null;
  try {
    peer = new webkitRTCPeerConnection(pc_config);
  } catch (e) {
    console.log("Failed to create PeerConnection, exception: " + e.message);
  }
  var conn = new Connection();  // &lt;--- Connectionを作成し、PeerConnectionを保持させる
  conn.id = id;
  conn.peerconnection = peer;
  peer.id = id;
  addConnection(id, conn);

  // send any ice candidates to the other peer
  peer.onicecandidate = function (evt) {
  if (evt.candidate) {
    console.log(evt.candidate);
    sendCandidate({type: &quot;candidate&quot;, 
                          sendto: conn.id, // &lt;-- 送信先を指定
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate});
    } else {
      console.log(&quot;End of candidates. ------------------- phase=&quot; + evt.eventPhase);
      conn.established = true;
    }
  };

  // ...
}</pre><p> 
<br />
Candidateの送信部分を変更したので、Candidateを受信した処理も変更しましょう。 onCandidate()も複数コネクションに対応させます。</p>

<p></p><pre class="crayon-plain-tag">function onCandidate(evt) {
  var id = evt.from;
  var conn = getConnection(id);
  if (! conn) {
    console.error('peerConnection not exist!');
    return;
  }
    
  // --- check if ice ready ---
  if (! conn.iceReady) {
    console.warn("PeerConn is not ICE ready, so ignore");
    return;
  }
	  
  var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate});
  console.log("Received Candidate...")
  console.log(candidate);
  conn.peerconnection.addIceCandidate(candidate);
}</pre><p>
<br />
さてさて、クライアント側の修正はいったん終わりにして、次はシグナリングサーバー側を修正します。前半では部屋の中だけに送信する機能を加えましたが、次は特定の相手にだけ送信できるようにします。 
</p><pre class="crayon-plain-tag">socket.on('message', function(message) {
    // 送信元のidをメッセージに追加（相手が分かるように）
    message.from = socket.id;

    // 送信先が指定されているか？
    var target = message.sendto;
    if (target) {
      // 送信先が指定されていた場合は、その相手のみに送信
      io.sockets.socket(target).emit('message', message);
      return;
    }

    // 特に指定がなければ、ブロードキャスト
    emitMessage('message', message);
  });</pre><p>
ここまででいったん動かしてみましょう。まだ映像が2人までしか出ませんが、通信はできるはずです。</p>

<h3>複数の映像を扱えるようにしよう</h3>

<p>ここまでで複数人相手に通信をできるようにしました。でも通信できても映像は見えていません。ちゃんと見えるようにしましょう。
まずHTMLに複数のvideoタグを配置します。
</p><pre class="crayon-plain-tag">&lt;div style="position: relative;"&gt;
   &lt;video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt;
   &lt;!-- &lt;video id="remote-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt; --&gt;
   &lt;video id="webrtc-remote-video-0" autoplay style="position: absolute; top: 250px; left: 0px; width: 320px; height: 240px; border: 1px solid black; "&gt;&lt;/video&gt;
   &lt;video id="webrtc-remote-video-1" autoplay style="position: absolute; top: 250px; left: 330px; width: 320px; height: 240px; border: 1px solid black; "&gt;&lt;/video&gt;
   &lt;video id="webrtc-remote-video-2" autoplay style="position: absolute; top: 0px; left: 330px; width: 320px; height: 240px; border: 1px solid black; " &gt;&lt;/video&gt;
  &lt;/div&gt;</pre><p></p>

<p>その複数のvideoタグを扱えるような関数群を追加します。※本当は動的にタグを作成、削除するのがかっこいいのですが…。
</p><pre class="crayon-plain-tag">var localVideo = document.getElementById('local-video');
  //var remoteVideo = document.getElementById('remote-video');
  var localStream = null;
  var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':true }};

  // ---- multi people video &amp; audio ----
  var videoElementsInUse = {};
  var videoElementsStandBy = {};
  pushVideoStandBy(getVideoForRemote(0));
  pushVideoStandBy(getVideoForRemote(1));
  pushVideoStandBy(getVideoForRemote(2));


  function getVideoForRemote(index) {
    var elementID = 'webrtc-remote-video-' + index;
    var element = document.getElementById(elementID);
    return element;
  }

  function getAudioForRemote(index) {
    var elementID = 'webrtc-remote-audio-' + index;
    var element = document.getElementById(elementID);
    return element;
  }

  // ---- video element management ---
  function pushVideoStandBy(element) {
    videoElementsStandBy[element.id] = element;
  }

  function popVideoStandBy() {
    var element = null;
    for (var id in videoElementsStandBy) {
      element = videoElementsStandBy[id];
      delete videoElementsStandBy[id];
      return element;
    }
    return null;
  }

  function pushVideoInUse(id, element) {
    videoElementsInUse[id] = element;
  }

  function popVideoInUse(id) {
    element = videoElementsInUse[id];
    delete videoElementsInUse[id];
    return element;
  }

  function attachVideo(id, stream) {
    console.log('try to attach video. id=' + id);
    var videoElement = popVideoStandBy();
    if (videoElement) {
      videoElement.src = window.URL.createObjectURL(stream);
      console.log("videoElement.src=" + videoElement.src);
      pushVideoInUse(id, videoElement);
      videoElement.style.display = 'block';
    }
    else {
      console.error('--- no video element stand by.');
    }
  }

  function detachVideo(id) {
    console.log('try to detach video. id=' + id);
    var videoElement = popVideoInUse(id);
    if (videoElement) {
      videoElement.pause();
      videoElement.src = "";
      console.log("videoElement.src=" + videoElement.src);
      pushVideoStandBy(videoElement);
    }
    else {
      console.warn('warning --- no video element using with id=' + id);
    }
  }

  // ...</pre><p> 
※ソース全体は最後に記載します。
<br /><br />
これで準備が整いました。早速接続してみましょう。
[Start video]ボタンを押して、[Connect]を押す、という操作を一人ずつ行ってください。一人、また一人と接続され、最大4人まで通信できます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/rtc4.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/rtc4-300x240.png" alt="rtc4" width="300" height="240" class="alignnone size-medium wp-image-5468" srcset="/wp-content/uploads/2014/02/rtc4-300x240.png 300w, /wp-content/uploads/2014/02/rtc4-207x165.png 207w, /wp-content/uploads/2014/02/rtc4.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<h2>補足 （2014/03/09追記）</h2>

<p>Twitter経由でご指摘をいただきました。<strong>User B/Cからresponseではなく、Offerを送れば良いのでは？</strong><br />
アドバイスに従うと、次のように改善されます。</p>

<ul>
    <li>現状：User Aからcall, User B/Cからresponse, User AからOffer, User B/CからAnswer</li>
    <li>改善：User Aからcallme, User B/CからOffer, User AからAnswer</li>
</ul>

<p>確かにその通りです。メッセージのやり取りが片道分少なくなり、すっきりしますね。
ご指摘ありがとうございました。</p>

<h2>次回は</h2>

<p>次回は最終回の予定です。NATやFirewallを越えて通信するための、STUN/TURNについて説明したいと思います。</p>

<h2>今回のソースコード</h2>

<h3>シグナリングサーバー (node.js)</h3>

<p></p><pre class="crayon-plain-tag">var port = 9001;
var io = require('socket.io').listen(port);
console.log((new Date()) + " Server is listening on port " + port);

io.sockets.on('connection', function(socket) {
  // 入室
  socket.on('enter', function(roomname) {
    socket.set('roomname', roomname);
    socket.join(roomname);
  });
 
  socket.on('message', function(message) {
    // 送信元のidをメッセージに追加（相手が分かるように）
    message.from = socket.id;

    // 送信先が指定されているか？
    var target = message.sendto;
    if (target) {
	　　// 送信先が指定されていた場合は、その相手のみに送信
      io.sockets.socket(target).emit('message', message);
      return;
    }

	// 特に指定がなければ、ブロードキャスト
    emitMessage('message', message);
  });
 
  socket.on('disconnect', function() {
    emitMessage('user disconnected');
  });
 
  // 会議室名が指定されていたら、室内だけに通知
  function emitMessage(type, message) {
    var roomname;
    socket.get('roomname', function(err, _room) {  roomname = _room;  });
 
    if (roomname) {  socket.broadcast.to(roomname).emit(type, message);   }
    else {   socket.broadcast.emit(type, message);   }
  }
});</pre><p></p>

<h3>クライアント側 (HTML, JavaScript)</h3>

<p></p><pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;WebRTC 4&lt;/title&gt;  
&lt;/head&gt;
&lt;body&gt;
  &lt;button type="button" onclick="startVideo();"&gt;Start video&lt;/button&gt;
  &lt;button type="button" onclick="stopVideo();"&gt;Stop video&lt;/button&gt;
  &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;!-- &lt;button type="button" onclick="connect();"&gt;Connect&lt;/button&gt; --&gt;
  &lt;button type="button" onclick="call();"&gt;Connect&lt;/button&gt;
  &lt;button type="button" onclick="hangUp();"&gt;Hang Up&lt;/button&gt;
  &lt;br /&gt;
  &lt;div style="position: relative;"&gt;
   &lt;video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt;
   &lt;!-- &lt;video id="remote-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt; --&gt;
   &lt;video id="webrtc-remote-video-0" autoplay style="position: absolute; top: 250px; left: 0px; width: 320px; height: 240px; border: 1px solid black; "&gt;&lt;/video&gt;
   &lt;video id="webrtc-remote-video-1" autoplay style="position: absolute; top: 250px; left: 330px; width: 320px; height: 240px; border: 1px solid black; "&gt;&lt;/video&gt;
   &lt;video id="webrtc-remote-video-2" autoplay style="position: absolute; top: 0px; left: 330px; width: 320px; height: 240px; border: 1px solid black; " &gt;&lt;/video&gt;
  &lt;/div&gt;
  
  &lt;!---
  &lt;p&gt;
   SDP to send:&lt;br /&gt;
   &lt;textarea id="text-for-send-sdp" rows="5" cols="100" disabled="1"&gt;SDP to send&lt;/textarea&gt;
  &lt;/p&gt;
  &lt;p&gt;
   SDP to receive:&lt;br /&gt;
   &lt;textarea id="text-for-receive-sdp" rows="5" cols="100"&gt;&lt;/textarea&gt;&lt;br /&gt;
   &lt;button type="button" onclick="onSDP();"&gt;Receive SDP&lt;/button&gt;
  &lt;/p&gt;
  
  &lt;p&gt;
   ICE Candidate to send:&lt;br /&gt;
   &lt;textarea id="text-for-send-ice" rows="5" cols="100" disabled="1"&gt;ICE Candidate to send&lt;/textarea&gt;
  &lt;/p&gt;
  &lt;p&gt;  
   ICE Candidates to receive:&lt;br /&gt;
   &lt;textarea id="text-for-receive-ice" rows="5" cols="100"&gt;&lt;/textarea&gt;&lt;br /&gt;
   &lt;button type="button" onclick="onICE();"&gt;Receive ICE Candidates&lt;/button&gt;
  &lt;/p&gt;
  ---&gt;
  
  &lt;!---- socket ------&gt;
  &lt;script src="http://localhost:9001/socket.io/socket.io.js"&gt;&lt;/script&gt;
  
  &lt;script&gt;
  var localVideo = document.getElementById('local-video');
  //var remoteVideo = document.getElementById('remote-video');
  var localStream = null;
  var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':true }};

  // ---- multi people video &amp; audio ----
  var videoElementsInUse = {};
  var videoElementsStandBy = {};
  pushVideoStandBy(getVideoForRemote(0));
  pushVideoStandBy(getVideoForRemote(1));
  pushVideoStandBy(getVideoForRemote(2));


  function getVideoForRemote(index) {
    var elementID = 'webrtc-remote-video-' + index;
    var element = document.getElementById(elementID);
    return element;
  }

  // ---- video element management ---
  function pushVideoStandBy(element) {
    videoElementsStandBy[element.id] = element;
  }

  function popVideoStandBy() {
    var element = null;
    for (var id in videoElementsStandBy) {
      element = videoElementsStandBy[id];
      delete videoElementsStandBy[id];
      return element;
    }
    return null;
  }

  function pushVideoInUse(id, element) {
    videoElementsInUse[id] = element;
  }

  function popVideoInUse(id) {
    element = videoElementsInUse[id];
    delete videoElementsInUse[id];
    return element;
  }

  function attachVideo(id, stream) {
    console.log('try to attach video. id=' + id);
    var videoElement = popVideoStandBy();
    if (videoElement) {
      videoElement.src = window.URL.createObjectURL(stream);
      console.log("videoElement.src=" + videoElement.src);
      pushVideoInUse(id, videoElement);
      videoElement.style.display = 'block';
    }
    else {
      console.error('--- no video element stand by.');
    }
  }

  function detachVideo(id) {
    console.log('try to detach video. id=' + id);
    var videoElement = popVideoInUse(id);
    if (videoElement) {
      videoElement.pause();
      videoElement.src = "";
      console.log("videoElement.src=" + videoElement.src);
      pushVideoStandBy(videoElement);
    }
    else {
      console.warn('warning --- no video element using with id=' + id);
    }
  }

  function detachAllVideo() {
    var element = null;
    for (var id in videoElementsInUse) {
      detachVideo(id);
    }
  }

  function getFirstVideoInUse() {
    var element = null;
    for (var id in videoElementsInUse) {
      element = videoElementsInUse[id];
      return element;
    }
    return null;
  }

  function getVideoCountInUse() {
    var count = 0;
    for (var id in videoElementsInUse) {
      count++;
    }
    return count;
  }
  
  
  function isLocalStreamStarted() {
    if (localStream) {
      return true;
    }
    else {
      return false;
    }
  }

  // -------------- multi connections --------------------
  var MAX_CONNECTION_COUNT = 3;
  var connections = {}; // Connection hash
  function Connection() { // Connection Class
    var self = this;
    var id = "";  // socket.id of partner
    var peerconnection = null; // RTCPeerConnection instance
    var established = false; // is Already Established
    var iceReady = false;
  }

  function getConnection(id) {
    var con = null;
    con = connections[id];
    return con;
  }

  function addConnection(id, connection) {
    connections[id] = connection;
  }

  function getConnectionCount() {
    var count = 0;
    for (var id in connections) {
      count++;
    }

    console.log('getConnectionCount=' + count);
    return count;
  }

  function isConnectPossible() {
    if (getConnectionCount() &lt; MAX_CONNECTION_COUNT)
      return true;
    else
      return false;
  }

  function getConnectionIndex(id_to_lookup) {
    var index = 0;
    for (var id in connections) {
      if (id == id_to_lookup) {
        return index;
      }

      index++;
    }

    // not found
    return -1;
  }

  function deleteConnection(id) {
    delete connections[id];
  }

  function stopAllConnections() {
    for (var id in connections) {
      var conn = connections[id];
      conn.peerconnection.close();
      conn.peerconnection = null;
      delete connections[id];
    }
  }

  function stopConnection(id) {
    var conn = connections[id];
    if(conn) {
      console.log('stop and delete connection with id=' + id);
      conn.peerconnection.close();
      conn.peerconnection = null;
      delete connections[id];
    }
    else {
      console.log('try to stop connection, but not found id=' + id);
    }
  }

  function isPeerStarted() {
    if (getConnectionCount() &gt; 0) {
      return true;
    }
    else {
      return false;
    }
  }

  
  // ---- socket ------
  // create socket
  var socketReady = false;
  var port = 9001;
  var socket = io.connect('http://localhost:/' + port + '/');
  
  // socket: channel connected
  socket.on('connect', onOpened)
        .on('message', onMessage);

  function onOpened(evt) {
    console.log('socket opened.');
    socketReady = true;
	
    var roomname = getRoomName(); // 会議室名を取得する
    socket.emit('enter', roomname);
	console.log('enter to ' + roomname);
  }

  // socket: accept connection request
  function onMessage(evt) {
    var id = evt.from;
    var target = evt.sendto;
    var conn = getConnection(id);

    if (evt.type === 'call') {
      if (! isLocalStreamStarted()) {
	    return;
	  }
      if (conn) {
	    return;  // already connected
	  }

      if (isConnectPossible()) {
        socket.json.send({type: "response", sendto: id });
      }
      else {
	    console.warn('max connections. so ignore call'); 
	  }
	  return;
    }
    else if (evt.type === 'response') {
      sendOffer(id);
      return;
    } else if (evt.type === 'offer') {
      console.log("Received offer, set offer, sending answer....")
      onOffer(evt);	  
    } else if (evt.type === 'answer' &amp;&amp; isPeerStarted()) {  // **
      console.log('Received answer, settinng answer SDP');
	  onAnswer(evt);
    } else if (evt.type === 'candidate' &amp;&amp; isPeerStarted()) { // **
      console.log('Received ICE candidate...');
	  onCandidate(evt);
    } else if (evt.type === 'user dissconnected' &amp;&amp; isPeerStarted()) { // **
      console.log("disconnected");
      //stop();
	  detachVideo(id); // force detach video
      stopConnection(id);
    }
  }

  function getRoomName() { // たとえば、 URLに  ?roomname  とする
    var url = document.location.href;
    var args = url.split('?');
    if (args.length &gt; 1) {
      var room = args[1];
      if (room != "") {
        return room;
      }
    }
    return "_defaultroom";
  }
  
  // ----------------- handshake --------------
  //var textForSendSDP = document.getElementById('text-for-send-sdp');
  //var textForSendICE = document.getElementById('text-for-send-ice');
  //var textToReceiveSDP = document.getElementById('text-for-receive-sdp');
  //var textToReceiveICE = document.getElementById('text-for-receive-ice');
  //var iceSeparator = '------ ICE Candidate -------';
  //var CR = String.fromCharCode(13);
  
  /*--
  function onSDP() {
    var text = textToReceiveSDP.value;
	var evt = JSON.parse(text);
	if (peerConnection) {
	  onAnswer(evt);
	}
	else {
	  onOffer(evt);
	}
	
	//textToReceiveSDP.value ="";
  }
  --*/
  
  //--- multi ICE candidate ---
  /*--
  function onICE() {
    var text = textToReceiveICE.value;
	var arr = text.split(iceSeparator);
	for (var i = 1, len = arr.length; i &lt; len; i++) {
      var evt = JSON.parse(arr[i]);
	  onCandidate(evt);
    }

	textToReceiveICE.value ="";
  }
  ---*/
  
  
  function onOffer(evt) {
    console.log("Received offer...")
	console.log(evt);
    setOffer(evt);
	sendAnswer(evt);
	//peerStarted = true; --
  }
  
  function onAnswer(evt) {
    console.log("Received Answer...")
	console.log(evt);
	setAnswer(evt);
  }
  
  function onCandidate(evt) {
	var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
	  console.error('peerConnection not exist!');
	  return;
	}
    
    // --- check if ice ready ---
    if (! conn.iceReady) {
      console.warn("PeerConn is not ICE ready, so ignore");
      return;
    }
	  
    var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate});
    console.log("Received Candidate...")
	console.log(candidate);
    conn.peerconnection.addIceCandidate(candidate);
  }

  function sendSDP(sdp) {
    var text = JSON.stringify(sdp);
	console.log("---sending sdp text ---");
	console.log(text);
	//textForSendSDP.value = text;
	
	// send via socket
	socket.json.send(sdp);
  }
  
  function sendCandidate(candidate) {
    var text = JSON.stringify(candidate);
	console.log("---sending candidate text ---");
	console.log(text);
	//textForSendICE.value = (textForSendICE.value + CR + iceSeparator + CR + text + CR);
	//textForSendICE.scrollTop = textForSendICE.scrollHeight;
	
	// send via socket
	socket.json.send(candidate);
  }
  
  // ---------------------- video handling -----------------------
  // start local video
  function startVideo() {
	navigator.webkitGetUserMedia({video: true, audio: true},
     function (stream) { // success
      localStream = stream;
      localVideo.src = window.webkitURL.createObjectURL(stream);
      localVideo.play();
	  localVideo.volume = 0;
     },
     function (error) { // error
      console.error('An error occurred:');
      console.error(error);
      return;
     }
	);
  }

  // stop local video
  function stopVideo() {
    localVideo.src = "";
    localStream.stop();
  }

  // ---------------------- connection handling -----------------------
  function prepareNewConnection(id) {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create PeerConnection, exception: " + e.message);
    }
    var conn = new Connection();
    conn.id = id;
    conn.peerconnection = peer;
    peer.id = id;
    addConnection(id, conn);

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
        sendCandidate({type: "candidate", 
                          sendto: conn.id,
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate});
      } else {
        console.log("End of candidates. ------------------- phase=" + evt.eventPhase);
        conn.established = true;
      }
    };

    console.log('Adding local stream...');
    peer.addStream(localStream);

    peer.addEventListener("addstream", onRemoteStreamAdded, false);
    peer.addEventListener("removestream", onRemoteStreamRemoved, false)

    // when remote adds a stream, hand it on to the local video element
    function onRemoteStreamAdded(event) {
      console.log("Added remote stream");
      attachVideo(this.id, event.stream);
	  //remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
    }

    // when remote removes a stream, remove it from the local video element
    function onRemoteStreamRemoved(event) {
      console.log("Remove remote stream");
      detachVideo(this.id);
	  //remoteVideo.pause();
      //remoteVideo.src = "";
    }

    return conn;
  }

  function sendOffer(id) {
	var conn = getConnection(id);
    if (!conn) {
      conn = prepareNewConnection(id);
    }

	conn.peerconnection.createOffer(function (sessionDescription) { // in case of success
      conn.iceReady = true;
      conn.peerconnection.setLocalDescription(sessionDescription);
      sessionDescription.sendto = id;
	  sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Offer failed");
    }, mediaConstraints);
    conn.iceReady = true;
  }

  function setOffer(evt) {
	var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
      conn = prepareNewConnection(id);
      conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt));
    }
	else {
	  console.error('peerConnection alreay exist!');
	}
  }
  
  function sendAnswer(evt) {
    console.log('sending Answer. Creating remote session description...' );
	var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
	  console.error('peerConnection not exist!');
	  return
    }

    conn.peerconnection.createAnswer(function (sessionDescription) { 
      // in case of success
      conn.iceReady = true;
      conn.peerconnection.setLocalDescription(sessionDescription);
      sessionDescription.sendto = id;
	  sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Answer failed");
    }, mediaConstraints);
    conn.iceReady = true;
  }

  function setAnswer(evt) {
	var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
	  console.error('peerConnection not exist!');
	  return
    }
    conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt));
  }
  
  // -------- handling user UI event -----
  /*-----
  // start the connection upon user request
  function connect() {
    if (!peerStarted &amp;&amp; localStream &amp;&amp; socketReady) { // **
	//if (!peerStarted &amp;&amp; localStream) { // --
      sendOffer();
      peerStarted = true;
    } else {
      alert("Local stream not running yet - try again.");
    }
  }
  ----------*/
  
  // call others before connecting peer
  function call() {
    if (! isLocalStreamStarted()) {
      alert("Local stream not running yet. Please [Start Video] or [Start Screen].");
      return;
    }
    if (! socketReady) {
      alert("Socket is not connected to server. Please reload and try again.");
      return;
    }

    // call others, in same room
    console.log("call others in same room, befeore offer");
    socket.json.send({type: "call"});
  }
  
  // stop the connection upon user request
  function hangUp() {
    console.log("Hang up.");
    socket.json.send({type: "bye"});
    detachAllVideo();
    stopAllConnections();
  }

  /*--
  function stop() {
    peerConnection.close();
    peerConnection = null;
    //peerStarted = false; --
  }
  --*/

  
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre><p></p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTCを使ってみよう！]]></series:name>
	</item>
		<item>
		<title>WebRTC初心者でも簡単にできる！Node.jsで仲介（シグナリング）を作ってみよう</title>
		<link>/mganeko/5349/</link>
		<pubDate>Fri, 21 Feb 2014 01:00:05 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[Node.js]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=5349</guid>
		<description><![CDATA[連載： WebRTCを使ってみよう！ (3)こんにちは！ がねこまさしです。前回はWebRTCの通信を手動でつなぎましたが、今回は仲介役のサーバーを作ってみましょう。 ※今回の内容は、Node学園祭2013で発表した内容...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc-beginner/" class="series-158" title="WebRTCを使ってみよう！" data-wpel-link="internal">WebRTCを使ってみよう！</a> (3)</div><p>こんにちは！ がねこまさしです。<a href="https://html5experts.jp/mganeko/5181/" title="WebRTCに触ってみたいエンジニア必見！手動でWebRTC通信をつなげてみよう" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">前回はWebRTCの通信を手動でつなぎました</a>が、今回は仲介役のサーバーを作ってみましょう。</p>

<p>※今回の内容は、Node学園祭2013で発表した内容(の一部)とほぼ同じです。<a href="http://www.slideshare.net/mganeko/2013-web-rtcnode" title="WebRTCをはじめよう" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">その時の資料</a>もご参照ください。</p>

<p><strong>※こちらの記事は2014年に書かれました。<a href="https://html5experts.jp/mganeko/20013/" target="_blank" data-wpel-link="internal">2016年7月のアップデート記事</a>がありますので、そちらもご参照ください。</strong></p>

<h2>シグナリングサーバーを立てよう</h2>

<p><a href="https://html5experts.jp/mganeko/5181/" title="WebRTCに触ってみたいエンジニア必見！手動でWebRTC通信をつなげてみよう" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">前回</a>は手動でコピー＆ペーストしてシグナリングを実現しました。今回はそれを楽にしましょう。</p>

<h3>シグナリングサーバーはどうして必要なの？</h3>

<p>シグナリングの過程では、お互いのIPアドレスやポート番号を渡す必要があります。この段階ではお互いIPアドレスを知らないので直接やりとりできません。そこで、仲介役となるシグナリングサーバーが必要となります。このサーバーは、どちらブラウザもIPアドレスを知っていることが前提となります。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/signaling_server.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/signaling_server-300x196.png" alt="signaling_server" width="300" height="196" class="alignnone size-medium wp-image-5355" srcset="/wp-content/uploads/2014/02/signaling_server-300x196.png 300w, /wp-content/uploads/2014/02/signaling_server-207x135.png 207w, /wp-content/uploads/2014/02/signaling_server.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
つまり、Peer-to-Peer通信の開始前には、普通のサーバー/クライアント型の通信が行わることになります。</p>

<h3>Node.jsを準備しよう</h3>

<p>今回はシグナリング処理をWebSocketを使って実現してみます。ソケットの処理が実現できればどのような言語でも構わないのですが、メッセージング処理が得意なNode.jsを使うことにします。
Node.jsのインストーラーを<a href="http://nodejs.jp/nodejs.org_ja/docs/v0.10/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">こちらのサイト</a>から入手し、手順に従ってインストールしてください。Windows,Mac OS X,Linux用のバイナリが用意されています。今回のサンプルはv0.10.15で動作確認していますが、v0.10.x系ならばそのまま動くはずです。</p>

<p>Node.jsのインストールが完了したら、こんどはWebSocket用のモジュールをインストールします。コマンドプロンプト/ターミナルから、 次のコマンドを実行してください。 ※必要に応じて、sudoなどをご利用ください。
</p><pre class="crayon-plain-tag">npm install socket.io</pre><p>
<a href="http://socket.io/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">socket.io</a>は、異なる種類のブラウザ間の通信を簡単に行えるようにしてくれるモジュールです。異なる複数の通信方式をサポートしています。</p>

<ul>
    <li>&#8216;websocket&#8217; , &#8216;flashsocket&#8217; , &#8216;htmlfile&#8217; , &#8216;xhr-polling&#8217; , &#8216;jsonp-polling&#8217;</li>
</ul>

<h3>シグナリングサーバーを動かそう</h3>

<p>次のコードを好きなファイル名で保存してください。（例えば signaling.js)</p>

<p></p><pre class="crayon-plain-tag">var port = 9001;
var io = require('socket.io').listen(port);
console.log((new Date()) + " Server is listening on port " + port);

io.sockets.on('connection', function(socket) {
  socket.on('message', function(message) {
    socket.broadcast.emit('message', message);
  });

  socket.on('disconnect', function() {
    socket.broadcast.emit('user disconnected');
  });
});</pre><p> 
起動するにはコマンドプロンプト/ターミナルから、 次のコマンドを実行してください。
</p><pre class="crayon-plain-tag">node signaling.js</pre><p>
シグナリングサーバーの動作は単純で、右からきたメッセージをそのまま左に流すだけです。</p>

<h2>シグナリング処理を変更しよう</h2>

<p>それでは前回のHTMLを、少しずつ変更して行きましょう。まず、socket.ioのクライアント用JavaScriptを読み込みます。localhostの部分は、実際のシグナリングサーバーに変更してください。
</p><pre class="crayon-plain-tag">&lt;script src="http://localhost:9001/socket.io/socket.io.js"&gt;&lt;/script&gt;</pre><p> 
<br />
次に、socket.ioの接続、通信処理をJavaScriptに追加します。
</p><pre class="crayon-plain-tag">// ---- socket ------
  // create socket
  var socketReady = false;
  var port = 9001;
  var socket = io.connect('http://localhost:/' + port + '/');
  // socket: channel connected
  socket.on('connect', onOpened)
        .on('message', onMessage);

  function onOpened(evt) {
    console.log('socket opened.');
    socketReady = true;
  }

  // socket: accept connection request
  function onMessage(evt) {
    if (evt.type === 'offer') {
      console.log("Received offer, set offer, sending answer....")
      onOffer(evt);	  
    } else if (evt.type === 'answer' &amp;&amp; peerStarted) {
      console.log('Received answer, settinng answer SDP');
      onAnswer(evt);
    } else if (evt.type === 'candidate' &amp;&amp; peerStarted) {
      console.log('Received ICE candidate...');
      onCandidate(evt);
    } else if (evt.type === 'user dissconnected' &amp;&amp; peerStarted) {
      console.log("disconnected");
      stop();
    }
  }</pre><p> 
重要なのはonMessage()の処理で、Offer SDP,Answer SDP,ICE Candidateのそれぞれに対応して、前回用意したOnOffer(), onAnswer(), onCandidate()を呼び出しています。<br /></p>

<p>今度は実際にSDP/ICE Candidateを送る部分を変更します。前回はテキストエリアに表示するだけでしたが、今回はそれをsocket.io経由で送信します。</p>

<p></p><pre class="crayon-plain-tag">function sendSDP(sdp) {
    var text = JSON.stringify(sdp);
	console.log("---sending sdp text ---");
	console.log(text);
	textForSendSDP.value = text;
	
	// send via socket
	socket.json.send(sdp); // &lt;--- ここを追加
  }
  
  function sendCandidate(candidate) {
    var text = JSON.stringify(candidate);
	console.log("---sending candidate text ---");
	console.log(text);
	textForSendICE.value = (textForSendICE.value + CR + iceSeparator + CR + text + CR);
	textForSendICE.scrollTop = textForSendICE.scrollHeight;
	
	// send via socket
	socket.json.send(candidate); // &lt;--- ここを追加
  }</pre><p></p>

<p>最後は、ちょっとしたフラグの処理の追加です。</p>

<p></p><pre class="crayon-plain-tag">function onOffer(evt) {
    console.log("Received offer...")
    console.log(evt);
    setOffer(evt);
    sendAnswer(evt);
    peerStarted = true;  // &lt;--- ここを追加
  }

  // start the connection upon user request
  function connect() {
    if (!peerStarted &amp;&amp; localStream &amp;&amp; socketReady) { // &lt;--- ここを変更
      sendOffer();
      peerStarted = true;
    } else {
      alert("Local stream not running yet - try again.");
    }
  }

  function stop() {
    peerConnection.close();
    peerConnection = null;
    peerStarted = false;  // &lt;--- ここを追加
  }</pre><p></p>

<h2>実際に動かしてみよう</h2>

<p>シグナリングサーバーが動いてることを確認したら、Chromeのウィンドウを2つ開いて修正したHTMLを読み込んでください。</p>

<p>(1) 両方のウィンドウで[Start video]ボタンをクリックします。カメラのアクセスを許可すると、それぞれリアルタイムの映像が表示されます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/signaling_1.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/signaling_1-300x162.png" alt="signaling_1" width="300" height="162" class="alignnone size-medium wp-image-5366" srcset="/wp-content/uploads/2014/02/signaling_1-300x162.png 300w, /wp-content/uploads/2014/02/signaling_1-1024x555.png 1024w, /wp-content/uploads/2014/02/signaling_1-207x112.png 207w, /wp-content/uploads/2014/02/signaling_1.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
<br />
(2) どちらかのウィンドウで[Connect]ボタンを押します。SDP, ICE Candidateが自動で交換され、ビデオ通信が始まります。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/signaling_2.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/signaling_2-300x154.png" alt="signaling_2" width="300" height="154" class="alignnone size-medium wp-image-5367" srcset="/wp-content/uploads/2014/02/signaling_2-300x154.png 300w, /wp-content/uploads/2014/02/signaling_2-1024x528.png 1024w, /wp-content/uploads/2014/02/signaling_2-207x106.png 207w, /wp-content/uploads/2014/02/signaling_2.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
前回の14ステップに比べて、ぐっと減って2ステップになりました。これなら使えそうですね。</p>

<h2>シグナリングの流れを追ってみる</h2>

<p>シグナリングの流れを追跡してみましょう。前回はコード上を追っかけたので、今回は流れを図で見てみます。<br /></p>

<h3>SDPの交換</h3>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/02/signaling_sdp.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/signaling_sdp-300x225.png" alt="signaling_sdp" width="300" height="225" class="alignnone size-medium wp-image-5369" srcset="/wp-content/uploads/2014/02/signaling_sdp-300x225.png 300w, /wp-content/uploads/2014/02/signaling_sdp-207x155.png 207w, /wp-content/uploads/2014/02/signaling_sdp.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
SDPのOffer, Answerが、シグナリングサーバー経由で交換されます。</p>

<h3>ICE Candidateの交換</h3>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/02/signaling_ice.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/signaling_ice-300x219.png" alt="signaling_ice" width="300" height="219" class="alignnone size-medium wp-image-5370" srcset="/wp-content/uploads/2014/02/signaling_ice-300x219.png 300w, /wp-content/uploads/2014/02/signaling_ice-207x151.png 207w, /wp-content/uploads/2014/02/signaling_ice.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
複数のICE Candidateが飛び交い、すべての交換が終わるとPeer-to-Peer通信が始まります。</p>

<h2>次回は</h2>

<p>今回はシグナリングサーバーを動かして、Peer-to-Peer通信確立までを自動化しました（それが普通ですけど）。実は今回の仕組みでは、一つのシグナリングサーバーで同時に2人までしか通信できません。まったく実用的ではありません。次回は複数人での通信にチャレンジする予定です。</p>

<h2>今回のソース</h2>

<p>最後に、今回使ったHTMLを掲載しておきます。</p>

<p></p><pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;WebRTC 1 to 1 signaling&lt;/title&gt;  
&lt;/head&gt;
&lt;body&gt;
  &lt;button type="button" onclick="startVideo();"&gt;Start video&lt;/button&gt;
  &lt;button type="button" onclick="stopVideo();"&gt;Stop video&lt;/button&gt;
  &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;button type="button" onclick="connect();"&gt;Connect&lt;/button&gt;
  &lt;button type="button" onclick="hangUp();"&gt;Hang Up&lt;/button&gt;
  &lt;br /&gt;
  &lt;div&gt;
   &lt;video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt;
   &lt;video id="remote-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt;
  &lt;/div&gt;
  
  &lt;p&gt;
   SDP to send:&lt;br /&gt;
   &lt;textarea id="text-for-send-sdp" rows="5" cols="100" disabled="1"&gt;SDP to send&lt;/textarea&gt;
  &lt;/p&gt;
  &lt;p&gt;
   SDP to receive:&lt;br /&gt;
   &lt;textarea id="text-for-receive-sdp" rows="5" cols="100"&gt;&lt;/textarea&gt;&lt;br /&gt;
   &lt;button type="button" onclick="onSDP();"&gt;Receive SDP&lt;/button&gt;
  &lt;/p&gt;
  
  &lt;p&gt;
   ICE Candidate to send:&lt;br /&gt;
   &lt;textarea id="text-for-send-ice" rows="5" cols="100" disabled="1"&gt;ICE Candidate to send&lt;/textarea&gt;
  &lt;/p&gt;
  &lt;p&gt;  
   ICE Candidates to receive:&lt;br /&gt;
   &lt;textarea id="text-for-receive-ice" rows="5" cols="100"&gt;&lt;/textarea&gt;&lt;br /&gt;
   &lt;button type="button" onclick="onICE();"&gt;Receive ICE Candidates&lt;/button&gt;
  &lt;/p&gt;
  
  &lt;!---- socket ------&gt;
  &lt;script src="http://localhost:9001/socket.io/socket.io.js"&gt;&lt;/script&gt;
  
  &lt;script&gt;
  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':false, 'OfferToReceiveVideo':true }};

  
  // ---- socket ------
  // create socket
  var socketReady = false;
  var port = 9001;
  var socket = io.connect('http://localhost:/' + port + '/');
  // socket: channel connected
  socket.on('connect', onOpened)
        .on('message', onMessage);

  function onOpened(evt) {
    console.log('socket opened.');
    socketReady = true;
  }

  // socket: accept connection request
  function onMessage(evt) {
    if (evt.type === 'offer') {
      console.log("Received offer, set offer, sending answer....")
      onOffer(evt);	  
    } else if (evt.type === 'answer' &amp;&amp; peerStarted) {
      console.log('Received answer, settinng answer SDP');
	  onAnswer(evt);
    } else if (evt.type === 'candidate' &amp;&amp; peerStarted) {
      console.log('Received ICE candidate...');
	  onCandidate(evt);
    } else if (evt.type === 'user dissconnected' &amp;&amp; peerStarted) {
      console.log("disconnected");
      stop();
    }
  }

  
  
  // ----------------- handshake --------------
  var textForSendSDP = document.getElementById('text-for-send-sdp');
  var textForSendICE = document.getElementById('text-for-send-ice');
  var textToReceiveSDP = document.getElementById('text-for-receive-sdp');
  var textToReceiveICE = document.getElementById('text-for-receive-ice');
  var iceSeparator = '------ ICE Candidate -------';
  var CR = String.fromCharCode(13);
  
  function onSDP() {
    var text = textToReceiveSDP.value;
	var evt = JSON.parse(text);
	if (peerConnection) {
	  onAnswer(evt);
	}
	else {
	  onOffer(evt);
	}
	
	textToReceiveSDP.value ="";
  }  
  
  //--- multi ICE candidate ---
  function onICE() {
    var text = textToReceiveICE.value;
	var arr = text.split(iceSeparator);
	for (var i = 1, len = arr.length; i &lt; len; i++) {
      var evt = JSON.parse(arr[i]);
	  onCandidate(evt);
    }

	textToReceiveICE.value ="";
  }
  
  
  function onOffer(evt) {
    console.log("Received offer...")
	console.log(evt);
    setOffer(evt);
	sendAnswer(evt);
	peerStarted = true;  // ++
  }
  
  function onAnswer(evt) {
    console.log("Received Answer...")
	console.log(evt);
	setAnswer(evt);
  }
  
  function onCandidate(evt) {
    var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.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);
	textForSendSDP.value = text;
	
	// send via socket
	socket.json.send(sdp);
  }
  
  function sendCandidate(candidate) {
    var text = JSON.stringify(candidate);
	console.log("---sending candidate text ---");
	console.log(text);
	textForSendICE.value = (textForSendICE.value + CR + iceSeparator + CR + text + CR);
	textForSendICE.scrollTop = textForSendICE.scrollHeight;
	
	// send via socket
	socket.json.send(candidate);
  }
  
  // ---------------------- video handling -----------------------
  // start local video
  function startVideo() {
	navigator.webkitGetUserMedia({video: true, audio: false},
    function (stream) { // success
      localStream = stream;
      localVideo.src = window.webkitURL.createObjectURL(stream);
      localVideo.play();
	  localVideo.volume = 0;
    },
    function (error) { // error
      console.error('An error occurred: [CODE ' + error.code + ']');
      return;
    }
	);
  }

  // stop local video
  function stopVideo() {
    localVideo.src = "";
    localStream.stop();
  }

  // ---------------------- connection handling -----------------------
  function prepareNewConnection() {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create peerConnection, exception: " + e.message);
    }

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
        sendCandidate({type: "candidate", 
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate}
		);
      } else {
        console.log("End of candidates. ------------------- phase=" + evt.eventPhase);
      }
    };

    console.log('Adding local stream...');
    peer.addStream(localStream);

    peer.addEventListener("addstream", onRemoteStreamAdded, false);
    peer.addEventListener("removestream", onRemoteStreamRemoved, false)

    // when remote adds a stream, hand it on to the local video element
    function onRemoteStreamAdded(event) {
      console.log("Added remote stream");
      remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
    }

    // when remote removes a stream, remove it from the local video element
    function onRemoteStreamRemoved(event) {
      console.log("Remove remote stream");
      remoteVideo.src = "";
    }

    return peer;
  }

  function sendOffer() {
    peerConnection = prepareNewConnection();
    peerConnection.createOffer(function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
      console.log("Sending: SDP");
      console.log(sessionDescription);
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Offer failed");
    }, mediaConstraints);
  }

  function setOffer(evt) {
    if (peerConnection) {
	  console.error('peerConnection alreay exist!');
	}
    peerConnection = prepareNewConnection();
    peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
  }
  
  function sendAnswer(evt) {
    console.log('sending Answer. Creating remote session description...' );
	if (! peerConnection) {
	  console.error('peerConnection NOT exist!');
	  return;
	}
	
    peerConnection.createAnswer(function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
      console.log("Sending: SDP");
      console.log(sessionDescription);
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Answer failed");
    }, mediaConstraints);
  }

  function setAnswer(evt) {
    if (! peerConnection) {
	  console.error('peerConnection NOT exist!');
	  return;
	}
	peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
  }
  
  // -------- handling user UI event -----
  // start the connection upon user request
  function connect() {
    if (!peerStarted &amp;&amp; localStream &amp;&amp; socketReady) { // **
	//if (!peerStarted &amp;&amp; localStream) { // --
      sendOffer();
      peerStarted = true;
    } else {
      alert("Local stream not running yet - try again.");
    }
  }

  // stop the connection upon user request
  function hangUp() {
    console.log("Hang up.");
    stop();
  }

  function stop() {
    peerConnection.close();
    peerConnection = null;
    peerStarted = false;
  }

  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre><p></p>

<h3>＜バックナンバー＞</h3>

<ul>
<li><a href="https://html5experts.jp/mganeko/5098/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">HTML5でWebRTCを使ってみよう！「カメラを使ってみよう」編</a></li>
<li><a href="https://html5experts.jp/mganeko/5181/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">WebRTCに触ってみたいエンジニア必見！手動でWebRTC通信をつなげてみよう編</a></li>
</ul>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTCを使ってみよう！]]></series:name>
	</item>
		<item>
		<title>WebRTCに触ってみたいエンジニア必見！手動でWebRTC通信をつなげてみよう</title>
		<link>/mganeko/5181/</link>
		<pubDate>Thu, 13 Feb 2014 03:30:21 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=5181</guid>
		<description><![CDATA[連載： WebRTCを使ってみよう！ (2)こんにちは！ がねこまさしです。前回はWebRTCでカメラを使いましたが、今回は通信をしてみましょう。 ※こちらの記事は2014年に書かれました。2016年6月のアップデート記...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc-beginner/" class="series-158" title="WebRTCを使ってみよう！" data-wpel-link="internal">WebRTCを使ってみよう！</a> (2)</div><p>こんにちは！ がねこまさしです。<a href="https://html5experts.jp/mganeko/5098/" title="HTML5でWebRTCを使ってみよう！「カメラを使ってみよう」編" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">前回はWebRTCでカメラ</a>を使いましたが、今回は通信をしてみましょう。</p>

<p><strong>※こちらの記事は2014年に書かれました。<a href="https://html5experts.jp/mganeko/19814/" data-wpel-link="internal">2016年6月のアップデート記事</a>がありますので、そちらもご参照ください。</strong></p>

<h2>WebRTCの通信はどうなっているの？</h2>

<p>WebRTCでは、映像や音声などリアルタイムに取得されたデータ（バイトストリーム）を、ブラウザ間で送受信することができます。それを司るのが　RTCPeerConnection です。
RTCPeerConnectionには2つの特徴があります。</p>

<ul>
    <li>Peer-to-Peer(P2P)の通信 → ブラウザとブラウザの間で直接通信する</li>
    <li>UDP/IPを使用 → TCP/IPのようにパケットの到着は保障しないが、オーバーヘッドが少ない</li>
</ul>

<p>多少の情報の欠落があっても許容する替わりに、通信のリアルタイム性を重視しています。UDPのポート番号は動的に割り振られ、49152 ～ 65535の範囲が使われるようです。
<a href="https://html5experts.jp/wp-content/uploads/2014/02/rtcpeerconnection.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/rtcpeerconnection-300x59.png" alt="rtcpeerconnection" width="300" height="59" class="alignnone size-medium wp-image-5185" srcset="/wp-content/uploads/2014/02/rtcpeerconnection-300x59.png 300w, /wp-content/uploads/2014/02/rtcpeerconnection-207x40.png 207w, /wp-content/uploads/2014/02/rtcpeerconnection.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h2>P2P通信が確立するまで</h2>

<p>ブラウザ間でP2P通信を行うには、相手のIPアドレスを知らなくてはなりません。また、動的に割り振られるUDPのポート番号も知る必要があります。そのためP2P通信が確立するまでに、WebRTCではいくつかの情報をやり取りしています。</p>

<h3>Session Description Protocol (SDP)</h3>

<p>各ブラウザの情報を示し、文字列で表現されます。例えば次のような情報を含んでいます。</p>

<ul>
    <li>セッションが含むメディアの種類（音声、映像）、メディアの形式（コーデック）</li>
    <li>IPアドレス、ポート番号</li>
    <li>P2Pのデータ転送プロトコル → WebRTCでは Secure RTP</li>
    <li>通信で使用する帯域</li>
    <li>セッションの属性（名前、識別子、アクティブな時間、など） → WebRTCでは使っていなさそう</li>
</ul>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/02/rtcpeer_ip_port.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/rtcpeer_ip_port-300x102.png" alt="rtcpeer_ip_port" width="300" height="102" class="alignnone size-medium wp-image-5190" srcset="/wp-content/uploads/2014/02/rtcpeer_ip_port-300x102.png 300w, /wp-content/uploads/2014/02/rtcpeer_ip_port-207x70.png 207w, /wp-content/uploads/2014/02/rtcpeer_ip_port.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<h3>Interactive Connectivity Establishment (ICE)</h3>

<p>可能性のある通信経路に関する情報を示し、文字列で表現されます。次のような複数の経路を候補としてリストアップします。</p>

<ul>
    <li>P2Pによる直接通信</li>
    <li>STUNによる、NAT通過のためのポートマッピング → 最終的にはP2Pになる</li>
    <li>TURNによる、リレーサーバーを介した中継通信</li>
</ul>

<p>候補が出そろったら、ネットワーク的に近い経路（オーバーヘッドの少ない経路）が選択されます。リストの上から順に優先です。※STUNやTURNについては、別の回で触れたいと思います。<br /></p>

<h2>手動シグナリングを実験してみる</h2>

<p>このように、P2Pを始めるまでの情報のやり取りを「シグナリング」と言います。実は、WebRTCではシグナリングのプロトコルは規定されていません（自由に選べます）。シグナリングを実現するには複数の方法がありますが、今回は最も原始的な方法を試してみましょう。（理論的にできることは皆知っていますが、実際に試した人は少ないと思います。本邦初公開？）<br />
ちょっと長いですが、こちらのHTMLをお好きなWebサーバーに配置してください。</p>

<p></p><pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;WebRTC 1 to 1 handshake&lt;/title&gt;  
&lt;/head&gt;
&lt;body&gt;
  &lt;button type="button" onclick="startVideo();"&gt;Start video&lt;/button&gt;
  &lt;button type="button" onclick="stopVideo();"&gt;Stop video&lt;/button&gt;
  &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;button type="button" onclick="connect();"&gt;Connect&lt;/button&gt;
  &lt;button type="button" onclick="hangUp();"&gt;Hang Up&lt;/button&gt;
  &lt;br /&gt;
  &lt;div&gt;
   &lt;video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt;
   &lt;video id="remote-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt;
  &lt;/div&gt;
  
  &lt;p&gt;
   SDP to send:&lt;br /&gt;
   &lt;textarea id="text-for-send-sdp" rows="5" cols="100" disabled="1"&gt;SDP to send&lt;/textarea&gt;
  &lt;/p&gt;
  &lt;p&gt;
   SDP to receive:&lt;br /&gt;
   &lt;textarea id="text-for-receive-sdp" rows="5" cols="100"&gt;&lt;/textarea&gt;&lt;br /&gt;
   &lt;button type="button" onclick="onSDP();"&gt;Receive SDP&lt;/button&gt;
  &lt;/p&gt;
  
  &lt;p&gt;
   ICE Candidate to send:&lt;br /&gt;
   &lt;textarea id="text-for-send-ice" rows="5" cols="100" disabled="1"&gt;ICE Candidate to send&lt;/textarea&gt;
  &lt;/p&gt;
  &lt;p&gt;  
   ICE Candidates to receive:&lt;br /&gt;
   &lt;textarea id="text-for-receive-ice" rows="5" cols="100"&gt;&lt;/textarea&gt;&lt;br /&gt;
   &lt;button type="button" onclick="onICE();"&gt;Receive ICE Candidates&lt;/button&gt;
  &lt;/p&gt;
  
  
  &lt;script&gt;
  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 }};

  // ----------------- handshake --------------
  var textForSendSDP = document.getElementById('text-for-send-sdp');
  var textForSendICE = document.getElementById('text-for-send-ice');
  var textToReceiveSDP = document.getElementById('text-for-receive-sdp');
  var textToReceiveICE = document.getElementById('text-for-receive-ice');
  var iceSeparator = '------ ICE Candidate -------';
  var CR = String.fromCharCode(13);
  
  function onSDP() {
    var text = textToReceiveSDP.value;
	var evt = JSON.parse(text);
	if (peerConnection) {
	  onAnswer(evt);
	}
	else {
	  onOffer(evt);
	}
	
	textToReceiveSDP.value ="";
  }  
  
  //--- multi ICE candidate ---
  function onICE() {
    var text = textToReceiveICE.value;
	var arr = text.split(iceSeparator);
	for (var i = 1, len = arr.length; i &lt; len; i++) {
      var evt = JSON.parse(arr[i]);
	  onCandidate(evt);
    }

	textToReceiveICE.value ="";
  }
  
  
  function onOffer(evt) {
    console.log("Received offer...")
	console.log(evt);
    setOffer(evt);
	sendAnswer(evt);
  }
  
  function onAnswer(evt) {
    console.log("Received Answer...")
	console.log(evt);
	setAnswer(evt);
  }
  
  function onCandidate(evt) {
    var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.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);
	
	textForSendSDP.value = text;
  }
  
  function sendCandidate(candidate) {
    var text = JSON.stringify(candidate);
	console.log("---sending candidate text ---");
	console.log(text);

	textForSendICE.value = (textForSendICE.value + CR + iceSeparator + CR + text + CR);
	textForSendICE.scrollTop = textForSendICE.scrollHeight;
  }
  
  // ---------------------- video handling -----------------------
  // start local video
  function startVideo() {
	navigator.webkitGetUserMedia({video: true, audio: false},
    function (stream) { // success
      localStream = stream;
      localVideo.src = window.webkitURL.createObjectURL(stream);
      localVideo.play();
	  localVideo.volume = 0;
    },
    function (error) { // error
      console.error('An error occurred: [CODE ' + error.code + ']');
      return;
    }
	);
  }

  // stop local video
  function stopVideo() {
    localVideo.src = "";
    localStream.stop();
  }

  // ---------------------- connection handling -----------------------
  function prepareNewConnection() {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create peerConnection, exception: " + e.message);
    }

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
        sendCandidate({type: "candidate", 
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate}
		);
      } else {
        console.log("End of candidates. ------------------- phase=" + evt.eventPhase);
      }
    };

    console.log('Adding local stream...');
    peer.addStream(localStream);

    peer.addEventListener("addstream", onRemoteStreamAdded, false);
    peer.addEventListener("removestream", onRemoteStreamRemoved, false)

    // when remote adds a stream, hand it on to the local video element
    function onRemoteStreamAdded(event) {
      console.log("Added remote stream");
      remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
    }

    // when remote removes a stream, remove it from the local video element
    function onRemoteStreamRemoved(event) {
      console.log("Remove remote stream");
      remoteVideo.src = "";
    }

    return peer;
  }

  function sendOffer() {
    peerConnection = prepareNewConnection();
    peerConnection.createOffer(function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
      console.log("Sending: SDP");
      console.log(sessionDescription);
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Offer failed");
    }, mediaConstraints);
  }

  function setOffer(evt) {
    if (peerConnection) {
	  console.error('peerConnection alreay exist!');
	}
    peerConnection = prepareNewConnection();
    peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
  }
  
  function sendAnswer(evt) {
    console.log('sending Answer. Creating remote session description...' );
	if (! peerConnection) {
	  console.error('peerConnection NOT exist!');
	  return;
	}
	
    peerConnection.createAnswer(function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
      console.log("Sending: SDP");
      console.log(sessionDescription);
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Answer failed");
    }, mediaConstraints);
  }

  function setAnswer(evt) {
    if (! peerConnection) {
	  console.error('peerConnection NOT exist!');
	  return;
	}
	peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
  }
  
  // -------- handling user UI event -----
  // start the connection upon user request
  function connect() {
    //if (!peerStarted &amp;&amp; localStream &amp;&amp; channelReady) {
	if (!peerStarted &amp;&amp; localStream) {
      sendOffer();
      peerStarted = true;
    } else {
      alert("Local stream not running yet - try again.");
    }
  }

  // stop the connection upon user request
  function hangUp() {
    console.log("Hang up.");
    stop();
  }

  function stop() {
    peerConnection.close();
    peerConnection = null;
    peerStarted = false;    
  }

  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre><p></p>

<p>Chromeを起動し、2つのウィンドウでWebサーバ上のHTMLにアクセスしてください。同一ウィンドウで複数タブを開くよりも、別のウィンドウで横に並べたほうが作業しやすいです。
以降、間違えやすいので慎重に操作してくださいね。</p>

<p style="text-indent: 0em">※ 2014/04/03 訂正 掲載したソースの45行目に誤りがありました。大変申し訳ありません。</p>

<p><code>誤 : var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':true }};
正 : var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }};</code></p>

<h3>SDPのやり取り</h3>

<p>(1) 両方のウィンドウで[Start video]ボタンをクリックします。カメラのアクセスを許可すると、それぞれリアルタイムの映像が表示されます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/handshake_sdp1.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/handshake_sdp1-300x66.png" alt="handshake_sdp1" width="300" height="66" class="alignnone size-medium wp-image-5202" srcset="/wp-content/uploads/2014/02/handshake_sdp1-300x66.png 300w, /wp-content/uploads/2014/02/handshake_sdp1-1024x228.png 1024w, /wp-content/uploads/2014/02/handshake_sdp1-207x45.png 207w, /wp-content/uploads/2014/02/handshake_sdp1.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>(2) 左のウィンドウで[Connect]ボタンをクリックしてください。すると[SDP to send:]のテキストエリアに、ドバドバっとSDPの文字列が表示されます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/handshake_sdp2.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/handshake_sdp2-300x104.png" alt="handshake_sdp2" width="300" height="104" class="alignnone size-medium wp-image-5207" srcset="/wp-content/uploads/2014/02/handshake_sdp2-300x104.png 300w, /wp-content/uploads/2014/02/handshake_sdp2-1024x356.png 1024w, /wp-content/uploads/2014/02/handshake_sdp2-207x71.png 207w, /wp-content/uploads/2014/02/handshake_sdp2.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>(3) これを全選択してコピーし、(4)右のウィンドウの[SDP to receive:]のテキストエリアにペーストします。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/handshake_sdp3.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/handshake_sdp3-300x110.png" alt="handshake_sdp3" width="300" height="110" class="alignnone size-medium wp-image-5206" srcset="/wp-content/uploads/2014/02/handshake_sdp3-300x110.png 300w, /wp-content/uploads/2014/02/handshake_sdp3-1024x375.png 1024w, /wp-content/uploads/2014/02/handshake_sdp3-207x75.png 207w, /wp-content/uploads/2014/02/handshake_sdp3.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>(5) 右のウィンドウの[Receive SDP]ボタンをクリックすると、今度は右のウィンドウの[SDP to send:]のテキストエリアに、ドバドバっとSDPの文字列が表示されます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/handshake_sdp4.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/handshake_sdp4-300x105.png" alt="handshake_sdp4" width="300" height="105" class="alignnone size-medium wp-image-5205" srcset="/wp-content/uploads/2014/02/handshake_sdp4-300x105.png 300w, /wp-content/uploads/2014/02/handshake_sdp4-1024x361.png 1024w, /wp-content/uploads/2014/02/handshake_sdp4-207x72.png 207w, /wp-content/uploads/2014/02/handshake_sdp4.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>(6) 右のウィンドウの[SDP to send:]のテキストエリアを全選択してコピー、(7)左に戻って[SDP to receive:]のテキストエリアにペーストします。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/handshake_sdp5.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/handshake_sdp5-300x103.png" alt="handshake_sdp5" width="300" height="103" class="alignnone size-medium wp-image-5204" srcset="/wp-content/uploads/2014/02/handshake_sdp5-300x103.png 300w, /wp-content/uploads/2014/02/handshake_sdp5-1024x351.png 1024w, /wp-content/uploads/2014/02/handshake_sdp5-207x70.png 207w, /wp-content/uploads/2014/02/handshake_sdp5.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>(8) 左のウィンドウの[Receive SDP]ボタンをクリックします。ここまででSDPのやり取りが終わりました。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/handshake_sdp6.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/handshake_sdp6-300x106.png" alt="handshake_sdp6" width="300" height="106" class="alignnone size-medium wp-image-5203" srcset="/wp-content/uploads/2014/02/handshake_sdp6-300x106.png 300w, /wp-content/uploads/2014/02/handshake_sdp6-1024x361.png 1024w, /wp-content/uploads/2014/02/handshake_sdp6-207x73.png 207w, /wp-content/uploads/2014/02/handshake_sdp6.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h3>ICE Candidateのやり取り</h3>

<p>通信経路を示すICE Candidateは、本来は1つずつやり取りされます。手動でやりとりするのは大変なので、今回は複数経路分まとめて渡してしまいましょう。</p>

<p>(9) 左のウィンドウの[ICE Candidate to send:]のテキストエリアを全選択してコピー、(10)右のウィンドウの[ICE Candidates to receive:]のテキストエリアにペーストします。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/handshake_ice1.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/handshake_ice1-300x153.png" alt="handshake_ice1" width="300" height="153" class="alignnone size-medium wp-image-5211" srcset="/wp-content/uploads/2014/02/handshake_ice1-300x153.png 300w, /wp-content/uploads/2014/02/handshake_ice1-1024x523.png 1024w, /wp-content/uploads/2014/02/handshake_ice1-207x105.png 207w, /wp-content/uploads/2014/02/handshake_ice1.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>(11) 右のウィンドウの[Receive ICE Candidate]ボタンをクリックします。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/handshake_ice2.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/handshake_ice2-300x158.png" alt="handshake_ice2" width="300" height="158" class="alignnone size-medium wp-image-5210" srcset="/wp-content/uploads/2014/02/handshake_ice2-300x158.png 300w, /wp-content/uploads/2014/02/handshake_ice2-1024x541.png 1024w, /wp-content/uploads/2014/02/handshake_ice2-207x109.png 207w, /wp-content/uploads/2014/02/handshake_ice2.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>(12) 右のウィンドウの[ICE Candidate to send:]のテキストエリアを全選択してコピー、(13)左に戻って[ICE Candidates to receive:]のテキストエリアにペーストします。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/handshake_ice3.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/handshake_ice3-300x153.png" alt="handshake_ice3" width="300" height="153" class="alignnone size-medium wp-image-5209" srcset="/wp-content/uploads/2014/02/handshake_ice3-300x153.png 300w, /wp-content/uploads/2014/02/handshake_ice3-1024x522.png 1024w, /wp-content/uploads/2014/02/handshake_ice3-207x105.png 207w, /wp-content/uploads/2014/02/handshake_ice3.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>(14) 左のウィンドウの[Receive ICE Candidate]ボタンをクリックします。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/handshake_ice4.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/handshake_ice4-300x168.png" alt="handshake_ice4" width="300" height="168" class="alignnone size-medium wp-image-5208" srcset="/wp-content/uploads/2014/02/handshake_ice4-300x168.png 300w, /wp-content/uploads/2014/02/handshake_ice4-1024x576.png 1024w, /wp-content/uploads/2014/02/handshake_ice4-207x116.png 207w, /wp-content/uploads/2014/02/handshake_ice4.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>これで上手くいけば、P2P通信が始まり両方のウィンドウに2つ目の動画が表示されるはずです。上手くいかない場合は、手順が抜けているか間違っている可能性があります。深呼吸してから両方のブラウザをリロードし、もう一度試してみてください。（私のソースのバグや、説明の間違いの可能性もあります。気がつかれたらご指摘ください）</p>

<h2>SDPのやり取りを追ってみる</h2>

<p>[Connect]ボタンを押してから、SDPのやり取りをソースで追ってみましょう。 ※ソースは抜粋しています。
</p><pre class="crayon-plain-tag">function connect() {
  sendOffer();
}

function sendOffer() {
  peerConnection = prepareNewConnection(); // webkitRTCPeerConnection を生成し、コールバックを設定している
  peerConnection.createOffer(
    function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
      sendSDP(sessionDescription);
    },
    function () { // in case of error
      console.log("Create Offer failed");
    },
  mediaConstraints);
}</pre><p> 
発信側で[Connect]ボタンを押すと connect() → sendOffer() という順に呼び出されます。中ではRTCPeerConnectionのインスタンスを生成し、そのインスタンスにSDPの作成を依頼します。SDPには通信開始を依頼するOfferと、応答するAnswerの2種類があり、ここではOfferを使います。</p>

<p>SDPはコールバック関数に渡されるので、何らかの手段を使って相手に渡します。今回のsendSDP()ではテキストエリアにSDPを表示するところまでになります。</p>

<p></p><pre class="crayon-plain-tag">function onOffer(evt) {
  setOffer(evt);
  sendAnswer(evt);
}
  
function setOffer(evt) {
  peerConnection = prepareNewConnection();  // webkitRTCPeerConnection を生成し、コールバックを設定している
  peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
}
  
function sendAnswer(evt) {
  peerConnection.createAnswer(
    function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
      sendSDP(sessionDescription);
    },
    function () { // in case of error
      console.log("Create Answer failed");
    },
  mediaConstraints);
}</pre><p> 
応答側にSDPを貼り付け、[Receive SDP]ボタンをクリックすると、onSDP() → onOffer() → setOffer() と呼び出されます。setOffer()の中ではRTCPeerConnectionのインスタンスを生成し、受け取ったOffer SDPを覚えさせます。</p>

<p>次にsendAnswer()で今度は応答用のAnswer SDPの生成を依頼し、コールバックからsendSDP()で発信側に送り返します。ここでもテキストエリアにSDPを表示するところまでになります。</p>

<p></p><pre class="crayon-plain-tag">function onAnswer(evt) {
  setAnswer(evt);
}

function setAnswer(evt) {
  peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
}</pre><p> 
発信側にSDPを貼り付け、[Receive SDP]ボタンをクリックすると、onSDP() → onAnswer() → setAnswer() と呼び出されます。setAnswer()の中では生成済のRTCPeerConnectionのインスタンスに受け取ったAnswer SDPを覚えさせます。</p>

<h2>ICEのやり取りも追ってみる</h2>

<p>同様に、ICE Candidateのやり取りも見てみましょう。まず、prepareNewConnection()の中身から。</p>

<p></p><pre class="crayon-plain-tag">function prepareNewConnection() {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create peerConnection, exception: " + e.message);
    }

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
        sendCandidate({type: "candidate", 
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate}
		);
      } else {
        console.log("End of candidates. ------------------- phase=" + evt.eventPhase);
      }
    };
    // ... 省略
}</pre><p> 
ICE Candidateの生成は、非同期に発生します。そのためRTCPeerConnection.onicecandidateに、コールバック関数を指定します。今回は sendCandidate()を呼び出していますが、その中では例によってテキストエリアに追記する処理を行います。※ICE Candidateは複数個生成され、コールバックもその度に呼び出されます。</p>

<p></p><pre class="crayon-plain-tag">function onICE() {
  var text = textToReceiveICE.value;
  var arr = text.split(iceSeparator);
  for (var i = 1, len = arr.length; i &lt; len; i++) {
    var evt = JSON.parse(arr[i]);
	onCandidate(evt);
  }
}
 
function onCandidate(evt) {
  var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate});
  peerConnection.addIceCandidate(candidate);
}</pre><p></p>

<p>ICE Candidateをペーストして[Receive ICE Candidates]ボタンをクリックすると、onICE()でテキストエリアの内容を分割し、1つ1つのICE Candidateを取り出します。それをonCandidate()に渡すと、RTCPeerConnectionのインスタンスにセットします。すべての経路の候補(ICE Candidate)の交換が終わると、P2P通信が開始されます。</p>

<h2>手動シグナリングの改良版ソース(2014年4月21日追加）</h2>

<p>手動シグナリングは操作が面倒なので、ちょっとでも楽になるように必要なテキストを自動で選択するようにしました。あとはコピー＆ペーストでできます。</p>

<p></p><pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;WebRTC 1 to 1 handshake V2&lt;/title&gt;  
&lt;/head&gt;
&lt;body&gt;
  &lt;button type="button" onclick="startVideo();"&gt;Start video&lt;/button&gt;
  &lt;button type="button" onclick="stopVideo();"&gt;Stop video&lt;/button&gt;
  &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;button type="button" onclick="connect();"&gt;Connect&lt;/button&gt;
  &lt;button type="button" onclick="hangUp();"&gt;Hang Up&lt;/button&gt;
  &lt;br /&gt;
  &lt;div&gt;
   &lt;video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt;
   &lt;video id="remote-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt;
  &lt;/div&gt;
  
  &lt;p&gt;
   SDP to send:&lt;br /&gt;
   &lt;textarea id="text-for-send-sdp" rows="5" cols="100" disabled="1"&gt;SDP to send&lt;/textarea&gt;
  &lt;/p&gt;
  &lt;p&gt;
   SDP to receive:&lt;br /&gt;
   &lt;textarea id="text-for-receive-sdp" rows="5" cols="100"&gt;&lt;/textarea&gt;&lt;br /&gt;
   &lt;button type="button" onclick="onSDP();"&gt;Receive SDP&lt;/button&gt;
  &lt;/p&gt;
  
  &lt;p&gt;
   ICE Candidate to send:&lt;br /&gt;
   &lt;textarea id="text-for-send-ice" rows="5" cols="100" disabled="1" onclick="this.focus(); this.select();"&gt;ICE Candidate to send&lt;/textarea&gt;
  &lt;/p&gt;
  &lt;p&gt;  
   ICE Candidates to receive:&lt;br /&gt;
   &lt;textarea id="text-for-receive-ice" rows="5" cols="100"&gt;&lt;/textarea&gt;&lt;br /&gt;
   &lt;button type="button" onclick="onICE();"&gt;Receive ICE Candidates&lt;/button&gt;
  &lt;/p&gt;
  
  
  &lt;script&gt;
  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':false, 'OfferToReceiveVideo':true }};
  var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }};

  // ----------------- handshake --------------
  var textForSendSDP = document.getElementById('text-for-send-sdp');
  var textForSendICE = document.getElementById('text-for-send-ice');
  var textToReceiveSDP = document.getElementById('text-for-receive-sdp');
  var textToReceiveICE = document.getElementById('text-for-receive-ice');
  var iceSeparator = '------ ICE Candidate -------';
  var CR = String.fromCharCode(13);
  
  function onSDP() {
    var text = textToReceiveSDP.value;
	var evt = JSON.parse(text);
	if (peerConnection) {
	  onAnswer(evt);
	  
	  textForSendICE.focus();
	  textForSendICE.select();
	}
	else {
	  onOffer(evt);
	}
	
	textToReceiveSDP.value ="";
  }
  
  //--- multi ICE candidate ---
  function onICE() {
    var text = textToReceiveICE.value;
	var arr = text.split(iceSeparator);
	for (var i = 1, len = arr.length; i &lt; len; i++) {
      var evt = JSON.parse(arr[i]);
	  onCandidate(evt);
    }

	textToReceiveICE.value ="";
	
	textForSendICE.focus();
	textForSendICE.select();
  }
  
  
  function onOffer(evt) {
    console.log("Received offer...")
	console.log(evt);
    setOffer(evt);
	sendAnswer(evt);
  }
  
  function onAnswer(evt) {
    console.log("Received Answer...")
	console.log(evt);
	setAnswer(evt);
  }
  
  function onCandidate(evt) {
    var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.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);
	
	textForSendSDP.value = text;
	textForSendSDP.focus();
	textForSendSDP.select();
  }
  
  function sendCandidate(candidate) {
    var text = JSON.stringify(candidate);
	console.log("---sending candidate text ---");
	console.log(text);

	textForSendICE.value = (textForSendICE.value + CR + iceSeparator + CR + text + CR);
	textForSendICE.scrollTop = textForSendICE.scrollHeight;
  }
  
  // ---------------------- video handling -----------------------
  // start local video
  function startVideo() {
	navigator.webkitGetUserMedia({video: true, audio: false},
    function (stream) { // success
      localStream = stream;
      localVideo.src = window.webkitURL.createObjectURL(stream);
      localVideo.play();
	  localVideo.volume = 0;
    },
    function (error) { // error
      console.error('An error occurred: [CODE ' + error.code + ']');
      return;
    }
	);
  }

  // stop local video
  function stopVideo() {
    localVideo.src = "";
    localStream.stop();
  }

  // ---------------------- connection handling -----------------------
  function prepareNewConnection() {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create peerConnection, exception: " + e.message);
    }

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
        sendCandidate({type: "candidate", 
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate}
		);
      } else {
        console.log("End of candidates. ------------------- phase=" + evt.eventPhase);
      }
    };

    console.log('Adding local stream...');
    peer.addStream(localStream);

    peer.addEventListener("addstream", onRemoteStreamAdded, false);
    peer.addEventListener("removestream", onRemoteStreamRemoved, false)

    // when remote adds a stream, hand it on to the local video element
    function onRemoteStreamAdded(event) {
      console.log("Added remote stream");
      remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
    }

    // when remote removes a stream, remove it from the local video element
    function onRemoteStreamRemoved(event) {
      console.log("Remove remote stream");
      remoteVideo.src = "";
    }

    return peer;
  }

  function sendOffer() {
    peerConnection = prepareNewConnection();
    peerConnection.createOffer(function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
      console.log("Sending: SDP");
      console.log(sessionDescription);
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Offer failed");
    }, mediaConstraints);
  }

  function setOffer(evt) {
    if (peerConnection) {
	  console.error('peerConnection alreay exist!');
	}
    peerConnection = prepareNewConnection();
    peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
  }
  
  function sendAnswer(evt) {
    console.log('sending Answer. Creating remote session description...' );
	if (! peerConnection) {
	  console.error('peerConnection NOT exist!');
	  return;
	}
	
    peerConnection.createAnswer(function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
      console.log("Sending: SDP");
      console.log(sessionDescription);
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Answer failed");
    }, mediaConstraints);
  }

  function setAnswer(evt) {
    if (! peerConnection) {
	  console.error('peerConnection NOT exist!');
	  return;
	}
	peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
  }
  
  // -------- handling user UI event -----
  // start the connection upon user request
  function connect() {
    //if (!peerStarted &amp;&amp; localStream &amp;&amp; channelReady) {
	if (!peerStarted &amp;&amp; localStream) {
      sendOffer();
      peerStarted = true;
    } else {
      alert("Local stream not running yet - try again.");
    }
  }

  // stop the connection upon user request
  function hangUp() {
    console.log("Hang up.");
    stop();
  }

  function stop() {
    peerConnection.close();
    peerConnection = null;
    peerStarted = false;    
  }

  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre><p></p>

<p>※手動シグナリングはなぜか動作が不安定で、端末によっては通信が確立しないケースがあります（今のところ成功は4台/7台)。原因がさっぱりわからないのですが、もし心当たりがあったら教えていただけると助かります。情報求む！</p>

<h2>次回は</h2>

<p>以上、今回は原始的なビデオチャットを動かしてみました。同一PC上ではなく異なるPC間でも(FirewallやNATを挟まない場合は)、SDP/ICEの文字列をチャットなどで渡せば「原理上は」P2P通信は成立するはずです。（私はやってません…）</p>

<p>実際には手動でシグナリングなんかやってられません。次回はシグナリングサーバーを Node.js + socket.io で動かしてみたいと思います。</p>

<h3>バックナンバーを読む</h3>

<ul>
<li><a href="https://html5experts.jp/mganeko/5098/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">HTML5でWebRTCを使ってみよう！「カメラを使ってみよう」編</a></li>
<li><a href="https://html5experts.jp/mganeko/5349/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">WebRTC初心者でも簡単にできる！Node.jsで仲介（シグナリング）を作ってみよう編</a></li>
</ul>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTCを使ってみよう！]]></series:name>
	</item>
		<item>
		<title>HTML5でWebRTCを使ってみよう！「カメラを使ってみよう」編</title>
		<link>/mganeko/5098/</link>
		<pubDate>Wed, 05 Feb 2014 01:00:04 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[CSS]]></category>
		<category><![CDATA[WebRTC]]></category>
		<category><![CDATA[カメラ]]></category>

		<guid isPermaLink="false">/?p=5098</guid>
		<description><![CDATA[連載： WebRTCを使ってみよう！ (1)こんにちは！ がねこまさしです。これから数回に渡って、WebRTCについて書かせていただきます。 内容は2013年10月にNode学園祭2013で発表したプレゼンを、再構成した...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc-beginner/" class="series-158" title="WebRTCを使ってみよう！" data-wpel-link="internal">WebRTCを使ってみよう！</a> (1)</div><p>こんにちは！ がねこまさしです。これから数回に渡って、WebRTCについて書かせていただきます。
内容は2013年10月にNode学園祭2013で発表したプレゼンを、再構成したものになる予定です。</p>

<p><strong>※こちらの記事は2014年に書かれました。<a href="https://html5experts.jp/mganeko/19728/" data-wpel-link="internal">2016年6月現在のアップデート記事</a>がありますので、そちらもご参照ください。</strong></p>

<h2>WebRTCとは？</h2>

<p><a href="http://www.webrtc.org/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">WebRTC</a>とは&#8221;Web Real-Time Communication&#8221;の略で、Webブラウザ上でビデオ/オーディオの通信や、データ通信を行うための規格です。HTML5で新しく策定されたもので、複数の技術の連携で成り立っています。
ちなみに策定には複数の団体が絡んでいています。</p>

<ul>
    <li>API → <a href="http://www.w3.org/TR/webrtc/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">WWW</a></li>
    <li>ビデオ/オーディオのコーデック  → <a href="http://tools.ietf.org/wg/rtcweb/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">IETF</a></li>
</ul>

<h2>WebRTCで何ができるの？</h2>

<p>WebRTCには大きく分けて2つの要素があります。</p>

<ul>
    <li>カメラ、マイクといったメディアへのアクセス(UserMedia)</li>
    <li>Peer-to-Peer通信を行うための仕組み(RTCPeerConnection)</li>
</ul>

<p>このほかにもHTML5の様々な要素と組み合わせて活用することができます。</p>

<ul>
    <li>JavaScript（大前提）</li>
    <li>videoタグ、audioタグ</li>
    <li>CSS3</li>
    <li>Canvas</li>
    <li>WebGL</li>
    <li>Web Audio API</li>
    <li>WebSocket</li>
</ul>

<h2>ユーザーメディアを使ってみよう</h2>

<p>それでは、さっそく使ってみましょう。ユーザーメディアで一番わかりやすいカメラを使います。Webカメラと最新のChromeかFirefoxをご用意ください。<br>
※今回はChrome 32, Firefox 26で動作確認しています。</p>

<p></p><pre class="crayon-plain-tag">&lt;!doctype html&gt;
&lt;html&gt;
 &lt;head&gt;
  &lt;title&gt;Self Camera&lt;/title&gt;
 &lt;/head&gt;
 &lt;body&gt;
   &lt;video id="myVideo" width="400" height="300" autoplay="1" &gt;&lt;/video&gt;

   &lt;script type="text/javascript"&gt;
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia;
    window.URL = window.URL || window.webkitURL;

    var video = document.getElementById('myVideo');
    var localStream = null;
    navigator.getUserMedia({video: true, audio: false}, 
     function(stream) { // for success case
      console.log(stream);
      video.src = window.URL.createObjectURL(stream);
     },
     function(err) { // for error case
      console.log(err);
     }
    );
   &lt;/script&gt;
 &lt;/body&gt;
&lt;/html&gt;</pre><p> 
こちらをお好きなWebサーバーに保存し、ChromeないしFirefoxでアクセスしてみてください。カメラにアクセスしても良いかどうか確認のダイアログが表示されますので、OKすればカメラの映像が表示されるはずです。<br />
FireFoxの場合：<a href="https://html5experts.jp/wp-content/uploads/2014/02/firefox_getusermedia.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/firefox_getusermedia-300x130.png" alt="firefox_getusermedia" width="300" height="130" class="alignnone size-medium wp-image-5120" srcset="/wp-content/uploads/2014/02/firefox_getusermedia-300x130.png 300w, /wp-content/uploads/2014/02/firefox_getusermedia-207x89.png 207w, /wp-content/uploads/2014/02/firefox_getusermedia.png 380w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /><br />
Chromeの場合：<a href="https://html5experts.jp/wp-content/uploads/2014/02/chrome_getusermedia.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/chrome_getusermedia-300x40.png" alt="chrome_getusermedia" width="300" height="40" class="alignnone size-medium wp-image-5123" srcset="/wp-content/uploads/2014/02/chrome_getusermedia-300x40.png 300w, /wp-content/uploads/2014/02/chrome_getusermedia-207x27.png 207w, /wp-content/uploads/2014/02/chrome_getusermedia.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>上手く動けば、このように自分の顔が映ると思います。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/02/chrome_selfcamera.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/chrome_selfcamera-300x294.png" alt="chrome_selfcamera" width="300" height="294" class="alignnone size-medium wp-image-5126" srcset="/wp-content/uploads/2014/02/chrome_selfcamera-300x294.png 300w, /wp-content/uploads/2014/02/chrome_selfcamera-207x203.png 207w, /wp-content/uploads/2014/02/chrome_selfcamera.png 470w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
※上手くいかない場合は、JavaScriptコンソールを表示してエラーを確認してくださいね。<br>
ちなみに、HTMLをWebサーバーに置かずに直接開いた場合(file:// ～ )、Chromeではセキュリティ制限に引っかかりカメラにアクセスするこができません。FireFoxは現状の実装ではアクセスできるようです。</p>

<h2>CSS3 と組み合わせてみる</h2>

<p>WebRTCは他の要素と組み合わせて使うことができると、最初に書きました。実際にやってみましょう。<br>
※コードを単純にするために、ここからはChrome用のコードを掲載します。Firefoxの場合は適宜プレフィックスを変更してください。
</p><pre class="crayon-plain-tag">&lt;video id="myVideo" style="-webkit-transform: scaleX(-1);" width="400" height="300" autoplay="1" &gt;&lt;/video&gt;</pre><p> 
このようにvideoタグを変更すると、左右反転した鏡の状態になります。この方が自分では見慣れた姿に映ります。skypeなど多くのビデオチャットアプリは、左右反転状態がデフォルトになっているものが多いようです。</p>

<p>ほかにも様々なバリエーションが考えられます。ぜひ自分でもいろいろ試してみてください。</p>

<p></p><pre class="crayon-plain-tag">&lt;video id="myVideo" style="-webkit-transform: scaleY(-1);" width="400" height="300" autoplay="1" &gt;&lt;/video&gt;</pre><p></p>

<p></p><pre class="crayon-plain-tag">&lt;video id="myVideo" style="-webkit-transform: scaleX(0.5);" width="400" height="300" autoplay="1" &gt;&lt;/video&gt;</pre><p></p>

<p></p><pre class="crayon-plain-tag">&lt;video id="myVideo" style="-webkit-filter: sepia(80%);" width="400" height="300" autoplay="1" &gt;&lt;/video&gt;</pre><p></p>

<p>CSS3のアニメーション機能も利用できます。たとえばこんな風に。</p>

<p></p><pre class="crayon-plain-tag">&lt;!doctype html&gt;
&lt;html&gt;
 &lt;head&gt;
  &lt;title&gt;Rotate Camera&lt;/title&gt;
  &lt;style type="text/css"&gt;
   #rotateVideo
   {
    position: absolute;
    -webkit-animation-duration:2s;
    -webkit-animation-iteration-count:infinite;
    -webkit-animation-timing-function:linear;
    -webkit-animation-name:rectRotate;     
   }
 
   @-webkit-keyframes rectRotate
   {
    0%{-webkit-transform:rotate(0deg);}
    99%,100%{-webkit-transform:rotate(360deg);}
   }
  &lt;/style&gt;
 &lt;/head&gt;
 &lt;body&gt;
   &lt;video id="rotateVideo" width="400" height="300" autoplay="1" &gt;&lt;/video&gt;

   &lt;script type="text/javascript"&gt;
    navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia;
    window.URL = window.URL || window.webkitURL;

    var video = document.getElementById('rotateVideo');
    navigator.getUserMedia({video: true, audio: false}, 
     function(stream) { // for success case
      console.log(stream);
      video.src = window.URL.createObjectURL(stream);
     },
     function(err) { // for error case
      console.log(err);
     }
    );
   &lt;/script&gt;
 &lt;/body&gt;
&lt;/html&gt;</pre><p> 
<a href="https://html5experts.jp/wp-content/uploads/2014/02/chrome_rotate.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/02/chrome_rotate-300x263.png" alt="chrome_rotate" width="300" height="263" class="alignnone size-medium wp-image-5134" srcset="/wp-content/uploads/2014/02/chrome_rotate-300x263.png 300w, /wp-content/uploads/2014/02/chrome_rotate-207x181.png 207w, /wp-content/uploads/2014/02/chrome_rotate.png 497w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<p>以上、ユーザメディア（カメラ）を使ってみました。次回は通信関連の要素についてご説明する予定です。</p>

<h3>続編を読む</h3>

<ul>
<li><a href="https://html5experts.jp/mganeko/5181/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">WebRTCに触ってみたいエンジニア必見！手動でWebRTC通信をつなげてみよう</a></li>
<li><a href="https://html5experts.jp/mganeko/5349/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">WebRTC初心者でも簡単にできる！Node.jsで仲介（シグナリング）を作ってみよう編</a></li>
</ul>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTCを使ってみよう！]]></series:name>
	</item>
	</channel>
</rss>
