Twilio websocket stream endpoint - javascript

I'm trying to create a websocket connection during the call. I have a flex function:
const VoiceResponse = require('twilio').twiml.VoiceResponse;
exports.handler = function(context, event, callback) {
let voiceResponse = new VoiceResponse();
const response = new Twilio.Response();
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS POST GET');
response.appendHeader('Content-Type', 'application/json');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');
const start = voiceResponse.start();
const stream = start.stream({
name: event.taskSid,
url: 'wss://....ngrok.io',
});
stream.parameter({
name:'track',
value: 'both_tracks'
});
console.log(voiceResponse.toString());
callback(null, response);
};
And in my flex ui project I call this function on the event:
Actions.addListener('afterAcceptTask', (payload) => {
if (isInboundCall(payload)) inboundCall(payload);
});
I've got a xml response in function.
But I can't get any response at my websocket server. May be I'm calling my function from the wrong event?

Have you tried to take a look at the REST API. Returning TwiML only works if Twilio is requesting TwiML.
Start/Stop Media Streams API
https://www.twilio.com/changelog/startstop-media-streams-api

Related

Unable to establish WebRTC connection with Node JS server as a peer

I am trying to send images captured from a canvas to my NodeJS backend server using the WebRTC data channel. That is I am trying to make my server a peer. But for some reason, I am unable to establish a connection.
Client Side
async function initChannel()
{
const offer = await peer.createOffer();
await peer.setLocalDescription(offer);
const response = await fetch("/connect", {
headers: {
'Content-Type': 'application/json',
},
method: 'post',
body: JSON.stringify({ sdp: offer, id: Math.random() })
}).then((res) => res.json());
peer.setRemoteDescription(response.sdp);
const imageChannel = peer.createDataChannel("imageChannel", { ordered: false, maxPacketLifeTime: 100 });
peer.addEventListener("icecandidate", console.log);
peer.addEventListener("icegatheringstatechange",console.log);
// drawCanvas function draws images got from the server.
imageChannel.addEventListener("message", message => drawCanvas(remoteCanvasCtx, message.data, imageChannel));
// captureImage function captures and sends image to server using imageChannel.send()
imageChannel.addEventListener("open", () => captureImage(recordCanvasCtx, recordCanvas, imageChannel));
}
const peer = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.stunprotocol.org:3478" }] });
initChannel();
Here both captureImage and drawCanvas are not being invoked.
Server Side
import webrtc from "wrtc"; // The wrtc module ( npm i wrtc )
function handleChannel(channel)
{
console.log(channel.label); // This function is not being called.
}
app.use(express.static(resolve(__dirname, "public")))
.use(bodyParser.json())
.use(bodyParser.urlencoded({ extended: true }));
app.post("/connect", async ({ body }, res) =>
{
console.log("Connecting to client...");
let answer, id = body.id;
const peer = new webrtc.RTCPeerConnection({ iceServers: [{ urls: "stun:stun.stunprotocol.org:3478" }] });
await peer.setRemoteDescription(new webrtc.RTCSessionDescription(body.sdp));
await peer.setLocalDescription(answer = await peer.createAnswer());
peer.addEventListener("datachannel",handleChannel)
return res.json({ sdp: answer });
});
app.listen(process.env.PORT || 2000);
Here the post request is handled fine but handleChannel is never called.
When I run this I don't get any errors but when I check the connection status it shows "new" forever. I console logged remote and local description and they seem to be all set.
What am I doing wrong here?
I am pretty new to WebRTC and I am not even sure if this is the correct approach to continuously send images (frames of user's webcam feed) to and back from the server, if anyone can tell me a better way please do.
And one more thing, how can I send image blobs ( got from canvas.toBlob() ) via the data channel with low latency.
I finally figured this out with the help of a friend of mine. The problem was that I have to create DataChannel before calling peer.createOffer(). peer.onnegotiationneeded callback is only called once the a channel is created. Usually this happens when you create a media channel ( either audio or video ) by passing a stream to WebRTC, but here since I am not using them I have to to this this way.
Client Side
const peer = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] });
const imageChannel = peer.createDataChannel("imageChannel");
imageChannel.onmessage = ({ data }) =>
{
// Do something with received data.
};
imageChannel.onopen = () => imageChannel.send(imageData);// Data channel opened, start sending data.
peer.onnegotiationneeded = initChannel
async function initChannel()
{
const offer = await peer.createOffer();
await peer.setLocalDescription(offer);
// Send offer and fetch answer from the server
const { sdp } = await fetch("/connect", {
headers: {
"Content-Type": "application/json",
},
method: "post",
body: JSON.stringify({ sdp: peer.localDescription }),
})
.then(res => res.json());
peer.setRemoteDescription(new RTCSessionDescription(sdp));
}
Server
Receive offer from client sent via post request. Create an answer for it and send as response.
app.post('/connect', async ({ body }, res) =>
{
const peer = new webrtc.RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
console.log('Connecting to client...');
peer.ondatachannel = handleChannel;
await peer.setRemoteDescription(new webrtc.RTCSessionDescription(body.sdp));
await peer.setLocalDescription(await peer.createAnswer());
return res.json({ sdp: peer.localDescription });
});
The function to handle data channel.
/**
* This function is called once a data channel is ready.
*
* #param {{ type: 'datachannel', channel: RTCDataChannel }} event
*/
function handleChannel({ channel })
{
channel.addEventListener("message", {data} =>
{
// Do something with data received from client.
});
// Can use the channel to send data to client.
channel.send("Hi from server");
}
So here is what happens :
Client creates a Data-Channel.
Once data channel is created onnegotiationneeded callback is called.
The client creates an offer and sends it to the server (as post request).
Server receives the offer and creates an answer.
Server sends the answer back to the client (as post response).
Client completes the initialization using the received answer.
ondatachannel callback gets called on the server and the client.
I have used post request here to exchange offer and answer but it should be fairly easy to do the same using Web Socket if that is what you prefer.

Serverless WebSockets - No method found matching route #connections/* for http method POST

I'm using Serverless Framework to host my WebSocket, which has the typical $connect, $disconnect, $default, etc methods that updates my connections db:
case '$connect':
await dynamoDb.put({
TableName: process.env.CONNECTIONS_TABLE,
Item: {
connectionId,
// Expire the connection an hour later. This is optional, but recommended.
// You will have to decide how often to time out and/or refresh the ttl.
ttl: parseInt((Date.now() / 1000) + 3600)
}
}).promise();
My WebSocket setup is:
WebSocket URL: wss://1111111111.execute-api.ap-southeast-2.amazonaws.com/dev/
Connection URL: https://1111111111.execute-api.ap-southeast-2.amazonaws.com/dev/#connections
My HTTP setup is:
Invoke API at: https://222222222.execute-api.ap-southeast-2.amazonaws.com/dev/
I have a broadcast function which I am using to send data to the connections, which I am invoking with:
sls invoke --function broadcast --data '{ \"body\": \"Hello from server\" }'
The source sends a message to each connection, as provided in the params of the request:
async function sendMessage(connectionId, body) {
try {
await apig.postToConnection({
ConnectionId: connectionId,
Data: body
}).promise();
} catch (err) {
// Ignore if connection no longer exists
if(err.statusCode !== 400 && err.statusCode !== 410) {
throw err;
}
}
}
async function getAllConnections(ExclusiveStartKey) {
const { Items, LastEvaluatedKey } = await dynamoDb.scan({
TableName: process.env.CONNECTIONS_TABLE,
AttributesToGet: [ 'connectionId' ],
ExclusiveStartKey
}).promise();
const connections = Items.map(({ connectionId }) => connectionId);
if(LastEvaluatedKey) {
connections.push(...await getAllConnections(LastEvaluatedKey));
}
return connections;
}
module.exports.handler = async function(event, context) {
const { body } = event;
const connections = await getAllConnections();
await Promise.all(
connections.map(connectionId => sendMessage(connectionId, body))
);
}
A connection can be established (I can connect to the WebSocket), however when I try to invoke this function I am receiving the error:
No method found matching route #connections/ZE4SDfSJSwMCJ4g%3D for http method POST.
The ZE4SDfSJSwMCJ4g is my connectionId, which exists in my database. I am unsure if this routing issue has to do with my HTTP API and my WebSocket API being pointed at different API Gateway URL?
I appreciate the help!
Make sure that the endpoint in your API Gateway management configuration is the same as your ws endpoint.
const agma = new AWS.ApiGatewayManagementApi({
apiVersion: AGMA_VERSION,
endpoint: WS_ENDPOINT // 1111111111.execute-api.ap-southeast-2.amazonaws.com/dev
})
I too had this issue with websockets and google cloud functions. I think it's because the web socket server is put to sleep and doesn't actively listen because it's serverless.
Functions are configured to wake up on http requests (ports 80/443) and go back to sleep, so they need to be specially configured to wake up to requests on web socket port.
This seems possible using a serverless websockets plugin https://github.com/serverless/serverless-websockets-plugin there is a how to article at https://www.serverless.com/blog/api-gateway-websockets-example

Running Async functions in Microsoft Bot Builder (with Node.JS)

I'm trying to make a test bot that, upon being chatted to responds with a (meaningless) string gotten from a JSON object through another API
Code:
var restify = require('restify');
var builder = require('botbuilder');
var request = require('request-promise');
// Setup Restify Server
var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
console.log('%s listening to %s', server.name, server.url);
});
// Create chat connector for communicating with the Bot Framework Service
var connector = new builder.ChatConnector({
appId: process.env.MicrosoftAppId,
appPassword: process.env.MicrosoftAppPassword
});
// Listen for messages from users
server.post('/api/messages', connector.listen());
// Receive messages from the user and respond by echoing each message back (prefixed with 'You said:')
var bot = new builder.UniversalBot(connector, function (session) {
var text = await MyRequest()
session.send("%s", text);
});
async function MyRequest() {
var options = {
uri: "https://jsonplaceholder.typicode.com/posts/1",
method: "GET",
json: true
}
try {
var result = await request(options);
return result;
} catch (err) {
console.error(err);
}
}
The problem is the bot var isn't an asynch function, so I can't put await in it. If I remove await, the bot replies with Object Promise. I'm fairly inexperienced in JS overall, so can I get any pointers?
e: The Request part works great, I've tested it alone in a different js program
Have you tried this. If you are using ES6 compatible Node environment this should work
var bot = new builder.UniversalBot(connector, async function (session) {
// Use JSON.stringify() if MyRequest Promise will resolve a object
var text = await MyRequest()
session.send("%s", text);
});
If async/await isn't possible, how about returning a promise? like below:
function MyRequest() {
var options = {
uri: "https://jsonplaceholder.typicode.com/posts/1",
method: "GET",
json: true
}
return request(options);
}
And use Promise.then to act on the result, like so:
var bot = new builder.UniversalBot(connector, function (session) {
MyRequest().then(function(text) {
session.send("%s", text);
}).catch(function(error) {
session.send("%s", error);
});
});

Enable CORS from a Node.JS Callback Function

I'm attempting to use Twilio Functions to handle token generation for my Twilio application. I previously was using a Node.js + Express Server to accomplish this, but I do not know how to figure out enable CORS in this type of environment.
My Client Code looks like this:
$('#new-twilio').click(function(){
var toNum = this.value;
if(token == undefined) {
$.getJSON('https://my-twilio-function/endpoint').done(function(data){
token = data.token;
Twilio.Device.setup(token, {debug: true});
Twilio.Device.ready(function(device){
Twilio.Device.connect({"PhoneNumber": toNum});
});
}).fail(function(error){
alert("Failure!");
alert(JSON.stringify(error));
});
} else {
Twilio.Device.connect({"PhoneNumber": toNum});
}
});
My function code looks like this:
exports.handler = function(context, event, callback) {
const client = context.getTwilioClient();
const ClientCapability = require('twilio').jwt.ClientCapability;
const responseHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST",
"Access-Control-Allow-Headers": "content-type, accept",
"Content-Type": "application/json"
};
let identity = "sampleIdentity";
const capability = new ClientCapability({
accountSid: context.ACCOUNT_SID,
authToken: context.AUTH_TOKEN
});
capability.addScope(new ClientCapability.IncomingClientScope(identity));
capability.addScope(new ClientCapability.OutgoingClientScope({
applicationSid: context.TWILIO_TWIML_APP_SID
}));
console.log(capability.toJwt());
callback(null, {headers: responseHeaders, identity: identity, token: capability.toJwt()});
};
Worth noting, the console.log proves that this function is returning the exact token I need, but I continue to get this error:
XMLHttpRequest cannot load https://my-twilio-function/endpoint. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8080' is therefore not allowed access.
Obviously, my twilio function is at a real URL. As much as I google, I cannot find how to allow access control to this type of node method.
This client code ended up working:
exports.handler = function(context, event, callback) {
const client = context.getTwilioClient();
const ClientCapability = require('twilio').jwt.ClientCapability;
const response = new Twilio.Response();
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');
response.appendHeader('Content-Type', 'application/json');
let identity = "sampleIdentity";
const capability = new ClientCapability({
accountSid: context.ACCOUNT_SID,
authToken: context.AUTH_TOKEN
});
capability.addScope(new ClientCapability.IncomingClientScope(identity));
capability.addScope(new ClientCapability.OutgoingClientScope({
applicationSid: context.TWILIO_TWIML_APP_SID
}));
response.setBody({identity: identity, token: capability.toJwt()})
console.log(capability.toJwt());
callback(null, response);
};
Twilio developer evangelist here.
I'm glad to see that K. Rhoda sorted out the issue. I just wanted to make obvious what made it work.
There is a custom response you can access from Twilio.Response within the Function. The response is initialized like:
const response = new Twilio.Response();
and then has the following useful methods:
// set the body of the response
response.setBody(body);
// set a header for the response
response.appendHeader(key, value);
// set all the headers for the response
response.setHeaders(obj);
// set the status code for the response
response.setStatusCode(200);
You can then send that response using the callback function, like so:
callback(null, response);
In my case everything above didn't work because i had "Check for valid Twilio signature" checked in my fuction (by default) and made requests without signature.
After I unchecked it, answers above worked. So pay attention to whether you have this checked and if so whether your request has proper signature.

How do I return mock data from requests to the Twitter Streaming API in Node

I have a Node application that connects to the Twitter REST and streaming APIs. In order to test code that makes requests to the REST API, I can use Nock to intercept the HTTP request and return mock data like so:
var nock = require('nock')
var mockData = [...]
nock('https://api.twitter.com/1.1')
.get('/search/tweets.json')
.reply(200, mockData)
My application also connects to the streaming API endpoint statuses/filter and performs some analysis on tweets received via the streaming API. In Nock's README it states that you can pass a function to .query() that returns a stream, however I haven't been able to get this to work.
How would I return mock data from requests to this endpoint using Nock (or another library if necessary)? I'd ideally like to be able to send tweets to the stream in my tests whenever I need to, for example:
it('should correctly process tweets coming in on the streaming API', () => {
var mockTweet = {...}
sendMockTweetToStream(mockTweet)
...verify that the mock tweet was received and processed
})
I've found a way to do this in my tests without mocking the streaming API.
I'm using the NPM Twitter package to access the Twitter streaming API like so:
var client = new Twitter({
consumer_key: twitterConsumerKey,
consumer_secret: twitterConsumerSecret,
access_token_key: twitterAccessTokenKey,
access_token_secret: twitterAccessTokenSecret
})
stream = client.stream('statuses/filter', {track: 'something'})
stream.on('data', event => {
// Do some processing of incoming tweets here
})
Instead of mocking the streaming API, I mocked the client object using my own stream with rewire, and set up a function to allow me to send it mock tweets whenever I want.
const rewire = require('rewire')
const stream = rewire('stream')
const moduleToTest = require('somemodule')
const mockStream = new stream.Readable()
mockStream._read = () => {}
mockStream.destroy = () => {}
// Send a Tweet event to the mock stream
function writeTweetToStream (text) {
mockStream.emit('data', {text: text})
}
describe('Module that uses the Twitter Streaming API', () => {
beforeAll(() => {
// client.stream should return our mock stream
moduleToTest.__set__('client', {stream: () => { return mockStream }})
})
it('should process incoming tweets', () => {
writeTweetToStream('This is a tweet #somehashtag')
// Assert that the tweet was received and processed
})
})

Categories