/* * libjingle * Copyright 2014, Google Inc. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.appspot.apprtc; import android.os.Handler; import android.os.Looper; import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.util.Scanner; import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents; import org.appspot.apprtc.WebSocketChannelClient.WebSocketChannelEvents; import org.appspot.apprtc.WebSocketChannelClient.WebSocketConnectionState; import org.json.JSONException; import org.json.JSONObject; import org.webrtc.IceCandidate; import org.webrtc.SessionDescription; /** * Negotiates signaling for chatting with apprtc.appspot.com "rooms". * Uses the client<->server specifics of the apprtc AppEngine webapp. * *
To use: create an instance of this object (registering a message handler) and
* call connectToRoom(). Once room connection is established
* onConnectedToRoom() callback with room parameters is invoked.
* Messages to other party (with local Ice candidates and answer SDP) can
* be sent after WebSocket connection is established.
*/
public class WebSocketRTCClient implements AppRTCClient,
RoomParametersFetcherEvents, WebSocketChannelEvents {
private static final String TAG = "WSRTCClient";
private enum ConnectionState {
NEW, CONNECTED, CLOSED, ERROR
};
private enum MessageType {
MESSAGE, BYE
};
private final Handler uiHandler;
private boolean loopback;
private boolean initiator;
private SignalingEvents events;
private WebSocketChannelClient wsClient;
private RoomParametersFetcher fetcher;
private ConnectionState roomState;
private String postMessageUrl;
private String byeMessageUrl;
public WebSocketRTCClient(SignalingEvents events) {
this.events = events;
uiHandler = new Handler(Looper.getMainLooper());
}
// --------------------------------------------------------------------
// RoomConnectionEvents interface implementation.
// All events are called on UI thread.
@Override
public void onSignalingParametersReady(final SignalingParameters params) {
Log.d(TAG, "Room connection completed.");
if (loopback && (!params.initiator || params.offerSdp != null)) {
reportError("Loopback room is busy.");
return;
}
if (!loopback && !params.initiator && params.offerSdp == null) {
Log.w(TAG, "No offer SDP in room response.");
}
initiator = params.initiator;
postMessageUrl = params.roomUrl + "/message/"
+ params.roomId + "/" + params.clientId;
byeMessageUrl = params.roomUrl + "/bye/"
+ params.roomId + "/" + params.clientId;
roomState = ConnectionState.CONNECTED;
// Connect to WebSocket server.
wsClient.connect(params.wssUrl, params.wssPostUrl);
wsClient.setClientParameters(params.roomId, params.clientId);
// Fire connection and signaling parameters events.
events.onConnectedToRoom(params);
if (!params.initiator) {
// For call receiver get sdp offer and ice candidates
// from room parameters.
if (params.offerSdp != null) {
events.onRemoteDescription(params.offerSdp);
}
if (params.iceCandidates != null) {
for (IceCandidate iceCandidate : params.iceCandidates) {
events.onRemoteIceCandidate(iceCandidate);
}
}
}
}
@Override
public void onSignalingParametersError(final String description) {
reportError("Room connection error: " + description);
}
// --------------------------------------------------------------------
// WebSocketChannelEvents interface implementation.
// All events are called on UI thread.
@Override
public void onWebSocketOpen() {
Log.d(TAG, "Websocket connection completed. Registering...");
wsClient.register();
}
@Override
public void onWebSocketMessage(final String msg) {
if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
Log.e(TAG, "Got WebSocket message in non registered state.");
return;
}
try {
JSONObject json = new JSONObject(msg);
String msgText = json.getString("msg");
String errorText = json.optString("error");
if (msgText.length() > 0) {
json = new JSONObject(msgText);
String type = json.optString("type");
if (type.equals("candidate")) {
IceCandidate candidate = new IceCandidate(
json.getString("id"),
json.getInt("label"),
json.getString("candidate"));
events.onRemoteIceCandidate(candidate);
} else if (type.equals("answer")) {
if (initiator) {
SessionDescription sdp = new SessionDescription(
SessionDescription.Type.fromCanonicalForm(type),
json.getString("sdp"));
events.onRemoteDescription(sdp);
} else {
reportError("Received answer for call initiator: " + msg);
}
} else if (type.equals("offer")) {
if (!initiator) {
SessionDescription sdp = new SessionDescription(
SessionDescription.Type.fromCanonicalForm(type),
json.getString("sdp"));
events.onRemoteDescription(sdp);
} else {
reportError("Received offer for call receiver: " + msg);
}
} else if (type.equals("bye")) {
events.onChannelClose();
} else {
reportError("Unexpected WebSocket message: " + msg);
}
} else {
if (errorText != null && errorText.length() > 0) {
reportError("WebSocket error message: " + errorText);
} else {
reportError("Unexpected WebSocket message: " + msg);
}
}
} catch (JSONException e) {
reportError("WebSocket message JSON parsing error: " + e.toString());
}
}
@Override
public void onWebSocketClose() {
events.onChannelClose();
}
@Override
public void onWebSocketError(String description) {
reportError("WebSocket error: " + description);
}
// --------------------------------------------------------------------
// AppRTCClient interface implementation.
// Asynchronously connect to an AppRTC room URL, e.g.
// https://apprtc.appspot.com/register/