├── app
├── .gitignore
├── libs
│ └── autobanh.jar
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── drawable-hdpi
│ │ │ │ ├── disconnect.png
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_loopback_call.png
│ │ │ │ ├── ic_action_full_screen.png
│ │ │ │ └── ic_action_return_from_full_screen.png
│ │ │ ├── drawable-ldpi
│ │ │ │ ├── disconnect.png
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_loopback_call.png
│ │ │ │ ├── ic_action_full_screen.png
│ │ │ │ └── ic_action_return_from_full_screen.png
│ │ │ ├── drawable-mdpi
│ │ │ │ ├── disconnect.png
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_loopback_call.png
│ │ │ │ ├── ic_action_full_screen.png
│ │ │ │ └── ic_action_return_from_full_screen.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ ├── disconnect.png
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_loopback_call.png
│ │ │ │ ├── ic_action_full_screen.png
│ │ │ │ └── ic_action_return_from_full_screen.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── styles.xml
│ │ │ │ ├── arrays.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── values-v17
│ │ │ │ └── styles.xml
│ │ │ ├── values-v21
│ │ │ │ └── styles.xml
│ │ │ ├── menu
│ │ │ │ └── connect_menu.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_call.xml
│ │ │ │ ├── activity_connect.xml
│ │ │ │ ├── fragment_hud.xml
│ │ │ │ └── fragment_call.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ │ └── xml
│ │ │ │ └── preferences.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── someshk
│ │ │ │ └── apprtc
│ │ │ │ ├── SettingsFragment.java
│ │ │ │ ├── util
│ │ │ │ ├── AppRTCUtils.java
│ │ │ │ └── AsyncHttpURLConnection.java
│ │ │ │ ├── RtcEventLog.java
│ │ │ │ ├── UnhandledExceptionHandler.java
│ │ │ │ ├── CaptureQualityController.java
│ │ │ │ ├── AppRTCClient.java
│ │ │ │ ├── CallFragment.java
│ │ │ │ ├── RecordedAudioToFileController.java
│ │ │ │ ├── AppRTCProximitySensor.java
│ │ │ │ ├── HudFragment.java
│ │ │ │ ├── RoomParametersFetcher.java
│ │ │ │ ├── WebSocketChannelClient.java
│ │ │ │ ├── TCPChannelClient.java
│ │ │ │ ├── DirectRTCClient.java
│ │ │ │ ├── SettingsActivity.java
│ │ │ │ └── WebSocketRTCClient.java
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── someshk
│ │ │ └── apprtc
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── someshk
│ │ └── apprtc
│ │ └── ExampleInstrumentedTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── OWNERS
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── .idea
├── encodings.xml
├── modules.xml
├── runConfigurations.xml
├── gradle.xml
├── codeStyles
│ └── Project.xml
└── misc.xml
├── README
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
--------------------------------------------------------------------------------
/OWNERS:
--------------------------------------------------------------------------------
1 | magjed@webrtc.org
2 | sakal@webrtc.org
3 |
4 | per-file *.py=phoglund@webrtc.org
5 |
--------------------------------------------------------------------------------
/app/libs/autobanh.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/libs/autobanh.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/disconnect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-hdpi/disconnect.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-ldpi/disconnect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-ldpi/disconnect.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-ldpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-ldpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/disconnect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-mdpi/disconnect.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/disconnect.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-xhdpi/disconnect.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_loopback_call.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-hdpi/ic_loopback_call.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-ldpi/ic_loopback_call.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-ldpi/ic_loopback_call.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_loopback_call.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-mdpi/ic_loopback_call.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_loopback_call.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-xhdpi/ic_loopback_call.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/ic_action_full_screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-hdpi/ic_action_full_screen.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-ldpi/ic_action_full_screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-ldpi/ic_action_full_screen.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/ic_action_full_screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-mdpi/ic_action_full_screen.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/ic_action_full_screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheSomeshKumar/AndroidWebRTCGradle/HEAD/app/src/main/res/drawable-xhdpi/ic_action_full_screen.png
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
Methods are guaranteed to be invoked on the UI thread of |activity|.
107 | */
108 | interface SignalingEvents {
109 | /**
110 | * Callback fired once the room's signaling parameters
111 | * SignalingParameters are extracted.
112 | */
113 | void onConnectedToRoom(final SignalingParameters params);
114 |
115 | /**
116 | * Callback fired once remote SDP is received.
117 | */
118 | void onRemoteDescription(final SessionDescription sdp);
119 |
120 | /**
121 | * Callback fired once remote Ice candidate is received.
122 | */
123 | void onRemoteIceCandidate(final IceCandidate candidate);
124 |
125 | /**
126 | * Callback fired once remote Ice candidate removals are received.
127 | */
128 | void onRemoteIceCandidatesRemoved(final IceCandidate[] candidates);
129 |
130 | /**
131 | * Callback fired once channel is closed.
132 | */
133 | void onChannelClose();
134 |
135 | /**
136 | * Callback fired once channel error happened.
137 | */
138 | void onChannelError(final String description);
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/java/com/someshk/apprtc/CallFragment.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2015 The WebRTC Project Authors. All rights reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree. An additional intellectual property rights grant can be found
7 | * in the file PATENTS. All contributing project authors may
8 | * be found in the AUTHORS file in the root of the source tree.
9 | */
10 |
11 | package com.someshk.apprtc;
12 |
13 | import android.app.Activity;
14 | import android.app.Fragment;
15 | import android.os.Bundle;
16 | import android.view.LayoutInflater;
17 | import android.view.View;
18 | import android.view.ViewGroup;
19 | import android.widget.ImageButton;
20 | import android.widget.SeekBar;
21 | import android.widget.TextView;
22 |
23 | import org.webrtc.RendererCommon.ScalingType;
24 |
25 | /**
26 | * Fragment for call control.
27 | */
28 | public class CallFragment extends Fragment {
29 | private TextView contactView;
30 | private ImageButton cameraSwitchButton;
31 | private ImageButton videoScalingButton;
32 | private ImageButton toggleMuteButton;
33 | private TextView captureFormatText;
34 | private SeekBar captureFormatSlider;
35 | public OnCallEvents callEvents;
36 | private ScalingType scalingType;
37 | private boolean videoCallEnabled = true;
38 |
39 | /**
40 | * Call control interface for container activity.
41 | */
42 | public interface OnCallEvents {
43 | void onCallHangUp();
44 |
45 | void onCameraSwitch();
46 |
47 | void onVideoScalingSwitch(ScalingType scalingType);
48 |
49 | void onCaptureFormatChange(int width, int height, int framerate);
50 |
51 | boolean onToggleMic();
52 | }
53 |
54 | @Override
55 | public View onCreateView(
56 | LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
57 | View controlView = inflater.inflate(R.layout.fragment_call, container, false);
58 | // Create UI controls.
59 | contactView = controlView.findViewById(R.id.contact_name_call);
60 | ImageButton disconnectButton = controlView.findViewById(R.id.button_call_disconnect);
61 | cameraSwitchButton = controlView.findViewById(R.id.button_call_switch_camera);
62 | videoScalingButton = controlView.findViewById(R.id.button_call_scaling_mode);
63 | toggleMuteButton = controlView.findViewById(R.id.button_call_toggle_mic);
64 | captureFormatText = controlView.findViewById(R.id.capture_format_text_call);
65 | captureFormatSlider = controlView.findViewById(R.id.capture_format_slider_call);
66 | // Add buttons click events.
67 | disconnectButton.setOnClickListener(new View.OnClickListener() {
68 | @Override
69 | public void onClick(View view) {
70 | callEvents.onCallHangUp();
71 | }
72 | });
73 | cameraSwitchButton.setOnClickListener(new View.OnClickListener() {
74 | @Override
75 | public void onClick(View view) {
76 | callEvents.onCameraSwitch();
77 | }
78 | });
79 | videoScalingButton.setOnClickListener(new View.OnClickListener() {
80 | @Override
81 | public void onClick(View view) {
82 | if (scalingType == ScalingType.SCALE_ASPECT_FILL) {
83 | videoScalingButton.setBackgroundResource(R.drawable.ic_action_full_screen);
84 | scalingType = ScalingType.SCALE_ASPECT_FIT;
85 | } else {
86 | videoScalingButton.setBackgroundResource(R.drawable.ic_action_return_from_full_screen);
87 | scalingType = ScalingType.SCALE_ASPECT_FILL;
88 | }
89 | callEvents.onVideoScalingSwitch(scalingType);
90 | }
91 | });
92 | scalingType = ScalingType.SCALE_ASPECT_FILL;
93 | toggleMuteButton.setOnClickListener(new View.OnClickListener() {
94 | @Override
95 | public void onClick(View view) {
96 | boolean enabled = callEvents.onToggleMic();
97 | toggleMuteButton.setAlpha(enabled ? 1.0f : 0.3f);
98 | }
99 | });
100 | return controlView;
101 | }
102 |
103 | @Override
104 | public void onStart() {
105 | super.onStart();
106 | boolean captureSliderEnabled = false;
107 | Bundle args = getArguments();
108 | if (args != null) {
109 | String contactName = args.getString(CallActivity.EXTRA_ROOMID);
110 | contactView.setText(contactName);
111 | videoCallEnabled = args.getBoolean(CallActivity.EXTRA_VIDEO_CALL, true);
112 | captureSliderEnabled = videoCallEnabled
113 | && args.getBoolean(CallActivity.EXTRA_VIDEO_CAPTUREQUALITYSLIDER_ENABLED, false);
114 | }
115 | if (!videoCallEnabled) {
116 | cameraSwitchButton.setVisibility(View.INVISIBLE);
117 | }
118 | if (captureSliderEnabled) {
119 | captureFormatSlider.setOnSeekBarChangeListener(
120 | new CaptureQualityController(captureFormatText, callEvents));
121 | } else {
122 | captureFormatText.setVisibility(View.GONE);
123 | captureFormatSlider.setVisibility(View.GONE);
124 | }
125 | }
126 |
127 | // TODO(sakal): Replace with onAttach(Context) once we only support API level 23+.
128 | @SuppressWarnings("deprecation")
129 | @Override
130 | public void onAttach(Activity activity) {
131 | super.onAttach(activity);
132 | callEvents = (OnCallEvents) activity;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 | All public methods should be called from a looper executor thread
37 | * passed in a constructor, otherwise exception will be thrown.
38 | * All events are dispatched on the same thread.
39 | */
40 | public class WebSocketChannelClient {
41 | private static final String TAG = "WSChannelRTCClient";
42 | private static final int CLOSE_TIMEOUT = 1000;
43 | private final WebSocketChannelEvents events;
44 | private final Handler handler;
45 | private WebSocketConnection ws;
46 | private String wsServerUrl;
47 | private String postServerUrl;
48 | @Nullable
49 | private String roomID;
50 | @Nullable
51 | private String clientID;
52 | private WebSocketConnectionState state;
53 | // Do not remove this member variable. If this is removed, the observer gets garbage collected and
54 | // this causes test breakages.
55 | private WebSocketObserver wsObserver;
56 | private final Object closeEventLock = new Object();
57 | private boolean closeEvent;
58 | // WebSocket send queue. Messages are added to the queue when WebSocket
59 | // client is not registered and are consumed in register() call.
60 | private final List
34 | * All public methods should be called from a looper executor thread
35 | * passed in a constructor, otherwise exception will be thrown.
36 | * All events are dispatched on the same thread.
37 | */
38 | public class TCPChannelClient {
39 | private static final String TAG = "TCPChannelClient";
40 | private final ExecutorService executor;
41 | private final ThreadUtils.ThreadChecker executorThreadCheck;
42 | private final TCPChannelEvents eventListener;
43 | private TCPSocket socket;
44 |
45 | /**
46 | * Callback interface for messages delivered on TCP Connection. All callbacks are invoked from the
47 | * looper executor thread.
48 | */
49 | public interface TCPChannelEvents {
50 | void onTCPConnected(boolean server);
51 |
52 | void onTCPMessage(String message);
53 |
54 | void onTCPError(String description);
55 |
56 | void onTCPClose();
57 | }
58 |
59 | /**
60 | * Initializes the TCPChannelClient. If IP is a local IP address, starts a listening server on
61 | * that IP. If not, instead connects to the IP.
62 | *
63 | * @param eventListener Listener that will receive events from the client.
64 | * @param ip IP address to listen on or connect to.
65 | * @param port Port to listen on or connect to.
66 | */
67 | public TCPChannelClient(
68 | ExecutorService executor, TCPChannelEvents eventListener, String ip, int port) {
69 | this.executor = executor;
70 | executorThreadCheck = new ThreadUtils.ThreadChecker();
71 | executorThreadCheck.detachThread();
72 | this.eventListener = eventListener;
73 | InetAddress address;
74 | try {
75 | address = InetAddress.getByName(ip);
76 | } catch (UnknownHostException e) {
77 | reportError("Invalid IP address.");
78 | return;
79 | }
80 | if (address.isAnyLocalAddress()) {
81 | socket = new TCPSocketServer(address, port);
82 | } else {
83 | socket = new TCPSocketClient(address, port);
84 | }
85 | socket.start();
86 | }
87 |
88 | /**
89 | * Disconnects the client if not already disconnected. This will fire the onTCPClose event.
90 | */
91 | public void disconnect() {
92 | executorThreadCheck.checkIsOnValidThread();
93 | socket.disconnect();
94 | }
95 |
96 | /**
97 | * Sends a message on the socket.
98 | *
99 | * @param message Message to be sent.
100 | */
101 | public void send(String message) {
102 | executorThreadCheck.checkIsOnValidThread();
103 | socket.send(message);
104 | }
105 |
106 | /**
107 | * Helper method for firing onTCPError events. Calls onTCPError on the executor thread.
108 | */
109 | private void reportError(final String message) {
110 | Log.e(TAG, "TCP Error: " + message);
111 | executor.execute(new Runnable() {
112 | @Override
113 | public void run() {
114 | eventListener.onTCPError(message);
115 | }
116 | });
117 | }
118 |
119 | /**
120 | * Base class for server and client sockets. Contains a listening thread that will call
121 | * eventListener.onTCPMessage on new messages.
122 | */
123 | private abstract class TCPSocket extends Thread {
124 | // Lock for editing out and rawSocket
125 | protected final Object rawSocketLock;
126 | @Nullable
127 | private PrintWriter out;
128 | @Nullable
129 | private Socket rawSocket;
130 |
131 | /**
132 | * Connect to the peer, potentially a slow operation.
133 | *
134 | * @return Socket connection, null if connection failed.
135 | */
136 | @Nullable
137 | public abstract Socket connect();
138 |
139 | /**
140 | * Returns true if sockets is a server rawSocket.
141 | */
142 | public abstract boolean isServer();
143 |
144 | TCPSocket() {
145 | rawSocketLock = new Object();
146 | }
147 |
148 | /**
149 | * The listening thread.
150 | */
151 | @Override
152 | public void run() {
153 | Log.d(TAG, "Listening thread started...");
154 | // Receive connection to temporary variable first, so we don't block.
155 | Socket tempSocket = connect();
156 | BufferedReader in;
157 | Log.d(TAG, "TCP connection established.");
158 | synchronized (rawSocketLock) {
159 | if (rawSocket != null) {
160 | Log.e(TAG, "Socket already existed and will be replaced.");
161 | }
162 | rawSocket = tempSocket;
163 | // Connecting failed, error has already been reported, just exit.
164 | if (rawSocket == null) {
165 | return;
166 | }
167 | try {
168 | out = new PrintWriter(
169 | new OutputStreamWriter(rawSocket.getOutputStream(), Charset.forName("UTF-8")), true);
170 | in = new BufferedReader(
171 | new InputStreamReader(rawSocket.getInputStream(), Charset.forName("UTF-8")));
172 | } catch (IOException e) {
173 | reportError("Failed to open IO on rawSocket: " + e.getMessage());
174 | return;
175 | }
176 | }
177 | Log.v(TAG, "Execute onTCPConnected");
178 | executor.execute(new Runnable() {
179 | @Override
180 | public void run() {
181 | Log.v(TAG, "Run onTCPConnected");
182 | eventListener.onTCPConnected(isServer());
183 | }
184 | });
185 | while (true) {
186 | final String message;
187 | try {
188 | message = in.readLine();
189 | } catch (IOException e) {
190 | synchronized (rawSocketLock) {
191 | // If socket was closed, this is expected.
192 | if (rawSocket == null) {
193 | break;
194 | }
195 | }
196 | reportError("Failed to read from rawSocket: " + e.getMessage());
197 | break;
198 | }
199 | // No data received, rawSocket probably closed.
200 | if (message == null) {
201 | break;
202 | }
203 | executor.execute(new Runnable() {
204 | @Override
205 | public void run() {
206 | Log.v(TAG, "Receive: " + message);
207 | eventListener.onTCPMessage(message);
208 | }
209 | });
210 | }
211 | Log.d(TAG, "Receiving thread exiting...");
212 | // Close the rawSocket if it is still open.
213 | disconnect();
214 | }
215 |
216 | /**
217 | * Closes the rawSocket if it is still open. Also fires the onTCPClose event.
218 | */
219 | public void disconnect() {
220 | try {
221 | synchronized (rawSocketLock) {
222 | if (rawSocket != null) {
223 | rawSocket.close();
224 | rawSocket = null;
225 | out = null;
226 | executor.execute(new Runnable() {
227 | @Override
228 | public void run() {
229 | eventListener.onTCPClose();
230 | }
231 | });
232 | }
233 | }
234 | } catch (IOException e) {
235 | reportError("Failed to close rawSocket: " + e.getMessage());
236 | }
237 | }
238 |
239 | /**
240 | * Sends a message on the socket. Should only be called on the executor thread.
241 | */
242 | public void send(String message) {
243 | Log.v(TAG, "Send: " + message);
244 | synchronized (rawSocketLock) {
245 | if (out == null) {
246 | reportError("Sending data on closed socket.");
247 | return;
248 | }
249 | out.write(message + "\n");
250 | out.flush();
251 | }
252 | }
253 | }
254 |
255 | private class TCPSocketServer extends TCPSocket {
256 | // Server socket is also guarded by rawSocketLock.
257 | @Nullable
258 | private ServerSocket serverSocket;
259 | final private InetAddress address;
260 | final private int port;
261 |
262 | public TCPSocketServer(InetAddress address, int port) {
263 | this.address = address;
264 | this.port = port;
265 | }
266 |
267 | /**
268 | * Opens a listening socket and waits for a connection.
269 | */
270 | @Nullable
271 | @Override
272 | public Socket connect() {
273 | Log.d(TAG, "Listening on [" + address.getHostAddress() + "]:" + Integer.toString(port));
274 | final ServerSocket tempSocket;
275 | try {
276 | tempSocket = new ServerSocket(port, 0, address);
277 | } catch (IOException e) {
278 | reportError("Failed to create server socket: " + e.getMessage());
279 | return null;
280 | }
281 | synchronized (rawSocketLock) {
282 | if (serverSocket != null) {
283 | Log.e(TAG, "Server rawSocket was already listening and new will be opened.");
284 | }
285 | serverSocket = tempSocket;
286 | }
287 | try {
288 | return tempSocket.accept();
289 | } catch (IOException e) {
290 | reportError("Failed to receive connection: " + e.getMessage());
291 | return null;
292 | }
293 | }
294 |
295 | /**
296 | * Closes the listening socket and calls super.
297 | */
298 | @Override
299 | public void disconnect() {
300 | try {
301 | synchronized (rawSocketLock) {
302 | if (serverSocket != null) {
303 | serverSocket.close();
304 | serverSocket = null;
305 | }
306 | }
307 | } catch (IOException e) {
308 | reportError("Failed to close server socket: " + e.getMessage());
309 | }
310 | super.disconnect();
311 | }
312 |
313 | @Override
314 | public boolean isServer() {
315 | return true;
316 | }
317 | }
318 |
319 | private class TCPSocketClient extends TCPSocket {
320 | final private InetAddress address;
321 | final private int port;
322 |
323 | public TCPSocketClient(InetAddress address, int port) {
324 | this.address = address;
325 | this.port = port;
326 | }
327 |
328 | /**
329 | * Connects to the peer.
330 | */
331 | @Nullable
332 | @Override
333 | public Socket connect() {
334 | Log.d(TAG, "Connecting to [" + address.getHostAddress() + "]:" + Integer.toString(port));
335 | try {
336 | return new Socket(address, port);
337 | } catch (IOException e) {
338 | reportError("Failed to connect: " + e.getMessage());
339 | return null;
340 | }
341 | }
342 |
343 | @Override
344 | public boolean isServer() {
345 | return false;
346 | }
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/app/src/main/java/com/someshk/apprtc/DirectRTCClient.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 The WebRTC Project Authors. All rights reserved.
3 | *
4 | * Use of this source code is governed by a BSD-style license
5 | * that can be found in the LICENSE file in the root of the source
6 | * tree. An additional intellectual property rights grant can be found
7 | * in the file PATENTS. All contributing project authors may
8 | * be found in the AUTHORS file in the root of the source tree.
9 | */
10 |
11 | package com.someshk.apprtc;
12 |
13 | import androidx.annotation.Nullable;
14 | import android.util.Log;
15 |
16 | import org.json.JSONArray;
17 | import org.json.JSONException;
18 | import org.json.JSONObject;
19 | import org.webrtc.IceCandidate;
20 | import org.webrtc.SessionDescription;
21 |
22 | import java.util.ArrayList;
23 | import java.util.concurrent.ExecutorService;
24 | import java.util.concurrent.Executors;
25 | import java.util.regex.Matcher;
26 | import java.util.regex.Pattern;
27 |
28 | /**
29 | * Implementation of AppRTCClient that uses direct TCP connection as the signaling channel.
30 | * This eliminates the need for an external server. This class does not support loopback
31 | * connections.
32 | */
33 | public class DirectRTCClient implements AppRTCClient, TCPChannelClient.TCPChannelEvents {
34 | private static final String TAG = "DirectRTCClient";
35 | private static final int DEFAULT_PORT = 8888;
36 | // Regex pattern used for checking if room id looks like an IP.
37 | static final Pattern IP_PATTERN = Pattern.compile("("
38 | // IPv4
39 | + "((\\d+\\.){3}\\d+)|"
40 | // IPv6
41 | + "\\[((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::"
42 | + "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)\\]|"
43 | + "\\[(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})\\]|"
44 | // IPv6 without []
45 | + "((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)|"
46 | + "(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|"
47 | // Literals
48 | + "localhost"
49 | + ")"
50 | // Optional port number
51 | + "(:(\\d+))?");
52 | private final ExecutorService executor;
53 | private final SignalingEvents events;
54 | @Nullable
55 | private TCPChannelClient tcpClient;
56 | private RoomConnectionParameters connectionParameters;
57 |
58 | private enum ConnectionState {NEW, CONNECTED, CLOSED, ERROR}
59 |
60 | // All alterations of the room state should be done from inside the looper thread.
61 | private ConnectionState roomState;
62 |
63 | public DirectRTCClient(SignalingEvents events) {
64 | this.events = events;
65 | executor = Executors.newSingleThreadExecutor();
66 | roomState = ConnectionState.NEW;
67 | }
68 |
69 | /**
70 | * Connects to the room, roomId in connectionsParameters is required. roomId must be a valid
71 | * IP address matching IP_PATTERN.
72 | */
73 | @Override
74 | public void connectToRoom(RoomConnectionParameters connectionParameters) {
75 | this.connectionParameters = connectionParameters;
76 | if (connectionParameters.loopback) {
77 | reportError("Loopback connections aren't supported by DirectRTCClient.");
78 | }
79 | executor.execute(new Runnable() {
80 | @Override
81 | public void run() {
82 | connectToRoomInternal();
83 | }
84 | });
85 | }
86 |
87 | @Override
88 | public void disconnectFromRoom() {
89 | executor.execute(new Runnable() {
90 | @Override
91 | public void run() {
92 | disconnectFromRoomInternal();
93 | }
94 | });
95 | }
96 |
97 | /**
98 | * Connects to the room.
99 | *
100 | * Runs on the looper thread.
101 | */
102 | private void connectToRoomInternal() {
103 | this.roomState = ConnectionState.NEW;
104 | String endpoint = connectionParameters.roomId;
105 | Matcher matcher = IP_PATTERN.matcher(endpoint);
106 | if (!matcher.matches()) {
107 | reportError("roomId must match IP_PATTERN for DirectRTCClient.");
108 | return;
109 | }
110 | String ip = matcher.group(1);
111 | String portStr = matcher.group(matcher.groupCount());
112 | int port;
113 | if (portStr != null) {
114 | try {
115 | port = Integer.parseInt(portStr);
116 | } catch (NumberFormatException e) {
117 | reportError("Invalid port number: " + portStr);
118 | return;
119 | }
120 | } else {
121 | port = DEFAULT_PORT;
122 | }
123 | tcpClient = new TCPChannelClient(executor, this, ip, port);
124 | }
125 |
126 | /**
127 | * Disconnects from the room.
128 | *
129 | * Runs on the looper thread.
130 | */
131 | private void disconnectFromRoomInternal() {
132 | roomState = ConnectionState.CLOSED;
133 | if (tcpClient != null) {
134 | tcpClient.disconnect();
135 | tcpClient = null;
136 | }
137 | executor.shutdown();
138 | }
139 |
140 | @Override
141 | public void sendOfferSdp(final SessionDescription sdp) {
142 | executor.execute(new Runnable() {
143 | @Override
144 | public void run() {
145 | if (roomState != ConnectionState.CONNECTED) {
146 | reportError("Sending offer SDP in non connected state.");
147 | return;
148 | }
149 | JSONObject json = new JSONObject();
150 | jsonPut(json, "sdp", sdp.description);
151 | jsonPut(json, "type", "offer");
152 | sendMessage(json.toString());
153 | }
154 | });
155 | }
156 |
157 | @Override
158 | public void sendAnswerSdp(final SessionDescription sdp) {
159 | executor.execute(new Runnable() {
160 | @Override
161 | public void run() {
162 | JSONObject json = new JSONObject();
163 | jsonPut(json, "sdp", sdp.description);
164 | jsonPut(json, "type", "answer");
165 | sendMessage(json.toString());
166 | }
167 | });
168 | }
169 |
170 | @Override
171 | public void sendLocalIceCandidate(final IceCandidate candidate) {
172 | executor.execute(new Runnable() {
173 | @Override
174 | public void run() {
175 | JSONObject json = new JSONObject();
176 | jsonPut(json, "type", "candidate");
177 | jsonPut(json, "label", candidate.sdpMLineIndex);
178 | jsonPut(json, "id", candidate.sdpMid);
179 | jsonPut(json, "candidate", candidate.sdp);
180 | if (roomState != ConnectionState.CONNECTED) {
181 | reportError("Sending ICE candidate in non connected state.");
182 | return;
183 | }
184 | sendMessage(json.toString());
185 | }
186 | });
187 | }
188 |
189 | /**
190 | * Send removed Ice candidates to the other participant.
191 | */
192 | @Override
193 | public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) {
194 | executor.execute(new Runnable() {
195 | @Override
196 | public void run() {
197 | JSONObject json = new JSONObject();
198 | jsonPut(json, "type", "remove-candidates");
199 | JSONArray jsonArray = new JSONArray();
200 | for (final IceCandidate candidate : candidates) {
201 | jsonArray.put(toJsonCandidate(candidate));
202 | }
203 | jsonPut(json, "candidates", jsonArray);
204 | if (roomState != ConnectionState.CONNECTED) {
205 | reportError("Sending ICE candidate removals in non connected state.");
206 | return;
207 | }
208 | sendMessage(json.toString());
209 | }
210 | });
211 | }
212 | // -------------------------------------------------------------------
213 | // TCPChannelClient event handlers
214 |
215 | /**
216 | * If the client is the server side, this will trigger onConnectedToRoom.
217 | */
218 | @Override
219 | public void onTCPConnected(boolean isServer) {
220 | if (isServer) {
221 | roomState = ConnectionState.CONNECTED;
222 | SignalingParameters parameters = new SignalingParameters(
223 | // Ice servers are not needed for direct connections.
224 | new ArrayList<>(),
225 | isServer, // Server side acts as the initiator on direct connections.
226 | null, // clientId
227 | null, // wssUrl
228 | null, // wwsPostUrl
229 | null, // offerSdp
230 | null // iceCandidates
231 | );
232 | events.onConnectedToRoom(parameters);
233 | }
234 | }
235 |
236 | @Override
237 | public void onTCPMessage(String msg) {
238 | try {
239 | JSONObject json = new JSONObject(msg);
240 | String type = json.optString("type");
241 | if (type.equals("candidate")) {
242 | events.onRemoteIceCandidate(toJavaCandidate(json));
243 | } else if (type.equals("remove-candidates")) {
244 | JSONArray candidateArray = json.getJSONArray("candidates");
245 | IceCandidate[] candidates = new IceCandidate[candidateArray.length()];
246 | for (int i = 0; i < candidateArray.length(); ++i) {
247 | candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i));
248 | }
249 | events.onRemoteIceCandidatesRemoved(candidates);
250 | } else if (type.equals("answer")) {
251 | SessionDescription sdp = new SessionDescription(
252 | SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp"));
253 | events.onRemoteDescription(sdp);
254 | } else if (type.equals("offer")) {
255 | SessionDescription sdp = new SessionDescription(
256 | SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp"));
257 | SignalingParameters parameters = new SignalingParameters(
258 | // Ice servers are not needed for direct connections.
259 | new ArrayList<>(),
260 | false, // This code will only be run on the client side. So, we are not the initiator.
261 | null, // clientId
262 | null, // wssUrl
263 | null, // wssPostUrl
264 | sdp, // offerSdp
265 | null // iceCandidates
266 | );
267 | roomState = ConnectionState.CONNECTED;
268 | events.onConnectedToRoom(parameters);
269 | } else {
270 | reportError("Unexpected TCP message: " + msg);
271 | }
272 | } catch (JSONException e) {
273 | reportError("TCP message JSON parsing error: " + e.toString());
274 | }
275 | }
276 |
277 | @Override
278 | public void onTCPError(String description) {
279 | reportError("TCP connection error: " + description);
280 | }
281 |
282 | @Override
283 | public void onTCPClose() {
284 | events.onChannelClose();
285 | }
286 |
287 | // --------------------------------------------------------------------
288 | // Helper functions.
289 | private void reportError(final String errorMessage) {
290 | Log.e(TAG, errorMessage);
291 | executor.execute(new Runnable() {
292 | @Override
293 | public void run() {
294 | if (roomState != ConnectionState.ERROR) {
295 | roomState = ConnectionState.ERROR;
296 | events.onChannelError(errorMessage);
297 | }
298 | }
299 | });
300 | }
301 |
302 | private void sendMessage(final String message) {
303 | executor.execute(new Runnable() {
304 | @Override
305 | public void run() {
306 | tcpClient.send(message);
307 | }
308 | });
309 | }
310 |
311 | // Put a |key|->|value| mapping in |json|.
312 | private static void jsonPut(JSONObject json, String key, Object value) {
313 | try {
314 | json.put(key, value);
315 | } catch (JSONException e) {
316 | throw new RuntimeException(e);
317 | }
318 | }
319 |
320 | // Converts a Java candidate to a JSONObject.
321 | private static JSONObject toJsonCandidate(final IceCandidate candidate) {
322 | JSONObject json = new JSONObject();
323 | jsonPut(json, "label", candidate.sdpMLineIndex);
324 | jsonPut(json, "id", candidate.sdpMid);
325 | jsonPut(json, "candidate", candidate.sdp);
326 | return json;
327 | }
328 |
329 | // Converts a JSON candidate to a Java object.
330 | private static IceCandidate toJavaCandidate(JSONObject json) throws JSONException {
331 | return new IceCandidate(
332 | json.getString("id"), json.getInt("label"), json.getString("candidate"));
333 | }
334 | }
335 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | To use: create an instance of this object (registering a message handler) and
36 | * call connectToRoom(). Once room connection is established
37 | * onConnectedToRoom() callback with room parameters is invoked.
38 | * Messages to other party (with local Ice candidates and answer SDP) can
39 | * be sent after WebSocket connection is established.
40 | */
41 | public class WebSocketRTCClient implements AppRTCClient, WebSocketChannelEvents {
42 | private static final String TAG = "WSRTCClient";
43 | private static final String ROOM_JOIN = "join";
44 | private static final String ROOM_MESSAGE = "message";
45 | private static final String ROOM_LEAVE = "leave";
46 |
47 | private enum ConnectionState {NEW, CONNECTED, CLOSED, ERROR}
48 |
49 | private enum MessageType {MESSAGE, LEAVE}
50 |
51 | private final Handler handler;
52 | private boolean initiator;
53 | private SignalingEvents events;
54 | private WebSocketChannelClient wsClient;
55 | private ConnectionState roomState;
56 | private RoomConnectionParameters connectionParameters;
57 | private String messageUrl;
58 | private String leaveUrl;
59 |
60 | public WebSocketRTCClient(SignalingEvents events) {
61 | this.events = events;
62 | roomState = ConnectionState.NEW;
63 | final HandlerThread handlerThread = new HandlerThread(TAG);
64 | handlerThread.start();
65 | handler = new Handler(handlerThread.getLooper());
66 | }
67 |
68 | // --------------------------------------------------------------------
69 | // AppRTCClient interface implementation.
70 | // Asynchronously connect to an AppRTC room URL using supplied connection
71 | // parameters, retrieves room parameters and connect to WebSocket server.
72 | @Override
73 | public void connectToRoom(RoomConnectionParameters connectionParameters) {
74 | this.connectionParameters = connectionParameters;
75 | handler.post(new Runnable() {
76 | @Override
77 | public void run() {
78 | connectToRoomInternal();
79 | }
80 | });
81 | }
82 |
83 | @Override
84 | public void disconnectFromRoom() {
85 | handler.post(new Runnable() {
86 | @Override
87 | public void run() {
88 | disconnectFromRoomInternal();
89 | handler.getLooper().quit();
90 | }
91 | });
92 | }
93 |
94 | // Connects to room - function runs on a local looper thread.
95 | private void connectToRoomInternal() {
96 | String connectionUrl = getConnectionUrl(connectionParameters);
97 | Log.d(TAG, "Connect to room: " + connectionUrl);
98 | roomState = ConnectionState.NEW;
99 | wsClient = new WebSocketChannelClient(handler, this);
100 | RoomParametersFetcherEvents callbacks = new RoomParametersFetcherEvents() {
101 | @Override
102 | public void onSignalingParametersReady(final SignalingParameters params) {
103 | WebSocketRTCClient.this.handler.post(new Runnable() {
104 | @Override
105 | public void run() {
106 | WebSocketRTCClient.this.signalingParametersReady(params);
107 | }
108 | });
109 | }
110 |
111 | @Override
112 | public void onSignalingParametersError(String description) {
113 | WebSocketRTCClient.this.reportError(description);
114 | }
115 | };
116 | new RoomParametersFetcher(connectionUrl, null, callbacks).makeRequest();
117 | }
118 |
119 | // Disconnect from room and send bye messages - runs on a local looper thread.
120 | private void disconnectFromRoomInternal() {
121 | Log.d(TAG, "Disconnect. Room state: " + roomState);
122 | if (roomState == ConnectionState.CONNECTED) {
123 | Log.d(TAG, "Closing room.");
124 | sendPostMessage(MessageType.LEAVE, leaveUrl, null);
125 | }
126 | roomState = ConnectionState.CLOSED;
127 | if (wsClient != null) {
128 | wsClient.disconnect(true);
129 | }
130 | }
131 |
132 | // Helper functions to get connection, post message and leave message URLs
133 | private String getConnectionUrl(RoomConnectionParameters connectionParameters) {
134 | return connectionParameters.roomUrl + "/" + ROOM_JOIN + "/" + connectionParameters.roomId
135 | + getQueryString(connectionParameters);
136 | }
137 |
138 | private String getMessageUrl(
139 | RoomConnectionParameters connectionParameters, SignalingParameters signalingParameters) {
140 | return connectionParameters.roomUrl + "/" + ROOM_MESSAGE + "/" + connectionParameters.roomId
141 | + "/" + signalingParameters.clientId + getQueryString(connectionParameters);
142 | }
143 |
144 | private String getLeaveUrl(
145 | RoomConnectionParameters connectionParameters, SignalingParameters signalingParameters) {
146 | return connectionParameters.roomUrl + "/" + ROOM_LEAVE + "/" + connectionParameters.roomId + "/"
147 | + signalingParameters.clientId + getQueryString(connectionParameters);
148 | }
149 |
150 | private String getQueryString(RoomConnectionParameters connectionParameters) {
151 | if (connectionParameters.urlParameters != null) {
152 | return "?" + connectionParameters.urlParameters;
153 | } else {
154 | return "";
155 | }
156 | }
157 |
158 | // Callback issued when room parameters are extracted. Runs on local
159 | // looper thread.
160 | private void signalingParametersReady(final SignalingParameters signalingParameters) {
161 | Log.d(TAG, "Room connection completed.");
162 | if (connectionParameters.loopback
163 | && (!signalingParameters.initiator || signalingParameters.offerSdp != null)) {
164 | reportError("Loopback room is busy.");
165 | return;
166 | }
167 | if (!connectionParameters.loopback && !signalingParameters.initiator
168 | && signalingParameters.offerSdp == null) {
169 | Log.w(TAG, "No offer SDP in room response.");
170 | }
171 | initiator = signalingParameters.initiator;
172 | messageUrl = getMessageUrl(connectionParameters, signalingParameters);
173 | leaveUrl = getLeaveUrl(connectionParameters, signalingParameters);
174 | Log.d(TAG, "Message URL: " + messageUrl);
175 | Log.d(TAG, "Leave URL: " + leaveUrl);
176 | roomState = ConnectionState.CONNECTED;
177 | // Fire connection and signaling parameters events.
178 | events.onConnectedToRoom(signalingParameters);
179 | // Connect and register WebSocket client.
180 | wsClient.connect(signalingParameters.wssUrl, signalingParameters.wssPostUrl);
181 | wsClient.register(connectionParameters.roomId, signalingParameters.clientId);
182 | }
183 |
184 | // Send local offer SDP to the other participant.
185 | @Override
186 | public void sendOfferSdp(final SessionDescription sdp) {
187 | handler.post(new Runnable() {
188 | @Override
189 | public void run() {
190 | if (roomState != ConnectionState.CONNECTED) {
191 | reportError("Sending offer SDP in non connected state.");
192 | return;
193 | }
194 | JSONObject json = new JSONObject();
195 | jsonPut(json, "sdp", sdp.description);
196 | jsonPut(json, "type", "offer");
197 | sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
198 | if (connectionParameters.loopback) {
199 | // In loopback mode rename this offer to answer and route it back.
200 | SessionDescription sdpAnswer = new SessionDescription(
201 | SessionDescription.Type.fromCanonicalForm("answer"), sdp.description);
202 | events.onRemoteDescription(sdpAnswer);
203 | }
204 | }
205 | });
206 | }
207 |
208 | // Send local answer SDP to the other participant.
209 | @Override
210 | public void sendAnswerSdp(final SessionDescription sdp) {
211 | handler.post(new Runnable() {
212 | @Override
213 | public void run() {
214 | if (connectionParameters.loopback) {
215 | Log.e(TAG, "Sending answer in loopback mode.");
216 | return;
217 | }
218 | JSONObject json = new JSONObject();
219 | jsonPut(json, "sdp", sdp.description);
220 | jsonPut(json, "type", "answer");
221 | wsClient.send(json.toString());
222 | }
223 | });
224 | }
225 |
226 | // Send Ice candidate to the other participant.
227 | @Override
228 | public void sendLocalIceCandidate(final IceCandidate candidate) {
229 | handler.post(new Runnable() {
230 | @Override
231 | public void run() {
232 | JSONObject json = new JSONObject();
233 | jsonPut(json, "type", "candidate");
234 | jsonPut(json, "label", candidate.sdpMLineIndex);
235 | jsonPut(json, "id", candidate.sdpMid);
236 | jsonPut(json, "candidate", candidate.sdp);
237 | if (initiator) {
238 | // Call initiator sends ice candidates to GAE server.
239 | if (roomState != ConnectionState.CONNECTED) {
240 | reportError("Sending ICE candidate in non connected state.");
241 | return;
242 | }
243 | sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
244 | if (connectionParameters.loopback) {
245 | events.onRemoteIceCandidate(candidate);
246 | }
247 | } else {
248 | // Call receiver sends ice candidates to websocket server.
249 | wsClient.send(json.toString());
250 | }
251 | }
252 | });
253 | }
254 |
255 | // Send removed Ice candidates to the other participant.
256 | @Override
257 | public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) {
258 | handler.post(new Runnable() {
259 | @Override
260 | public void run() {
261 | JSONObject json = new JSONObject();
262 | jsonPut(json, "type", "remove-candidates");
263 | JSONArray jsonArray = new JSONArray();
264 | for (final IceCandidate candidate : candidates) {
265 | jsonArray.put(toJsonCandidate(candidate));
266 | }
267 | jsonPut(json, "candidates", jsonArray);
268 | if (initiator) {
269 | // Call initiator sends ice candidates to GAE server.
270 | if (roomState != ConnectionState.CONNECTED) {
271 | reportError("Sending ICE candidate removals in non connected state.");
272 | return;
273 | }
274 | sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
275 | if (connectionParameters.loopback) {
276 | events.onRemoteIceCandidatesRemoved(candidates);
277 | }
278 | } else {
279 | // Call receiver sends ice candidates to websocket server.
280 | wsClient.send(json.toString());
281 | }
282 | }
283 | });
284 | }
285 |
286 | // --------------------------------------------------------------------
287 | // WebSocketChannelEvents interface implementation.
288 | // All events are called by WebSocketChannelClient on a local looper thread
289 | // (passed to WebSocket client constructor).
290 | @Override
291 | public void onWebSocketMessage(final String msg) {
292 | if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
293 | Log.e(TAG, "Got WebSocket message in non registered state.");
294 | return;
295 | }
296 | try {
297 | JSONObject json = new JSONObject(msg);
298 | String msgText = json.getString("msg");
299 | String errorText = json.optString("error");
300 | if (msgText.length() > 0) {
301 | json = new JSONObject(msgText);
302 | String type = json.optString("type");
303 | if (type.equals("candidate")) {
304 | events.onRemoteIceCandidate(toJavaCandidate(json));
305 | } else if (type.equals("remove-candidates")) {
306 | JSONArray candidateArray = json.getJSONArray("candidates");
307 | IceCandidate[] candidates = new IceCandidate[candidateArray.length()];
308 | for (int i = 0; i < candidateArray.length(); ++i) {
309 | candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i));
310 | }
311 | events.onRemoteIceCandidatesRemoved(candidates);
312 | } else if (type.equals("answer")) {
313 | if (initiator) {
314 | SessionDescription sdp = new SessionDescription(
315 | SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp"));
316 | events.onRemoteDescription(sdp);
317 | } else {
318 | reportError("Received answer for call initiator: " + msg);
319 | }
320 | } else if (type.equals("offer")) {
321 | if (!initiator) {
322 | SessionDescription sdp = new SessionDescription(
323 | SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp"));
324 | events.onRemoteDescription(sdp);
325 | } else {
326 | reportError("Received offer for call receiver: " + msg);
327 | }
328 | } else if (type.equals("bye")) {
329 | events.onChannelClose();
330 | } else {
331 | reportError("Unexpected WebSocket message: " + msg);
332 | }
333 | } else {
334 | if (errorText != null && errorText.length() > 0) {
335 | reportError("WebSocket error message: " + errorText);
336 | } else {
337 | reportError("Unexpected WebSocket message: " + msg);
338 | }
339 | }
340 | } catch (JSONException e) {
341 | reportError("WebSocket message JSON parsing error: " + e.toString());
342 | }
343 | }
344 |
345 | @Override
346 | public void onWebSocketClose() {
347 | events.onChannelClose();
348 | }
349 |
350 | @Override
351 | public void onWebSocketError(String description) {
352 | reportError("WebSocket error: " + description);
353 | }
354 |
355 | // --------------------------------------------------------------------
356 | // Helper functions.
357 | private void reportError(final String errorMessage) {
358 | Log.e(TAG, errorMessage);
359 | handler.post(new Runnable() {
360 | @Override
361 | public void run() {
362 | if (roomState != ConnectionState.ERROR) {
363 | roomState = ConnectionState.ERROR;
364 | events.onChannelError(errorMessage);
365 | }
366 | }
367 | });
368 | }
369 |
370 | // Put a |key|->|value| mapping in |json|.
371 | private static void jsonPut(JSONObject json, String key, Object value) {
372 | try {
373 | json.put(key, value);
374 | } catch (JSONException e) {
375 | throw new RuntimeException(e);
376 | }
377 | }
378 |
379 | // Send SDP or ICE candidate to a room server.
380 | private void sendPostMessage(
381 | final MessageType messageType, final String url, @Nullable final String message) {
382 | String logInfo = url;
383 | if (message != null) {
384 | logInfo += ". Message: " + message;
385 | }
386 | Log.d(TAG, "C->GAE: " + logInfo);
387 | AsyncHttpURLConnection httpConnection =
388 | new AsyncHttpURLConnection("POST", url, message, new AsyncHttpEvents() {
389 | @Override
390 | public void onHttpError(String errorMessage) {
391 | reportError("GAE POST error: " + errorMessage);
392 | }
393 |
394 | @Override
395 | public void onHttpComplete(String response) {
396 | if (messageType == MessageType.MESSAGE) {
397 | try {
398 | JSONObject roomJson = new JSONObject(response);
399 | String result = roomJson.getString("result");
400 | if (!result.equals("SUCCESS")) {
401 | reportError("GAE POST error: " + result);
402 | }
403 | } catch (JSONException e) {
404 | reportError("GAE POST JSON error: " + e.toString());
405 | }
406 | }
407 | }
408 | });
409 | httpConnection.send();
410 | }
411 |
412 | // Converts a Java candidate to a JSONObject.
413 | private JSONObject toJsonCandidate(final IceCandidate candidate) {
414 | JSONObject json = new JSONObject();
415 | jsonPut(json, "label", candidate.sdpMLineIndex);
416 | jsonPut(json, "id", candidate.sdpMid);
417 | jsonPut(json, "candidate", candidate.sdp);
418 | return json;
419 | }
420 |
421 | // Converts a JSON candidate to a Java object.
422 | IceCandidate toJavaCandidate(JSONObject json) throws JSONException {
423 | return new IceCandidate(
424 | json.getString("id"), json.getInt("label"), json.getString("candidate"));
425 | }
426 | }
--------------------------------------------------------------------------------
78 |
83 |
88 |
93 |