I am learning about webRTC and am trying to create a really simple chat app for multiple peers. Everything works great when using devices in the same network, but when I try to access my site on mobile using 4g it doesn't seem to connect (or atleast send messages). I added a google stun server to my config, but that didn't solve the problem. Does anyone see what could possibly cause my trouble? I am not receiving any errors in chrome, but firefox does tell me: WebRTC: ICE failed, add a TURN server and see about:webrtc for more details.
class socket
{
constructor()
{
this.socket = new WebSocket(`${window.location.protocol == "https:" ? "wss" : "ws"}://${window.location.host}`);
this.socket.onmessage = e => this.messageHandler(e.data);
}
async messageHandler(data)
{
data = JSON.parse(data);
let channelName = data.sender_channel_name;
switch (data.type)
{
case "sendOffer":
peerConnections[channelName] = new peerConnection();
const offer = await peerConnections[channelName].createOffer();
this.socket.send(JSON.stringify({type: 'sendOffer', sdp: offer, 'sender_channel_name':channelName}));
break;
case "offer":
peerConnections[channelName] = new peerConnection();
let answer = await peerConnections[channelName].sendAnswer(data.sdp);
this.socket.send(JSON.stringify({'type':'offer', 'sender_channel_name':channelName, 'anwser':JSON.stringify(answer)}))
break;
case "answer":
peerConnections[channelName].setAnswer(data.answer);
break;
}
}
}
class peerConnection
{
constructor ()
{
let peerConnectionConfig = {
iceServers:[
{urls:["stun:stun.l.google.com:19302"]}
]};
this.pc = new RTCPeerConnection(peerConnectionConfig);
this.pc.ondatachannel = e => {
this.dc = e.channel;
this.dc.onmessage = e => this.messageCallback(e.data);
}
this.pc.addEventListener("iceconnectionstatechange", (e) => ((pc) => {
if(pc.pc.iceConnectionState == "disconnected") {
delete peerConnections[Object.keys(peerConnections).find(key => peerConnections[key] === pc)];
}
})(this), false);
}
waitToCompleteIceGathering() {
return new Promise(resolve => {
this.pc.addEventListener('icegatheringstatechange', e => (e.target.iceGatheringState === 'complete') && resolve(this.pc.localDescription));
});
}
async createOffer()
{
this.dc = this.pc.createDataChannel("channel");
this.dc.onmessage = e => this.messageCallback(e.data);
this.pc.createOffer().then( o => this.pc.setLocalDescription(o) )
const offer = await this.waitToCompleteIceGathering();
return JSON.stringify(offer);
}
async sendAnswer (sdp)
{
this.pc.setRemoteDescription(JSON.parse(sdp));
this.pc.createAnswer().then(a => this.pc.setLocalDescription(a));
const answer = await this.waitToCompleteIceGathering();
return answer;
}
setAnswer (sdp)
{
this.pc.setRemoteDescription(JSON.parse(sdp));
}
messageCallback (data)
{
data = JSON.parse(data);
var para = document.createElement("p");
var node = document.createTextNode(data.message);
para.appendChild(node);
document.getElementById('mailbox').appendChild(para);
}
}
window.sendData = (value) =>
{
for (const connection in peerConnections)
{
try
{
peerConnections[connection].dc.send(JSON.stringify({"message":value}));
}
catch (DOMException)
{
// This when someone refreshes page, but 5 seconds to update didn't pass yet.
// Not really a problem.
}
}
}
let s = new socket();```
Related
I used the code from Nikolay answer https://jsfiddle.net/makhalin/nzw5tv1q/ on my Ionic - Angular PWA (I put it on a custom.js file and imported it on angular.json). It's working great if I open it in Chrome or Edge on Android but if I install it as a PWA it works the first time, then stops working.
Is there anything I must do to make it work as a PWA?
//have a console on mobile
const consoleOutput = document.getElementById("console");
const log = function (msg) {
consoleOutput.innerText = `${consoleOutput.innerText}\n${msg}`;
console.log(msg);
}
//Test browser support
const SUPPORTS_MEDIA_DEVICES = 'mediaDevices' in navigator;
if (SUPPORTS_MEDIA_DEVICES) {
//Get the environment camera (usually the second one)
navigator.mediaDevices.enumerateDevices().then(devices => {
const cameras = devices.filter((device) => device.kind === 'videoinput');
if (cameras.length === 0) {
log('No camera found on this device.');
}
// Create stream and get video track
navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment',
}
}).then(stream => {
const track = stream.getVideoTracks()[0];
//Create image capture object and get camera capabilities
const imageCapture = new ImageCapture(track)
imageCapture.getPhotoCapabilities().then(capabilities => {
//let there be light!
const btn = document.querySelector('.switch');
const torchSupported = !!capabilities.torch || (
'fillLightMode' in capabilities &&
capabilities.fillLightMode.length != 0 &&
capabilities.fillLightMode != 'none'
);
if (torchSupported) {
let torch = false;
btn.addEventListener('click', function (e) {
try {
track.applyConstraints({
advanced: [{
torch: (torch = !torch)
}]
});
} catch (err) {
log(err);
}
});
} else {
log("No torch found");
}
}).catch(log);
}).catch(log);
}).catch(log);
//The light will be on as long the track exists
}
I am using twilio TURN server for webRTC peer connecting two browsers located on different sides of the world, still the connection does not open.
Log shows the local and remote descriptions are set on both sides. Audio/video tracks are also pushed and received, but the "onopen" method on either of the data channels are not firing. Below is the code extract.
create offer code
async createOffer(){
this.initiated = true
this.conn = new RTCPeerConnection(this.servers);
if (this.conn){
this.conn.ontrack = e => {
e.streams[0].getTracks().forEach(track => {
this.calleeStream?.addTrack(track);
this.logs.push('received track:' + track.label);
})
}
if (this.callerStream)
{
const s = this.callerStream;
this.callerStream.getTracks().forEach(track =>
{
this.conn?.addTrack(track,s);
this.logs.push('pushed track:' + track.label);
});
}
}
this.channel = this.conn.createDataChannel('channelX');
this.channel.onmessage = e => this.logs.push('received =>'+ e.data);
this.channel.onopen = e => {
this.logs.push('connection OPENED!!!');
this.enabletestmessage = true;
};
this.conn.onicecandidate = async e=> {
if (e.candidate===null && !this.iceCandiSent){
this.iceCandiSent = true;
this.logs.push('new ICE candidate received- reprinting SDP'+JSON.stringify(this.conn?.localDescription));
await this.dataService.createOffer(this.data.callerid,this.data.calleeid,JSON.stringify(this.conn?.localDescription));
this.logs.push('offer set in db');
this.logs.push('waiting for answer...');
}
}
const offer = await this.conn.createOffer();
await this.conn?.setLocalDescription(offer);
this.logs.push('local description (offer) set');
}
create answer code
async createAnswer(offerSDP:string){
this.initiated = true;
this.conn = new RTCPeerConnection(this.servers);
if (this.conn)
{
this.conn.ontrack = e => {
e.streams[0].getTracks().forEach(track => {
this.callerStream?.addTrack(track);
this.logs.push('received track:' + track.label);
})
}
if (this.calleeStream)
{
const s = this.calleeStream;
this.calleeStream.getTracks().forEach(track =>
{
this.conn?.addTrack(track,s);
this.logs.push('pushed track:' + track.label);
});
}
}
await this.conn.setRemoteDescription(JSON.parse(offerSDP));
this.logs.push('remote description (offer) set');
this.conn.onicecandidate = async e => {
if (e.candidate === null && !this.iceCandiSent){
this.iceCandiSent=true;
this.logs.push('new ICE candidate received- reprinting SDP'+JSON.stringify(this.conn?.localDescription));
await this.dataService.updateAnswer(this.data.callerid,this.data.calleeid,JSON.stringify(this.conn?.localDescription));
this.logs.push('answer set in db');
}
}
this.conn.ondatachannel = e => {
this.channel = e.channel;
this.channel.onmessage = e => this.logs.push('received =>'+ e.data);
this.channel.onopen = e => {
this.logs.push('connection RECEIVED!!!');
this.enabletestmessage = true;
};
}
const answer = await this.conn.createAnswer();
await this.conn.setLocalDescription(answer);
this.logs.push('local description (answer) set');
}
server side code for retrieving ice servers from Twillio
const twilio = require('twilio');
const client = twilio(<MY ACCOUNT SID>,<MY AUTH TOKEN>);
const result = await client.tokens.create();
return result.iceServers; //this is set to this.servers in the code above
Everything works when I run on two browser windows in my local machine. However even afer implementing TURN they dont work between browsers in Nepal and USA. The onopen event handlers on data channel does notfire even though local and remote descriptions are set on both sides. What am I missing ?
NOTE: signalling is done inside the onicecandidate event handler ( the line that calls dataService createOffer/updateAnswer methods)
I'm currently implementing a WebSocket connection and I'm using a command pattern approach to emit some messages according to the command that users execute.
This is an abstraction of my implementation:
let socketInstance;
const globalName = 'ws'
const globalObject = window[globalName];
const commandsQueue = isArray(globalObject.q) ? globalObject.q : [];
globalObject.q = {
push: executeCommand
};
commandsQueue.forEach(command => {
executeCommand(command);
});
function executeCommand(params) {
const actions = {
create,
send
};
const [command, ...arg] = params;
if (actions[command]) {
actions[command](arg);
}
}
function send([message]) {
socketInstance.send(message);
}
function create([url]) {
socketInstance = new WebSocket(url);
}
In order to start sending messages, the user should be run:
window.ws.push('create', 'ws://url:port');
window.ws.push('send', 'This is a message');
The problem that I have is the connection is async, and I need to wait until the connection is done to continue to the next command. Is it a good idea to implement an async/await in commandsQueue.forEach or an iterator is a better approach? What other best approaches do you recommend?
The solution that I'm using right now is: I created an empty array of messages at the beginning and then every time that I call the send command I verify if the connection wasn't opened and I added to this array.
Something like that:
const messages = [];
let socketInstance;
let isConnectionOpen = false;
const globalName = "ws";
const globalObject = window[globalName];
const commandsQueue = isArray(globalObject.q) ? globalObject.q : [];
globalObject.q = {
push: executeCommand,
};
commandsQueue.forEach((command) => {
executeCommand(command);
});
function executeCommand(params) {
const actions = {
create,
send,
};
const [command, ...arg] = params;
if (actions[command]) {
actions[command](arg);
}
}
function send([message]) {
if (isConnectionOpen) {
socketInstance.send(message);
} else {
messages.push(message);
}
}
function onOpen() {
isConnectionOpen = true;
messages.forEach((m) => {
send([m]);
});
messages.length = 0;
}
function create([url]) {
socketInstance = new WebSocket(url);
socketInstance.onopen = onOpen;
}
I have a piece of code where I consume events from RabbitMQ and save the events(3 types of events A,B,C) into 2 different databases A,B. I can push into Database A without any problem but I need to wait for no of events to be at least 100 to push events of type B & C into database or until the code is trying to fill up the queue for last 5 minutes from the point saveToDb in invoked. I am not able to figure out how to wait for events for B and C and then save data in database.
Note that Event A will go into Database A and Event B,C will go into Database B.
I have written following piece of code.
import { Channel, ConsumeMessage } from 'amqplib';
const BATCH_SIZE = 100;
var eventBQueue = [];
var eventAQueue = [];
const shiftElements = (message) => {
if ( message.length >= BATCH_SIZE) {
const batch= message.splice(0, BATCH_SIZE);
return batch;
}
return message;
}
const saveToDb = async (messages, database) => {
const eventsA = filterEventsA(messages);
const eventsB = filterEventsB(messages);
const eventsC = filterEventsC(eventsB);
const promises = [];
promises.push(databaseAsync.publish(eventsC));
if (eventBQueue.length < BATCH_SIZE) {
eventBQueue.push.apply(eventBQueue, eventsB);
}
else {
var eventsBBatched = shiftElements(eventBQueue);
promises.push(database.publish(eventsBBatched, EVENTS_TABLE_A));
}
if (eventAQueue.length < BATCH_SIZE) {
eventAQueue.push.apply(eventAQueue, eventsA);
}
else {
var eventsABatched = shiftElements(eventAQueue);
promises.push(database.publish(eventsABatched, EVENTS_TABLE_B));
}
return new Promise((resolve, reject) => {
Promise.all(promises).then(resolve).catch(reject);
});
}
export const process = async (database,
rabbitmq): Promise<void> => {
return new Promise((resolve, _) => {
rabbitmq.consume(async (channel, message: ConsumeMessage) => {
const messages = somefunction(message);
await saveToDb(messages,database)
.then(_ => {
try {
channel.ack(message)
} catch (error) {
}
})
.catch((error) => {
try {
console.error('error');
channel.ack(message)
} catch (error) {
}
});
});
somefunction(resolve)
});
}
Now I want to add some condition in if where no of events < Batch_SIZE to wait for data from rabbitMQ and to save in database when eventAQueue and eventBQueue has adequate size or there is a time limit waiting for this data. But I am not sure how to add it.
I've got a special producer consumer problem in RxJS: The producer slowly produces elements. A consumer is requesting elements and often has to wait for the producer. This can be achieved by zipping the producer and the request stream:
var produce = getProduceStream();
var request = getRequestStream();
var consume = Rx.Observable.zipArray(produce, request).pluck(0);
Sometimes a request gets aborted. A produced element should only consumed after a not aborted request:
produce: -------------p1-------------------------p2--------->
request: --r1--------------r2---------------r3-------------->
abort: ------a(r1)------------------a(?)------------------>
consume: ------------------c(p1, r2)-------------c(p2, r3)-->
The first request r1 would consume the first produced element p1, but r1 gets aborted by a(r1) before it can consume p1. p1 is produced and gets consumed c(p1, r2) on second request r2. The second abort a(?) is ignored, because no unanswered request happened before. The third request r3 has to wait on the next produced element p2 and is not aborted till p2 is produced. Thus, p2 is consumed c(p2, r3) immediately after it got produced.
How can I achieve this in RxJS?
Edit:
I created an example with a QUnit test on jsbin. You can edit the function createConsume(produce, request, abort) to try/test your solution.
The example contains the function definition of the previously accepted answer.
This (core idea minus details) passes your JSBin test:
var consume = request
.zip(abort.merge(produce), (r,x) => [r,x])
.filter(([r,x]) => isNotAbort(x))
.map(([r,p]) => p);
And the JSBin code.
I can't quite wrap my brain around how to do it with existing operators. Here's how to do it with Observable.create():
return Rx.Observable.create(function (observer) {
var rsub = new Rx.SingleAssignmentDisposable();
var asub = new Rx.SingleAssignmentDisposable();
var psub = new Rx.SingleAssignmentDisposable();
var sub = new Rx.CompositeDisposable(rsub, asub, psub);
var rq = [];
var pq = [];
var completeCount = 0;
var complete = function () {
if (++completeCount === 2) {
observer.onCompleted();
}
};
var consume = function () {
if (pq.length && rq.length) {
var p = pq.shift();
var r = rq.shift();
observer.onNext('p' + p);
}
};
rsub.setDisposable(request.subscribe(
function (r) {
rq.push(r);
consume();
},
function (e) { observer.onError(e); },
complete));
asub.setDisposable(abort.subscribe(
function (a) {
rq.shift();
},
function (e) { observer.onError(e); }
));
psub.setDisposable(produce.subscribe(
function (p) {
pq.push(p);
consume();
},
function (e) { observer.onError(e); },
complete));
return sub;
});
http://jsbin.com/zurepesijo/1/
This solution ignores aborts that don't follow an unanswered request:
const {merge} = Rx.Observable;
Rx.Observable.prototype.wrapValue = function(wrapper) {
wrapper = (wrapper || {});
return this.map(function (value) {
wrapper.value = value;
return wrapper;
});
};
function createConsume(produce, request, abort) {
return merge(
produce.wrapValue({type: 'produce'}),
request.wrapValue({type: 'request'}),
abort.wrapValue({type: 'abort'})
)
.scan(
[false, []],
([isRequest, products], e) => {
// if last time the request was answered
if (isRequest && products.length) {
// remove consumed product
products.shift();
// mark request as answered
isRequest = false;
}
if (e.type === 'produce') {
// save product to consume later
products.push(e.value);
} else {
// if evaluated to false, e.type === 'abort'
isRequest = (e.type === 'request');
}
return [isRequest, products];
}
)
.filter( ([isRequest, products]) => (isRequest && products.length) )
.map( ([isRequest, products]) => products[0] ); // consume
}
Code in newest test on JSBin.