Webrtc-signaling
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_コマンドを実行します。 あなたは、クライアント側で次のように表示されるはずです-
サーバーは、接続ユーザーも記録する必要があります-
ユーザー登録
シグナリングサーバーでは、接続ごとに文字列ベースのユーザー名を使用するため、メッセージの送信先がわかります。 _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つのクライアントを同時に接続し、オファーとアンサーを作成してみてください。 次が表示されるはずです-
この例では、 offer と answer は単純な文字列ですが、実際のアプリケーションでは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接続を確立する際に順番を間違えると問題が発生する可能性があることに注意してください。
概要
この章では、シンプルでわかりやすいシグナリングサーバーを構築しました。 シグナリングプロセス、ユーザー登録、オファー/アンサーメカニズムについて説明しました。 また、ユーザー間の候補者の送信も実装しました。