Webrtc-voice-demo

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

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/[[[1]]]をご覧ください。たとえば、「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;
};