package org.communiquons.signalexchangerclient; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import org.json.JSONException; import org.json.JSONObject; import org.webrtc.IceCandidate; import org.webrtc.SessionDescription; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; /** * Signal exchanger client * * @author Pierre HUBERT */ public class SignalExchangerClient extends WebSocketListener { /** * Debug log */ private static final String TAG = SignalExchangerClient.class.getSimpleName(); /** * Instance configuration */ private SignalExchangerInitConfig mConfig; /** * Signal exchanger callback */ @Nullable private SignalExchangerCallback mCallback; /** * Http Client */ private OkHttpClient mClient; /** * Current WebSocket connection */ private WebSocket mWebSocket; /** * Initialize a SignalExchanger client * * @param config Configuration of the client * @param cb Callback function to call when we got information update */ public SignalExchangerClient(@NonNull SignalExchangerInitConfig config, @Nullable SignalExchangerCallback cb){ //Save configuration this.mConfig = config; this.mCallback = cb; //Connect to the WebSocket String url = (config.isSecure() ? "wss" : "ws") + "://" + config.getDomain() + ":" + config.getPort() + "/socket"; mClient = new OkHttpClient(); Request request = new Request.Builder().url(url).build(); mWebSocket = mClient.newWebSocket(request, this); } /** * Close this socket */ public void close(){ if(mWebSocket != null) mWebSocket.close(4999, null); } /** * Get current client configuration * * @return Configuration of the client */ public SignalExchangerInitConfig getConfig() { return mConfig; } /** * Set the callback to use on new updates * * @param mCallback Callback to use */ public void setCallback(@Nullable SignalExchangerCallback mCallback) { this.mCallback = mCallback; } /** * Check out whether the current client is connected to a server or not * * @return true if the client is connected to a server / false else */ public boolean isConnected(){ return mWebSocket != null; } /** * Send ready message to a client * * @param target_client_id The ID of the target client */ public void sendReadyMessage(String target_client_id){ sendData(new ClientRequest() .addBoolean("ready_msg", true) .addString("target_id", target_client_id)); } /** * Send a signal to a target * * @param target_id The ID of the target * @param signal The signal to send */ public void sendSignal(String target_id, String signal){ sendData(new ClientRequest() .addString("target_id", target_id) .addString("signal", signal)); } /** * Send a session description to a target * * @param target_id The ID of the target * @param description The description */ public void sendSessionDescription(String target_id, SessionDescription description){ try { JSONObject object = new JSONObject(); object.put("type", description.type.canonicalForm()); object.put("sdp", description.description); sendSignal(target_id, object.toString()); } catch (JSONException e) { e.printStackTrace(); } } /** * Send an Ice Candidate to a remote peer * * @param target_id The ID of the target * @param candidate The candidate to send */ public void sendIceCandidate(String target_id, IceCandidate candidate){ try { JSONObject candidateObj = new JSONObject(); candidateObj.put("sdpMid", candidate.sdpMid); candidateObj.put("sdpMLineIndex", candidate.sdpMLineIndex); candidateObj.put("candidate", candidate.sdp); JSONObject object = new JSONObject(); object.put("candidate", candidateObj); sendSignal(target_id, object.toString()); } catch (JSONException e) { e.printStackTrace(); } } /** * Send data to the server * * @param request The data to send to the server */ private void sendData(@NonNull ClientRequest request){ //Continues only in case of active connection if(!isConnected()) { return; } //Send data to the server Log.v(TAG, "Sending " + request.get().toString()); mWebSocket.send(request.get().toString()); } /** * Invoked when a web socket has been accepted by the remote peer and may begin transmitting * messages. */ public void onOpen(WebSocket webSocket, Response response) { //Save WebSocket object this.mWebSocket = webSocket; //Send the ID of current client to the server sendData(new ClientRequest() .addString("client_id", mConfig.getClientID())); //Inform we are connected if(mCallback != null) mCallback.onConnectedToSignalingServer(); } /** Invoked when a text (type {@code 0x1}) message has been received. */ @Override public void onMessage(WebSocket webSocket, String text) { Log.v(TAG, "Received new message from server: " + text); //Decode message try { JSONObject message = new JSONObject(text); //Ready message callback if(message.has("ready_message_sent")){ if(mCallback != null) mCallback.onReadyMessageCallback( message.getString("target_id"), message.getInt("number_of_targets") ); } //Ready message else if(message.has("ready_msg")){ if(mCallback != null) mCallback.onReadyMessage( message.getString("source_id") ); } //Signal else if(message.has("signal")) { if(mCallback != null) mCallback.onSignal( message.getString("source_id"), message.getString("signal") ); processReceivedSignal(message.getString("source_id"), message.getString("signal")); } //Send signal callback else if(message.has("signal_sent")){ if(mCallback != null) mCallback.onSendSignalCallback( message.getInt("number_of_targets") ); } //Success message else if(message.has("success")) Log.v(TAG, "Success: " + message.getString("success")); //Unrecognized message else Log.e(TAG, "Message from server not understood!"); } catch (JSONException e) { e.printStackTrace(); if(mCallback != null) mCallback.onSignalServerError("Could not parse response from server!", e); } } /** * Process a received signal * * @param source_id The source of the signal * @param signal The signal to process */ private void processReceivedSignal(String source_id, String signal) throws JSONException { JSONObject object = new JSONObject(signal); //Ice candidate if(object.has("candidate")) { JSONObject candidate = object.getJSONObject("candidate"); if (mCallback != null) mCallback.gotRemoteIceCandidate( source_id, new IceCandidate( candidate.getString("sdpMid"), candidate.getInt("sdpMLineIndex"), candidate.getString("candidate") ) ); } //Sdp signal else if(object.has("sdp") && object.has("type")){ SessionDescription.Type type = SessionDescription.Type.fromCanonicalForm( object.getString("type")); String sdp = object.getString("sdp"); if(mCallback != null) mCallback.gotRemoteSessionDescription(source_id, new SessionDescription(type, sdp)); } else Log.e(TAG, "Could not understand received signal!"); } /** * Invoked when both peers have indicated that no more messages will be transmitted and the * connection has been successfully released. No further calls to this listener will be made. */ public void onClosed(WebSocket webSocket, int code, String reason) { mWebSocket = null; } /** * Invoked when a web socket has been closed due to an error reading from or writing to the * network. Both outgoing and incoming messages may have been lost. No further calls to this * listener will be made. */ public void onFailure(WebSocket webSocket, Throwable t, @Nullable Response response) { if(mCallback != null) mCallback.onSignalServerError(t.getMessage(), t); } }