WebRTCを利用し、画面共有+テキストチャットを行う

2015年5月27日

WebRTC(Web Real-Time Communication)とはリアルタイムコミュニケーション用のAPIの定義で、プラグイン無しでウェブブラウザ間のボイスチャット、ビデオチャット、ファイル共有ができる仕組みです。
今回は画面共有とテキストチャットをリアルタイムで行えるようにします。

参考サイト

WebRTC Experiments & Demos ® Muaz Khan

上記サイトからリンクされている「RTCMultiConnection.js」というライブラリを利用して作成しております。

ここで一言。。。

今回構築した段階では、ボイスチャットも加えておりましたが、環境がノートPCということで、マイクとスピーカーが近いせいかハウリングを起こしてしまいます。一応ノイズキャンセラなどを利用しましたが、限界がありましたので、ボイスチャットをする場合はヘッドセットの利用をお勧めします。
という訳で、今回はボイスチャットは組み込んでおりません。要望があれば載せますので、その際はご連絡ください。

NAT/Firewallの環境を超える必要がある!

WerRTCでは各クライアント間をPeer-to-Peerで繋ぐため、シグナリング処理でお互いにインターネット側から見た情報を通知する必要があります。しかし、ブラウザ側はグローバルIPやNATでマッピングされたUDPポートなどがわかりません。
これをうまい具合にしてくれるのが、STUN/TURNサーバです。今回は「RTCMultiConnection.js」内に記載されているstun:stun.l.google.com:19302などを利用しますので省きますが、自分でも用意することが可能です。Google Codeに公開されております。

ソース公開、のまえに。。。

今回作成した物の仕様ですが、以下様になります。

  • 共有元のウインドウを共有先にリアルタイムに配信します
  • 共有先は共有元のウインドウをリアルタイムに閲覧できます
  • 共有先のウインドウは共有されない(共有元からのウインドウ配信は片方向通信)
  • 共有元から共有先、及び共有先が複数あった場合は共有先同士でもテキストチャットが可能(双方向(マルチ)通信)

画面共有が片方向通信で、テキストチャットが双方向(マルチ)通信なので、コネクションを張る際、1コネクションだと想定する動きが難しいので、今回は2コネクション張るようにしております。画面共有(片方向通信)のチャンネルと、テキストチャット(双方向通信)のチャンネルという様に分けてます。

また、PeerToPeer通信なので、共有元にアクセスが集中してしまい、PC(ブラウザ)に負荷が掛かってしまうため、共有先の接続数を4まで縛っております。

共有を行う前の事前準備

まず、共有画面を共有する元となる端末のブラウザ側の設定を変更する必要があります。今回はFirefoxの例を記述します。Chromeも同じことをやる必要がありますが、当たり前ですがやり方は違います。

  1.  Firefoxのアドレスバーに「about:config」と入力します。
    通常の項目とは異なるため、注意事項が出ますが、そのまま続行してください。
  2. media.getusermedia.screensharing.allowed_domains」にソースを設置するドメイン名を入力します。
  3. media.getusermedia.screensharing.enabled」をtrueに設定します。

プログラムの動きについて

今回公開するソースの手順というかReadmeの様なものです。
すでにソースをサーバー上にて公開していることを前提としております。(httpsをご利用ください)

  1. URLでページを表示させる
  2. 「共有を開始する」ボタンを押下し、共有するウィンドウを選択します。
  3. 共有URLを共有先の人に送ります。
  4. 共有先の人は、送られてきた共有URLを開くだけで、共有が開始されます。

たまに正常に接続できず、テキストチャットなどが送られない場合があります。その場合は共有先の人がブラウザの再読み込みをしてください。
おまけでテキストチャットを行う際に、共有画面が邪魔になりそうだったので、画面ON/OFFボタンを追加してます。

 ソースはこちら

<!DOCTYPE html>
<html>
<head>
<title>画面共有+テキストチャット</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="pragma" content="non-cache">
<meta http-equiv="expires" content="0">
<meta http-equiv="cache-control" content="non-cache">
<script src="RTCMultiConnection.js"></script>
<script src="firebase.js"></script>
</head>

<body>
<div id="ScreenShare"></div>
<div id="OpenShare"><button name="OpenShareButton" id="OpenShareButton">共有を開始する</button></div>

<button name="ScreenOn" id="ScreenOn">画面を表示</button>
<button name="ScreenOff" id="ScreenOff">画面を消す</button>
<div id="viewArea">
<video id="screen" controls muted></video>
</div>
<div id="InputArea">
<input type="text" id="name" size="10" placeholder="名前" /><br/>
<input type="text" id="chat-input" size="50" placeholder="コメントを入力" />
</div>
<div id="chat-output"></div>
<script>
(function() {

// ユニークキー作成
var uniqueToken = '#' + (Math.random() * new Date().getTime()).toString(36).toUpperCase().replace(/\./g, '-');
var domain = 'https://DOMAINNAME';   // この部分は編集してください
var resultingURL = domain + location.pathname + uniqueToken;

// URLに"#"があり、配列が2以下の場合
if(location.hash.length < 2){
window.history.pushState(null,null,resultingURL);
document.querySelector('#ScreenShare').innerHTML = '共有先URLは以下となります<br/><input type=\"text\" name=\"resultingURL\" size=\"80\" value=\"' + resultingURL + '\" />';
document.getElementById('OpenShareButton').disabled = false;
}else{
document.getElementById('OpenShareButton').disabled = true;
}

// 初期のスクリーンボタン状態
document.getElementById('ScreenOn').disabled = true;
document.getElementById('ScreenOff').disabled = false;

// 共有ボタンを押した場合
document.getElementById('OpenShareButton').onclick = function() {
// すでにセッションがあるか
// セッションがあれば、join(共有先)とする
if(chatConn.sessionid && screenConn.sessionid){
chatConn.join(chatConn.sessionid);
screenConn.join(screenConn.sessionid);

}else{
screenConn.open();
chatConn.open();

}

this.disabled = true;
document.getElementById('ScreenOn').disabled = true;
document.getElementById('ScreenOff').disabled = false;
};

// スクリーンon/off
document.getElementById('ScreenOn').onclick = function() {
document.getElementById('ScreenOn').disabled = true;
document.getElementById('ScreenOff').disabled = false;
document.getElementById('viewArea').style.display = '';
};
document.getElementById('ScreenOff').onclick = function() {
document.getElementById('ScreenOn').disabled = false;
document.getElementById('ScreenOff').disabled = true;
document.getElementById('viewArea').style.display = 'none';
};

// URLからチャンネルを作成
var channel = location.href.replace(/\/|:|#|%|\.|\[|\]/g, '');

// コネクション値取得
var chatConn = new RTCMultiConnection(channel + 'chatConn');
var screenConn = new RTCMultiConnection(channel + 'screenConn');

// チャットの利用セッション
chatConn.session = {
data: true
};

// 画面共有の利用セッション
screenConn.session = {
screen: true,
oneway: true
};

// 共有先のMAX許可数
chatConn.maxParticipantsAllowed = 4;
screenConn.maxParticipantsAllowed = 4;

// チャット処理
chatConn.onmessage = function(e) {
appendDIV(e.data);
};

if (document.getElementById('chat-input')){
document.getElementById('chat-input').disabled = false;
}
var chatOutput = document.getElementById('chat-output');

function appendDIV(data){
var div = document.createElement('div');
div.innerHTML = data;
chatOutput.insertBefore(div, chatOutput.firstChild);
div.tabIndex = 0;

}

document.getElementById('chat-input').onkeypress = function(e){
if (e.keyCode !== 13 || !this.value) return;
var name = document.getElementById('name').value;
if(!name){
name = "Anonymous";
}
var comment = name+":"+this.value;
appendDIV(comment);
chatConn.send(comment);
this.value = '';
this.focus();
};

// 共有画面処理
screenConn.onstream = function(event) {
if(event.isScreen){
var screen = document.getElementById('screen');
screen.src = event.blobURL;
screen.autoplay = true;
}
};

// 既存セッションに接続
// 指定セッションがあるかどうか
chatConn.connect(chatConn.sessionid);
screenConn.connect(screenConn.sessionid);

})();
</script>
</body>
</html>

 

end