Improve WebRTC Stability

Improve the stability of the WebRTC IceCandidate negotiation when the
round trip time to the Rendezvous server is large (> 400ms) which can be
the case when the Rendezvous server is on the other side of the planet!

Change-Id: I7a8883d30696ada2ea13393fde82f1d8f49bbc0c
parent 0eef5a9e
...@@ -1446,6 +1446,8 @@ Blockly.Msg.en.switch_language_to_english = { ...@@ -1446,6 +1446,8 @@ Blockly.Msg.en.switch_language_to_english = {
Blockly.Msg.REPL_YOUR_CODE_IS = "Your code is"; Blockly.Msg.REPL_YOUR_CODE_IS = "Your code is";
Blockly.Msg.REPL_DO_YOU_REALLY_Q = "Do You Really?"; Blockly.Msg.REPL_DO_YOU_REALLY_Q = "Do You Really?";
Blockly.Msg.REPL_FACTORY_RESET = 'This will attempt to reset your Emulator to its "factory" state. If you had previously updated the Companion installed in the Emulator, you will likely have to do this again.'; Blockly.Msg.REPL_FACTORY_RESET = 'This will attempt to reset your Emulator to its "factory" state. If you had previously updated the Companion installed in the Emulator, you will likely have to do this again.';
Blockly.Msg.REPL_WEBRTC_CONNECTION_ERROR = "Companion Connection Error";
// Messages from Blockly.js // Messages from Blockly.js
Blockly.Msg.WARNING_DELETE_X_BLOCKS = "Are you sure you want to delete all %1 of these blocks?"; Blockly.Msg.WARNING_DELETE_X_BLOCKS = "Are you sure you want to delete all %1 of these blocks?";
......
...@@ -334,6 +334,24 @@ Blockly.ReplMgr.putYail = (function() { ...@@ -334,6 +334,24 @@ Blockly.ReplMgr.putYail = (function() {
var offer; var offer;
var key = rs.replcode; var key = rs.replcode;
var haveoffer = false; var haveoffer = false;
var connectionstate = "none";
var webrtcerror = function(doalert, msg) {
webrtcdata = null;
webrtcstarting = false;
webrtcrunning = false;
webrtcforcestop = true;
top.BlocklyPanel_indicateDisconnect();
top.ConnectProgressBar_hide();
if (!webrtcpeer) {
webrtcpeer.close();
}
if (doalert) {
var dialog = new Blockly.Util.Dialog(Blockly.Msg.REPL_NETWORK_ERROR, msg, Blockly.Msg.REPL_OK, false, null, 0,
function() {
dialog.hide();
});
}
};
webrtcisopen = false; webrtcisopen = false;
webrtcforcestop = false; webrtcforcestop = false;
top.ConnectProgressBar_setProgress(20, Blockly.Msg.DIALOG_SECURE_ESTABLISHING); top.ConnectProgressBar_setProgress(20, Blockly.Msg.DIALOG_SECURE_ESTABLISHING);
...@@ -348,11 +366,17 @@ Blockly.ReplMgr.putYail = (function() { ...@@ -348,11 +366,17 @@ Blockly.ReplMgr.putYail = (function() {
var hunk = json[i]; var hunk = json[i];
var candidate = hunk['candidate']; var candidate = hunk['candidate'];
offer = hunk['offer']; offer = hunk['offer'];
if (candidate && haveoffer) { if (candidate && haveoffer && connectionstate != "none") {
var nonce = hunk['nonce']; var nonce = hunk['nonce'];
if (!seennonce[nonce]) { if (!seennonce[nonce]) {
seennonce[nonce] = true; seennonce[nonce] = true;
webrtcpeer.addIceCandidate(candidate); console.log("addIceCandidate: signalingState = " + webrtcpeer.signalingState +
" iceConnectionState = " + webrtcpeer.iceConnectionState);
console.log("addIceCandidate: candidate = " + JSON.stringify(candidate));
webrtcpeer.addIceCandidate(candidate)["catch"](function(e) {
console.error(e);
webrtcerror(true, Bockly.Msg.REPL_WEBRTC_CONNECTION_ERROR + "\n" + e);
});
} else { } else {
console.log("Seen nonce " + nonce); console.log("Seen nonce " + nonce);
} }
...@@ -368,24 +392,24 @@ Blockly.ReplMgr.putYail = (function() { ...@@ -368,24 +392,24 @@ Blockly.ReplMgr.putYail = (function() {
if (!webrtcisopen && !webrtcforcestop) { if (!webrtcisopen && !webrtcforcestop) {
setTimeout(poll, 1000); // Try again in one second setTimeout(poll, 1000); // Try again in one second
} }
} else if (this.readyState == 4) { // Done, but didn't get a 200 back
webrtcerror(true, Blockly.Msg.REPL_WEBRTC_CONNECTION_ERROR + "\n" + "Rendezvous Fail: " + this.status);
} }
}; };
xhr.send(); xhr.send();
}; };
webrtcpeer = new RTCPeerConnection(top.ReplState.iceservers); webrtcpeer = new RTCPeerConnection(top.ReplState.iceservers);
webrtcpeer.oniceconnectionstatechange = function(evt) { webrtcpeer.oniceconnectionstatechange = function(evt) {
console.log("oniceconnectionstatechange: evt.type = " + evt.type); console.log("oniceconnectionstatechange: evt.type = " + evt.type + " connection state = " + this.iceConnectionState);
if (this.iceConnectionState == "disconnected" || connectionstate = this.iceConnectionState;
this.iceConnectionState == "failed") { if (connectionstate == "disconnected" ||
webrtcdata = null; connectionstate == "failed") {
webrtcstarting = false; webrtcerror(true, Blockly.Msg.REPL_WEBRTC_CONNECTION_ERROR + "\n" + "state = " + connectionstate);
webrtcrunning = false;
top.BlocklyPanel_indicateDisconnect();
webrtcpeer.close();
} }
}; };
webrtcpeer.onsignalingstatechange = function(evt) { webrtcpeer.onsignalingstatechange = function(evt) {
console.log("onsignalingstatechange: evt.type = " + evt.type); console.log("onsignalingstatechange: evt.type = " + evt.type);
console.log("onsignalingstatechange: signalingstate = " + this.signalingState);
}; };
webrtcpeer.onicecandidate = function(evt) { webrtcpeer.onicecandidate = function(evt) {
if (evt.type == 'icecandidate') { if (evt.type == 'icecandidate') {
......
...@@ -11,6 +11,7 @@ import android.util.Log; ...@@ -11,6 +11,7 @@ import android.util.Log;
import com.google.appinventor.components.common.YaVersion; import com.google.appinventor.components.common.YaVersion;
import com.google.appinventor.components.runtime.ReplForm; import com.google.appinventor.components.runtime.ReplForm;
import com.google.appinventor.components.runtime.util.AsynchUtil;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
...@@ -75,6 +76,7 @@ public class WebRTCNativeMgr { ...@@ -75,6 +76,7 @@ public class WebRTCNativeMgr {
private boolean haveOffer = false; private boolean haveOffer = false;
private String rCode; private String rCode;
private volatile boolean keepPolling = true; private volatile boolean keepPolling = true;
private volatile boolean haveLocalDescription = false;
private boolean first = true; // This is used for logging in the Rendezvous server private boolean first = true; // This is used for logging in the Rendezvous server
private Random random = new Random(); private Random random = new Random();
private DataChannel dataChannel = null; private DataChannel dataChannel = null;
...@@ -86,7 +88,11 @@ public class WebRTCNativeMgr { ...@@ -86,7 +88,11 @@ public class WebRTCNativeMgr {
/* Callback that handles sdp offer/answers */ /* Callback that handles sdp offer/answers */
SdpObserver sdpObserver = new SdpObserver() { SdpObserver sdpObserver = new SdpObserver() {
public void onCreateFailure(String str) { public void onCreateFailure(String str) {
if (DEBUG) {
Log.d(LOG_TAG, "onCreateFailure: " + str);
}
} }
public void onCreateSuccess(SessionDescription sessionDescription) { public void onCreateSuccess(SessionDescription sessionDescription) {
...@@ -97,9 +103,16 @@ public class WebRTCNativeMgr { ...@@ -97,9 +103,16 @@ public class WebRTCNativeMgr {
} }
DataChannel.Init init = new DataChannel.Init(); DataChannel.Init init = new DataChannel.Init();
if (sessionDescription.type == SessionDescription.Type.OFFER) { if (sessionDescription.type == SessionDescription.Type.OFFER) {
if (DEBUG) {
Log.d(LOG_TAG, "Got offer, about to set remote description (again?)");
}
peerConnection.setRemoteDescription(sdpObserver, sessionDescription); peerConnection.setRemoteDescription(sdpObserver, sessionDescription);
} else if (sessionDescription.type == SessionDescription.Type.ANSWER) { } else if (sessionDescription.type == SessionDescription.Type.ANSWER) {
if (DEBUG) {
Log.d(LOG_TAG, "onCreateSuccess: type = ANSWER");
}
peerConnection.setLocalDescription(sdpObserver, sessionDescription); peerConnection.setLocalDescription(sdpObserver, sessionDescription);
haveLocalDescription = true;
/* Send to peer */ /* Send to peer */
JSONObject offer = new JSONObject(); JSONObject offer = new JSONObject();
offer.put("type", "answer"); offer.put("type", "answer");
...@@ -254,7 +267,7 @@ public class WebRTCNativeMgr { ...@@ -254,7 +267,7 @@ public class WebRTCNativeMgr {
/* Create the peer connection using the iceServers we received in the constructor */ /* Create the peer connection using the iceServers we received in the constructor */
peerConnection = factory.createPeerConnection(iceServers, new MediaConstraints(), peerConnection = factory.createPeerConnection(iceServers, new MediaConstraints(),
observer); observer);
timer.scheduleAtFixedRate(new TimerTask() { timer.schedule(new TimerTask() {
@Override @Override
public void run() { public void run() {
Poller(); Poller();
...@@ -343,6 +356,9 @@ public class WebRTCNativeMgr { ...@@ -343,6 +356,9 @@ public class WebRTCNativeMgr {
Log.d(LOG_TAG, "type = " + type); Log.d(LOG_TAG, "type = " + type);
Log.d(LOG_TAG, "About to set remote offer"); Log.d(LOG_TAG, "About to set remote offer");
} }
if (DEBUG) {
Log.d(LOG_TAG, "Got offer, about to set remote description (maincode)");
}
peerConnection.setRemoteDescription(sdpObserver, peerConnection.setRemoteDescription(sdpObserver,
new SessionDescription(SessionDescription.Type.OFFER, sdp)); new SessionDescription(SessionDescription.Type.OFFER, sdp));
peerConnection.createAnswer(sdpObserver, new MediaConstraints()); peerConnection.createAnswer(sdpObserver, new MediaConstraints());
...@@ -351,11 +367,29 @@ public class WebRTCNativeMgr { ...@@ -351,11 +367,29 @@ public class WebRTCNativeMgr {
} }
i = -1; i = -1;
} else if (element.has("nonce")) { } else if (element.has("nonce")) {
if (element.isNull("candidate")) { if (!haveLocalDescription) {
if (DEBUG) { if (DEBUG) {
Log.d(LOG_TAG, "Received a null candidate, skipping..."); Log.d(LOG_TAG, "Incoming candidate before local description set, punting");
} }
return;
}
if (element.has("offer")) { // Only take in the offer once!
i++; i++;
if (DEBUG) {
Log.d(LOG_TAG, "skipping offer, already processed");
}
continue;
}
if (element.isNull("candidate")) {
i++;
String nonce = element.optString("nonce");
if (!seenNonces.contains(nonce)) {
if (DEBUG) {
Log.d(LOG_TAG, "Received a null candidate, done?");
}
peerConnection.addIceCandidate(new IceCandidate("", 0, ""));
seenNonces.add(nonce);
}
continue; continue;
} }
String nonce = element.optString("nonce"); String nonce = element.optString("nonce");
...@@ -363,13 +397,11 @@ public class WebRTCNativeMgr { ...@@ -363,13 +397,11 @@ public class WebRTCNativeMgr {
String sdpcandidate = candidate.optString("candidate"); String sdpcandidate = candidate.optString("candidate");
String sdpMid = candidate.optString("sdpMid"); String sdpMid = candidate.optString("sdpMid");
int sdpMLineIndex = candidate.optInt("sdpMLineIndex"); int sdpMLineIndex = candidate.optInt("sdpMLineIndex");
if (DEBUG) {
Log.d(LOG_TAG, "candidate = " + sdpcandidate);
}
if (!seenNonces.contains(nonce)) { if (!seenNonces.contains(nonce)) {
seenNonces.add(nonce); seenNonces.add(nonce);
if (DEBUG) { if (DEBUG) {
Log.d(LOG_TAG, "new nonce, about to add candidate!"); Log.d(LOG_TAG, "new nonce, about to add candidate!");
Log.d(LOG_TAG, "candidate = " + sdpcandidate);
} }
IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, sdpcandidate); IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, sdpcandidate);
peerConnection.addIceCandidate(iceCandidate); peerConnection.addIceCandidate(iceCandidate);
...@@ -392,29 +424,34 @@ public class WebRTCNativeMgr { ...@@ -392,29 +424,34 @@ public class WebRTCNativeMgr {
} }
} }
private void sendRendezvous(JSONObject data) { private void sendRendezvous(final JSONObject data) {
try { AsynchUtil.runAsynchronously(new Runnable() {
data.put("first", first); @Override
data.put("webrtc", true); public void run() {
data.put("key", rCode + "-r"); try {
if (first) { data.put("first", first);
first = false; data.put("webrtc", true);
data.put("apiversion", SdkLevel.getLevel()); data.put("key", rCode + "-r");
} if (first) {
HttpClient client = new DefaultHttpClient(); first = false;
HttpPost post = new HttpPost("http://" + rendezvousServer2 + "/rendezvous2/"); data.put("apiversion", SdkLevel.getLevel());
try { }
if (DEBUG) { HttpClient client = new DefaultHttpClient();
Log.d(LOG_TAG, "About to send = " + data.toString()); HttpPost post = new HttpPost("http://" + rendezvousServer2 + "/rendezvous2/");
try {
if (DEBUG) {
Log.d(LOG_TAG, "About to send = " + data.toString());
}
post.setEntity(new StringEntity(data.toString()));
client.execute(post);
} catch (IOException e) {
Log.e(LOG_TAG, "sendRedezvous IOException", e);
}
} catch (Exception e) {
Log.e(LOG_TAG, "Exception in sendRendezvous", e);
}
} }
post.setEntity(new StringEntity(data.toString())); });
client.execute(post);
} catch (IOException e) {
Log.e(LOG_TAG, "sendRedezvous IOException", e);
}
} catch (Exception e) {
Log.e(LOG_TAG, "Exception in sendRendezvous", e);
}
} }
public void send(String output) { public void send(String output) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment