Webrtc-quick-guide

提供:Dev Guides
移動先:案内検索

WebRTC-概要

  • WebRTC(Web Real-Time Communication)が登場するにつれて、Webはリアルタイム通信にとってもはや見慣れたものではなくなりました。 2011年5月にリリースされましたが、まだ開発中であり、その基準は変化しています。 プロトコルのセットは、Webブラウザワーキンググループでのリアルタイム通信によって標準化されています。 IETF(Internet Engineering Task Force)のhttp://tools.ietf.org/wg/rtcweb/で、APIの新しいセットが標準化されています W3C(World Wide Web Consortium)*のhttp://www.w3.org/2011/04/webrtc/の_Web Real-Time Communications Working Groupe_によって。 WebRTCの登場により、最新のWebアプリケーションは何百万人もの人々に簡単にオーディオおよびビデオコンテンツをストリーミングできます。

基本スキーム

WebRTCを使用すると、他のWebブラウザーへのピアツーピア接続をすばやく簡単にセットアップできます。 このようなアプリケーションをゼロから構築するには、データ損失、接続の切断、NATトラバーサルなどの典型的な問題に対処する豊富なフレームワークとライブラリが必要です。 WebRTCを使用すると、これらすべてがそのままブラウザに組み込まれます。 この技術は、プラグインやサードパーティのソフトウェアを必要としません。 これはオープンソースであり、そのソースコードはhttp://www.webrtc.org/[[[1]]]から自由に入手できます。

WebRTC APIには、メディアキャプチャ、オーディオとビデオのエンコードとデコード、トランスポートレイヤー、およびセッション管理が含まれます。

基本スキーム

メディアキャプチャ

最初のステップは、ユーザーのデバイスのカメラとマイクにアクセスすることです。 利用可能なデバイスのタイプを検出し、これらのデバイスにアクセスするためのユーザー許可を取得して、ストリームを管理します。

オーディオとビデオのエンコードとデコード

インターネットを介してオーディオおよびビデオデータのストリームを送信するのは簡単な作業ではありません。 ここでエンコードとデコードが使用されます。 これは、ビデオフレームとオーディオウェーブを小さなチャンクに分割して圧縮するプロセスです。 このアルゴリズムは codec と呼ばれます。 膨大な量の異なるコーデックがあり、それらは異なるビジネス目標を持つ異なる企業によって維持されています。 H.264、iSAC、Opus、VP8など、WebRTC内には多くのコーデックもあります。 2つのブラウザーが一緒に接続するとき、2人のユーザーの間で最も最適なサポートされるコーデックを選択します。 幸いなことに、WebRTCはほとんどのエンコーディングをバックグラウンドで実行します。

輸送レイヤー

トランスポート層は、パケットの順序を管理し、パケット損失を処理し、他のユーザーに接続します。 この場合も、WebRTC APIを使用すると、接続に問題があるときに通知するイベントに簡単にアクセスできます。

セッション管理

セッション管理は、接続の管理、オープン、および整理を扱います。 これは一般に*シグナリング*と呼ばれます。 オーディオとビデオのストリームをユーザーに転送する場合、付随データを転送することも理にかなっています。 これは RTCDataChannel API によって行われます。

Google、Mozilla、Operaなどの企業のエンジニアは、このリアルタイムエクスペリエンスをWebにもたらすために素晴らしい仕事をしました。

ブラウザの互換性

WebRTC標準はWeb上で最も高速に進化しているものの1つであるため、すべてのブラウザーがすべての同じ機能を同時にサポートしているわけではありません。 ブラウザーがWebRTCをサポートしているかどうかを確認するには、http://caniuse.com/#feat=rtcpeerconnection [[[2]]]にアクセスしてください。すべてのチュートリアルで、すべての例については、Chrome。

WebRTCを試す

今すぐWebRTCの使用を始めましょう。 ブラウザでhttps://apprtc.appspot.com/のデモサイトに移動します

JOINをクリック

[参加]ボタンをクリックします。 ドロップダウン通知が表示されるはずです。

クリック許可

「許可」ボタンをクリックして、Webページへのビデオとオーディオのストリーミングを開始します。 自分のビデオストリームが表示されます。

URLを開く

次に、現在使用しているURLを新しいブラウザタブで開き、「JOIN」をクリックします。 最初のクライアントからのビデオストリームと2番目のクライアントからのビデオストリームの2つのビデオストリームが表示されます。

ビデオストリーム

ここで、WebRTCが強力なツールである理由を理解する必要があります。

ユースケース

リアルタイムWebは、テキストベースのチャット、画面とファイルの共有、ゲーム、ビデオチャットなど、まったく新しいアプリケーションへの扉を開きます。 通信以外に、WebRTCを次のような他の目的に使用できます-

  • リアルタイムマーケティング
  • リアルタイム広告
  • バックオフィスコミュニケーション(CRM、ERP、SCM、FFM)
  • 人事管理
  • ソーシャルネットワーキング
  • 出会い系サービス
  • オンライン医療相談
  • 金融業務
  • 監視
  • マルチプレイヤーゲーム
  • 生放送
  • eラーニング

概要

これで、WebRTCという用語を明確に理解できるはずです。 また、WebRTCを使用してWebRTCを作成したことがあるため、WebRTCを使用してどのような種類のアプリケーションを構築できるかについても把握しておく必要があります。 要約すると、WebRTCは非常に有用なテクノロジーです。

WebRTC-アーキテクチャ

全体的なWebRTCアーキテクチャは非常に複雑です。

WebRTCアーキテクチャ

ここでは、3つの異なる層を見つけることができます-

  • Web開発者向けのAPI -このレイヤーには、RTCPeerConnection、RTCDataChannel、MediaStreanオブジェクトなど、Web開発者が必要とするすべてのAPIが含まれています。
  • ブラウザメーカー向けのAPI
  • ブラウザーメーカーがフックできるオーバーライド可能なAPI。

トランスポートコンポーネントを使用すると、さまざまなタイプのネットワーク間で接続を確立できます。一方、音声およびビデオエンジンは、サウンドカードおよびカメラからネットワークにオーディオおよびビデオストリームを転送するフレームワークです。 Web開発者にとって最も重要な部分はWebRTC APIです。

クライアントサーバー側からWebRTCアーキテクチャを見ると、最も一般的に使用されているモデルの1つがSIP(Session Initiation Protocol)Trapezoidに触発されていることがわかります。

SIP台形

このモデルでは、両方のデバイスが異なるサーバーからWebアプリケーションを実行しています。 RTCPeerConnectionオブジェクトは、ピアツーピアで互いに接続できるようにストリームを構成します。 このシグナリングは、HTTPまたはWebSocketを介して行われます。

しかし、最も一般的に使用されるモデルは三角形です-

トライアングルモデル

このモデルでは、両方のデバイスが同じWebアプリケーションを使用します。 これにより、Web開発者はユーザー接続をより柔軟に管理できます。

WebRTC API

それはいくつかの主要なjavascriptオブジェクトで構成されています-

  • RTCPeerConnection
  • MediaStream
  • RTCDataChannel

RTCPeerConnectionオブジェクト

このオブジェクトは、WebRTC APIへの主要なエントリポイントです。 ピアへの接続、接続の初期化、およびメディアストリームの接続に役立ちます。 また、別のユーザーとのUDP接続を管理します。

RTCPeerConnectionオブジェクトの主なタスクは、ピア接続をセットアップして作成することです。 このオブジェクトはイベントが発生すると一連のイベントを発生させるため、接続のキーポイントを簡単にフックできます。 これらのイベントにより、接続の構成にアクセスできます-

RTCPeerConnectionオブジェクト

RTCPeerConnectionはシンプルなjavascriptオブジェクトであり、この方法で簡単に作成できます-

[code]
var conn = new RTCPeerConnection(conf);

conn.onaddstream = function(stream) {
  //use stream here
};

[/code]

RTCPeerConnectionオブジェクトは、_conf_パラメーターを受け入れます。これについては、これらのチュートリアルの後半で説明します。 _onaddstream_イベントは、リモートユーザーがピア接続にビデオまたはオーディオストリームを追加したときに発生します。

MediaStream API

最新のブラウザにより、開発者は_MediaStream_ APIとも呼ばれる_getUserMedia_ APIにアクセスできます。 機能の3つのキーポイントがあります-

  • 開発者は、ビデオおよびオーディオストリームを表す_stream_オブジェクトにアクセスできます。
  • ユーザーがデバイスに複数のカメラまたはマイクを持っている場合に、入力ユーザーデバイスの選択を管理します
  • ユーザーがストリームを取得するたびにセキュリティレベルを要求します。

このAPIをテストするには、簡単なHTMLページを作成しましょう。 単一の<video>要素が表示され、カメラの使用許可をユーザーに要求し、カメラのライブストリームをページに表示します。 _indexl_ファイルを作成して追加します-

[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]

_indexl_を開くと、顔を表示するビデオストリームが表示されます。

ただし、WebRTCはサーバー側でのみ機能するため、注意してください。 このページをブラウザで開くだけでは機能しません。 これらのファイルは、ApacheまたはNodeサーバーでホストする必要があります。

RTCDataChannelオブジェクト

ピア間でメディアストリームを送信するだけでなく、DataChannel APIを使用して追加データを送信することもできます。 このAPIはMediaStream APIと同じくらい簡単です。 主な仕事は、既存の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行のコードだけです。 それ以外はすべて、ブラウザの内部層で行われます。 _RTCPeerConnectionobject_が閉じられるまで、任意のピア接続でチャネルを作成できます。

概要

これで、WebRTCアーキテクチャをしっかりと把握できるはずです。 また、MediaStream、RTCPeerConnection、およびRTCDataChannel APIも取り上げました。 WebRTC APIは動くターゲットなので、常に最新の仕様に遅れずについていく必要があります。

WebRTC-環境

WebRTCアプリケーションの構築を開始する前に、コーディング環境を設定する必要があります。 まず第一に、HTMLとJavascriptを編集できるテキストエディターまたはIDEが必要です。 このチュートリアルを読んでいるときに、すでに希望するものを選択している可能性があります。 私に関しては、WebStorm IDEを使用しています。 試用版はhttps://www.jetbrains.com/webstorm/からダウンロードできます。 また、選択するOSとしてLinux Mintを使用しています。

一般的なWebRTCアプリケーションのその他の要件は、HTMLファイルとJavascriptファイルをホストするサーバーを持つことです。 ファイルが実際のサーバーによって提供されていない限り、ブラウザはカメラとマイクに接続できないため、ファイルをダブルクリックするだけではコードは機能しません。 これは明らかにセキュリティの問題が原因です。

さまざまなWebサーバーが大量にありますが、このチュートリアルでは、Node.jsをnode-staticで使用します-

  • [[3]] */usr/local/nodejsディレクトリに展開します。 */home/YOUR_USERNAME/.profileファイルを開き、次の行を最後に追加します-export PATH = $ PATH:/usr/local/nodejs/bin
  • コンピューターを再起動するか、source _/home/YOUR_USERNAME/.profile_を実行できます
  • これで、コマンドラインから_node_コマンドを使用できるようになります。 _npm_コマンドも使用できます。 NMPはNode.jsのパッケージマネージャーです。 詳細については、https://www.npmjs.com/をご覧ください。
  • ターミナルを開き、_sudo npm install -g node-static_を実行します。 これにより、Node.jsの静的Webサーバーがインストールされます。
  • 次に、HTMLファイルを含む任意のディレクトリに移動し、ディレクトリ内で_static_コマンドを実行してWebサーバーを起動します。
  • http://localhost:8080 に移動して、ファイルを表示できます。

nodejsをインストールする別の方法があります。 ターミナルウィンドウで_sudo apt-get install nodejs_を実行するだけです。

Node.jsのインストールをテストするには、ターミナルを開いて_node_コマンドを実行します。 いくつかのコマンドを入力して、動作を確認します-

ターミナルを開く

Node.jsは、端末に入力されたコマンドだけでなく、Javascriptファイルも実行します。 次の内容で_index.js_ファイルを作成します-

console.log(“Testing Node.js”);

次に、_node index_コマンドを実行します。 次が表示されます-

ノードターミナルの実行

シグナルサーバーを構築するとき、Node.js用のWebSocketsライブラリを使用します。 ターミナルで_npm install ws_を実行してインストールします。

シグナリングサーバーをテストするには、wscatユーティリティを使用します。 インストールするには、ターミナルウィンドウで_npm install -g wscat_を実行します。

S.No Protocols & Description
1

WebRTC Protocols

WebRTCアプリケーションは、トランスポートプロトコルとしてUDP(ユーザーデータグラムプロトコル)を使用します。 現在、ほとんどのWebアプリケーションはTCP(Transmission Control Protocol)を使用して構築されています

2

Session Description Protocol

SDPはWebRTCの重要な部分です。 メディア通信セッションを記述するためのプロトコルです。

3

Finding a Route

別のユーザーに接続するには、自分のネットワークと他のユーザーのネットワークの周りに明確なパスを見つける必要があります。 ただし、使用しているネットワークには、セキュリティの問題を回避するために、いくつかのレベルのアクセス制御がある可能性があります。

4

Stream Control Transmission Protocol

ピア接続を使用すると、ビデオとオーディオのデータを迅速に送信できます。 現在、SCTPプロトコルは、RTCDataChannelオブジェクトを使用するときに現在設定されているピア接続の上にblobデータを送信するために使用されます。

概要

この章では、UDP、TCP、STUN、TURN、ICE、SCTPなど、ピア接続を可能にするいくつかのテクノロジーについて説明しました。 これで、SDPの仕組みとその使用例について、表面レベルで理解できるはずです。

WebRTC-MediaStream API

MediaStream APIは、ローカルカメラとマイクからのメディアストリームに簡単にアクセスできるように設計されました。 _getUserMedia()_メソッドは、ローカル入力デバイスにアクセスする主要な方法です。

APIにはいくつかのキーポイントがあります-

  • リアルタイムメディアストリームは、ビデオまたはオーディオの形式の_stream_オブジェクトで表されます
  • Webアプリケーションがストリームのフェッチを開始する前にユーザーに確認するユーザーのアクセス許可を通じてセキュリティレベルを提供します。
  • 入力デバイスの選択は、MediaStream APIによって処理されます(たとえば、2つのカメラまたはマイクがデバイスに接続されている場合)

各MediaStreamオブジェクトには、複数のMediaStreamTrackオブジェクトが含まれます。 これらは、異なる入力デバイスからのビデオとオーディオを表します。

各MediaStreamTrackオブジェクトには、いくつかのチャネル(右および左のオーディオチャネル)が含まれる場合があります。 これらは、MediaStream APIによって定義される最小の部分です。

MediaStreamオブジェクトを出力するには2つの方法があります。 まず、出力をビデオまたはオーディオ要素にレンダリングできます。 第二に、RTCPeerConnectionオブジェクトに出力を送信し、RTCPeerConnectionオブジェクトはそれをリモートピアに送信します。

MediaStream APIを使用する

簡単なWebRTCアプリケーションを作成しましょう。 画面にビデオ要素を表示し、カメラを使用する許可をユーザーに求め、ブラウザにライブビデオストリームを表示します。 _indexl_ファイルを作成する-

<!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番目のパラメーターは、ユーザーのデバイスからのストリームを受け入れるコールバックです。 次に、パラメータで指定されたオブジェクトを表すURLを作成する_window.URL.createObjectURL_を使用して、ストリームを_video_要素に読み込みます。

ページを更新し、[許可]をクリックすると、画面に顔が表示されます。

Media Stream API

Webサーバーを使用してすべてのスクリプトを実行することを忘れないでください。 WebRTC環境チュートリアルで既にインストール済みです。

MediaStream API

プロパティ

  • * MediaStream.active(読み取り専用)*-MediaStreamがアクティブな場合はtrue、そうでない場合はfalseを返します。
  • * MediaStream.ended(読み取り専用、非推奨)*-終了した_event_がオブジェクトで発生した場合はtrueを返します。これは、ストリームが完全に読み取られたことを意味し、ストリームの終わりに達していない場合はfalseを返します。
  • * MediaStream.id(読み取り専用)*-オブジェクトの一意の識別子。
  • * MediaStream.label(読み取り専用、非推奨)*-ユーザーエージェントによって割り当てられた一意の識別子。

あなたは私のブラウザで上記のプロパティがどのように見えるかを見ることができます-

プロパティ

イベントハンドラ

  • MediaStream.onactive -MediaStreamオブジェクトがアクティブになったときに発生する_active_イベントのハンドラー。
  • MediaStream.onaddtrack -新しい_MediaStreamTrack_オブジェクトが追加されたときに発生する_addtrack_イベントのハンドラー。
  • * MediaStream.onended(非推奨)*-ストリーミングの終了時に発生する_ended_イベントのハンドラ。
  • MediaStream.oninactive -_MediaStream_オブジェクトが非アクティブになったときに起動される_inactive_イベントのハンドラー。
  • MediaStream.onremovetrack -_MediaStreamTrack_オブジェクトが削除されたときに発生する_removetrack_イベントのハンドラー。

方法

  • * MediaStream.addTrack()*-引数として指定された_MediaStreamTrack_オブジェクトをMediaStreamに追加します。 トラックがすでに追加されている場合、何も起こりません。
  • * MediaStream.clone()*-新しいIDを持つMediaStreamオブジェクトのクローンを返します。
  • * MediaStream.getAudioTracks()*-_MediaStream_オブジェクトからオーディオ_MediaStreamTrack_オブジェクトのリストを返します。
  • * MediaStream.getTrackById()*-IDでトラックを返します。 引数が空であるか、IDが見つからない場合、nullを返します。 複数のトラックのIDが同じ場合、最初のトラックが返されます。
  • * MediaStream.getTracks()*-_MediaStream_オブジェクトからすべての_MediaStreamTrack_オブジェクトのリストを返します。
  • * MediaStream.getVideoTracks()*-_MediaStream_オブジェクトからビデオ_MediaStreamTrack_オブジェクトのリストを返します。
  • * MediaStream.removeTrack()*-引数として指定された_MediaStreamTrack_オブジェクトをMediaStreamから削除します。 トラックがすでに削除されている場合、何も起こりません。

上記のAPIの変更をテストするには、次のように_indexl_を変更します-

<!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>

いくつかのボタンを追加して、いくつかのMediaStream APIを試してみました。 次に、新しく作成したボタンのイベントハンドラーを追加する必要があります。 このように_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()-audio_ボタンをクリックします。 これで、オーディオトラックが削除されます。 次に、ビデオトラックに対して同じ操作を行います。

getAudioTracksをクリック

getTracks()_ボタンをクリックすると、すべての_MediaStreamTracks(接続されているすべてのビデオおよびオーディオ入力)が表示されます。 次に、_getTrackById()_をクリックして、オーディオMe​​diaStreamTrackを取得します。

getTrackByIdをクリック

概要

この章では、MediaStream APIを使用して簡単なWebRTCアプリケーションを作成しました。 これで、WebRTCを機能させるさまざまなMediaStream APIの概要が明確になりました。

WebRTC-RTCPeerConnection API

RTCPeerConnection APIは、各ブラウザー間のピアツーピア接続の中核です。 RTCPeerConnectionオブジェクトを作成するには、単に書く

var pc = RTCPeerConnection(config);

ここで、_config_引数には少なくとも1つのキーiceServersが含まれます。 これは、ICE候補の検索中に使用される、STUNおよびTURNサーバーに関する情報を含むURLオブジェクトの配列です。 利用可能な公開STUNサーバーのリストは、https://code.google.com/p/natvpn/source/browse/trunk/stun_server_list [code.google.com]で見つけることができます。

呼び出し元であるか呼び出し先であるかに応じて、RTCPeerConnectionオブジェクトは、接続の両側でわずかに異なる方法で使用されます。

これは、ユーザーのフローの例です-

  • _onicecandidate_ハンドラーを登録します。 受信したICE候補は、他のピアに送信されます。
  • _onaddstream_ハンドラーを登録します。 リモートピアから受信したビデオストリームの表示を処理します。
  • _message_ハンドラーを登録します。 シグナリングサーバーには、他のピアから受信したメッセージのハンドラーも必要です。 メッセージに_RTCSessionDescription_オブジェクトが含まれる場合、_setRemoteDescription()_メソッドを使用して_RTCPeerConnection_オブジェクトに追加する必要があります。 メッセージに_RTCIceCandidate_オブジェクトが含まれる場合、_addIceCandidate()_メソッドを使用して_RTCPeerConnection_オブジェクトに追加する必要があります。
  • _getUserMedia()_を使用してローカルメディアストリームを設定し、_addStream()_メソッドを使用して_RTCPeerConnection_オブジェクトに追加します。
  • オファー/アンサーネゴシエーションプロセスを開始します。 これは、呼び出し元のフローが呼び出し先のフローと異なる唯一のステップです。 呼び出し元は、_createOffer()_メソッドを使用してネゴシエーションを開始し、_RTCSessionDescription_オブジェクトを受け取るコールバックを登録します。 次に、このコールバックは、_setLocalDescription()_を使用して、この_RTCSessionDescription_オブジェクトを_RTCPeerConnection_オブジェクトに追加する必要があります。 最後に、呼び出し元は、シグナリングサーバーを使用して、この_RTCSessionDescription_をリモートピアに送信する必要があります。 一方、呼び出し先は同じコールバックを登録しますが、_createAnswer()_メソッドに登録します。 呼び出し先からのオファーが受信された後にのみ、呼び出し先フローが開始されることに注意してください。

RTCPeerConnection API

プロパティ

  • * RTCPeerConnection.iceConnectionState(読み取り専用)*-接続の状態を説明するRTCIceConnectionState列挙を返します。 この値が変更されると、iceconnectionstatechangeイベントが発生します。 可能な値-
  • 新規-ICEエージェントはリモート候補者を待っているか、アドレスを収集しています
  • checking -ICEエージェントにはリモート候補がありますが、まだ接続が見つかりません
  • connected -ICEエージェントは使用可能な接続を検出しましたが、接続を改善するためにさらに多くのリモート候補をチェックしています。
  • 完了-ICEエージェントは使用可能な接続を検出し、リモート候補のテストを停止しました。
  • 失敗-ICEエージェントはすべてのリモート候補をチェックしましたが、少なくとも1つのコンポーネントに一致するものが見つかりませんでした。
  • 切断-少なくとも1つのコンポーネントが動作していません。
  • closed -ICEエージェントは閉じられています。
  • * RTCPeerConnection.iceGatheringState(読み取り専用)*-接続のICE収集状態を記述するRTCIceGatheringState列挙を返します-
  • new -オブジェクトは作成されたばかりです。
  • gathering -ICEエージェントは候補者を収集中です
  • 完了 ICEエージェントは収集を完了しました。
  • * RTCPeerConnection.localDescription(読み取り専用)*-ローカルセッションを説明するRTCSessionDescriptionを返します。 まだ設定されていない場合、nullになる可能性があります。
  • * RTCPeerConnection.peerIdentity(読み取り専用)*-RTCIdentityAssertionを返します。 idp(ドメイン名)とリモートピアのIDを表す名前で構成されます。
  • * RTCPeerConnection.remoteDescription(読み取り専用)*-リモートセッションを説明するRTCSessionDescriptionを返します。 まだ設定されていない場合、nullになる可能性があります。
  • * RTCPeerConnection.signalingState(読み取り専用)*-ローカル接続のシグナリング状態を記述するRTCSignalingState列挙を返します。 この状態は、SDPオファーを説明しています。 この値が変更されると、signalingstatechangeイベントが発生します。 可能な値-
  • 安定-初期状態。 進行中のSDPオファー/アンサー交換はありません。
  • have-local-offer -接続のローカル側がSDPオファーをローカルに適用しました。
  • have-remote-offer -接続のリモート側がローカルでSDPオファーを適用しました。
  • have-local-pranswer -リモートSDPオファーが適用され、SDP pranswerがローカルに適用されました。
  • have-remote-pranswer -ローカルSDPが適用され、SDP pranswerがリモートで適用されました。
  • closed -接続は閉じられています。

イベントハンドラ

以下は、RTCPeerConnectionの一般的に使用されるイベントハンドラーです。

S.No. Event Handlers & Description
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(IDプロバイダー)がエラーを検出したときに送信されます。

7

RTCPeerConnection.onidpvalidation

このハンドラは、idpvalidationerrorイベントが発生したときに呼び出されます。 このイベントは、IDアサーションの検証中にIdP(IDプロバイダー)がエラーを検出したときに送信されます。

8

RTCPeerConnection.onnegotiationneeded

このハンドラは、negotiationneededイベントが発生したときに呼び出されます。 このイベントは、将来のある時点でネゴシエーションが必要になることを知らせるために、ブラウザーによって送信されます。

9

RTCPeerConnection.onpeeridentity

このハンドラは、peeridentityイベントが発生したときに呼び出されます。 このイベントは、この接続でピアIDが設定および検証されたときに送信されます。

10

RTCPeerConnection.onremovestream

このハンドラーは、signalingstatechangeイベントが発生したときに呼び出されます。 このイベントは、signalingStateの値が変更されたときに送信されます。

11

RTCPeerConnection.onsignalingstatechange

このハンドラーは、removestreamイベントが発生したときに呼び出されます。 このイベントは、この接続からMediaStreamが削除されると送信されます。

方法

以下に、RTCPeerConnectionの一般的に使用されるメソッドを示します。

S.No. Methods & Description
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つは接続するユーザー名用です。 _indexl_ファイルを作成し、次のコードを追加します-

<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 API

WebRTCは、オーディオストリームとビデオストリームの転送に優れているだけでなく、任意のデータを持っている可能性があります。 これは、RTCDataChannelオブジェクトが作用する場所です。

RTCDataChannel API

プロパティ

  • * RTCDataChannel.label(読み取り専用)*-データチャネル名を含む文字列を返します。
  • * RTCDataChannel.ordered(読み取り専用)*-メッセージの配信順序が保証されている場合はtrueを、保証されていない場合はfalseを返します。
  • * RTCDataChannel.protocol(読み取り専用)*-このチャネルに使用されるサブプロトコル名を含む文字列を返します。
  • * RTCDataChannel.id(読み取り専用)*-RTCDataChannelオブジェクトの作成時に設定されたチャネルの一意のIDを返します。
  • * RTCDataChannel.readyState(読み取り専用)*-接続の状態を表すRTCDataChannelState列挙を返します。 可能な値-
  • connecting -接続がまだアクティブではないことを示します。 これは初期状態です。
  • open -接続が実行されていることを示します。
  • closing -接続がシャットダウンの処理中であることを示します。 キャッシュされたメッセージは送受信中ですが、新しく作成されたタスクは受け付けていません。
  • closed -接続を確立できなかったか、シャットダウンされたことを示します。
  • * RTCDataChannel.bufferedAmount(読み取り専用)*-送信のためにキューに入れられたバイト数を返します。 これは、RTCDataChannel.send()を介してまだ送信されていないデータの量です。
  • RTCDataChannel.bufferedAmountLowThreshold -RTCDataChannel.bufferedAmountがローとして使用されるバイト数を返します。 RTCDataChannel.bufferedAmountがこのしきい値を下回ると、bufferedamountlowイベントが発生します。
  • RTCDataChannel.binaryType -接続によって送信されたバイナリデータのタイプを返します。 「blob」または「arraybuffer」にすることができます。
  • * RTCDataChannel.maxPacketLifeType(読み取り専用)*-メッセージングが信頼性の低いモードになっているときのウィンドウのミリ秒単位の長さを示す符号なしshortを返します。
  • * RTCDataChannel.maxRetransmits(読み取り専用)*-配信されない場合にチャネルがデータを再送信する最大回数を示す符号なしショートを返します。
  • * RTCDataChannel.negotiated(読み取り専用)*-チャネルがユーザーエージェントまたはアプリケーションによってネゴシエートされたかどうかを示すブール値を返します。
  • * RTCDataChannel.reliable(読み取り専用)*-接続が信頼できないモードでメッセージを送信できることを示すブール値を返します。
  • * RTCDataChannel.stream(読み取り専用)*-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のいずれかです。

WebRTC-メッセージの送信

それでは、簡単な例を作成しましょう。 まず、「ノードサーバー」を介して「シグナリングサーバー」チュートリアルで作成したシグナリングサーバーを実行します。

ページには3つのテキスト入力があり、1つはログイン用、もう1つはユーザー名用、もう1つは他のピアに送信するメッセージ用です。 _indexl_ファイルを作成し、次のコードを追加します-

<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_オブジェクトを作成し、見つかったすべてのicecandidatesを他のピアに送信する_onicecandidate_ハンドラーをセットアップします。 また、dataChannelを作成するopenDataChannel()関数も実行します。 RTCPeerConnectionオブジェクトを作成する場合、ChromeまたはOperaを使用している場合、コンストラクターの2番目の引数(オプション:[\ {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でテストされていることに注意してください。

Operaの例

RTCDataChannelがWebRTC APIの非常に強力な部分であることがわかります。 ピアツーピアゲームやトレントベースのファイル共有など、このオブジェクトには他にも多くのユースケースがあります。

WebRTC-シグナリング

ほとんどのWebRTCアプリケーションは、ビデオとオーディオを介して通信できるだけではありません。 他の多くの機能が必要です。 この章では、基本的なシグナリングサーバーを構築します。

シグナリングとネゴシエーション

別のユーザーに接続するには、そのユーザーがWebのどこにいるかを知っている必要があります。 デバイスのIPアドレスにより、インターネット対応デバイスは相互に直接データを送信できます。 _RTCPeerConnection_オブジェクトがこれを担当します。 デバイスは、インターネット上でお互いを見つける方法を知るとすぐに、各デバイスがサポートするプロトコルとコーデックに関するデータの交換を開始します。

別のユーザーと通信するには、連絡先情報を交換するだけで、残りはWebRTCによって行われます。 他のユーザーに接続するプロセスは、シグナリングとネゴシエーションとも呼ばれます。 それはいくつかのステップで構成されています-

  • ピア接続の潜在的な候補のリストを作成します。
  • ユーザーまたはアプリケーションは、接続するユーザーを選択します。
  • シグナリングレイヤーは、他のユーザーに誰かが接続したいことを通知します。 彼は受け入れるか拒否することができます。
  • 最初のユーザーには、オファーの受け入れが通知されます。
  • 最初のユーザーが別のユーザーで_RTCPeerConnection_を開始します。
  • 両方のユーザーは、信号サーバーを介してソフトウェアとハ​​ードウェアの情報を交換します。
  • 両方のユーザーが位置情報を交換します。
  • 接続は成功または失敗します。

WebRTC仕様には、情報交換に関する標準は含まれていません。 したがって、上記はシグナリングがどのように発生するかを示す例にすぎないことに注意してください。 任意のプロトコルまたはテクノロジーを使用できます。

サーバーの構築

構築するサーバーは、同じコンピューター上にいない2人のユーザーを接続できます。 独自の信号メカニズムを作成します。 私たちのシグナルサーバーは、あるユーザーが別のユーザーを呼び出すことを許可します。 ユーザーが別のユーザーを呼び出すと、サーバーはオファー、アンサー、ICE候補をそれらの間で渡し、WebRTC接続をセットアップします。

サーバーの構築

上の図は、シグナリングサーバーを使用する場合のユーザー間のメッセージングフローです。 まず、各ユーザーはサーバーに登録します。 私たちの場合、これは単純な文字列のユーザー名になります。 ユーザーが登録すると、お互いに電話をかけることができます。 ユーザー1は、電話をかけたいユーザーIDを提示します。 他のユーザーが応答する必要があります。 最後に、ICE候補は、ユーザーが接続できるまでユーザー間で送信されます。

WebRTC接続を作成するには、クライアントはWebRTCピア接続を使用せずにメッセージを転送できる必要があります。 ここで、HTML5 WebSockets – 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にソケットサーバーを作成します。 次に、_connection_イベントをリッスンします。 このコードは、ユーザーがサーバーにWebSocket接続するときに実行されます。 次に、ユーザーから送信されたメッセージを聞きます。 最後に、接続したユーザーに「サーバーからこんにちは」という応答を送信します。

ここで_node server_を実行すると、サーバーはソケット接続のリッスンを開始します。

サーバーをテストするために、すでにインストールされている_wscat_ユーティリティを使用します。 このツールは、WebSocketサーバーに直接接続してコマンドをテストするのに役立ちます。 あるターミナルウィンドウでサーバーを実行し、別のウィンドウを開いて_wscat -c ws://localhost:9090_コマンドを実行します。 あなたは、クライアント側で次のように表示されるはずです-

wscatユーティリティを使用

サーバーは、接続ユーザーも記録する必要があります-

接続ユーザーのログ

ユーザー登録

シグナリングサーバーでは、接続ごとに文字列ベースのユーザー名を使用するため、メッセージの送信先がわかります。 _connection_ハンドラーを少し変更しましょう-

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 = {};

クライアントからのメッセージごとに_type_フィールドを追加します。 たとえば、ユーザーがログインする場合は、_login_タイプのメッセージを送信します。 それを定義しましょう-

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;
   }

});

ユーザーが_login_タイプでメッセージを送信する場合、私たちは-

  • 誰かがこのユーザー名で既にログインしているかどうかを確認してください
  • その場合、ユーザーにログインに失敗したことを伝えます
  • このユーザー名を使用しているユーザーがいない場合は、ユーザー名をキーとして接続オブジェクトに追加します。
  • コマンドが認識されない場合、エラーを送信します。

次のコードは、接続にメッセージを送信するためのヘルパー関数です。 _server.js_ファイルに追加します-

function sendTo(connection, message) {
   connection.send(JSON.stringify(message));
}

上記の関数は、すべてのメッセージがJSON形式で送信されるようにします。

ユーザーが切断したら、その接続をクリーンアップする必要があります。 _close_イベントが発生したときにユーザーを削除できます。 _connection_ハンドラに次のコードを追加します-

connection.on("close", function() {
   if(connection.name) {
      delete users[connection.name];
   }
});

さて、ログインコマンドでサーバーをテストしましょう。 すべてのメッセージはJSON形式でエンコードする必要があることに注意してください。 サーバーを実行し、ログインしてみてください。 このようなものが表示されるはずです-

ログインコマンドでテスト

電話をかける

ログインに成功した後、ユーザーは別の電話をかけたいと考えています。 彼はそれを達成するために別のユーザーに_offer_する必要があります。 _offer_ハンドラを追加します-

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;

まず、呼び出しようとしているユーザーの_connection_を取得します。 存在する場合は、_offer_詳細を送信します。 また、_otherName_を_connection_オブジェクトに追加します。 これは、後で簡単に見つけられるようにするためです。

返事

応答への応答には、_offer_ハンドラーで使用したのと同様のパターンがあります。 サーバーは、すべてのメッセージを別のユーザーに_answer_として渡すだけです。 _offer_ハンドラーの後に次のコードを追加します-

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;

これが_offer_ハンドラーにどのように似ているかを見ることができます。 このコードは、_RTCPeerConnection_オブジェクトの_createOffer_および_createAnswer_関数に従っていることに注意してください。

これで、オファー/アンサーのメカニズムをテストできます。 2つのクライアントを同時に接続し、オファーとアンサーを作成してみてください。 次が表示されるはずです-

2つのクライアントを接続

この例では、 offeranswer は単純な文字列ですが、実際のアプリケーションではSDPデータで埋められます。

ICE候補者

最後の部分は、ユーザー間のICE候補の処理です。 ユーザー間でメッセージを渡すだけで同じ手法を使用します。 主な違いは、候補メッセージがユーザーごとに複数回発生する可能性があることです。 _candidate_ハンドラーを追加します-

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;

_offer_および_answer_ハンドラーと同様に機能するはずです。

接続を離れる

ユーザーが別のユーザーから切断できるようにするには、ハングアップ機能を実装する必要があります。 また、サーバーにすべてのユーザー参照を削除するように指示します。 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;

これにより、他のユーザーに_leave_イベントも送信されるため、ピア接続を適宜切断できます。 また、ユーザーが信号サーバーから接続をドロップした場合も処理する必要があります。 _close_ハンドラを変更しましょう-

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"
            });
         }
      }
   }
});

接続が終了すると、ユーザーは切断されます。 off _、 answer_、または_candidate_状態のまま、ユーザーがブラウザウィンドウを閉じると、_close_イベントが発生します。

完全なシグナリングサーバー

ここに私たちのシグナルサーバーのコード全体があります-

//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接続を確立する際に順番を間違えると問題が発生する可能性があることに注意してください。

概要

この章では、シンプルでわかりやすいシグナリングサーバーを構築しました。 シグナリングプロセス、ユーザー登録、オファー/アンサーメカニズムについて説明しました。 また、ユーザー間の候補者の送信も実装しました。

WebRTC-ブラウザのサポート

Webは非常に速く動いており、常に改善されています。 新しい標準は毎日作成されます。 ブラウザーを使用すると、ユーザーが気付かないうちに更新プログラムをインストールできるため、WebとWebRTCの世界で何が起こっているのかを把握する必要があります。 これが今日までの概要です。

ブラウザのサポート

すべてのブラウザに、同じWebRTC機能がすべて同時に搭載されているわけではありません。 さまざまなブラウザが先を行く可能性があります。これにより、一部のWebRTC機能が1つのブラウザで機能し、別のブラウザでは機能しなくなります。 ブラウザでのWebRTCの現在のサポートを次の図に示します。

ブラウザサポート

最新のWebRTCサポート状況は、http://caniuse.com/#feat=rtcpeerconnection [[[4]]]で確認できます。

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)を実装することを公式に確認しています。 WebRTC 1.0をサポートする予定はありません。 彼らはORTCにWebRTC 1.1というラベルを付けましたが、これは単なるコミュニティの拡張機能であり、公式の標準ではありません。 最近、彼らは最新のMicrosoft EdgeバージョンにORTCサポートを追加しました。 詳細については、https://blogs.windows.com/msedgedev/2015/09/18/ortc-api-is-now-available-in-microsoft-edge/[[[5]]]

概要

WebRTCは、単一のAPIではなく、APIとプロトコルのコレクションであることに注意してください。 これらのそれぞれのサポートは、異なるレベルの異なるブラウザーとオペレーティングシステムで開発されています。 最新レベルのサポートを確認する優れた方法は、http://canisue.com/[[[6]]]を使用することです。複数のブラウザーにわたる最新のAPIの採用を追跡します。 また、Mozilla、Google、Operaでサポートされているhttp://www.webrtc.org/[[[7]]]で、ブラウザサポートの最新情報とWebRTCデモを見つけることができます。

WebRTC-モバイルサポート

モバイルの世界では、WebRTCサポートはデスクトップと同じレベルではありません。 モバイルデバイスには独自の方法があるため、WebRTCはモバイルプラットフォーム上でも異なるものです。

モバイルサポート

デスクトップ用のWebRTCアプリケーションを開発するときは、Chrome、Firefox、またはOperaの使用を検討します。 それらはすべて、そのままWebRTCをサポートします。 一般に、デスクトップのハードウェアに煩わされるのではなく、ブラウザが必要なだけです。

モバイルの世界では、今日のWebRTCには3つのモードがあります-

  • ネイティブアプリケーション
  • ブラウザアプリケーション
  • ネイティブブラウザ

アンドロイド

2013年、Android向けのFirefox Webブラウザには、すぐにWebRTCサポートが提供されました。 Firefoxモバイルブラウザを使用して、Androidデバイスでビデオ通話を行うことができるようになりました。

3つの主要なWebRTCコンポーネントがあります-

  • PeerConnection -ブラウザ間の呼び出しを有効にします
  • getUserMedia -カメラとマイクへのアクセスを提供します
  • DataChannels -ピアツーピアのデータ転送を提供します

Android向けGoogle ChromeはWebRTCサポートも提供します。 既にお気づきのように、最も興味深い機能は通常Chromeに最初に表示されます。

過去1年で、OperaモバイルブラウザがWebRTCサポート付きで登場しました。 AndroidにはChrome、Firefox、Operaがあります。 他のブラウザはWebRTCをサポートしていません。

iOS

残念ながら、WebRTCは現在iOSではサポートされていません。 Firefox、Opera、またはChromeを使用する場合、WebRTCはMacで正常に動作しますが、iOSではサポートされていません。

最近では、WebRTCアプリケーションはそのままではAppleモバイルデバイスで動作しません。 しかし、ブラウザがあります-クッパ。 これは、エリクソンが開発したWebブラウザーであり、そのままWebRTCをサポートします。 ホームページはhttp://www.openwebrtc.org/bowser/[[[8]]]で確認できます。

今日、これはiOSでWebRTCアプリケーションをサポートする唯一の友好的な方法です。 別の方法は、ネイティブアプリケーションを自分で開発することです。

Windows Phone

Microsoftは、モバイルプラットフォームでWebRTCをサポートしていません。 しかし、IEの将来のバージョンでORTC(Object Realtime Communications)を実装することを公式に確認しています。 WebRTC 1.0をサポートする予定はありません。 彼らはORTCにWebRTC 1.1というラベルを付けましたが、これは単なるコミュニティの拡張機能であり、公式の標準ではありません。

そのため、今日のWindow PhoneユーザーはWebRTCアプリケーションを使用できず、この状況に勝つ方法はありません。

ブラックベリー

WebRTCアプリケーションは、Blackberryでもサポートされていません。

WebRTCネイティブブラウザーの使用

ユーザーがWebRTCを利用するための最も便利で快適なケースは、デバイスのネイティブブラウザーを使用することです。 この場合、デバイスは追加の構成を使用する準備ができています。

現在、バージョン4以降のAndroidデバイスのみがこの機能を提供しています。 AppleはまだWebRTCサポートのアクティビティを表示していません。 そのため、SafariユーザーはWebRTCアプリケーションを使用できません。 また、MicrosoftはWindows Phone 8でも導入していません。

ブラウザアプリケーション経由でWebRTCを使用する

これは、WebRTC機能を提供するために、サードパーティアプリケーション(非ネイティブWebブラウザー)を使用することを意味します。 今のところ、このようなサードパーティのアプリケーションが2つあります。 Bowserは、WebRTC機能をiOSデバイスとOperaに提供する唯一の方法であり、Androidプラットフォームの優れた代替手段です。 残りの利用可能なモバイルブラウザーはWebRTCをサポートしていません。

ネイティブモバイルアプリケーション

ご覧のとおり、WebRTCはまだモバイルの世界で大きなサポートを提供していません。 したがって、可能な解決策の1つは、WebRTC APIを利用するネイティブアプリケーションを開発することです。 しかし、メインのWebRTC機能はクロスプラットフォームソリューションであるため、これは良い選択ではありません。 とにかく、場合によっては、ネイティブアプリケーションがHTML5ブラウザーでサポートされていないデバイス固有の機能を利用できるため、これが唯一の方法です。

モバイルおよびデスクトップデバイスのビデオストリームの制約

getUserMedia APIの最初のパラメーターは、ブラウザーにストリームの処理方法を指示するキーと値のオブジェクトを想定しています。 https://tools.ietf.org/html/draft-alvestrand-constraints-resolutionですべての制約を確認できます。 -03。ビデオのアスペクト比、frameRate、およびその他のオプションのパラメーターを設定できます。

モバイルデバイスの画面スペースは限られており、リソースも限られているため、モバイルデバイスのサポートは最大の苦痛の1つです。 消費電力と帯域幅を節約するために、モバイルデバイスで解像度480x320またはそれより小さいビデオストリームのみをキャプチャしたい場合があります。 ブラウザでユーザーエージェント文字列を使用すると、ユーザーがモバイルデバイスを使用しているかどうかをテストできます。 例を見てみましょう。 _indexl_ファイルを作成します-

<!DOCTYPE html>
<html lang = "en">

   <head>
      <meta charset = "utf-8"/>
   </head>

   <body>
      <video autoplay></video>
      <script src = "client.js"></script>
   </body>

</html>

その後、次の_client.js_ファイルを作成します-

//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");
}

_static_コマンドを使用してWebサーバーを実行し、ページを開きます。 800x600になっているはずです。 次に、クロムツールを使用してこのページをモバイルビューポートで開き、解像度を確認します。 480x320である必要があります。

Webサーバーの実行

制約は、WebRTCアプリケーションのパフォーマンスを向上させる最も簡単な方法です。

概要

この章では、モバイルデバイス用のWebRTCアプリケーションを開発するときに発生する可能性のある問題について学びました。 モバイルプラットフォームでWebRTC APIをサポートする場合のさまざまな制限を発見しました。 また、デスクトップブラウザとモバイルブラウザにさまざまな制約を設定するデモアプリケーションを起動しました。

WebRTC-ビデオデモ

この章では、別々のデバイス上の2人のユーザーがWebRTCを使用して通信できるクライアントアプリケーションを構築します。 アプリケーションには2つのページがあります。 1つはログイン用で、もう1つは別のユーザーを呼び出すためのものです。

ログインページ

2つのページは_div_タグになります。 ほとんどの入力は、単純なイベントハンドラーを介して行われます。

呼び出しページ

シグナリングサーバー

WebRTC接続を作成するには、クライアントはWebRTCピア接続を使用せずにメッセージを転送できる必要があります。 ここで、HTML5 WebSockets-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にソケットサーバーを作成します。 次に、_connection_イベントをリッスンします。 このコードは、ユーザーがサーバーにWebSocket接続するときに実行されます。 次に、ユーザーから送信されたメッセージを聞きます。 最後に、接続したユーザーに「サーバーからこんにちは」という応答を送信します。

シグナリングサーバーでは、接続ごとに文字列ベースのユーザー名を使用するため、メッセージの送信先がわかります。 接続_handler_を少し変更しましょう-

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 = {};

クライアントからのメッセージごとに_type_フィールドを追加します。 たとえば、ユーザーがログインする場合は、_login_タイプのメッセージを送信します。 それを定義しましょう-

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;
   }
});

ユーザーが_login_タイプでメッセージを送信する場合、私たちは-

  • 誰かがこのユーザー名で既にログインしているかどうかを確認してください
  • その場合、ユーザーにログインに失敗したことを伝えます
  • このユーザー名を使用しているユーザーがいない場合は、ユーザー名をキーとして接続オブジェクトに追加します。
  • コマンドが認識されない場合、エラーを送信します。

次のコードは、接続にメッセージを送信するためのヘルパー関数です。 _server.js_ファイルに追加します-

function sendTo(connection, message) {
   connection.send(JSON.stringify(message));
}

ユーザーが切断したら、その接続をクリーンアップする必要があります。 _close_イベントが発生したときにユーザーを削除できます。 _connection_ハンドラに次のコードを追加します-

connection.on("close", function() {
   if(connection.name) {
      delete users[connection.name];
   }
});

ログインに成功した後、ユーザーは別の電話をかけたいと考えています。 彼はそれを達成するために別のユーザーに_offer_する必要があります。 _offer_ハンドラを追加します-

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;

まず、呼び出しようとしているユーザーの_connection_を取得します。 存在する場合は、_offer_詳細を送信します。 また、_otherName_を_connection_オブジェクトに追加します。 これは、後で簡単に見つけられるようにするためです。

応答への応答には、_offer_ハンドラーで使用したのと同様のパターンがあります。 サーバーは、すべてのメッセージを別のユーザーに_answer_として渡すだけです。 _offer_ハンドラの後に次のコードを追加します-

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候補の処理です。 ユーザー間でメッセージを渡すだけで同じ手法を使用します。 主な違いは、候補メッセージがユーザーごとに複数回発生する可能性があることです。 _candidate_ハンドラーを追加します-

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;

これにより、他のユーザーに_leave_イベントも送信されるため、ピア接続を適宜切断できます。 また、ユーザーが信号サーバーから接続をドロップした場合も処理する必要があります。 _close_ハンドラを変更しましょう-

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/[[[9]]]をご覧ください。たとえば、「videochat」という名前のフォルダーを作成します。 これがルートアプリケーションフォルダになります。 このフォルダ内にファイル_package.json_を作成し(npmの依存関係を管理するために必要です)、次を追加します-

{
   "name": "webrtc-videochat",
   "version": "0.1.0",
   "description": "webrtc-videochat",
   "author": "Author",
   "license": "BSD-2-Clause"
}

次に、_npm install bootstrap_を実行します。 これにより、_videochat/node_modules_フォルダーにブートストラップライブラリがインストールされます。

次に、基本的なHTMLページを作成する必要があります。 次のコードでルートフォルダに_indexl_ファイルを作成します-

<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>

このページはおなじみのはずです。 bootstrap 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));
};

ここで、_node server_を介してシグナルサーバーを実行します。 次に、ルートフォルダー内で_static_コマンドを実行し、ブラウザー内でページを開きます。 次のコンソール出力が表示されるはずです-

クライアントアプリケーション

次の手順では、一意のユーザー名でユーザーログインを実装します。 ユーザー名をサーバーに送信するだけで、ユーザー名が取得されたかどうかがわかります。 _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);
      });
   }
};

これで、コードを実行すると、ページでログインしてローカルビデオストリームをページに表示できるようになります。

ローカルビデオストリーム

これで、通話を開始する準備が整いました。 まず、別のユーザーに_offer_を送信します。 ユーザーがオファーを取得すると、_answer_を作成し、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));
};

Callボタンに_click_ハンドラーを追加して、オファーを開始します。 次に、_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;
};

ユーザーがハングアップボタンをクリックすると-

  • 他のユーザーに「leave」メッセージを送信します
  • 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アプリケーションを安定させるための努力が払われました。

WebRTC-音声デモ

この章では、別々のデバイス上の2人のユーザーがWebRTCオーディオストリームを使用して通信できるようにするクライアントアプリケーションを構築します。 アプリケーションには2つのページがあります。 1つはログイン用で、もう1つは別のユーザーに音声通話を行うためのものです。

ログインおよび音声通話ページ。

2つのページは_div_タグになります。 ほとんどの入力は、単純なイベントハンドラーを介して行われます。

シグナリングサーバー

WebRTC接続を作成するには、クライアントはWebRTCピア接続を使用せずにメッセージを転送できる必要があります。 ここで、HTML5 WebSockets-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にソケットサーバーを作成します。 次に、_connection_イベントをリッスンします。 このコードは、ユーザーがサーバーにWebSocket接続するときに実行されます。 次に、ユーザーから送信されたメッセージを聞きます。 最後に、接続したユーザーに「サーバーからこんにちは」という応答を送信します。

シグナリングサーバーでは、接続ごとに文字列ベースのユーザー名を使用するため、メッセージの送信先がわかります。 _connection_ハンドラーを少し変更しましょう-

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 = {};

クライアントからのメッセージごとに_type_フィールドを追加します。 たとえば、ユーザーがログインする場合は、_login_タイプのメッセージを送信します。 それを定義しましょう-

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;
   }
});

ユーザーが_login_タイプでメッセージを送信する場合、私たちは-

  • 誰かがこのユーザー名で既にログインしているかどうかを確認してください。
  • その場合は、ユーザーにログインに失敗したことを伝えます。
  • このユーザー名を使用しているユーザーがいない場合は、ユーザー名をキーとして接続オブジェクトに追加します。
  • コマンドが認識されない場合、エラーを送信します。

次のコードは、接続にメッセージを送信するためのヘルパー関数です。 _server.js_ファイルに追加します-

function sendTo(connection, message) {
   connection.send(JSON.stringify(message));
}

ユーザーが切断したら、その接続をクリーンアップする必要があります。 _close_イベントが発生したときにユーザーを削除できます。 _connection_ハンドラーに次のコードを追加します

connection.on("close", function() {
   if(connection.name) {
      delete users[connection.name];
   }
});

ログインに成功した後、ユーザーは別の電話をかけたいと考えています。 彼はそれを達成するために別のユーザーに_offer_する必要があります。 _offer_ハンドラを追加します-

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;

まず、呼び出しようとしているユーザーの_connection_を取得します。 存在する場合は、_offer_詳細を送信します。 また、_otherName_を_connection_オブジェクトに追加します。 これは、後で簡単に見つけられるようにするためです。

応答への応答には、_offer_ハンドラーで使用したのと同様のパターンがあります。 サーバーは、すべてのメッセージを別のユーザーに_answer_として渡すだけです。 _offer_ハンドラの後に次のコードを追加します-

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候補の処理です。 ユーザー間でメッセージを渡すだけで同じ手法を使用します。 主な違いは、候補メッセージがユーザーごとに複数回発生する可能性があることです。 _candidate_ハンドラーを追加します-

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;

これにより、他のユーザーに_leave_イベントも送信されるため、ピア接続を適宜切断できます。 また、ユーザーが信号サーバーから接続をドロップした場合も処理する必要があります。 _close_ハンドラを変更しましょう-

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/[[[10]]]をご覧ください。たとえば、「audiochat」という名前のフォルダーを作成します。 これがルートアプリケーションフォルダになります。 このフォルダ内にファイル_package.json_を作成し(npmの依存関係を管理するために必要です)、次を追加します-

{
   "name": "webrtc-audiochat",
   "version": "0.1.0",
   "description": "webrtc-audiochat",
   "author": "Author",
   "license": "BSD-2-Clause"
}

次に、_npm install bootstrap_を実行します。 これにより、ブートストラップライブラリが_audiochat/node_modules_フォルダーにインストールされます。

次に、基本的なHTMLページを作成する必要があります。 次のコードでルートフォルダに_indexl_ファイルを作成します-

<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>

このページはおなじみのはずです。 bootstrap 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));
};

ここで、_node server_を介してシグナルサーバーを実行します。 次に、ルートフォルダー内で_static_コマンドを実行し、ブラウザー内でページを開きます。 次のコンソール出力が表示されるはずです-

信号サーバーの実行

次の手順では、一意のユーザー名でユーザーログインを実装します。 ユーザー名をサーバーに送信するだけで、ユーザー名が取得されたかどうかがわかります。 _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);
      });

   }
};

これで、コードを実行すると、ページでログインしてページにローカルオーディオストリームを表示できるようになります。

ログインを許可

これで、通話を開始する準備が整いました。 まず、別のユーザーに_offer_を送信します。 ユーザーがオファーを取得すると、_answer_を作成し、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));
};

Callボタンに_click_ハンドラーを追加して、オファーを開始します。 次に、_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;
};

ユーザーがハングアップボタンをクリックすると-

  • 他のユーザーに「leave」メッセージを送信します
  • RTCPeerConnectionを閉じ、接続をローカルで破棄します

次に、コードを実行します。 2つのブラウザータブを使用してサーバーにログインできるはずです。 その後、タブに音声通話を発信し、通話を終了できます。

Login Page Call and Hang up page

以下は_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;
};

WebRTC-テキストデモ

この章では、別々のデバイス上の2人のユーザーがWebRTCを使用して互いにメッセージを送信できるようにするクライアントアプリケーションを構築します。 アプリケーションには2つのページがあります。 1つはログイン用で、もう1つは別のユーザーにメッセージを送信するためのものです。

ログインしてメッセージページを送信

2つのページは_div_タグになります。 ほとんどの入力は、単純なイベントハンドラーを介して行われます。

シグナリングサーバー

WebRTC接続を作成するには、クライアントはWebRTCピア接続を使用せずにメッセージを転送できる必要があります。 ここで、HTML5 WebSockets-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にソケットサーバーを作成します。 次に、_connection_イベントをリッスンします。 このコードは、ユーザーがサーバーにWebSocket接続するときに実行されます。 次に、ユーザーから送信されたメッセージを聞きます。 最後に、接続したユーザーに「サーバーからこんにちは」という応答を送信します。

シグナリングサーバーでは、接続ごとに文字列ベースのユーザー名を使用するため、メッセージの送信先がわかります。 _connection_ハンドラーを少し変更しましょう-

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 = {};

クライアントからのメッセージごとに_type_フィールドを追加します。 たとえば、ユーザーがログインする場合は、_login_タイプのメッセージを送信します。 それを定義しましょう-

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;
   }
});

ユーザーが_login_タイプでメッセージを送信する場合、私たちは-

  • 誰かがこのユーザー名で既にログインしているかどうかを確認してください。
  • その場合は、ユーザーにログインに失敗したことを伝えます。
  • このユーザー名を使用しているユーザーがいない場合は、ユーザー名をキーとして接続オブジェクトに追加します。
  • コマンドが認識されない場合、エラーを送信します。

次のコードは、接続にメッセージを送信するためのヘルパー関数です。 _server.js_ファイルに追加します-

function sendTo(connection, message) {
   connection.send(JSON.stringify(message));
}

ユーザーが切断したら、その接続をクリーンアップする必要があります。 _close_イベントが発生したときにユーザーを削除できます。 _connection_ハンドラに次のコードを追加します-

connection.on("close", function() {
   if(connection.name) {
      delete users[connection.name];
   }
});

ログインに成功した後、ユーザーは別の電話をかけたいと考えています。 彼はそれを達成するために別のユーザーに_offer_する必要があります。 _offer_ハンドラを追加します-

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;

まず、呼び出しようとしているユーザーの_connection_を取得します。 存在する場合は、_offer_詳細を送信します。 また、_otherName_を_connection_オブジェクトに追加します。 これは、後で簡単に見つけられるようにするためです。

応答への応答には、_offer_ハンドラーで使用したのと同様のパターンがあります。 サーバーは、すべてのメッセージを別のユーザーに_answer_として渡すだけです。 _offer_ハンドラの後に次のコードを追加します-

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候補の処理です。 ユーザー間でメッセージを渡すだけで同じ手法を使用します。 主な違いは、候補メッセージがユーザーごとに複数回発生する可能性があることです。 _candidate_ハンドラーを追加します-

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;

これにより、他のユーザーに_leave_イベントも送信されるため、ピア接続を適宜切断できます。 また、ユーザーが信号サーバーから接続をドロップした場合も処理する必要があります。 _close_ハンドラを変更しましょう-

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/[[[11]]]をご覧ください。たとえば、「textchat」という名前のフォルダーを作成します。 これがルートアプリケーションフォルダになります。 このフォルダ内にファイル_package.json_を作成し(npmの依存関係を管理するために必要です)、次を追加します-

{
   "name": "webrtc-textochat",
   "version": "0.1.0",
   "description": "webrtc-textchat",
   "author": "Author",
   "license": "BSD-2-Clause"
}

次に、_npm install bootstrap_を実行します。 これにより、ブートストラップライブラリが_textchat/node_modules_フォルダーにインストールされます。

次に、基本的なHTMLページを作成する必要があります。 次のコードでルートフォルダに_indexl_ファイルを作成します-

<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>

このページはおなじみのはずです。 bootstrap cssファイルを追加しました。 また、2つのページを定義しました。 最後に、ユーザーから情報を取得するためのテキストフィールドとボタンをいくつか作成しました。 「チャット」ページには、すべてのメッセージが表示される「chatarea」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));
};

ここで、_node server_を介してシグナルサーバーを実行します。 次に、ルートフォルダー内で_static_コマンドを実行し、ブラウザー内でページを開きます。 次のコンソール出力が表示されるはずです-

コンソール出力

次の手順では、一意のユーザー名でユーザーログインを実装します。 ユーザー名をサーバーに送信するだけで、ユーザー名が取得されたかどうかがわかります。 _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_オブジェクトを作成し、見つかったすべてのicecandidatesを他のピアに送信する_onicecandidate_ハンドラーをセットアップします。 また、dataChannelも作成します。 RTCPeerConnectionオブジェクトを作成する場合、ChromeまたはOperaを使用している場合、コンストラクターの2番目の引数はオプションです:[\ {RtpDataChannels:true}]は必須です。 次のステップは、他のピアへのオファーを作成することです。 ユーザーがオファーを取得すると、_answer_を作成し、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));
};

Callボタンに_click_ハンドラーを追加して、オファーを開始します。 次に、_onmessage_ハンドラーが期待するいくつかのハンドラーを実装します。 両方のユーザーが接続するまで、非同期で処理されます。

次のステップは、ハングアップ機能の実装です。 これにより、データの送信が停止され、他のユーザーにデータチャネルを閉じるように指示されます。 次のコードを追加します-

//hang up
hangUpBtn.addEventListener("click", function () {
   send({
      type: "leave"
   });

   handleLeave();
});

function handleLeave() {
   connectedUser = null;
   yourConn.close();
   yourConn.onicecandidate = null;
};

ユーザーがハングアップボタンをクリックすると-

  • 他のユーザーに「leave」メッセージを送信します。
  • 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-セキュリティ

この章では、「WebRTCシグナリング」の章で作成したシグナリングサーバーにセキュリティ機能を追加します。 2つの機能強化があります-

  • Redisデータベースを使用したユーザー認証
  • セキュアソケット接続を有効にする

まず、Redisをインストールする必要があります。

  • 最新の安定版リリースをhttp://redis.io/download(私の場合は3.05)からダウンロードします
  • それを開梱する
  • ダウンロードしたフォルダー内で_sudo make install_を実行します
  • インストールが完了したら、_make test_を実行して、すべてが正常に機能しているかどうかを確認します。

Redisには2つの実行可能なコマンドがあります-

  • redis-cli -Redisのコマンドラインインターフェイス(クライアント部分)
  • redis-server -Redisデータストア

Redisサーバーを実行するには、ターミナルコンソールで_redis-server_と入力します。 次が表示されるはずです-

Redis Server

次に、新しいターミナルウィンドウを開き、_redis-cli_を実行してクライアントアプリケーションを開きます。

Redis-cli

基本的に、Redisはキーと値のデータベースです。 文字列値でキーを作成するには、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クライアントの作成が必要です。

認証を追加するには、接続オブジェクトの_message_ハンドラを変更します-

//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_を実行して、インストールされているかどうかを確認します。

Opensslを使用

公開および秘密のセキュリティ証明書キーを生成するには、以下に示す手順に従う必要があります-

  • 一時サーバーパスワードキーを生成
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048

一時サーバーのパスワードキー

  • サーバー秘密鍵を生成
openssl rsa -passin pass:12345 -in server.pass.key -out server.key

サーバー秘密鍵

  • 署名要求を生成します。 会社について追加の質問があります。 常に「Enter」ボタンを押すだけです。
openssl req -new -key server.key -out server.csr

署名リクエストの生成

  • 証明書を生成
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サーバーを作成します。

Operaで [[12]] を開きます。 次が表示されるはずです-

無効な証明書

[続行]ボタンをクリックします。 「OK」メッセージが表示されます。

安全な信号サーバーをテストするために、「WebRTC Text Demo」チュートリアルで作成したチャットアプリケーションを変更します。 パスワードフィールドを追加するだけです。 以下は_indexl_ファイル全体です-

<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 = "";
});

ここで、_node server_を介して安全なシグナリングサーバーを実行します。 変更されたチャットデモフォルダー内で_node static_を実行します。 2つのブラウザタブで localhost:8080 を開きます。 ログインしてみてください。 「password1」を含む「user1」と「password2」を含む「user2」のみがログインできることを覚えておいてください。 次に、RTCPeerConnectionを確立し(別のユーザーを呼び出す)、メッセージの送信を試みます。

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アプリケーションの範囲でそれらを使用する方法も学びました。