WebRTCでブラウザ間通信
「WebRTCはじめました:Webカメラ編」でWebカメラからストリームの取得ができたので,いよいよWebRTCの本番,ブラウザ間通信によってストリームの転送を実装.
普通はnodejsとか使って最初のシグナリングを行うけれど,まずはコピペによるシグナリングから始める.
基本的にWebRTCに関する実装はlibwebrtc.jsという形でまとめておく.
WebRTCの細かい内容については説明しないので適宜調べてください.
配信側:コネクションの作成
まず,呼び出す側の実装は下記のようになる.
カメラが取得できたらmainが呼び出される.カメラの取得方法等は「WebRTCはじめました:Webカメラ編」を参照.
<!DOCTYPE html>
<html>
<head>
<title>Camera</title>
<script src="js/libwebrtc.js"></script>
</head>
<body>
<div>
<video id="video" autoplay="1" />
</div>
<form id="control"></form>
<form>
<textarea id="textarea" rows=5 cols=50></textarea>
<input type="button" id="button" onclick="receive()" value="onAnswer"></input>
</form>
<script>
var video = document.getElementById("video");
var control = document.getElementById("control");
var localStream = null;
var connection = null;
var msg_obj = [];
var textarea = document.getElementById("textarea");
// 指定されたメッセージをテキストエリアに表示する
// 追記するためにリストを使っているけれど,直接今のvalueに足してもよい
function appendMessage(msg) {
msg_obj.push(msg);
textarea.value = JSON.stringify(msg_obj);
}
getVideoSources(function(cam) {
console.log("cam", cam);
var b = document.createElement("input");
b.type = "button";
b.value = cam.name;
b.onclick = getMain(cam.id);
control.appendChild(b);
console.log('add button');
});
function getMain(cam_id) {
return function() {
main(cam_id);
};
}
function main(cam_id) {
navigator.getUserMedia({
audio: false,
video: { // とりあえず試す場合は「video : true」としてカメラの指定を行わなければよい
optional: [{
sourceId: cam_id
}]
}
}, function(stream) { // success
console.log("Start Video", stream);
localStream = stream;
video.src = URL.createObjectURL(stream);
video.play();
video.volume = 0;
webrtc();
}, function(e) { // error
console.error("Error on start video: " + e.code);
});
};
function webrtc() {
var stop;
msg_obj = [];
// 相手に配信するストリーム,メッセージを伝えるメソッド,エラーコールバックを指定
connection = start_webrtc(localStream, appendMessage, function() {
console.log("Error");
});
}
// Answerを受け取った時に呼び出されるメソッド
function receive(){
var msg = JSON.parse(textarea.value);
msg.some(function(v, i){
if(v.type==msg_type.answer){
on_answer(connection, msg);
}
});
}
</script>
</body>
</html>
カメラ取得ができた後はライブラリ化されたstart_webrtc()メソッドを呼び出すだけにしている.
受信側に伝えるメッセージはコールバックを通してテキストエリアに表示される.
ユーザはテキストエリアの内容をコピーして受信側のテキストエリアにペーストする.
受信側で処理が終わってAnswerのテキストが取得できると,それをテキストエリアにペーストしてボタンを押す.
webrtc()メソッドは下記の通り.
var msg_type = {
offer: "offer",
candidate: "candidate",
answer: "answer"
}
// 配信側はこのメソッドを呼び出す.引数は,相手に配信するストリーム,受信側にメッセージを伝える場合のコールバック,エラーコールバック.
function start_webrtc(stream, sendMsg, error) {
var connection = new RTCPeerConnection(ice_config);
connection.addStream(stream);
connection.createOffer(
function(des) {
// Offerに成功すると呼び出される.
connection.setLocalDescription(des);
// ローカルにDescriptionをセットし,次は受信側connectionのRemoteにセットする必要があるため,メッセージコールバックを呼び出す.
sendMsg(des);
},
function() {
// エラーの場合
error();
}, mediaC
);
connection.onicecandidate = function(evt) {
if (evt.candidate) {
// candidateの場合は受信側に伝える
sendMsg({
type: msg_type.candidate,
sdpMLineIndex: evt.candidate.sdpMLineIndex,
sdpMid: evt.candidate.sdpMid,
candidate: evt.candidate.candidate
});
} else {
// それ以外の場合は全てのcandidateが揃った場合なので,リストにcandidateを貯めてここで相手側に伝えても構わない
}
}
return connection;
}
function on_answer(connection, msg) {
// connectionにAnswerをセットして通信を開始する
connection.setRemoteDescription(new RTCSessionDescription(msg));
}
onicecandidateは複数回呼ばれる点に注意.
例えばvideoの通信路とaudioの通信路といった形で別々に呼ばれる.まとめて送る方が合理的だけれど,とりあえずこのライブラリでは余計なことをせず,呼び出す側に任せている.
適当にtextareaを作っておいて,そこに相手に渡すメッセージを追記していく.
単純に追記して改行でパースしてもいいけれど,面倒なのでJSONのリストで管理している.
受信側:コネクションの作成
受信側の実装は下記の通り.
ちなみに受信側はカメラを取得しないのでSSLである必要は無い.
<!DOCTYPE html>
<html>
<head>
<title>Viewer</title>
<script src="js/libwebrtc.js"></script>
</head>
<body>
<div>
<video id="remote" autoplay="1"></video>
</div>
<form>
<textarea id="textarea" rows=5 cols=50></textarea>
<input type="button" id="button" onclick="receive()" value="onOffer"></input>
</form>
<script>
var remote = document.getElementById("remote");
var control = document.getElementById("control");
var msg_obj = [];
var textarea = document.getElementById("textarea");
function appendMessage(msg) {
msg_obj.push(msg);
console.log("TEXT", msg_obj);
textarea.value = JSON.stringify(msg_obj);
}
// メッセージを受け取ると呼び出される
function receive() {
msg_obj = [];
var msg = JSON.parse(textarea.value);
console.log("msg", msg);
var connection = null;
var candidate = [];
// テキストエリアに入力されたメッセージはリストになっているのでtype別に分解する
msg.some(function(v, i) {
// offerの場合は1つしか無い(はず)なのでon_offerを呼び出す
if (v.type == msg_type.offer) {
// offerに成功するとAnswerを配信側に伝える必要があるため,テキストエリアを更新する
connection = on_offer(v, remote, appendMessage, function() {
console.log("Error");
});
} else if (v.type == msg_type.candidate) {
// candidateは複数あるのでリストに追記する
candidate.push(v);
}
});
if (connection != null) {
// 受け取ったcandidateを全てセットする.
candidate.some(function(v, i) {
on_icecandidate(connection, v);
});
}
}
</script>
</body>
</html>
受信側のテキストエリアにメッセージをペーストしたらonOfferのボタンを押下してreceive()
メソッドを呼び出す.
JSONのリストになっているはずなので,分解してon_offerとon_icecandidateを呼び出す.
複数のカメラを閲覧することを考えるとreceiveしてからvideoタグを作成して追加する方がスマートかもしれない.
Offerに成功したら今度はAnswerを配信側に伝えなければならないので,Answerをテキストエリアに表示させる.
ユーザは再度このメッセージをコピーし,配信側のテキストエリアにペースト,onAnswerのボタンを押下して完了する.
on_offerとon_icecandidateの実装は下記の通り.
// offerを受け取ったら呼び出すメソッド.受け取ったメッセージ,ストリームを表示するvideo要素,相手にメッセージを渡すコールバックを引数に指定する.
function on_offer(offer_msg, viewer, sendMsg) {
var connection = new RTCPeerConnection(ice_config);
connection.addEventListener("addstream", function(evt) {
// ストリームが追加されると呼び出される
// 実際には接続の処理が完了し,ICEの通信路が確立されてからになる
console.log("Add stream");
// evt.streamに相手側のストリームが格納されているのでblob情報に変換してviewerのsrcに指定する
// canvasでお絵かきする場合はフレーム毎にvideoをcanvasに書き出す
viewer.src = URL.createObjectURL(evt.stream);
console.log("stream src", viewer.src);
}, false);
connection.addEventListener("removestream", function(evt) {
// ストリームが削除された場合に呼び出される.接続が切れた場合にも呼び出される(っぽい)
viewer.src = "";
}, false);
// 受け取ったOfferをRemoteにセットする
connection.setRemoteDescription(new RTCSessionDescription(offer_msg));
// Answerを作成
connection.createAnswer(
function(des) {
// 成功したらLocalにセットして配信側にAnswerを伝える
connection.setLocalDescription(des);
sendMsg(des);
},
function() {
// error
}, mediaC);
return connection;
}
function on_icecandidate(connection, candidate) {
connection.addIceCandidate(new RTCIceCandidate(candidate));
}
基本的には受け取った情報をセットして,相手に伝える情報をコールバックするだけなのでとても簡単.
まとめ
まずはコピペでシグナリングしてWebRTCの通信を開始.
コピペした部分をなんらかの方法で相手に伝えればいいので,nodejsなりなんなりを使えばいい.
で,次は予告した通りこれをQRコードでやってみる.