WebRTC: One to one Audio call not working in different machine - node.js

I am trying to implement a one to one audio call using webRTC (signalling using websockets) . But it works when i try it in one system using multiple tabs of chrome (localhost). When I try to hit my server from another machine it does initial handshakes , but call doesn't happen.
But when i try to change the tag to and changed the constraints to video constraints . it works even if the we try to access from other machine (i.e video call works ).
I initially thought it was because if firewall but when video call worked I was puzzled .
Here is my code:
// Constraints to get audio stream only
$scope.constraints = {
audio: {
mandatory: {
googEchoCancellation: true
},
optional: []
},
video:false
};
navigator.getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
// success Callback of getUserMedia(), stream variable is the audio stream.
$scope.successCallback = function (stream) {
if (window.URL) {
myVideo.src = window.URL.createObjectURL(stream); // converting media stream to Blob URL.
} else {
myVideo.src = stream;
}
//attachMediaStream(audioTag, stream);
localStream = stream;
if (initiator)
maybeStart();
else
doAnswer();
};
// failure Callback of getUserMedia()
$scope.failureCallback = function (error) {
console.log('navigator.getUserMedia Failed: ', error);
};
var initiator, started = false;
$("#call").click(function () {
socket.emit("message", undefined);
initiator = true;
navigator.getUserMedia($scope.constraints, $scope.successCallback, $scope.failureCallback);
});
var channelReady = false;
socket.on('message', function (data) {
channelReady = true;
if (data) {
if (data.type === 'offer') {
if (!initiator) {
$("#acceptCall").show();
$("#acceptCall").click(function(){
if (!initiator && !started) {
var pc_config = {
iceServers: [
{ url: "stun:stun.l.google.com:19302" },
{ url: "turn:numb.viagenie.ca", credential: "drfunk", username: "toadums#hotmail.com"}
]
};
pc = new webkitRTCPeerConnection(pc_config);
pc.onicecandidate = onIceCandidate;
pc.onaddstream = onRemoteStreamAdded;
}
pc.setRemoteDescription(new RTCSessionDescription(data));
$scope.acceptCall();
});
}
} else if (data.type === 'answer' && started) {
pc.onaddstream = onRemoteStreamAdded;
pc.setRemoteDescription(new RTCSessionDescription(data));
} else if (data.type === 'candidate' && started) {
var candidate = new RTCIceCandidate({
sdpMLineIndex: data.label,
candidate: data.candidate
});
pc.addIceCandidate(candidate);
} else if (data.type === 'bye' && started) {
console.log("Bye");
}
}
});
function onRemoteStreamAdded(event) {
othersVideo.src = URL.createObjectURL(event.stream);
};
var sdpConstraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': false
}
};
function doAnswer() {
pc.addStream(localStream);
pc.createAnswer(gotDescription,null,sdpConstraints);
}
function gotDescription(desc) {
pc.setLocalDescription(desc);
socket.send(desc);
}
function maybeStart() {
if (!started && localStream && channelReady)
createPeerConnection();
pc.addStream(localStream);
started = true;
if (initiator)
doCall();
}
$scope.acceptCall = function () {
navigator.getUserMedia($scope.constraints, $scope.successCallback, $scope.failureCallback);
}
function createPeerConnection() {
var pc_config = {
iceServers: [
{ url: "stun:stun.l.google.com:19302" },
{ url: "turn:numb.viagenie.ca", credential: "drfunk", username: "toadums#hotmail.com"}
]
};
pc = new webkitRTCPeerConnection(pc_config);
pc.onicecandidate = onIceCandidate;
console.log("Created RTCPeerConnnection with config:\n" + " \"" +
JSON.stringify(pc_config) + "\".");
};
function doCall() {
$scope.caller = true;
pc.createOffer(setLocalAndSendMessage,null,sdpConstraints);
};
function setLocalAndSendMessage(sessionDescription) {
pc.setLocalDescription(sessionDescription);
socket.send(sessionDescription);
}
function onIceCandidate(event) {
if (event.candidate) {
socket.emit('message', {
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate
});
} else {
console.log("End of candidates.");
}
}

If navigator.mediaDevices is undefined, this because work only in secure context (https)
see:
https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia

Related

Camera flipping issue -Twilio version 2.24

The flip camera doesn't work on devices where they have more than 2 video inputs. In the first load, the video appears but when the flip camera button is clicked the application throws an error.
Expected behavior:
The camera should be flipped (environment)
Actual behavior:
It throws the following error:
error : call to getusermedia failed domexception could not start video source
Software versions:
Browser(s): Chrome
Operating System: Android (devices that I'm checking & it's not working eq. Samsung M31, Redmi note 11 T, One Plus 7T)
twilio-video.js: 2.24.0
Third-party libraries (e.g., Angular, nodejs, etc.):
Code used to start twilio stream
async startTwilioStream(twilioToken: string, localVideo: ElementRef, remoteVideo: ElementRef): Promise<void> {
console.log('startTwilioStream');
this.localVideoElement = localVideo;
this.remoteVideoElement = remoteVideo;
await this.startLocalVideo(this.localVideoElement);
this.connectOptions = {
video: false,
audio: false,
tracks: [this.localAudioTrack, this.localVideoTrack],
audioConstraints: {
mandatory: {
googAutoGainControl: false,
},
},
region: 'in1',
preferredAudioCodecs: ['opus'],
preferredVideoCodecs: ['H264'],
};
connect(twilioToken, this.connectOptions).then((twilioRoom: any) => {
console.log('twilioRoom.localParticipant ================== ', twilioRoom.localParticipant);
setTimeout(() => {
if (this.remoteVideoElement?.nativeElement) {
this.remoteVideoElement.nativeElement.muted = false;
}
}, 5000);
this.twilioRoom = twilioRoom;
console.log('this.twilioRoom vvvv', this.twilioRoom);
twilioRoom.localParticipant.setNetworkQualityConfiguration({
local: 2,
remote: 1,
});
// flip.addEventListener('change', this.updateVideoDevice);
twilioRoom.on('participantConnected', participant => {
console.log('participant Connected===============', participant);
participant.tracks.forEach((publication) => {
console.log('publication', publication);
if (publication.isSubscribed) {
const track = publication.track;
this.attachTracks([track]);
}
});
this.twilioRoom = twilioRoom;
});
twilioRoom.on('participantDisconnected', participant => {
console.log('participantDisconnected', participant);
console.log('SOME PARTICIPANT DISCONNECTED');
if ((participant.identity === 'agent-screen-share' && this.serviceUserType !== 'agent') || (participant.identity === 'consumer-screen-share' && this.serviceUserType !== 'consumer')) {
this.changeDetectionEmitter.emit('remoteScreenShareStopped');
this.isRemoteScreenShareOn = false;
} else if (participant.identity !== 'agent-screen-share' && participant.identity !== 'consumer-screen-share') {
console.log('real participant dced');
this.remoteMediaStream = null;
this.detachTracks(participant);
this.isRemoteVideoOn = false;
}
this.twilioRoom = twilioRoom;
});
twilioRoom.participants.forEach((participant) => {
participant.tracks.forEach((publication) => {
if (publication.track) {
const track = publication.track;
this.attachTracks([track]);
}
});
participant.on('trackSubscribed', (track) => {
console.log('trackSubscribed', track);
this.attachTracks([track]);
});
this.twilioRoom = twilioRoom;
});
twilioRoom.on('trackAdded', (track, participant) => {
console.log('trackAdded', track, participant);
this.attachTracks([track]);
this.twilioRoom = twilioRoom;
});
// When a Participant adds a Track, attach it to the DOM.
twilioRoom.on('trackSubscribed', (track, err, participant) => {
console.log('trackSubscribed', track);
this.sendLoaderStatus('ringing');
if ((participant.identity === 'agent-screen-share' && this.serviceUserType !== 'agent') || (participant.identity === 'consumer-screen-share' && this.serviceUserType !== 'consumer')) {
this.attachScreenShareTrack([track]);
} else if (participant.identity === 'agent-screen-share' || participant.identity === 'consumer-screen-share') {
} else {
this.attachTracks([track]);
}
this.twilioRoom = twilioRoom;
});
// When a Participant removes a Track, detach it from the DOM.
twilioRoom.on('trackRemoved', (track, participant) => {
console.log('trackRemoved', track);
this.detachTracks([track]);
this.twilioRoom = twilioRoom;
});
}, err => {
});
}
Start local video and local audio track
async startLocalVideo(localVideo: ElementRef, deviceId = 'user'): Promise<void> {
this.localVideoElement = localVideo;
const localAudioTrack = await createLocalAudioTrack({
audio: true
});
const localVideoTrack = await createLocalVideoTrack({
facingMode: deviceId
});
this.localAudioTrack = localAudioTrack;
this.localVideoTrack = localVideoTrack;
if (!this.localAudioTrack) {
alert('Audio source not found, do you hava a mic connected ?');
}
if (!this.localVideoTrack) {
alert('Video source not found, do you hava a videocam connected ?');
}
console.log('this.localVideoTrack to check', this.localVideoTrack);
this.localDisplayMediaStream = new MediaStream();
console.log('this.localVideoTrack.mediaStreamTrack to check', this.localVideoTrack.mediaStreamTrack);
this.localDisplayMediaStream.addTrack(this.localVideoTrack.mediaStreamTrack);
console.log('this.localDisplayMediaStream to check', this.localDisplayMediaStream);
this.localVideoElement.nativeElement.srcObject = this.localDisplayMediaStream;
}
Flip event listener calls on the click of switch button
const flip = document.querySelector('#flip');
flip.addEventListener('click', (e) => {
if (this.facingMode == "user") {
this.facingMode = "environment";
this.twilioService.switch(this.facingMode)
} else {
this.facingMode = "user";
this.twilioService.switch(this.facingMode)
}
});
Switch camera function calls in flip event listener
async switch(facingMode) {
console.log(this.localDisplayMediaStream);
if (this.localDisplayMediaStream) {
this.localDisplayMediaStream.getTracks().forEach(track => {
track.stop();
});
if (this.twilioRoom) {
await this.twilioRoom.localParticipant.videoTracks.forEach((track: any) => {
console.log('track', track);
track.track.stop();
});
}
}
const localVideoTrack = await createLocalVideoTrack({
facingMode: facingMode
});
this.localVideoTrack = localVideoTrack;
this.localDisplayMediaStream = new MediaStream();
this.localDisplayMediaStream.addTrack(this.localVideoTrack.mediaStreamTrack);
this.localVideoElement.nativeElement.srcObject = this.localDisplayMediaStream;
}

WebSocket and multiple Dyno's on Heroku Node.js app Using Redis

I'm building an App deployed to Heroku which uses WebSocket and Redis.
The WebSocket connection is working properly when I use only 1 dyno, but when I scale to 2, then I send event my application do it twice.
const ws = require('ws')
const jwt = require('jsonwebtoken')
const redis = require('redis')
const User = require('../models/user')
function verifyClient (info, callback) {
let token = info.req.headers['sec-websocket-protocol']
if (!token) { callback(false, 401, 'Unauthorized') } else {
jwt.verify(token, Config.APP_SECRET, (err, decoded) => {
if (err) { callback(false, 401, 'Unauthorized') } else {
if (info.req.headers.gameId) { info.req.gameId = info.req.headers.gameId }
info.req.userId = decoded.aud
callback(true)
}
})
}
};
let websocketServer, pub, sub
let clients = {}
let namespaces = {}
exports.initialize = function (httpServer) {
websocketServer = new ws.Server({
server: httpServer,
verifyClient: verifyClient
})
pub = redis.createClient(Config.REDIS_URL, { no_ready_check: true, detect_buffers: true })
pub.auth(Config.REDIS_PASSWORD, function (err) {
if (err) throw err
})
sub = redis.createClient(Config.REDIS_URL, { no_ready_check: true, detect_buffers: true })
sub.auth(Config.REDIS_PASSWORD, function (err) {
if (err) throw err
})
function handleConnection (socket) {
// socket.send(socket.upgradeReq.userId);
socket.userId = socket.upgradeReq.userId // get the user id parsed from the decoded JWT in the middleware
socket.isAlive = true
socket.scope = socket.upgradeReq.url.split('/')[1] // url = "/scope/whatever" => ["", "scope", "whatever"]
console.log('New connection: ' + socket.userId + ', scope: ' + socket.scope)
socket.on('message', (data, flags) => { handleIncomingMessage(socket, data, flags) })
socket.once('close', (code, reason) => { handleClosedConnection(socket, code, reason) })
socket.on('pong', heartbeat)
if (socket.scope === 'gameplay') {
try {
User.findByIdAndUpdate(socket.userId, { $set: { isOnLine: 2, lastSeen: Date.now() } }).select('id').lean()
let key = [socket.userId, socket.scope].join(':')
clients[key] = socket
sub.psubscribe(['dispatch', '*', socket.userId, socket.scope].join(':'))
} catch (e) { console.log(e) }
} else {
console.log('Scope : ' + socket.scope)
}
console.log('Connected Users : ' + Object.keys(clients))
}
function handleIncomingMessage (socket, message, flags) {
let scope = socket.scope
let userId = socket.userId
let channel = ['dispatch', 'in', userId, scope].join(':')
pub.publish(channel, message)
}
function handleClosedConnection (socket, code, reason) {
console.log('Connection with ' + socket.userId + ' closed. Code: ' + code)
if (socket.scope === 'gameplay') {
try {
User.findByIdAndUpdate(socket.userId, { $set: { isOnLine: 1 } }).select('id').lean()
let key = [socket.userId, socket.scope].join(':')
delete clients[key]
} catch (e) {
console.log(e)
}
} else {
console.log('Scope : ' + socket.scope)
}
}
function heartbeat (socket) {
socket.isAlive = true
}
sub.on('pmessage', (pattern, channel, message) => {
let channelComponents = channel.split(':')
let dir = channelComponents[1]
let userId = channelComponents[2]
let scope = channelComponents[3]
if (dir === 'in') {
try {
let handlers = namespaces[scope] || []
if (handlers.length) {
handlers.forEach(h => {
h(userId, message)
})
}
} catch (e) {
console.log(e)
}
} else if (dir === 'out') {
try {
let key = [userId, scope].join(':')
if (clients[key]) { clients[key].send(message) }
} catch (e) {
console.log(e)
}
}
// otherwise ignore
})
websocketServer.on('connection', handleConnection)
}
exports.on = function (scope, callback) {
if (!namespaces[scope]) { namespaces[scope] = [callback] } else { namespaces[scope].push(callback) }
}
exports.send = function (userId, scope, data) {
let channel = ['dispatch', 'out', userId, scope].join(':')
if (typeof (data) === 'object') { data = JSON.stringify(data) } else if (typeof (data) !== 'string') { throw new Error('DispatcherError: Cannot send this type of message ' + typeof (data)) }
pub.publish(channel, data)
}
exports.clients = clients
This is working on localhost.
Please let me know if I need to provide more info or code.Any help on this would be greatly appreciated, thanks in advance!
You have alot of extraneous info in the code you posted, so it's difficult to understand exactly what you mean.
However, if I understand correctly, you currently have multiple worker dyno instances subscribing to the same channels in some kind of pub/sub network. If you don't want all dynos to subscribe to the same channels, you need to put some logic in to make sure that your channels get distributed across dynos.
One simple way to do that might be to use something like the logic described in this answer.
In your case you might be able to use socket.userId as the key to distribute your channels across dynos.

Failed to set remote offer sdp: Session error code: ERROR_CONTENT

I have a problem connecting peer-to-peer video from native android browser to safari 11 desktop (vice versa), here is the error:
Unhandled Promise Rejection: OperationError (DOM Exception 34): Failed to set remote offer sdp: Session error code: ERROR_CONTENT. Session error description: Failed to set remote video description send parameters..
I'm currently stuck in this issue
and here is my whole client videochat code, thanks.
import app from '../../../config';
const videoChatService = app.service('participants/video-chat');
let localVid;
let remoteVid;
let localstream;
let rtcPeerConn;
let conversationId;
let userId;
let isSdpSent = false;
let hasAddTrack;
const configuration = {
iceServers: [{
urls: 'stun:stun.l.google.com:19302',
},
{
urls: 'stun:stun.services.mozilla.com',
username: 'louis#mozilla.com',
credential: 'webrtcdemo',
},
// {
// urls: 'turn:mdn-samples.mozilla.org',
// username: 'webrtc',
// credential: 'turnserver' }
] };
function closeVideoCall() {
if (rtcPeerConn) {
rtcPeerConn.onaddstream = null;
rtcPeerConn.ontrack = null;
rtcPeerConn.onremovestream = null;
rtcPeerConn.onicecandidate = null;
if (remoteVid.srcObject) {
remoteVid.srcObject.getTracks().forEach(track => track.stop());
remoteVid.srcObject = null;
}
if (localVid.srcObject) {
localVid.srcObject.getTracks().forEach(track => track.stop());
localVid.srcObject = null;
}
rtcPeerConn.close();
rtcPeerConn = null;
}
}
// set local sdp
function setLocalSDP(desc) {
console.log('>>> setLocalSDP', rtcPeerConn);
return rtcPeerConn.setLocalDescription(desc);
}
function logError(error) {
console.log(`>>>>logError, ${error.name}: ${error.message}`);
}
function handleNegotiatedNeededEvent() {
console.log('>>>>> on negotiation called');
console.log('query >>>', conversationId, userId);
if (!isSdpSent) {
rtcPeerConn.createOffer()
.then(setLocalSDP)
.then(() => {
isSdpSent = true;
videoChatService.patch(null, {
data: {
type: 'video-offer',
message: JSON.stringify(rtcPeerConn.localDescription),
},
}, {
query: {
conversation_id: conversationId,
user_id: userId,
},
}).then().catch(e => { console.log('patch error', e); });
})
.catch(logError);
}
}
function handleRemoveStreamEvent() {
closeVideoCall();
}
function handleTrackEvent (evt) {
console.log('>>>>> going to add their stream...', evt);
remoteVid = document.getElementById('remoteStream');
if (!remoteVid.srcObject) {
remoteVid.srcObject = evt.streams[0];
}
}
function handleAddStreamEvent(evt) {
console.log('>>>>> stream added');
remoteVid = document.getElementById('remoteStream');
remoteVid.srcObject = event.stream;
}
function handleICECandidateEvent(evt) {
console.log('>>>> onicecandidate', evt);
console.log('query >>>', conversationId, userId);
if (evt.candidate) {
videoChatService.patch(null, {
data: {
type: 'new-ice-candidate',
message: JSON.stringify(evt.candidate),
},
}, {
query: {
conversation_id: conversationId,
user_id: userId,
},
});
}
}
function handleICEConnectionStateChangeEvent() {
console.log(`>>>>> ICE connection state changed to ${rtcPeerConn.iceConnectionState}`);
switch (rtcPeerConn.iceConnectionState) {
case 'closed':
case 'failed':
case 'disconnected':
console.log('>>>> disconnected');
closeVideoCall();
break;
}
}
function handleSignalingStateChangeEvent() {
console.log(`>>>>> WebRTC signaling state changed to: ${rtcPeerConn.signalingState}`);
switch (rtcPeerConn.signalingState) {
case 'closed':
console.log('>>>> closed');
closeVideoCall();
break;
}
}
function createPeerConnection() {
rtcPeerConn = new RTCPeerConnection(configuration);
console.log('>>>>> create peer connection', rtcPeerConn);
hasAddTrack = (rtcPeerConn.addTrack !== undefined);
rtcPeerConn.onicecandidate = handleICECandidateEvent;
rtcPeerConn.onnegotiationneeded = handleNegotiatedNeededEvent;
rtcPeerConn.oniceconnectionstatechange = handleICEConnectionStateChangeEvent;
rtcPeerConn.onsignalingstatechange = handleSignalingStateChangeEvent;
rtcPeerConn.onremovestream = handleRemoveStreamEvent;
if (hasAddTrack) {
rtcPeerConn.ontrack = handleTrackEvent;
} else {
rtcPeerConn.onaddstream = handleAddStreamEvent;
}
}
function handleGetUSerMediaError(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;
}
}
// add video to local and add to track
function gotStream(stream) {
console.log('>>>> gotStream', stream);
localVid.srcObject = stream;
localstream = stream;
if (hasAddTrack) {
stream.getTracks().forEach(track => rtcPeerConn.addTrack(track, localstream));
} else {
rtcPeerConn.addStream(localstream);
}
}
// start signaling
export function startSignaling(conversation_id, user_id) {
localVid = document.getElementById('localStream');
remoteVid = document.getElementById('remoteStream');
console.log('>>>>> startSignaling');
conversationId = conversation_id;
userId = user_id;
return () => {
if (!rtcPeerConn) {
createPeerConnection();
navigator.mediaDevices.getUserMedia({
audio: true,
video: {
facingMode: 'user',
},
})
.then(gotStream)
.catch(handleGetUSerMediaError);
}
};
}
export function handleVideoOfferMsg(conversation_id, user_id, message) {
console.log('>>>>> handleVideoOfferMsg');
localstream = null;
conversationId = conversation_id;
userId = user_id;
localVid = document.getElementById('localStream');
remoteVid = document.getElementById('remoteStream');
return () => {
createPeerConnection();
console.log('query >>>', conversationId, userId);
const sdp = new RTCSessionDescription(message);
// sdp.sdp = sdp.replace('a=setup:active', 'a=setup:passive');
rtcPeerConn.setRemoteDescription(sdp)
.then(() => (
navigator.mediaDevices.getUserMedia({
audio: true,
video: {
facingMode: 'user',
},
})
))
.then(gotStream)
.then(() => (
rtcPeerConn.createAnswer()
))
.then(setLocalSDP)
.then(() => {
videoChatService.patch(null, {
data: {
type: 'video-answer',
message: JSON.stringify(rtcPeerConn.localDescription),
},
}, {
query: {
conversation_id: conversationId,
user_id: userId,
},
}).then().catch(e => { console.log('patch error', e); });
});
};
}
export function handleVideoAnswerMsg(message) {
console.log('>>>>> handle video answer message', message);
return () => {
const sdp = new RTCSessionDescription(message);
rtcPeerConn.setRemoteDescription(sdp)
.catch(logError);
};
}
// Adding ice candidate
export function addIceCandidate(message) {
console.log('>>>> addIceCandidate', message);
return () => {
const candidate = new RTCIceCandidate(message);
rtcPeerConn.addIceCandidate(candidate)
.then(() => {
console.log('>>> candidate added ');
})
.catch(e => {
console.log('Error candidate', e);
});
};
}
There are two different issues making a WebRTC connection between Chrome on Android and iOS/Safari not working:
1) No H.264 implementation on device
Chrome for Android has only a hardware implementation for H.264 and there is no software implementation. At this moment H.264 works only with devices with a processor of Qualcomm (Kitkat and later) or Samsung Exynos (Lollipop and later). Since Apple only supports H.264 other Android devices can't connect with iOS and Safari.
2) There is bug in Chrome for Android:
Chrome Android does not offer/answer H.264 Constrained Baseline Profile
Because Apple only supports H.264, Android/Chrome can not connect with iOS at this moment.
This problem will be solved in Chrome Android 65 (now Canary). See this for more information.
I see your error message that is exactly this bug so I am pretty sure this is the problem. But in the end it doesn't matter. But you should be aware of both problems.
This might be related to this issue.
Chrome on Android does not always support H264, Safari does only support H264.
To verify it is a video codec issue as mentioned by the other answers you can adjust the stream constraints:
navigator.mediaDevices.getUserMedia({
video: false,
audio: true
})
If the ICE handling succeeds after this change it is likely the SDP contained a codec that is not supported on the other side.
Then you should i.e. extend your code to fallback to audio only after displaying an error message in your GUI.

Solve Socket.io connection in a external module with expressjs

I have the following case: When I try to connect the module called 'Signaling-Server.js'
in the view of html the console says: GET ERROR [HTTP/1.1 400 Bad Request 1ms]
But this only happens when I add these Module. When I try to connect without him the socket.io connections works perfectly.
app.js
//Modules
var express = require("express"),
http = require("http"),
morgan= require("morgan"),
bodyParser = require('body-parser'),
methodOverride = require('method-override'),
app = express(),
server = http.createServer(app),
io = require("socket.io").listen(server);
app.use(express.static(__dirname + '/public'));
app.use(morgan('dev'));
app.use(bodyParser.urlencoded({ extended:false}));
app.use(bodyParser.json());
app.use(methodOverride());
app.set("views",__dirname + "/views");
//I need to 'onnect with this module
//require('./Signaling-Server.js')(myServerOrPort);
require('./Signaling-Server.js')(server);
//Server is ready
server.listen(3000);
The code of Signaling-Server is here (Cortesy of MuazKhan Proyect "RTCMultiConnection")
Signaling-Server.js
// Muaz Khan - www.MuazKhan.com
// MIT License - www.WebRTC-Experiment.com/licence
// Documentation - github.com/muaz-khan/RTCMultiConnection
module.exports = exports = function(app, socketCallback) {
var io = require('socket.io').listen(app, {
log: false,
origins: '*:*'
});
io.set('transports', [
'websocket', // 'disconnect' EVENT will work only with 'websocket'
'xhr-polling',
'jsonp-polling'
]);
var listOfUsers = {};
var shiftedModerationControls = {};
var ScalableBroadcast;
io.sockets.on('connection', function(socket) {
var params = socket.handshake.query;
var socketMessageEvent = params.msgEvent || 'RTCMultiConnection-Message';
if (params.enableScalableBroadcast) {
if (!ScalableBroadcast) {
ScalableBroadcast = require('./Scalable-Broadcast.js');
}
var singleBroadcastAttendees = params.singleBroadcastAttendees;
ScalableBroadcast(socket, singleBroadcastAttendees);
}
socket.userid = params.userid;
listOfUsers[socket.userid] = {
socket: socket,
connectedWith: {},
isPublic: false, // means: isPublicModerator
extra: {}
};
socket.on('extra-data-updated', function(extra) {
try {
if (!listOfUsers[socket.userid]) return;
listOfUsers[socket.userid].extra = extra;
for (var user in listOfUsers[socket.userid].connectedWith) {
listOfUsers[user].socket.emit('extra-data-updated', socket.userid, extra);
}
} catch (e) {}
});
socket.on('become-a-public-moderator', function() {
try {
if (!listOfUsers[socket.userid]) return;
listOfUsers[socket.userid].isPublic = true;
} catch (e) {}
});
socket.on('get-public-moderators', function(userIdStartsWith, callback) {
try {
userIdStartsWith = userIdStartsWith || '';
var allPublicModerators = [];
for (var moderatorId in listOfUsers) {
if (listOfUsers[moderatorId].isPublic && moderatorId.indexOf(userIdStartsWith) === 0 && moderatorId !== socket.userid) {
var moderator = listOfUsers[moderatorId];
allPublicModerators.push({
userid: moderatorId,
extra: moderator.extra
});
}
}
callback(allPublicModerators);
} catch (e) {}
});
socket.on('changed-uuid', function(newUserId) {
try {
if (listOfUsers[socket.userid] && listOfUsers[socket.userid].socket.id == socket.userid) {
if (newUserId === socket.userid) return;
var oldUserId = socket.userid;
listOfUsers[newUserId] = listOfUsers[oldUserId];
listOfUsers[newUserId].socket.userid = socket.userid = newUserId;
delete listOfUsers[oldUserId];
return;
}
socket.userid = newUserId;
listOfUsers[socket.userid] = {
socket: socket,
connectedWith: {},
isPublic: false,
extra: {}
};
} catch (e) {}
});
socket.on('set-password', function(password) {
try {
if (listOfUsers[socket.userid]) {
listOfUsers[socket.userid].password = password;
}
} catch (e) {}
});
socket.on('disconnect-with', function(remoteUserId, callback) {
try {
if (listOfUsers[socket.userid] && listOfUsers[socket.userid].connectedWith[remoteUserId]) {
delete listOfUsers[socket.userid].connectedWith[remoteUserId];
socket.emit('user-disconnected', remoteUserId);
}
if (!listOfUsers[remoteUserId]) return callback();
if (listOfUsers[remoteUserId].connectedWith[socket.userid]) {
delete listOfUsers[remoteUserId].connectedWith[socket.userid];
listOfUsers[remoteUserId].socket.emit('user-disconnected', socket.userid);
}
callback();
} catch (e) {}
});
function onMessageCallback(message) {
try {
if (!listOfUsers[message.sender]) {
socket.emit('user-not-found', message.sender);
return;
}
if (!listOfUsers[message.sender].connectedWith[message.remoteUserId] && !!listOfUsers[message.remoteUserId]) {
listOfUsers[message.sender].connectedWith[message.remoteUserId] = listOfUsers[message.remoteUserId].socket;
listOfUsers[message.sender].socket.emit('user-connected', message.remoteUserId);
if (!listOfUsers[message.remoteUserId]) {
listOfUsers[message.remoteUserId] = {
socket: null,
connectedWith: {},
isPublic: false,
extra: {}
};
}
listOfUsers[message.remoteUserId].connectedWith[message.sender] = socket;
if (listOfUsers[message.remoteUserId].socket) {
listOfUsers[message.remoteUserId].socket.emit('user-connected', message.sender);
}
}
if (listOfUsers[message.sender].connectedWith[message.remoteUserId] && listOfUsers[socket.userid]) {
message.extra = listOfUsers[socket.userid].extra;
listOfUsers[message.sender].connectedWith[message.remoteUserId].emit(socketMessageEvent, message);
}
} catch (e) {}
}
var numberOfPasswordTries = 0;
socket.on(socketMessageEvent, function(message, callback) {
if (message.remoteUserId && message.remoteUserId === socket.userid) {
// remoteUserId MUST be unique
return;
}
try {
if (message.remoteUserId && message.remoteUserId != 'system' && message.message.newParticipationRequest) {
if (listOfUsers[message.remoteUserId] && listOfUsers[message.remoteUserId].password) {
if (numberOfPasswordTries > 3) {
socket.emit('password-max-tries-over', message.remoteUserId);
return;
}
if (!message.password) {
numberOfPasswordTries++;
socket.emit('join-with-password', message.remoteUserId);
return;
}
if (message.password != listOfUsers[message.remoteUserId].password) {
numberOfPasswordTries++;
socket.emit('invalid-password', message.remoteUserId, message.password);
return;
}
}
}
if (message.message.shiftedModerationControl) {
if (!message.message.firedOnLeave) {
onMessageCallback(message);
return;
}
shiftedModerationControls[message.sender] = message;
return;
}
if (message.remoteUserId == 'system') {
if (message.message.detectPresence) {
if (message.message.userid === socket.userid) {
callback(false, socket.userid);
return;
}
callback(!!listOfUsers[message.message.userid], message.message.userid);
return;
}
}
if (!listOfUsers[message.sender]) {
listOfUsers[message.sender] = {
socket: socket,
connectedWith: {},
isPublic: false,
extra: {}
};
}
// if someone tries to join a person who is absent
if (message.message.newParticipationRequest) {
var waitFor = 120; // 2 minutes
var invokedTimes = 0;
(function repeater() {
invokedTimes++;
if (invokedTimes > waitFor) {
socket.emit('user-not-found', message.remoteUserId);
return;
}
if (listOfUsers[message.remoteUserId] && listOfUsers[message.remoteUserId].socket) {
onMessageCallback(message);
return;
}
setTimeout(repeater, 1000);
})();
return;
}
onMessageCallback(message);
} catch (e) {}
});
socket.on('disconnect', function() {
try {
var message = shiftedModerationControls[socket.userid];
if (message) {
delete shiftedModerationControls[message.userid];
onMessageCallback(message);
}
} catch (e) {}
try {
// inform all connected users
if (listOfUsers[socket.userid]) {
for (var s in listOfUsers[socket.userid].connectedWith) {
listOfUsers[socket.userid].connectedWith[s].emit('user-disconnected', socket.userid);
if (listOfUsers[s] && listOfUsers[s].connectedWith[socket.userid]) {
delete listOfUsers[s].connectedWith[socket.userid];
listOfUsers[s].socket.emit('user-disconnected', socket.userid);
}
}
}
} catch (e) {}
delete listOfUsers[socket.userid];
});
if (socketCallback) {
socketCallback(socket);
}
});
};
Anybody knows whats the fix?
You can either try Ahmed's solution i.e. passing server object here:
require('./Signaling-Server.js') (server);
In your codes, the server object is using http.
I'll suggest trying this instead:
var fs = require('fs');
var options = {
key: fs.readFileSync('fake-keys/privatekey.pem'),
cert: fs.readFileSync('fake-keys/certificate.pem')
};
var express = require("express"),
http = require("https"), // Use HTTPs here -------------
app = express(),
server = http.createServer(options, app);
require('./Signaling-Server.js')(server);
You can either try valid SSL certificate keys or fake-keys.
Here is how to use valid certificates:
var options = {
key: fs.readFileSync('../ssl/private/domain.com.key'),
cert: fs.readFileSync('../ssl/certs/domain.com.crt'),
ca: fs.readFileSync('../ssl/certs/domain.com.cabundle')
};
In Express 3, you should pass the app object to socket.io not the server like in Express 2. Assuming you are using Express 3 and not 2. You just need to change one line to set up socket.io correctly
Try replacing this
require('./Signaling-Server.js')(server);
with this
require('./Signaling-Server.js')(app);

WebSocket connection to 'ws://localhost:3434/' failed: Connection closed before receiving a handshake response

When using the cmd to run the node server.js and launch the localhost:3434 in the web browser I am getting the error:
in Chrome:
Connection closed before receiving a handshake response.
in firefox:
Firefox can't establish a connection to the server at ws://localhost:3434/
Could you pls help
server.js
var http = require('http'),
fs = require('fs');
fs.readFile('../index.html', function (err, html) {
if (err) {
throw err;
}
http.createServer(function (request, response) {
response.writeHeader(200, { "Content-Type": "text/html" });
response.write(html);
response.end();
}).listen(3434);
});
index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<!--<script type="text/javascript" src="../../js/adapter.js"></script>-->
<!--<script type="text/javascript" src="../../js/webrtc.js"></script>-->
<title>Simple WebRTC video chat</title>
</head>
<body>
<header>
<video id="localVideo" autoplay="autoplay" muted="muted" style="width:40%;"></video>
<video id="remoteVideo" autoplay="autoplay" style="width:40%;"></video>
<input type="button" id="start" onclick="start(true)" value="Start Video"; />
</header>
<script>
var RTCPeerConnection = null;
var getUserMedia = null;
var attachMediaStream = null;
var reattachMediaStream = null;
var webrtcDetectedBrowser = null;
var webrtcDetectedVersion = null;
function trace(text) {
// This function is used for logging.
if (text[text.length - 1] == '\n') {
text = text.substring(0, text.length - 1);
}
console.log((performance.now() / 1000).toFixed(3) + ": " + text);
}
if (navigator.mozGetUserMedia) {
console.log("This appears to be Firefox");
webrtcDetectedBrowser = "firefox";
webrtcDetectedVersion =
parseInt(navigator.userAgent.match(/Firefox\/([0- 9]+)\./)[1]);
// The RTCPeerConnection object.
RTCPeerConnection = mozRTCPeerConnection;
// The RTCSessionDescription object.
RTCSessionDescription = mozRTCSessionDescription;
// The RTCIceCandidate object.
RTCIceCandidate = mozRTCIceCandidate;
// Get UserMedia (only difference is the prefix).
getUserMedia = navigator.mozGetUserMedia.bind(navigator);
// Creates iceServer from the url for FF.
createIceServer = function (url, username, password) {
var iceServer = null;
var url_parts = url.split(':');
if (url_parts[0].indexOf('stun') === 0) {
// Create iceServer with stun url.
iceServer = { 'url': url };
} else if (url_parts[0].indexOf('turn') === 0 &&
(url.indexOf('transport=udp') !== -1 ||
url.indexOf('?transport') === -1)) {
// Create iceServer with turn url.
// Ignore the transport parameter from TURN url.
var turn_url_parts = url.split("?");
iceServer = {
'url': turn_url_parts[0],
'credential': password,
'username': username
};
}
return iceServer;
};
// Attach a media stream to an element.
attachMediaStream = function (element, stream) {
console.log("Attaching media stream");
element.mozSrcObject = stream;
element.play();
};
reattachMediaStream = function (to, from) {
console.log("Reattaching media stream");
to.mozSrcObject = from.mozSrcObject;
to.play();
};
MediaStream.prototype.getVideoTracks = function () {
return [];
};
MediaStream.prototype.getAudioTracks = function () {
return [];
};
} else if (navigator.webkitGetUserMedia) {
console.log("This appears to be Chrome");
webrtcDetectedBrowser = "chrome";
webrtcDetectedVersion =
parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]);
// Creates iceServer from the url for Chrome.
createIceServer = function (url, username, password) {
var iceServer = null;
var url_parts = url.split(':');
if (url_parts[0].indexOf('stun') === 0) {
// Create iceServer with stun url.
iceServer = { 'url': url };
} else if (url_parts[0].indexOf('turn') === 0) {
if (webrtcDetectedVersion < 28) {
var url_turn_parts = url.split("turn:");
iceServer = {
'url': 'turn:' + username + '#' + url_turn_parts[1],
'credential': password
};
} else {
iceServer = {
'url': url,
'credential': password,
'username': username
};
}
}
return iceServer;
};
// The RTCPeerConnection object.
RTCPeerConnection = webkitRTCPeerConnection;
// Get UserMedia (only difference is the prefix).
// Code from Adam Barth.
getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
// Attach a media stream to an element.
attachMediaStream = function (element, stream) {
if (typeof element.srcObject !== 'undefined') {
element.srcObject = stream;
} else if (typeof element.mozSrcObject !== 'undefined') {
element.mozSrcObject = stream;
} else if (typeof element.src !== 'undefined') {
element.src = URL.createObjectURL(stream);
} else {
console.log('Error attaching stream to element.');
}
};
reattachMediaStream = function (to, from) {
to.src = from.src;
};
// The representation of tracks in a stream is changed in M26.
// Unify them for earlier Chrome versions in the coexisting period.
if (!webkitMediaStream.prototype.getVideoTracks) {
webkitMediaStream.prototype.getVideoTracks = function () {
return this.videoTracks;
};
webkitMediaStream.prototype.getAudioTracks = function () {
return this.audioTracks;
};
}
if (!webkitRTCPeerConnection.prototype.getLocalStreams) {
webkitRTCPeerConnection.prototype.getLocalStreams = function () {
return this.localStreams;
};
webkitRTCPeerConnection.prototype.getRemoteStreams = function () {
return this.remoteStreams;
};
}
} else {
console.log("Browser does not appear to be WebRTC-capable");
}
var localVideo;
var remoteVideo;
var peerConnection;
var localStream;
var optional = { optional: [{ DtlsSrtpKeyAgreement: true }] }
var peerConnectionConfig = { 'iceServers': [{ 'url': 'stun:stun.services.mozilla.com' }, { 'url': 'stun:stun.l.google.com:19302' }] };
navigator.getUserMedia = navigator.getUserMedia || navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
localVideo = document.getElementById("localVideo");
remoteVideo = document.getElementById("remoteVideo");
serverConnection = new WebSocket('ws://localhost:3434');
serverConnection.onmessage = gotMessageFromServer;
var constraints = {
video: true,
audio: true,
};
if (navigator.getUserMedia) {
navigator.getUserMedia(constraints, getUserMediaSuccess, getUserMediaError);
} else {
alert('Your browser does not support getUserMedia API');
}
function getUserMediaSuccess(stream) {
localStream = stream;
localVideo.src = window.URL.createObjectURL(stream);
}
function start(isCaller) {
peerConnection = new RTCPeerConnection(peerConnectionConfig, optional);
peerConnection.onicecandidate = gotIceCandidate;
peerConnection.onaddstream = gotRemoteStream;
peerConnection.addStream(localStream);
if (isCaller) {
peerConnection.createOffer(gotDescription, createOfferError);
}
}
function gotMessageFromServer(message) {
if (!peerConnection) start(false);
var signal = JSON.parse(message.data);
if (signal.sdp) {
peerConnection.setRemoteDescription(new RTCSessionDescription(signal.sdp), function () {
peerConnection.createAnswer(gotDescription, createAnswerError);
});
} else if (signal.ice) {
peerConnection.addIceCandidate(new RTCIceCandidate(signal.ice));
}
}
function gotIceCandidate(event) {
if (event.candidate != null) {
serverConnection.send(JSON.stringify({ 'ice': event.candidate }));
}
}
function gotDescription(description) {
console.log('got description');
peerConnection.setLocalDescription(description, function () {
serverConnection.send(JSON.stringify({ 'sdp': description }));
}, function () { console.log('set description error') });
}
function gotRemoteStream(event) {
console.log("got remote stream");
remoteVideo.src = window.URL.createObjectURL(event.stream);
}
// Error functions....
function getUserMediaError(error) {
console.log(error);
}
function createOfferError(error) {
console.log(error);
}
function createAnswerError(error) {
console.log(error);
}
</script>
</body>
</html>
In order to handle websocket connections your http-server should handle 'upgrade'-event (see here - https://nodejs.org/api/http.html#http_event_upgrade_1)
For example, here is the working example (it uses ws-module):
var WebSocketServer = require('ws').Server;
var wss = new WebSocketServer({noServer: true});
var http = require('http'),
fs = require('fs');
fs.readFile('../index.html', function (err, html) {
if (err) {
throw err;
}
var server = http.createServer(function (request, response) {
response.writeHeader(200, { "Content-Type": "text/html" });
response.write(html);
response.end();
})
server.listen(3434);
server.on('upgrade', wss.handleUpgrade);
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('received: %s', message);
});
ws.send('something');
});
});
Here are docs for ws - https://github.com/websockets/ws

Resources