package com.example.licodeclient; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.webrtc.AudioTrack; import org.webrtc.DataChannel; import org.webrtc.IceCandidate; import org.webrtc.MediaConstraints; import org.webrtc.MediaStream; import org.webrtc.PeerConnection; import org.webrtc.PeerConnection.IceConnectionState; import org.webrtc.PeerConnection.IceGatheringState; import org.webrtc.PeerConnection.SignalingState; import org.webrtc.PeerConnectionFactory; import org.webrtc.SdpObserver; import org.webrtc.SessionDescription; import org.webrtc.SessionDescription.Type; import org.webrtc.VideoCapturer; import org.webrtc.VideoRenderer; import org.webrtc.VideoRenderer.I420Frame; import org.webrtc.VideoSource; import org.webrtc.VideoTrack; import android.app.Activity; import android.os.Handler; import android.os.HandlerThread; import android.util.Base64; import com.example.licodeclient.apprtc.VideoStreamsView; import com.koushikdutta.async.http.AsyncHttpClient; import com.koushikdutta.async.http.socketio.Acknowledge; import com.koushikdutta.async.http.socketio.ConnectCallback; import com.koushikdutta.async.http.socketio.EventCallback; import com.koushikdutta.async.http.socketio.SocketIOClient; /** * A simple class to connect to a licode server and provides callbacks for the * standard events associated with this. */ public class LicodeConnector implements VideoConnectorInterface { /** flag to store if basic initialization has happened */ private static boolean sInitializedAndroidGlobals; /** socket.io client */ volatile SocketIOClient mIoClient = null; /** lock object for socket communication */ private Object mSocketLock = new Object(); /** current state of the connection */ volatile State mState = State.kUninitialized; /** description of the turn server, username, password, and url */ JSONObject mTurnServer; /** stun server url */ String mStunServerUrl; /** default video bandwidth */ int mDefaultVideoBW; /** max video bandwidth */ int mMaxVideoBW = 75; /** max audio bandwidth */ int mMaxAudioBW = 25; /** list of the streams */ ConcurrentHashMap<String, StreamDescriptionInterface> mRemoteStream = new ConcurrentHashMap<String, StreamDescriptionInterface>(); /** list of the streams */ HashMap<String, StreamDescription> mLocalStream = new HashMap<String, StreamDescription>(); /** current room id */ String mRoomId; /** list of all current observers */ ConcurrentLinkedQueue<RoomObserver> mObservers = new ConcurrentLinkedQueue<LicodeConnector.RoomObserver>(); /** local video stream */ private VideoSource mVideoSource; /** local video capturer */ private VideoCapturer mVideoCapturer; /** if local video stream was paused */ private boolean mVideoStopped = false; /** factory for peer connections */ private static PeerConnectionFactory sFactory; /** list of stun and turn servers available for all connections */ volatile ArrayList<PeerConnection.IceServer> mIceServers = new ArrayList<PeerConnection.IceServer>(); /** the handler for the special video chat thread */ private static Handler sVcHandler = null; /** special lock object when accessing the vc handler instance */ private static Object sVcLock = new Object(); /** server confirmed rights */ private boolean mPermissionPublish, mPermissionSubscribe; /** helper class - runnable that can be cancelled */ private static interface CancelableRunnable extends Runnable { /** cancels the runnable */ void cancel(); } /** refresh token runnable */ private CancelableRunnable mRefreshTokenRunnable; /** may or may not provide logging output - as desired */ static void log(String s) { // TODO dk: logging?! System.out.println(s); } EventCallback mOnAddStream = new EventCallback() { @Override public void onEvent(JSONArray args, Acknowledge ack) { // [{"data":true,"id":331051653483882560,"screen":"","audio":true,"video":true}] log("mOnAddStream"); try { StreamDescription stream = StreamDescription.parseJson(args .getJSONObject(0)); boolean isLocal = mLocalStream.get(stream.getId()) != null; if (!isLocal) { mRemoteStream.put(stream.getId(), stream); triggerStreamAdded(stream); } } catch (JSONException e) { } } }; EventCallback mOnSubscribeP2P = new EventCallback() { @Override public void onEvent(JSONArray args, Acknowledge ack) { // not yet relevant } }; EventCallback mOnPublishP2P = new EventCallback() { @Override public void onEvent(JSONArray args, Acknowledge ack) { // not yet relevant } }; EventCallback mOnDataStream = new EventCallback() { @Override public void onEvent(JSONArray args, Acknowledge ack) { log("mOnDataStream"); try { JSONObject param = args.getJSONObject(0); String streamId = param.getString("id"); String message = param.getString("msg"); StreamDescriptionInterface stream = mRemoteStream.get(streamId); for (RoomObserver obs : mObservers) { obs.onStreamData(message, stream); } } catch (JSONException e) { } } }; EventCallback mOnRemoveStream = new EventCallback() { @Override public void onEvent(JSONArray args, Acknowledge ack) { // [{"id":331051653483882560}] log("mOnRemoveStream"); try { JSONObject param = args.getJSONObject(0); String streamId = param.getString("id"); StreamDescription stream = (StreamDescription) mRemoteStream .get(streamId); if (stream != null) { removeStream(stream); mRemoteStream.remove(streamId); triggerStreamRemoved(stream); } } catch (JSONException e) { } } }; EventCallback mDisconnect = new EventCallback() { @Override public void onEvent(JSONArray args, Acknowledge ack) { log("mDisconnect"); disconnect(); } }; /** peer connection observer */ private class MyPcObserver implements PeerConnection.Observer { /** the associated sdp observer */ private LicodeSdpObserver mSdpObserver; /** stream description */ private StreamDescriptionInterface mDesc; public MyPcObserver(LicodeSdpObserver observer, StreamDescriptionInterface desc) { mSdpObserver = observer; mDesc = desc; } public LicodeSdpObserver getSdpObserver() { return mSdpObserver; } @Override public void onSignalingChange(SignalingState arg0) { } @Override public void onRemoveStream(MediaStream arg0) { // stream gone? } @Override public void onIceGatheringChange(IceGatheringState iceGatherState) { if (iceGatherState == IceGatheringState.COMPLETE) { mSdpObserver.iceReady(); } } @Override public void onIceConnectionChange(IceConnectionState arg0) { } @Override public void onIceCandidate(IceCandidate iceCandidate) { } @Override public void onError() { log("PeerConenctionObserver.onError"); } @Override public void onDataChannel(DataChannel arg0) { } @Override public void onAddStream(final MediaStream media) { if (mSdpObserver.isLocal()) { return; } if (media.videoTracks.size() == 1 && mDesc != null) { ((StreamDescription) mDesc).setMedia(media); triggerMediaAvailable(mDesc); } } @Override public void onRenegotiationNeeded() { log("PeerConnectionObserver.onRenegotiationNeeded"); } }; /** context/activity */ private volatile Activity mActivity; /** local media stream */ private MediaStream lMS; /** the currently active nick */ private String mNick; public LicodeConnector() { } @Override public void onPause() { sVcHandler.post(new Runnable() { @Override public void run() { if (mVideoSource != null) { mVideoSource.stop(); mVideoStopped = true; } } }); } @Override public void onResume() { sVcHandler.post(new Runnable() { @Override public void run() { if (mVideoSource != null && mVideoStopped) { mVideoSource.restart(); mVideoStopped = false; } } }); } @Override public State getState() { return mState; } @Override public boolean isConnected() { return mState == State.kConnected || mState == State.kConnecting; } @Override public void init(Activity context, String nick) { synchronized (sVcLock) { if (sVcHandler == null) { HandlerThread vcthread = new HandlerThread( "LicodeConnectorThread"); vcthread.start(); sVcHandler = new Handler(vcthread.getLooper()); } } if (context == null) { throw new NullPointerException( "Failed to initialize LicodeConnector. Activity is required."); } mActivity = context; mState = State.kDisconnected; mNick = nick; Runnable init = new Runnable() { @Override public void run() { if (!sInitializedAndroidGlobals) { sInitializedAndroidGlobals = true; // newer libjingle versions have options for video and audio PeerConnectionFactory.initializeAndroidGlobals(mActivity);// , // true, // true); } if (sFactory == null) { sFactory = new PeerConnectionFactory(); } }; }; sVcHandler.post(init); } @Override public void setBandwidthLimits(int video, int audio) { mMaxVideoBW = video; mMaxAudioBW = audio; } @Override public void connect(final String token) { if (mState == State.kUninitialized) { return; } if (isConnected()) { return; } mState = State.kConnecting; mActivity.runOnUiThread(new Runnable() { @Override public void run() { createToken(token); } }); } /** sends a token - when required */ public void refreshVideoToken(String token) { token = LicodeConnector.decodeToken(token); if (token == null) { return; } try { JSONObject jsonToken = new JSONObject(token); handleTokenRefresh(jsonToken); sendMessageSocket("refreshToken", jsonToken, new Acknowledge() { @Override public void acknowledge(JSONArray arg0) { // read publish right from result log("Refresh token Acknowledge: " + arg0.toString()); parseVideoTokenResponse(arg0); if (mPermissionPublish) { triggerPublishAllowed(); } else { unpublish(); } } }); } catch (JSONException e) { } } @Override public void disconnect() { if (mState == State.kUninitialized || mState == State.kDisconnected || mState == State.kDisconnecting) { return; } if (mState == State.kConnecting) { // TODO dk: figure out how to handle this! } sVcHandler.post(new Runnable() { @Override public void run() { doDisconnect(); } }); } /** handle actual disconnecting - from ui thread only */ void doDisconnect() { mState = State.kDisconnecting; for (RoomObserver obs : mObservers) { obs.onRoomDisconnected(); } Set<String> keyset = mRemoteStream.keySet(); for (String key : keyset) { StreamDescription stream = (StreamDescription) mRemoteStream .get(key); removeStream(stream); triggerStreamRemoved(stream); } mRemoteStream.clear(); if (mLocalStream.size() > 0) { unpublish(); } synchronized (mSocketLock) { if (mIoClient != null) { mIoClient.disconnect(); mIoClient = null; } } mState = State.kDisconnected; } /** handles time based refreshing of tokens - when they have a duration */ void handleTokenRefresh(JSONObject jsonToken) { int duration = 0; try { duration = jsonToken.getInt("duration"); } catch (JSONException e) { e.printStackTrace(); } if (duration > 0) { if (mRefreshTokenRunnable != null) { mRefreshTokenRunnable.cancel(); } mRefreshTokenRunnable = new CancelableRunnable() { /** * keeps track if this is still to be run, or has been cancelled */ private volatile boolean mIsActive = true; @Override public void run() { if (!mIsActive) { return; } triggerRequestVideoToken(); } @Override public void cancel() { mIsActive = false; } }; long refreshTime = duration - 10; if (refreshTime < 1) { refreshTime = 1; } sVcHandler.postDelayed(mRefreshTokenRunnable, refreshTime * 1000L); } } /** * decodes a video token into a string which can then be turned into a json * object, returns null on errors */ private static final String decodeToken(String result) { try { String token = new String(Base64.decode(result.getBytes(), Base64.DEFAULT), "UTF-8"); log("Licode token decoded: " + token); return token; } catch (UnsupportedEncodingException e) { log("Failed to decode token: " + e.getMessage()); } return null; } /** called with the connection token */ void createToken(String result) { if (result == null) { return; } String token = LicodeConnector.decodeToken(result); if (token == null) { return; } try { mRemoteStream.clear(); final JSONObject jsonToken = new JSONObject(token); String host = jsonToken.getString("host"); if (!host.startsWith("http://")) { host = "http://" + host; } handleTokenRefresh(jsonToken); SocketIOClient.connect(AsyncHttpClient.getDefaultInstance(), host, new ConnectCallback() { @Override public void onConnectCompleted(Exception err, SocketIOClient client) { if (err != null) { err.printStackTrace(); return; } try { // workaround - 2nd connection event JSONObject jsonParam = new JSONObject(); jsonParam.put("reconnect", false); jsonParam.put("secure", jsonToken.getBoolean("secure")); jsonParam.put("force new connection", true); JSONArray arg = new JSONArray(); arg.put(jsonParam); client.emit("connection", arg); log("Licode: Connection established!"); } catch (JSONException e) { e.printStackTrace(); } synchronized (mSocketLock) { mIoClient = client; client.on("onAddStream", mOnAddStream); client.on("onSubscribeP2P", mOnSubscribeP2P); client.on("onPublishP2P", mOnPublishP2P); client.on("onDataStream", mOnDataStream); client.on("onRemoveStream", mOnRemoveStream); client.on("disconnect", mDisconnect); } sendMessageSocket("token", jsonToken, new Acknowledge() { @Override public void acknowledge(JSONArray result) { log("Licode: createToken -> connect"); log(result.toString()); try { // ["success",{"maxVideoBW":300,"id":"5384684c918b864466c853d6","streams":[],"defaultVideoBW":300,"turnServer":{"password":"","username":"","url":""},"stunServerUrl":"stun:stun.l.google.com:19302"}] // ["success",{"maxVideoBW":300,"id":"5384684c918b864466c853d6","streams":[{"data":true,"id":897203996079042600,"screen":"","audio":true,"video":true},{"data":true,"id":841680482029914900,"screen":"","audio":true,"video":true}],"defaultVideoBW":300,"turnServer":{"password":"","username":"","url":""},"stunServerUrl":"stun:stun.l.google.com:19302"}] if ("success" .equalsIgnoreCase(result .getString(0)) == false) { return; } JSONObject jsonObject = result .getJSONObject(1); parseVideoTokenResponse(result); if (jsonObject .has("turnServer")) { mTurnServer = jsonObject .getJSONObject("turnServer"); String url = mTurnServer .getString("url"); String usr = mTurnServer .getString("username"); String pwd = mTurnServer .getString("password"); if (!url.isEmpty()) { mIceServers .add(new PeerConnection.IceServer( url, usr, pwd)); } } if (jsonObject .has("stunServerUrl")) { mStunServerUrl = jsonObject .getString("stunServerUrl"); if (!mStunServerUrl .isEmpty()) { mIceServers .add(new PeerConnection.IceServer( mStunServerUrl)); } } if (jsonObject .has("defaultVideoBW")) { mDefaultVideoBW = jsonObject .getInt("defaultVideoBW"); } if (jsonObject .has("maxVideoBW")) { mMaxVideoBW = jsonObject .getInt("maxVideoBW"); } mState = State.kConnected; // update room id mRoomId = jsonObject .getString("id"); for (RoomObserver obs : mObservers) { obs.onRoomConnected(mRemoteStream); } // retrieve list of streams JSONArray streams = jsonObject .getJSONArray("streams"); for (int index = 0, n = streams .length(); index < n; ++index) { // {"data":true,"id":897203996079042600,"screen":"","audio":true,"video":true} JSONObject arg = streams .getJSONObject(index); StreamDescription stream = StreamDescription .parseJson(arg); mRemoteStream.put( stream.getId(), stream); triggerStreamAdded(stream); } } catch (JSONException e) { } } }); } }); } catch (JSONException e) { } } /** send a json something on the specified channel via socket.io */ void sendMessageSocket(String channel, Object param, Acknowledge ack) { synchronized (mSocketLock) { if (mIoClient == null) { return; } JSONArray jsonArgs = new JSONArray(); jsonArgs.put(param); if (ack == null) { ack = new Acknowledge() { @Override public void acknowledge(JSONArray arg0) { log("LicodeConnector: No one interested in response: " + arg0.toString()); } }; } mIoClient.emit(channel, jsonArgs, ack); } } void sendSDPSocket(String type, JSONObject param0, JSONObject param1, Acknowledge ack) { synchronized (mSocketLock) { if (mIoClient == null) { return; } JSONArray jsonArgs = new JSONArray(); jsonArgs.put(param0); jsonArgs.put(param1); mIoClient.emit(type, jsonArgs, ack); } } void sendSDPSocket(String type, JSONArray params, Acknowledge ack) { synchronized (mSocketLock) { if (mIoClient == null) { return; } mIoClient.emit(type, params, ack); } } void sendDataSocket(String streamId, String message) { JSONObject param = new JSONObject(); try { param.put("id", streamId); param.put("msg", message); } catch (JSONException e) { e.printStackTrace(); } sendMessageSocket("sendDataStream", param, null); } void removeStream(StreamDescription stream) { stream.onClosing(); triggerStreamRemoved(stream); } @Override public void unsubscribe(String streamId) { StreamDescription stream = (StreamDescription) mRemoteStream .get(streamId); if (stream != null) { disable(stream); } } @Override public void addObserver(final RoomObserver observer) { mObservers.add(observer); if (isConnected()) { mActivity.getWindow().getDecorView().post(new Runnable() { @Override public void run() { observer.onRoomConnected(mRemoteStream); } }); } } @Override public void removeObserver(RoomObserver observer) { mObservers.remove(observer); } /** get access to the camera */ private VideoCapturer getVideoCapturer() { String[] cameraFacing = { "front", "back" }; int[] cameraIndex = { 0, 1 }; int[] cameraOrientation = { 0, 90, 180, 270 }; for (String facing : cameraFacing) { for (int index : cameraIndex) { for (int orientation : cameraOrientation) { String name = "Camera " + index + ", Facing " + facing + ", Orientation " + orientation; VideoCapturer capturer = VideoCapturer.create(name); if (capturer != null) { log("Using camera: " + name); return capturer; } } } } throw new RuntimeException("Failed to open capturer"); } // Implementation detail: bridge the VideoRenderer.Callbacks interface to // the // VideoStreamsView implementation. public static class VideoCallbacks implements VideoRenderer.Callbacks { private final VideoStreamsView view; private final String streamId; public VideoCallbacks(VideoStreamsView view, String streamId) { this.view = view; this.streamId = streamId; } @Override public void setSize(final int width, final int height) { view.queueEvent(new Runnable() { public void run() { view.setSize(streamId, width, height); } }); } @Override public void renderFrame(I420Frame frame) { view.queueFrame(streamId, frame); } } private class LicodeSdpObserver implements SdpObserver { /** the sdp created locally */ SessionDescription mLocalSdp = null; /** whether or not this is a publish attempt */ boolean mIsPublish = false; /** the current signalling channel on socket.io */ String mSignalChannel = "subscribe"; /** the associated stream */ final StreamDescription mStream; /** id of the offerers session */ private int mOffererSessionId = 104; /** id of the answerers session */ private int mAnswererSessionId = 0; /** tracks if ice candidates are all collected */ boolean mIceReady = false; /** create an observer for given stream */ LicodeSdpObserver(StreamDescription stream, boolean publishing) { mStream = stream; mIsPublish = publishing; mSignalChannel = mIsPublish ? "publish" : "subscribe"; } public boolean isLocal() { return mStream == null ? false : mStream.isLocal(); } /** waits for ice candidates to be gathered before triggering release */ public void iceReady() { mIceReady = true; startConnecting(); } private void startConnecting() { mStream.pc.createOffer(this, mStream.sdpConstraints()); } @Override public void onCreateFailure(String arg0) { log("SdpObserver#onCreateFailure: " + arg0); } private SessionDescription modifySdpMaxBW(SessionDescription sdp) { StringBuffer desc = new StringBuffer(); int audioLine = -1; int videoLine = -1; ArrayList<Integer> bLines = new ArrayList<Integer>(); String[] lines = sdp.description.split("\r\n"); for (int i = 0; i < lines.length; ++i) { if (lines[i].startsWith("m=audio")) { audioLine = i; } else if (lines[i].startsWith("m=video")) { videoLine = i; } else if (lines[i].startsWith("b=AS:")) { bLines.add(i); } } // TODO dk: this may want to check for existing B-Lines! boolean addVideoB = mMaxVideoBW > 0; boolean addAudioB = mMaxAudioBW > 0; for (int i = 0; i < lines.length; ++i) { desc.append(lines[i]); desc.append("\r\n"); if (i == audioLine && addAudioB) { desc.append("b=AS:" + mMaxAudioBW + "\r\n"); } else if (i == videoLine && addVideoB) { desc.append("b=AS:" + mMaxVideoBW + "\r\n"); } } return new SessionDescription(sdp.type, desc.toString()); } @Override public void onCreateSuccess(SessionDescription sdp) { if (mLocalSdp != null) { return; } if (mIceReady) { mLocalSdp = sdp; } final SessionDescription finalSdp = modifySdpMaxBW(sdp); mActivity.runOnUiThread(new Runnable() { @Override public void run() { mStream.pc.setLocalDescription(LicodeSdpObserver.this, finalSdp); } }); } @Override public void onSetFailure(String arg0) { log("SdpObserver#onSetFailure: " + arg0); } @Override public void onSetSuccess() { if (mLocalSdp == null) { return; } mActivity.runOnUiThread(new Runnable() { @Override public void run() { if (mStream.pc.getRemoteDescription() == null) { sendLocalDescription(); } else { // drain remote candidates?! // also confirm exchange with licode server! sendConfirmation(); } } }); } void sendLocalDescription() { JSONObject desc = null; if (mIsPublish) { desc = mStream.toJsonOffer("offer"); } else { desc = mStream.toJsonOffer(null); try { desc.put("streamId", mStream.getId()); } catch (JSONException e) { } } JSONObject p1 = new JSONObject(); try { p1.put("messageType", "OFFER"); p1.put("sdp", mLocalSdp.description); p1.put("tiebreaker", (int) (Math.random() * (Integer.MAX_VALUE - 2)) + 1); p1.put("offererSessionId", mOffererSessionId); // hardcoded in // Licode? p1.put("seq", 1); // should not be hardcoded, but works for now } catch (JSONException e) { } log("SdpObserver#sendLocalDescription; to: " + mSignalChannel + "; msg: " + p1.toString()); sendSDPSocket(mSignalChannel, desc, p1, new Acknowledge() { @Override public void acknowledge(JSONArray arg0) { log("SdpObserver#sendLocalDescription#sendSDPSocket#Acknowledge: " + arg0.toString()); String streamId = null; SessionDescription remoteSdp = null; try { // log(arg0.getString(0)); // JSONObject jsonAnswer = arg0.getJSONObject(0); // licode server sends answer as string which is // basically a json string, though JSONObject jsonAnswer = new JSONObject(arg0 .getString(0)); boolean answer = "ANSWER".equals(jsonAnswer .getString("messageType")); if (!answer) { log("SdpObserver: expected ANSWER, got: " + jsonAnswer.getString("messageType")); } remoteSdp = new SessionDescription(Type.ANSWER, jsonAnswer.getString("sdp")); if (mIsPublish) { streamId = arg0.getString(1); } mAnswererSessionId = jsonAnswer .getInt("answererSessionId"); } catch (JSONException e1) { } if (mIsPublish) { mStream.setId(streamId); mLocalStream.put(streamId, mStream); } final SessionDescription finalRemoteSdp = remoteSdp; mActivity.runOnUiThread(new Runnable() { @Override public void run() { mStream.pc.setRemoteDescription( LicodeSdpObserver.this, finalRemoteSdp); } }); } }); } void sendConfirmation() { JSONObject p0 = mStream.toJsonOffer("ok"); try { p0.put("streamId", mStream.getId()); p0.put("messageType", "OK"); p0.put("offererSessionId", mOffererSessionId); p0.put("answererSessionId", mAnswererSessionId); p0.put("seq", 1); // p0.put("sdp", " "); } catch (JSONException e) { } sendSDPSocket(mSignalChannel, p0, p0, null); } } public MediaConstraints makePcConstraints() { MediaConstraints pcConstraints = new MediaConstraints(); pcConstraints.optional.add(new MediaConstraints.KeyValuePair( "RtpDataChannels", "true")); pcConstraints.optional.add(new MediaConstraints.KeyValuePair( "EnableDtlsSrtp", "true")); pcConstraints.optional.add(new MediaConstraints.KeyValuePair( "DtlsSrtpKeyAgreement", "true")); return pcConstraints; } @Override public void publish(final VideoStreamsView view) { if (mPermissionPublish) { sVcHandler.post(new Runnable() { @Override public void run() { doPublish(view); } }); } } /** begin streaming to server - MUST run on VcThread */ void doPublish(VideoStreamsView view) { if (mVideoCapturer != null) { return; } MediaConstraints videoConstraints = new MediaConstraints(); videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair( "maxWidth", "320")); videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair( "maxHeight", "240")); videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair( "maxFrameRate", "10")); MediaConstraints audioConstraints = new MediaConstraints(); audioConstraints.optional.add(new MediaConstraints.KeyValuePair( "googEchoCancellation2", "true")); audioConstraints.optional.add(new MediaConstraints.KeyValuePair( "googNoiseSuppression", "true")); lMS = sFactory.createLocalMediaStream("ARDAMS"); if (videoConstraints != null) { mVideoCapturer = getVideoCapturer(); mVideoSource = sFactory.createVideoSource(mVideoCapturer, videoConstraints); VideoTrack videoTrack = sFactory.createVideoTrack("ARDAMSv0", mVideoSource); lMS.addTrack(videoTrack); } if (audioConstraints != null) { AudioTrack audioTrack = sFactory.createAudioTrack("ARDAMSa0", sFactory.createAudioSource(audioConstraints)); lMS.addTrack(audioTrack); audioTrack.setEnabled(false); } StreamDescription stream = new StreamDescription("", false, true, true, false, null, mNick); MediaConstraints pcConstraints = makePcConstraints(); MyPcObserver pcObs = new MyPcObserver(new LicodeSdpObserver(stream, true), stream); PeerConnection pc = sFactory.createPeerConnection(mIceServers, pcConstraints, pcObs); pc.addStream(lMS, new MediaConstraints()); stream.setMedia(lMS); if (view != null) { stream.attachRenderer(new VideoCallbacks(view, VideoStreamsView.LOCAL_STREAM_ID)); } stream.initLocal(pc, pcObs.getSdpObserver()); } @Override public void unpublish() { sVcHandler.post(new Runnable() { @Override public void run() { doUnpublish(); } }); } /** stop all streams from being cast to the server */ void doUnpublish() { for (String key : mLocalStream.keySet()) { final StreamDescription stream = mLocalStream.get(key); if (stream != null && stream.isLocal()) { stream.pc.removeStream(lMS); for (RoomObserver obs : mObservers) { obs.onStreamRemoved(stream); } if (mObservers.size() == 0) { destroy(stream); } } } mLocalStream.clear(); if (lMS != null) { lMS.dispose(); } if (mVideoCapturer != null) { mVideoCapturer.dispose(); } lMS = null; mVideoCapturer = null; if (mVideoSource != null && !mVideoStopped) { mVideoSource.stop(); } mVideoSource = null; } @Override public void subscribe(final StreamDescriptionInterface stream) { sVcHandler.post(new Runnable() { @Override public void run() { doSubscribe((StreamDescription) stream); } }); } void doSubscribe(final StreamDescription stream) { if (stream.isLocal()) { return; } if (stream.getMedia() != null) { // already subscribed! triggerMediaAvailable(stream); return; } // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging. // NOTE: this _must_ happen while |factory| is alive! // Logging.enableTracing("logcat:", // EnumSet.of(Logging.TraceLevel.TRACE_ALL), // Logging.Severity.LS_SENSITIVE); MyPcObserver pcObs = new MyPcObserver(new LicodeSdpObserver(stream, false), stream); PeerConnection pc = sFactory.createPeerConnection(mIceServers, makePcConstraints(), pcObs); stream.initRemote(pc, pcObs.getSdpObserver()); } /** * triggers the event that a stream was added - will eventually happen with * delay */ void triggerStreamAdded(StreamDescription stream) { for (RoomObserver obs : mObservers) { obs.onStreamAdded(stream); } } /** triggers the event that a stream was removed */ void triggerStreamRemoved(StreamDescription stream) { for (RoomObserver obs : mObservers) { obs.onStreamRemoved(stream); } if (mObservers.size() == 0) { destroy(stream); } } /** triggers the event that publish has been allowed now */ void triggerPublishAllowed() { for (RoomObserver obs : mObservers) { obs.onPublishAllowed(); } } /** * triggers that subscribe was successful, and media is now available to * stream */ void triggerMediaAvailable(StreamDescriptionInterface stream) { for (RoomObserver obs : mObservers) { obs.onStreamMediaAvailable(stream); } } /** * triggers that a new video token is required - very soon - or the * connection will end */ void triggerRequestVideoToken() { for (RoomObserver obs : mObservers) { obs.onRequestRefreshToken(); } } @Override public void destroy(final StreamDescriptionInterface param0) { final StreamDescription stream = (StreamDescription) param0; if (stream == null) { return; } sVcHandler.post(new Runnable() { @Override public void run() { if (stream.pc != null) { stream.pc.close(); stream.pc.dispose(); } stream.onDestroyed(); if (stream.isLocal()) { sendMessageSocket("unpublish", stream.getId(), null); } } }); } @Override public void disable(final StreamDescriptionInterface param0) { final StreamDescription stream = (StreamDescription) param0; if (stream.isLocal()) { return; } sVcHandler.post(new Runnable() { @Override public void run() { sendMessageSocket("unsubscribe", stream.getId(), null); stream.detachRenderer(); stream.pc.close(); stream.pc.dispose(); stream.onDisable(); } }); } @Override public void setAudioEnabled(boolean enabled) { if (mState != State.kConnected || lMS == null) { return; } for (AudioTrack audioTrack : lMS.audioTracks) { audioTrack.setEnabled(enabled); } } @Override public void setActivity(Activity activity) { mActivity = activity; } @Override public Map<String, StreamDescriptionInterface> getRemoteStreams() { return mRemoteStream; } @Override public boolean isPublishing() { return mLocalStream.size() > 0; } @Override public void attachLocalStream(final VideoStreamsView vsv) { sVcHandler.post(new Runnable() { @Override public void run() { for (String key : mLocalStream.keySet()) { StreamDescription stream = (StreamDescription) mLocalStream .get(key); stream.attachRenderer(new VideoCallbacks(vsv, VideoStreamsView.LOCAL_STREAM_ID)); break; } } }); } @Override public void detachLocalStream() { sVcHandler.post(new Runnable() { @Override public void run() { for (String key : mLocalStream.keySet()) { StreamDescriptionInterface stream = mLocalStream.get(key); if (stream != null) { stream.detachRenderer(); } } } }); } @Override public void post(Runnable r) { sVcHandler.post(r); } @Override public void attachRenderer(StreamDescriptionInterface stream, VideoStreamsView mVsv) { ((StreamDescription) stream) .attachRenderer(new LicodeConnector.VideoCallbacks(mVsv, stream .getId())); } @Override public void setNick(String nickname) { mNick = nickname; } @Override public boolean requestPublish() { if (mPermissionPublish) { sVcHandler.post(new Runnable() { @Override public void run() { triggerPublishAllowed(); } }); return true; } return false; } /** * parse an acknowledge to a token sent, analyze for permissions, disconnect * on error */ protected void parseVideoTokenResponse(JSONArray arg) { // TODO dk: parse all the other things that come with the response? TURN // Server, etc? boolean success = false; String message = ""; try { success = "success".equalsIgnoreCase(arg.getString(0)); if (success) { JSONObject obj = arg.getJSONObject(1); boolean subscribe = false; boolean publish = false; if (obj.has("permissions")) { JSONObject permissions = obj.getJSONObject("permissions"); subscribe = permissions.has("subscribe") && permissions.getBoolean("subscribe"); publish = permissions.has("publish") && permissions.getBoolean("publish"); } mPermissionSubscribe = subscribe; mPermissionPublish = publish; } else { message = arg.get(1).toString(); } } catch (JSONException e) { log(e.getMessage()); } if (!success) { log("Token failed: " + message); disconnect(); } } }