WebRTC-クイックガイド
Webは、リアルタイム通信にとってもはや見知らぬ人ではありません。 WebRTC (Web Real-Time Communication)登場します。2011年5月にリリースされましたが、まだ開発中であり、標準は変更されています。プロトコルのセットはによって標準化されたWEB-ブラウザワーキンググループでリアルタイム通信でhttp://tools.ietf.org/wg/rtcweb/ の IETF (Internet Engineering Task Force)新しいAPIのセットがによって標準化されている間のWebリアルタイムコミュニケーションワーキングのGroupeでhttp://www.w3.org/2011/04/webrtc/ の W3C (World Wide Web Consortium)。WebRTCの登場により、最新のWebアプリケーションは、オーディオおよびビデオコンテンツを何百万もの人々に簡単にストリーミングできます。
基本スキーム
WebRTCを使用すると、他のWebブラウザーへのピアツーピア接続をすばやく簡単に設定できます。このようなアプリケーションを最初から構築するには、データ損失、接続の切断、NATトラバーサルなどの一般的な問題に対処する豊富なフレームワークとライブラリが必要になります。WebRTCを使用すると、これらすべてがブラウザーに組み込まれ、すぐに使用できます。このテクノロジーには、プラグインやサードパーティのソフトウェアは必要ありません。オープンソースであり、そのソースコードはで無料で入手できます。http://www.webrtc.org/.
WebRTC APIには、メディアキャプチャ、オーディオとビデオのエンコードとデコード、トランスポート層、およびセッション管理が含まれます。
メディアキャプチャ
最初のステップは、ユーザーのデバイスのカメラとマイクにアクセスすることです。利用可能なデバイスの種類を検出し、これらのデバイスにアクセスしてストリームを管理するためのユーザー権限を取得します。
オーディオとビデオのエンコードとデコード
インターネットを介してオーディオおよびビデオデータのストリームを送信することは簡単な作業ではありません。ここでエンコードとデコードが使用されます。これは、ビデオフレームとオーディオウェーブを小さなチャンクに分割して圧縮するプロセスです。このアルゴリズムはcodec。さまざまなビジネス目標を持つさまざまな企業によって維持されている、膨大な量のさまざまなコーデックがあります。WebRTCには、H.264、iSAC、Opus、VP8などの多くのコーデックもあります。2つのブラウザが相互に接続すると、2人のユーザー間でサポートされている最適なコーデックが選択されます。幸い、WebRTCはほとんどのエンコーディングをバックグラウンドで実行します。
トランスポート層
トランスポート層は、パケットの順序を管理し、パケット損失を処理し、他のユーザーに接続します。この場合も、WebRTC APIを使用すると、接続に問題がある場合に通知するイベントに簡単にアクセスできます。
セッション管理
セッション管理は、接続の管理、開始、および編成を扱います。これは一般的に呼ばれますsignaling。オーディオストリームとビデオストリームをユーザーに転送する場合は、付随データを転送することも理にかなっています。これはによって行われますRTCDataChannel API。
Google、Mozilla、Operaなどの企業のエンジニアは、このリアルタイム体験をWebにもたらすために素晴らしい仕事をしてきました。
ブラウザの互換性
WebRTC標準は、Web上で最も急速に進化しているものの1つであるため、すべてのブラウザーがすべて同じ機能を同時にサポートすることを意味するわけではありません。お使いのブラウザがWebRTCをサポートしているかどうかを確認するには、次のWebサイトにアクセスしてください。http://caniuse.com/#feat=rtcpeerconnection. すべてのチュートリアルを通じて、すべての例でChromeを使用することをお勧めします。
WebRTCを試す
今すぐWebRTCの使用を始めましょう。ブラウザを次のデモサイトに移動します。https://apprtc.appspot.com/
「参加」ボタンをクリックしてください。ドロップダウン通知が表示されます。
「許可」ボタンをクリックして、ビデオとオーディオのWebページへのストリーミングを開始します。あなたはあなた自身のビデオストリームを見るはずです。
次に、現在使用しているURLを新しいブラウザタブで開き、[参加]をクリックします。2つのビデオストリームが表示されます。1つは最初のクライアントからのもので、もう1つは2番目のクライアントからのものです。
これで、WebRTCが強力なツールである理由を理解する必要があります。
ユースケース
リアルタイムWebは、テキストベースのチャット、画面とファイルの共有、ゲーム、ビデオチャットなど、まったく新しい範囲のアプリケーションへの扉を開きます。通信に加えて、WebRTCを次のような他の目的に使用できます。
- リアルタイムマーケティング
- リアルタイム広告
- バックオフィスコミュニケーション(CRM、ERP、SCM、FFM)
- 人事管理
- ソーシャルネットワーキング
- 出会い系サービス
- オンライン医療相談
- 金融業務
- surveillance
- マルチプレイヤーゲーム
- 生放送
- e-learning
概要
これで、WebRTCという用語を明確に理解できたはずです。また、ブラウザですでに試しているので、WebRTCを使用して構築できるアプリケーションの種類についても理解しておく必要があります。要約すると、WebRTCは非常に便利なテクノロジーです。
WebRTCアーキテクチャ全体は、非常に複雑です。
ここでは、3つの異なるレイヤーを見つけることができます-
API for web developers −このレイヤーには、RTCPeerConnection、RTCDataChannel、MediaStreanオブジェクトなど、Web開発者が必要とするすべてのAPIが含まれています。
ブラウザメーカー向けのAPI
ブラウザメーカーがフックできるオーバーライド可能なAPI。
トランスポートコンポーネントを使用すると、さまざまなタイプのネットワーク間で接続を確立できます。音声およびビデオエンジンは、オーディオおよびビデオストリームをサウンドカードおよびカメラからネットワークに転送するためのフレームワークです。Web開発者にとって、最も重要な部分はWebRTCAPIです。
クライアントサーバー側からWebRTCアーキテクチャを見ると、最も一般的に使用されているモデルの1つがSIP(Session Initiation Protocol)台形に触発されていることがわかります。
このモデルでは、両方のデバイスが異なるサーバーからWebアプリケーションを実行しています。RTCPeerConnectionオブジェクトは、ストリームがピアツーピアで相互に接続できるようにストリームを構成します。このシグナリングは、HTTPまたはWebSocketを介して行われます。
しかし、最も一般的に使用されるモデルはTriangle −です。
このモデルでは、両方のデバイスが同じWebアプリケーションを使用します。これにより、Web開発者はユーザー接続を管理する際の柔軟性が高まります。
WebRTC API
それはいくつかの主要なjavascriptオブジェクトで構成されています-
- RTCPeerConnection
- MediaStream
- RTCDataChannel
RTCPeerConnectionオブジェクト
このオブジェクトは、WebRTCAPIへの主要なエントリポイントです。ピアへの接続、接続の初期化、メディアストリームの接続に役立ちます。また、別のユーザーとのUDP接続を管理します。
RTCPeerConnectionオブジェクトの主なタスクは、ピア接続をセットアップして作成することです。このオブジェクトはイベントが表示されたときに一連のイベントを発生させるため、接続のキーポイントを簡単にフックできます。これらのイベントにより、接続の構成にアクセスできます-
RTCPeerConnectionは単純なjavascriptオブジェクトであり、この方法で簡単に作成できます-
[code]
var conn = new RTCPeerConnection(conf);
conn.onaddstream = function(stream) {
// use stream here
};
[/code]
RTCPeerConnectionオブジェクトはconfパラメーターを受け入れます。これについては、これらのチュートリアルの後半で説明します。onaddstreamリモートユーザが自分のピア接続にビデオまたはオーディオストリームを追加したときにイベントが発生しています。
MediaStream API
最新のブラウザは、MediaStreamAPIとしても知られるgetUserMediaAPIへのアクセスを開発者に提供します。機能には3つの重要なポイントがあります-
開発者は、ビデオストリームとオーディオストリームを表すストリームオブジェクトにアクセスできます。
ユーザーがデバイスに複数のカメラまたはマイクを持っている場合に、入力ユーザーデバイスの選択を管理します
これは、ユーザーがストリームをフェッチしたいときはいつでもユーザーに尋ねるセキュリティレベルを提供します
このAPIをテストするために、簡単なHTMLページを作成しましょう。単一の<video>要素が表示され、カメラを使用する許可をユーザーに求め、カメラからのライブストリームをページに表示します。index.htmlファイルを作成し、-を追加します
[code]
<html>
<head>
<meta charset = "utf-8">
</head>
<body>
<video autoplay></video>
<script src = "client.js"></script>
</body>
</html>
[/code]
次に、client.jsファイルを追加します-
[code]
//checks if the browser supports WebRTC
function hasUserMedia() {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
|| navigator.mozGetUserMedia || navigator.msGetUserMedia;
return !!navigator.getUserMedia;
}
if (hasUserMedia()) {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
|| navigator.mozGetUserMedia || navigator.msGetUserMedia;
//get both video and audio streams from user's camera
navigator.getUserMedia({ video: true, audio: true }, function (stream) {
var video = document.querySelector('video');
//insert stream into the video tag
video.src = window.URL.createObjectURL(stream);
}, function (err) {});
}else {
alert("Error. WebRTC is not supported!");
}
[/code]
index.htmlを開くと、顔を表示するビデオストリームが表示されます。
ただし、WebRTCはサーバー側でのみ機能するため、注意が必要です。このページをブラウザで開くだけでは機能しません。これらのファイルは、ApacheサーバーまたはNodeサーバー、あるいはどちらを使用するかでホストする必要があります。
RTCDataChannelオブジェクト
ピア間でメディアストリームを送信するだけでなく、DataChannelAPIを使用して追加のデータを送信することもできます。このAPIはMediaStreamAPIと同じくらい簡単です。主な仕事は、既存のRTCPeerConnectionオブジェクトからのチャネルを作成することです-
[code]
var peerConn = new RTCPeerConnection();
//establishing peer connection
//...
//end of establishing peer connection
var dataChannel = peerConnection.createDataChannel("myChannel", dataChannelOptions);
// here we can start sending direct messages to another peer
[/code]
必要なのはこれだけで、2行のコードだけです。それ以外はすべてブラウザの内部レイヤーで行われます。RTCPeerConnectionオブジェクトが閉じられるまで、任意のピア接続でチャネルを作成できます。
概要
これで、WebRTCアーキテクチャをしっかりと把握できたはずです。MediaStream、RTCPeerConnection、およびRTCDataChannelAPIについても説明しました。WebRTC APIは動くターゲットであるため、常に最新の仕様に対応してください。
WebRTCアプリケーションの構築を開始する前に、コーディング環境を設定する必要があります。まず、HTMLとJavascriptを編集できるテキストエディタまたはIDEが必要です。このチュートリアルを読んでいるときに、すでに優先するものを選択している可能性があります。私はWebStormIDEを使用しています。試用版は次のURLからダウンロードできます。https://www.jetbrains.com/webstorm/。また、選択したOSとしてLinuxMintを使用しています。
一般的なWebRTCアプリケーションのもう1つの要件は、HTMLファイルとJavascriptファイルをホストするサーバーを用意することです。ファイルが実際のサーバーによって提供されていない限り、ブラウザーはカメラやマイクに接続できないため、コードはファイルをダブルクリックしただけでは機能しません。これは明らかにセキュリティの問題のために行われます。
さまざまなWebサーバーがたくさんありますが、このチュートリアルでは、node-staticでNode.jsを使用します。
訪問 https://nodejs.org/en/ そして、最新のNode.jsバージョンをダウンロードします。
/ usr / local / nodejsディレクトリに解凍します。
/home/YOUR_USERNAME/.profileファイルを開き、最後に次の行を追加します-export PATH = $ PATH:/ usr / local / nodejs / bin
コンピュータを再起動するか、ソース/home/YOUR_USERNAME/.profileを実行できます
これで、ノードコマンドがコマンドラインから利用できるようになります。NPMのコマンドも使用可能です。NMPは、Node.jsのパッケージマネージャーです。あなたはでもっと学ぶことができますhttps://www.npmjs.com/。
ターミナルを開き、sudo npm install -gnode-staticを実行します。これにより、Node.jsの静的Webサーバーがインストールされます。
次に、HTMLファイルを含む任意のディレクトリに移動し、ディレクトリ内でstaticコマンドを実行して、Webサーバーを起動します。
に移動できます http://localhost:8080 ファイルを表示します。
nodejsをインストールする別の方法があります。ターミナルウィンドウでsudoapt-get installnodejsを実行するだけです。
Node.jsのインストールをテストするには、ターミナルを開いてnodeコマンドを実行します。いくつかのコマンドを入力して、どのように機能するかを確認します-
Node.jsは、Javascriptファイルとターミナルに入力されたコマンドを実行します。次の内容でindex.jsファイルを作成します-
console.log(“Testing Node.js”);
次に、nodeindexコマンドを実行します。次のように表示されます-
シグナリングサーバーを構築するときは、Node.js用のWebSocketライブラリを使用します。実行npmにインストールするには、ターミナルにwsをインストールします。
シグナリングサーバーのテストには、wscatユーティリティを使用します。インストールするには、ターミナルウィンドウでnpm install -gwscatを実行します。
S.No | プロトコルと説明 |
---|---|
1 | WebRTCプロトコル WebRTCアプリケーションは、トランスポートプロトコルとしてUDP(ユーザーデータグラムプロトコル)を使用します。今日のほとんどのWebアプリケーションは、TCP(Transmission Control Protocol)を使用して構築されています。 |
2 | セッション記述プロトコル SDPはWebRTCの重要な部分です。これは、メディア通信セッションを説明することを目的としたプロトコルです。 |
3 | ルートを見つける 別のユーザーに接続するには、自分のネットワークと他のユーザーのネットワークの周りに明確なパスを見つける必要があります。ただし、セキュリティの問題を回避するために、使用しているネットワークにいくつかのレベルのアクセス制御がある可能性があります。 |
4 | ストリーム制御伝送プロトコル ピア接続を使用すると、ビデオとオーディオのデータをすばやく送信できます。現在、SCTPプロトコルは、RTCDataChannelオブジェクトを使用するときに、現在セットアップされているピア接続の上にblobデータを送信するために使用されています。 |
概要
この章では、UDP、TCP、STUN、TURN、ICE、SCTPなど、ピア接続を可能にするいくつかのテクノロジーについて説明しました。これで、SDPがどのように機能するかとそのユースケースについて表面レベルで理解できたはずです。
MediaStream APIは、ローカルカメラとマイクからメディアストリームに簡単にアクセスできるように設計されています。getUserMedia()メソッドは、ローカル入力デバイスにアクセスするための主要な方法です。
APIにはいくつかの重要なポイントがあります-
リアルタイムメディアストリームは、ビデオまたはオーディオの形式のストリームオブジェクトによって表されます
Webアプリケーションがストリームのフェッチを開始する前にユーザーに尋ねるユーザー権限を通じてセキュリティレベルを提供します
入力デバイスの選択は、MediaStream APIによって処理されます(たとえば、デバイスに2つのカメラまたはマイクが接続されている場合)
各MediaStreamオブジェクトには、いくつかのMediaStreamTrackオブジェクトが含まれています。これらは、さまざまな入力デバイスからのビデオとオーディオを表します。
各MediaStreamTrackオブジェクトには、複数のチャネル(右および左のオーディオチャネル)が含まれる場合があります。これらは、MediaStreamAPIによって定義された最小の部分です。
MediaStreamオブジェクトを出力する方法は2つあります。まず、出力をビデオまたはオーディオ要素にレンダリングできます。次に、出力をRTCPeerConnectionオブジェクトに送信し、RTCPeerConnectionオブジェクトがそれをリモートピアに送信します。
MediaStreamAPIの使用
簡単なWebRTCアプリケーションを作成しましょう。画面にビデオ要素を表示し、カメラの使用許可をユーザーに要求し、ブラウザーにライブビデオストリームを表示します。index.htmlファイルを作成します-
<!DOCTYPE html>
<html lang = "en">
<head>
<meta charset = "utf-8" />
</head>
<body>
<video autoplay></video>
<script src = "client.js"></script>
</body>
</html>
次に、client.jsファイルを作成し、以下を追加します。
function hasUserMedia() {
//check if the browser supports the WebRTC
return !!(navigator.getUserMedia || navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia);
}
if (hasUserMedia()) {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
|| navigator.mozGetUserMedia;
//enabling video and audio channels
navigator.getUserMedia({ video: true, audio: true }, function (stream) {
var video = document.querySelector('video');
//inserting our stream to the video tag
video.src = window.URL.createObjectURL(stream);
}, function (err) {});
} else {
alert("WebRTC is not supported");
}
ここでは、WebRTCがサポートされているかどうかをチェックするhasUserMedia()関数を作成します。次に、getUserMedia関数にアクセスします。ここで、2番目のパラメーターは、ユーザーのデバイスからのストリームを受け入れるコールバックです。次に、window.URL.createObjectURLを使用してストリームをビデオ要素にロードします。これにより、パラメーターで指定されたオブジェクトを表すURLが作成されます。
ページを更新し、[許可]をクリックすると、画面に顔が表示されます。
Webサーバーを使用してすべてのスクリプトを実行することを忘れないでください。WebRTC環境チュートリアルにすでにインストールされています。
MediaStream API
プロパティ
MediaStream.active (read only) − MediaStreamがアクティブな場合はtrueを返し、そうでない場合はfalseを返します。
MediaStream.ended (read only, deprecated)−終了イベントがオブジェクトで発生した場合はtrueを返します。これは、ストリームが完全に読み取られたことを意味します。ストリームの終わりに達していない場合はfalseを返します。
MediaStream.id (read only) −オブジェクトの一意の識別子。
MediaStream.label (read only, deprecated) −ユーザーエージェントによって割り当てられた一意の識別子。
上記のプロパティが私のブラウザでどのように表示されるかを確認できます-
イベントハンドラー
MediaStream.onactive−MediaStreamオブジェクトがアクティブになったときに発生するアクティブイベントのハンドラー。
MediaStream.onaddtrack−新しいMediaStreamTrackオブジェクトが追加されたときに発生するaddtrackイベントのハンドラー。
MediaStream.onended (deprecated)−ストリーミングの終了時に発生する終了イベントのハンドラー。
MediaStream.oninactive−MediaStreamオブジェクトが非アクティブになったときに発生する非アクティブイベントのハンドラー。
MediaStream.onremovetrack-ハンドラのremoveTrackときに発生するイベントMediaStreamTrackのオブジェクトは、それから削除されます。
メソッド
MediaStream.addTrack()−引数として指定されたMediaStreamTrackオブジェクトをMediaStreamに追加します。トラックがすでに追加されている場合、何も起こりません。
MediaStream.clone() −新しいIDを持つMediaStreamオブジェクトのクローンを返します。
MediaStream.getAudioTracks()-オーディオのリストを返しMediaStreamTrackがからオブジェクトMediaStreamののオブジェクトを。
MediaStream.getTrackById()−トラックをIDで返します。引数が空の場合、またはIDが見つからない場合は、nullを返します。複数のトラックが同じIDを持っている場合、最初のトラックを返します。
MediaStream.getTracks()-すべてのリストを返しますMediaStreamTrackがからオブジェクトMediaStreamののオブジェクトを。
MediaStream.getVideoTracks()-ビデオのリストを返しMediaStreamTrackがからオブジェクトMediaStreamののオブジェクトを。
MediaStream.removeTrack()−引数として指定されたMediaStreamTrackオブジェクトをMediaStreamから削除します。トラックがすでに削除されている場合、何も起こりません。
上記のAPIの変更をテストするには、index.htmlを次のように変更します-
<!DOCTYPE html>
<html lang = "en">
<head>
<meta charset = "utf-8" />
</head>
<body>
<video autoplay></video>
<div><button id = "btnGetAudioTracks">getAudioTracks()
</button></div>
<div><button id = "btnGetTrackById">getTrackById()
</button></div>
<div><button id = "btnGetTracks">getTracks()</button></div>
<div><button id = "btnGetVideoTracks">getVideoTracks()
</button></div>
<div><button id = "btnRemoveAudioTrack">removeTrack() - audio
</button></div>
<div><button id = "btnRemoveVideoTrack">removeTrack() - video
</button></div>
<script src = "client.js"></script>
</body>
</html>
いくつかのMediaStreamAPIを試すために、いくつかのボタンを追加しました。次に、新しく作成したボタンのイベントハンドラーを追加する必要があります。client.jsファイルをこのように変更します-
var stream;
function hasUserMedia() {
//check if the browser supports the WebRTC
return !!(navigator.getUserMedia || navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia);
}
if (hasUserMedia()) {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia
|| navigator.mozGetUserMedia;
//enabling video and audio channels
navigator.getUserMedia({ video: true, audio: true }, function (s) {
stream = s;
var video = document.querySelector('video');
//inserting our stream to the video tag
video.src = window.URL.createObjectURL(stream);
}, function (err) {});
} else {
alert("WebRTC is not supported");
}
btnGetAudioTracks.addEventListener("click", function(){
console.log("getAudioTracks");
console.log(stream.getAudioTracks());
});
btnGetTrackById.addEventListener("click", function(){
console.log("getTrackById");
console.log(stream.getTrackById(stream.getAudioTracks()[0].id));
});
btnGetTracks.addEventListener("click", function(){
console.log("getTracks()");
console.log(stream.getTracks());
});
btnGetVideoTracks.addEventListener("click", function(){
console.log("getVideoTracks()");
console.log(stream.getVideoTracks());
});
btnRemoveAudioTrack.addEventListener("click", function(){
console.log("removeAudioTrack()");
stream.removeTrack(stream.getAudioTracks()[0]);
});
btnRemoveVideoTrack.addEventListener("click", function(){
console.log("removeVideoTrack()");
stream.removeTrack(stream.getVideoTracks()[0]);
});
次に、ページを更新します。クリックしてgetAudioTracks()ボタン、その後、をクリックしてのremoveTrack() -オーディオボタン。これで、オーディオトラックが削除されます。次に、ビデオトラックについても同じようにします。
getTracks()ボタンをクリックすると、すべてのMediaStreamTracks(接続されているすべてのビデオおよびオーディオ入力)が表示されます。次に、getTrackById()をクリックして、オーディオMediaStreamTrackを取得します。
概要
この章では、MediaStreamAPIを使用して簡単なWebRTCアプリケーションを作成しました。これで、WebRTCを機能させるさまざまなMediaStreamAPIの概要が明確になりました。
RTCPeerConnection APIは、各ブラウザー間のピアツーピア接続の中核です。RTCPeerConnectionオブジェクトを作成するには、次のように記述します。
var pc = RTCPeerConnection(config);
ここで、config引数には、少なくともキーであるiceServersが含まれています。これは、ICE候補の検索中に使用されるSTUNおよびTURNサーバーに関する情報を含むURLオブジェクトの配列です。code.google.comで利用可能なパブリックSTUNサーバーのリストを見つけることができます
呼び出し元であるか呼び出し先であるかに応じて、RTCPeerConnectionオブジェクトは、接続の両側でわずかに異なる方法で使用されます。
これがユーザーのフローの例です-
onicecandidateハンドラーを登録します。ICE候補は、受信されると、他のピアに送信されます。
onaddstreamハンドラーを登録します。リモートピアから受信したビデオストリームの表示を処理します。
メッセージハンドラを登録します。シグナリングサーバーには、他のピアから受信したメッセージのハンドラーも必要です。メッセージにRTCSessionDescriptionオブジェクトが含まれている場合は、setRemoteDescription()メソッドを使用してRTCPeerConnectionオブジェクトに追加する必要があります。メッセージにRTCIceCandidateオブジェクトが含まれている場合は、addIceCandidate()メソッドを使用してRTCPeerConnectionオブジェクトに追加する必要があります。
getUserMedia()を利用してローカルメディアストリームを設定し、addStream()メソッドを使用してRTCPeerConnectionオブジェクトに追加します。
オファー/アンサー交渉プロセスを開始します。これは、呼び出し元のフローが呼び出し先のフローと異なる唯一のステップです。呼び出し元は、createOffer()メソッドを使用してネゴシエーションを開始し、RTCSessionDescriptionオブジェクトを受信するコールバックを登録します。そして、このコールバックは、この追加する必要がありRTCSessionDescriptionのあなたにオブジェクトをRTCPeerConnectionの使用してオブジェクトsetLocalDescriptionを() 。そして最後に、呼び出し元は、シグナリングサーバーを使用してこのRTCSessionDescriptionをリモートピアに送信する必要があります。一方、呼び出し先は同じコールバックを登録しますが、createAnswer()メソッドに登録します。呼び出し先フローは、呼び出し元からオファーを受け取った後にのみ開始されることに注意してください。
RTCPeerConnection API
プロパティ
RTCPeerConnection.iceConnectionState (read only)−接続の状態を説明するRTCIceConnectionState列挙型を返します。この値が変更されると、iceconnectionstatechangeイベントが発生します。可能な値-
new −ICEエージェントはリモート候補者を待っているかアドレスを収集しています
checking − ICEエージェントにはリモート候補がありますが、まだ接続が見つかりません
connected − ICEエージェントは使用可能な接続を検出しましたが、より良い接続のためにより多くのリモート候補をチェックしています。
completed − ICEエージェントは使用可能な接続を検出し、リモート候補のテストを停止しました。
failed − ICEエージェントはすべてのリモート候補をチェックしましたが、少なくとも1つのコンポーネントに一致するものが見つかりませんでした。
disconnected −少なくとも1つのコンポーネントが生きていません。
closed −ICEエージェントは閉じられています。
RTCPeerConnection.iceGatheringState (read only) −接続のICE収集状態を説明するRTCIceGatheringState列挙型を返します−
new −オブジェクトが作成されたばかりです。
gathering −ICEエージェントは候補者を集める過程にあります
complete ICEエージェントは収集を完了しました。
RTCPeerConnection.localDescription (read only)−ローカルセッションを説明するRTCSessionDescriptionを返します。まだ設定されていない場合はnullになる可能性があります。
RTCPeerConnection.peerIdentity (read only)−RTCIdentityAssertionを返します。これは、idp(ドメイン名)とリモートピアのIDを表す名前で構成されます。
RTCPeerConnection.remoteDescription (read only)−リモートセッションを説明するRTCSessionDescriptionを返します。まだ設定されていない場合はnullになる可能性があります。
RTCPeerConnection.signalingState (read only)−ローカル接続のシグナリング状態を説明するRTCSignalingState列挙型を返します。この状態は、SDPオファーを表します。この値が変更されると、signalingstatechangeイベントが発生します。可能な値-
stable−初期状態。進行中のSDPオファー/アンサー交換はありません。
have-local-offer −接続のローカル側がSDPオファーをローカルに適用しました。
have-remote-offer −接続のリモート側がローカルでSDPオファーを適用しました。
have-local-pranswer −リモートSDPオファーが適用され、SDPpranswerがローカルに適用されました。
have-remote-pranswer −ローカルSDPが適用され、SDPpranswerがリモートで適用されました。
closed −接続が閉じられます。
イベントハンドラー
S.No. | イベントハンドラーと説明 |
---|---|
1 | RTCPeerConnection.onaddstream このハンドラーは、addstreamイベントが発生したときに呼び出されます。このイベントは、MediaStreamがリモートピアによってこの接続に追加されたときに送信されます。 |
2 | RTCPeerConnection.ondatachannel このハンドラーは、データチャネルイベントが発生したときに呼び出されます。このイベントは、RTCDataChannelがこの接続に追加されたときに送信されます。 |
3 | RTCPeerConnection.onicecandidate このハンドラーは、icecandidateイベントが発生したときに呼び出されます。このイベントは、RTCIceCandidateオブジェクトがスクリプトに追加されたときに送信されます。 |
4 | RTCPeerConnection.oniceconnectionstatechange このハンドラーは、iceconnectionstatechangeイベントが発生したときに呼び出されます。このイベントは、iceConnectionStateの値が変更されたときに送信されます。 |
5 | RTCPeerConnection.onidentityresult このハンドラーは、identityresultイベントが発生したときに呼び出されます。このイベントは、getIdentityAssertion()を介したオファーまたはアンサーの作成中にIDアサーションが生成されたときに送信されます。 |
6 | RTCPeerConnection.onidpassertionerror このハンドラーは、idpassertionerrorイベントが発生したときに呼び出されます。このイベントは、IDアサーションの生成中にIdP(Identitry Provider)がエラーを検出したときに送信されます。 |
7 | RTCPeerConnection.onidpvalidation このハンドラーは、idpvalidationerrorイベントが発生したときに呼び出されます。このイベントは、IDアサーションの検証中にIdP(Identitry Provider)がエラーを検出したときに送信されます。 |
8 | RTCPeerConnection.onnegotiationneeded このハンドラーは、交渉が必要なイベントが発生したときに呼び出されます。このイベントはブラウザによって送信され、将来のある時点でネゴシエーションが必要になることを通知します。 |
9 | RTCPeerConnection.onpeeridentity このハンドラーは、peeridentityイベントが発生したときに呼び出されます。このイベントは、この接続でピアIDが設定および検証されたときに送信されます。 |
10 | RTCPeerConnection.onremovestream このハンドラーは、signalingstatechangeイベントが発生したときに呼び出されます。このイベントは、signallingStateの値が変更されたときに送信されます。 |
11 | RTCPeerConnection.onsignalingstatechange このハンドラーは、removestreamイベントが発生したときに呼び出されます。このイベントは、MediaStreamがこの接続から削除されたときに送信されます。 |
メソッド
S.No. | 方法と説明 |
---|---|
1 | RTCPeerConnection() 新しいRTCPeerConnectionオブジェクトを返します。 |
2 | RTCPeerConnection.createOffer() リモートピアを見つけるためのオファー(リクエスト)を作成します。このメソッドの最初の2つのパラメーターは、成功とエラーのコールバックです。オプションの3番目のパラメーターは、オーディオまたはビデオストリームの有効化などのオプションです。 |
3 | RTCPeerConnection.createAnswer() オファー/アンサーネゴシエーションプロセス中にリモートピアが受信したオファーに対するアンサーを作成します。このメソッドの最初の2つのパラメーターは、成功とエラーのコールバックです。オプションの3番目のパラメーターは、作成する回答のオプションです。 |
4 | RTCPeerConnection.setLocalDescription() ローカル接続の説明を変更します。説明は、接続のプロパティを定義します。接続は、古い説明と新しい説明の両方をサポートできる必要があります。このメソッドは、RTCSessionDescriptionオブジェクト、説明の変更が成功した場合のコールバック、説明の変更が失敗した場合のコールバックの3つのパラメーターを取ります。 |
5 | RTCPeerConnection.setRemoteDescription() リモート接続の説明を変更します。説明は、接続のプロパティを定義します。接続は、古い説明と新しい説明の両方をサポートできる必要があります。このメソッドは、RTCSessionDescriptionオブジェクト、説明の変更が成功した場合のコールバック、説明の変更が失敗した場合のコールバックの3つのパラメーターを取ります。 |
6 | RTCPeerConnection.updateIce() リモート候補にpingを実行し、ローカル候補を収集するICEエージェントプロセスを更新します。 |
7 | RTCPeerConnection.addIceCandidate() リモート候補をICEエージェントに提供します。 |
8 | RTCPeerConnection.getConfiguration() RTCConfigurationオブジェクトを返します。これは、RTCPeerConnectionオブジェクトの構成を表します。 |
9 | RTCPeerConnection.getLocalStreams() ローカルMediaStream接続の配列を返します。 |
10 | RTCPeerConnection.getRemoteStreams() リモートMediaStream接続の配列を返します。 |
11 | RTCPeerConnection.getStreamById() 指定されたIDでローカルまたはリモートのMediaStreamを返します。 |
12 | RTCPeerConnection.addStream() ビデオまたはオーディオのローカルソースとしてMediaStreamを追加します。 |
13 | RTCPeerConnection.removeStream() ビデオまたはオーディオのローカルソースとしてMediaStreamを削除します。 |
14 | RTCPeerConnection.close() 接続を閉じます。 |
15 | RTCPeerConnection.createDataChannel() 新しいRTCDataChannelを作成します。 |
16 | RTCPeerConnection.createDTMFSender() 特定のMediaStreamTrackに関連付けられた新しいRTCDTMFSenderを作成します。接続を介してDTMF(デュアルトーン多重周波数)電話信号を送信できます。 |
17 | RTCPeerConnection.getStats() 接続に関する統計を含む新しいRTCStatsReportを作成します。 |
18 | RTCPeerConnection.setIdentityProvider() IdPを設定します。名前、通信に使用されるプロトコル、およびオプションのユーザー名の3つのパラメーターを取ります。 |
19 | RTCPeerConnection.getIdentityAssertion() IDアサーションを収集します。アプリケーションでこのメソッドを処理することは期待されていません。したがって、必要性を予測するためにのみ明示的に呼び出すことができます。 |
接続の確立
それでは、サンプルアプリケーションを作成しましょう。まず、「シグナリングサーバー」チュートリアルで作成したシグナリングサーバーを「ノードサーバー」経由で実行します。
ページには2つのテキスト入力があります。1つはログイン用で、もう1つは接続するユーザー名用です。index.htmlファイルを作成し、次のコードを追加します-
<html lang = "en">
<head>
<meta charset = "utf-8" />
</head>
<body>
<div>
<input type = "text" id = "loginInput" />
<button id = "loginBtn">Login</button>
</div>
<div>
<input type = "text" id = "otherUsernameInput" />
<button id = "connectToOtherUsernameBtn">Establish connection</button>
</div>
<script src = "client2.js"></script>
</body>
</html>
ログイン用のテキスト入力、ログインボタン、他のピアユーザー名用のテキスト入力、および彼に接続するボタンが追加されたことがわかります。次に、client.jsファイルを作成し、次のコードを追加します-
var connection = new WebSocket('ws://localhost:9090');
var name = "";
var loginInput = document.querySelector('#loginInput');
var loginBtn = document.querySelector('#loginBtn');
var otherUsernameInput = document.querySelector('#otherUsernameInput');
var connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn');
var connectedUser, myConnection;
//when a user clicks the login button
loginBtn.addEventListener("click", function(event){
name = loginInput.value;
if(name.length > 0){
send({
type: "login",
name: name
});
}
});
//handle messages from the server
connection.onmessage = function (message) {
console.log("Got message", message.data);
var data = JSON.parse(message.data);
switch(data.type) {
case "login":
onLogin(data.success);
break;
case "offer":
onOffer(data.offer, data.name);
break;
case "answer":
onAnswer(data.answer);
break;
case "candidate":
onCandidate(data.candidate);
break;
default:
break;
}
};
//when a user logs in
function onLogin(success) {
if (success === false) {
alert("oops...try a different username");
} else {
//creating our RTCPeerConnection object
var configuration = {
"iceServers": [{ "url": "stun:stun.1.google.com:19302" }]
};
myConnection = new webkitRTCPeerConnection(configuration);
console.log("RTCPeerConnection object was created");
console.log(myConnection);
//setup ice handling
//when the browser finds an ice candidate we send it to another peer
myConnection.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
}
};
connection.onopen = function () {
console.log("Connected");
};
connection.onerror = function (err) {
console.log("Got error", err);
};
// Alias for sending messages in JSON format
function send(message) {
if (connectedUser) {
message.name = connectedUser;
}
connection.send(JSON.stringify(message));
};
シグナリングサーバーへのソケット接続を確立していることがわかります。ユーザーがログインボタンをクリックすると、アプリケーションはユーザー名をサーバーに送信します。ログインが成功すると、アプリケーションはRTCPeerConnectionオブジェクトを作成し、見つかったすべてのicecandidateを他のピアに送信するonicecandidateハンドラーをセットアップします。次に、ページを開いてログインしてみます。次のコンソール出力が表示されます-
次のステップは、他のピアへのオファーを作成することです。次のコードをclient.jsファイルに追加します-
//setup a peer connection with another user
connectToOtherUsernameBtn.addEventListener("click", function () {
var otherUsername = otherUsernameInput.value;
connectedUser = otherUsername;
if (otherUsername.length > 0) {
//make an offer
myConnection.createOffer(function (offer) {
console.log();
send({
type: "offer",
offer: offer
});
myConnection.setLocalDescription(offer);
}, function (error) {
alert("An error has occurred.");
});
}
});
//when somebody wants to call us
function onOffer(offer, name) {
connectedUser = name;
myConnection.setRemoteDescription(new RTCSessionDescription(offer));
myConnection.createAnswer(function (answer) {
myConnection.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("oops...error");
});
}
//when another user answers to our offer
function onAnswer(answer) {
myConnection.setRemoteDescription(new RTCSessionDescription(answer));
}
//when we got ice candidate from another user
function onCandidate(candidate) {
myConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
ユーザーが「接続の確立」ボタンをクリックすると、アプリケーションが他のピアにSDPオファーを行うことがわかります。また、設定onAnswerとonCandidateハンドラを。ページをリロードし、2つのタブで開き、2人のユーザーでログインして、それらの間の接続を確立してみます。次のコンソール出力が表示されます-
これで、ピアツーピア接続が確立されました。次のチュートリアルでは、ビデオとオーディオのストリーム、およびテキストチャットのサポートを追加します。
WebRTCは、オーディオおよびビデオストリームの転送だけでなく、任意のデータの転送にも優れています。ここで、RTCDataChannelオブジェクトが役立ちます。
RTCDataChannel API
プロパティ
RTCDataChannel.label (read only) −データチャネル名を含む文字列を返します。
RTCDataChannel.ordered (read only) −メッセージの配信順序が保証されている場合はtrueを返し、保証されていない場合はfalseを返します。
RTCDataChannel.protocol (read only) −このチャネルに使用されるサブプロトコル名を含む文字列を返します。
RTCDataChannel.id (read only) −RTCDataChannelオブジェクトの作成時に設定されたチャネルの一意のIDを返します。
RTCDataChannel.readyState (read only)−接続の状態を表すRTCDataChannelState列挙型を返します。可能な値-
connecting−接続がまだアクティブでないことを示します。これが初期状態です。
open −接続が実行中であることを示します。
closing−接続がシャットダウン中であることを示します。キャッシュされたメッセージは送信または受信中ですが、新しく作成されたタスクは受け入れていません。
closed −接続を確立できなかったか、シャットダウンされたことを示します。
RTCDataChannel.bufferedAmount (read only)−送信のためにキューに入れられたバイト数を返します。これは、RTCDataChannel.send()を介してまだ送信されていないデータの量です。
RTCDataChannel.bufferedAmountLowThreshold−RTCDataChannel.bufferedAmountがローとして使用されるバイト数を返します。RTCDataChannel.bufferedAmountがこのしきい値を下回ると、bufferedamountlowイベントが発生します。
RTCDataChannel.binaryType−接続によって送信されたバイナリデータのタイプを返します。「blob」または「arraybuffer」にすることができます。
RTCDataChannel.maxPacketLifeType (read only) −メッセージングが信頼できないモードになっているときのウィンドウの長さをミリ秒単位で示すunsignedshortを返します。
RTCDataChannel.maxRetransmits (read only) −チャネルが配信されない場合にデータを再送信する最大回数を示すunsignedshortを返します。
RTCDataChannel.negotiated (read only) −チャネルがユーザーエージェントによってネゴシエートされたか、アプリケーションによってネゴシエートされたかを示すブール値を返します。
RTCDataChannel.reliable (read only) −接続が信頼できないモードでメッセージを送信できることを示すブール値を返します。
RTCDataChannel.stream (read only) −RTCDataChannel.idの同義語
イベントハンドラー
RTCDataChannel.onopen−このイベントハンドラは、openイベントが発生したときに呼び出されます。このイベントは、データ接続が確立されたときに送信されます。
RTCDataChannel.onmessage−このイベントハンドラは、メッセージイベントが発生したときに呼び出されます。イベントは、メッセージがデータチャネルで利用可能になったときに送信されます。
RTCDataChannel.onbufferedamountlow−このイベントハンドラは、bufferedamoutlowイベントが発生したときに呼び出されます。このイベントは、RTCDataChannel.bufferedAmountがRTCDataChannel.bufferedAmountLowThresholdプロパティを下回ったときに送信されます。
RTCDataChannel.onclose−このイベントハンドラは、closeイベントが発生したときに呼び出されます。このイベントは、データ接続が閉じられたときに送信されます。
RTCDataChannel.onerror−このイベントハンドラは、エラーイベントが発生したときに呼び出されます。このイベントは、エラーが発生したときに送信されます。
メソッド
RTCDataChannel.close() −データチャネルを閉じます。
RTCDataChannel.send()−パラメータのデータをチャネル経由で送信します。データは、blob、文字列、ArrayBuffer、またはArrayBufferViewにすることができます。
それでは、簡単な例を作成しましょう。まず、「シグナリングサーバー」チュートリアルで作成したシグナリングサーバーを「ノードサーバー」経由で実行します。
ページには3つのテキスト入力があります。1つはログイン用、1つはユーザー名用、もう1つは他のピアに送信するメッセージ用です。index.htmlファイルを作成し、次のコードを追加します-
<html lang = "en">
<head>
<meta charset = "utf-8" />
</head>
<body>
<div>
<input type = "text" id = "loginInput" />
<button id = "loginBtn">Login</button>
</div>
<div>
<input type = "text" id = "otherUsernameInput" />
<button id = "connectToOtherUsernameBtn">Establish connection</button>
</div>
<div>
<input type = "text" id = "msgInput" />
<button id = "sendMsgBtn">Send text message</button>
</div>
<script src = "client.js"></script>
</body>
</html>
また、ログイン、接続の確立、メッセージの送信のための3つのボタンを追加しました。次に、client.jsファイルを作成し、次のコードを追加します-
var connection = new WebSocket('ws://localhost:9090');
var name = "";
var loginInput = document.querySelector('#loginInput');
var loginBtn = document.querySelector('#loginBtn');
var otherUsernameInput = document.querySelector('#otherUsernameInput');
var connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn');
var msgInput = document.querySelector('#msgInput');
var sendMsgBtn = document.querySelector('#sendMsgBtn');
var connectedUser, myConnection, dataChannel;
//when a user clicks the login button
loginBtn.addEventListener("click", function(event) {
name = loginInput.value;
if(name.length > 0) {
send({
type: "login",
name: name
});
}
});
//handle messages from the server
connection.onmessage = function (message) {
console.log("Got message", message.data);
var data = JSON.parse(message.data);
switch(data.type) {
case "login":
onLogin(data.success);
break;
case "offer":
onOffer(data.offer, data.name);
break;
case "answer":
onAnswer(data.answer);
break;
case "candidate":
onCandidate(data.candidate);
break;
default:
break;
}
};
//when a user logs in
function onLogin(success) {
if (success === false) {
alert("oops...try a different username");
} else {
//creating our RTCPeerConnection object
var configuration = {
"iceServers": [{ "url": "stun:stun.1.google.com:19302" }]
};
myConnection = new webkitRTCPeerConnection(configuration, {
optional: [{RtpDataChannels: true}]
});
console.log("RTCPeerConnection object was created");
console.log(myConnection);
//setup ice handling
//when the browser finds an ice candidate we send it to another peer
myConnection.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
openDataChannel();
}
};
connection.onopen = function () {
console.log("Connected");
};
connection.onerror = function (err) {
console.log("Got error", err);
};
// Alias for sending messages in JSON format
function send(message) {
if (connectedUser) {
message.name = connectedUser;
}
connection.send(JSON.stringify(message));
};
シグナリングサーバーへのソケット接続を確立していることがわかります。ユーザーがログインボタンをクリックすると、アプリケーションはユーザー名をサーバーに送信します。ログインが成功すると、アプリケーションはRTCPeerConnectionオブジェクトを作成し、見つかったすべてのicecandidateを他のピアに送信するonicecandidateハンドラーをセットアップします。また、dataChannelを作成するopenDataChannel()関数も実行します。RTCPeerConnectionオブジェクトを作成する場合、コンストラクターの2番目の引数はオプションであることに注意してください。ChromeまたはOperaを使用している場合は、[{RtpDataChannels:true}]が必須です。次のステップは、他のピアへのオファーを作成することです。次のコードをclient.jsファイルに追加します-
//setup a peer connection with another user
connectToOtherUsernameBtn.addEventListener("click", function () {
var otherUsername = otherUsernameInput.value;
connectedUser = otherUsername;
if (otherUsername.length > 0) {
//make an offer
myConnection.createOffer(function (offer) {
console.log();
send({
type: "offer",
offer: offer
});
myConnection.setLocalDescription(offer);
}, function (error) {
alert("An error has occurred.");
});
}
});
//when somebody wants to call us
function onOffer(offer, name) {
connectedUser = name;
myConnection.setRemoteDescription(new RTCSessionDescription(offer));
myConnection.createAnswer(function (answer) {
myConnection.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("oops...error");
});
}
//when another user answers to our offer
function onAnswer(answer) {
myConnection.setRemoteDescription(new RTCSessionDescription(answer));
}
//when we got ice candidate from another user
function onCandidate(candidate) {
myConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
ユーザーが「接続の確立」ボタンをクリックすると、アプリケーションが他のピアにSDPオファーを行うことがわかります。また、設定onAnswerとonCandidateハンドラを。最後に、dataChannelを作成するopenDataChannel()関数を実装しましょう。次のコードをclient.jsファイルに追加します-
//creating data channel
function openDataChannel() {
var dataChannelOptions = {
reliable:true
};
dataChannel = myConnection.createDataChannel("myDataChannel", dataChannelOptions);
dataChannel.onerror = function (error) {
console.log("Error:", error);
};
dataChannel.onmessage = function (event) {
console.log("Got message:", event.data);
};
}
//when a user clicks the send message button
sendMsgBtn.addEventListener("click", function (event) {
console.log("send message");
var val = msgInput.value;
dataChannel.send(val);
});
ここでは、接続用のdataChannelを作成し、「メッセージの送信」ボタンのイベントハンドラーを追加します。次に、このページを2つのタブで開き、2人のユーザーでログインし、接続を確立して、メッセージを送信してみます。コンソール出力にそれらが表示されるはずです。上記の例はOperaでテストされていることに注意してください。
これで、RTCDataChannelがWebRTCAPIの非常に強力な部分であることがわかります。このオブジェクトには、ピアツーピアゲームやトレントベースのファイル共有など、他にも多くのユースケースがあります。
ほとんどのWebRTCアプリケーションは、ビデオとオーディオを介して通信できるだけではありません。他にも多くの機能が必要です。この章では、基本的なシグナリングサーバーを構築します。
シグナリングとネゴシエーション
別のユーザーに接続するには、そのユーザーがWeb上のどこにいるかを知っておく必要があります。デバイスのIPアドレスを使用すると、インターネット対応デバイスが相互に直接データを送信できます。RTCPeerConnectionのオブジェクトは、このために責任があります。デバイスがインターネット上でお互いを見つける方法を知るとすぐに、各デバイスがサポートするプロトコルとコーデックに関するデータの交換を開始します。
別のユーザーと通信するには、連絡先情報を交換するだけで、残りはWebRTCによって行われます。他のユーザーに接続するプロセスは、シグナリングおよびネゴシエーションとも呼ばれます。それはいくつかのステップで構成されています-
ピア接続の潜在的な候補のリストを作成します。
ユーザーまたはアプリケーションは、接続するユーザーを選択します。
シグナリング層は、誰かが彼に接続したいことを別のユーザーに通知します。彼は受け入れるか拒否することができます。
最初のユーザーには、オファーの受諾が通知されます。
最初のユーザーは、別のユーザーとのRTCPeerConnectionを開始します。
両方のユーザーは、シグナリングサーバーを介してソフトウェアとハードウェアの情報を交換します。
両方のユーザーが位置情報を交換します。
接続は成功または失敗します。
WebRTC仕様には、情報交換に関する標準は含まれていません。したがって、上記はシグナリングがどのように発生するかの一例にすぎないことに注意してください。好きなプロトコルやテクノロジーを使用できます。
サーバーの構築
これから構築するサーバーは、同じコンピューター上にいない2人のユーザーを接続できるようになります。独自のシグナリングメカニズムを作成します。私たちのシグナリングサーバーは、あるユーザーが別のユーザーに電話をかけることを可能にします。ユーザーが別のユーザーに電話をかけると、サーバーはオファー、アンサー、ICE候補をそれらの間で渡し、WebRTC接続をセットアップします。
上の図は、シグナリングサーバーを使用する場合のユーザー間のメッセージングフローです。まず、各ユーザーがサーバーに登録します。この場合、これは単純な文字列のユーザー名になります。ユーザーが登録すると、お互いに電話をかけることができます。ユーザー1は、呼び出したいユーザーIDを使用してオファーを行います。他のユーザーが答える必要があります。最後に、ICE候補は、ユーザーが接続できるようになるまでユーザー間で送信されます。
WebRTC接続を作成するには、クライアントはWebRTCピア接続を使用せずにメッセージを転送できる必要があります。ここで、HTML5 WebSocket(2つのエンドポイント間の双方向ソケット接続)(WebサーバーとWebブラウザー)を使用します。それでは、WebSocketライブラリの使用を開始しましょう。server.jsファイルを作成し、次のコードを挿入します-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message){
console.log("Got message from a user:", message);
});
connection.send("Hello from server");
});
最初の行には、すでにインストールされているWebSocketライブラリが必要です。次に、ポート9090にソケットサーバーを作成します。次に、接続イベントをリッスンします。このコードは、ユーザーがサーバーにWebSocket接続を行うときに実行されます。次に、ユーザーから送信されたメッセージを聞きます。最後に、接続されたユーザーに「サーバーからこんにちは」という応答を送信します。
ここでノードサーバーを実行すると、サーバーはソケット接続のリッスンを開始するはずです。
サーバーをテストするには、すでにインストールされているwscatユーティリティを使用します。このツールは、WebSocketサーバーに直接接続してコマンドをテストするのに役立ちます。サーバーを1つのターミナルウィンドウで実行し、次に別のウィンドウを開いてwscat -c ws:// localhost:9090コマンドを実行します。クライアント側で次のように表示されます-
サーバーは、接続されたユーザーもログに記録する必要があります-
ユーザー登録
シグナリングサーバーでは、接続ごとに文字列ベースのユーザー名を使用するため、メッセージの送信先がわかります。接続ハンドラーを少し変更してみましょう-
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
このようにして、JSONメッセージのみを受け入れます。次に、接続されているすべてのユーザーをどこかに保存する必要があります。単純なJavascriptオブジェクトを使用します。ファイルの先頭を変更します-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
クライアントからのすべてのメッセージにタイプフィールドを追加します。たとえば、ユーザーがログインしたい場合、ユーザーはログインタイプのメッセージを送信します。それを定義しましょう-
connection.on('message', function(message){
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
ユーザーがログインタイプのメッセージを送信した場合、次のようになります。
誰かがすでにこのユーザー名でログインしていないか確認してください
その場合は、ログインに成功していないことをユーザーに伝えます
このユーザー名を使用しているユーザーがいない場合は、接続オブジェクトのキーとしてユーザー名を追加します。
コマンドが認識されない場合、エラーを送信します。
次のコードは、接続にメッセージを送信するためのヘルパー関数です。server.jsファイルに追加します-
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
上記の関数は、すべてのメッセージがJSON形式で送信されることを保証します。
ユーザーが切断したら、接続をクリーンアップする必要があります。closeイベントが発生したときにユーザーを削除できます。次のコードを接続ハンドラーに追加します-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
それでは、loginコマンドを使用してサーバーをテストしましょう。すべてのメッセージはJSON形式でエンコードする必要があることに注意してください。サーバーを実行してログインしてみてください。このようなものが表示されるはずです-
電話をかける
ログインに成功した後、ユーザーは別のユーザーに電話をかけたいと考えています。彼はそれを達成するために別のユーザーに申し出をする必要があります。オファーハンドラーを追加します-
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null){
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
まず、呼び出しようとしているユーザーの接続を取得します。存在する場合は、オファーの詳細を送信します。また、otherNameを接続オブジェクトに追加します。これは、後で見つけやすくするために作成されています。
答える
応答への応答には、オファーハンドラーで使用したのと同様のパターンがあります。私たちのサーバーは、別のユーザーへの回答としてすべてのメッセージを通過させるだけです。オファーハンダーの後に次のコードを追加します-
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
これがオファーハンドラーとどのように似ているかがわかります。このコードは、RTCPeerConnectionオブジェクトのcreateOffer関数とcreateAnswer関数の後に続くことに注意してください。
これで、オファー/アンサーメカニズムをテストできます。2つのクライアントを同時に接続し、申し出と回答を試みます。次のように表示されます-
この例では、 offer そして answer は単純な文字列ですが、実際のアプリケーションでは、SDPデータが入力されます。
ICE候補
最後の部分は、ユーザー間でICE候補を処理することです。ユーザー間でメッセージを渡すだけでも同じ手法を使用します。主な違いは、候補メッセージがユーザーごとに任意の順序で複数回発生する可能性があることです。候補ハンドラーを追加します-
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
オファーハンドラーとアンサーハンドラーと同様に機能するはずです。
接続を離れる
ユーザーが別のユーザーから切断できるようにするには、ハングアップ機能を実装する必要があります。また、すべてのユーザー参照を削除するようにサーバーに指示します。追加しますleave ハンドラー-
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
これにより、他のユーザーにも休暇イベントが送信されるため、他のユーザーはそれに応じてピア接続を切断できます。また、ユーザーがシグナリングサーバーから接続を切断した場合にも対処する必要があります。クローズハンドラーを変更しましょう-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
これで、接続が終了すると、ユーザーは切断されます。近くに私たちがまだいる間、ユーザーが自分のブラウザウィンドウを閉じたときにイベントが発生します提供、解答または候補状態。
完全なシグナリングサーバー
これが私たちのシグナリングサーバーのコード全体です-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "candidate" state
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
connection.send("Hello world");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
これで作業が完了し、シグナリングサーバーの準備が整いました。WebRTC接続を確立するときに順不同で行うと、問題が発生する可能性があることに注意してください。
概要
この章では、シンプルでわかりやすいシグナリングサーバーを構築しました。シグナリングプロセス、ユーザー登録、およびオファー/アンサーメカニズムについて説明しました。また、ユーザー間での候補の送信も実装しました。
Webは非常に速く動いており、常に改善されています。新しい標準は毎日作成されます。ブラウザーを使用すると、ユーザーが知らないうちに更新をインストールできるため、WebおよびWebRTCの世界で何が起こっているかについていく必要があります。これが今日までのことの概要です。
ブラウザのサポート
すべてのブラウザーに、同じWebRTC機能がすべて同時に備わっているわけではありません。さまざまなブラウザーが時代を先取りしている可能性があります。これにより、一部のWebRTC機能が1つのブラウザーで機能し、別のブラウザーでは機能しなくなります。次の図に、ブラウザーでのWebRTCの現在のサポートを示します。
最新のWebRTCサポートステータスは、次のURLで確認できます。 http://caniuse.com/#feat=rtcpeerconnection.
Chrome、Firefox、Opera
Mac OS X、Windows、Linuxなどの主流のPCオペレーティングシステム上のChrome、Firefox、Operaの最新バージョンは、すべてWebRTCをすぐにサポートします。そして最も重要なことは、ChromeとFirefoxの開発者チームのエンジニアが協力して問題を修正し、これら2つのブラウザーが互いに簡単に通信できるようにしたことです。
Android OS
Androidオペレーティングシステムでは、ChromeおよびFirefox用のWebRTCアプリケーションはそのままで動作するはずです。Android Ice Cream Sandwichバージョン(4.0)以降は、他のブラウザで動作するようになります。これは、デスクトップバージョンとモバイルバージョンの間でコードが共有されているためです。
林檎
Appleは、OS XのSafariでWebRTCをサポートする計画についてまだ発表していません。ハイブリッドネイティブiOSアプリケーションの可能な回避策の1つは、WebRTCコードをアプリケーションに直接埋め込み、このアプリをWebViewにロードすることです。
インターネットエクスプローラ
Microsoftは、デスクトップでのWebRTCをサポートしていません。しかし、彼らはIE(Edge)の将来のバージョンでORTC(Object Realtime Communications)を実装することを公式に確認しています。彼らはWebRTC1.0をサポートする予定はありません。彼らはORTCをWebRTC1.1とラベル付けしましたが、これは単なるコミュニティの機能強化であり、公式の標準ではありません。最近、彼らは最新のMicrosoftEdgeバージョンにORTCサポートを追加しました。あなたはでもっと学ぶかもしれませんhttps://blogs.windows.com/msedgedev/2015/09/18/ortc-api-is-now-available-in-microsoftedge/.
概要
WebRTCはAPIとプロトコルのコレクションであり、単一のAPIではないことに注意してください。これらのそれぞれのサポートは、さまざまなブラウザとオペレーティングシステムでさまざまなレベルで開発されています。最新レベルのサポートを確認するための優れた方法は、http://canisue.com.複数のブラウザでの最新のAPIの採用を追跡します。また、ブラウザのサポートやWebRTCデモに関する最新情報は次の場所にあります。http://www.webrtc.org, これは、Mozilla、Google、およびOperaでサポートされています。
モバイルの世界では、WebRTCのサポートはデスクトップと同じレベルではありません。モバイルデバイスには独自の方法があるため、WebRTCもモバイルプラットフォームでは異なります。
デスクトップ用のWebRTCアプリケーションを開発するときは、Chrome、Firefox、またはOperaの使用を検討します。それらはすべて、そのままWebRTCをサポートします。一般に、必要なのはブラウザだけで、デスクトップのハードウェアについては気にしません。
モバイルの世界では、今日のWebRTCには3つの可能なモードがあります-
- ネイティブアプリケーション
- ブラウザアプリケーション
- ネイティブブラウザ
アンドロイド
2013年、Android用のFirefox Webブラウザーには、そのままWebRTCサポートが提供されました。これで、Firefoxモバイルブラウザを使用してAndroidデバイスでビデオ通話を行うことができます。
3つの主要なWebRTCコンポーネントがあります-
PeerConnection −ブラウザ間の通話を有効にします
getUserMedia −カメラとマイクへのアクセスを提供します
DataChannels −ピアツーピアのデータ転送を提供します
Google Chrome for Androidは、WebRTCサポートも提供します。すでにお気づきのように、最も興味深い機能は通常Chromeで最初に表示されます。
昨年、OperaモバイルブラウザはWebRTCをサポートして登場しました。つまり、Androidには、Chrome、Firefox、Operaがあります。他のブラウザはWebRTCをサポートしていません。
iOS
残念ながら、WebRTCは現在iOSではサポートされていません。WebRTCは、Firefox、Opera、またはChromeを使用しているMacでは正常に機能しますが、iOSではサポートされていません。
現在、WebRTCアプリケーションはそのままではAppleモバイルデバイスでは機能しません。しかし、ブラウザがあります-クッパ。これはエリクソンによって開発されたWebブラウザーであり、そのままWebRTCをサポートします。あなたはそのホームページをチェックすることができますhttp://www.openwebrtc.org/bowser/.
今日、これはiOSでWebRTCアプリケーションをサポートする唯一のフレンドリーな方法です。もう1つの方法は、ネイティブアプリケーションを自分で開発することです。
WindowsPhone
Microsoftは、モバイルプラットフォームでのWebRTCをサポートしていません。しかし、彼らは、IEの将来のバージョンでORTC(Object Realtime Communications)を実装することを公式に確認しています。彼らはWebRTC1.0をサポートする予定はありません。彼らはORTCをWebRTC1.1とラベル付けしましたが、これは単なるコミュニティの機能強化であり、公式の標準ではありません。
そのため、今日、Window PhoneユーザーはWebRTCアプリケーションを使用できず、この状況に打ち勝つ方法はありません。
ブラックベリー
WebRTCアプリケーションは、Blackberryでもサポートされていません。
WebRTCネイティブブラウザの使用
ユーザーがWebRTCを利用するための最も便利で快適なケースは、デバイスのネイティブブラウザーを使用することです。この場合、デバイスは追加の構成を実行する準備ができています。
現在、バージョン4以降のAndroidデバイスのみがこの機能を提供しています。Appleは、WebRTCをサポートするアクティビティをまだ表示していません。そのため、SafariユーザーはWebRTCアプリケーションを使用できません。Microsoftは、Windows Phone8にも導入していません。
ブラウザアプリケーションを介したWebRTCの使用
これは、WebRTC機能を提供するためにサードパーティアプリケーション(非ネイティブWebブラウザ)を使用することを意味します。今のところ、そのようなサードパーティアプリケーションは2つあります。Bowserは、iOSデバイスにWebRTC機能を導入する唯一の方法であり、OperaはAndroidプラットフォームの優れた代替手段です。利用可能な残りのモバイルブラウザはWebRTCをサポートしていません。
Native Mobile Applications
As you can see, WebRTC does not have a large support in the mobile world yet. So, one of the possible solutions is to develop a native applications that utilize the WebRTC API. But it is not the better choice because the main WebRTC feature is a cross-platform solution. Anyway, in some cases this is the only way because a native application can utilize device-specific functions or features that are not supported by HTML5 browsers.
Constraining Video Stream for Mobile and Desktop Devices
The first parameter of the getUserMedia API expects an object of keys and values telling the browser how to process streams. You can check the full set of constraints at https://tools.ietf.org/html/draft-alvestrand-constraints-resolution-03. You can setup video aspect ration, frameRate, and other optional parameters.
Supporting mobile devices is one of the biggest pains because mobile devices have limited screen space along with limited resources. You might want the mobile device to only capture a 480x320 resolution or smaller video stream to save power and bandwidth. Using the user agent string in the browser is a good way to test whether the user is on a mobile device or not. Let's see an example. Create the index.html file −
<!DOCTYPE html>
<html lang = "en">
<head>
<meta charset = "utf-8" />
</head>
<body>
<video autoplay></video>
<script src = "client.js"></script>
</body>
</html>
Then create the following client.js file −
//constraints for desktop browser
var desktopConstraints = {
video: {
mandatory: {
maxWidth:800,
maxHeight:600
}
},
audio: true
};
//constraints for mobile browser
var mobileConstraints = {
video: {
mandatory: {
maxWidth: 480,
maxHeight: 320,
}
},
audio: true
}
//if a user is using a mobile browser
if(/Android|iPhone|iPad/i.test(navigator.userAgent)) {
var constraints = mobileConstraints;
} else {
var constraints = desktopConstraints;
}
function hasUserMedia() {
//check if the browser supports the WebRTC
return !!(navigator.getUserMedia || navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia);
}
if (hasUserMedia()) {
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia;
//enabling video and audio channels
navigator.getUserMedia(constraints, function (stream) {
var video = document.querySelector('video');
//inserting our stream to the video tag
video.src = window.URL.createObjectURL(stream);
}, function (err) {});
} else {
alert("WebRTC is not supported");
}
Run the web server using the static command and open the page. You should see it is 800x600. Then open this page in a mobile viewport using chrome tools and check the resolution. It should be 480x320.
Constraints are the easiest way to increase the performance of your WebRTC application.
Summary
In this chapter, we learned about the issues that can occur when developing WebRTC applications for mobile devices. We discovered different limitations of supporting the WebRTC API on mobile platforms. We also launched a demo application where we set different constraints for desktop and mobile browsers.
In this chapter, we are going to build a client application that allows two users on separate devices to communicate using WebRTC. Our application will have two pages. One for login and the other for calling another user.
The two pages will be the div tags. Most input is done through simple event handlers.
Signaling Server
To create a WebRTC connection clients have to be able to transfer messages without using a WebRTC peer connection. This is where we will use HTML5 WebSockets − a bidirectional socket connection between two endpoints − a web server and a web browser. Now let's start using the WebSocket library. Create the server.js file and insert the following code −
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
console.log("Got message from a user:", message);
});
connection.send("Hello from server");
});
The first line requires the WebSocket library which we have already installed. Then we create a socket server on the port 9090. Next, we listen to the connection event. This code will be executed when a user makes a WebSocket connection to the server. We then listen to any messages sent by the user. Finally, we send a response to the connected user saying “Hello from server”.
In our signaling server, we will use a string-based username for each connection so we know where to send messages. Let's change our connection handler a bit −
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
This way we accept only JSON messages. Next, we need to store all connected users somewhere. We will use a simple Javascript object for it. Change the top of our file −
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
We are going to add a type field for every message coming from the client. For example if a user wants to login, he sends the login type message. Let's define it −
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
If the user sends a message with the login type, we −
Check if anyone has already logged in with this username
If so, then tell the user that he hasn't successfully logged in
If no one is using this username, we add username as a key to the connection object.
If a command is not recognized we send an error.
The following code is a helper function for sending messages to a connection. Add it to the server.js file −
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
When the user disconnects we should clean up its connection. We can delete the user when the close event is fired. Add the following code to the connection handler −
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
After successful login the user wants to call another. He should make an offer to another user to achieve it. Add the offer handler −
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
Firstly, we get the connection of the user we are trying to call. If it exists we send him offer details. We also add otherName to the connection object. This is made for the simplicity of finding it later.
Answering to the response has a similar pattern that we used in the offer handler. Our server just passes through all messages as answer to another user. Add the following code after the offer handler −
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
The final part is handling ICE candidate between users. We use the same technique just passing messages between users. The main difference is that candidate messages might happen multiple times per user in any order. Add the candidate handler −
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
To allow our users to disconnect from another user we should implement the hanging up function. It will also tell the server to delete all user references. Add the leave handler −
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
This will also send the other user the leave event so he can disconnect his peer connection accordingly. We should also handle the case when a user drops his connection from the signaling server. Let's modify our close handler −
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
The following is the entire code of our signaling server −
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "candidate" state
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
connection.send("Hello world");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
クライアントアプリケーション
このアプリケーションをテストする1つの方法は、2つのブラウザタブを開いて、相互に呼び出しを試みることです。
まず、ブートストラップライブラリをインストールする必要があります。Bootstrapは、Webアプリケーションを開発するためのフロントエンドフレームワークです。あなたはでもっと学ぶことができますhttp://getbootstrap.com/.たとえば、「videochat」というフォルダを作成します。これがルートアプリケーションフォルダになります。このフォルダー内にファイルpackage.json(npmの依存関係を管理するために必要)を作成し、以下を追加します-
{
"name": "webrtc-videochat",
"version": "0.1.0",
"description": "webrtc-videochat",
"author": "Author",
"license": "BSD-2-Clause"
}
次に、npm installbootstrapを実行します。これにより、ブートストラップライブラリがvideochat / node_modulesフォルダーにインストールされます。
次に、基本的なHTMLページを作成する必要があります。次のコードを使用して、ルートフォルダにindex.htmlファイルを作成します-
<html>
<head>
<title>WebRTC Video Demo</title>
<link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
</head>
<style>
body {
background: #eee;
padding: 5% 0;
}
video {
background: black;
border: 1px solid gray;
}
.call-page {
position: relative;
display: block;
margin: 0 auto;
width: 500px;
height: 500px;
}
#localVideo {
width: 150px;
height: 150px;
position: absolute;
top: 15px;
right: 15px;
}
#remoteVideo {
width: 500px;
height: 500px;
}
</style>
<body>
<div id = "loginPage" class = "container text-center">
<div class = "row">
<div class = "col-md-4 col-md-offset-4">
<h2>WebRTC Video Demo. Please sign in</h2>
<label for = "usernameInput" class = "sr-only">Login</label>
<input type = "email" id = "usernameInput" c
lass = "form-control formgroup" placeholder = "Login"
required = "" autofocus = "">
<button id = "loginBtn" class = "btn btn-lg btn-primary btnblock">
Sign in</button>
</div>
</div>
</div>
<div id = "callPage" class = "call-page">
<video id = "localVideo" autoplay></video>
<video id = "remoteVideo" autoplay></video>
<div class = "row text-center">
<div class = "col-md-12">
<input id = "callToUsernameInput" type = "text"
placeholder = "username to call" />
<button id = "callBtn" class = "btn-success btn">Call</button>
<button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button>
</div>
</div>
</div>
<script src = "client.js"></script>
</body>
</html>
このページはおなじみのはずです。ブートストラップcssファイルを追加しました。また、2つのページを定義しました。最後に、ユーザーから情報を取得するためのいくつかのテキストフィールドとボタンを作成しました。ローカルビデオストリームとリモートビデオストリームの2つのビデオ要素が表示されます。client.jsファイルへのリンクを追加したことに注意してください。
次に、シグナリングサーバーとの接続を確立する必要があります。次のコードを使用して、ルートフォルダーにclient.jsファイルを作成します-
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
次に、ノードサーバーを介してシグナリングサーバーを実行します。次に、ルートフォルダー内で静的コマンドを実行し、ブラウザー内でページを開きます。次のコンソール出力が表示されます-
次のステップは、一意のユーザー名でログインするユーザーを実装することです。サーバーにユーザー名を送信するだけで、サーバーが取得されたかどうかを通知します。次のコードをclient.jsファイルに追加します-
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
//hide call page
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
//display the call page if login is successful
loginPage.style.display = "none";
callPage.style.display = "block";
//start peer connection
}
};
まず、ページ上の要素への参照をいくつか選択します。通話ページを非表示にします。次に、ログインボタンにイベントリスナーを追加します。ユーザーがクリックすると、ユーザー名がサーバーに送信されます。最後に、handleLoginコールバックを実装します。ログインに成功すると、通話ページが表示され、ピア接続のセットアップが開始されます。
ピア接続を開始するには、次のものが必要です。
- Webカメラからストリームを取得します。
- RTCPeerConnectionオブジェクトを作成します。
次のコードを「UIセレクターブロック」に追加します-
var localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');
var yourConn;
var stream;
handleLogin関数を変更します-
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//getting local video stream
navigator.webkitGetUserMedia({ video: true, audio: true }, function (myStream) {
stream = myStream;
//displaying local video stream on the page
localVideo.src = window.URL.createObjectURL(stream);
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration);
// setup stream listening
yourConn.addStream(stream);
//when a remote user adds stream to the peer connection, we display it
yourConn.onaddstream = function (e) {
remoteVideo.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
}, function (error) {
console.log(error);
});
}
};
これで、コードを実行すると、ページでログインしてローカルビデオストリームをページに表示できるようになります。
これで、通話を開始する準備が整いました。まず、別のユーザーにオファーを送信します。ユーザーがオファーを受け取ると、回答を作成し、ICE候補の取引を開始します。次のコードをclient.jsファイルに追加します-
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
オファーを開始する[通話]ボタンにクリックハンドラーを追加します。次に、onmessageハンドラーが期待するいくつかのハンドラーを実装します。これらは、両方のユーザーが接続を確立するまで非同期で処理されます。
最後のステップは、ハングアップ機能の実装です。これにより、データの送信が停止し、他のユーザーに通話を終了するように指示します。次のコードを追加します-
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
remoteVideo.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};
ユーザーが[電話を切る]ボタンをクリックしたとき-
- 他のユーザーに「脱退」メッセージを送信します
- RTCPeerConnectionを閉じ、接続をローカルで破棄します
次に、コードを実行します。2つのブラウザタブを使用してサーバーにログインできるはずです。その後、タブを呼び出して電話を切ることができます。
以下はclient.jsファイル全体です-
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
var localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');
var yourConn;
var stream;
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//getting local video stream
navigator.webkitGetUserMedia({ video: true, audio: true }, function (myStream) {
stream = myStream;
//displaying local video stream on the page
localVideo.src = window.URL.createObjectURL(stream);
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration);
// setup stream listening
yourConn.addStream(stream);
//when a remote user adds stream to the peer connection, we display it
yourConn.onaddstream = function (e) {
remoteVideo.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
}, function (error) {
console.log(error);
});
}
};
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
remoteVideo.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};
概要
このデモは、すべてのWebRTCアプリケーションが必要とする機能のベースラインを提供します。このデモを改善するには、FacebookやGoogleなどのプラットフォームを介してユーザーIDを追加し、無効なデータに対するユーザー入力を処理します。また、テクノロジーをサポートしていない、ファイアウォールを通過できないなどのいくつかの理由により、WebRTC接続が失敗する可能性があります。WebRTCアプリケーションを安定させるために多大な労力が費やされました。
この章では、別々のデバイス上の2人のユーザーがWebRTCオーディオストリームを使用して通信できるようにするクライアントアプリケーションを構築します。私たちのアプリケーションは2ページになります。1つはログイン用で、もう1つは別のユーザーに音声通話を発信するためのものです。
2つのページがdivタグになります。ほとんどの入力は、単純なイベントハンドラーを介して行われます。
シグナリングサーバー
WebRTC接続を作成するには、クライアントはWebRTCピア接続を使用せずにメッセージを転送できる必要があります。ここで、HTML5 WebSocket(2つのエンドポイント間の双方向ソケット接続)を使用します。WebサーバーとWebブラウザーです。それでは、WebSocketライブラリの使用を開始しましょう。server.jsファイルを作成し、次のコードを挿入します-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
console.log("Got message from a user:", message);
});
connection.send("Hello from server");
});
最初の行には、すでにインストールされているWebSocketライブラリが必要です。次に、ポート9090にソケットサーバーを作成します。次に、接続イベントをリッスンします。このコードは、ユーザーがサーバーにWebSocket接続を行うときに実行されます。次に、ユーザーから送信されたメッセージを聞きます。最後に、接続されたユーザーに「サーバーからこんにちは」という応答を送信します。
シグナリングサーバーでは、接続ごとに文字列ベースのユーザー名を使用するため、メッセージの送信先がわかります。接続ハンドラーを少し変更してみましょう-
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
このようにして、JSONメッセージのみを受け入れます。次に、接続されているすべてのユーザーをどこかに保存する必要があります。単純なJavascriptオブジェクトを使用します。ファイルの先頭を変更します-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
クライアントからのすべてのメッセージにタイプフィールドを追加します。たとえば、ユーザーがログインしたい場合、ユーザーはログインタイプのメッセージを送信します。それを定義しましょう-
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
ユーザーがログインタイプのメッセージを送信した場合、次のようになります。
- 誰かがすでにこのユーザー名でログインしていないか確認してください。
- その場合は、正常にログインしていないことをユーザーに伝えます。
- このユーザー名を使用しているユーザーがいない場合は、接続オブジェクトのキーとしてユーザー名を追加します。
- コマンドが認識されない場合、エラーを送信します。
次のコードは、接続にメッセージを送信するためのヘルパー関数です。server.jsファイルに追加します-
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
ユーザーが切断したら、接続をクリーンアップする必要があります。closeイベントが発生したときにユーザーを削除できます。次のコードを接続ハンドラーに追加します-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
ログインに成功した後、ユーザーは別のユーザーに電話をかけたいと考えています。彼はそれを達成するために別のユーザーに申し出をする必要があります。オファーハンドラーを追加します-
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
まず、呼び出しようとしているユーザーの接続を取得します。存在する場合は、オファーの詳細を送信します。また、otherNameを接続オブジェクトに追加します。これは、後で見つけやすくするために作成されています。
応答への応答には、オファーハンドラーで使用したのと同様のパターンがあります。私たちのサーバーは、別のユーザーへの回答としてすべてのメッセージを通過させるだけです。オファーハンドラの後に次のコードを追加します-
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
最後の部分は、ユーザー間でICE候補を処理することです。ユーザー間でメッセージを渡すだけでも同じ手法を使用します。主な違いは、候補メッセージがユーザーごとに任意の順序で複数回発生する可能性があることです。候補ハンドラーを追加します-
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
ユーザーが別のユーザーから切断できるようにするには、ハングアップ機能を実装する必要があります。また、すべてのユーザー参照を削除するようにサーバーに指示します。休暇ハンドラーを追加します-
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
これにより、他のユーザーにも休暇イベントが送信されるため、他のユーザーはそれに応じてピア接続を切断できます。また、ユーザーがシグナリングサーバーから接続を切断した場合にも対処する必要があります。クローズハンドラーを変更しましょう-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
以下は、シグナリングサーバーのコード全体です-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "candidate" state
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
connection.send("Hello world");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
クライアントアプリケーション
このアプリケーションをテストする1つの方法は、2つのブラウザタブを開いて、相互に音声通話を発信することです。
まず、ブートストラップライブラリをインストールする必要があります。Bootstrapは、Webアプリケーションを開発するためのフロントエンドフレームワークです。あなたはでもっと学ぶことができますhttp://getbootstrap.com/.たとえば、「audiochat」というフォルダを作成します。これがルートアプリケーションフォルダになります。このフォルダー内にファイルpackage.json(npmの依存関係を管理するために必要)を作成し、以下を追加します-
{
"name": "webrtc-audiochat",
"version": "0.1.0",
"description": "webrtc-audiochat",
"author": "Author",
"license": "BSD-2-Clause"
}
次に、npm installbootstrapを実行します。これにより、ブートストラップライブラリがaudiochat / node_modulesフォルダーにインストールされます。
次に、基本的なHTMLページを作成する必要があります。次のコードを使用して、ルートフォルダにindex.htmlファイルを作成します-
<html>
<head>
<title>WebRTC Voice Demo</title>
<link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
</head>
<style>
body {
background: #eee;
padding: 5% 0;
}
</style>
<body>
<div id = "loginPage" class = "container text-center">
<div class = "row">
<div class = "col-md-4 col-md-offset-4">
<h2>WebRTC Voice Demo. Please sign in</h2>
<label for = "usernameInput" class = "sr-only">Login</label>
<input type = "email" id = "usernameInput"
class = "form-control formgroup"
placeholder = "Login" required = "" autofocus = "">
<button id = "loginBtn" class = "btn btn-lg btn-primary btnblock">
Sign in</button>
</div>
</div>
</div>
<div id = "callPage" class = "call-page">
<div class = "row">
<div class = "col-md-6 text-right">
Local audio: <audio id = "localAudio"
controls autoplay></audio>
</div>
<div class = "col-md-6 text-left">
Remote audio: <audio id = "remoteAudio"
controls autoplay></audio>
</div>
</div>
<div class = "row text-center">
<div class = "col-md-12">
<input id = "callToUsernameInput"
type = "text" placeholder = "username to call" />
<button id = "callBtn" class = "btn-success btn">Call</button>
<button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button>
</div>
</div>
</div>
<script src = "client.js"></script>
</body>
</html>
このページはおなじみのはずです。ブートストラップcssファイルを追加しました。また、2つのページを定義しました。最後に、ユーザーから情報を取得するためのいくつかのテキストフィールドとボタンを作成しました。ローカルオーディオストリームとリモートオーディオストリームの2つのオーディオ要素が表示されます。client.jsファイルへのリンクを追加したことに注意してください。
次に、シグナリングサーバーとの接続を確立する必要があります。次のコードを使用して、ルートフォルダーにclient.jsファイルを作成します-
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
次に、ノードサーバーを介してシグナリングサーバーを実行します。次に、ルートフォルダー内で静的コマンドを実行し、ブラウザー内でページを開きます。次のコンソール出力が表示されます-
次のステップは、一意のユーザー名でログインするユーザーを実装することです。サーバーにユーザー名を送信するだけで、サーバーが取得されたかどうかを通知します。次のコードをclient.jsファイルに追加します-
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
}
};
まず、ページ上の要素への参照をいくつか選択します。通話ページを非表示にします。次に、ログインボタンにイベントリスナーを追加します。ユーザーがクリックすると、ユーザー名がサーバーに送信されます。最後に、handleLoginコールバックを実装します。ログインに成功すると、通話ページが表示され、ピア接続のセットアップが開始されます。
ピア接続を開始するには、次のものが必要です。
- マイクからオーディオストリームを取得する
- RTCPeerConnectionオブジェクトを作成します
次のコードを「UIセレクターブロック」に追加します-
var localAudio = document.querySelector('#localAudio');
var remoteAudio = document.querySelector('#remoteAudio');
var yourConn;
var stream;
handleLogin関数を変更します-
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//getting local audio stream
navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) {
stream = myStream;
//displaying local audio stream on the page
localAudio.src = window.URL.createObjectURL(stream);
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration);
// setup stream listening
yourConn.addStream(stream);
//when a remote user adds stream to the peer connection, we display it
yourConn.onaddstream = function (e) {
remoteAudio.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
});
}
};
}, function (error) {
console.log(error);
});
}
};
これで、コードを実行すると、ページでログインしてローカルオーディオストリームをページに表示できるようになります。
これで、通話を開始する準備が整いました。まず、別のユーザーにオファーを送信します。ユーザーがオファーを受け取ると、回答を作成し、ICE候補の取引を開始します。次のコードをclient.jsファイルに追加します-
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
オファーを開始する[通話]ボタンにクリックハンドラーを追加します。次に、onmessageハンドラーが期待するいくつかのハンドラーを実装します。これらは、両方のユーザーが接続を確立するまで非同期で処理されます。
最後のステップは、ハングアップ機能の実装です。これにより、データの送信が停止し、他のユーザーに通話を終了するように指示します。次のコードを追加します-
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
remoteAudio.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};
ユーザーが[電話を切る]ボタンをクリックしたとき-
- 他のユーザーに「脱退」メッセージを送信します
- RTCPeerConnectionを閉じ、接続をローカルで破棄します
次に、コードを実行します。2つのブラウザタブを使用してサーバーにログインできるはずです。次に、タブに音声通話を発信して、電話を切ることができます。
以下はclient.jsファイル全体です-
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
var localAudio = document.querySelector('#localAudio');
var remoteAudio = document.querySelector('#remoteAudio');
var yourConn;
var stream;
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//getting local audio stream
navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) {
stream = myStream;
//displaying local audio stream on the page
localAudio.src = window.URL.createObjectURL(stream);
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration);
// setup stream listening
yourConn.addStream(stream);
//when a remote user adds stream to the peer connection, we display it
yourConn.onaddstream = function (e) {
remoteAudio.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
}, function (error) {
console.log(error);
});
}
};
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
remoteAudio.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};
この章では、別々のデバイス上の2人のユーザーがWebRTCを使用して互いにメッセージを送信できるようにするクライアントアプリケーションを構築します。私たちのアプリケーションは2ページになります。1つはログイン用で、もう1つは別のユーザーにメッセージを送信するためのものです。
2つのページがdivタグになります。ほとんどの入力は、単純なイベントハンドラーを介して行われます。
シグナリングサーバー
WebRTC接続を作成するには、クライアントはWebRTCピア接続を使用せずにメッセージを転送できる必要があります。ここで、HTML5 WebSocket(2つのエンドポイント間の双方向ソケット接続)を使用します。WebサーバーとWebブラウザーです。それでは、WebSocketライブラリの使用を開始しましょう。server.jsファイルを作成し、次のコードを挿入します-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
console.log("Got message from a user:", message);
});
connection.send("Hello from server");
});
最初の行には、すでにインストールされているWebSocketライブラリが必要です。次に、ポート9090にソケットサーバーを作成します。次に、接続イベントをリッスンします。このコードは、ユーザーがサーバーにWebSocket接続を行うときに実行されます。次に、ユーザーから送信されたメッセージを聞きます。最後に、接続されたユーザーに「サーバーからこんにちは」という応答を送信します。
シグナリングサーバーでは、接続ごとに文字列ベースのユーザー名を使用するため、メッセージの送信先がわかります。接続ハンドラーを少し変更してみましょう-
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
このようにして、JSONメッセージのみを受け入れます。次に、接続されているすべてのユーザーをどこかに保存する必要があります。単純なJavascriptオブジェクトを使用します。ファイルの先頭を変更します-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
クライアントからのすべてのメッセージにタイプフィールドを追加します。たとえば、ユーザーがログインしたい場合、ユーザーはログインタイプのメッセージを送信します。それを定義しましょう-
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
ユーザーがログインタイプのメッセージを送信した場合、次のようになります。
- 誰かがすでにこのユーザー名でログインしていないか確認してください。
- その場合は、正常にログインしていないことをユーザーに伝えます。
- このユーザー名を使用しているユーザーがいない場合は、接続オブジェクトのキーとしてユーザー名を追加します。
- コマンドが認識されない場合、エラーを送信します。
次のコードは、接続にメッセージを送信するためのヘルパー関数です。server.jsファイルに追加します-
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
ユーザーが切断したら、接続をクリーンアップする必要があります。closeイベントが発生したときにユーザーを削除できます。次のコードを接続ハンドラーに追加します-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
ログインに成功した後、ユーザーは別のユーザーに電話をかけたいと考えています。彼はそれを達成するために別のユーザーに申し出をする必要があります。オファーハンドラーを追加します-
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null){
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
break;
まず、呼び出しようとしているユーザーの接続を取得します。存在する場合は、オファーの詳細を送信します。また、otherNameを接続オブジェクトに追加します。これは、後で見つけやすくするために作成されています。
応答への応答には、オファーハンドラーで使用したのと同様のパターンがあります。私たちのサーバーは、別のユーザーへの回答としてすべてのメッセージを通過させるだけです。オファーハンドラの後に次のコードを追加します-
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
最後の部分は、ユーザー間でICE候補を処理することです。ユーザー間でメッセージを渡すだけでも同じ手法を使用します。主な違いは、候補メッセージがユーザーごとに任意の順序で複数回発生する可能性があることです。候補ハンドラーを追加します-
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
ユーザーが別のユーザーから切断できるようにするには、ハングアップ機能を実装する必要があります。また、すべてのユーザー参照を削除するようにサーバーに指示します。休暇ハンドラーを追加します-
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
これにより、他のユーザーにも休暇イベントが送信されるため、他のユーザーはそれに応じてピア接続を切断できます。また、ユーザーがシグナリングサーバーから接続を切断した場合にも対処する必要があります。クローズハンドラーを変更しましょう-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
以下は、シグナリングサーバーのコード全体です-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "candidate" state
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
connection.send("Hello world");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
クライアントアプリケーション
このアプリケーションをテストする1つの方法は、2つのブラウザタブを開いて、互いにメッセージを送信しようとすることです。
まず、ブートストラップライブラリをインストールする必要があります。Bootstrapは、Webアプリケーションを開発するためのフロントエンドフレームワークです。あなたはでもっと学ぶことができますhttp://getbootstrap.com/.たとえば、「textchat」というフォルダを作成します。これがルートアプリケーションフォルダになります。このフォルダー内にファイルpackage.json(npmの依存関係を管理するために必要)を作成し、以下を追加します-
{
"name": "webrtc-textochat",
"version": "0.1.0",
"description": "webrtc-textchat",
"author": "Author",
"license": "BSD-2-Clause"
}
次に、npm installbootstrapを実行します。これにより、ブートストラップライブラリがtextchat / node_modulesフォルダーにインストールされます。
次に、基本的なHTMLページを作成する必要があります。次のコードを使用して、ルートフォルダにindex.htmlファイルを作成します-
<html>
<head>
<title>WebRTC Text Demo</title>
<link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
</head>
<style>
body {
background: #eee;
padding: 5% 0;
}
</style>
<body>
<div id = "loginPage" class = "container text-center">
<div class = "row">
<div class = "col-md-4 col-md-offset-4">
<h2>WebRTC Text Demo. Please sign in</h2>
<label for = "usernameInput" class = "sr-only">Login</label>
<input type = "email" id = "usernameInput"
class = "form-control formgroup" placeholder = "Login"
required = "" autofocus = "">
<button id = "loginBtn" class = "btn btn-lg btn-primary btnblock">
Sign in</button>
</div>
</div>
</div>
<div id = "callPage" class = "call-page container">
<div class = "row">
<div class = "col-md-4 col-md-offset-4 text-center">
<div class = "panel panel-primary">
<div class = "panel-heading">Text chat</div>
<div id = "chatarea" class = "panel-body text-left"></div>
</div>
</div>
</div>
<div class = "row text-center form-group">
<div class = "col-md-12">
<input id = "callToUsernameInput" type = "text"
placeholder = "username to call" />
<button id = "callBtn" class = "btn-success btn">Call</button>
<button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button>
</div>
</div>
<div class = "row text-center">
<div class = "col-md-12">
<input id = "msgInput" type = "text" placeholder = "message" />
<button id = "sendMsgBtn" class = "btn-success btn">Send</button>
</div>
</div>
</div>
<script src = "client.js"></script>
</body>
</html>
このページはおなじみのはずです。ブートストラップcssファイルを追加しました。また、2つのページを定義しました。最後に、ユーザーから情報を取得するためのいくつかのテキストフィールドとボタンを作成しました。「チャット」ページに、すべてのメッセージが表示される「チャットエリア」IDのdivタグが表示されます。client.jsファイルへのリンクを追加したことに注意してください。
次に、シグナリングサーバーとの接続を確立する必要があります。次のコードを使用して、ルートフォルダーにclient.jsファイルを作成します-
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
次に、ノードサーバーを介してシグナリングサーバーを実行します。次に、ルートフォルダー内で静的コマンドを実行し、ブラウザー内でページを開きます。次のコンソール出力が表示されます-
次のステップは、一意のユーザー名でログインするユーザーを実装することです。サーバーにユーザー名を送信するだけで、サーバーが取得されたかどうかを通知します。次のコードをclient.jsファイルに追加します-
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
}
};
まず、ページ上の要素への参照をいくつか選択します。通話ページを非表示にします。次に、ログインボタンにイベントリスナーを追加します。ユーザーがクリックすると、ユーザー名がサーバーに送信されます。最後に、handleLoginコールバックを実装します。ログインが成功した場合は、通話ページを表示し、ピア接続を設定して、データチャネルを作成します。
データチャネルとのピア接続を開始するには、次のものが必要です。
- RTCPeerConnectionオブジェクトを作成します
- RTCPeerConnectionオブジェクト内にデータチャネルを作成します
次のコードを「UIセレクターブロック」に追加します-
var msgInput = document.querySelector('#msgInput');
var sendMsgBtn = document.querySelector('#sendMsgBtn');
var chatArea = document.querySelector('#chatarea');
var yourConn;
var dataChannel;
handleLogin関数を変更します-
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]});
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
//creating data channel
dataChannel = yourConn.createDataChannel("channel1", {reliable:true});
dataChannel.onerror = function (error) {
console.log("Ooops...error:", error);
};
//when we receive a message from the other peer, display it on the screen
dataChannel.onmessage = function (event) {
chatArea.innerHTML += connectedUser + ": " + event.data + "<br />";
};
dataChannel.onclose = function () {
console.log("data channel is closed");
};
}
};
ログインが成功した場合、アプリケーションはRTCPeerConnectionオブジェクトを作成し、見つかったすべてのicecandidateを他のピアに送信するonicecandidateハンドラーをセットアップします。また、dataChannelを作成します。RTCPeerConnectionオブジェクトを作成する場合、コンストラクターの2番目の引数はオプションであることに注意してください。ChromeまたはOperaを使用している場合は、[{RtpDataChannels:true}]が必須です。次のステップは、他のピアへのオファーを作成することです。ユーザーがオファーを受け取ると、回答を作成し、ICE候補の取引を開始します。次のコードをclient.jsファイルに追加します-
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
オファーを開始する[通話]ボタンにクリックハンドラーを追加します。次に、onmessageハンドラーが期待するいくつかのハンドラーを実装します。これらは、両方のユーザーが接続を確立するまで非同期で処理されます。
次のステップは、ハングアップ機能の実装です。これにより、データの送信が停止し、他のユーザーにデータチャネルを閉じるように指示します。次のコードを追加します-
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
yourConn.close();
yourConn.onicecandidate = null;
};
ユーザーが[電話を切る]ボタンをクリックしたとき-
- 他のユーザーに「脱退」メッセージを送信します。
- RTCPeerConnectionとデータチャネルを閉じます。
最後のステップは、別のピアにメッセージを送信することです。「メッセージの送信」ボタンに「クリック」ハンドラーを追加します-
//when user clicks the "send message" button
sendMsgBtn.addEventListener("click", function (event) {
var val = msgInput.value;
chatArea.innerHTML += name + ": " + val + "<br />";
//sending a message to a connected peer
dataChannel.send(val);
msgInput.value = "";
});
次に、コードを実行します。2つのブラウザタブを使用してサーバーにログインできるはずです。次に、他のユーザーへのピア接続を設定してメッセージを送信したり、[電話を切る]ボタンをクリックしてデータチャネルを閉じたりすることができます。
以下はclient.jsファイル全体です-
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
var msgInput = document.querySelector('#msgInput');
var sendMsgBtn = document.querySelector('#sendMsgBtn');
var chatArea = document.querySelector('#chatarea');
var yourConn;
var dataChannel;
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]});
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
//creating data channel
dataChannel = yourConn.createDataChannel("channel1", {reliable:true});
dataChannel.onerror = function (error) {
console.log("Ooops...error:", error);
};
//when we receive a message from the other peer, display it on the screen
dataChannel.onmessage = function (event) {
chatArea.innerHTML += connectedUser + ": " + event.data + "<br />";
};
dataChannel.onclose = function () {
console.log("data channel is closed");
};
}
};
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
yourConn.close();
yourConn.onicecandidate = null;
};
//when user clicks the "send message" button
sendMsgBtn.addEventListener("click", function (event) {
var val = msgInput.value;
chatArea.innerHTML += name + ": " + val + "<br />";
//sending a message to a connected peer
dataChannel.send(val);
msgInput.value = "";
});
この章では、「WebRTCシグナリング」の章で作成したシグナリングサーバーにセキュリティ機能を追加します。2つの機能強化があります-
- Redisデータベースを使用したユーザー認証
- 安全なソケット接続の有効化
まず、Redisをインストールする必要があります。
で最新の安定版リリースをダウンロードしてください http://redis.io/download(私の場合は3.05)
開梱
ダウンロードしたフォルダ内でsudomakeinstallを実行します
インストールが完了したら、make testを実行して、すべてが正しく機能しているかどうかを確認します。
Redisには2つの実行可能コマンドがあります-
redis-cli − Redisのコマンドラインインターフェイス(クライアント部分)
redis-server −Redisデータストア
Redisサーバーを実行するには、ターミナルコンソールでredis-serverと入力します。次のように表示されます-
次に、新しいターミナルウィンドウを開き、redis-cliを実行してクライアントアプリケーションを開きます。
基本的に、RedisはKey-Valueデータベースです。文字列値を使用してキーを作成するには、SETコマンドを使用する必要があります。キー値を読み取るには、GETコマンドを使用する必要があります。2人のユーザーとそれらのパスワードを追加しましょう。キーはユーザー名になり、これらのキーの値は対応するパスワードになります。
次に、シグナリングサーバーを変更して、ユーザー認証を追加する必要があります。server.jsファイルの先頭に次のコードを追加します-
//require the redis library in Node.js
var redis = require("redis");
//creating the redis client object
var redisClient = redis.createClient();
上記のコードでは、Node.js用のRedisライブラリが必要であり、サーバー用のredisクライアントを作成しています。
認証を追加するには、接続オブジェクトのメッセージハンドラを変更します-
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//check whether a user is authenticated
if(data.type != "login") {
//if user is not authenticated
if(!connection.isAuth) {
sendTo(connection, {
type: "error",
message: "You are not authenticated"
});
return;
}
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//get password for this username from redis database
redisClient.get(data.name, function(err, reply) {
//check if password matches with the one stored in redis
var loginSuccess = reply === data.password;
//if anyone is logged in with this username or incorrect password
then refuse
if(users[data.name] || !loginSuccess) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
connection.isAuth = true;
sendTo(connection, {
type: "login",
success: true
});
}
});
break;
}
});
}
//...
//*****other handlers*******
上記のコードでは、ユーザーがログインしようとすると、Redisからパスワードを取得し、保存されているパスワードと一致するかどうかを確認し、成功した場合はユーザー名をサーバーに保存します。また、接続にisAuthフラグを追加して、ユーザーが認証されているかどうかを確認します。このコードに注意してください-
//check whether a user is authenticated
if(data.type != "login") {
//if user is not authenticated
if(!connection.isAuth) {
sendTo(connection, {
type: "error",
message: "You are not authenticated"
});
return;
}
}
認証されていないユーザーがオファーを送信しようとしたり、接続を終了しようとした場合、エラーが返されます。
次のステップは、安全なソケット接続を有効にすることです。WebRTCアプリケーションに強くお勧めします。PKI(公開鍵インフラストラクチャ)は、CA(認証局)からのデジタル署名です。次に、ユーザーは、証明書の署名に使用される秘密鍵がCAの証明書の公開鍵と一致することを確認します。開発目的のため。自己署名セキュリティ証明書を使用します。
opensslを使用します。これは、SSL(Secure Sockets Layer)およびTLS(Transport Layer Security)プロトコルを実装するオープンソースツールです。多くの場合、Unixシステムにデフォルトでインストールされます。openssl version -aを実行して、インストールされているかどうかを確認します。
公開および秘密のセキュリティ証明書キーを生成するには、以下の手順に従う必要があります-
Generate a temporary server password key
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
Generate a server private key
openssl rsa -passin pass:12345 -in server.pass.key -out server.key
Generate a signing request. You will be asked additional questions about your company. Just hit the “Enter” button all the time.
openssl req -new -key server.key -out server.csr
Generate the certificate
openssl x509 -req -days 1095 -in server.csr -signkey server.key -out server.crt
これで、証明書(server.crt)と秘密鍵(server.key)の2つのファイルができました。それらをシグナリングサーバーのルートフォルダにコピーします。
セキュアソケット接続を有効にするには、シグナリングサーバーを変更します。
//require file system module
var fs = require('fs');
var httpServ = require('https');
//https://github.com/visionmedia/superagent/issues/205
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
//out secure server will bind to the port 9090
var cfg = {
port: 9090,
ssl_key: 'server.key',
ssl_cert: 'server.crt'
};
//in case of http request just send back "OK"
var processRequest = function(req, res) {
res.writeHead(200);
res.end("OK");
};
//create our server with SSL enabled
var app = httpServ.createServer({
key: fs.readFileSync(cfg.ssl_key),
cert: fs.readFileSync(cfg.ssl_cert)
}, processRequest).listen(cfg.port);
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({server: app});
//all connected to the server users
var users = {};
//require the redis library in Node.js
var redis = require("redis");
//creating the redis client object
var redisClient = redis.createClient();
//when a user connects to our sever
wss.on('connection', function(connection){
//...other code
上記のコードでは、fsライブラリが秘密鍵と証明書を読み取り、秘密鍵と証明書のバインディングポートとパスを使用してcfgオブジェクトを作成する必要があります。次に、ポート9090のWebSocketサーバーと一緒にキーを使用してHTTPSサーバーを作成します。
開催中 https://localhost:9090Operaで。次のように表示されます-
「とにかく続行」ボタンをクリックします。「OK」メッセージが表示されます。
安全なシグナリングサーバーをテストするために、「WebRTCテキストデモ」チュートリアルで作成したチャットアプリケーションを変更します。パスワードフィールドを追加するだけです。以下はindex.htmlファイル全体です-
<html>
<head>
<title>WebRTC Text Demo</title>
<link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
</head>
<style>
body {
background: #eee;
padding: 5% 0;
}
</style>
<body>
<div id = "loginPage" class = "container text-center">
<div class = "row">
<div class = "col-md-4 col-md-offset-4">
<h2>WebRTC Text Demo. Please sign in</h2>
<label for = "usernameInput" class = "sr-only">Login</label>
<input type = "email" id = "usernameInput"
class = "form-control formgroup" placeholder = "Login"
required = "" autofocus = "">
<input type = "text" id = "passwordInput"
class = "form-control form-group" placeholder = "Password"
required = "" autofocus = "">
<button id = "loginBtn" class = "btn btn-lg btn-primary btnblock"
>Sign in</button>
</div>
</div>
</div>
<div id = "callPage" class = "call-page container">
<div class = "row">
<div class = "col-md-4 col-md-offset-4 text-center">
<div class = "panel panel-primary">
<div class = "panel-heading">Text chat</div>
<div id = "chatarea" class = "panel-body text-left"></div>
</div>
</div>
</div>
<div class = "row text-center form-group">
<div class = "col-md-12">
<input id = "callToUsernameInput" type = "text"
placeholder = "username to call" />
<button id = "callBtn" class = "btn-success btn">Call</button>
<button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button>
</div>
</div>
<div class = "row text-center">
<div class = "col-md-12">
<input id = "msgInput" type = "text" placeholder = "message" />
<button id = "sendMsgBtn" class = "btn-success btn">Send</button>
</div>
</div>
</div>
<script src = "client.js"></script>
</body>
</html>
また、この行を介してclient.jsファイルでセキュアソケット接続を有効にする必要がありますvar conn = new WebSocket( 'wss:// localhost:9090'); 。wssプロトコルに注意してください。次に、ログインボタンハンドラーを変更して、ユーザー名と一緒にパスワードを送信する必要があります-
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
var pwd = passwordInput.value;
if (name.length > 0) {
send({
type: "login",
name: name,
password: pwd
});
}
});
以下はclient.jsファイル全体です-
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('wss://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var passwordInput = document.querySelector('#passwordInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
var msgInput = document.querySelector('#msgInput');
var sendMsgBtn = document.querySelector('#sendMsgBtn');
var chatArea = document.querySelector('#chatarea');
var yourConn;
var dataChannel;
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
var pwd = passwordInput.value;
if (name.length > 0) {
send({
type: "login",
name: name,
password: pwd
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...incorrect username or password");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]});
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
//creating data channel
dataChannel = yourConn.createDataChannel("channel1", {reliable:true});
dataChannel.onerror = function (error) {
console.log("Ooops...error:", error);
};
//when we receive a message from the other peer, display it on the screen
dataChannel.onmessage = function (event) {
chatArea.innerHTML += connectedUser + ": " + event.data + "<br />";
};
dataChannel.onclose = function () {
console.log("data channel is closed");
};
}
};
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
yourConn.close();
yourConn.onicecandidate = null;
};
//when user clicks the "send message" button
sendMsgBtn.addEventListener("click", function (event) {
var val = msgInput.value;
chatArea.innerHTML += name + ": " + val + "<br />";
//sending a message to a connected peer
dataChannel.send(val);
msgInput.value = "";
});
次に、ノードサーバーを介して安全なシグナリングサーバーを実行します。変更されたチャットデモフォルダー内でノード静的を実行します。開いたlocalhost:80802つのブラウザタブで。ログインしてみてください。ログインできるのは、「password1」の「user1」と「password2」の「user2」だけです。次に、RTCPeerConnectionを確立し(別のユーザーを呼び出します)、メッセージの送信を試みます。
以下は、安全なシグナリングサーバーのコード全体です-
//require file system module
var fs = require('fs');
var httpServ = require('https');
//https://github.com/visionmedia/superagent/issues/205
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
//out secure server will bind to the port 9090
var cfg = {
port: 9090,
ssl_key: 'server.key',
ssl_cert: 'server.crt'
};
//in case of http request just send back "OK"
var processRequest = function(req, res){
res.writeHead(200);
res.end("OK");
};
//create our server with SSL enabled
var app = httpServ.createServer({
key: fs.readFileSync(cfg.ssl_key),
cert: fs.readFileSync(cfg.ssl_cert)
}, processRequest).listen(cfg.port);
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({server: app});
//all connected to the server users
var users = {};
//require the redis library in Node.js
var redis = require("redis");
//creating the redis client object
var redisClient = redis.createClient();
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//check whether a user is authenticated
if(data.type != "login") {
//if user is not authenticated
if(!connection.isAuth) {
sendTo(connection, {
type: "error",
message: "You are not authenticated"
});
return;
}
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//get password for this username from redis database
redisClient.get(data.name, function(err, reply) {
//check if password matches with the one stored in redis
var loginSuccess = reply === data.password;
//if anyone is logged in with this username or incorrect password
then refuse
if(users[data.name] || !loginSuccess) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
connection.isAuth = true;
sendTo(connection, {
type: "login",
success: true
});
}
});
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "candidate" state
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
connection.send("Hello from server");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
概要
この章では、シグナリングサーバーにユーザー認証を追加しました。また、自己署名SSL証明書を作成し、WebRTCアプリケーションの範囲で使用する方法も学びました。