Webrtc-video-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接続するときに実行されます。 次に、ユーザーから送信されたメッセージを聞きます。 最後に、接続したユーザーに「サーバーからこんにちは」という応答を送信します。

シグナリングサーバーでは、接続ごとに文字列ベースのユーザー名を使用するため、メッセージの送信先がわかります。 接続_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/[[[1]]]をご覧ください。たとえば、「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アプリケーションを安定させるための努力が払われました。