/* Peer.java Copyright (c) 2015 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.deviceplugin.webrtc.core; import android.content.Context; import android.util.Log; import org.deviceconnect.android.deviceplugin.webrtc.BuildConfig; import org.deviceconnect.android.profile.VideoChatProfileConstants; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.webrtc.EglBase; import org.webrtc.IceCandidate; import org.webrtc.PeerConnectionFactory; import org.webrtc.SessionDescription; import org.webrtc.voiceengine.WebRtcAudioManager; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Connection of WebRTC. * * @author NTT DOCOMO, INC. */ public class Peer { /** * Tag for debugging. */ private static final String TAG = "WebRTC"; /** * Define the parameter of WebRTC. */ private static final String FIELD_TRIAL_AUTOMATIC_RESIZE = "WebRTC-MediaCodecVideoEncoder-AutomaticResize/Enabled/"; /** * Config of a Peer. */ private PeerConfig mConfig; /** * Instance of PeerConnectionFactory. */ private PeerConnectionFactory mFactory; /** * Application context. */ private Context mContext; /** * シグナリングサーバとの通信を行うクラス。 */ private SignalingClient mSignaling; /** * Peer's id. */ private String mPeerId; /** * Peerのイベントを通知するリスナー. */ private PeerEventListener mEventListener; /** * Map that contains the offer message. */ private Map<String, JSONObject> mOfferMap = new ConcurrentHashMap<>(); /** * Map that contains the MediaConnection. */ private Map<String, MediaConnection> mConnections = new ConcurrentHashMap<>(); /** * Constructor. * @param context context * @param config config */ public Peer(final Context context, final PeerConfig config) { if (context == null) { throw new NullPointerException("context is null."); } if (config == null) { throw new NullPointerException("config is null."); } mContext = context; mConfig = config; initPeerConnectionFactory(); } /** * Retrieves a config of Peer. * @return instance of PeerConfig */ public PeerConfig getConfig() { return mConfig; } /** * Retrieves the id of Peer. * @return peer's id */ public String getMyAddressId() { return mPeerId; } /** * Destroy a instance of Peer. */ public synchronized void destroy() { if (BuildConfig.DEBUG) { Log.e(TAG, "@@ Peer::destroy"); } if (mSignaling != null) { mSignaling.destroy(); mSignaling = null; } synchronized (mConnections) { for (MediaConnection conn : mConnections.values()) { conn.close(); } mConnections.clear(); } if (mFactory != null) { mFactory.dispose(); mFactory = null; } } /** * Sets an event listener. * @param listener listener */ public void setPeerEventListener(final PeerEventListener listener) { mEventListener = listener; } /** * Removes an event listener. */ public void removePeerEventListener() { mEventListener = null; } /** * Returns whether the peer has a offer from addressId. * @param addressId id of address * @return {@code true} if this peer has a offer from addressId, {@code false} otherwise. */ public boolean hasOffer(final String addressId) { return mOfferMap.containsKey(addressId); } public void setVideoHwAccelerationOptions(EglBase.Context renderEGLContext) { mFactory.setVideoHwAccelerationOptions(renderEGLContext, renderEGLContext); } /** * Makes a call to the addressId. * @param addressId id of address * @param option options for making a call * @param listener * @return Instance of MediaConnection */ public synchronized MediaConnection call(final String addressId, final PeerOption option, final MediaConnection.OnMediaEventListener listener) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@ Peer::call"); Log.d(TAG, "@@ address: " + addressId); } if (addressId == null) { throw new NullPointerException("address is null."); } if (option == null) { throw new NullPointerException("option is null."); } if (mSignaling.isDisconnectFlag()) { if (BuildConfig.DEBUG) { Log.e(TAG, "@@ websocket had disconnected."); } return null; } else { MediaStream stream = createMediaStream(option); if (stream == null) { Log.e(TAG, "@@ Failed to create a MediaStream."); return null; } MediaConnection conn = createMediaConnection(addressId, option); conn.setOnMediaEventListener(listener); conn.setLocalMediaStream(stream); conn.createOffer(); mConnections.put(addressId, conn); return conn; } } /** * Takes a phone call to the addressId. * @param addressId id of address * @param option options for take a call * @param listener * @return Instance of MediaConnection */ public synchronized MediaConnection answer(final String addressId, final PeerOption option, final MediaConnection.OnMediaEventListener listener) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@ Peer::answer"); Log.d(TAG, "@@ address: " + addressId); } if (addressId == null) { throw new NullPointerException("address is null."); } if (option == null) { throw new NullPointerException("option is null."); } JSONObject json = mOfferMap.remove(addressId); if (json == null) { Log.w(TAG, "@@ do not have an offer."); return null; } if (mSignaling.isDisconnectFlag()) { if (BuildConfig.DEBUG) { Log.e(TAG, "@@ websocket had disconnected."); } return null; } else { MediaStream stream = createMediaStream(option); if (stream == null) { Log.e(TAG, "@@ Failed to create a MediaStream."); return null; } MediaConnection conn = createMediaConnection(addressId, option); conn.setOnMediaEventListener(listener); conn.setLocalMediaStream(stream); conn.createAnswer(json); mConnections.put(addressId, conn); return conn; } } /** * Hang up a call to the address. * @param addressId id of address * @return {@code true} if hang up a call, {@code false} otherwise. */ public synchronized boolean hangup(final String addressId) { if (BuildConfig.DEBUG) { Log.e(TAG, "@@ Peer::hangup"); } synchronized (mConnections) { MediaConnection conn = mConnections.remove(addressId); if (conn != null) { conn.close(); return true; } } return false; } /** * Returns whether this peer has a MediaConnection. * @return {@code true} if this peer has a MediaConnection, {@code false} otherwise. */ public boolean hasConnections() { return !mConnections.isEmpty(); } /** * Returns whether this peer is connected to the server. * @return {@code true} if this peer is connected, {@code false} otherwise. */ public boolean isConnected() { if (mSignaling != null) { return mSignaling.isOpen(); } return false; } /** * Retrieve the list of ids from peer server. * @param callback callback of notify the list */ public void getListPeerList(final OnGetAddressCallback callback) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@ getListPeerList"); } mSignaling.listAllPeers(new SignalingClient.OnAllPeersCallback() { @Override public void onCallback(JSONArray peers) { if (BuildConfig.DEBUG) { Log.e(TAG, "@@ getListPeerList Response: " + peers); } List<Address> addressList = new ArrayList<>(); for (int i = 0; i < peers.length(); i++) { String strValue; try { strValue = peers.getString(i); } catch (JSONException e) { continue; } // if id is myself, not included int the list if (mPeerId.equalsIgnoreCase(strValue)) { continue; } Address address = new Address(); address.setAddressId(strValue); address.setName(strValue); if (mConnections.containsKey(strValue)) { address.setState(VideoChatProfileConstants.State.TALKING); } else if (mOfferMap.containsKey(strValue)) { address.setState(VideoChatProfileConstants.State.INCOMING); } else { address.setState(VideoChatProfileConstants.State.IDLE); } addressList.add(address); } if (callback != null) { callback.onGetAddresses(addressList); } } @Override public void onErrorCallback() { if (callback != null) { callback.onGetAddresses(null); } } }); } /** * Connect to the server that performs signaling. * @param callback callback that notifies the state of connection */ public void connect(final OnConnectCallback callback) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ connect"); } mSignaling = new SignalingClient(mConfig); mSignaling.setOnSignalingCallback(new SignalingClient.OnSignalingCallback() { @Override public void onOpen(final String peerId) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onOpen"); } mPeerId = peerId; if (callback != null) { callback.onConnected(Peer.this); } } @Override public void onClose() { if (BuildConfig.DEBUG) { Log.w(TAG, "@@@ onClose"); } } @Override public void onOffer(final JSONObject json) { String src = json.optString("src"); mOfferMap.put(src, json); if (mEventListener != null) { Address address = getAddress(src); mEventListener.onIncoming(Peer.this, address); } } @Override public void onAnswer(final JSONObject json) { String src = json.optString("src"); MediaConnection mc = getConnection(src); if (mc != null) { mc.handleAnswer(json); } } @Override public void onCandidate(final JSONObject json) { String src = json.optString("src"); MediaConnection mc = getConnection(src); if (mc != null) { mc.handleCandidate(json); } } @Override public void onDisconnect() { if (BuildConfig.DEBUG) { Log.w(TAG, "@@@@ onDisconnect"); } } @Override public void onError(final String message) { if (BuildConfig.DEBUG) { Log.e(TAG, "@@@ Signaling Server Error: " + message); } if (callback != null) { callback.onError(); } } }); } /** * Create a local media stream. * @param option option * @return MediaStream */ public MediaStream createMediaStream(final PeerOption option) { MediaStream stream = new MediaStream(mFactory, option); return stream; } /** * Initializes the PeerConnectionFactory. * @throws RuntimeException occurs if failed to create a PeerConnectionFactory */ private void initPeerConnectionFactory() { if (mFactory == null) { boolean enableAudio = true; boolean enableVideo = true; boolean enableHWCodec = false; if (enableHWCodec) { enableHWCodec = PeerUtil.validateHWCodec(); } if (BuildConfig.DEBUG) { Log.i(TAG, "initPeerConnectionFactory(enableAudio=" + enableAudio + " enableVideo=" + enableVideo + " enableHWCodec=" + enableHWCodec + ")"); } PeerConnectionFactory.initializeFieldTrials(FIELD_TRIAL_AUTOMATIC_RESIZE); WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true); boolean result = PeerConnectionFactory.initializeAndroidGlobals(mContext, enableAudio, enableVideo, enableHWCodec); if (!result) { throw new RuntimeException("PeerConnectionFactory global initialize is failed."); } try { mFactory = new PeerConnectionFactory(); } catch (Exception e) { throw new RuntimeException("PeerConnectionFactory global initialize is failed.", e); } } else { if (BuildConfig.DEBUG) { Log.w(TAG, "PeerConnectionFactory already exists."); } } } /** * Retrieve the connection by address. * @param address address * @return MediaConnection */ private MediaConnection getConnection(final String address) { return mConnections.get(address); } /** * Create a media connection. * @param addressId address * @param option option * @return null if failed to create a MediaConnection. */ private MediaConnection createMediaConnection(final String addressId, final PeerOption option) { try { MediaConnection connection = new MediaConnection(mFactory, option); connection.setPeerId(addressId); connection.setOnConnectionCallback(mConnectionCallback); return connection; } catch (Exception e) { if (BuildConfig.DEBUG) { Log.e(TAG, "Failed to create a media connection.", e); } return null; } } /** * Gets a Address by addressId. * @param addressId if of address * @return Address */ private Address getAddress(final String addressId) { Address address = new Address(); address.setAddressId(addressId); address.setName(addressId); address.setState(VideoChatProfileConstants.State.INCOMING); return address; } /** * Implementation of OnMediaConnectionCallback. */ private MediaConnection.OnMediaConnectionCallback mConnectionCallback = new MediaConnection.OnMediaConnectionCallback() { @Override public void onLocalDescription(final MediaConnection conn, final SessionDescription sdp) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onLocalDescription"); } String type; switch (sdp.type) { case OFFER: type = "offer"; break; case PRANSWER: type = "pranswer"; break; case ANSWER: type = "answer"; break; default: type = ""; break; } try { JSONObject sdpMsg = new JSONObject(); sdpMsg.put("sdp", sdp.description); sdpMsg.put("type", type); JSONObject payload = new JSONObject(); payload.put("sdp", sdpMsg); payload.put("type", conn.getType()); payload.put("label", conn.getConnectionId()); payload.put("connectionId", conn.getConnectionId()); payload.put("reliable", "false"); payload.put("serialization", "binary"); payload.put("browser", "Supported"); JSONObject message = new JSONObject(); message.put("type", type.toUpperCase()); message.put("payload", payload); message.put("dst", conn.getPeerId()); mSignaling.queueMessage(message.toString()); } catch (Exception e) { if (BuildConfig.DEBUG) { Log.e(TAG, "Failed to create a message that send to a signaling server.", e); } } } @Override public void onIceCandidate(final MediaConnection conn, final IceCandidate candidate) { try { JSONObject candidateMsg = new JSONObject(); candidateMsg.put("candidate", candidate.sdp); candidateMsg.put("sdpMLineIndex", candidate.sdpMLineIndex); candidateMsg.put("sdpMid", candidate.sdpMid); JSONObject payload = new JSONObject(); payload.put("candidate", candidateMsg); payload.put("type", "media"); payload.put("connectionId", conn.getConnectionId()); JSONObject message = new JSONObject(); message.put("type", "CANDIDATE"); message.put("payload", payload); message.put("dst", conn.getPeerId()); mSignaling.queueMessage(message.toString()); } catch (Exception e) { if (BuildConfig.DEBUG) { Log.e(TAG, "Failed to create a message that send to a signaling server.", e); } } } @Override public void onClose(final MediaConnection conn) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onClose: " + conn.getPeerId()); } mConnections.remove(conn.getPeerId()); } @Override public void onError(final MediaConnection conn) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onError: " + conn.getPeerId()); } } }; /** * This interface is used to implement {@link Peer} callbacks. */ public interface PeerEventListener { void onIncoming(Peer core, Address address); void onHangup(Peer core, Address address); void onCalling(Peer core, Address address); } /** * This interface is used to implement {@link Peer} callbacks. */ public interface OnConnectCallback { void onConnected(Peer core); void onError(); } /** * This interface is used to implement {@link Peer} callbacks. */ public interface OnGetAddressCallback { void onGetAddresses(List<Address> addresses); } }