Merge 71b2fc1a63 into 4f4c071693
commit
5c561165e4
|
|
@ -0,0 +1,9 @@
|
||||||
|
language: node_js
|
||||||
|
|
||||||
|
node_js:
|
||||||
|
- "8"
|
||||||
|
|
||||||
|
script:
|
||||||
|
- cd peer-exchange
|
||||||
|
- npm install
|
||||||
|
- npm test
|
||||||
32
cert.pem
32
cert.pem
|
|
@ -1,32 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFhTCCA22gAwIBAgIJALqPiXsVwZGuMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV
|
|
||||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
|
||||||
aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNjA1MDMwODA1
|
|
||||||
NDdaFw0xNzA1MDMwODA1NDdaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l
|
|
||||||
LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV
|
|
||||||
BAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMu4
|
|
||||||
8c4IwR1pO7N0H1D1PFFOgveRtLZu7vk8iEjsjAf+DD8CGpZ9BeV1/D4cMmn5Q2bp
|
|
||||||
y3UpwIiUeuz8fGVN3hhR3vaY2JS97qTK5+83pP09gperD5/+yZqT6CLuRoh6CYp0
|
|
||||||
vEHD5CyfUK9HSRhlKVeAnWYrIWD0krN1WRrgZVIcvzlV/pmbj1aKuQ1+cXJxAEfj
|
|
||||||
2/rhWTZf15hNu2fLWXemBeqQZTMOAl55CFVx5r7wPyYw4i9I2az9aj/fQRPjMQDV
|
|
||||||
j/d5bdF68t5hxIf5wk/gGo8XBMBmo0uAj60Qp81wHirEr2FFMK7I+48XMFA9SfxR
|
|
||||||
SahVZhjbz58K0+URD/taJewL3qrkoivJGQfL9waF+NlLJ8DoGQLN/tWNX9nsQIwu
|
|
||||||
HBijg7z4hDP/cTm2dzIyxaSpSp904r3SE74n/+jI+BOWmZwo3iRJMKzWhx80VtM4
|
|
||||||
qXZyHUrxCSf0Q1yqSlcyNAVpe3fCpGO9hR6ao3MX6xx3fgdQ1LUcNyZbmHFWPZ0N
|
|
||||||
pp+oI8kx+9ksMMtcCJq4KGgoLaCEtJjf4a0TDiCL1eNnym79YSto2hQc8cyvc4my
|
|
||||||
kerRAy9mcvmpd/e7j/eKQikNi/usWxGbeIOiy/5zG2Xrf9S2fNsc4/TdnLn+OLJX
|
|
||||||
5pJ4AuVIxkA/zHsE0kcANssoJk6Hgxxek6GizytdAgMBAAGjUDBOMB0GA1UdDgQW
|
|
||||||
BBSP8O2pIi6niJ4w0gHXR6PzawwRrDAfBgNVHSMEGDAWgBSP8O2pIi6niJ4w0gHX
|
|
||||||
R6PzawwRrDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCkxybb3ci2
|
|
||||||
NjbzjmvPflqmdTwyGOvreouCXmVHvm5vNL69LSOgZ6PmyEpyywALJqhaEhCXLJrq
|
|
||||||
yyZ82My9TxYn84rukH20lyBgbjIMFnabamFAOB9LqMqM4jaedT00OEbfY+uEE5Nw
|
|
||||||
NwOjNdUrrwfD2PQLAF06Orft4wex/L9psHBHaxWybnPkx+UAlA/Moz8kmCWWq9SA
|
|
||||||
JPvFNXwN3WylgCgAG8kR9PYtLWQEFSp8D9lSezI1C8Ddk0ymTw4aF+p9gHbbjZw4
|
|
||||||
XEEpKgRwhpHa7IrcVyeXdQ78/pYc1f7fThL88LLv7lwtJnDWqWYOJrO4HH064IKn
|
|
||||||
KazLz1v9UT6yegUFiP/y688BH9LTNNf4pxGOee7LiDpfc7wyIkwrAjxoy4gc8xjp
|
|
||||||
Gp6aAvLC9Wzc+i2P0Hz/CvS5ffhZ9k0XGIvSHGbMRN1G4l9cOEQadyzGZGUryFRC
|
|
||||||
yBvXGBfSEYdr4OWOhdrelwpZXvl8rLRaAKU2xxIYZT9JtAXmTjWRFnjYLAuJil7e
|
|
||||||
ywPbGFYsB5d9Buiw+rJLnGF4NOudk1wXBElrhdjoajDS3Miw5vn2TbuHxIleoszC
|
|
||||||
fojNr80RenHT+EnY+0MecCUaeKfU4xd9nIl9I/1MpvM84zSu7Rhd0xEli6SN3fen
|
|
||||||
8zRCZGBfwD1y21dDrpgWDPFxjc5Cas63bg==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const peerConnectionConfig = {
|
||||||
|
'iceServers': [
|
||||||
|
{'urls': 'stun:stun.services.mozilla.com'},
|
||||||
|
{'urls': 'stun:stun.l.google.com:19302'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dataChannelConfig = {
|
||||||
|
ordered: false, // do not guarantee order
|
||||||
|
maxRetransmitTime: 500, // in milliseconds
|
||||||
|
};
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
|
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
|
||||||
<script src="webrtc.js"></script>
|
<script type="module" src="webrtc.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -11,10 +11,7 @@
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<input type="button" id="start" onclick="start(true)" value="Start Video"></input>
|
<button id="host">Host</button>
|
||||||
|
<button id="connect">Connect</button>
|
||||||
<script type="text/javascript">
|
|
||||||
pageReady();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
export async function createPeerExchange(address) {
|
||||||
|
const server = new WebSocket(address);
|
||||||
|
const listeners = new Set();
|
||||||
|
|
||||||
|
function onMessage(message) {
|
||||||
|
const data = JSON.parse(message.data);
|
||||||
|
listeners.forEach(callback => callback(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function listen(callback) {
|
||||||
|
listeners.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function send(data) {
|
||||||
|
server.send(JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
server.addEventListener('message', onMessage);
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
server.addEventListener('open', () => {
|
||||||
|
resolve({
|
||||||
|
listen,
|
||||||
|
send,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { createUUID } from './random.js';
|
||||||
|
|
||||||
|
function errorHandler(error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createHost(peerExchange, peerConnectionConfig) {
|
||||||
|
const listeners = new Set();
|
||||||
|
|
||||||
|
function onConnection(callback) {
|
||||||
|
listeners.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitConnection(conn) {
|
||||||
|
listeners.forEach(callback => callback(conn));
|
||||||
|
}
|
||||||
|
|
||||||
|
peerExchange.listen(async signal => {
|
||||||
|
console.log("Host signal", signal);
|
||||||
|
if (signal.sdp && signal.sdp.type === "offer") {
|
||||||
|
const {conn, send} = createConn(peerExchange, peerConnectionConfig);
|
||||||
|
|
||||||
|
emitConnection(conn);
|
||||||
|
|
||||||
|
const remoteDesc = new RTCSessionDescription(signal.sdp);
|
||||||
|
conn.setRemoteDescription(remoteDesc).catch(errorHandler);
|
||||||
|
|
||||||
|
const localDesc = await conn.createAnswer();
|
||||||
|
conn.setLocalDescription(localDesc).catch(errorHandler);
|
||||||
|
|
||||||
|
send({sdp: localDesc});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
onConnection,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGuest(peerExchange, peerConnectionConfig) {
|
||||||
|
const {conn, onSignal, send} = createConn(peerExchange, peerConnectionConfig);
|
||||||
|
|
||||||
|
onSignal(signal => {
|
||||||
|
console.log("Guest signal", signal);
|
||||||
|
if (signal.sdp && signal.sdp.type === "answer") {
|
||||||
|
const remoteDesc = new RTCSessionDescription(signal.sdp);
|
||||||
|
conn.setRemoteDescription(remoteDesc).catch(errorHandler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
const localDesc = await conn.createOffer();
|
||||||
|
conn.setLocalDescription(localDesc).catch(errorHandler);
|
||||||
|
send({sdp: localDesc});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
conn,
|
||||||
|
connect,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function createConn(peerExchange, peerConnectionConfig) {
|
||||||
|
const conn = new RTCPeerConnection(peerConnectionConfig);
|
||||||
|
const uuid = createUUID();
|
||||||
|
|
||||||
|
conn.addEventListener('icecandidate', event => {
|
||||||
|
if(event.candidate != null) {
|
||||||
|
send({ice: event.candidate});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const listeners = new Set();
|
||||||
|
|
||||||
|
function onSignal(callback) {
|
||||||
|
listeners.add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
peerExchange.listen(signal => {
|
||||||
|
if (signal.uuid === uuid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners.forEach(callback => callback(signal));
|
||||||
|
});
|
||||||
|
|
||||||
|
onSignal(signal => {
|
||||||
|
if(signal.ice) {
|
||||||
|
conn.addIceCandidate(new RTCIceCandidate(signal.ice)).catch(errorHandler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function send(data) {
|
||||||
|
peerExchange.send(Object.assign({}, data, {uuid}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
conn,
|
||||||
|
onSignal,
|
||||||
|
send,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
// Taken from http://stackoverflow.com/a/105074/515584
|
||||||
|
// Strictly speaking, it's not a real UUID, but it gets the job done here
|
||||||
|
export function createUUID() {
|
||||||
|
function s4() {
|
||||||
|
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
||||||
|
}
|
||||||
126
client/webrtc.js
126
client/webrtc.js
|
|
@ -1,101 +1,81 @@
|
||||||
var localVideo;
|
import { dataChannelConfig, peerConnectionConfig } from './config.js';
|
||||||
var remoteVideo;
|
import { createPeerExchange } from './peer-exchange.js';
|
||||||
|
import { createHost, createGuest} from './peer.js';
|
||||||
|
|
||||||
var peerConnection;
|
var peerConnection;
|
||||||
var uuid;
|
var serverConnection;
|
||||||
|
|
||||||
var peerConnectionConfig = {
|
async function pageReady() {
|
||||||
'iceServers': [
|
document.querySelector("button#host")
|
||||||
{'urls': 'stun:stun.services.mozilla.com'},
|
.addEventListener("click", host);
|
||||||
{'urls': 'stun:stun.l.google.com:19302'},
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
function pageReady() {
|
document.querySelector("button#connect")
|
||||||
uuid = uuid();
|
.addEventListener("click", connect);
|
||||||
|
|
||||||
localVideo = document.getElementById('localVideo');
|
serverConnection = await createPeerExchange('ws://' + window.location.hostname + ':8080');
|
||||||
remoteVideo = document.getElementById('remoteVideo');
|
}
|
||||||
|
|
||||||
serverConnection = new WebSocket('wss://' + window.location.hostname + ':8443');
|
function host() {
|
||||||
serverConnection.onmessage = gotMessageFromServer;
|
const host = createHost(serverConnection, peerConnectionConfig);
|
||||||
|
host.onConnection(conn => {
|
||||||
|
console.log("Host Received Connection", conn);
|
||||||
|
|
||||||
|
conn.addEventListener('addstream', stream => {
|
||||||
|
console.log("Host received stream", stream);
|
||||||
|
document.getElementById('remoteVideo').srcObject = event.stream;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log("Host waiting for connections");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
var constraints = {
|
var constraints = {
|
||||||
video: true,
|
video: true,
|
||||||
audio: true,
|
audio: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(navigator.mediaDevices.getUserMedia) {
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
navigator.mediaDevices.getUserMedia(constraints).then(getUserMediaSuccess).catch(errorHandler);
|
|
||||||
} else {
|
const guest = createGuest(serverConnection, peerConnectionConfig);
|
||||||
alert('Your browser does not support getUserMedia API');
|
guest.conn.addStream(stream);
|
||||||
}
|
|
||||||
|
console.log("Connecting");
|
||||||
|
guest.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUserMediaSuccess(stream) {
|
async function setup() {
|
||||||
localStream = stream;
|
var constraints = {
|
||||||
localVideo.src = window.URL.createObjectURL(stream);
|
video: true,
|
||||||
|
audio: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
document.getElementById('localVideo').srcObject = stream;
|
||||||
|
|
||||||
|
peerConnection = createPeer(serverConnection, peerConnectionConfig);
|
||||||
|
|
||||||
|
peerConnection.addEventListener('addstream', stream => {
|
||||||
|
document.getElementById('remoteVideo').srcObject = event.stream;
|
||||||
|
});
|
||||||
|
|
||||||
|
peerConnection.addStream(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
function start(isCaller) {
|
function extendOffer() {
|
||||||
peerConnection = new RTCPeerConnection(peerConnectionConfig);
|
console.log('Extending offer');
|
||||||
peerConnection.onicecandidate = gotIceCandidate;
|
peerConnection.createOffer().then(createdDescription).catch(errorHandler);
|
||||||
peerConnection.onaddstream = gotRemoteStream;
|
|
||||||
peerConnection.addStream(localStream);
|
|
||||||
|
|
||||||
if(isCaller) {
|
|
||||||
peerConnection.createOffer().then(createdDescription).catch(errorHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function gotMessageFromServer(message) {
|
|
||||||
if(!peerConnection) start(false);
|
|
||||||
|
|
||||||
var signal = JSON.parse(message.data);
|
|
||||||
|
|
||||||
// Ignore messages from ourself
|
|
||||||
if(signal.uuid == uuid) return;
|
|
||||||
|
|
||||||
if(signal.sdp) {
|
|
||||||
peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp)).then(function() {
|
|
||||||
// Only create answers in response to offers
|
|
||||||
if(signal.sdp.type == 'offer') {
|
|
||||||
peerConnection.createAnswer().then(createdDescription).catch(errorHandler);
|
|
||||||
}
|
|
||||||
}).catch(errorHandler);
|
|
||||||
} else if(signal.ice) {
|
|
||||||
peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice)).catch(errorHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function gotIceCandidate(event) {
|
|
||||||
if(event.candidate != null) {
|
|
||||||
serverConnection.send(JSON.stringify({'ice': event.candidate, 'uuid': uuid}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createdDescription(description) {
|
function createdDescription(description) {
|
||||||
console.log('got description');
|
console.log('got description');
|
||||||
|
|
||||||
peerConnection.setLocalDescription(description).then(function() {
|
peerConnection.setLocalDescription(description).then(function() {
|
||||||
serverConnection.send(JSON.stringify({'sdp': peerConnection.localDescription, 'uuid': uuid}));
|
serverConnection.send({'sdp': peerConnection.localDescription});
|
||||||
}).catch(errorHandler);
|
}).catch(errorHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
function gotRemoteStream(event) {
|
|
||||||
console.log('got remote stream');
|
|
||||||
remoteVideo.src = window.URL.createObjectURL(event.stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
function errorHandler(error) {
|
function errorHandler(error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Taken from http://stackoverflow.com/a/105074/515584
|
pageReady();
|
||||||
// Strictly speaking, it's not a real UUID, but it gets the job done here
|
|
||||||
function uuid() {
|
|
||||||
function s4() {
|
|
||||||
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
#! /bin/bash
|
|
||||||
|
|
||||||
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
|
|
||||||
56
key.pem
56
key.pem
|
|
@ -1,56 +0,0 @@
|
||||||
# DO NOT USE THIS OUTSIDE OF THIS EXAMPLE.
|
|
||||||
# Yes, this file is purposely checked in to Git. It is a self-signed cert used for demonstration purposes.
|
|
||||||
# There's even a generate_cert.sh script included if you need to generate your own so no excuses!
|
|
||||||
|
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
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
|
|
||||||
0qgfH8qILf4t1ULeWxPK4n3dHZ49K2E=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"name": "snex-peering",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "A generic WebRTC library and peering server.",
|
||||||
|
"scripts": {
|
||||||
|
"start:server": "node peer-exchange/src/server.js",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/shanet/WebRTC-Example.git"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-static": "^0.7.10",
|
||||||
|
"ws": "^3.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^21.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "@snex/peer-exchange",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Peer Exchange server for WebRTC",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/server.js",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "Pontus Alexander <pontus.alexander@gmail.com>",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^3.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"jest": "^21.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
const {Client} = require('../models/client.js');
|
||||||
|
const {createConnectionHandler} = require('../handler.js');
|
||||||
|
|
||||||
|
describe('createConnectionHandler', () => {
|
||||||
|
it('listens to messages', () => {
|
||||||
|
const regMock = {
|
||||||
|
handleMessage: jest.fn(),
|
||||||
|
handleClose: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = createConnectionHandler(regMock);
|
||||||
|
|
||||||
|
const connMock = {
|
||||||
|
on: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
handler(connMock);
|
||||||
|
|
||||||
|
expect(connMock.on).toHaveBeenCalledTimes(2);
|
||||||
|
expect(connMock.on.mock.calls[0][0]).toBe('message');
|
||||||
|
expect(connMock.on.mock.calls[0][1]).toBeInstanceOf(Function);
|
||||||
|
expect(connMock.on.mock.calls[1][0]).toBe('close');
|
||||||
|
expect(connMock.on.mock.calls[1][1]).toBeInstanceOf(Function);
|
||||||
|
|
||||||
|
const messageCallback = connMock.on.mock.calls[0][1];
|
||||||
|
expect(regMock.handleMessage).toHaveBeenCalledTimes(0);
|
||||||
|
messageCallback('arbitrary message');
|
||||||
|
expect(regMock.handleMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(regMock.handleMessage.mock.calls[0][0]).toBeInstanceOf(Client);
|
||||||
|
expect(regMock.handleMessage.mock.calls[0][0].conn).toBe(connMock);
|
||||||
|
expect(regMock.handleMessage.mock.calls[0][1]).toBe('arbitrary message');
|
||||||
|
|
||||||
|
const disconnectCallback = connMock.on.mock.calls[1][1];
|
||||||
|
expect(regMock.handleClose).toHaveBeenCalledTimes(0);
|
||||||
|
disconnectCallback();
|
||||||
|
expect(regMock.handleClose).toHaveBeenCalledTimes(1);
|
||||||
|
expect(regMock.handleClose.mock.calls[0][0]).toBeInstanceOf(Client);
|
||||||
|
expect(regMock.handleClose.mock.calls[0][0].conn).toBe(connMock);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
const {Client} = require('./models/client.js');
|
||||||
|
|
||||||
|
function createConnectionHandler(registry) {
|
||||||
|
return function handleConnection(conn) {
|
||||||
|
const client = new Client(conn);
|
||||||
|
|
||||||
|
conn.on('message', message => {
|
||||||
|
registry.handleMessage(client, message);
|
||||||
|
});
|
||||||
|
|
||||||
|
conn.on('close', () => {
|
||||||
|
registry.handleClose(client);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createConnectionHandler,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
const {Channel} = require('../channel.js');
|
||||||
|
const {Client} = require('../client.js');
|
||||||
|
|
||||||
|
describe('Channel', () => {
|
||||||
|
it('when joined by two parties broadcasting sends signal to each except sender', () => {
|
||||||
|
const channel = new Channel();
|
||||||
|
const client1 = new Client();
|
||||||
|
const client2 = new Client();
|
||||||
|
client1.send = jest.fn();
|
||||||
|
client2.send = jest.fn();
|
||||||
|
channel.join(client1);
|
||||||
|
channel.join(client2);
|
||||||
|
const data = {my: 'data'};
|
||||||
|
channel.broadcast(client1, data);
|
||||||
|
expect(client1.send).toHaveBeenCalledTimes(0);
|
||||||
|
expect(client2.send).toHaveBeenCalledTimes(1);
|
||||||
|
expect(client2.send).toHaveBeenLastCalledWith(data);
|
||||||
|
|
||||||
|
const client3 = new Client();
|
||||||
|
client3.send = jest.fn();
|
||||||
|
channel.join(client3);
|
||||||
|
channel.broadcast(client1, data);
|
||||||
|
expect(client1.send).toHaveBeenCalledTimes(0);
|
||||||
|
expect(client2.send).toHaveBeenCalledTimes(2);
|
||||||
|
expect(client3.send).toHaveBeenCalledTimes(1);
|
||||||
|
expect(client2.send).toHaveBeenLastCalledWith(data);
|
||||||
|
expect(client2.send).toHaveBeenLastCalledWith(data);
|
||||||
|
|
||||||
|
channel.leave(client2);
|
||||||
|
channel.broadcast(client3, data);
|
||||||
|
expect(client1.send).toHaveBeenCalledTimes(1);
|
||||||
|
expect(client1.send).toHaveBeenLastCalledWith(data);
|
||||||
|
expect(client3.send).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
const {ack, Client} = require('../client.js');
|
||||||
|
|
||||||
|
describe('Client', () => {
|
||||||
|
it('send messages encoded as JSON to connection', () => {
|
||||||
|
const connMock = {
|
||||||
|
send: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = new Client(connMock);
|
||||||
|
client.send({my: 'data'});
|
||||||
|
|
||||||
|
expect(connMock.send).toBeCalledWith('{"my":"data"}', ack);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
const {Registry} = require('../registry.js');
|
||||||
|
const {Client} = require('../client.js');
|
||||||
|
|
||||||
|
describe('Registry', () => {
|
||||||
|
it('broadcasts message to everyone with same id', () => {
|
||||||
|
const reg = new Registry();
|
||||||
|
const client1 = new Client();
|
||||||
|
const client2 = new Client();
|
||||||
|
|
||||||
|
client1.send = jest.fn();
|
||||||
|
client2.send = jest.fn();
|
||||||
|
|
||||||
|
reg.handleMessage(client1, JSON.stringify({channelId: 'x', type: "greet"}));
|
||||||
|
reg.handleMessage(client2, JSON.stringify({channelId: 'x', type: "offer"}));
|
||||||
|
|
||||||
|
expect(client1.send).toHaveBeenCalledTimes(1);
|
||||||
|
expect(client1.send).lastCalledWith({"channelId": "x", "type": "offer"});
|
||||||
|
expect(client2.send).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleans up when leaving', () => {
|
||||||
|
const reg = new Registry();
|
||||||
|
const client = new Client();
|
||||||
|
|
||||||
|
reg.handleMessage(client, JSON.stringify({channelId: 'x'}));
|
||||||
|
reg.handleDisconnect(client);
|
||||||
|
|
||||||
|
expect(reg.channels.size).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
class Channel
|
||||||
|
{
|
||||||
|
constructor() {
|
||||||
|
this.clients = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast(originClient, data) {
|
||||||
|
[...this.clients]
|
||||||
|
.filter(candidateClient => candidateClient !== originClient)
|
||||||
|
.forEach(targetClient => targetClient.send(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
join(client) {
|
||||||
|
this.clients.add(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(client) {
|
||||||
|
this.clients.delete(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Channel,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
function ack(err) {
|
||||||
|
if (err) {
|
||||||
|
console.log('Error sending message', msg, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Client
|
||||||
|
{
|
||||||
|
constructor(conn) {
|
||||||
|
this.conn = conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data) {
|
||||||
|
const msg = JSON.stringify(data);
|
||||||
|
this.conn.send(msg, ack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ack,
|
||||||
|
Client,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
const {Channel} = require('./channel');
|
||||||
|
|
||||||
|
class Registry {
|
||||||
|
constructor() {
|
||||||
|
this.channels = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnect(client) {
|
||||||
|
this.channels.forEach((channel, id) => {
|
||||||
|
channel.leave(client);
|
||||||
|
if (channel.clients.size === 0) {
|
||||||
|
this.channels.delete(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(client, message) {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
const {channelId} = data;
|
||||||
|
|
||||||
|
if (!this.channels.has(channelId)) {
|
||||||
|
this.channels.set(channelId, new Channel());
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = this.channels.get(channelId);
|
||||||
|
channel.join(client);
|
||||||
|
channel.broadcast(client, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
Registry,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
const WebSocketServer = require('ws').Server;
|
||||||
|
const {Registry} = require('./models/registry.js');
|
||||||
|
const {createConnectionHandler} = require('./handler.js');
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 9000;
|
||||||
|
const server = new WebSocketServer({port: 9000});
|
||||||
|
|
||||||
|
server.on('connection', createConnectionHandler(new Registry()));
|
||||||
|
|
||||||
|
console.log(`Running on port ${PORT}`);
|
||||||
|
|
@ -1,39 +1,30 @@
|
||||||
const HTTPS_PORT = 8443;
|
const HTTP_PORT = 8080;
|
||||||
|
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const https = require('https');
|
const http = require('http');
|
||||||
|
const static = require('node-static');
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
const WebSocketServer = WebSocket.Server;
|
const WebSocketServer = WebSocket.Server;
|
||||||
|
|
||||||
// Yes, SSL is required
|
const file = new static.Server('./client');
|
||||||
const serverConfig = {
|
|
||||||
key: fs.readFileSync('key.pem'),
|
|
||||||
cert: fs.readFileSync('cert.pem'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// Create a server for the client html page
|
// Create a server for the client html page
|
||||||
var handleRequest = function(request, response) {
|
var handleRequest = function(request, response) {
|
||||||
// Render the single client html file for any request the HTTP server receives
|
request.addListener('end', () => {
|
||||||
console.log('request received: ' + request.url);
|
file.serve(request, response);
|
||||||
|
}).resume();
|
||||||
if(request.url === '/') {
|
|
||||||
response.writeHead(200, {'Content-Type': 'text/html'});
|
|
||||||
response.end(fs.readFileSync('client/index.html'));
|
|
||||||
} else if(request.url === '/webrtc.js') {
|
|
||||||
response.writeHead(200, {'Content-Type': 'application/javascript'});
|
|
||||||
response.end(fs.readFileSync('client/webrtc.js'));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var httpsServer = https.createServer(serverConfig, handleRequest);
|
var httpServer = http.createServer(handleRequest);
|
||||||
httpsServer.listen(HTTPS_PORT, '0.0.0.0');
|
httpServer.listen(HTTP_PORT, '0.0.0.0');
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// Create a server for handling websocket calls
|
// Create a server for handling websocket calls
|
||||||
var wss = new WebSocketServer({server: httpsServer});
|
var wss = new WebSocketServer({server: httpServer});
|
||||||
|
|
||||||
wss.on('connection', function(ws) {
|
wss.on('connection', function(ws) {
|
||||||
ws.on('message', function(message) {
|
ws.on('message', function(message) {
|
||||||
|
|
@ -51,4 +42,4 @@ wss.broadcast = function(data) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Server running. Visit https://localhost:' + HTTPS_PORT + ' in Firefox/Chrome (note the HTTPS; there is no HTTP -> HTTPS redirect!)');
|
console.log('Server running. Visit http://localhost:' + HTTP_PORT + ' in Firefox/Chrome (note the HTTPS; there is no HTTP -> HTTPS redirect!)');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue