WebRTC-音声デモ
この章では、別々のデバイス上の2人のユーザーがWebRTCオーディオストリームを使用して通信できるようにするクライアントアプリケーションを構築します。私たちのアプリケーションは2ページになります。1つはログイン用で、もう1つは別のユーザーに音声通話を発信するためのものです。
2つのページがdivタグになります。ほとんどの入力は、単純なイベントハンドラーを介して行われます。
シグナリングサーバー
WebRTC接続を作成するには、クライアントはWebRTCピア接続を使用せずにメッセージを転送できる必要があります。ここで、HTML5 WebSocket(2つのエンドポイント間の双方向ソケット接続)を使用します。WebサーバーとWebブラウザーです。それでは、WebSocketライブラリの使用を開始しましょう。server.jsファイルを作成し、次のコードを挿入します-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("user connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
console.log("Got message from a user:", message);
});
connection.send("Hello from server");
});
最初の行には、すでにインストールされているWebSocketライブラリが必要です。次に、ポート9090にソケットサーバーを作成します。次に、接続イベントをリッスンします。このコードは、ユーザーがサーバーにWebSocket接続を行うときに実行されます。次に、ユーザーから送信されたメッセージを聞きます。最後に、接続されたユーザーに「サーバーからこんにちは」という応答を送信します。
シグナリングサーバーでは、接続ごとに文字列ベースのユーザー名を使用するため、メッセージの送信先がわかります。接続ハンドラーを少し変更しましょう-
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
});
このようにして、JSONメッセージのみを受け入れます。次に、接続されているすべてのユーザーをどこかに保存する必要があります。単純なJavascriptオブジェクトを使用します。ファイルの先頭を変更します-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
クライアントからのすべてのメッセージにタイプフィールドを追加します。たとえば、ユーザーがログインしたい場合、ユーザーはログインタイプのメッセージを送信します。それを定義しましょう-
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged:", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command no found: " + data.type
});
break;
}
});
ユーザーがログインタイプのメッセージを送信した場合、次のようになります。
- 誰かがすでにこのユーザー名でログインしていないか確認してください。
- その場合は、正常にログインしていないことをユーザーに伝えます。
- このユーザー名を使用しているユーザーがいない場合は、接続オブジェクトのキーとしてユーザー名を追加します。
- コマンドが認識されない場合、エラーを送信します。
次のコードは、接続にメッセージを送信するためのヘルパー関数です。server.jsファイルに追加します-
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
ユーザーが切断したら、接続をクリーンアップする必要があります。closeイベントが発生したときにユーザーを削除できます。次のコードを接続ハンドラーに追加します-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
}
});
ログインに成功した後、ユーザーは別のユーザーに電話をかけたいと考えています。彼はそれを達成するために別のユーザーに申し出をする必要があります。オファーハンドラーを追加します-
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
まず、呼び出しようとしているユーザーの接続を取得します。存在する場合は、オファーの詳細を送信します。また、otherNameを接続オブジェクトに追加します。これは、後で見つけやすくするために作成されています。
応答への応答には、オファーハンドラーで使用したのと同様のパターンがあります。私たちのサーバーは、別のユーザーへの回答としてすべてのメッセージを通過させるだけです。オファーハンドラの後に次のコードを追加します-
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
最後の部分は、ユーザー間でICE候補を処理することです。ユーザー間でメッセージを渡すだけでも同じ手法を使用します。主な違いは、候補メッセージがユーザーごとに任意の順序で複数回発生する可能性があることです。候補ハンドラーを追加します-
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
ユーザーが別のユーザーから切断できるようにするには、ハングアップ機能を実装する必要があります。また、すべてのユーザー参照を削除するようにサーバーに指示します。休暇ハンドラーを追加します-
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
これにより、他のユーザーにも休暇イベントが送信されるため、他のユーザーはそれに応じてピア接続を切断できます。また、ユーザーがシグナリングサーバーから接続を切断した場合にも対処する必要があります。クローズハンドラーを変更しましょう-
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
以下は、シグナリングサーバーのコード全体です-
//require our websocket library
var WebSocketServer = require('ws').Server;
//creating a websocket server at port 9090
var wss = new WebSocketServer({port: 9090});
//all connected to the server users
var users = {};
//when a user connects to our sever
wss.on('connection', function(connection) {
console.log("User connected");
//when server gets a message from a connected user
connection.on('message', function(message) {
var data;
//accepting only JSON messages
try {
data = JSON.parse(message);
} catch (e) {
console.log("Invalid JSON");
data = {};
}
//switching type of the user message
switch (data.type) {
//when a user tries to login
case "login":
console.log("User logged", data.name);
//if anyone is logged in with this username then refuse
if(users[data.name]) {
sendTo(connection, {
type: "login",
success: false
});
} else {
//save user connection on the server
users[data.name] = connection;
connection.name = data.name;
sendTo(connection, {
type: "login",
success: true
});
}
break;
case "offer":
//for ex. UserA wants to call UserB
console.log("Sending offer to: ", data.name);
//if UserB exists then send him offer details
var conn = users[data.name];
if(conn != null) {
//setting that UserA connected with UserB
connection.otherName = data.name;
sendTo(conn, {
type: "offer",
offer: data.offer,
name: connection.name
});
}
break;
case "answer":
console.log("Sending answer to: ", data.name);
//for ex. UserB answers UserA
var conn = users[data.name];
if(conn != null) {
connection.otherName = data.name;
sendTo(conn, {
type: "answer",
answer: data.answer
});
}
break;
case "candidate":
console.log("Sending candidate to:",data.name);
var conn = users[data.name];
if(conn != null) {
sendTo(conn, {
type: "candidate",
candidate: data.candidate
});
}
break;
case "leave":
console.log("Disconnecting from", data.name);
var conn = users[data.name];
conn.otherName = null;
//notify the other user so he can disconnect his peer connection
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
break;
default:
sendTo(connection, {
type: "error",
message: "Command not found: " + data.type
});
break;
}
});
//when user exits, for example closes a browser window
//this may help if we are still in "offer","answer" or "candidate" state
connection.on("close", function() {
if(connection.name) {
delete users[connection.name];
if(connection.otherName) {
console.log("Disconnecting from ", connection.otherName);
var conn = users[connection.otherName];
conn.otherName = null;
if(conn != null) {
sendTo(conn, {
type: "leave"
});
}
}
}
});
connection.send("Hello world");
});
function sendTo(connection, message) {
connection.send(JSON.stringify(message));
}
クライアントアプリケーション
このアプリケーションをテストする1つの方法は、2つのブラウザタブを開いて、相互に音声通話を発信することです。
まず、ブートストラップライブラリをインストールする必要があります。Bootstrapは、Webアプリケーションを開発するためのフロントエンドフレームワークです。あなたはでもっと学ぶことができますhttp://getbootstrap.com/.たとえば、「audiochat」というフォルダを作成します。これがルートアプリケーションフォルダになります。このフォルダー内にファイルpackage.json(npmの依存関係を管理するために必要)を作成し、以下を追加します-
{
"name": "webrtc-audiochat",
"version": "0.1.0",
"description": "webrtc-audiochat",
"author": "Author",
"license": "BSD-2-Clause"
}
次に、npm installbootstrapを実行します。これにより、ブートストラップライブラリがaudiochat / node_modulesフォルダーにインストールされます。
次に、基本的なHTMLページを作成する必要があります。次のコードを使用して、ルートフォルダにindex.htmlファイルを作成します-
<html>
<head>
<title>WebRTC Voice Demo</title>
<link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
</head>
<style>
body {
background: #eee;
padding: 5% 0;
}
</style>
<body>
<div id = "loginPage" class = "container text-center">
<div class = "row">
<div class = "col-md-4 col-md-offset-4">
<h2>WebRTC Voice Demo. Please sign in</h2>
<label for = "usernameInput" class = "sr-only">Login</label>
<input type = "email" id = "usernameInput"
class = "form-control formgroup"
placeholder = "Login" required = "" autofocus = "">
<button id = "loginBtn" class = "btn btn-lg btn-primary btnblock">
Sign in</button>
</div>
</div>
</div>
<div id = "callPage" class = "call-page">
<div class = "row">
<div class = "col-md-6 text-right">
Local audio: <audio id = "localAudio"
controls autoplay></audio>
</div>
<div class = "col-md-6 text-left">
Remote audio: <audio id = "remoteAudio"
controls autoplay></audio>
</div>
</div>
<div class = "row text-center">
<div class = "col-md-12">
<input id = "callToUsernameInput"
type = "text" placeholder = "username to call" />
<button id = "callBtn" class = "btn-success btn">Call</button>
<button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button>
</div>
</div>
</div>
<script src = "client.js"></script>
</body>
</html>
このページはおなじみのはずです。ブートストラップcssファイルを追加しました。また、2つのページを定義しました。最後に、ユーザーから情報を取得するためのいくつかのテキストフィールドとボタンを作成しました。ローカルオーディオストリームとリモートオーディオストリームの2つのオーディオ要素が表示されます。client.jsファイルへのリンクを追加したことに注意してください。
次に、シグナリングサーバーとの接続を確立する必要があります。次のコードを使用して、ルートフォルダーにclient.jsファイルを作成します-
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
次に、ノードサーバーを介してシグナリングサーバーを実行します。次に、ルートフォルダー内で静的コマンドを実行し、ブラウザー内でページを開きます。次のコンソール出力が表示されます-
次のステップは、一意のユーザー名でログインするユーザーを実装することです。サーバーにユーザー名を送信するだけで、サーバーが取得されたかどうかを通知します。次のコードをclient.jsファイルに追加します-
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
}
};
まず、ページ上の要素への参照をいくつか選択します。通話ページを非表示にします。次に、ログインボタンにイベントリスナーを追加します。ユーザーがクリックすると、ユーザー名がサーバーに送信されます。最後に、handleLoginコールバックを実装します。ログインに成功すると、通話ページが表示され、ピア接続のセットアップが開始されます。
ピア接続を開始するには、次のものが必要です。
- マイクからオーディオストリームを取得する
- RTCPeerConnectionオブジェクトを作成します
次のコードを「UIセレクターブロック」に追加します-
var localAudio = document.querySelector('#localAudio');
var remoteAudio = document.querySelector('#remoteAudio');
var yourConn;
var stream;
handleLogin関数を変更します-
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//getting local audio stream
navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) {
stream = myStream;
//displaying local audio stream on the page
localAudio.src = window.URL.createObjectURL(stream);
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration);
// setup stream listening
yourConn.addStream(stream);
//when a remote user adds stream to the peer connection, we display it
yourConn.onaddstream = function (e) {
remoteAudio.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
});
}
};
}, function (error) {
console.log(error);
});
}
};
これで、コードを実行すると、ページでログインしてローカルオーディオストリームをページに表示できるようになります。
これで、通話を開始する準備が整いました。まず、別のユーザーにオファーを送信します。ユーザーがオファーを受け取ると、回答を作成し、ICE候補の取引を開始します。次のコードをclient.jsファイルに追加します-
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
オファーを開始する[通話]ボタンにクリックハンドラーを追加します。次に、onmessageハンドラーが期待するいくつかのハンドラーを実装します。これらは、両方のユーザーが接続を確立するまで非同期で処理されます。
最後のステップは、ハングアップ機能の実装です。これにより、データの送信が停止し、他のユーザーに通話を終了するように指示します。次のコードを追加します-
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
remoteAudio.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};
ユーザーが[電話を切る]ボタンをクリックしたとき-
- 他のユーザーに「脱退」メッセージを送信します
- RTCPeerConnectionを閉じ、接続をローカルで破棄します
次に、コードを実行します。2つのブラウザタブを使用してサーバーにログインできるはずです。次に、タブに音声通話を発信して、電話を切ることができます。
以下はclient.jsファイル全体です-
//our username
var name;
var connectedUser;
//connecting to our signaling server
var conn = new WebSocket('ws://localhost:9090');
conn.onopen = function () {
console.log("Connected to the signaling server");
};
//when we got a message from a signaling server
conn.onmessage = function (msg) {
console.log("Got message", msg.data);
var data = JSON.parse(msg.data);
switch(data.type) {
case "login":
handleLogin(data.success);
break;
//when somebody wants to call us
case "offer":
handleOffer(data.offer, data.name);
break;
case "answer":
handleAnswer(data.answer);
break;
//when a remote peer sends an ice candidate to us
case "candidate":
handleCandidate(data.candidate);
break;
case "leave":
handleLeave();
break;
default:
break;
}
};
conn.onerror = function (err) {
console.log("Got error", err);
};
//alias for sending JSON encoded messages
function send(message) {
//attach the other peer username to our messages
if (connectedUser) {
message.name = connectedUser;
}
conn.send(JSON.stringify(message));
};
//******
//UI selectors block
//******
var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var loginBtn = document.querySelector('#loginBtn');
var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');
var localAudio = document.querySelector('#localAudio');
var remoteAudio = document.querySelector('#remoteAudio');
var yourConn;
var stream;
callPage.style.display = "none";
// Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
name = usernameInput.value;
if (name.length > 0) {
send({
type: "login",
name: name
});
}
});
function handleLogin(success) {
if (success === false) {
alert("Ooops...try a different username");
} else {
loginPage.style.display = "none";
callPage.style.display = "block";
//**********************
//Starting a peer connection
//**********************
//getting local audio stream
navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) {
stream = myStream;
//displaying local audio stream on the page
localAudio.src = window.URL.createObjectURL(stream);
//using Google public stun server
var configuration = {
"iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
};
yourConn = new webkitRTCPeerConnection(configuration);
// setup stream listening
yourConn.addStream(stream);
//when a remote user adds stream to the peer connection, we display it
yourConn.onaddstream = function (e) {
remoteAudio.src = window.URL.createObjectURL(e.stream);
};
// Setup ice handling
yourConn.onicecandidate = function (event) {
if (event.candidate) {
send({
type: "candidate",
candidate: event.candidate
});
}
};
}, function (error) {
console.log(error);
});
}
};
//initiating a call
callBtn.addEventListener("click", function () {
var callToUsername = callToUsernameInput.value;
if (callToUsername.length > 0) {
connectedUser = callToUsername;
// create an offer
yourConn.createOffer(function (offer) {
send({
type: "offer",
offer: offer
});
yourConn.setLocalDescription(offer);
}, function (error) {
alert("Error when creating an offer");
});
}
});
//when somebody sends us an offer
function handleOffer(offer, name) {
connectedUser = name;
yourConn.setRemoteDescription(new RTCSessionDescription(offer));
//create an answer to an offer
yourConn.createAnswer(function (answer) {
yourConn.setLocalDescription(answer);
send({
type: "answer",
answer: answer
});
}, function (error) {
alert("Error when creating an answer");
});
};
//when we got an answer from a remote user
function handleAnswer(answer) {
yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};
//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};
//hang up
hangUpBtn.addEventListener("click", function () {
send({
type: "leave"
});
handleLeave();
});
function handleLeave() {
connectedUser = null;
remoteAudio.src = null;
yourConn.close();
yourConn.onicecandidate = null;
yourConn.onaddstream = null;
};