WebRTCによるビデオチャットサービスの開発

  • 594
    Like
  • 0
    Comment
More than 1 year has passed since last update.

はじめに

Webで使えるビデオチャットサービスを開発しましたので、システム構成や使用したフレームワークやサービス、開発のポイントなどまとめてみました。
(去年にはQiitaの投稿を作っていたのですが、下書き保存したまま忘れて今頃の投稿になってしました。。)

月々のランニングコストは1000円以内、開発工数は1人月以内で作成しています。

WebRTCが出てきたおかげで、かなり容易にビデオチャットサービスが開発できるようになっていますので、よければ参考にしてください。

開発のポイント、コードだけでなく、開発の動機から、システム構成を決める為、開発前後に行ったビデオチャット関連の技術調査なども載せています。必要最小限の事だけ把握したい方はその辺りは読み飛ばしてください。

開発ビデオチャットサービス: ビデオチャッターズ (http://videochatters.net/)
page.png

開発動機

5~6年ぐらい前にビデオチャットに興味があり、Flashを使用した簡単なビデオチャットを試しにやった事はあったのですが、その時はビデオチャット用のサーバーライセンス料が高価である事や、サーバー側の帯域がかなり必要になるので試しにやるにはランニングコストなどがかかりすぎるなと断念しました。

その後、Html5のWebRTCが出てきました。WebRTCはP2Pでビデオチャットの通信を行うので、ビデオチャットに関するサーバーのネットワークの帯域の心配をする必要がありません。その事を知った後にWebRTC関連などいろいろと調べてみました。その結果、以下の事が判明しました。

簡単なビデオチャットサービスなら開発工数は1~2人月以内で作れそう。
ランニングコストはほとんどかからない。

ビデオチャットって技術面でも運用面でも敷居が高いと考えている人が多いですが(自分もそうでした)、サービスの組み合わせの工夫や便利なフレームワークも出ていますので、工夫しだいで工数をかけずにサービスの開発が可能です。開発動機はいろいろと調査をしている時に、上記が判明した事が大きかったです。

それでは次にどのような調査をしたのか、どうして上記結論に至ったかなどを明記しています。

WebRTCの事前調査

まずはWebRTCについて調査をしました。

WebRTCは対応ブラウザが限定

WebRTCは対応ブラウザが現在限定されており、Chrome、Firefox、Operaなどは対応していますが、IEやSafariなどは対応していないようです。(将来的にも現状では対応予定もない)サービスとして考えるとこれは結構痛いですが、現状では将来的にブラウザでのビデオチャットが主流になり、IE、Safariも対応せざるえなくなると、期待するしかありません。

Html5は仕様が未確定

Html5は現状では仕様策定中であり、未確定のようです。という事は現状の仕様でWebRTCのビデオチャットサービスを作成しても、将来的に何らかの修正を行う必要があるかもしれません。

その他WebRTCについて

参考)
http://www.html5rocks.com/ja/tutorials/webrtc/datachannels/
http://blog.wnotes.net/blog/article/beginning-webrtc-datachannel
http://blog.livedoor.jp/kotesaki/archives/1794148.html
http://tjun.org/blog/2013/12/webrtc_p2p/

上記にWebRTC関連について、詳しく書いてくれているので、ここでの説明は割愛します。
ポイントにしてまとめると・・、

  • クライント同士はP2Pで暗号化されたビデオストリームでやり取りするが、その前のシグナリングやNAT越えなどでサーバーが必要になる。
  • ビデオチャットを行うだけなら、比較的簡単なコードで実装可能。但し、ファイル転送などを行う場合は一度のバケットで送れるサイズが決まっているので、一度に送る送信パケットサイズの制限など独自実装する必要がある。
  • WebRTCは非対応ブラウザがあったり、未確定であったりと、WebRTCを選定する事によるデメリットもある。

次はWebRTCの提供フレームワークやサービスなどを調査してみます。

WebRTCのフレームワーク・サービスの事前調査

参考
http://qiita.com/atskimura/items/97b2cc04e19781f4a4e6
http://html5experts.jp/yusuke-naka/1130/

上記で、WebRTC関連のフレームワークやサービスなど、結構詳しく紹介してくれています。IEやSafariなどにも対応したSDKを出しているクラウドサービス系も結構あるようです。ただクラウドサービス系では調べた限りでは無料で提供しているところはほとんどなく、無料で提供している所も問合せた所、現在対応できないほどのリクエストが来ているから待ってくれ、返答がありました。

サービスとしてどうなるか分からないので、有料のクラウドサービスを最初から使うのは抵抗があります。そこで、OSSでの使用を検討してみる事にしました。

MITライセンスが使えて、PaasもやっているPeerJSがよさそうでした。
PeerJS
page2.png


そのPeerJSを使用したWebRTCのサービスをNTTコミュニケーションズが出していました。
http://www.ntt.com/release/monthNEWS/detail/20140421.html

SkyWay
page3.png

無料で使えて、日本語のドキュメントもあります。
ドキュメントを見るとかなり簡単に実装できそうです。
SkyWayドキュメント

将来的にどうなるかは分からないですが、Skyway提供は日本企業ですし、無料でしかも簡単に使用できそうなので、このサービスを使用してみることにしてみました。

ここまででWebRTCの使用フレームワーク(PeerJS)が決まりました。ただPeerJSとSkywayを使用してもWebRTCのビデオチャットができるだけです。ビデオチャットサービスを誰が使用していて、誰と誰をWebRTCで繋げるのか、というのはこちら側で指定してやる必要があります。

開発したビデオチャットサービスですが、サービスの特性上、誰が今ブラウザを開いていてリアルタイムにビデオチャットが可能か、というのが分かる必要があります。(Skypeの現在接続中が分かるような機能)

という事で次はWebSocketについて調査してみます。

WebSocketの事前調査

WebSocketはJava,php,.Net,Ruby,nodejs等々で対応しているようなので、WebSocket自体はどの言語や環境(apache,tomcat,nginx)を使用しても基本的に使えそうです。

それでは懸念となるのは実装容易性と同時接続した時のパフォーマンスや性能です。Javaやphpではクライアントに接続するごとに新たなスレッドが作成されます。数千の接続があると仮定するとメモリを大量に消費する事になり、これがボトルネックになる可能性があります。

一方nodejsはシングルスレッドモデルで、クライアントがどれだけ増えても新たにスレッドが作られる事がありません。シングルスレッドで動くのでサーバーサイドはそれを考慮して実装する必要がありますがWebSocketを使う為に、という事で考えるとnodejsを使うのが現状では良さそうだという結論になりました。

nodejsは未経験なので、WebSocketはチャレンジの意味でもnodejsを使ってみます。
次にサーバーデータ保存などについて調査、検討していきたいと思います。

サーバーデータ保存の事前調査

nodejsはMySQLやPostgreSQLなどに対応しており、データ保存や操作に関しては問題なさそうです。ただMySQLやPostgreSQLを使用した場合はレプリケーションをかけるなどして、適切にデータのバックアップを取る必要があります。

またPeerJSとnodejsの組み合わせも特には問題なさそうですが、個人的にnodejsにはWebSocketのクライアントとの接続関係だけを面倒をみさせて、他のクライアントサイド処理には依存させたくないというのがありました。
(将来的にphpやJavaに移行する場合に大変になる為)

nodejsにDB関連もさせるとクライアントサイドのページ生成からnodejsに依存します。
RestAPI形式にすればクライアントサイドは依存しませんが、工数がかさみます。

ここまで来て、何かいいのがないかなあと探していたらPaasのデータ系サービスで、parse.comというのを見つけました。
parse.com
page4.png

モバイルやJavascriptから使用でき、プッシュ通知なども使用できるようです。サーバー障害でデータが無くなるような事もなさそうです。またFacebookなどのSNS系との連携も容易に可能で、各種SDKが用意されているので、工数的にも少なく済みそうです。

クライアントサイドはHtml,css,JavaScriptで接続情報関連以外はnodejsに依存させないようにし、データ保存などはparse.comでやる事にしました。この構成ならクライアントサイドは基本的にデスクトップアプリを作成するように簡単にJavaScriptでWebアプリを作成する事ができます。

セキュリティ的には若干不安がありますが、将来的にユーザー数が増えたら、サーバーサイドで本格的にRestAPIなどを実装してやればいいかなと現状では考えています。資金面などに余裕があるわけなく、少ない工数とランニングコストで早くいいサービスを開発するのが現状の課題になりますので・・。

後、parse.comはbackbone.jsを前提としており、親和性があるようでした。backbone.jsはJavaScriptでMVC処理を行うためのフレームワークです。
(http://acro-engineer.hatenablog.com/entry/2013/10/08/121636)

backbone.jsについて少し調べてみましたが、使用するとコードがどうしても助長になり、ある程度以上の規模の複数人の開発で、使用する分には効果があるかもしれまんせが、今回のような小規模で一人で作るようなサービスだとかえって工数がかさむと感じましたので、今回は採用は見送っています。

webサーバーに関してはnginxを使用する事にします。apatchでもwebsocketのnodejsは使用する事はできそうですが、nginxが軽量で、早いとの事なので、チャレンジにも今回はnginxを使用してみます。

それではここまででシステム構成は大方決まりました。次にシステム構成について、簡単にまとめてみます。

ビデオチャットサービスのシステム構成

システム構成.png
今回は上記のようなシステム構成になりました。
外部サービスについては無料なので、かかるのはサーバー代のみです。
サーバーにherokuなどを使用すれば無料で運用する事も可能だと思います。

開発するサービスに応じて、どのようなフレームワークを使用して、
アーキテクチャやシステム構成をどのようにするか考えるのは結構楽しいです。

今回はランニングコストを抑えてスモールスタートという所を主題に考えての
システム構成ですが、これが最適という訳ではないと思いますので、
参考程度にしてください。

それでは次に開発のポイントについて明記していきます。

開発のポイント(WebRTCによるビデオチャット)

今回はSkyWay(PeerJS)を使用したビデオチャットを行います。

Htmlにビデオ要素を用意します。my-videoが自身のビデオ領域。
their-videoが相手のビデオ領域になります。
自身のビデオ領域にはmutedをセットし、ハウリングが生じないようにします。

Html
<video id="my-video" muted autoplay></video>
<video id="their-video" autoplay></video>

ブラウザに応じたgetUserMediaをセットします。

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

自身のビデオ接続がされた時、htmlのvideo要素にストリームを設定し、
window.localStreamにもストリームをセットしておきます。

Javascript
navigator.getUserMedia({video: true, audio: true}, 
    function(stream){
        // Set your video displays
        $('#my-video').prop('src', URL.createObjectURL(stream));
        $('#my-video').css("display", "none");

        window.localStream = stream;
    }, 
    function(){ 
    }
)

Skyway用のPeerJSの初期化を行います。keystringにSkywayから発行された、
文字列を設定します。

Javascript
_peer = new Peer("keystring");

PeerJSの初期化が成功するとIDが発行されるので、それを保持しておきます。

Javascript
_peer.on('open', function(id) {
    _peerId = id;
});

相手先とビデオチャットを開始する時は第一引数に相手先のPeerId、第二引数に自身のビデオ用のローカルストリームをセットしてCallします。
Callがされるとcallオブジェクトが返ってきます。
相手先のPeerIdはなんらかの手段を使って、ビデオチャットを行う相手に渡す必要があります。(今回はこの部分はWebSocektを使ってやり取りしますので後述します。)

Javascript
var call = _peer.call(targetPeerId, window.localStream);
displayTheirVideo(call);

callオブジェクトのstreamイベントをハンドリングする事で、相手先の
ビデオストリームを自身のビデオ描画領域に流し込み、相手の映像を画面に表示します。

Javascript
function displayTheirVideo = function(call) {

    if (window.existingCall) {
        window.existingCall.close();
    }

    // 相手方とビデオ通信がされた時
    call.on('stream', function(stream){
        $('#their-video').prop('src', URL.createObjectURL(stream));
    });
}

これで、WebRTCによるビデオ通信ができます。
それでは次にWebSocektによるサーバーとの通信を記述します。

開発のポイント(node.jsによるWebSocket通信)

  • クライアントサイド

Html5標準のWebSocketの初期化を行います。

Javascript
_ws = new WebSocket("wss://hostname:433/....");

WebSocketを扱う際、暗号化を行う場合はwss://...、暗号化を行わない場合はws://...というように記述します。
wssで暗号化する場合はサーバーサイドでSSLの設定が必要です。

※開発時にwssでSSLの自己署名証明書を使用した時、通信がされませんでした。(エラーなども発生せず)自己署名証明書の場合、信頼されていない証明書になりますので、これを使用しているブラウザで信頼する証明書にすると通信がされました。またhttpsサイトでwsによる通信を行うと通信に失敗します。

下記のように記述する事で、open時やclose時、メッセージ受信時のイベントをハンドリングできます。
メッセージの受信データについてはevent.dataに格納されてきます。

Javascript
// Open処理
_ws.onopen = function() {
};

// Close処理
_ws.onclose = function() {
};

// 再接続処理
_ws.reconnect = function() {
};

// 再接続中処理
_ws.reconnecting = function() {
};

// 再接続失敗処理
_ws.reconnect_failed = function() {
;

// メッセージ受信処理
_ws.onmessage = function(event) {
    console.log(event.data);
}

メッセージの送信は下記のように記述します。

Javascript
ws.send("peerId:" + _peerId);

今回はビデオチャットをする際に相手先のPeerIdが必要になりますので、
自身のPeerIDが取得された時にWebSocketで自身のPeerIDをサーバーに送信。
相手先とビデオ通話を開始する時にサーバーで保持している相手先のPeerIDを
取得して、ビデオチャットを開始するような処理になります。

  • サーバーサイド(node.js)

https,websocket.ioを使用します。
下記のように記述するとクライアントサイドでHtml5標準のWebSocketを使用して、
サーバーサイドのnode.jsとWebSocketによる通信が可能になります。

app.js
var ws = require('websocket.io');
var https = require('https');
var fs = require('fs');
var hostname = 'hostname';
var port = 4443;
var url = 'wss://' + hostname + ':' + port + '/';

var opts = {
  key: fs.readFileSync('server.key'),
  cert: fs.readFileSync('server.crt'),
  ca: fs.readFileSync('ca.crt'),
  requestCert: true,
  rejectUnauthorized: false
};

var ssl_server = https.createServer(opts, function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
});

var server = ws.attach(ssl_server);

ssl_server.listen(port, function() {
  console.log('Listening on ' + port);
});

クライアントとの通信については下記の記述になります。

app.js
//Websocket接続を保存用
var connections = [];

// クライアント接続イベント
server.on('connection', function(socket) {

    //配列にWebSocket接続を保存
    var object = new Object();
    object.userId = "";
    object.peerId = "";
    object.data = "";
    object.socket = socket;
    connections.push(object);

    // エラー時
    socket.on('error', function(err){
        console.log("error:" + err);
    }); 

    // 切断時
    socket.on('close', function () {
        connections = connections.filter(function (conn, i) {
            return (conn.socket === socket) ? false : true;
        });
    });

    // 受信時
    socket.on('message', function(data) {
        console.log('受信:' + data);
        connections.forEach(function (con, i) {
            if (con.socket == socket) {
                return;
            }
        });

    });
});

connectionイベント時にconnectionsにsocketやPeerIdを保持するようにします。
受信時に送信されてきたsocketとconnectionsに保持しているsocketを判定する事で、特定のユーザーに対する処理を行う事が可能です。

まとめるとビデオチャットの流れは簡単には下記のようになります。

  1. クライアントサイド:WebSocket初期化
  2. サーバーサイド:通信開始されたSocketをconnectionsに保持
  3. クライアントサイド:PeerID取得、サーバーに取得したPeerIDやUserIdなどを送信
  4. サーバーサイド:受信したPeerIDやUserIdなどをconnectionsに保持
  5. クライアントサイド:ビデオチャットを開始するUserIdをサーバーに送信
  6. サーバーサイド:受信したUserIdのPeerIDをクライアントに送信
  7. クライアントサイド:受信した相手先のPeerIDをPeerでCallする

ここまででビデオチャットの簡単なポイントは終わりです。
次にデータ保存系のParse.comについて記述します。

開発のポイント(Parse.comによるデータ保存・操作)

parse.comで会員登録行い、Application IDとJavascript Keyを取得します。

取得したApplication IDとJavascript Keyを使用してParse.comの初期化を行います。

Javascprit
Parse.initialize("Application ID", "Javascript Key");

データの保存については下記のような記述になります。
Parse.comのData BrowserでSampleClassを事前に定義しておく必要があります。
save時のsuccessやerrorイベントはコールバックイベントになります。

Javascprit
var SampleClass = Parse.Object.extend("SampleClass")
var model = new SampleClass();

var model.set("hogeId", ***);
....

model.save(null, {
    success: function(data) {
    },
    error: function(data, error) {
    }
});

また保存時のデータを設定する時に読取はパブリックにして、書き込みは自身のみ許可するような設定が可能です。レコード単位で特定のユーザー毎の読取や書き込み権限を付与する事ができますので、Parse.comだけである程度のセキュリティを担保する事ができます。

Javascprit
// ACL設定
var groupACL = new Parse.ACL();
groupACL.setWriteAccess(Parse.User.current(), true);
groupACL.setPublicReadAccess(true);
model.setACL(groupACL);

検索処理については下記のような記述になります。
検索成功時にコールバック内のresultsにSampleClassの取得データが格納されてきます。

Javascprit
var objSampleClass = Parse.Object.extend("SampleClass");
var querySampleClass = new Parse.Query(objSampleClass);

querySampleClass.equalTo("hogeId", 1);

querySampleClass.find().then(function(results) {
    fnCallback(results, true, null);

}, function(error) {
    fnCallback(null ,false, error);
}); 

Parse.comはユーザー管理も対応しており、下記のような記述でログイン処理が可能です。

Javascprit
Parse.User.logIn("email_string", "password_string", {
    success: function(user) {
        fnCallback(true, null);

    },
    error: function(user, error) {
        fnCallback(false, error);
    }
});

ログインユーザーについては下記で取得可能です。

Javascprit
var user = Parse.User.current()
if (!user) {
    // ログイン未
}

ユーザー登録については下記になります。
Parse.com側の設定で、ユーザー登録後の認証メール(カスタマイズ可能)を送信する事が可能です。また認証メール画面などもParse.comの機能で使用する事ができ、その内容も柔軟に設定する事が可能です。

Javascprit
Parse.User.signUp("email_string", "password_string", {
    ACL: new Parse.ACL(), email: "email_string" }, {
        success: function(user) {
        },
        error: function(user, error) {
        }
}); 

Parse.comを使用すればある程度セキュリティを担保して、Javascriptを使用して、データ保存や取得などを簡単に使用する事が可能です。

簡単な開発のポイントとしてはこれで終了です。

最後に

前記した内容でビデオチャッターズ (https://videochatters.net/)を工数的には15人日ぐらいで開発しています。(デザインについては別途お願いしています。)

便利なフレームワークやクラウドサービスが出ていますのでいろいろと工夫すれば、スピード感のあるサービスのスタートアップが可能になってきていると思います。

今回の投稿はコードなどの記述内容だけでなく開発動機やいろいろな調査内容なども載せました。コードだけなら検索すれば何かしらの情報が出てきますので、システム構成などをどのように検討したかを明記したらおもしろいかなと思いましたので、今回のような投稿の構成になりました。

今回の投稿がなにかしらのサービスの開発などに役立てば幸いです。

以上です