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

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;
}