This commit is contained in:
bel
2021-09-14 06:30:17 -06:00
commit 7ab1723a5e
327 changed files with 127104 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
webRTC-over-websockets
======================
Small demo of webRTC over websockets, using node.js and HTML5. It allows you to setup peer2peer and group chat calls. You can also add new users on the fly (but not rooms, there are predefined - see `data` directory).
Ideally, would be best to use XMPP (with strophe.js library) for signaling since it has presence and roster support, with BOSH or even better websockets over XMPP.
Files
-------
### Data
- `data/people.json` this is the roster file, where people are defined.
- `data/rooms.json` this is where rooms are defined
### JS files
- `js/main.js` this is the file responsible for UI updates
- `js/webrtc.js` this is the webrtc wrapper
- `js/signaling.js` signalling client, implemented using websockets
- `js/bandwitdh.js` this is the file for bandwith estimation
- `js/adapter.js` this is the file used in webrtc demo's, it wraps browser dependencies
### Server
- `websocket-server.js` this is the websocket server, used by node.js. It requires websocket library, you can install it using the following command :
- $ npm install websocket
After this you can run the server with:
$ node server.js [settings.json]
- `index.html` portal page (see settings below)
#### Settings
Defined in settings.json, in case you don't provide it:
- refreshRate [default 5s], refresh presence rate to all connected users
- autoCall [default true], whether to call all people in the room as soon as you connect to it
- acceptNewUsers [default false], whether to accept users that are not defined in people.json file
Things to change if you install the app on your server:
- in `js/main.js` change the websocket address to point to your server
- change json files `data/people.json` and `data/rooms.json` with your address book
- change images

View File

@@ -0,0 +1,56 @@
[
{
"name" : "Veselin",
"id": "veselin",
"img" : "images/avatar.png",
"room" : ["work", "home", "webrtc"]
},
{
"name" : "Johan",
"id": "johan",
"img" : "images/johan.jpg",
"room" : ["work"]
},
{
"name" : "Rob",
"id": "rob",
"img" : "images/rob.jpg",
"room" : ["work"]
},
{
"name" : "Pieter",
"id": "pieter",
"img" : "images/pieter.jpg",
"room" : ["work"]
},
{
"name" : "Andre",
"id": "andre",
"img" : "images/andre.jpg",
"room" : ["webrtc"]
},
{
"name" : "Jonas",
"id": "jonas",
"img" : "images/jonas.jpg",
"room" : ["work" , "webrtc"]
},
{
"name" : "Jo",
"id": "jo",
"img" : "images/jo.jpg",
"room" : ["work"]
},
{
"name" : "Nick",
"id": "nikola",
"img" : "images/nikola.jpg",
"room" : ["home"]
},
{
"name" : "Christof",
"id": "christof",
"img" : "images/christof.jpg",
"room" : ["work"]
}
]

Binary file not shown.

View File

@@ -0,0 +1,17 @@
[
{
"name" : "Home",
"id": "home",
"img" : "images/home.jpg"
},
{
"name" : "webrtc",
"id": "webrtc",
"img" : "images/webrtc.jpg"
},
{
"name" : "work",
"id": "work",
"img" : "images/work.jpg"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebRTC</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<!-- Le styles -->
<link href="js/bootstrap/css/bootstrap.css" rel="stylesheet">
<style type="text/css">
body {
padding-top: 40px;
padding-bottom: 40px;
}
video {
width: 100%;
height: 100%;
float: left;
}
.sidebar-nav {
padding: 9px 0;
}
@media (max-width: 980px) {
/* Enable use of floated navbar text */
.navbar-text.pull-right {
float: none;
padding-left: 5px;
padding-right: 5px;
}
}
</style>
</head>
<body>
<div id="main">
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<div class="nav-collapse collapse">
<ul class="nav">
<li><a href="#" onclick="talkFunction(false)"><i class="icon-user icon-white"></i> People</a></li>
<li><a href="#" onclick="talkFunction(true)"><i class="icon-facetime-video icon-white"></i> Talk</a></li>
</ul>
<form id="form" class="navbar-form pull-right">
<input id="user" class="span2" type="text" placeholder="User name">
<button id="submit" type="submit" class="btn">Sign in</button>
</form>
</div><!--/.nav-collapse -->
</div>
</div>
</div>
<div class="container-fluid">
<div class="row-fluid">
<div id="people">
<div id="people_content">
<div class="carousel slide span10" id="myPeopleCarousel">
<div class="carousel-inner people-carousel"></div>
<a data-slide="prev" href="#myPeopleCarousel" class="left carousel-control">&lsaquo;</a>
<a data-slide="next" href="#myPeopleCarousel" class="right carousel-control">&rsaquo;</a>
</div>
</div>
<div class="thumbnail span2">
<video id="sourceSmallvid" autoplay></video>
</div>
<div id="room_content">
<div class="carousel slide span10" id="myRoomCarousel">
<div class="carousel-inner rooms-carousel"></div>
<a data-slide="prev" href="#myRoomCarousel" class="left carousel-control">&lsaquo;</a>
<a data-slide="next" href="#myRoomCarousel" class="right carousel-control">&rsaquo;</a>
</div>
</div>
<div class="inner"></div><!--/inner-->
</div><!--/people-->
<div id="share" class="span12">
</div><!--/share-->
<div id="me" class="span12">
</div><!--/me-->
</div><!--/row-->
</div><!--/.fluid-container-->
</div><!--/.main-->
<div id="acceptModal" class="modal hide fade">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 id="callerTitle"></h3>
</div>
<div class="modal-body">
<p id="caller"></p>
</div>
<div class="modal-footer">
<a id="accept" href="#" class="btn btn-primary">Accept</a>
<a id="reject" href="#" class="btn">Decline</a>
</div>
</div>
<div id="userModal" class="modal hide fade">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3 id="userTitle"></h3>
</div>
<div class="modal-body">
<p id="callerUser"></p>
</div>
<div class="modal-footer">
<a id="acceptNewUser" href="#" class="btn btn-primary">Accept</a>
<a id="rejectNewUser" href="#" class="btn">Decline</a>
</div>
</div>
<!--<script src="http://code.jquery.com/jquery.js"></script> -->
<script src="js/jquery-1.7.2.min.js"></script>
<script src="js/bootstrap/js/bootstrap.min.js"></script>
<script src="js/adapter.js"></script>
<script src="js/bandwidth.js"></script>
<script src="js/webrtc.js"></script>
<script src="js/signaling.js"></script>
<script src="js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,89 @@
var RTCPeerConnection = null;
var getUserMedia = null;
var attachMediaStream = null;
var reattachMediaStream = null;
var webrtcDetectedBrowser = null;
if (navigator.mozGetUserMedia) {
console.log("This appears to be Firefox");
webrtcDetectedBrowser = "firefox";
// The RTCPeerConnection object.
RTCPeerConnection = mozRTCPeerConnection;
// The RTCSessionDescription object.
RTCSessionDescription = mozRTCSessionDescription;
// The RTCIceCandidate object.
RTCIceCandidate = mozRTCIceCandidate;
// Get UserMedia (only difference is the prefix).
// Code from Adam Barth.
getUserMedia = navigator.mozGetUserMedia.bind(navigator);
// Attach a media stream to an element.
attachMediaStream = function(element, stream) {
console.log("Attaching media stream");
element.mozSrcObject = stream;
element.play();
};
reattachMediaStream = function(to, from) {
console.log("Reattaching media stream");
to.mozSrcObject = from.mozSrcObject;
to.play();
};
// Fake get{Video,Audio}Tracks
MediaStream.prototype.getVideoTracks = function() {
return [];
};
MediaStream.prototype.getAudioTracks = function() {
return [];
};
} else if (navigator.webkitGetUserMedia) {
console.log("This appears to be Chrome");
webrtcDetectedBrowser = "chrome";
// The RTCPeerConnection object.
RTCPeerConnection = webkitRTCPeerConnection;
// Get UserMedia (only difference is the prefix).
// Code from Adam Barth.
getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
// Attach a media stream to an element.
attachMediaStream = function(element, stream) {
element.src = webkitURL.createObjectURL(stream);
};
reattachMediaStream = function(to, from) {
to.src = from.src;
};
// The representation of tracks in a stream is changed in M26.
// Unify them for earlier Chrome versions in the coexisting period.
if (!webkitMediaStream.prototype.getVideoTracks) {
webkitMediaStream.prototype.getVideoTracks = function() {
return this.videoTracks;
};
webkitMediaStream.prototype.getAudioTracks = function() {
return this.audioTracks;
};
}
// New syntax of getXXXStreams method in M26.
if (!webkitRTCPeerConnection.prototype.getLocalStreams) {
webkitRTCPeerConnection.prototype.getLocalStreams = function() {
return this.localStreams;
};
webkitRTCPeerConnection.prototype.getRemoteStreams = function() {
return this.remoteStreams;
};
}
} else {
console.log("Browser does not appear to be WebRTC-capable");
}

View File

@@ -0,0 +1,36 @@
//http://stackoverflow.com/questions/4583395/calculate-speed-using-javascript
/*
this is only download estimation, obviously, in peer2peer default deployment, you will
have full mesh, n*times both upstream and donwstream stream comming to you (which is bad,
but unless you have a server in between this is what it is). Remark: simetrical upstream/downstream
is not common in DSL deplyoment, where downstream bandwith is much higher.
*/
var BANDWITDH = (function(){
var imageAddr;
var size;
var startTime, endTime;
var downloadSize;
var download = new Image();
return {
/*
don't forget to change the address every time you make a request to avoid browser caching.
For a demo, I will be using the same as in the stackoverflow example
*/
init: function(callback, address, size){
imageAddr = address !== undefined ? address : "http://www.tranquilmusic.ca/images/cats/Cat2.JPG" + "?n=" + Math.random();
downloadSize = size !== undefined ? size : 5616998;
startTime = (new Date()).getTime();
download.src = imageAddr;
download.onload = function() {
endTime = (new Date()).getTime();
var duration = (endTime - startTime) / 1000;
var bitsLoaded = downloadSize * 8;
var speedBps = (bitsLoaded / duration).toFixed(2);
var speedKbps = (speedBps / 1024).toFixed(2);
var speedMbps = (speedKbps / 1024).toFixed(2);
callback(speedMbps);
}
},
}
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,258 @@
var socketAddress = 'ws://localhost:1337/';
var RTCApp = {
name: null,
webRTC: null,
commChannel: null,
message: null
};
RTCApp.commChannel = signaling({webSocketAddress : socketAddress, id: RTCApp.name });
RTCApp.commChannel.addCallback('presence' , presenceCallback);
RTCApp.commChannel.addCallback('s-offer', accept);
RTCApp.commChannel.addCallback('roster', roster);
RTCApp.webRTC = webrtc({sourcevid : document.getElementById('sourceSmallvid'),
stunServer : "stun.l.google.com:19302",
commChannel : RTCApp.commChannel,
onremote : remoteCallback,
constrains : 'dynamic'
});
RTCApp.webRTC.startVideo();
var users = {};
var caller, newUser;
var snd = new Audio("data/ringtone.wav");
var newUsers = {};
$(document).ready(function() {
//List of people and rooms is either retrived by uncommenting lines below,
//or after first successfull login (when the socket server send it to the logged user)
//loadFromJSON("data/people.json", "ajax-modal", ".people-carousel", false);
//loadFromJSON("data/rooms.json", "ajax-room-modal", ".rooms-carousel", true);
$('#people_content').hide();
$('#room_content').hide();
$('#people').fadeIn();
$('[id^="myCarousel"]').carousel({interval: false});
});
$("#form").submit(function(event) {
event.preventDefault();
RTCApp.name = $("#user").val();
RTCApp.commChannel.sendPresence(RTCApp.name, 'on');
$('#user').attr('readonly', true);
$("#submit").hide();
});
$('#accept').bind('click', function() {
RTCApp.commChannel.answer(caller, 'accept');
$('#acceptModal').modal('hide');
});
$('#reject').bind('click', function() {
RTCApp.commChannel.answer(caller, 'reject');
$('#acceptModal').modal('hide');
});
function accept(message){
if(message.from === RTCApp.name){
console.log("can't call yourself ");
return;
}
caller = message.from;
snd.play();
$('#callerTitle').text('Incoming call');
$('#caller').text('Caller id '+ message.from);
$('#acceptModal').modal('show');
}
function talkFunction(flag){
var callback = flag === true? "hide" : "show";
$('#people_content')[callback]();
$('#room_content')[callback]();
}
function remoteCallback(from, added, stream){
var resource = 'resource_'+ from;
if(added){
if (window.webkitURL) {
$('.inner').append('<video class="span4" id=' + resource + ' src=' +
window.webkitURL.createObjectURL(event.stream) + ' autoplay></video>');
} else {
$('.inner').append('<video class="span4" id=' + resource + ' mozSrcObject=' +
event.stream + ' autoplay></video>');
}
} else {
$('#'+resource).attr('src',"");
$('#'+resource).remove();
}
}
$('#acceptNewUser').bind('click', function() {
delete newUsers[newUser];
addNewUser({
"name" : newUser,
"id": newUser,
"img" : "images/person.jpg"
}, "ajax-modal", ".people-carousel");
$('#userModal').modal('hide');
});
$('#rejectNewUser').bind('click', function() {
newUsers[newUser] = false;
$('#userModal').modal('hide');
});
function presenceCallback(message){
roomFlag = message.room !== undefined;
var user_id, room_id;
if(!roomFlag){
$(".ajax-modal").each(function(){
user_id = $(this).attr('user-id');
if(user_id !== undefined && user_id === message.name){
if(message.status === 'on'){
$(this).find('img').attr('src', 'images/online-icon.png');
} else {
$(this).find('img').attr('src', 'images/offline-icon.png');
}
}
});
if(users[message.name] === undefined && message.status === 'on'){
console.log('presence received from the person that is not in the address book ' + message.name);
if(newUsers[message.name] === undefined){
newUsers[message.name] = true;
}
}
}
else{
$(".ajax-room-modal").each(function(){
room_id = $(this).attr('user-id');
if(room_id !== undefined && room_id === message.name){
if(message.status === 'on'){
$(this).find('img').attr('src', 'images/online-icon.png');
}else {
$(this).find('img').attr('src', 'images/offline-icon.png');
}
}
});
}
}
$('#main').delegate('a.ajax-modal', 'click', function() {
event.preventDefault();
var user_id = $(this).attr('user-id');
if(user_id !== undefined && user_id !== RTCApp.name)
RTCApp.commChannel.callOtherParty(user_id);
});
$('#main').delegate('a.ajax-room-modal', 'click', function() {
event.preventDefault();
var room = $(this).attr('user-id');
RTCApp.commChannel.joinRoom(room);
});
$.ajaxSetup({
'beforeSend' : function(xhr) {
xhr.overrideMimeType('text/html; charset=ISO-8859-1');
},
});
window.onbeforeunload = function() {
if(RTCApp.commChannel !== null)
RTCApp.commChannel.sendPresence(RTCApp.name, 'off');
}
function addNewUser(jsonData, class_name, class_div){
users[jsonData.id] = jsonData;
$('.people-carousel').empty();
var array = $.map(users, function (value, key) { return value; });
addDataToDiv(array, class_name, class_div, false);
}
function loadFromJSON(file, class_name, class_div, roomFlag){
$.getJSON(file, function(data) {
addDataToDiv(data, class_name, class_div, roomFlag);
});
}
function roster(message){
addDataToDiv(message.people, "ajax-modal", ".people-carousel", false);
addDataToDiv(message.rooms, "ajax-room-modal", ".rooms-carousel", true);
if(message.people.length > 0)
$('#people_content').show();
if(message.rooms.length > 0)
$('#room_content').show();
if(users[RTCApp.name] === undefined){
console.log('user not known by the system, create an avatar');
addNewUser({
"name" : RTCApp.name,
"id": RTCApp.name,
"img" : "images/person.jpg"
}, "ajax-modal", ".people-carousel");
}
if(users[RTCApp.name].room !== undefined){
for(var i=0; i < users[RTCApp.name].room.length; i ++)
RTCApp.commChannel.joinRoom(users[RTCApp.name].room[i]);
}
}
function addDataToDiv(data, class_name, class_div, roomFlag){
var items = [];
var i = 0;
var groupIndex = 12;
$.each(data, function(key, value) {
var name = this.name;
var image = this.img;
var id = this.id;
if(!roomFlag)
users[id] = value;
if(i % groupIndex === 0){
if(i === 0)
items.push('<div class="item active">');
else
items.push('<div class="item">');
items.push('<ul class="thumbnails">');
}
items.push('<li class="span1"><div class="thumbnail"><img src="' + image
+' " alt=""></a> <h4>' + name + '</h4><p><a href="#" class="btn btn-primary ' +
class_name + ' " user-id="' + id +
'" >Talk<img src="images/offline-icon.png" width="24" height="24" align="left" alt=""> </a></p></div></li>');
if( (i % groupIndex) === (groupIndex - 1) ){
items.push('</ul>');
items.push('</div>');
}
i++;
});
if(items.lenght > 0 && items[items.lenght - 1].indexOf('div') < 0 ){
items.push('</ul>');
items.push('</div>');
}
$(items.join('')).appendTo(class_div);
}
setInterval(function(){
for(newUser in newUsers){
if(newUsers[newUser]){
$('#userTitle').text('Accept a new user?');
$('#callerUser').text('User id '+ newUser);
$('#userModal').modal('show');
break;
}
}
}, 10000);

View File

@@ -0,0 +1,76 @@
var signaling = function(options){
var socket = new WebSocket(options.webSocketAddress);
var logg = function(s) { console.log(s); };
var myId = options.id;
var that = {};
var callbacks = {};
function getCallback(type){
return callbacks[type] !== undefined ? callbacks[type] : function(){
console.log("Callback of type " + type + " not found");
};
}
that.addCallback = function(type, f){
callbacks[type] = f;
}
that.sendMessage = function(message, to) {
sendMsg(message, to);
}
that.sendPresence = function(_name, stat){
_status = stat || 'on';
myId = _name;
sendMsg({type: 'presence', name: _name, status: _status});
}
that.joinRoom = function(room){
sendMsg({type: 'room'}, room);
}
that.callOtherParty = function(to){
sendMsg({type: 's-offer'}, to);
};
that.answer = function(to, _answer){
sendMsg({type: 's-answer', answer: _answer}, to);
};
function sendMsg(message, to){
message.from = myId;
if(to !== undefined)
message.to = to;
var mymsg = JSON.stringify(message);
logg("SOCKET Send: " + mymsg);
socket.send(mymsg);
}
socket.addEventListener("message", onMessage, false);
socket.addEventListener("error", function(event) {
logg("SOCKET Error: " + event);
});
socket.addEventListener("close", function(event) {
logg("SOCKET Close: " + event);
});
function onMessage(evt) {
logg("RECEIVED: " + evt.data);
processSignalingMessage(evt.data);
}
//message comes as a JSON from the websocket server
function processSignalingMessage(message) {
var msg = JSON.parse(message);
logg("processSignalingMessage type(" + msg.type + ")= " + message);
getCallback(msg.type)(msg);
}
return that;
}

View File

@@ -0,0 +1,210 @@
var webrtc = function(options) {
var my = {};
var commChannel = options.commChannel,
stunServer = options.stunServer,
sourcevid = options.sourcevid,
remoteCallback = options.onremote;
var localStream;
var peerConn = {};
var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }};
if(options.constrains === 'dynamic'){
BANDWITDH.init(function(bandwitdh){
console.log('calculate bandwitdh');
if(!isNaN(bandwitdh) && bandwitdh < 0.5){
mediaConstraints = {'mandatory': {
'OfferToReceiveAudio':true,
'OfferToReceiveVideo':false }};
}
console.log('bandwitdh is ' + bandwitdh + ' [Mbps]');
});
}
//callback to start p2p connection between two parties
commChannel.addCallback('s-answer', call);
commChannel.addCallback('offer', processSignalingMessage);
commChannel.addCallback('answer', processSignalingMessage);
commChannel.addCallback('candidate', processSignalingMessage);
commChannel.addCallback('bye', processSignalingMessage);
function RTCPeer(pc_config, name) {
this.from = name;
this.rtc = new RTCPeerConnection(pc_config);
that = this;
this.rtc.onaddstream = function(event){
logg("Added remote stream");
remoteCallback(that.from, true, event.stream);
};
this.rtc.onremovestream = function(event) {
logg("Remove remote stream");
remoteCallback(that.from, false);
};
this.rtc.onicecandidate = function(event) {
logg("send on Icecandidate");
if (event.candidate) {
commChannel.sendMessage({type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate}, that.from);
} else {
logg("End of candidates.");
}
};
}
function setLocalDescriptionAndMessage(sessionDescription){
logg("setLocalDescriptionAndMessage");
this.rtc.setLocalDescription(sessionDescription);
commChannel.sendMessage(sessionDescription, this.from);
}
RTCPeer.prototype.createOffer = function(callback){
logg("createOffer to " + this.from);
that = this;
this.rtc.createOffer(function(sessionDescription){
callback.call(that, sessionDescription);
}, null, mediaConstraints);
}
RTCPeer.prototype.createAnswer = function(callback){
logg("createAnswer to " + this.from);
that = this;
this.rtc.createAnswer(function(sessionDescription){
callback.call(that, sessionDescription);
}, null, mediaConstraints);
}
RTCPeer.prototype.getRTC = function(){
return this.rtc;
}
RTCPeer.prototype.getFrom = function(){
return this.from;
}
var logg = function(s) { console.log(s); };
my.startVideo = function() {
try {
getUserMedia({audio: true, video: true}, successCallback, errorCallback);
} catch (e) {
getUserMedia("video,audio", successCallback, errorCallback);
}
function successCallback(stream) {
attachMediaStream(sourcevid, stream);
localStream = stream;
logg('local stream started');
}
function errorCallback(error) {
logg('An error occurred: [CODE ' + error.code + ']');
}
}
my.stopVideo = function() {
sourcevid.src = "";
}
my.onHangUp = function() {
logg("Hang up.");
closeSession();
}
// start the connection upon user request
function call(msg) {
if(msg.answer !== 'accept') {
console.log('call not accepted');
return;
}
if (peerConn[msg.from] === undefined && localStream) {
logg("Creating PeerConnection with "+ msg.from);
createPeerConnection(msg.from);
} else if (!localStream){
alert("Please start the video first");
logg("localStream not started");
return;
} else {
logg("peer SDP offer already made");
}
logg("create offer");
peerConn[msg.from].createOffer(setLocalDescriptionAndMessage);
}
function createPeerConnection(from) {
try {
logg("Creating peer connection with " + from);
var servers = [];
servers.push({'url':'stun:' + stunServer});
var pc_config = {'iceServers':servers};
peerConn[from] = new RTCPeer(pc_config, from);
logg("Connected using stun server "+ stunServer);
} catch (e) {
alert("Failed to create PeerConnection, exception: " + e.message);
return;
}
logg('Adding local stream...');
peerConn[from].getRTC().addStream(localStream);
}
function processSignalingMessage(msg) {
logg("processSignalingMessage type(" + msg.type + ")= " + msg);
if (msg.type === 'offer') {
if(peerConn[msg.from] === undefined && localStream) {
createPeerConnection(msg.from);
//set remote description
peerConn[msg.from].getRTC().setRemoteDescription(new RTCSessionDescription(msg));
//create answer
logg("Sending answer to peer.");
peerConn[msg.from].createAnswer(setLocalDescriptionAndMessage);
} else {
logg('peerConnection has already been started');
}
} else if (msg.type === 'answer' && peerConn[msg.from] !== undefined) {
logg("setRemoteDescription...");
peerConn[msg.from].getRTC().setRemoteDescription(new RTCSessionDescription(msg));
} else if (msg.type === 'candidate' && peerConn[msg.from] !== undefined) {
var candidate = new RTCIceCandidate({sdpMLineIndex:msg.label, candidate:msg.candidate});
peerConn[msg.from].getRTC().addIceCandidate(candidate);
} else if (msg.type === 'bye' && peerConn[msg.from] !== undefined) {
onRemoteHangUp(msg.from);
} else {
logg("message unknown:" + msg);
}
}
function onRemoteHangUp(from) {
logg("Remote(" + from + ") Hang up ");
remoteCallback(from, false);
peerConn[from].getRTC().close();
delete peerConn[from];
}
function closeSession() {
for(var index in peerConn){
remoteCallback(peerConn[index].getFrom(), false);
peerConn[index].getRTC().close();
delete peerConn[index];
}
commChannel.sendMessage({type: 'bye'});
}
window.onbeforeunload = function() {
if (Object.keys(peerConn).length > 0) {
closeSession();
}
}
return my;
};

View File

@@ -0,0 +1,117 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"d": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
"integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
"requires": {
"es5-ext": "^0.10.50",
"type": "^1.0.1"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"es5-ext": {
"version": "0.10.53",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
"integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
"requires": {
"es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.3",
"next-tick": "~1.0.0"
}
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
"integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
"requires": {
"d": "1",
"es5-ext": "^0.10.35",
"es6-symbol": "^3.1.1"
}
},
"es6-symbol": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
"integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
"requires": {
"d": "^1.0.1",
"ext": "^1.1.2"
}
},
"ext": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
"integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
"requires": {
"type": "^2.0.0"
},
"dependencies": {
"type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz",
"integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow=="
}
}
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw=="
},
"next-tick": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
"integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
},
"type": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
"integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"requires": {
"is-typedarray": "^1.0.0"
}
},
"websocket": {
"version": "1.0.31",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.31.tgz",
"integrity": "sha512-VAouplvGKPiKFDTeCCO65vYHsyay8DqoBSlzIO3fayrfOgU94lQN5a1uWVnFrMLceTJw/+fQXR5PGbUVRaHshQ==",
"requires": {
"debug": "^2.2.0",
"es5-ext": "^0.10.50",
"nan": "^2.14.0",
"typedarray-to-buffer": "^3.1.5",
"yaeti": "^0.0.6"
}
},
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
}
}
}

View File

@@ -0,0 +1,7 @@
{
"websocketPort" : 1337,
"refreshRate" : 5000,
"autoCall" : true,
"acceptNewUsers" : true,
"sendRoster" : true
}

View File

@@ -0,0 +1,195 @@
var WebSocketServer = require('websocket').server;
var http = require('http');
var fs = require('fs');
var people = {};
var connectionDict = {};
var rooms = {};
var numberOfConnections = 0;
var roster = {};
var settings = {
websocketPort: 1337,
refreshRate : 5000,
autoCall : true,
acceptNewUsers : false
}
fs.readFile(process.argv[2] || './settings.json', function(err, data) {
if (err) {
console.log('No settings.json found ('+err+'). Using default settings');
} else {
settings = JSON.parse(data.toString('utf8', 0, data.length));
}
console.log(settings);
});
fs.readFile('data/people.json', 'utf8', function (err, data) {
if (err) throw err;
var obj = JSON.parse(data.toString('utf8', 0, data.length));
roster.people = obj;
for(var i=0; i< obj.length; i ++){
people[obj[i].id] = 'off';
};
console.log('Loaded ' + obj.length + ' people from people.json file');
});
fs.readFile('data/rooms.json', 'utf8', function (err, data) {
if (err) throw err;
var obj = JSON.parse(data.toString('utf8', 0, data.length));
roster.rooms = obj;
console.log('Loaded ' + obj.length + ' rooms from rooms.json file');
});
var server = http.createServer(function(request, response) {
// process HTTP request. Since we're writing just WebSockets serve we don't have to implement anything.
}).listen(settings.websocketPort, function() {
console.log('Socket server is listening on port ' + settings.websocketPort);
});
wsServer = new WebSocketServer({
httpServer: server
});
setInterval(sendPresence, settings.refreshRate);
// This callback function is called every time someone tries to connect to the WebSocket server
// if the type of message is presence, it will send the broadcast. If the type is room, it will send the offer type as multicast,
// if from and to field are defined, and connection.name that maches to exists, it will send the unicast.
// Otherwise, it will send the broadcast.
wsServer.on('request', function(request) {
numberOfConnections ++;
console.log('Connection from origin ' + request.origin);
var connection = request.accept(null, request.origin);
console.log('Connection address ' + connection.remoteAddress);
console.log('Number of connections ' + numberOfConnections);
connection.on('message', function(message) {
if (message.type === 'utf8') {
// process WebSocket message
console.log('Received Message ' + message.utf8Data);
//parse the message
msg = JSON.parse(message.utf8Data);
var type = msg.type;
var from = msg.from;
var to = msg.to;
var name;
//accept only messages that are authorized, in simple case, we assume that the
//first call is the presence with a name of the caller
if(type !== 'presence' && connection.name === undefined){
console.log('connection not allowed, name not provided');
return;
}
//after presence message, socket connection is 'named', only such connections participate later.
if(type === 'presence'){
name = msg.name;
if(people[name] === undefined && !settings.acceptNewUsers){
console.log('Unknown user not allowed');
return;
}
var status = msg.status;
if(status === 'on'){
connection.name = name;
connectionDict[name] = connection;
console.log('adding '+ name)
people[name] = 'on';
if(settings.sendRoster){
connection.send(JSON.stringify({type: "roster", people: roster.people, rooms: roster.rooms}));
}
} else {
remove(name);
}
} else if(type === 'room'){
if(rooms[to] === undefined)
rooms[to] = [];
console.log('Number of people in the room ' + rooms[to].length);
console.log('Sending multicast from ' + from + ": to room "+ to);
if(settings.autoCall){
for(var i = 0; i < rooms[to].length; i ++){
var x = rooms[to][i];
if(connectionDict[x] !== undefined){
console.log('Sending offer from ' + from + ": to "+ x);
sendOffer(connectionDict[x], from, x);
}
}
}
if(rooms[to].indexOf(from) < 0)
rooms[to].push(from);
return;
} else if(to !== undefined && from !== undefined){
if(connectionDict[to] !== undefined && people[to] !== 'off'){
console.log('Sending unicast from ' + from + ":"+ to);
connectionDict[to].send(message.utf8Data, sendCallback);
}
} else {
console.log("message couldn't be passed to " + to);
}
return;
}
console.log('Sending broadcast from '+ from);
// broadcast message to all clients that have name attached
for(var client in connectionDict){
if(client !== name){
console.log('Sending data to '+ client);
connectionDict[client].send(message.utf8Data, sendCallback);
}
}
});
connection.on('close', function(conn) {
console.log('Peer disconnected.');
numberOfConnections --;
remove(connection.name);
});
});
function sendOffer(connection, _from, _to){
connection.send(JSON.stringify({type: 's-offer', from: _from, to: _to}), sendCallback);
}
function sendPresence(){
for(var _name in people){
for(var client in connectionDict){
if(people[_name] === 'off'){
console.log('Person '+ _name + '[off] -> ' + client);
connectionDict[client].send(JSON.stringify({type: 'presence', name: _name, status: 'off'}), sendCallback);
} else if(people[_name] === 'on'){
console.log('Person '+ _name + '[on] -> ' + client);
connectionDict[client].send(JSON.stringify({type: 'presence', name: _name, status: 'on'}), sendCallback);
}
}
}
for(var room in rooms){
for(var i = 0; i < rooms[room].length; i ++){
var client = rooms[room][i];
if(connectionDict[client] !== undefined){
console.log('Room '+ room + ' -> ' + client);
connectionDict[client].send(JSON.stringify({type: 'presence', name: room,
status: 'on', room: true}), sendCallback);
}
}
}
}
function remove(name){
if(name !== undefined && name !== null){
console.log('removing '+ name);
people[name] = 'off';
delete connectionDict[name];
}
}
function sendCallback(err) {
if (err){
console.error("send() error: " + err);
}
}

3
comms/webrtc-ws-example/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
adapter.js
cert

View File

@@ -0,0 +1,31 @@
WebRTC Video and Signaling sample
=================================
This sample demonstrates how to use WebSockets to create a signaling server for WebRTC calling. The signaling server is based on the WebSocket, opening a video call to another user by clicking on their name in the user list sidebar.
See the article [Signaling and video calling](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling) on MDN for detailed information on how this code works.
Original example at https://github.com/mdn/samples-server/tree/master/s/webrtc-from-chat doesn't work properly. This project fixing it and use only video call, without a chatbox.
**This is a demo purpose project, should not be used in production**
- Create a Linux VM
- Install NodeJs and npm
```
apt install nodejs npm openssl
```
- create a folder /app
```
mkdir -p /app
```
- upload content into the folder
- run the server
```
cd /app; sh startup.sh
```
- open web browser at https://<YOUR_SERVER_IP>
**NOTE on SSL**
The certificate is self signed, so allow your browser to open your untrusted website.

635
comms/webrtc-ws-example/client.js Executable file
View File

@@ -0,0 +1,635 @@
// WebSocket and WebRTC based multi-user chat sample with two-way video
// calling, including use of TURN if applicable or necessary.
//
// This file contains the JavaScript code that implements the client-side
// features for connecting and managing chat and video calls.
//
// To read about how this sample works: http://bit.ly/webrtc-from-chat
//
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/
"use strict";
// Get our hostname
var myHostname = window.location.hostname;
console.log("Hostname: " + myHostname);
// WebSocket chat/signaling channel variables.
var connection = null;
var clientID = 0;
// The media constraints object describes what sort of stream we want
// to request from the local A/V hardware (typically a webcam and
// microphone). Here, we specify only that we want both audio and
// video; however, you can be more specific. It's possible to state
// that you would prefer (or require) specific resolutions of video,
// whether to prefer the user-facing or rear-facing camera (if available),
// and so on.
//
// See also:
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
//
var mediaConstraints = {
audio: true, // We want an audio track
video: true // ...and we want a video track
};
var myUsername = null;
var targetUsername = null; // To store username of other peer
var myPeerConnection = null; // RTCPeerConnection
// To work both with and without addTrack() we need to note
// if it's available
var hasAddTrack = false;
// Output logging information to console.
function log(text) {
var time = new Date();
console.log("[" + time.toLocaleTimeString() + "] " + text);
}
// Output an error message to console.
function log_error(text) {
var time = new Date();
console.error("[" + time.toLocaleTimeString() + "] " + text);
}
// Send a JavaScript object by converting it to JSON and sending
// it as a message on the WebSocket connection.
function sendToServer(msg) {
var msgJSON = JSON.stringify(msg);
log("Sending '" + msg.type + "' message: " + msgJSON);
connection.send(msgJSON);
}
// Called when the "id" message is received; this message is sent by the
// server to assign this login session a unique ID number; in response,
// this function sends a "username" message to set our username for this
// session.
function setUsername() {
myUsername = document.getElementById("name").value;
sendToServer({
name: myUsername,
date: Date.now(),
id: clientID,
type: "username"
});
}
// Open and configure the connection to the WebSocket server.
function connect() {
var serverUrl;
var scheme = "ws";
// If this is an HTTPS connection, we have to use a secure WebSocket
// connection too, so add another "s" to the scheme.
if (document.location.protocol === "https:") {
scheme += "s";
}
serverUrl = scheme + "://" + myHostname + ":443";
connection = new WebSocket(serverUrl, "json");
connection.onopen = function(evt) {
};
connection.onerror = function(evt) {
console.dir(evt);
}
connection.onmessage = function(evt) {
var text = "";
var msg = JSON.parse(evt.data);
log("Message received: ");
console.dir(msg);
var time = new Date(msg.date);
var timeStr = time.toLocaleTimeString();
switch(msg.type) {
case "id":
clientID = msg.id;
setUsername();
break;
case "rejectusername":
myUsername = msg.name;
break;
case "userlist": // Received an updated user list
handleUserlistMsg(msg);
break;
// Signaling messages: these messages are used to trade WebRTC
// signaling information during negotiations leading up to a video
// call.
case "video-offer": // Invitation and offer to chat
handleVideoOfferMsg(msg);
break;
case "video-answer": // Callee has answered our offer
handleVideoAnswerMsg(msg);
break;
case "new-ice-candidate": // A new ICE candidate has been received
handleNewICECandidateMsg(msg);
break;
case "hang-up": // The other peer has hung up the call
handleHangUpMsg(msg);
break;
// Unknown message; output to console for debugging.
default:
log_error("Unknown message received:");
log_error(msg);
}
};
}
// Create the RTCPeerConnection which knows how to talk to our
// selected STUN/TURN server and then uses getUserMedia() to find
// our camera and microphone and add that stream to the connection for
// use in our video call. Then we configure event handlers to get
// needed notifications on the call.
function createPeerConnection() {
log("Setting up a connection...");
// Create an RTCPeerConnection which knows to use our chosen
// STUN server.
myPeerConnection = new RTCPeerConnection({
iceServers: [ // Information about ICE servers - Use your own!
{
url: 'stun:stun.l.google.com:19302'
},
{
url: 'turn:numb.viagenie.ca',
credential: 'muazkh',
username: 'webrtc@live.com'
}
]
});
// Do we have addTrack()? If not, we will use streams instead.
hasAddTrack = (myPeerConnection.addTrack !== undefined);
// Set up event handlers for the ICE negotiation process.
myPeerConnection.onicecandidate = handleICECandidateEvent;
myPeerConnection.onremovestream = handleRemoveStreamEvent;
myPeerConnection.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
myPeerConnection.onicegatheringstatechange = handleICEGatheringStateChangeEvent;
myPeerConnection.onsignalingstatechange = handleSignalingStateChangeEvent;
myPeerConnection.onnegotiationneeded = handleNegotiationNeededEvent;
// Because the deprecation of addStream() and the addstream event is recent,
// we need to use those if addTrack() and track aren't available.
if (hasAddTrack) {
myPeerConnection.ontrack = handleTrackEvent;
} else {
myPeerConnection.onaddstream = handleAddStreamEvent;
}
}
// Called by the WebRTC layer to let us know when it's time to
// begin (or restart) ICE negotiation. Starts by creating a WebRTC
// offer, then sets it as the description of our local media
// (which configures our local media stream), then sends the
// description to the callee as an offer. This is a proposed media
// format, codec, resolution, etc.
function handleNegotiationNeededEvent() {
log("*** Negotiation needed");
log("---> Creating offer");
myPeerConnection.createOffer().then(function(offer) {
log("---> Creating new description object to send to remote peer");
return myPeerConnection.setLocalDescription(offer);
})
.then(function() {
log("---> Sending offer to remote peer");
sendToServer({
name: myUsername,
target: targetUsername,
type: "video-offer",
sdp: myPeerConnection.localDescription
});
})
.catch(reportError);
}
// Called by the WebRTC layer when events occur on the media tracks
// on our WebRTC call. This includes when streams are added to and
// removed from the call.
//
// track events include the following fields:
//
// RTCRtpReceiver receiver
// MediaStreamTrack track
// MediaStream[] streams
// RTCRtpTransceiver transceiver
function handleTrackEvent(event) {
log("*** Track event");
document.getElementById("received_video").srcObject = event.streams[0];
document.getElementById("hangup-button").disabled = false;
}
// Called by the WebRTC layer when a stream starts arriving from the
// remote peer. We use this to update our user interface, in this
// example.
function handleAddStreamEvent(event) {
log("*** Stream added");
document.getElementById("received_video").srcObject = event.stream;
document.getElementById("hangup-button").disabled = false;
}
// An event handler which is called when the remote end of the connection
// removes its stream. We consider this the same as hanging up the call.
// It could just as well be treated as a "mute".
//
// Note that currently, the spec is hazy on exactly when this and other
// "connection failure" scenarios should occur, so sometimes they simply
// don't happen.
function handleRemoveStreamEvent(event) {
log("*** Stream removed");
closeVideoCall();
}
// Handles |icecandidate| events by forwarding the specified
// ICE candidate (created by our local ICE agent) to the other
// peer through the signaling server.
function handleICECandidateEvent(event) {
if (event.candidate) {
log("Outgoing ICE candidate: " + event.candidate.candidate);
sendToServer({
type: "new-ice-candidate",
target: targetUsername,
candidate: event.candidate
});
}
}
// Handle |iceconnectionstatechange| events. This will detect
// when the ICE connection is closed, failed, or disconnected.
//
// This is called when the state of the ICE agent changes.
function handleICEConnectionStateChangeEvent(event) {
log("*** ICE connection state changed to " + myPeerConnection.iceConnectionState);
switch(myPeerConnection.iceConnectionState) {
case "closed":
case "failed":
case "disconnected":
closeVideoCall();
break;
}
}
// Set up a |signalingstatechange| event handler. This will detect when
// the signaling connection is closed.
//
// NOTE: This will actually move to the new RTCPeerConnectionState enum
// returned in the property RTCPeerConnection.connectionState when
// browsers catch up with the latest version of the specification!
function handleSignalingStateChangeEvent(event) {
log("*** WebRTC signaling state changed to: " + myPeerConnection.signalingState);
switch(myPeerConnection.signalingState) {
case "closed":
closeVideoCall();
break;
}
}
// Handle the |icegatheringstatechange| event. This lets us know what the
// ICE engine is currently working on: "new" means no networking has happened
// yet, "gathering" means the ICE engine is currently gathering candidates,
// and "complete" means gathering is complete. Note that the engine can
// alternate between "gathering" and "complete" repeatedly as needs and
// circumstances change.
//
// We don't need to do anything when this happens, but we log it to the
// console so you can see what's going on when playing with the sample.
function handleICEGatheringStateChangeEvent(event) {
log("*** ICE gathering state changed to: " + myPeerConnection.iceGatheringState);
}
// Given a message containing a list of usernames, this function
// populates the user list box with those names, making each item
// clickable to allow starting a video call.
function handleUserlistMsg(msg) {
var i;
var listElem = document.getElementById("userlistbox");
// Remove all current list members. We could do this smarter,
// by adding and updating users instead of rebuilding from
// scratch but this will do for this sample.
while (listElem.firstChild) {
listElem.removeChild(listElem.firstChild);
}
// Add member names from the received list
for (i=0; i < msg.users.length; i++) {
var item = document.createElement("li");
item.appendChild(document.createTextNode(msg.users[i]));
item.addEventListener("click", invite, false);
listElem.appendChild(item);
}
}
// Close the RTCPeerConnection and reset variables so that the user can
// make or receive another call if they wish. This is called both
// when the user hangs up, the other user hangs up, or if a connection
// failure is detected.
function closeVideoCall() {
var remoteVideo = document.getElementById("received_video");
var localVideo = document.getElementById("local_video");
log("Closing the call");
// Close the RTCPeerConnection
if (myPeerConnection) {
log("--> Closing the peer connection");
// Disconnect all our event listeners; we don't want stray events
// to interfere with the hangup while it's ongoing.
myPeerConnection.onaddstream = null; // For older implementations
myPeerConnection.ontrack = null; // For newer ones
myPeerConnection.onremovestream = null;
myPeerConnection.onnicecandidate = null;
myPeerConnection.oniceconnectionstatechange = null;
myPeerConnection.onsignalingstatechange = null;
myPeerConnection.onicegatheringstatechange = null;
myPeerConnection.onnotificationneeded = null;
// Stop the videos
if (remoteVideo.srcObject) {
remoteVideo.srcObject.getTracks().forEach(track => track.stop());
}
if (localVideo.srcObject) {
localVideo.srcObject.getTracks().forEach(track => track.stop());
}
remoteVideo.src = null;
localVideo.src = null;
// Close the peer connection
myPeerConnection.close();
myPeerConnection = null;
}
// Disable the hangup button
document.getElementById("hangup-button").disabled = true;
targetUsername = null;
}
// Handle the "hang-up" message, which is sent if the other peer
// has hung up the call or otherwise disconnected.
function handleHangUpMsg(msg) {
log("*** Received hang up notification from other peer");
closeVideoCall();
}
// Hang up the call by closing our end of the connection, then
// sending a "hang-up" message to the other peer (keep in mind that
// the signaling is done on a different connection). This notifies
// the other peer that the connection should be terminated and the UI
// returned to the "no call in progress" state.
function hangUpCall() {
closeVideoCall();
sendToServer({
name: myUsername,
target: targetUsername,
type: "hang-up"
});
}
// Handle a click on an item in the user list by inviting the clicked
// user to video chat. Note that we don't actually send a message to
// the callee here -- calling RTCPeerConnection.addStream() issues
// a |notificationneeded| event, so we'll let our handler for that
// make the offer.
function invite(evt) {
log("Starting to prepare an invitation");
if (myPeerConnection) {
alert("You can't start a call because you already have one open!");
} else {
var clickedUsername = evt.target.textContent;
// Don't allow users to call themselves, because weird.
if (clickedUsername === myUsername) {
alert("I'm afraid I can't let you talk to yourself. That would be weird.");
return;
}
// Record the username being called for future reference
targetUsername = clickedUsername;
log("Inviting user " + targetUsername);
// Call createPeerConnection() to create the RTCPeerConnection.
log("Setting up connection to invite user: " + targetUsername);
createPeerConnection();
// Now configure and create the local stream, attach it to the
// "preview" box (id "local_video"), and add it to the
// RTCPeerConnection.
log("Requesting webcam access...");
navigator.mediaDevices.getUserMedia(mediaConstraints)
.then(function(localStream) {
log("-- Local video stream obtained");
document.getElementById("local_video").srcObject = localStream;
if (hasAddTrack) {
log("-- Adding tracks to the RTCPeerConnection");
localStream.getTracks().forEach(track => myPeerConnection.addTrack(track, localStream));
} else {
log("-- Adding stream to the RTCPeerConnection");
myPeerConnection.addStream(localStream);
}
})
.catch(handleGetUserMediaError);
}
}
// Accept an offer to video chat. We configure our local settings,
// create our RTCPeerConnection, get and attach our local camera
// stream, then create and send an answer to the caller.
function handleVideoOfferMsg(msg) {
var localStream = null;
targetUsername = msg.name;
// Call createPeerConnection() to create the RTCPeerConnection.
log("Starting to accept invitation from " + targetUsername);
createPeerConnection();
// We need to set the remote description to the received SDP offer
// so that our local WebRTC layer knows how to talk to the caller.
var desc = new RTCSessionDescription(msg.sdp);
myPeerConnection.setRemoteDescription(desc).then(function () {
log("Setting up the local media stream...");
return navigator.mediaDevices.getUserMedia(mediaConstraints);
})
.then(function(stream) {
log("-- Local video stream obtained");
localStream = stream;
document.getElementById("local_video").srcObject = localStream;
if (hasAddTrack) {
log("-- Adding tracks to the RTCPeerConnection");
localStream.getTracks().forEach(track =>
myPeerConnection.addTrack(track, localStream)
);
} else {
log("-- Adding stream to the RTCPeerConnection");
myPeerConnection.addStream(localStream);
}
})
.then(function() {
log("------> Creating answer");
// Now that we've successfully set the remote description, we need to
// start our stream up locally then create an SDP answer. This SDP
// data describes the local end of our call, including the codec
// information, options agreed upon, and so forth.
return myPeerConnection.createAnswer();
})
.then(function(answer) {
log("------> Setting local description after creating answer");
// We now have our answer, so establish that as the local description.
// This actually configures our end of the call to match the settings
// specified in the SDP.
return myPeerConnection.setLocalDescription(answer);
})
.then(function() {
var msg = {
name: myUsername,
target: targetUsername,
type: "video-answer",
sdp: myPeerConnection.localDescription
};
// We've configured our end of the call now. Time to send our
// answer back to the caller so they know that we want to talk
// and how to talk to us.
log("Sending answer packet back to other peer");
sendToServer(msg);
})
.catch(handleGetUserMediaError);
}
// Responds to the "video-answer" message sent to the caller
// once the callee has decided to accept our request to talk.
function handleVideoAnswerMsg(msg) {
log("Call recipient has accepted our call");
// Configure the remote description, which is the SDP payload
// in our "video-answer" message.
var desc = new RTCSessionDescription(msg.sdp);
myPeerConnection.setRemoteDescription(desc).catch(reportError);
}
// A new ICE candidate has been received from the other peer. Call
// RTCPeerConnection.addIceCandidate() to send it along to the
// local ICE framework.
function handleNewICECandidateMsg(msg) {
var candidate = new RTCIceCandidate(msg.candidate);
log("Adding received ICE candidate: " + JSON.stringify(candidate));
myPeerConnection.addIceCandidate(candidate)
.catch(reportError);
}
// Handle errors which occur when trying to access the local media
// hardware; that is, exceptions thrown by getUserMedia(). The two most
// likely scenarios are that the user has no camera and/or microphone
// or that they declined to share their equipment when prompted. If
// they simply opted not to share their media, that's not really an
// error, so we won't present a message in that situation.
function handleGetUserMediaError(e) {
log(e);
switch(e.name) {
case "NotFoundError":
alert("Unable to open your call because no camera and/or microphone" +
"were found.");
break;
case "SecurityError":
case "PermissionDeniedError":
// Do nothing; this is the same as the user canceling the call.
break;
default:
alert("Error opening your camera and/or microphone: " + e.message);
break;
}
// Make sure we shut down our end of the RTCPeerConnection so we're
// ready to try again.
closeVideoCall();
}
// Handles reporting errors. Currently, we just dump stuff to console but
// in a real-world application, an appropriate (and user-friendly)
// error message should be displayed.
function reportError(errMessage) {
log_error("Error " + errMessage.name + ": " + errMessage.message);
}

View File

@@ -0,0 +1,52 @@
<!doctype html>
<!--
WebSocket chat client
WebSocket and WebRTC based multi-user chat sample with two-way video
calling, including use of TURN if applicable or necessary.
This file provides the structure of the chat client's web page, including
logging in, text chatting, and making private video calls to other users.
To read about how this sample works: http://bit.ly/webrtc-from-chat
Any copyright is dedicated to the Public Domain.
http: creativecommons.org/publicdomain/zero/1.0/
-->
<html>
<head>
<title>WebSocket Demo with WebRTC Calling</title>
<meta charset="utf-8">
<link href="style.css" rel="stylesheet">
<script type="text/javascript" src="client.js"></script>
<script type="text/javascript" src="adapter.js"></script>
</head>
<body>
<p>This is a simple video call example implemented using WebSockets and WebRTC.
<p>Click a username in the user list to ask them to enter a one-on-one video
call with you.</p>
<p>Enter a username: <input id="name" type="text" maxlength="12" required
autocomplete="username" inputmode="verbatim" placeholder="Username">
<input type="button" name="login" value="Log in" onclick="connect()"></p>
<div id="container" class="flexChild columnParent">
<div class="flexChild rowParent">
<div class="flexChild" id="userlist-container">
<ul id="userlistbox"></ul>
</div>
</div>
<div class="flexChild" id="camera-container">
<div class="camera-box">
<video id="received_video" autoplay></video>
<video id="local_video" autoplay muted></video>
<button id="hangup-button" onclick="hangUpCall();" disabled>
Hang Up
</button>
</div>
</div>
</div>
</body>
</html>

116
comms/webrtc-ws-example/package-lock.json generated Normal file
View File

@@ -0,0 +1,116 @@
{
"name": "webrtc-video-call",
"version": "0.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"colors": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz",
"integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg=="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"nan": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
},
"node-static": {
"version": "0.7.11",
"resolved": "https://registry.npmjs.org/node-static/-/node-static-0.7.11.tgz",
"integrity": "sha512-zfWC/gICcqb74D9ndyvxZWaI1jzcoHmf4UTHWQchBNuNMxdBLJMDiUgZ1tjGLEIe/BMhj2DxKD8HOuc2062pDQ==",
"requires": {
"colors": ">=0.6.0",
"mime": "^1.2.9",
"optimist": ">=0.3.4"
}
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"requires": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
}
},
"rtcpeerconnection-shim": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
"requires": {
"sdp": "^2.6.0"
}
},
"sdp": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.9.0.tgz",
"integrity": "sha512-XAVZQO4qsfzVTHorF49zCpkdxiGmPNjA8ps8RcJGtGP3QJ/A8I9/SVg/QnkAFDMXIyGbHZBBFwYBw6WdnhT96w=="
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"requires": {
"is-typedarray": "^1.0.0"
}
},
"webrtc-adapter": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-7.2.4.tgz",
"integrity": "sha512-i3Qg8gFY/XSpr9c8WtkXH50phJ2woXad5wdtF0uIZ5P/WHCGIRvElGOYhHRuA26+t0c+9/GsCv9L1/PGqXXu/A==",
"requires": {
"rtcpeerconnection-shim": "^1.2.15",
"sdp": "^2.9.0"
}
},
"websocket": {
"version": "1.0.28",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.28.tgz",
"integrity": "sha512-00y/20/80P7H4bCYkzuuvvfDvh+dgtXi5kzDf3UcZwN6boTYaKvsrtZ5lIYm1Gsg48siMErd9M4zjSYfYFHTrA==",
"requires": {
"debug": "^2.2.0",
"nan": "^2.11.0",
"typedarray-to-buffer": "^3.1.5",
"yaeti": "^0.0.6"
}
},
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
},
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
}
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "webrtc-video-call",
"version": "0.0.0",
"description": "A simple WebSocket-based server with two-way WebRTC video call",
"license": "MIT",
"dependencies": {
"node-static": "^0.7.11",
"webrtc-adapter": "^7.2.4",
"websocket": "^1.0.28"
}
}

305
comms/webrtc-ws-example/server.js Executable file
View File

@@ -0,0 +1,305 @@
//#!/usr/bin/env node
//
// WebSocket chat server
// Implemented using Node.js
//
// Requires the websocket module.
//
// WebSocket and WebRTC based multi-user chat sample with two-way video
// calling, including use of TURN if applicable or necessary.
//
// This file contains the JavaScript code that implements the server-side
// functionality of the chat system, including user ID management, message
// reflection, and routing of private messages, including support for
// sending through unknown JSON objects to support custom apps and signaling
// for WebRTC.
//
// Requires Node.js and the websocket module (WebSocket-Node):
//
// - http://nodejs.org/
// - https://github.com/theturtle32/WebSocket-Node
//
// To read about how this sample works: http://bit.ly/webrtc-from-chat
//
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/publicdomain/zero/1.0/
"use strict";
var https = require('https');
var fs = require('fs');
var nodeStatic = require('node-static');
var WebSocketServer = require('websocket').server;
// Used for managing the text chat user list.
var connectionArray = [];
var nextID = Date.now();
var appendToMakeUnique = 1;
// Output logging information to console
function log(text) {
var time = new Date();
console.log("[" + time.toLocaleTimeString() + "] " + text);
}
// If you want to implement support for blocking specific origins, this is
// where you do it. Just return false to refuse WebSocket connections given
// the specified origin.
function originIsAllowed(origin) {
return true; // We will accept all connections
}
// Scans the list of users and see if the specified name is unique. If it is,
// return true. Otherwise, returns false. We want all users to have unique
// names.
function isUsernameUnique(name) {
var isUnique = true;
var i;
for (i=0; i<connectionArray.length; i++) {
if (connectionArray[i].username === name) {
isUnique = false;
break;
}
}
return isUnique;
}
// Sends a message (which is already stringified JSON) to a single
// user, given their username. We use this for the WebRTC signaling,
// and we could use it for private text messaging.
function sendToOneUser(target, msgString) {
var isUnique = true;
var i;
for (i=0; i<connectionArray.length; i++) {
if (connectionArray[i].username === target) {
connectionArray[i].sendUTF(msgString);
break;
}
}
}
// Scan the list of connections and return the one for the specified
// clientID. Each login gets an ID that doesn't change during the session,
// so it can be tracked across username changes.
function getConnectionForID(id) {
var connect = null;
var i;
for (i=0; i<connectionArray.length; i++) {
if (connectionArray[i].clientID === id) {
connect = connectionArray[i];
break;
}
}
return connect;
}
// Builds a message object of type "userlist" which contains the names of
// all connected users. Used to ramp up newly logged-in users and,
// inefficiently, to handle name change notifications.
function makeUserListMessage() {
var userListMsg = {
type: "userlist",
users: []
};
var i;
// Add the users to the list
for (i=0; i<connectionArray.length; i++) {
userListMsg.users.push(connectionArray[i].username);
}
return userListMsg;
}
// Sends a "userlist" message to all chat members. This is a cheesy way
// to ensure that every join/drop is reflected everywhere. It would be more
// efficient to send simple join/drop messages to each user, but this is
// good enough for this simple example.
function sendUserListToAll() {
var userListMsg = makeUserListMessage();
var userListMsgStr = JSON.stringify(userListMsg);
var i;
for (i=0; i<connectionArray.length; i++) {
connectionArray[i].sendUTF(userListMsgStr);
}
}
// Load the key and certificate data to be used for our HTTPS/WSS
// server.
var httpsOptions = {
key: fs.readFileSync('cert/key.pem'),
cert: fs.readFileSync('cert/cert.pem')
};
var fileServer = new(nodeStatic.Server)();
// Our HTTPS server act as a static web server to deliver static files and used for a WebSocket layer
var httpsServer = https.createServer(httpsOptions, function(request, response) {
fileServer.serve(request, response);
})
// Spin up the HTTPS server on the port assigned to this sample.
// This will be turned into a WebSocket port very shortly.
httpsServer.listen(443, function() {
log("Server is listening on port 443");
});
// Create the WebSocket server by converting the HTTPS server into one.
var wsServer = new WebSocketServer({
httpServer: httpsServer,
autoAcceptConnections: false
});
if (!wsServer) {
log("ERROR: Unable to create WbeSocket server!");
}
// Set up a "connect" message handler on our WebSocket server. This is
// called whenever a user connects to the server's port using the
// WebSocket protocol.
wsServer.on('request', function(request) {
if (!originIsAllowed(request.origin)) {
request.reject();
log("Connection from " + request.origin + " rejected.");
return;
}
// Accept the request and get a connection.
var connection = request.accept("json", request.origin);
// Add the new connection to our list of connections.
log("Connection accepted from " + connection.remoteAddress + ".");
connectionArray.push(connection);
connection.clientID = nextID;
nextID++;
// Send the new client its token; it send back a "username" message to
// tell us what username they want to use.
var msg = {
type: "id",
id: connection.clientID
};
connection.sendUTF(JSON.stringify(msg));
// Set up a handler for the "message" event received over WebSocket. This
// is a message sent by a client, and may be text to share with other
// users, a private message (text or signaling) for one user, or a command
// to the server.
connection.on('message', function(message) {
if (message.type === 'utf8') {
log("Received Message: " + message.utf8Data);
// Process incoming data.
var sendToClients = true;
msg = JSON.parse(message.utf8Data);
var connect = getConnectionForID(msg.id);
// Take a look at the incoming object and act on it based
// on its type. Unknown message types are passed through,
// since they may be used to implement client-side features.
// Messages with a "target" property are sent only to a user
// by that name.
switch(msg.type) {
// Username change
case "username":
var nameChanged = false;
var origName = msg.name;
// Ensure the name is unique by appending a number to it
// if it's not; keep trying that until it works.
while (!isUsernameUnique(msg.name)) {
msg.name = origName + appendToMakeUnique;
appendToMakeUnique++;
nameChanged = true;
}
// If the name had to be changed, we send a "rejectusername"
// message back to the user so they know their name has been
// altered by the server.
if (nameChanged) {
var changeMsg = {
id: msg.id,
type: "rejectusername",
name: msg.name
};
connect.sendUTF(JSON.stringify(changeMsg));
}
// Set this connection's final username and send out the
// updated user list to all users. Yeah, we're sending a full
// list instead of just updating. It's horribly inefficient
// but this is a demo. Don't do this in a real app.
connect.username = msg.name;
sendUserListToAll();
sendToClients = false; // We already sent the proper responses
break;
}
// Convert the revised message back to JSON and send it out
// to the specified client or all clients, as appropriate. We
// pass through any messages not specifically handled
// in the select block above. This allows the clients to
// exchange signaling and other control objects unimpeded.
if (sendToClients) {
var msgString = JSON.stringify(msg);
var i;
// If the message specifies a target username, only send the
// message to them. Otherwise, send it to every user.
if (msg.target && msg.target !== undefined && msg.target.length !== 0) {
sendToOneUser(msg.target, msgString);
} else {
for (i=0; i<connectionArray.length; i++) {
connectionArray[i].sendUTF(msgString);
}
}
}
}
});
// Handle the WebSocket "close" event; this means a user has logged off
// or has been disconnected.
connection.on('close', function(reason, description) {
// First, remove the connection from the list of connections.
connectionArray = connectionArray.filter(function(el, idx, ar) {
return el.connected;
});
// Now send the updated user list. Again, please don't do this in a
// real application. Your users won't like you very much.
sendUserListToAll();
// Build and output log output for close information.
var logMessage = "Connection closed: " + connection.remoteAddress + " (" +
reason;
if (description !== null && description.length !== 0) {
logMessage += ": " + description;
}
logMessage += ")";
log(logMessage);
});
});

View File

@@ -0,0 +1,12 @@
#!/bin/sh
npm install
cp node_modules/webrtc-adapter/out/adapter.js .
mkdir -p cert
if [ ! -f cert/cert.pem ]; then
# Generate ssl certificate to use at web server side
openssl req -x509 -nodes -subj '/CN=demotest' -newkey rsa:4096 -keyout cert/key.pem -out cert/cert.pem -days 365
fi
node server.js

164
comms/webrtc-ws-example/style.css Executable file
View File

@@ -0,0 +1,164 @@
/*
WebSocket chat client
WebSocket and WebRTC based multi-user chat sample with two-way video
calling, including use of TURN if applicable or necessary.
This file describes the styling for the contents of the site as
presented to users.
To read about how this sample works: http://bit.ly/webrtc-from-chat
Any copyright is dedicated to the Public Domain.
http: creativecommons.org/publicdomain/zero/1.0/
*/
/* The list of users on the left side */
#userlistbox {
border: 1px solid black;
width:100%;
height:60px;
margin-top:0px;
margin-right:10px;
padding:1px;
list-style:none;
display:block;
line-height:1.1;
overflow-y:auto;
overflow-x:hidden;
}
#userlistbox li {
cursor: pointer;
padding: 1px;
}
/* The video panel */
.camera-box {
width: 500px;
height: 760px;
display:block;
vertical-align:top;
position:relative;
overflow:auto;
}
/* The main incoming video box */
#received_video {
border: 1px solid black;
box-shadow: 2px 2px 3px black;
width: 480px;
height: 360px;
position:absolute;
}
/* The small "preview" view of your camera */
#local_video {
width: 120px;
height: 90px;
position: absolute;
top: 10px;
left: 10px;
border: 1px solid rgba(255, 255, 255, 0.75);
box-shadow: 0 0 4px black;
}
/* The "Hang up" button */
#hangup-button {
display:block;
width:80px;
height:24px;
border-radius: 8px;
position:relative;
margin:auto;
top:324px;
background-color: rgba(150, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.7);
box-shadow: 0px 0px 1px 2px rgba(0, 0, 0, 0.2);
font-size: 14px;
font-family: "Lucida Grande", "Arial", sans-serif;
color: rgba(255, 255, 255, 1.0);
cursor: pointer;
}
#hangup-button:hover {
filter: brightness(150%);
-webkit-filter: brightness(150%);
}
#hangup-button:disabled {
filter: grayscale(50%);
-webkit-filter: grayscale(50%);
cursor: default;
}
/*** Flexbox layout ***/
.rowParent, .columnParent {
display: -webkit-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-box-direction: normal;
-webkit-box-orient: horizontal;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-align-content: flex-start;
-ms-flex-line-pack: stretch;
align-content: flex-start;
-webkit-box-align: stretch;
-webkit-align-items: stretch;
-ms-flex-align: stretch;
align-items: stretch;
}
.columnParent {
-webkit-box-orient: vertical;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.flexChild {
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
-webkit-align-self: auto;
-ms-flex-item-align: auto;
align-self: auto;
}
#control-row {
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto; height: 70px;
}
#camera-container {
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto; width: 500px;
}
#userlist-container {
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto; width: 120px;
line-height:1.1;
}