/* MediaConnection.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.util.Log; import org.deviceconnect.android.deviceplugin.webrtc.BuildConfig; import org.json.JSONObject; import org.webrtc.DataChannel; import org.webrtc.IceCandidate; import org.webrtc.MediaConstraints; import org.webrtc.PeerConnection; import org.webrtc.PeerConnectionFactory; import org.webrtc.SdpObserver; import org.webrtc.SessionDescription; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * MediaConnection. * * @author NTT DOCOMO, INC. */ public class MediaConnection { /** * Tag for debugging. */ private static final String TAG = "WEBRTC"; private static final String PREFIX_MEDIA_PEERJS = "mc_"; private static final String AUDIO_CODEC_ISAC = "ISAC"; private static final String VIDEO_CODEC_H264 = "H264"; private PeerConnectionFactory mFactory; private PeerConnection mPeerConnection; private MediaStream mLocalMediaStream; private MediaStream mRemoteMediaStream; private PeerOption mOption; private String mPeerId; private String mConnectionId; private String mType = "media"; private String mBrowser; private SessionDescription mRemoteSdp; private boolean mOpen; private OnMediaConnectionCallback mCallback; private OnMediaEventListener mListener; /** * Constructor. * @param factory Instance of PeerConnectionFactory * @param option option of connection */ MediaConnection(final PeerConnectionFactory factory, final PeerOption option) { mFactory = factory; mOption = option; mConnectionId = PREFIX_MEDIA_PEERJS + PeerUtil.randomToken(16); createPeerConnection(createIceServerList(option.getIceServer())); } /** * Retrieves the peer id of the destination. * @return peer id */ public String getPeerId() { return mPeerId; } /** * Sets the peer id of the destination. * @param peerId peer id */ public void setPeerId(final String peerId) { mPeerId = peerId; } /** * Retrieves the connection id. * @return connection id */ public String getConnectionId() { return mConnectionId; } /** * Retrieves the type. * @return type */ public String getType() { return mType; } /** * Returns true if connected to the p2p. * @return true if connected to the p2p. */ public boolean isOpen() { return mOpen; } /** * Set a local media stream. * @param stream local media stream */ public void setLocalMediaStream(final MediaStream stream) { if (mLocalMediaStream != null) { // TODO: 既にローカルメディアが設定されている場合 } mLocalMediaStream = stream; mPeerConnection.addStream(mLocalMediaStream.getMediaStream()); } /** * Gets a local MediaStream. * @return MediaStream */ public MediaStream getLocalMediaStream() { return mLocalMediaStream; } /** * Gets a remote MediaStream. * @return MediaStream */ public MediaStream getRemoteMediaStream() { return mRemoteMediaStream; } /** * Sets the callback. * @param callback callback */ void setOnConnectionCallback(final OnMediaConnectionCallback callback) { mCallback = callback; } /** * Sets the OnMediaEventListener. * @param listener listener */ public void setOnMediaEventListener(final OnMediaEventListener listener) { mListener = listener; } /** * Starts a video. */ public void startVideo() { if (mLocalMediaStream != null) { mLocalMediaStream.startVideo(); } } /** * Stops a video. */ public void stopVideo() { if (mLocalMediaStream != null) { mLocalMediaStream.stopVideo(); } } /** * Closes a MediaConnection. */ public synchronized void close() { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ MediaConnection::close: " + this); } mOpen = false; if (mPeerConnection != null) { mPeerConnection.dispose(); mPeerConnection = null; } if (mLocalMediaStream != null) { mLocalMediaStream.close(); mLocalMediaStream = null; } if (mRemoteMediaStream != null) { mRemoteMediaStream.close(); mRemoteMediaStream = null; } if (mCallback != null) { mCallback.onClose(this); } if (mListener != null) { mListener.onClose(); } } /** * Creates an offer message. */ public void createOffer() { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ MediaConnection::createOffer"); } mPeerConnection.createOffer(mSdpObserver, createSDPMediaConstraints()); } /** * Creates an answer message from json message. * @param json offer message */ public void createAnswer(final JSONObject json) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ MediaConnection::createAnswer"); Log.d(TAG, "@@@@ json: " + json.toString()); } JSONObject payload = json.optJSONObject("payload"); JSONObject sdpObj = payload.optJSONObject("sdp"); String type = PeerUtil.getJSONString(sdpObj, "type", ""); String sdp = PeerUtil.getJSONString(sdpObj, "sdp", ""); if (getType().equalsIgnoreCase("media")) { sdp = preferCodec(sdp, AUDIO_CODEC_ISAC, true); sdp = preferCodec(sdp, VIDEO_CODEC_H264, false); } SessionDescription.Type sdpType = SessionDescription.Type.OFFER; mConnectionId = PeerUtil.getJSONString(payload, "connectionId", null); mRemoteSdp = new SessionDescription(sdpType, sdp); if (mPeerConnection != null) { mPeerConnection.setRemoteDescription(mSdpObserver, mRemoteSdp); } mOpen = true; } /** * Handles an answer message. * @param json message */ public void handleAnswer(final JSONObject json) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@@ handleAnswer"); Log.d(TAG, "@@@@ json: " + json.toString()); } JSONObject payload = json.optJSONObject("payload"); JSONObject sdpObj = payload.optJSONObject("sdp"); String type = PeerUtil.getJSONString(sdpObj, "type", ""); String sdp = PeerUtil.getJSONString(sdpObj, "sdp", ""); if (getType().equalsIgnoreCase("media")) { sdp = preferCodec(sdp, AUDIO_CODEC_ISAC, true); sdp = preferCodec(sdp, VIDEO_CODEC_H264, false); } SessionDescription.Type sdpType = SessionDescription.Type.ANSWER; mBrowser = PeerUtil.getJSONString(payload, "browser", "Supported"); mRemoteSdp = new SessionDescription(sdpType, sdp); if (mPeerConnection != null) { mPeerConnection.setRemoteDescription(mSdpObserver, mRemoteSdp); } mOpen = true; } /** * Handles a candidate message. * @param json candidate message */ public void handleCandidate(final JSONObject json) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@@ handleCandidate"); Log.d(TAG, "@@@@ json: " + json.toString()); } PeerConnection.IceConnectionState state = mPeerConnection.iceConnectionState(); if (state == PeerConnection.IceConnectionState.COMPLETED) { return; } JSONObject payload = json.optJSONObject("payload"); JSONObject candidateObj = payload.optJSONObject("candidate"); String sdpMid = candidateObj.optString("sdpMid"); Integer sdpMLineIndex = candidateObj.optInt("sdpMLineIndex"); String candidate = candidateObj.optString("candidate"); IceCandidate ice = new IceCandidate(sdpMid, sdpMLineIndex, candidate); boolean result = mPeerConnection.addIceCandidate(ice); if (BuildConfig.DEBUG) { if (!result) { Log.i(TAG, "@@@ handleCandidate NG"); } else { Log.i(TAG, "@@@ handleCandidate OK"); } } } /** * Creates a PeerConnection. * @param config configuration of PeerConnection */ private void createPeerConnection(final List<PeerConnection.IceServer> config) { MediaConstraints mc = new MediaConstraints(); try { mPeerConnection = mFactory.createPeerConnection(config, mc, mObserver); } catch (Exception e) { if (BuildConfig.DEBUG) { Log.e(TAG, "@@@ Failed to create PeerConnection.", e); } throw new RuntimeException(e); } } private String preferCodec(final String sdpDescription, final String codec, final boolean isAudio) { String[] lines = sdpDescription.split("\r\n"); int mLineIndex = -1; String codecRtpMap = null; // a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>] String regex = "^a=rtpmap:(\\d+) " + codec + "(/\\d+)+[\r]?$"; Pattern codecPattern = Pattern.compile(regex); String mediaDescription = "m=video "; if (isAudio) { mediaDescription = "m=audio "; } for (int i = 0; (i < lines.length) && (mLineIndex == -1 || codecRtpMap == null); i++) { if (lines[i].startsWith(mediaDescription)) { mLineIndex = i; continue; } Matcher codecMatcher = codecPattern.matcher(lines[i]); if (codecMatcher.matches()) { codecRtpMap = codecMatcher.group(1); continue; } } if (mLineIndex == -1) { if (BuildConfig.DEBUG) { Log.w(TAG, "No " + mediaDescription + " line, so can't prefer " + codec); } return sdpDescription; } if (codecRtpMap == null) { if (BuildConfig.DEBUG) { Log.w(TAG, "No rtpmap for " + codec); } return sdpDescription; } if (BuildConfig.DEBUG) { Log.d(TAG, "Found " + codec + " rtpmap " + codecRtpMap + ", prefer at " + lines[mLineIndex]); } String[] origMLineParts = lines[mLineIndex].split(" "); if (origMLineParts.length > 3) { StringBuilder newMLine = new StringBuilder(); int origPartIndex = 0; // Format is: m=<media> <port> <proto> <fmt> ... newMLine.append(origMLineParts[origPartIndex++]).append(" "); newMLine.append(origMLineParts[origPartIndex++]).append(" "); newMLine.append(origMLineParts[origPartIndex++]).append(" "); newMLine.append(codecRtpMap); for (; origPartIndex < origMLineParts.length; origPartIndex++) { if (!origMLineParts[origPartIndex].equals(codecRtpMap)) { newMLine.append(" ").append(origMLineParts[origPartIndex]); } } lines[mLineIndex] = newMLine.toString(); if (BuildConfig.DEBUG) { Log.d(TAG, "Change media description: " + lines[mLineIndex]); } } else { if (BuildConfig.DEBUG) { Log.e(TAG, "Wrong SDP media description format: " + lines[mLineIndex]); } } StringBuilder newSdpDescription = new StringBuilder(); for (String line : lines) { newSdpDescription.append(line).append("\r\n"); } return newSdpDescription.toString(); } /** * Creates the list of IceServer from uri. * If uriList is null, returns the list of default. * @param uriList list of IceServer's uri * @return list of IceServer */ private List<PeerConnection.IceServer> createIceServerList(final List<String> uriList) { if (uriList == null) { return PeerUtil.getSkyWayIceServer(); } else { List<PeerConnection.IceServer> list = new ArrayList<>(); for (String uri : uriList) { list.add(new PeerConnection.IceServer(uri)); } return list; } } /** * Creates the MediaConstraints of SessionDescription. * @return MediaConstraints */ private MediaConstraints createSDPMediaConstraints() { MediaConstraints mc = new MediaConstraints(); mc.mandatory.add(new MediaConstraints.KeyValuePair( "OfferToReceiveAudio", "true")); mc.mandatory.add(new MediaConstraints.KeyValuePair( "OfferToReceiveVideo", "true")); return mc; } /** * Implements the {@link org.webrtc.PeerConnection.Observer}. */ private final PeerConnection.Observer mObserver = new PeerConnection.Observer() { @Override public void onIceConnectionReceivingChange(boolean b) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onIceConnectionReceivingChange"); Log.d(TAG, "@@@ b: " + b); } } @Override public void onSignalingChange(final PeerConnection.SignalingState signalingState) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onSignalingChange"); Log.d(TAG, "@@@ SignalingState: " + signalingState.toString()); } } @Override public void onIceConnectionChange(final PeerConnection.IceConnectionState iceConnectionState) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onIceConnectionChange"); Log.d(TAG, "@@@ IceConnectionState: " + iceConnectionState.toString()); } switch (iceConnectionState) { case FAILED: if (mCallback != null) { mCallback.onError(MediaConnection.this); } // no break; case DISCONNECTED: close(); break; default: if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ iceConnectionState=" + iceConnectionState); } break; } } @Override public void onIceGatheringChange(final PeerConnection.IceGatheringState iceGatheringState) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onIceGatheringChange"); Log.d(TAG, "@@@ IceGatheringState: " + iceGatheringState.toString()); } } @Override public void onIceCandidate(final IceCandidate iceCandidate) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onIceCandidate"); Log.d(TAG, "@@@ IceCandidate: " + iceCandidate.toString()); } if (mCallback != null) { mCallback.onIceCandidate(MediaConnection.this, iceCandidate); } } @Override public void onAddStream(final org.webrtc.MediaStream mediaStream) { if (BuildConfig.DEBUG) { Log.e(TAG, "@@@ onAddStream"); Log.d(TAG, "@@@ MediaStream: " + mediaStream.toString()); } mRemoteMediaStream = new MediaStream(mediaStream); if (mListener != null) { mListener.onAddStream(mRemoteMediaStream); } } @Override public void onRemoveStream(final org.webrtc.MediaStream mediaStream) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onRemoveStream"); Log.d(TAG, "@@@ MediaStream: " + mediaStream.toString()); } if (mRemoteMediaStream != null) { if (mListener != null) { mListener.onRemoveStream(mRemoteMediaStream); } mRemoteMediaStream.close(); mRemoteMediaStream = null; } if (mediaStream.videoTracks.size() == 1) { mediaStream.videoTracks.get(0).dispose(); } } @Override public void onDataChannel(final DataChannel dataChannel) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onDataChannel"); Log.d(TAG, "@@@ DataChannel: " + dataChannel.toString()); } } @Override public void onRenegotiationNeeded() { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onRenegotiationNeeded"); } } }; /** * Implements the {@link SdpObserver}. */ private final SdpObserver mSdpObserver = new SdpObserver() { private SessionDescription mSdp; @Override public void onCreateSuccess(final SessionDescription sessionDescription) { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onCreateSuccess"); Log.d(TAG, "@@@ Description: " + sessionDescription.toString()); } String sdp = sessionDescription.description; if (getType().equalsIgnoreCase("media")) { sdp = preferCodec(sdp, AUDIO_CODEC_ISAC, true); sdp = preferCodec(sdp, VIDEO_CODEC_H264, false); } mSdp = new SessionDescription(sessionDescription.type, sdp); mPeerConnection.setLocalDescription(mSdpObserver, mSdp); } @Override public void onSetSuccess() { if (BuildConfig.DEBUG) { Log.d(TAG, "@@@ onSetSuccess"); } PeerConnection.SignalingState state = mPeerConnection.signalingState(); if (state == PeerConnection.SignalingState.HAVE_REMOTE_OFFER) { mPeerConnection.createAnswer(this, createSDPMediaConstraints()); } else /*if (state == PeerConnection.SignalingState.HAVE_LOCAL_OFFER)*/ { if (mCallback != null && mSdp != null) { mCallback.onLocalDescription(MediaConnection.this, mSdp); } mSdp = null; } } @Override public void onCreateFailure(final String s) { if (BuildConfig.DEBUG) { Log.e(TAG, "@@@ onCreateFailure"); Log.e(TAG, "@@@ s=" + s); } if (mCallback != null) { mCallback.onError(MediaConnection.this); } } @Override public void onSetFailure(String s) { if (BuildConfig.DEBUG) { Log.e(TAG, "@@@ onCreateFailure"); Log.e(TAG, "@@@ s=" + s); } if (mCallback != null) { mCallback.onError(MediaConnection.this); } } }; /** * This interface is used to implement {@link MediaConnection} callbacks. */ public interface OnMediaConnectionCallback { /** * Calls when the SessionDescription for local is set to the MediaConnection. * @param conn MediaConnection * @param sdp session description */ void onLocalDescription(MediaConnection conn, SessionDescription sdp); /** * Calls when the IceCandidate is set to the MediaConnection. * @param conn MediaConnection * @param iceCandidate iceCandidate */ void onIceCandidate(MediaConnection conn, IceCandidate iceCandidate); /** * Calls when the MediaConnection has been closed. * @param conn Closed the MediaConnection */ void onClose(MediaConnection conn); /** * Calls when the error has occurred. * @param conn Closed the MediaConnection */ void onError(MediaConnection conn); } /** * This interface is used to implement {@link MediaConnection} listeners. */ public interface OnMediaEventListener { /** * Calls when the MediaStream has been added. * @param mediaStream Added the MediaStream */ void onAddStream(MediaStream mediaStream); /** * Calls when the MediaStream has been removed. * @param mediaStream Removed the MediaStream */ void onRemoveStream(MediaStream mediaStream); /** * Calls when the MediaStream has been closed. */ void onClose(); /** * Calls when the error has occurred. */ void onError(); } }