I've implemented a call center in Salesforce with Twilio client JavaScript SDK. I'm trying to save the call record information in Salesforce and I'm using the connection.parameters.CallSid to identify the correct record when the recording call back fires. However my CallSid in client side is getting changed automatically sometimes to a different format and hence the recording call back function can't find the Salesforce end record to update it with the RecordingUrl. Have anyone experienced this before or appreciate any guidance.
Below is my JavaScript code. To be more specific, in the startCall function console.log print the CallSid correctly but when goes to saveLog function it's having a different value with a different format and hence the saved record having an incorrect value.
<script type="text/javascript">
Twilio.Device.setup("{! token }");
var callerSid; // hold the Twilio generated CallSid unique to this call
var parentId; // hold the parent record being called in order to associate as the parent of task being logged for the call
var newTaskId; // hold the id of newly created task
// function to fire when click 2 dial executes
var startCall = function (payload) {
sforce.opencti.setSoftphonePanelVisibility({visible: true}); //pop up CTI softphone
parentId = payload.recordId; // the record that the phone number being called belongs to
var cleanednumber = cleanFormatting(payload.number);
params = {"PhoneNumber": cleanednumber};
var connection = Twilio.Device.connect(params);
callerSid = connection.parameters; // track the unique Twilio CallSid
console.log('clk2dial : ', callerSid.CallSid); **// here it logs correcly as CAc57d05994cd69498e0353a5f4b07f2dc**
setTimeout(function(){
saveLog(); // save the call information in a Task record
}, 2000
);
};
//OpenCTI!!
sforce.opencti.enableClickToDial();
sforce.opencti.onClickToDial({listener : startCall}); // click 2 dial
function cleanFormatting(number) {
//changes a SFDC formatted US number, which would be 415-555-1212 into a twilio understanble number 4155551212
return number.replace(' ','').replace('-','').replace('(','').replace(')','').replace('+','');
}
// save the call information in a Task record
function saveLog() {
var keyPrefix;
var taskToSave;
console.log('callerSid.CallSid : ', callerSid.CallSid); **// surprisingly here it logs as TJSce253eb4-c2a0-47f3-957f-8178e95162aa**
if(parentId != null) {
keyPrefix = parentId.slice(0,3);
}
if(keyPrefix != null && keyPrefix == '003') {
taskToSave = {WhoId:parentId, Type: "Call", CallObject: callerSid.CallSid, entityApiName: "Task", Subject: "Call log"};
} else {
taskToSave = {WhatId:parentId, Type: "Call", CallObject: callerSid.CallSid, entityApiName: "Task", Subject: "Call log"};
}
sforce.opencti.saveLog({value:taskToSave, callback: saveLogCallBack});
}
// call back function for saving the call information in a Task record
var saveLogCallBack = function(response) {
if(response.success) {
newTaskId = response.returnValue.recordId;
console.log('save success! : ', newTaskId);
} else {
console.error('Error saving : ', response.errors);
}
}
</script>
Answering my own question as I got through this. I registered a function for Twilio.Device.connect and in the call back function retrieved the CallSid. Along with that I've updated my click 2 dial function as well accordigly as below. However I was unable to find this approach in Twilio documentation and any comments are welcome.
// function to fire when click 2 dial executes
var startCall = function (payload) {
sforce.opencti.setSoftphonePanelVisibility({visible: true}); //pop up CTI softphone
parentId = payload.recordId; // the record that the phone number being called belongs to
var cleanednumber = cleanFormatting(payload.number);
params = {"PhoneNumber": cleanednumber};
Twilio.Device.connect(params);
};
//OpenCTI!!
sforce.opencti.enableClickToDial();
sforce.opencti.onClickToDial({listener : startCall}); // click 2 dial
// registered a function for Twilio Device connect
Twilio.Device.connect(function(response) {
callSid = response.parameters; // track the unique Twilio CallSid
// nothing change in save function so not posting again
saveLog(); // save the call information in a Task record
});
I'm attempting to setup a node js server to stream some events via SSE and I cannot get it to work unless I start the stream with the data field.
When i try and include other fields for example id or event they dont show in the chrome inspector?
If I attempt to put these field before data no events other than the connection opening occurs.
Here is the route that I am playing around with.
router.get('/stream', function * (){
let stream = new PassThrough();
let send = (message, id) => stream.write(`id: ${JSON.stringify(id)}\n data: ${JSON.stringify(message)}\n\n`);
let finish = () => dispatcher.removeListener('message', send);
this.socket.setTimeout(Number.MAX_VALUE);
this.type = 'text/event-stream;charset=utf-8';
this.set('Cache-Control', 'no-cache');
this.set('Connection', 'keep-alive');
this.body = stream;
stream.write(': open stream\n\n');
dispatcher.on('message', send);
this.req.on('close', finish);
this.req.on('finish', finish);
this.req.on('error', finish);
setInterval(function(){
dispatcher.emit('message', {date: Date.now()}, '12345')
}, 5000)
});
I am not too certain if I am writing to the stream correctly.
Thanks
Silly me, it was indeed a malformed event stream. There isnt supposed to be a space between the fields. It should instead be:
id: ${JSON.stringify(id)}\ndata: ${JSON.stringify(message)}\n\n
I have been trying to get data chat working using webrtc. It was working previously in google chrome and suddenly stopped working, I have narrowed down the issue to 'ondatachannel' callback function not getting triggered. The exact same code works fine in Mozilla.
Here's the overall code:
app.pc_config =
{'iceServers': [
{'url': 'stun:stun.l.google.com:19302'}
]};
app.pc_constraints = {
'optional': [
/* {'DtlsSrtpKeyAgreement': true},*/
{'RtpDataChannels': true}
]};
var localConnection = null, remoteConnection = null;
try {
localConnection = new app.RTCPeerConnection(app.pc_config, app.pc_constraints);
localConnection.onicecandidate = app.setIceCandidate;
localConnection.onaddstream = app.handleRemoteStreamAdded;
localConnection.onremovestream = app.handleRemoteStreamRemoved;
}
catch (e) {
console.log('Failed to create PeerConnection, exception: ' + e.message);
return;
}
isStarted = true;
In Create Channel that follows this:
var localConnection = app.localConnection;
var sendChannel = null;
try {
sendChannel = localConnection.createDataChannel(app.currentchannel,
{reliable: false});
sendChannel.onopen = app.handleOpenState;
sendChannel.onclose = app.handleCloseState;
sendChannel.onerror = app.handleErrorState;
sendChannel.onmessage = app.handleMessage;
console.log('created send channel');
} catch (e) {
console.log('channel creation failed ' + e.message);
}
if (!app.isInitiator){
localConnection.ondatachannel = app.gotReceiveChannel;
}
app.sendChannel = sendChannel;
I create Offer:
app.localConnection.createOffer(app.gotLocalDescription, app.handleError);
and Answer:
app.localConnection.createAnswer(app.gotLocalDescription, app.handleError);
the offer and answer get created successfully, candidates are exchanged and onicecandidate event is triggered at both ends! Local Description and RemoteDescription are set on both respective ends.
I have a pusher server for signalling, I am able to send and receive messages through the pusher server successfully.
The same webrtc code works for audio/video = true, the only issue is when I try to create datachannel. The only step that does not get executed is the callback function not getting executed i.e "gotReceiveChannel"
I'm starting to think it's the version of chrome.. I am not able to get the GitHub example working in chrome either: (Step4 for data chat)
https://bitbucket.org/webrtc/codelab
While the same code works in Mozilla.
The sendChannel from the offerer has a "readyState" of "connecting"
Any help much appreciated.
I am writing a multi-peer WebRTC video chat.
Two peers have no trouble connecting, no error or warning in console, and video works well, but I cannot add a third party to the chat successfully.
On the host (the first participant, Firefox), the error appear as "Cannot set remote answer in state stable" when trying to create an answer. At the second participant (Chrome), the error is "Failed to set remote answer sdp: Called in wrong state: STATE_INPROGRESS". At he third peer, the error is "the error is "Failed to set remote answer sdp: Called in wrong state: STATE_RECEIVEDINITIATE".
As it turn out, the first peer failed to have video with the third peer. Other two links appear fine.
Generally, my communication model is as below, self_id is a unique id per each peer in the session, and locate_peer_connection() will return the local peer_connection of the particular peer from which we receive message:
a new client send "peer_arrival" to the session using signalling server
all peers already in the session setlocaldescription, create offer and send to the new client
new client create answers to all other peers and setremotedescription
new client has video coming up
Signalling is done using WebSocket on a node.js server.
I have some of the core code below, some more note:
self_id is an unique id per client in a session
peer_connection stores peerConnection to other nodes, and peer_id store the respective user_id of these objects
local_stream is the local video stream from getUserMedia (already considered different browser)
Any insights in to the issue? Is there something wrong with my model?
// locate a peer connection according to its id
function locate_peer_connection(id) {
var index = peer_id.indexOf(id);
// not seen before
if (index == -1) {
add_peer_connection();
peer_id.push(id);
index = peer_id.length - 1;
}
return index;
}
// add a peer connection
function add_peer_connection() {
console.log('add peer connection');
// add another peer connection for use
peer_connection.push(new rtc_peer_connection({ "iceServers": [{ "url": "stun:"+stun_server }]}));
// generic handler that sends any ice candidate to the other peer
peer_connection[peer_connection.length - 1].onicecandidate = function (ice_event) {
if (ice_event.candidate) {
signaling_server.send(
JSON.stringify({
type: "new_ice_candidate",
candidate: ice_event.candidate,
id: self_id,
token:call_token
})
);
console.log('send new ice candidate, from ' + self_id);
}
};
// display remote video streams when they arrive using local <video> MediaElement
peer_connection[peer_connection.length - 1].onaddstream = function (event) {
video_src.push(event.stream); // store this src
video_src_id.push(peer_connection.length - 1);
if (video_src.length == 1) { // first peer
connect_stream_to_src(event.stream, document.getElementById("remote_video"));
// video rotating function
setInterval(function() {
// rorating video src
var video_now = video_rotate;
if (video_rotate == video_src.length - 1) {
video_rotate = 0;
} else {
video_rotate++;
}
var status = peer_connection[video_src_id[video_rotate]].iceConnectionState;
if (status == "disconnected" || status == "closed") { // connection lost, do not show video
console.log('connection ' + video_rotate + ' liveness check failed');
} else if (video_now != video_rotate) {
connect_stream_to_src(video_src[video_rotate], document.getElementById("remote_video"));
}
}, 8000);
// hide placeholder and show remote video
console.log('first remote video');
document.getElementById("loading_state").style.display = "none";
document.getElementById("open_call_state").style.display = "block";
}
console.log('remote video');
};
peer_connection[peer_connection.length - 1].addStream(local_stream);
}
// handle new peer
function new_peer(signal) {
// locate peer connection
var id = locate_peer_connection(signal.id);
console.log('new peer ' + id);
// create offer
peer_connection[id].createOffer(function(sdp) {
peer_connection[id].setLocalDescription(sdp,
function() { // call back
console.log('set local, send offer, connection '+ id);
signaling_server.send(
JSON.stringify({
token: call_token,
id: self_id,
type:"new_offer",
sdp: sdp
})
);
}, log_error);
}, log_error);
}
// handle offer
function new_offer_handler(signal) {
var id = locate_peer_connection(signal.id);
console.log('new offer ' + id);
// set remote description
peer_connection[id].setRemoteDescription(
new rtc_session_description(signal.sdp),
function() { // call back
peer_connection[id].createAnswer(function(sdp) {
peer_connection[id].setLocalDescription(sdp, function () {
console.log('set local, send answer, connection '+ id);
signaling_server.send(
JSON.stringify({
token: call_token,
id: self_id,
type:"new_answer",
sdp: sdp
})
);
},
log_error);
}, log_error);
}, log_error);
}
// handle answer
function new_answer_handler(signal) {
var id = locate_peer_connection(signal.id);
console.log('new answer ' + id);
peer_connection[id].setRemoteDescription(new rtc_session_description(signal.sdp),
function() {
console.log('receive offer answer, set remote, connection '+ id);
}
, log_error);
}
// handle ice candidate
function ice_candidate_handler(signal) {
var id = locate_peer_connection(signal.id);
console.log('get new_ice_candidate from ' + id);
if (typeof(RTCIceCandidate) != "undefined") {
peer_connection[id].addIceCandidate(
new RTCIceCandidate(signal.candidate)
);
} else { // firefox
peer_connection[id].addIceCandidate(
new mozRTCIceCandidate(signal.candidate)
);
}
}
function event_handler(event) {
var signal = JSON.parse(event.data);
if (signal.type === "peer_arrival") {
new_peer(signal);
} else if (signal.type === "new_ice_candidate") {
ice_candidate_handler(signal);
} else if (signal.type === "new_offer") { // get peer description offer
new_offer_handler(signal);
} else if (signal.type === "new_answer") { // get peer description answer
new_answer_handler(signal);
} else if (signal.type === "new_chat_message") { // chat message and file sharing info
add_chat_message(signal);
} else if (signal.type === "new_file_thumbnail_part") { // thumbnail
store_file_part(signal.name, "thumbnail", signal.id, signal.part, signal.length, signal.data);
if (file_store[signal.id].thumbnail.parts.length == signal.length) {
document.getElementById("file_list").innerHTML = get_file_div(signal.id, signal.name)+document.getElementById("file_list").innerHTML;
document.getElementById("file-img-"+signal.id).src = file_store[signal.id].thumbnail.parts.join("");
}
} else if (signal.type === "new_file_part") { // file
console.log('get new_file_part ' + signal.id);
store_file_part(signal.name, "file", signal.id, signal.part, signal.length, signal.data);
update_file_progress(signal.name, signal.id, file_store[signal.id].file.parts.length, signal.length);
}
}
// generic error handler
function log_error(error) {
console.log(error);
}
I might be wrong, but these two must be your major problems:
signaling_server.send(... I am not seeing any target here, so guessing that the server just broadcasts this message to everyone. When you are sending an sdp to already established peer connection, you are bound to get the error which you are getting now. My suggesting would be to add a target id in the message, either the server can forward it to that particular peer or, server can just broadcast, but event_handler of the peer can check if the target id of message is same as it's own id, if not, just ignore the message.
onicecandidate event, you are broadcasting the ICE candidates to all remote peers, again this is meant for single peer, another issue might be, addIceCandidate on PeerConnection before setting it's local and remote description would throw error, you need to some sort of mechanism to handle this( add ICE candidates only after setting the connections descriptions).
finally a suggestion. I am guessing peer_connection is an Array, if you change it Object, you can remove the redundancy of locate_peer_connection,
you can do something like.
if(peer_connection[signal.id]){
//do something...
}else{
peer_connection[signal.id] = new PeerConnection(...
}
i had the same problem when i was implementing one-to-many rtc broadcast, and what mido22 said is right. you might be sending/resetting existing established peer object with other incoming client. you have to create new RTCPeerConenction object for every new incoming client. the general work flow would be as follow.
peerconnection=[];//empty array
then you initialize your media devices with getUserMedia
and store media stream into global variable so that it can be added when creating offer.
once this is done you inform your singalling server with unique id
your signalling server may then broadcast this to all clients except from which it is received. each client will then check if that unique id does exist in there peerconnection array and like mido22 said you can do this as
if(peerconnection[signal.id])
{
//do something with existing peerconnections
}
else
{
peerconnection[signal.id]=new RTCPeerConnection({"stun_server_address"});
//register peerconnection[signal.id].onicecandidate callback
//register peerconnection[signal.id].onaddstream callback
//createoffer
//if local stream is ready then
peerconnection[signal.id].addStream(localStream);
//and rest of the stuff go as it is like in one-to-one call..
//setLocalDescriptor setRemoteDescriptor
}
I have the following callback for the onicecandidate event of RTCPeerConnection:
function iceCallback(event) {
if (event.candidate) {
var candidate = event.candidate;
socSend("candidate", candidate. event.target.id);
}
}
I am able to read the candidate from the event.candidate. But when I try to read the event.target.id, I get an exception, cannot read property target of undefined.
"id" is a property I previously created for the RTCPeerConnection. This is for a multiuser video chat.
I am trying to figure out which RTCPeerConnection fired the onicecandidate event so I know to which client I need to send the icecandidate. I am using the adapter.js library.
I assumed that some part of your code would be like below, I have just added a simple modification:
peerConnections ={};
function createNewConncection(id){
var pc = new RTCPeerConnection();
pc.onaddstream = ...
...
//pc.onicecandidate = iceCallback; // OLD CODE
pc.onicecandidate = iceCallback.bind({pc:pc, id:id}); // NEW CODE
peerConnections[id] = pc;
}
function iceCallback(event) {
if (event.candidate) {
var candidate = event.candidate;
//socSend("candidate", candidate. event.target.id); // OLD CODE
socSend("candidate", this.id); // NEW CODE
}
}