1 /* 2 * Copyright 2014 The WebRTC Project Authors. All rights reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 package org.appspot.apprtc; 12 13 import android.util.Log; 14 import java.io.IOException; 15 import java.io.InputStream; 16 import java.net.HttpURLConnection; 17 import java.net.URL; 18 import java.util.ArrayList; 19 import java.util.Scanner; 20 import java.util.List; 21 import org.appspot.apprtc.AppRTCClient.SignalingParameters; 22 import org.appspot.apprtc.util.AsyncHttpURLConnection; 23 import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents; 24 import org.json.JSONArray; 25 import org.json.JSONException; 26 import org.json.JSONObject; 27 import org.webrtc.IceCandidate; 28 import org.webrtc.PeerConnection; 29 import org.webrtc.SessionDescription; 30 31 /** 32 * AsyncTask that converts an AppRTC room URL into the set of signaling 33 * parameters to use with that room. 34 */ 35 public class RoomParametersFetcher { 36 private static final String TAG = "RoomRTCClient"; 37 private static final int TURN_HTTP_TIMEOUT_MS = 5000; 38 private final RoomParametersFetcherEvents events; 39 private final String roomUrl; 40 private final String roomMessage; 41 42 /** 43 * Room parameters fetcher callbacks. 44 */ 45 public interface RoomParametersFetcherEvents { 46 /** 47 * Callback fired once the room's signaling parameters 48 * SignalingParameters are extracted. 49 */ onSignalingParametersReady(final SignalingParameters params)50 void onSignalingParametersReady(final SignalingParameters params); 51 52 /** 53 * Callback for room parameters extraction error. 54 */ onSignalingParametersError(final String description)55 void onSignalingParametersError(final String description); 56 } 57 RoomParametersFetcher( String roomUrl, String roomMessage, final RoomParametersFetcherEvents events)58 public RoomParametersFetcher( 59 String roomUrl, String roomMessage, final RoomParametersFetcherEvents events) { 60 this.roomUrl = roomUrl; 61 this.roomMessage = roomMessage; 62 this.events = events; 63 } 64 makeRequest()65 public void makeRequest() { 66 Log.d(TAG, "Connecting to room: " + roomUrl); 67 AsyncHttpURLConnection httpConnection = 68 new AsyncHttpURLConnection("POST", roomUrl, roomMessage, new AsyncHttpEvents() { 69 @Override 70 public void onHttpError(String errorMessage) { 71 Log.e(TAG, "Room connection error: " + errorMessage); 72 events.onSignalingParametersError(errorMessage); 73 } 74 75 @Override 76 public void onHttpComplete(String response) { 77 roomHttpResponseParse(response); 78 } 79 }); 80 httpConnection.send(); 81 } 82 roomHttpResponseParse(String response)83 private void roomHttpResponseParse(String response) { 84 Log.d(TAG, "Room response: " + response); 85 try { 86 List<IceCandidate> iceCandidates = null; 87 SessionDescription offerSdp = null; 88 JSONObject roomJson = new JSONObject(response); 89 90 String result = roomJson.getString("result"); 91 if (!result.equals("SUCCESS")) { 92 events.onSignalingParametersError("Room response error: " + result); 93 return; 94 } 95 response = roomJson.getString("params"); 96 roomJson = new JSONObject(response); 97 String roomId = roomJson.getString("room_id"); 98 String clientId = roomJson.getString("client_id"); 99 String wssUrl = roomJson.getString("wss_url"); 100 String wssPostUrl = roomJson.getString("wss_post_url"); 101 boolean initiator = (roomJson.getBoolean("is_initiator")); 102 if (!initiator) { 103 iceCandidates = new ArrayList<>(); 104 String messagesString = roomJson.getString("messages"); 105 JSONArray messages = new JSONArray(messagesString); 106 for (int i = 0; i < messages.length(); ++i) { 107 String messageString = messages.getString(i); 108 JSONObject message = new JSONObject(messageString); 109 String messageType = message.getString("type"); 110 Log.d(TAG, "GAE->C #" + i + " : " + messageString); 111 if (messageType.equals("offer")) { 112 offerSdp = new SessionDescription( 113 SessionDescription.Type.fromCanonicalForm(messageType), message.getString("sdp")); 114 } else if (messageType.equals("candidate")) { 115 IceCandidate candidate = new IceCandidate( 116 message.getString("id"), message.getInt("label"), message.getString("candidate")); 117 iceCandidates.add(candidate); 118 } else { 119 Log.e(TAG, "Unknown message: " + messageString); 120 } 121 } 122 } 123 Log.d(TAG, "RoomId: " + roomId + ". ClientId: " + clientId); 124 Log.d(TAG, "Initiator: " + initiator); 125 Log.d(TAG, "WSS url: " + wssUrl); 126 Log.d(TAG, "WSS POST url: " + wssPostUrl); 127 128 List<PeerConnection.IceServer> iceServers = 129 iceServersFromPCConfigJSON(roomJson.getString("pc_config")); 130 boolean isTurnPresent = false; 131 for (PeerConnection.IceServer server : iceServers) { 132 Log.d(TAG, "IceServer: " + server); 133 for (String uri : server.urls) { 134 if (uri.startsWith("turn:")) { 135 isTurnPresent = true; 136 break; 137 } 138 } 139 } 140 // Request TURN servers. 141 if (!isTurnPresent && !roomJson.optString("ice_server_url").isEmpty()) { 142 List<PeerConnection.IceServer> turnServers = 143 requestTurnServers(roomJson.getString("ice_server_url")); 144 for (PeerConnection.IceServer turnServer : turnServers) { 145 Log.d(TAG, "TurnServer: " + turnServer); 146 iceServers.add(turnServer); 147 } 148 } 149 150 SignalingParameters params = new SignalingParameters( 151 iceServers, initiator, clientId, wssUrl, wssPostUrl, offerSdp, iceCandidates); 152 events.onSignalingParametersReady(params); 153 } catch (JSONException e) { 154 events.onSignalingParametersError("Room JSON parsing error: " + e.toString()); 155 } catch (IOException e) { 156 events.onSignalingParametersError("Room IO error: " + e.toString()); 157 } 158 } 159 160 // Requests & returns a TURN ICE Server based on a request URL. Must be run 161 // off the main thread! requestTurnServers(String url)162 private List<PeerConnection.IceServer> requestTurnServers(String url) 163 throws IOException, JSONException { 164 List<PeerConnection.IceServer> turnServers = new ArrayList<>(); 165 Log.d(TAG, "Request TURN from: " + url); 166 HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); 167 connection.setDoOutput(true); 168 connection.setRequestProperty("REFERER", "https://appr.tc"); 169 connection.setConnectTimeout(TURN_HTTP_TIMEOUT_MS); 170 connection.setReadTimeout(TURN_HTTP_TIMEOUT_MS); 171 int responseCode = connection.getResponseCode(); 172 if (responseCode != 200) { 173 throw new IOException("Non-200 response when requesting TURN server from " + url + " : " 174 + connection.getHeaderField(null)); 175 } 176 InputStream responseStream = connection.getInputStream(); 177 String response = drainStream(responseStream); 178 connection.disconnect(); 179 Log.d(TAG, "TURN response: " + response); 180 JSONObject responseJSON = new JSONObject(response); 181 JSONArray iceServers = responseJSON.getJSONArray("iceServers"); 182 for (int i = 0; i < iceServers.length(); ++i) { 183 JSONObject server = iceServers.getJSONObject(i); 184 JSONArray turnUrls = server.getJSONArray("urls"); 185 String username = server.has("username") ? server.getString("username") : ""; 186 String credential = server.has("credential") ? server.getString("credential") : ""; 187 for (int j = 0; j < turnUrls.length(); j++) { 188 String turnUrl = turnUrls.getString(j); 189 PeerConnection.IceServer turnServer = 190 PeerConnection.IceServer.builder(turnUrl) 191 .setUsername(username) 192 .setPassword(credential) 193 .createIceServer(); 194 turnServers.add(turnServer); 195 } 196 } 197 return turnServers; 198 } 199 200 // Return the list of ICE servers described by a WebRTCPeerConnection 201 // configuration string. iceServersFromPCConfigJSON(String pcConfig)202 private List<PeerConnection.IceServer> iceServersFromPCConfigJSON(String pcConfig) 203 throws JSONException { 204 JSONObject json = new JSONObject(pcConfig); 205 JSONArray servers = json.getJSONArray("iceServers"); 206 List<PeerConnection.IceServer> ret = new ArrayList<>(); 207 for (int i = 0; i < servers.length(); ++i) { 208 JSONObject server = servers.getJSONObject(i); 209 String url = server.getString("urls"); 210 String credential = server.has("credential") ? server.getString("credential") : ""; 211 PeerConnection.IceServer turnServer = 212 PeerConnection.IceServer.builder(url) 213 .setPassword(credential) 214 .createIceServer(); 215 ret.add(turnServer); 216 } 217 return ret; 218 } 219 220 // Return the contents of an InputStream as a String. drainStream(InputStream in)221 private static String drainStream(InputStream in) { 222 Scanner s = new Scanner(in, "UTF-8").useDelimiter("\\A"); 223 return s.hasNext() ? s.next() : ""; 224 } 225 } 226