WebRTCを使ったビデオチャットサンプル
WebRTCを使うとブラウザ間で映像や音声をやり取りすることができます。あらかじめメールをやり取りする必要はなく、自社のサイト内に専用のビデオ会議用ページをつくることもできる。
ただし、設置するには「シグナリング・サーバー」と呼ばれる通信用のサーバーを自前で設置する必要がある。WebRTCは、ビデオ会議・チャット用のホームページを置いたサーバーと、シグナリング・サーバー、IPアドレスを調整するSTUN/TURNサーバーの3つのサーバーを介してブラウザ間の通信を構築し、構築後は直接ブラウザ同士でやり取りする。
WebRTCのイメージ |
---|
![]() |
社内ネットワークなどでWebRTCを構築するのは比較的容易だが、顧客など不特定多数が利用するには、シグナリング・サーバーを外部に公開する必要がある。
シグナリング・サーバーを公開する場合、通常は独自ドメインをシグナリング・サーバー用に新たに取得し、当該シグナリング・サーバーと独自ドメインを紐づける必要がある。
結論としては、社内用はともかく、外部とのリモートワークならZoomやSkypeの方が(はるかに)簡単。以下のコードは参考までに。
//シグナリングサーバへの接続
var localStream=null;
var peerConnections=[];
//local:ローカル側,remote:リモート側
var video=document.querySelector('#localVideo');
var remoteVideos=[];
//リモート側の映像の表示場所
var remoteVideoContainer=document.querySelector('#remoteVideoContainer');
var constraints={audio:true,video:{width:400,height:300}};
const MAX_CONNECTIONS=8;
//接続の有無
var connection=true;
var present_remote_id=null;
//シグナリング・サーバへの接続(フロント側⇒シグナリング・サーバ)
const port=8080;
let socket=io.connect('https://example.luckyhawk.work:'+port+'/');
let room='kuckyhawk';
socket.on('connect',function(event){
socket.emit('enter',room);
console.log('Socket.IO connected. ID:'+socket.id);
});
//socket.onによるメッセージ処理
socket.on('message',function(message){
let from=message.from;
switch(message.type){
case 'offer':{
console.log('--offer from '+from);
let offer=new window.RTCSessionDescription(message);
setOffer(from,offer);
break;
}
case 'answer':{
console.log('--answer from '+from);
let answer=new window.RTCSessionDescription(message);
setAnswer(from,answer);
break;
}
case 'candidate':{
console.log('--candidate from '+from);
let candidate=new window.RTCIceCandidate(message.ice);
//addIceCandidate(from,candidate);
console.log('add ICE cadidate.');
if(!isConnected(from)){
console.warn('Not connected or already closed from id:'+from);
break;
}
let peerConnection=getConnection(from);
if(!peerConnection){
console.error('PeerConnection does not exist.');
break;
}
peerConnection.addIceCandidate(candidate);
break;
}
case 'call'://リモートからの接続要求が来た場合
console.log('--call from '+from);
if(connection==false) break;
if(!localStream){
console.log('Not ready for connecting.');
break;
}
if(peerConnections.length>=MAX_CONNECTIONS){
console.warn('Too many connections.');
console.log('keys:'+Object.keys(peerConnections));
break;
}
if(isConnected(from)){
console.log('already connected.');
break;
}
createOffer(from);
break;
case 'bye':
console.log('-- bye from '+from);
stopConnection(from);
break;
}
});
socket.on('user disconnected',function(event){
stopConnection(event.id);
});
//peer connection 関数
function displayPeerConnections(){
console.log('display peer connections list.');
Object.keys(peerConnections).forEach(function(key){
console.log(key,peerConnections[key]);
});
}
function isConnected(id){
if(!peerConnections[id]){
console.log('not connected. ID:'+id);
return false;
}else{
console.log('connected. ID:'+id);
return true;
}
}
function addConnection(id,peer){
if(peerConnections[id]){
console.log('peer already listed.');
}else{
peerConnections[id]=peer;
if(peerConnections[id]){
console.log('secceeded to add. ID:'+id);
}else{
console.log('failed to add. ID:'+id);
}
}
}
function getConnection(id){
if(isConnected(id)){
return peerConnections[id];
}else{
return null;
}
}
function stopConnection(id){
detachRemoteVideo(id);
if(isConnected(id)){
let peer=getConnection(id);
peer.close();
delete peerConnections[id];
}
}
function stopAllConnection(){
for(let id in peerConnections){
detachRemoteVideo(id);
if(isConnected(id)){
let peer=getConnection(id);
peer.close();
delete peerConnections[id];
}
stopConnection(id);
}
}
//remote video 関数
function attachRemoteVideo(id,stream){
let video=document.createElement('video');
video.height=window.innerHeight-100;
video.id=id;
console.log('video id:'+id);
present_remote_id=id;
video.style.padding='5px';
remoteVideoContainer.appendChild(video);
remoteVideos[id]=video;
video.srcObject=stream;
video.play();
console.log('attach remote video.');
}
//ウィンドウリサイズ時のリモート側ビデオのサイズ変更
window.addEventListener('resize',function(){
let video=document.getElementById(present_remote_id);
video.width=window.innerWidth*0.7;
},false)
function detachRemoteVideo(id){
if(remoteVideos[id]){
remoteVideos[id].pause();
delete remoteVideos[id];
let video=document.getElementById(id);
remoteVideoContainer.removeChild(video);
}
}
function createOffer(id){
console.log('create offer.');
let peerConnection=createPeerConnection(id);
addConnection(id,peerConnection);
//offerの生成
peerConnection.createOffer()
.then(function(sessionDescription){
console.log('type:'+sessionDescription.type);
return peerConnection.setLocalDescription(sessionDescription);
}).then(function(){
console.log('set local description.');
//Vanilla ICE
sendSdp(id,peerConnection.localDescription);
}).catch(function(error){
console.error(error);
});
}
function setOffer(id,sessionDescription){
console.log('set offer.');
let peerConnection=createPeerConnection(id);
console.log('peer who offered has been added to peerConnections.');
addConnection(id,peerConnection);
peerConnection.setRemoteDescription(sessionDescription)
.then(function(){
console.log('set remote description.');
createAnswer(id);
}).catch(function(error){
console.error('(offer)set remote description error: ',error);
});
}
function createAnswer(id){
console.log('create answer.');
let peerConnection=getConnection(id);
if(!peerConnection) return;
peerConnection.createAnswer()
.then(function(sessionDescription){
console.log('type:'+sessionDescription.type);
return peerConnection.setLocalDescription(sessionDescription);
}).then(function(){
console.log('set local description.');
//Trickle ICE
sendSdp(id,peerConnection.localDescription);
}).catch(function(error){
console.log(error);
});
}
function setAnswer(id,sessionDescription){
console.log('set answer.');
let peerConnection=getConnection(id);
if(!peerConnection) return;
peerConnection.setRemoteDescription(sessionDescription)
.then(function(){
console.log('set remote description.');
}).catch(function(error){
console.error('(answer)set remote description error: ',error);
});
}
function createPeerConnection(id){
//TURN,STUNサーバ
let pcConfig={"iceServers":[{"url": "stun:stun.l.google.com:19302"}]};
let peer=new RTCPeerConnection(pcConfig);
//ストリームがある場合
if('ontrack'in peer){
console.log('--ontrack--');
peer.ontrack=function(event){
//すでにリモート側の映像・音声ストリームを受信している場合
if(remoteVideos[id]){
console.log('already stream attached.');
}else{
//eventのストリームに接続
let stream=event.streams[0];
attachRemoteVideo(id,stream);
}
};
console.log('ontrack end.');
}else{
console.log('--onaddstream--');
peer.onaddstream=function(event){
let stream=event.stream;
attachRemoteVideo(id,stream);
}
}
//ICE Candidateの送信
peer.onicecandidate=function(event){
console.log('on ICE Candidate.');
if(event.candidate){//新しい候補者がいる場合
let msg={type:'candidate',ice:event.candidate};
if(isConnected(id)){
msg.sendto=id;
socket.emit('message',msg);
}else{
//Trickle ICE
}
}
};
peer.oniceconnectionstatechange=function(){
switch(peer.iceConnectionState){
case 'closed':
case 'failed':
stopConnection(id);
break;
case 'disconnected':
break;
}
};
peer.onremovestream=function(event){
detachRemoteVideo(id);
}
//localStreamの追加
if(localStream){
localStream.getTracks().forEach(track=>peer.addTrack(track,localStream));
}
return peer;
}
function sendSdp(id,sessionDescription){
console.log('send SDP to '+id);
//シグナリング・サーバにSDPを送る
let msg={type: sessionDescription.type,sdp: sessionDescription.sdp};
msg.sendto=id;
socket.emit('message',msg);
}
function hangUp(){
socket.emit('message',{type: 'bye'});
stopAllConnection();
}
$(function(){
connect();
$('#make-call').click(function(){
console.log('call');
if(peerConnections.length>=MAX_CONNECTIONS) return;
connection=true;
socket.emit('message',{type:'call'});
});
$('#end-call').click(function(){
hangUp();
connection=false;
console.log('disconnected');
});
});
async function connect(){
console.log('connect.');
await navigator.mediaDevices.getUserMedia(constraints)
.then(function(mediaStream){
if(mediaStream) console.log('media stream.');
localStream=mediaStream;
video.srcObject=mediaStream;
video.onloadedmetadata=function(e){
video.play();
};
}).catch(function(error){
console.error('mediaDevices.getUserMedia error:',error);
});
if(localStream){
console.log('local stream generated.');
}else{
console.log('local stream not generated.');
}
socket.emit('message',{type:'call'});
}
function showMyVideo(){
console.log('show my video.');
navigator.mediaDevices.getUserMedia(constraints)
.then(function(mediaStream){
if(mediaStream) console.log('media stream.');
localStream=mediaStream;
video.srcObject=mediaStream;
video.onloadedmetadata=function(e){
video.play();
};
}).catch(function(error){
console.error('mediaDevices.getUserMedia error:',error);
});
if(localStream){
console.log('local stream generated.');
}else{
console.log('local stream not generated.');
}
};
ビデオチャットサンプル
(独自ドメインの契約を更新してないので今は使えません)