WebRTC とのリアルタイムのコミュニケーションを実現する

1. 始める前に

この Codelab では、動画を入手してウェブカメラでスナップショットを撮影し、WebRTC でピアツーピアで共有するためのアプリの作成方法について説明します。また、コア WebRTC API の使用方法と、Node.js を使用したメッセージング サーバーの設定方法についても学習します。

Prerequisites

  • HTML、CSS、JavaScript の基本的な知識

作成するアプリの概要

  • ウェブカメラから動画を取得します。
  • RTCPeerConnectionで動画をストリーミングします。
  • RTCDataChannel でデータをストリーミングします。
  • メッセージを交換するためのシグナリング サービスを設定します。
  • ピア接続とシグナリングを組み合わせる。
  • 写真を撮影し、データチャネルを使用して共有します。

必要なもの

2. サンプルコードを取得する

コードをダウンロードする

  1. Git に精通している場合は、次のコマンドを実行して、GitHub からこの Codelab のコードのクローンを作成します。
git clone https://github.com/googlecodelabs/webrtc-web

または、次のリンクをクリックして、コードの zip ファイルをダウンロードします。

  1. ダウンロードした zip ファイルを開いて webrtc-web-master という名前のプロジェクト フォルダを展開します。このフォルダには、この Codelab の各ステップにつき 1 つのフォルダと必要なすべてのリソースが含まれています。

すべてのコードは work という名前のディレクトリで処理します。

step-nn フォルダには、この Codelab の各ステップの完了版が含まれています。参照用に表示されています。

ウェブサーバーのインストールと確認

ご自身のウェブサーバーを使用してもかまいませんが、この Codelab は Chrome 用のウェブサーバーと適切に機能するように設計されています。

  1. Chrome 版ウェブサーバーをお持ちでない場合は、次のリンクをクリックして Chrome ウェブストアからインストールしてください。

d0a4649b4920cf3.png

  1. [Chrome に追加] をクリックします。Chrome 用ウェブサーバーをインストールすると、Google アプリが自動的に新しいタブで開きます。
  2. [ウェブサーバー] をクリックします。

27FCE4494F641883.PNG

ローカル ウェブサーバーを構成するためのダイアログが表示されます。

a300381a486b9e22.png

  1. [フォルダを選択] をクリックします。
  2. 作成した work フォルダを選択します。

[ウェブサーバーの URL] に、処理中の作業を確認できる URL が表示されます。

Chrome。

  1. [オプション(再起動が必要な場合があります)] で [index.html を自動的に表示] チェックボックスをオンにします。
  2. [Web Server: Start] を 2 回切り替えて、サーバーを停止して再起動します。

f23cafb3993dfac1.png

  1. [ウェブサーバーの URL] で URL をクリックし、ウェブブラウザで結果を確認します。

次のようなページが表示されます。work/index.html に対応しています。

18a705cb6ccc5181.png

当然、このアプリはまだ何もしません。ウェブサーバーが正常に機能するための最小のスケルトンです。以降のステップで、機能とレイアウト機能を追加します。

3. ウェブカメラから動画をストリーミングする

このステップの完全なバージョンは、step-01 フォルダにあります。

HTML でダッシュを追加する

次のコードをコピーして work ディレクトリの index.html ファイルに貼り付けて、video 要素と script 要素を追加します。

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <video autoplay playsinline></video>

  <script src="js/main.js"></script>

</body>

</html>

アジャイルな JavaScript を追加する

次のコードをコピーして、js フォルダの main.js ファイルに貼り付けます。

'use strict';

// In this codelab, you  only stream video, not audio (video: true).
const mediaStreamConstraints = {
  video: true,
};

// The video element where the stream is displayed
const localVideo = document.querySelector('video');

// The local stream that's displayed on the video
let localStream;

// Handle success and add the MediaStream to the video element
function gotLocalMediaStream(mediaStream) {
  localStream = mediaStream;
  localVideo.srcObject = mediaStream;
}

// Handle error and log a message to the console with the error message
function handleLocalMediaStreamError(error) {
  console.log('navigator.getUserMedia error: ', error);
}

// Initialize media stream
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);

試してみる

ブラウザで index.html ファイルを開くと、次のように表示されますが、ウェブカメラからのビューの場合は当然です。

9297048e43ed0f3d.png

仕組み

getUserMedia() 呼び出しの後、現在のオリジンに対するカメラへのアクセスを初めてリクエストした場合、ブラウザはカメラへのアクセス権限をリクエストします。

成功すると、MediaStream が返されます。これは、media 要素が srcObject 属性を介して使用できます。

navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
  .then(gotLocalMediaStream).catch(handleLocalMediaStreamError);


}
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

constraints 引数を使用すると、取得するメディアを指定できます。この例で音声がデフォルトで無効になっているため、メディアは動画のみです。

const mediaStreamConstraints = {
  video: true,
};

動画解像度など、追加の要件には制約を使用できます。

const hdConstraints = {
  video: {
    width: {
      min: 1280
    },
    height: {
      min: 720
    }
  }
}

MediaTrackConstraints 仕様には、考えられるすべての制約タイプが表示されますが、すべてのブラウザがすべてのオプションをサポートしているわけではありません。リクエストされた解像度が現在選択されているカメラでサポートされていない場合、getUserMedia()OverconstrainedError により拒否され、カメラへのアクセスを許可するよう求められます。

getUserMedia() が成功した場合、ウェブカメラからの動画ストリームは動画要素のソースとして設定されます。

function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
}

ボーナス ポイントを獲得

  • getUserMedia() に渡される localStream オブジェクトはグローバル スコープであるため、ブラウザ コンソールから検査できます。コンソールを開いて「stream,」と入力し、Enter(Mac では Return)を押します。Chrome でコンソールを表示するには、Control+Shift+J(Mac では Command+Option+J)を押します。
  • localStream.getVideoTracks() ではどのような結果が返されますか。
  • localStream.getVideoTracks()[0].stop()にお電話ください。
  • 制約オブジェクトを確認します。{audio: true, video: true} に変更するとどうなりますか?
  • 動画要素のサイズを指定します。ディスプレイのサイズではなく、JavaScript から自然なサイズで動画を取得するにはどうすればよいですか?Google Chrome デベロッパー ツールを使用して確認します。
  • 次のように、動画要素に CSS フィルタを追加します。
video {
  filter: blur(4px) invert(1) opacity(0.5);
}
  • SVG フィルタを次のように追加します。
video {
   filter: hue-rotate(180deg) saturate(200%);
 }

ヒント

  • video 要素の autoplay 属性を忘れないでください。そのようにしないと、フレームが 1 つだけ表示されます。
  • getUserMedia() 制約には他にもさまざまなオプションがあります。その他の例については、制約と統計情報、およびその他の制約と統計情報WebRTC サンプルでご覧ください。

ベスト プラクティス

動画要素がコンテナからはみ出ないようにしてください。この Codelab では、動画の推奨サイズと最大サイズを設定する widthmax-width を追加しました。高さは自動的に計算されます。

video {
  max-width: 100%;
  width: 320px;
}

4. RTCPeerConnection API を使用した動画のストリーミング

このステップの完全なバージョンは、step-2 フォルダにあります。

動画要素とコントロール ボタンを追加する

index.html ファイルで、1 つの video 要素を 2 つの video 要素と 3 つの button 要素に置き換えます。

<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>


<div>
  <button id="startButton">Start</button>
  <button id="callButton">Call</button>
  <button id="hangupButton">Hang Up</button>
</div>

1 つの動画要素に getUserMedia() のストリームが表示され、もう 1 つの要素が RTCPeerconnection でストリーミングされた同じ動画を示しています。(実際のアプリでは、1 つの video 要素でローカル ストリームを表示し、もう 1 つでリモート ストリームを表示します)。

アダプタ.js shim を追加する

このスクリプト要素をコピーし、main.js のスクリプト要素の上に貼り付けます。

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>

index.html ファイルは次のようになります。

<!DOCTYPE html>
<html>

<head>
  <title>Real-time communication with WebRTC</title>
  <link rel="stylesheet" href="css/main.css" />
</head>

<body>
  <h1>Real-time communication with WebRTC</h1>

  <video id="localVideo" autoplay playsinline></video>
  <video id="remoteVideo" autoplay playsinline></video>

  <div>
    <button id="startButton">Start</button>
    <button id="callButton">Call</button>
    <button id="hangupButton">Hang Up</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

RTCPeerConnection コードをインストールする

main.js は、step-02 フォルダのバージョンに置き換えます。

通話を発信する

  1. index.html ファイルを開きます。
  2. [Start] をクリックすると、ウェブカメラから動画を取得できます。
  3. [通話] をクリックして、ピア接続を確立します。

両方の video 要素にウェブカメラの映像が表示されます。

  1. ブラウザのコンソールで WebRTC ロギングを確認します。

仕組み

このステップは多くのことを行います。

WebRTC では、RTCPeerConnection API を使用して、WebRTC クライアント(ピア)間で動画をストリーミングする接続を設定します。

この例では、2 つの RTCPeerConnection オブジェクト(pc1pc2)が同じページにあります。

WebRTC ピア間の通話設定には、次の 3 つのタスクがあります。

  1. 通話の終わりごとに RTCPeerConnection を作成し、両端に getUserMedia() のローカル ストリームを追加します。
  2. ネットワーク情報を取得して共有する。

考えられる接続エンドポイントは、ICE 候補と呼ばれます。

  1. ローカルの説明とリモートの説明を取得して共有できます。

ローカル メディアのメタデータは Session Description Protocol(SDP)形式です。

Alice と Bob が RTCPeerConnection を使用してビデオチャットを設定するとします。

まず Alice と Bob がネットワーク情報を交換します。候補候補は、ICEフレームワークを使用してネットワーク インターフェースとポートを見つけるプロセスを指します。

  1. Alice は onicecandidate (addEventListener('icecandidate')) ハンドラで RTCPeerConnection オブジェクトを作成します。

これは、main.js の次のコードに対応します。

let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
    'iceconnectionstatechange', handleConnectionChange);
  1. Alice が getUserMedia() を呼び出し、渡されたストリームを追加します。
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).
  then(gotLocalMediaStream).
  catch(handleLocalMediaStreamError);
function gotLocalMediaStream(mediaStream) {
  localVideo.srcObject = mediaStream;
  localStream = mediaStream;
  trace('Received local stream.');
  callButton.disabled = false;  // Enable call button.
}
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
  1. 最初のステップの onicecandidate ハンドラは、ネットワーク候補が利用可能になると呼び出されます。
  2. Alice はシリアル化された候補データを Bob に送信します。

実際のアプリでは、シグナリングと呼ばれ、メッセージング サービスを通じて行われます。その方法については、後のステップで説明します。もちろん、このステップでは 2 つの RTCPeerConnection オブジェクトが同じページ上にあり、外部メッセージングを必要とせずに直接通信できます。

  1. Alice から候補メッセージを受け取った Bob は、addIceCandidate() を呼び出して、リモートピアの説明に候補を追加します。
function handleConnection(event) {
  const peerConnection = event.target;
  const iceCandidate = event.candidate;

  if (iceCandidate) {
    const newIceCandidate = new RTCIceCandidate(iceCandidate);
    const otherPeer = getOtherPeer(peerConnection);

    otherPeer.addIceCandidate(newIceCandidate)
      .then(() => {
        handleConnectionSuccess(peerConnection);
      }).catch((error) => {
        handleConnectionFailure(peerConnection, error);
      });

    trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
          `${event.candidate.candidate}.`);
  }
}

WebRTC ピアでは、解像度やコーデックの機能など、ローカルとリモートの音声や動画のメディア情報を検出し、交換する必要があります。メディア構成情報を交換するためのシグナリングは、SDP 形式を使用したメタデータの blob(オファーと回答)の交換に進みます。

  1. Alice は RTCPeerConnection createOffer() メソッドを実行します。

返された Promise は、RTCSessionDescription(Alice)のローカル セッションの説明を提供します。

trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
  .then(createdOffer).catch(setSessionDescriptionError);
  1. 成功した場合、Alice は setLocalDescription() を使用してローカルの説明を設定し、セッション シグナルをシグナル チャネルを介して田中さんに送信します。
  2. Bob は setRemoteDescription() で、Alice にリモートで送る説明を設定しました。
  3. Bob は RTCPeerConnection createAnswer() メソッドを実行し、Alice からリモートで取得した説明を渡します。これにより、彼女と互換性のあるローカル セッションが生成されます。
  4. createAnswer() Promise は RTCSessionDescription を渡します。これは、Bob がローカルの説明として設定し、Alice に送信します。
  5. Alice がボブのセッションの説明を取得したときは、setRemoteDescription() でリモートの説明としてそれを設定します。
// Logs offer creation and sets peer connection session descriptions
function createdOffer(description) {
  trace(`Offer from localPeerConnection:\n${description.sdp}`);

  trace('localPeerConnection setLocalDescription start.');
  localPeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection setRemoteDescription start.');
  remotePeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('remotePeerConnection createAnswer start.');
  remotePeerConnection.createAnswer()
    .then(createdAnswer)
    .catch(setSessionDescriptionError);
}

// Logs answer to offer creation and sets peer-connection session descriptions
function createdAnswer(description) {
  trace(`Answer from remotePeerConnection:\n${description.sdp}.`);

  trace('remotePeerConnection setLocalDescription start.');
  remotePeerConnection.setLocalDescription(description)
    .then(() => {
      setLocalDescriptionSuccess(remotePeerConnection);
    }).catch(setSessionDescriptionError);

  trace('localPeerConnection setRemoteDescription start.');
  localPeerConnection.setRemoteDescription(description)
    .then(() => {
      setRemoteDescriptionSuccess(localPeerConnection);
    }).catch(setSessionDescriptionError);
}

ボーナス ポイントを獲得

  1. chrome://webrtc-internals に移動します。

このページでは、WebRTC の統計情報とデバッグデータを提供します。(Chrome の URL の詳細なリストについては、chrome://about をご覧ください)。

  1. CSS でページのスタイルを設定します。
  2. 動画を配置します。
  3. ボタンの幅をテキストに合わせて変更します。
  4. レイアウトがモバイルに対応していることを確認します。
  5. Chrome デベロッパー ツールのコンソールで、localStreamlocalPeerConnectionremotePeerConnection を確認します。
  6. Console で localPeerConnectionpc1.localDescription を確認します。

SDP のフォーマット

ヒント

  • アダプタ.js shim の詳細については、adapter.jsGitHub リポジトリをご覧ください。
  • AppRTC とそのコード(WebRTC 呼び出し用の WebRTC プロジェクトの正規アプリ)を確認できます。通話のセットアップ時間が 500 ミリ秒未満。

ベスト プラクティス

今後もコードを利用し続けるには、新しい Promise ベースの API を使用し、adapter.js でブラウザをサポートしていないブラウザとの互換性を確保してください。

5. データチャネルを使用してデータを交換する

このステップの完全なバージョンは、step-03 フォルダにあります。

HTML を更新する

この手順では、WebRTC データチャネルを使用して、同じページの 2 つの textarea 要素間でテキストを送信します。これはあまり役立ちませんが、WebRTC を使用してデータをストリーミングしたり、ストリーミングしたりする方法を示しています。

video 要素と button 要素を index.html, から削除し、次の HTML で置き換えます。

<textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
<textarea id="dataChannelReceive" disabled></textarea>

<div id="buttons">
  <button id="startButton">Start</button>
  <button id="sendButton">Send</button>
  <button id="closeButton">Stop</button>
</div>

1 つは textarea でテキストを入力するもので、もう 1 つはピア間でストリーミングされるテキストを表示するためのものです。

index.html ファイルは次のようになります。

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <textarea id="dataChannelSend" disabled
    placeholder="Press Start, enter some text, then press Send."></textarea>
  <textarea id="dataChannelReceive" disabled></textarea>

  <div id="buttons">
    <button id="startButton">Start</button>
    <button id="sendButton">Send</button>
    <button id="closeButton">Stop</button>
  </div>

  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>

JavaScript を更新する

  1. main.js は、step-03/js/main.js の内容に置き換えます。
  1. ピア間でデータをストリーミングしてみます。
  2. index.html を開きます。
  3. [Start] をクリックして、ピア接続を設定します。
  4. 左側の textarea にテキストを入力します。
  5. [送信] をクリックすると、WebRTC データチャネルを使用してテキストが転送されます。

仕組み

このコードは、RTCPeerConnectionRTCDataChannel を使用してテキスト メッセージの交換を有効にします。

このステップのコードの多くは、RTCPeerConnection の例と同じです。sendData() 関数と createConnection() 関数には、新しいコードのほとんどが含まれています。

function createConnection() {
  dataChannelSend.placeholder = '';
  var servers = null;
  pcConstraint = null;
  dataConstraint = null;
  trace('Using SCTP based data channels');
  // For SCTP, reliable and ordered delivery is true by default.
  // Add localConnection to global scope to make it visible
  // from the browser console.
  window.localConnection = localConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created local peer connection object localConnection');

  sendChannel = localConnection.createDataChannel('sendDataChannel',
      dataConstraint);
  trace('Created send data channel');

  localConnection.onicecandidate = iceCallback1;
  sendChannel.onopen = onSendChannelStateChange;
  sendChannel.onclose = onSendChannelStateChange;

  // Add remoteConnection to global scope to make it visible
  // from the browser console.
  window.remoteConnection = remoteConnection =
      new RTCPeerConnection(servers, pcConstraint);
  trace('Created remote peer connection object remoteConnection');

  remoteConnection.onicecandidate = iceCallback2;
  remoteConnection.ondatachannel = receiveChannelCallback;

  localConnection.createOffer().then(
    gotDescription1,
    onCreateSessionDescriptionError
  );
  startButton.disabled = true;
  closeButton.disabled = false;
}

function sendData() {
  var data = dataChannelSend.value;
  sendChannel.send(data);
  trace('Sent Data: ' + data);
}

RTCDataChannel の構文は意図的に WebSocket と類似しており、send() メソッドと message イベントを使用します。

dataConstraint が使用されていることに注意してください。データチャネルは、パフォーマンスよりも信頼性の高い配信を優先するなど、さまざまな種類のデータ共有を可能にするように設定できます。

ボーナス ポイントを獲得

  1. WebRTC データチャネルで使用されるプロトコルである SCTP により、信頼性が高く、順序付けされたデータ配信がデフォルトでオンになります。どのような場合に RTCDataChannel が信頼性の高いデータ提供を必要とし、それによって一部のデータが失われてもパフォーマンスが重要になるのでしょうか。
  2. CSS を使用してページ レイアウトを改善し、プレースホルダ属性を dataChannelReceive textarea に追加します。
  3. モバイル デバイスでページをテストします。

詳細

6. メッセージを交換するためのシグナリング サービスの設定

同じページのピア間でデータを交換する方法を学びましたが、それを異なるマシン間で行うにはどうすればよいでしょうか。まず、メタデータ メッセージを交換するためのシグナリング チャネルを設定する必要があります。

このステップの完全なバージョンは、step-04 フォルダにあります。

アプリについて

WebRTC はクライアント側の JavaScript API を使用しますが、実際の環境では、シグナリング(メッセージング)サーバーと STUN サーバー、TURN サーバーも必要です。詳しくはこちらをご覧ください。

このステップでは、Socket.IO Node.js モジュールとメッセージング用の JavaScript ライブラリを使用して、簡単な Node.js シグナリング サーバーを構築します。

この例では、サーバー(Node.js アプリ)は index.js に実装され、サーバーを実行するクライアント(ウェブアプリ)は index.html に実装されています。

このステップの Node.js アプリには 2 つのタスクがあります。

まず、これはメッセージのリレーとして機能します。

socket.on('message', function (message) {
  log('Got message: ', message);
  socket.broadcast.emit('message', message);
});

次に、WebRTC ビデオチャット ルームを管理します。

if (numClients === 0) {
  socket.join(room);
  socket.emit('created', room, socket.id);
} else if (numClients === 1) {
  socket.join(room);
  socket.emit('joined', room, socket.id);
  io.sockets.in(room).emit('ready');
} else { // max two clients
  socket.emit('full', room);
}

シンプルな WebRTC アプリでは、最大 2 人のピアメンバーが部屋を共有できます。

HTML と JavaScript

  1. index.html を次のように更新します。
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <script src="/socket.io/socket.io.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>

このステップでは、ページに何も表示されません。すべてのロギングはブラウザ コンソールに行われます。Chrome でコンソールを表示するには、Control+Shift+J(Mac では Command+Option+J)を押します。

  1. js/main.js を次のように置き換えます。
'use strict';

var isInitiator;

window.room = prompt("Enter room name:");

var socket = io.connect();

if (room !== "") {
  console.log('Message from client: Asking to join room ' + room);
  socket.emit('create or join', room);
}

socket.on('created', function(room, clientId) {
  isInitiator = true;
});

socket.on('full', function(room) {
  console.log('Message from client: Room ' + room + ' is full :^(');
});

socket.on('ipaddr', function(ipaddr) {
  console.log('Message from client: Server IP address is ' + ipaddr);
});

socket.on('joined', function(room, clientId) {
  isInitiator = false;
});

socket.on('log', function(array) {
  console.log.apply(console, array);
});

Node.js で実行する Socket.IO ファイルを設定する

HTML ファイルで Socket.IO ファイルを使用していることがわかります。

<script src="/socket.io/socket.io.js"></script>
  1. work ディレクトリの最上位に、次の内容のファイルを package.json という名前で作成します。
{
  "name": "webrtc-codelab",
  "version": "0.0.1",
  "description": "WebRTC codelab",
  "dependencies": {
    "node-static": "^0.7.10",
    "socket.io": "^1.2.0"
  }
}

これは、Node Package Manager(npm)にプロジェクトを指定するアプリ マニフェストです。

依存関係をインストールします。

  1. /socket.io/socket.io.js などの依存関係をインストールするには、コマンドライン ターミナルで work ディレクトリから次のコマンドを実行します。
npm install

次のようなインストール ログが表示されます。

3ab06b7bcc7664b9.png

ご覧のように、npmpackage.json で定義された依存関係をインストールしました。

  1. work ディレクトリの最上位(js ディレクトリではない)に新しいファイル index.js を作成し、次のコードを追加します。
'use strict';

var os = require('os');
var nodeStatic = require('node-static');
var http = require('http');
var socketIO = require('socket.io');

var fileServer = new(nodeStatic.Server)();
var app = http.createServer(function(req, res) {
  fileServer.serve(req, res);
}).listen(8080);

var io = socketIO.listen(app);
io.sockets.on('connection', function(socket) {

  // Convenience function to log server messages on the client
  function log() {
    var array = ['Message from server:'];
    array.push.apply(array, arguments);
    socket.emit('log', array);
  }

  socket.on('message', function(message) {
    log('Client said: ', message);
    // For a real app, would be room-only (not broadcast)
    socket.broadcast.emit('message', message);
  });

  socket.on('create or join', function(room) {
    log('Received request to create or join room ' + room);

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;

    log('Room ' + room + ' now has ' + numClients + ' client(s)');

    if (numClients === 0) {
      socket.join(room);
      log('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 1) {
      log('Client ID ' + socket.id + ' joined room ' + room);
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // max two clients
      socket.emit('full', room);
    }
  });

  socket.on('ipaddr', function() {
    var ifaces = os.networkInterfaces();
    for (var dev in ifaces) {
      ifaces[dev].forEach(function(details) {
        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
          socket.emit('ipaddr', details.address);
        }
      });
    }
  });

});
  1. コマンドラインから、work ディレクトリで次のコマンドを実行します。
node index.js
  1. ブラウザから http://localhost:8080 にアクセスします。

この URL に移動するたびに、チャットルーム名を入力するよう求められます。

同じチャットルームに参加するには、毎回同じチャットルーム名(foo など)を入力します。

  1. 新しいタブを開き、もう一度 http://localhost:8080 に移動して同じ部屋名をもう一度入力します。
  2. 別の新しいタブを開き、http://localhost:8080 に再度移動して同じ部屋名をもう一度入力します。
  3. 各タブのコンソールを確認します。

JavaScript によるロギングが表示されます。

ボーナス ポイントを獲得

  • どのようなメッセージ メカニズムを採用できますか。純粋な WebSocket を使用すると、どのような問題が発生する可能性がありますか。
  • このアプリのスケーリングに関連する問題は何か数千から数百万の会議室の同時リクエストをテストする方法を開発できますか。
  • このアプリは、JavaScript プロンプトを使って部屋名を取得します。URL からチャットルーム名を取得する方法を見つけます。たとえば、http://localhost:8080/foo の場合、客室名は foo になります。

詳細

7. ピア接続とシグナリングを組み合わせる

このステップの完全なバージョンは、step-05 フォルダにあります。

HTML と JavaScript を置き換える

  1. index.html の内容を次のように置き換えます。
<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <div id="videos">
    <video id="localVideo" autoplay muted></video>
    <video id="remoteVideo" autoplay></video>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>
  
</body>

</html>
  1. js/main.js は、step-05/js/main.js の内容に置き換えます。

Node.js サーバーを実行する

work ディレクトリからこの Codelab に従っていない場合は、step-05 フォルダまたは現在の作業フォルダの依存関係をインストールする必要があります。

  1. 作業ディレクトリから次のコマンドを実行します。
npm install
  1. インストールした後、Node.js サーバーが実行されていない場合は、work ディレクトリで次のコマンドを実行して起動します。
node index.js

Socket.IO を実装する前のステップの index.js のバージョンを使用していることを確認します。ノードおよびソケット IO の詳細については、「メッセージを交換するためのシグナリング サービスの設定」をご覧ください。

  1. ブラウザから http://localhost:8080 にアクセスします。
  2. 新しいタブを開き、もう一度 http://localhost:8080 に移動します。

1 つの video 要素は getUserMedia() からのローカル ストリームを表示し、もう 1 つは RTCPeerconnection を介してストリーミングされたリモート動画を表示します。

  1. ブラウザ コンソールでロギングを表示します。

スコア****ボーナス ポイント

  • このアプリは 1 対 1 のビデオチャットのみをサポートしています。デザインをどう変更すれば、複数のユーザーが同じビデオチャット ルームを共有できるか。
  • この例には、ハードコードされた客室名が foo されています。他の会議室名を有効にするのに最適な方法は次のうちどれですか。
  • ユーザーは会議室名をどのように共有しますか?ルーム名を共有する代わりに、
  • アプリをどのように変更すればよいですか。

ヒント

  • chrome://webrtc-internals で WebRTC の統計情報とデバッグデータを確認できます。
  • WebRTC トラブルシューティングを使用してローカル環境を確認し、カメラとマイクをテストします。
  • キャッシュ保存に関して問題が発生した場合は、次の手順をお試しください。
  1. Control キーを押して、[このページを再読み込み] をクリックします。
  2. ブラウザを再起動します。
  3. コマンドラインから npm cache clean を実行します。

8. 写真を撮り、データチャネルで共有する

このステップの完全なバージョンは、step-06 フォルダにあります。

仕組み

以前に、RTCDataChannel を使用してテキスト メッセージを交換する方法を学習しました。この手順により、ファイル全体を共有することが可能になります。この例では、getUserMedia() で写真をキャプチャしています。

このステップの主な要素は次のとおりです。

  1. データチャネルを確立する。

この手順では、メディア ストリームをピア接続に追加しません。

  1. getUserMedia() でウェブカメラの動画ストリームをキャプチャします。
var video = document.getElementById('video');

function grabWebCamVideo() {
  console.log('Getting user media (video) ...');
  navigator.mediaDevices.getUserMedia({
    video: true
  })
  .then(gotStream)
  .catch(function(e) {
    alert('getUserMedia() error: ' + e.name);
  });
}
  1. [スナップ] をクリックして動画ストリームからスナップショット(動画フレーム)を取得し、canvas 要素に表示します。
var photo = document.getElementById('photo');
var photoContext = photo.getContext('2d');

function snapPhoto() {
  photoContext.drawImage(video, 0, 0, photo.width, photo.height);
  show(photo, sendBtn);
}
  1. [送信] をクリックして画像をバイトに変換し、データチャネルを介して送信します。
function sendPhoto() {
  // Split the data-channel message in chunks of this byte length.
  var CHUNK_LEN = 64000;
  var img = photoContext.getImageData(0, 0, photoContextW, photoContextH),
    len = img.data.byteLength,
    n = len / CHUNK_LEN | 0;

  console.log('Sending a total of ' + len + ' byte(s)');
  dataChannel.send(len);

  // Split the photo and send in chunks of approximately 64KB.
  for (var i = 0; i < n; i++) {
    var start = i * CHUNK_LEN,
      end = (i + 1) * CHUNK_LEN;
    console.log(start + ' - ' + (end - 1));
    dataChannel.send(img.data.subarray(start, end));
  }

  // Send the reminder, if applicable.
  if (len % CHUNK_LEN) {
    console.log('last ' + len % CHUNK_LEN + ' byte(s)');
    dataChannel.send(img.data.subarray(n * CHUNK_LEN));
  }
}

受信側は、データチャネル メッセージのバイトを画像に変換し、ユーザーに画像を表示します。

function receiveDataChromeFactory() {
  var buf, count;

  return function onmessage(event) {
    if (typeof event.data === 'string') {
      buf = window.buf = new Uint8ClampedArray(parseInt(event.data));
      count = 0;
      console.log('Expecting a total of ' + buf.byteLength + ' bytes');
      return;
    }

    var data = new Uint8ClampedArray(event.data);
    buf.set(data, count);

    count += data.byteLength;
    console.log('count: ' + count);

    if (count === buf.byteLength) {
      // we're done: all data chunks have been received
      console.log('Done. Rendering photo.');
      renderPhoto(buf);
    }
  };
}

function renderPhoto(data) {
  var canvas = document.createElement('canvas');
  canvas.width = photoContextW;
  canvas.height = photoContextH;
  canvas.classList.add('incomingPhoto');
  // The trail is the element that holds the incoming images.
  trail.insertBefore(canvas, trail.firstChild);

  var context = canvas.getContext('2d');
  var img = context.createImageData(photoContextW, photoContextH);
  img.data.set(data);
  context.putImageData(img, 0, 0);
}

コードを取得する

  1. work フォルダの内容を step-06 の内容に置き換えます。

workindex.html ファイルは次のようになります。**:**

<!DOCTYPE html>
<html>

<head>

  <title>Real-time communication with WebRTC</title>

  <link rel="stylesheet" href="/css/main.css" />

</head>

<body>

  <h1>Real-time communication with WebRTC</h1>

  <h2>
    <span>Room URL: </span><span id="url">...</span>
  </h2>

  <div id="videoCanvas">
    <video id="camera" autoplay></video>
    <canvas id="photo"></canvas>
  </div>

  <div id="buttons">
    <button id="snap">Snap</button><span> then </span><button id="send">Send</button>
    <span> or </span>
    <button id="snapAndSend">Snap &amp; Send</button>
  </div>

  <div id="incoming">
    <h2>Incoming photos</h2>
    <div id="trail"></div>
  </div>

  <script src="/socket.io/socket.io.js"></script>
  <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
  <script src="js/main.js"></script>

</body>

</html>
  1. work ディレクトリからこの Codelab に従っていない場合は、step-06 フォルダまたは現在の作業フォルダの依存関係をインストールする必要があります。まず、作業ディレクトリから次のコマンドを実行します。
npm install
  1. インストールした後、Node.js サーバーが実行されていない場合は、work ディレクトリで次のコマンドを実行して起動します。
node index.js
    Make sure that you're using the version of `index.js` that implements Socket.IO and 

変更した場合は、必ず Node.js サーバーを再起動してください。

Node と Socket.IO の詳細については、シグナリングを設定する

サービス間でメッセージをやり取りできます。

  1. 必要に応じて、[許可] をクリックして、アプリにウェブカメラの使用を許可します。

アプリがランダムなチャットルーム ID を作成し、その ID を URL に追加します。

  1. ブラウザの新しいタブまたはウィンドウでアドレスバーの URL を開きます。
  2. [スナップ &送信] をクリックし、ページ下部のもう 1 つのタブにある [写真の受信] を確認します。

アプリは、タブ間で写真を転送します。

次のように表示されます。

103b40f36ba6ba8.png

ボーナス ポイントを獲得

どのような形式のファイルでも共有できるようにするには、どのようにコードを変更すればよいでしょうか。

詳細

9. 完了

リアルタイムの動画ストリーミングとデータ交換用のアプリを作成しました。

詳細