WebRTC: Unable to successfully complete signalling process using DataChannel - javascript

I've been having trouble establishing a WebRTC session and am trying to simplify the issue as much as possible. So I've written up a simple copy & paste example, where you just paste the offer/answer into webforms and click submit.
The HTML+JS, all in one file, can be found here: http://pastebin.com/Ktmb3mVf
I'm on a local network, and am therefore removing the ICE server initialisation process to make this example as bare-bones as possible.
Here are the steps I'm carrying out in the example:
Page 1
Page 1 (loads page), enters a channel name (e.g. test) and clicks create.
A new Host object is created, new PeerConnection() and createDataChannel are called.
createOffer is called, and the resulting offerSDP is pasted into the offer textarea.
Page 2
Copy offerSDP from Page 1 and paste into offer textarea on Page 2, click join.
New Guest object is created, PeerConnection and an ondatachannel handler is set.
setRemoteDescription is called for the Guest object, with the offerSDP data.
createAnswer is called and the result is pasted into the answer textarea box.
Page 1
The answerSDP is copied from Page 2 and pasted into the answer textarea of Page 1, submit answer is clicked.
Host.setRemoteDescription is called with the answerSDP data. This creates a SessionDescription, then peer.setRemoteDescription is called with the resulting data.
Those are the steps carried out in the example, but it seems I'm missing something critical. After the offerer's remoteDescription is set with the answerSDP, I try to send a test message on the dataChannel:
Chrome 40
"-- complete"
> host.dataChannel.send('hello world');
VM1387:2 Uncaught DOMException: Failed to execute 'send' on 'RTCDataChannel': RTCDataChannel.readyState is not 'open'
Firefox 35
"-- complete"
ICE failed, see about:webrtc for more details
> host.dataChannel.send('hello world');
InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable
I also had a more complicated demo operating, with a WebSocket signalling server, and ICE candidates listed, but was getting the same error. So I hope this simplification can help to track down the issue.
Again, the single-file code link: http://pastebin.com/Ktmb3mVf

To enable webRTC clients to connect to each other, you need ICE. While STUN and TURN, which you don't need for such a test, are part of that, even without these helpers you still need to use ICE to tell the other end which IP/port/protocol to connect to.
There are two ways to do this: Google's "trickle ice", where the SDP (answer/offer) is passed on without any ICE candidates. These are then transported over a separate signaling layer and added as they are discovered. This speeds up the connection process, as ICE takes time and some late ICE candidates might not be needed.
The classic method is to wait until all ICE candidates have been gathered, and then generate the SDP with these already included.
I have modified your latest version to do that: http://pastebin.com/g2YVvrRd
You also need to wait for the datachannel/connection to become available before being able to use it, so I've moved the sending of the message to the channels onopen event.
The significant changes to the original code:
The interface callbacks were removed from Host.prototype.createOffer and Guest.prototype.createAnswer, instead we attach the provided callback function to the respective objects for later use.
self.cb = cb;
Both Host and Guest have an added ICE handler for the PeerConnection:
var self = this;
this.peer.onicecandidate = function (event) {
// This event is called for every discovered ICE candidate.
// If this was trickle ICE, you'd pass them on here.
// An event without an actual candidate signals the end of the
// ICE collection process, which is what we need for classic ICE.
if (!event.candidate) {
// We fetch the up to date description from the PeerConnection
// It now contains lines with the available ICE candidates
self.offer = self.peer.localDescription;
// Now we move on to the deferred callback function
self.cb(self.offer);
}
}
For the guest self.offer becomes self.answer
The interface handler $("#submitAnswer").click() does not send the message anymore, instead it is send when the datachannel is ready in the onopen event defined in setChannelEvents().
channel.onopen = function () {
console.log('** channel.onopen');
channel.send('hello world!');
};

Related

How to enable sync manager in electron?

I am building an offline cross-platform using electron. It uses a service worker to make the app offline first. When I try to register the sync manager through following command:-
swRegistration.sync.register('myFirstSync')
Expected behavior
On the page with a service worker registered, this snippet should produce no errors.
navigator.serviceWorker.ready.then(function(swRegistration) {
return swRegistration.sync.register('myFirstSync');
});
Actual behavior
When running with an electron, I get
Uncaught (in promise) DOMException: Background Sync is disabled.
I need to enable sync manager. Any idea of how it can be done?
According to developers.google.com
You just have to follow this article, which is on their website and you can see it on the site by clicking here.
This is the article:
How to request a background sync
In true extensible web style, this is a low level feature that gives you the freedom to do what you need. You ask for an event to be fired when the user has connectivity, which is immediate if the user already has connectivity. Then, you listen for that event and do whatever you need to do.
Like push messaging, it uses a service worker as the event target, which enables it to work when the page isn’t open. To begin, register for a sync from a page:
//register your service worker:
navigator.serviceWorker.register('/sw.js')
//then, later, request a one-off sync:
navigator.serviceWorker.ready.then(function(swRegistration) {
return swRegistration.sync.register('myFirstSync');
});
Then listen for the event in /sw.js:
self.addEventListener('sync', function(event) {
if (event.tag == 'myFirstSync') {
event.waitUntil(doSomeStuff());
}
});
And that's it! In the above, doSomeStuff() should return a promise indicating the success/failure of whatever it’s trying to do. If it fulfills, the sync is complete. If it fails, another sync will be scheduled to retry. Retry syncs also wait for connectivity, and employ an exponential back-off.
The tag name of the sync ('myFirstSync' in the above example) should be unique for a given sync. If you register for a sync using the same tag as a pending sync, it coalesces with the existing sync. That means you can register for an "clear-outbox" sync every time the user sends a message, but if they send 5 messages while offline, you'll only get one sync when they become online. If you want 5 separate sync events, just use unique tags!

Firebase real-time efficiency question: How to set up listeners for a chat app

I have a chat app using Firebase as a realtime database and React Native. I'm trying to figure out the most efficient way to set up the listener for chat messages from Firebase in terms of minimizing read operations and transferring data. Here is my data structure:
- messages
- chatId
- messageId
- sentBy
- timestamp
- text
As I see it I have 2 options, either ref.on("child_added) or ref.on("value")
If I use ref.on("child_added"), the advantage is that when a new message is sent then only the newest message is retrieved. The problem though is that when the conversation is loaded the read operation is called for each message in the chat. If a conversation is hundreds of messages long, then that's hundreds of read operations.
The other option is to use ref.on("value"). The problem here is that on every new message added, the entire conversation is resent instead of just the most recent message. The advantage is that when the conversation is loaded, only one read operation is called to transfer the entire conversation to the screen.
I want some combination of the two of these in which when the conversation is loaded, there is one read operation that brings the entire contents of the conversation, AND when a new child node is added (a new message) only that message is transmitted to the listener. How can I achieve this?
firebaser here
There is no difference between the wire traffic for a value listener and child_ listeners on the same location/query. If you check the Network tab of your browser, you can see exactly what is sent retrieved, and you'll see that it's exactly the same between the listener types.
The difference between value and child_* events is purely made client-side to make it easier for you to update the UI. In fact, even when you attach both value and child_* listeners to the same query/location, Firebase will only retrieve the data only once.
The common way to do what you want is to attach both child_* and value listeners to the query/location. Since the value listener is guaranteed to be fired last, you can use that fact to detect when the initial load is done.
Something like:
var chatRef = firebase.database().ref("messages/chatId");
var initialLoadDone = false;
chatRef.on("child_added", (snapshot) => {
if (initialLoadDone) {
...
}
});
chatRef.once("value", (snapshot) => {
snapshot.forEach((messageSnapshot) => {
...
});
initialLoadDone = true;
});
Suggestion: Use Firestore. It maintains a cache of your data and efficiently handles such scenarios.
You can use ref.once('value') to get current nodes only once and then ref.on('child_added') for subsequent additions. More performance notes.
Edit: I believe Firebase Database handles this efficiently by just ref.on('value'). On checking the network tab after adding a new node to my database, I notified the amount of data that got transferred was very low. This might mean that firebase by default caches your previous data. Would recommend you to look at your network tab and take decisions as such or wait from someone from their team show directions.

What about to remove listener once we signal the ice candidate to other peer?

I found the following lines of statements (code) in Pro JavaScript Development while googlin.
// Google chrome often finds multiple candidates, so let's ensure we only
// ever get the first it supplies by removing the event handler once a
// candidate has been found
that.peerConnection.onicecandidate = null;
...
that.peerConnection.addIceCandidate(new IceCandidate(JSON.parse(candidate)))
Is it good practice to avoid icecandidate once we find the candidate?
Nope. Don't do that!
There are different types of ice candidates: host, srflx, prflx, relay.
So, it is not guaranteed that on first negotiation between peers they would be connected. They will try to connect with different routes(don't know exactly what I should say) STUN, TURN servers. With the first attempt of negotiation by using STUN server let's assume both peers are connected successfully. But what if they are not connected? They will try to connect with TURN server.
Thus, if we remove the onicecandidate listener by assigning it to null after we got the ice candidate, then we're not guaranteed to be connected between peers.

Tokbox streamCreated being called same number of times client is called

I'm calling on a client, one-to-one, multiple times during a session and the streamCreated event gets called on the host. When I hang up, I unsubscribe and the client unpublishes. However, when I call on the client again, the streamCreated event gets called twice on the host's side. I call on the client 3, 4, 5, etc. more times and the streamCreated event fires the same number of times as I have called on the client. For example on the 7th time I call the client, streamCreated gets called 7 times! It seems like I'm not really destroying the streams although streamDestroyed gets called.
On the client side, I was desperate enough to try and unpublish with:
clientSession.unpublish(clientPublisher, handleError);
clientPublisher.stream.destroy();
clientPublisher.destroy();
clientPublisher = null;
On the host side, I've also tried to make sure the subscriber was destroyed:
clientSession.unsubscribe(clientSubscriber);
clientSubscriber.destroy();
clientSubscriber = null;
The problem with this is when I open a video monitor with multiple clients and have each client publish without audio. However, I can still hear the client I called on... like their original stream(s) still exists. What am I doing wrong?
Every time I called on the person, I was using:
clientSession.on('streamCreated', function (event) {
clientSubscriber = clientSession.subscribe(event.stream, vid, {
...
So, each time I called on a client, it created a new event handler. To correct the issue, I added the following code when I disconnected from the client.
clientSession.unsubscribe(clientSubscriber);
clientSession.off();
That killed the event handler and everything works properly now.

WebRTC: Can't set local description after creating an answer

I'm currently attempting to create a simple video chat service using WebRTC with Ajax for the signalling method.
As per the recommendation of another Stack Overflow user, in order to make sure I was understanding the flow of a standard WebRTC app properly, I first created a simple WebRTC video chat service in which I printed the created offer or answer and ICE candidates out to the screen, and manually copied and pasted that info into a text area in the other client window to process everything. Upon doing that, I was able to successfully get both videos to pop up.
After getting that to work properly, I decided to try and use Ajax as the signalling method. However, I can't seem to get it to work now.
In my current implementation, every time offer/answer or ICE candidate info is created, I instantly create a new Ajax object, which is used to add that info (after the JSON.stringify method has been executed on it) to a DB table. Both clients are constantly polling that DB table, searching for new info from the other client.
I've been echoing a lot of information out to the console, and as far as I can tell, a valid offer is always sent from one client to another, but upon receiving that offer, successfully setting it as the remote description, and creating an answer, any attempts I make to set the local description of the "answerer" fails.
Is there any particular reason why this might happen? Here's a snippet of my code:
var i,
len;
for (i = 0, len = responseData.length; i < len; i += 1) {
message = JSON.parse(responseData[i]);
if (message.type === 'offer') {
makeAnswer(message);
}
// Code omitted,
}
...
makeAnswer = function (offer) {
pc.setRemoteDescription(new RTCSessionDescription(offer), function () {
pc.createAnswer(function (desc) {
// An answer is always properly generated here.
pc.setLocalDescription(desc, function () {
// This success callback function is never executed.
setPayload(JSON.stringify(pc.localDescription));
}, function () {
// I always end up here.
});
});
});
};
In essence, I loop through any data retrieved from the DB (sometimes there's both an offer and lots of candidate info that's gathered all at once), and if the type property of a message is 'offer', I call the makeAnswer function, and from there, I set the remote description to the received offer, create an answer, and try to set the answer to the local description, but it always fails at that last step.
If anyone can offer any advice as to why this might be happening, I would be very appreciative.
Thank you very much.
Well, I figured out the problem. It turns out that I wasn't encoding the SDP and ICE info before sending it to a PHP script via Ajax. As a result, any plus signs (+) in the SDP/ICE info were being turned into spaces, thus causing the strings to differ between the local and remote clients and not work.
I've always used encodeURIComponent on GET requests with Ajax, but I never knew you had to use that function with POST requests as well. That's good to know.
Anyway, after I started using the encodeURIComponent function with the posted data, and then fixed my logic up a bit so that ICE candidates are never set until after both local and remote descriptions are set, it started working like a charm every time.
That's the good news. The bad news is that everything was working fine on my local host, but as soon as I ported the exact same code over to my web-hosted server, even though the console was reporting that the offer/answer and ICE info were all properly being received and set, the remote video isn't popping up.
Sigh. One more hurdle to cross before I can be done with this.
Anyway, just to let everyone know, the key is to use encodeURIComponent before sending the SDP/ICE info to a server-side script, so that the string received on the other end is exactly the same.

Categories