archive
This commit is contained in:
3
comms/webrtc-ws-example/.gitignore
vendored
Normal file
3
comms/webrtc-ws-example/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
adapter.js
|
||||
cert
|
||||
31
comms/webrtc-ws-example/README.md
Executable file
31
comms/webrtc-ws-example/README.md
Executable 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
635
comms/webrtc-ws-example/client.js
Executable 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);
|
||||
}
|
||||
52
comms/webrtc-ws-example/index.html
Executable file
52
comms/webrtc-ws-example/index.html
Executable 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
116
comms/webrtc-ws-example/package-lock.json
generated
Normal 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="
|
||||
}
|
||||
}
|
||||
}
|
||||
11
comms/webrtc-ws-example/package.json
Executable file
11
comms/webrtc-ws-example/package.json
Executable 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
305
comms/webrtc-ws-example/server.js
Executable 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);
|
||||
});
|
||||
});
|
||||
12
comms/webrtc-ws-example/startup.sh
Executable file
12
comms/webrtc-ws-example/startup.sh
Executable 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
164
comms/webrtc-ws-example/style.css
Executable 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;
|
||||
}
|
||||
Reference in New Issue
Block a user