<?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入門2016 &#8211; HTML5Experts.jp</title>
	<atom:link href="/series/webrtc2016/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>そして壁の向こうへ。 NAT/Firewallを越えて通信しよう―WebRTC入門2016</title>
		<link>/mganeko/20618/</link>
		<pubDate>Mon, 12 Dec 2016 00:21:24 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[最新動向]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=20618</guid>
		<description><![CDATA[連載： WebRTC入門2016 (6)こんにちは！ 2014年に連載した「WebRTCを使ってみよう！」シリーズのアップデート記事も番外編を含めて6回目となりました。2016年の最後として、実際の通信では欠かせないNA...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc2016/" class="series-380" title="WebRTC入門2016" data-wpel-link="internal">WebRTC入門2016</a> (6)</div><p>こんにちは！ 2014年に連載した<a href="https://html5experts.jp/series/webrtc-beginner/" target="_blank" data-wpel-link="internal">「WebRTCを使ってみよう！」</a>シリーズの<a href="https://html5experts.jp/series/webrtc2016/" target="_blank" data-wpel-link="internal">アップデート記事</a>も番外編を含めて6回目となりました。2016年の最後として、実際の通信では欠かせないNAT越えと、企業ネットワークで使うために必要なFirewallを通過する方法について見ていきましょう。</p>

<h2>NATを越えて</h2>

<h4>NATの役割</h4>

<p>NAT(+IPマスカレード)は企業だけでなく、一般家庭でも使われています。ブロードバンドルーターや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アドレス</li>
<li>自分が使う（動的に割り振った)UDPポート</li>
</ul>

<h4>ブラウザでは分からない情報</h4>

<ul>
<li>グローバルIPアドレス</li>
<li>NATによってマッピングされた、外部に向けたUDPポート</li>
</ul>

<h4>例えば</h4>

<p>次の架空の例を見てみましょう。二つのPCのブラウザでは、それぞれ自分のローカルIPアドレス、UDPは知ることができます。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/12/nat_address.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/12/nat_address-300x181.png" alt="nat_address" width="300" height="181" class="alignnone size-medium wp-image-21744" srcset="/wp-content/uploads/2016/12/nat_address-300x181.png 300w, /wp-content/uploads/2016/12/nat_address.png 640w, /wp-content/uploads/2016/12/nat_address-207x125.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<ul>
<li>左のPCのブラウザが分かること

<ul>
<li>ローカルIPアドレス 192.168.10.11</li>
<li>ローカルUDPポート 4001/UDP</li>
</ul></li>
<li>右のPCのブラウザが分かること

<ul>
<li>ローカルIPアドレス 10.2.100.51</li>
<li>ローカルUDPポート 5001/UDP</li>
</ul></li>
</ul>

<p>しかし、NATの外側から見たグローバルの情報は自分では分かりません。</p>

<ul>
<li>左のPCのブラウザが知らないこと

<ul>
<li>グローバルIPアドレス 178.50.111.222</li>
<li>グローバルUDPポート 51111/UDP</li>
</ul></li>
<li>右のPCのブラウザが知らないこと

<ul>
<li>グローバルIPアドレス 200.100.50.81</li>
<li>グローバルUDPポート 52222/UDP</li>
</ul></li>
</ul>

<p>NATを越えてPeer-to-Peer通信を行うには、シグナリング処理でお互いに自分の知らないグローバルの情報を交換する必要があります。</p>

<h2>STUNが教えてくれること</h2>

<p>自分で知らない情報は、誰かから教えてもらうしかありません。それを可能にするのがSTUN(Session Traversal Utilities for NATs)です。</p>

<p>STUNの仕組みはシンプルです。NATで変換されたIPアドレス/ポートを、外側にいるSTUNサーバーに教えてもらいます。先ほどの例で言えば、このようになります。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/12/stun.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/12/stun-300x217.png" alt="stun" width="300" height="217" class="alignnone size-medium wp-image-21745" srcset="/wp-content/uploads/2016/12/stun-300x217.png 300w, /wp-content/uploads/2016/12/stun.png 640w, /wp-content/uploads/2016/12/stun-207x150.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>自分のグローバル情報が分かったら、それをシグナリングサーバー経由で相手に渡します。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/12/stun_signaling.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/12/stun_signaling-300x215.png" alt="stun_signaling" width="300" height="215" class="alignnone size-medium wp-image-21748" srcset="/wp-content/uploads/2016/12/stun_signaling-300x215.png 300w, /wp-content/uploads/2016/12/stun_signaling.png 640w, /wp-content/uploads/2016/12/stun_signaling-207x148.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>STUNサーバーから得られた情報は、シグナリングの過程でICE Candidateとして取得できます。これをシグナリングサーバーなどを経由して相手に送ることになります。（参考： <a href="https://html5experts.jp/mganeko/20013/" target="_blank" data-wpel-link="internal">シグナリングサーバーを動かそう</a>、<a href="https://html5experts.jp/mganeko/20112/" target="_blank" data-wpel-link="internal">シグナリングを拡張して、複数人で通信してみよう </a>）</p>

<p>首尾よくNATとポート変換を潜り抜ければ、Peer-to-Peerで通信を行うことができます。</p>

<h4>STUNサーバーを使ってみよう</h4>

<p>STUNサーバーを自分で動かすこともできますが、幸い公開されているSTUNサーバーがあります。まず、そちらを使ってみましょう。Googleが公開しているサーバーがよく利用されているようです。</p>

<ul>
<li>stun.l.google.com:19302</li>
<li>stun1.l.google.com:19302</li>
<li>stun2.l.google.com:19302</li>
<li>stun3.l.google.com:19302</li>
<li>stun4.l.google.com:19302</li>
</ul>

<p>通常はどれか1つ指定すればOKですが、複数指定することもできます。それでは、実際に<a href="https://html5experts.jp/mganeko/20112/" target="_blank" data-wpel-link="internal">以前のソース</a>の一部を修正してみましょう。</p>

<p></p><pre class="crayon-plain-tag">function prepareNewConnection(id) {
  let pc_config = {"iceServers":[
    {"urls": "stun:stun.l.google.com:19302"},
    {"urls": "stun:stun1.l.google.com:19302"},
    {"urls": "stun:stun2.l.google.com:19302"}
  ]};
  let peer = new RTCPeerConnection(pc_config);
  // ... 省略 ...
}</pre><p></p>

<p>さあ、これで接続を試してください。上手く行けば、例えば自宅と友人の家で通信が可能になっているはずです。</p>

<h4>STUNでNATを越えられないとき</h4>

<p>NATにはグローバルIPアドレスを共有するだけでなく、セキュリティ対策としての役割もあります。内部の端末を隠したり、通信できるポートを制限したり、一種の簡易Firewallとして利用されているケースもあります。その場合はFirewallの場合と同じく、次に説明するTURNを利用する必要があります。</p>

<p>またNATの構造によっては、接続先によって（今回の場合、STUNサーバーとPeer-to-Peerの通信相手）別のポートが割り当てられる Symmetric NAT という物があるようです。この場合もSTUNの仕組みでは通信することができません。やはりTURNの出番ということになります。</p>

<h2>Firewallを越えたい</h2>

<p>一般家庭のようにブロードバンドルーターなどでNATがある環境では、STUNを使えば通信が可能になります。</p>

<p>それに対して一般的な企業では、Firewallにより通信できるポートが制限されるケースが多いようです。STUNを使った場合でもUDPポートは動的に割り振られるままなので、通信をするためにはFirewallにとても大きな穴を空ける必要があります。それではきっとセキュリティ管理者に怒られてしまいます。</p>

<p>こんなケースに対応するのが、TURN(Traversal Using Relays around NAT)の仕組みです。先ほどのSTUNもTURNもどちらもWebRTCのために生まれたのではなく、以前からVoIPやネットワークゲームの世界で使われていたものです。</p>

<h3>TURNの仕組み</h3>

<p>TURNを使った通信では、TURNサーバが実際のストリームデータを受け渡す間に入ります。すべてのパケットをTURNサーバーがリレーすることになり、もはや厳密にはPeer-to-Peer通信ではなくなります。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/12/turn.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/12/turn-300x209.png" alt="turn" width="300" height="209" class="alignnone size-medium wp-image-21788" srcset="/wp-content/uploads/2016/12/turn-300x209.png 300w, /wp-content/uploads/2016/12/turn.png 640w, /wp-content/uploads/2016/12/turn-207x144.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>ただしこの際、TURNサーバーではパケットをそのままリレーし、内容については一切変更はしません。</p>

<ul>
<li>動画のデコード、エンコードは行わない</li>
<li>データはそのまま受け渡し、両端のPeerで施された暗号は解除しない</li>
</ul>

<p>そのため個人的には、アプリケーション的に見た場合にはPeer-to-Peerを維持していると考えていいと思っています。</p>

<p>TURNサーバーでは暗号化処理や動画のデコード/エンコーディングは行わないので、CPU負荷よりもネットワーク負荷が高くなりやすいです。</p>

<h3>TURNサーバーを用意するには</h3>

<p>それでは、実際にTURNサーバーを動かしてみましょう。2014年の記事では<a href="https://code.google.com/archive/p/rfc5766-turn-server/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">rfc5766-turn-server</a>を使いましたが、今回はその後継である<a href="https://github.com/coturn/coturn" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">coturn</a>を使います。coturnが生まれた背景は、こちらの記事に詳しく書かれています。</p>

<ul>
<li><a href="https://html5experts.jp/iwase/11300/" target="_blank" data-wpel-link="internal">[翻訳] coTURN:マルチテナント型のオープンソースのSTUN/TURNサーバ</a></li>
</ul>

<p>今回は、OSはUbuntu 16.04 LTSを使って導入してみました。 なるべくOSセットアップ直後の素の状態から自分でビルドしてみましょう。</p>

<h4>自分でビルドする手順</h4>

<p>まずは、依存ツールやライブラリを導入します。</p>

<p></p><pre class="crayon-plain-tag">sudo apt-get update
sudo apt-get upgrade</pre><p></p>

<p></p><pre class="crayon-plain-tag">sudo apt-get install gcc
sudo apt-get install sqlite3
sudo apt-get install libssl-dev
sudo apt-get install libevent-dev
sudo apt-get install make</pre><p></p>

<p>次にcoturnのソースを、こちらのサイトからダウンロードします。</p>

<ul>
<li><a href="https://github.com/coturn/coturn/wiki/Downloads" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">https://github.com/coturn/coturn/wiki/Downloads</a></li>
</ul>

<p>私が試したときは4.5.0.4が最新でしたが、2016年12月現在は4.5.0.5が最新のようです。
利用したいバージョンのソースをダウンロードして、解凍してください。</p>

<p></p><pre class="crayon-plain-tag">wget http://turnserver.open-sys.org/downloads/v4.5.0.4/turnserver-4.5.0.4.tar.gz
tar xvfz turnserver-4.5.0.4.tar.gz</pre><p></p>

<p>それではビルドしてみましょう。
</p><pre class="crayon-plain-tag">cd turnserver-4.5.0.4
./configure
make
sudo make install</pre><p></p>

<p>もし途中で依存ライブラリ不足でエラーが出た場合には、追加でインストールしてからビルドし直してみてください。ビルドに成功すると、Ubuntu16の場合は次の場所に関連ファイルがインストールされます。</p>

<ul>
<li>バイナリ →  /usr/local/bin/turnserver</li>
<li>設定ファイル（ひな形） → /usr/local/etc/turnserver.conf.default</li>
</ul>

<p>設定ファイルは適宜 turnserver.conf にコピーして、それを編集して利用してください。</p>

<p>※こちらの手順はcoturn 4.5.0.4 をビルドした際のものです。最新のものでは手順が変更されている可能性がありますので、もし何か見つけたらコメントいただけると嬉しいです。</p>

<h2>coturn を動かすまで</h2>

<p>coturnをTURNサーバーとして動かす際に、通信方法を2種類を使うことがことができます。</p>

<ul>
<li>TURN &#8230; 特定のUDPポートを利用する</li>
<li>TURN over TCP &#8230; 特定のTCPポートを利用する</li>
</ul>

<p>企業で使う場合は、後者を利用したいケースが多いと思います。今回は両方使えるように設定しましょう。</p>

<h3>tunserver.conf の設定</h3>

<p>まず、/usr/local/etc/turnserver.conf.default を /usr/local/etc/turnserver.conf にコピーし、それを編集します。たくさんの設定項目がありますが、TURNをWebRTCで使う際のポイントを見ておきましょう。</p>

<p><strong>
ポートの指定 </strong>
</p><pre class="crayon-plain-tag"># TURN listener port for UDP and TCP (Default: 3478).
# Note: actually, TLS &amp; DTLS sessions can connect to the
# "plain" TCP &amp; UDP port(s), too - if allowed by configuration.
#
listening-port=80</pre><p> 
デフォルトでは3478/UDP を使いますが、それを変更して80/TCPを使うようにします。コメントアウトを外してポート番号80を指定してください。</p>

<p><strong>資格証明方法（クレデンシャル）</strong>
</p><pre class="crayon-plain-tag"># Uncomment to use long-term credential mechanism.
# By default no credentials mechanism is used (any user allowed).
#
lt-cred-mech

# 'Static' user accounts for long term credentials mechanism, only.
# This option cannot be used with TURN REST API.
# 'Static' user accounts are NOT dynamically checked by the turnserver process,
# so that they can NOT be changed while the turnserver is running.
#
#user=username1:key1
#user=username2:key2
# OR:
#user=username1:password1
#user=username2:password2

user=user:password</pre><p> 
WebRTCで使うには、lt-cred-mech を有効にする必要があります。デフォルトではコメントアウトされているので、これを有効にしてください。ユーザアカウントは外部のDBを使うのが適切ですが、今回はシンプルに設定ファイルに直接記入しました。ご利用のシステムに合わせて、外部ファイルや外部DBをご利用ください。</p>

<p>同時にrealmも指定します。自分のサーバーのドメインに合わせて、適宜指定してください。 
</p><pre class="crayon-plain-tag"># The default realm to be used for the users when no explicit
# origin/realm relationship was found in the database, or if the TURN
# server is not using any database (just the commands-line settings
# and the userdb file). Must be used with long-term credentials
# mechanism or with TURN REST API.
#
realm=coturn.yourdomain.com</pre><p></p>

<p><strong>通信プロトコルの指定（変更不要）</strong>
</p><pre class="crayon-plain-tag"># Uncomment if no UDP client listener is desired.
# By default UDP client listener is always started.
#
#no-udp

# Uncomment if no TCP client listener is desired.
# By default TCP client listener is always started.
#
#no-tcp

# 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>
プロトコルの指定で no-tcp のコメントアウトを外すとTURN over TCPを利用しなくなります。今回はTCPを使いますので、コメントアウトのままにしておきます。もしUDPを使いたくない場合は no-udp のコメントアウトを外してください。</p>

<p>以前試したときはブラウザでの実装の不十分でtlsやdtlsを利用できなかったのですが、今回試したところデフォルトのまま（有効のまま）で通信できました。もしうまく通信できない場合は、コメントアウトを消してtlsやdtlsを無効にして試してみてください。</p>

<h4>coturnを起動しよう</h4>

<p>それではcoturnを起動しましょう。今回は80ポートを利用しているため、環境によってはroot権限が必要となりますので、sudoを利用してください。</p>

<p></p><pre class="crayon-plain-tag">sudo /usr/local/bin/turnserver -o -v -c /usr/local/etc/turnserver.conf</pre><p></p>

<p>ここで指定しているコマンドライン引数は次の意味です。</p>

<ul>
<li>-o .. デーモンとして起動</li>
<li>-v .. verboseモード。ログを多く出す</li>
<li>-c .. 設定ファイルのパスを指定</li>
</ul>

<h3>クライアントのソースを修正しよう</h3>

<p>それでは準備したcoturnサーバーを使うように、に<a href="https://html5experts.jp/mganeko/20112/" target="_blank" data-wpel-link="internal">クライアント側のソース</a>を修正しましょう。STUNで修正した部分と同じ個所になります。 coturnサーバーが coturn.yourdomain.com で、ポート80で動いていると仮定します。その情報をPeerConnectionに教えてあげましょう。</p>

<p></p><pre class="crayon-plain-tag">function prepareNewConnection(id) {
  let pc_config = {"iceServers":[
    {"urls": "stun:coturn.yourdomain.com:80"},
    {"urls":"turn:coturn.yourdomain.com:80?transport=udp", "username":"user", "credential":"password"},
    {"urls":"turn:coturn.yourdomain.com:80?transport=tcp", "username":"user", "credential":"password"}
  ]};
  let peer = new RTCPeerConnection(pc_config);
  // ... 省略 ...
}</pre><p></p>

<p>また、企業内から外部のシグナリングサーバーを利用するには、そちらも80/TCPや443/TCPを使う必要があるかもしれません。環境に応じてご準備ください。</p>

<p>ここまでできたら、無事Firewallを越えて通信ができるはずです。企業内のブラウザと、外のブラウザ（自宅のPCやアンドロイドのブラウザ）で試してみてください。</p>

<h2>最後に</h2>

<p>WebRTC入門も2016年中になんとか予定していた記事を書き終えることができました。書いている間にもどんどんブラウザがバージョンアップし、新しいAPIが使えるようになっています。最新情報はブラウザのリリースノートやブログをご覧いただくとよいと思います。</p>

<ul>
<li>WebRTC.org <a href="https://webrtc.org/release-notes/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">Chrome Report Notes</a></li>
<li>Mozilla blog <a href="https://blog.mozilla.org/webrtc/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">Advancing WebRTC</a></li>
</ul>

<p>みなさんもWebRTCを活用した素敵なアプリケーションを作ってくださいね。ここまでお付き合いいただき、どうもありがとうございました！</p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTC入門2016]]></series:name>
	</item>
		<item>
		<title>Firebaseで楽々シグナリング──WebRTC入門2016番外編</title>
		<link>/mganeko/20273/</link>
		<pubDate>Wed, 31 Aug 2016 00:00:42 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[最新動向]]></category>
		<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[Firebase]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=20273</guid>
		<description><![CDATA[連載： WebRTC入門2016 (5)こんにちは！ 2014年に連載した「WebRTCを使ってみよう！」シリーズのアップデートとしてお送りしているこの連載ですが、今回はもとの連載にはなかった内容を番外編としてお届けしま...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc2016/" class="series-380" title="WebRTC入門2016" data-wpel-link="internal">WebRTC入門2016</a> (5)</div><p>こんにちは！ 2014年に連載した<a href="https://html5experts.jp/series/webrtc-beginner/" target="_blank" data-wpel-link="internal">「WebRTCを使ってみよう！」</a>シリーズのアップデートとしてお送りしている<a href="https://html5experts.jp/series/webrtc2016/" target="_blank" data-wpel-link="internal">この連載</a>ですが、今回はもとの連載にはなかった内容を番外編としてお届けします。</p>

<h2>httpsのハードル</h2>

<p><a href="https://html5experts.jp/mganeko/20112/" target="_blank" data-wpel-link="internal">前回は複数人、複数会議室</a>で利用できるようにして、実用的なアプリを作る準備ができました。ところが実際に使おうとすると、Chromeのセキュリティポリシーと向き合わなくてはなりません。</p>

<p>「getUserMedia()やService Workerなどの強力なAPIは、セキュアな環境でなくては利用できない」というポリシーは今のWebの状況に合わせたものだと思います。では、その環境をどうやって用意すればよいのでしょうか？</p>

<p>もちろん証明書を取得して、きちんとサーバーを立てるのがまっとうなやり方です。最近はLet&#8217;s Encryptなど無料で証明書を発行するサービスもあります（参考：<a href="https://webrtchacks.com/lets-encrypt/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">&#8220;Let’s Encrypt – how get to free SSL for WebRTC&#8221;</a>）。とはいえ、試験的な利用でそこまで準備するのは大変というのも、正直なところです。そこで今回は、比較的とっつきやすい方法をご紹介します。</p>

<h3>Webサーバー</h3>

<p>HTMLやJavaScriptなどの静的コンテンツを配置できる手段はいくつかあります。エンジニアの皆さんであれば、次の2つをすでに利用されている方々も多いのではないでしょうか？</p>

<ul>
<li>GitHub Pages</li>
<li>Google App Engine</li>
</ul>

<p>どちらも http / https の両方でアクセスできますし、独自ドメインで利用することも可能です。（※利用方法についてはWebに多くの情報がありますので、そちらをご参照ください）</p>

<p>また、シグナリングで利用するFirebaseにも静的コンテンツのホスティング機能があります。</p>

<h3>シグナリングサーバー</h3>

<p>シグナリングにはsocket.ioなど、WebSocketを活用した仕組みが使われる例が多いようです。もちろん他の方法（例えば手動シグナリング）でもいいのですが、サーバーとクライアント（ブラウザ）で双方向に通信するにはWebSocketが適切なのでしょう。</p>

<p>https://～から取得されたHTML/JavaScriptからWebSocketサーバーに接続する場合には、そちらもセキュアでなくてはなりません。(ws://～ではなく、wss://～）。WebSocketを利用したシグナリングサーバーを自分で用意する場合、そちらでも証明書が必要になります。</p>

<p>そこで今回はリアルタイムメッセージングに利用できるBaaSである<a href="https://www.firebase.com/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">Firebase</a>を使って、シグナリングの仕組みを構築したいと思います。</p>

<h2>Firebaseでシグナリングを実現するには</h2>

<p>まずはFirebaseにサインアップし、必要なキーやURLを入手しましょう。</p>

<ul>
<li>参考：<a href="https://html5experts.jp/technohippy/18040/" target="_blank" data-wpel-link="internal">Firebaseで作る簡単リアルタイムウェブアプリケーション </a></li>
</ul>

<p>（※手順については今回は省略させていただきます。あしらかず）</p>

<p>ブラウザでFirebaseの機能を利用するために、必要なライブラリを読み込んでおきます。今回はデータベースを利用するので、必要なjsファイルは次の通りとなります。</p>

<p></p><pre class="crayon-plain-tag">&lt;script src="https://www.gstatic.com/firebasejs/3.1.0/firebase-app.js"&gt;&lt;/script&gt;
 &lt;script src="https://www.gstatic.com/firebasejs/3.1.0/firebase-database.js"&gt;&lt;/script&gt;</pre><p></p>

<p>それから取得しておいたキーとURLを用いてFirebaseに接続します。
</p><pre class="crayon-plain-tag">// Initialize Firebase
  let config = {
    apiKey: "yourAPIKey",  // &lt;-- please set your API key
    databaseURL: "https://yourapp.firebaseio.com/",  // &lt;-- please set your database URL
  };
  firebase.initializeApp(config);
  let database = firebase.database();</pre><p></p>

<h3>シグナリングで送りたいモノ</h3>

<p>前回の記事にもあるように、シグナリングでは2つの通信ケースがあります。</p>

<ul>
<li>ルーム内の他のメンバー全員（接続している他のクライアントすべて）に送る</li>
<li>特定のメンバー（特定のクライアント）だけに送る</li>
</ul>

<p>また後者のためには、特定のメンバーを識別するための何らかのIDが必要となります。</p>

<ul>
<li>クライアント側で、重ならない/重なりにくい ようにIDを決める（UUID、タイムスタンプ、乱数など）</li>
<li>サーバー側でIDを振り出す</li>
</ul>

<p>Firebaseを使うとデータベースに格納されるオブジェクトのすべてにIDが振られるので、今回はそれを利用します。</p>

<h3>データベースの構造</h3>

<p>Firebaseはデータベースによる階層構造をもったデータの永続化と、その追加/変更/削除のイベント通知が行えます。今回は次のようなアプリ/ルーム/メンバーの階層構造としました。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/08/firebase_structure_11.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/08/firebase_structure_11-300x218.png" alt="firebase_structure_1" width="300" height="218" class="alignnone size-medium wp-image-20292" srcset="/wp-content/uploads/2016/08/firebase_structure_11-300x218.png 300w, /wp-content/uploads/2016/08/firebase_structure_11.png 640w, /wp-content/uploads/2016/08/firebase_structure_11-207x151.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>例えばルーム&#8221;test&#8221;にメンバー&#8221;bbb&#8221;が参加する場合、次の2カ所のイベントを待ち受けています。</p>

<ul>
<li>multi/room_test/&#95;broadcast&#95; の child_added イベント</li>
<li>multi/room_test/&#95;direct&#95;/member_bbb の child_added イベント</li>
</ul>

<p>また、multi/room_test/&#95;join&#95; はメッセージのやりとり以外の用途で使います。</p>

<h3>メンバーのIDの決定</h3>

<p>メンバーを特定してメッセージを送るには、メンバーを識別するIDが必要です。先ほどは仮に&#8221;bbb&#8221;としましたが、実際には衝突を避けるためにFirebase側で振り出されるキーを利用することにしました。</p>

<p></p><pre class="crayon-plain-tag">let databaseRoot = 'myapp/multi/';
    let key = database.ref(databaseRoot + room + '/_join_').push({ joined : 'unknown'}).key
    clientId = 'member_' + key;
    database.ref(databaseRoot + room + '/_join_/' + key).update({ joined : clientId});</pre><p></p>

<p>データベースにパスを指定してpush()すると、子要素が追加されて、そのキーが返っています。その値を使って先ほどの子要素の内容を更新しています。</p>

<p>ここまで終わった時のDatabaseの内容をFirebaseのコンソールで見てみると、次のようになっています。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/08/firebase_join.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/08/firebase_join-300x182.png" alt="firebase_join" width="300" height="182" class="alignnone size-medium wp-image-20299" srcset="/wp-content/uploads/2016/08/firebase_join-300x182.png 300w, /wp-content/uploads/2016/08/firebase_join.png 640w, /wp-content/uploads/2016/08/firebase_join-207x125.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h3>ルーム内へのブロードキャスト</h3>

<p>シグナリングの流れは<a href="https://html5experts.jp/mganeko/20112/" target="_blank" data-wpel-link="internal">前回と同じ</a>です。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/07/multi_callme_simple.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/multi_callme_simple-300x225.png" alt="multi_callme_simple" width="300" height="225" class="alignnone size-medium wp-image-20140" srcset="/wp-content/uploads/2016/07/multi_callme_simple-300x225.png 300w, /wp-content/uploads/2016/07/multi_callme_simple.png 640w, /wp-content/uploads/2016/07/multi_callme_simple-207x155.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<ul>
<li>新たに通信を開始したい人（member_xxxx）が、通信開始の合図のルーム内にブロードキャスする(“call me”）</li>
</ul>

<p>&#8220;test&#8221;ルームにブロードキャストする場合には、 multi/room_test/&#95;broadcast&#95; の下を使います。</p>

<p></p><pre class="crayon-plain-tag">let roomBroadcastRef = database.ref(databaseRoot + room + '/_broadcast_');

  // 通信開始の合図
  function callMe() {
    emitRoom({type: 'call me'});
  }

  function emitRoom(msg) {
    msg.from = clientId; // メッセージに送信元（自分のID)をセット
    roomBroadcastRef.push(msg);
  }</pre><p></p>

<p>各メンバーはこのブロードキャストのイベントを待ち受けていて、typeに応じて処理を行います。</p>

<p></p><pre class="crayon-plain-tag">roomBroadcastRef.on('child_added', function(data) {
      let message = data.val();
      let fromId = message.from;
      if (fromId === clientId) {
        // ignore self message (自分自身からのメッセージは無視する）
        return;
      }
      
      if (message.type === 'call me') {
        // 接続処理
      }
    });</pre><p></p>

<p>socket.ioでは自分自身からメッセージは飛んで来ませんが、Firebaseでは自分でpush()してもイベントが飛んでくるので、それは無視しています。</p>

<h3>特定のメンバーへのメッセージ</h3>

<p>通信開始の合図である&#8221;call me&#8221;以外は、特定のメンバー宛のメッセージのやり取りになります。例えば member_bbb 宛のメッセージは、この下にpush()します。</p>

<ul>
<li>multi/room_test/<em>direct</em>/member_bbb</li>
</ul>

<p></p><pre class="crayon-plain-tag">function emitTo(id, msg) {
    msg.from = clientId; // メッセージに送信元（自分のID)をセット
    database.ref(databaseRoot + room + '/_direct_/' + id).push(msg);
  }</pre><p></p>

<p>Offer/Answerの交換やICE Candidateのやり取りは、すべてこちらのメンバー宛のメッセージになります。</p>

<h3>Firebaseのルール設定</h3>

<p>Firebaseのデータベースにはアクセスのルール指定があります。デフィルトではread/writeにAuth必要なルールが生成されていますが、今回のサンプルでは特定のパス以下はAuth不要で読み書きできるよう、ルールを追加しました。</p>

<p></p><pre class="crayon-plain-tag">{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null",
    "myapp" : {
      "multi" : {
        ".read": true,
        ".write": true
      }
    }
  }
}</pre><p></p>

<h2>NAT越えの設定 STUNの設定</h2>

<p>せっかくFirebaseでメッセージをやり取りできるので、NATを超えてWebRTCで通信できるようにしましょう。</p>

<p>NATを超えて通信を行うには、ローカルネットワークでのIPアドレスではなく、グローバルIPを伝える必要があります。今回は詳しく説明しませんが、そのための仕組みがSTUNです。GoogleがSTUNサーバーを公開しているので、それを使わせてもらいましょう。</p>

<p></p><pre class="crayon-plain-tag">function prepareNewConnection(id) {
    let pc_config = {"iceServers":[{"urls": "stun:stun.l.google.com:19302"}]}; // for STUN server
    let peer = new RTCPeerConnection(pc_config);
    // ... 省略 ...
  }</pre><p></p>

<p>※ <a href="https://html5experts.jp/mganeko/5554/" target="_blank" data-wpel-link="internal">2014年の記事</a>では&#8221;url&#8221; と指定していましたが、現在の仕様では&#8221;urls&#8221;と指定する必要があります。</p>

<p>このようにSTUNサーバーを指定することで、通信経路の候補(ICE Candidate)に、STUN経由で取得した情報も含まれるようになります。</p>

<h2>動かしてみよう</h2>

<p>今回のサンプルをGitHub Pagesで公開しています。Firebaseもしばらく(〜2016年9月末の予定）使える状態にしておきますので、皆さんもお試しください。</p>

<ul>
<li>GitHub Pages で試す <a href="https://mganeko.github.io/webrtcexpjp/basic2016/multi_firebase.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">multi_firebase.html</a> (Chrome/Firefox）</li>
<li>GitHub でソースを見る <a href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/multi_firebase.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">multi_firebase.html</a> </li>
</ul>

<p>※シグナリングの手段以外は、<a href="https://html5experts.jp/mganeko/20112/" target="_blank" data-wpel-link="internal">前回のソース</a>と同様の処理になっています。</p>

<h3>使い方</h3>

<ul>
<li>URLの後ろに ?<em>room</em> という形で、好きなルーム名を指定してください

<ul>
<li>https://mganeko.github.io/webrtcexpjp/basic2016/multi_firebase.html?<em>お好きなルーム名</em></li>
</ul></li>
<li>ルーム名を指定せずに multi_firebase.html を開くと、ランダムにルーム名を決定します</li>
<li>[Start Video]をクリックし、カメラの映像とマイクの音声を取得します</li>
<li>通信相手にも同じルーム名を指定してブラウザ(Chrome/Firebox)でアクセスしてもらいます

<ul>
<li>?<em>room</em> も含むURLを伝えてください</li>
<li>&#8220;Mail link of this room&#8221; をクリックすると、URLを送るためのメーラーが開きます。宛先を指定して送信してください</li>
</ul></li>
<li>通信相手にも[Start Video]をクリックし、カメラの映像とマイクの音声を取得してもらいます</li>
<li>自分、あるいは相手から[Connect]ボタンを押してください</li>
<li>Firebase経由で情報が交換され、P2P通信が始まります</li>
<li>このサンプルでは、同じルームに同時に4人まで(3人の相手と)通信することができます</li>
</ul>

<h3>トラブルシューティング</h3>

<ul>
<li>異なるPCで通信できない場合

<ul>
<li>→ ルーム名が一致しているか確認していください</li>
<li>ルーム名を指定せずにブラウザでアクセスすると、ランダムにルーム名が決定されます</li>
<li>異なるPCでは、それぞれ異なるルーム名がランダムに決定されます</li>
</ul></li>
<li>一度接続できたが、その後できなくなった場合

<ul>
<li>ルーム内の過去のブロードキャストメッセージが残っている可能性があります</li>
<li>ルーム名を変更して、再度接続してみてください</li>
</ul></li>
<li>（例えば）会社と自宅で通信できない場合

<ul>
<li>→ NATだけでなく、FirewallでUDP通信やポートが制限されている可能性があります</li>
<li>※Firewallを超えての通信については、次回の記事で取り上げる予定です</li>
</ul></li>
</ul>

<h3>Edge同士での通信</h3>

<p>webrtc.orgが提供している<a href="https://github.com/webrtc/adapter" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">adapter.js</a>を使うと、多くのブラウザの差異を吸収することができます。最新の<a href="https://webrtc.github.io/adapter/adapter-latest.js" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">https://webrtc.github.io/adapter/adapter-latest.js</a>を読み込めば、Microsoft EdgeのサポートしてるORTCを、WebRTCのオブジェクトのインターフェイスを通して利用することができます。</p>

<p>残念ながらサポートしているビデオコーデックの制限で、ChromeやFirefoxとのビデオ通信はできませんが、Edge同士であれば今回のFirebaseを使ったシグナリングでビデオ/オーディオのP2P通信を行うことができます。</p>

<ul>
<li>GitHub Pages で試す <a href="https://mganeko.github.io/webrtcexpjp/basic2016/multi_firebase_adapter.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">multi_firebase_adapter.html</a> (Chrome/Firefox）</li>
<li>GitHub でソースを見る <a href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/multi_firebase_adapter.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">multi_firebase_adapter.html</a> </li>
</ul>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/08/edge_firebase.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/08/edge_firebase-300x135.png" alt="edge_firebase" width="300" height="135" class="alignnone size-medium wp-image-20323" srcset="/wp-content/uploads/2016/08/edge_firebase-300x135.png 300w, /wp-content/uploads/2016/08/edge_firebase.png 640w, /wp-content/uploads/2016/08/edge_firebase-207x93.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h4>注意点</h4>

<ul>
<li>Edgeでは、同一のカメラを複数のウィンドウ/タブで利用することができません。同一PCで通信する場合は、複数のカメラを用意してください</li>
<li>Edgeでは現在のところSTUNはサポートされていない(TURNのみサポート)ため、今回のサンプルでNATを越えた通信はできません</li>
</ul>

<h2>次回は</h2>

<p>今回はFirebaseを使ってシグナリングを実現しました。次回はNAT/Firewallを超えてのWebRTC通信についてお届けする予定です。</p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTC入門2016]]></series:name>
	</item>
		<item>
		<title>シグナリングを拡張して、複数人で通信してみよう ーWebRTC入門2016</title>
		<link>/mganeko/20112/</link>
		<pubDate>Mon, 01 Aug 2016 00:00:18 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[最新動向]]></category>
		<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=20112</guid>
		<description><![CDATA[連載： WebRTC入門2016 (4)こんにちは！ 2014年に連載した「WebRTCを使ってみよう！」シリーズのアップデート記事も4回目となり、佳境に入りました。前回の1対1の通信をベースに、今回はより実用的なビデオ...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc2016/" class="series-380" title="WebRTC入門2016" data-wpel-link="internal">WebRTC入門2016</a> (4)</div><p>こんにちは！ 2014年に連載した<a href="https://html5experts.jp/series/webrtc-beginner/" target="_blank" data-wpel-link="internal">「WebRTCを使ってみよう！」シリーズ</a>のアップデート記事も4回目となり、佳境に入りました。<a href="https://html5experts.jp/mganeko/20013/" target="_blank" data-wpel-link="internal">前回の1対1の通信</a>をベースに、今回はより実用的なビデオチャットを目指して複数人で通信可能なように拡張してみましょう。</p>

<h2>複数人、複数会議室を目指して</h2>

<p>前回作ったのは、1つのシグナリングサーバーに対して、同時に1ペアだけが利用できる仕組みでした。これを複数人で、複数会議室で利用できるようにしていきましょう。こちらの図の左のBeforeの状態から、右のAfterの状態を実現します。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/07/rtc_11_to_nn.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/rtc_11_to_nn-300x176.png" alt="rtc_11_to_nn" width="300" height="176" class="alignnone size-medium wp-image-20119" srcset="/wp-content/uploads/2016/07/rtc_11_to_nn-300x176.png 300w, /wp-content/uploads/2016/07/rtc_11_to_nn.png 640w, /wp-content/uploads/2016/07/rtc_11_to_nn-207x122.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h2>複数人で通信するためには</h2>

<p>WebRTCはPeer-to-Peer (P2P) で通信する仕組みです。なので複数人と通信するためには、複数の<code>RTCPeerConnection</code>を用意する必要があります。図にするとこんな感じです。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/07/PeerConnection_multi.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/PeerConnection_multi-300x205.png" alt="PeerConnection_multi" width="300" height="205" class="alignnone size-medium wp-image-20113" srcset="/wp-content/uploads/2016/07/PeerConnection_multi-300x205.png 300w, /wp-content/uploads/2016/07/PeerConnection_multi.png 640w, /wp-content/uploads/2016/07/PeerConnection_multi-207x142.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>また、P2P通信を開始するためにSDP(Offer/Answer)を交換する必要がありますが、それぞれの相手ごとに生成、交換しなければなりません。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/07/PeerConnection_sdp_multi.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/PeerConnection_sdp_multi-300x230.png" alt="PeerConnection_sdp_multi" width="300" height="230" class="alignnone size-medium wp-image-20114" srcset="/wp-content/uploads/2016/07/PeerConnection_sdp_multi-300x230.png 300w, /wp-content/uploads/2016/07/PeerConnection_sdp_multi.png 640w, /wp-content/uploads/2016/07/PeerConnection_sdp_multi-207x159.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>P2Pなので当然かもしれませんが、私は最初に複数の相手と通信しようとして混乱してしまいました。この考え方を踏まえていれば、あとは力技になります。</p>

<h2>シグナリングサーバーの対応</h2>

<p>シグナリングサーバーは前回の1対1の時と同じく、Node.jsを使って用意しましょう。複数会議室を実現するのに便利なため、今回はwsモジュールではなく、より高機能のsocket.ioを使います。Node.jsのインストールは終わっていると思うので、コマンドプロンプト/ターミナルから、次のコマンドを実行してください。 ※必要に応じて、sudoなどをご利用ください。</p>

<p></p><pre class="crayon-plain-tag">npm install socket.io</pre><p></p>

<p>2014年にはsocket.ioはv0.9でしが、今回はv1.4.xになっています。</p>

<p>シグナリングサーバーのコードは前回の1対1と同様にメッセージを中継するのが役目ですが、接続してきたクライアントがルーム（会議室）に入室要求を送ってきたら、socket.ioのサーバ側でそのルームに <code>join()</code> してあげます。</p>

<p></p><pre class="crayon-plain-tag">// ---- multi room ----
    socket.on('enter', function(roomname) {
      socket.join(roomname);
      console.log('id=' + socket.id + ' enter room=' + roomname);
      setRoomname(roomname);
    });

    function setRoomname(room) {
      socket.roomname = room;
    }</pre><p></p>

<p>クライアントからのメッセージ送信には、次の2つのパターンがあります。</p>

<ul>
<li>ルーム内の他のメンバー全員（接続している他のクライアントすべて）に送る</li>
<li>特定のメンバー（特定のクライアント）だけに送る</li>
</ul>

<p>シグナリングサーバーでは、送信先が指定されていればその相手だけに、指定されていなければルーム内の全員（送信者以外）にメッセージを送ります。
※その際に、送信元を特定できるID（socket.ioが管理しているID)を追加しています。</p>

<p></p><pre class="crayon-plain-tag">socket.on('message', function(message) {
        message.from = socket.id; // 送信元のIDをメッセージに追加

        // get send target
        var target = message.sendto;
        if (target) { // 特定の相手に送る場合
          socket.to(target).emit('message', message);　
          return;
        }

        // broadcast in room
        emitMessage('message', message);
    });

    // ルーム内の全員に送る場合
    function emitMessage(type, message) {
      // ----- multi room ----
      var roomname = getRoomname();

      if (roomname) {
        // ルーム内に送る
        socket.broadcast.to(roomname).emit(type, message);
      }
      else {
        // ルーム未入室の場合は、全体に送る
        socket.broadcast.emit(type, message);
      }
    }</pre><p></p>

<p>サーバーの全体のソースは次の通りです。これを例えば <code>signaling_room.js</code> というファイル名で保存します。</p>

<p></p><pre class="crayon-plain-tag">"use strict";

var srv = require('http').Server();
var io = require('socket.io')(srv);
var port = 3002;
srv.listen(port);
console.log('signaling server started on port:' + port);

// This callback function is called every time a socket
// tries to connect to the server
io.on('connection', function(socket) {
    // ---- multi room ----
    socket.on('enter', function(roomname) {
      socket.join(roomname);
      console.log('id=' + socket.id + ' enter room=' + roomname);
      setRoomname(roomname);
    });

    function setRoomname(room) {
      socket.roomname = room;
    }

    function getRoomname() {
      var room = socket.roomname;
      return room;
    }

    function emitMessage(type, message) {
      // ----- multi room ----
      var roomname = getRoomname();

      if (roomname) {
        console.log('===== message broadcast to room --&gt;' + roomname);
        socket.broadcast.to(roomname).emit(type, message);
      }
      else {
        console.log('===== message broadcast all');
        socket.broadcast.emit(type, message);
      }
    }

    // When a user send a SDP message
    // broadcast to all users in the room
    socket.on('message', function(message) {
        var date = new Date();
        message.from = socket.id;
        console.log(date + 'id=' + socket.id + ' Received Message: ' + JSON.stringify(message));

        // get send target
        var target = message.sendto;
        if (target) {
          console.log('===== message emit to --&gt;' + target);
          socket.to(target).emit('message', message);
          return;
        }

        // broadcast in room
        emitMessage('message', message);
    });

    // When the user hangs up
    // broadcast bye signal to all users in the room
    socket.on('disconnect', function() {
        // close user connection
        console.log((new Date()) + ' Peer disconnected. id=' + socket.id);

        // --- emit ----
        emitMessage('user disconnected', {id: socket.id});

        // --- leave room --
        var roomname = getRoomname();
        if (roomname) {
          socket.leave(roomname);
        }
    });

});</pre><p></p>

<p>コマンドプロンプト/ターミナルから、 次のように起動してください。（ファイル名は適宜置き換えてくださいね）</p>

<p></p><pre class="crayon-plain-tag">node signaling_room.js</pre><p></p>

<h2>クライアント側の拡張</h2>

<p>次はクライアントとなるブラウザ側の処理を拡張していきます。</p>

<h3>socket.io サーバーへの接続</h3>

<p>今回はシグナリングサーバーを同じPCの3002番ポートで動かしていると想定します。HTMLファイルの先頭で、socket.ioのクライアント用のjsファイルを読み込みます。</p>

<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;multi party&lt;/title&gt;
 &lt;script src="http://localhost:3002/socket.io/socket.io.js"&gt;&lt;/script&gt;
&lt;/head&gt;
... 省略 ...
&lt;/htm&gt;</pre><p></p>

<p>クライアントではsocket.ioのサーバー (localhost:3002) に接続します。このとき、<code>ws://～</code> ではなく、<code>http://～/</code> となることがWebSocketを直接利用した場合と異なります。</p>

<p></p><pre class="crayon-plain-tag">// ----- use socket.io ---
  let port = 3002;
  let socket = io.connect('http://localhost:/' + port + '/');
  socket.on('connect', function(evt) {
    // 接続したときの処理
  });</pre><p></p>

<p>※実際のシグナリングサーバーの環境に合わせて、URLやポート番号は変更してください。</p>

<h3>ルーム（会議室）への入室</h3>

<p>クライアンではsocket.ioのサーバーに接続したら、希望のルームに入室を依頼します。ルーム名は今回はURLの後ろに　<code>?部屋名</code> という形で指定することにしてみました。（お好きな方法で指定してください）</p>

<p></p><pre class="crayon-plain-tag">let room = getRoomName();
  socket.on('connect', function(evt) {
    console.log('socket.io connected. enter room=' + room );
    socket.emit('enter', room);
  });

  // -- room名を取得 --
  function getRoomName() { // たとえば、 URLに  ?roomname  とする
    let url = document.location.href;
    let args = url.split('?');
    if (args.length &gt; 1) {
      let room = args[1];
      if (room != '') {
        return room;
      }
    }
    return '_testroom';
  }</pre><p></p>

<h3>複数通信の流れ</h3>

<p>1対1の時は相手が1人しかいない前提だったので、ただちにOffer SDP / Answer SDPを送信していました。今回は相手が何人いるか分からない状況からスタートしますし、相手ごとに個々にOffer SDP / Answer SDPを送受信する必要があります。そこで、相手を確認するやりとりを追加しました。図にすると、次のような流れになります。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/07/multi_callme_simple.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/multi_callme_simple-300x225.png" alt="multi_callme_simple" width="300" height="225" class="alignnone size-medium wp-image-20140" srcset="/wp-content/uploads/2016/07/multi_callme_simple-300x225.png 300w, /wp-content/uploads/2016/07/multi_callme_simple.png 640w, /wp-content/uploads/2016/07/multi_callme_simple-207x155.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>もう少し細かく見ると、次のような処理を行っています。</p>

<ul>
<li>新たに通信を開始したい人（ブラウザA）が、通信開始の合図のルーム内にブロードキャストする(&#8220;call me&#8221;）</li>
<li>受け取った人（ブラウザB、ブラウザC）は、Offer SDPを生成して、ブラウザAを宛先に指定して送ります

<ul>
<li><code>RTCPeerConnection</code>を生成</li>
<li><code>RTCPeerConnection.createOffer()</code>でOffer SDPを生成</li>
<li><code>setLocalDescription()</code>で覚える</li>
<li>ブラウザA宛てに送信</li>
</ul></li>
<li>ブラウザAは、Offer SDPを受け取り、Answer SDPを生成してそれぞれの相手に送り返します

<ul>
<li>相手ごとに<code>RTCPeerConnection</code>を生成</li>
<li>それぞれ受け取ったOffer SDPを<code>RTCPeerConnection.setRemoteDescription()</code>で覚える</li>
<li><code>RTCPeerConnection.createAnswer()</code>でAnswer SDPを生成</li>
<li><code>setLocalDescription()</code>で覚える</li>
<li>それぞれの相手にAnswer SDPを返信</li>
</ul></li>
<li>ブラウザB、ブラウザCは、Answer SDPを受け取る

<ul>
<li>受け取ったAnswer SDPを<code>RTCPeerConnection.setRemoteDescription()</code>で覚える</li>
</ul></li>
</ul>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/07/multi_callme.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/multi_callme-300x225.png" alt="multi_callme" width="300" height="225" class="alignnone size-medium wp-image-20136" srcset="/wp-content/uploads/2016/07/multi_callme-300x225.png 300w, /wp-content/uploads/2016/07/multi_callme.png 640w, /wp-content/uploads/2016/07/multi_callme-207x156.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h3>ソースの修正：複数通信の準備</h3>

<p>それでは、ブラウザ側のソースも手を入れていきましょう。まず、複数の<code>RTCPeerConnection</code>を扱えるように用意します。</p>

<p></p><pre class="crayon-plain-tag">// ---- for multi party -----
  let peerConnections = [];
  const MAX_CONNECTION_COUNT = 3;

  // --- RTCPeerConnections ---
  function getConnectionCount() {
    return peerConnections.length;
  }

  function canConnectMore() {
    return (getConnectionCount() &lt; MAX_CONNECTION_COUNT);
  }

  function isConnectedWith(id) {
    if (peerConnections[id])  {
      return true;
    }
    else {
      return false;
    }
  }

  function addConnection(id, peer) {
    peerConnections[id] = peer;
  }

  function getConnection(id) {
    let peer = peerConnections[id];
    return peer;
  }

  function deleteConnection(id) {
    delete peerConnections[id];
  }</pre><p></p>

<p>相手の映像を表示する<code>video</code>タグも、動的に生成して複数管理できるようにします。</p>

<p></p><pre class="crayon-plain-tag">let remoteVideos = [];
  let container = document.getElementById('container');

  // --- video elements ---
  function addRemoteVideoElement(id) {
    let video = createVideoElement('remote_video_' + id);
    remoteVideos[id] = video;
    return video;
  }

  function getRemoteVideoElement(id) {
    let video = remoteVideos[id];
    return video;
  }

  function deleteRemoteVideoElement(id) {
    removeVideoElement('remote_video_' + id);
    delete remoteVideos[id];
  }

  function createVideoElement(elementId) {
    let video = document.createElement('video');
    video.width = '160';
    video.height = '120';
    video.id = elementId;

    video.style.border = 'solid black 1px';
    video.style.margin = '2px';

    container.appendChild(video);

    return video;
  }

  function removeVideoElement(elementId) {
    let video = document.getElementById(elementId);
    container.removeChild(video);
    return video;
  }</pre><p></p>

<p>さらに<code>RTCPeerConnection</code>の接続や、相手からのメディアストリーム、<code>video</code>タグを連動して扱う処理も追加しておきましょう。</p>

<p></p><pre class="crayon-plain-tag">// --- video elements ---
  function attachVideo(id, stream) {
    let video = addRemoteVideoElement(id);
    playVideo(video, stream);
    video.volume = 1.0;
  }

  function detachVideo(id) {
    let video = getRemoteVideoElement(id);
    pauseVideo(video);
    deleteRemoteVideoElement(id);
  }
  
  function isRemoteVideoAttached(id) {
    if (remoteVideos[id]) {
      return true;
    }
    else {
      return false;
    }
  }

  // --- RTCPeerConnections ---
  function stopConnection(id) {
    detachVideo(id);

    if (isConnectedWith(id)) {
      let peer = getConnection(id);
      peer.close();
      deleteConnection(id);
    }
  }

  function stopAllConnection() {
    for (let id in peerConnections) {
      stopConnection(id);
    }
  }</pre><p></p>

<h3>ソースの修正：シグナリングの変更</h3>

<p>WebSocket直接利用から、socket.ioの利用に変わったので、シグナリングも変更します。</p>

<p></p><pre class="crayon-plain-tag">// ----- use socket.io ---
  let port = 3002;
  let socket = io.connect('http://localhost:/' + port + '/');
  let room = getRoomName();
  socket.on('connect', function(evt) {
    socket.emit('enter', room);
  });
  socket.on('message', function(message) {
    let fromId = message.from;

    if (message.type === 'offer') {
      // -- got offer ---
      let offer = new RTCSessionDescription(message);
      setOffer(fromId, offer);
    }
    else if (message.type === 'answer') {
      // --- got answer ---
      let answer = new RTCSessionDescription(message);
      setAnswer(fromId, answer);
    }
    else if (message.type === 'candidate') {
      // --- got ICE candidate ---
      let candidate = new RTCIceCandidate(message.ice);
      addIceCandidate(fromId, candidate);
    }
    else if (message.type === 'call me') {
      if (! isReadyToConnect()) {
        console.log('Not ready to connect, so ignore');
        return;
      }
      else if (! canConnectMore()) {
        console.warn('TOO MANY connections, so ignore');
      }

      if (isConnectedWith(fromId)) {
        // already connnected, so skip
        console.log('already connected, so ignore');
      }
      else {
        // connect new party
        makeOffer(fromId);
      }
    }
    else if (message.type === 'bye') {
      if (isConnectedWith(fromId)) {
        stopConnection(fromId);
      }
    }
  });
  socket.on('user disconnected', function(evt) {
    let id = evt.id;
    if (isConnectedWith(id)) {
      stopConnection(id);
    }
  });

  // --- broadcast message to all members in room
  function emitRoom(msg) {
    socket.emit('message', msg);
  }

  function emitTo(id, msg) {
    msg.sendto = id;
    socket.emit('message', msg);
  }</pre><p></p>

<p>通信開始要求の&#8221;call me&#8221;や、切断要求の&#8221;bye&#8221;の処理を追加しています。また接続準備が整っていない場合（カメラやマイクを取得していない場合）や、すでに接続中の相手からの接続要求は無視するようにしています。</p>

<h3>SDPやICE candidateのハンドリング</h3>

<p>複数の相手とOffer/Answer SDPやICE candidateをやり取りするので、相手を意識した処理に拡張します。といっても違いは対応する<code>RTCPeerConnection</code>のオブジェクトを生成したり取り出したりするところだけで、他は1対1の場合と同様です。すべてを掲載すると長いので全体は<a href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/multi.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">GitHubを参照</a>してくただくとして、ここでは例を取り上げます。</p>

<h4>Offer SDPの送信</h4>

<p>新たに <code>RTCPeerConnection</code>を生成し、Offer SDPの作成、送信を行います。
</p><pre class="crayon-plain-tag">function makeOffer(id) {
    peerConnection = prepareNewConnection(id);
    addConnection(id, peerConnection);

    peerConnection.createOffer()
    .then(function (sessionDescription) {
      console.log('createOffer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICEの場合は、初期SDPを相手に送る -- 
      sendSdp(id, peerConnection.localDescription);

      // -- Vanilla ICEの場合には、まだSDPは送らない --
    }).catch(function(err) {
      console.error(err);
    });
  }

  function sendSdp(id, sessionDescription) {
    let message = { type: sessionDescription.type, sdp: sessionDescription.sdp };
    emitTo(id, message);
  }</pre><p></p>

<h4>Answer SDPを受け取った場合の処理</h4>

<p>対応する<code>RTCPeerConnection</code>を取り出し、Answer SDPを覚えさせます。
</p><pre class="crayon-plain-tag">function setAnswer(id, sessionDescription) {
    let peerConnection = getConnection(id);
    if (! peerConnection) {
      console.error('peerConnection NOT exist!');
      return;
    }

    peerConnection.setRemoteDescription(sessionDescription)
    .then(function() {
      console.log('setRemoteDescription(answer) succsess in promise');
    }).catch(function(err) {
      console.error('setRemoteDescription(answer) ERROR: ', err);
    });
  }</pre><p></p>

<p>ICE candidateのやりとりなども、同様に対応する<code>RTCPeerConnection</code>のオブジェクトを取り出して行います。また、手動シグナリングで使っていたSDPをテキストエリアに表示する部分も取り除きました。</p>

<h3>カメラ、マイクの取得</h3>

<p>前回までは映像だけでしたが、今回はマイクの音声も取得してより実用的にしましょう。ただし1台のPCでやる場合はハウリングしてしまうので、ヘッドフォンを使ってください。</p>

<p></p><pre class="crayon-plain-tag">// start local video
  function startVideo() {
    getDeviceStream({video: true, audio: true})
    .then(function (stream) { // success
      localStream = stream;
      playVideo(localVideo, stream);
    }).catch(function (error) { // error
      console.error('getUserMedia error:', error);
      return;
    });
  }</pre><p></p>

<p><code>getDeviceStream()</code>は、新旧<code>getUserMedia()</code>をラップするために用意した関数です。これに<code>audio:true</code>の指定を渡してマイクも取得しています。</p>

<p>今回マイクの音声も取得するようにしたため、メディアストリームにはビデオとオーディオの2つのトラックが含まれます。このため、相手側の<code>RTCPeerConnection</code>の<code>ontrack()</code>イベントが2回呼び出されますが、2回目は無視するように修正しました。（<code>RTCPeerConnection.ontrack()</code>は現在Firefoxのみがサポートしています）
</p><pre class="crayon-plain-tag">function prepareNewConnection(id) {
    let pc_config = {"iceServers":[]};
    let peer = new RTCPeerConnection(pc_config);

    // --- on get remote stream ---
    if ('ontrack' in peer) {
      peer.ontrack = function(event) {
        let stream = event.streams[0];
        if (isRemoteVideoAttached(id)) {
          console.log('stream already attached, so ignore'); // &lt;--- 同じ相手からの2回目以降のイベントは無視する
        }
        else {
          //playVideo(remoteVideo, stream);
          attachVideo(id, stream);
        }
      };
    }
    else {
      peer.onaddstream = function(event) {
        let stream = event.stream;
        console.log('-- peer.onaddstream() stream.id=' + stream.id);
        //playVideo(remoteVideo, stream);
        attachVideo(id, stream);
      };
    }

    // ... 省略 ....
  }</pre><p></p>

<h3>全体のソース</h3>

<p>主要な部分は以上の通りですが、他にも細かい修正があります。全体のソースはGitHubでご覧ください。</p>

<ul>
<li>クライアント用のソース &#8230;  <a href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/multi.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">GitHubで見る</a></li>
<li>シグナリングサーバーのソース &#8230; <a href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/server/signaling_room.js" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">GitHubで見る</a></li>
</ul>

<h2>接続してみよう</h2>

<p>今回のサンプルでは4人まで同時に通信できるようにしてみました。それぞれブラウザを立ち上げて[Start Video]→[Connect]の順にボタンをクリックし接続してください。このように複数の相手と接続できるはずです。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2016/07/multi_connected_4.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/multi_connected_4-300x144.png" alt="multi_connected_4" width="300" height="144" class="alignnone size-medium wp-image-20158" srcset="/wp-content/uploads/2016/07/multi_connected_4-300x144.png 300w, /wp-content/uploads/2016/07/multi_connected_4.png 640w, /wp-content/uploads/2016/07/multi_connected_4-207x100.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>※localhost以外に繋ぐときは、Chromeではカメラ/マイクの取得に失敗します。その場合はFirefoxをご利用ください。</p>

<h2>次回は</h2>

<p>今回のサンプルでは複数人で同時にビデオチャットができるようにしました。が、Chromeではhttp://～で/<code>getUserMedia()</code>が許可されていないため、他のPCと通信するのは厄介です。</p>

<p>Webサーバだけであれば、GitHub Pagesなどでhttps://～を利用することができますが、シグナリングサーバーのWebSocket通信も暗号化する必要がありまり、そちらは自分で証明書を取って対処しなけばなりません。/</p>

<p>そこで次回は番外編として、Firebaseを使った暗号化通信をシグナリングに利用した例をご紹介したいと思います。</p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTC入門2016]]></series:name>
	</item>
		<item>
		<title>シグナリングサーバーを動かそう ーWebRTC入門2016</title>
		<link>/mganeko/20013/</link>
		<pubDate>Thu, 14 Jul 2016 00:45:36 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[最新動向]]></category>
		<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[WebRTC]]></category>
		<category><![CDATA[WebSocket]]></category>

		<guid isPermaLink="false">/?p=20013</guid>
		<description><![CDATA[連載： WebRTC入門2016 (3)こんにちは！ 2014年に連載した「WebRTCを使ってみよう！」シリーズのアップデート記事も3回目となりました。今回は、前回の「手動」で行ったP2P通信の準備を、自動で行えるよう...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc2016/" class="series-380" title="WebRTC入門2016" data-wpel-link="internal">WebRTC入門2016</a> (3)</div><p>こんにちは！ 2014年に連載した<a href="https://html5experts.jp/series/webrtc-beginner/" target="_blank" data-wpel-link="internal">「WebRTCを使ってみよう！」シリーズ</a>のアップデート記事も3回目となりました。今回は、前回の「手動」で行ったP2P通信の準備を、自動で行えるようにしてみましょう。</p>

<h2>シグナリングサーバーを立てよう</h2>

<p><a href="https://html5experts.jp/mganeko/19814/" target="_blank" data-wpel-link="internal">前回は手動でコピー＆ペーストを行い</a>、WebRTCのP2P通信を始めるために次の情報を交換しました。</p>

<ul>
<li>SDP</li>
<li>ICE candidate</li>
</ul>

<p>今回はこれを仲介するサーバー（シグナリングサーバー）を動かしてみましょう。方法として次の2つをご用意しました。</p>

<ul>
<li>Node.jsを使ったシグナリングサーバー</li>
<li>Chromeアプリ</li>
</ul>

<h3>Node.jsを準備しよう</h3>

<p>まず、WebSocketを使ってシグナリングを行う方法をご紹介します。WebSocketの扱いやすさから、ここではNode.jsを使います。（もちろん他の言語を使っても同様にシグナリングサーバーを作ることができます）<a href="https://nodejs.org/en/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">こちらの公式サイト</a>から、プラットフォームに対応したNode.jsを入手してインストールしてください。今回私は 4.4.7 LTSを使いました。</p>

<p>Node.jsのインストールが完了したら、次はWebSocketサーバー用のモジュールをインストールします。コマンドプロンプト/ターミナルから、 次のコマンドを実行してください。 ※必要に応じて、sudoなどをご利用ください。
</p><pre class="crayon-plain-tag">npm install ws</pre><p> 
※<a href="https://html5experts.jp/mganeko/5349/" target="_blank" data-wpel-link="internal">以前の連載</a>ではsocket.ioを使いましたが、今回はよりプリミティブなwsを使っています。</p>

<h3>シグナリングサーバーを動かそう</h3>

<p>次のコードを好きなファイル名で保存してください。（例えば signaling.js)
</p><pre class="crayon-plain-tag">"use strict";

let WebSocketServer = require('ws').Server;
let port = 3001;
let wsServer = new WebSocketServer({ port: port });
console.log('websocket server start. port=' + port);

wsServer.on('connection', function(ws) {
  console.log('-- websocket connected --');
  ws.on('message', function(message) {
    wsServer.clients.forEach(function each(client) {
      if (isSame(ws, client)) {
        console.log('- skip sender -');
      }
      else {
        client.send(message);
      }
    });
  });
});

function isSame(ws1, ws2) {
  // -- compare object --
  return (ws1 === ws2);     
}</pre><p> 
ポート番号は必要に応じて変更してください。起動するにはコマンドプロンプト/ターミナルから、 次のコマンドを実行します。</p><pre class="crayon-plain-tag">node signaling.js</pre><p>
シグナリングサーバーの動作はシンプルで、クライアントからメッセージを受け取ったら他のクライアントに送信するだけです。</p>

<h3>Chromeアプリを使う場合は</h3>

<p>場合によってはNode.jsをインストールして動かすのは、ハードルが高くて難しいケースもあるかもしれません。そんな人のために、Chromeアプリで「<a href="https://chrome.google.com/webstore/detail/simple-message-server/bihajhgkmpfnmbmdnobjcdhagncbkmmp" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">simple message server</a>」というものを作ってみました。
<a href="https://html5experts.jp/wp-content/uploads/2016/07/simple_message_server_store.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/simple_message_server_store-300x183.png" alt="simple_message_server_store" width="300" height="183" class="alignnone size-medium wp-image-20028" srcset="/wp-content/uploads/2016/07/simple_message_server_store-300x183.png 300w, /wp-content/uploads/2016/07/simple_message_server_store.png 640w, /wp-content/uploads/2016/07/simple_message_server_store-207x126.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
Chromeを利用したアプリとしてインストールし、アプリタブから起動して利用します。デスクトップ用のChromeが動く環境（Windows, MaxOS X, Linux, ChromeOS）で動くはずです。</p>

<p>起動すると、 ws://localhost:3001/ でクライアントからの接続を待ち受けます。※実装があまいので時々不安定になります。その場合は[restart]ボタンを押してリセットし、ブラウザもリロードして接続しなおしてください。</p>

<h2>シグナリング処理を変更しよう</h2>

<p>それでは前回の手動シグナリングのコードを、少しずつ変更していきましょう。まずWebSocketで用意したシグナリングサーバーに接続します。JavaScriptに次の処理を追加してください。（URLは使っているポートに合わせて修正してください）
</p><pre class="crayon-plain-tag">let wsUrl = 'ws://localhost:3001/';
  let ws = new WebSocket(wsUrl);
  ws.onopen = function(evt) {
    console.log('ws open()');
  };
  ws.onerror = function(err) {
    console.error('ws onerror() ERR:', err);
  };</pre><p></p>

<p>次に、WebSocketでメッセージを受け取った場合の処理を追加します。
</p><pre class="crayon-plain-tag">ws.onmessage = function(evt) {
    console.log('ws onmessage() data:', evt.data);
    let message = JSON.parse(evt.data);
    if (message.type === 'offer') {
      // -- got offer ---
      console.log('Received offer ...');
      textToReceiveSdp.value = message.sdp;
      let offer = new RTCSessionDescription(message);
      setOffer(offer);
    }
    else if (message.type === 'answer') {
      // --- got answer ---
      console.log('Received answer ...');
      textToReceiveSdp.value = message.sdp;
      let answer = new RTCSessionDescription(message);
      setAnswer(answer);
    }
  };</pre><p> 
JSONテキストからオブジェクトを復元し、typeに応じて前回用意したsetOffer()/setAnswer()を呼び出し、RTCPeerConnectionに渡しています。</p>

<h3>SDPの送信</h3>

<p>Offer/AnswerのSDPの送信も、WebSocket経由で行います。前回要したsendSdp()を次のように変更します。
</p><pre class="crayon-plain-tag">function sendSdp(sessionDescription) {
    console.log('---sending sdp ---');

    textForSendSdp.value = sessionDescription.sdp;
    /*--- テキストエリアをハイライトするのを止める
    textForSendSdp.focus();
    textForSendSdp.select();
    ----*/

    // --- シグナリングサーバーに送る ---
    let message = JSON.stringify(sessionDescription);
    console.log('sending SDP=' + message);
    ws.send(message);
  }</pre><p> 
SDPをJSONテキストに変換してWebSocketでシグナリングサーバーに送信しています。</p>

<h2>実際に動かしてみよう</h2>

<p>シグナリングサーバーを起動して、ChromeかFirefoxのウィンドウを2つ開いて修正したHTMLを読み込んでください。ChromeとFirefoxの間で通信することもできます。</p>

<p>Webサーバーを立てるのが難しい場合は、GitHub Pages にもサンプルを公開しているので、そちらで試すこともできます。その場合でもシグナリングサーバーは自分で用意する必要があるのでご注意ください。</p>

<ul>
<li>GitHub Pages で試す <a href="http://mganeko.github.io/webrtcexpjp/basic2016/ws_signaling_1to1_vanilla.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">ws_signaling_1to1_vanilla.html</a>

<ul>
<li>※http://～/ なのでFirefoxのみ</li>
</ul></li>
<li>GitHub でソースを見る <a href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/ws_signaling_1to1_vanilla.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">ws_signaling_1to1_vanilla.html</a></li>
</ul>

<h4>(1) カメラの取得</h4>

<p>両方のウィンドウで[Start Video]ボタンをクリックします。カメラのアクセスを許可すると、それぞれリアルタイムの映像が表示されます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/07/ws_signaling_startvideo.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/ws_signaling_startvideo-300x161.png" alt="ws_signaling_startvideo" width="300" height="161" class="alignnone size-medium wp-image-20032" srcset="/wp-content/uploads/2016/07/ws_signaling_startvideo-300x161.png 300w, /wp-content/uploads/2016/07/ws_signaling_startvideo.png 640w, /wp-content/uploads/2016/07/ws_signaling_startvideo-207x111.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<h4>(2) 通信開始</h4>

<p>どちらかのウィンドウで[Connect]ボタンを押します。(3)SDP（ICE candidateを含む）が自動で交換され、(4)ビデオ通信が始まります。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/07/ws_signaling_connect.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/ws_signaling_connect-300x162.png" alt="ws_signaling_connect" width="300" height="162" class="alignnone size-medium wp-image-20033" srcset="/wp-content/uploads/2016/07/ws_signaling_connect-300x162.png 300w, /wp-content/uploads/2016/07/ws_signaling_connect.png 640w, /wp-content/uploads/2016/07/ws_signaling_connect-207x112.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>手動シグナリングに比べて操作がずっと簡単になりました。これなら実際に利用できそうですね。</p>

<h2>Trickle ICE を使ってみよう</h2>

<p>コピー＆ペーストを手動で行う必要がなくなったので、ICE candidateを発生するたびに交換するTrickle ICE を使ってみましょう。流れはこのような形になります。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/06/hand2016_trickle.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/hand2016_trickle-300x224.png" alt="hand2016_trickle" width="300" height="224" class="alignnone size-medium wp-image-19863" srcset="/wp-content/uploads/2016/06/hand2016_trickle-300x224.png 300w, /wp-content/uploads/2016/06/hand2016_trickle.png 640w, /wp-content/uploads/2016/06/hand2016_trickle-207x155.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
すべてのICE candidateが出そろう前にP2P通信が確立する（ことがある）メリットがあります。（※<a href="https://html5experts.jp/mganeko/5349/" target="_blank" data-wpel-link="internal">2014年の記事</a>では「すべてのICE candidateの交換が終わるとP2P通信が始まる」と書いていましたが、これは誤りです）</p>

<h3>SDPをすぐに送信する</h3>

<p>Offer SDP/Answer SDPを生成したら、すぐに相手に送るように変更します。
</p><pre class="crayon-plain-tag">function makeOffer() {
    peerConnection = prepareNewConnection();
    peerConnection.createOffer()
    .then(function (sessionDescription) {
      console.log('createOffer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      sendSdp(peerConnection.localDescription);　// &lt;--- ここを加える

      // -- Vanilla ICE の場合には、まだSDPは送らない --
    }).catch(function(err) {
      console.error(err);
    });
  }

  function makeAnswer() {
    console.log('sending Answer. Creating remote session description...' );
    if (! peerConnection) {
      console.error('peerConnection NOT exist!');
      return;
    }
    
    peerConnection.createAnswer()
    .then(function (sessionDescription) {
      console.log('createAnswer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      sendSdp(peerConnection.localDescription);　// &lt;--- ここを加える

      // -- Vanilla ICE の場合には、まだSDPは送らない --
    }).catch(function(err) {
      console.error(err);
    });
  }</pre><p></p>

<h3>ICE candidateも、すぐに交換する</h3>

<p>ICE candidateを収集した際も、すぐに送るように変更します。</p>

<p></p><pre class="crayon-plain-tag">function prepareNewConnection() {
    // ... 省略 ...

    // --- on get local ICE candidate
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);

        // Trickle ICE の場合は、ICE candidateを相手に送る
        sendIceCandidate(evt.candidate); // &lt;--- ここを追加する

        // Vanilla ICE の場合には、何もしない
      } else {
        console.log('empty ice event');

        // Trickle ICE の場合は、何もしない
        
        // Vanilla ICE の場合には、ICE candidateを含んだSDPを相手に送る
        //sendSdp(peer.localDescription); // &lt;-- ここをコメントアウトする
      }
    };

    // ... 省略 ....
  }

  function sendIceCandidate(candidate) {
    console.log('---sending ICE candidate ---');
    let obj = { type: 'candidate', ice: candidate };
    let message = JSON.stringify(obj);
    console.log('sending candidate=' + message);
    ws.send(message);
  }</pre><p></p>

<p>合わせてICE candidateをWebSocket経由で受け取った場合の処理も追加しましょう。相手からICE candidateを受け取ったら、その度にRTCPeerConnection.addIceCandidate()で覚えさせます。
</p><pre class="crayon-plain-tag">ws.onmessage = function(evt) {
    console.log('ws onmessage() data:', evt.data);
    let message = JSON.parse(evt.data);
    if (message.type === 'offer') {
      // -- got offer ---
      console.log('Received offer ...');
      textToReceiveSdp.value = message.sdp;
      let offer = new RTCSessionDescription(message);
      setOffer(offer);
    }
    else if (message.type === 'answer') {
      // --- got answer ---
      console.log('Received answer ...');
      textToReceiveSdp.value = message.sdp;
      let answer = new RTCSessionDescription(message);
      setAnswer(answer);
    }
    else if (message.type === 'candidate') { // &lt;--- ここから追加
      // --- got ICE candidate ---
      console.log('Received ICE candidate ...');
      let candidate = new RTCIceCandidate(message.ice);
      console.log(candidate);
      addIceCandidate(candidate);
    }
  };

  function addIceCandidate(candidate) {
    if (peerConnection) {
      peerConnection.addIceCandidate(candidate);
    }
    else {
      console.error('PeerConnection not exist!');
      return;
    }
  }</pre><p> 
さあ、これで修正は完了です。</p>

<h3>Trickle ICEを実行しよう</h3>

<p>手順はVanilla ICEの場合と同じです。シグナリングサーバーを起動して、ChromeかFirefoxのウィンドウを2つ開いて修正したHTMLを読み込んでください。あとは同様に[Start Video]→[Connect]です。</p>

<p>見た目も特に変わりはありません。もしかしたら人によっては早く繋がるのを実感できるかもしれません。</p>

<p>GitHub Pages/GitHubも用意しています。</p>

<ul>
<li>GitHub Pages で試す <a href="http://mganeko.github.io/webrtcexpjp/basic2016/ws_signaling_1to1_trickle.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">ws_signaling_1to1_trickle.html</a>

<ul>
<li>※http://～/ なのでFirefoxのみ。シグナリングサーバーは別途localhost上で起動しておく必要があります</li>
</ul></li>
<li>GitHub でソースを見る <a href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/ws_signaling_1to1_trickle.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">ws_signaling_1to1_trickle.html</a></li>
</ul>

<h2>2台のPC間の通信</h2>

<p>ここまできたら、せっかくなので2台の別々のPCで通信してみたくなります。同じネットワークに属するPC同士ならば通信できるはずです。例として次のような状況を考えてみましょう。</p>

<ul>
<li>IPアドレスが 192.168.0.2 と、 192.168.0.3 の2台のPCがある</li>
<li>前者(192.168.0.2)のポート:8080でWebサーバー、ポート:3001でNode.jsのシグナリングサーバーが動いている
<br /><a href="https://html5experts.jp/wp-content/uploads/2016/07/2pc_firefox.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/2pc_firefox-300x227.png" alt="2pc_firefox" width="300" height="227" class="alignnone size-medium wp-image-20049" srcset="/wp-content/uploads/2016/07/2pc_firefox-300x227.png 300w, /wp-content/uploads/2016/07/2pc_firefox.png 640w, /wp-content/uploads/2016/07/2pc_firefox-207x157.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></li>
</ul>

<p>Firefoxの場合は、接続するURLを変更すれば問題なく動きます。やっかいなのはChromeの場合です。</p>

<ul>
<li>Chromeでは、カメラやマイクにアクセスするためのgetUserMedia()が、原則としてhttp://～では許可されていない/</li>
<li>http://localhost/～ は例外的な扱いで許可されている
<br /><a href="https://html5experts.jp/wp-content/uploads/2016/07/2pc_chrome.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/2pc_chrome-300x227.png" alt="2pc_chrome" width="300" height="227" class="alignnone size-medium wp-image-20048" srcset="/wp-content/uploads/2016/07/2pc_chrome-300x227.png 300w, /wp-content/uploads/2016/07/2pc_chrome.png 640w, /wp-content/uploads/2016/07/2pc_chrome-207x157.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></li>
</ul>

<p>きちんと対処すると、次のような対策が必要です。ちょっと試すにはハードルが高いですよね。</p>

<ul>
<li>証明書を取得して https://～/ でアクセスするように、Webサーバーに設定</li>
<li>合わせて、シグナリングサーバーも wss://～ の暗号化通信を使うように設定</li>
</ul>

<p>そこで実験的に無理やり動かすには、次のような方法があります。Webサーバーとシグナリングサーバーは同一である必要はなく、また異なるWebサーバーでも構わないことを利用しています。
<br /><a href="https://html5experts.jp/wp-content/uploads/2016/07/2pc_chrome_force.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/07/2pc_chrome_force-300x226.png" alt="2pc_chrome_force" width="300" height="226" class="alignnone size-medium wp-image-20050" srcset="/wp-content/uploads/2016/07/2pc_chrome_force-300x226.png 300w, /wp-content/uploads/2016/07/2pc_chrome_force.png 640w, /wp-content/uploads/2016/07/2pc_chrome_force-207x156.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<p>お勧めはしませんが、どうしてもやりたい場合の参考としてどうぞ。</p>

<h2>次回は</h2>

<p>今回はNode.jsとWebSocketを使ったシグナリングを実現しました。残念ながら今回の仕組みでは、1対1の通信しか行うことができません。次回はこれを拡張し、複数人で同時に通信できるようにしたいと思います。</p>

<h2>オマケ：WebRTCの仕様の差分のおさらい</h2>

<p>オマケとして、今回のWebRTC再入門2016シリーズで取り上げているWebRTC関連仕様の変更箇所について、おさらいしおきましょう。(2016年6月現在）</p>

<h3>getUserMeida</h3>

<ul>
<li>navigator.mediaDevices.getUserMedia() が新しく用意された

<ul>
<li>旧APIの navigator.getUserMedia()は Firefoxでは非推奨</li>
</ul></li>
<li>ベンダープレフィックスが取れた</li>
<li>コールバックではなくPromiseベースになった</li>
<li>Firefox, Edge で利用可能。Chromeではフラグ指定が必要</li>
</ul>

<h3>ベンダープレフィックスの除去</h3>

<ul>
<li>Firefoxでは、主要なオブジェクトのベンダープレフィックスが取れた。mozプレフィックス付は非推奨に

<ul>
<li>新：RTCPeerConnection, RTCSessionDescription, RTCIceCandidate</li>
<li>旧：mozRTCPeerConnection, mozRTCSessionDescription, mozRTCIceCandidate　（非推奨）</li>
</ul></li>
<li>ただしChromeでは、一部ベンダープレフィックス付のまま

<ul>
<li>プレフィックス有り： webkitRTCPeerConnection</li>
<li>プレフィックス無し： RTCSessionDescription, RTCIceCandidate</li>
</ul></li>
</ul>

<h3>RTCPeerConnection</h3>

<ul>
<li>主要なメソッドがPromiseベースになった

<ul>
<li>createOffer(), createAnswer()</li>
<li>setLocalDescription(), setRemoteDescription()</li>
</ul></li>
<li>メディアストリーム処理の新しいイベントハンドラontrack()が追加、onaddstream()は非推奨

<ul>
<li>Firefoxではサポート済、Chromeでは未サポート</li>
</ul></li>
</ul>

<p><a href="https://www.w3.org/TR/webrtc/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">仕様</a>は常に更新されていますし、ブラウザの実装状況も異なります。最新の情報もご確認ください。</p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTC入門2016]]></series:name>
	</item>
		<item>
		<title>手動でWebRTCの通信をつなげよう ーWebRTC入門2016</title>
		<link>/mganeko/19814/</link>
		<pubDate>Fri, 01 Jul 2016 02:13:12 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[最新動向]]></category>
		<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=19814</guid>
		<description><![CDATA[連載： WebRTC入門2016 (2)こんにちは！ がねこまさしです。2014年に連載した「WebRTCを使ってみよう！」シリーズを、2016年6月の最新情報に基づき、内容をアップデートして改めてお届けしています。1回...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc2016/" class="series-380" title="WebRTC入門2016" data-wpel-link="internal">WebRTC入門2016</a> (2)</div><p>こんにちは！ がねこまさしです。2014年に連載した<a target="_blank" href="/series/webrtc-beginner/" data-wpel-link="internal">「WebRTCを使ってみよう！」シリーズ</a>を、2016年6月の最新情報に基づき、内容をアップデートして改めてお届けしています。<a target="_blank" href="/mganeko/19728/" data-wpel-link="internal">1回目はカメラにアクセス</a>してみました。2回目となる今回は、WebRTCの通信の仕組みを実感するために、「手動」でP2P通信をつなげてみましょう。</p>

<h2>WebRTCの通信はどうなっているの？</h2>

<p>WebRTCでは、映像/音声/アプリケーションデータなどをリアルタイムにブラウザ間で送受信することができます。それをつかさどるのが「<strong>RTCPeerConnection</strong>」です。 RTCPeerConnectionには3つの特徴があります。</p>

<ul>
<li>Peer-to-Peer(P2P)の通信 → ブラウザとブラウザの間で直接通信する</li>
<li>UDP/IPを使用 → TCP/IPのようにパケットの到着は保障しないが、オーバーヘッドが少ない</li>
<li>PeerとPeerの間で暗号化通信を行う → P2P通信の前に鍵の交換を行う</li>
</ul>

<p>多少の情報の欠落があっても許容する替わりに、通信のリアルタイム性を重視しています。UDPのポート番号は動的に割り振られ、49152 ～ 65535の範囲が使われるようです。 
<a href="https://html5experts.jp/wp-content/uploads/2014/02/rtcpeerconnection.png" data-wpel-link="internal"><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>暗号化の鍵</li>
<li>セッションの属性（名前、識別子、アクティブな時間など）→ WebRTCでは使っていないようです</li>
</ul>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/02/rtcpeer_ip_port.png" data-wpel-link="internal"><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></p>

<h3>ICE Candidate</h3>

<p>P2P通信を行う際にどのような通信経路が使えるかは、お互いのネットワーク環境に依存します。通信経路を定めるための仕組みが「<strong>Interactive Connectivity Establishment</strong> (ICE)」で、その通信経路の候補が「<strong>ICE Candidate</strong>」になります。WebRTCの通信を始める前に、可能性のある候補がリストアップされます。</p>

<ul>
<li>P2Pによる直接通信</li>
<li>NATを通過するためのSTUNサーバーから取得したポートマッピング → 最終的にはP2Pになる</li>
<li>Firefallを越えるための、TURNによるリレーサーバーを介した中継通信</li>
</ul>

<p>候補が見つかったら順次通信を試み、最初につながった経路が採用されます。</p>

<h2>手動シグナリングを実験してみよう</h2>

<p>このように、P2Pを始めるまでの情報のやり取りを「シグナリング」と言います。実は、WebRTCではシグナリングのプロトコルは規定されていません（自由に選べます）。シグナリングを実現するにはWebSocketを使うなど複数の方法がありますが、今回は最も原始的な方法であるコピー＆ペーストを試してみましょう。</p>

<p>ちょっと長いですが、こちらのHTMLをお好きなWebサーバーに配置してください。</p>

<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;Hand Signaling&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  Hand Signaling 2016&lt;br /&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;
  &lt;button type="button" onclick="connect();"&gt;Connect&lt;/button&gt;
  &lt;button type="button" onclick="hangUp();"&gt;Hang Up&lt;/button&gt; 
  &lt;div&gt;
    &lt;video id="local_video" autoplay style="width: 160px; height: 120px; border: 1px solid black;"&gt;&lt;/video&gt;
    &lt;video id="remote_video" autoplay style="width: 160px; height: 120px; 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="60" readonly="readonly"&gt;SDP to send&lt;/textarea&gt;
  &lt;/p&gt;
  &lt;p&gt;SDP to receive:&amp;nbsp;
    &lt;button type="button" onclick="onSdpText();"&gt;Receive remote SDP&lt;/button&gt;&lt;br /&gt;
    &lt;textarea id="text_for_receive_sdp" rows="5" cols="60"&gt;&lt;/textarea&gt;
  &lt;/p&gt;
&lt;/body&gt;
&lt;script type="text/javascript"&gt;
  let localVideo = document.getElementById('local_video');
  let remoteVideo = document.getElementById('remote_video');
  let localStream = null;
  let peerConnection = null;
  let textForSendSdp = document.getElementById('text_for_send_sdp');
  let textToReceiveSdp = document.getElementById('text_for_receive_sdp');

  // --- prefix -----
  navigator.getUserMedia  = navigator.getUserMedia    || navigator.webkitGetUserMedia ||
                            navigator.mozGetUserMedia || navigator.msGetUserMedia;
  RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
  RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription;

  // ---------------------- media handling ----------------------- 
  // start local video
  function startVideo() {
    getDeviceStream({video: true, audio: false})
    .then(function (stream) { // success
      localStream = stream;
      playVideo(localVideo, stream);
    }).catch(function (error) { // error
      console.error('getUserMedia error:', error);
      return;
    });
  }

  // stop local video
  function stopVideo() {
    pauseVideo(localVideo);
    stopLocalStream(localStream);
  }

  function stopLocalStream(stream) {
    let tracks = stream.getTracks();
    if (! tracks) {
      console.warn('NO tracks');
      return;
    }
    
    for (let track of tracks) {
      track.stop();
    }
  }
  
  function getDeviceStream(option) {
    if ('getUserMedia' in navigator.mediaDevices) {
      console.log('navigator.mediaDevices.getUserMadia');
      return navigator.mediaDevices.getUserMedia(option);
    }
    else {
      console.log('wrap navigator.getUserMadia with Promise');
      return new Promise(function(resolve, reject){    
        navigator.getUserMedia(option,
          resolve,
          reject
        );
      });      
    }
  }

  function playVideo(element, stream) {
    if ('srcObject' in element) {
      element.srcObject = stream;
    }
    else {
      element.src = window.URL.createObjectURL(stream);
    }
    element.play();
    element.volume = 0;
  }

  function pauseVideo(element) {
    element.pause();
    if ('srcObject' in element) {
      element.srcObject = null;
    }
    else {
      if (element.src &amp;&amp; (element.src !== '') ) {
        window.URL.revokeObjectURL(element.src);
      }
      element.src = '';
    }
  }

  // ----- hand signaling ----
  function onSdpText() {
    let text = textToReceiveSdp.value;
    if (peerConnection) {
      console.log('Received answer text...');
      let answer = new RTCSessionDescription({
        type : 'answer',
        sdp : text,
      });
      setAnswer(answer);
    }
    else {
      console.log('Received offer text...');
      let offer = new RTCSessionDescription({
        type : 'offer',
        sdp : text,
      });
      setOffer(offer);
    }
    textToReceiveSdp.value ='';
  }
 
  function sendSdp(sessionDescription) {
    console.log('---sending sdp ---');
    textForSendSdp.value = sessionDescription.sdp;
    textForSendSdp.focus();
    textForSendSdp.select();
  }

  // ---------------------- connection handling -----------------------
  function prepareNewConnection() {
    let pc_config = {"iceServers":[]};
    let peer = new RTCPeerConnection(pc_config);

    // --- on get remote stream ---
    if ('ontrack' in peer) {
      peer.ontrack = function(event) {
        console.log('-- peer.ontrack()');
        let stream = event.streams[0];
        playVideo(remoteVideo, stream);
      };
    }
    else {
      peer.onaddstream = function(event) {
        console.log('-- peer.onaddstream()');
        let stream = event.stream;
        playVideo(remoteVideo, stream);
      };
    }

    // --- on get local ICE candidate
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);

        // Trickle ICE の場合は、ICE candidateを相手に送る
        // Vanilla ICE の場合には、何もしない
      } else {
        console.log('empty ice event');

        // Trickle ICE の場合は、何もしない
        // Vanilla ICE の場合には、ICE candidateを含んだSDPを相手に送る
        sendSdp(peer.localDescription);
      }
    };

    
    // -- add local stream --
    if (localStream) {
      console.log('Adding local stream...');
      peer.addStream(localStream);
    }
    else {
      console.warn('no local stream, but continue.');
    }

    return peer;
  }

  function makeOffer() {
    peerConnection = prepareNewConnection();
    peerConnection.createOffer()
    .then(function (sessionDescription) {
      console.log('createOffer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      // -- Vanilla ICE の場合には、まだSDPは送らない --
      //sendSdp(peerConnection.localDescription);
    }).catch(function(err) {
      console.error(err);
    });
  }

  function setOffer(sessionDescription) {
    if (peerConnection) {
      console.error('peerConnection alreay exist!');
    }
    peerConnection = prepareNewConnection();
    peerConnection.setRemoteDescription(sessionDescription)
    .then(function() {
      console.log('setRemoteDescription(offer) succsess in promise');
      makeAnswer();
    }).catch(function(err) {
      console.error('setRemoteDescription(offer) ERROR: ', err);
    });
  }
  
  function makeAnswer() {
    console.log('sending Answer. Creating remote session description...' );
    if (! peerConnection) {
      console.error('peerConnection NOT exist!');
      return;
    }
    
    peerConnection.createAnswer()
    .then(function (sessionDescription) {
      console.log('createAnswer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      // -- Vanilla ICE の場合には、まだSDPは送らない --
      //sendSdp(peerConnection.localDescription);
    }).catch(function(err) {
      console.error(err);
    });
  }

  function setAnswer(sessionDescription) {
    if (! peerConnection) {
      console.error('peerConnection NOT exist!');
      return;
    }

    peerConnection.setRemoteDescription(sessionDescription)
    .then(function() {
      console.log('setRemoteDescription(answer) succsess in promise');
    }).catch(function(err) {
      console.error('setRemoteDescription(answer) ERROR: ', err);
    });
  }
  
  // start PeerConnection
  function connect() {
    if (! peerConnection) {
      console.log('make Offer');
      makeOffer();
    }
    else {
      console.warn('peer already exist.');
    }
  }

  // close PeerConnection
  function hangUp() {
    if (peerConnection) {
      console.log('Hang up.');
      peerConnection.close();
      peerConnection = null;
      pauseVideo(remoteVideo);
    }
    else {
      console.warn('peer NOT exist.');
    }
  }

&lt;/script&gt;
&lt;/html&gt;</pre><p></p>

<p>GitHub Pagesでも公開していますので、すぐに試すことができます。</p>

<ul>
<li>GitHub Pages で試す <a target="_blank" href="https://mganeko.github.io/webrtcexpjp/basic2016/hand_signaling.html" data-wpel-link="external" rel="follow external noopener noreferrer">hand_signaling.html</a> (Chromeもフラグ設定不要）</li>
<li>GitHub でソースを見る <a target="_blank" href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/hand_signaling.html" data-wpel-link="external" rel="follow external noopener noreferrer">hand_signaling.html</a></li>
<li>Safari Technology Preview 対応版を試す <a target="_blank" href="https://mganeko.github.io/webrtcexpjp/basic2016/hand_signaling_track.html" data-wpel-link="external" rel="follow external noopener noreferrer">hand_signaling_track.html</a></li>
<li>Safari Technology Preview 対応版のソース <a target="_blank" href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/hand_signaling_track.html" data-wpel-link="external" rel="follow external noopener noreferrer">hand_signaling_track.html</a></li>
</ul>

<p>次に、PCにWebカメラを接続してからFirefox 47 またはChrome 51でアクセスしてみてください。（※Chromeの場合はカメラ映像取得の制限があるので、https://～か/ http://localhost/～のWebサーバーが必要になります）残念ながら今回はEdgeでは利用できません。</p>

<p>通信するために2つページを開く必要があります。同一ウィンドウで複数タブを開くよりも、別のウィンドウで横に並べたほうが作業しやすいです。</p>

<h3>接続手順</h3>

<p>接続手順は<a target="_blank" href="/mganeko/5181/" data-wpel-link="internal">2014年のもの</a>よりも簡略化しました。それでも間違えやすいので慎重に操作してくださいね。</p>

<h4>(1) 映像の取得</h4>

<p>両方のウィンドウで[Start Video]ボタンをクリックします。カメラのアクセスを許可すると、それぞれリアルタイムの映像が表示されます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/06/hand2016_1.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/hand2016_1-300x137.png" alt="hand2016_1" width="300" height="137" class="alignnone size-medium wp-image-19841" srcset="/wp-content/uploads/2016/06/hand2016_1-300x137.png 300w, /wp-content/uploads/2016/06/hand2016_1.png 640w, /wp-content/uploads/2016/06/hand2016_1-207x94.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h4>(2) 通信の開始</h4>

<p>左のウィンドウで[Connect]ボタンをクリックしてください。すると[SDP to send:]のテキストエリアに、ドバドバっとSDPの文字列が表示されます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/06/hand2016_2.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/hand2016_2-300x133.png" alt="hand2016_2" width="300" height="133" class="alignnone size-medium wp-image-19842" srcset="/wp-content/uploads/2016/06/hand2016_2-300x133.png 300w, /wp-content/uploads/2016/06/hand2016_2.png 640w, /wp-content/uploads/2016/06/hand2016_2-207x92.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h4>(3)(4) SDPの送信（左→右）</h4>

<p>(3)左の[SDP to send:]の内容をコピーし、(4)右の[SDP to receive:]の下のテキストエリアにペーストします。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/06/hand2016_3_4.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/hand2016_3_4-300x140.png" alt="hand2016_3_4" width="300" height="140" class="alignnone size-medium wp-image-19844" srcset="/wp-content/uploads/2016/06/hand2016_3_4-300x140.png 300w, /wp-content/uploads/2016/06/hand2016_3_4.png 640w, /wp-content/uploads/2016/06/hand2016_3_4-207x96.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<h4>(5) SDPの受信（右）</h4>

<p>右の[Receive remote SDP]ボタンをクリックすると、今度は右のウィンドウの[SDP to send:]のテキストエリアに、ドバドバっとSDPの文字列が表示されます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/06/hand2016_5.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/hand2016_5-300x136.png" alt="hand2016_5" width="300" height="136" class="alignnone size-medium wp-image-19847" srcset="/wp-content/uploads/2016/06/hand2016_5-300x136.png 300w, /wp-content/uploads/2016/06/hand2016_5.png 640w, /wp-content/uploads/2016/06/hand2016_5-207x94.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h4>(6)(7) SDPの返信（左←右）</h4>

<p>さっきと反対に(6)右の[SDP to send:]の内容をコピーし、(7)左の[SDP to receive:]の下のテキストエリアにペーストします。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/06/hand2016_6_7.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/hand2016_6_7-300x137.png" alt="hand2016_6_7" width="300" height="137" class="alignnone size-medium wp-image-19848" srcset="/wp-content/uploads/2016/06/hand2016_6_7-300x137.png 300w, /wp-content/uploads/2016/06/hand2016_6_7.png 640w, /wp-content/uploads/2016/06/hand2016_6_7-207x94.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h4>(8) SDPの受信（左）</h4>

<p>左の[Receive remote SDP]ボタンをクリックします。しばらくすると（～数秒）P2P通信が始まり両方のウィンドウに2つ目の動画が表示されるはずです。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/06/hand2016_8.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/hand2016_8-300x139.png" alt="hand2016_8" width="300" height="139" class="alignnone size-medium wp-image-19849" srcset="/wp-content/uploads/2016/06/hand2016_8-300x139.png 300w, /wp-content/uploads/2016/06/hand2016_8.png 640w, /wp-content/uploads/2016/06/hand2016_8-207x96.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<p>上手くいかなかった場合は、コピー範囲が欠けているか、手順が抜けている可能性があります。深呼吸してから両方のブラウザをリロードし、もう一度試してみてください。</p>

<p>それでも通信できない場合は、実はネットワーク環境の問題の可能性があります。この記事の「トラブルシューティング」の章をご覧ください。（あるいはソースのバグや、説明の不備の可能性もあります。何かお気づきの際にはご指摘ください）</p>

<h2>トラブルシューティング</h2>

<p>これまで何度も手動シグナリングを試して/試していただいて、通信ができないケースがありました。当初は原因が分からなかったのですが、その後に判明したケースを説明します。</p>

<h3>外部ネットワークにつながっていない場合</h3>

<p>PCが全くネットワークに接続されていない状態では、カメラ映像の取得に成功しても通信ができません。これはネットワークに接続されていない状態では、通信経路の情報であるICE Candidateが収集できないためです。<br />
例え同一PC内で通信を行う場合にも、外部に接続できる状態で利用する必要があります。</p>

<p>ハンズオン等で手動シグナリングを試してもらうことがあるのですが、長いことこの制約に気が付かず通信できないで悩んでいました。</p>

<h3>Chrome &#8211; Firefox 間での通信</h3>

<p>WebRTCではChrome &#8211; Chrome間や、Firefox &#8211; Firefox 間のように同一種類のブラウザ同士だけでなく、Chrome &#8211; Firefox間でも通信することができます。もちろん手動シグナリングでも同様です。<br />
ところが実際に1台のPCで Firefox &#8211; Chrome 間で手動シグナリングを行おうとすると、カメラ映像の取得で衝突してしまうケースがあります。この場合は次のどちらかをお試しください</p>

<ul>
<li>(a) 2台のカメラをご用意して、ブラウザごとに違うカメラの映像を取得する</li>
<li>(b) 映像は片方のブラウザのみで取得し、そのブラウザから[Connect]で通信を始める

<ul>
<li>→ ※この場合は片方向の映像通信となります</li>
</ul></li>
</ul>

<h2>裏側で起こっていること</h2>

<p>それでは映像通信に成功したところで、その裏側で起きていることを見てみましょう。</p>

<h3>Vanilla ICE と Trickle ICE</h3>

<p>WebRTCのP2P通信を確立するためのシグナリングでは、次の2種類の情報を交換する必要があると説明しました。</p>

<ul>
<li>SDP</li>
<li>ICE candidate</li>
</ul>

<p>ところが今回の手動シグナリングではSDPしか交換していません。いったいぜんたい、ICE candidateの情報はどうなっているのでしょうか？</p>

<p>実はICE Candidateの情報は、今回交換しているSDPの中に含まれています。実際に私のPCで取得したSDPの一部を掲載します。（※IPアドレスは一部マスクしています）</p>

<p></p><pre class="crayon-plain-tag">m=video 58461 UDP/TLS/RTP/SAVPF 100 101 116 117 96 97 98
c=IN IP4 192.168.xxx.xxx
a=rtcp:58465 IN IP4 192.168.xxx.xxx
a=candidate:2999745851 1 udp 2122260223 192.168.xxx.xxx 58461 typ host generation 0 network-id 4
a=candidate:2747735740 1 udp 2122194687 192.168.xxx.xxx 58462 typ host generation 0 network-id 3
a=candidate:1606961068 1 udp 2122129151 10.2.xxx.xxx 58463 typ host generation 0 network-id 2
a=candidate:1435463253 1 udp 2122063615 192.168.xxx.xxx 58464 typ host generation 0 network-id 1
a=candidate:2999745851 2 udp 2122260222 192.168.xxx.xxx 58465 typ host generation 0 network-id 4
a=candidate:2747735740 2 udp 2122194686 192.168.xxx.xxx 58466 typ host generation 0 network-id 3
a=candidate:1606961068 2 udp 2122129150 10.2.xxx.xxx 58467 typ host generation 0 network-id 2
a=candidate:1435463253 2 udp 2122063614 192.168.xxx.xxx 58468 typ host generation 0 network-id 1
a=candidate:4233069003 1 tcp 1518280447 192.168.xxx.xxx 9 typ host tcptype active generation 0 network-id 4
a=candidate:3980714572 1 tcp 1518214911 192.168.xxx.xxx 9 typ host tcptype active generation 0 network-id 3
a=candidate:290175836 1 tcp 1518149375 10.2.xxx.xxx 9 typ host tcptype active generation 0 network-id 2
a=candidate:453808805 1 tcp 1518083839 192.168.xxx.xxx 9 typ host tcptype active generation 0 network-id 1
a=candidate:4233069003 2 tcp 1518280446 192.168.xxx.xxx 9 typ host tcptype active generation 0 network-id 4
a=candidate:3980714572 2 tcp 1518214910 192.168.xxx.xxx 9 typ host tcptype active generation 0 network-id 3
a=candidate:290175836 2 tcp 1518149374 10.2.xxx.xxx 9 typ host tcptype active generation 0 network-id 2
a=candidate:453808805 2 tcp 1518083838 192.168.xxx.xxx 9 typ host tcptype active generation 0 network-id 1
a=ice-ufrag:xxxxxxxxxxxxx
a=ice-pwd:xxxxxxxxxxxx</pre><p> 
a=candidate: で始まる行がICE candidateになります。（※仮想化ソフトを入れている影響で複数のネットワークが候補になっています）。
SDPを最初に取得したときにはICE candidateの行は含まれず、その後ICE candidateが収集されるにしたがって、SDPの中に追加されます。<br />
今回は全てのICE candidateが出そろった後に、SDPとまとめて交換しています。このような方式を <strong>&#8220;Vanilla ICE&#8221;</strong> と呼びます。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/06/hand2016_vanilla.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/hand2016_vanilla-300x225.png" alt="hand2016_vanilla" width="300" height="225" class="alignnone size-medium wp-image-19861" srcset="/wp-content/uploads/2016/06/hand2016_vanilla-300x225.png 300w, /wp-content/uploads/2016/06/hand2016_vanilla.png 640w, /wp-content/uploads/2016/06/hand2016_vanilla-207x156.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>これに対して、初期のSDPを交換し、その後ICE Candidateを順次交換する方式を <strong>&#8220;Trickle ICE&#8221;</strong> と呼びます。すべてのICE candidateを交換し終わる前にP2P通信が始まることがあるので、Trickle ICEの方が一般的に早く接続が確立します。<br />
<a href="https://html5experts.jp/wp-content/uploads/2016/06/hand2016_trickle.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/hand2016_trickle-300x224.png" alt="hand2016_trickle" width="300" height="224" class="alignnone size-medium wp-image-19863" srcset="/wp-content/uploads/2016/06/hand2016_trickle-300x224.png 300w, /wp-content/uploads/2016/06/hand2016_trickle.png 640w, /wp-content/uploads/2016/06/hand2016_trickle-207x155.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h3>Offer と Answer</h3>

<p>SDPには通信を始める側(Offer)と、通信を受け入れる側(Answer)があります。必ずOffer → Answerの順番でやりとりする必要があります。</p>

<h2>ソースコードを追いかけてみよう</h2>

<h4>Offer SDPの生成</h4>

<p>それでは、SDP(+ ICE candidate)のやり取りをソースコードで見てみましょう。まずは[Connect]ボタンを押してSDPを生成するところまでです。（ソースコードは抜粋しています）
</p><pre class="crayon-plain-tag">// start PeerConnection
  function connect() {
      makeOffer();
  }

  // Offer SDPを生成する
  function makeOffer() {
    peerConnection = prepareNewConnection(); // RTCPeerConnectionを生成し、必要なメッセージハンドラを設定

    peerConnection.createOffer()
    .then(function (sessionDescription) {
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      // -- Vanilla ICE の場合には、まだSDPは送らない --
      //sendSdp(peerConnection.localDescription);  &lt;-- Vanilla ICEなので、まだ送らない
    }).catch(function(err) {
      console.error(err);
    });
  }</pre><p> 
発信側で[Connect]ボタンをクリックすると、次の処理が行われます。</p>

<ul>
<li>RTCPeerConnection のオブジェクトを生成</li>
<li>RTCPeerConnection.createOffer() で Offer SDPを生成</li>
<li>生成したOffer SDPを、RTCPeerConnection.setLocalDescription()で覚える</li>
<li>Trickle ICEの場合はすぐにSDPを送信するが、今回は送信しない</li>
</ul>

<p>createOffer(), setLocalDescription()は非同期で処理が行われます。従来はコールバックで後続処理を記述していましたが、現在はPromiseを返すので、then()の中に処理を記述します。<br />
※<a target="_blank" href="/mganeko/5181/" data-wpel-link="internal">2014年の記事では</a>setLocalDescription()が非同期であることを意識しおらず、誤った記述になっていました。</p>

<h4>ICE candidateの収集</h4>

<p>次は ICE candidateの収集です。ICE candidateの収集も非同期に行われるため、RTCPeerConnectionのイベントハンドラで行います。
</p><pre class="crayon-plain-tag">function prepareNewConnection() {
    let pc_config = {"iceServers":[]};
    let peer = new RTCPeerConnection(pc_config);

    // --- on get local ICE candidate
    peer.onicecandidate = function (evt) {
      if (evt.candidate) { // ICE candidate が収集された場合
        console.log(evt.candidate);

        // Trickle ICE の場合は、ICE candidateを相手に送る
        // Vanilla ICE の場合には、何もしない
      } else { // ICE candidateの収集が完了した場合
        console.log('empty ice event');

        // Trickle ICE の場合は、何もしない
        // Vanilla ICE の場合には、ICE candidateを含んだSDPを相手に送る
        sendSdp(peer.localDescription);
      }
    };

    // ... 省略 ....

    // 通信対象の映像/音声ストリームを追加する
    if (localStream) {
      console.log('Adding local stream...');
      peer.addStream(localStream);
    }


    return peer;
  }</pre><p> 
今回のコードではprepareNewConnection()の中でRTCPeerConnectionオブジェクトを生成し、各種イベントハンドラを設定しています。ICE candidateのためRTCPeerConnection.onicecandidateにイベントハンドラを記述しています。このイベントは複数回発生します。<br />
全てのICE candidateを収集し終わると空のイベントが渡ってきます。このタイミングで最終的なSDPを相手に送信します。今回の手動シグナリングではsendSdp()の中でテキストエリアに表示しています。</p>

<h4>Offser SDPの受信</h4>

<p>応答側にOffer SDPをペーストして[Receive remote SDP]ボタンをクリックすると、onSdpText() → setOffer() と呼び出されます。
</p><pre class="crayon-plain-tag">function onSdpText() {
    let text = textToReceiveSdp.value;
    if (peerConnection) { // Answerの場合
      // ... 省略 ...
    }
    else { // Offerの場合
      let offer = new RTCSessionDescription({
        type : 'offer',
        sdp : text,
      });
      setOffer(offer);
    }
    textToReceiveSdp.value ='';
  }</pre><p></p>

<p>さらに setOffer()の中では次の処理が行われています。</p>

<ul>
<li>PeerConnectionのオブジェクトを生成</li>
<li>受け取ったOffer SDPを setRemoteDescription()で覚える。Promiseを使った非同期処理を行う</li>
<li>成功したら makeAnswer()の中でAnswer SDPを生成
<pre class="crayon-plain-tag">function setOffer(sessionDescription) {
    peerConnection = prepareNewConnection();
    peerConnection.setRemoteDescription(sessionDescription)
    .then(function() {
      makeAnswer();
    }).catch(function(err) {
      console.error('setRemoteDescription(offer) ERROR: ', err);
    });
  }</pre> </li>
</ul>

<h4>Answer SDPの生成→送信</h4>

<p>makeAnswer()の中ではOfferの時と同様な処理が行われます。
</p><pre class="crayon-plain-tag">function makeAnswer() {   
    peerConnection.createAnswer()
    .then(function (sessionDescription) {
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      // -- Vanilla ICE の場合には、まだSDPは送らない --
      //sendSdp(peerConnection.localDescription);
    }).catch(function(err) {
      console.error(err);
    });
  }</pre><p></p>

<ul>
<li>RTCPeerConnection.createAnswer() で Answer SDPを生成</li>
<li>生成したAnswer SDPを、RTCPeerConnection.setLocalDescription()で覚える。Promiseを使った非同期処理を行う</li>
<li>Trickle ICEの場合はすぐにSDPを送信するが、今回は送信しない</li>
</ul>

<p>この後 RTCPeerConnection.onicecandidate()でICE candidateを収集し、すべて揃ったらsendSdp()でOffer側に送り返します。</p>

<h4>Answer SDPの受信</h4>

<p>発信側にAnser SDPをペーストして[Receive remote SDP]ボタンをクリックすると、onSdpText() → setAnswer() と呼び出されます。
</p><pre class="crayon-plain-tag">function onSdpText() {
    let text = textToReceiveSdp.value;
    if (peerConnection) { // Answerの場合
      let answer = new RTCSessionDescription({
        type : 'answer',
        sdp : text,
      });
      setAnswer(answer);
    }
    else { // Offerの場合
      // ... 省略 ...
    }
    textToReceiveSdp.value ='';
  }

  function setAnswer(sessionDescription) {
    peerConnection.setRemoteDescription(sessionDescription)
    .then(function() {
      console.log('setRemoteDescription(answer) succsess in promise');
    }).catch(function(err) {
      console.error('setRemoteDescription(answer) ERROR: ', err);
    });
  }</pre><p> 
setAnswer()の中ではRTCPeerConnection.setRemoteDescription()で受け取ったSDPを覚えます。</p>

<h4>映像/音声の送受信</h4>

<p>PeerConnectionのオブジェクトを生成した際に、送信する映像/音声ストリームをRTCPeerConnection.addStream()で指定しておきます。
</p><pre class="crayon-plain-tag">function prepareNewConnection() {
    let pc_config = {"iceServers":[]};
    let peer = new RTCPeerConnection(pc_config);

    // ... 省略 ...

    // -- add local stream --
    if (localStream) {
      peer.addStream(localStream);
    }

    return peer;
  }</pre><p></p>

<p>SDPの交換が終わると、P2P通信に相手の映像/音声が含まれていればイベントが発生します。従来はRTCPeerConnection.onaddstream() にハンドラを記述していましたが、新しいイベントが策定されRTCPeerConnection.ontrack() にハンドラを記述するようになっています。Firefoxはontrack()がすでに使えるようになっていて、onaddstream()は非推奨になっています。（Chromeは未対応です）</p>

<p></p><pre class="crayon-plain-tag">function prepareNewConnection() {
    let pc_config = {"iceServers":[]};
    let peer = new RTCPeerConnection(pc_config);

    // --- on get remote stream ---
    if ('ontrack' in peer) {
      peer.ontrack = function(event) {
        let stream = event.streams[0];
        playVideo(remoteVideo, stream);
      };
    }
    else {
      peer.onaddstream = function(event) {
        let stream = event.stream;
        playVideo(remoteVideo, stream);
      };
    }

    // ... 省略 ....
  }</pre><p> 
以上で主要な処理の解説は終わりです。</p>

<h2>次回は</h2>

<p>今回は手動で情報交換を行い、原始的なビデオチャットを動かしてみました。P2P通信が確立するまでの動きを実感していただけたのではないでしょうか？</p>

<p>実際の利用場面では手動シグナリングなんかやってらません。次回はシグナリングサーバーを使って、通信を行ってみたいと思います。</p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTC入門2016]]></series:name>
	</item>
		<item>
		<title>カメラを使ってみよう ーWebRTC入門2016</title>
		<link>/mganeko/19728/</link>
		<pubDate>Fri, 24 Jun 2016 00:00:28 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[最新動向]]></category>
		<category><![CDATA[CSS]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[WebRTC]]></category>
		<category><![CDATA[カメラ]]></category>

		<guid isPermaLink="false">/?p=19728</guid>
		<description><![CDATA[連載： WebRTC入門2016 (1)こんにちは！ がねこまさしです。2014年に連載した「WebRTCを使ってみよう！」シリーズですが、内容がすっかり古くなってしまいました。そこで2016年6月の最新情報に基づき、内...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc2016/" class="series-380" title="WebRTC入門2016" data-wpel-link="internal">WebRTC入門2016</a> (1)</div><p>こんにちは！ がねこまさしです。2014年に連載した<a href="https://html5experts.jp/series/webrtc-beginner/" target="_blank" data-wpel-link="internal">「WebRTCを使ってみよう！」シリーズ</a>ですが、内容がすっかり古くなってしまいました。そこで2016年6月の最新情報に基づき、内容をアップデートして改めてお届けしたいと思います。</p>

<h2>WebRTCとは？</h2>

<p>WebRTCとは”Web Real-Time Communication”の略で、Webブラウザ上でビデオ/オーディオの通信や、データ通信を行うための規格です。HTML5で新しく策定されたもので、複数の技術の連携で成り立っています。 ちなみに策定には複数の団体が絡んでいています。</p>

<ul>
<li>API → <a href="https://www.w3.org/TR/webrtc/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">W3C</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>

<p>APIの策定作業はWebRTC 1.0に向けて大詰めに入っています。またより詳細な低レベルのAPIを定義している<a href="http://ortc.org/wp-content/uploads/2016/05/ortc.html" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">ORTC</a>も登場し、将来の統合に向けた動きも始まっています。</p>

<p>コーデックの選定では、ブラウザがサポートしなければならないビデオコーデックに、既にデファクトスタンダードともいえるH.264が加わりました。また、Googleが開発しているオープンソースのビデオコーデック「VP9」をサポートするブラウザも増えています。</p>

<h2>WebRTCで何ができるの？</h2>

<p>WebRTCは厳密に言うとビデオ/オーディオ/データ通信を行うための仕組みですが、他にも関連が深い技術があります。この連載では3つをまとめてWebRTC（とその仲間たち）ということで扱います。</p>

<ul>
<li>カメラ、マイクといったデバイスへのアクセスする &#8230; <a href="https://www.w3.org/TR/mediacapture-streams/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">Media Capture and Streams</a></li>
<li>ビデオ/オーディオ/データ通信を行う &#8230; <a href="https://www.w3.org/TR/webrtc/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">WebRTC 1.0: Real-time Communication Between Browsers</a></li>
<li>ビデオ/オーディオの録画/録音を行う &#8230; <a href="https://www.w3.org/TR/mediastream-recording/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">MediaStream Recording</a></li>
</ul>

<p>この3兄弟に、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>新しいAPIでカメラを使ってみよう</h2>

<p>カメラやマイクと言ったデバイスにアクセスするには、従来は<code>navigator.getUserMedia()</code>というAPIを使いました。2016年6月現在では、これに代わる新しいAPIが登場しています。</p>

<ul>
<li><code>navigator.mediaDevices.getUserMedia()</code> になった</li>
<li>ベンダープレフィックスが取れた</li>
<li>コールバックではなく<code>Promise</code>ベースになった</li>
</ul>

<p>またデスクトップブラウザの場合は次のブラウザで利用することができます。</p>

<ul>
<li>Firefox 47 &#8230; 利用可能</li>
<li>Chrome 51 &#8230; 利用する場合にはフラグ設定が必要

<ul>
<li>chrome://flagsというURLを開く</li>
<li>「試験運用版のウェブ プラットフォームの機能 #enable-experimental-web-platform-features」を有効に</li>
<li>Chromeを再起動</li>
</ul></li>
<li>Edge 25 &#8230; Windows 10 のEdgeでも利用可能</li>
</ul>

<h3>サンプルコード</h3>

<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;Camera with mediaDevice&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;button onclick="startVideo()"&gt;Start&lt;/button&gt;
  &lt;br /&gt;
  &lt;video id="local_video" autoplay style="width: 320px; height: 240px; border: 1px solid black;"&gt;&lt;/video&gt;
&lt;/body&gt;
&lt;script type="text/javascript"&gt;
  let localVideo = document.getElementById('local_video');
  let localStream;
  
  // start local video
  function startVideo() {
    navigator.mediaDevices.getUserMedia({video: true, audio: false})
    .then(function (stream) { // success
      localStream = stream;
      localVideo.src = window.URL.createObjectURL(localStream);
    }).catch(function (error) { // error
      console.error('mediaDevice.getUserMedia() error:', error);
      return;
    });
  }
&lt;/script&gt;
&lt;/html&gt;</pre><p></p>

<p>このHTMLファイルに、（Webサーバ経由で）ブラウザからアクセスしてみてください。GitHub Pagesでも公開していますので、すぐに試すことができます。</p>

<ul>
<li>GitHub Pages で試す <a target="_blank" href="https://mganeko.github.io/webrtcexpjp/basic2016/camera_new.html" data-wpel-link="external" rel="follow external noopener noreferrer">camera_new.html</a>（※Chromeの場合は、上記のフラグ設定が必要です）</li>
<li>GitHub でソースを見る <a target="_blank" href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/camera_new.html" data-wpel-link="external" rel="follow external noopener noreferrer">camera_new.html</a></li>
</ul>

<p>ブラウザで[Start]ボタンをクリックすると、カメラへのアクセスの許可を求めるダイアログが表示されますので、許可してください。<br />
<strong>Firefoxの場合</strong>
<a href="https://html5experts.jp/wp-content/uploads/2016/06/getusermedia_ff.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/getusermedia_ff-300x144.png" alt="getusermedia_ff" width="300" height="144" class="alignnone size-medium wp-image-19758" srcset="/wp-content/uploads/2016/06/getusermedia_ff-300x144.png 300w, /wp-content/uploads/2016/06/getusermedia_ff-207x100.png 207w, /wp-content/uploads/2016/06/getusermedia_ff.png 322w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<p><strong>Chromeの場合</strong>
<a href="https://html5experts.jp/wp-content/uploads/2016/06/getusermedia_chrome.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/getusermedia_chrome-300x112.png" alt="getusermedia_chrome" width="300" height="112" class="alignnone size-medium wp-image-19759" srcset="/wp-content/uploads/2016/06/getusermedia_chrome-300x112.png 300w, /wp-content/uploads/2016/06/getusermedia_chrome-207x77.png 207w, /wp-content/uploads/2016/06/getusermedia_chrome.png 354w" sizes="(max-width: 300px) 100vw, 300px" /></a><br /></p>

<p><strong>Edgeの場合</strong><br />
<a href="https://html5experts.jp/wp-content/uploads/2016/06/getusermedia_edge.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/getusermedia_edge-300x45.png" alt="getusermedia_edge" width="300" height="45" class="alignnone size-medium wp-image-19789" srcset="/wp-content/uploads/2016/06/getusermedia_edge-300x45.png 300w, /wp-content/uploads/2016/06/getusermedia_edge.png 640w, /wp-content/uploads/2016/06/getusermedia_edge-207x31.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
ウィンドウの下の部分にこのように表示されます。他のブラウザと違うので、ちょっと気が付きにくいかもしれません。</p>

<p>すると、このようにカメラの映像が表示されるはずです。
<a href="https://html5experts.jp/wp-content/uploads/2016/06/camera_ff.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/camera_ff-300x247.png" alt="camera_ff" width="300" height="247" class="alignnone size-medium wp-image-19768" srcset="/wp-content/uploads/2016/06/camera_ff-300x247.png 300w, /wp-content/uploads/2016/06/camera_ff-207x171.png 207w, /wp-content/uploads/2016/06/camera_ff.png 428w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h3>HTMLファイルの置き場所</h3>

<p>カメラやマイクにアクセスするには、サンプルのHTMLファイルをWebサーバーに配置する必要がありますが、実はブラウザによって条件が異なります。</p>

<ul>
<li>Chromeの場合 &#8230; 原則 https://～/ のみ。例外として http://localhost/～ でも利用可能</li>
<li>Firefoxの場合 &#8230; https://～/ および http://～/ の両方で利用可能。さらに file://～ でも利用可能</li>
<li>Edgeの場合 &#8230; https://～/ および http://～/ の両方で利用可能。さらに file://～ でも利用可能</li>
</ul>

<p>FirefoxやEdgeの場合は、ローカルのHTMLファイルを直接読み込んでも、利用可能です。</p>

<h3>トラブルシューティング</h3>

<p>カメラのアクセスの許可を求められた際に「常に拒否」すると、そのサイトからはカメラの映像を取得することができなくなります。その場合は明示的に再許可してあげる必要があります。</p>

<p><strong>Firefoxの場合</strong><a href="https://html5experts.jp/wp-content/uploads/2016/06/getusermedia_ff_ng.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/getusermedia_ff_ng-300x204.png" alt="getusermedia_ff_ng" width="300" height="204" class="alignnone size-medium wp-image-19761" srcset="/wp-content/uploads/2016/06/getusermedia_ff_ng-300x204.png 300w, /wp-content/uploads/2016/06/getusermedia_ff_ng-207x141.png 207w, /wp-content/uploads/2016/06/getusermedia_ff_ng.png 403w" sizes="(max-width: 300px) 100vw, 300px" /></a>で「許可」または「毎回確認する」を選択</p>

<p><strong>Chromeの場合</strong><a href="https://html5experts.jp/wp-content/uploads/2016/06/getusermedia_chrome_ng.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/getusermedia_chrome_ng-300x151.png" alt="getusermedia_chrome_ng" width="300" height="151" class="alignnone size-medium wp-image-19762" srcset="/wp-content/uploads/2016/06/getusermedia_chrome_ng-300x151.png 300w, /wp-content/uploads/2016/06/getusermedia_chrome_ng-207x104.png 207w, /wp-content/uploads/2016/06/getusermedia_chrome_ng.png 471w" sizes="(max-width: 300px) 100vw, 300px" /></a>「常に許可する」を選択</p>

<p><strong>Edgeの場合</strong><br />
※いまのところ「常に拒否」することはできないようなので、このケースは発生しません。</p>

<h2>新旧 getUserMedia() をラップしてみると</h2>

<p>新しいAPIである navigator.mediaDevices.getUserMedia()は、まだChromeではデフォルトの状態では使えません。これは、引数で渡すオプションの指定方法が、まだ新しい書き方に沿っていないことが原因です。
</p><pre class="crayon-plain-tag">let deviceID = getSelectedIDsomehow(); // デバイスIDを指定する

// Firefoxの場合
let constraints = {
  audio: false,
  video: { 
   deviceId: {exact: deviceID}
  }
};

// Chromeの場合
let constraintsForChrome = {
  audio: false,
  video: {
    optional: [{sourceId: deviceID}]
  }
};</pre><p></p>

<p>そのため、Chromeではしばらく新しいAPIはデフォルトでは使えない状態が続くと思われます。そこで古いAPIをPromise型にラップする例を用意してみました。（オプション指定の違いは吸収できていません）
</p><pre class="crayon-plain-tag">// --- prefix -----
  navigator.getUserMedia  = navigator.getUserMedia    || navigator.webkitGetUserMedia ||
                            navigator.mozGetUserMedia || navigator.msGetUserMedia;

  // ---- 新旧APIをPromiseでラップする ----
  function getDeviceStream(option) {
    if ('getUserMedia' in navigator.mediaDevices) {
      console.log('navigator.mediaDevices.getUserMadia');
      return navigator.mediaDevices.getUserMedia(option);
    }
    else {
      console.log('wrap navigator.getUserMadia with Promise');
      return new Promise(function(resolve, reject){    
        navigator.getUserMedia(option,
          resolve,
          reject
        );
      });      
    }
  }

  // 利用例
  function startVideo() {
    getDeviceStream({video: true, audio: false})
    .then(function (stream) { // success
      localStream = stream;
      localVideo.src = window.URL.createObjectURL(localStream);
    }).catch(function (error) { // error
      console.error('getUserMedia error:', error);
      return;
    });
  }</pre><p> 
これを使ってカメラ映像を取得する例もご用意しました。Chrome 51でもデフォルトのままで利用できます。</p>

<ul>
<li>GitHub Pages で試す <a target="_blank" href="https://mganeko.github.io/webrtcexpjp/basic2016/camera_old_new.html" data-wpel-link="external" rel="follow external noopener noreferrer">camera_old_new.html</a></li>
<li>GitHub でソースを見る <a target="_blank" href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/camera_old_new.html" data-wpel-link="external" rel="follow external noopener noreferrer">camera_old_new.html</a></li>
</ul>

<p>様々なWebRTC関連のブラウザの違いを吸収する <code>adapter.js</code> が <a href="https://github.com/webrtc/adapter" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">本家のGitHub</a>で公開されています。より便利に使いたい場合は、<a href="http://webrtc.github.io/adapter/adapter-latest.js" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">そちらを</a>ご利用ください。</p>

<h2>CSS3と組み合わせてみよう</h2>

<p>WebRTCは他の要素と組み合わせて使うことができると書きました。実際にCSS3と組み合わせてみましょう。</p>

<p></p><pre class="crayon-plain-tag">&lt;!-- 通常 --&gt;
  &lt;video id="local_video" autoplay style="width: 320px; height: 240px; border: 1px solid black;"&gt;&lt;/video&gt;

  &lt;!-- 左右反転 --&gt;
  &lt;video id="flip_video" autoplay style="transform: scaleX(-1); width: 320px; height: 240px; border: 1px solid black;"&gt;&lt;/video&gt;

  &lt;!-- 角丸 --&gt;
  &lt;video id="round_video" autoplay style="border-radius: 80px 80px 80px 80px; width: 320px; height: 240px; border: 1px solid black;"&gt;&lt;/video&gt;

  &lt;!-- セピア --&gt;
  &lt;video id="filter_video" autoplay style="filter: sepia(100%); -webkit-filter: sepia(100%); width: 320px; height: 240px; border: 1px solid black;"&gt;&lt;/video&gt;</pre><p> 
ほかにも様々なバリエーションが考えられます。ぜひ自分でもいろいろ試してみてください。</p>

<p>また、CSS3アニメーションを使うこともできます。例えばこんな感じです。
</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;Camera with mediaDevice&lt;/title&gt;
 &lt;style type="text/css"&gt;
   #rotate_video
   {
    animation-duration:4s;
    animation-iteration-count:infinite;
    animation-timing-function:linear;
    animation-name:rotate360;     
   }

   #shake_video
   {
    animation-duration:0.5s;
    animation-iteration-count:infinite;
    animation-timing-function:ease-in-out;
    animation-name:shake;     
   }
   
   @keyframes rotate360
   {
    0%{transform:rotate(0deg);}
    100%{transform:rotate(360deg);}
   }

   @keyframes shake
   {
    0%{transform:rotate(-20deg);}
    50%{transform:rotate(20deg);}
    100%{transform:rotate(-20deg);}
   }
  &lt;/style&gt; 
&lt;/head&gt;
&lt;body&gt;
  Camera with mediaDevice.getUserMedia()&lt;br /&gt;
  &lt;button onclick="startVideo()"&gt;Start&lt;/button&gt;
  &lt;br /&gt;
  &lt;!-- 通常 --&gt;
  &lt;video id="local_video" autoplay style="width: 320px; height: 240px; border: 1px solid black;"&gt;&lt;/video&gt;
  &lt;br /&gt;

  &lt;!-- 回転アニメーション --&gt;
  &lt;video id="rotate_video" autoplay style="width: 320px; height: 240px; border: 1px solid black;"&gt;&lt;/video&gt;

  &lt;!-- 振動アニメーション --&gt;
  &lt;video id="shake_video" autoplay style="width: 320px; height: 240px; border: 1px solid black;"&gt;&lt;/video&gt;
  
&lt;/body&gt;
&lt;script type="text/javascript"&gt;
  // --- prefix -----
  navigator.getUserMedia  = navigator.getUserMedia    || navigator.webkitGetUserMedia ||
                            navigator.mozGetUserMedia || navigator.msGetUserMedia;

  let localVideo = document.getElementById('local_video');
  let localStream;
  
  // start local video
  function startVideo() {
    getDeviceStream({video: true, audio: false})
    .then(function (stream) { // success
      localStream = stream;
      localVideo.src = window.URL.createObjectURL(localStream);
	  
      document.getElementById('rotate_video').src = localVideo.src;
      document.getElementById('shake_video').src = localVideo.src;
    }).catch(function (error) { // error
      console.error('mediaDevice.getUserMedia() error:', error);
      return;
    });
  }

  // ---- 新旧APIをPromiseでラップする ----
  function getDeviceStream(option) {
    if ('getUserMedia' in navigator.mediaDevices) {
      console.log('navigator.mediaDevices.getUserMadia');
      return navigator.mediaDevices.getUserMedia(option);
    }
    else {
      console.log('wrap navigator.getUserMadia with Promise');
      return new Promise(function(resolve, reject){    
        navigator.getUserMedia(option,
          resolve,
          reject
        );
      });      
    }
  }
&lt;/script&gt;
&lt;/html&gt;</pre><p> 
<a href="https://html5experts.jp/wp-content/uploads/2016/06/camera_css_animation.png" data-wpel-link="internal"><img src="/wp-content/uploads/2016/06/camera_css_animation-300x123.png" alt="camera_css_animation" width="300" height="123" class="alignnone size-medium wp-image-19778" srcset="/wp-content/uploads/2016/06/camera_css_animation-300x123.png 300w, /wp-content/uploads/2016/06/camera_css_animation.png 640w, /wp-content/uploads/2016/06/camera_css_animation-207x85.png 207w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>CSS3と組み合わせたサンプルも、GitHub Pagesでも公開していますので、試してみてください。（ラップ関数を使っているので、デフォルトのChrome 51でも利用できます）</p>

<ul>
<li>GitHub Pages で試す <a target="_blank" href="https://mganeko.github.io/webrtcexpjp/basic2016/camera_css_wrap.html" data-wpel-link="external" rel="follow external noopener noreferrer">camera_css_wrap.html</a></li>
<li>GitHub でソースを見る <a target="_blank" href="https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/camera_css_wrap.html" data-wpel-link="external" rel="follow external noopener noreferrer">camera_css_wrap.html</a></li>
</ul>

<h2>次回は</h2>

<p>今回は新しいAPIを使って、カメラの映像を取得してみました。次回はWebRTCによる通信を手動で接続してみます。</p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTC入門2016]]></series:name>
	</item>
	</channel>
</rss>
