<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:series="http://organizeseries.com/"
	>

<channel>
	<title>WebRTC &#8211; HTML5Experts.jp</title>
	<atom:link href="/series/webrtc/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>SENSEI NOTEのWebRTC導入事例─Opentokで安定したビデオチャットを提供する</title>
		<link>/sue738/11431/</link>
		<pubDate>Fri, 21 Nov 2014 00:00:26 +0000</pubDate>
		<dc:creator><![CDATA[末永昌也]]></dc:creator>
				<category><![CDATA[最新動向]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=11431</guid>
		<description><![CDATA[連載： WebRTC (4)はじめまして、LOUPEの末永です。本記事では、私たちが運営している先生専用SNS「SENSEI NOTE」でのWebRTC導入事例について、ご紹介させていただきます。 全国の先生がつながるS...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc/" class="series-395" title="WebRTC" data-wpel-link="internal">WebRTC</a> (4)</div><p>はじめまして、LOUPEの末永です。本記事では、私たちが運営している先生専用SNS「<a href="https://senseinote.com/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">SENSEI NOTE</a>」でのWebRTC導入事例について、ご紹介させていただきます。</p>

<h2>全国の先生がつながるSENSEI NOTE</h2>

<p>SENSEI NOTEは「全国の先生がつながる」をコンセプトにした先生専用SNSです。招待もしくは申請制で、小学校から高校までの学校の先生が、ご利用いただけます。サイトの中では先生が他の先生とのつながりを作れたり、学校で起きている課題の相談や、授業のTips共有をすることができます。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/11/f6e6a878-093c-ab2a-1640-b0edb26c26f2.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/f6e6a878-093c-ab2a-1640-b0edb26c26f2.png" alt="f6e6a878-093c-ab2a-1640-b0edb26c26f2" width="934" height="616" class="alignnone size-full wp-image-11389" srcset="/wp-content/uploads/2014/11/f6e6a878-093c-ab2a-1640-b0edb26c26f2.png 640w, /wp-content/uploads/2014/11/f6e6a878-093c-ab2a-1640-b0edb26c26f2-300x197.png 300w, /wp-content/uploads/2014/11/f6e6a878-093c-ab2a-1640-b0edb26c26f2-207x136.png 207w" sizes="(max-width: 934px) 100vw, 934px" /></a></p>

<p>SENSEI NOTEでは、テキストやファイルベースのナレッジ共有が主たるものでした。しかし、悩みや課題は誰かと話をすることで本当の課題や解決策は見えてくるものですし、全国には小規模学校で同じ科目を教える先生が一人だけだったり、地理的に他の学校との隔たりが多い学校もあります。そのような状況で、ビデオチャットを用いて全国の先生とやりとりできる場は非常に重要と考え、WebRTCに着目しました。</p>

<h2>WebRTC導入時に直面した2つの課題</h2>

<p>WebRTCは近年、PeerJSやSkyWayなどのプラグインの登場により、実装が非常に容易になってきました。ですが、実環境で運用しようとすると、かなりの困難を伴います。</p>

<h3>1. フロントエンジニアが困るシグナリング、NAT越え問題</h3>

<p>WebRTCはHTML5で策定されている規格で、フロントエンジニアの方が注目していると思います。たしかに接続のための処理はJavascriptで構築することができますし、PeerJSやSkyWayといったライブラリを用いることで簡単に実装することができるようになってきました。</p>

<p>ところが、インターネット越しにWebRTCでビデオチャットをしようとすると、別の問題が発生します。サーバを介してセッションの交換をする必要がありますし、NATの構成によってはNAT越えのためにSTUNサーバ(公開されているサーバもあります)や、TURNサーバを構築しなければなりません。</p>

<h3>2. もっと厄介な多人数接続問題</h3>

<p>もう一つの問題が、多人数でのビデオチャットの問題です。上記がクリアできたとしても、いざ5人以上でビデオチャットを開始すると、次第にパソコンが唸り始めます。WebRTCで多人数接続をする際に、接続したそれぞれのPCとコネクションを張る形(メッシュ接続)となり、CPUが負荷に耐えられなくなるためです。SENSEI NOTEにて、ユーザに安定したサービスを提供する上ではこちらが非常に大きな課題となりました。</p>

<h2>問題を解決するOpentok WebRTC Platform</h2>

<p>これらの問題を解決するために、SENSEI NOTEでは<a href="https://tokbox.com/opentok/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">OpenTok</a>というWebRTC Platformを採用しました。OpenTokは、WebRTCを用いて簡単にリアルタイムコネクションを構築するAPIを、多数提供しています。OpenTokはWebRTCの厄介な部分を、諸々解決してくれるサービスです。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/11/e41ff7f3-70b1-0a1b-796f-ad9fee418f53.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/e41ff7f3-70b1-0a1b-796f-ad9fee418f53.png" alt="e41ff7f3-70b1-0a1b-796f-ad9fee418f53" width="985" height="636" class="alignnone size-full wp-image-11394" srcset="/wp-content/uploads/2014/11/e41ff7f3-70b1-0a1b-796f-ad9fee418f53.png 640w, /wp-content/uploads/2014/11/e41ff7f3-70b1-0a1b-796f-ad9fee418f53-300x193.png 300w, /wp-content/uploads/2014/11/e41ff7f3-70b1-0a1b-796f-ad9fee418f53-207x133.png 207w" sizes="(max-width: 985px) 100vw, 985px" /></a></p>

<p>料金も月額50ドルからの従量課金で始められます。30日間のフリートライアルも用意されているので、その期間で十分に実装と動作テストを行うことが可能です。</p>

<h2>チュートリアルも充実したフロントサイド実装</h2>

<p>フロントサイドの実装は、非常に簡単です。下記のコードだけ、ビデオチャットを行うことが可能になります。</p>

<p></p><pre class="crayon-plain-tag">&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;Opentok Quick Start&lt;/title&gt;
    &lt;script src='http://static.opentok.com/webrtc/v2.2/js/opentok.min.js'&gt;&lt;/script&gt;
    &lt;script type="text/javascript"&gt;
      var apiKey = "your-api-key";
      var sessionId = "your-session-id";
      var token = "your-token";

      var session = OT.initSession(apiKey, sessionId);
 
      session.on("streamCreated", function(event) {
        session.subscribe(event.stream);
      });
     
      session.connect(token, function(error) {
        var publisher = OT.initPublisher();
        session.publish(publisher);
      });
    &lt;/script&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="myPublisherDiv"&gt;&lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;</pre><p></p>

<p>本家サイトにはステップバイステップ形式のチュートリアルも用意されているので、内容を理解しながら進めることが可能です。</p>

<h2>充実したサーバーSDK</h2>

<p>SENSEI NOTEは、Ruby on Railsを使っています。下記では簡単に、Ruby on Railsでのサーバサイド実装について紹介したいと思います。</p>

<p>gemが用意されているので、bundlerを用いてgemのインストールを行います。</p>

<p></p><pre class="crayon-plain-tag">gem "opentok", "~&gt; 2.2"</pre><p></p>

<p>Opentokに登録するとapi_key, api_secretが発行されるので、それを元にOpenTok::OpenTokオブジェクトを初期化し、ビデオチャットを構築するためのsession、そのsessionに入るためのtokenを生成します。</p>

<p></p><pre class="crayon-plain-tag">require "opentok"
opentok = OpenTok::OpenTok.new api_key, api_secret

session = opentok.create_session
token = session.generate_token</pre><p></p>

<p>これを先ほどのにパラメータを渡すことで、簡単にビデオチャットを開始することができます。以下にSENSEI NOTEでのビデオチャットの例をご紹介します。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/11/7024652e-1481-eefa-4fa3-b7ac15f59d6a.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/7024652e-1481-eefa-4fa3-b7ac15f59d6a.png" alt="7024652e-1481-eefa-4fa3-b7ac15f59d6a" width="813" height="475" class="alignnone size-full wp-image-11388" srcset="/wp-content/uploads/2014/11/7024652e-1481-eefa-4fa3-b7ac15f59d6a.png 640w, /wp-content/uploads/2014/11/7024652e-1481-eefa-4fa3-b7ac15f59d6a-300x175.png 300w, /wp-content/uploads/2014/11/7024652e-1481-eefa-4fa3-b7ac15f59d6a-207x120.png 207w" sizes="(max-width: 813px) 100vw, 813px" /></a></p>

<p>Ruby on Railsでのサーバサイド実装について紹介しましたが、他の言語のSDKも充実していますので、お使いの環境に合わせて参照してみてください。</p>

<ul>
<li><a href="https://tokbox.com/opentok/libraries/server/java/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">Java(Official)</a></li>
<li><a href="https://tokbox.com/opentok/libraries/server/php/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">PHP(Official)</a></li>
<li><a href="https://tokbox.com/opentok/libraries/server/python/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">Python(Official)</a></li>
<li><a href="https://tokbox.com/opentok/libraries/server/ruby/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">Ruby(Official)</a></li>
<li><a href="https://tokbox.com/opentok/libraries/server/dot-net/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">.NET(Official)</a></li>
<li><a href="https://tokbox.com/opentok/libraries/server/node/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">Node.js(Official)</a></li>
</ul>

<h2>多人数接続問題を解決するMantis</h2>

<p>WebRTC実装において問題となっていた多人数接続ですが、Opentokには<a href="http://www.tokbox.com/blog/mantis-next-generation-cloud-technology-for-webrtc/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">Mantis</a>といった中継サーバがあります。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/11/af83ab6c-0c65-5482-c814-328331b9a0b3.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/af83ab6c-0c65-5482-c814-328331b9a0b3.png" alt="af83ab6c-0c65-5482-c814-328331b9a0b3" width="1164" height="656" class="alignnone size-full wp-image-11393" srcset="/wp-content/uploads/2014/11/af83ab6c-0c65-5482-c814-328331b9a0b3.png 640w, /wp-content/uploads/2014/11/af83ab6c-0c65-5482-c814-328331b9a0b3-300x169.png 300w, /wp-content/uploads/2014/11/af83ab6c-0c65-5482-c814-328331b9a0b3-1024x577.png 1024w, /wp-content/uploads/2014/11/af83ab6c-0c65-5482-c814-328331b9a0b3-207x116.png 207w" sizes="(max-width: 1164px) 100vw, 1164px" /></a></p>

<p>これにより、多数のpeer-to-peerの接続が張られる(メッシュ接続になる)のを防ぎ、多人数でも安定したビデオチャットの提供が可能となります。</p>

<h2>まとめ</h2>

<p>WebRTCは簡単に実装できるようになってきてはいるものの、実際にインターネットを通して接続しようとするといくつかの問題が発生します。リソースの限られているスタートアップにおいては、サーバ構築やメンテナンスへのコストを考えると、このようなライブラリを使ってスモールに始め、軌道に乗ったところで、本格的に社内実装(有料サービス・ライブラリを利用せずに実装)する流れがよいように思います。</p>

<p>WebRTCを用いたビデオチャットサービスはユーザに新しい体験を提供することが可能です。OpenTokを使うと非常に容易にビデオチャットサービスを構築することができますのでぜひ試してみてはいかがでしょうか。</p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTC]]></series:name>
	</item>
		<item>
		<title>WebRTCでキャスしよう！片方向リアルタイム映像配信を作ろう</title>
		<link>/mganeko/11444/</link>
		<pubDate>Thu, 20 Nov 2014 03:00:54 +0000</pubDate>
		<dc:creator><![CDATA[がねこまさし]]></dc:creator>
				<category><![CDATA[最新動向]]></category>
		<category><![CDATA[プログラミング]]></category>
		<category><![CDATA[WebRTC]]></category>

		<guid isPermaLink="false">/?p=11444</guid>
		<description><![CDATA[連載： WebRTC (3)こんにちは！がねこまさしです。「WebRTCを使ってみよう」シリーズの最新話をお送りします。今回は、簡易的な放送局を作ってみましょう。 片方向配信の特徴 WebRTCを使った音声通話、ビデオチ...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc/" class="series-395" title="WebRTC" data-wpel-link="internal">WebRTC</a> (3)</div><p>こんにちは！がねこまさしです。「WebRTCを使ってみよう」シリーズの最新話をお送りします。今回は、簡易的な放送局を作ってみましょう。</p>

<h2>片方向配信の特徴</h2>

<p>WebRTCを使った音声通話、ビデオチャットのサンプルには、双方向のものが多く見られます。ライブラリもそれを前提とした作りのモノが多いようです。なので今回は、片方向配信を実際に動かしてみましょう。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/11/multi_or_1tomany.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/multi_or_1tomany-300x182.png" alt="multi_or_1tomany" width="300" height="182" class="alignnone size-medium wp-image-11449" srcset="/wp-content/uploads/2014/11/multi_or_1tomany-300x182.png 300w, /wp-content/uploads/2014/11/multi_or_1tomany-207x125.png 207w, /wp-content/uploads/2014/11/multi_or_1tomany.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>片方向配信には、双方向通信とは異なる特徴があります。</p>

<ul>
    <li>視聴側はカメラやマイクといった機器が不要なので、参加のハードルが下がる</li>
    <li>Peer-to-Peerでもフルメッシュ構造にはならないので、より多くの人が同時に利用できる</li>
</ul>

<p>特に同時接続数はは双方向では4～5人が実用範囲なのに対し、片方向では10～30人程度に対して1つのPCから配信できます。ちょっとした仲間内のイベントや、社内イベントであれば、十分にカバーできるのではないでしょうか？（社内で動かせば、社内ネットワーク内で完結するので、セキュリティ部門に怒られることもないでしょうし&#8230;）</p>

<h2>片方向配信をつなぐまで</h2>

<p>今回の記事は、技術的には過去の記事<a href="https://html5experts.jp/mganeko/5438/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">「WebRTCを使って複数人で話してみよう」</a>の内容でカバーされています。違いはPeer-to-Peer接続までの手順にあります。</p>

<p>片方向配信では、話す側(talk)と見る側(watch)が非対称です。どちらから通信を始めるかで、2つのシナリオに分かれます。</p>

<h4>(A) 配信中に、新たに見る人(watch)が現れて、視聴を開始する</h4>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/11/begin_watch.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/begin_watch-300x232.png" alt="begin_watch" width="300" height="232" class="alignnone size-medium wp-image-11451" srcset="/wp-content/uploads/2014/11/begin_watch-300x232.png 300w, /wp-content/uploads/2014/11/begin_watch-207x160.png 207w, /wp-content/uploads/2014/11/begin_watch.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<p>この場合、Peer-to-Peer確立までのシグナリングの流れは、次のようになります。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/11/signaling_new_watch.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/signaling_new_watch-300x220.png" alt="signaling_new_watch" width="300" height="220" class="alignnone size-medium wp-image-11453" srcset="/wp-content/uploads/2014/11/signaling_new_watch-300x220.png 300w, /wp-content/uploads/2014/11/signaling_new_watch-207x152.png 207w, /wp-content/uploads/2014/11/signaling_new_watch.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<ul>
    <li>新しい視聴者(watchA)が、同じ部屋の中にいる全員に「映像ちょうだい(talk_request)」を送る</li>
    <li>他の視聴者(watchB)は、単に無視する</li>
    <li>配信側(talk)は新しくOffer SDPを生成し、watchAに対して送信する</li>
    <li>watchAはOffer SDPを受け取ったら、Answer SDPを生成してtalkに送り返す</li>
    <li>その後 ICE Candidate が複数交換され、最終的にPeer-to-Peerで片方向の映像ストリームが流れる</li>
</ul>

<h4>(2)視聴者(watch)が待機しているところに、話す側(talk)が配信を開始する</h4>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/11/begin_talk.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/begin_talk-300x230.png" alt="begin_talk" width="300" height="230" class="alignnone size-medium wp-image-11457" srcset="/wp-content/uploads/2014/11/begin_talk-300x230.png 300w, /wp-content/uploads/2014/11/begin_talk-207x158.png 207w, /wp-content/uploads/2014/11/begin_talk.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
こちらの場合は、Peer-to-Peer確立までのシグナリングの流れは、次のようになります。<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/11/signaling_new_talk3.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/signaling_new_talk3-300x225.png" alt="signaling_new_talk" width="300" height="225" class="alignnone size-medium wp-image-11462" srcset="/wp-content/uploads/2014/11/signaling_new_talk3-300x225.png 300w, /wp-content/uploads/2014/11/signaling_new_talk3-207x155.png 207w, /wp-content/uploads/2014/11/signaling_new_talk3.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<ul>
    <li>配信側(talk)が、同じ部屋の中にいる全員に「始めるよー(talk_ready)」を送る</li>
    <li>各視聴者(watchA, watchB)は、「映像ちょうだい(talk_request)」を返す</li>
    <li>配信側(talk)は各視聴者ごとに別々のOffer SDPを生成し、それぞれ送信する</li>
    <li>各視聴者(watch)はOffer SDPを受け取ったら、Answer SDPを生成してtalkに送り返す</li>
    <li>その後 ICE Candidate が複数交換され、最終的にPeer-to-Peerで片方向の映像ストリームが流れる</li>
</ul>

<p>先ほどのシーケンスの前に、開始の合図(talk_ready)を加えただけですね。</p>

<h2>シグナリングサーバーのソースコード</h2>

<p>シグナリングサーバーは、過去の記事<a href="https://html5experts.jp/mganeko/5438/" target="_blank" data-wpel-link="external" rel="follow external noopener noreferrer">「WebRTCを使って複数人で話してみよう」</a>と同じものが使えます。ただしこの時はSocket.IO v0.9を使っていたので、今回はSocket.IO v1.0/v1.1の場合を掲載しておきます。サーバー開始の部分と、部屋名の保持の仕方が少し異なります。</p>

<p></p><pre class="crayon-plain-tag">var BROADCAST_ID = '_broadcast_';

// -- create the socket server on the port ---
var srv = require('http').Server();
var io = require('socket.io')(srv);
var port = 9001;
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) {
      //// for v0.9
      //socket.set('roomname', room);

      // for v1.0
      socket.roomname = room;
    }

    function getRoomname() {
      var room = null;

      //// for v0.9
      //socket.get('roomname', function(err, _room) {
      //  room = _room;
      //});

      // for v1.0
      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) {
        message.from = socket.id;

        // get send target
        var target = message.sendto;
        if ( (target) &amp;&amp; (target != BROADCAST_ID) ) {
          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() {
        console.log('-- user disconnect: ' + socket.id);
        // --- emit ----
        emitMessage('user disconnected', {id: socket.id});

        // --- leave room --
        var roomname = getRoomname();
        if (roomname) {
          socket.leave(roomname);
        }

    });

});</pre><p></p>

<h2>クライアント：配信側(talk)のソースコード</h2>

<p>以前の複数人でのケースとの違いを見ていきましょう。配信側では、相手側の映像を受け取る必要がないので、複数のvideoを処理する部分はごっそり削れます。また配信側の通信処理は、複数人で話す場合ととても近いです。<br /></p>

<h4>Peer-to-Peerの管理</h4>

<p>以前と同じく、複数のPeer-to-Peer接続を管理するために便宜上のクラスと、関連する関数を作っておきます。</p>

<p></p><pre class="crayon-plain-tag">// -------------- multi connections --------------------
  var MAX_CONNECTION_COUNT = 10;
  var connections = {}; // Connection hash
  function Connection() { // Connection Class
    var self = this;
    var id = "";  // socket.id of partner
    var peerconnection = null; // RTCPeerConnection instance
    //var established = false; // is Already Established
    //var iceReady = false;
  }

  function getConnection(id) {
    var con = null;
    con = connections[id];
    return con;
  }

  function addConnection(id, connection) {
    connections[id] = connection;
  }

  function getConnectionCount() {
    var count = 0;
    for (var id in connections) {
      count++;
    }

    console.log('getConnectionCount=' + count);
    return count;
  }

  function isConnectPossible() {
    if (getConnectionCount() &lt; MAX_CONNECTION_COUNT)
      return true;
    else
      return false;
  }

  function getConnectionIndex(id_to_lookup) {
    var index = 0;
    for (var id in connections) {
      if (id == id_to_lookup) {
        return index;
      }

      index++;
    }

    // not found
    return -1;
  }

  function deleteConnection(id) {
    delete connections[id];
  }

  function stopAllConnections() {
    for (var id in connections) {
      var conn = connections[id];
      conn.peerconnection.close();
      conn.peerconnection = null;
      delete connections[id];
    }
  }

  function stopConnection(id) {
    var conn = connections[id];
    if(conn) {
      console.log('stop and delete connection with id=' + id);
      conn.peerconnection.close();
      conn.peerconnection = null;
      delete connections[id];
    }
    else {
      console.log('try to stop connection, but not found id=' + id);
    }
  }

  function isPeerStarted() {
    if (getConnectionCount() &gt; 0) {
      return true;
    }
    else {
      return false;
    }
  }</pre><p> 
※以前は自前の過剰なステータス管理フラグを使っていましたが、無用なのでやめました。</p>

<h4>シグナリングサーバーへの接続とイベント処理</h4>

<p>シグナリングサーバーに対して、socket.ioクライアントを使って接続しておきます。また、接続時（会議室への入室）、切断時、メッセージ受信時のイベントハンドラを設定します。 
</p><pre class="crayon-plain-tag">// ---- socket ------
  // create socket
  var socketReady = false;
  var port = 9001;
  var socket = io.connect('http://signaling.yourdomain:/' + port + '/'); // サーバのURLに変更

  // socket: channel connected
  socket.on('connect', onOpened)
        .on('message', onMessage)
        .on('user disconnected', onUserDisconnect);

  function onOpened(evt) {
    console.log('socket opened.');
    socketReady = true;
	
    var roomname = getRoomName(); // 会議室名を取得する
    socket.emit('enter', roomname);
    console.log('enter to ' + roomname);
  }

  // socket: accept connection request
  function onMessage(evt) {
    var id = evt.from;
    var target = evt.sendto;
    var conn = getConnection(id);

    if (evt.type === 'talk_request') {
      if (! isLocalStreamStarted()) {
        console.warn('local stream not started. ignore request');
        return;
      }

      console.log("receive request, start offer.");
      sendOffer(id);
      return;
    }
    else if (evt.type === 'answer' &amp;&amp; isPeerStarted()) {  // **
      console.log('Received answer, settinng answer SDP');
      onAnswer(evt);
    } else if (evt.type === 'candidate' &amp;&amp; isPeerStarted()) { // **
      console.log('Received ICE candidate...');
      onCandidate(evt);
    }
    else if (evt.type === 'bye') { // **
      console.log("got bye.");
      stopConnection(id);
    }
  }

  function onUserDisconnect(evt) {
    console.log("disconnected");
    if (evt) {
      stopConnection(evt.id);
    }
  }

  function getRoomName() { // たとえば、 URLに  ?roomname  とする
    var url = document.location.href;
    var args = url.split('?');
    if (args.length &gt; 1) {
      var room = args[1];
      if (room != "") {
        return room;
      }
    }
    return "_defaultroom";
  }</pre><p> 
応答するメッセージは、talk_request, answer, candidate です。talk_ready, offerは来ないはずなので、処理はしていません。</p>

<h4>映像、音声の取得開始</h4>

<p></p><pre class="crayon-plain-tag">// ---------------------- video handling -----------------------
  // start local video
  function startVideo() {
    navigator.webkitGetUserMedia({video: true, audio: true},
      function (stream) { // success
        localStream = stream;
        localVideo.src = window.webkitURL.createObjectURL(stream);
        localVideo.play();
        localVideo.volume = 0;

        // auto start
        tellReady();
      },
      function (error) { // error
        console.error('An error occurred:');
        console.error(error);
        return;
      }
    );
  }</pre><p> 
特に変わったところはありませんが、映像取得後に tellReady()を呼び、その中で「準備できたよ(talk_ready)」と通知しています。</p>

<h4>配信要求への応答</h4>

<p>視聴側(watch)から、配信要求(talk_request)があった場合、次の処理が呼び出されます。
</p><pre class="crayon-plain-tag">var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':false }};

  function sendOffer(id) {
    var conn = getConnection(id);
    if (!conn) {
      conn = prepareNewConnection(id);
    }

    conn.peerconnection.createOffer(function (sessionDescription) { // in case of success
      conn.peerconnection.setLocalDescription(sessionDescription);
      sessionDescription.sendto = id;
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Offer failed");
    }, mediaConstraints);
  }

  // ---------------------- connection handling -----------------------
  function prepareNewConnection(id) {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create PeerConnection, exception: " + e.message);
    }
    var conn = new Connection();
    conn.id = id;
    conn.peerconnection = peer;
    peer.id = id;
    addConnection(id, conn);

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
        sendCandidate({type: "candidate", 
                          sendto: conn.id,
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate});
      } else {
        console.log("ICE event. phase=" + evt.eventPhase);
      }
    };

    console.log('Adding local stream...');
    peer.addStream(localStream);

    return conn;
  }</pre><p> 
複数人の双方向のケースと違い、映像や音声を受け取る必要がないので、mediaConstraintsの内容がどちらも受信不要(false)にしています。実際に通信を行うオブジェクトを用意するのは、prepareNewConnection()の中で行っています。</p>

<ul>
    <li>RTCPeerConnectionを生成</li>
    <li>ICE candidate生成時のイベントハンドラを設定</li>
</ul>

<h4>配信開始時の通知</h4>

<p>反対に、配信側(talk)から新たに配信開始を通知する処理はこちらです。単にsocket越しに部屋内にメッセージを投げるだけですね。
</p><pre class="crayon-plain-tag">function tellReady() {
    if (! isLocalStreamStarted()) {
      alert("Local stream not running yet. Please [Start Video] or [Start Screen].");
      return;
    }
    if (! socketReady) {
      alert("Socket is not connected to server. Please reload and try again.");
      return;
    }

    // call others, in same room
    console.log("tell ready to others in same room, befeore offer");
    socket.json.send({type: "talk_ready"});
  }</pre><p></p>

<h4>SDP、ICEのやり取り</h4>

<p>SDPやICE Candidateも、socket越しに相手(1人)に送るだけです。
</p><pre class="crayon-plain-tag">function sendSDP(sdp) {
    var text = JSON.stringify(sdp);
    console.log("---sending sdp text ---");
    console.log(text);
	
    // send via socket
    socket.json.send(sdp);
  }

  function sendCandidate(candidate) {
    var text = JSON.stringify(candidate);
    console.log("---sending candidate text ---");
    console.log(text);
	
    // send via socket
    socket.json.send(candidate);
  }</pre><p> 
Answerや、Candidateを受け取った場合は、PeerConnectionに覚えさせます。
</p><pre class="crayon-plain-tag">function setAnswer(evt) {
    var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
	  console.error('peerConnection not exist!');
	  return
    }
    conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt));
  }
  
  function onCandidate(evt) {
   var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
      console.error('peerConnection not exist!');
      return;
    }
   
    var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate});
    console.log("Received Candidate...")
    console.log(candidate);
    conn.peerconnection.addIceCandidate(candidate);
  }</pre><p></p>

<h2>クライアント：視聴側(watch)のソースコード</h2>

<p>視聴側は自分のユーザーメディアは取得しません。映像も1つだけ受け取るので処理はシンプルです。</p>

<h4>Peer-to-Peerの管理</h4>

<p>Peer-to-Peerは1つだけなので、本来便宜上のクラス、関数は不要です。とは言えtalk側と共通にするために（あるいは複数人からの変更を減らすため）、同じコードにしておきました。</p>

<p></p><pre class="crayon-plain-tag">// -------------- multi connections --------------------
  var MAX_CONNECTION_COUNT = 1;
  var connections = {}; // Connection hash
  function Connection() { // Connection Class
    var self = this;
    var id = "";  // socket.id of partner
    var peerconnection = null; // RTCPeerConnection instance
  }

  function getConnection(id) {
    var con = null;
    con = connections[id];
    return con;
  }

  function addConnection(id, connection) {
    connections[id] = connection;
  }

  function getConnectionCount() {
    var count = 0;
    for (var id in connections) {
      count++;
    }

    console.log('getConnectionCount=' + count);
    return count;
  }

  function isConnectPossible() {
    if (getConnectionCount() &lt; MAX_CONNECTION_COUNT)
      return true;
    else
      return false;
  }

  function getConnectionIndex(id_to_lookup) {
    var index = 0;
    for (var id in connections) {
      if (id == id_to_lookup) {
        return index;
      }

      index++;
    }

    // not found
    return -1;
  }

  function deleteConnection(id) {
    delete connections[id];
  }

  function stopAllConnections() {
    for (var id in connections) {
      var conn = connections[id];
      conn.peerconnection.close();
      conn.peerconnection = null;
      delete connections[id];
    }
  }

  function stopConnection(id) {
    var conn = connections[id];
    if(conn) {
      console.log('stop and delete connection with id=' + id);
      conn.peerconnection.close();
      conn.peerconnection = null;
      delete connections[id];
    }
    else {
      console.log('try to stop connection, but not found id=' + id);
    }
  }

  function isPeerStarted() {
    if (getConnectionCount() &gt; 0) {
      return true;
    }
    else {
      return false;
    }
  }</pre><p> 
※同じと言いましたが、1点違いがありました。 MAX_CONNECTION_COUNT = 1 にしています。</p>

<h4>シグナリングサーバーへの接続とイベント処理</h4>

<p>シグナリングサーバーへの接続はtalkと同じです。処理するメッセージの種類が異なります。</p>

<p></p><pre class="crayon-plain-tag">// ---- socket ------
  // create socket
  var socketReady = false;
  var port = 9001;
  var socket = io.connect('http://signaling.yourdomain:/' + port + '/');
  
  // socket: channel connected
  socket.on('connect', onOpened)
        .on('message', onMessage)
        .on('user disconnected', onUserDisconnect);

  function onOpened(evt) {
    console.log('socket opened.');
    socketReady = true;
	
    var roomname = getRoomName(); // 会議室名を取得する
    socket.emit('enter', roomname);
    console.log('enter to ' + roomname);
  }

  // socket: accept connection request
  function onMessage(evt) {
    var id = evt.from;
    var target = evt.sendto;
    var conn = getConnection(id);

    if (evt.type === 'talk_ready') {
      if (conn) {
        return;  // already connected
      }

      if (isConnectPossible()) {
        socket.json.send({type: "talk_request", sendto: id });
      }
      else {
        console.warn('max connections. so ignore call');
      }
      return;
    }
    else if (evt.type === 'offer') {
      console.log("Received offer, set offer, sending answer....")
      onOffer(evt);	  
    }
    else if (evt.type === 'candidate' &amp;&amp; isPeerStarted()) { // **
      console.log('Received ICE candidate...');
      onCandidate(evt);
    }
    else if (evt.type === 'end_talk') { // **
      console.log("got talker bye.");
      detachVideo(id); // force detach video
      stopConnection(id);
    }

  }

  function onUserDisconnect(evt) {
    console.log("disconnected");
    if (evt) {
      detachVideo(evt.id); // force detach video
      stopConnection(evt.id);
    }
  }

  function getRoomName() { // たとえば、 URLに  ?roomname  とする
    var url = document.location.href;
    var args = url.split('?');
    if (args.length &gt; 1) {
      var room = args[1];
      if (room != "") {
        return room;
      }
    }
    return "_defaultroom";
  }</pre><p> 
応答するメッセージは talk_ready, offer, candidate です。talk_requestやanswerには反応しません。</p>

<h4>配信の要求</h4>

<p>配信の依頼は、socket.ioで会議室内全員に（相手を特定せずに）投げます。 
</p><pre class="crayon-plain-tag">function sendRequest() {
    if (! socketReady) {
      alert("Socket is not connected to server. Please reload and try again.");
      return;
    }

    // call others, in same room
    console.log("send request in same room, ask for offer");
    socket.json.send({type: "talk_request"});

  }</pre><p></p>

<h4>SDP受信時の処理、ストリーム受信時の処理</h4>

<p>talkからOffer SDPを受け取ったら、Peer-to-Peer通信の準備をします。PeerConnectionを生成し、Offerを覚えて、Answerを返します。</p>

<p></p><pre class="crayon-plain-tag">var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }};

  function setOffer(evt) {
    var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
      conn = prepareNewConnection(id);
      conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt));
    }
    else {
      console.error('peerConnection alreay exist!');
    }
  }
  
  function sendAnswer(evt) {
    console.log('sending Answer. Creating remote session description...' );
    var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
      console.error('peerConnection not exist!');
      return;
    }

    conn.peerconnection.createAnswer(function (sessionDescription) { 
      // in case of success
      conn.peerconnection.setLocalDescription(sessionDescription);
      sessionDescription.sendto = id;
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Answer failed");
    }, mediaConstraints);
  }</pre><p> 
PeerConnectionを準備しているのは、prepareNewConnection()の中です。talkとほとんど同じですが、一部異なる部分があります。</p>

<p></p><pre class="crayon-plain-tag">// ---------------------- connection handling -----------------------
  function prepareNewConnection(id) {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create PeerConnection, exception: " + e.message);
    }
    var conn = new Connection();
    conn.id = id;
    conn.peerconnection = peer;
    peer.id = id;
    addConnection(id, conn);

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
        sendCandidate({type: "candidate", 
                          sendto: conn.id,
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate});
      } else {
        console.log("on ice event. phase=" + evt.eventPhase);
      }
    };

    //console.log('Adding local stream...');
    //peer.addStream(localStream); // 自分の映像ストリームは無し

    // 相手のストリームのハンドラを追加
    peer.addEventListener("addstream", onRemoteStreamAdded, false);
    peer.addEventListener("removestream", onRemoteStreamRemoved, false)

    // when remote adds a stream, hand it on to the local video element
    function onRemoteStreamAdded(event) {
      console.log("Added remote stream");
      remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
    }

    // when remote removes a stream, remove it from the local video element
    function onRemoteStreamRemoved(event) {
      console.log("Remove remote stream");
      detachVideo(this.id);
    }

    return conn;
  }</pre><p> 
自分のストリーム(localStream)がない代わりに、相手のストリーム(RemoteStream)のハンドラを用意しています。</p>

<h4>配信開始通知を受けた場合</h4>

<p>talk側から talk_ready を受け取った場合、まだ接続が確立していなければ、talk_requestを返します。その後処理は配信の要求と同様に進みます。</p>

<p></p><pre class="crayon-plain-tag">function onMessage(evt) {
    var id = evt.from;
    var target = evt.sendto;
    var conn = getConnection(id);

    if (evt.type === 'talk_ready') {
      if (conn) {
        return;  // already connected
      }

      if (isConnectPossible()) {
        socket.json.send({type: "talk_request", sendto: id });
      }
      else {
        console.warn('max connections. so ignore call');
      }
      return;
    }</pre><p></p>

<h2>動かしてみよう</h2>

<p>配信前<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/11/before_onair.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/before_onair-300x189.png" alt="before_onair" width="300" height="189" class="alignnone size-medium wp-image-11480" srcset="/wp-content/uploads/2014/11/before_onair-300x189.png 300w, /wp-content/uploads/2014/11/before_onair-207x130.png 207w, /wp-content/uploads/2014/11/before_onair.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
配信開始後<br />
<a href="https://html5experts.jp/wp-content/uploads/2014/11/after_onair.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/after_onair-300x188.png" alt="after_onair" width="300" height="188" class="alignnone size-medium wp-image-11481" srcset="/wp-content/uploads/2014/11/after_onair-300x188.png 300w, /wp-content/uploads/2014/11/after_onair-207x130.png 207w, /wp-content/uploads/2014/11/after_onair.png 640w" sizes="(max-width: 300px) 100vw, 300px" /></a><br />
このように、複数のブラウザに配信を行うことができます。ぜひ社内などで試してみてください。シグナリングサーバーを流用してテキストチャットなどを付けると、より楽しく利用できますよ！<br />
※今回のソースには含まれていませんが、TURNサーバーを用意すれば、Firewall/NATの内側から外側に配信することも可能です。</p>

<h2>クライアント側のソースコード（全体）</h2>

<h4>配信側と視聴側の主な違い</h4>

<p>配信側(talk)と視聴側(watch)の仕組みは似通っていますが、違う部分もあります。改めて違う部分を整理しておきます。<br />
<b>(1) 配信側だけがユーザーメディアを取得する。PeerConnectionを生成したときに、そのストリームを追加する。</b></p>

<ul>
    <li>peerconnection.addStream(localStream)</li>
</ul>

<p><br />
<b>(2) 受信側だけが、相手のメディアストリームの接続、除去イベントを処理する。</b></p>

<ul>
    <li>peer.addEventListener(&#8220;addstream&#8221;, onRemoteStreamAdded, false);</li>
    <li>peer.addEventListener(&#8220;removestream&#8221;, onRemoteStreamRemoved, false);</li>
</ul>

<p><br />
<b>(3) 配信側と受信側は異なるメッセージに応答する。</b></p>

<ul>
    <li>配信側だけ：talk_request, answer</li>
    <li>受信側だけ：talk_ready, offer</li>
</ul>

<h4>配信側(talk.html)</h4>

<p></p><pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;Broadcast Talk&lt;/title&gt;  
  &lt;meta http-equiv="content-type" content="text/html; charset=UTF-8"&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;button type="button" onclick="startVideo();"&gt;Start video&lt;/button&gt;
  &lt;button type="button" onclick="stopVideo();"&gt;Stop video&lt;/button&gt;
  &amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
  &lt;button type="button" onclick="tellReady();"&gt;On Air&lt;/button&gt;
  &lt;br /&gt;
  &lt;div style="position: relative;"&gt;
   &lt;video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"&gt;&lt;/video&gt;
  &lt;/div&gt;
  
  &lt;!---- socket ※自分のシグナリングサーバーに合わせて変更してください------&gt;
  &lt;script src="http://signaling.yourdomain:9001/socket.io/socket.io.js"&gt;&lt;/script&gt;
  
  &lt;script&gt;
  var localVideo = document.getElementById('local-video');
  var localStream = null;
  var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':false }};

  
  function isLocalStreamStarted() {
    if (localStream) {
      return true;
    }
    else {
      return false;
    }
  }

  // -------------- multi connections --------------------
  var MAX_CONNECTION_COUNT = 10;
  var connections = {}; // Connection hash
  function Connection() { // Connection Class
    var self = this;
    var id = "";  // socket.id of partner
    var peerconnection = null; // RTCPeerConnection instance
  }

  function getConnection(id) {
    var con = null;
    con = connections[id];
    return con;
  }

  function addConnection(id, connection) {
    connections[id] = connection;
  }

  function getConnectionCount() {
    var count = 0;
    for (var id in connections) {
      count++;
    }

    console.log('getConnectionCount=' + count);
    return count;
  }

  function isConnectPossible() {
    if (getConnectionCount() &lt; MAX_CONNECTION_COUNT)
      return true;
    else
      return false;
  }

  function getConnectionIndex(id_to_lookup) {
    var index = 0;
    for (var id in connections) {
      if (id == id_to_lookup) {
        return index;
      }

      index++;
    }

    // not found
    return -1;
  }

  function deleteConnection(id) {
    delete connections[id];
  }

  function stopAllConnections() {
    for (var id in connections) {
      var conn = connections[id];
      conn.peerconnection.close();
      conn.peerconnection = null;
      delete connections[id];
    }
  }

  function stopConnection(id) {
    var conn = connections[id];
    if(conn) {
      console.log('stop and delete connection with id=' + id);
      conn.peerconnection.close();
      conn.peerconnection = null;
      delete connections[id];
    }
    else {
      console.log('try to stop connection, but not found id=' + id);
    }
  }

  function isPeerStarted() {
    if (getConnectionCount() &gt; 0) {
      return true;
    }
    else {
      return false;
    }
  }

  
  // ---- socket ------
  // create socket
  var socketReady = false;
  var port = 9001;
  var socket = io.connect('http://signaling.yourdomain:/' + port + '/'); // ※自分のシグナリングサーバーに合わせて変更してください

  // socket: channel connected
  socket.on('connect', onOpened)
        .on('message', onMessage)
        .on('user disconnected', onUserDisconnect);

  function onOpened(evt) {
    console.log('socket opened.');
    socketReady = true;
	
    var roomname = getRoomName(); // 会議室名を取得する
    socket.emit('enter', roomname);
    console.log('enter to ' + roomname);
  }

  // socket: accept connection request
  function onMessage(evt) {
    var id = evt.from;
    var target = evt.sendto;
    var conn = getConnection(id);

    if (evt.type === 'talk_request') {
      if (! isLocalStreamStarted()) {
        console.warn('local stream not started. ignore request');
        return;
      }

      console.log("receive request, start offer.");
      sendOffer(id);
      return;
    }
    else if (evt.type === 'answer' &amp;&amp; isPeerStarted()) {  
      console.log('Received answer, settinng answer SDP');
      onAnswer(evt);
    } else if (evt.type === 'candidate' &amp;&amp; isPeerStarted()) { 
      console.log('Received ICE candidate...');
      onCandidate(evt);
    }
    else if (evt.type === 'bye') { 
      console.log("got bye.");
      stopConnection(id);
    }
  }

  function onUserDisconnect(evt) {
    console.log("disconnected");
    if (evt) {
      stopConnection(evt.id);
    }
  }

  function getRoomName() { // たとえば、 URLに  ?roomname  とする
    var url = document.location.href;
    var args = url.split('?');
    if (args.length &gt; 1) {
      var room = args[1];
      if (room != "") {
        return room;
      }
    }
    return "_defaultroom";
  }
  
  
  
  function onAnswer(evt) {
    console.log("Received Answer...")
    console.log(evt);
    setAnswer(evt);
  }
  
  function onCandidate(evt) {
   var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
      console.error('peerConnection not exist!');
      return;
    }
   
    var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate});
    console.log("Received Candidate...")
    console.log(candidate);
    conn.peerconnection.addIceCandidate(candidate);
  }

  function sendSDP(sdp) {
    var text = JSON.stringify(sdp);
    console.log("---sending sdp text ---");
    console.log(text);
	
    // send via socket
    socket.json.send(sdp);
  }
  
  function sendCandidate(candidate) {
    var text = JSON.stringify(candidate);
    console.log("---sending candidate text ---");
    console.log(text);
	
    // send via socket
    socket.json.send(candidate);
  }
  
  // ---------------------- video handling -----------------------
  // start local video
  function startVideo() {
    navigator.webkitGetUserMedia({video: true, audio: true},
      function (stream) { // success
        localStream = stream;
        localVideo.src = window.webkitURL.createObjectURL(stream);
        localVideo.play();
        localVideo.volume = 0;

        // auto start
        tellReady();
      },
      function (error) { // error
        console.error('An error occurred:');
        console.error(error);
        return;
      }
    );
  }

  // stop local video
  function stopVideo() {
    hangUp();

    localVideo.src = "";
    localStream.stop();
    localStream = null;
  }

  // ---------------------- connection handling -----------------------
  function prepareNewConnection(id) {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create PeerConnection, exception: " + e.message);
    }
    var conn = new Connection();
    conn.id = id;
    conn.peerconnection = peer;
    peer.id = id;
    addConnection(id, conn);

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
        sendCandidate({type: "candidate", 
                          sendto: conn.id,
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate});
      } else {
        console.log("ICE event. phase=" + evt.eventPhase);
        //conn.established = true;
      }
    };

    console.log('Adding local stream...');
    peer.addStream(localStream);

    return conn;
  }

  function sendOffer(id) {
    var conn = getConnection(id);
    if (!conn) {
      conn = prepareNewConnection(id);
    }

    conn.peerconnection.createOffer(function (sessionDescription) { // in case of success
      conn.peerconnection.setLocalDescription(sessionDescription);
      sessionDescription.sendto = id;
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Offer failed");
    }, mediaConstraints);
  }

  function setAnswer(evt) {
	var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
	  console.error('peerConnection not exist!');
	  return
    }
    conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt));
  }
  
  // -------- handling user UI event -----
  function tellReady() {
    if (! isLocalStreamStarted()) {
      alert("Local stream not running yet. Please [Start Video] or [Start Screen].");
      return;
    }
    if (! socketReady) {
      alert("Socket is not connected to server. Please reload and try again.");
      return;
    }

    // call others, in same room
    console.log("tell ready to others in same room, befeore offer");
    socket.json.send({type: "talk_ready"});
  }

  
  // stop the connection upon user request
  function hangUp() {
    console.log("Hang up.");
    socket.json.send({type: "end_talk"});
    stopAllConnections();
  }

  
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre><p></p>

<h4>視聴側(watch.html)</h4>

<p></p><pre class="crayon-plain-tag">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;title&gt;broadcast watch&lt;/title&gt;  
  &lt;meta http-equiv="content-type" content="text/html; charset=UTF-8"&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;button type="button" onclick="sendRequest();"&gt;Request&lt;/button&gt;
  &lt;button type="button" onclick="hangUp();"&gt;Hang Up&lt;/button&gt;
  &lt;br /&gt;
  &lt;div style="position: relative;"&gt;
   &lt;video id="remote-video" autoplay style="width: 320px; height: 240px; border: 1px solid black;"&gt;&lt;/video&gt;
  &lt;/div&gt;
  
  &lt;!---- socket ※自分のシグナリングサーバーに合わせて変更してください ------&gt;
  &lt;script src="http://signaling.yourdomain:9001/socket.io/socket.io.js"&gt;&lt;/script&gt;
  
  &lt;script&gt;
  //var localVideo = document.getElementById('local-video');
  var remoteVideo = document.getElementById('remote-video');
  //var localStream = null;
  var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }};

  function detachVideo(id) {
    if (id) { 
      var conn = getConnection(id);
      if (conn) {
        remoteVideo.pause();
        remoteVideo.src = "";
      }
    }
    else {
      // force detach
      remoteVideo.pause();
      remoteVideo.src = "";
    }
  }

  function resizeRemoteVideo() {
    console.log('--resize--');
    var top_margin = 40;
    var left_margin = 20;
    var video_margin = 10;

    var new_width = window.innerWidth - left_margin - video_margin;
    var new_height = window.innerHeight - top_margin - video_margin;
    remoteVideo.style.width = new_width + 'px';
    remoteVideo.style.height = new_height + 'px';
    remoteVideo.style.top = top_margin + 'px';
    remoteVideo.style.left = left_margin + 'px';
  }
  document.body.onresize = resizeRemoteVideo;
  resizeRemoteVideo();

  // -------------- multi connections --------------------
  var MAX_CONNECTION_COUNT = 1;
  var connections = {}; // Connection hash
  function Connection() { // Connection Class
    var self = this;
    var id = "";  // socket.id of partner
    var peerconnection = null; // RTCPeerConnection instance
  }

  function getConnection(id) {
    var con = null;
    con = connections[id];
    return con;
  }

  function addConnection(id, connection) {
    connections[id] = connection;
  }

  function getConnectionCount() {
    var count = 0;
    for (var id in connections) {
      count++;
    }

    console.log('getConnectionCount=' + count);
    return count;
  }

  function isConnectPossible() {
    if (getConnectionCount() &lt; MAX_CONNECTION_COUNT)
      return true;
    else
      return false;
  }

  function getConnectionIndex(id_to_lookup) {
    var index = 0;
    for (var id in connections) {
      if (id == id_to_lookup) {
        return index;
      }

      index++;
    }

    // not found
    return -1;
  }

  function deleteConnection(id) {
    delete connections[id];
  }

  function stopAllConnections() {
    for (var id in connections) {
      var conn = connections[id];
      conn.peerconnection.close();
      conn.peerconnection = null;
      delete connections[id];
    }
  }

  function stopConnection(id) {
    var conn = connections[id];
    if(conn) {
      console.log('stop and delete connection with id=' + id);
      conn.peerconnection.close();
      conn.peerconnection = null;
      delete connections[id];
    }
    else {
      console.log('try to stop connection, but not found id=' + id);
    }
  }

  function isPeerStarted() {
    if (getConnectionCount() &gt; 0) {
      return true;
    }
    else {
      return false;
    }
  }

  
  // ---- socket ------
  // create socket
  var socketReady = false;
  var port = 9001;
  var socket = io.connect('http://signaling.yourdomain:/' + port + '/'); // ※自分のシグナリングサーバーに合わせて変更してください 
  
  // socket: channel connected
  socket.on('connect', onOpened)
        .on('message', onMessage)
        .on('user disconnected', onUserDisconnect);

  function onOpened(evt) {
    console.log('socket opened.');
    socketReady = true;
	
    var roomname = getRoomName(); // 会議室名を取得する
    socket.emit('enter', roomname);
    console.log('enter to ' + roomname);
  }

  // socket: accept connection request
  function onMessage(evt) {
    var id = evt.from;
    var target = evt.sendto;
    var conn = getConnection(id);

    console.log('onMessage() evt.type='+ evt.type);

    if (evt.type === 'talk_ready') {
      if (conn) {
        return;  // already connected
      }

      if (isConnectPossible()) {
        socket.json.send({type: "talk_request", sendto: id });
      }
      else {
        console.warn('max connections. so ignore call');
      }
      return;
    }
    else if (evt.type === 'offer') {
      console.log("Received offer, set offer, sending answer....")
      onOffer(evt);	  
    }
    else if (evt.type === 'candidate' &amp;&amp; isPeerStarted()) {
      console.log('Received ICE candidate...');
      onCandidate(evt);
    }
    else if (evt.type === 'end_talk') { 
      console.log("got talker bye.");
      detachVideo(id); // force detach video
      stopConnection(id);
    }

  }

  function onUserDisconnect(evt) {
    console.log("disconnected");
    if (evt) {
      detachVideo(evt.id); // force detach video
      stopConnection(evt.id);
    }
  }

  function getRoomName() { // たとえば、 URLに  ?roomname  とする
    var url = document.location.href;
    var args = url.split('?');
    if (args.length &gt; 1) {
      var room = args[1];
      if (room != "") {
        return room;
      }
    }
    return "_defaultroom";
  }
  
  
  function onOffer(evt) {
    console.log("Received offer...")
    console.log(evt);
    setOffer(evt);
    sendAnswer(evt);
  }
 
  function onCandidate(evt) {
    var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
       console.error('peerConnection not exist!');
       return;
    }
    
    var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate});
    console.log("Received Candidate...")
    console.log(candidate);
    conn.peerconnection.addIceCandidate(candidate);
  }

  function sendSDP(sdp) {
    var text = JSON.stringify(sdp);
    console.log("---sending sdp text ---");
    console.log(text);
	
    // send via socket
    socket.json.send(sdp);
  }
  
  function sendCandidate(candidate) {
    var text = JSON.stringify(candidate);
    console.log("---sending candidate text ---");
    console.log(text);
	
    // send via socket
    socket.json.send(candidate);
  }
  

  // ---------------------- connection handling -----------------------
  function prepareNewConnection(id) {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create PeerConnection, exception: " + e.message);
    }
    var conn = new Connection();
    conn.id = id;
    conn.peerconnection = peer;
    peer.id = id;
    addConnection(id, conn);

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
        sendCandidate({type: "candidate", 
                          sendto: conn.id,
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate});
      } else {
        console.log("on ice event. phase=" + evt.eventPhase);
      }
    };

    //console.log('Adding local stream...');
    //peer.addStream(localStream);

    peer.addEventListener("addstream", onRemoteStreamAdded, false);
    peer.addEventListener("removestream", onRemoteStreamRemoved, false)

    // when remote adds a stream, hand it on to the local video element
    function onRemoteStreamAdded(event) {
      console.log("Added remote stream");
      //attachVideo(this.id, event.stream);
      remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
    }

    // when remote removes a stream, remove it from the local video element
    function onRemoteStreamRemoved(event) {
      console.log("Remove remote stream");
      detachVideo(this.id);
    }

    return conn;
  }

  function setOffer(evt) {
    var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
      conn = prepareNewConnection(id);
      conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt));
    }
    else {
      console.error('peerConnection alreay exist!');
    }
  }
  
  function sendAnswer(evt) {
    console.log('sending Answer. Creating remote session description...' );
    var id = evt.from;
    var conn = getConnection(id);
    if (! conn) {
      console.error('peerConnection not exist!');
      return;
    }

    conn.peerconnection.createAnswer(function (sessionDescription) { 
      // in case of success
      conn.peerconnection.setLocalDescription(sessionDescription);
      sessionDescription.sendto = id;
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Answer failed");
    }, mediaConstraints);
  }

  function sendRequest() {
    if (! socketReady) {
      alert("Socket is not connected to server. Please reload and try again.");
      return;
    }

    // call others, in same room
    console.log("send request in same room, ask for offer");
    socket.json.send({type: "talk_request"});
 
  }
 
  // stop the connection upon user request
  function hangUp() {
    console.log("Hang up.");
    socket.json.send({type: "bye"});
    detachVideo(null);
    stopAllConnections();
  }

  
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre><p></p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTC]]></series:name>
	</item>
		<item>
		<title>Promise議論が白熱！TPAC 2014 WebRTCワーキンググループレポート</title>
		<link>/alan-iida/11325/</link>
		<pubDate>Wed, 19 Nov 2014 03:00:25 +0000</pubDate>
		<dc:creator><![CDATA[飯田 アレン真人]]></dc:creator>
				<category><![CDATA[最新動向]]></category>
		<category><![CDATA[TPAC2014]]></category>
		<category><![CDATA[W3C仕様]]></category>
		<category><![CDATA[WebRTC特集]]></category>

		<guid isPermaLink="false">/?p=11325</guid>
		<description><![CDATA[連載： WebRTC (2)10月26日から31日にかけて、Santa Claraで開催された「TPAC 2014」。本レポートでは、TPACで行われたWebRTCワーキンググループについてレポートします。 TPACとは...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc/" class="series-395" title="WebRTC" data-wpel-link="internal">WebRTC</a> (2)</div><p>10月26日から31日にかけて、Santa Claraで開催された「<a href="http://www.w3.org/2014/11/TPAC/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">TPAC 2014</a>」。本レポートでは、TPACで行われたWebRTCワーキンググループについてレポートします。</p>

<h2>TPACとは?</h2>

<p>TPACとは、Webの標準化団体であるW3C(World Wide Web Consortium)が開催する1週間のイベントのことです。様々な国、様々な企業からメンバーが集まり、現在のWeb標準・将来的なWebの機能(例えば、cryptoやaudio)について議論します。</p>

<p><img src="/wp-content/uploads/2014/11/tpac3.jpg" alt="" width="300" height="400" class="alignnone size-full wp-image-11509" srcset="/wp-content/uploads/2014/11/tpac3.jpg 300w, /wp-content/uploads/2014/11/tpac3-225x300.jpg 225w, /wp-content/uploads/2014/11/tpac3-155x207.jpg 155w" sizes="(max-width: 300px) 100vw, 300px" /></p>

<p>本記事では、WebRTC WG(ワーキンググループ)で議論された注目の話題をレポートします。WebRTC WGには、WebRTC(※)をより安心・安全に、一方で開発者には強力な柔軟性を与えたいと考えるメンバーが集まっています。</p>

<p>※ WebRTCとは？：
WebRTC(Web Real Time Communication)とは、サーバを経由せずにブラウザ同士が通信する技術のことで、ブラウザから音声や映像を取得するようなAPIを含んでいます。これらのAPIにより、プラグインなしで、お好きな複数のデバイス間で音声・映像通信を実現できます。WebRTC自体の技術的な内容については、本サイトの<a href="https://html5experts.jp/series/webrtc-beginner/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">WebRTCを使ってみよう！</a>をご覧ください。</p>

<h2>Media Capture タスクフォース</h2>

<p>Media Captureタスクフォースは、Device APIsとWebRTC WGsの両者が参加するタスクフォースです。音声・映像をデバイスから取得するgetUserMedia APIを担当しています。今年のTACでは、いかに開発者に簡単にAPIを利用させるか、また、ラストコール(勧告候補の前にコメントや提案を受け付けるタイミングです)の準備ができた、という点について議論が集中しました。</p>

<h3>Promises</h3>

<p>MozillaのJan-Ivar Bruaroeyが推進するPromiseが議論が白熱したトピックです。
具体的には、getUserMediaを取り巻くイベント(例えばMedia Stream Track <a href="http://w3c.github.io/mediacapture-main/getusermedia.html#widl-MediaStreamTrack-onended" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">onended</a> イベント)について、Promiseを利用する案が出ています。Promiseによってコードを綺麗に書くだけでなく、より詳細なエラー情報を開発者が利用可能になると、彼は述べています。</p>

<p>対して、RFTMの創設者であるEric Rescorlaは「不必要に複雑で、開発者が悪いコードを書いてしまう可能性がある」と述べ、強く反対しました。また、Promiseは複数回発生ようなイベントを扱えない、という問題があるとも述べています。
結局、MediaStreamTrack onendedイベントのようなイベントについては、reason codeを付けるのが最適という妥協点に達しました。getUserMediaは、メディアキャプチャに関するAPIの中でPromiseを使う唯一のAPIとなります。</p>

<h3>出力デバイスの選択</h3>

<p>WebRTCのアプリ開発の上で、開発者が直面する問題の1つに、音をスピーカから出力するか・ヘッドセットから出力するか、指定できないという問題があります。残念なことに、自由に出力デバイスを選択できるようにしてしまうと、セキュリティ上の懸念（例えば、悪意のあるサイトが広告を音声として流すようにな懸念）が出てきます。</p>

<p>GoogleのJustin Ubertiは、デバイスグループを定義するという、シンプルな解決方法を提案しました。
もし、入力と出力のデバイスが同一であるなら、同じデバイスグループに属するという考え方です。例えば、ヘッドセットや会議室のマイクが典型になります。本機能の前提として、もしユーザが入力デバイスに制御権を与えるなら、出力デバイスにも同じ権利が与えられてもよい、という考えがあります。本提案は歓迎されましたが、参加者はさらなる詳細(permissionの継続や仮のデバイス利用)について要望しました。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/11/20141031_090710.jpg" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/20141031_090710-300x225.jpg" alt="20141031_090710" width="300" height="225" class="alignnone size-medium wp-image-11327" srcset="/wp-content/uploads/2014/11/20141031_090710-300x225.jpg 300w, /wp-content/uploads/2014/11/20141031_090710-1024x768.jpg 1024w, /wp-content/uploads/2014/11/20141031_090710-207x155.jpg 207w, /wp-content/uploads/2014/11/20141031_090710.jpg 640w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>

<h2>WebRTCワーキンググループ</h2>

<p>WebRTC WGで議論された主なトピックは、オブジェクトを利用してWebRTC APIをモダンなものにする、ということ。マイクロソフトが推進するORTC(Object RTC)は明に言及されていないものの、明らかに多くの提案はORTCが推進力になっているものでした。</p>

<h3>RTPSender と RTPReceiver</h3>

<p>GoogleのJustinが、WebRTCでSDPを使うための新たな方法である、RTPSenderとRTPReceiverを提案しました。
現時点でストリームのビットレートを変更するためには、SDPのプレインテキストを書き換える必要があり、かなり大変です。また、SDPを見なければ、セッションで使われている最大のビットレートのような情報を取得できません。
Justinの提案は、これらの問題を解決するための第一歩になります。</p>

<p>この提案の中で最も受け入れられた部分は、<a href="http://w3c.github.io/webrtc-pc/#rtcpeerconnection-interface" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">RTCPeerConnection</a>オブジェクトの扱いにおいて、ストリームベースからトラックベース(RTCSender/Receiverオブジェクトにトラックを結びつける方法)へ、メディアを扱う方法を変えていくといった部分でした。</p>

<p>つまり、全てのトラックがRTCSenderオブジェクトを送り、全てのトラックがRTCReceiverオブジェクトを受け取ります。このようにすることで、同じトラックを複数の箇所で複製・利用できるようになり、ビットレート等もトラックごとに操作できるようになります。</p>

<p>また、提案の中には、ローカル/リモート候補、リモートのDTLS証明書、ユーザに対するビットレートのようなエンコードパラメタに挙げられる、トランスポートに関するデータを公開する方法も含まれています。全体として、この提案は良いと考えられたものの、エラーに対する操作やWebRTC 1.0に含める最小の仕様については、さらなる議論が必要となっています。</p>

<p>最後に、Justinはトラックを直接編集可能にする、という提案をしています。この提案によりモバイル機器の全面と背面のカメラを切り替えるときに発生する問題が解消できます（現在は、古いトラックを削除して、新しいトラックを追加し、再度ネゴシエーションする方法で対応しないといけません）。提案されたAPIであれば、トラックの内容を変更するだけで済み、再度ネゴシエーションする必要がありません。</p>

<h3>またまたPromises</h3>

<p>Jan-Ivarが再度登壇し、WebRTCについてもPromiseを利用するという、提案をしています。Media Captureセッションから受け取るフィードバックを利用して、promisesの実験的な利用を削減し、ICE・ピアコネクション・コールの発信のフローの中でPromiseを利用する教科書的な利用方法を提案しています。</p>

<p>Eric Rescorlaはまだ納得していなかったようなものの、エラー処理の簡単さに対するJan-Ivarのアピールが残りのグループの同意を勝ち取り、promiseが含められる点が合意され、既存のコールバックは非推奨となりました。</p>

<h3>DTLS証明書</h3>

<p>DTLS証明書は、ピア自身が本人であることを証明するために利用できます。マイクロソフトのMartin Thompsonは、Web Crypto APIを利用した、DTLS証明書を生成する新たなAPIを提案しました。</p>

<p>これにより、ドメイン毎・ユーザ毎に同じDTLS証明書を再利用できるようになりますし、証明書を毎回作成し直すことも可能になります。ただし残念なことに、<a href="http://en.wikipedia.org/wiki/Elliptic_curve_cryptography" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">Elliptic curve cryptography</a> (<a href="http://ja.wikipedia.org/wiki/%E6%A5%95%E5%86%86%E6%9B%B2%E7%B7%9A%E6%9A%97%E5%8F%B7" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">日本語リンク</a>)がWeb Crypto APIで動作するか誰も知らなかったので、明らかになるまで延期するのがよいという結論になりました。</p>

<h3>Stats API (統計API)</h3>

<p>Callstats.ioのファウンダであるVarun Singhは、新しいWebRTC stats APIドラフトの詳細について、WGの意見を募集しました。詳細とは例えば、変数名、コード名のフォーマット、bytesSent/Receivedがヘッダに計算結果を加えるべきか・それとも単にペイロードにすべきか、等になります。</p>

<h3>Bugs, bugs and more bugs!</h3>

<p>翌朝2日目の8:30という早朝から、bugzillaに出されているWebRTC関連のバグについて議論をするために、グループが集まりました。グループはそれぞれのバグについてのアクションを決めるために、3時間以上を費やしました。グループの参加者は徐々に減っていき、最後にはアクションを決定できなくなってしまいました。（主要メンバーが既に帰ってしまった！）</p>

<h2>結論</h2>

<p>2日間に渡り続いたTPAC 2014のWebRTC WGも終わりです。WebRTC劇場へPromiseが追加されますし、策定中の大きな仕様変更もまだあります。今後数年間で追加されるである新機能が楽しみですね！</p>

<p>(本記事は、飯田 アレン真人が英語で執筆し、編集部の岩瀬 義昌により翻訳されました)</p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTC]]></series:name>
	</item>
		<item>
		<title>[翻訳] coTURN:マルチテナント型のオープンソースのSTUN/TURNサーバ</title>
		<link>/iwase/11300/</link>
		<pubDate>Tue, 18 Nov 2014 00:30:37 +0000</pubDate>
		<dc:creator><![CDATA[岩瀬 義昌]]></dc:creator>
				<category><![CDATA[最新動向]]></category>
		<category><![CDATA[TURN]]></category>
		<category><![CDATA[WebRTC]]></category>
		<category><![CDATA[coTURN]]></category>

		<guid isPermaLink="false">/?p=11300</guid>
		<description><![CDATA[連載： WebRTC (1)本記事は、webrtcHacksにて英語で掲載されている記事を、webrtcHacks様の許可を得た上で、翻訳＆掲載している記事となります。修正・更新・コメント等がございましたら、webrtc...]]></description>
				<content:encoded><![CDATA[<div class="seriesmeta">連載： <a href="https://html5experts.jp/series/webrtc/" class="series-395" title="WebRTC" data-wpel-link="internal">WebRTC</a> (1)</div><p>本記事は、webrtcHacksにて英語で掲載されている<a href="http://webrtchacks.com/coturn/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">記事</a>を、webrtcHacks様の許可を得た上で、翻訳＆掲載している記事となります。修正・更新・コメント等がございましたら、<a href="http://webrtchacks.com/coturn/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">webrtchacks/coturn/</a> までお願いいたします。</p>

<p>This article originally appeared in English at webrtcHacks and has been translated with webrtcHack&#8217;s permission for posting to html5experts.jp in Japanese. Please visit http://webrtchacks.com/coturn for edits, updates, and comments.</p>

<h2>イントロダクション</h2>

<p>昨年、Oleg Moskalenkoへインタビューし、オープンソースのSTUN&amp;TURNサーバで極めて人気のあるrfc-5766-turn-serverプロジェクトについての記事を公開しました。その数カ月後、Amazonが自身のサービスであるMaydayにこのプロジェクトを利用していることがわかりました。それ以後、IETFにてRFC 5766に多くの機能が新たに定義され、オープンソースの新プロジェクトであるcoTURNプロジェクトが生まれました。</p>

<p>今日はOlegに再度、話を伺って、coTURNの何が新しいのか・coTURNとは何なのか、という点についてキャッチアップしたいと思います。</p>

<p>{“導入の執筆者”, “victor”}</p>

<div id="attachment_11301" style="width: 448px" class="wp-caption alignnone"><a href="https://html5experts.jp/wp-content/uploads/2014/11/coturn01.jpg" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/coturn01.jpg" alt="coturn image" width="438" height="640" class="size-full wp-image-11301" srcset="/wp-content/uploads/2014/11/coturn01.jpg 438w, /wp-content/uploads/2014/11/coturn01-205x300.jpg 205w, /wp-content/uploads/2014/11/coturn01-141x207.jpg 141w" sizes="(max-width: 438px) 100vw, 438px" /></a><p class="wp-caption-text">Photo courtesy of flikr user Grant Nicholson</p></div>

<h2>インタビュー</h2>

<p>webrtcHacks: 前回インタビューしたときは、rfc5766-turn-serverプロジェクトについて話をしていて、既に商用でも使われている例(WebRTCの例、WebRTC以外の例)があるのを教えてもらったよね。 coTURNプロジェクトの何が新しくて、現在人気のあるrfc5766-turn-serverと何が違うの？</p>

<p>Oleg: TURNとSTUNのプロトコルは進化が早くて、新しいネットワークの接続要件を備えていたり、もっとロバストな能力を提供してきている。ある時点で、新しい要件が現在の&#8221;レガシー&#8221;なユーザ要求と衝突する可能性があることがわかったんだ。そこで、プロジェクトを2つの開発ラインに分けて、2つのモデルをサポートしようと決めたんだよ。古いプロジェクト(rfc5766-turn-server)は安定したコードと多くのユーザを受けて継続する。つまり、バグフィックスや本当に必要な機能だけを加えて、信頼性のあるコードになる。このプロジェクトは古いスタイルのRFC 5766に準拠して欲しいという要望がある限り、存在し続けるよ。</p>

<p>一方で新しいプロジェクトである&#8221;<a href="https://code.google.com/p/coturn/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">coTURN</a>&#8220;(複数レルムでco-locationを提供するTURN)は4月にはじまり、2014年の5月に<a href="https://groups.google.com/forum/#!topic/turn-server-project-rfc5766-turn-server/rYF8nbm5rxc" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">公開</a>した。最初の主な違いは、新しい<a href="https://tools.ietf.org/wg/tram/charters" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">IETF TRAMワーキンググループ</a>で提案されているORIGIN属性をサポートしたmultiple-realms(複数領域)の仕様に対応している点だよ。</p>

<p>webrtcHacks: 読者はIETFでTRAMが何をやっていることについて詳しくないひともいるので、簡単に教えてもらえる？</p>

<p>Oleg: もちろん。TRAM WGのゴールは、複数の取組みを整理・統合して、TURNとSTUNをもっとWebRTCの環境に適したものにすること。その中では、以下の取組みが含まれている：</p>

<ul>
<li>追加トランスポートとしての<a href="https://tools.ietf.org/html/rfc7350" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">DTLS</a>を追加する</li>
<li>認証の仕組み</li>
<li>TURNとSTUNの拡張</li>
</ul>

<p>実際、知っていると思うけど、TURNbisやSTUNbisのドラフトにも取り組んでいるよ。私の意見だと、今後やって来る標準の中で最も重要な機能は、oAuthベースの認証と認可の仕組みだね。エンタープライズのユーザ・ISPのユーザにとって、マルチテナントサーバになるのが主要な機能になるだろうね。</p>

<p>これに加えて、多くな小さな追加もある：</p>

<ul>
<li>ALPN (Application Layer Protocol Negotiation)</li>
<li>帯域制御</li>
<li>IPv4とIPv6の割り当て(dual allocation)</li>
</ul>

<p>webrtcHacks: あと、さっき言ってたORIGIN属性もあるよね。</p>

<p>Oleg: その通り、これまで決めてきたものの1つに、新しい<a href="https://tools.ietf.org/html/draft-ietf-tram-stun-origin" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">ORIGIN属性</a>がある。これは、セッション単位での領域選択を可能にするんだ。coturnのデータベースは、複数のレルムでグループ分けされたユーザを保持するようになる。それぞれのレルムは異なるパラメータをおそらく持つ。この機能のおかげで、複雑な大規模環境でもcoturnは対応できる。例えば、1つのIPアドレスとポートしないけど、複数のユーザに対応する必要がある場合とかね。エンタープライズ向けの大規模TURNサーバやISPの運用するTURNサーバも適用例になるだろうね。</p>

<p>webrtcHacks: マルチテナントへの変更って、rfc 5766のTURNサーバのプロジェクトから大きなアーキテクチャの変更が必要なの？</p>

<p>Oleg: アーキテクチャに大きな変更は無いんだけど、データマネジメントでかなりの量の変更があるんだ。これまでの多くのことはシステム全体でグローバルだと想定してたんだけど、これからはレルムに限定されるようになる。そのため、かなりの量のコードを書き換えないといけなくて、それがマルチテナントの移行に向けてとても大変なところなんだ。</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/11/coturn02.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/coturn02.png" alt="Example from latest TURNbis draft" width="593" height="404" class="alignnone size-full wp-image-11302" srcset="/wp-content/uploads/2014/11/coturn02.png 593w, /wp-content/uploads/2014/11/coturn02-300x204.png 300w, /wp-content/uploads/2014/11/coturn02-207x141.png 207w" sizes="(max-width: 593px) 100vw, 593px" /></a></p>

<p>webrtcHacks: マルチテナント以外の機能は他にある？</p>

<p>Oleg: うん、もう一つの大きな変更はIPv4とIPv6の<a href="http://tools.ietf.org/html/draft-martinsen-tram-ssoda" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">2つの割り当て</a>だね。この機能のおかげで、サーバは1つのセッションに対して2つのエンドポイントを割り当てできる。つまり、1つがIPv4でもう1つがIPv6。もしTURNのクライアントがv4とv6の両方でピアとの接続を確立したい場合は、クライアントは同じセッションの中で両方の接続を扱えるから、クライアントとサーバのリソースの節約になる。</p>

<p>Coturnは、サーバ上であるレベルの<a href="http://tools.ietf.org/html/draft-thomson-tram-turn-bandwidth" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">帯域制御</a>も提供する仕様もサポートしているよ。この機能は、TURNクライアントへ提供するサービスレベルを保証したい場合に役立つよ。</p>

<p>webrtcHacks: プロジェクトのコミットを追っかけてて気づいんたんだけど、データベースのサポートについても何かしているよね。これについて教えてくれる？</p>

<p>Oleg: Coturnでは、もっとロバストで抽象化できるようにデータベースのコードを書き直したんだ。これは、新しい機能の開発に役立つし、新しいデータベースも採用できる。例えば、Coturnは古いTURNサーバのプロジェクトでもサポートしてたRedis、MySQL、Postgresに加えて、MongoDBもサポートしているよ。</p>

<p>webrtcHacks: セキュリティについてはどう？何か改善した？　</p>

<p>Oleg: あるよ、今のCoturnの開発はoAuthベースのサードパーティ認証＆認可について取り組んでいる。また、多くのTURNのセキュリティ課題も修正するつもりだよ。CoturnサーバはoAuthの鍵をデータベースから取得するようになり、外部の独立したエージェントがその鍵があるデータベースを扱うようになる。</p>

<p>webrtcHacks: Coturnのパフォーマンスについてはどう？前のrfc5766-turn-serverプロジェクトに比べてどう？</p>

<p>Oleg: Coturnはrfc5766-turn-serverと同じテストスイートを使ってる。Coturnのコードと機能は、より複雑だから理論的にはパフォーマンスは落ちる。だけど、これまでのテストだと気づけるパフォーマンスは落ちてないね。</p>

<p>TURNとして使うのならCPU1つで数千のコールをさばけるし、STUNだけでいいのなら数万のコールをさばけるよ。</p>

<p>webrtcHacks: デザイン面からパフォーマンスに貢献してそうなところはある？</p>

<p>Oleg: 高いパフォーマンスとスケーラビリティのために、TURNサーバはいくつかの機能を実装しているんだ：</p>

<ul>
<li>libevent2の利用 &#8211; 高パフォーマンスで、耐久性のあるのネットワークIOエンジン</li>
<li>設定可能なマルチスレッドモデル(OSがマルチスレッドを使えるなら、CPUのリソースをフルに使えるように)</li>
<li>設定可能な複数のリスニング、および複数のリレーアドレス</li>
<li>より効率的なメモリ管理モデル</li>
<li>ユーザスペースで動作して、システムに特別な制約を課さないこと</li>
</ul>

<p>このTURNプロジェクトのコードは、個別に専有されたネットワーク環境で使われることもある。TURNサーバのコードでは、抽象化されたネットワークAPIが使われているんだ。いくつかのファイルを書き直すだけで、TURNサーバに専有環境に適するコードをプラグインできるよ。このプロジェクトのおかげで、プロジェクトでは提供するのは標準のUNIXのネットワーク/IO APIだけでいいんだ。ユーザは他の環境に適したものを実装できる。TURNサーバのコードはもともと企業の専有環境で高パフォーマンスがでるように作られていて、その後にUNIXネットワークAPIを採用したんだ。　</p>

<p><a href="https://html5experts.jp/wp-content/uploads/2014/11/coturn03.png" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer"><img src="/wp-content/uploads/2014/11/coturn03.png" alt="New coturn Project on Google code" width="796" height="407" class="alignnone size-full wp-image-11303" srcset="/wp-content/uploads/2014/11/coturn03.png 640w, /wp-content/uploads/2014/11/coturn03-300x153.png 300w, /wp-content/uploads/2014/11/coturn03-207x105.png 207w" sizes="(max-width: 796px) 100vw, 796px" /></a></p>

<p>webrtcHacks: スケールやロードバランスはどうするつもり？</p>

<p>Oleg: もちろん、仮想的には無制限にスケールするようにロードバランスのスキーマは使うよ。ロードバランスは次のようなツールを使って実装できる。1つでも複数の組み合わせでもいい：</p>

<ul>
<li>DNS SRVを用いたロードバランス</li>
<li>組み込みの300 ALTERNATE-SERVERメカニズム(TURNクライアントからの300レスポンスが必要)</li>
<li>ネットワークロードバランスサーバ</li>
</ul>

<p>webrtcHacks: メディアリレーするためにTURNサーバってどのぐらい使われるか知ってる？</p>

<p>Oleg: 異なる統計が異なるデータを出しているね。一般的に言えば、8-15%のコールで利用されているよ。もちろん、特別なアプリケーションによっては100％がTURNサーバを通る。ただ、僕の知るかぎりだとWebRTCのアプリケーションじゃないけど。</p>

<p>webrtcHacks: 開発者が全てのトラフィックをTURNサーバ経由にしないとならないシナリオって何か思いつく？伝統的なVoIPだと本質的にSBCを使うような。</p>

<p>Oleg: WebRTCの世界だとそういうシナリオに関する絶対的な情報は知らないな。だけど、一部のエンタープライズユーザはそういうネットワークパターンを議論しているから、ありえると思うよ。</p>

<p>webrtcHacks: Coturnはどのプラットフォームがサポートしているの？</p>

<p>Oleg: リストにすると長いね。サポートしているプラットフォームは</p>

<ul>
<li>Linux &#8211; Debina, Ubuntu, Mint, CentOS, Fedora, Redhat, Amazon Linux, Arch Linux, Open SUSE</li>
<li>BSD &#8211; FreeBSD, NetBSD, OpenBSD, DragonFlyBSD</li>
<li>Solaris</li>
<li>Mac OS X</li>
<li>Cygwin &#8211; R&amp;D用途でプロダクジョン向けじゃない</li>
</ul>

<p>このプロジェクトは、*NIXプラットフォームで利用できるはず。ただ、公式にはサポートしてないけど。</p>

<p>クライアントプラットフォームはなんでも大丈夫で、Android、iOS、Linux、OS X、Windows、Windows Phoneをサポートしているよ。</p>

<p>webrtcHacks: 昨年、統計付きパフォーマンスモニタリングも追加したいと言ってたけど、Coturnはそういう仕組を備えているの？</p>

<p>Oleg: 今のところは、パフォーマンスモニタリングのデータはTURNサーバに対してtelnetのインターフェースで取得できて、いくつかのパフォーマンス統計は、Redisの統計データベースから取得できるよ。統計以外だと、今のTURNサーバはパフォーマンスベースの輻輳制御の仕組みを備えているよ。この仕組みはTCPとTLSの接続で動作して、TCP/TLSのコネクションを効率良く自動的にチューニングしてくれる。</p>

<p>webrtcHacks: Coturnの将来のロードマップについて教えてくれる？</p>

<p>Oleg: Coturnは最新のTURNとSTUNの機能に追従していくつもりだよ。IETF TRAMグループは新しいTURNとSTUNのRFC(TURNbisとSTUNbisというコードネームがついてる)を採用する方向に進んでいる。RFCが最終的に定まったら、Coturnサーバは新しい仕様のサポートするように準備するだろう。</p>

<p>加えて、データベースやREST API等を扱える分離されたマネジメントサーバを追加したいんだ。リソースが不足しているから、ボランティアで参加してもらえると非常に助かるよ。</p>

<p>webrtcHacks: 読者がCoturnについての情報を知りたかったらどこを見ればいい？</p>

<p>Oleg: 一番良いのはプロジェクトのウェブサイ卜だよ。<a href="https://code.google.com/p/coturn/" data-wpel-link="external" target="_blank" rel="follow external noopener noreferrer">https://code.google.com/p/coturn/</a>　必要な情報は、全てリンクするようにしてる。</p>

<p>{“インタビューした人, “victor“}</p>

<p>{“インタビューを受けた人&#8221;, “Oleg Moskalenko“}</p>

<p>{“編集者&#8221;, “chad“}</p>

<p>{“翻訳者&#8221;, “岩瀬 義昌&#8221;}</p>
]]></content:encoded>
		
		<series:name><![CDATA[WebRTC]]></series:name>
	</item>
	</channel>
</rss>
