Web技術だけでビデオメッセンジャーが作れる!PeerJSを使ってみた

2017/03/17

Wern Ancheta

0

Articles in this issue reproduced from SitePoint
Copyright © 2017, All rights reserved. SitePoint Pty Ltd. www.sitepoint.com. Translation copyright © 2017, KADOKAWA CorporationJapanese syndication rights arranged with SitePoint Pty Ltd, Collingwood, Victoria,Australia through Tuttle-Mori Agency, Inc., Tokyo

SkypeのようなビデオチャットがJavaScriptだけで作れる!? WebRTCを手軽に扱えるPeerJSでリアルタイムなビデオチャットアプリを作ってみました。

※本記事は2016年7月16日に掲載した記事の翻訳を一部更新したものです。執筆時点の情報をベースにしており、最新ではない可能性があります。

WebRTCの登場と、ブラウザーの大容量化によってリアルタイムでP2Pを扱えるようになったので、これまでよりずっと簡単にリアルタイムアプリケーションが構築できるようになりました。この記事では、PeerJSを取り上げ、WebRTCを効率的に実装する方法を紹介します。WebRTCを使った、メッセージ機能付きビデオチャットアプリを構築してみましょう。

WebRTCとP2Pについて少し予備知識が必要な人は、『The Dawn of WebRTC』と『An Introduction to the getUserMedia API』を読んでください。

PeerJSとは?

本題に入る前に、これから使うメインツール「PeerJS」への理解が必要です。PeerJSはWebRTCのP2Pによって、ビデオ、音声通話を簡単に実現できるJavaScriptライブラリーです。

PeerJSは、ブラウザーにおけるWebRTCの実装まわりのラッパーとしての役割を果たします。ご存じの通り、ブラウザーベンダーはそれぞれの機能を実装する方法について、厳密には合意しません。ですから、WebRTCでもブラウザーごとに実装方法が異なります。開発者であれば、サポート予定のブラウザーごとに違うコードを記述するはずです。PeerJSはそのコードのラッパーとなります。紹介するAPIは使いやすく、クロスブラウザー対応のWebRTCの実装のためには、本当に適していることが理解できるでしょう。

PeerServer

WebRTCは完全なP2Pではありません。実際にはピア間を接続するサーバーが常にあります。2つ以上のデバイスがどこからともなく魔法のように接続されるわけではありません。ファイアウォール、ルーターなどのP2Pを妨げる障害物を乗り越えるには、サーバーの出番です。

PeerServerはPeerJSのサーバー側のコンポーネントで、2つ以上のデバイスを同時に接続できます。PeerServerを使うときの選択肢は、PeerJSサーバーライブラリーを使って自分で実装するか、PeerServerクラウドサービスを使うかの2つです。

では、プロジェクトにふさわしいのはどちらでしょうか?

もしすぐに使いたかったら、PeerServerクラウドサービスがおすすめです。サインアップしてAPIキーを取得すれば、PeerServerクラウドに接続して利用できます。この方法の唯一のデメリットは、実際のプロダクトにおいてほとんど役に立たないことです。同時接続数は50のみで、サーバーがHTTPSに対応していないためです。ビデオ通信機能は2つの端末間の通信経路を暗号化するために、接続するPeerServerもHTTPS対応でなければなりません。

Webサイトにビデオや音声通話機能を実装しようとしてこの記事を読んでいる人は、PeerJSサーバーライブラリーが良いでしょう。ただし、単にチャット機能だけが目的であれば、Webサイトの同時接続制限を超えないのでPeerServerのクラウドサービスも使えます。

この記事では、PeerServerクラウドサービスとPeerJSサーバーライブラリーの両方を使ったサーバーの実装方法について説明します。

WebRTCのビデオチャットアプリを構築する

さて、ここからアプリの構築を始めます。クライアント側コードの作業をしてからサーバー側の作業に入ります。

GitHubから記事で使っているコードがダウンロードできます。コードを実行したり家で復習する際には、Nodeとnpmのインストールが必要です。Nodeやnpmについてよく分からない人、インストール手順が知りたい人は、以下の記事を参考にしてください。

クライアント

この項では主にアプリのユーザー側の部分について作業をします。次のようなディレクトリ構造になります。

public
├── css
│ └── style.css
├── index.html
├── js
│ └── script.js
└── package.json

依存関係

クライアント側のファイル構成には次のような依存関係があります。

  • Picnic —軽量なCSSフレームワーク
  • jQuery—ページの要素の選択やイベントハンドラーに使用
  • PeerJS—PeerJSのクライアント側コンポーネント
  • Handlebars—JavaScriptのテンプレートライブラリー。メッセージ用のHTML生成に使用

npmからライブラリーをインストールできます。publicディレクトリのルート内にpackage.jsonファイルを作成し、以下を追加します。

{
  "name": "peer-messenger-client",
  "version": "0.0.1",
  "dependencies": {
    "jquery": "~2.1.4",
    "picnic": "~4.1.2",
    "peerjs": "~0.3.14",
    "handlebars": "~4.0.5"
  }
}

npm installを実行して、すべてのファイルをインストールします。

マークアップ

index.htmlファイルは見れば分かると思いますので、ここで確認してください。重要な部分はインラインでコメントをしています。

メッセージリスト作成用のHandlebarsのテンプレートを見てください。messages配列内のすべてのメッセージをループします。ループを繰り返すごとに、送信者の名前とメッセージを表示します。あとで、データがこのテンプレートにどのように渡されているかを説明します。

<script id="messages-template" type="text/x-handlebars-template">
  {{#each messages}}
  <li>
    <span class="from">{{from}}:</span> {{text}}
  </li>
  {{/each}}
</script>

スタイル

このアプリのスタイリングはほとんどがPicnicで賄われているので、最小限のスタイルだけ用意します。css/style.cssの追加のスタイルには要素の位置調整、非表示、ブラウザーのデフォルトのスタイルを上書きするためのルールがいくつか含まれています。

アプリのメインスクリプト

メインのJavaScriptファイル(js/script.js)には、ログイン、通話開始、ユーザーのビデオキャプチャーなど、アプリのロジックがすべてが詰まっています。コードを簡単に説明します。

まず、各ピアから送信されるメッセージを格納するためのmessages配列と、リモートピアのIDを格納するpeer_id、現ユーザーの名前を格納するname、現在のピア接続を格納するconnの変数を宣言します。またHandlebarsを使ったmessagesテンプレートをコンパイルして、その結果をmessages_templateに格納します。

var messages = [];
var peer_id, name, conn;
var messages_template = Handlebars.compile($('#messages-template').html());

次に、設定オプションを含むオブジェクトを受け入れる、新しいPeerJSインスタンスを作成します。このデモではPeerJSサーバーライブラリーを使うので、hostportpathのオプションを指定します(PeerServerクラウドサービスの設定はあとで説明します)。debugオプションでデバッグモードの冗長性(最高は3)を設定し、configオプションではICE(Interactive Connectivity Establishment:相互接続設定)サーバーをSTUNかTURNのいずれかに指定できます。

ここで出てきた新しい専門用語について心配する必要はまったくありません。説明したサーバーはピア接続がうまく設定されているかの確認に使われることだけを知っておけば十分です。WebRTCをもっと詳しく知りたい人は、HTML5Rocks の『Getting Started with WebRTC(DevTools Timeline)』か『WebRTC and the Ocean of Acronyms(Mozilla Hacks)』を読んでください。もしほかのSTUNかTURNサーバーを利用したいなら、無料公開のSTUNとTURNサーバーのリストを参考にしてください。

var peer = new Peer({
  host: 'localhost',
  port: 9000,
  path: '/peerjs',
  debug: 3,
  config: {'iceServers': [
  { url: 'stun:stun1.l.google.com:19302' },
  { url: 'turn:numb.viagenie.ca',
    credential: 'muazkh', username: 'webrtc@live.com' }
  ]}
});

接続の初期化がサーバーでうまくできていれば、openイベントがpeerオブジェクトで発生します。このとき、現在のユーザーのIDが利用できるようになるので、ページに挿入します。このユーザーがIDをもう1人のユーザーに渡すと、お互いが接続できます。

peer.on('open', function(){
  $('#id').text(peer.id);
});

次の行は、navigatorオブジェクトgetUserMediaプロパティを追加し、値にブラウザーのgetUserMediaを渡します。ChromeやSafariなどのWebkit系ブラウザーで使うときは、webkitGetUserMediaになります。FirefoxのときはmozGetUserMediaです。UserMedia APIを実装するほかのブラウザーでは単にgetUserMediaです。ただし、これは確実ではないことに注意してください。getUserMedia APIをもっと統一された方法で使いたい場合は、getUserMedia.jsなどの小さなライブラリーを使うとよいでしょう。

navigator.getUserMedia = navigator.getUserMedia ||
                         navigator.webkitGetUserMedia ||
                         navigator.mozGetUserMedia;

次に現在のユーザーのビデオを配信するnavigator.getUserMedia関数を説明します。この関数は3つの引数を受け取ります。

  1. 設定オプションを含むオブジェクト。ここではaudiovideotrueにして、デバイスのカメラとマイクをビデオストリーミングで利用できるようにします
  2. getVideo関数の引数に指定されているサクセスコールバック関数。この無名のコールバック関数にはストリームが引数として渡されることに注意してください
  3. エラーが発生したときにユーザーに知らせる、エラーのコールバック関数
function getVideo(callback){
  navigator.getUserMedia(
    {audio: true, video: true},
    callback,
    function(error){
      console.log(error);
      alert('An error occurred. Please try again');
    }
  );
}

getVideo(function(stream){
  window.localStream = stream;
  onReceiveStream(stream, 'my-camera');
});

onReceiveStream関数はビデオストリームを初期化します。

function onReceiveStream(stream, element_id){
  var video = $('#' + element_id + ' video')[0];
  video.src = window.URL.createObjectURL(stream);
  window.peer_stream = stream;
}

ユーザーが「Login」をクリックすると、PeerJSを使って指定されたIDを持つユーザーに接続します。metadataオブジェクトは、カスタムデータをピアに渡すために使います。このとき、ピアにユーザー名が表示されるように、usernameで渡します。接続が確立したら、メッセージが現ユーザーに送信されるたびに、dataイベントが発生します。便宜上、現ユーザー(接続を始めた人)をユーザーAとし、ピア(接続された人)をユーザーBとします。

$('#login').click(function(){
  name = $('#name').val();
  peer_id = $('#peer_id').val();
  if(peer_id){
    conn = peer.connect(peer_id, {metadata: {
      'username': name
    }});
    conn.on('data', handleMessage);
  }

  $('#chat').removeClass('hidden');
  $('#connect').addClass('hidden');
});

ユーザーAがユーザーBに接続しようとするとき、ユーザーBのpeerオブジェクトでconnectionイベントが発生します。これでユーザーBとの接続を設定し、ユーザーA(connection.peer)のIDを取得できます。接続すると、ユーザーAがユーザーBにメッセージを送るたびに発生するdataイベントをリッスンできます。そのあと、ピアIDのテキスト項目を非表示にし、その値を更新して接続しているピアのIDに使い、ユーザーAのユーザー名を表示します。

peer.on('connection', function(connection){
  conn = connection;
  peer_id = connection.peer;
  conn.on('data', handleMessage);

  $('#peer_id').addClass('hidden').val(peer_id);
  $('#connected_peer_container').removeClass('hidden');
  $('#connected_peer').text(connection.metadata.username);
});

handleMessage関数はリモートピアが送信したデータを受け取ります。このデータはメッセージリストのHTML生成に使います。

function handleMessage(data){
  var header_plus_footer_height = 285;
  var base_height = $(document).height() - header_plus_footer_height;
  var messages_container_height = $('#messages-container').height();
  messages.push(data);

  var html = messages_template({'messages' : messages});
  $('#messages').html(html);

  if(messages_container_height >= base_height){
    $('html, body').animate({ scrollTop: $(document).height() }, 500);
  }
}

sendMessage関数は、その名のとおり、リモートピアへのメッセージ送信に使います。

function sendMessage(){
  var text = $('#message').val();
  var data = {'from': name, 'text': text};

  conn.send(data);
  handleMessage(data);
  $('#message').val('');
}

ユーザーが呼び出しを開始すると、peerオブジェクトのcallメソッドを使ってリモートピアを呼び出します。このコール関数の結果は、streamイベントで処理できるオブジェクトを返します。このイベントは、リモートピアのビデオをコールを始めたユーザーに配信できる状態になったときに発生します。

$('#call').click(function(){
  var call = peer.call(peer_id, window.localStream);
  call.on('stream', function(stream){
    window.peer_stream = stream;
    onReceiveStream(stream, 'peer-camera');
  });
});

ユーザーがリモートピアにコールするとcallイベントが発生します。ここから、onReceiveCall関数を実行して、コールを受けます。コールを受ける際に追加機能が欲しいなら、ここに追加するのがお勧めです。

peer.on('call', function(call){
  onReceiveCall(call);
});

onReceiveCall関数は引数として現在のcallオブジェクトを受け取ります。callオブジェクトのanswerメソッドを呼び出し、処理します。これは引数として現ユーザーのビデオ配信を受けます。リポートピアのビデオ配信ができるようになったときにstreamイベントが発生します。そこからonReceiveStream関数を呼び出し、リモートピアのビデオをセットアップします。

function onReceiveCall(call){
  call.answer(window.localStream);
  call.on('stream', function(stream){
    window.peer_stream = stream;
    onReceiveStream(stream, 'peer-camera');
  });
}

サーバー

サーバーで処理する準備が整いました。まずPeerServerクラウドサービスから使ってみます。そのあと、peerjs-serverを使ってPeerServerの動かし方を説明します。

PeerServerクラウドサービスからAPIキーを取得する

peerjs.comで、Developer – Freeボタンをクリックします。証明を求めるモーダルウィンドウが表示されます。フォームに入力し、Complete Registrationをクリックすると、APIキーが表示されたダッシュボードページに移ります。

APIキーをjs/script.jsファイルにコピーして更新します。keyプロパティの設定オプションからhostportpathのプロパティを削除できます。

var peer = new Peer( {
  key: 'YOUR PEERSERVER CLOUD SERVICE API KEY',
  debug: 3,
  config: {'iceServers': [
    { url: 'stun:stun1.l.google.com:19302' },
    { url: 'turn:numb.viagenie.ca',
      credential: 'muazkh', username: 'webrtc@live.com' }
  ]}
});

これでOKです。記事の最後にあるデモで、できあがりがどうなったか見てみます。この方法の限界(同時接続は50までで、ビデオは利用できない点)を覚えておいてください。

自身のPeerServerを実行する

この項では自身のPeerサーバーの動かし方を説明します。

以下のディレクトリ構造になります。

server
├── peer-server.js
└── package.json

サーバーのコンポーネントにはpeerjs-serverが必要です。serverディレクトリのルートにpackage.jsonファイルを作って、以下を追加します。

{
  "name": "peer-messenger-peerserver",
  "version": "0.0.1",
  "dependencies": {
    "peer": "^0.2.8"
  }
}

npm installを実行して依存性を注入します。

ローカルで作業しているので、サーバーのフォルダーは(GitHubのリポジトリと同じように)プロジェクトのルートディレクトリのパブリックフォルダの隣に置くことができます。ローカルで作業していなければ、serverフォルダーはリモートサーバーのどこかに置きます。

PeerServerを作成する

サーバーコンポーネントの作業ディレクトリに、peer-server.jsファイルを作成し、以下のコードを加えます。

var PeerServer = require('peer').PeerServer;
var server = PeerServer({port: 9000, path: '/peerjs'});

コードの追加によりポート9000で実行するPeerJSサーバーを作成します。ターミナルからnode peer-server.jsを実行してサーバーを動かします。サーバーが動いていれば、ブラウザーからhttp://localhost:9000/peerjsにアクセスすると以下のように表示されるはずです。

{"name":"PeerJS Server","description":"A server side element to broker connections between PeerJS clients.","website":"http://peerjs.com/"}

リモートサーバーにデプロイする

あとでリモートサーバーにデプロイする場合は、HTTPS経由にしなければなりません。接続が安全な場合、ブラウザーがデバイスのカメラとマイクへのアクセスだけを許可するためです。PeerServerをHTTPSで実行するコードがこちらです。

var fs = require('fs');
var PeerServer = require('peer').PeerServer;

var server = PeerServer({
  port: 9000,
  ssl: {
    key: fs.readFileSync('/path/to/your/ssl/key/here.key'),
    cert: fs.readFileSync('/path/to/your/ssl/certificate/here.crt')
  }
});

SSL証明書プロバイダーから取得したキーと証明書ファイルの提供が必要です。

アプリケーションを実行する

WebRTCのビデオチャットアプリを実行するには、先ほどクライアント側で作成したファイルを提供するサーバーが必要になります。getUserMedia APIは単にブラウザーでindex.htmlファイルを開くだけでは動きません。方法はたくさんありますが、その中のいくつかを紹介します。

PHPを利用する

PCにPHPがインストールしてあれば、publicディレクトリにファイルを置いて、以下のコマンドを実行できます。

php -S localhost:3000

これで、http://localhost:3000からアプリにアクセスできます。

Apacheを利用する

Apacheがインストールしてある場合は、Webフォルダーのルートにpeer-messengerディレクトリをコピーすると、http://localhost/publicからアプリにアクセスできます。

Nodeを利用する

Nodeを使う場合、Expressをインストールすると、静的ファイルが提供されます。次のコマンドでExpressをインストールできます。

npm install express

次にapp-server.jsファイルを作成し、以下のコードを加えます。

var express = require('express');
var app = express();

app.listen(3000);
app.use(express.static('public'));

node app-server.jsを実行してアプリを起動します。http://localhost:3000からアプリにアクセスできます。

デモ

どの方法を使っても、アプリは次のように表示されるはずです。

1467783386_image1.png

この画面では、ユーザーは自分のユーザー名と接続しようとするピアのIDを入力します。便宜上、接続を始めるこのユーザーを「発信者」と呼びます。

1467783386_image2.png

発信者がログインするとPeerJSは接続データをピアに送信します。便宜上、このユーザーを「受信者」と呼びます。受信者側でデータが受信されると、画面は更新されて発信者のユーザー名を表示します。

1467783386_image3.png

受信者は自分のユーザー名を入力し、Loginボタンをクリックして発信者と話し始めます。

1467783386_image4.png

発信者は応答して、Callボタンをクリックします。

1467783386_image5.png

接続すると、発信者と受信者の両方に接続相手の映像が表示されます。

1467783386_image6.png

最後に

この記事では、PeerJSとPeerJSを使ったリアルタイムアプリの作成方法について説明し、実際にリモートピアへのテキスト送信やビデオ通話ができるメッセージアプリを作成しました。PeerJSはWebアプリケーションでWebRTCを実装するのにとても適したクロスブラウザー対応のライブラリーです。

このチュートリアルで使用したコードはGitHubで入手可能です。ぜひコピーして、クールなアプリ開発を楽しんでください。

※本記事はPanayiotis VelisarakosRavi Kiranが査読を担当しています。最高のコンテンツに仕上げるために尽力してくれたSitePointの査読担当者のみなさんに感謝します。

(原文:Building a WebRTC Video Chat Application with PeerJS

[翻訳:和田麻紀子/編集:Livit

Copyright © 2017, Wern Ancheta All Rights Reserved.

Wern Ancheta

Wern Ancheta

フィリピン出身のWeb開発者です。Web構築に情熱を注ぎ、ノウハウをブログで公開しています。アニメとビデオゲームも大好きです。

Loading...