1. はじめに
WebRTC は、ウェブアプリとネイティブ アプリで音声、動画、データのリアルタイム通信を可能にするオープンソース プロジェクトです。
WebRTC には複数の JavaScript API があります。リンクをクリックしてデモをご覧ください。
getUserMedia()
: 音声と動画をキャプチャします。MediaRecorder
: 音声と動画を記録します。RTCPeerConnection
: ユーザー間で音声と動画をストリーミングします。RTCDataChannel
: ユーザー間でデータをストリーミングします。
WebRTC はどこで使用できますか?
Firefox、Opera、Chrome(パソコンおよび Android)で利用できます。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 以降
- Chrome 用のウェブサーバーを使用するか、独自のウェブサーバーを使用します。
- サンプルコード
- テキスト エディタ
- HTML、CSS、JavaScript に関する基本的な知識
3. サンプルコードを取得する
コードをダウンロードする
git に精通している場合は、この Codelab のコードを GitHub からクローンを作成してダウンロードできます。
git clone https://github.com/googlecodelabs/webrtc-web
または、次のボタンをクリックしてコードの .zip ファイルをダウンロードします。
ダウンロードした zip ファイルを開きます。これにより、プロジェクト フォルダ(adaptive-web-media)が展開されます。このフォルダには、この Codelab のステップごとに 1 つのフォルダと必要なリソースがすべて含まれています。
コーディング作業はすべて work という名前のディレクトリで行われます。
step-nn フォルダには、この Codelab の各ステップの完成版が含まれています。これらは参照用に用意されています。
ウェブサーバーをインストールして確認する
独自のウェブサーバーは自由に使用できますが、この Codelab は Chrome ウェブサーバーと適切に連携するように設計されています。アプリをまだインストールしていない場合は、Chrome ウェブストアからインストールできます。
Chrome 用ウェブサーバー アプリをインストールしたら、ブックマーク バー、新しいタブページ、アプリ ランチャーから Chrome アプリのショートカットをクリックします。
ウェブサーバーのアイコンをクリックします。
次に表示されるダイアログで、ローカル ウェブサーバーを構成できます。
[フォルダを選択] ボタンをクリックして、作成した work フォルダを選択します。これにより、[ウェブサーバー] ダイアログの [ウェブサーバーの URL] セクションでハイライト表示されている URL を使用して、Chrome で進行中の作業を確認できるようになります。
[オプション] で、[index.html を自動的に表示] の横にあるチェックボックスをオンにします(下図を参照)。
次に、[Web Server: STARTED] というトグルを左にスライドしてから右に戻し、サーバーを停止して再起動します。
ウェブブラウザで、ハイライト表示されているウェブサーバーの URL をクリックして、職場のサイトにアクセスします。work/index.html に対応する次のようなページが表示されます。
明らかに、このアプリはまだ何か面白いことをしているわけではありません。今のところは、ウェブサーバーが正常に動作していることを確認するための最小限の枠組みにすぎません。機能とレイアウト機能は後続のステップで追加します。
4. ウェブカメラから動画をストリーミングする
学習内容
このステップでは、次の方法について説明します。
- ウェブカメラで動画ストリームを取得します。
- ストリーム再生を操作する。
- CSS や SVG を使用して動画を操作できます。
このステップの完全なバージョンは、step-01 フォルダにあります。
HTML のダッシュ
video
要素と script
要素を 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>
<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 で、1 つの動画要素を 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 つの動画要素にリモート ストリームが表示されます)。
Adapt.js shim を追加する
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 を開き、[開始] ボタンをクリックしてウェブカメラから動画を取得し、[通話] をクリックしてピア接続を確立します。両方の動画要素に(自分のウェブカメラから)同じ動画が表示されます。WebRTC ログを確認するには、ブラウザ コンソールを表示します。
仕組み
このステップでは多くのことが行われます...
WebRTC は RTCPeerConnection API を使用して、WebRTC クライアント間で動画をストリーミングするための接続(ピア)を設定します。
この例では、2 つの RTCPeerConnection オブジェクト(pc1
と pc2
)は同じページにあります。あまり実用的ではありませんが、API の仕組みを示すのに適しています。
WebRTC ピア間の呼び出しを設定するには、次の 3 つのタスクを行います。
- 呼び出しの両端に RTCPeerConnection を作成し、それぞれの端で
getUserMedia()
からのローカル ストリームを追加します。 - ネットワーク情報の取得と共有: 潜在的な接続エンドポイントは ICEICE の候補として知られています。
- ローカルとリモートの説明(SDP 形式のローカル メディアに関するメタデータ)を取得して共有します。
アリスとボブが 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 ピアは、解像度やコーデック機能など、ローカルとリモートの音声と動画のメディア情報を検索し、交換する必要があります。メディア構成情報を交換するためのシグナリングは、オファーと応答と呼ばれるメタデータの blob を、SDP と呼ばれるセッション記述プロトコル形式を使用して交換することによって行われます。
- Alice は RTCPeerConnection の
createOffer()
メソッドを実行します。返される Promise は、RTCSessionDescription: Alice のローカル セッションの説明:
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
- 成功した場合、Alice は
setLocalDescription()
を使用してローカルの説明を設定し、このセッションの説明をシグナリング チャネル経由で Bob に送信します。 - Bob は、
setRemoteDescription()
を使用して、Alice が送信した説明をリモートの説明として設定します。 - ユーザー A は RTCPeerConnection の
createAnswer()
メソッドを実行し、Alice から取得したリモートの説明を渡します。これにより、Alice のセッションに対応したローカル セッションを生成できます。createAnswer()
Promise が RTCSessionDescription を渡します。ボブはそれをローカルな説明として設定し、それを 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);
}
- ping!
ボーナス ポイント
- chrome://webrtc-internals をご覧ください。WebRTC の統計情報とデバッグデータが提供されます。(Chrome の URL 一覧は chrome://about でご覧いただけます)。
- CSS でページのスタイルを設定します。
- 動画を横並びで並べます。
- ボタンの幅を同じにして、文字を大きくします。
- レイアウトがモバイルでも機能することを確認します。
- Chrome Dev Tools コンソールから、
localStream
、localPeerConnection
、remotePeerConnection
を確認します。 - コンソールから
localPeerConnectionpc1.localDescription
を確認します。SDP の形式
学習した内容
このステップでは、以下の方法を学びました。
- WebRTC shim(adapter.js)を使用してブラウザの違いを抽象化します。
- RTCPeerConnection API を使用して動画をストリーミングします。
- メディアのキャプチャとストリーミングをコントロールする。
- ピア間でメディアとネットワーク情報を共有して、WebRTC 呼び出しを可能にします。
このステップの完全なバージョンは、step-2 フォルダにあります。
ヒント
- このステップでは学ぶべきことがたくさんあります。RTCPeerConnection について詳しく説明しているその他のリソースについては、webrtc.org をご覧ください。このページでは、WebRTC を使用しながら API をラングリングしたくない場合は、JavaScript フレームワークに関する推奨事項を紹介します。
- Adapter.js shim について詳しくは、adapter.js GitHub リポジトリをご覧ください。
- 世界最高のビデオチャット アプリとはどのようなものか、チェックしてみましょう。WebRTC 呼び出し用の WebRTC プロジェクトの正規アプリである AppRTC(app、code)に注目してみましょう。通話のセットアップ時間が 500 ミリ秒未満。
ベスト プラクティス
- コードの将来の変化にも対応するには、新しい Promise ベースの API を使用し、adapter.js を使用して、この API に対応していないブラウザとの互換性を有効にします。
次のステップ
このステップでは、WebRTC を使用してピア間で動画をストリーミングする方法について説明しますが、この Codelab もデータに関するものです。
次のステップでは、RTCDataChannel を使用して任意のデータをストリーミングする方法を確認します。
6. RTCDataChannel を使用してデータを交換する
学習内容
- WebRTC エンドポイント(ピア)間でデータを交換する方法。
このステップの完全なバージョンは、step-03 フォルダにあります。
HTML を更新する
このステップでは、WebRTC データチャネルを使用して、同じページ上の 2 つの textarea
要素間でテキストを送信します。これはあまり有用ではありませんが、WebRTC を使用してデータの共有や動画のストリーミングにどのように使用できるかを示しています。
index.html から video 要素と button 要素を削除し、次の 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>
一方のテキスト領域はテキストを入力するためのもので、もう一方はピア間でストリーミングされたテキストを表示します。
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 をご覧ください。
ボーナス ポイント
- SCTP では、WebRTC データ チャネルで使用される、信頼性が高く順序付けされたデータ配信がデフォルトでオンになっています。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 クライアント(ピア)がメタデータを交換する必要があります。
- 受験者(ネットワーク)情報。
- 解像度やコーデックなどのメディアに関する情報を提供するオファー メッセージと応答メッセージ。
つまり、音声、動画、データのピアツーピア ストリーミングを行うには、メタデータの交換が必要になります。このプロセスはシグナリングと呼ばれます。
前のステップでは、送信者と受信者の RTCPeerConnection オブジェクトが同じページ上にあるため、「シグナリング」オブジェクト間でメタデータを渡すだけです。
実際のアプリでは、送信者と受信者の RTCPeerConnections は異なるデバイスのウェブページで実行され、それらがメタデータを伝達する方法が必要です。
そのためには、シグナリング サーバー、つまり 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
インストール ログが次のように終了しているはずです。
ご覧のとおり、package.json で定義された依存関係が npm
によってインストールされています。
(js ディレクトリではなく)work ディレクトリの最上位に新しいファイル 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 からのロギングがあるはずです。
ボーナス ポイント
- 他にどのようなメッセージング メカニズムが考えられますか。「pure」の使用時にどのような問題が発生する可能性があるかWebSocket は?
- このアプリケーションのスケーリングにはどのような問題が考えられますか。何千、何百万もの会議室の同時リクエストをテストする方法を開発できますか?
- このアプリは、JavaScript プロンプトを使用して部屋の名前を取得します。URL から部屋の名前を取得する方法を考えてみましょう。たとえば、localhost:8080/foo を使用すると、チャットルーム名は
foo
になります。
学習した内容
このステップでは、次の内容を学習しました。
- package.json で指定されているように、npm を使用してプロジェクトの依存関係をインストールします。
- 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 サーバーを実行する
この Codelab を work ディレクトリから実行していない場合は、step-05 フォルダまたは現在の作業フォルダの依存関係をインストールする必要があります。作業ディレクトリから次のコマンドを実行します。
npm install
インストール後に Node.js サーバーが実行されていない場合は、work ディレクトリで次のコマンドを呼び出して起動します。
node index.js
前のステップで Socket.IO を実装する index.js のバージョンを使用していることを確認します。ノード IO と Socket IO について詳しくは、「メッセージを交換するためのシグナリング サービスを設定する」セクションをご覧ください。
ブラウザで localhost:8080 を開きます。
新しいタブまたはウィンドウで、もう一度 localhost:8080 を開きます。1 つの動画要素に getUserMedia()
からのローカル ストリームが表示され、もう 1 つの動画要素に「remote」が表示されます。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);
}
- ユーザーが [Send] ボタンをクリックすると、画像をバイトに変換し、データチャネルで送信します。
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>
この Codelab を work ディレクトリから実行していない場合は、step-06 フォルダまたは現在の作業フォルダの依存関係をインストールする必要があります。作業ディレクトリから次のコマンドを実行します。
npm install
インストール後に Node.js サーバーが実行されていない場合は、work ディレクトリから次のコマンドを呼び出して起動します。
node index.js
Socket.IO を実装する index.js のバージョンを使用していることを確認します。また、変更を加えた場合は、必ず Node.js サーバーを再起動してください。ノード IO と Socket IO について詳しくは、「メッセージを交換するためのシグナリング サービスを設定する」セクションをご覧ください。
必要に応じて、[Allow] ボタンをクリックして、ウェブカメラの使用をアプリに許可してください。
アプリはランダムな部屋 ID を作成し、その ID を URL に追加します。ブラウザの新しいタブまたは新しいウィンドウで、アドレスバーの URL を開きます。
[Snap &送信] ボタンをクリックし、ページの下部にあるもう一方のタブの [受信エリア] を確認します。タブ間で写真が転送されます。
次のように表示されます。
ボーナス ポイント
- あらゆる形式のファイルを共有できるようにするには、コードをどのように変更すればよいでしょうか。
補足説明
- MediaStream Image Capture API: 写真の撮影やカメラ操作用の API が、まもなくお使いのブラウザに提供されます。
- MediaRecorder API による音声と動画の録画: デモ、ドキュメント
学習した内容
- 写真を撮影し、キャンバス要素を使用してそのデータを取得する方法。
- そのデータをリモート ユーザーと交換する方法。
このステップの完全なバージョンは、step-06 フォルダにあります。
10.完了
リアルタイムの動画ストリーミングとデータ交換を行うためのアプリを作成できました。
学習した内容
この Codelab では、以下について学びました。
- ウェブカメラから動画を取得します。
- RTCPeerConnection で動画をストリーミングします。
- RTCDataChannel を使用してデータをストリーミングします。
- メッセージを交換するためのシグナリング サービスを設定します。
- ピア接続とシグナリングを組み合わせる。
- 写真を撮影し、データチャネル経由で共有する。
次のステップ
- 正規の WebRTC チャット アプリケーション AppRTC(app、code)のコードとアーキテクチャを確認します。
- github.com/webrtc/samples のライブデモをお試しください。
その他の情報
- WebRTC を開始するためのさまざまなリソースが webrtc.org から提供されています。