Java tutorial
/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2015, Telestax Inc and individual contributors * by the @authors tag. * * This program is free software: you can redistribute it and/or modify * under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * * For questions related to commercial use licensing, please contact sales@telestax.com. * */ /* * 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.mobicents.restcomm.android.client.sdk; import android.Manifest; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.media.AudioManager; import android.os.Handler; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.view.Gravity; import android.view.View; import android.widget.Toast; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.mobicents.restcomm.android.client.sdk.MediaClient.AppRTCAudioManager; import org.mobicents.restcomm.android.client.sdk.MediaClient.PeerConnectionClient; import org.mobicents.restcomm.android.client.sdk.SignalingClient.SignalingParameters; import org.mobicents.restcomm.android.client.sdk.SignalingClient.SignalingClient; import org.mobicents.restcomm.android.client.sdk.MediaClient.util.IceServerFetcher; import org.mobicents.restcomm.android.client.sdk.util.PercentFrameLayout; import org.mobicents.restcomm.android.client.sdk.util.RCLogger; import org.webrtc.EglBase; import org.webrtc.IceCandidate; import org.webrtc.PeerConnection; import org.webrtc.SessionDescription; import org.webrtc.StatsReport; import org.webrtc.SurfaceViewRenderer; import org.webrtc.RendererCommon.ScalingType; import org.webrtc.VideoTrack; /** * RCConnection represents a call. An RCConnection can be either incoming or outgoing. RCConnections are not created by themselves but * as a result on an action on RCDevice. For example to initiate an outgoing connection you call RCDevice.connect() which instantiates * and returns a new RCConnection. On the other hand when an incoming connection arrives the RCDevice delegate is notified with * RCDeviceListener.onIncomingConnection() and passes the new RCConnection object that is used by the delegate to * control the connection. * <p/> * When an incoming connection arrives through RCDeviceListener.onIncomingConnection() it is considered RCConnectionStateConnecting until it is either * accepted with RCConnection.accept() or rejected with RCConnection.reject(). Once the connection is accepted the RCConnection transitions to RCConnectionStateConnected * state. * <p/> * When an outgoing connection is created with RCDevice.connect() it starts with state RCConnectionStatePending. Once it starts ringing on the remote party it * transitions to RCConnectionStateConnecting. When the remote party answers it, the RCConnection state transitions to RCConnectionStateConnected. * <p/> * Once an RCConnection (either incoming or outgoing) is established (i.e. RCConnectionStateConnected) media can start flowing over it. DTMF digits can be sent over to * the remote party using RCConnection.sendDigits(). When done with the RCConnection you can disconnect it with RCConnection.disconnect(). */ public class RCConnection implements PeerConnectionClient.PeerConnectionEvents, IceServerFetcher.IceServerFetcherEvents, SignalingClient.SignalingClientCallListener { /** * Connection State */ public enum ConnectionState { PENDING, /** * An (outgoing) Connection enters this state when RCDevice.connect() is called and until peer starts ringing at which point in enters CONNECTING state (or DISCONNECTED) */ CONNECTING, /** * A Connection enters this state a. when it is outgoing and peer starts ringing, b. when it is incoming and at the point it first arrives at RCDevice listener */ SIGNALING_CONNECTED, /** * A Connection enters this intermediate state when signaling is connected but before media is connected and RCConnection is deemed actually connected */ CONNECTED, /** * A Connection enters this state when actual media starts flowing */ DISCONNECTING, /** Connection is being disconnected. When client App calls RCConnection.disconnect(), RCConnection state transitions to this until we get a response at which point we transition to DISCONNECTED */ DISCONNECTED, /** Connection is in state disconnected */ } public enum ConnectionMediaType { UNDEFINED, /** * We don't know the type of media yet, for example for remote video before they answer */ AUDIO, /** * Connection is audio only */ AUDIO_VIDEO, /** Connection audio & video */ } String IncomingParameterFromKey = "RCConnectionIncomingParameterFromKey"; String IncomingParameterToKey = "RCConnectionIncomingParameterToKey"; String IncomingParameterAccountSIDKey = "RCConnectionIncomingParameterAccountSIDKey"; String IncomingParameterAPIVersionKey = "RCConnectionIncomingParameterAPIVersionKey"; String IncomingParameterCallSIDKey = "RCConnectionIncomingParameterCallSIDKey"; /** * State of the connection. For more info please check ConnectionState */ ConnectionState state; /** * Type of local media transferred over the RCConnection. */ private ConnectionMediaType localMediaType; /** * Type of local media transferred over the RCConnection. */ private ConnectionMediaType remoteMediaType; /** * Direction of the connection. True if connection is incoming; false otherwise */ boolean incoming; /** * Connection parameters (**Not Implemented yet**) */ private HashMap<String, String> parameters; /** * Listener that will be called on RCConnection events described at RCConnectionListener */ private RCConnectionListener listener; /** * Is connection currently muted? If a connection is muted the remote party cannot hear the local party */ private boolean muted; /** * Parameter keys for RCCDevice.connect() and RCConnection.accept() */ public static class ParameterKeys { public static final String CONNECTION_PEER = "username"; public static final String CONNECTION_VIDEO_ENABLED = "video-enabled"; public static final String CONNECTION_LOCAL_VIDEO = "local-video"; public static final String CONNECTION_REMOTE_VIDEO = "remote-video"; public static final String CONNECTION_PREFERRED_VIDEO_CODEC = "preferred-video-codec"; public static final String CONNECTION_CUSTOM_SIP_HEADERS = "sip-headers"; public static final String CONNECTION_CUSTOM_INCOMING_SIP_HEADERS = "sip-headers-incoming"; } // Let's use a builder since RCConnections don't have uniform way to construct static class Builder { // Required parameters private final boolean incoming; private final RCConnection.ConnectionState state; private final RCDevice device; private final SignalingClient signalingClient; private final AppRTCAudioManager audioManager; // Optional parameters - initialized to default values private String jobId = null; private RCConnectionListener listener = null; private String incomingCallSdp = null; private ConnectionMediaType remoteMediaType = ConnectionMediaType.UNDEFINED; public Builder(boolean incoming, RCConnection.ConnectionState state, RCDevice device, SignalingClient signalingClient, AppRTCAudioManager audioManager) { this.incoming = incoming; this.state = state; this.device = device; this.signalingClient = signalingClient; this.audioManager = audioManager; } public Builder jobId(String val) { jobId = val; return this; } public Builder listener(RCConnectionListener val) { listener = val; return this; } public Builder incomingCallSdp(String val) { incomingCallSdp = val; return this; } public Builder remoteMediaType(ConnectionMediaType val) { remoteMediaType = val; return this; } public RCConnection build() { return new RCConnection(this); } } public RCDevice device = null; private String jobId; private SignalingClient signalingClient; private String incomingCallSdp = ""; private EglBase rootEglBase; private boolean localVideoReceived = false; private boolean remoteVideoReceived = false; private SurfaceViewRenderer localRender; private SurfaceViewRenderer remoteRender; private PercentFrameLayout localRenderLayout; private PercentFrameLayout remoteRenderLayout; private PeerConnectionClient peerConnectionClient = null; private SignalingParameters signalingParameters; private AppRTCAudioManager audioManager = null; private ScalingType scalingType; private Toast logToast; private PeerConnectionClient.PeerConnectionParameters peerConnectionParameters; private boolean iceConnected; private HashMap<String, Object> callParams = null; private long callStartedTimeMs = 0; private final boolean DO_TOAST = false; // if a call takes too long to establish this handler is used to emit a time out private Handler timeoutHandler = null; // call times out if it hasn't been established after 15 seconds private final int CALL_TIMEOUT_DURATION_MILIS = 15 * 1000; // Local preview screen position before call is connected. private static final int LOCAL_X_CONNECTING = 0; private static final int LOCAL_Y_CONNECTING = 0; private static final int LOCAL_WIDTH_CONNECTING = 100; private static final int LOCAL_HEIGHT_CONNECTING = 100; // Local preview screen position after call is connected. private static final int LOCAL_X_CONNECTED = 72; private static final int LOCAL_Y_CONNECTED = 2; private static final int LOCAL_WIDTH_CONNECTED = 25; private static final int LOCAL_HEIGHT_CONNECTED = 25; // Remote video screen position private static final int REMOTE_X = 0; private static final int REMOTE_Y = 0; private static final int REMOTE_WIDTH = 100; private static final int REMOTE_HEIGHT = 100; private enum VideoViewState { NONE, LOCAL_VIEW_RECEIVED, REMOTE_VIEW_RECEIVED, ICE_CONNECTED, } // List of 'dangerous' permissions that we need to check (CAMERA is added dynamically only if the local user uses video) private static final String[] MANDATORY_PERMISSIONS = { Manifest.permission.RECORD_AUDIO, Manifest.permission.USE_SIP }; private static final String TAG = "RCConnection"; // Construct RCConnection from Buider private RCConnection(Builder builder) { RCLogger.i(TAG, "RCConnection(Builder)"); if (builder.jobId == null) { // create a unique jobId for the RCConnection, this is used for signaling actions to maintain state jobId = Long.toString(System.currentTimeMillis()); } else { jobId = builder.jobId; } incoming = builder.incoming; state = builder.state; device = builder.device; signalingClient = builder.signalingClient; audioManager = builder.audioManager; listener = builder.listener; incomingCallSdp = builder.incomingCallSdp; incomingCallSdp = builder.incomingCallSdp; if (incomingCallSdp != null) { remoteMediaType = RCConnection.sdp2Mediatype(builder.incomingCallSdp); } audioManager.startCall(); if (incoming) { audioManager.playRingingSound(); } timeoutHandler = new Handler(RCClient.getContext().getMainLooper()); } /** * Initialize a new RCConnection object. <b>Important</b>: this is used internally by RCDevice and is not meant for application use * * @param connectionListener RCConnection listener that will be receiving RCConnection events (@see RCConnectionListener) */ public RCConnection(RCConnectionListener connectionListener) { RCLogger.i(TAG, "RCConnection(RCConnectionListener)"); this.listener = connectionListener; } /** * Retrieves the current state of the connection */ public ConnectionState getState() { return this.state; } /** * Retrieves the current local media type of the connection */ public ConnectionMediaType getLocalMediaType() { return this.localMediaType; } /** * Retrieves the current local media type of the connection */ public ConnectionMediaType getRemoteMediaType() { return this.remoteMediaType; } /** * Retrieves the set of application parameters associated with this connection (<b>Not Implemented yet</b>) * * @return Connection parameters */ public Map<String, String> getParameters() { return parameters; } /** * Returns whether the connection is incoming or outgoing * * @return True if incoming, false otherwise */ public boolean isIncoming() { return this.incoming; } // Make a call using the passed parameters public void open(Map<String, Object> parameters) { setupWebrtcAndCall(parameters); } /** * Accept the incoming connection. Important: if you work with Android API 23 or above you will need to handle dynamic Android permissions in your Activity * as described at https://developer.android.com/training/permissions/requesting.html. More specifically the Restcomm Client SDK needs RECORD_AUDIO, CAMERA (only if the local user * has enabled local video via RCConnection.ParameterKeys.CONNECTION_VIDEO_ENABLED; if not then this permission isn't needed), and USE_SIP permission * to be able to accept() a connection. For an example of such permission handling you can check MainActivity of restcomm-hello world sample App. Notice that if any of these permissions * are missing, the call will fail with a ERROR_CONNECTION_PERMISSION_DENIED error. * * @param parameters Parameters such as whether we want video enabled, etc. Possible keys: <br> * <b>RCConnection.ParameterKeys.CONNECTION_VIDEO_ENABLED</b>: Whether we want WebRTC video enabled or not <br> * <b>RCConnection.ParameterKeys.CONNECTION_LOCAL_VIDEO</b>: View where we want the local video to be rendered <br> * <b>RCConnection.ParameterKeys.CONNECTION_REMOTE_VIDEO</b>: View where we want the remote video to be rendered <br> * <b>RCConnection.ParameterKeys.CONNECTION_PREFERRED_VIDEO_CODEC</b>: Preferred video codec to use. Default is VP8. Possible values: <i>'VP8', 'VP9'</i> <br> * means that RCDevice.state not ready to make a call (this usually means no WiFi available) */ public void accept(Map<String, Object> parameters) { RCLogger.i(TAG, "accept(): " + parameters.toString()); if (!checkPermissions((Boolean) parameters.get(ParameterKeys.CONNECTION_VIDEO_ENABLED))) { return; } if (state == ConnectionState.CONNECTING) { this.callParams = (HashMap<String, Object>) parameters; initializeWebrtc((Boolean) this.callParams.get(ParameterKeys.CONNECTION_VIDEO_ENABLED), (PercentFrameLayout) parameters.get(ParameterKeys.CONNECTION_LOCAL_VIDEO), (PercentFrameLayout) parameters.get(ParameterKeys.CONNECTION_REMOTE_VIDEO), (String) parameters.get(ParameterKeys.CONNECTION_PREFERRED_VIDEO_CODEC)); startTurn(); } else { // let's delay a millisecond to avoid calling code in the App getting intertwined with App listener code new Handler(RCClient.getContext().getMainLooper()).postDelayed(new Runnable() { @Override public void run() { listener.onError(RCConnection.this, RCClient.ErrorCodes.ERROR_CONNECTION_ACCEPT_WRONG_STATE.ordinal(), RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_ACCEPT_WRONG_STATE)); } }, 1); } } /** * Ignore incoming connection (<b>Not Implemented yet</b>) */ public void ignore() { if (state == ConnectionState.CONNECTING) { audioManager.stop(); signalingClient.disconnect(jobId, null); } else { // let's delay a millisecond to avoid calling code in the App getting intertwined with App listener code new Handler(RCClient.getContext().getMainLooper()).postDelayed(new Runnable() { @Override public void run() { listener.onError(RCConnection.this, RCClient.ErrorCodes.ERROR_CONNECTION_IGNORE_WRONG_STATE.ordinal(), RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_IGNORE_WRONG_STATE)); } }, 1); } } /** * Reject incoming connection */ public void reject() { RCLogger.i(TAG, "reject()"); if (state == ConnectionState.CONNECTING) { audioManager.stop(); signalingClient.disconnect(jobId, null); // TODO: (minor) if reject() is called while we are already connected then we will disconnect, but in that // edge case we shouldn't set connection state to DICONNECTED right away // update state right away since rejecting a call is a response to the INVITE, so no further messages will come this.state = ConnectionState.DISCONNECTED; // also update RCDevice state if (RCDevice.state == RCDevice.DeviceState.BUSY) { RCDevice.state = RCDevice.DeviceState.READY; } } else { // let's delay a millisecond to avoid calling code in the App getting intertwined with App listener code new Handler(RCClient.getContext().getMainLooper()).postDelayed(new Runnable() { @Override public void run() { listener.onError(RCConnection.this, RCClient.ErrorCodes.ERROR_CONNECTION_REJECT_WRONG_STATE.ordinal(), RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_REJECT_WRONG_STATE)); } }, 1); } } /** * Disconnect the established connection */ public void disconnect() { RCLogger.i(TAG, "disconnect()"); handleDisconnect(null); } /** * Mute connection so that the other party cannot hear local audio * * @param muted True to mute and false in order to unmute */ public void setAudioMuted(boolean muted) { RCLogger.i(TAG, "setAudioMuted(): " + muted); if (audioManager != null) { audioManager.setMute(muted); } } /** * Retrieve whether connection audio is muted or not * * @return True connection is muted and false otherwise */ public boolean isAudioMuted() { if (audioManager != null) { return audioManager.getMute(); } else { RCLogger.e(TAG, "isMuted called on null audioManager -check memory management"); } return false; } /** * Mute connection so that the other party cannot see local video * * @param muted True to mute and false in order to unmute */ public void setVideoMuted(boolean muted) { RCLogger.i(TAG, "setVideoMuted(): " + muted); if (this.peerConnectionClient != null) { this.peerConnectionClient.setLocalVideoEnabled(!muted); } } /** * Retrieve whether connection video is muted or not * * @return True connection is muted and false otherwise */ public boolean isVideoMuted() { if (this.peerConnectionClient != null) { return !this.peerConnectionClient.getLocalVideoEnabled(); } return false; } /** * Send DTMF digits over the connection * * @param digits A string of DTMF digits to be sent */ public void sendDigits(String digits) { RCLogger.i(TAG, "sendDigits(): " + digits); if (state == ConnectionState.CONNECTED) { signalingClient.sendDigits(this.jobId, digits); } else { // let's delay a millisecond to avoid calling code in the App getting intertwined with App listener code new Handler(RCClient.getContext().getMainLooper()).postDelayed(new Runnable() { @Override public void run() { listener.onError(RCConnection.this, RCClient.ErrorCodes.ERROR_CONNECTION_DTMF_DIGITS_WRONG_STATE.ordinal(), RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_DTMF_DIGITS_WRONG_STATE)); } }, 1); } } /** * Update connection listener to be receiving Connection related events. This is * usually needed when we switch activities and want the new activity to receive * events * * @param listener New connection listener */ public void setConnectionListener(RCConnectionListener listener) { RCLogger.i(TAG, "setConnectionListener()"); this.listener = listener; } // ------ Call-related callbacks received from signaling thread are handled here public void onCallOutgoingPeerRingingEvent(String jobId) { RCLogger.i(TAG, "onCallOutgoingPeerRingingEvent(): jobId: " + jobId); //audioManager.play(R.raw.calling, true); audioManager.playCallingSound(); state = ConnectionState.CONNECTING; listener.onConnecting(this); // Phone state Intents to capture connecting event sendQoSConnectionIntent("connecting"); } public void onCallIncomingConnectedEvent(String jobId) { // no need to do any notifying as the App is notified when ICE is connected RCLogger.i(TAG, "onCallIncomingConnectedEvent(): jobId: " + jobId); state = ConnectionState.SIGNALING_CONNECTED; } public void onCallOutgoingConnectedEvent(String jobId, String sdpAnswer, HashMap<String, String> customHeaders) { RCLogger.i(TAG, "onCallOutgoingConnectedEvent(): jobId: " + jobId + " customHeaders: " + customHeaders); state = ConnectionState.SIGNALING_CONNECTED; if (customHeaders != null) { callParams.put(ParameterKeys.CONNECTION_CUSTOM_INCOMING_SIP_HEADERS, customHeaders); } // we want to notify webrtc onRemoteDescription *only* on an outgoing call if (!this.isIncoming()) { remoteMediaType = sdp2Mediatype(sdpAnswer); onRemoteDescription(sdpAnswer); } sendQoSConnectionIntent("connected"); } public void onCallLocalDisconnectedEvent(String jobId) { RCLogger.i(TAG, "onCallLocalDisconnectedEvent(): jobId: " + jobId); handleDisconnected(jobId, true); } public void onCallIncomingCanceledEvent(String jobId) { RCLogger.i(TAG, "onCallIncomingCanceledEvent(): jobId: " + jobId); handleDisconnected(jobId, false); } public void onCallPeerDisconnectEvent(String jobId) { RCLogger.i(TAG, "onCallPeerDisconnectEvent(): jobId: " + jobId); handleDisconnected(jobId, false); } public void onCallSentDigitsEvent(String jobId, RCClient.ErrorCodes statusCode, String statusText) { RCLogger.i(TAG, "onCallSentDigitsEvent(): jobId: " + jobId + ", status: " + statusCode + ", text: " + statusText); listener.onDigitSent(this, statusCode.ordinal(), statusText); } public void onCallErrorEvent(String jobId, RCClient.ErrorCodes errorCode, String errorText) { RCLogger.e(TAG, "onCallErrorEvent(): jobId: " + jobId + ", error code: " + errorCode + ", error text: " + errorText); // TODO: we need to see if there's a chance a call that causes an error to remain up, // if not we need to avoid disconnecting below if (state != ConnectionState.DISCONNECTING) { // only disconnect signaling facilities if we are not already disconnecting signalingClient.disconnect(jobId, null); } disconnectWebrtc(); if (RCDevice.state == RCDevice.DeviceState.BUSY) { RCDevice.state = RCDevice.DeviceState.READY; } this.state = ConnectionState.DISCONNECTED; device.removeConnection(jobId); if (listener != null) { listener.onDisconnected(this, errorCode.ordinal(), errorText); } } // Common disconnect code for local/remote disconnect and remote cancel // If this is called after we have disconnected locally (i.e. RCConnection.disconnect() was called) we // don't need to media private void handleDisconnected(String jobId, boolean haveDisconnectedLocally) { // IMPORTANT: we 're first notifying listener and then setting new state because we want the listener to be able to // differentiate between disconnect and remote cancel events with the same listener method: onDisconnected. // In the first case listener will see state CONNECTED and in the second CONNECTING if (!isIncoming() && state == ConnectionState.CONNECTING) { // outgoing call is ringing at the peer, and the peer disconnects, need to play busy audioManager.playDeclinedSound(); } else { audioManager.stop(); } //if (inboundDisconnect && RCDevice.state == RCDevice.DeviceState.BUSY) { if (!haveDisconnectedLocally && RCDevice.state == RCDevice.DeviceState.BUSY) { // No need to disconnect signaling, it is already disconnected both when we cause // disconnect and when the remote party does disconnectWebrtc(); } listener.onDisconnected(this); RCDevice.state = RCDevice.DeviceState.READY; this.state = ConnectionState.DISCONNECTED; device.removeConnection(jobId); // Phone state Intents to capture normal disconnect event sendQoSConnectionIntent("disconnected"); } // Handle local disconnect private void handleDisconnect(String reason) { RCLogger.i(TAG, "handleDisconnect(): reason: " + reason); audioManager.stop(); if (state != ConnectionState.DISCONNECTED && state != ConnectionState.DISCONNECTING) { signalingClient.disconnect(jobId, reason); disconnectWebrtc(); state = ConnectionState.DISCONNECTING; // also update RCDevice state. Reason we need that is twofold: a. if a call times out in signaling for a reason it will take around half a minute to // get response from signaling, during which period we won't be able to make a call, b. there are some edge cases where signaling hangs and never times out if (RCDevice.state == RCDevice.DeviceState.BUSY) { RCDevice.state = RCDevice.DeviceState.READY; } } else if (state == ConnectionState.DISCONNECTING) { RCLogger.w(TAG, "disconnect(): Attempting to disconnect while we are in state disconnecting, skipping."); } else { // let's delay a millisecond to avoid calling code in the App getting intertwined with App listener code new Handler(RCClient.getContext().getMainLooper()).postDelayed(new Runnable() { @Override public void run() { listener.onError(RCConnection.this, RCClient.ErrorCodes.ERROR_CONNECTION_DISCONNECT_WRONG_STATE.ordinal(), RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_DISCONNECT_WRONG_STATE)); } }, 1); } } public String getId() { return jobId; } // ------ WebRTC stuff: // IceServerFetcher callbacks @Override public void onIceServersReady(final LinkedList<PeerConnection.IceServer> iceServers) { // Important: need to fire the event in UI context to make sure no races will arise Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { if (!RCConnection.this.incoming) { // we are the initiator // create a new hash map HashMap<String, String> sipHeaders = null; if (RCConnection.this.callParams.containsKey(ParameterKeys.CONNECTION_CUSTOM_SIP_HEADERS)) { sipHeaders = (HashMap<String, String>) RCConnection.this.callParams .get(ParameterKeys.CONNECTION_CUSTOM_SIP_HEADERS); } RCConnection.this.signalingParameters = new SignalingParameters(iceServers, true, "", (String) RCConnection.this.callParams.get(ParameterKeys.CONNECTION_PEER), "", null, null, sipHeaders, (Boolean) RCConnection.this.callParams.get(ParameterKeys.CONNECTION_VIDEO_ENABLED)); } else { // we are not the initiator RCConnection.this.signalingParameters = new SignalingParameters(iceServers, false, "", "", "", null, null, null, (Boolean) RCConnection.this.callParams.get(ParameterKeys.CONNECTION_VIDEO_ENABLED)); SignalingParameters params = SignalingParameters.extractCandidates( new SessionDescription(SessionDescription.Type.OFFER, incomingCallSdp)); RCConnection.this.signalingParameters.offerSdp = params.offerSdp; RCConnection.this.signalingParameters.iceCandidates = params.iceCandidates; } startCall(RCConnection.this.signalingParameters); } }; mainHandler.post(myRunnable); } @Override public void onIceServersError(final String description) { // Important: need to fire the event in UI context cause currently we 're in JAIN SIP thread Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { handleDisconnect(null); if (RCDevice.state == RCDevice.DeviceState.BUSY) { RCDevice.state = RCDevice.DeviceState.READY; } RCConnection.this.listener.onDisconnected(RCConnection.this, RCClient.ErrorCodes.ERROR_CONNECTION_WEBRTC_TURN_ERROR.ordinal(), description); } }; mainHandler.post(myRunnable); } // Outgoing call private void setupWebrtcAndCall(Map<String, Object> parameters) { if (!checkPermissions((Boolean) parameters.get(ParameterKeys.CONNECTION_VIDEO_ENABLED))) { return; } this.callParams = (HashMap<String, Object>) parameters; initializeWebrtc((Boolean) this.callParams.get(ParameterKeys.CONNECTION_VIDEO_ENABLED), (PercentFrameLayout) parameters.get(ParameterKeys.CONNECTION_LOCAL_VIDEO), (PercentFrameLayout) parameters.get(ParameterKeys.CONNECTION_REMOTE_VIDEO), (String) parameters.get(ParameterKeys.CONNECTION_PREFERRED_VIDEO_CODEC)); startTurn(); } private void startTurn() { HashMap<String, Object> deviceParameters = device.getParameters(); String url = deviceParameters.get(RCDevice.ParameterKeys.MEDIA_ICE_URL) + "?ident=" + deviceParameters.get(RCDevice.ParameterKeys.MEDIA_ICE_USERNAME) + "&secret=" + deviceParameters.get(RCDevice.ParameterKeys.MEDIA_ICE_PASSWORD) + "&domain=cloud.restcomm.com&application=default&room=default&secure=1"; boolean turnEnabled = false; if (deviceParameters.containsKey(RCDevice.ParameterKeys.MEDIA_TURN_ENABLED) && !deviceParameters.get(RCDevice.ParameterKeys.MEDIA_TURN_ENABLED).equals("")) { turnEnabled = true; } //String url = "https://service.xirsys.com/ice?ident=atsakiridis&secret=4e89a09e-bf6f-11e5-a15c-69ffdcc2b8a7&domain=cloud.restcomm.com&application=default&room=default&secure=1"; new IceServerFetcher(url, turnEnabled, this).makeRequest(); // cancel any pending timers before we start new one timeoutHandler.removeCallbacksAndMessages(null); Runnable runnable = new Runnable() { @Override public void run() { onCallTimeout(); } }; timeoutHandler.postDelayed(runnable, CALL_TIMEOUT_DURATION_MILIS); } // If permission is granted we return true private boolean checkPermissions(boolean isVideo) { ArrayList<String> permissions = new ArrayList<>(Arrays.asList(MANDATORY_PERMISSIONS)); if (isVideo) { // Only add CAMERA permission if this is a video call permissions.add(Manifest.permission.CAMERA); } // Check for mandatory permissions. for (String permission : permissions) { if (RCClient.getContext() .checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { RCLogger.e(TAG, "Permission " + permission + " is not granted"); handleDisconnect("Device-Permissions-Denied"); listener.onError(RCConnection.this, RCClient.ErrorCodes.ERROR_CONNECTION_PERMISSION_DENIED.ordinal(), RCClient.errorText(RCClient.ErrorCodes.ERROR_CONNECTION_PERMISSION_DENIED)); if (!isIncoming()) { // Only remove connection in outgoing calls where no signaling ever starts (hence we are really done with the connection). // Remember that for incoming signaling has already kicked in, hence the connection will be removed // when onCallLocalDisconnectedEvent() is called device.removeConnection(jobId); } return false; } } return true; } // Called if call hasn't been established in the predefined period private void onCallTimeout() { // This situation can be legit, as long as we not SIGNALING_CONNECTED. If we are, then it means that signaling is established but // media takes way too long if (state == ConnectionState.SIGNALING_CONNECTED) { RCLogger.e(TAG, "onCallTimeout(): State: " + state + ", after: " + CALL_TIMEOUT_DURATION_MILIS); String reason = "Call-Timeout-Media"; RCClient.ErrorCodes errorCode = RCClient.ErrorCodes.ERROR_CONNECTION_MEDIA_TIMEOUT; handleDisconnect(reason); if (this.listener != null) { this.listener.onDisconnected(this, errorCode.ordinal(), RCClient.errorText(errorCode)); } // Phone state Intents to capture dropped call event sendQoSDisconnectErrorIntent(errorCode.ordinal(), RCClient.errorText(errorCode)); } } // initialize webrtc facilities for the call private void initializeWebrtc(boolean videoEnabled, PercentFrameLayout localRenderLayout, PercentFrameLayout remoteRenderLayout, String preferredVideoCodec) { PercentFrameLayout test; RCLogger.i(TAG, "initializeWebrtc "); //Context context = RCClient.getContext(); iceConnected = false; signalingParameters = null; scalingType = ScalingType.SCALE_ASPECT_FILL; this.localRenderLayout = localRenderLayout; this.remoteRenderLayout = remoteRenderLayout; rootEglBase = EglBase.create(); localRender = (SurfaceViewRenderer) localRenderLayout.getChildAt(0); remoteRender = (SurfaceViewRenderer) remoteRenderLayout.getChildAt(0); if (videoEnabled) { localMediaType = ConnectionMediaType.AUDIO_VIDEO; } else { localMediaType = ConnectionMediaType.AUDIO; } localRender.init(rootEglBase.getEglBaseContext(), null); localRender.setZOrderMediaOverlay(true); remoteRender.init(rootEglBase.getEglBaseContext(), null); updateVideoView(VideoViewState.NONE); // default to VP8 as VP9 doesn't seem to have that great android device support if (preferredVideoCodec == null) { preferredVideoCodec = "VP8"; } peerConnectionParameters = new PeerConnectionClient.PeerConnectionParameters(videoEnabled, // video call false, // loopback false, // tracing 0, // video width 0, // video height 0, // video fps 0, // video start bitrate preferredVideoCodec, // video codec true, // video condec hw acceleration false, // capture to texture 0, // audio start bitrate "OPUS", // audio codec false, // no audio processing false, // aec dump false); // use opengles createPeerConnectionFactory(); } /* private void updateVideoView() { if (remoteRender != null) { remoteRender.setScalingType(scalingType); remoteRender.setMirror(false); remoteRender.requestLayout(); } if (localRender != null) { localRender.setMirror(true); localRender.requestLayout(); } } */ private void updateVideoView(VideoViewState state) { if (state == VideoViewState.NONE) { // when call starts both local and remote video views should be hidden localRender.setVisibility(View.INVISIBLE); remoteRender.setVisibility(View.INVISIBLE); } else if (state == VideoViewState.LOCAL_VIEW_RECEIVED) { // local video became available, which also means that local user has previously requested a video call, // hence we need to show local video view localRender.setVisibility(View.VISIBLE); localRenderLayout.setPosition(LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING, LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING); localRender.setScalingType(scalingType); localRender.setMirror(true); localRender.requestLayout(); } else if (state == VideoViewState.REMOTE_VIEW_RECEIVED) { // remote video became available, which also means that remote user has requested a video call, // hence we need to show remote video view //remoteRender.setVisibility(View.VISIBLE); } else if (state == VideoViewState.ICE_CONNECTED) { if (remoteVideoReceived) { remoteRender.setVisibility(View.VISIBLE); remoteRenderLayout.setPosition(REMOTE_X, REMOTE_Y, REMOTE_WIDTH, REMOTE_HEIGHT); remoteRender.setScalingType(scalingType); remoteRender.setMirror(false); localRenderLayout.setPosition(LOCAL_X_CONNECTED, LOCAL_Y_CONNECTED, LOCAL_WIDTH_CONNECTED, LOCAL_HEIGHT_CONNECTED); localRender.setScalingType(ScalingType.SCALE_ASPECT_FIT); localRender.setMirror(true); localRender.requestLayout(); remoteRender.requestLayout(); } } /* remoteRenderLayout.setPosition(REMOTE_X, REMOTE_Y, REMOTE_WIDTH, REMOTE_HEIGHT); remoteRender.setScalingType(scalingType); remoteRender.setMirror(false); if (iceConnected) { localRenderLayout.setPosition( LOCAL_X_CONNECTED, LOCAL_Y_CONNECTED, LOCAL_WIDTH_CONNECTED, LOCAL_HEIGHT_CONNECTED); localRender.setScalingType(ScalingType.SCALE_ASPECT_FIT); } else { localRenderLayout.setPosition( LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING, LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING); localRender.setScalingType(scalingType); } localRender.setMirror(true); localRender.requestLayout(); remoteRender.requestLayout(); */ } private void startCall(SignalingParameters signalingParameters) { RCLogger.i(TAG, "startCall"); callStartedTimeMs = System.currentTimeMillis(); // Start room connection. logAndToast("Preparing call"); //audioManager.startCall(); // we don't have room functionality to notify us when ready; instead, we start connecting right now this.onConnectedToRoom(signalingParameters); } // Disconnect from remote resources, dispose of local resources, and exit. private void disconnectWebrtc() { RCLogger.i(TAG, "disconnectWebrtc"); if (peerConnectionClient != null) { peerConnectionClient.close(); peerConnectionClient = null; } if (localRender != null) { localRender.release(); localRender = null; } if (remoteRender != null) { remoteRender.release(); remoteRender = null; } audioManager.endCall(); /* if (audioManager != null) { audioManager.close(); audioManager = null; } */ if (rootEglBase != null) { rootEglBase.release(); rootEglBase = null; } } /* private void onAudioManagerChangedState() { // TODO(henrika): disable video if AppRTCAudioManager.AudioDevice.EARPIECE // is active. } */ // Create peer connection factory when EGL context is ready. private void createPeerConnectionFactory() { final RCConnection connection = this; // Important: need to fire the event in UI context cause currently we 're in JAIN SIP thread Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.i(TAG, "createPeerConnectionFactory"); if (peerConnectionClient == null) { final long delta = System.currentTimeMillis() - callStartedTimeMs; RCLogger.d(TAG, "Creating peer connection factory, delay=" + delta + "ms"); peerConnectionClient = PeerConnectionClient.getInstance(); peerConnectionClient.createPeerConnectionFactory(RCClient.getContext(), peerConnectionParameters, connection); logAndToast("Created PeerConnectionFactory"); } if (signalingParameters != null) { RCLogger.w(TAG, "EGL context is ready after room connection."); // #WEBRTC-VIDEO TODO: when I disabled the video view stuff, I also had to comment this out cause it turns out // that in that case this part of the code was executed (as if signalingParameters was null and now it isn't), // which resulted in onConnectedToRoomInternal being called twice for the same call! When I reinstate // video this should probably be uncommented: //onConnectedToRoomInternal(signalingParameters); } } }; mainHandler.post(myRunnable); } // Log |msg| and Toast about it. private void logAndToast(String msg) { RCLogger.d(TAG, msg); if (DO_TOAST) { if (logToast != null) { logToast.cancel(); } logToast = Toast.makeText(RCClient.getContext(), msg, Toast.LENGTH_SHORT); logToast.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL, 0, 0); logToast.show(); } } // -----Implementation of PeerConnectionClient.PeerConnectionEvents.--------- // Send local peer connection SDP and ICE candidates to remote party. // All callbacks are invoked from peer connection client looper thread and // are routed to UI thread. @Override public void onLocalDescription(final SessionDescription sdp) { final long delta = System.currentTimeMillis() - callStartedTimeMs; final RCConnection connection = this; Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.i(TAG, "onLocalDescription"); if (signalingParameters != null) { // && !signalingParameters.sipUrl.isEmpty()) { logAndToast("Sending " + sdp.type + ", delay=" + delta + "ms"); if (signalingParameters.initiator) { // keep it around so that we combine it with candidates before sending it over connection.signalingParameters.offerSdp = sdp; //appRtcClient.sendOfferSdp(sdp); } else { //appRtcClient.sendAnswerSdp(sdp); connection.signalingParameters.answerSdp = sdp; // for an incoming call we have already stored the offer candidates there, now // we are done with those and need to come up with answer candidates // TODO: this might prove dangerous as the signalingParms struct used to be all const, // but I changed it since with JAIN sip signalling where various parts are picked up // at different points in time connection.signalingParameters.iceCandidates.clear(); } } } }; mainHandler.post(myRunnable); } @Override public void onIceCandidate(final IceCandidate candidate) { final RCConnection connection = this; Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.i(TAG, "onIceCandidate:"); connection.signalingParameters.addIceCandidate(candidate); } }; mainHandler.post(myRunnable); } @Override public void onIceCandidatesRemoved(final IceCandidate[] candidates) { final RCConnection connection = this; Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.i(TAG, "onIceCandidateRemoved: Not Implemented Yet"); } }; mainHandler.post(myRunnable); } public void onIceGatheringComplete() { final RCConnection connection = this; Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.i(TAG, "onIceGatheringComplete"); if (peerConnectionClient == null) { // if the user hangs up the call before its setup we need to bail return; } if (signalingParameters.initiator) { HashMap<String, Object> parameters = new HashMap<String, Object>(); parameters.put(RCConnection.ParameterKeys.CONNECTION_PEER, signalingParameters.sipUrl); parameters.put("sdp", connection.signalingParameters.generateSipSdp( connection.signalingParameters.offerSdp, connection.signalingParameters.iceCandidates)); parameters.put(ParameterKeys.CONNECTION_CUSTOM_SIP_HEADERS, connection.signalingParameters.sipHeaders); signalingClient.call(jobId, parameters); } else { HashMap<String, Object> parameters = new HashMap<>(); parameters.put("sdp", connection.signalingParameters.generateSipSdp(connection.signalingParameters.answerSdp, connection.signalingParameters.iceCandidates)); signalingClient.accept(jobId, parameters); //connection.state = ConnectionState.CONNECTING; } } }; mainHandler.post(myRunnable); } @Override public void onIceConnected() { final long delta = System.currentTimeMillis() - callStartedTimeMs; Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.i(TAG, "onIceConnected"); // stop any calling or ringing sound audioManager.stop(); // we 're connected, cancel any pending timeout timers timeoutHandler.removeCallbacksAndMessages(null); logAndToast("ICE connected, delay=" + delta + "ms"); iceConnected = true; RCConnection.this.state = ConnectionState.CONNECTED; updateVideoView(VideoViewState.ICE_CONNECTED); HashMap<String, String> customHeaders = null; if (callParams.containsKey(ParameterKeys.CONNECTION_CUSTOM_INCOMING_SIP_HEADERS)) { customHeaders = (HashMap<String, String>) callParams .get(ParameterKeys.CONNECTION_CUSTOM_INCOMING_SIP_HEADERS); } listener.onConnected(RCConnection.this, customHeaders); } }; mainHandler.post(myRunnable); } @Override public void onIceDisconnected() { // Notice that this is actually means that media connectivity has been lost, hence showing an error (maps to IceConnectionState.DISCONNECTED) Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.i(TAG, "onIceDisconnected"); logAndToast("ICE disconnected"); iceConnected = false; handleDisconnect("Connectivity-Drop"); } }; mainHandler.post(myRunnable); } @Override public void onPeerConnectionClosed() { RCLogger.i(TAG, "onPeerConnectionClosed"); } @Override public void onPeerConnectionStatsReady(final StatsReport[] reports) { Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { if (iceConnected) { //hudFragment.updateEncoderStatistics(reports); } } }; mainHandler.post(myRunnable); } @Override public void onPeerConnectionError(final String description) { final RCConnection connection = this; Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.e(TAG, "PeerConnection error: " + description); String reason = null; if (description.equals("ICE connection failed")) { // in cases where this is the result of IceConnectionState.FAILED, which means that media connectivity is lost we need to add proper reason header reason = "Connectivity-Drop"; } handleDisconnect(reason); if (connection.listener != null) { connection.listener.onDisconnected(connection, RCClient.ErrorCodes.ERROR_CONNECTION_WEBRTC_PEERCONNECTION_ERROR.ordinal(), description); } // Phone state Intents to capture dropped call event sendQoSDisconnectErrorIntent( RCClient.ErrorCodes.ERROR_CONNECTION_WEBRTC_PEERCONNECTION_ERROR.ordinal(), description); } }; mainHandler.post(myRunnable); } public void onLocalVideo(VideoTrack videoTrack) { Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.i(TAG, "onLocalVideo"); localVideoReceived = true; updateVideoView(VideoViewState.LOCAL_VIEW_RECEIVED); listener.onLocalVideo(RCConnection.this); } }; mainHandler.post(myRunnable); } public void onRemoteVideo(VideoTrack videoTrack) { Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.i(TAG, "onRemoteVideo"); remoteVideoReceived = true; updateVideoView(VideoViewState.REMOTE_VIEW_RECEIVED); listener.onRemoteVideo(RCConnection.this); } }; mainHandler.post(myRunnable); } // -----Implementation of AppRTCClient.AppRTCSignalingEvents --------------- // All callbacks are invoked from websocket signaling looper thread and // are routed to UI thread. //@Override private void onConnectedToRoom(final SignalingParameters params) { Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { onConnectedToRoomInternal(params); } }; mainHandler.post(myRunnable); // Phone state Intents to capture dialing or answering event if (signalingParameters.initiator) sendQoSConnectionIntent("dialing"); else sendQoSConnectionIntent("answering"); } private void onConnectedToRoomInternal(final SignalingParameters params) { RCLogger.i(TAG, "onConnectedToRoomInternal"); final long delta = System.currentTimeMillis() - callStartedTimeMs; signalingParameters = params; if (peerConnectionClient == null) { RCLogger.w(TAG, "Room is connected, but EGL context is not ready yet."); return; } logAndToast("Creating peer connection, delay=" + delta + "ms"); peerConnectionClient.createPeerConnection(rootEglBase.getEglBaseContext(), localRender, remoteRender, signalingParameters); if (signalingParameters.initiator) { logAndToast("Creating OFFER..."); // Create offer. Offer SDP will be sent to answering client in // PeerConnectionEvents.onLocalDescription event. peerConnectionClient.createOffer(); } else { if (params.offerSdp != null) { peerConnectionClient.setRemoteDescription(params.offerSdp); logAndToast("Creating ANSWER..."); // Create answer. Answer SDP will be sent to offering client in // PeerConnectionEvents.onLocalDescription event. peerConnectionClient.createAnswer(); } if (params.iceCandidates != null) { // Add remote ICE candidates from room. for (IceCandidate iceCandidate : params.iceCandidates) { peerConnectionClient.addRemoteIceCandidate(iceCandidate); } } } } private void onRemoteDescription(String sdpString) { onRemoteDescription(new SessionDescription(SessionDescription.Type.ANSWER, sdpString)); } //@Override private void onRemoteDescription(final SessionDescription sdp) { final long delta = System.currentTimeMillis() - callStartedTimeMs; Handler mainHandler = new Handler(RCClient.getContext().getMainLooper()); Runnable myRunnable = new Runnable() { @Override public void run() { RCLogger.i(TAG, "onRemoteDescription"); if (peerConnectionClient == null) { RCLogger.e(TAG, "Received remote SDP for non-initilized peer connection."); return; } logAndToast("Received remote " + sdp.type + ", delay=" + delta + "ms"); SignalingParameters params = SignalingParameters.extractCandidates(sdp); peerConnectionClient.setRemoteDescription(params.offerSdp); onRemoteIceCandidates(params.iceCandidates); if (!signalingParameters.initiator) { logAndToast("Creating ANSWER..."); // Create answer. Answer SDP will be sent to offering client in // PeerConnectionEvents.onLocalDescription event. peerConnectionClient.createAnswer(); } } }; mainHandler.post(myRunnable); } //@Override private void onRemoteIceCandidates(final List<IceCandidate> candidates) { RCLogger.i(TAG, "onRemoteIceCandidates"); // no need to run it in UI thread it is already there due to onRemoteDescription if (peerConnectionClient == null) { RCLogger.e(TAG, "Received ICE candidates for non-initilized peer connection."); return; } for (IceCandidate candidate : candidates) { peerConnectionClient.addRemoteIceCandidate(candidate); } } // Helpers // get from SDP if this is an audio or audio/video call static ConnectionMediaType sdp2Mediatype(String sdp) { boolean foundVideo = false; // split the media SDP sections String[] sections = sdp.split("m="); for (int i = 0; i < sections.length; i++) { // and check if the media secion starts with 'video' if (sections[i].matches("(?s)^video.*")) { // if so checks if the video section has recvonly if (sections[i].matches("(?s).*a=recvonly.*")) { return ConnectionMediaType.AUDIO; } else if (sections[i].matches("(?s).*video 0.*")) { return ConnectionMediaType.AUDIO; } foundVideo = true; } } if (!foundVideo) { return ConnectionMediaType.AUDIO; } else { return ConnectionMediaType.AUDIO_VIDEO; } // if there is a video line AND the port value is different than 0 (hence 1-9 in the first digit) then we have video /* Let's keep this around commented in case Firefox changes how it works if (sdp.matches("(?s).*m=video [1-9].*")) { return ConnectionMediaType.AUDIO_VIDEO; } else { return ConnectionMediaType.AUDIO; } */ } // -- Notify QoS module of Connection related events through intents, if the module is available // Phone state Intents to capture events private void sendQoSConnectionIntent(String state) { SignalingParameters params = this.signalingParameters; Intent intent = new Intent("org.mobicents.restcomm.android.CALL_STATE"); intent.putExtra("STATE", state); intent.putExtra("INCOMING", this.isIncoming()); if (params != null) { intent.putExtra("VIDEO", params.videoEnabled); intent.putExtra("REQUEST", params.sipUrl); } if (this.getState() != null) intent.putExtra("CONNECTIONSTATE", this.getState().toString()); Context context = RCClient.getContext(); try { // Restrict the Intent to MMC Handler running within the same application Class aclass = Class.forName("com.cortxt.app.corelib.Services.Intents.IntentHandler"); intent.setClass(context.getApplicationContext(), aclass); context.sendBroadcast(intent); } catch (ClassNotFoundException e) { // If the MMC class isn't here, no intent } } // Phone state Intents to capture dropped call event with reason private void sendQoSDisconnectErrorIntent(int error, String errorText) { Intent intent = new Intent("org.mobicents.restcomm.android.DISCONNECT_ERROR"); intent.putExtra("STATE", "disconnect error"); if (errorText != null) intent.putExtra("ERRORTEXT", errorText); intent.putExtra("ERROR", error); intent.putExtra("INCOMING", this.isIncoming()); Context context = RCClient.getContext(); try { // Restrict the Intent to MMC Handler running within the same application Class aclass = Class.forName("com.cortxt.app.corelib.Services.Intents.IntentHandler"); intent.setClass(context.getApplicationContext(), aclass); context.sendBroadcast(intent); } catch (ClassNotFoundException e) { // If the MMC class isn't here, no intent } } }