listsize = parms.getSupportedPictureSizes();
50 | Size sizeOut = null;
51 | for (Size size : listsize) {
52 | if (size.width >= width) {
53 | break;
54 | }
55 | sizeOut = size;
56 | }
57 |
58 | parms.setPreviewSize(videoWidth, videoHeight);
59 | parms.setPictureSize(videoWidth, videoHeight);
60 | parms.setPreviewFormat(ImageFormat.NV21);
61 | camera.setParameters(parms);
62 | camera.setPreviewCallback(previewCallback);
63 | try {
64 | camera.setPreviewDisplay(holder);
65 | camera.startPreview();//开始预览
66 | } catch (IOException e) {
67 | // TODO Auto-generated catch block
68 | e.printStackTrace();
69 | }
70 | }
71 |
72 | @Override
73 | public void surfaceDestroyed(SurfaceHolder holder) {
74 | // 结束预览时关闭摄像头
75 | if (camera == null) {
76 | return;
77 | }
78 | camera.setPreviewCallback(null);
79 | camera.stopPreview();// 停止预览
80 | camera.release();
81 | camera = null;
82 | }
83 | });
84 |
85 | }
86 |
87 | @Override
88 | public void onPreviewFrame(byte[] data, Camera camera) {
89 | EMClient.getInstance()
90 | .callManager()
91 | .inputExternalVideoData(data, videoWidth, videoHeight, 0);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vmloft/develop/app/demo/call/single/CallActivity.java:
--------------------------------------------------------------------------------
1 | package com.vmloft.develop.app.demo.call.single;
2 |
3 | import android.content.Context;
4 | import android.os.Bundle;
5 | import android.os.Vibrator;
6 | import android.view.WindowManager;
7 | import com.hyphenate.chat.EMClient;
8 | import com.vmloft.develop.library.tools.VMActivity;
9 | import org.greenrobot.eventbus.EventBus;
10 |
11 | /**
12 | * Created by lzan13 on 2016/8/8.
13 | *
14 | * 通话界面的父类,做一些音视频通话的通用操作
15 | */
16 | public class CallActivity extends VMActivity {
17 |
18 | // 呼叫方名字
19 | protected String chatId;
20 |
21 | // 震动器
22 | private Vibrator vibrator;
23 |
24 | @Override protected void onCreate(Bundle savedInstanceState) {
25 | super.onCreate(savedInstanceState);
26 | // 设置通话界面属性,保持屏幕常亮,关闭输入法,以及解锁
27 | getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
28 | | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
29 | | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
30 | | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
31 | }
32 |
33 | /**
34 | * 初始化界面方法,做一些界面的初始化操作
35 | */
36 | protected void initView() {
37 | activity = this;
38 |
39 | initCallPushProvider();
40 |
41 | // 初始化振动器
42 | vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
43 |
44 | if (CallManager.getInstance().getCallState() == CallManager.CallState.DISCONNECTED) {
45 | // 收到呼叫或者呼叫对方时初始化通话状态监听
46 | CallManager.getInstance().setCallState(CallManager.CallState.CONNECTING);
47 | CallManager.getInstance().registerCallStateListener();
48 | CallManager.getInstance().attemptPlayCallSound();
49 |
50 | // 如果不是对方打来的,就主动呼叫
51 | if (!CallManager.getInstance().isInComingCall()) {
52 | CallManager.getInstance().makeCall();
53 | }
54 | }
55 | }
56 |
57 | /**
58 | * 初始化通话推送提供者
59 | */
60 | private void initCallPushProvider() {
61 | CallPushProvider pushProvider = new CallPushProvider();
62 | EMClient.getInstance().callManager().setPushProvider(pushProvider);
63 | }
64 |
65 | /**
66 | * 挂断通话
67 | */
68 | protected void endCall() {
69 | CallManager.getInstance().endCall();
70 | onFinish();
71 | }
72 |
73 | /**
74 | * 拒绝通话
75 | */
76 | protected void rejectCall() {
77 | CallManager.getInstance().rejectCall();
78 | onFinish();
79 | }
80 |
81 | /**
82 | * 接听通话
83 | */
84 | protected void answerCall() {
85 | CallManager.getInstance().answerCall();
86 | }
87 |
88 | /**
89 | * 调用系统振动,触发按钮的震动反馈
90 | */
91 | protected void vibrate() {
92 | vibrator.vibrate(88);
93 | }
94 |
95 | /**
96 | * 销毁界面时做一些自己的操作
97 | */
98 | @Override public void onFinish() {
99 | super.onFinish();
100 | }
101 |
102 | @Override protected void onStart() {
103 | super.onStart();
104 | EventBus.getDefault().register(this);
105 | }
106 |
107 | @Override protected void onStop() {
108 | super.onStop();
109 | EventBus.getDefault().unregister(this);
110 | }
111 |
112 | @Override protected void onResume() {
113 | // 判断当前通话状态,如果已经挂断,则关闭通话界面
114 | if (CallManager.getInstance().getCallState() == CallManager.CallState.DISCONNECTED) {
115 | onFinish();
116 | return;
117 | } else {
118 | CallManager.getInstance().removeFloatWindow();
119 | }
120 | super.onResume();
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vmloft/develop/app/demo/call/conference/ConferenceMemberView.java:
--------------------------------------------------------------------------------
1 | package com.vmloft.develop.app.demo.call.conference;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.widget.ImageView;
8 | import android.widget.RelativeLayout;
9 | import android.widget.TextView;
10 |
11 | import com.hyphenate.media.EMCallSurfaceView;
12 | import com.superrtc.sdk.VideoView;
13 | import com.vmloft.develop.app.demo.call.R;
14 |
15 | /**
16 | * Created by lzan13 on 2017/8/21.
17 | */
18 | public class ConferenceMemberView extends RelativeLayout {
19 |
20 | private Context context;
21 |
22 | private EMCallSurfaceView surfaceView;
23 | private ImageView avatarView;
24 | private ImageView audioOffView;
25 | private ImageView talkingView;
26 | private TextView nameView;
27 |
28 | private boolean isVideoOff = true;
29 | private boolean isAudioOff = false;
30 | private boolean isDesktop = false;
31 | private String streamId;
32 |
33 |
34 | public ConferenceMemberView(Context context) {
35 | this(context, null);
36 | }
37 |
38 | public ConferenceMemberView(Context context, AttributeSet attrs) {
39 | this(context, attrs, 0);
40 | }
41 |
42 | public ConferenceMemberView(Context context, AttributeSet attrs, int defStyleAttr) {
43 | super(context, attrs, defStyleAttr);
44 | this.context = context;
45 | LayoutInflater.from(context).inflate(R.layout.widget_conference_view, this);
46 | init();
47 | }
48 |
49 | private void init() {
50 | surfaceView = (EMCallSurfaceView) findViewById(R.id.item_surface_view);
51 | avatarView = (ImageView) findViewById(R.id.img_call_avatar);
52 | audioOffView = (ImageView) findViewById(R.id.icon_mute);
53 | talkingView = (ImageView) findViewById(R.id.icon_talking);
54 | nameView = (TextView) findViewById(R.id.text_name);
55 |
56 | surfaceView.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFit);
57 | }
58 |
59 | public EMCallSurfaceView getSurfaceView() {
60 | return surfaceView;
61 | }
62 |
63 | /**
64 | * 更新静音状态
65 | */
66 | public void setAudioOff(boolean state) {
67 | if (isDesktop) {
68 | return;
69 | }
70 | isAudioOff = state;
71 | if (isAudioOff) {
72 | audioOffView.setVisibility(View.VISIBLE);
73 | } else {
74 | audioOffView.setVisibility(View.GONE);
75 | }
76 | }
77 |
78 | public boolean isAudioOff() {
79 | return isAudioOff;
80 | }
81 |
82 | /**
83 | * 更新视频显示状态
84 | */
85 | public void setVideoOff(boolean state) {
86 | isVideoOff = state;
87 | if (isVideoOff) {
88 | avatarView.setVisibility(View.VISIBLE);
89 | } else {
90 | avatarView.setVisibility(View.GONE);
91 | }
92 | }
93 |
94 | public boolean isVideoOff() {
95 | return isVideoOff;
96 | }
97 |
98 | public void setDesktop(boolean desktop) {
99 | isDesktop = desktop;
100 | avatarView.setVisibility(View.GONE);
101 | }
102 |
103 | /**
104 | * 更新说话状态
105 | */
106 | public void setTalking(boolean talking) {
107 | if (isDesktop) {
108 | return;
109 | }
110 | if (talking) {
111 | talkingView.setVisibility(VISIBLE);
112 | } else {
113 | talkingView.setVisibility(GONE);
114 | }
115 | }
116 |
117 | /**
118 | * 设置当前 view 对应的 stream 的用户,主要用来语音通话时显示对方头像
119 | */
120 | public void setUsername(String username) {
121 | nameView.setText(username);
122 | }
123 |
124 | /**
125 | * 设置当前控件显示的 Stream Id
126 | */
127 | public void setStreamId(String streamId) {
128 | this.streamId = streamId;
129 | }
130 |
131 | public String getStreamId() {
132 | return streamId;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
12 |
17 |
18 |
24 |
25 |
34 |
35 |
36 |
42 |
43 |
52 |
53 |
54 |
60 |
61 |
70 |
71 |
72 |
75 |
76 |
80 |
81 |
82 |
85 |
86 |
90 |
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vmloft/develop/app/demo/call/single/CallStateListener.java:
--------------------------------------------------------------------------------
1 | package com.vmloft.develop.app.demo.call.single;
2 |
3 | import com.hyphenate.chat.EMCallStateChangeListener;
4 | import com.vmloft.develop.library.tools.utils.VMLog;
5 | import org.greenrobot.eventbus.EventBus;
6 |
7 | /**
8 | * Created by lzan13 on 2016/10/18.
9 | *
10 | * 通话状态监听类,用来监听通话过程中状态的变化
11 | */
12 |
13 | public class CallStateListener implements EMCallStateChangeListener {
14 |
15 | @Override public void onCallStateChanged(CallState callState, CallError callError) {
16 | CallEvent event = new CallEvent();
17 | event.setState(true);
18 | event.setCallError(callError);
19 | event.setCallState(callState);
20 | EventBus.getDefault().post(event);
21 | switch (callState) {
22 | case CONNECTING: // 正在呼叫对方,TODO 没见回调过
23 | VMLog.i("正在呼叫对方" + callError);
24 | CallManager.getInstance().setCallState(CallManager.CallState.CONNECTING);
25 | break;
26 | case CONNECTED: // 正在等待对方接受呼叫申请(对方申请与你进行通话)
27 | VMLog.i("正在连接" + callError);
28 | CallManager.getInstance().setCallState(CallManager.CallState.CONNECTED);
29 | break;
30 | case ACCEPTED: // 通话已接通
31 | VMLog.i("通话已接通");
32 | CallManager.getInstance().stopCallSound();
33 | CallManager.getInstance().startCallTime();
34 | CallManager.getInstance().setEndType(CallManager.EndType.NORMAL);
35 | CallManager.getInstance().setCallState(CallManager.CallState.ACCEPTED);
36 | break;
37 | case DISCONNECTED: // 通话已中断
38 | VMLog.i("通话已结束" + callError);
39 | // 通话结束,重置通话状态
40 | if (callError == CallError.ERROR_UNAVAILABLE) {
41 | VMLog.i("对方不在线" + callError);
42 | CallManager.getInstance().setEndType(CallManager.EndType.OFFLINE);
43 | } else if (callError == CallError.ERROR_BUSY) {
44 | VMLog.i("对方正忙" + callError);
45 | CallManager.getInstance().setEndType(CallManager.EndType.BUSY);
46 | } else if (callError == CallError.REJECTED) {
47 | VMLog.i("对方已拒绝" + callError);
48 | CallManager.getInstance().setEndType(CallManager.EndType.REJECTED);
49 | } else if (callError == CallError.ERROR_NORESPONSE) {
50 | VMLog.i("对方未响应,可能手机不在身边" + callError);
51 | CallManager.getInstance().setEndType(CallManager.EndType.NORESPONSE);
52 | } else if (callError == CallError.ERROR_TRANSPORT) {
53 | VMLog.i("连接建立失败" + callError);
54 | CallManager.getInstance().setEndType(CallManager.EndType.TRANSPORT);
55 | } else if (callError == CallError.ERROR_LOCAL_SDK_VERSION_OUTDATED) {
56 | VMLog.i("双方通讯协议不同" + callError);
57 | CallManager.getInstance().setEndType(CallManager.EndType.DIFFERENT);
58 | } else if (callError == CallError.ERROR_REMOTE_SDK_VERSION_OUTDATED) {
59 | VMLog.i("双方通讯协议不同" + callError);
60 | CallManager.getInstance().setEndType(CallManager.EndType.DIFFERENT);
61 | } else if (callError == CallError.ERROR_NO_DATA) {
62 | VMLog.i("没有通话数据" + callError);
63 | } else {
64 | VMLog.i("通话已结束 %s", callError);
65 | if (CallManager.getInstance().getEndType() == CallManager.EndType.CANCEL) {
66 | CallManager.getInstance().setEndType(CallManager.EndType.CANCELLED);
67 | }
68 | }
69 | // 通话结束,保存消息
70 | CallManager.getInstance().saveCallMessage();
71 | CallManager.getInstance().reset();
72 | break;
73 | case NETWORK_DISCONNECTED:
74 | VMLog.i("对方网络不可用");
75 | break;
76 | case NETWORK_NORMAL:
77 | VMLog.i("网络正常");
78 | break;
79 | case NETWORK_UNSTABLE:
80 | if (callError == EMCallStateChangeListener.CallError.ERROR_NO_DATA) {
81 | VMLog.i("没有通话数据" + callError);
82 | } else {
83 | VMLog.i("网络不稳定" + callError);
84 | }
85 | break;
86 | case VIDEO_PAUSE:
87 | VMLog.i("视频传输已暂停");
88 | break;
89 | case VIDEO_RESUME:
90 | VMLog.i("视频传输已恢复");
91 | break;
92 | case VOICE_PAUSE:
93 | VMLog.i("语音传输已暂停");
94 | break;
95 | case VOICE_RESUME:
96 | VMLog.i("语音传输已恢复");
97 | break;
98 | default:
99 | break;
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/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/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
50 |
51 |
52 |
53 |
54 |
62 |
63 |
64 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
82 |
83 |
88 |
89 |
94 |
95 |
96 |
99 |
100 |
103 |
104 |
105 |
111 |
115 |
119 |
122 |
123 |
124 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_conference.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
18 |
19 |
24 |
25 |
26 |
27 |
32 |
33 |
34 |
38 |
39 |
47 |
48 |
54 |
55 |
56 |
66 |
67 |
68 |
76 |
77 |
78 |
86 |
87 |
92 |
93 |
94 |
101 |
102 |
103 |
110 |
111 |
112 |
119 |
120 |
121 |
128 |
129 |
130 |
139 |
140 |
141 |
146 |
147 |
148 |
157 |
158 |
159 |
168 |
169 |
170 |
179 |
180 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_voice_call.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
21 |
22 |
28 |
29 |
34 |
35 |
44 |
51 |
52 |
53 |
63 |
64 |
75 |
76 |
77 |
78 |
85 |
86 |
92 |
93 |
103 |
104 |
105 |
114 |
115 |
120 |
121 |
122 |
130 |
131 |
132 |
140 |
141 |
142 |
150 |
151 |
152 |
153 |
158 |
159 |
169 |
170 |
171 |
182 |
183 |
184 |
194 |
195 |
196 |
197 |
198 |
199 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_video_call.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
18 |
19 |
23 |
24 |
25 |
30 |
31 |
32 |
36 |
37 |
45 |
46 |
52 |
53 |
54 |
63 |
64 |
74 |
75 |
76 |
84 |
85 |
86 |
87 |
95 |
96 |
100 |
101 |
106 |
107 |
108 |
113 |
114 |
115 |
121 |
122 |
123 |
129 |
130 |
131 |
137 |
138 |
139 |
145 |
146 |
147 |
153 |
154 |
162 |
163 |
164 |
169 |
170 |
171 |
180 |
181 |
182 |
192 |
193 |
194 |
203 |
204 |
205 |
206 |
207 |
208 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vmloft/develop/app/demo/call/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.vmloft.develop.app.demo.call;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.view.ContextThemeWrapper;
6 | import android.view.View;
7 | import android.widget.Button;
8 | import android.widget.EditText;
9 | import android.widget.TextView;
10 |
11 | import butterknife.BindView;
12 | import butterknife.ButterKnife;
13 |
14 | import com.hyphenate.EMCallBack;
15 | import com.hyphenate.chat.EMClient;
16 | import com.hyphenate.chat.EMMessage;
17 | import com.hyphenate.exceptions.HyphenateException;
18 | import com.vmloft.develop.app.demo.call.conference.ConferenceActivity;
19 | import com.vmloft.develop.app.demo.call.single.CallManager;
20 | import com.vmloft.develop.app.demo.call.single.VideoCallActivity;
21 | import com.vmloft.develop.app.demo.call.single.VoiceCallActivity;
22 | import com.vmloft.develop.library.tools.VMActivity;
23 | import com.vmloft.develop.library.tools.utils.VMLog;
24 | import com.vmloft.develop.library.tools.utils.VMSPUtil;
25 | import com.vmloft.develop.library.tools.widget.VMViewGroup;
26 |
27 | import org.json.JSONException;
28 | import org.json.JSONObject;
29 |
30 |
31 | /**
32 | * 音视频项目主类
33 | */
34 | public class MainActivity extends VMActivity {
35 |
36 | @BindView(R.id.layout_root) View rootView;
37 | @BindView(R.id.view_group) VMViewGroup viewGroup;
38 |
39 | @BindView(R.id.edit_username) EditText usernameView;
40 | @BindView(R.id.edit_password) EditText passwordView;
41 | @BindView(R.id.edit_contacts_username) EditText contactsView;
42 | @BindView(R.id.text_info) TextView infoView;
43 |
44 | private String username;
45 | private String password;
46 | private String toUsername;
47 |
48 | @Override
49 | protected void onCreate(Bundle savedInstanceState) {
50 | super.onCreate(savedInstanceState);
51 | setContentView(R.layout.activity_main);
52 |
53 | ButterKnife.bind(activity);
54 |
55 | init();
56 | }
57 |
58 | private void init() {
59 | username = (String) VMSPUtil.get("username", "");
60 | password = (String) VMSPUtil.get("password", "");
61 | toUsername = (String) VMSPUtil.get("toUsername", "");
62 | usernameView.setText(username);
63 | passwordView.setText(password);
64 | contactsView.setText(toUsername);
65 |
66 | String[] btnTitle = {"登录", "注册", "退出", "语音呼叫", "视频呼叫", "发起会议", "发送消息", "新版推送"};
67 |
68 | for (int i = 0; i < btnTitle.length; i++) {
69 | Button btn = new Button(new ContextThemeWrapper(activity, R.style.VMBtn_Green), null, 0);
70 | btn.setText(btnTitle[i]);
71 | btn.setId(100 + i);
72 | btn.setOnClickListener(viewListener);
73 | viewGroup.addView(btn);
74 | }
75 |
76 | }
77 |
78 | private View.OnClickListener viewListener = new View.OnClickListener() {
79 | @Override
80 | public void onClick(View v) {
81 | switch (v.getId()) {
82 | case 100:
83 | signIn();
84 | break;
85 | case 101:
86 | signUp();
87 | break;
88 | case 102:
89 | signOut();
90 | break;
91 | case 103:
92 | callVoice();
93 | break;
94 | case 104:
95 | callVideo();
96 | break;
97 | case 105:
98 | videoConference(true);
99 | break;
100 | case 106:
101 | sendMessage();
102 | break;
103 | case 107:
104 | sendNewPushMessage();
105 | break;
106 | }
107 | }
108 | };
109 |
110 | /**
111 | * 登录
112 | */
113 | private void signIn() {
114 | username = usernameView.getText().toString().trim();
115 | password = passwordView.getText().toString().trim();
116 | if (username.isEmpty() || password.isEmpty()) {
117 | printInfo("username or password null");
118 | return;
119 | }
120 | EMClient.getInstance().login(username, password, new EMCallBack() {
121 | @Override
122 | public void onSuccess() {
123 | VMLog.i("login success");
124 |
125 | VMSPUtil.put("username", username);
126 | VMSPUtil.put("password", password);
127 | printInfo("login success");
128 | }
129 |
130 | @Override
131 | public void onError(final int i, final String s) {
132 | String errorMsg = "login error: " + i + "; " + s;
133 | VMLog.i(errorMsg);
134 | printInfo(errorMsg);
135 | }
136 |
137 | @Override
138 | public void onProgress(int i, String s) {
139 |
140 | }
141 | });
142 | }
143 |
144 | /**
145 | * 注册账户
146 | */
147 | private void signUp() {
148 | username = usernameView.getText().toString().trim();
149 | password = passwordView.getText().toString().trim();
150 | if (username.isEmpty() || password.isEmpty()) {
151 | printInfo("username or password null");
152 | return;
153 | }
154 | new Thread(new Runnable() {
155 | @Override
156 | public void run() {
157 | try {
158 | EMClient.getInstance().createAccount(username, password);
159 | } catch (HyphenateException e) {
160 | String errorMsg = "sign up error " + e.getErrorCode() + "; " + e.getMessage();
161 | VMLog.d(errorMsg);
162 | printInfo(errorMsg);
163 | e.printStackTrace();
164 | }
165 | }
166 | }).start();
167 | }
168 |
169 | /**
170 | * 退出登录
171 | */
172 | private void signOut() {
173 | EMClient.getInstance().logout(EMClient.getInstance().isConnected(), new EMCallBack() {
174 | @Override
175 | public void onSuccess() {
176 | VMLog.i("logout success");
177 | printInfo("logout success");
178 | }
179 |
180 | @Override
181 | public void onError(int i, String s) {
182 | String errorMsg = "logout error: " + i + "; " + s;
183 | VMLog.i(errorMsg);
184 | printInfo(errorMsg);
185 | }
186 |
187 | @Override
188 | public void onProgress(int i, String s) {
189 |
190 | }
191 | });
192 | }
193 |
194 | /**
195 | * 视频呼叫
196 | */
197 | private void callVideo() {
198 | checkContacts();
199 | Intent intent = new Intent(MainActivity.this, VideoCallActivity.class);
200 | CallManager.getInstance().setChatId(toUsername);
201 | CallManager.getInstance().setInComingCall(false);
202 | CallManager.getInstance().setCallType(CallManager.CallType.VIDEO);
203 | startActivity(intent);
204 | }
205 |
206 | /**
207 | * 语音呼叫
208 | */
209 | private void callVoice() {
210 | checkContacts();
211 | Intent intent = new Intent(MainActivity.this, VoiceCallActivity.class);
212 | CallManager.getInstance().setChatId(toUsername);
213 | CallManager.getInstance().setInComingCall(false);
214 | CallManager.getInstance().setCallType(CallManager.CallType.VOICE);
215 | startActivity(intent);
216 | }
217 |
218 | private void checkContacts() {
219 | toUsername = contactsView.getText().toString().trim();
220 | if (toUsername.isEmpty()) {
221 | printInfo("contact user not null");
222 | return;
223 | }
224 | VMSPUtil.put("toUsername", toUsername);
225 | }
226 |
227 | /**
228 | * 发送消息
229 | */
230 | private void sendMessage() {
231 | checkContacts();
232 | EMMessage message = EMMessage.createTxtSendMessage("测试发送消息,主要是为了测试是否在线", toUsername);
233 | //设置强制推送
234 | message.setAttribute("em_force_notification", "true");
235 | //设置自定义推送提示
236 | JSONObject extObj = new JSONObject();
237 | try {
238 | extObj.put("em_push_title", "老版本推送显示内容");
239 | extObj.put("extern", "定义推送扩展内容");
240 | } catch (JSONException e) {
241 | e.printStackTrace();
242 | }
243 | message.setAttribute("em_apns_ext", extObj);
244 | sendMessage(message);
245 | }
246 |
247 | /**
248 | * 发送新版推送消息
249 | */
250 | private void sendNewPushMessage() {
251 | checkContacts();
252 | EMMessage message = EMMessage.createTxtSendMessage("测试发送消息,主要是为了测试是否在线", toUsername);
253 | //设置强制推送
254 | message.setAttribute("em_force_notification", "true");
255 | //设置自定义推送提示
256 | JSONObject extObj = new JSONObject();
257 | try {
258 | extObj.put("em_push_name", "新版推送标题");
259 | extObj.put("em_push_content", "新版推送显示内容");
260 | extObj.put("extern", "定义推送扩展内容");
261 | } catch (JSONException e) {
262 | e.printStackTrace();
263 | }
264 | message.setAttribute("em_apns_ext", extObj);
265 | sendMessage(message);
266 | }
267 |
268 | /**
269 | * 最终调用发送信息方法
270 | *
271 | * @param message 需要发送的消息
272 | */
273 | private void sendMessage(final EMMessage message) {
274 | /**
275 | * 调用sdk的消息发送方法发送消息,发送消息时要尽早的设置消息监听,防止消息状态已经回调,
276 | * 但是自己没有注册监听,导致检测不到消息状态的变化
277 | * 所以这里在发送之前先设置消息的状态回调
278 | */
279 | message.setMessageStatusCallback(new EMCallBack() {
280 | @Override
281 | public void onSuccess() {
282 | String str = String.format("消息发送成功 msgId %s, content %s", message.getMsgId(), message
283 | .getBody());
284 | VMLog.i(str);
285 | printInfo(str);
286 | }
287 |
288 | @Override
289 | public void onError(final int i, final String s) {
290 | String str = String.format("消息发送失败 code: %d, error: %s", i, s);
291 | VMLog.i(str);
292 | printInfo(str);
293 | }
294 |
295 | @Override
296 | public void onProgress(int i, String s) {
297 | // TODO 消息发送进度,这里不处理,留给消息Item自己去更新
298 | VMLog.i("消息发送中 progress: %d, %s", i, s);
299 | }
300 | });
301 | // 发送消息
302 | EMClient.getInstance().chatManager().sendMessage(message);
303 | }
304 |
305 | /**
306 | * 发起视频会议
307 | */
308 | private void videoConference(boolean isCreator) {
309 | Intent intent = new Intent(activity, ConferenceActivity.class);
310 | intent.putExtra("isCreator", isCreator);
311 | intent.putExtra("username", toUsername);
312 | onStartActivity(activity, intent);
313 | }
314 |
315 | private void printInfo(final String info) {
316 | runOnUiThread(new Runnable() {
317 | @Override
318 | public void run() {
319 | infoView.setText(infoView.getText().toString() + info + "\n");
320 | }
321 | });
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vmloft/develop/app/demo/call/single/FloatWindow.java:
--------------------------------------------------------------------------------
1 | package com.vmloft.develop.app.demo.call.single;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.graphics.PixelFormat;
6 | import android.view.Gravity;
7 | import android.view.LayoutInflater;
8 | import android.view.MotionEvent;
9 | import android.view.View;
10 | import android.view.WindowManager;
11 | import android.widget.RelativeLayout;
12 | import android.widget.TextView;
13 |
14 | import com.hyphenate.chat.EMCallStateChangeListener;
15 | import com.hyphenate.chat.EMClient;
16 | import com.hyphenate.media.EMCallSurfaceView;
17 | import com.superrtc.sdk.VideoView;
18 | import com.vmloft.develop.app.demo.call.R;
19 | import com.vmloft.develop.library.tools.utils.VMDimen;
20 | import com.vmloft.develop.library.tools.utils.VMLog;
21 |
22 | import org.greenrobot.eventbus.EventBus;
23 | import org.greenrobot.eventbus.Subscribe;
24 | import org.greenrobot.eventbus.ThreadMode;
25 |
26 | /**
27 | * Created by lzan13 on 2017/3/27.
28 | *
29 | * 音视频通话悬浮窗操作类
30 | */
31 | public class FloatWindow {
32 |
33 | // 上下文菜单
34 | private Context context;
35 |
36 | // 当前单例类实例
37 | private static FloatWindow instance;
38 |
39 | private WindowManager windowManager = null;
40 | private WindowManager.LayoutParams layoutParams = null;
41 |
42 | // 悬浮窗需要显示的布局
43 | private View floatView;
44 | private TextView callTimeView;
45 |
46 | private EMCallSurfaceView localView;
47 | private EMCallSurfaceView oppositeView;
48 |
49 | public FloatWindow(Context context) {
50 | this.context = context;
51 | windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
52 | }
53 |
54 | public static FloatWindow getInstance(Context context) {
55 | if (instance == null) {
56 | instance = new FloatWindow(context);
57 | }
58 | return instance;
59 | }
60 |
61 | /**
62 | * 开始展示悬浮窗
63 | */
64 | public void addFloatWindow() {
65 | if (floatView != null) {
66 | return;
67 | }
68 | EventBus.getDefault()
69 | .register(this);
70 | layoutParams = new WindowManager.LayoutParams();
71 | // 位置为右侧顶部
72 | layoutParams.gravity = Gravity.LEFT | Gravity.TOP;
73 | // 设置宽高自适应
74 | layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
75 | layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
76 |
77 | // 设置悬浮窗透明
78 | layoutParams.format = PixelFormat.TRANSPARENT;
79 |
80 | // 设置窗口类型
81 | layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
82 |
83 | // 设置窗口标志类型,其中 FLAG_NOT_FOCUSABLE 是放置当前悬浮窗拦截点击事件,造成桌面控件不可操作
84 | layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
85 |
86 | // 获取要现实的布局
87 | floatView = LayoutInflater.from(context)
88 | .inflate(R.layout.widget_float_window, null);
89 | // 添加悬浮窗 View 到窗口
90 | windowManager.addView(floatView, layoutParams);
91 | if (CallManager.getInstance()
92 | .getCallType() == CallManager.CallType.VOICE) {
93 | floatView.findViewById(R.id.layout_call_voice)
94 | .setVisibility(View.VISIBLE);
95 | floatView.findViewById(R.id.layout_call_video)
96 | .setVisibility(View.GONE);
97 | callTimeView = (TextView) floatView.findViewById(R.id.text_call_time);
98 | refreshCallTime();
99 | } else {
100 | setupSurfaceView();
101 | }
102 |
103 | // 当点击悬浮窗时,返回到通话界面
104 | floatView.setOnClickListener(new View.OnClickListener() {
105 | @Override
106 | public void onClick(View v) {
107 | Intent intent = new Intent();
108 | if (CallManager.getInstance()
109 | .getCallType() == CallManager.CallType.VOICE) {
110 | intent.setClass(context, VoiceCallActivity.class);
111 | } else {
112 | intent.setClass(context, VideoCallActivity.class);
113 | }
114 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
115 | context.startActivity(intent);
116 | }
117 | });
118 |
119 | //设置监听浮动窗口的触摸移动
120 | floatView.setOnTouchListener(new View.OnTouchListener() {
121 | boolean result = false;
122 |
123 | float x = 0;
124 | float y = 0;
125 | float startX = 0;
126 | float startY = 0;
127 |
128 | @Override
129 | public boolean onTouch(View v, MotionEvent event) {
130 | switch (event.getAction()) {
131 | case MotionEvent.ACTION_DOWN:
132 | result = false;
133 | x = event.getX();
134 | y = event.getY();
135 | startX = event.getRawX();
136 | startY = event.getRawY();
137 | VMLog.d("start x: %f, y: %f", startX, startY);
138 | break;
139 | case MotionEvent.ACTION_MOVE:
140 | VMLog.d("move x: %f, y: %f", event.getRawX(), event.getRawY());
141 | // 当移动距离大于特定值时,表示是拖动悬浮窗,则不触发后边的点击监听
142 | if (Math.abs(event.getRawX() - startX) > 20 || Math.abs(event.getRawY() - startY) > 20) {
143 | result = true;
144 | }
145 | // getRawX 获取触摸点相对于屏幕的坐标,getX 相对于当前悬浮窗坐标
146 | // 根据当前触摸点 X 坐标计算悬浮窗 X 坐标,
147 | layoutParams.x = (int) (event.getRawX() - x);
148 | // 根据当前触摸点 Y 坐标计算悬浮窗 Y 坐标,减25为状态栏的高度
149 | layoutParams.y = (int) (event.getRawY() - y - 25);
150 | // 刷新悬浮窗
151 | windowManager.updateViewLayout(floatView, layoutParams);
152 | break;
153 | case MotionEvent.ACTION_UP:
154 | break;
155 | }
156 | return result;
157 | }
158 | });
159 | }
160 |
161 | /**
162 | * 设置本地与远程画面显示控件
163 | */
164 | private void setupSurfaceView() {
165 | floatView.findViewById(R.id.layout_call_voice)
166 | .setVisibility(View.GONE);
167 | floatView.findViewById(R.id.layout_call_video)
168 | .setVisibility(View.VISIBLE);
169 |
170 | RelativeLayout surfaceLayout = (RelativeLayout) floatView.findViewById(R.id.layout_call_video);
171 |
172 | // 将 SurfaceView设置给 SDK
173 | surfaceLayout.removeAllViews();
174 |
175 | localView = new EMCallSurfaceView(context);
176 | oppositeView = new EMCallSurfaceView(context);
177 |
178 | int lw = VMDimen.dp2px(24);
179 | int lh = VMDimen.dp2px(32);
180 | int ow = VMDimen.dp2px(96);
181 | int oh = VMDimen.dp2px(128);
182 | RelativeLayout.LayoutParams localParams = new RelativeLayout.LayoutParams(lw, lh);
183 | RelativeLayout.LayoutParams oppositeParams = new RelativeLayout.LayoutParams(ow, oh);
184 | // 设置本地图像靠右
185 | localParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
186 |
187 | // 设置本地预览图像显示在最上层
188 | localView.setZOrderOnTop(false);
189 | localView.setZOrderMediaOverlay(true);
190 | // 将 view 添加到界面
191 | surfaceLayout.addView(localView, localParams);
192 | surfaceLayout.addView(oppositeView, oppositeParams);
193 |
194 | // 设置通话界面画面填充方式
195 | localView.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFill);
196 | oppositeView.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFill);
197 | // 设置本地以及对方显示画面控件,这个要设置在上边几个方法之后,不然会概率出现接收方无画面
198 | EMClient.getInstance()
199 | .callManager()
200 | .setSurfaceView(localView, oppositeView);
201 | }
202 |
203 | /**
204 | * 停止悬浮窗
205 | */
206 | public void removeFloatWindow() {
207 | EventBus.getDefault()
208 | .unregister(this);
209 | if (localView != null) {
210 | if (localView.getRenderer() != null) {
211 | localView.getRenderer()
212 | .dispose();
213 | }
214 | localView.release();
215 | localView = null;
216 | }
217 | if (oppositeView != null) {
218 | if (oppositeView.getRenderer() != null) {
219 | oppositeView.getRenderer()
220 | .dispose();
221 | }
222 | oppositeView.release();
223 | oppositeView = null;
224 | }
225 | if (windowManager != null && floatView != null) {
226 | windowManager.removeView(floatView);
227 | floatView = null;
228 | }
229 | }
230 |
231 | @Subscribe(threadMode = ThreadMode.MAIN)
232 | public void onEventBus(CallEvent event) {
233 | if (event.isState()) {
234 | refreshCallView(event);
235 | }
236 | if (event.isTime() && CallManager.getInstance()
237 | .getCallType() == CallManager.CallType.VOICE) {
238 | refreshCallTime();
239 | }
240 | }
241 |
242 | /**
243 | * 刷新通话界面
244 | */
245 | private void refreshCallView(CallEvent event) {
246 | EMCallStateChangeListener.CallError callError = event.getCallError();
247 | EMCallStateChangeListener.CallState callState = event.getCallState();
248 | switch (callState) {
249 | case CONNECTING: // 正在呼叫对方,TODO 没见回调过
250 | VMLog.i("正在呼叫对方" + callError);
251 | break;
252 | case CONNECTED: // 正在等待对方接受呼叫申请(对方申请与你进行通话)
253 | VMLog.i("正在连接" + callError);
254 | break;
255 | case ACCEPTED: // 通话已接通
256 | VMLog.i("通话已接通");
257 | break;
258 | case DISCONNECTED: // 通话已中断
259 | VMLog.i("通话已结束" + callError);
260 | CallManager.getInstance()
261 | .removeFloatWindow();
262 | break;
263 | // TODO 3.3.0版本 SDK 下边几个暂时都没有回调
264 | case NETWORK_UNSTABLE:
265 | if (callError == EMCallStateChangeListener.CallError.ERROR_NO_DATA) {
266 | VMLog.i("没有通话数据" + callError);
267 | } else {
268 | VMLog.i("网络不稳定" + callError);
269 | }
270 | break;
271 | case NETWORK_NORMAL:
272 | VMLog.i("网络正常");
273 | break;
274 | case VIDEO_PAUSE:
275 | VMLog.i("视频传输已暂停");
276 | break;
277 | case VIDEO_RESUME:
278 | VMLog.i("视频传输已恢复");
279 | break;
280 | case VOICE_PAUSE:
281 | VMLog.i("语音传输已暂停");
282 | break;
283 | case VOICE_RESUME:
284 | VMLog.i("语音传输已恢复");
285 | break;
286 | default:
287 | break;
288 | }
289 | }
290 |
291 | private void refreshCallTime() {
292 | int t = CallManager.getInstance()
293 | .getCallTime();
294 | int h = t / 60 / 60;
295 | int m = t / 60 % 60;
296 | int s = t % 60 % 60;
297 | String time = "";
298 | if (h > 9) {
299 | time = "" + h;
300 | } else {
301 | time = "0" + h;
302 | }
303 | if (m > 9) {
304 | time += ":" + m;
305 | } else {
306 | time += ":0" + m;
307 | }
308 | if (s > 9) {
309 | time += ":" + s;
310 | } else {
311 | time += ":0" + s;
312 | }
313 | if (!callTimeView.isShown()) {
314 | callTimeView.setVisibility(View.VISIBLE);
315 | }
316 | callTimeView.setText(time);
317 | }
318 | }
319 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vmloft/develop/app/demo/call/single/VoiceCallActivity.java:
--------------------------------------------------------------------------------
1 | package com.vmloft.develop.app.demo.call.single;
2 |
3 | import android.content.res.Configuration;
4 | import android.os.Bundle;
5 | import android.support.design.widget.FloatingActionButton;
6 | import android.support.design.widget.Snackbar;
7 | import android.view.View;
8 | import android.widget.ImageButton;
9 | import android.widget.ImageView;
10 | import android.widget.TextView;
11 | import android.widget.Toast;
12 | import butterknife.BindView;
13 | import butterknife.ButterKnife;
14 | import butterknife.OnClick;
15 |
16 | import com.hyphenate.chat.EMCallStateChangeListener;
17 | import com.hyphenate.chat.EMClient;
18 | import com.hyphenate.exceptions.HyphenateException;
19 |
20 | import com.vmloft.develop.app.demo.call.R;
21 | import com.vmloft.develop.library.tools.utils.VMLog;
22 | import org.greenrobot.eventbus.Subscribe;
23 | import org.greenrobot.eventbus.ThreadMode;
24 |
25 | /**
26 | * Created by lzan13 on 2016/10/18.
27 | *
28 | * 音频通话界面处理
29 | */
30 | public class VoiceCallActivity extends CallActivity {
31 |
32 | // 使用 ButterKnife 注解的方式获取控件
33 | @BindView(R.id.layout_root) View rootView;
34 | @BindView(R.id.text_call_state) TextView callStateView;
35 | @BindView(R.id.text_call_time) TextView callTimeView;
36 | @BindView(R.id.img_call_avatar) ImageView avatarView;
37 | @BindView(R.id.text_call_username) TextView usernameView;
38 | @BindView(R.id.btn_exit_full_screen) ImageButton exitFullScreenBtn;
39 | @BindView(R.id.btn_mic_switch) ImageButton micSwitch;
40 | @BindView(R.id.btn_speaker_switch) ImageButton speakerSwitch;
41 | @BindView(R.id.btn_record_switch) ImageButton recordSwitch;
42 | @BindView(R.id.fab_reject_call) FloatingActionButton rejectCallFab;
43 | @BindView(R.id.fab_end_call) FloatingActionButton endCallFab;
44 | @BindView(R.id.fab_answer_call) FloatingActionButton answerCallFab;
45 |
46 | @Override protected void onCreate(Bundle savedInstanceState) {
47 | super.onCreate(savedInstanceState);
48 | setContentView(R.layout.activity_voice_call);
49 |
50 | ButterKnife.bind(this);
51 |
52 | initView();
53 | }
54 |
55 | /**
56 | * 重载父类方法,实现一些当前通话的操作,
57 | */
58 | @Override protected void initView() {
59 | super.initView();
60 | if (CallManager.getInstance().isInComingCall()) {
61 | endCallFab.setVisibility(View.GONE);
62 | answerCallFab.setVisibility(View.VISIBLE);
63 | rejectCallFab.setVisibility(View.VISIBLE);
64 | callStateView.setText(R.string.call_connected_is_incoming);
65 | } else {
66 | endCallFab.setVisibility(View.VISIBLE);
67 | answerCallFab.setVisibility(View.GONE);
68 | rejectCallFab.setVisibility(View.GONE);
69 | callStateView.setText(R.string.call_connecting);
70 | }
71 |
72 | usernameView.setText(CallManager.getInstance().getChatId());
73 |
74 | micSwitch.setActivated(!CallManager.getInstance().isOpenMic());
75 | speakerSwitch.setActivated(CallManager.getInstance().isOpenSpeaker());
76 | recordSwitch.setActivated(CallManager.getInstance().isOpenRecord());
77 |
78 | // 判断当前通话时刚开始,还是从后台恢复已经存在的通话
79 | if (CallManager.getInstance().getCallState() == CallManager.CallState.ACCEPTED) {
80 | endCallFab.setVisibility(View.VISIBLE);
81 | answerCallFab.setVisibility(View.GONE);
82 | rejectCallFab.setVisibility(View.GONE);
83 | callStateView.setText(R.string.call_accepted);
84 | refreshCallTime();
85 | }
86 | }
87 |
88 | /**
89 | * 界面控件点击监听器
90 | */
91 | @OnClick({
92 | R.id.btn_exit_full_screen, R.id.btn_mic_switch, R.id.btn_speaker_switch, R.id.btn_record_switch, R.id.fab_reject_call,
93 | R.id.fab_end_call, R.id.fab_answer_call
94 | }) void onClick(View v) {
95 | switch (v.getId()) {
96 | case R.id.btn_exit_full_screen:
97 | // 最小化通话界面
98 | exitFullScreen();
99 | break;
100 | case R.id.btn_mic_switch:
101 | // 麦克风开关
102 | onMicrophone();
103 | break;
104 | case R.id.btn_speaker_switch:
105 | // 扬声器开关
106 | onSpeaker();
107 | break;
108 | case R.id.btn_record_switch:
109 | // 录制开关
110 | recordCall();
111 | break;
112 | case R.id.fab_end_call:
113 | // 结束通话
114 | endCall();
115 | break;
116 | case R.id.fab_reject_call:
117 | // 拒绝接听通话
118 | rejectCall();
119 | break;
120 | case R.id.fab_answer_call:
121 | // 接听通话
122 | answerCall();
123 | break;
124 | }
125 | }
126 |
127 | /**
128 | * 接听通话
129 | */
130 | @Override protected void answerCall() {
131 | super.answerCall();
132 |
133 | endCallFab.setVisibility(View.VISIBLE);
134 | rejectCallFab.setVisibility(View.GONE);
135 | answerCallFab.setVisibility(View.GONE);
136 | }
137 |
138 | /**
139 | * 退出全屏通话界面
140 | */
141 | private void exitFullScreen() {
142 | CallManager.getInstance().addFloatWindow();
143 | onFinish();
144 | }
145 |
146 | /**
147 | * 麦克风开关,主要调用环信语音数据传输方法
148 | */
149 | private void onMicrophone() {
150 | try {
151 | // 根据麦克风开关是否被激活来进行判断麦克风状态,然后进行下一步操作
152 | if (micSwitch.isActivated()) {
153 | // 设置按钮状态
154 | micSwitch.setActivated(false);
155 | // 暂停语音数据的传输
156 | EMClient.getInstance().callManager().resumeVoiceTransfer();
157 | CallManager.getInstance().setOpenMic(true);
158 | } else {
159 | // 设置按钮状态
160 | micSwitch.setActivated(true);
161 | // 恢复语音数据的传输
162 | EMClient.getInstance().callManager().pauseVoiceTransfer();
163 | CallManager.getInstance().setOpenMic(false);
164 | }
165 | } catch (HyphenateException e) {
166 | VMLog.e("exception code: %d, %s", e.getErrorCode(), e.getMessage());
167 | e.printStackTrace();
168 | }
169 | }
170 |
171 | /**
172 | * 扬声器开关
173 | */
174 | private void onSpeaker() {
175 | // 根据按钮状态决定打开还是关闭扬声器
176 | if (speakerSwitch.isActivated()) {
177 | // 设置按钮状态
178 | speakerSwitch.setActivated(false);
179 | CallManager.getInstance().closeSpeaker();
180 | CallManager.getInstance().setOpenSpeaker(false);
181 | } else {
182 | // 设置按钮状态
183 | speakerSwitch.setActivated(true);
184 | CallManager.getInstance().openSpeaker();
185 | CallManager.getInstance().setOpenSpeaker(true);
186 | }
187 | }
188 |
189 | /**
190 | * 录制通话内容 TODO 后期实现
191 | */
192 | private void recordCall() {
193 | Snackbar.make(rootView, "暂未实现", Snackbar.LENGTH_LONG).show();
194 | // 根据开关状态决定是否开启录制
195 | if (recordSwitch.isActivated()) {
196 | // 设置按钮状态
197 | recordSwitch.setActivated(false);
198 | CallManager.getInstance().setOpenRecord(false);
199 | } else {
200 | // 设置按钮状态
201 | recordSwitch.setActivated(true);
202 | CallManager.getInstance().setOpenRecord(true);
203 | }
204 | }
205 |
206 | @Subscribe(threadMode = ThreadMode.MAIN) public void onEventBus(CallEvent event) {
207 | if (event.isState()) {
208 | refreshCallView(event);
209 | }
210 | if (event.isTime()) {
211 | // 不论什么情况都检查下当前时间
212 | refreshCallTime();
213 | }
214 | }
215 |
216 | /**
217 | * 刷新通话界面
218 | */
219 | private void refreshCallView(CallEvent event) {
220 | EMCallStateChangeListener.CallError callError = event.getCallError();
221 | EMCallStateChangeListener.CallState callState = event.getCallState();
222 | switch (callState) {
223 | case CONNECTING: // 正在呼叫对方,TODO 没见回调过
224 | VMLog.i("正在呼叫对方" + callError);
225 | break;
226 | case CONNECTED: // 正在等待对方接受呼叫申请(对方申请与你进行通话)
227 | VMLog.i("正在连接" + callError);
228 | runOnUiThread(new Runnable() {
229 | @Override public void run() {
230 | if (CallManager.getInstance().isInComingCall()) {
231 | callStateView.setText(R.string.call_connected_is_incoming);
232 | } else {
233 | callStateView.setText(R.string.call_connected);
234 | }
235 | }
236 | });
237 | break;
238 | case ACCEPTED: // 通话已接通
239 | VMLog.i("通话已接通");
240 | runOnUiThread(new Runnable() {
241 | @Override public void run() {
242 | callStateView.setText(R.string.call_accepted);
243 | }
244 | });
245 | break;
246 | case DISCONNECTED: // 通话已中断
247 | VMLog.i("通话已结束" + callError);
248 | onFinish();
249 | break;
250 | case NETWORK_DISCONNECTED:
251 | Toast.makeText(activity, "对方网络断开", Toast.LENGTH_SHORT).show();
252 | VMLog.i("对方网络断开");
253 | break;
254 | case NETWORK_NORMAL:
255 | VMLog.i("网络正常");
256 | break;
257 | case NETWORK_UNSTABLE:
258 | if (callError == EMCallStateChangeListener.CallError.ERROR_NO_DATA) {
259 | VMLog.i("没有通话数据" + callError);
260 | } else {
261 | VMLog.i("网络不稳定" + callError);
262 | }
263 | break;
264 | case VIDEO_PAUSE:
265 | Toast.makeText(activity, "对方已暂停视频传输", Toast.LENGTH_SHORT).show();
266 | VMLog.i("对方已暂停视频传输");
267 | break;
268 | case VIDEO_RESUME:
269 | Toast.makeText(activity, "对方已恢复视频传输", Toast.LENGTH_SHORT).show();
270 | VMLog.i("对方已恢复视频传输");
271 | break;
272 | case VOICE_PAUSE:
273 | Toast.makeText(activity, "对方已暂停语音传输", Toast.LENGTH_SHORT).show();
274 | VMLog.i("对方已暂停语音传输");
275 | break;
276 | case VOICE_RESUME:
277 | Toast.makeText(activity, "对方已恢复语音传输", Toast.LENGTH_SHORT).show();
278 | VMLog.i("对方已恢复语音传输");
279 | break;
280 | default:
281 | break;
282 | }
283 | }
284 |
285 | /**
286 | * 刷新通话时间显示
287 | */
288 | private void refreshCallTime() {
289 | int t = CallManager.getInstance().getCallTime();
290 | int h = t / 60 / 60;
291 | int m = t / 60 % 60;
292 | int s = t % 60 % 60;
293 | String time = "";
294 | if (h > 9) {
295 | time = "" + h;
296 | } else {
297 | time = "0" + h;
298 | }
299 | if (m > 9) {
300 | time += ":" + m;
301 | } else {
302 | time += ":0" + m;
303 | }
304 | if (s > 9) {
305 | time += ":" + s;
306 | } else {
307 | time += ":0" + s;
308 | }
309 | if (!callTimeView.isShown()) {
310 | callTimeView.setVisibility(View.VISIBLE);
311 | }
312 | callTimeView.setText(time);
313 | }
314 |
315 | /**
316 | * 屏幕方向改变回调方法
317 | */
318 | @Override public void onConfigurationChanged(Configuration newConfig) {
319 | super.onConfigurationChanged(newConfig);
320 | }
321 |
322 | @Override protected void onUserLeaveHint() {
323 | //super.onUserLeaveHint();
324 | exitFullScreen();
325 | }
326 |
327 | /**
328 | * 通话界面拦截 Back 按键,不能返回
329 | */
330 | @Override public void onBackPressed() {
331 | //super.onBackPressed();
332 | exitFullScreen();
333 | }
334 | }
335 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vmloft/develop/app/demo/call/App.java:
--------------------------------------------------------------------------------
1 | package com.vmloft.develop.app.demo.call;
2 |
3 | import android.app.ActivityManager;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.content.IntentFilter;
7 | import android.os.Handler;
8 | import android.os.Message;
9 | import android.widget.Toast;
10 |
11 | import com.hyphenate.EMConferenceListener;
12 | import com.hyphenate.EMConnectionListener;
13 | import com.hyphenate.EMError;
14 | import com.hyphenate.EMMessageListener;
15 | import com.hyphenate.chat.EMClient;
16 | import com.hyphenate.chat.EMConferenceStream;
17 | import com.hyphenate.chat.EMMessage;
18 | import com.hyphenate.chat.EMOptions;
19 | import com.hyphenate.chat.EMStreamStatistics;
20 | import com.vmloft.develop.app.demo.call.conference.ConferenceActivity;
21 | import com.vmloft.develop.app.demo.call.single.CallManager;
22 | import com.vmloft.develop.app.demo.call.single.CallReceiver;
23 | import com.vmloft.develop.library.tools.VMApp;
24 | import com.vmloft.develop.library.tools.VMTools;
25 | import com.vmloft.develop.library.tools.utils.VMLog;
26 |
27 | import java.util.Iterator;
28 | import java.util.List;
29 |
30 | /**
31 | * Created by lzan13 on 2016/5/25.
32 | *
33 | * 程序入口,做一些必要的初始化操作
34 | */
35 | public class App extends VMApp {
36 |
37 | private CallReceiver callReceiver;
38 |
39 | @Override
40 | public void onCreate() {
41 | super.onCreate();
42 |
43 | VMTools.init(context);
44 | // 初始化环信sdk
45 | initHyphenate();
46 | }
47 |
48 | /**
49 | * 初始化环信sdk,并做一些注册监听的操作
50 | */
51 | private void initHyphenate() {
52 | // 获取当前进程 id 并取得进程名
53 | int pid = android.os.Process.myPid();
54 | String processAppName = getAppName(pid);
55 | /**
56 | * 如果app启用了远程的service,此application:onCreate会被调用2次
57 | * 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
58 | * 默认的app会在以包名为默认的process name下运行,如果查到的process name不是app的process name就立即返回
59 | */
60 | if (processAppName == null || !processAppName.equalsIgnoreCase(context.getPackageName())) {
61 | // 则此application的onCreate 是被service 调用的,直接返回
62 | return;
63 | }
64 |
65 | // 初始化sdk的一些配置
66 | EMOptions options = new EMOptions();
67 | // options.enableDNSConfig(false);
68 | // options.setIMServer("im1.easemob.cm");
69 | // options.setImPort(443);
70 | // options.setRestServer("a1.easemob.com:80");
71 | // options.setAppKey("easemob-demo#chatdemoui");
72 | // options.setAppKey("easemob-demo#chatuidemo");
73 | // options.setAppKey("hx-ps#api4vip6");
74 | // options.setAppKey("cx-dev#cxstudy");
75 |
76 | options.setAutoLogin(true);
77 | // 设置小米推送 appID 和 appKey
78 | // options.setMipushConfig("2882303761517573806", "5981757315806");
79 |
80 | // 设置消息是否按照服务器时间排序
81 | options.setSortMessageByServerTime(false);
82 |
83 | // 初始化环信SDK,一定要先调用init()
84 | EMClient.getInstance().init(context, options);
85 |
86 | // 开启 debug 模式
87 | EMClient.getInstance().setDebugMode(true);
88 |
89 | // 设置通话广播监听器
90 | IntentFilter callFilter = new IntentFilter(EMClient.getInstance()
91 | .callManager()
92 | .getIncomingCallBroadcastAction());
93 | if (callReceiver == null) {
94 | callReceiver = new CallReceiver();
95 | }
96 | //注册通话广播接收者
97 | context.registerReceiver(callReceiver, callFilter);
98 |
99 | CallManager.getInstance().setExternalInputData(false);
100 | // 通话管理类的初始化
101 | CallManager.getInstance().init(context);
102 |
103 | setConnectionListener();
104 |
105 | setConferenceListener();
106 |
107 | setMessageListener();
108 | }
109 |
110 | /**
111 | * 设置连接监听
112 | */
113 | private void setConnectionListener() {
114 | EMConnectionListener connectionListener = new EMConnectionListener() {
115 | @Override
116 | public void onConnected() {
117 |
118 | }
119 |
120 | @Override
121 | public void onDisconnected(int i) {
122 | String str = "" + i;
123 | switch (i) {
124 | case EMError.USER_REMOVED:
125 | str = "账户被移除";
126 | break;
127 | case EMError.USER_LOGIN_ANOTHER_DEVICE:
128 | str = "其他设备登录";
129 | break;
130 | case EMError.USER_KICKED_BY_OTHER_DEVICE:
131 | str = "其他设备强制下线";
132 | break;
133 | case EMError.USER_KICKED_BY_CHANGE_PASSWORD:
134 | str = "密码修改";
135 | break;
136 | case EMError.SERVER_SERVICE_RESTRICTED:
137 | str = "被后台限制";
138 | break;
139 | }
140 | VMLog.i("onDisconnected %s", str);
141 | }
142 | };
143 | EMClient.getInstance().addConnectionListener(connectionListener);
144 | }
145 |
146 | /**
147 | * 设置多人会议监听
148 | */
149 | private void setConferenceListener() {
150 | EMClient.getInstance()
151 | .conferenceManager()
152 | .addConferenceListener(new EMConferenceListener() {
153 | @Override
154 | public void onMemberJoined(String username) {
155 | VMLog.i("Joined username: %s", username);
156 | }
157 |
158 | @Override
159 | public void onMemberExited(String username) {
160 | VMLog.i("Exited username: %s", username);
161 | }
162 |
163 | @Override
164 | public void onStreamAdded(EMConferenceStream stream) {
165 | VMLog.i("Stream added streamId: %s, streamName: %s, memberName: %s, username: %s, extension: %s, videoOff: %b, mute: %b", stream
166 | .getStreamId(), stream.getStreamName(), stream.getMemberName(), stream.getUsername(), stream
167 | .getExtension(), stream.isVideoOff(), stream.isAudioOff());
168 | VMLog.i("Conference stream subscribable: %d, subscribed: %d", EMClient.getInstance()
169 | .conferenceManager()
170 | .getAvailableStreamMap()
171 | .size(), EMClient.getInstance()
172 | .conferenceManager()
173 | .getSubscribedStreamMap()
174 | .size());
175 | }
176 |
177 | @Override
178 | public void onStreamRemoved(EMConferenceStream stream) {
179 | VMLog.i("Stream removed streamId: %s, streamName: %s, memberName: %s, username: %s, extension: %s, videoOff: %b, mute: %b", stream
180 | .getStreamId(), stream.getStreamName(), stream.getMemberName(), stream.getUsername(), stream
181 | .getExtension(), stream.isVideoOff(), stream.isAudioOff());
182 | VMLog.i("Conference stream subscribable: %d, subscribed: %d", EMClient.getInstance()
183 | .conferenceManager()
184 | .getAvailableStreamMap()
185 | .size(), EMClient.getInstance()
186 | .conferenceManager()
187 | .getSubscribedStreamMap()
188 | .size());
189 | }
190 |
191 | @Override
192 | public void onStreamUpdate(EMConferenceStream stream) {
193 | VMLog.i("Stream added streamId: %s, streamName: %s, memberName: %s, username: %s, extension: %s, videoOff: %b, mute: %b", stream
194 | .getStreamId(), stream.getStreamName(), stream.getMemberName(), stream.getUsername(), stream
195 | .getExtension(), stream.isVideoOff(), stream.isAudioOff());
196 | VMLog.i("Conference stream subscribable: %d, subscribed: %d", EMClient.getInstance()
197 | .conferenceManager()
198 | .getAvailableStreamMap()
199 | .size(), EMClient.getInstance()
200 | .conferenceManager()
201 | .getSubscribedStreamMap()
202 | .size());
203 | }
204 |
205 | @Override
206 | public void onPassiveLeave(int error, String message) {
207 | VMLog.i("passive leave code: %d, message: %s", error, message);
208 | }
209 |
210 | @Override
211 | public void onConferenceState(ConferenceState state) {
212 | VMLog.i("State " + state);
213 | }
214 |
215 | @Override
216 | public void onStreamStatistics(EMStreamStatistics emStreamStatistics) {
217 | VMLog.i(emStreamStatistics.toString());
218 | }
219 |
220 | @Override
221 | public void onStreamSetup(String streamId) {
222 | VMLog.i("Stream id %s", streamId);
223 | }
224 |
225 | @Override
226 | public void onSpeakers(List list) {
227 |
228 | }
229 |
230 | @Override
231 | public void onReceiveInvite(String confId, String password, String extension) {
232 | VMLog.i("Receive conference invite confId: %s, password: %s, extension: %s", confId, password, extension);
233 |
234 | Intent conferenceIntent = new Intent(context, ConferenceActivity.class);
235 | conferenceIntent.putExtra("isCreator", false);
236 | conferenceIntent.putExtra("confId", confId);
237 | conferenceIntent.putExtra("password", password);
238 | conferenceIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
239 | context.startActivity(conferenceIntent);
240 | }
241 | });
242 | }
243 |
244 | private void setMessageListener() {
245 | EMClient.getInstance().chatManager().addMessageListener(new EMMessageListener() {
246 | @Override
247 | public void onMessageReceived(List list) {
248 | Message msg = handler.obtainMessage(0);
249 | msg.obj = list.get(0).toString();
250 | handler.sendMessage(msg);
251 | for (EMMessage message : list) {
252 | VMLog.i("收到新消息" + message);
253 | }
254 | }
255 |
256 | @Override
257 | public void onCmdMessageReceived(List list) {
258 |
259 | }
260 |
261 | @Override
262 | public void onMessageRead(List list) {
263 |
264 | }
265 |
266 | @Override
267 | public void onMessageDelivered(List list) {
268 |
269 | }
270 |
271 | @Override
272 | public void onMessageRecalled(List list) {
273 |
274 | }
275 |
276 | @Override
277 | public void onMessageChanged(EMMessage emMessage, Object o) {
278 |
279 | }
280 | });
281 | }
282 |
283 | Handler handler = new Handler() {
284 | @Override
285 | public void handleMessage(Message msg) {
286 | String str = (String) msg.obj;
287 | switch (msg.what) {
288 | case 0:
289 | Toast.makeText(context, str, Toast.LENGTH_SHORT).show();
290 | break;
291 | }
292 | }
293 | };
294 |
295 | /**
296 | * 根据Pid获取当前进程的名字,一般就是当前app的包名
297 | *
298 | * @param pid 进程的id
299 | * @return 返回进程的名字
300 | */
301 | private String getAppName(int pid) {
302 | String processName = null;
303 | ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
304 | List list = activityManager.getRunningAppProcesses();
305 | Iterator i = list.iterator();
306 | while (i.hasNext()) {
307 | ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i.next());
308 | try {
309 | if (info.pid == pid) {
310 | // 根据进程的信息获取当前进程的名字
311 | processName = info.processName;
312 | // 返回当前进程名
313 | return processName;
314 | }
315 | } catch (Exception e) {
316 | e.printStackTrace();
317 | }
318 | }
319 | // 没有匹配的项,返回为null
320 | return processName;
321 | }
322 | }
323 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vmloft/develop/app/demo/call/single/VideoCallActivity.java:
--------------------------------------------------------------------------------
1 | package com.vmloft.develop.app.demo.call.single;
2 |
3 | import android.content.res.Configuration;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapFactory;
6 | import android.hardware.Camera;
7 | import android.os.Bundle;
8 | import android.support.design.widget.FloatingActionButton;
9 | import android.view.SurfaceView;
10 | import android.view.View;
11 | import android.widget.ImageButton;
12 | import android.widget.RelativeLayout;
13 | import android.widget.TextView;
14 | import android.widget.Toast;
15 |
16 | import butterknife.BindView;
17 | import butterknife.ButterKnife;
18 | import butterknife.OnClick;
19 |
20 | import com.hyphenate.chat.EMCallStateChangeListener;
21 | import com.hyphenate.chat.EMClient;
22 | import com.hyphenate.chat.EMVideoCallHelper;
23 | import com.hyphenate.exceptions.HyphenateException;
24 | import com.hyphenate.media.EMCallSurfaceView;
25 | import com.superrtc.sdk.VideoView;
26 | import com.vmloft.develop.app.demo.call.R;
27 | import com.vmloft.develop.app.demo.call.camera.PreviewManager;
28 | import com.vmloft.develop.library.tools.utils.VMDimen;
29 | import com.vmloft.develop.library.tools.utils.VMFile;
30 | import com.vmloft.develop.library.tools.utils.VMLog;
31 | import com.vmloft.develop.library.tools.utils.VMViewUtil;
32 |
33 | import java.io.File;
34 |
35 | import org.greenrobot.eventbus.Subscribe;
36 | import org.greenrobot.eventbus.ThreadMode;
37 |
38 | /**
39 | * Created by lzan13 on 2016/10/18.
40 | * 视频通话界面处理
41 | */
42 | public class VideoCallActivity extends CallActivity {
43 |
44 | // 视频通话帮助类
45 | private EMVideoCallHelper videoCallHelper;
46 | // SurfaceView 控件状态,-1 表示通话未接通,0 表示本小远大,1 表示远小本大
47 | private int surfaceState = -1;
48 | private boolean isMonitor = false;
49 |
50 | private int littleWidth;
51 | private int littleHeight;
52 | private int rightMargin;
53 | private int topMargin;
54 |
55 | private EMCallSurfaceView localSurface = null;
56 | private EMCallSurfaceView oppositeSurface = null;
57 | private RelativeLayout.LayoutParams localParams = null;
58 | private RelativeLayout.LayoutParams oppositeParams = null;
59 |
60 | // 使用 ButterKnife 注解的方式获取控件
61 | @BindView(R.id.layout_root) View rootView;
62 | @BindView(R.id.layout_call_control) View controlLayout;
63 | @BindView(R.id.layout_surface_container) RelativeLayout surfaceLayout;
64 | @BindView(R.id.surface_view) SurfaceView surfaceView;
65 |
66 | @BindView(R.id.btn_exit_full_screen) ImageButton exitFullScreenBtn;
67 | @BindView(R.id.text_call_state) TextView callStateView;
68 | @BindView(R.id.text_call_time) TextView callTimeView;
69 | @BindView(R.id.btn_call_info) ImageButton callInfoBtn;
70 | @BindView(R.id.text_call_info) TextView callInfoView;
71 | @BindView(R.id.btn_mic_switch) ImageButton micSwitch;
72 | @BindView(R.id.btn_camera_switch) ImageButton cameraSwitch;
73 | @BindView(R.id.btn_speaker_switch) ImageButton speakerSwitch;
74 | @BindView(R.id.btn_record_switch) ImageButton recordSwitch;
75 | @BindView(R.id.btn_screenshot) ImageButton screenshotSwitch;
76 | @BindView(R.id.btn_change_camera_switch) ImageButton changeCameraSwitch;
77 | @BindView(R.id.fab_reject_call) FloatingActionButton rejectCallFab;
78 | @BindView(R.id.fab_end_call) FloatingActionButton endCallFab;
79 | @BindView(R.id.fab_answer_call) FloatingActionButton answerCallFab;
80 |
81 | @Override
82 | protected void onCreate(Bundle savedInstanceState) {
83 | super.onCreate(savedInstanceState);
84 | setContentView(R.layout.activity_video_call);
85 |
86 | ButterKnife.bind(this);
87 |
88 | initView();
89 | }
90 |
91 | /**
92 | * 重载父类方法,实现一些当前通话的操作,
93 | */
94 | @Override
95 | protected void initView() {
96 | VMViewUtil.getAllChildViews(activity.getWindow().getDecorView(), 1);
97 |
98 | littleWidth = VMDimen.dp2px(96);
99 | littleHeight = VMDimen.dp2px(128);
100 | rightMargin = VMDimen.dp2px(16);
101 | topMargin = VMDimen.dp2px(96);
102 |
103 | super.initView();
104 | if (CallManager.getInstance().isInComingCall()) {
105 | endCallFab.setVisibility(View.GONE);
106 | answerCallFab.setVisibility(View.VISIBLE);
107 | rejectCallFab.setVisibility(View.VISIBLE);
108 | callStateView.setText(R.string.call_connected_is_incoming);
109 | } else {
110 | endCallFab.setVisibility(View.VISIBLE);
111 | answerCallFab.setVisibility(View.GONE);
112 | rejectCallFab.setVisibility(View.GONE);
113 | callStateView.setText(R.string.call_connecting);
114 | }
115 |
116 | micSwitch.setActivated(!CallManager.getInstance().isOpenMic());
117 | cameraSwitch.setActivated(!CallManager.getInstance().isOpenCamera());
118 | speakerSwitch.setActivated(CallManager.getInstance().isOpenSpeaker());
119 | recordSwitch.setActivated(CallManager.getInstance().isOpenRecord());
120 |
121 | // 初始化视频通话帮助类
122 | videoCallHelper = EMClient.getInstance().callManager().getVideoCallHelper();
123 |
124 | // 初始化显示通话画面
125 | initCallSurface();
126 | // 判断当前通话时刚开始,还是从后台恢复已经存在的通话
127 | if (CallManager.getInstance().getCallState() == CallManager.CallState.ACCEPTED) {
128 | endCallFab.setVisibility(View.VISIBLE);
129 | answerCallFab.setVisibility(View.GONE);
130 | rejectCallFab.setVisibility(View.GONE);
131 | callStateView.setText(R.string.call_accepted);
132 | refreshCallTime();
133 | // 通话已接通,修改画面显示
134 | onCallSurface();
135 | }
136 |
137 | try {
138 | // 设置默认摄像头为前置
139 | EMClient.getInstance()
140 | .callManager()
141 | .setCameraFacing(Camera.CameraInfo.CAMERA_FACING_FRONT);
142 | } catch (HyphenateException e) {
143 | e.printStackTrace();
144 | }
145 | if (CallManager.getInstance().isExternalInputData()) {
146 | new PreviewManager(surfaceView);
147 | }
148 | }
149 |
150 | /**
151 | * 界面控件点击监听器
152 | */
153 | @OnClick({
154 | R.id.layout_call_control, R.id.btn_exit_full_screen, R.id.btn_call_info,
155 | R.id.btn_mic_switch, R.id.btn_camera_switch, R.id.btn_speaker_switch,
156 | R.id.btn_record_switch, R.id.btn_screenshot, R.id.btn_change_camera_switch,
157 | R.id.fab_reject_call, R.id.fab_end_call, R.id.fab_answer_call
158 | })
159 | void onClick(View v) {
160 | switch (v.getId()) {
161 | case R.id.layout_call_control:
162 | onControlLayout();
163 | break;
164 | case R.id.btn_exit_full_screen:
165 | // 最小化通话界面
166 | exitFullScreen();
167 | break;
168 | case R.id.btn_call_info:
169 | callInfoMonitor();
170 | break;
171 | case R.id.btn_mic_switch:
172 | // 麦克风开关
173 | onMicrophone();
174 | break;
175 | case R.id.btn_camera_switch:
176 | // 摄像头开关
177 | onCamera();
178 | break;
179 | case R.id.btn_speaker_switch:
180 | // 扬声器开关
181 | onSpeaker();
182 | break;
183 | case R.id.btn_screenshot:
184 | // 保存通话截图
185 | onScreenShot();
186 | break;
187 | case R.id.btn_record_switch:
188 | // 录制开关
189 | onRecordCall();
190 | break;
191 | case R.id.btn_change_camera_switch:
192 | // 切换摄像头
193 | changeCamera();
194 | break;
195 | case R.id.fab_end_call:
196 | // 结束通话
197 | endCall();
198 | break;
199 | case R.id.fab_reject_call:
200 | // 拒绝接听通话
201 | rejectCall();
202 | break;
203 | case R.id.fab_answer_call:
204 | // 接听通话
205 | answerCall();
206 | break;
207 | }
208 | }
209 |
210 | /**
211 | * 控制界面的显示与隐藏
212 | */
213 | private void onControlLayout() {
214 | if (controlLayout.isShown()) {
215 | controlLayout.setVisibility(View.GONE);
216 | } else {
217 | controlLayout.setVisibility(View.VISIBLE);
218 | }
219 | }
220 |
221 | /**
222 | * 退出全屏通话界面
223 | */
224 | private void exitFullScreen() {
225 | CallManager.getInstance().addFloatWindow();
226 | // 结束当前界面
227 | onFinish();
228 | }
229 |
230 | /**
231 | * 通话信息监听器
232 | */
233 | private void callInfoMonitor() {
234 | if (isMonitor) {
235 | isMonitor = false;
236 | callInfoView.setVisibility(View.GONE);
237 | callInfoBtn.setActivated(isMonitor);
238 | } else {
239 | isMonitor = true;
240 | callInfoView.setVisibility(View.VISIBLE);
241 | callInfoBtn.setActivated(isMonitor);
242 | new Thread(new Runnable() {
243 | public void run() {
244 | while (isMonitor) {
245 | final String info = String.format("分辨率: %d*%d, \n延迟: %d, \n帧率: %d, \n丢失: %d, \n本地码率: %d, \n远端码率: %d, \n直连: %b", videoCallHelper
246 | .getVideoWidth(), videoCallHelper.getVideoHeight(), videoCallHelper.getVideoLatency(), videoCallHelper
247 | .getVideoFrameRate(), videoCallHelper.getVideoLostRate(), videoCallHelper
248 | .getLocalBitrate(), videoCallHelper.getRemoteBitrate(), EMClient.getInstance()
249 | .callManager()
250 | .isDirectCall());
251 | runOnUiThread(new Runnable() {
252 | public void run() {
253 | callInfoView.setText(info);
254 | }
255 | });
256 | try {
257 | Thread.sleep(1500);
258 | } catch (InterruptedException e) {
259 | }
260 | }
261 | }
262 | }).start();
263 | }
264 | }
265 |
266 | /**
267 | * 麦克风开关,主要调用环信语音数据传输方法
268 | */
269 | private void onMicrophone() {
270 | try {
271 | // 根据麦克风开关是否被激活来进行判断麦克风状态,然后进行下一步操作
272 | if (micSwitch.isActivated()) {
273 | // 设置按钮状态
274 | micSwitch.setActivated(false);
275 | // 暂停语音数据的传输
276 | EMClient.getInstance().callManager().resumeVoiceTransfer();
277 | CallManager.getInstance().setOpenMic(true);
278 | } else {
279 | // 设置按钮状态
280 | micSwitch.setActivated(true);
281 | // 恢复语音数据的传输
282 | EMClient.getInstance().callManager().pauseVoiceTransfer();
283 | CallManager.getInstance().setOpenMic(false);
284 | }
285 | } catch (HyphenateException e) {
286 | VMLog.e("exception code: %d, %s", e.getErrorCode(), e.getMessage());
287 | e.printStackTrace();
288 | }
289 | }
290 |
291 | /**
292 | * 摄像头开关
293 | */
294 | private void onCamera() {
295 | try {
296 | // 根据摄像头开关按钮状态判断摄像头状态,然后进行下一步操作
297 | if (cameraSwitch.isActivated()) {
298 | // 设置按钮状态
299 | cameraSwitch.setActivated(false);
300 | // 暂停视频数据的传输
301 | EMClient.getInstance().callManager().resumeVideoTransfer();
302 | CallManager.getInstance().setOpenCamera(true);
303 | } else {
304 | // 设置按钮状态
305 | cameraSwitch.setActivated(true);
306 | // 恢复视频数据的传输
307 | EMClient.getInstance().callManager().pauseVideoTransfer();
308 | CallManager.getInstance().setOpenCamera(false);
309 | }
310 | } catch (HyphenateException e) {
311 | VMLog.e("exception code: %d, %s", e.getErrorCode(), e.getMessage());
312 | e.printStackTrace();
313 | }
314 | }
315 |
316 | /**
317 | * 扬声器开关
318 | */
319 | private void onSpeaker() {
320 | // 根据按钮状态决定打开还是关闭扬声器
321 | if (speakerSwitch.isActivated()) {
322 | // 设置按钮状态
323 | speakerSwitch.setActivated(false);
324 | CallManager.getInstance().closeSpeaker();
325 | CallManager.getInstance().setOpenSpeaker(false);
326 | } else {
327 | // 设置按钮状态
328 | speakerSwitch.setActivated(true);
329 | CallManager.getInstance().openSpeaker();
330 | CallManager.getInstance().setOpenSpeaker(true);
331 | }
332 | }
333 |
334 | /**
335 | * 录制视屏通话内容
336 | */
337 | private void onRecordCall() {
338 | // 根据开关状态决定是否开启录制
339 | if (recordSwitch.isActivated()) {
340 | // 设置按钮状态
341 | recordSwitch.setActivated(false);
342 | String path = videoCallHelper.stopVideoRecord();
343 | CallManager.getInstance().setOpenRecord(false);
344 | File file = new File(path);
345 | if (file.exists()) {
346 | Toast.makeText(activity, "录制视频成功 " + path, Toast.LENGTH_LONG).show();
347 | } else {
348 | Toast.makeText(activity, "录制失败/(ㄒoㄒ)/~~", Toast.LENGTH_LONG).show();
349 | }
350 | } else {
351 | // 设置按钮状态
352 | recordSwitch.setActivated(true);
353 | // 先创建文件夹
354 | String dirPath = getExternalFilesDir("").getAbsolutePath() + "/videos";
355 | File dir = new File(dirPath);
356 | if (!dir.isDirectory()) {
357 | dir.mkdirs();
358 | }
359 | videoCallHelper.startVideoRecord(dirPath);
360 | VMLog.d("开始录制视频");
361 | Toast.makeText(activity, "开始录制", Toast.LENGTH_LONG).show();
362 | CallManager.getInstance().setOpenRecord(true);
363 | }
364 | }
365 |
366 | /**
367 | * 保存通话截图
368 | */
369 | private void onScreenShot() {
370 | String dirPath = VMFile.getFilesFromSDCard() + "videos/";
371 | File dir = new File(dirPath);
372 | if (!dir.isDirectory()) {
373 | dir.mkdirs();
374 | }
375 | String path = dirPath + "IMG_" + System.currentTimeMillis() + ".jpg";
376 | videoCallHelper.takePicture(path);
377 | Toast.makeText(activity, "拍照保存成功 " + path, Toast.LENGTH_LONG).show();
378 | Bitmap bitmap = BitmapFactory.decodeFile(path);
379 | // testImgView.setImageBitmap(bitmap);
380 | // testImgView.setVisibility(View.VISIBLE);
381 | // testImgView.setOnClickListener(new View.OnClickListener() {
382 | // @Override public void onClick(View v) {
383 | // testImgView.setVisibility(View.GONE);
384 | // }
385 | // });
386 | }
387 |
388 | /**
389 | * 切换摄像头
390 | */
391 | private void changeCamera() {
392 | // 根据切换摄像头开关是否被激活确定当前是前置还是后置摄像头
393 | try {
394 | if (EMClient.getInstance().callManager().getCameraFacing() == 1) {
395 | EMClient.getInstance().callManager().switchCamera();
396 | EMClient.getInstance().callManager().setCameraFacing(0);
397 | } else {
398 | EMClient.getInstance().callManager().switchCamera();
399 | EMClient.getInstance().callManager().setCameraFacing(1);
400 | }
401 | } catch (HyphenateException e) {
402 | e.printStackTrace();
403 | }
404 | }
405 |
406 | /**
407 | * 接听通话
408 | */
409 | @Override
410 | protected void answerCall() {
411 | super.answerCall();
412 | endCallFab.setVisibility(View.VISIBLE);
413 | rejectCallFab.setVisibility(View.GONE);
414 | answerCallFab.setVisibility(View.GONE);
415 | }
416 |
417 | /**
418 | * 初始化通话界面控件
419 | */
420 | private void initCallSurface() {
421 | // 初始化显示远端画面控件
422 | oppositeSurface = new EMCallSurfaceView(activity);
423 | oppositeParams = new RelativeLayout.LayoutParams(0, 0);
424 | oppositeParams.width = RelativeLayout.LayoutParams.MATCH_PARENT;
425 | oppositeParams.height = RelativeLayout.LayoutParams.MATCH_PARENT;
426 | oppositeSurface.setLayoutParams(oppositeParams);
427 | surfaceLayout.addView(oppositeSurface);
428 |
429 | // 初始化显示本地画面控件
430 | localSurface = new EMCallSurfaceView(activity);
431 | localParams = new RelativeLayout.LayoutParams(0, 0);
432 | localParams.width = RelativeLayout.LayoutParams.MATCH_PARENT;
433 | localParams.height = RelativeLayout.LayoutParams.MATCH_PARENT;
434 | localParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
435 | localSurface.setLayoutParams(localParams);
436 | surfaceLayout.addView(localSurface);
437 |
438 | localSurface.setOnClickListener(new View.OnClickListener() {
439 | @Override
440 | public void onClick(View v) {
441 | onControlLayout();
442 | }
443 | });
444 |
445 | localSurface.setZOrderOnTop(false);
446 | localSurface.setZOrderMediaOverlay(true);
447 |
448 | // 设置本地和远端画面的显示方式,是填充,还是居中
449 | localSurface.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFit);
450 | oppositeSurface.setScaleMode(VideoView.EMCallViewScaleMode.EMCallViewScaleModeAspectFit);
451 | // 设置通话画面显示控件
452 | EMClient.getInstance().callManager().setSurfaceView(localSurface, oppositeSurface);
453 | }
454 |
455 | /**
456 | * 接通通话,这个时候要做的只是改变本地画面 view 大小,不需要做其他操作
457 | */
458 | private void onCallSurface() {
459 | // 更新通话界面控件状态
460 | surfaceState = 0;
461 |
462 | localParams = new RelativeLayout.LayoutParams(littleWidth, littleHeight);
463 | localParams.width = littleWidth;
464 | localParams.height = littleHeight;
465 | localParams.rightMargin = rightMargin;
466 | localParams.topMargin = topMargin;
467 | localParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
468 | localSurface.setLayoutParams(localParams);
469 |
470 | localSurface.setOnClickListener(new View.OnClickListener() {
471 | @Override
472 | public void onClick(View v) {
473 | changeCallSurface();
474 | }
475 | });
476 |
477 | oppositeSurface.setOnClickListener(new View.OnClickListener() {
478 | @Override
479 | public void onClick(View v) {
480 | onControlLayout();
481 | }
482 | });
483 | }
484 |
485 | /**
486 | * 切换通话界面,这里就是交换本地和远端画面控件设置,以达到通话大小画面的切换
487 | */
488 | private void changeCallSurface() {
489 | if (surfaceState == 0) {
490 | surfaceState = 1;
491 | EMClient.getInstance().callManager().setSurfaceView(oppositeSurface, localSurface);
492 | } else {
493 | surfaceState = 0;
494 | EMClient.getInstance().callManager().setSurfaceView(localSurface, oppositeSurface);
495 | }
496 | }
497 |
498 | @Subscribe(threadMode = ThreadMode.MAIN)
499 | public void onEventBus(CallEvent event) {
500 | if (event.isState()) {
501 | refreshCallView(event);
502 | }
503 | if (event.isTime()) {
504 | // 不论什么情况都检查下当前时间
505 | refreshCallTime();
506 | }
507 | }
508 |
509 | /**
510 | * 刷新通话界面
511 | */
512 | private void refreshCallView(CallEvent event) {
513 | EMCallStateChangeListener.CallError callError = event.getCallError();
514 | EMCallStateChangeListener.CallState callState = event.getCallState();
515 | switch (callState) {
516 | case CONNECTING: // 正在呼叫对方,TODO 没见回调过
517 | VMLog.i("正在呼叫对方" + callError);
518 | break;
519 | case CONNECTED: // 正在等待对方接受呼叫申请(对方申请与你进行通话)
520 | VMLog.i("正在连接" + callError);
521 | if (CallManager.getInstance().isInComingCall()) {
522 | callStateView.setText(R.string.call_connected_is_incoming);
523 | } else {
524 | callStateView.setText(R.string.call_connected);
525 | }
526 | break;
527 | case ACCEPTED: // 通话已接通
528 | VMLog.i("通话已接通");
529 | callStateView.setText(R.string.call_accepted);
530 | // 通话接通,更新界面 UI 显示
531 | onCallSurface();
532 | break;
533 | case DISCONNECTED: // 通话已中断
534 | VMLog.i("通话已结束" + callError);
535 | onFinish();
536 | break;
537 | case NETWORK_DISCONNECTED:
538 | Toast.makeText(activity, "对方网络断开", Toast.LENGTH_SHORT).show();
539 | VMLog.i("对方网络断开");
540 | break;
541 | case NETWORK_NORMAL:
542 | VMLog.i("网络正常");
543 | break;
544 | case NETWORK_UNSTABLE:
545 | if (callError == EMCallStateChangeListener.CallError.ERROR_NO_DATA) {
546 | VMLog.i("没有通话数据" + callError);
547 | } else {
548 | VMLog.i("网络不稳定" + callError);
549 | }
550 | break;
551 | case VIDEO_PAUSE:
552 | Toast.makeText(activity, "对方已暂停视频传输", Toast.LENGTH_SHORT).show();
553 | VMLog.i("对方已暂停视频传输");
554 | break;
555 | case VIDEO_RESUME:
556 | Toast.makeText(activity, "对方已恢复视频传输", Toast.LENGTH_SHORT).show();
557 | VMLog.i("对方已恢复视频传输");
558 | break;
559 | case VOICE_PAUSE:
560 | Toast.makeText(activity, "对方已暂停语音传输", Toast.LENGTH_SHORT).show();
561 | VMLog.i("对方已暂停语音传输");
562 | break;
563 | case VOICE_RESUME:
564 | Toast.makeText(activity, "对方已恢复语音传输", Toast.LENGTH_SHORT).show();
565 | VMLog.i("对方已恢复语音传输");
566 | break;
567 | default:
568 | break;
569 | }
570 | }
571 |
572 | /**
573 | * 刷新通话时间显示
574 | */
575 | private void refreshCallTime() {
576 | int t = CallManager.getInstance().getCallTime();
577 | int h = t / 60 / 60;
578 | int m = t / 60 % 60;
579 | int s = t % 60 % 60;
580 | String time = "";
581 | if (h > 9) {
582 | time = "" + h;
583 | } else {
584 | time = "0" + h;
585 | }
586 | if (m > 9) {
587 | time += ":" + m;
588 | } else {
589 | time += ":0" + m;
590 | }
591 | if (s > 9) {
592 | time += ":" + s;
593 | } else {
594 | time += ":0" + s;
595 | }
596 | if (!callTimeView.isShown()) {
597 | callTimeView.setVisibility(View.VISIBLE);
598 | }
599 | callTimeView.setText(time);
600 | }
601 |
602 | /**
603 | * 屏幕方向改变回调方法
604 | */
605 | @Override
606 | public void onConfigurationChanged(Configuration newConfig) {
607 | super.onConfigurationChanged(newConfig);
608 | }
609 |
610 | @Override
611 | protected void onUserLeaveHint() {
612 | //super.onUserLeaveHint();
613 | exitFullScreen();
614 | }
615 |
616 | /**
617 | * 通话界面拦截 Back 按键,不能返回
618 | */
619 | @Override
620 | public void onBackPressed() {
621 | //super.onBackPressed();
622 | exitFullScreen();
623 | }
624 |
625 | @Override
626 | public void onFinish() {
627 | // release surface view
628 | if (localSurface != null) {
629 | if (localSurface.getRenderer() != null) {
630 | localSurface.getRenderer().dispose();
631 | }
632 | localSurface.release();
633 | localSurface = null;
634 | }
635 | if (oppositeSurface != null) {
636 | if (oppositeSurface.getRenderer() != null) {
637 | oppositeSurface.getRenderer().dispose();
638 | }
639 | oppositeSurface.release();
640 | oppositeSurface = null;
641 | }
642 | super.onFinish();
643 | }
644 | }
645 |
--------------------------------------------------------------------------------
/app/src/main/java/com/vmloft/develop/app/demo/call/single/CallManager.java:
--------------------------------------------------------------------------------
1 | package com.vmloft.develop.app.demo.call.single;
2 |
3 | import android.app.Notification;
4 | import android.app.NotificationManager;
5 | import android.app.PendingIntent;
6 | import android.bluetooth.BluetoothAdapter;
7 | import android.bluetooth.BluetoothHeadset;
8 | import android.bluetooth.BluetoothProfile;
9 | import android.content.Context;
10 | import android.content.Intent;
11 | import android.media.AudioAttributes;
12 | import android.media.AudioManager;
13 | import android.media.SoundPool;
14 | import android.support.v4.app.NotificationCompat;
15 |
16 | import com.hyphenate.chat.EMCallManager;
17 | import com.hyphenate.chat.EMClient;
18 | import com.hyphenate.chat.EMMessage;
19 | import com.hyphenate.chat.EMTextMessageBody;
20 | import com.hyphenate.exceptions.EMNoActiveCallException;
21 | import com.hyphenate.exceptions.EMServiceNotReadyException;
22 |
23 | import com.vmloft.develop.app.demo.call.R;
24 | import com.vmloft.develop.library.tools.utils.VMLog;
25 |
26 | import java.util.Timer;
27 | import java.util.TimerTask;
28 |
29 | import org.greenrobot.eventbus.EventBus;
30 |
31 | /**
32 | * Created by lzan13 on 2017/2/8.
33 | *
34 | * 实时音视频通话管理类,这是一个单例类,用来管理 app 通话操作
35 | */
36 | public class CallManager {
37 |
38 | // 上下文菜单
39 | private Context context;
40 |
41 | // 蓝牙相关对象
42 | private BluetoothAdapter bluetoothAdapter;
43 | private BluetoothHeadset bluetoothHeadset;
44 |
45 | // 单例类实例
46 | private static CallManager instance;
47 |
48 | // 通知栏提醒管理类
49 | private NotificationManager notificationManager;
50 | private int callNotificationId = 0526;
51 |
52 | // 音频管理器
53 | private AudioManager audioManager;
54 | // 音频池
55 | private SoundPool soundPool;
56 | // 声音资源 id
57 | private int streamID;
58 | private int loadId;
59 | private boolean isLoaded = false;
60 |
61 | // 通话状态监听
62 | private CallStateListener callStateListener;
63 |
64 | // 记录通话方向,是呼出还是呼入
65 | private boolean isInComingCall = true;
66 | private boolean isExternalInputData = false;
67 | // 设备相关开关
68 | private boolean isOpenCamera = true;
69 | private boolean isOpenMic = true;
70 | private boolean isOpenSpeaker = true;
71 | private boolean isOpenRecord = false;
72 |
73 | // 计时器
74 | private Timer timer;
75 | // 通话时间
76 | private int callTime = 0;
77 |
78 | // 当前通话对象 id
79 | private String chatId;
80 | private CallState callState = CallState.DISCONNECTED;
81 | private CallType callType = CallType.VIDEO;
82 | private EndType endType = EndType.CANCEL;
83 |
84 | /**
85 | * 私有化构造函数
86 | */
87 | private CallManager() {
88 | }
89 |
90 | /**
91 | * 获取单例对象实例方法
92 | */
93 | public static CallManager getInstance() {
94 | if (instance == null) {
95 | instance = new CallManager();
96 | }
97 | return instance;
98 | }
99 |
100 | /**
101 | * 通话管理类的初始化
102 | */
103 | public void init(Context context) {
104 | this.context = context;
105 | // 初始化蓝牙监听
106 | initBluetoothListener();
107 | // 初始化音频池
108 | initSoundPool();
109 | // 音频管理器
110 | audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
111 |
112 | /**
113 | * SDK 3.2.x 版本后通话相关设置,一定要在初始化后,开始音视频功能前设置,否则设置无效
114 | */
115 | // 设置通话过程中对方如果离线是否发送离线推送通知,默认 false,这里需要和推送配合使用
116 | EMClient.getInstance().callManager().getCallOptions().setIsSendPushIfOffline(false);
117 | /**
118 | * 设置是否启用外部输入视频数据,默认 false,如果设置为 true,需要自己调用
119 | * {@link EMCallManager#inputExternalVideoData(byte[], int, int, int)}输入视频数据
120 | * 视频数据的格式是摄像头采集的格式即:NV21 420sp 自己手动传入时需要将 rgb 格式的数据转为 yuv
121 | */
122 | EMClient.getInstance().callManager().getCallOptions().setEnableExternalVideoData(isExternalInputData);
123 | // 设置视频旋转角度,启动前和视频通话中均可设置
124 | //EMClient.getInstance().callManager().getCallOptions().setRotation(90);
125 | // 设置自动调节分辨率,默认为 true
126 | EMClient.getInstance().callManager().getCallOptions().enableFixedVideoResolution(true);
127 | /**
128 | * 设置视频通话最大和最小比特率,可以不用设置,比特率会根据分辨率进行计算,默认最大(800), 默认最小(80)
129 | * 这里的带宽是指理想带宽,指单人单线情况下的最低要求
130 | * >240p: 100k ~ 400kbps
131 | * >480p: 300k ~ 1Mbps
132 | * >720p: 900k ~ 2.5Mbps
133 | * >1080p: 2M ~ 5Mbps
134 | */
135 | EMClient.getInstance().callManager().getCallOptions().setMaxVideoKbps(800);
136 | EMClient.getInstance().callManager().getCallOptions().setMinVideoKbps(80);
137 | // 需要录制视频
138 | EMClient.getInstance().callManager().getCallOptions().enableFixedVideoResolution(true);
139 | // 设置视频通话分辨率 默认是(640, 480)
140 | EMClient.getInstance().callManager().getCallOptions().setVideoResolution(640, 480);
141 | // 设置通话最大帧率,SDK 最大支持(30),默认(20)
142 | EMClient.getInstance().callManager().getCallOptions().setMaxVideoFrameRate(20);
143 | // 设置音视频通话采样率,一般不需要设置,为了减少噪音,可以讲采集了适当调低,这里默认设置32k
144 | EMClient.getInstance().callManager().getCallOptions().setAudioSampleRate(16000);
145 | EMClient.getInstance().callManager().getCallOptions().setMaxAudioKbps(16);
146 | // 设置录制视频采用 mov 编码 TODO 后期这个而接口需要移动到 EMCallOptions 中
147 | EMClient.getInstance().callManager().getVideoCallHelper().setPreferMovFormatEnable(true);
148 | // 设置通话音频源类型
149 | // EMClient.getInstance().callManager().getCallOptions().setCallAudioSource(MediaRecorder.AudioSource.MIC);
150 | }
151 |
152 | /**
153 | * 通话结束,保存一条记录通话的消息
154 | */
155 | public void saveCallMessage() {
156 | VMLog.d("The call ends and the call log message is saved! " + endType);
157 | EMMessage message = null;
158 | EMTextMessageBody body = null;
159 | String content = null;
160 | if (isInComingCall) {
161 | message = EMMessage.createReceiveMessage(EMMessage.Type.TXT);
162 | message.setFrom(chatId);
163 | } else {
164 | message = EMMessage.createSendMessage(EMMessage.Type.TXT);
165 | message.setTo(chatId);
166 | }
167 | switch (endType) {
168 | case NORMAL: // 正常结束通话
169 | content = String.valueOf(getCallTime());
170 | break;
171 | case CANCEL: // 取消
172 | content = context.getString(R.string.call_cancel);
173 | break;
174 | case CANCELLED: // 被取消
175 | content = context.getString(R.string.call_cancel_is_incoming);
176 | break;
177 | case BUSY: // 对方忙碌
178 | content = context.getString(R.string.call_busy);
179 | break;
180 | case OFFLINE: // 对方不在线
181 | content = context.getString(R.string.call_offline);
182 | break;
183 | case REJECT: // 拒绝的
184 | content = context.getString(R.string.call_reject_is_incoming);
185 | break;
186 | case REJECTED: // 被拒绝的
187 | content = context.getString(R.string.call_reject);
188 | break;
189 | case NORESPONSE: // 未响应
190 | content = context.getString(R.string.call_no_response);
191 | break;
192 | case TRANSPORT: // 建立连接失败
193 | content = context.getString(R.string.call_connection_fail);
194 | break;
195 | case DIFFERENT: // 通讯协议不同
196 | content = context.getString(R.string.call_offline);
197 | break;
198 | default:
199 | // 默认取消
200 | content = context.getString(R.string.call_cancel);
201 | break;
202 | }
203 | body = new EMTextMessageBody(content);
204 | message.addBody(body);
205 | message.setStatus(EMMessage.Status.SUCCESS);
206 | if (callType == CallType.VIDEO) {
207 | message.setAttribute("attr_call_video", true);
208 | } else {
209 | message.setAttribute("attr_call_voice", true);
210 | }
211 | message.setUnread(false);
212 | // 调用sdk的保存消息方法
213 | EMClient.getInstance().chatManager().saveMessage(message);
214 | }
215 |
216 | /**
217 | * 开始呼叫对方
218 | */
219 | public void makeCall() {
220 | try {
221 | if (callType == CallType.VIDEO) {
222 | EMClient.getInstance()
223 | .callManager()
224 | .makeVideoCall(chatId, "{'ext':{'type':'video','key':'value'}}");
225 | } else {
226 | EMClient.getInstance()
227 | .callManager()
228 | .makeVoiceCall(chatId, "{'ext':{'type':'voice','key':'value'}}");
229 | }
230 | setEndType(EndType.CANCEL);
231 | } catch (EMServiceNotReadyException e) {
232 | e.printStackTrace();
233 | }
234 | }
235 |
236 | /**
237 | * 拒绝通话
238 | */
239 | public void rejectCall() {
240 | try {
241 | VMLog.i("rejectCall");
242 | // 调用 SDK 的拒绝通话方法
243 | EMClient.getInstance().callManager().rejectCall();
244 | // 设置结束原因为拒绝
245 | setEndType(EndType.REJECT);
246 | } catch (EMNoActiveCallException e) {
247 | e.printStackTrace();
248 | }
249 | // 保存一条通话消息
250 | saveCallMessage();
251 | // 通话结束,重置通话状态
252 | reset();
253 | }
254 |
255 | /**
256 | * 结束通话
257 | */
258 | public void endCall() {
259 | try {
260 | VMLog.i("endCall");
261 | // 调用 SDK 的结束通话方法
262 | EMClient.getInstance().callManager().endCall();
263 | } catch (EMNoActiveCallException e) {
264 | e.printStackTrace();
265 | VMLog.e("结束通话失败:error %d - %s", e.getErrorCode(), e.getMessage());
266 | }
267 | // 挂断电话调用保存消息方法
268 | saveCallMessage();
269 | // 通话结束,重置通话状态
270 | reset();
271 | }
272 |
273 | /**
274 | * 接听通话
275 | */
276 | public boolean answerCall() {
277 | // 接听通话后关闭通知铃音
278 | stopCallSound();
279 | // 调用接通通话方法
280 | try {
281 | VMLog.i("answerCall");
282 | EMClient.getInstance().callManager().answerCall();
283 | return true;
284 | } catch (EMNoActiveCallException e) {
285 | e.printStackTrace();
286 | return false;
287 | }
288 | }
289 |
290 | /**
291 | * 打开扬声器
292 | * 主要是通过扬声器的开关以及设置音频播放模式来实现
293 | * 1、MODE_NORMAL:是正常模式,一般用于外放音频
294 | * 2、MODE_IN_CALL:
295 | * 3、MODE_IN_COMMUNICATION:这个和 CALL 都表示通讯模式,不过 CALL 在华为上不好使,故使用 COMMUNICATION
296 | * 4、MODE_RINGTONE:铃声模式
297 | */
298 | public void openSpeaker() {
299 | // 检查是否已经开启扬声器
300 | if (!audioManager.isSpeakerphoneOn()) {
301 | // 打开扬声器
302 | audioManager.setSpeakerphoneOn(true);
303 | }
304 | if (callState == CallManager.CallState.ACCEPTED) {
305 | // 开启了扬声器之后,因为是进行通话,声音的模式也要设置成通讯模式
306 | audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
307 | } else {
308 | // 在播放通话音效时声音模式需要设置为铃音模式
309 | audioManager.setMode(AudioManager.MODE_NORMAL);
310 | }
311 | setOpenSpeaker(true);
312 |
313 | disconnectBluetoothAudio();
314 | }
315 |
316 | /**
317 | * 关闭扬声器,即开启听筒播放模式
318 | * 更多内容看{@link #openSpeaker()}
319 | */
320 | public void closeSpeaker() {
321 | // 检查是否已经开启扬声器
322 | if (audioManager.isSpeakerphoneOn()) {
323 | // 关闭扬声器
324 | audioManager.setSpeakerphoneOn(false);
325 | }
326 | if (callState == CallManager.CallState.ACCEPTED) {
327 | // 设置声音模式为通讯模式
328 | audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
329 | } else {
330 | // 在播放通话音效时声音模式需要设置为铃音模式
331 | audioManager.setMode(AudioManager.MODE_NORMAL);
332 | }
333 | setOpenSpeaker(false);
334 |
335 | connectBluetoothAudio();
336 | }
337 |
338 | /**
339 | * 初始化蓝牙监听
340 | */
341 | private void initBluetoothListener() {
342 | bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
343 | if (bluetoothAdapter != null) {
344 | bluetoothAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {
345 | @Override
346 | public void onServiceConnected(int profile, BluetoothProfile proxy) {
347 | bluetoothHeadset = (BluetoothHeadset) proxy;
348 | VMLog.d("bluetooth is ");
349 | }
350 |
351 | @Override
352 | public void onServiceDisconnected(int profile) {
353 | bluetoothHeadset = null;
354 | }
355 | }, BluetoothProfile.HEADSET);
356 | }
357 | }
358 |
359 | /**
360 | * 连接蓝牙音频输出设备,通过蓝牙输出声音
361 | */
362 | private void connectBluetoothAudio() {
363 | try {
364 | if (bluetoothHeadset != null) {
365 | bluetoothHeadset.startVoiceRecognition(bluetoothHeadset.getConnectedDevices()
366 | .get(0));
367 | }
368 | } catch (Exception e) {
369 | e.printStackTrace();
370 | }
371 | }
372 |
373 | /**
374 | * 与蓝牙输出设备断开连接
375 | */
376 | private void disconnectBluetoothAudio() {
377 | try {
378 | if (bluetoothHeadset != null) {
379 | bluetoothHeadset.stopVoiceRecognition(bluetoothHeadset.getConnectedDevices()
380 | .get(0));
381 | }
382 | } catch (Exception e) {
383 | e.printStackTrace();
384 | }
385 | }
386 |
387 | /**
388 | * ----------------------------- Sound start -----------------------------
389 | * 初始化 SoundPool
390 | */
391 | private void initSoundPool() {
392 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
393 | AudioAttributes attributes = new AudioAttributes.Builder()
394 | // 设置音频要用在什么地方,这里选择电话通知铃音
395 | .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
396 | .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
397 | .build();
398 | // 当系统的 SDK 版本高于21时,使用 build 的方式实例化 SoundPool
399 | soundPool = new SoundPool.Builder().setAudioAttributes(attributes)
400 | .setMaxStreams(1)
401 | .build();
402 | } else {
403 | // 老版本使用构造函数方式实例化 SoundPool,MODE 设置为铃音 MODE_RINGTONE
404 | soundPool = new SoundPool(1, AudioManager.MODE_RINGTONE, 0);
405 | }
406 | }
407 |
408 | /**
409 | * 加载音效资源
410 | */
411 | private void loadSound() {
412 | if (isInComingCall) {
413 | loadId = soundPool.load(context, R.raw.sound_call_incoming, 1);
414 | } else {
415 | loadId = soundPool.load(context, R.raw.sound_calling, 1);
416 | }
417 | }
418 |
419 | /**
420 | * 尝试播放呼叫通话提示音
421 | */
422 | public void attemptPlayCallSound() {
423 | // 检查音频资源是否已经加载完毕
424 | if (isLoaded) {
425 | playCallSound();
426 | } else {
427 | // 播放之前先去加载音效
428 | loadSound();
429 | // 设置资源加载监听,也因为加载资源在单独的进程,需要时间,所以等监听到加载完成才能播放
430 | soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
431 | @Override
432 | public void onLoadComplete(SoundPool soundPool, int i, int i1) {
433 | VMLog.d("SoundPool load complete! loadId: %d", loadId);
434 | isLoaded = true;
435 | // 首次监听到加载完毕,开始播放音频
436 | playCallSound();
437 | }
438 | });
439 | }
440 | }
441 |
442 | /**
443 | * 播放音频
444 | */
445 | private void playCallSound() {
446 | // 打开扬声器
447 | openSpeaker();
448 | // 设置音频管理器音频模式为铃音模式
449 | audioManager.setMode(AudioManager.MODE_NORMAL);
450 | // 播放提示音,返回一个播放的音频id,等下停止播放需要用到
451 | if (soundPool != null) {
452 | streamID = soundPool.play(loadId, // 播放资源id;就是加载到SoundPool里的音频资源顺序
453 | 0.5f, // 左声道音量
454 | 0.5f, // 右声道音量
455 | 1, // 优先级,数值越高,优先级越大
456 | -1, // 是否循环;0 不循环,-1 循环,N 表示循环次数
457 | 1); // 播放速率;从0.5-2,一般设置为1,表示正常播放
458 | }
459 | }
460 |
461 | /**
462 | * 关闭音效的播放,并释放资源
463 | */
464 | protected void stopCallSound() {
465 | if (soundPool != null) {
466 | // 停止播放音效
467 | soundPool.stop(streamID);
468 | // 卸载音效
469 | //soundPool.unload(loadId);
470 | // 释放资源
471 | //soundPool.release();
472 | }
473 | }
474 |
475 | /**
476 | * ----------------------------- Call state -----------------------------
477 | * 注册通话状态监听,监听音视频通话状态
478 | * 状态监听详细实现在 {@link CallStateListener} 类中
479 | */
480 | public void registerCallStateListener() {
481 | if (callStateListener == null) {
482 | callStateListener = new CallStateListener();
483 | }
484 | EMClient.getInstance().callManager().addCallStateChangeListener(callStateListener);
485 | }
486 |
487 | /**
488 | * 删除通话状态监听
489 | */
490 | private void unregisterCallStateListener() {
491 | if (callStateListener != null) {
492 | EMClient.getInstance().callManager().removeCallStateChangeListener(callStateListener);
493 | callStateListener = null;
494 | }
495 | }
496 |
497 | /**
498 | * 添加通话悬浮窗并发送通知栏提醒
499 | */
500 | public void addFloatWindow() {
501 | // 发送通知栏提醒
502 | addCallNotification();
503 | // 开启悬浮窗
504 | FloatWindow.getInstance(context).addFloatWindow();
505 | }
506 |
507 | /**
508 | * 移除通话悬浮窗和通知栏提醒
509 | */
510 | public void removeFloatWindow() {
511 | // 取消通知栏提醒
512 | cancelCallNotification();
513 | // 关闭悬浮窗
514 | FloatWindow.getInstance(context).removeFloatWindow();
515 | }
516 |
517 | /**
518 | * 发送通知栏提醒,告知用户通话继续进行中
519 | */
520 | private void addCallNotification() {
521 | notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
522 | NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
523 |
524 | builder.setSmallIcon(R.mipmap.ic_launcher);
525 | builder.setPriority(Notification.PRIORITY_HIGH);
526 | builder.setAutoCancel(true);
527 | builder.setDefaults(Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS);
528 |
529 | builder.setContentText("通话进行中,点击恢复");
530 |
531 | builder.setContentTitle(context.getString(R.string.app_name));
532 | Intent intent = new Intent();
533 | if (callType == CallType.VIDEO) {
534 | intent.setClass(context, VideoCallActivity.class);
535 | } else {
536 | intent.setClass(context, VoiceCallActivity.class);
537 | }
538 | PendingIntent pIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
539 | builder.setContentIntent(pIntent);
540 | builder.setOngoing(true);
541 |
542 | builder.setWhen(System.currentTimeMillis());
543 |
544 | notificationManager.notify(callNotificationId, builder.build());
545 | }
546 |
547 | /**
548 | * 取消通话状态通知栏提醒
549 | */
550 | public void cancelCallNotification() {
551 | if (notificationManager != null) {
552 | notificationManager.cancel(callNotificationId);
553 | }
554 | }
555 |
556 | /**
557 | * 开始通话计时,这里在全局管理器中开启一个定时器进行计时,可以做到最小化,以及后台时进行计时
558 | */
559 | public void startCallTime() {
560 | final CallEvent event = new CallEvent();
561 | EventBus.getDefault().post(event);
562 | event.setTime(true);
563 | if (timer == null) {
564 | timer = new Timer();
565 | }
566 | timer.purge();
567 | TimerTask task = new TimerTask() {
568 | @Override
569 | public void run() {
570 | callTime++;
571 | EventBus.getDefault().post(event);
572 | }
573 | };
574 | timer.scheduleAtFixedRate(task, 1000, 1000);
575 | }
576 |
577 | /**
578 | * 停止计时
579 | */
580 | public void stopCallTime() {
581 | if (timer != null) {
582 | timer.purge();
583 | timer.cancel();
584 | timer = null;
585 | }
586 | callTime = 0;
587 | }
588 |
589 | /**
590 | * 释放资源
591 | */
592 | public void reset() {
593 | isOpenCamera = true;
594 | isOpenMic = true;
595 | isOpenSpeaker = true;
596 | isOpenRecord = false;
597 | // 设置通话状态为已断开
598 | setCallState(CallState.DISCONNECTED);
599 | // 停止计时
600 | stopCallTime();
601 | // 取消注册通话状态的监听
602 | unregisterCallStateListener();
603 | // 释放音频资源
604 | if (soundPool != null) {
605 | // 停止播放音效
606 | soundPool.stop(streamID);
607 | }
608 | // 重置音频管理器
609 | if (audioManager != null) {
610 | audioManager.setSpeakerphoneOn(true);
611 | audioManager.setMode(AudioManager.MODE_NORMAL);
612 | }
613 | }
614 |
615 | /**
616 | * 相关的 get 以及 set 方法
617 | */
618 | public CallState getCallState() {
619 | return callState;
620 | }
621 |
622 | public void setCallState(CallState callState) {
623 | this.callState = callState;
624 | }
625 |
626 | public CallType getCallType() {
627 | return callType;
628 | }
629 |
630 | public void setCallType(CallType callType) {
631 | this.callType = callType;
632 | }
633 |
634 | public String getChatId() {
635 | return chatId;
636 | }
637 |
638 | public void setChatId(String chatId) {
639 | this.chatId = chatId;
640 | }
641 |
642 | public boolean isInComingCall() {
643 | return isInComingCall;
644 | }
645 |
646 | public void setInComingCall(boolean isInComingCall) {
647 | this.isInComingCall = isInComingCall;
648 | }
649 |
650 | public int getCallTime() {
651 | return callTime;
652 | }
653 |
654 | public void setEndType(EndType endType) {
655 | this.endType = endType;
656 | }
657 |
658 | public EndType getEndType() {
659 | return endType;
660 | }
661 |
662 | public boolean isExternalInputData() {
663 | return isExternalInputData;
664 | }
665 |
666 | public void setExternalInputData(boolean externalInputData) {
667 | isExternalInputData = externalInputData;
668 | }
669 |
670 | public boolean isOpenCamera() {
671 | return isOpenCamera;
672 | }
673 |
674 | public void setOpenCamera(boolean openCamera) {
675 | isOpenCamera = openCamera;
676 | }
677 |
678 | public boolean isOpenMic() {
679 | return isOpenMic;
680 | }
681 |
682 | public void setOpenMic(boolean openMic) {
683 | isOpenMic = openMic;
684 | }
685 |
686 | public boolean isOpenSpeaker() {
687 | return isOpenSpeaker;
688 | }
689 |
690 | public void setOpenSpeaker(boolean openSpeaker) {
691 | isOpenSpeaker = openSpeaker;
692 | }
693 |
694 | public boolean isOpenRecord() {
695 | return isOpenRecord;
696 | }
697 |
698 | public void setOpenRecord(boolean openRecord) {
699 | isOpenRecord = openRecord;
700 | }
701 |
702 | /**
703 | * 通话类型
704 | */
705 | public enum CallType {
706 | VIDEO,
707 | // 视频通话
708 | VOICE // 音频通话
709 | }
710 |
711 | /**
712 | * 通话状态枚举值
713 | */
714 | public enum CallState {
715 | CONNECTING,
716 | // 连接中
717 | CONNECTED,
718 | // 连接成功,等待接受
719 | ACCEPTED,
720 | // 通话中
721 | DISCONNECTED // 通话中断
722 |
723 | }
724 |
725 | /**
726 | * 通话结束状态类型
727 | */
728 | public enum EndType {
729 | NORMAL,
730 | // 正常结束通话
731 | CANCEL,
732 | // 取消
733 | CANCELLED,
734 | // 被取消
735 | BUSY,
736 | // 对方忙碌
737 | OFFLINE,
738 | // 对方不在线
739 | REJECT,
740 | // 拒绝的
741 | REJECTED,
742 | // 被拒绝的
743 | NORESPONSE,
744 | // 未响应
745 | TRANSPORT,
746 | // 建立连接失败
747 | DIFFERENT // 通讯协议不同
748 | }
749 | }
750 |
--------------------------------------------------------------------------------