Users in this app layout must be subscribed to `username-stdby`, a standby channel. The Android side checks if a user is online by checking presence on the standby channel.
In this app, a call can be placed by sending a JSON packet to the user's standby channel:
{"call_user":"UserName","call_time":currentTimeMillis}
Upon accepting the call, the answerer creates the SDP Offer, and video chat begins.
### Incoming Calls
AndroidRTC provides a good example of how to handle incoming calls.
### User Messages
This app also shows how to send custom user messages. These messages could be chat, game scores, and much more.
[PnWebRTC]:https://github.com/GleasonK/pubnub-android-webrtc
[JavaDoc]:http://kevingleason.me/pubnub-android-webrtc/
[AndroidRTC]:https://github.com/GleasonK/AndroidRTC/
[JS SDK]:https://github.com/stephenlb/webrtc-sdk
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "20.0.0"
defaultConfig {
applicationId "me.kevingleason.androidrtc"
minSdkVersion 17
targetSdkVersion 21
versionCode 1
versionName "1.0"
}
buildTypes {
release {
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir {
dirs 'libs'
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.pubnub:pubnub-android:3.7.4'
compile 'io.pristine:libjingle:9694@aar'
compile project(':pnwebrtc')
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/GleasonK/algs4/AndroidSDK/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
================================================
FILE: app/src/androidTest/java/me/kevingleason/androidrtc/ApplicationTest.java
================================================
package me.kevingleason.androidrtc;
import android.app.Application;
import android.test.ApplicationTestCase;
/**
* Testing Fundamentals
*/
public class ApplicationTest extends ApplicationTestCaseCreated by GleasonK on 7/23/15.
*/ public class LogRTCListener extends PnRTCListener { @Override public void onCallReady(String callId) { Log.i("RTCListener", "OnCallReady - " + callId); } @Override public void onConnected(String userId) { Log.i("RTCListener", "OnConnected - " + userId); } @Override public void onPeerStatusChanged(PnPeer peer) { Log.i("RTCListener", "OnPeerStatusChanged - " + peer.toString()); } @Override public void onPeerConnectionClosed(PnPeer peer) { Log.i("RTCListener", "OnPeerConnectionClosed - " + peer.toString()); } @Override public void onLocalStream(MediaStream localStream) { Log.i("RTCListener", "OnLocalStream - " + localStream.toString()); } @Override public void onAddRemoteStream(MediaStream remoteStream, PnPeer peer) { Log.i("RTCListener", "OnAddRemoteStream - " + peer.toString()); } @Override public void onRemoveRemoteStream(MediaStream remoteStream, PnPeer peer) { Log.i("RTCListener", "OnRemoveRemoteStream - " + peer.toString()); } @Override public void onMessage(PnPeer peer, Object message) { Log.i("RTCListener", "OnMessage - " + message.toString()); } @Override public void onDebug(PnRTCMessage message) { Log.i("RTCListener", "OnDebug - " + message.getMessage()); } } ================================================ FILE: app/src/main/res/drawable/light_fade_down.xml ================================================* Author: Kevin Gleason - Boston College '16 * File: PnPeer.java * Date: 7/22/15 * Use: Store information about various Peer Connections * © 2009 - 2015 PubNub, Inc. **/ public class PnPeer implements SdpObserver, PeerConnection.Observer { public static final String TAG = "PnPeer"; public static final String STATUS_CONNECTING = "CONNECTING"; public static final String STATUS_CONNECTED = "CONNECTED"; // TODO: Where to change status to this? public static final String STATUS_DISCONNECTED = "DISCONNECTED"; public static final String TYPE_NONE = "NONE"; public static final String TYPE_OFFER = "offer"; public static final String TYPE_ANSWER = "answer"; private PnPeerConnectionClient pcClient; PeerConnection pc; String id; String type; String status; boolean dialed; boolean received; // Todo: Maybe attach MediaStream as private var? public PnPeer(String id, PnPeerConnectionClient pcClient) { Log.d(TAG, "new Peer: " + id); this.id = id; this.type = TYPE_NONE; this.dialed = false; this.received = false; this.pcClient = pcClient; this.pc = pcClient.pcFactory.createPeerConnection(pcClient.signalingParams.iceServers, pcClient.signalingParams.pcConstraints, this); setStatus(STATUS_CONNECTING); pc.addStream(pcClient.getLocalMediaStream()); } public synchronized void setStatus(String status){ this.status = status; pcClient.mRtcListener.onPeerStatusChanged(this); } public String getStatus() { return status; } public void setType(String type){this.type = type;} public String getType() { return type; } public boolean isDialed() { return dialed; } public void setDialed(boolean dialed) { this.dialed = dialed; } public boolean isReceived() { return received; } public void setReceived(boolean received) { this.received = received; } public PeerConnection getPc() { return pc; } public String getId() { return id; } public void hangup(){ if (this.status.equals(STATUS_DISCONNECTED)) return; // Already hung up on. this.pcClient.removePeer(this.id); setStatus(STATUS_DISCONNECTED); } @Override public void onCreateSuccess(final SessionDescription sdp) { // TODO: modify sdp to use pcParams prefered codecs try { JSONObject payload = new JSONObject(); payload.put("type", sdp.type.canonicalForm()); payload.put("sdp", sdp.description); pcClient.transmitMessage(id, payload); pc.setLocalDescription(PnPeer.this, sdp); } catch (JSONException e) { e.printStackTrace(); } } @Override public void onSetSuccess() { } @Override public void onCreateFailure(String s) { } @Override public void onSetFailure(String s) { } @Override public void onSignalingChange(PeerConnection.SignalingState signalingState) { } @Override public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) { if (this.status.equals(STATUS_DISCONNECTED)) return; // Already hung up on. if (iceConnectionState == PeerConnection.IceConnectionState.DISCONNECTED) { pcClient.removePeer(id); // TODO: Ponder. Also, might want to Pub a disconnect. setStatus(STATUS_DISCONNECTED); } } // Todo: Look into what this should be used for @Override public void onIceConnectionReceivingChange(boolean iceConnectionReceivingChange){ } @Override public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) { } @Override public void onIceCandidate(final IceCandidate candidate) { try { JSONObject payload = new JSONObject(); payload.put("sdpMLineIndex", candidate.sdpMLineIndex); payload.put("sdpMid", candidate.sdpMid); payload.put("candidate", candidate.sdp); pcClient.transmitMessage(id, payload); } catch (JSONException e) { e.printStackTrace(); } } @Override public void onAddStream(MediaStream mediaStream) { Log.d(TAG, "onAddStream " + mediaStream.label()); // remote streams are displayed from 1 to MAX_PEER (0 is localStream) pcClient.mRtcListener.onAddRemoteStream(mediaStream, PnPeer.this); } @Override public void onRemoveStream(MediaStream mediaStream) { Log.d(TAG, "onRemoveStream " + mediaStream.label()); PnPeer peer = pcClient.removePeer(id); pcClient.mRtcListener.onRemoveRemoteStream(mediaStream, peer); } @Override public void onDataChannel(DataChannel dataChannel) { } @Override public void onRenegotiationNeeded() { } /** * Overriding toString for debugging purposes. * @return String representation of a peer. */ @Override public String toString(){ return this.id + " Status: " + this.status + " Dialed: " + this.dialed + " Received: " + this.received + " Type: " + this.type; } } ================================================ FILE: pnwebrtc/src/main/java/me/kevingleason/pnwebrtc/PnPeerConnectionClient.java ================================================ package me.kevingleason.pnwebrtc; import android.util.Log; import com.pubnub.api.Callback; import com.pubnub.api.Pubnub; import com.pubnub.api.PubnubError; import com.pubnub.api.PubnubException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.webrtc.IceCandidate; import org.webrtc.MediaStream; import org.webrtc.PeerConnection; import org.webrtc.PeerConnectionFactory; import org.webrtc.SessionDescription; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** *
* Author: Kevin Gleason - Boston College '16 * File: PnPeerConnectionClient.java * Date: 7/20/15 * Use: WebRTC PeerConnection Manager * © 2009 - 2015 PubNub, Inc. ** * {@link PnPeerConnectionClient} is used to manage peer connections. */ public class PnPeerConnectionClient { private SessionDescription localSdp = null; // either offer or answer SDP private MediaStream localMediaStream = null; PeerConnectionFactory pcFactory; PnRTCListener mRtcListener; PnSignalingParams signalingParams; int MAX_CONNECTIONS = Integer.MAX_VALUE; private Pubnub mPubNub; private PnRTCReceiver mSubscribeReceiver; private Map
* Author: Kevin Gleason - Boston College '16 * File: PnRTCClient.java * Date: 7/20/15 * Use: PubNub WebRTC Signaling * © 2009 - 2015 PubNub, Inc. **/ public class PnRTCClient { private PnSignalingParams pnSignalingParams; private Pubnub mPubNub; private PnPeerConnectionClient pcClient; private String UUID; /** * Minimal constructor. Requires a valid Pub and Sub key. Get your Pub/Sub keys for free at * https://admin.pubnub.com/#/register and find keys on developer portal. * No UUID provided so a random phone number will be generated with this constructor (XXX-XXXX). * @param pubKey PubNub Pub Key * @param subKey PubNub Sub Key */ public PnRTCClient(String pubKey, String subKey) { this.UUID = generateRandomNumber(); this.mPubNub = new Pubnub(pubKey, subKey); this.mPubNub.setUUID(this.UUID); this.pnSignalingParams = PnSignalingParams.defaultInstance(); this.pcClient = new PnPeerConnectionClient(this.mPubNub, this.pnSignalingParams, new PnRTCListener() {}); } /** * Slightly more verbose constructor. Requires a valid Pub and Sub key. Get your Pub/Sub keys for free at * https://admin.pubnub.com/#/register and find keys on developer portal. * @param pubKey PubNub Pub Key * @param subKey PubNub Sub Key * @param UUID Any UUID to be used as a username */ public PnRTCClient(String pubKey, String subKey, String UUID) { this.UUID = UUID; this.mPubNub = new Pubnub(pubKey, subKey); this.mPubNub.setUUID(this.UUID); this.pnSignalingParams = PnSignalingParams.defaultInstance(); this.pcClient = new PnPeerConnectionClient(this.mPubNub, this.pnSignalingParams, new PnRTCListener() {}); } /** * Return the {@link me.kevingleason.pnwebrtc.PnRTCClient} peer connection constraints. * @return Peer Connection Constrains */ public MediaConstraints pcConstraints() { return pnSignalingParams.pcConstraints; } /** * Return the {@link me.kevingleason.pnwebrtc.PnRTCClient} video constraints. * @return Video Constrains */ public MediaConstraints videoConstraints() { return pnSignalingParams.videoConstraints; } /** * Return the {@link me.kevingleason.pnwebrtc.PnRTCClient} audio constraints. * @return Audio Constrains */ public MediaConstraints audioConstraints() { return pnSignalingParams.audioConstraints; } /** * Return the {@link me.kevingleason.pnwebrtc.PnRTCClient} Pubnub instance. * @return The PnRTCClient's {@link com.pubnub.api.Pubnub} instance */ public Pubnub getPubNub(){ return this.mPubNub; } /** * Return the UUID (username) of the {@link me.kevingleason.pnwebrtc.PnRTCClient}. If not * provided by the constructor, a random phone number is generated and can be retrieived * with this method * @return The UUID username of the client */ public String getUUID() { return UUID; } /** * Set the signaling parameters. This includes {@link org.webrtc.MediaConstraints} for * {@link org.webrtc.PeerConnection}, Video, and Audio, as well as a list of possible * {@link org.webrtc.PeerConnection.IceServer} candidates. * @param signalParams Parameters for WebRTC Signaling */ public void setSignalParams(PnSignalingParams signalParams){ this.pnSignalingParams = signalParams; } /** * Need to attach mediaStream before you can connect. * @param mediaStream Not null local media stream */ public void attachLocalMediaStream(MediaStream mediaStream){ this.pcClient.setLocalMediaStream(mediaStream); } /** * Attach custom listener for callbacks! * @param listener The listener which extends PnRTCListener to implement callbacks */ public void attachRTCListener(PnRTCListener listener){ this.pcClient.setRTCListener(listener); } /** * Set the maximum simultaneous connections allowed * @param max Max simultaneous connections */ public void setMaxConnections(int max){ this.pcClient.MAX_CONNECTIONS = max; } /** * Subscribe to a channel using PubNub to listen for calls. * @param channel The channel to listen on, your "phone number" */ public void listenOn(String channel){ this.pcClient.listenOn(channel); } /** * Connect with another user by their ID. * @param userId The user to establish a WebRTC connection with */ public void connect(String userId){ this.pcClient.connect(userId); } /** * Close a single peer connection. Send a PubNub hangup signal as well * @param userId User to close a connection with */ public void closeConnection(String userId){ this.pcClient.closeConnection(userId); } /** * Close all peer connections. Send a PubNub hangup signal as well. */ public void closeAllConnections(){ this.pcClient.closeAllConnections(); } /** * Send a custom JSONObject user message to a single peer. * @param userId user to send a message to * @param message the JSON message to pass to a peer. */ public void transmit(String userId, JSONObject message){ JSONObject usrMsgJson = new JSONObject(); try { usrMsgJson.put(PnRTCMessage.JSON_USERMSG, message); this.pcClient.transmitMessage(userId, usrMsgJson); } catch (JSONException e){ e.printStackTrace(); } } /** * Send a custom JSONObject user message to all peers. * @param message the JSON message to pass to all peers. */ public void transmitAll(JSONObject message){ List
* Author: Kevin Gleason - Boston College '16 * File: PnRTCListener.java * Date: 7/20/15 * Use: Callback listener for various WebRTC events * © 2009 - 2015 PubNub, Inc. **
* Implement this interface to be notified of WebRTC events. * It is an abstract class with default behaviors of doing nothing. * Use a PnRTCListener to implement the various callbacks of your WebRTC application. *
*/ public abstract class PnRTCListener{ public void onCallReady(String callId){} // TODO: Maybe not needed? /** * Called in {@link com.pubnub.api.Pubnub} object's subscribe connected callback. * Means that you are ready to receive calls. * @param userId The channel you are subscribed to, the userId you may be called on. */ public void onConnected(String userId){} /** * Peer status changed. {@link PnPeer} status changed, can be * CONNECTING, CONNECTED, or DISCONNECTED. * @param peer The peer object, can use to check peer.getStatus() */ public void onPeerStatusChanged(PnPeer peer){} /**TODO: Is this different than onPeerStatusChanged == DISCONNECTED? * Called when a hangup occurs. * @param peer The peer who was hung up on, or who hung up on you */ public void onPeerConnectionClosed(PnPeer peer){} /** * Called in {@link PnPeerConnectionClient} when setLocalStream * is invoked. * @param localStream The users local stream from Android's front or back camera. */ public void onLocalStream(MediaStream localStream){} /** * Called when a remote stream is added in the {@link org.webrtc.PeerConnection.Observer} * in {@link PnPeer}. * @param remoteStream The remote stream that was added * @param peer The peer that added the remote stream * Todo: Maybe not the right peer? */ public void onAddRemoteStream(MediaStream remoteStream, PnPeer peer){} /** * Called in the {@link org.webrtc.PeerConnection.Observer} implemented * by {@link PnPeer}. * @param remoteStream The stream that was removed by your peer * @param peer The peer that removed the stream. */ public void onRemoveRemoteStream(MediaStream remoteStream, PnPeer peer){} /** * Called when a user message is send via {@link com.pubnub.api.Pubnub} object. * @param peer The peer who sent the message * @param message The {@link org.json.JSONObject} message sent by the user. */ public void onMessage(PnPeer peer, Object message){} /** * A helpful debugging callback for testing and developing your app. * @param message The {@link PnRTCMessage} debug message. */ public void onDebug(PnRTCMessage message){} } ================================================ FILE: pnwebrtc/src/main/java/me/kevingleason/pnwebrtc/PnRTCMessage.java ================================================ package me.kevingleason.pnwebrtc; import org.json.JSONException; import org.json.JSONObject; /** ** Author: Kevin Gleason - Boston College '16 * File: PnRTCMessage.java * Date: 7/20/15 * Use: Debug messages and JSON key definitions * © 2009 - 2015 PubNub, Inc. **/ public class PnRTCMessage extends JSONObject { public static final String JSON_TYPE = "type"; public static final String JSON_PACKET = "packet"; public static final String JSON_ID = "id"; public static final String JSON_NUMBER = "number"; // Todo: Change to more accurate name. public static final String JSON_MESSAGE = "message"; public static final String JSON_USERMSG = "usermsg"; public static final String JSON_HANGUP = "hangup"; public static final String JSON_THUMBNAIL = "thumbnail"; public static final String JSON_SDP = "sdp"; public static final String JSON_ICE = "candidate"; // Identify ICE private String message; private JSONObject json; public PnRTCMessage(String message){ super(); try { this.put(JSON_MESSAGE, message); } catch (JSONException e){ throw new RuntimeException("Invalid JSON Payload"); } this.message = message; this.json = this; } public PnRTCMessage(JSONObject json){ super(); try { if (json==null){ json = new JSONObject("{error:1}"); } this.put(JSON_MESSAGE, json); } catch (JSONException e){ throw new RuntimeException("Invalid JSON Payload"); } this.message = json.toString(); this.json = this; } public String getMessage() { return message; } public JSONObject getJSON(){ return this.json; } } ================================================ FILE: pnwebrtc/src/main/java/me/kevingleason/pnwebrtc/PnSignalingParams.java ================================================ package me.kevingleason.pnwebrtc; import org.webrtc.MediaConstraints; import org.webrtc.PeerConnection; import java.util.ArrayList; import java.util.List; /** *
* Author: Kevin Gleason - Boston College '16 * File: PnSignalingParams.java * Date: 7/20/15 * Use: Hold the signaling parameters of a WebRTC PeerConnection * © 2009 - 2015 PubNub, Inc. **
IceServers allow Trickling, so they are not final.
* */ public class PnSignalingParams { public List