はじめまして、@kenzan100です。
私が最近仕事で手を動かしているWebRTCについてお話しします。このスクリーンショットのようなビデオチャットアプリが三分後につくれていることを目指します。
どんな人に読んでほしいの?
「WebRTCって聞いたことあるし面白そう、でも概要の理解のおさらいをしたいし、実際に手を動かして何か達成感を感じてみたい」というあたりの方です。
そもそも..WebRTCってなに?
概要を理解したい人は、ここらへんが大変参考になると思います。
http://documents.tips/technology/webrtc--55849089da16b.html
http://qiita.com/yusuke84/items/286f569d110daede721e
それぞれボリュームがあるので、お時間あるときに読んだ方が良いかもしれませんが。
この記事で何がわかるの?
もうすでに色々な方が「始め方」みたいな記事は書かれているので、あらためて新しい情報がここで登場することはないかもしれません。ただ、個人的に初回学習で探すのに苦労した 「フルメッシュ(多対多)のP2P通信を実現する(要は不特定多数のビデオカンファレンスができる感じ)」 というサンプルコードを、WebRTC初学者にもわかるようにコメント多めに書くことで、一つの価値が生まれれば良いなと思って書いています。
ツッコミ、リクエストお待ちしています。
このコードはオリジナルなの?
多対多のコードについては、多くをこちらを参考にしました。
https://groups.google.com/forum/#!msg/skywayjs/lbqcvPEvuz4/ghYdjRMzjmwJ
最初にどんな全体像なのか教えてくれない?
WebRTC(ウェブリアルタイムコミュニケーション)は、大きく分けて次の二つの技術の組み合わせで実現されています。
- 各自の環境でのメディア(動画・音声・データ)の取得パート
- 取得したメディアをお互いに接続するP2P通信パート
実は、 __WebRTCを学んでいて一番難しいのがこの二番目のP2P通信パート__です。P2P接続を確立する際のパターンが多くあることと、その際に専門知識、専門用語(ICE, STUN, TURN, シグナリング...)の理解が必要とされるからです。
難しい部分を肩代わりしてくれるライブラリはないの?
あります。多くの説明では、このP2P通信部分は抽象化されたライブラリを使うことを勧めています。自分も同意です。さまざまな選択肢がありますが、多くの説明記事で名前があげられている PeerJSは、2015/12/16 時点ではなかなかアクティブとは言い難いようです。
https://github.com/peers/peerjs/issues/318
https://github.com/peers/peerjs/pulse/monthly
しかし、日本語ネイティブで説明されている SkyWayがPeerJSのライブラリを拡張したものであること、
SkyWay自体は現時点(2015/12/16)でもアクティブっぽいことから、この記事でもSkyWayを用いた解説を行います。http://nttcom.github.io/skyway/news.html
通信を保証するようなサービスを行う場合は、ビジネス目的でライブラリを公開している__OpenTok__などの利用を検討した方が良いかもしれません。彼らの説明を読むことで技術の理解も深まりますし、その後自ら(シグナリングサーバなど)を実装する際も、押さえておくべきポイントが分かった状態で開発に入れることでしょう。
OK、じゃあどうやって実装するの?
さて、いよいよサンプルコードです。
JSコードの全体は下記 Gistに公開しました。
https://gist.github.com/kenzan100/d14ded1195f04d70f503
まず全体を理解する上で一番大きな箇所は以下ですね。
function initialize() {
initializeMedia(function() {
initializePeer(function() {
callRemoteAll();
});
});
}
これはそのまま、先ほど説明した二つのパートに対応しています。メディア取得パートと対応しているのがinitializeMedia
、P2P接続パートに対応しているのがinitializePeer
とcallRemoteAll
です。
メディア取得パートの中身は?
initializeMedia
では何をやっているのでしょうか。
function initializeMedia(callback) {
navigator.getUserMedia(
{ audio: true, video: true },
function(stream) {
localStream = stream;
var video = document.getElementById('myVideo');
video.src = URL.createObjectURL(stream);
video.play();
callback();
},
function(err) {
console.error(err);
}
);
}
ここで大事なAPIは、 getUserMedia
です。これが、メディアストリームを取得する際のキモになります。引数の一番目である {}オブジェクトは、どのメディアをどんな条件で取得するか指定するものです(MediaStreamConstraints
)。
メディアストリームの概要理解は、下記を参考にしてください。
http://qiita.com/udonchan/items/77ca19f9aa8420e769c8
注意: 最新のドラフトによると、 navigator.getUserMedia
は非推奨のようです。navigator.mediaDevices.getUserMedia
を使ったコードに読み替えてください。
https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
ここまでの時点で、実はすでに自環境での映像・音声の取得・表示/再生は完了しています。この時点でも、はじめてWebRTCを触るときには __「こんな簡単にメディアにアクセスできるのか!」__ということで、ちょっとした感動がありました。ぜひお試しください。
表示させるvideo
要素にはmuted
を記述しておいてください。でないと、おそらくハウリングしてしまうので。
P2P接続パートはどんな感じなの?
さて、次にやることはinitializePeer
です。関数名そのままで、P2P接続をする際の通信に、各種イベントリスナーを設定します。
function initializePeer(callback) {
peer = new Peer({ key: SKYWAY_API_KEY });
peer.on('open', function(id) {
selfId = id;
callback();
});
peer.on('call', function(mediaConnection) {
mediaConnection.answer(localStream);
settingMediaConnection(mediaConnection);
});
peer.on('close', function(){
peer.destroy();
});
peer.on('error', function(err){
console.error(err);
});
}
open
call
などの各イベントが何に対応しているのかは、SkyWay版 PeerJSのドキュメントをご覧ください。
http://nttcom.github.io/skyway/docs/#peeron
settingMediaConnectionって初めてみるんだけど..?
initializePeer
の中で、settingMediaConnection
という関数が出てきました。
function settingMediaConnection(mediaConnection) {
var remoteId = mediaConnection.peer;
var remoteStream = null;
var video = null;
mediaConnection.on('stream', function(stream) {
video = document.createElement('video');
video.src = URL.createObjectURL(stream);
video.play();
var parent = document.getElementById('remoteVideos');
parent.appendChild(video);
});
mediaConnection.on('close', function(){
URL.revokeObjectURL(video.src);
video.parentNode.removeChild(video);
});
mediaConnection.on('error', function(err){
console.error(err);
});
}
peer.on('call'...
で相手からの着信に応答した後、相手側のストリームを処理するための関数です。相手側からのストリームを処理するイベントリスナーをここで設定します。
さぁ、これで動くかな!?
いいえ、ここまでだと、通信が開始した __「後の」__流れは書きましたが、まだ誰も通信を開始(発信)していません。
そこで、最後にcallRemoteAll
をして、すべてのクライアントが、他のクライアントすべてに発信します(なのでメッシュ構造と言います)。
function callRemoteAll() {
peer.listAllPeers(function(remoteIds) {
for (var i = 0; i < remoteIds.length; i++ ) {
var remoteId = remoteIds[i];
var mediaConnection = peer.call(remoteId, localStream);
settingMediaConnection(mediaConnection);
}
});
}
SKYWAYは、ここで便利なlistAllPeers
という関数を用意してくれています。これも関数名そのままで、現在のこのSKYWAY API KEY
を使ったシグナリングサーバーに接続しているピアすべてのIDを配列で返してくれます。
IDそれぞれに対して、発信した端からsettingMediaConnection
で 接続を処理してあげれば...?
あら簡単!これで(あなたのマシンとネットワーク帯域が許す限りの)多対多での動画チャットが可能となりました。
いかがでしたか?
さて、ここまで簡素なJSのコードだけで、多対多のWebRTC通信を実現するコードについて解説してきました。
実際にこれをつかって実用的なアプリケーションを書こうとすると、さまざまな壁にぶち当たるはずです。
一番は、 __P2Pであることに由来する各クライアントへの品質の依存__でしょう。
クライアントのマシン・帯域がイケていないときに、サービス側でなんとかする工夫は必須だと思います。
そのときには、MediaStreamConstraints
で定義されているメディアストリームを細かく制御する方法にも目を通すことで、より安定したユーザー体験の提供ができるでしょう。
__それ以外の用途としては、メディアアート分野でしょうか。__通信をさせないまでも、 JSで映像・音声を簡単に取得(他のHTML5 APIと組み合わせれば)加工できるのは大変な魅力だと思います。
下記デモにあるような カメラの映像を、アスキーアートとして表示させるアプリ などは、その手の人はそそられるのではないでしょうか。
https://idevelop.ro/ascii-camera/
__個々人のアイデア発想次第でなんでもできる、世界を変えるアイデアを夢想できるツール__として、 WebRTCは素晴らしいものだと思います。この記事がみなさんの想像を刺激する一助になれば幸いです。
エンジニア向け学習コミュニティ Dokugaku Dojo やってます!
私個人の活動として、 エンジニアがお互いから学び合う学習コミュニティ 「Dokugaku Dojo」を主催しています。
オンラインでのもくもく会と、オフラインでの月一のミートアップが中心のコミュニティです。
http://kenzan100.hatenadiary.jp/entry/2015/09/14/145126
ぜひ興味ある方は、下記ページよりお問い合わせください
https://dokugaku.io/
補足資料
ちょっと古い記事ですが、P2P接続を確立するための技術についてわかりやすい記事です。
http://tjun.org/2013/12/webrtc_p2p/