I am developing a cross-plattform application with peer.js and webrtc.
I am using cordova, crosswalk.
Additionaly I am using the webrtc adapter (https://github.com/webrtc/adapter)
My code is based on the webrtc-crosswalk sample. (https://github.com/crosswalk-project/crosswalk-samples)
I want to change the videosource of the stream without creating a new call.
My approche is to remove the tracks of the stream and add the new tracks of the other camera.
The result is that the local video shows the right content, but the callee's remote video freezes.
Probably I am doing a very basic mistake, but i can't find a solution.
I am looking forward to your answers and solutions.
My main codefile is attached.
//Notwendig, um die Dialogfunktion zu aktivieren
document.addEventListener("deviceready", onDeviceReady, false);
function onDeviceReady() {
console.log(navigator.notification);
// Now safe to use device APIs
}
document.addEventListener('DOMContentLoaded', function () {
// PeerJS server location
var SERVER_IP = '172.20.37.147';
var SERVER_PORT = 9000;
// DOM elements manipulated as user interacts with the app
var messageBox = document.querySelector('#messages');
var callerIdEntry = document.querySelector('#caller-id');
var connectBtn = document.querySelector('#connect');
var recipientIdEntry = document.querySelector('#recipient-id');
var dialBtn = document.querySelector('#dial');
var remoteVideo = document.querySelector('#remote-video');
var localVideo = document.querySelector('#local-video');
var cameraTurn = document.querySelector('#camera_turn');
var stop = document.querySelector('#stop');
// the default facing direction
var dir = "environment";
// the ID set for this client
var callerId = null;
// PeerJS object, instantiated when this client connects with its
// caller ID
var peer = null;
// the local video stream captured with getUserMedia()
var localStream = null;
// DOM utilities
var makePara = function (text) {
var p = document.createElement('p');
p.innerText = text;
return p;
};
var addMessage = function (para) {
if (messageBox.firstChild) {
messageBox.insertBefore(para, messageBox.firstChild);
}
else {
messageBox.appendChild(para);
}
};
var logError = function (text) {
var p = makePara('ERROR: ' + text);
p.style.color = 'red';
addMessage(p);
};
var logMessage = function (text) {
addMessage(makePara(text));
};
// get the local video and audio stream and show preview in the
// "LOCAL" video element
// successCb: has the signature successCb(stream); receives
// the local video stream as an argument
var getLocalStream = function (successCb, ask = true) {
if (localStream && successCb) {
successCb(localStream);
}
else {
navigator.mediaDevices.getUserMedia({ audio: true, video: { facingMode: dir } })
.then(function (stream) {
if (localStream == null) {
/* use the stream */
localStream = stream;
}
else {
stream.getTracks().forEach(function (track) {
localStream.addTrack(track);
});
}
localVideo.src = window.URL.createObjectURL(localStream);
if (successCb) {
successCb(stream);
}
})
.catch(function (err) {
/* handle the error */
logError('failed to access local camera');
logError(err.message);
});
}
};
// set the "REMOTE" video element source
var showRemoteStream = function (stream) {
remoteVideo.src = window.URL.createObjectURL(stream);
};
// set caller ID and connect to the PeerJS server
var connect = function () {
callerId = callerIdEntry.value;
if (!callerId) {
logError('please set caller ID first');
return;
}
try {
// create connection to the ID server
peer = new Peer(callerId, { host: SERVER_IP, port: SERVER_PORT });
// hack to get around the fact that if a server connection cannot
// be established, the peer and its socket property both still have
// open === true; instead, listen to the wrapped WebSocket
// and show an error if its readyState becomes CLOSED
peer.socket._socket.onclose = function () {
logError('no connection to server');
peer = null;
};
// get local stream ready for incoming calls once the wrapped
// WebSocket is open
peer.socket._socket.onopen = function () {
getLocalStream();
};
// handle events representing incoming calls
peer.on('call', answer);
}
catch (e) {
peer = null;
logError('error while connecting to server');
}
};
// make an outgoing call
var dial = function () {
if (!peer) {
logError('please connect first');
return;
}
if (!localStream) {
logError('could not start call as there is no local camera');
return
}
var recipientId = recipientIdEntry.value;
if (!recipientId) {
logError('could not start call as no recipient ID is set');
return;
}
getLocalStream(function (stream) {
logMessage('outgoing call initiated');
var call = peer.call(recipientId, stream);
call.on('stream', showRemoteStream);
call.on('error', function (e) {
logError('error with call');
logError(e.message);
});
});
};
// answer an incoming call
var answer = function (call) {
if (!peer) {
logError('cannot answer a call without a connection');
return;
}
if (!localStream) {
logError('could not answer call as there is no localStream ready');
return;
}
//Asks user to answer the call
navigator.notification.confirm(
"Receive a call?",
function (buttonIndex) {
if (buttonIndex === 1) {
//user clicked "yes"
logMessage('incoming call answered');
call.on('stream', showRemoteStream);
call.answer(localStream);
}
else {
//user clicked "no"
logMessage('incoming call denied');
}
}
,
'Incoming Call',
['Yes', 'No']
);
};
function turnDirection() {
if (dir === "user")
return "environment";
else
return "user";
}
var turnCamera = function (call) {
dir = turnDirection();
localStream.getTracks().forEach(function (track) {
track.stop();
localStream.removeTrack(track);
});
getLocalStream(false);
};
var stopCall = function (call) { };
// wire up button events
connectBtn.addEventListener('click', connect);
dialBtn.addEventListener('click', dial);
cameraTurn.addEventListener('click', turnCamera);
stop.addEventListener('click', stopCall);
});
If you remove and then add a new track to a PeerConnection you need to renegotiate the offer-answer to get it working. I will recommend you to use the replaceTrack API to avoid the re-negotiation problem while changing the camera input.
Related
After checking similar questions on stackoverflow I did not find anything much helpful for what I want to do in my project. Reading and researching I successfully made the application work having multiple connections to my Ratchet PHP websocket server, but I noticed every time the user reloaded a page or opened a link in a new tab, the client websocket got disconnected and then reconnected again.
So, I wonder how to get only one persistent connection to a WebSocket Server, for multiple users, in a web application using a Sharedworker.
What I have in the client side is this:
<script>
$(document).ready(function($) {
let socket = new WebSocket("ws://realtime:8090");
socket.onopen = function(e) {
console.log("Browser client connected to websocket server");
socket.send("Greetings from the browser!");
};
socket.onmessage = function(event) {
console.log('Data received from server: ' + event.data);
};
socket.onclose = function(event) {
if (event.wasClean) {
console.log(`Connection closed cleanly, code=${event.code} reason=${event.reason}`);
}
else {
// e.g. server process killed or network down
// event.code is usually 1006 in this case
console.log('Connection closed unexpectedly.');
}
};
socket.onerror = function(error) {
alert(error.message);
};
});
</script>
Ok after reading, researching and trying different things and code samples, I came to this solution:
The client side (browser) should have the connection to a Sharedworker.
The sharedworker is a separated javascript file containing the core of the sharedworker and whatever other JS code that needs to be executed within it.
I first tested the sharedworker to work fine with the browser tabs, counting the number of opened tabs per user and sharing messages to one user, and then to a group of users.
Once the communication between the browser and the Sharedworker passed those tests I added the websocket code to the body of the Sharedworker JS file.
In the end, the client side (browser) looks like this:
<script>
$(document).ready(function($) {
var currentUser = "{{ Auth::user()->name }}";
let worker = new SharedWorker('worker.js');
worker.port.start();
worker.port.postMessage({
action: 'connect',
username: currentUser
});
worker.port.onmessage = function(message) {
console.log(message.data);
};
});
</script>
The Sharedworker looks like this:
// All this code is executed only once, until the onconnect() function.
//---------------------------------------------------------------------
// The array AllPorts contains objects with the format {user:<string>, port:<MessagePort>}
let AllPorts = [];
var socket = new WebSocket("ws://ssa:8090");
// Called when the WebSocket Server accepts the connection.
socket.onopen = function(e) {
//
};
// Event handler fired when the WebSocket Server sends a message to this client.
socket.onmessage = function(e) {
var message = JSON.parse(e.data);
// This loop sends a message to each tab opened by the given user.
for (var i = 0; i < AllPorts.length; i++) {
if (AllPorts[i].user == message.to) {
AllPorts[i].port.postMessage(message.msg);
}
}
};
socket.onclose = function(event) {
if (event.wasClean) {
console.log('Connection closed normally');
}
else {
console.log('Connection closed unexpectedly.');
}
};
socket.onerror = function(error) {
console.log(error.message);
};
// This event handler is fired every time a new tab is opened on the web browser.
onconnect = function(ev) {
let port = ev.ports[0];
port.onmessage = function(e) {
console.log(e.data.action);
let currentUser = e.data.username;
let userIsConnected = false;
switch (e.data.action) {
case "connect":
for (var i = 0; i < AllPorts.length; i++) {
if (AllPorts[i].user == currentUser) {
userIsConnected = true;
}
}
// Add new connected tab to AllPorts array.
AllPorts.push({user: currentUser, port: port});
if (!userIsConnected) {
// New users are added to the list of the WebSocket Server.
setTimeout(() => {
socket.send(JSON.stringify({action: 'connect', username: currentUser}));
}, 600);
}
break;
case "close":
console.log(AllPorts);
var index;
// This is also executed when the user reloads the Tab.
for (var i = 0; i < AllPorts.length; i++) {
if (AllPorts[i].port == port) {
index = i;
currentUser = AllPorts[i].user;
}
}
AllPorts.splice(index, 1);
userIsConnected = false;
// Check for any connected tab.
for (var i = 0; i < AllPorts.length; i++) {
if (AllPorts[i].user == currentUser) {
userIsConnected = true;
}
}
if (!userIsConnected) {
// User doen't have more tabs opened. Remove user from WebSocket Server.
socket.send(JSON.stringify({action: 'disconnect', username: currentUser}));
}
break;
case "notify":
// Check if given user is connected.
for (var i = 0; i < AllPorts.length; i++) {
if (AllPorts[i].user == currentUser) {
userIsConnected = true;
}
}
if (userIsConnected) {
socket.send(JSON.stringify({action: 'notify', to: currentUser, message: e.data.message}));
}
} // switch
} // port.onmessage
} // onconnect
I am getting the error from this page. The error is client.js:166 Uncaught TypeError: Cannot read property 'addIceCandidate' of undefined. Below is the Code. How to remove that error? The video from the other browser is sending stream to the server, while adding the stream in both browser, there becomes the error. Where is the error occurs after it got stream.
var divSelectRoom = document.getElementById("selectRoom");
var divConsultingRoom = document.getElementById("consultingRoom");
var inputRoomNumber = document.getElementById("roomNumber");
var btnGoRoom = document.getElementById("goRoom");
var localVideo = document.getElementById("localVideo");
var remoteVideo = document.getElementById("remoteVideo");
// these are the global variables
var roomNumber;
var localStream;
var remotestream;
var rtcPeerConnection;
//these are the STUN servers
var iceServers = {
'iceServers': [
{
url:'stun:stun.l.google.com:19302'
},
{
url:'stun:stun.services.mozilla.com'
},
{
url: 'turn:numb.viagenie.ca',
credential: 'muazkh',
username: 'webrtc#live.com'
}
]
};
var streamConstraints = { audio: true, video: true };
var isCaller;
// Here we connect to the socket iO server. We Will create it later.
var socket = io();
// Here we Odd a click event to the button
btnGoRoom.onclick = function() {
if (inputRoomNumber.value == ""){
alert("Please type a room number");
}
else {
roomNumber = inputRoomNumber.value; //we take the value from the element
socket.emit('create or join', roomNumber); //we send a message to server
divSelectRoom.style = "display: none;"; //hide selectRoom div
divConsultingRoom.style = "display block;"; //show consultingRoom div
}
};
// when server emits created
socket.on("created", function(room){
console.log('created function');
//caller gets user media devices with defined constraints
navigator.mediaDevices.getUserMedia(streamConstraints).then(function(stream){
console.log('Created function');
const mediaStream = new MediaStream();
const video = document.getElementById('localVideo');
video.srcObject = stream;
localStream = stream; //sets local stream to variable
//localVideo.src = URL.createObjectURL(stream); //shows stream to user
isCaller = true;//sets current user as caller
}).catch(function(err){
console.log('An error occured when accessing media devices');
console.log(err.name + ": " + err.message);
});
});
// when server emits ends
socket.on("joined", function(room){
console.log('Joined function');
//caller gets user media devices with defined constraints
navigator.mediaDevices.getUserMedia(streamConstraints).then(function(stream){
localStream = stream; //sets local stream to variable
const mediaStream = new MediaStream();
const video = document.getElementById('localVideo');
video.srcObject = stream;
//localVideo.src = URL.createObjectURL(stream); //shows stream to user
socket.emit('ready',roomNumber); //sends message to the server
console.log('Joined function');
}).catch(function(err){
console.log('An error occured when accessing media devices');
console.log(err.name + ": " + err.message);
});
});
//when server emits ready
socket.on('ready', function(){
console.log('client ready function');
if(isCaller){
//creates an RTCPeerConnection object
rtcPeerConnection = new RTCPeerConnection(iceServers);
//adds event listeners to the newly created object
rtcPeerConnection.onicecandidate = onIceCandidate;
rtcPeerConnection.ontrack = onAddStream;
//add the current local stream to the object
rtcPeerConnection.addStream(localStream);
//prepares an offer
rtcPeerConnection.createOffer(setLocalAndOffer, function(e){
console.log(e);
});
}
});
//when server emits offer
socket.on('offer',function(event){
if(isCaller){
console.log('client offer function');
//creates an RTCPeerConnection object
rtcPeerConnection = new RTCPeerConnection(iceServers);
//adds event listeners to the newly created object
rtcPeerConnection.onicecandidate = onIceCandidate;
rtcPeerConnection.ontrack = onAddStream;
//adds the current local stream to the object
rtcPeerConnection.addStream(localStream);
//stores the offer as remote description
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(event));
//Prepares an Answer
rtcPeerConnection.createAnswer(setLocalAndAnswer, function(e){
console.log(e);
});
}
});
//when server emits answer
socket.on('answer', function(event){
console.log('client answer function');
//stores it as remote description
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(event));
});
//when server emits candidate
socket.on('candidate', function(event){
console.log('client candidate function');
var pc1 = {
addIceCandidate : function(val) {
console.log(val);
}
}
//creates a candidate object
var candidate1 = new RTCIceCandidate({
type: 'offer',
sdpMLineIndex: event.label,
candidate: event.candidate
});
addIceCandidate(candidate1);
// if(rtcPeerConnection)
// console.log('Okay Peer');
// //stores candidate
// rtcPeerConnection.addIceCandidate(candidate);
});
function addIceCandidate(message) {
if (message.candidate != null) {
rtcPeerConnection.addIceCandidate(message);
}
}
//when a user receives the other user's video and audio stream
function onAddStream(event){
console.log('On Add Stream function');
const mediaStream = new MediaStream();
const rvideo = document.getElementById('remoteVideo');
rvideo.srcObject = event.stream;
//remoteVideo.src = URL.createObjectURL(event.stream);
remoteStream = event.stream;
}
//These are the functions referenced before as listeners for the peer connection
//sends a candidate message to server
function onIceCandidate(event){
console.log('On Ice candidate function');
if(event.candidate){
console.log('sending ice candidate');
socket.emit('candidate', {
type: 'candidate',
label: event.candidate.sdpMLineIndex,
id: event.candidate.sdpMid,
candidate: event.candidate.candidate,
room: roomNumber
});
}
}
//stores offer and sends message to server
function setLocalAndOffer(sessionDescription){
console.log('LocalAndOffer function');
rtcPeerConnection.setLocalDescription(sessionDescription);
socket.emit('offer', {
type: 'offer',
sdp: sessionDescription,
room: roomNumber
});
}
//stores answer and sends message to server
function setLocalAndAnswer(sessionDescription){
console.log('LocalAndAnswer function');
rtcPeerConnection.setLocalDescription(sessionDescription);
socket.emit('answer', {
type: 'answer',
sdp: sessionDescription,
room: roomNumber
});
}
You are probably sending the candidate socket message before the rtcPeerConnection is initialized. Then you get the error in the addIceCandidate function.
You need to check if you have a webrtc peer connection object before you call addIceCandidate when candidate web socket message arrives.
Once you init it and add event andlers it can any moment find ice candidates and hence trigger the related event, onicecandidate. Same for the session descriptions and its related event onnegotiationneeded event.
So be ready on other end you send those messages over websocket to upon they trigger on one end.
When using
const audioIncoming = document.createElement('audio');
For calls over the browser.
What is the proper way of muting the microphone but not the incoming audio sound from the network? Or is this even possible?
Here's a complete code example:
const audioProgress = document.createElement('audio');
const audioIncoming = document.createElement('audio');
/*** Define listener for managing calls ***/
var callListeners = {
onCallProgressing: function(call) {
audioProgress.src = 'style/ringback.wav';
audioProgress.loop = true;
audioProgress.play();
$('div#callLog').append('<div id="stats">Ringing...</div>');
},
onCallEstablished: function(call) {
audioIncoming.srcObject = call.incomingStream;
audioIncoming.play();
audioProgress.pause();
//Report call stats
var callDetails = call.getDetails();
$('div#callLog').append('<div id="stats">Answered at: '+(callDetails.establishedTime && new Date(callDetails.establishedTime))+'</div>');
},
onCallEnded: function(call) {
audioProgress.pause();
audioIncoming.srcObject = null;
$('button').removeClass('incall');
$('input#phoneNumber').removeAttr('disabled');
//Report call stats
var callDetails = call.getDetails();
$('div#callLog').append('<div id="stats">Ended: '+new Date(callDetails.endedTime)+'</div>');
$('div#callLog').append('<div id="stats">Duration (s): '+callDetails.duration+'</div>');
$('div#callLog').append('<div id="stats">End cause: '+call.getEndCause()+'</div>');
if(call.error) {
$('div#callLog').append('<div id="stats">Failure message: '+call.error.message+'</div>');
}
}
}
I try to implement Speech recognititon using Watson Speech To Text service.
I wrote some code in javascript using "MediaStreamRecorder" library. I send data through Websocket and get this problem: if I use "content-type": "audio/wav", Watson recognizes only first blob and set inactivity_timeout to defaul value meanwhile I set it to 2 seconds.
I use this code for opening websocket:
initWebSocket(startRecordingCallback) {
var that = this;
that.websocket = new WebSocket(that.wsURI);
that.websocket.onopen = function (evt) {
console.log("WebSocket: connection OK ");
var message = {
"action": "start",
"content-type": "audio/wav",
"interim_results": true,
"continuous": true,
"inactivity_timeout": 2
};
that.websocket.send(JSON.stringify(message));
};
that.websocket.onclose = function (evt) {
if (event.wasClean) {
console.log("WebSocket: connection closed clearly " + JSON.stringify(evt));
} else {
console.log("WebSocket: disconnect " + JSON.stringify(evt));
}
};
that.websocket.onmessage = function (evt) {
console.log(evt)
};
that.websocket.onerror = function (evt) {
console.log("WebSocket: error " + JSON.stringify(evt));
};
}
And this code for recording audio:
startRecording() {
var that = this;
this.initWebSocket(function () {
var mediaConstraints = {
audio: true
};
function onMediaSuccess(stream) {
that.mediaRecorder = new MediaStreamRecorder(stream);
that.mediaRecorder.mimeType = 'audio/wav';
that.mediaRecorder.ondataavailable = function (blob) {
that.websocket.send(blob);
};
that.mediaRecorder.start(3000);
}
function onMediaError(e) {
console.error('media error', e);
}
navigator.getUserMedia(mediaConstraints, onMediaSuccess, onMediaError);
});
}
I need do recognition in real-time using websocket with socket auto closing after 2 second of inactivity.
Please, advice me.
As #Daniel Bolanos said, inactivity_timeout is not triggered if the transcript is empty for more than inactivity_timeout seconds. The service uses a different way to detect if there is speech rather than relying on the transcription.
If the service detects speech it won't trigger the inactivity_timeout even if the transcript is empty.
Here is a snippet of code that does what you were trying to do with your question but using the speech-javascript-sdk.
Hopefully, it will help future StackOverflow users trying to recognize audio from the microphone.
document.querySelector('#button').onclick = function () {
// you need to provide this endpoint to fetch a watson token
fetch('/api/speech-to-text/token')
.then(function(response) {
return response.text();
}).then(function (token) {
var stream = WatsonSpeech.SpeechToText.recognizeMicrophone({
token: token,
outputElement: '#output' // CSS selector or DOM Element
});
stream.on('error', function(err) {
console.log(err);
});
document.querySelector('#stop').onclick = function() {
stream.stop();
};
}).catch(function(error) {
console.log(error);
});
};
Demo: https://watson-speech.mybluemix.net/microphone-streaming.html
Credits to #Nathan Friedly who wrote the library.
So, every time I refresh the page, it seems like sockjs is creating a new connection.
I am saving every message to my mongodb on every channel.onmessage, so if I refresh my page 7 times and send a message, I would save 7 messages of the same content into my mongodb.
This is very problematic because when I retrieve those messages when I go into the chat room, to see the log, I would see bunch of duplicate messages.
I want to keep track of all connections that are 'active', and if a user tries to make another connection, I want to terminate the old one so there is only one connection listening to each message at a time.
How do I do this ?
var connections = {};
//creating the sockjs server
var chat = sockjs.createServer();
//installing handlers for sockjs server instance, with the same url as client
chat.installHandlers(server, {prefix:'/chat/private'});
var multiplexer = new multiplexServer.MultiplexServer(chat);
var configChannel = function (channelId, userId, userName){
var channel = multiplexer.registerChannel(channelId);
channel.on('connection', function (conn) {
// console.log('connection');
console.log(connections);
connections[channelId] = connections[channelId] || {};
if (connections[channelId][userId]) {
//want to close the extra connection
} else {
connections[channelId][userId] = conn;
}
// }
// if (channels[channelId][userId]) {
// conn = channels[channelId][userId];
// } else {
// channels[channelId][userId] = conn;
// }
// console.log('accessing channel! ', channels[channelId]);
conn.on('new user', function (data, message) {
console.log('new user! ', data, message);
});
// var number = connections.length;
conn.on('data', function(message) {
var messageObj = JSON.parse(message);
handler.saveMessage(messageObj.channelId, messageObj.user, messageObj.message);
console.log('received the message, ', messageObj.message);
conn.write(JSON.stringify({channelId: messageObj.channelId, user: messageObj.user, message: messageObj.message }));
});
conn.on('close', function() {
conn.write(userName + ' has disconnected');
});
});
return channel;
};
The way I resolve a problem like yours was with a Closure and Promises, I don't know if that could help you. I let you the code that help me, this is with EventBus from Vertx:
window.Events = (function NewEvents() {
var eventBusUrl = $('#eventBusUrl').val();
var eventBus = null;
return new RSVP.Promise(function(resolve, reject) {
if(!eventBus) {
eventBus = new vertx.EventBus(eventBusUrl);
eventBus.onopen = function eventBusOpened() {
console.log('Event bus online');
resolve(eventBus);
}
eventBus.onclose = function() {
eventBus = null;
};
}
});
}());
And then in other script I call it in this way:
Events.then(function(eventBus) {
console.log("registering handlers for comments");
eventBus.registerHandler(address, function(incomingMessage) {
console.log(incomingMessage);
});
});
I hope this can help you.
Regards.