Quantcast
Viewing all articles
Browse latest Browse all 8695

自前ビデオチャットサーバーを立てる

はじめに

外部につながらないネットワークの中でビデオチャットをしたい

参考にしたところ

nginxを立てる

準備

HTTPSのための証明書を準備する

[root@localhost nginx]# mkdir ssl
[root@localhost nginx]# openssl genrsa 2048 > ssl/key.pem
Generating RSA private key, 2048 bit long modulus
.........+++
..........................+++
e is 65537 (0x10001)
[root@localhost nginx]# openssl req -new -key ssl/key.pem > ssl/cacert.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [XX]:JP
State or Province Name (full name) []:
Locality Name (eg, city) [Default City]:
Organization Name (eg, company) [Default Company Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
[root@localhost nginx]# openssl x509 -days 3650 -req -signkey ssl/key.pem < ssl/cacert.pem > ssl/cert.pem
Signature ok
subject=/C=JP/L=Default City/O=Default Company Ltd
Getting Private key
  • confの変更
conf.d/default.conf
server {
    listen 443 ssl;
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    location / {
        index index.html index.htm;
        root /usr/share/nginx/html;
    }
}
  • 動作確認
# docker run -p 443:443 -p 80:80 -v $(pwd)/ssl:/etc/nginx/ssl -v $(pwd)/conf.d:/etc/nginx/conf.d  nginx

Image may be NSFW.
Clik here to view.
スクリーンショット 2020-07-01 23.23.06.png

https://qiita.com/colomney/items/887f9ea7b68a3b427060
を参考にしてchromeで表示できるようにする

カメラをブラウザから使う

- 参考:https://html5experts.jp/mganeko/19728/

index.html
<!doctype html><html><head><metahttp-equiv="Content-Type"content="text/html; charset=UTF-8"><title>Wrap old and new getUserMedia</title></head><body>
  Wrap old and new getUserMedia<br/><buttontype="button"onclick="startVideo();">Start</button><buttontype="button"onclick="stopVideo();">Stop</button><br/><videoid="local_video"autoplaystyle="width: 320px; height: 240px; border: 1px solid black;"></video></body><script type="text/javascript">varlocalVideo=document.getElementById('local_video');varlocalStream=null;// --- prefix -----navigator.getUserMedia=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.msGetUserMedia;// ---------------------- video handling ----------------------- // start local videofunctionstartVideo(){getDeviceStream({video:true,audio:false}).then(function(stream){// successlocalStream=stream;playVideo(localVideo,stream);}).catch(function(error){// errorconsole.error('getUserMedia error:',error);return;});}// stop local videofunctionstopVideo(){pauseVideo(localVideo);stopLocalStream(localStream);}functionstopLocalStream(stream){lettracks=stream.getTracks();if(!tracks){console.warn('NO tracks');return;}for(lettrackoftracks){track.stop();}}functiongetDeviceStream(option){if('getUserMedia'innavigator.mediaDevices){console.log('navigator.mediaDevices.getUserMadia');returnnavigator.mediaDevices.getUserMedia(option);}else{console.log('wrap navigator.getUserMadia with Promise');returnnewPromise(function(resolve,reject){navigator.getUserMedia(option,resolve,reject);});}}functionplayVideo(element,stream){if('srcObject'inelement){element.srcObject=stream;}else{element.src=window.URL.createObjectURL(stream);}element.play();element.volume=0;}functionpauseVideo(element){element.pause();if('srcObject'inelement){element.srcObject=null;}else{if(element.src&&(element.src!=='')){window.URL.revokeObjectURL(element.src);}element.src='';}}</script></html>
# docker run -p 443:443 -v $(pwd)/ssl:/etc/nginx/ssl -v $(pwd)/conf.d:/etc/nginx/conf.d -v $(pwd)/html:/usr/share/nginx/html 
 nginx

Image may be NSFW.
Clik here to view.
スクリーンショット 2020-07-01 23.51.27.png

シグナリングサーバーを立てる

FROM node:12# アプリケーションディレクトリを作成するWORKDIR /usr/src/app# アプリケーションの依存関係をインストールする# ワイルドカードを使用して、package.json と package-lock.json の両方が確実にコピーされるようにします。# 可能であれば (npm@5+)COPY package*.json ./RUN npm install# 本番用にコードを作成している場合# RUN npm install --only=production# アプリケーションのソースをバンドルするCOPY signaling.js ./# サーバー証明書COPY ssl /etc/sslCMD [ "node", "signaling.js" ]
package.json
{"name":"node-signaling-server","version":"1.0.0","description":"Sigaling Server by Node.js on Docker","author":"First Last <first.last@example.com>","main":"signaling.js","scripts":{"start":"node signaling.js"},"dependencies":{"ws":"^7.3.0"}}

httpsを使っていので、WebSocketもSecure WebSocketにする必要があります。
(証明書はnginxと同じもをとりあえず使用)

signaling.js
"use strict";letfs=require('fs');lethttps=require('https');letwebSocketServer=require('ws').Server;letport=3001;lethttpsServer=https.createServer({cert:fs.readFileSync('/etc/ssl/cert.pem'),ca:fs.readFileSync('/etc/ssl/cacert.pem'),key:fs.readFileSync('/etc/ssl/key.pem')}).listen(port);letwssServer=newwebSocketServer({server:httpsServer});console.log('secure websocket server start. port='+port);wssServer.on('connection',function(wss){console.log('-- secure websocket connected --');wss.on('message',function(message){wssServer.clients.forEach(functioneach(client){if(isSame(wss,client)){console.log('- skip sender -');}else{client.send(message);}});});});functionisSame(ws1,ws2){// -- compare object --return(ws1===ws2);}
".dockerignore"
node_modules
npm-debug.log

ビルドと実行

# docker build . -t node-signaling-server
# docker run -d -p 3001:3001 node-signaling-server

1対1でつなげる

wc_1to1.html
<!doctype html><html><head><metahttp-equiv="Content-Type"content="text/html; charset=UTF-8"><title>WebSocket Signaling 1to1</title></head><body>
  WebSocket Signaling 1to1 (trickle ICE)<br/><buttontype="button"onclick="startVideo();">Start Video</button><buttontype="button"onclick="stopVideo();">Stop Video</button>&nbsp;<buttontype="button"onclick="connect();">Connect</button><buttontype="button"onclick="hangUp();">Hang Up</button><div><videoid="local_video"autoplaystyle="width: 160px; height: 120px; border: 1px solid black;"></video><videoid="remote_video"autoplaystyle="width: 160px; height: 120px; border: 1px solid black;"></video></div><p>SDP to send:<br/><textareaid="text_for_send_sdp"rows="5"cols="60"readonly="readonly">SDP to send</textarea></p><p>SDP received:&nbsp;<!--
    <button type="button" onclick="onSdpText();">Receive remote SDP</button>
    --><br/><textareaid="text_for_receive_sdp"rows="5"cols="60"></textarea></p></body><script type="text/javascript">letlocalVideo=document.getElementById('local_video');letremoteVideo=document.getElementById('remote_video');letlocalStream=null;letpeerConnection=null;lettextForSendSdp=document.getElementById('text_for_send_sdp');lettextToReceiveSdp=document.getElementById('text_for_receive_sdp');// --- prefix -----navigator.getUserMedia=navigator.getUserMedia||navigator.webkitGetUserMedia||navigator.mozGetUserMedia||navigator.msGetUserMedia;RTCPeerConnection=window.RTCPeerConnection||window.webkitRTCPeerConnection||window.mozRTCPeerConnection;RTCSessionDescription=window.RTCSessionDescription||window.webkitRTCSessionDescription||window.mozRTCSessionDescription;// -------- websocket ----// please use node.js app//  // or you can use chrome app (only work with Chrome)//  https://chrome.google.com/webstore/detail/simple-message-server/bihajhgkmpfnmbmdnobjcdhagncbkmmp//letwsUrl='wss://192.168.111.106:3001/';letws=newWebSocket(wsUrl);ws.onopen=function(evt){console.log('ws open()');};ws.onerror=function(err){console.error('ws onerror() ERR:',err);};ws.onmessage=function(evt){console.log('ws onmessage() data:',evt.data);letmessage=JSON.parse(evt.data);if(message.type==='offer'){// -- got offer ---console.log('Received offer ...');textToReceiveSdp.value=message.sdp;letoffer=newRTCSessionDescription(message);setOffer(offer);}elseif(message.type==='answer'){// --- got answer ---console.log('Received answer ...');textToReceiveSdp.value=message.sdp;letanswer=newRTCSessionDescription(message);setAnswer(answer);}elseif(message.type==='candidate'){// --- got ICE candidate ---console.log('Received ICE candidate ...');letcandidate=newRTCIceCandidate(message.ice);console.log(candidate);addIceCandidate(candidate);}};// ---------------------- media handling ----------------------- // start local videofunctionstartVideo(){getDeviceStream({video:true,audio:false}).then(function(stream){// successlocalStream=stream;playVideo(localVideo,stream);}).catch(function(error){// errorconsole.error('getUserMedia error:',error);return;});}// stop local videofunctionstopVideo(){pauseVideo(localVideo);stopLocalStream(localStream);}functionstopLocalStream(stream){lettracks=stream.getTracks();if(!tracks){console.warn('NO tracks');return;}for(lettrackoftracks){track.stop();}}functiongetDeviceStream(option){if('getUserMedia'innavigator.mediaDevices){console.log('navigator.mediaDevices.getUserMadia');returnnavigator.mediaDevices.getUserMedia(option);}else{console.log('wrap navigator.getUserMadia with Promise');returnnewPromise(function(resolve,reject){navigator.getUserMedia(option,resolve,reject);});}}functionplayVideo(element,stream){if('srcObject'inelement){element.srcObject=stream;}else{element.src=window.URL.createObjectURL(stream);}element.play();element.volume=0;}functionpauseVideo(element){element.pause();if('srcObject'inelement){element.srcObject=null;}else{if(element.src&&(element.src!=='')){window.URL.revokeObjectURL(element.src);}element.src='';}}// ----- hand signaling ----functiononSdpText(){lettext=textToReceiveSdp.value;if(peerConnection){console.log('Received answer text...');letanswer=newRTCSessionDescription({type:'answer',sdp:text,});setAnswer(answer);}else{console.log('Received offer text...');letoffer=newRTCSessionDescription({type:'offer',sdp:text,});setOffer(offer);}textToReceiveSdp.value='';}functionsendSdp(sessionDescription){console.log('---sending sdp ---');textForSendSdp.value=sessionDescription.sdp;/*---
    textForSendSdp.focus();
    textForSendSdp.select();
    ----*/letmessage=JSON.stringify(sessionDescription);console.log('sending SDP='+message);ws.send(message);}functionsendIceCandidate(candidate){console.log('---sending ICE candidate ---');letobj={type:'candidate',ice:candidate};letmessage=JSON.stringify(obj);console.log('sending candidate='+message);ws.send(message);}// ---------------------- connection handling -----------------------functionprepareNewConnection(){letpc_config={"iceServers":[]};letpeer=newRTCPeerConnection(pc_config);// --- on get remote stream ---if('ontrack'inpeer){peer.ontrack=function(event){console.log('-- peer.ontrack()');letstream=event.streams[0];playVideo(remoteVideo,stream);};}else{peer.onaddstream=function(event){console.log('-- peer.onaddstream()');letstream=event.stream;playVideo(remoteVideo,stream);};}// --- on get local ICE candidatepeer.onicecandidate=function(evt){if(evt.candidate){console.log(evt.candidate);// Trickle ICE の場合は、ICE candidateを相手に送るsendIceCandidate(evt.candidate);// Vanilla ICE の場合には、何もしない}else{console.log('empty ice event');// Trickle ICE の場合は、何もしない// Vanilla ICE の場合には、ICE candidateを含んだSDPを相手に送る//sendSdp(peer.localDescription);}};// --- when need to exchange SDP ---peer.onnegotiationneeded=function(evt){console.log('-- onnegotiationneeded() ---');};// --- other events ----peer.onicecandidateerror=function(evt){console.error('ICE candidate ERROR:',evt);};peer.onsignalingstatechange=function(){console.log('== signaling status='+peer.signalingState);};peer.oniceconnectionstatechange=function(){console.log('== ice connection status='+peer.iceConnectionState);if(peer.iceConnectionState==='disconnected'){console.log('-- disconnected --');hangUp();}};peer.onicegatheringstatechange=function(){console.log('==***== ice gathering state='+peer.iceGatheringState);};peer.onconnectionstatechange=function(){console.log('==***== connection state='+peer.connectionState);};peer.onremovestream=function(event){console.log('-- peer.onremovestream()');pauseVideo(remoteVideo);};// -- add local stream --if(localStream){console.log('Adding local stream...');peer.addStream(localStream);}else{console.warn('no local stream, but continue.');}returnpeer;}functionmakeOffer(){peerConnection=prepareNewConnection();peerConnection.createOffer().then(function(sessionDescription){console.log('createOffer() succsess in promise');returnpeerConnection.setLocalDescription(sessionDescription);}).then(function(){console.log('setLocalDescription() succsess in promise');// -- Trickle ICE の場合は、初期SDPを相手に送る -- sendSdp(peerConnection.localDescription);// -- Vanilla ICE の場合には、まだSDPは送らない --}).catch(function(err){console.error(err);});}functionsetOffer(sessionDescription){if(peerConnection){console.error('peerConnection alreay exist!');}peerConnection=prepareNewConnection();peerConnection.setRemoteDescription(sessionDescription).then(function(){console.log('setRemoteDescription(offer) succsess in promise');makeAnswer();}).catch(function(err){console.error('setRemoteDescription(offer) ERROR: ',err);});}functionmakeAnswer(){console.log('sending Answer. Creating remote session description...');if(!peerConnection){console.error('peerConnection NOT exist!');return;}peerConnection.createAnswer().then(function(sessionDescription){console.log('createAnswer() succsess in promise');returnpeerConnection.setLocalDescription(sessionDescription);}).then(function(){console.log('setLocalDescription() succsess in promise');// -- Trickle ICE の場合は、初期SDPを相手に送る -- sendSdp(peerConnection.localDescription);// -- Vanilla ICE の場合には、まだSDPは送らない --}).catch(function(err){console.error(err);});}functionsetAnswer(sessionDescription){if(!peerConnection){console.error('peerConnection NOT exist!');return;}peerConnection.setRemoteDescription(sessionDescription).then(function(){console.log('setRemoteDescription(answer) succsess in promise');}).catch(function(err){console.error('setRemoteDescription(answer) ERROR: ',err);});}// --- tricke ICE ---functionaddIceCandidate(candidate){if(peerConnection){peerConnection.addIceCandidate(candidate);}else{console.error('PeerConnection not exist!');return;}}// start PeerConnectionfunctionconnect(){if(!peerConnection){console.log('make Offer');makeOffer();}else{console.warn('peer already exist.');}}// close PeerConnectionfunctionhangUp(){if(peerConnection){console.log('Hang up.');peerConnection.close();peerConnection=null;pauseVideo(remoteVideo);}else{console.warn('peer NOT exist.');}}</script></html>

複数接続

package.json
{"name":"node-signaling-server","version":"1.0.0","description":"Sigaling Server by Node.js on Docker","author":"First Last <first.last@example.com>","main":"signaling.js","scripts":{"start":"node signaling.js"},"dependencies":{"socket.io":"^2.3.0"}}
signaling.js
"use strict";constfs=require('fs');consthttps=require('https');consthttpsServer=https.createServer({cert:fs.readFileSync('/etc/ssl/cert.pem'),ca:fs.readFileSync('/etc/ssl/cacert.pem'),key:fs.readFileSync('/etc/ssl/key.pem')});constio=require('socket.io')(httpsServer);constport=3002;httpsServer.listen(port);console.log('secure signaling server started on port:'+port);// This callback function is called every time a socket// tries to connect to the serverio.on('connection',function(socket){// ---- multi room ----socket.on('enter',function(roomname){socket.join(roomname);console.log('id='+socket.id+' enter room='+roomname);setRoomname(roomname);});functionsetRoomname(room){socket.roomname=room;}functiongetRoomname(){varroom=socket.roomname;returnroom;}functionemitMessage(type,message){// ----- multi room ----varroomname=getRoomname();if(roomname){console.log('===== message broadcast to room -->'+roomname);socket.broadcast.to(roomname).emit(type,message);}else{console.log('===== message broadcast all');socket.broadcast.emit(type,message);}}// When a user send a SDP message// broadcast to all users in the roomsocket.on('message',function(message){vardate=newDate();message.from=socket.id;console.log(date+'id='+socket.id+' Received Message: '+JSON.stringify(message));// get send targetvartarget=message.sendto;if(target){console.log('===== message emit to -->'+target);socket.to(target).emit('message',message);return;}// broadcast in roomemitMessage('message',message);});// When the user hangs up// broadcast bye signal to all users in the roomsocket.on('disconnect',function(){// close user connectionconsole.log((newDate())+' Peer disconnected. id='+socket.id);// --- emit ----emitMessage('user disconnected',{id:socket.id});// --- leave room --varroomname=getRoomname();if(roomname){socket.leave(roomname);}});});
wc_multi.html
こちらは参照先のソースのhttp->httpsにしてアドレス指定しただけなので割愛します

Viewing all articles
Browse latest Browse all 8695

Trending Articles