1. はじめに
WebRTC は、ウェブアプリとネイティブ アプリで音声、動画、データのリアルタイム通信を可能にするオープンソース プロジェクトです。
WebRTC には複数の JavaScript API があります。リンクをクリックしてデモをご覧ください。
getUserMedia(): 音声と動画をキャプチャします。MediaRecorder: 音声と動画を録画します。RTCPeerConnection: ユーザー間で音声と動画をストリーミングします。RTCDataChannel: ユーザー間でデータをストリーミングします。
WebRTC はどこで使用できますか?
Firefox、Opera、パソコン版 Chrome、Android 版 Chrome で利用できます。WebRTC は iOS と Android のネイティブ アプリでも利用できます。
シグナリングとは
WebRTC は RTCPeerConnection を使用してブラウザ間でストリーミング データを通信しますが、通信を調整して制御メッセージを送信するメカニズム(シグナリングと呼ばれるプロセス)も必要です。シグナリング方法とプロトコルは WebRTC で指定されていません。この Codelab ではメッセージングに Socket.IO を使用しますが、多くの代替手段があります。
STUN と TURN とは何ですか?
WebRTC はピアツーピアで動作するように設計されているため、ユーザーは可能な限り直接的なルートで接続できます。ただし、WebRTC は実際のネットワーキングに対応するように構築されています。クライアント アプリケーションは NAT ゲートウェイとファイアウォールを通過する必要があり、ピアツーピア ネットワーキングでは直接接続が失敗した場合のフォールバックが必要です。このプロセスの一環として、WebRTC API は STUN サーバーを使用してパソコンの IP アドレスを取得し、ピアツーピア通信が失敗した場合にリレーサーバーとして機能する TURN サーバーを使用します。(詳しくは、現実世界の WebRTC をご覧ください)。
WebRTC は安全ですか?
暗号化はすべての WebRTC コンポーネントで必須であり、その JavaScript API は安全なオリジン(HTTPS または localhost)からのみ使用できます。シグナリング メカニズムは WebRTC 標準で定義されていないため、安全なプロトコルを使用するようにしてください。
2. 概要
ウェブカメラで動画を取得してスナップショットを撮影し、WebRTC 経由でピアツーピアで共有するアプリを構築します。この過程で、コア WebRTC API の使用方法と、Node.js を使用したメッセージング サーバーの設定方法を学習します。
学習内容
- ウェブカメラから動画を取得する
- RTCPeerConnection を使用して動画をストリーミングする
- RTCDataChannel を使用してデータをストリーミングする
- メッセージを交換するためのシグナリング サービスを設定する
- ピア接続とシグナリングを組み合わせる
- 写真を撮影してデータチャンネル経由で共有する
必要なもの
- Chrome 47 以降
- Web Server for Chrome、または任意のウェブサーバーを使用します。
- サンプルコード
- テキスト エディタ
- HTML、CSS、JavaScript に関する基礎的な知識
3. サンプルコードを取得する
コードをダウンロードする
git に精通している場合は、GitHub からこの Codelab のコードをクローンしてダウンロードできます。
git clone https://github.com/googlecodelabs/webrtc-web
または、次のボタンをクリックしてコードの .zip ファイルをダウンロードします。
ダウンロードした ZIP ファイルを開きます。プロジェクト フォルダ(adaptive-web-media)が展開されます。このフォルダには、この Codelab のステップごとに 1 つのフォルダと、必要なすべてのリソースが含まれています。
コーディング作業はすべて、work という名前のディレクトリで行います。
step-nn フォルダには、この Codelab の各ステップの完成版が含まれています。これらは参照用に用意されています。
ウェブサーバーをインストールして確認する
独自のウェブサーバーを自由に使用できますが、この Codelab は Chrome ウェブサーバーでうまく動作するように設計されています。目的のアプリをまだインストールしていない場合は、Chrome ウェブストアからインストールできます。

Web Server for Chrome アプリをインストールしたら、ブックマーク バー、新しいタブページ、またはアプリ ランチャーから Chrome アプリのショートカットをクリックします。

ウェブサーバー アイコンをクリックします。

次に、ローカル ウェブサーバーを構成できる次のダイアログが表示されます。

[フォルダを選択] ボタンをクリックし、作成した work フォルダを選択します。これにより、[ウェブサーバーの URL] セクションのウェブサーバー ダイアログでハイライト表示されている URL を使用して、Chrome で処理中の作業を表示できます。
[オプション] で、次の図のように [index.html を自動的に表示] の横にあるチェックボックスをオンにします。

次に、[ウェブサーバー: 開始] と表示された切り替えボタンを左にスライドしてから右に戻して、サーバーを停止して再起動します。

ハイライト表示されたウェブサーバーの URL をクリックして、ウェブブラウザで作業サイトにアクセスします。次のようなページが表示されます。これは work/index.html に対応しています。

このアプリはまだ何も面白いことをしていません。今のところ、ウェブサーバーが正しく動作していることを確認するために使用する最小限のスケルトンにすぎません。機能とレイアウト機能は、後続のステップで追加します。
4. ウェブカメラから動画をストリーミングする
学習内容
このステップでは、次の方法について説明します。
- ウェブカメラから動画ストリームを取得します。
- ストリームの再生を操作します。
- CSS と SVG を使用して動画を操作します。
このステップの完全なバージョンは step-01 フォルダにあります。
HTML を少しだけ...
work ディレクトリの index.html に video 要素と script 要素を追加します。
<!DOCTYPE html>
<html>
<head>
<title>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Realtime communication with WebRTC</h1>
<video autoplay playsinline></video>
<script src="js/main.js"></script>
</body>
</html>
...そして JavaScript を少し
js フォルダの main.js に以下を追加します。
'use strict';
// On this codelab, you will be streaming only video (video: true).
const mediaStreamConstraints = {
video: true,
};
// Video element where stream will be placed.
const localVideo = document.querySelector('video');
// Local stream that will be reproduced on the video.
let localStream;
// Handles success by adding the MediaStream to the video element.
function gotLocalMediaStream(mediaStream) {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
}
// Handles error by logging a message to the console with the error message.
function handleLocalMediaStreamError(error) {
console.log('navigator.getUserMedia error: ', error);
}
// Initializes media stream.
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
試してみる
ブラウザで index.html を開くと、次のような画面が表示されます(もちろん、ウェブカメラの映像も表示されます)。

仕組み
getUserMedia() 呼び出しの後、ブラウザはユーザーにカメラへのアクセス権限をリクエストします(現在のオリジンでカメラへのアクセスがリクエストされるのが初めての場合)。成功すると、MediaStream が返されます。これは、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」と入力して Return キーを押します。(Chrome でコンソールを表示するには、Ctrl+Shift+J キーを押します。Mac の場合は Command+Option+J キーを押します)。localStream.getVideoTracks()は何を返しますか?localStream.getVideoTracks()[0].stop()に電話をかけてみてください。- 制約オブジェクトを見てください。これを
{audio: true, video: true}に変更するとどうなりますか? - 動画要素のサイズはどのくらいですか?表示サイズではなく、動画の本来のサイズを JavaScript で取得するにはどうすればよいですか?Chrome Dev Tools を使用して確認します。
- 動画要素に CSS フィルタを追加してみてください。次に例を示します。
video {
filter: blur(4px) invert(1) opacity(0.5);
}
- SVG フィルタを追加してみましょう。次に例を示します。
video {
filter: hue-rotate(180deg) saturate(200%);
}
学習した内容
このステップでは、次のことを学習しました。
- ウェブカメラから動画を取得します。
- メディアの制約を設定します。
- 動画要素を操作します。
このステップの完全なバージョンは step-01 フォルダにあります。
ヒント
video要素のautoplay属性を忘れないでください。これがないと、1 つのフレームしか表示されません。getUserMedia()制約には他にも多くのオプションがあります。webrtc.github.io/samples/src/content/peerconnection/constraints でデモをご覧ください。このサイトには、興味深い WebRTC のサンプルが多数あります。
ベスト プラクティス
- 動画要素がコンテナからオーバーフローしないようにします。動画の推奨サイズと最大サイズを設定するための
widthとmax-widthが追加されました。ブラウザは高さを自動的に計算します。
video {
max-width: 100%;
width: 320px;
}
次のステップ
動画は用意できましたが、どのようにストリーミングすればよいでしょうか?次の手順で確認しましょう。
5. RTCPeerConnection を使用して動画をストリーミングする
学習内容
このステップでは、次の方法について説明します。
- WebRTC shim の adapter.js を使用して、ブラウザ間の違いを抽象化します。
- RTCPeerConnection API を使用して動画をストリーミングします。
- メディアのキャプチャとストリーミングを制御します。
このステップの完全なバージョンは step-2 フォルダにあります。
RTCPeerConnection とは
RTCPeerConnection は、WebRTC 呼び出しを行って動画と音声をストリーミングし、データを交換するための API です。
この例では、同じページにある 2 つの RTCPeerConnection オブジェクト(ピア)間の接続を設定します。
実用性はあまりありませんが、RTCPeerConnection の仕組みを理解するのに役立ちます。
動画要素とコントロール ボタンを追加する
index.html で、単一の動画要素を 2 つの動画要素と 3 つのボタンに置き換えます。
<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 つの動画要素がローカル ストリームを表示し、もう 1 つがリモート ストリームを表示します)。
adapter.js シムを追加する
main.js へのリンクの上に、adapter.js の現在のバージョンへのリンクを追加します。
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
Index.html は次のようになります。
<!DOCTYPE html>
<html>
<head>
<title>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Realtime 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 フォルダのバージョンに置き換えます。
電話をかける
index.html を開き、[Start] ボタンをクリックしてウェブカメラから動画を取得し、[Call] をクリックしてピア接続を確立します。両方の動画要素に同じ動画(ウェブカメラからの動画)が表示されます。ブラウザ コンソールを表示して、WebRTC ロギングを確認します。
仕組み
このステップでは、多くの処理が行われます。
WebRTC は RTCPeerConnection API を使用して、ピアと呼ばれる WebRTC クライアント間で動画をストリーミングするための接続を設定します。
この例では、2 つの RTCPeerConnection オブジェクト(pc1 と pc2)が同じページにあります。実用性はあまりありませんが、API の仕組みを示すには適しています。
WebRTC ピア間の通話の設定には、次の 3 つのタスクがあります。
- 通話の各エンドポイントに RTCPeerConnection を作成し、各エンドポイントで
getUserMedia()からローカル ストリームを追加します。 - ネットワーク情報を取得して共有する: 接続候補のエンドポイントは ICE 候補と呼ばれます。
- ローカルとリモートの説明を取得して共有する: SDP 形式のローカル メディアに関するメタデータ。
Alice と Bob が RTCPeerConnection を使用してビデオチャットを設定するとします。
まず、アリスとボブがネットワーク情報を交換します。「候補の検索」という表現は、ICE フレームワークを使用してネットワーク インターフェースとポートを検索するプロセスを指します。
- Alice は
onicecandidate (addEventListener('icecandidate'))ハンドラを使用して RTCPeerConnection オブジェクトを作成します。これは、main.js の次のコードに対応しています。
let localPeerConnection;
localPeerConnection = new RTCPeerConnection(servers);
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
'iceconnectionstatechange', handleConnectionChange);
- 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ハンドラが呼び出されます。 - Alice はシリアル化された候補データを Bob に送信します。実際のアプリケーションでは、このプロセス(シグナリングと呼ばれます)はメッセージング サービスを介して行われます。その方法については、後のステップで説明します。もちろん、このステップでは、2 つの RTCPeerConnection オブジェクトは同じページにあり、外部メッセージングを必要とせずに直接通信できます。
- Bob が Alice から候補メッセージを受け取ると、
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 を交換することで行われます。
- Alice は RTCPeerConnection
createOffer()メソッドを実行します。返された Promise は、アリスのローカル セッションの説明である RTCSessionDescription を提供します。
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
- 成功すると、アリスは
setLocalDescription()を使用してローカルの説明を設定し、シグナリング チャネルを介してこのセッションの説明をボブに送信します。 - Bob は、
setRemoteDescription()を使用して、Alice から送信された説明をリモートの説明として設定します。 - Bob は RTCPeerConnection
createAnswer()メソッドを実行し、Alice から取得したリモート記述を渡します。これにより、Alice の記述と互換性のあるローカル セッションを生成できます。createAnswer()プロミスは RTCSessionDescription を渡します。Bob はそれをローカルの説明として設定し、Alice に送信します。 - Alice は Bob のセッションの説明を取得すると、
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);
}
- Ping!
ボーナス ポイント
- chrome://webrtc-internals をご覧ください。これにより、WebRTC の統計情報とデバッグデータが提供されます。(Chrome URL の完全なリストは chrome://about で確認できます)。
- CSS でページをスタイル設定します。
- 動画を並べて表示します。
- ボタンの幅を同じにし、テキストを大きくします。
- レイアウトがモバイルで機能することを確認します。
- Chrome デベロッパー ツールのコンソールで、
localStream、localPeerConnection、remotePeerConnectionを確認します。 - コンソールで
localPeerConnectionpc1.localDescriptionを確認します。SDP 形式はどのようなものですか?
学習した内容
このステップでは、次のことを学習しました。
- WebRTC shim の adapter.js を使用して、ブラウザ間の違いを抽象化します。
- RTCPeerConnection API を使用して動画をストリーミングします。
- メディアのキャプチャとストリーミングを制御します。
- ピア間でメディアとネットワーク情報を共有して、WebRTC 通話を有効にします。
このステップの完全なバージョンは step-2 フォルダにあります。
ヒント
- このステップでは、多くのことを学習します。RTCPeerConnection について詳しく説明している他のリソースについては、webrtc.org をご覧ください。このページでは、WebRTC を使用したいが API を操作したくない場合に役立つ JavaScript フレームワークの提案を紹介します。
- adapter.js シムについて詳しくは、adapter.js GitHub リポジトリをご覧ください。
- 世界最高のビデオチャット アプリがどのようなものか見てみませんか?WebRTC 通話用の WebRTC プロジェクトの標準アプリである AppRTC をご覧ください。アプリ、コード。通話のセットアップ時間が 500 ミリ秒未満である。
ベスト プラクティス
- コードを将来にわたって使用できるようにするには、新しい Promise ベースの API を使用し、adapter.js を使用して、それらをサポートしていないブラウザとの互換性を有効にします。
次のステップ
このステップでは、WebRTC を使用してピア間で動画をストリーミングする方法を示しますが、この Codelab はデータに関するものでもあります。
次のステップでは、RTCDataChannel を使用して任意のデータをストリーミングする方法について説明します。
6. RTCDataChannel を使用してデータを交換する
学習内容
- WebRTC エンドポイント(ピア)間でデータを交換する方法。
このステップの完全なバージョンは step-03 フォルダにあります。
HTML を更新する
このステップでは、WebRTC データチャンネルを使用して、同じページの 2 つの textarea 要素間でテキストを送信します。これはあまり役に立ちませんが、WebRTC を使用して動画をストリーミングするだけでなく、データを共有する方法も示しています。
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>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Realtime 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 を更新する
main.js を step-03/js/main.js の内容に置き換えます。
ピア間でストリーミング データを試す: index.html を開き、[Start] を押してピア接続を設定し、左側の textarea にテキストを入力して、[Send] をクリックして WebRTC データチャネルを使用してテキストを転送します。
仕組み
このコードでは、RTCPeerConnection と RTCDataChannel を使用してテキスト メッセージの交換を可能にしています。
このステップのコードの多くは、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 の使用に注意してください。データチャネルは、さまざまな種類のデータ共有を有効にするように構成できます。たとえば、パフォーマンスよりも信頼性の高い配信を優先するなどです。オプションの詳細については、Mozilla Developer Network をご覧ください。
ボーナス ポイント
- WebRTC データチャネルで使用されるプロトコルである SCTP では、信頼性の高い順序付きデータ配信がデフォルトで有効になっています。RTCDataChannel でデータの信頼性の高い配信が必要になるのはどのような場合ですか?また、パフォーマンスがより重要になるのはどのような場合ですか?(たとえ一部のデータが失われるとしても)
- CSS を使用してページ レイアウトを改善し、「dataChannelReceive」テキストエリアにプレースホルダ属性を追加します。
- モバイル デバイスでページをテストします。
学習した内容
このステップでは、次のことを学習しました。
- 2 つの WebRTC ピア間の接続を確立します。
- ピア間でテキストデータを交換します。
このステップの完全なバージョンは step-03 フォルダにあります。
詳細
- WebRTC データチャネル(数年前のものですが、まだ読む価値があります)
- WebRTC のデータチャネルに SCTP が選択されたのはなぜですか?
次のステップ
同じページのピア間でデータを交換する方法は学びましたが、異なるマシン間でデータを交換するにはどうすればよいでしょうか?まず、メタデータ メッセージを交換するためのシグナリング チャネルを設定する必要があります。次のステップでその方法をご紹介します。
7. メッセージを交換するためのシグナリング サービスを設定する
学習内容
このステップでは、次の方法について説明します。
npmを使用して、package.json で指定されたプロジェクトの依存関係をインストールします。- Node.js サーバーを実行し、node-static を使用して静的ファイルを提供します。
- Socket.IO を使用して Node.js でメッセージング サービスを設定します。
- これを使用して「チャットルーム」を作成し、メッセージを交換します。
このステップの完全なバージョンは step-04 フォルダにあります。
コンセプト
WebRTC 通話を確立して維持するには、WebRTC クライアント(ピア)がメタデータを交換する必要があります。
- 候補(ネットワーク)情報。
- 解像度やコーデックなどのメディアに関する情報を提供する offer メッセージと answer メッセージ。
つまり、音声、動画、データのピアツーピア ストリーミングを行う前に、メタデータの交換が必要になります。このプロセスは「シグナリング」と呼ばれます。
前の手順では、送信側と受信側の RTCPeerConnection オブジェクトが同じページにあるため、シグナリングはオブジェクト間でメタデータを渡すだけの処理です。
実際のアプリケーションでは、送信側と受信側の RTCPeerConnection は異なるデバイスのウェブページで実行されるため、メタデータを通信する方法が必要です。
このために、WebRTC クライアント(ピア)間でメッセージを渡すことができるサーバーであるシグナリング サーバーを使用します。実際のメッセージは、文字列化された JavaScript オブジェクトであるプレーン テキストです。
前提条件: Node.js をインストールする
この Codelab の次の手順(フォルダ step-04 から step-06)を実行するには、Node.js を使用して localhost でサーバーを実行する必要があります。
Node.js は、こちらのリンクからダウンロードしてインストールするか、任意のパッケージ マネージャーからインストールできます。
インストールが完了すると、次のステップ(npm install の実行)に必要な依存関係をインポートできるようになります。また、小さな localhost サーバーを実行して、Codelab を実行することもできます(node index.js の実行)。これらのコマンドは、必要になったときに後で示します。
アプリについて
WebRTC はクライアントサイドの JavaScript API を使用しますが、実際の使用ではシグナリング(メッセージング)サーバーと STUN サーバー、TURN サーバーも必要です。詳しくは、こちらをご覧ください。
このステップでは、Socket.IO Node.js モジュールとメッセージング用の JavaScript ライブラリを使用して、シンプルな Node.js シグナリング サーバーをビルドします。Node.js と Socket.IO の経験があると役立ちますが、必須ではありません。メッセージング コンポーネントは非常にシンプルです。
この例では、サーバー(Node.js アプリケーション)は index.js で実装され、その上で実行されるクライアント(ウェブアプリ)は index.html で実装されています。
このステップの Node.js アプリケーションには 2 つのタスクがあります。
まず、メッセージ リレーとして機能します。
socket.on('message', function (message) {
log('Got message: ', message);
socket.broadcast.emit('message', message);
});
第 2 に、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
index.html を次のように更新します。
<!DOCTYPE html>
<html>
<head>
<title>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<h1>Realtime communication with WebRTC</h1>
<script src="/socket.io/socket.io.js"></script>
<script src="js/main.js"></script>
</body>
</html>
このステップでは、ページに何も表示されません。すべてのロギングはブラウザ コンソールで行われます。(Chrome でコンソールを表示するには、Ctrl+Shift+J キーを押します。Mac の場合は Command+Option+J キーを押します)。
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>
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)に伝えるアプリ マニフェストです。
依存関係(/socket.io/socket.io.js など)をインストールするには、work ディレクトリのコマンドライン ターミナルから次のコマンドを実行します。
npm install
次のようなインストール ログが表示されます。

ご覧のとおり、npm は package.json で定義された依存関係をインストールしました。
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);
}
});
}
});
});
コマンドライン ターミナルで、work ディレクトリに次のコマンドを実行します。
node index.js
ブラウザで localhost:8080 を開きます。
この URL を開くたびに、ルーム名の入力を求められます。同じ部屋に参加するには、毎回同じ部屋名(「foo」など)を選択します。
新しいタブページを開き、localhost:8080 を再度開きます。同じチャットルーム名を選択します。
3 つ目のタブまたはウィンドウで localhost:8080 を開きます。同じチャットルーム名をもう一度選択します。
各タブのコンソールを確認します。上記の JavaScript からのロギングが表示されます。
ボーナス ポイント
- 代替のメッセージ メカニズムとして、どのようなものが考えられますか?「純粋な」WebSocket を使用する際に発生する可能性のある問題は何ですか?
- このアプリケーションのスケーリングに関連する問題として考えられるのはどれですか?数千件、数百万件のルーム リクエストを同時にテストする方法を開発できますか?
- このアプリは、JavaScript プロンプトを使用してチャットルーム名を取得します。URL からチャットルーム名を取得する方法を考えます。たとえば、localhost:8080/foo の場合、ルーム名は
fooになります。
学習した内容
このステップでは、次の方法について学習しました。
- npm を使用して、package.json で指定されたプロジェクトの依存関係をインストールする
- Node.js サーバーを実行して静的ファイルを提供します。
- socket.io を使用して Node.js でメッセージング サービスを設定します。
- これを使用して「チャットルーム」を作成し、メッセージを交換します。
このステップの完全なバージョンは step-04 フォルダにあります。
詳細
次のステップ
シグナリングを使用して 2 人のユーザーがピア接続を確立する方法について説明します。
8. ピア接続とシグナリングを組み合わせる
学習内容
このステップでは、次の方法について説明します。
- Node.js で実行されている Socket.IO を使用して WebRTC シグナリング サービスを実行する
- このサービスを使用して、ピア間で WebRTC メタデータを交換します。
このステップの完全なバージョンは step-05 フォルダにあります。
HTML と JavaScript を置き換える
index.html の内容を次のように置き換えます。
<!DOCTYPE html>
<html>
<head>
<title>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="/css/main.css" />
</head>
<body>
<h1>Realtime 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>
js/main.js を step-05/js/main.js の内容に置き換えます。
Node.js サーバーを実行する
work ディレクトリからこの Codelab を実行していない場合は、step-05 フォルダまたは現在の作業フォルダの依存関係をインストールする必要があります。作業ディレクトリから次のコマンドを実行します。
npm install
インストールが完了したら、Node.js サーバーが実行されていない場合は、work ディレクトリで次のコマンドを呼び出して起動します。
node index.js
Socket.IO を実装する前のステップの index.js のバージョンを使用していることを確認します。Node と Socket IO について詳しくは、「メッセージを交換するためのシグナリング サービスを設定する」をご覧ください。
ブラウザで localhost:8080 を開きます。
新しいタブまたはウィンドウで localhost:8080 を再度開きます。1 つの動画要素には getUserMedia() からのローカル ストリームが表示され、もう 1 つには RTCPeerconnection を介してストリーミングされた「リモート」動画が表示されます。
ブラウザ コンソールでロギングを表示します。
ボーナス ポイント
- このアプリは 1 対 1 のビデオチャットのみをサポートしています。複数のユーザーが同じビデオチャット ルームを共有できるようにするには、どのように設計を変更しますか?
- この例では、部屋名 foo がハードコードされています。他の部屋の名前を有効にするには、どのような方法が最適ですか?
- ユーザーはどのようにしてルーム名を共有しますか?会議室名を共有する代替手段を構築してみてください。
- アプリをどのように変更できますか?
学習した内容
このステップでは、次のことを学習しました。
- Node.js で実行されている Socket.IO を使用して WebRTC シグナリング サービスを実行します。
- このサービスを使用して、ピア間で WebRTC メタデータを交換します。
このステップの完全なバージョンは step-05 フォルダにあります。
ヒント
- WebRTC の統計情報とデバッグデータは chrome://webrtc-internals で確認できます。
- test.webrtc.org を使用して、ローカル環境を確認し、カメラとマイクをテストできます。
- キャッシュ保存で問題が発生した場合は、次の手順をお試しください。
- Ctrl キーを押したまま再読み込みボタンをクリックして、ハード更新を行います。
- ブラウザを再起動する
- コマンドラインから
npm cache cleanを実行します。
次のステップ
写真を撮り、画像データを取得して、リモート ピア間で共有する方法について説明します。
9. 写真を撮影してデータチャンネル経由で共有する
学習内容
このステップでは、次の方法について説明します。
- 写真を撮り、キャンバス要素を使用して写真からデータを取得します。
- リモート ユーザーと画像データを交換します。
このステップの完全なバージョンは step-06 フォルダにあります。
仕組み
これまでに、RTCDataChannel を使用してテキスト メッセージを交換する方法を学びました。
このステップにより、ファイル全体(この例では getUserMedia() でキャプチャされた写真)を共有できるようになります。
このステップの主な部分は次のとおりです。
- データチャネルを確立します。この手順では、ピア接続にメディア ストリームを追加しません。
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);
});
}
- ユーザーが [Snap] ボタンをクリックしたら、動画ストリームからスナップショット(動画フレーム)を取得し、
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);
}
- ユーザーが [送信] ボタンをクリックしたら、画像をバイトに変換してデータチャンネル経由で送信します。
function sendPhoto() {
// Split 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 about 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 any
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');
// trail is the element holding 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);
}
コードを取得する
work フォルダの内容を step-06 の内容に置き換えます。work の index.html ファイルは次のようになります。
<!DOCTYPE html>
<html>
<head>
<title>Realtime communication with WebRTC</title>
<link rel="stylesheet" href="/css/main.css" />
</head>
<body>
<h1>Realtime 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 & 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>
work ディレクトリからこの Codelab を実行していない場合は、step-06 フォルダまたは現在の作業フォルダの依存関係をインストールする必要があります。作業ディレクトリから次のコマンドを実行するだけです。
npm install
インストールが完了したら、Node.js サーバーが実行されていない場合は、work ディレクトリから次のコマンドを呼び出して起動します。
node index.js
Socket.IO を実装する index.js のバージョンを使用していることを確認し、変更を加えた場合は Node.js サーバーを再起動してください。Node と Socket IO について詳しくは、「メッセージを交換するためのシグナリング サービスを設定する」をご覧ください。
必要に応じて、[許可] ボタンをクリックして、アプリにウェブカメラの使用を許可します。
アプリはランダムなルーム ID を作成し、その ID を URL に追加します。アドレスバーの URL を新しいブラウザタブまたはウィンドウで開きます。
[Snap & Send] ボタンをクリックし、ページ下部の別のタブの [Incoming] 領域を確認します。アプリがタブ間で写真を移動します。
次のように表示されます。

ボーナス ポイント
- あらゆるファイル形式を共有できるようにコードを変更するにはどうすればよいですか?
詳細
- MediaStream Image Capture API: 写真を撮影してカメラを制御するための API。まもなくブラウザに登場します。
- 音声と動画を録画するための MediaRecorder API: デモ、ドキュメント。
学習した内容
- canvas 要素を使用して写真を撮影し、その写真からデータを取得する方法。
- そのデータをリモート ユーザーと交換する方法。
このステップの完全なバージョンは step-06 フォルダにあります。
10. 完了
リアルタイムの動画ストリーミングとデータ交換を行うアプリを構築しました。
学習した内容
この Codelab では、以下について学びました。
- ウェブカメラから動画を取得します。
- RTCPeerConnection を使用して動画をストリーミングします。
- RTCDataChannel を使用してデータをストリーミングします。
- メッセージを交換するためのシグナリング サービスを設定します。
- ピア接続とシグナリングを組み合わせます。
- 写真を撮影してデータチャンネル経由で共有します。
次のステップ
- 標準的な WebRTC チャット アプリケーション AppRTC のコードとアーキテクチャ(app、code)をご覧ください。
- github.com/webrtc/samples のライブデモをお試しください。
その他の情報
- WebRTC を始めるためのさまざまなリソースが webrtc.org で提供されています。