فهرست منبع

<集成webrtc音视频通话>

weizhengliang 4 سال پیش
والد
کامیت
016def6f6a
100فایلهای تغییر یافته به همراه7941 افزوده شده و 2 حذف شده
  1. 5 0
      app/build.gradle
  2. 1 0
      app/src/main/AndroidManifest.xml
  3. 11 0
      app/src/main/code/com/wdkl/app/ncs/application/Application.kt
  4. 1 1
      build.gradle
  5. 4 0
      nursehome/build.gradle
  6. 24 1
      nursehome/src/main/java/com/wdkl/ncs/android/component/nursehome/activity/NurseHomeActivity.kt
  7. 1 0
      rtc-chat/.gitignore
  8. 60 0
      rtc-chat/bintray.gradle
  9. 41 0
      rtc-chat/build.gradle
  10. 21 0
      rtc-chat/proguard-rules.pro
  11. 2 0
      rtc-chat/src/main/AndroidManifest.xml
  12. 196 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/AVEngine.java
  13. 549 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/CallSession.java
  14. 41 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/EnumType.java
  15. 206 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/SkyEngineKit.java
  16. 43 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/engine/EngineCallback.java
  17. 121 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/engine/IEngine.java
  18. 355 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/engine/webrtc/Peer.java
  19. 693 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/engine/webrtc/WebRTCEngine.java
  20. 11 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/except/NotInitializedException.java
  21. 12 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/inter/ILogEvent.java
  22. 47 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/inter/ISkyEvent.java
  23. 8 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/model/RoomInfo.java
  24. 18 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/model/UserInfo.java
  25. 28 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/render/ProxyVideoSink.java
  26. 171 0
      rtc-chat/src/main/java/com/wdkl/skywebrtc/render/VideoFileRenderer.java
  27. 5 0
      webrtc/.gitignore
  28. 82 0
      webrtc/build.gradle
  29. 25 0
      webrtc/proguard-rules.pro
  30. 57 0
      webrtc/src/main/AndroidManifest.xml
  31. 23 0
      webrtc/src/main/java/com/wdkl/core/base/BaseActivity.java
  32. 30 0
      webrtc/src/main/java/com/wdkl/core/consts/Urls.java
  33. 48 0
      webrtc/src/main/java/com/wdkl/core/socket/IEvent.java
  34. 15 0
      webrtc/src/main/java/com/wdkl/core/socket/IUserState.java
  35. 500 0
      webrtc/src/main/java/com/wdkl/core/socket/MyWebSocket.java
  36. 367 0
      webrtc/src/main/java/com/wdkl/core/socket/SocketManager.java
  37. 35 0
      webrtc/src/main/java/com/wdkl/core/ui/event/MsgEvent.java
  38. 42 0
      webrtc/src/main/java/com/wdkl/core/ui/user/UserBean.java
  39. 53 0
      webrtc/src/main/java/com/wdkl/core/ui/user/UserListViewModel.java
  40. 131 0
      webrtc/src/main/java/com/wdkl/core/util/ActivityStackManager.java
  41. 58 0
      webrtc/src/main/java/com/wdkl/core/util/CrashHandler.java
  42. 127 0
      webrtc/src/main/java/com/wdkl/core/util/OSUtils.java
  43. 25 0
      webrtc/src/main/java/com/wdkl/core/util/StringUtil.java
  44. 52 0
      webrtc/src/main/java/com/wdkl/core/util/Utils.java
  45. 187 0
      webrtc/src/main/java/com/wdkl/core/voip/AsyncPlayer.java
  46. 129 0
      webrtc/src/main/java/com/wdkl/core/voip/CallForegroundNotification.java
  47. 198 0
      webrtc/src/main/java/com/wdkl/core/voip/CallMultiActivity.java
  48. 380 0
      webrtc/src/main/java/com/wdkl/core/voip/CallSingleActivity.java
  49. 150 0
      webrtc/src/main/java/com/wdkl/core/voip/FragmentAudio.java
  50. 118 0
      webrtc/src/main/java/com/wdkl/core/voip/FragmentMeeting.java
  51. 317 0
      webrtc/src/main/java/com/wdkl/core/voip/FragmentVideo.java
  52. 73 0
      webrtc/src/main/java/com/wdkl/core/voip/NineGridView.java
  53. 153 0
      webrtc/src/main/java/com/wdkl/core/voip/RomUtil.java
  54. 265 0
      webrtc/src/main/java/com/wdkl/core/voip/SettingsCompat.java
  55. 321 0
      webrtc/src/main/java/com/wdkl/core/voip/SingleCallFragment.java
  56. 7 0
      webrtc/src/main/java/com/wdkl/core/voip/Utils.java
  57. 111 0
      webrtc/src/main/java/com/wdkl/core/voip/VoipEvent.java
  58. 232 0
      webrtc/src/main/java/com/wdkl/core/voip/VoipReceiver.java
  59. 37 0
      webrtc/src/main/java/com/wdkl/net/HttpRequest.java
  60. 47 0
      webrtc/src/main/java/com/wdkl/net/HttpRequestPresenter.java
  61. 12 0
      webrtc/src/main/java/com/wdkl/net/ICallback.java
  62. 54 0
      webrtc/src/main/java/com/wdkl/net/urlconn/UrlConnRequest.java
  63. 263 0
      webrtc/src/main/java/com/wdkl/net/urlconn/UrlConnUtils.java
  64. 18 0
      webrtc/src/main/java/com/wdkl/permission/Consumer.java
  65. 144 0
      webrtc/src/main/java/com/wdkl/permission/Permissions.java
  66. BIN
      webrtc/src/main/res/drawable-xhdpi/av_audio_answer.png
  67. BIN
      webrtc/src/main/res/drawable-xhdpi/av_audio_answer_hover.png
  68. BIN
      webrtc/src/main/res/drawable-xhdpi/av_camera.png
  69. BIN
      webrtc/src/main/res/drawable-xhdpi/av_camera_hover.png
  70. BIN
      webrtc/src/main/res/drawable-xhdpi/av_default_header.png
  71. BIN
      webrtc/src/main/res/drawable-xhdpi/av_float_audio.png
  72. BIN
      webrtc/src/main/res/drawable-xhdpi/av_handfree.png
  73. BIN
      webrtc/src/main/res/drawable-xhdpi/av_handfree_hover.png
  74. BIN
      webrtc/src/main/res/drawable-xhdpi/av_hang_up.png
  75. BIN
      webrtc/src/main/res/drawable-xhdpi/av_hang_up_hover.png
  76. BIN
      webrtc/src/main/res/drawable-xhdpi/av_minimize.png
  77. BIN
      webrtc/src/main/res/drawable-xhdpi/av_mute.png
  78. BIN
      webrtc/src/main/res/drawable-xhdpi/av_mute_hover.png
  79. BIN
      webrtc/src/main/res/drawable-xhdpi/av_phone.png
  80. BIN
      webrtc/src/main/res/drawable-xhdpi/av_trans_audio.png
  81. BIN
      webrtc/src/main/res/drawable-xhdpi/av_video_answer.png
  82. BIN
      webrtc/src/main/res/drawable-xhdpi/av_video_answer_hover.png
  83. 20 0
      webrtc/src/main/res/drawable-xhdpi/bg_btn_white.xml
  84. 10 0
      webrtc/src/main/res/drawable/av_audio_answer_selector.xml
  85. 16 0
      webrtc/src/main/res/drawable/av_float_bg.xml
  86. 10 0
      webrtc/src/main/res/drawable/av_hangup_selector.xml
  87. 5 0
      webrtc/src/main/res/drawable/av_mute_selector.xml
  88. 5 0
      webrtc/src/main/res/drawable/av_speaker_selector.xml
  89. 10 0
      webrtc/src/main/res/drawable/av_switch_camera_selector.xml
  90. 10 0
      webrtc/src/main/res/drawable/av_video_answer_selector.xml
  91. 9 0
      webrtc/src/main/res/drawable/ic_dashboard_black_24dp.xml
  92. 9 0
      webrtc/src/main/res/drawable/ic_home_black_24dp.xml
  93. 9 0
      webrtc/src/main/res/drawable/ic_notifications_black_24dp.xml
  94. 52 0
      webrtc/src/main/res/layout/activity_launcher.xml
  95. 29 0
      webrtc/src/main/res/layout/activity_main.xml
  96. 21 0
      webrtc/src/main/res/layout/activity_multi_call.xml
  97. 9 0
      webrtc/src/main/res/layout/activity_single_call.xml
  98. 64 0
      webrtc/src/main/res/layout/av_p2p_audio_incoming.xml
  99. 92 0
      webrtc/src/main/res/layout/av_p2p_audio_outgoing.xml
  100. 0 0
      webrtc/src/main/res/layout/av_p2p_meeting_action.xml

+ 5 - 0
app/build.gradle

@@ -58,6 +58,11 @@ android {
 
         threadCount=8
     }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
 }
 
 dependencies {

+ 1 - 0
app/src/main/AndroidManifest.xml

@@ -22,6 +22,7 @@
         android:label="@string/javashop_app_name"
         android:supportsRtl="true"
         tools:replace="android:label"
+        tools:remove="android:requestLegacyExternalStorage"
         android:networkSecurityConfig="@xml/network_security_config"
         android:name="com.wdkl.app.ncs.application.Application"
         android:theme="@style/MyAppTheme">

+ 11 - 0
app/src/main/code/com/wdkl/app/ncs/application/Application.kt

@@ -5,6 +5,11 @@ import com.wdkl.ncs.android.lib.base.BaseApplication
 import com.enation.javashop.net.engine.config.NetEngineConfig
 import com.enation.javashop.net.engine.plugin.exception.RestfulExceptionInterceptor
 import com.enation.javashop.utils.base.config.BaseConfig
+import com.wdkl.core.socket.SocketManager
+import com.wdkl.core.voip.VoipEvent
+import com.wdkl.net.HttpRequestPresenter
+import com.wdkl.net.urlconn.UrlConnRequest
+import com.wdkl.skywebrtc.SkyEngineKit
 
 /**
  * @author LDD
@@ -78,5 +83,11 @@ class Application : BaseApplication() {
         NetEngineConfig.init(baseContext)
                 .openLogger()
                 .addNetInterceptor(RestfulExceptionInterceptor())
+
+        // 初始化网络请求
+        HttpRequestPresenter.init(UrlConnRequest())
+        // 初始化信令
+        SkyEngineKit.init(VoipEvent())
+        SocketManager.getInstance().init(applicationContext)
     }
 }

+ 1 - 1
build.gradle

@@ -153,7 +153,7 @@ buildscript {
     /**
      * SDK最小支持版本
      */
-    ext.min_sdk_version = 17
+    ext.min_sdk_version = 24
 
     /**
      * SDK目标支持版本

+ 4 - 0
nursehome/build.gradle

@@ -108,6 +108,10 @@ dependencies {
 
     compile project(':starRTC')
     compile project(':AmDemo_R')
+
+    //web rtc
+    compile project(':webrtc')
+    compile project(':rtc-chat')
 }
 
 /**

+ 24 - 1
nursehome/src/main/java/com/wdkl/ncs/android/component/nursehome/activity/NurseHomeActivity.kt

@@ -12,6 +12,7 @@ import android.provider.Settings
 import android.support.v4.app.Fragment
 import android.support.v4.app.FragmentManager
 import android.support.v4.app.FragmentTransaction
+import android.text.TextUtils
 import android.util.Log
 import android.view.View
 import com.enation.javashop.android.jrouter.external.annotation.Router
@@ -19,6 +20,9 @@ import com.enation.javashop.net.engine.model.NetState
 import com.starrtc.demo.demo.MLOC
 import com.starrtc.demo.demo.voip.VoipActivity
 import com.starrtc.demo.demo.voip.VoipAudioActivity
+import com.wdkl.core.consts.Urls
+import com.wdkl.core.socket.IUserState
+import com.wdkl.core.socket.SocketManager
 import com.wdkl.ncs.android.component.nursehome.R
 import com.wdkl.ncs.android.component.nursehome.SipUtil.SipCallBack
 import com.wdkl.ncs.android.component.nursehome.SipUtil.SipHelperUtil
@@ -32,6 +36,7 @@ import com.wdkl.ncs.android.component.nursehome.util.TimeTransition
 import com.wdkl.ncs.android.component.nursehome.util.VoiceManagerUtil
 import com.wdkl.ncs.android.lib.base.BaseActivity
 import com.wdkl.ncs.android.lib.utils.AppTool
+import com.wdkl.ncs.android.lib.utils.debugLog
 import com.wdkl.ncs.android.lib.utils.showMessage
 import com.wdkl.ncs.android.middleware.logic.contract.nursehome.NurseHomeActivityContract
 import com.wdkl.ncs.android.middleware.logic.presenter.nursehome.NurseHomeActivityPresenter
@@ -51,7 +56,8 @@ import org.greenrobot.eventbus.ThreadMode
  * 首页Activity
  */
 @Router(path = "/nursehome/main")
-class NurseHomeActivity  : BaseActivity<NurseHomeActivityPresenter, ActivityNurseHomeBinding>(), NurseHomeActivityContract.View, CallRecordsFragment.OnItemListener, View.OnClickListener, FramePartFragment.OnItemListener, SipCallBack {
+class NurseHomeActivity  : BaseActivity<NurseHomeActivityPresenter, ActivityNurseHomeBinding>(), NurseHomeActivityContract.View,
+        CallRecordsFragment.OnItemListener, View.OnClickListener, FramePartFragment.OnItemListener, SipCallBack, IUserState {
     var TAG = NurseHomeActivity::class.java.getSimpleName()
 
 
@@ -119,6 +125,13 @@ class NurseHomeActivity  : BaseActivity<NurseHomeActivityPresenter, ActivityNurs
             Log.e(TAG, Constants.hospital_name+ Constants.part_name)
         }
 
+        // 添加登录回调
+        SocketManager.getInstance().addUserStateCallback(this)
+        // 连接socket:登录
+        if (!TextUtils.isEmpty(Constants.sip_id)) {
+            SocketManager.getInstance().connect(Urls.WS, Constants.sip_id, 0)
+        }
+
         //设置设置配置
         setSettingConfiguration()
     }
@@ -144,6 +157,16 @@ class NurseHomeActivity  : BaseActivity<NurseHomeActivityPresenter, ActivityNurs
         SipHelperUtil.getInstance(this@NurseHomeActivity).obtainSipInfo()
     }
 
+    override fun userLogin() {
+        debugLog("NurseHome", "user login")
+        //updateStatus(SipStatus.REGISTERCOM)
+    }
+
+    override fun userLogout() {
+        debugLog("NurseHome", "user logout")
+        //updateStatus(SipStatus.REGISTERFAIL)
+    }
+
     /**
      * 设置设置配置
      */

+ 1 - 0
rtc-chat/.gitignore

@@ -0,0 +1 @@
+/build

+ 60 - 0
rtc-chat/bintray.gradle

@@ -0,0 +1,60 @@
+apply plugin: 'com.github.dcendents.android-maven'
+apply plugin: 'com.jfrog.bintray'
+
+Properties properties = new Properties()
+properties.load(project.rootProject.file('local.properties').newDataInputStream())
+
+group = GROUP
+version = VERSION_NAME
+
+bintray {
+    user = properties.getProperty("bintray.user")
+    key = properties.getProperty("bintray.apikey")
+    configurations = ['archives']
+    publish = true
+    dryRun = false
+    pkg {
+        repo = "maven"
+        name = rootProject.ext.android.SDK_BASENAME
+      //  userOrg = POM_USER_ORG
+        desc = POM_DESCRIPTION
+        websiteUrl = POM_URL
+        vcsUrl = POM_SCM_URL
+        issueTrackerUrl = POM_ISSUE_URL
+        licenses = [POM_LICENSE_NAME]
+        labels = ['videoChat', 'Android', 'webrtc']    //标签
+        publicDownloadNumbers = true
+    }
+
+}
+
+install {
+    repositories.mavenInstaller {
+        pom.project {
+            name rootProject.ext.android.SDK_BASENAME
+            packaging 'aar'
+            url POM_URL
+            description POM_DESCRIPTION
+            licenses {
+                license {
+                    name 'The Apache Software License, Version 2.0'
+                    url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+                }
+            }
+            scm {
+                url POM_SCM_URL
+                connection POM_SCM_CONNECTION
+                developerConnection POM_SCM_DEV_CONNECTION
+            }
+            developers {
+                developer {
+                    id POM_DEVELOPER_ID
+                    name POM_DEVELOPER_NAME
+                    email POM_DEVELOPER_EMAIL
+                }
+            }
+        }
+    }
+}
+
+

+ 41 - 0
rtc-chat/build.gradle

@@ -0,0 +1,41 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion target_sdk_version
+    buildToolsVersion build_tools_version
+
+    defaultConfig {
+        minSdkVersion min_sdk_version
+        targetSdkVersion target_sdk_version
+        versionCode app_version_code
+        versionName app_version
+
+        compileOptions {
+            sourceCompatibility JavaVersion.VERSION_1_8
+            targetCompatibility JavaVersion.VERSION_1_8
+        }
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+    // 官方库
+    api group: 'org.webrtc', name: 'google-webrtc', version: '1.0.32006'
+    // 自己编译的库
+//    api project(path: ':libwebrtc')
+}
+
+// upload
+//apply from: 'bintray.gradle'
+
+
+

+ 21 - 0
rtc-chat/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 2 - 0
rtc-chat/src/main/AndroidManifest.xml

@@ -0,0 +1,2 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.wdkl.skywebrtc" />

+ 196 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/AVEngine.java

@@ -0,0 +1,196 @@
+package com.wdkl.skywebrtc;
+
+import android.util.Log;
+import android.view.View;
+
+import com.wdkl.skywebrtc.engine.EngineCallback;
+import com.wdkl.skywebrtc.engine.IEngine;
+
+import java.util.List;
+
+public class AVEngine implements IEngine {
+    private static final String TAG = "AVEngine";
+    private final IEngine iEngine;
+    private static volatile AVEngine instance;
+
+    private AVEngine(IEngine engine) {
+        iEngine = engine;
+    }
+
+    public static AVEngine createEngine(IEngine engine) {
+        if (null == instance) {
+            synchronized (AVEngine.class) {
+                if (null == instance) {
+                    instance = new AVEngine(engine);
+                }
+            }
+        }
+
+        return instance;
+    }
+
+    @Override
+    public void init(EngineCallback callback) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.init(callback);
+    }
+
+    @Override
+    public void joinRoom(List<String> userIds) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.joinRoom(userIds);
+    }
+
+    @Override
+    public void userIn(String userId) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.userIn(userId);
+    }
+
+    @Override
+    public void userReject(String userId, int type) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.userReject(userId, type);
+    }
+
+    @Override
+    public void disconnected(String userId, EnumType.CallEndReason reason) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.disconnected(userId, reason);
+    }
+
+    @Override
+    public void receiveOffer(String userId, String description) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.receiveOffer(userId, description);
+    }
+
+    @Override
+    public void receiveAnswer(String userId, String sdp) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.receiveAnswer(userId, sdp);
+    }
+
+    @Override
+    public void receiveIceCandidate(String userId, String id, int label, String candidate) {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.receiveIceCandidate(userId, id, label, candidate);
+    }
+
+
+    @Override
+    public void leaveRoom(String userId) {
+        Log.d(TAG, "leaveRoom iEngine = " + iEngine);
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.leaveRoom(userId);
+    }
+
+    @Override
+    public View startPreview(boolean isO) {
+        if (iEngine == null) {
+            return null;
+        }
+        return iEngine.startPreview(isO);
+    }
+
+    @Override
+    public void stopPreview() {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.stopPreview();
+    }
+
+    @Override
+    public void startStream() {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.startStream();
+    }
+
+    @Override
+    public void stopStream() {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.stopStream();
+    }
+
+    @Override
+    public View setupRemoteVideo(String userId, boolean isO) {
+        if (iEngine == null) {
+            return null;
+        }
+        return iEngine.setupRemoteVideo(userId, isO);
+    }
+
+    @Override
+    public void stopRemoteVideo() {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.stopRemoteVideo();
+    }
+
+
+    @Override
+    public void switchCamera() {
+        if (iEngine == null) {
+            return;
+        }
+        iEngine.switchCamera();
+    }
+
+    @Override
+    public boolean muteAudio(boolean enable) {
+        if (iEngine == null) {
+            return false;
+        }
+        return iEngine.muteAudio(enable);
+    }
+
+    @Override
+    public boolean toggleSpeaker(boolean enable) {
+        if (iEngine == null) {
+            return false;
+        }
+        return iEngine.toggleSpeaker(enable);
+    }
+
+    @Override
+    public boolean toggleHeadset(boolean isHeadset) {
+        if (iEngine == null) {
+            return false;
+        }
+        return iEngine.toggleHeadset(isHeadset);
+    }
+
+    @Override
+    public void release() {
+        if (iEngine == null) {
+            return;
+        }
+         Log.d(TAG,"release");
+        iEngine.release();
+    }
+
+}

+ 549 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/CallSession.java

@@ -0,0 +1,549 @@
+package com.wdkl.skywebrtc;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import com.wdkl.skywebrtc.engine.EngineCallback;
+import com.wdkl.skywebrtc.engine.webrtc.WebRTCEngine;
+import com.wdkl.skywebrtc.inter.ISkyEvent;
+
+import org.webrtc.IceCandidate;
+import org.webrtc.SessionDescription;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 会话层
+ * Created by dds on 2019/8/19.
+ *
+ */
+public class CallSession implements EngineCallback {
+    private static final String TAG = "CallSession";
+    private WeakReference<CallSessionCallback> sessionCallback;
+    private ExecutorService executor;
+    private Handler handler = new Handler(Looper.getMainLooper());
+    // session参数
+    private boolean mIsAudioOnly;
+    // 房间人列表
+    private List<String> mUserIDList;
+    // 单聊对方Id/群聊邀请人
+    public String mTargetId;
+    // 房间Id
+    private String mRoomId;
+    // myId
+    public String mMyId;
+    // 房间大小
+    private int mRoomSize;
+
+    private boolean mIsComing;
+    private EnumType.CallState _callState = EnumType.CallState.Idle;
+    private long startTime;
+
+    private AVEngine iEngine;
+    private ISkyEvent mEvent;
+
+    public CallSession(Context context, String roomId, boolean audioOnly, ISkyEvent event) {
+        executor = Executors.newSingleThreadExecutor();
+        this.mIsAudioOnly = audioOnly;
+        this.mRoomId = roomId;
+
+        this.mEvent = event;
+        iEngine = AVEngine.createEngine(new WebRTCEngine(audioOnly, context));
+        iEngine.init(this);
+    }
+
+
+    // ----------------------------------------各种控制--------------------------------------------
+
+    // 创建房间
+    public void createHome(String room, int roomSize) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                mEvent.createRoom(room, roomSize);
+            }
+        });
+    }
+
+    // 加入房间
+    public void joinHome(String roomId) {
+        executor.execute(() -> {
+            _callState = EnumType.CallState.Connecting;
+             Log.d(TAG, "joinHome mEvent = " + mEvent);
+            setIsComing(true);
+            if (mEvent != null) {
+                mEvent.sendJoin(roomId);
+            }
+        });
+
+    }
+
+    //开始响铃
+    public void shouldStartRing() {
+        if (mEvent != null) {
+            mEvent.shouldStartRing(true);
+        }
+    }
+
+    // 关闭响铃
+    public void shouldStopRing() {
+        Log.d(TAG, "shouldStopRing mEvent != null is " + (mEvent != null));
+        if (mEvent != null) {
+            mEvent.shouldStopRing();
+        }
+    }
+
+    // 发送响铃回复
+    public void sendRingBack(String targetId, String room) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                mEvent.sendRingBack(targetId, room);
+            }
+        });
+    }
+
+    // 发送拒绝信令
+    public void sendRefuse() {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                // 取消拨出
+                mEvent.sendRefuse(mRoomId, mTargetId, EnumType.RefuseType.Hangup.ordinal());
+            }
+        });
+		release(EnumType.CallEndReason.Hangup);
+    }
+
+    // 发送忙时拒绝
+    void sendBusyRefuse(String room, String targetId) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                // 取消拨出
+                mEvent.sendRefuse(room, targetId, EnumType.RefuseType.Busy.ordinal());
+            }
+        });
+		release(EnumType.CallEndReason.Hangup);
+
+    }
+
+    // 发送取消信令
+    public void sendCancel() {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                // 取消拨出
+                List<String> list = new ArrayList<>();
+                list.add(mTargetId);
+                mEvent.sendCancel(mRoomId, list);
+            }
+        });
+		release(EnumType.CallEndReason.Hangup);
+
+    }
+
+    // 离开房间
+    public void leave() {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                mEvent.sendLeave(mRoomId, mMyId);
+            }
+        });
+        // 释放变量
+        release(EnumType.CallEndReason.Hangup);
+
+    }
+
+    // 切换到语音接听
+    public void sendTransAudio() {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                // 发送到对面,切换到语音
+                mEvent.sendTransAudio(mTargetId);
+            }
+        });
+    }
+
+    // 设置静音
+    public boolean toggleMuteAudio(boolean enable) {
+        return iEngine.muteAudio(enable);
+    }
+
+    // 设置扬声器
+    public boolean toggleSpeaker(boolean enable) {
+
+        return iEngine.toggleSpeaker(enable);
+    }
+
+    // 设置扬声器
+    public boolean toggleHeadset(boolean isHeadset) {
+        return iEngine.toggleHeadset(isHeadset);
+    }
+
+    // 切换到语音通话
+    public void switchToAudio() {
+        mIsAudioOnly = true;
+        // 告诉远端
+        sendTransAudio();
+        // 本地切换
+        if (sessionCallback != null && sessionCallback.get() != null) {
+            sessionCallback.get().didChangeMode(true);
+        }
+
+    }
+
+    // 调整摄像头前置后置
+    public void switchCamera() {
+        iEngine.switchCamera();
+    }
+
+    // 释放资源
+    private void release(EnumType.CallEndReason reason) {
+        executor.execute(() -> {
+            // 释放内容
+            iEngine.release();
+            // 状态设置为Idle
+            _callState = EnumType.CallState.Idle;
+
+            //界面回调
+            if (sessionCallback != null && sessionCallback.get() != null) {
+                sessionCallback.get().didCallEndWithReason(reason);
+            } else {
+				//TODO 结束会话
+			}
+        });
+    }
+
+    //------------------------------------receive---------------------------------------------------
+
+    // 加入房间成功
+    public void onJoinHome(String myId, String users, int roomSize) {
+        // 开始计时
+        mRoomSize = roomSize;
+        startTime = 0;
+        handler.post(() -> executor.execute(() -> {
+            mMyId = myId;
+            List<String> strings;
+            if (!TextUtils.isEmpty(users)) {
+                String[] split = users.split(",");
+                strings = Arrays.asList(split);
+                mUserIDList = strings;
+            }
+
+            // 发送邀请
+            if (!mIsComing) {
+                if (roomSize == 2) {
+                    List<String> inviteList = new ArrayList<>();
+                    inviteList.add(mTargetId);
+                    mEvent.sendInvite(mRoomId, inviteList, mIsAudioOnly);
+                }
+            } else {
+                iEngine.joinRoom(mUserIDList);
+            }
+
+            if (!isAudioOnly()) {
+                // 画面预览
+                if (sessionCallback != null && sessionCallback.get() != null) {
+                    sessionCallback.get().didCreateLocalVideoTrack();
+                }
+
+            }
+
+
+        }));
+
+
+    }
+
+    // 新成员进入
+    public void newPeer(String userId) {
+        handler.post(() -> executor.execute(() -> {
+            // 其他人加入房间
+            iEngine.userIn(userId);
+
+            // 关闭响铃
+            if (mEvent != null) {
+                mEvent.shouldStopRing();
+            }
+            // 更换界面
+            _callState = EnumType.CallState.Connected;
+
+            if (sessionCallback != null && sessionCallback.get() != null) {
+                startTime = System.currentTimeMillis();
+                sessionCallback.get().didChangeState(_callState);
+
+            }
+        }));
+
+    }
+
+    // 对方已拒绝
+    public void onRefuse(String userId, int type) {
+        iEngine.userReject(userId, type);
+    }
+
+    // 对方已响铃
+    public void onRingBack(String userId) {
+        if (mEvent != null) {
+            mEvent.onRemoteRing();
+            mEvent.shouldStartRing(false);
+        }
+    }
+
+    // 切换到语音
+    public void onTransAudio(String userId) {
+        mIsAudioOnly = true;
+        // 本地切换
+        if (sessionCallback != null && sessionCallback.get() != null) {
+            sessionCallback.get().didChangeMode(true);
+        }
+    }
+
+    // 对方网络断开
+    public void onDisConnect(String userId, EnumType.CallEndReason reason) {
+        executor.execute(() -> {
+            iEngine.disconnected(userId, reason);
+        });
+    }
+
+    // 对方取消拨出
+    public void onCancel(String userId) {
+        Log.d(TAG, "onCancel userId = " + userId);
+        shouldStopRing();
+        release(EnumType.CallEndReason.RemoteHangup);
+    }
+
+    public void onReceiveOffer(String userId, String description) {
+        executor.execute(() -> {
+            iEngine.receiveOffer(userId, description);
+        });
+
+    }
+
+    public void onReceiverAnswer(String userId, String sdp) {
+        executor.execute(() -> {
+            iEngine.receiveAnswer(userId, sdp);
+        });
+
+    }
+
+    public void onRemoteIceCandidate(String userId, String id, int label, String candidate) {
+        executor.execute(() -> {
+            iEngine.receiveIceCandidate(userId, id, label, candidate);
+        });
+
+    }
+
+    // 对方离开房间
+    public void onLeave(String userId) {
+        if (mRoomSize > 2) {
+            // 返回到界面上
+            if (sessionCallback != null && sessionCallback.get() != null) {
+                sessionCallback.get().didUserLeave(userId);
+            }
+        }
+        executor.execute(() -> iEngine.leaveRoom(userId));
+
+
+    }
+
+
+    // --------------------------------界面显示相关--------------------------------------------/
+
+    public long getStartTime() {
+        return startTime;
+    }
+
+    public View setupLocalVideo(boolean isOverlay) {
+        return iEngine.startPreview(isOverlay);
+    }
+
+
+    public View setupRemoteVideo(String userId, boolean isOverlay) {
+        return iEngine.setupRemoteVideo(userId, isOverlay);
+    }
+
+
+    //------------------------------------各种参数----------------------------------------------/
+
+    public void setIsAudioOnly(boolean _isAudioOnly) {
+        this.mIsAudioOnly = _isAudioOnly;
+    }
+
+    public boolean isAudioOnly() {
+        return mIsAudioOnly;
+    }
+
+    public void setTargetId(String targetIds) {
+        this.mTargetId = targetIds;
+    }
+
+    public void setIsComing(boolean isComing) {
+        this.mIsComing = isComing;
+    }
+
+    public boolean isComing() {
+        return mIsComing;
+    }
+
+    public void setRoom(String _room) {
+        this.mRoomId = _room;
+    }
+
+    public String getRoomId() {
+        return mRoomId;
+    }
+
+    public EnumType.CallState getState() {
+        return _callState;
+    }
+
+    public void setCallState(EnumType.CallState callState) {
+        this._callState = callState;
+    }
+
+    public void setSessionCallback(CallSessionCallback sessionCallback) {
+        this.sessionCallback = new WeakReference<>(sessionCallback);
+    }
+
+    //-----------------------------Engine回调-----------------------------------------
+
+    @Override
+    public void joinRoomSucc() {
+        // 关闭响铃
+        if (mEvent != null) {
+            mEvent.shouldStopRing();
+        }
+        // 更换界面
+        _callState = EnumType.CallState.Connected;
+        //Log.d(TAG, "joinRoomSucc, sessionCallback.get() = " + sessionCallback.get());
+        if (sessionCallback != null && sessionCallback.get() != null) {
+            startTime = System.currentTimeMillis();
+            sessionCallback.get().didChangeState(_callState);
+
+        }
+    }
+
+    @Override
+    public void exitRoom() {
+        // 状态设置为Idle
+        if (mRoomSize == 2) {
+            handler.post(() -> {
+                release(EnumType.CallEndReason.RemoteHangup);
+            });
+        }
+
+
+    }
+
+    @Override
+    public void reject(int type) {
+        shouldStopRing();
+        Log.d(TAG, "reject type = " + type);
+//        handler.post(() -> {
+        switch (type) {
+            case 0:
+                release(EnumType.CallEndReason.Busy);
+                break;
+            case 1:
+                release(EnumType.CallEndReason.RemoteHangup);
+                break;
+
+        }
+//        });
+    }
+
+    @Override
+    public void disconnected(EnumType.CallEndReason reason) {
+        handler.post(() -> {
+            shouldStopRing();
+            release(reason);
+        });
+    }
+
+    @Override
+    public void onSendIceCandidate(String userId, IceCandidate candidate) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                try {
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+                Log.d("dds_test", "onSendIceCandidate");
+                mEvent.sendIceCandidate(userId, candidate.sdpMid, candidate.sdpMLineIndex, candidate.sdp);
+            }
+        });
+
+    }
+
+    @Override
+    public void onSendOffer(String userId, SessionDescription description) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                Log.d("dds_test", "onSendOffer");
+                mEvent.sendOffer(userId, description.description);
+            }
+        });
+
+    }
+
+    @Override
+    public void onSendAnswer(String userId, SessionDescription description) {
+        executor.execute(() -> {
+            if (mEvent != null) {
+                Log.d("dds_test", "onSendAnswer");
+                mEvent.sendAnswer(userId, description.description);
+            }
+        });
+
+    }
+
+    @Override
+    public void onRemoteStream(String userId) {
+        // 画面预览
+        if (sessionCallback != null && sessionCallback.get() != null) {
+           Log.d(TAG, "onRemoteStream sessionCallback.get() != null ");
+            sessionCallback.get().didReceiveRemoteVideoTrack(userId);
+        } else {
+            Log.d(TAG, "onRemoteStream sessionCallback.get() == null ");
+        }
+    }
+
+    @Override
+    public void onDisconnected(String userId) {
+        //断线了,需要关闭通话界面
+        if (sessionCallback != null && sessionCallback.get() != null) {
+           Log.d(TAG, "onDisconnected sessionCallback.get() != null ");
+            sessionCallback.get().didDisconnected(userId);
+        } else {
+            Log.d(TAG, "onDisconnected sessionCallback.get() == null ");
+        }
+    }
+
+    public interface CallSessionCallback {
+        void didCallEndWithReason(EnumType.CallEndReason var1);
+
+        void didChangeState(EnumType.CallState var1);
+
+        void didChangeMode(boolean isAudioOnly);
+
+        void didCreateLocalVideoTrack();
+
+        void didReceiveRemoteVideoTrack(String userId);
+
+        void didUserLeave(String userId);
+
+        void didError(String error);
+
+        void didDisconnected(String userId);
+
+    }
+
+
+}

+ 41 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/EnumType.java

@@ -0,0 +1,41 @@
+package com.wdkl.skywebrtc;
+
+/**
+ * Created by dds on 2019/8/22.
+ * android_shuai@163.com
+ */
+public class EnumType {
+
+    public enum CallState {
+        Idle,
+        Outgoing,
+        Incoming,
+        Connecting,
+        Connected;
+
+        CallState() {
+        }
+    }
+
+    public enum CallEndReason {
+        Busy,
+        SignalError,
+        RemoteSignalError,
+        Hangup,
+        MediaError,
+        RemoteHangup,
+        OpenCameraFailure,
+        Timeout,
+        AcceptByOtherClient;
+
+        CallEndReason() {
+        }
+    }
+
+    public enum RefuseType {
+        Busy,
+        Hangup,
+    }
+
+
+}

+ 206 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/SkyEngineKit.java

@@ -0,0 +1,206 @@
+package com.wdkl.skywebrtc;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.wdkl.skywebrtc.except.NotInitializedException;
+import com.wdkl.skywebrtc.inter.ISkyEvent;
+
+/**
+ * 主控类
+ * Created by dds on 2019/8/19.
+ */
+public class SkyEngineKit {
+    private final static String TAG = "dds_AVEngineKit";
+    private static SkyEngineKit avEngineKit;
+    private CallSession mCurrentCallSession;
+    private ISkyEvent mEvent;
+    private boolean isAudioOnly = false;
+    private boolean isOutGoing = false;
+
+
+    public static SkyEngineKit Instance() {
+        SkyEngineKit var;
+        if ((var = avEngineKit) != null) {
+            return var;
+        } else {
+            throw new NotInitializedException();
+        }
+    }
+
+    // 初始化
+    public static void init(ISkyEvent iSocketEvent) {
+        if (avEngineKit == null) {
+            avEngineKit = new SkyEngineKit();
+            avEngineKit.mEvent = iSocketEvent;
+        }
+    }
+
+
+    public void sendRefuseOnPermissionDenied(String room, String inviteId) {
+        // 未初始化
+        if (avEngineKit == null) {
+            Log.e(TAG, "startOutCall error,please init first");
+            return;
+        }
+        if (mCurrentCallSession != null) {
+            endCall();
+        } else {
+            avEngineKit.mEvent.sendRefuse(room, inviteId, EnumType.RefuseType.Hangup.ordinal());
+        }
+    }
+
+    public void sendDisconnected(String room, String toId, boolean isCrashed) {
+        // 未初始化
+        if (avEngineKit == null) {
+            Log.e(TAG, "startOutCall error,please init first");
+            return;
+        }
+        avEngineKit.mEvent.sendDisConnect(room, toId, isCrashed);
+    }
+
+    // 拨打电话
+    public boolean startOutCall(Context context, final String room, final String targetId,
+                                final boolean audioOnly) {
+        // 未初始化
+        if (avEngineKit == null) {
+            Log.e(TAG, "startOutCall error,please init first");
+            return false;
+        }
+        // 忙线中
+        if (mCurrentCallSession != null && mCurrentCallSession.getState() != EnumType.CallState.Idle) {
+            Log.i(TAG, "startCall error,currentCallSession is exist");
+            return false;
+        }
+        isAudioOnly = audioOnly;
+        isOutGoing = true;
+        // 初始化会话
+        mCurrentCallSession = new CallSession(context, room, audioOnly, mEvent);
+        mCurrentCallSession.setTargetId(targetId);
+        mCurrentCallSession.setIsComing(false);
+        mCurrentCallSession.setCallState(EnumType.CallState.Outgoing);
+        // 创建房间
+        mCurrentCallSession.createHome(room, 2);
+
+
+        return true;
+    }
+
+    // 接听电话
+    public boolean startInCall(Context context, final String room, final String targetId,
+                               final boolean audioOnly) {
+        if (avEngineKit == null) {
+            Log.e(TAG, "startInCall error,init is not set");
+            return false;
+        }
+        // 忙线中
+        if (mCurrentCallSession != null && mCurrentCallSession.getState() != EnumType.CallState.Idle) {
+            // 发送->忙线中...
+            Log.i(TAG, "startInCall busy,currentCallSession is exist,start sendBusyRefuse!");
+            mCurrentCallSession.sendBusyRefuse(room, targetId);
+            return false;
+        }
+		isOutGoing = false;
+		this.isAudioOnly = audioOnly;
+        // 初始化会话
+        mCurrentCallSession = new CallSession(context, room, audioOnly, mEvent);
+        mCurrentCallSession.setTargetId(targetId);
+        mCurrentCallSession.setIsComing(true);
+        mCurrentCallSession.setCallState(EnumType.CallState.Incoming);
+
+        // 开始响铃并回复
+        mCurrentCallSession.shouldStartRing();
+        mCurrentCallSession.sendRingBack(targetId, room);
+
+
+        return true;
+    }
+
+    // 挂断会话
+    public void endCall() {
+        Log.d(TAG, "endCall mCurrentCallSession != null is " + (mCurrentCallSession != null));
+        if (mCurrentCallSession != null) {
+            // 停止响铃
+            mCurrentCallSession.shouldStopRing();
+
+            if (mCurrentCallSession.isComing()) {
+                if (mCurrentCallSession.getState() == EnumType.CallState.Incoming) {
+                    // 接收到邀请,还没同意,发送拒绝
+                    mCurrentCallSession.sendRefuse();
+                } else {
+                    // 已经接通,挂断电话
+                    mCurrentCallSession.leave();
+                }
+            } else {
+                if (mCurrentCallSession.getState() == EnumType.CallState.Outgoing) {
+                    mCurrentCallSession.sendCancel();
+                } else {
+                    // 已经接通,挂断电话
+                    mCurrentCallSession.leave();
+                }
+            }
+            mCurrentCallSession.setCallState(EnumType.CallState.Idle);
+        }
+
+    }
+
+    // 加入房间
+    public void joinRoom(Context context, String room) {
+        if (avEngineKit == null) {
+            Log.e(TAG, "joinRoom error,init is not set");
+            return;
+        }
+        // 忙线中
+        if (mCurrentCallSession != null && mCurrentCallSession.getState() != EnumType.CallState.Idle) {
+            Log.e(TAG, "joinRoom error,currentCallSession is exist");
+            return;
+        }
+        mCurrentCallSession = new CallSession(context, room, false, mEvent);
+        mCurrentCallSession.setIsComing(true);
+        mCurrentCallSession.joinHome(room);
+    }
+
+    public void createAndJoinRoom(Context context, String room) {
+        if (avEngineKit == null) {
+            Log.e(TAG, "joinRoom error,init is not set");
+            return;
+        }
+        // 忙线中
+        if (mCurrentCallSession != null && mCurrentCallSession.getState() != EnumType.CallState.Idle) {
+            Log.e(TAG, "joinRoom error,currentCallSession is exist");
+            return;
+        }
+        mCurrentCallSession = new CallSession(context, room, false, mEvent);
+        mCurrentCallSession.setIsComing(false);
+        mCurrentCallSession.createHome(room, 9);
+    }
+
+    // 离开房间
+    public void leaveRoom() {
+        if (avEngineKit == null) {
+            Log.e(TAG, "leaveRoom error,init is not set");
+            return;
+        }
+        if (mCurrentCallSession != null) {
+            mCurrentCallSession.leave();
+            mCurrentCallSession.setCallState(EnumType.CallState.Idle);
+        }
+    }
+
+    public void transferToAudio() {
+        isAudioOnly = true;
+    }
+    public boolean isOutGoing() {
+        return isOutGoing;
+    }
+
+    public boolean isAudioOnly() {
+        return isAudioOnly;
+    }
+    // 获取对话实例
+    public CallSession getCurrentSession() {
+        return this.mCurrentCallSession;
+    }
+
+
+}

+ 43 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/engine/EngineCallback.java

@@ -0,0 +1,43 @@
+package com.wdkl.skywebrtc.engine;
+
+import com.wdkl.skywebrtc.EnumType;
+
+import org.webrtc.IceCandidate;
+import org.webrtc.SessionDescription;
+
+/**
+ * Created by dds on 2020/4/12.
+ * 框架回调
+ */
+public interface EngineCallback {
+
+
+    /**
+     * 加入房间成功
+     */
+    void joinRoomSucc();
+
+    /**
+     * 退出房间成功
+     */
+    void exitRoom();
+
+    /**
+     * 拒绝连接
+     * @param type type
+     */
+    void reject(int type);
+
+    void disconnected(EnumType.CallEndReason reason);
+
+    void onSendIceCandidate(String userId, IceCandidate candidate);
+
+    void onSendOffer(String userId, SessionDescription description);
+
+    void onSendAnswer(String userId, SessionDescription description);
+
+    void onRemoteStream(String userId);
+
+    void onDisconnected(String userId);
+
+}

+ 121 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/engine/IEngine.java

@@ -0,0 +1,121 @@
+package com.wdkl.skywebrtc.engine;
+
+
+import android.view.View;
+
+import com.wdkl.skywebrtc.EnumType;
+
+import java.util.List;
+
+/**
+ * rtc基类
+ */
+public interface IEngine {
+
+    /**
+     * 初始化
+     */
+    void init(EngineCallback callback);
+
+    /**
+     * 加入房間
+     */
+    void joinRoom(List<String> userIds);
+
+    /**
+     * 有人进入房间
+     */
+    void userIn(String userId);
+
+    /**
+     * 用户拒绝
+     * @param userId userId
+     * @param type type
+     */
+    void userReject(String userId,int type);
+
+    /**
+     * 用户网络断开
+     * @param userId userId
+     * @param reason
+     */
+    void disconnected(String userId, EnumType.CallEndReason reason);
+
+    /**
+     * receive Offer
+     */
+    void receiveOffer(String userId, String description);
+
+    /**
+     * receive Answer
+     */
+    void receiveAnswer(String userId, String sdp);
+
+    /**
+     * receive IceCandidate
+     */
+    void receiveIceCandidate(String userId, String id, int label, String candidate);
+
+    /**
+     * 离开房间
+     *
+     * @param userId userId
+     */
+    void leaveRoom(String userId);
+
+    /**
+     * 开启本地预览
+     */
+    View startPreview(boolean isOverlay);
+
+    /**
+     * 关闭本地预览
+     */
+    void stopPreview();
+
+    /**
+     * 开始远端推流
+     */
+    void startStream();
+
+    /**
+     * 停止远端推流
+     */
+    void stopStream();
+
+    /**
+     * 开始远端预览
+     */
+    View setupRemoteVideo(String userId, boolean isO);
+
+    /**
+     * 关闭远端预览
+     */
+    void stopRemoteVideo();
+
+    /**
+     * 切换摄像头
+     */
+    void switchCamera();
+
+    /**
+     * 设置静音
+     */
+    boolean muteAudio(boolean enable);
+
+    /**
+     * 开启扬声器
+     */
+    boolean toggleSpeaker(boolean enable);
+
+    /**
+     * 切换外放和耳机
+     */
+    boolean toggleHeadset(boolean isHeadset);
+
+    /**
+     * 释放所有内容
+     */
+    void release();
+
+}

+ 355 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/engine/webrtc/Peer.java

@@ -0,0 +1,355 @@
+package com.wdkl.skywebrtc.engine.webrtc;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.wdkl.skywebrtc.render.ProxyVideoSink;
+
+import org.webrtc.DataChannel;
+import org.webrtc.EglBase;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.RendererCommon;
+import org.webrtc.RtpReceiver;
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+import org.webrtc.SurfaceViewRenderer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by dds on 2020/3/11.
+ * android_shuai@163.com
+ */
+public class Peer implements SdpObserver, PeerConnection.Observer {
+    private final static String TAG = "dds_Peer";
+    private final PeerConnection pc;
+    private final String mUserId;
+    private List<IceCandidate> queuedRemoteCandidates;
+    private SessionDescription localSdp;
+    private final PeerConnectionFactory mFactory;
+    private final List<PeerConnection.IceServer> mIceLis;
+    private final IPeerEvent mEvent;
+    private boolean isOffer;
+
+    public MediaStream _remoteStream;
+    public SurfaceViewRenderer renderer;
+    public ProxyVideoSink sink;
+
+
+    public Peer(PeerConnectionFactory factory, List<PeerConnection.IceServer> list, String userId, IPeerEvent event) {
+        mFactory = factory;
+        mIceLis = list;
+        mEvent = event;
+        mUserId = userId;
+        queuedRemoteCandidates = new ArrayList<>();
+        this.pc = createPeerConnection();
+        Log.d("dds_test", "create Peer:" + mUserId);
+
+    }
+
+    public PeerConnection createPeerConnection() {
+        PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(mIceLis);
+        if (mFactory != null) {
+            return mFactory.createPeerConnection(rtcConfig, this);
+        } else {
+            return null;
+        }
+    }
+
+    public void setOffer(boolean isOffer) {
+        this.isOffer = isOffer;
+    }
+
+    // 创建offer
+    public void createOffer() {
+        if (pc == null) return;
+        Log.d("dds_test", "createOffer");
+        pc.createOffer(this, offerOrAnswerConstraint());
+    }
+
+    // 创建answer
+    public void createAnswer() {
+        if (pc == null) return;
+        Log.d("dds_test", "createAnswer");
+        pc.createAnswer(this, offerOrAnswerConstraint());
+
+    }
+
+    // 设置LocalDescription
+    public void setLocalDescription(SessionDescription sdp) {
+        Log.d("dds_test", "setLocalDescription");
+        if (pc == null) return;
+        pc.setLocalDescription(this, sdp);
+    }
+
+    // 设置RemoteDescription
+    public void setRemoteDescription(SessionDescription sdp) {
+        if (pc == null) return;
+        Log.d("dds_test", "setRemoteDescription");
+        pc.setRemoteDescription(this, sdp);
+    }
+
+    //添加本地流
+    public void addLocalStream(MediaStream stream) {
+        if (pc == null) return;
+        Log.d("dds_test", "addLocalStream" + mUserId);
+        pc.addStream(stream);
+    }
+
+    // 添加RemoteIceCandidate
+    public synchronized void addRemoteIceCandidate(final IceCandidate candidate) {
+        Log.d("dds_test", "addRemoteIceCandidate");
+        if (pc != null) {
+            if (queuedRemoteCandidates != null) {
+               Log.d("dds_test", "addRemoteIceCandidate  2222");
+                synchronized (Peer.class) {
+                    if (queuedRemoteCandidates != null) {
+                        queuedRemoteCandidates.add(candidate);
+                    }
+                }
+
+            } else {
+               Log.d("dds_test", "addRemoteIceCandidate1111");
+                pc.addIceCandidate(candidate);
+            }
+        }
+    }
+
+    // 移除RemoteIceCandidates
+    public void removeRemoteIceCandidates(final IceCandidate[] candidates) {
+        if (pc == null) {
+            return;
+        }
+        drainCandidates();
+        pc.removeIceCandidates(candidates);
+    }
+
+    public void createRender(EglBase mRootEglBase, Context context, boolean isOverlay) {
+        renderer = new SurfaceViewRenderer(context);
+        renderer.init(mRootEglBase.getEglBaseContext(), new RendererCommon.RendererEvents() {
+            @Override
+            public void onFirstFrameRendered() {
+                Log.d(TAG, "createRender onFirstFrameRendered");
+
+            }
+
+            @Override
+            public void onFrameResolutionChanged(int videoWidth, int videoHeight, int rotation) {
+                Log.d(TAG, "createRender onFrameResolutionChanged");
+            }
+        });
+        renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
+        renderer.setMirror(true);
+        renderer.setZOrderMediaOverlay(isOverlay);
+        sink = new ProxyVideoSink();
+        sink.setTarget(renderer);
+        if (_remoteStream != null && _remoteStream.videoTracks.size() > 0) {
+            _remoteStream.videoTracks.get(0).addSink(sink);
+        }
+
+    }
+
+    // 关闭Peer
+    public void close() {
+        if (renderer != null) {
+            renderer.release();
+            renderer = null;
+        }
+        if (sink != null) {
+            sink.setTarget(null);
+        }
+        if (pc != null) {
+            try {
+                pc.close();
+                pc.dispose();
+            } catch (Exception e) {
+
+            }
+
+
+        }
+
+
+    }
+
+    //------------------------------Observer-------------------------------------
+    @Override
+    public void onSignalingChange(PeerConnection.SignalingState signalingState) {
+        Log.i(TAG, "onSignalingChange: " + signalingState);
+    }
+
+    @Override
+    public void onIceConnectionChange(PeerConnection.IceConnectionState newState) {
+        Log.i(TAG, "onIceConnectionChange: " + newState);
+        if (newState == PeerConnection.IceConnectionState.DISCONNECTED || newState == PeerConnection.IceConnectionState.FAILED) {
+            mEvent.onDisconnected(mUserId);
+        }
+
+    }
+
+    @Override
+    public void onIceConnectionReceivingChange(boolean receiving) {
+        Log.i(TAG, "onIceConnectionReceivingChange:" + receiving);
+    }
+
+    @Override
+    public void onIceGatheringChange(PeerConnection.IceGatheringState newState) {
+        Log.i(TAG, "onIceGatheringChange:" + newState.toString());
+    }
+
+    @Override
+    public void onIceCandidate(IceCandidate candidate) {
+        // 发送IceCandidate
+        mEvent.onSendIceCandidate(mUserId, candidate);
+    }
+
+    @Override
+    public void onIceCandidatesRemoved(IceCandidate[] candidates) {
+        Log.i(TAG, "onIceCandidatesRemoved:");
+    }
+
+    @Override
+    public void onAddStream(MediaStream stream) {
+        Log.i(TAG, "onAddStream:");
+        stream.audioTracks.get(0).setEnabled(true);
+        _remoteStream = stream;
+        if (mEvent != null) {
+            mEvent.onRemoteStream(mUserId, stream);
+        }
+    }
+
+    @Override
+    public void onRemoveStream(MediaStream stream) {
+        Log.i(TAG, "onRemoveStream:");
+        if (mEvent != null) {
+            mEvent.onRemoveStream(mUserId, stream);
+        }
+    }
+
+    @Override
+    public void onDataChannel(DataChannel dataChannel) {
+        Log.i(TAG, "onDataChannel:");
+    }
+
+    @Override
+    public void onRenegotiationNeeded() {
+        Log.i(TAG, "onRenegotiationNeeded:");
+    }
+
+    @Override
+    public void onAddTrack(RtpReceiver receiver, MediaStream[] mediaStreams) {
+        Log.i(TAG, "onAddTrack:" + mediaStreams.length);
+    }
+
+
+    //-------------SdpObserver--------------------
+    @Override
+    public void onCreateSuccess(SessionDescription origSdp) {
+        Log.d(TAG, "sdp创建成功       " + origSdp.type);
+        String sdpString = origSdp.description;
+        final SessionDescription sdp = new SessionDescription(origSdp.type, sdpString);
+        localSdp = sdp;
+        setLocalDescription(sdp);
+
+    }
+
+    @Override
+    public void onSetSuccess() {
+        Log.d(TAG, "sdp连接成功   " + pc.signalingState().toString());
+        if (pc == null) return;
+        // 发送者
+        if (isOffer) {
+            if (pc.getRemoteDescription() == null) {
+                Log.d(TAG, "Local SDP set succesfully");
+                if (!isOffer) {
+                    //接收者,发送Answer
+                    mEvent.onSendAnswer(mUserId, localSdp);
+                } else {
+                    //发送者,发送自己的offer
+                    mEvent.onSendOffer(mUserId, localSdp);
+                }
+            } else {
+                Log.d(TAG, "Remote SDP set succesfully");
+
+                drainCandidates();
+            }
+
+        } else {
+            if (pc.getLocalDescription() != null) {
+                Log.d(TAG, "Local SDP set succesfully");
+                if (!isOffer) {
+                    //接收者,发送Answer
+                    mEvent.onSendAnswer(mUserId, localSdp);
+                } else {
+                    //发送者,发送自己的offer
+                    mEvent.onSendOffer(mUserId, localSdp);
+                }
+
+                drainCandidates();
+            } else {
+                Log.d(TAG, "Remote SDP set succesfully");
+            }
+        }
+
+
+    }
+
+    @Override
+    public void onCreateFailure(String error) {
+        Log.i(TAG, " SdpObserver onCreateFailure:" + error);
+    }
+
+    @Override
+    public void onSetFailure(String error) {
+        Log.i(TAG, "SdpObserver onSetFailure:" + error);
+    }
+
+
+    private void drainCandidates() {
+        Log.i("dds_test", "drainCandidates");
+        synchronized (Peer.class) {
+            if (queuedRemoteCandidates != null) {
+                Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates");
+                for (IceCandidate candidate : queuedRemoteCandidates) {
+                    pc.addIceCandidate(candidate);
+                }
+                queuedRemoteCandidates = null;
+            }
+
+        }
+    }
+
+    private MediaConstraints offerOrAnswerConstraint() {
+        MediaConstraints mediaConstraints = new MediaConstraints();
+        ArrayList<MediaConstraints.KeyValuePair> keyValuePairs = new ArrayList<>();
+        keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
+        keyValuePairs.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
+        mediaConstraints.mandatory.addAll(keyValuePairs);
+        return mediaConstraints;
+    }
+
+    // ----------------------------回调-----------------------------------
+
+    public interface IPeerEvent {
+
+
+        void onSendIceCandidate(String userId, IceCandidate candidate);
+
+        void onSendOffer(String userId, SessionDescription description);
+
+        void onSendAnswer(String userId, SessionDescription description);
+
+        void onRemoteStream(String userId, MediaStream stream);
+
+        void onRemoveStream(String userId, MediaStream stream);
+
+
+        void onDisconnected(String userId);
+    }
+
+}

+ 693 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/engine/webrtc/WebRTCEngine.java

@@ -0,0 +1,693 @@
+package com.wdkl.skywebrtc.engine.webrtc;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.projection.MediaProjection;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.engine.EngineCallback;
+import com.wdkl.skywebrtc.engine.IEngine;
+import com.wdkl.skywebrtc.render.ProxyVideoSink;
+
+import org.webrtc.AudioSource;
+import org.webrtc.AudioTrack;
+import org.webrtc.Camera1Enumerator;
+import org.webrtc.Camera2Enumerator;
+import org.webrtc.CameraEnumerator;
+import org.webrtc.CameraVideoCapturer;
+import org.webrtc.DefaultVideoDecoderFactory;
+import org.webrtc.DefaultVideoEncoderFactory;
+import org.webrtc.EglBase;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.RendererCommon;
+import org.webrtc.ScreenCapturerAndroid;
+import org.webrtc.SessionDescription;
+import org.webrtc.SurfaceTextureHelper;
+import org.webrtc.SurfaceViewRenderer;
+import org.webrtc.VideoCapturer;
+import org.webrtc.VideoDecoderFactory;
+import org.webrtc.VideoEncoderFactory;
+import org.webrtc.VideoSource;
+import org.webrtc.VideoTrack;
+import org.webrtc.audio.AudioDeviceModule;
+import org.webrtc.audio.JavaAudioDeviceModule;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class WebRTCEngine implements IEngine, Peer.IPeerEvent {
+    private static final String TAG = "WebRTCEngine";
+    private PeerConnectionFactory _factory;
+    private EglBase mRootEglBase;
+    private MediaStream _localStream;
+    private VideoSource videoSource;
+    private AudioSource audioSource;
+    private AudioTrack _localAudioTrack;
+    private VideoCapturer captureAndroid;
+    private SurfaceTextureHelper surfaceTextureHelper;
+
+    private ProxyVideoSink localSink;
+    private SurfaceViewRenderer localRenderer;
+
+
+    private static final String VIDEO_TRACK_ID = "ARDAMSv0";
+    private static final String AUDIO_TRACK_ID = "ARDAMSa0";
+    public static final String VIDEO_CODEC_H264 = "H264";
+    private static final int VIDEO_RESOLUTION_WIDTH = 640;
+    private static final int VIDEO_RESOLUTION_HEIGHT = 480;
+    private static final int FPS = 20;
+
+    // 对话实例列表
+    private ConcurrentHashMap<String, Peer> peers = new ConcurrentHashMap<>();
+    // 服务器实例列表
+    private List<PeerConnection.IceServer> iceServers = new ArrayList<>();
+
+    private EngineCallback mCallback;
+
+    public boolean mIsAudioOnly;
+    private Context mContext;
+    private AudioManager audioManager;
+    private boolean isSpeakerOn = true;
+
+    public WebRTCEngine(boolean mIsAudioOnly, Context mContext) {
+        this.mIsAudioOnly = mIsAudioOnly;
+        this.mContext = mContext;
+        audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        // 初始化ice地址
+        initIceServer();
+    }
+
+
+    // -----------------------------------对外方法------------------------------------------
+    @Override
+    public void init(EngineCallback callback) {
+        mCallback = callback;
+
+        if (mRootEglBase == null) {
+            mRootEglBase = EglBase.create();
+        }
+        if (_factory == null) {
+            _factory = createConnectionFactory();
+        }
+        if (_localStream == null) {
+            createLocalStream();
+        }
+    }
+
+    @Override
+    public void joinRoom(List<String> userIds) {
+        for (String id : userIds) {
+            // create Peer
+
+            Peer peer = new Peer(_factory, iceServers, id, this);
+            peer.setOffer(false);
+            // add localStream
+            peer.addLocalStream(_localStream);
+            // 添加列表
+            peers.put(id, peer);
+        }
+        if (mCallback != null) {
+            mCallback.joinRoomSucc();
+        }
+
+        if (isHeadphonesPlugged()) {
+            toggleHeadset(true);
+        } else {
+            if (mIsAudioOnly)
+                toggleSpeaker(false);
+            else {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+                } else {
+                    audioManager.setMode(AudioManager.MODE_IN_CALL);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void userIn(String userId) {
+        // create Peer
+        Peer peer = new Peer(_factory, iceServers, userId, this);
+        peer.setOffer(true);
+        // add localStream
+        peer.addLocalStream(_localStream);
+        // 添加列表
+        peers.put(userId, peer);
+        // createOffer
+        peer.createOffer();
+    }
+
+    @Override
+    public void userReject(String userId, int type) {
+        //拒绝接听userId应该是没有添加进peers里去不需要remove
+//       Peer peer = peers.get(userId);
+//        if (peer != null) {
+//            peer.close();
+//            peers.remove(userId);
+//        }
+//        if (peers.size() == 0) {
+
+        if (mCallback != null) {
+            mCallback.reject(type);
+        }
+//        }
+    }
+
+    @Override
+    public void disconnected(String userId, EnumType.CallEndReason reason) {
+        if (mCallback != null) {
+            mCallback.disconnected(reason);
+        }
+    }
+
+    @Override
+    public void receiveOffer(String userId, String description) {
+        Peer peer = peers.get(userId);
+        if (peer != null) {
+            SessionDescription sdp = new SessionDescription(SessionDescription.Type.OFFER, description);
+            peer.setOffer(false);
+            peer.setRemoteDescription(sdp);
+            peer.createAnswer();
+        }
+
+
+    }
+
+    @Override
+    public void receiveAnswer(String userId, String sdp) {
+        Log.d("dds_test", "receiveAnswer--" + userId);
+        Peer peer = peers.get(userId);
+        if (peer != null) {
+            SessionDescription sessionDescription = new SessionDescription(SessionDescription.Type.ANSWER, sdp);
+            peer.setRemoteDescription(sessionDescription);
+        }
+
+
+    }
+
+    @Override
+    public void receiveIceCandidate(String userId, String id, int label, String candidate) {
+        Log.d("dds_test", "receiveIceCandidate--" + userId);
+        Peer peer = peers.get(userId);
+        if (peer != null) {
+            IceCandidate iceCandidate = new IceCandidate(id, label, candidate);
+            peer.addRemoteIceCandidate(iceCandidate);
+
+        }
+    }
+
+    @Override
+    public void leaveRoom(String userId) {
+        Peer peer = peers.get(userId);
+        if (peer != null) {
+            peer.close();
+            peers.remove(userId);
+        }
+       Log.d(TAG, "leaveRoom peers.size() = " + peers.size() + "; mCallback = " + mCallback);
+        if (peers.size() <= 1) {
+
+            if (mCallback != null) {
+                mCallback.exitRoom();
+            }
+            if (peers.size() == 1) {
+                for (Map.Entry<String, Peer> set : peers.entrySet()) {
+                    set.getValue().close();
+                }
+                peers.clear();
+            }
+        }
+
+
+    }
+
+    @Override
+    public View startPreview(boolean isOverlay) {
+        if (mRootEglBase == null) {
+            return null;
+        }
+        localRenderer = new SurfaceViewRenderer(mContext);
+        localRenderer.init(mRootEglBase.getEglBaseContext(), null);
+        localRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
+        localRenderer.setMirror(true);
+        localRenderer.setZOrderMediaOverlay(isOverlay);
+
+        localSink = new ProxyVideoSink();
+        localSink.setTarget(localRenderer);
+        if (_localStream.videoTracks.size() > 0) {
+            _localStream.videoTracks.get(0).addSink(localSink);
+        }
+        return localRenderer;
+    }
+
+    @Override
+    public void stopPreview() {
+        if (localSink != null) {
+            localSink.setTarget(null);
+            localSink = null;
+        }
+        if (audioSource != null) {
+            audioSource.dispose();
+            audioSource = null;
+        }
+        // 释放摄像头
+        if (captureAndroid != null) {
+            try {
+                captureAndroid.stopCapture();
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            captureAndroid.dispose();
+            captureAndroid = null;
+        }
+        // 释放画布
+        if (surfaceTextureHelper != null) {
+            surfaceTextureHelper.dispose();
+            surfaceTextureHelper = null;
+        }
+
+        if (videoSource != null) {
+            videoSource.dispose();
+            videoSource = null;
+        }
+        if (_localStream != null) {
+            _localStream = null;
+        }
+        if (localRenderer != null) {
+            localRenderer.release();
+        }
+
+
+    }
+
+    @Override
+    public void startStream() {
+
+    }
+
+    @Override
+    public void stopStream() {
+
+    }
+
+
+    @Override
+    public View setupRemoteVideo(String userId, boolean isO) {
+        if (TextUtils.isEmpty(userId)) {
+            Log.e(TAG, "setupRemoteVideo userId is null ");
+            return null;
+        }
+        Peer peer = peers.get(userId);
+        if (peer == null) return null;
+
+        if (peer.renderer == null) {
+            peer.createRender(mRootEglBase, mContext, isO);
+        }
+
+        return peer.renderer;
+
+    }
+
+    @Override
+    public void stopRemoteVideo() {
+
+    }
+
+    private boolean isSwitch = false; // 是否正在切换摄像头
+
+    @Override
+    public void switchCamera() {
+        if (isSwitch) return;
+        isSwitch = true;
+        if (captureAndroid == null) return;
+        if (captureAndroid instanceof CameraVideoCapturer) {
+            CameraVideoCapturer cameraVideoCapturer = (CameraVideoCapturer) captureAndroid;
+            try {
+                cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() {
+                    @Override
+                    public void onCameraSwitchDone(boolean isFrontCamera) {
+                        isSwitch = false;
+                    }
+
+                    @Override
+                    public void onCameraSwitchError(String errorDescription) {
+                        isSwitch = false;
+                    }
+                });
+            } catch (Exception e) {
+                isSwitch = false;
+            }
+        } else {
+            Log.d(TAG, "Will not switch camera, video caputurer is not a camera");
+        }
+    }
+
+    @Override
+    public boolean muteAudio(boolean enable) {
+        if (_localAudioTrack != null) {
+            _localAudioTrack.setEnabled(!enable);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean toggleSpeaker(boolean enable) {
+        if (audioManager != null) {
+            isSpeakerOn = enable;
+            audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+            if (enable) {
+                audioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+                        audioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL),
+                        AudioManager.FX_KEY_CLICK);
+                audioManager.setSpeakerphoneOn(true);
+            } else {
+                //5.0以上
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                    //设置mode
+                    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+                } else {
+                    //设置mode
+                    audioManager.setMode(AudioManager.MODE_IN_CALL);
+                }
+                //设置音量,解决有些机型切换后没声音或者声音突然变大的问题
+                audioManager.setStreamVolume(
+                        AudioManager.STREAM_VOICE_CALL,
+                        audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL),
+                        AudioManager.FX_KEY_CLICK
+                );
+                audioManager.setSpeakerphoneOn(false);
+            }
+            return true;
+        }
+        return false;
+
+    }
+
+    @Override
+    public boolean toggleHeadset(boolean isHeadset) {
+        if (audioManager != null) {
+            if (isHeadset) {
+                //5.0以上
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                    //设置mode
+                    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+                } else {
+                    //设置mode
+                    audioManager.setMode(AudioManager.MODE_IN_CALL);
+                }
+                audioManager.setSpeakerphoneOn(false);
+            } else {
+                if (mIsAudioOnly) {
+                    toggleSpeaker(isSpeakerOn);
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isHeadphonesPlugged() {
+        if (audioManager == null) {
+            return false;
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            AudioDeviceInfo[] audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
+            for (AudioDeviceInfo deviceInfo : audioDevices) {
+                if (deviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
+                        || deviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
+                    return true;
+                }
+            }
+            return false;
+        } else {
+            return audioManager.isWiredHeadsetOn();
+        }
+    }
+
+    @Override
+    public void release() {
+        if (audioManager != null) {
+            audioManager.setMode(AudioManager.MODE_NORMAL);
+        }
+        // 清空peer
+        if (peers != null) {
+            for (Peer peer : peers.values()) {
+                peer.close();
+            }
+            peers.clear();
+        }
+
+
+        // 停止预览
+        stopPreview();
+
+        if (_factory != null) {
+            _factory.dispose();
+            _factory = null;
+        }
+
+        if (mRootEglBase != null) {
+            mRootEglBase.release();
+            mRootEglBase = null;
+        }
+
+
+    }
+
+    // -----------------------------其他方法--------------------------------
+
+    private void initIceServer() {
+        // 初始化一些stun和turn的地址
+        PeerConnection.IceServer var1 = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302")
+                .createIceServer();
+        iceServers.add(var1);
+
+        PeerConnection.IceServer var11 = PeerConnection.IceServer.builder("stun:42.192.40.58:3478?transport=udp")
+                .createIceServer();
+        PeerConnection.IceServer var12 = PeerConnection.IceServer.builder("turn:42.192.40.58:3478?transport=udp")
+                .setUsername("ddssingsong")
+                .setPassword("123456")
+                .createIceServer();
+        PeerConnection.IceServer var13 = PeerConnection.IceServer.builder("turn:42.192.40.58:3478?transport=tcp")
+                .setUsername("ddssingsong")
+                .setPassword("123456")
+                .createIceServer();
+        iceServers.add(var11);
+        iceServers.add(var12);
+        iceServers.add(var13);
+    }
+
+    /**
+     * 构造PeerConnectionFactory
+     *
+     * @return PeerConnectionFactory
+     */
+    public PeerConnectionFactory createConnectionFactory() {
+
+        // 1. 初始化的方法,必须在开始之前调用
+        PeerConnectionFactory.initialize(PeerConnectionFactory
+                .InitializationOptions
+                .builder(mContext)
+                .createInitializationOptions());
+
+        // 2. 设置编解码方式:默认方法
+        final VideoEncoderFactory encoderFactory;
+        final VideoDecoderFactory decoderFactory;
+
+        encoderFactory = new DefaultVideoEncoderFactory(
+                mRootEglBase.getEglBaseContext(),
+                true,
+                true);
+        decoderFactory = new DefaultVideoDecoderFactory(mRootEglBase.getEglBaseContext());
+
+        // 构造Factory
+        AudioDeviceModule audioDeviceModule = JavaAudioDeviceModule.builder(mContext).createAudioDeviceModule();
+        PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
+        return PeerConnectionFactory.builder()
+                .setOptions(options)
+                .setAudioDeviceModule(audioDeviceModule)
+                .setVideoEncoderFactory(encoderFactory)
+                .setVideoDecoderFactory(decoderFactory)
+                .createPeerConnectionFactory();
+    }
+
+    /**
+     * 创建本地流
+     */
+    public void createLocalStream() {
+        _localStream = _factory.createLocalMediaStream("ARDAMS");
+        // 音频
+        audioSource = _factory.createAudioSource(createAudioConstraints());
+        _localAudioTrack = _factory.createAudioTrack(AUDIO_TRACK_ID, audioSource);
+        _localStream.addTrack(_localAudioTrack);
+
+        // 视频
+        if (!mIsAudioOnly) {
+            captureAndroid = createVideoCapture();
+            surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", mRootEglBase.getEglBaseContext());
+            videoSource = _factory.createVideoSource(captureAndroid.isScreencast());
+
+            captureAndroid.initialize(surfaceTextureHelper, mContext, videoSource.getCapturerObserver());
+            captureAndroid.startCapture(VIDEO_RESOLUTION_WIDTH, VIDEO_RESOLUTION_HEIGHT, FPS);
+
+
+            VideoTrack _localVideoTrack = _factory.createVideoTrack(VIDEO_TRACK_ID, videoSource);
+            _localStream.addTrack(_localVideoTrack);
+        }
+
+    }
+
+
+    // 是否使用录屏
+    private boolean screencaptureEnabled = false;
+
+    /**
+     * 创建媒体方式
+     *
+     * @return VideoCapturer
+     */
+    private VideoCapturer createVideoCapture() {
+        VideoCapturer videoCapturer;
+
+
+        if (screencaptureEnabled) {
+            return createScreenCapturer();
+        }
+
+        if (Camera2Enumerator.isSupported(mContext)) {
+            videoCapturer = createCameraCapture(new Camera2Enumerator(mContext));
+        } else {
+            videoCapturer = createCameraCapture(new Camera1Enumerator(true));
+        }
+        return videoCapturer;
+    }
+
+    /**
+     * 创建相机媒体流
+     */
+    private VideoCapturer createCameraCapture(CameraEnumerator enumerator) {
+        final String[] deviceNames = enumerator.getDeviceNames();
+
+        // First, try to find front facing camera
+        for (String deviceName : deviceNames) {
+            if (enumerator.isFrontFacing(deviceName)) {
+                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
+
+                if (videoCapturer != null) {
+                    return videoCapturer;
+                }
+            }
+        }
+
+        // Front facing camera not found, try something else
+        for (String deviceName : deviceNames) {
+            if (!enumerator.isFrontFacing(deviceName)) {
+                VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
+
+                if (videoCapturer != null) {
+                    return videoCapturer;
+                }
+            }
+        }
+
+        return null;
+    }
+
+
+    private static Intent mediaProjectionPermissionResultData;
+    private static int mediaProjectionPermissionResultCode;
+
+    @TargetApi(21)
+    private VideoCapturer createScreenCapturer() {
+        if (mediaProjectionPermissionResultCode != Activity.RESULT_OK) {
+            return null;
+        }
+        return new ScreenCapturerAndroid(
+                mediaProjectionPermissionResultData, new MediaProjection.Callback() {
+            @Override
+            public void onStop() {
+                Log.e(TAG, "User revoked permission to capture the screen.");
+            }
+        });
+    }
+
+    //**************************************各种约束******************************************/
+    private static final String AUDIO_ECHO_CANCELLATION_CONSTRAINT = "googEchoCancellation";
+    private static final String AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT = "googAutoGainControl";
+    private static final String AUDIO_HIGH_PASS_FILTER_CONSTRAINT = "googHighpassFilter";
+    private static final String AUDIO_NOISE_SUPPRESSION_CONSTRAINT = "googNoiseSuppression";
+
+    // 配置音频参数
+    private MediaConstraints createAudioConstraints() {
+        MediaConstraints audioConstraints = new MediaConstraints();
+        audioConstraints.mandatory.add(
+                new MediaConstraints.KeyValuePair(AUDIO_ECHO_CANCELLATION_CONSTRAINT, "true"));
+        audioConstraints.mandatory.add(
+                new MediaConstraints.KeyValuePair(AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT, "false"));
+        audioConstraints.mandatory.add(
+                new MediaConstraints.KeyValuePair(AUDIO_HIGH_PASS_FILTER_CONSTRAINT, "false"));
+        audioConstraints.mandatory.add(
+                new MediaConstraints.KeyValuePair(AUDIO_NOISE_SUPPRESSION_CONSTRAINT, "true"));
+        return audioConstraints;
+    }
+
+    //------------------------------------回调---------------------------------------------
+    @Override
+    public void onSendIceCandidate(String userId, IceCandidate candidate) {
+        if (mCallback != null) {
+            mCallback.onSendIceCandidate(userId, candidate);
+        }
+
+    }
+
+    @Override
+    public void onSendOffer(String userId, SessionDescription description) {
+        if (mCallback != null) {
+            mCallback.onSendOffer(userId, description);
+        }
+    }
+
+    @Override
+    public void onSendAnswer(String userId, SessionDescription description) {
+        if (mCallback != null) {
+            mCallback.onSendAnswer(userId, description);
+        }
+    }
+
+    @Override
+    public void onRemoteStream(String userId, MediaStream stream) {
+        if (mCallback != null) {
+            mCallback.onRemoteStream(userId);
+        }
+    }
+
+    @Override
+    public void onRemoveStream(String userId, MediaStream stream) {
+        leaveRoom(userId);
+    }
+
+    @Override
+    public void onDisconnected(String userId) {
+        if (mCallback != null) {
+           Log.d(TAG, "onDisconnected mCallback != null");
+            mCallback.onDisconnected(userId);
+        } else {
+           Log.d(TAG, "onDisconnected mCallback == null");
+        }
+    }
+
+}

+ 11 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/except/NotInitializedException.java

@@ -0,0 +1,11 @@
+package com.wdkl.skywebrtc.except;
+
+/**
+ * Created by dds on 17/02/2018.
+ */
+
+public class NotInitializedException extends RuntimeException {
+    public NotInitializedException() {
+        super("Not init!!!");
+    }
+}

+ 12 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/inter/ILogEvent.java

@@ -0,0 +1,12 @@
+package com.wdkl.skywebrtc.inter;
+
+/**
+ * Created by dds on 2020/2/10.
+ * 通话记录
+ * <p>
+ * 去电  来电  通话时长  通话类型  是否接听
+ */
+public interface ILogEvent {
+
+
+}

+ 47 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/inter/ISkyEvent.java

@@ -0,0 +1,47 @@
+package com.wdkl.skywebrtc.inter;
+
+import java.util.List;
+
+/**
+ * Created by dds on 2019/8/21.
+ * android_shuai@163.com
+ */
+public interface ISkyEvent {
+
+    // 创建房间
+    void createRoom(String room, int roomSize);
+
+    // 发送单人邀请
+    void sendInvite(String room, List<String> userIds, boolean audioOnly);
+
+    void sendRefuse(String room, String inviteId, int refuseType);
+
+    void sendTransAudio(String toId);
+
+    void sendDisConnect(String room, String toId, boolean isCrashed);
+
+    void sendCancel(String mRoomId, List<String> toId);
+
+    void sendJoin(String room);
+
+    void sendRingBack(String targetId, String room);
+
+    void sendLeave(String room, String userId);
+
+    // sendOffer
+    void sendOffer(String userId, String sdp);
+
+    // sendAnswer
+    void sendAnswer(String userId, String sdp);
+
+    // sendIceCandidate
+    void sendIceCandidate(String userId, String id, int label, String candidate);
+
+    void onRemoteRing();
+
+    void shouldStartRing(boolean isComing);
+
+    void shouldStopRing();
+
+
+}

+ 8 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/model/RoomInfo.java

@@ -0,0 +1,8 @@
+package com.wdkl.skywebrtc.model;
+
+public class RoomInfo {
+
+    public String roomId;
+    public int roomSize;
+
+}

+ 18 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/model/UserInfo.java

@@ -0,0 +1,18 @@
+package com.wdkl.skywebrtc.model;
+
+/**
+ * Created by dds on 2020/4/11.
+ * 用户信息
+ */
+public class UserInfo {
+
+    private String userId;
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+}

+ 28 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/render/ProxyVideoSink.java

@@ -0,0 +1,28 @@
+package com.wdkl.skywebrtc.render;
+
+import org.webrtc.Logging;
+import org.webrtc.VideoFrame;
+import org.webrtc.VideoSink;
+
+/**
+ * Created by dds on 2019/4/4.
+ * android_shuai@163.com
+ */
+public class ProxyVideoSink implements VideoSink {
+    private static final String TAG = "dds_ProxyVideoSink";
+    private VideoSink target;
+
+    @Override
+    synchronized public void onFrame(VideoFrame frame) {
+        if (target == null) {
+            Logging.d(TAG, "Dropping frame in proxy because target is null.");
+            return;
+        }
+        target.onFrame(frame);
+    }
+
+    synchronized public void setTarget(VideoSink target) {
+        this.target = target;
+    }
+
+}

+ 171 - 0
rtc-chat/src/main/java/com/wdkl/skywebrtc/render/VideoFileRenderer.java

@@ -0,0 +1,171 @@
+/*
+ *  Copyright 2016 The WebRTC Project Authors. All rights reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree. An additional intellectual property rights grant can be found
+ *  in the file PATENTS.  All contributing project authors may
+ *  be found in the AUTHORS file in the root of the source tree.
+ */
+
+package com.wdkl.skywebrtc.render;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import org.webrtc.EglBase;
+import org.webrtc.Logging;
+import org.webrtc.ThreadUtils;
+import org.webrtc.VideoFrame;
+import org.webrtc.VideoSink;
+import org.webrtc.YuvConverter;
+import org.webrtc.YuvHelper;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * Can be used to save the video frames to file.
+ */
+public class VideoFileRenderer implements VideoSink {
+  private static final String TAG = "VideoFileRenderer";
+
+  private final HandlerThread renderThread;
+  private final Handler renderThreadHandler;
+  private final HandlerThread fileThread;
+  private final Handler fileThreadHandler;
+  private final FileOutputStream videoOutFile;
+  private final String outputFileName;
+  private final int outputFileWidth;
+  private final int outputFileHeight;
+  private final int outputFrameSize;
+  private final ByteBuffer outputFrameBuffer;
+  private EglBase eglBase;
+  private YuvConverter yuvConverter;
+  private int frameCount;
+
+  public VideoFileRenderer(String outputFile, int outputFileWidth, int outputFileHeight,
+                           final EglBase.Context sharedContext) throws IOException {
+    if ((outputFileWidth % 2) == 1 || (outputFileHeight % 2) == 1) {
+      throw new IllegalArgumentException("Does not support uneven width or height");
+    }
+
+    this.outputFileName = outputFile;
+    this.outputFileWidth = outputFileWidth;
+    this.outputFileHeight = outputFileHeight;
+
+    outputFrameSize = outputFileWidth * outputFileHeight * 3 / 2;
+    outputFrameBuffer = ByteBuffer.allocateDirect(outputFrameSize);
+
+    videoOutFile = new FileOutputStream(outputFile);
+    videoOutFile.write(
+        ("YUV4MPEG2 C420 W" + outputFileWidth + " H" + outputFileHeight + " Ip F30:1 A1:1\n")
+            .getBytes(Charset.forName("US-ASCII")));
+
+    renderThread = new HandlerThread(TAG + "RenderThread");
+    renderThread.start();
+    renderThreadHandler = new Handler(renderThread.getLooper());
+
+    fileThread = new HandlerThread(TAG + "FileThread");
+    fileThread.start();
+    fileThreadHandler = new Handler(fileThread.getLooper());
+
+    ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() {
+      @Override
+      public void run() {
+        eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
+        eglBase.createDummyPbufferSurface();
+        eglBase.makeCurrent();
+        yuvConverter = new YuvConverter();
+      }
+    });
+  }
+
+  @Override
+  public void onFrame(VideoFrame frame) {
+    frame.retain();
+    renderThreadHandler.post(() -> renderFrameOnRenderThread(frame));
+  }
+
+  private void renderFrameOnRenderThread(VideoFrame frame) {
+    final VideoFrame.Buffer buffer = frame.getBuffer();
+
+    // If the frame is rotated, it will be applied after cropAndScale. Therefore, if the frame is
+    // rotated by 90 degrees, swap width and height.
+    final int targetWidth = frame.getRotation() % 180 == 0 ? outputFileWidth : outputFileHeight;
+    final int targetHeight = frame.getRotation() % 180 == 0 ? outputFileHeight : outputFileWidth;
+
+    final float frameAspectRatio = (float) buffer.getWidth() / (float) buffer.getHeight();
+    final float fileAspectRatio = (float) targetWidth / (float) targetHeight;
+
+    // Calculate cropping to equalize the aspect ratio.
+    int cropWidth = buffer.getWidth();
+    int cropHeight = buffer.getHeight();
+    if (fileAspectRatio > frameAspectRatio) {
+      cropHeight = (int) (cropHeight * (frameAspectRatio / fileAspectRatio));
+    } else {
+      cropWidth = (int) (cropWidth * (fileAspectRatio / frameAspectRatio));
+    }
+
+    final int cropX = (buffer.getWidth() - cropWidth) / 2;
+    final int cropY = (buffer.getHeight() - cropHeight) / 2;
+
+    final VideoFrame.Buffer scaledBuffer =
+        buffer.cropAndScale(cropX, cropY, cropWidth, cropHeight, targetWidth, targetHeight);
+    frame.release();
+
+    final VideoFrame.I420Buffer i420 = scaledBuffer.toI420();
+    scaledBuffer.release();
+
+    fileThreadHandler.post(() -> {
+      YuvHelper.I420Rotate(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(),
+          i420.getDataV(), i420.getStrideV(), outputFrameBuffer, i420.getWidth(), i420.getHeight(),
+          frame.getRotation());
+      i420.release();
+
+      try {
+        videoOutFile.write("FRAME\n".getBytes(Charset.forName("US-ASCII")));
+        videoOutFile.write(
+            outputFrameBuffer.array(), outputFrameBuffer.arrayOffset(), outputFrameSize);
+      } catch (IOException e) {
+        throw new RuntimeException("Error writing video to disk", e);
+      }
+      frameCount++;
+    });
+  }
+
+  /**
+   * Release all resources. All already posted frames will be rendered first.
+   */
+  public void release() {
+    final CountDownLatch cleanupBarrier = new CountDownLatch(1);
+    renderThreadHandler.post(() -> {
+      yuvConverter.release();
+      eglBase.release();
+      renderThread.quit();
+      cleanupBarrier.countDown();
+    });
+    ThreadUtils.awaitUninterruptibly(cleanupBarrier);
+    fileThreadHandler.post(() -> {
+      try {
+        videoOutFile.close();
+        Logging.d(TAG,
+            "Video written to disk as " + outputFileName + ". The number of frames is " + frameCount
+                + " and the dimensions of the frames are " + outputFileWidth + "x"
+                + outputFileHeight + ".");
+      } catch (IOException e) {
+        throw new RuntimeException("Error closing output file", e);
+      }
+      fileThread.quit();
+    });
+    try {
+      fileThread.join();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      Logging.e(TAG, "Interrupted while waiting for the write to disk to complete.", e);
+    }
+  }
+}

+ 5 - 0
webrtc/.gitignore

@@ -0,0 +1,5 @@
+/build
+*.iml
+.DS_Store
+/.idea
+/gradle

+ 82 - 0
webrtc/build.gradle

@@ -0,0 +1,82 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion target_sdk_version
+    buildToolsVersion build_tools_version
+
+    defaultConfig {
+        minSdkVersion min_sdk_version
+        targetSdkVersion target_sdk_version
+        versionCode app_version_code
+        versionName app_version
+
+        compileOptions {
+            sourceCompatibility JavaVersion.VERSION_1_8
+            targetCompatibility JavaVersion.VERSION_1_8
+        }
+        vectorDrawables.useSupportLibrary = true
+
+        //ndk {
+            // 设置支持的SO库架构
+        //    abiFilters 'armeabi-v7a', 'x86'//, 'arm64-v8a'
+        //}
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+
+        debug {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+
+    //implementation 'androidx.appcompat:appcompat:1.2.0'
+    //implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    //implementation 'androidx.legacy:legacy-support-v4:1.0.0'
+    //implementation 'androidx.vectordrawable:vectordrawable:1.1.0'
+    // navigation
+    //implementation 'androidx.navigation:navigation-fragment:2.3.0'
+    //implementation 'androidx.navigation:navigation-ui:2.3.0'
+
+    //implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+
+    implementation 'com.google.android.material:material:1.1.0'
+    /**
+     *  Android基础依赖库
+     */
+    implementation "com.android.support:design:$support_library_version"
+    implementation "com.android.support:support-v4:$support_library_version"
+    implementation "com.android.support:cardview-v7:$support_library_version"
+    implementation "com.android.support:appcompat-v7:$support_library_version"
+
+    implementation "com.android.support.constraint:constraint-layout:1.1.0-beta5"
+    implementation "com.android.support:support-vector-drawable:26.1.0"
+    implementation "android.arch.lifecycle:extensions:1.0.0-alpha4"
+
+    // 内存泄漏检测
+    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1'
+
+    implementation project(path: ':rtc-chat')
+    // java
+    implementation 'org.java-websocket:Java-WebSocket:1.4.0'
+
+    compile 'com.alibaba:fastjson:1.2.23'
+    //强大的弹窗库
+    implementation 'com.lxj:xpopup:2.2.0'
+    //eventbus
+    compile 'org.greenrobot:eventbus:3.1.1'
+
+    compile 'com.blankj:utilcodex:1.30.5'
+
+    //通知提示弹出库
+    api 'com.tapadoo.android:alerter:6.2.1'
+}

+ 25 - 0
webrtc/proguard-rules.pro

@@ -0,0 +1,25 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/dongxiangjun/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 57 - 0
webrtc/src/main/AndroidManifest.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.wdkl.webrtc">
+    <!-- 设置视频直播权限 -->
+    <uses-feature android:name="android.hardware.camera" />
+    <uses-feature android:name="android.hardware.camera.autofocus" />
+    <uses-feature
+        android:glEsVersion="0x00020000"
+        android:required="true" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 悬浮窗显示 -->
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
+
+    <application
+        android:allowBackup="true"
+        android:label="@string/app_name"
+        android:supportsRtl="true">
+
+        <!-- ======================java====================== -->
+        <activity
+            android:name="com.wdkl.core.voip.CallSingleActivity"
+            android:screenOrientation="landscape"
+            android:showOnLockScreen="true"
+            android:showWhenLocked="true"
+            android:theme="@style/AppTheme.NoActionBar"
+            tools:ignore="UnusedAttribute">
+            <intent-filter>
+                <action android:name="${applicationId}.kit.voip.single" />
+
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <activity
+            android:name="com.wdkl.core.voip.CallMultiActivity"
+            android:screenOrientation="landscape"
+            android:showOnLockScreen="true"
+            android:theme="@style/AppTheme.NoActionBar" />
+
+        <receiver android:name="com.wdkl.core.voip.VoipReceiver">
+            <intent-filter>
+                <action android:name="${applicationId}.voip.Receiver" />
+            </intent-filter>
+        </receiver>
+    </application>
+
+</manifest>

+ 23 - 0
webrtc/src/main/java/com/wdkl/core/base/BaseActivity.java

@@ -0,0 +1,23 @@
+package com.wdkl.core.base;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+
+
+import com.wdkl.core.util.ActivityStackManager;
+
+public class BaseActivity extends AppCompatActivity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        // 添加Activity到堆栈
+        ActivityStackManager.getInstance().onCreated(this);
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    protected void onDestroy() {
+        ActivityStackManager.getInstance().onDestroyed(this);
+        super.onDestroy();
+    }
+}

+ 30 - 0
webrtc/src/main/java/com/wdkl/core/consts/Urls.java

@@ -0,0 +1,30 @@
+package com.wdkl.core.consts;
+
+/**
+ * Created by dds on 2020/4/19.
+ * ddssingsong@163.com
+ */
+public class Urls {
+
+    //private final static String IP = "192.168.2.111";
+    //public final static String IP = "42.192.40.58:5000";
+    public final static String IP = "172.28.100.100:5000";
+
+    private final static String HOST = "http://" + IP + "/";
+
+    // 信令地址
+    public final static String WS = "ws://" + IP + "/ws";
+
+    //用户名称
+    public static String USER_ID;
+
+    // 获取用户列表
+    public static String getUserList() {
+        return HOST + "userList";
+    }
+
+    // 获取房间列表
+    public static String getRoomList() {
+        return HOST + "roomList";
+    }
+}

+ 48 - 0
webrtc/src/main/java/com/wdkl/core/socket/IEvent.java

@@ -0,0 +1,48 @@
+package com.wdkl.core.socket;
+
+/**
+ * Created by dds on 2019/7/26.
+ * ddssingsong@163.com
+ */
+public interface IEvent {
+
+
+    void onOpen();
+
+    void loginSuccess(String userId, String avatar);
+
+
+    void onInvite(String room, boolean audioOnly, String inviteId, String userList);
+
+
+    void onCancel(String inviteId);
+
+    void onRing(String userId);
+
+
+    void onPeers(String myId, String userList, int roomSize);
+
+    void onNewPeer(String myId);
+
+    void onReject(String userId, int type);
+
+    // onOffer
+    void onOffer(String userId, String sdp);
+
+    // onAnswer
+    void onAnswer(String userId, String sdp);
+
+    // ice-candidate
+    void onIceCandidate(String userId, String id, int label, String candidate);
+
+    void onLeave(String userId);
+
+    void logout(String str);
+
+    void onTransAudio(String userId);
+
+    void onDisConnect(String userId);
+
+    void reConnect();
+
+}

+ 15 - 0
webrtc/src/main/java/com/wdkl/core/socket/IUserState.java

@@ -0,0 +1,15 @@
+package com.wdkl.core.socket;
+
+/**
+ * Created by dds on 2019/8/2.
+ * android_shuai@163.com
+ */
+public interface IUserState {
+
+
+    void userLogin();
+
+    void userLogout();
+
+
+}

+ 500 - 0
webrtc/src/main/java/com/wdkl/core/socket/MyWebSocket.java

@@ -0,0 +1,500 @@
+package com.wdkl.core.socket;
+
+import android.annotation.SuppressLint;
+import android.util.Log;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.wdkl.core.util.StringUtil;
+
+import org.java_websocket.client.WebSocketClient;
+import org.java_websocket.handshake.ServerHandshake;
+
+import java.net.URI;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Created by dds on 2019/7/26.
+ * android_shuai@163.com
+ */
+public class MyWebSocket extends WebSocketClient {
+    private final static String TAG = "dds_WebSocket";
+    private final IEvent iEvent;
+    private boolean connectFlag = false;
+
+
+    public MyWebSocket(URI serverUri, IEvent event) {
+        super(serverUri);
+        this.iEvent = event;
+    }
+
+    @Override
+    public void onClose(int code, String reason, boolean remote) {
+        Log.e("dds_error", "onClose:" + reason + "remote:" + remote);
+        if (connectFlag) {
+            try {
+                Thread.sleep(3000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            this.iEvent.reConnect();
+        } else {
+            this.iEvent.logout("onClose");
+        }
+
+    }
+
+    @Override
+    public void onError(Exception ex) {
+        Log.e("dds_error", "onError:" + ex.toString());
+        this.iEvent.logout("onError");
+        connectFlag = false;
+    }
+
+    @Override
+    public void onOpen(ServerHandshake handshakedata) {
+        Log.e("dds_info", "onOpen");
+        this.iEvent.onOpen();
+        connectFlag = true;
+    }
+
+    @Override
+    public void onMessage(String message) {
+        Log.d(TAG, message);
+        handleMessage(message);
+    }
+
+
+    public void setConnectFlag(boolean flag) {
+        connectFlag = flag;
+    }
+
+    // ---------------------------------------处理接收消息-------------------------------------
+
+    private void handleMessage(String message) {
+        Map map = JSON.parseObject(message, Map.class);
+        String eventName = (String) map.get("eventName");
+        if (eventName == null) return;
+        // 登录成功
+        if (eventName.equals("__login_success")) {
+            handleLogin(map);
+            return;
+        }
+        // 被邀请
+        if (eventName.equals("__invite")) {
+            handleInvite(map);
+            return;
+        }
+        // 取消拨出
+        if (eventName.equals("__cancel")) {
+            handleCancel(map);
+            return;
+        }
+        // 响铃
+        if (eventName.equals("__ring")) {
+            handleRing(map);
+            return;
+        }
+        // 进入房间
+        if (eventName.equals("__peers")) {
+            handlePeers(map);
+            return;
+        }
+        // 新人入房间
+        if (eventName.equals("__new_peer")) {
+            handleNewPeer(map);
+            return;
+        }
+        // 拒绝接听
+        if (eventName.equals("__reject")) {
+            handleReject(map);
+            return;
+        }
+        // offer
+        if (eventName.equals("__offer")) {
+            handleOffer(map);
+            return;
+        }
+        // answer
+        if (eventName.equals("__answer")) {
+            handleAnswer(map);
+            return;
+        }
+        // ice-candidate
+        if (eventName.equals("__ice_candidate")) {
+            handleIceCandidate(map);
+        }
+        // 离开房间
+        if (eventName.equals("__leave")) {
+            handleLeave(map);
+        }
+        // 切换到语音
+        if (eventName.equals("__audio")) {
+            handleTransAudio(map);
+        }
+        // 意外断开
+        if (eventName.equals("__disconnect")) {
+            handleDisConnect(map);
+        }
+
+
+    }
+
+    private void handleDisConnect(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String fromId = (String) data.get("fromID");
+            this.iEvent.onDisConnect(fromId);
+        }
+    }
+
+    private void handleTransAudio(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String fromId = (String) data.get("fromID");
+            this.iEvent.onTransAudio(fromId);
+        }
+    }
+
+    private void handleLogin(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String userID = (String) data.get("userID");
+            String avatar = (String) data.get("avatar");
+            this.iEvent.loginSuccess(userID, avatar);
+        }
+
+
+    }
+
+    private void handleIceCandidate(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String userID = (String) data.get("fromID");
+            String id = (String) data.get("id");
+            int label = (int) data.get("label");
+            String candidate = (String) data.get("candidate");
+            this.iEvent.onIceCandidate(userID, id, label, candidate);
+        }
+    }
+
+    private void handleAnswer(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String sdp = (String) data.get("sdp");
+            String userID = (String) data.get("fromID");
+            this.iEvent.onAnswer(userID, sdp);
+        }
+    }
+
+    private void handleOffer(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String sdp = (String) data.get("sdp");
+            String userID = (String) data.get("fromID");
+            this.iEvent.onOffer(userID, sdp);
+        }
+    }
+
+    private void handleReject(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String fromID = (String) data.get("fromID");
+            int rejectType = Integer.parseInt(String.valueOf(data.get("refuseType")));
+            this.iEvent.onReject(fromID, rejectType);
+        }
+    }
+
+    private void handlePeers(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String you = (String) data.get("you");
+            String connections = (String) data.get("connections");
+            int roomSize = (int) data.get("roomSize");
+            this.iEvent.onPeers(you, connections, roomSize);
+        }
+    }
+
+    private void handleNewPeer(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String userID = (String) data.get("userID");
+            this.iEvent.onNewPeer(userID);
+        }
+    }
+
+    private void handleRing(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String fromId = (String) data.get("fromID");
+            this.iEvent.onRing(fromId);
+        }
+    }
+
+    private void handleCancel(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String inviteID = (String) data.get("inviteID");
+            String userList = (String) data.get("userList");
+            this.iEvent.onCancel(inviteID);
+        }
+    }
+
+    private void handleInvite(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String room = (String) data.get("room");
+            boolean audioOnly = (boolean) data.get("audioOnly");
+            String inviteID = (String) data.get("inviteID");
+            String userList = (String) data.get("userList");
+            this.iEvent.onInvite(room, audioOnly, inviteID, userList);
+        }
+    }
+
+    private void handleLeave(Map map) {
+        Map data = (Map) map.get("data");
+        if (data != null) {
+            String fromID = (String) data.get("fromID");
+            this.iEvent.onLeave(fromID);
+        }
+    }
+
+    /**
+     * ------------------------------发送消息----------------------------------------
+     */
+    public void createRoom(String room, int roomSize, String myId) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__create");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("room", room);
+        childMap.put("roomSize", roomSize);
+        childMap.put("userID", myId);
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 发送邀请
+    public void sendInvite(String room, String myId, List<String> users, boolean audioOnly) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__invite");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("room", room);
+        childMap.put("audioOnly", audioOnly);
+        childMap.put("inviteID", myId);
+
+        String join = StringUtil.listToString(users);
+        childMap.put("userList", join);
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 取消邀请
+    public void sendCancel(String mRoomId, String useId, List<String> users) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__cancel");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("inviteID", useId);
+        childMap.put("room", mRoomId);
+
+        String join = StringUtil.listToString(users);
+        childMap.put("userList", join);
+
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 发送响铃通知
+    public void sendRing(String myId, String toId, String room) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__ring");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("fromID", myId);
+        childMap.put("toID", toId);
+        childMap.put("room", room);
+
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    //加入房间
+    public void sendJoin(String room, String myId) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__join");
+
+        Map<String, String> childMap = new HashMap<>();
+        childMap.put("room", room);
+        childMap.put("userID", myId);
+
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 拒接接听
+    public void sendRefuse(String room, String inviteID, String myId, int refuseType) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__reject");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("room", room);
+        childMap.put("toID", inviteID);
+        childMap.put("fromID", myId);
+        childMap.put("refuseType", String.valueOf(refuseType));
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 离开房间
+    public void sendLeave(String myId, String room, String userId) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__leave");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("room", room);
+        childMap.put("fromID", myId);
+        childMap.put("userID", userId);
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        if (isOpen()) {
+            send(jsonString);
+        }
+    }
+
+    // send offer
+    public void sendOffer(String myId, String userId, String sdp) {
+        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("sdp", sdp);
+        childMap.put("userID", userId);
+        childMap.put("fromID", myId);
+        map.put("data", childMap);
+        map.put("eventName", "__offer");
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // send answer
+    public void sendAnswer(String myId, String userId, String sdp) {
+        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("sdp", sdp);
+        childMap.put("fromID", myId);
+        childMap.put("userID", userId);
+        map.put("data", childMap);
+        map.put("eventName", "__answer");
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // send ice-candidate
+    public void sendIceCandidate(String myId, String userId, String id, int label, String candidate) {
+        Map<String, Object> map = new HashMap<>();
+        map.put("eventName", "__ice_candidate");
+
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("userID", userId);
+        childMap.put("fromID", myId);
+        childMap.put("id", id);
+        childMap.put("label", label);
+        childMap.put("candidate", candidate);
+
+        map.put("data", childMap);
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        if (isOpen()) {
+            send(jsonString);
+        }
+    }
+
+    // 切换到语音
+    public void sendTransAudio(String myId, String userId) {
+        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("fromID", myId);
+        childMap.put("userID", userId);
+        map.put("data", childMap);
+        map.put("eventName", "__audio");
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 断开重连
+    public void sendDisconnect(String room, String myId, String userId) {
+        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> childMap = new HashMap<>();
+        childMap.put("fromID", myId);
+        childMap.put("userID", userId);
+        childMap.put("room", room);
+        map.put("data", childMap);
+        map.put("eventName", "__disconnect");
+        JSONObject object = new JSONObject(map);
+        final String jsonString = object.toString();
+        Log.d(TAG, "send-->" + jsonString);
+        send(jsonString);
+    }
+
+    // 忽略证书
+    public static class TrustManagerTest implements X509TrustManager {
+
+        @SuppressLint("TrustAllX509TrustManager")
+        @Override
+        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+
+        }
+
+        @SuppressLint("TrustAllX509TrustManager")
+        @Override
+        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
+
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[0];
+        }
+    }
+
+}

+ 367 - 0
webrtc/src/main/java/com/wdkl/core/socket/SocketManager.java

@@ -0,0 +1,367 @@
+package com.wdkl.core.socket;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.wdkl.core.voip.Utils;
+import com.wdkl.core.voip.VoipReceiver;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.SkyEngineKit;
+
+import java.lang.ref.WeakReference;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.SecureRandom;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+
+/**
+ * Created by dds on 2019/7/26.
+ * ddssignsong@163.com
+ */
+public class SocketManager implements IEvent {
+    private final static String TAG = "dds_SocketManager";
+    private MyWebSocket webSocket;
+    private int userState;
+    private String myId;
+    private Context mContext;
+
+    private final Handler handler = new Handler(Looper.getMainLooper());
+
+    private SocketManager() {
+
+    }
+
+    public void init(Context context) {
+        mContext = context;
+    }
+
+    private static class Holder {
+        private static final SocketManager socketManager = new SocketManager();
+    }
+
+    public static SocketManager getInstance() {
+        return Holder.socketManager;
+    }
+
+    public void connect(String url, String userId, int device) {
+        if (webSocket == null || !webSocket.isOpen()) {
+            URI uri;
+            try {
+                String urls = url + "/" + userId + "/" + device;
+                uri = new URI(urls);
+            } catch (URISyntaxException e) {
+                e.printStackTrace();
+                return;
+            }
+            webSocket = new MyWebSocket(uri, this);
+            // 设置wss
+            if (url.startsWith("wss")) {
+                try {
+                    SSLContext sslContext = SSLContext.getInstance("TLS");
+                    if (sslContext != null) {
+                        sslContext.init(null, new TrustManager[]{new MyWebSocket.TrustManagerTest()}, new SecureRandom());
+                    }
+
+                    SSLSocketFactory factory = null;
+                    if (sslContext != null) {
+                        factory = sslContext.getSocketFactory();
+                    }
+
+                    if (factory != null) {
+                        webSocket.setSocket(factory.createSocket());
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+            // 开始connect
+            webSocket.connect();
+        }
+
+
+    }
+
+    public void unConnect() {
+        if (webSocket != null) {
+            webSocket.setConnectFlag(false);
+            webSocket.close();
+            webSocket = null;
+        }
+
+    }
+
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public void onOpen() {
+        Log.i(TAG, "socket is open!");
+
+    }
+
+    @Override
+    public void loginSuccess(String userId, String avatar) {
+        Log.i(TAG, "loginSuccess:" + userId);
+        myId = userId;
+        userState = 1;
+        if (iUserState != null && iUserState.get() != null) {
+            iUserState.get().userLogin();
+        }
+    }
+
+
+    // ======================================================================================
+    public void createRoom(String room, int roomSize) {
+        if (webSocket != null) {
+            webSocket.createRoom(room, roomSize, myId);
+        }
+
+    }
+
+    public void sendInvite(String room, List<String> users, boolean audioOnly) {
+        if (webSocket != null) {
+            webSocket.sendInvite(room, myId, users, audioOnly);
+        }
+    }
+
+    public void sendLeave(String room, String userId) {
+        if (webSocket != null) {
+            webSocket.sendLeave(myId, room, userId);
+        }
+    }
+
+    public void sendRingBack(String targetId, String room) {
+        if (webSocket != null) {
+            webSocket.sendRing(myId, targetId, room);
+        }
+    }
+
+    public void sendRefuse(String room, String inviteId, int refuseType) {
+        if (webSocket != null) {
+            webSocket.sendRefuse(room, inviteId, myId, refuseType);
+        }
+    }
+
+    public void sendCancel(String mRoomId, List<String> userIds) {
+        if (webSocket != null) {
+            webSocket.sendCancel(mRoomId, myId, userIds);
+        }
+    }
+
+    public void sendJoin(String room) {
+        if (webSocket != null) {
+            webSocket.sendJoin(room, myId);
+        }
+    }
+
+    public void sendMeetingInvite(String userList) {
+
+    }
+
+    public void sendOffer(String userId, String sdp) {
+        if (webSocket != null) {
+            webSocket.sendOffer(myId, userId, sdp);
+        }
+    }
+
+    public void sendAnswer(String userId, String sdp) {
+        if (webSocket != null) {
+            webSocket.sendAnswer(myId, userId, sdp);
+        }
+    }
+
+    public void sendIceCandidate(String userId, String id, int label, String candidate) {
+        if (webSocket != null) {
+            webSocket.sendIceCandidate(myId, userId, id, label, candidate);
+        }
+    }
+
+    public void sendTransAudio(String userId) {
+        if (webSocket != null) {
+            webSocket.sendTransAudio(myId, userId);
+        }
+    }
+
+    public void sendDisconnect(String room, String userId) {
+        if (webSocket != null) {
+            webSocket.sendDisconnect(room, myId, userId);
+        }
+    }
+
+
+    // ========================================================================================
+    @Override
+    public void onInvite(String room, boolean audioOnly, String inviteId, String userList) {
+        //todo: 待处理
+        Intent intent = new Intent();
+        intent.putExtra("room", room);
+        intent.putExtra("audioOnly", audioOnly);
+        intent.putExtra("inviteId", inviteId);
+        intent.putExtra("userList", userList);
+        intent.setAction(Utils.ACTION_VOIP_RECEIVER);
+        intent.setComponent(new ComponentName(mContext.getPackageName(), VoipReceiver.class.getName()));
+        // 发送广播
+        mContext.sendBroadcast(intent);
+
+    }
+
+    @Override
+    public void onCancel(String inviteId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onCancel(inviteId);
+            }
+        });
+
+    }
+
+    @Override
+    public void onRing(String fromId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onRingBack(fromId);
+            }
+        });
+
+
+    }
+
+    @Override  // 加入房间
+    public void onPeers(String myId, String connections, int roomSize) {
+        handler.post(() -> {
+            //自己进入了房间,然后开始发送offer
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onJoinHome(myId, connections, roomSize);
+            }
+        });
+
+    }
+
+    @Override
+    public void onNewPeer(String userId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.newPeer(userId);
+            }
+        });
+
+    }
+
+    @Override
+    public void onReject(String userId, int type) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onRefuse(userId, type);
+            }
+        });
+
+    }
+
+    @Override
+    public void onOffer(String userId, String sdp) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onReceiveOffer(userId, sdp);
+            }
+        });
+
+
+    }
+
+    @Override
+    public void onAnswer(String userId, String sdp) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onReceiverAnswer(userId, sdp);
+            }
+        });
+
+    }
+
+    @Override
+    public void onIceCandidate(String userId, String id, int label, String candidate) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onRemoteIceCandidate(userId, id, label, candidate);
+            }
+        });
+
+    }
+
+    @Override
+    public void onLeave(String userId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onLeave(userId);
+            }
+        });
+    }
+
+    @Override
+    public void logout(String str) {
+        Log.i(TAG, "logout:" + str);
+        userState = 0;
+        if (iUserState != null && iUserState.get() != null) {
+            iUserState.get().userLogout();
+        }
+    }
+
+    @Override
+    public void onTransAudio(String userId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onTransAudio(userId);
+            }
+        });
+    }
+
+    @Override
+    public void onDisConnect(String userId) {
+        handler.post(() -> {
+            CallSession currentSession = SkyEngineKit.Instance().getCurrentSession();
+            if (currentSession != null) {
+                currentSession.onDisConnect(userId, EnumType.CallEndReason.RemoteSignalError);
+            }
+        });
+    }
+
+    @Override
+    public void reConnect() {
+        handler.post(() -> {
+            webSocket.reconnect();
+        });
+    }
+    //===========================================================================================
+
+
+    public int getUserState() {
+        return userState;
+    }
+
+    private WeakReference<IUserState> iUserState;
+
+    public void addUserStateCallback(IUserState userState) {
+        iUserState = new WeakReference<>(userState);
+    }
+
+}

+ 35 - 0
webrtc/src/main/java/com/wdkl/core/ui/event/MsgEvent.java

@@ -0,0 +1,35 @@
+package com.wdkl.core.ui.event;
+
+public class MsgEvent<T> {
+    private int code;
+    private T data;
+    public static final int CODE_ON_CALL_ENDED = 0X01;//语音视频通话结束
+    public static final int CODE_ON_REMOTE_RING = 0X02;//对方已响铃
+
+
+    public MsgEvent(int code) {
+        this.code = code;
+    }
+
+    public MsgEvent(int code, T data) {
+        this.code = code;
+        this.data = data;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public void setCode(int code) {
+        this.code = code;
+    }
+
+    public T getData() {
+        return data;
+    }
+
+    public void setData(T data) {
+        this.data = data;
+    }
+}
+

+ 42 - 0
webrtc/src/main/java/com/wdkl/core/ui/user/UserBean.java

@@ -0,0 +1,42 @@
+package com.wdkl.core.ui.user;
+
+import android.text.TextUtils;
+
+/**
+ * Created by dds on 2020/4/13.
+ * android_shuai@163.com
+ */
+public class UserBean {
+    private String userId;
+    private String avatar;
+    private String nickName;
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public String getAvatar() {
+        return avatar;
+    }
+
+    public void setAvatar(String avatar) {
+        this.avatar = avatar;
+    }
+
+    public String getNickName() {
+        if (TextUtils.isEmpty(nickName)) {
+            return userId;
+        }
+        return nickName;
+    }
+
+    public void setNickName(String nickName) {
+        this.nickName = nickName;
+    }
+
+
+}

+ 53 - 0
webrtc/src/main/java/com/wdkl/core/ui/user/UserListViewModel.java

@@ -0,0 +1,53 @@
+package com.wdkl.core.ui.user;
+
+import android.util.Log;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.alibaba.fastjson.JSON;
+import com.wdkl.core.consts.Urls;
+import com.wdkl.net.HttpRequestPresenter;
+import com.wdkl.net.ICallback;
+
+import java.util.List;
+
+public class UserListViewModel extends ViewModel {
+
+    private MutableLiveData<List<UserBean>> mList;
+
+    public LiveData<List<UserBean>> getUserList() {
+        if (mList == null) {
+            mList = new MutableLiveData<>();
+            loadUsers();
+        }
+        return mList;
+    }
+
+
+    // 获取远程用户列表
+    public void loadUsers() {
+        Thread thread = new Thread(() -> {
+            String url = Urls.getUserList();
+            HttpRequestPresenter.getInstance()
+                    .get(url, null, new ICallback() {
+                        @Override
+                        public void onSuccess(String result) {
+                            Log.d("dds_test", result);
+                            List<UserBean> userBeans = JSON.parseArray(result, UserBean.class);
+                            mList.postValue(userBeans);
+                        }
+
+                        @Override
+                        public void onFailure(int code, Throwable t) {
+                            Log.d("dds_test", "code:" + code + ",msg:" + t.toString());
+                        }
+                    });
+        });
+        thread.start();
+
+
+    }
+
+}

+ 131 - 0
webrtc/src/main/java/com/wdkl/core/util/ActivityStackManager.java

@@ -0,0 +1,131 @@
+package com.wdkl.core.util;
+
+import android.app.Activity;
+import android.app.Application;
+import android.util.Log;
+
+import androidx.collection.ArrayMap;
+
+/**
+ * 应用程序Activity管理类,用于Activity管理和应用程序退出
+ *
+ * @author gong
+ */
+public class ActivityStackManager {
+    private static final String TAG = "ActivityStackManager";
+    private static volatile ActivityStackManager sInstance;
+
+    private final ArrayMap<String, Activity> mActivitySet = new ArrayMap<>();
+
+    /**
+     * 当前 Activity 对象标记
+     */
+    private String mCurrentTag;
+
+    private ActivityStackManager() {
+    }
+
+    public static ActivityStackManager getInstance() {
+        // 加入双重校验锁
+        if (sInstance == null) {
+            synchronized (ActivityStackManager.class) {
+                if (sInstance == null) {
+                    sInstance = new ActivityStackManager();
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    /**
+     * 获取 Application 对象
+     */
+    public Application getApplication() {
+        return getTopActivity().getApplication();
+    }
+
+    /**
+     * 获取栈顶的 Activity
+     */
+    public Activity getTopActivity() {
+        return mActivitySet.get(mCurrentTag);
+    }
+
+    /**
+     * 销毁所有的 Activity
+     */
+    public void finishAllActivities() {
+        finishAllActivities((Class<? extends Activity>) null);
+    }
+
+
+    /**
+     * 获取栈底部的Activity
+     */
+    public Activity getBottomActivity() {
+        Log.d(TAG, "getBottomActivity mActivitySet.size() = " + mActivitySet.size());
+        if (mActivitySet.size() > 0) {
+            return mActivitySet.get(mActivitySet.keyAt(0));
+        } else {
+            return getTopActivity();
+        }
+
+    }
+    /**
+     * 销毁所有的 Activity,除这些 Class 之外的 Activity
+     */
+    @SafeVarargs
+    public final void finishAllActivities(Class<? extends Activity>... classArray) {
+        String[] keys = mActivitySet.keySet().toArray(new String[]{});
+        for (String key : keys) {
+            Activity activity = mActivitySet.get(key);
+            if (activity != null && !activity.isFinishing()) {
+                boolean whiteClazz = false;
+                if (classArray != null) {
+                    for (Class<? extends Activity> clazz : classArray) {
+                        if (activity.getClass() == clazz) {
+                            whiteClazz = true;
+                        }
+                    }
+                }
+                // 如果不是白名单上面的 Activity 就销毁掉
+                if (!whiteClazz) {
+                    activity.finish();
+                    mActivitySet.remove(key);
+                }
+            }
+        }
+    }
+
+    /**
+     * Activity 同名方法回调
+     */
+    public void onCreated(Activity activity) {
+        mCurrentTag = getObjectTag(activity);
+        mActivitySet.put(getObjectTag(activity), activity);
+    }
+
+    /**
+     * Activity 同名方法回调
+     */
+    public void onDestroyed(Activity activity) {
+        mActivitySet.remove(getObjectTag(activity));
+        // 如果当前的 Activity 是最后一个的话
+        if (getObjectTag(activity).equals(mCurrentTag)) {
+            // 清除当前标记
+            mCurrentTag = null;
+        }
+        if (mActivitySet.size() != 0) {
+            mCurrentTag = mActivitySet.keyAt(mActivitySet.size() - 1);
+        }
+    }
+
+    /**
+     * 获取一个对象的独立无二的标记
+     */
+    private static String getObjectTag(Object object) {
+        // 对象所在的包名 + 对象的内存地址
+        return object.getClass().getName() + Integer.toHexString(object.hashCode());
+    }
+
+}

+ 58 - 0
webrtc/src/main/java/com/wdkl/core/util/CrashHandler.java

@@ -0,0 +1,58 @@
+package com.wdkl.core.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.SkyEngineKit;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+
+public class CrashHandler implements Thread.UncaughtExceptionHandler {
+    private static final String TAG = "MyUncaughtExceptionHand";
+
+    private Context appContext;
+
+    public CrashHandler(Context context) {
+        appContext = context;
+    }
+
+    @Override
+    public void uncaughtException(@NotNull Thread thread, @NotNull Throwable ex) {
+        SkyEngineKit gEngineKit = SkyEngineKit.Instance();
+        CallSession session = gEngineKit.getCurrentSession();
+
+        Log.d(TAG, "uncaughtException session = " + session);
+        if (session != null) {
+            gEngineKit.endCall();
+        } else {
+            //gEngineKit.sendDisconnected(App.getInstance().getRoomId(), App.getInstance().getOtherUserId(),true);
+        }
+        final Writer result = new StringWriter();
+        final PrintWriter printWriter = new PrintWriter(result);
+        //如果异常时在AsyncTask里面的后台线程抛出的
+        //那么实际的异常仍然可以通过getCause获得
+        Throwable cause = ex;
+        while (null != cause) {
+            cause.printStackTrace(printWriter);
+            cause = cause.getCause();
+        }
+        //stacktraceAsString就是获取的carsh堆栈信息
+        final String stacktraceAsString = result.toString();
+        printWriter.close();
+        restartApp();
+    }
+
+    private void restartApp() {
+        /*Intent i = new Intent(appContext, LauncherActivity.class);
+        i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+        appContext.startActivity(i);*/
+    }
+
+
+}

+ 127 - 0
webrtc/src/main/java/com/wdkl/core/util/OSUtils.java

@@ -0,0 +1,127 @@
+package com.wdkl.core.util;
+
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * Created by rhm on 2018/1/12.
+ */
+
+public class OSUtils {
+
+private static final String TAG = "Rom";
+
+    public static final String ROM_MIUI = "MIUI";
+    public static final String ROM_EMUI = "EMUI";
+    public static final String ROM_FLYME = "FLYME";
+    public static final String ROM_OPPO = "OPPO";
+    public static final String ROM_SMARTISAN = "SMARTISAN";
+    public static final String ROM_VIVO = "VIVO";
+    public static final String ROM_QIKU = "QIKU";
+
+    private static final String KEY_VERSION_MIUI = "ro.miui.ui.version.name";
+    private static final String KEY_VERSION_EMUI = "ro.build.version.emui";
+    private static final String KEY_VERSION_OPPO = "ro.build.version.opporom";
+    private static final String KEY_VERSION_SMARTISAN = "ro.smartisan.version";
+    private static final String KEY_VERSION_VIVO = "ro.vivo.os.version";
+
+    private static String sName;
+    private static String sVersion;
+
+    public static boolean isEmui() {
+        return check(ROM_EMUI);
+    }
+
+    public static boolean isMiui() {
+        return check(ROM_MIUI);
+    }
+
+    public static boolean isVivo() {
+        return check(ROM_VIVO);
+    }
+
+    public static boolean isOppo() {
+        return check(ROM_OPPO);
+    }
+
+    public static boolean isFlyme() {
+        return check(ROM_FLYME);
+    }
+
+    public static boolean is360() {
+        return check(ROM_QIKU) || check("360");
+    }
+
+    public static boolean isSmartisan() {
+        return check(ROM_SMARTISAN);
+    }
+
+    public static String getName() {
+        if (sName == null) {
+            check("");
+        }
+        return sName;
+    }
+
+    public static String getVersion() {
+        if (sVersion == null) {
+            check("");
+        }
+        return sVersion;
+    }
+
+    public static boolean check(String rom) {
+        if (sName != null) {
+            return sName.equals(rom);
+        }
+
+        if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_MIUI))) {
+            sName = ROM_MIUI;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_EMUI))) {
+            sName = ROM_EMUI;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_OPPO))) {
+            sName = ROM_OPPO;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_VIVO))) {
+            sName = ROM_VIVO;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_SMARTISAN))) {
+            sName = ROM_SMARTISAN;
+        } else {
+            sVersion = Build.DISPLAY;
+            if (sVersion.toUpperCase().contains(ROM_FLYME)) {
+                sName = ROM_FLYME;
+            } else {
+                sVersion = Build.UNKNOWN;
+                sName = Build.MANUFACTURER.toUpperCase();
+            }
+        }
+        return sName.equals(rom);
+    }
+
+    public static String getProp(String name) {
+        String line = null;
+        BufferedReader input = null;
+        try {
+            Process p = Runtime.getRuntime().exec("getprop " + name);
+            input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
+            line = input.readLine();
+            input.close();
+        } catch (IOException ex) {
+            Log.e(TAG, "Unable to read prop " + name, ex);
+            return null;
+        } finally {
+            if (input != null) {
+                try {
+                    input.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return line;
+    }
+}

+ 25 - 0
webrtc/src/main/java/com/wdkl/core/util/StringUtil.java

@@ -0,0 +1,25 @@
+package com.wdkl.core.util;
+
+import java.util.List;
+
+/**
+ * Created by dds on 2020/5/4.
+ * ddssingsong@163.com
+ */
+public class StringUtil {
+
+    public static String listToString(List<String> mList) {
+        final String SEPARATOR = ",";
+        StringBuilder sb = new StringBuilder();
+        String convertedListStr;
+        if (null != mList && mList.size() > 0) {
+            for (String item : mList) {
+                sb.append(item);
+                sb.append(SEPARATOR);
+            }
+            convertedListStr = sb.toString();
+            convertedListStr = convertedListStr.substring(0, convertedListStr.length() - SEPARATOR.length());
+            return convertedListStr;
+        } else return "";
+    }
+}

+ 52 - 0
webrtc/src/main/java/com/wdkl/core/util/Utils.java

@@ -0,0 +1,52 @@
+package com.wdkl.core.util;
+
+import android.app.ActivityManager;
+import android.app.Application;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.os.Build;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import java.util.List;
+
+public class Utils {
+    //设置界面全屏
+    public static void setFullScreenWindowLayout(Window window) {
+        window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.
+                SYSTEM_UI_FLAG_LAYOUT_STABLE);
+        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            window.setStatusBarColor(Color.TRANSPARENT);
+        }
+        //设置页面全屏显示
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            WindowManager.LayoutParams layoutParams = window.getAttributes();
+            layoutParams.layoutInDisplayCutoutMode =
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+            window.setAttributes(layoutParams);
+        }
+    }
+
+    public static int getStatusBarHeight(Context context) {
+        Resources resources = context.getResources();
+        int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
+        return resources.getDimensionPixelSize(resourceId);
+    }
+
+
+    public static boolean isAppRunningForeground(Context context) {
+        ActivityManager activityManager =
+                (ActivityManager) context.getSystemService(Application.ACTIVITY_SERVICE);
+        List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = activityManager.getRunningAppProcesses();
+        for (ActivityManager.RunningAppProcessInfo appProcessInfo : runningAppProcesses) {
+            if (appProcessInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
+                if (appProcessInfo.processName.equals(context.getApplicationInfo().processName))
+                    return true;
+            }
+        }
+        return false;
+    }
+}

+ 187 - 0
webrtc/src/main/java/com/wdkl/core/voip/AsyncPlayer.java

@@ -0,0 +1,187 @@
+package com.wdkl.core.voip;
+
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.LinkedList;
+
+/**
+ * 响铃相关类
+ */
+public class AsyncPlayer {
+    private static final int PLAY = 1;
+    private static final int STOP = 2;
+    private AudioManager audioManager;
+
+    private static final class Command {
+        int code;
+        Context context;
+        int resId;
+        boolean looping;
+        int stream;
+        long requestTime;
+
+        public String toString() {
+            return "{ code=" + code + " looping=" + looping + " stream=" + stream + " resId=" + resId + " }";
+        }
+    }
+
+    private final LinkedList mCmdQueue = new LinkedList();
+
+    private void startSound(Command cmd) {
+
+        try {
+            //MediaPlayer player = new MediaPlayer();
+            MediaPlayer player = MediaPlayer.create(cmd.context, cmd.resId);
+            player.setAudioStreamType(cmd.stream);
+            //player.setDataSource(cmd.context, cmd.uri);
+            player.setLooping(cmd.looping);
+            //player.prepare();
+            player.start();
+            if (mPlayer != null) {
+                mPlayer.release();
+            }
+            mPlayer = player;
+            Log.w(mTag, "start sound " + cmd.resId);
+        } catch (Exception e) {
+            Log.w(mTag, "error loading sound for " + cmd.resId, e);
+        }
+    }
+
+    private final class Thread extends java.lang.Thread {
+        Thread() {
+            super("AsyncPlayer-" + mTag);
+        }
+
+        public void run() {
+            while (true) {
+                Command cmd = null;
+
+                synchronized (mCmdQueue) {
+
+                    cmd = (Command) mCmdQueue.removeFirst();
+                }
+
+                switch (cmd.code) {
+                    case PLAY:
+                        startSound(cmd);
+                        break;
+                    case STOP:
+
+                        if (mPlayer != null) {
+                            mPlayer.stop();
+                            mPlayer.release();
+                            mPlayer = null;
+                        } else {
+                            Log.w(mTag, "STOP command without a player");
+                        }
+                        break;
+                }
+
+                synchronized (mCmdQueue) {
+                    if (mCmdQueue.size() == 0) {
+
+                        mThread = null;
+                        releaseWakeLock();
+                        return;
+                    }
+                }
+            }
+        }
+    }
+
+    private String mTag;
+    private Thread mThread;
+    private MediaPlayer mPlayer;
+    private PowerManager.WakeLock mWakeLock;
+
+    private int mState = STOP;
+
+    public AsyncPlayer(String tag) {
+        if (tag != null) {
+            mTag = tag;
+        } else {
+            mTag = "AsyncPlayer";
+        }
+    }
+
+    public void play(Context context, int res, boolean looping, int stream) {
+        Command cmd = new Command();
+        cmd.requestTime = SystemClock.uptimeMillis();
+        cmd.code = PLAY;
+        cmd.context = context;
+        cmd.resId = res;
+        cmd.looping = looping;
+        cmd.stream = stream;
+        synchronized (mCmdQueue) {
+            enqueueLocked(cmd);
+            mState = PLAY;
+        }
+    }
+
+    public void stop() {
+        synchronized (mCmdQueue) {
+            if (mState != STOP) {
+                Command cmd = new Command();
+                cmd.requestTime = SystemClock.uptimeMillis();
+                cmd.code = STOP;
+                enqueueLocked(cmd);
+                mState = STOP;
+            }
+        }
+    }
+
+    private void enqueueLocked(Command cmd) {
+        mCmdQueue.add(cmd);
+        if (mThread == null) {
+            acquireWakeLock();
+            mThread = new Thread();
+            mThread.start();
+        }
+    }
+
+    public void setUsesWakeLock(Context context) {
+        if (mWakeLock != null || mThread != null) {
+            throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock + " mThread=" + mThread);
+        }
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
+    }
+
+    private void acquireWakeLock() {
+        if (mWakeLock != null) {
+            mWakeLock.acquire();
+        }
+    }
+
+    private void releaseWakeLock() {
+        if (mWakeLock != null) {
+            mWakeLock.release();
+        }
+    }
+
+    private boolean isHeadphonesPlugged(Context context) {
+        if (audioManager == null) {
+            audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        }
+        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
+            AudioDeviceInfo[] audioDevices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL);
+            for (AudioDeviceInfo deviceInfo : audioDevices) {
+                if (deviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
+                        || deviceInfo.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
+                    return true;
+                }
+            }
+            return false;
+        } else {
+            return audioManager.isWiredHeadsetOn();
+        }
+    }
+}

+ 129 - 0
webrtc/src/main/java/com/wdkl/core/voip/CallForegroundNotification.java

@@ -0,0 +1,129 @@
+package com.wdkl.core.voip;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.app.NotificationCompat;
+
+import com.wdkl.core.util.ActivityStackManager;
+import com.wdkl.webrtc.R;
+
+import java.util.Random;
+
+/**
+ * <pre>
+ *     author : Jasper
+ *     e-mail : 229605030@qq.com
+ *     time   : 2021/02/01
+ *     desc   :
+ * </pre>
+ */
+public class CallForegroundNotification extends ContextWrapper {
+    private static final String TAG = "CallForegroundNotificat";
+    private static final String id = "channel1";
+    private static final String name = "voip";
+    private NotificationManager manager;
+
+    public CallForegroundNotification(Context base) {
+        super(base);
+        manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+    }
+
+    @RequiresApi(api = 26)
+    public void createNotificationChannel() {
+        NotificationChannel channel = new NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH);
+        channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
+        manager.createNotificationChannel(channel);
+    }
+
+    public void sendRequestIncomingPermissionsNotification(
+            Context context, String room, String userList, String inviteId, String inviteUserName, Boolean isAudioOnly
+    ) {
+        clearAllNotification();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            createNotificationChannel();
+        }
+        //发送广播,调起接听界面
+        Intent intent = new Intent(context, ActivityStackManager.getInstance().getBottomActivity().getClass()); //栈底是MainActivity
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra("room", room);
+        intent.putExtra("isFromCall", true);
+        intent.putExtra("audioOnly", isAudioOnly);
+        intent.putExtra("inviteUserName", inviteUserName);
+        intent.putExtra("inviteId", inviteId);
+        intent.putExtra("userList", userList);
+        PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, new Random().nextInt(100), intent, PendingIntent.FLAG_UPDATE_CURRENT);
+        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, id)
+                .setSmallIcon(R.mipmap.ic_launcher)
+                .setContentTitle(getResources().getString(R.string.app_name))
+                .setTicker(
+                        "您收到" + inviteUserName + "的来电邀请,请允许"
+                                + (isAudioOnly ? "录音" :
+                                "录音和相机") + "权限来通话"
+                )
+                .setContentText("您收到" + inviteUserName + "的来电邀请,请允许"
+                        + (isAudioOnly ? "录音" :
+                        "录音和相机") + "权限来通话"
+                )
+                .setAutoCancel(true)
+                .setDefaults(Notification.DEFAULT_ALL)
+                .setPriority(NotificationCompat.PRIORITY_MAX)
+                .setCategory(Notification.CATEGORY_CALL);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            notificationBuilder.setFullScreenIntent(fullScreenPendingIntent, true);
+        } else {
+            notificationBuilder.setContentIntent(fullScreenPendingIntent);
+        }
+//
+        manager.notify(10086, notificationBuilder.build());
+    }
+
+    public void sendIncomingCallNotification(
+            Context context, String targetId, Boolean isOutgoing, String inviteUserName,
+            Boolean isAudioOnly, Boolean isClearTop
+    ) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            clearAllNotification();
+            createNotificationChannel();
+            Notification notification = getChannelNotificationQ(context, targetId, isOutgoing, inviteUserName, isAudioOnly, isClearTop);
+            manager.notify(10086, notification);
+        }
+    }
+
+    private void clearAllNotification() {
+        manager.cancelAll();
+    }
+
+    private Notification getChannelNotificationQ(
+            Context context, String targetId, Boolean isOutgoing, String inviteUserName,
+            Boolean isAudioOnly, Boolean isClearTop
+    ) {
+        Intent fullScreenIntent = CallSingleActivity.getCallIntent(context, targetId, isOutgoing, inviteUserName, isAudioOnly, isClearTop);
+        fullScreenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        PendingIntent fullScreenPendingIntent = PendingIntent.getActivity(this, new Random().nextInt(100), fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, id)
+                .setSmallIcon(R.mipmap.ic_launcher)
+                .setContentTitle(getResources().getString(R.string.app_name))
+                .setTicker(inviteUserName + "来电")
+                .setContentText(inviteUserName + "来电")
+                .setAutoCancel(true)
+                .setDefaults(Notification.DEFAULT_ALL)
+                .setPriority(NotificationCompat.PRIORITY_MAX)
+                .setCategory(Notification.CATEGORY_CALL)
+                .setFullScreenIntent(fullScreenPendingIntent, true);
+
+        Log.d(TAG, "getChannelNotificationQ");
+        return notificationBuilder.build();
+    }
+
+
+}

+ 198 - 0
webrtc/src/main/java/com/wdkl/core/voip/CallMultiActivity.java

@@ -0,0 +1,198 @@
+package com.wdkl.core.voip;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.ImageView;
+
+import com.wdkl.core.base.BaseActivity;
+import com.wdkl.permission.Permissions;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.skywebrtc.except.NotInitializedException;
+import com.wdkl.webrtc.R;
+
+import java.util.UUID;
+
+/**
+ * Created by dds on 2018/7/26.
+ * 多人通话界面
+ */
+public class CallMultiActivity extends BaseActivity implements CallSession.CallSessionCallback, View.OnClickListener {
+    private SkyEngineKit gEngineKit;
+    private final Handler handler = new Handler(Looper.getMainLooper());
+    private ImageView meetingHangupImageView;
+    private CallSession.CallSessionCallback currentFragment;
+    public static final String EXTRA_MO = "isOutGoing";
+    private boolean isOutgoing;
+
+
+    public static void openActivity(Activity activity, String room, boolean isOutgoing) {
+        Intent intent = new Intent(activity, CallMultiActivity.class);
+        intent.putExtra("room", room);
+        intent.putExtra(EXTRA_MO, isOutgoing);
+        activity.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN |
+                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
+                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
+                WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+        getWindow().getDecorView().setSystemUiVisibility(getSystemUiVisibility());
+        setContentView(R.layout.activity_multi_call);
+        initView();
+        initListener();
+        initData();
+    }
+
+
+    private void initView() {
+        meetingHangupImageView = findViewById(R.id.meetingHangupImageView);
+        Fragment fragment = new FragmentMeeting();
+        FragmentManager fragmentManager = getSupportFragmentManager();
+        fragmentManager.beginTransaction()
+                .add(R.id.meeting_container, fragment)
+                .commit();
+        currentFragment = (CallSession.CallSessionCallback) fragment;
+    }
+
+    private void initListener() {
+        meetingHangupImageView.setOnClickListener(this);
+    }
+
+    private void initData() {
+        Intent intent = getIntent();
+        String room = intent.getStringExtra("room");
+        isOutgoing = intent.getBooleanExtra(EXTRA_MO, false);
+        try {
+            gEngineKit = SkyEngineKit.Instance();
+        } catch (NotInitializedException e) {
+            finish();
+        }
+        String[] per = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
+        Permissions.request(this, per, integer -> {
+            if (integer == 0) {
+                // 权限同意
+                init(room, isOutgoing);
+            } else {
+                // 权限拒绝
+                CallMultiActivity.this.finish();
+            }
+        });
+
+
+    }
+
+    private void init(String room, boolean isOutgoing) {
+        SkyEngineKit.init(new VoipEvent());
+        if (isOutgoing) {
+            // 创建一个房间并进入
+            gEngineKit.createAndJoinRoom(this,
+                    "room-" + UUID.randomUUID().toString().substring(0, 16));
+        } else {
+            // 加入房间
+            gEngineKit.joinRoom(this, room);
+        }
+
+
+        CallSession session = gEngineKit.getCurrentSession();
+        if (session == null) {
+            this.finish();
+        } else {
+            session.setSessionCallback(this);
+
+        }
+
+
+    }
+
+
+    public SkyEngineKit getEngineKit() {
+        return gEngineKit;
+    }
+
+
+    @TargetApi(19)
+    private static int getSystemUiVisibility() {
+        int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+        }
+        return flags;
+    }
+
+
+    //-------------------------------------------------回调相关------------------------------------
+    @Override
+    public void didCallEndWithReason(EnumType.CallEndReason var1) {
+        finish();
+    }
+
+    @Override
+    public void didChangeState(EnumType.CallState callState) {
+        handler.post(() -> currentFragment.didChangeState(callState));
+    }
+
+    @Override
+    public void didChangeMode(boolean var1) {
+        handler.post(() -> currentFragment.didChangeMode(var1));
+    }
+
+    @Override
+    public void didCreateLocalVideoTrack() {
+        handler.post(() -> currentFragment.didCreateLocalVideoTrack());
+    }
+
+    @Override
+    public void didReceiveRemoteVideoTrack(String userId) {
+        handler.post(() -> currentFragment.didReceiveRemoteVideoTrack(userId));
+    }
+
+    @Override
+    public void didUserLeave(String userId) {
+        handler.post(() -> currentFragment.didUserLeave(userId));
+    }
+
+    @Override
+    public void didError(String var1) {
+        finish();
+    }
+
+    @Override
+    public void didDisconnected(String userId) {
+
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v.getId() == R.id.meetingHangupImageView) {
+            handleHangup();
+        }
+
+        /*switch (v.getId()) {
+            case R.id.meetingHangupImageView:
+                handleHangup();
+                break;
+        }*/
+
+    }
+
+    // 处理挂断事件
+    private void handleHangup() {
+        SkyEngineKit.Instance().leaveRoom();
+        this.finish();
+    }
+}

+ 380 - 0
webrtc/src/main/java/com/wdkl/core/voip/CallSingleActivity.java

@@ -0,0 +1,380 @@
+package com.wdkl.core.voip;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.v4.app.FragmentManager;
+import android.util.Log;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+
+import com.wdkl.core.base.BaseActivity;
+import com.wdkl.permission.Permissions;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.skywebrtc.except.NotInitializedException;
+import com.wdkl.webrtc.R;
+
+import java.util.UUID;
+
+
+/**
+ * Created by dds on 2018/7/26.
+ * 单人通话界面
+ */
+public class CallSingleActivity extends BaseActivity implements CallSession.CallSessionCallback {
+
+    public static final String EXTRA_TARGET = "targetId";
+    public static final String EXTRA_MO = "isOutGoing";
+    public static final String EXTRA_AUDIO_ONLY = "audioOnly";
+    public static final String EXTRA_USER_NAME = "userName";
+    public static final String EXTRA_FROM_FLOATING_VIEW = "fromFloatingView";
+    private static final String TAG = "CallSingleActivity";
+
+    private Handler handler = new Handler(Looper.getMainLooper());
+    private boolean isOutgoing;
+    private String targetId;
+    boolean isAudioOnly;
+    private boolean isFromFloatingView;
+
+    private SkyEngineKit gEngineKit;
+
+    private SingleCallFragment currentFragment;
+    private String room;
+
+    public static Intent getCallIntent(Context context, String targetId, boolean isOutgoing, String inviteUserName,
+                                       boolean isAudioOnly, boolean isClearTop) {
+        Intent voip = new Intent(context, CallSingleActivity.class);
+        voip.putExtra(CallSingleActivity.EXTRA_MO, isOutgoing);
+        voip.putExtra(CallSingleActivity.EXTRA_TARGET, targetId);
+        voip.putExtra(CallSingleActivity.EXTRA_USER_NAME, inviteUserName);
+        voip.putExtra(CallSingleActivity.EXTRA_AUDIO_ONLY, isAudioOnly);
+        voip.putExtra(CallSingleActivity.EXTRA_FROM_FLOATING_VIEW, false);
+        if (isClearTop) {
+            voip.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        }
+        return voip;
+    }
+
+
+    public static void openActivity(Context context, String targetId, boolean isOutgoing, String inviteUserName,
+                                    boolean isAudioOnly, boolean isClearTop) {
+        Intent intent = getCallIntent(context, targetId, isOutgoing, inviteUserName, isAudioOnly, isClearTop);
+        //if (context instanceof Activity) {
+        //    context.startActivity(intent);
+        //} else {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            context.startActivity(intent);
+        //}
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setStatusBarOrScreenStatus(this);
+        setContentView(R.layout.activity_single_call);
+
+        try {
+            gEngineKit = SkyEngineKit.Instance();
+        } catch (NotInitializedException e) {
+            SkyEngineKit.init(new VoipEvent()); //重新初始化
+            try {
+                gEngineKit = SkyEngineKit.Instance();
+            } catch (NotInitializedException ex) {
+                finish();
+            }
+        }
+        final Intent intent = getIntent();
+        targetId = intent.getStringExtra(EXTRA_TARGET);
+        isFromFloatingView = intent.getBooleanExtra(EXTRA_FROM_FLOATING_VIEW, false);
+        isOutgoing = intent.getBooleanExtra(EXTRA_MO, false);
+        isAudioOnly = intent.getBooleanExtra(EXTRA_AUDIO_ONLY, false);
+
+        if (isFromFloatingView) {
+            //Intent serviceIntent = new Intent(this, FloatingVoipService.class);
+            //stopService(serviceIntent);
+            //init(targetId, false, isAudioOnly, false);
+        } else {
+            // 权限检测
+            String[] per;
+            if (isAudioOnly) {
+                per = new String[]{Manifest.permission.RECORD_AUDIO};
+            } else {
+                per = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
+            }
+            Permissions.request(this, per, integer -> {
+                Log.d(TAG, "Permissions.request integer = " + integer);
+                if (integer == 0) {
+                    // 权限同意
+                    init(targetId, isOutgoing, isAudioOnly);
+                } else {
+                    Toast.makeText(this, "权限被拒绝", Toast.LENGTH_SHORT).show();
+                    // 权限拒绝
+                    finish();
+                }
+            });
+        }
+
+
+    }
+
+    @Override
+    public void onBackPressed() {
+        //通话时不能按返回键,跟微信同现象,只能挂断结束或者接听
+//        super.onBackPressed();
+//        if (currentFragment != null) {
+//            if (currentFragment instanceof FragmentAudio) {
+//                ((FragmentAudio) currentFragment).onBackPressed();
+//            } else if (currentFragment instanceof FragmentVideo) {
+//                ((FragmentVideo) currentFragment).onBackPressed();
+//            }
+//        }
+
+    }
+
+    private void init(String targetId, boolean outgoing, boolean audioOnly) {
+        SingleCallFragment fragment;
+        if (audioOnly) {
+            fragment = new FragmentAudio();
+        } else {
+            fragment = new FragmentVideo();
+        }
+        FragmentManager fragmentManager = getSupportFragmentManager();
+        currentFragment = fragment;
+        fragmentManager.beginTransaction()
+                    .replace(android.R.id.content, fragment)
+                    .commit();
+
+        if (outgoing) {
+            // 创建会话
+            room = UUID.randomUUID().toString() + System.currentTimeMillis();
+            boolean b = gEngineKit.startOutCall(getApplicationContext(), room, targetId, audioOnly);
+            if (!b) {
+                finish();
+                return;
+            }
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session == null) {
+                finish();
+            } else {
+                session.setSessionCallback(this);
+            }
+        } else {
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session == null) {
+                finish();
+            } else {
+                /*if (session.isAudioOnly() && !audioOnly) { //这种情况是,对方切换成音频的时候,activity还没启动,这里启动后需要切换一下
+                    isAudioOnly = session.isAudioOnly();
+                    fragment.didChangeMode(true);
+                }*/
+                session.setSessionCallback(this);
+            }
+        }
+
+    }
+
+    private void init(String targetId, boolean outgoing, boolean audioOnly, boolean isReplace) {
+        SingleCallFragment fragment;
+        if (audioOnly) {
+            fragment = new FragmentAudio();
+        } else {
+            fragment = new FragmentVideo();
+        }
+        FragmentManager fragmentManager = getSupportFragmentManager();
+        currentFragment = fragment;
+        if (isReplace) {
+            fragmentManager.beginTransaction()
+                    .replace(android.R.id.content, fragment)
+                    .commit();
+        } else {
+            fragmentManager.beginTransaction()
+                    .add(android.R.id.content, fragment)
+                    .commit();
+        }
+        if (outgoing && !isReplace) {
+            // 创建会话
+            room = UUID.randomUUID().toString() + System.currentTimeMillis();
+            boolean b = gEngineKit.startOutCall(getApplicationContext(), room, targetId, audioOnly);
+            if (!b) {
+                finish();
+                return;
+            }
+            //App.getInstance().setRoomId(room);
+            //App.getInstance().setOtherUserId(targetId);
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session == null) {
+                finish();
+            } else {
+                session.setSessionCallback(this);
+            }
+        } else {
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session == null) {
+                finish();
+            } else {
+                //if (session.isAudioOnly() && !audioOnly) { //这种情况是,对方切换成音频的时候,activity还没启动,这里启动后需要切换一下
+                //    isAudioOnly = session.isAudioOnly();
+                //    fragment.didChangeMode(true);
+                //}
+                session.setSessionCallback(this);
+            }
+        }
+
+    }
+
+    public SkyEngineKit getEngineKit() {
+        return gEngineKit;
+    }
+
+    public boolean isOutgoing() {
+        return isOutgoing;
+    }
+
+
+    public boolean isFromFloatingView() {
+        return isFromFloatingView;
+    }
+
+    // 显示小窗
+    /*public void showFloatingView() {
+        if (!checkOverlayPermission()) {
+            return;
+        }
+        Intent intent = new Intent(this, FloatingVoipService.class);
+        intent.putExtra(EXTRA_TARGET, targetId);
+        intent.putExtra(EXTRA_AUDIO_ONLY, isAudioOnly);
+        intent.putExtra(EXTRA_MO, isOutgoing);
+        startService(intent);
+        finish();
+    }*/
+
+    // 切换到语音通话
+    public void switchAudio() {
+        init(targetId, isOutgoing, true, true);
+    }
+
+    public String getRoomId() {
+        return room;
+    }
+
+    // ======================================界面回调================================
+    @Override
+    public void didCallEndWithReason(EnumType.CallEndReason reason) {
+        //App.getInstance().setOtherUserId("0");
+        //交给fragment去finish
+//        finish();
+        handler.post(() -> currentFragment.didCallEndWithReason(reason));
+    }
+
+    @Override
+    public void didChangeState(EnumType.CallState callState) {
+        if (callState == EnumType.CallState.Connected) {
+            isOutgoing = false;
+        }
+        handler.post(() -> currentFragment.didChangeState(callState));
+    }
+
+    @Override
+    public void didChangeMode(boolean var1) {
+        handler.post(() -> currentFragment.didChangeMode(var1));
+    }
+
+    @Override
+    public void didCreateLocalVideoTrack() {
+        handler.post(() -> currentFragment.didCreateLocalVideoTrack());
+    }
+
+    @Override
+    public void didReceiveRemoteVideoTrack(String userId) {
+        handler.post(() -> currentFragment.didReceiveRemoteVideoTrack(userId));
+    }
+
+    @Override
+    public void didUserLeave(String userId) {
+        handler.post(() -> currentFragment.didUserLeave(userId));
+    }
+
+    @Override
+    public void didError(String var1) {
+        handler.post(() -> currentFragment.didError(var1));
+//        finish();
+    }
+
+    @Override
+    public void didDisconnected(String userId) {
+        handler.post(() -> currentFragment.didDisconnected(userId));
+    }
+
+
+    // ========================================================================================
+
+    private boolean checkOverlayPermission() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            SettingsCompat.setDrawOverlays(this, true);
+            if (!SettingsCompat.canDrawOverlays(this)) {
+                Toast.makeText(this, "需要悬浮窗权限", Toast.LENGTH_LONG).show();
+                SettingsCompat.manageDrawOverlays(this);
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    @TargetApi(19)
+    private static int getSystemUiVisibility() {
+        int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN |
+                View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+        }
+        return flags;
+    }
+
+    /**
+     * 设置状态栏透明
+     */
+    @TargetApi(19)
+    public void setStatusBarOrScreenStatus(Activity activity) {
+        Window window = activity.getWindow();
+        //全屏+锁屏+常亮显示
+        window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN |
+                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
+                WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED |
+                WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+        window.getDecorView().setSystemUiVisibility(getSystemUiVisibility());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
+            layoutParams.layoutInDisplayCutoutMode =
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+            window.setAttributes(layoutParams);
+        }
+        // 5.0以上系统状态栏透明
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            //清除透明状态栏
+            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+            //设置状态栏颜色必须添加
+            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+            window.setStatusBarColor(Color.TRANSPARENT);//设置透明
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { //19
+            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+    }
+}

+ 150 - 0
webrtc/src/main/java/com/wdkl/core/voip/FragmentAudio.java

@@ -0,0 +1,150 @@
+package com.wdkl.core.voip;
+
+import android.os.Build;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+import com.blankj.utilcode.util.BarUtils;
+import com.wdkl.core.util.OSUtils;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType.CallState;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.webrtc.R;
+
+/**
+ * Created by dds on 2018/7/26.
+ * android_shuai@163.com
+ * 语音通话控制界面
+ */
+public class FragmentAudio extends SingleCallFragment implements View.OnClickListener {
+    private static final String TAG = "FragmentAudio";
+    private ImageView muteImageView;
+    private ImageView speakerImageView;
+    private boolean micEnabled = false; // 静音
+    private boolean isSpeakerOn = false; // 扬声器
+
+    @Override
+    int getLayout() {
+        return R.layout.fragment_audio;
+    }
+
+    @Override
+    public void initView(View view) {
+        super.initView(view);
+        muteImageView = view.findViewById(R.id.muteImageView);
+        speakerImageView = view.findViewById(R.id.speakerImageView);
+        minimizeImageView.setVisibility(View.GONE);
+        outgoingHangupImageView.setOnClickListener(this);
+        incomingHangupImageView.setOnClickListener(this);
+        minimizeImageView.setOnClickListener(this);
+        muteImageView.setOnClickListener(this);
+        acceptImageView.setOnClickListener(this);
+        speakerImageView.setOnClickListener(this);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || OSUtils.isMiui() || OSUtils.isFlyme()) {
+            lytParent.post(() -> {
+                RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) minimizeImageView.getLayoutParams();
+                params.topMargin = BarUtils.getStatusBarHeight();
+                minimizeImageView.setLayoutParams(params);
+            });
+        }
+    }
+
+    @Override
+    public void init() {
+        super.init();
+        CallSession currentSession = gEngineKit.getCurrentSession();
+        currentState = currentSession.getState();
+        // 如果已经接通
+        if (currentSession != null && currentState == CallState.Connected) {
+            descTextView.setVisibility(View.GONE); // 提示语
+            outgoingActionContainer.setVisibility(View.VISIBLE);
+            durationTextView.setVisibility(View.VISIBLE);
+            //minimizeImageView.setVisibility(View.VISIBLE);
+            startRefreshTime();
+        } else {
+            // 如果未接通
+            if (isOutgoing) {
+                descTextView.setText(R.string.av_waiting);
+                outgoingActionContainer.setVisibility(View.VISIBLE);
+                incomingActionContainer.setVisibility(View.GONE);
+            } else {
+                descTextView.setText(R.string.av_audio_invite);
+                outgoingActionContainer.setVisibility(View.GONE);
+                incomingActionContainer.setVisibility(View.VISIBLE);
+            }
+        }
+    }
+
+    @Override
+    public void didChangeState(CallState state) {
+
+        currentState = state;
+        runOnUiThread(() -> {
+            if (state == CallState.Connected) {
+                handler.removeMessages(WHAT_DELAY_END_CALL);
+                incomingActionContainer.setVisibility(View.GONE);
+                outgoingActionContainer.setVisibility(View.VISIBLE);
+                //minimizeImageView.setVisibility(View.VISIBLE);
+                descTextView.setVisibility(View.GONE);
+
+                startRefreshTime();
+            } else {
+                // do nothing now
+            }
+        });
+    }
+
+    @Override
+    public void onClick(View v) {
+        int id = v.getId();
+        // 接听
+        if (id == R.id.acceptImageView) {
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session != null)
+                Log.d(TAG, "session = " + session + "; session.getState() = " + session.getState());
+            if (session != null && session.getState() == CallState.Incoming) {
+                session.joinHome(session.getRoomId());
+            } else if (session != null) {
+                session.sendRefuse();
+            }
+        }
+        // 挂断电话
+        if (id == R.id.incomingHangupImageView || id == R.id.outgoingHangupImageView) {
+            //App.getInstance().setOtherUserId("0");
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session != null) {
+                SkyEngineKit.Instance().endCall();
+            }
+            //            activity.finish();
+            //再onEvent中结束,防止ChatActivity结束了,消息发送不了
+        }
+        // 静音
+        if (id == R.id.muteImageView) {
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session != null && session.getState() != CallState.Idle) {
+                if (session.toggleMuteAudio(!micEnabled)) {
+                    micEnabled = !micEnabled;
+                }
+                muteImageView.setSelected(micEnabled);
+            }
+        }
+        // 扬声器
+        if (id == R.id.speakerImageView) {
+            CallSession session = gEngineKit.getCurrentSession();
+            if (session != null && session.getState() != CallState.Idle) {
+                if (session.toggleSpeaker(!isSpeakerOn)) {
+                    isSpeakerOn = !isSpeakerOn;
+                }
+                speakerImageView.setSelected(isSpeakerOn);
+            }
+        }
+        // 小窗
+        /*if (id == R.id.minimizeImageView) {
+            if (callSingleActivity != null) {
+                callSingleActivity.showFloatingView();
+            }
+        }*/
+    }
+}

+ 118 - 0
webrtc/src/main/java/com/wdkl/core/voip/FragmentMeeting.java

@@ -0,0 +1,118 @@
+package com.wdkl.core.voip;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.webrtc.R;
+
+/**
+ * Created by dds on 2020/5/24.
+ * ddssingsong@163.com
+ */
+public class FragmentMeeting extends Fragment implements CallSession.CallSessionCallback, View.OnClickListener {
+    private SkyEngineKit gEngineKit;
+    private CallMultiActivity activity;
+    private NineGridView grid_view;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setRetainInstance(true);
+    }
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        activity = (CallMultiActivity) getActivity();
+        if (activity != null) {
+
+        }
+
+    }
+
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
+                             Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.fragment_meeting, container, false);
+        initView(view);
+        init();
+        return view;
+    }
+
+    private void initView(View view) {
+        grid_view = view.findViewById(R.id.grid_view);
+
+    }
+
+
+    private void init() {
+        gEngineKit = activity.getEngineKit();
+    }
+
+    @Override
+    public void onClick(View v) {
+
+    }
+
+
+    @Override
+    public void didCallEndWithReason(EnumType.CallEndReason var1) {
+
+    }
+
+    @Override
+    public void didChangeState(EnumType.CallState var1) {
+
+    }
+
+    @Override
+    public void didChangeMode(boolean isAudioOnly) {
+
+    }
+
+    @Override
+    public void didCreateLocalVideoTrack() {
+        View surfaceView = gEngineKit.getCurrentSession().setupLocalVideo(true);
+        if (surfaceView != null) {
+            CallSession callSession = SkyEngineKit.Instance().getCurrentSession();
+            grid_view.addView(callSession.mMyId, surfaceView);
+
+
+        }
+
+    }
+
+    @Override
+    public void didReceiveRemoteVideoTrack(String userId) {
+        View surfaceView = gEngineKit.getCurrentSession().setupRemoteVideo(userId, true);
+        if (surfaceView != null) {
+            grid_view.addView(userId, surfaceView);
+        }
+    }
+
+    @Override
+    public void didUserLeave(String userId) {
+        grid_view.removeView(userId);
+    }
+
+    @Override
+    public void didError(String error) {
+
+    }
+
+    @Override
+    public void didDisconnected(String userId) {
+
+    }
+
+
+}

+ 317 - 0
webrtc/src/main/java/com/wdkl/core/voip/FragmentVideo.java

@@ -0,0 +1,317 @@
+package com.wdkl.core.voip;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+
+import com.blankj.utilcode.util.BarUtils;
+import com.wdkl.core.util.OSUtils;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType.CallState;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.webrtc.R;
+
+import org.webrtc.SurfaceViewRenderer;
+
+/**
+ * Created by dds on 2018/7/26.
+ * android_shuai@163.com
+ * 视频通话控制界面
+ */
+public class FragmentVideo extends SingleCallFragment implements View.OnClickListener {
+    private static final String TAG = "FragmentVideo";
+    private ImageView outgoingAudioOnlyImageView;
+    private LinearLayout audioLayout;
+    private ImageView incomingAudioOnlyImageView;
+    private LinearLayout hangupLinearLayout;
+    private LinearLayout acceptLinearLayout;
+    private ImageView connectedAudioOnlyImageView;
+    private ImageView connectedHangupImageView;
+    private ImageView switchCameraImageView;
+    private FrameLayout fullscreenRenderer;
+    private FrameLayout pipRenderer;
+    private LinearLayout inviteeInfoContainer;
+    private boolean isFromFloatingView = false;
+    private SurfaceViewRenderer localSurfaceView;
+    private SurfaceViewRenderer remoteSurfaceView;
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        if (callSingleActivity != null) {
+            isFromFloatingView = callSingleActivity.isFromFloatingView();
+        }
+    }
+
+    @Override
+    int getLayout() {
+        return R.layout.fragment_video;
+    }
+
+    @Override
+    public void initView(View view) {
+        super.initView(view);
+        fullscreenRenderer = view.findViewById(R.id.fullscreen_video_view);
+        pipRenderer = view.findViewById(R.id.pip_video_view);
+        inviteeInfoContainer = view.findViewById(R.id.inviteeInfoContainer);
+        outgoingAudioOnlyImageView = view.findViewById(R.id.outgoingAudioOnlyImageView);
+        audioLayout = view.findViewById(R.id.audioLayout);
+        incomingAudioOnlyImageView = view.findViewById(R.id.incomingAudioOnlyImageView);
+        hangupLinearLayout = view.findViewById(R.id.hangupLinearLayout);
+        acceptLinearLayout = view.findViewById(R.id.acceptLinearLayout);
+        connectedAudioOnlyImageView = view.findViewById(R.id.connectedAudioOnlyImageView);
+        connectedHangupImageView = view.findViewById(R.id.connectedHangupImageView);
+        switchCameraImageView = view.findViewById(R.id.switchCameraImageView);
+        outgoingHangupImageView.setOnClickListener(this);
+        incomingHangupImageView.setOnClickListener(this);
+        minimizeImageView.setOnClickListener(this);
+        connectedHangupImageView.setOnClickListener(this);
+        acceptImageView.setOnClickListener(this);
+        switchCameraImageView.setOnClickListener(this);
+        pipRenderer.setOnClickListener(this);
+        outgoingAudioOnlyImageView.setOnClickListener(this);
+        incomingAudioOnlyImageView.setOnClickListener(this);
+        connectedAudioOnlyImageView.setOnClickListener(this);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M || OSUtils.isMiui() || OSUtils.isFlyme()) {
+            lytParent.post(() -> {
+                RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) inviteeInfoContainer.getLayoutParams();
+                params.topMargin = (int) (BarUtils.getStatusBarHeight() * 1.2);
+                inviteeInfoContainer.setLayoutParams(params);
+                RelativeLayout.LayoutParams params1 = (RelativeLayout.LayoutParams) minimizeImageView.getLayoutParams();
+                params1.topMargin = BarUtils.getStatusBarHeight();
+                minimizeImageView.setLayoutParams(params1);
+            });
+
+            pipRenderer.post(() -> {
+                FrameLayout.LayoutParams params2 = (FrameLayout.LayoutParams) pipRenderer.getLayoutParams();
+                params2.topMargin = (int) (BarUtils.getStatusBarHeight() * 1.2);
+                pipRenderer.setLayoutParams(params2);
+            });
+        }
+//        if(isOutgoing){ //测试崩溃对方是否会停止
+//            lytParent.postDelayed(() -> {
+//                int i = 1 / 0;
+//            }, 10000);
+//        }
+
+    }
+
+
+    @Override
+    public void init() {
+        super.init();
+        CallSession session = gEngineKit.getCurrentSession();
+        if (session != null) {
+            currentState = session.getState();
+        }
+        if (session == null || CallState.Idle == session.getState()) {
+            if (callSingleActivity != null) {
+                callSingleActivity.finish();
+            }
+        } else if (CallState.Connected == session.getState()) {
+            incomingActionContainer.setVisibility(View.GONE);
+            outgoingActionContainer.setVisibility(View.GONE);
+            connectedActionContainer.setVisibility(View.VISIBLE);
+            inviteeInfoContainer.setVisibility(View.GONE);
+            //minimizeImageView.setVisibility(View.VISIBLE);
+            startRefreshTime();
+        } else {
+            if (isOutgoing) {
+                incomingActionContainer.setVisibility(View.GONE);
+                outgoingActionContainer.setVisibility(View.VISIBLE);
+                connectedActionContainer.setVisibility(View.GONE);
+                descTextView.setText(R.string.av_waiting);
+            } else {
+                incomingActionContainer.setVisibility(View.VISIBLE);
+                outgoingActionContainer.setVisibility(View.GONE);
+                connectedActionContainer.setVisibility(View.GONE);
+                descTextView.setText(R.string.av_video_invite);
+                if (currentState == CallState.Incoming) {
+                    View surfaceView = gEngineKit.getCurrentSession().setupLocalVideo(false);
+                    Log.d(TAG, "init surfaceView != null is " + (surfaceView != null) + "; isOutgoing = " + isOutgoing + "; currentState = " + currentState);
+                    if (surfaceView != null) {
+                        localSurfaceView = (SurfaceViewRenderer) surfaceView;
+                        localSurfaceView.setZOrderMediaOverlay(false);
+                        fullscreenRenderer.addView(localSurfaceView);
+                    }
+                }
+            }
+        }
+        if (isFromFloatingView) {
+            didCreateLocalVideoTrack();
+            if (session != null) {
+                didReceiveRemoteVideoTrack(session.mTargetId);
+            }
+        }
+    }
+
+    @Override
+    public void didChangeState(CallState state) {
+        currentState = state;
+        Log.d(TAG, "didChangeState, state = " + state);
+        runOnUiThread(() -> {
+            if (state == CallState.Connected) {
+                handler.removeMessages(WHAT_DELAY_END_CALL);
+                incomingActionContainer.setVisibility(View.GONE);
+                outgoingActionContainer.setVisibility(View.GONE);
+                connectedActionContainer.setVisibility(View.VISIBLE);
+                inviteeInfoContainer.setVisibility(View.GONE);
+                descTextView.setVisibility(View.GONE);
+                //minimizeImageView.setVisibility(View.VISIBLE);
+                // 开启计时器
+                startRefreshTime();
+            } else {
+                // do nothing now
+            }
+        });
+    }
+
+    @Override
+    public void didChangeMode(Boolean isAudio) {
+        runOnUiThread(() -> callSingleActivity.switchAudio());
+    }
+
+
+    @Override
+    public void didCreateLocalVideoTrack() {
+        if (localSurfaceView == null) {
+            View surfaceView = gEngineKit.getCurrentSession().setupLocalVideo(true);
+            if (surfaceView != null) {
+                localSurfaceView = (SurfaceViewRenderer) surfaceView;
+            } else {
+                if (callSingleActivity != null) callSingleActivity.finish();
+                return;
+            }
+        } else {
+            localSurfaceView.setZOrderMediaOverlay(true);
+        }
+        Log.d(TAG,
+                "didCreateLocalVideoTrack localSurfaceView != null is " + (localSurfaceView != null) + "; remoteSurfaceView == null = " + (remoteSurfaceView == null)
+        );
+
+        if (localSurfaceView.getParent() != null) {
+            ((ViewGroup) localSurfaceView.getParent()).removeView(localSurfaceView);
+        }
+        if (isOutgoing && remoteSurfaceView == null) {
+            if (fullscreenRenderer != null && fullscreenRenderer.getChildCount() != 0)
+                fullscreenRenderer.removeAllViews();
+            fullscreenRenderer.addView(localSurfaceView);
+        } else {
+            if (pipRenderer.getChildCount() != 0) pipRenderer.removeAllViews();
+            pipRenderer.addView(localSurfaceView);
+        }
+    }
+
+
+    @Override
+    public void didReceiveRemoteVideoTrack(String userId) {
+        pipRenderer.setVisibility(View.VISIBLE);
+        if (localSurfaceView != null) {
+            localSurfaceView.setZOrderMediaOverlay(true);
+            if (isOutgoing) {
+                if (localSurfaceView.getParent() != null) {
+                    ((ViewGroup) localSurfaceView.getParent()).removeView(localSurfaceView);
+                }
+                pipRenderer.addView(localSurfaceView);
+            }
+        }
+
+
+        View surfaceView = gEngineKit.getCurrentSession().setupRemoteVideo(userId, false);
+        Log.d(TAG, "didReceiveRemoteVideoTrack,surfaceView = " + surfaceView);
+        if (surfaceView != null) {
+            fullscreenRenderer.setVisibility(View.VISIBLE);
+            remoteSurfaceView = (SurfaceViewRenderer) surfaceView;
+            fullscreenRenderer.removeAllViews();
+            if (remoteSurfaceView.getParent() != null) {
+                ((ViewGroup) remoteSurfaceView.getParent()).removeView(remoteSurfaceView);
+            }
+            fullscreenRenderer.addView(remoteSurfaceView);
+        }
+    }
+
+    @Override
+    public void didUserLeave(String userId) {
+
+    }
+
+    @Override
+    public void didError(String error) {
+
+    }
+
+
+    @Override
+    public void onClick(View v) {
+        int id = v.getId();
+        // 接听
+        CallSession session = gEngineKit.getCurrentSession();
+        if (id == R.id.acceptImageView) {
+            if (session != null && session.getState() == CallState.Incoming) {
+                session.joinHome(session.getRoomId());
+            } else if (session != null) {
+                if (callSingleActivity != null) {
+                    session.sendRefuse();
+                    callSingleActivity.finish();
+                }
+            }
+        }
+        // 挂断电话
+        if (id == R.id.incomingHangupImageView || id == R.id.outgoingHangupImageView || id == R.id.connectedHangupImageView) {
+            if (session != null) {
+                Log.d(TAG, "endCall");
+                SkyEngineKit.Instance().endCall();
+            }
+            if (callSingleActivity != null) callSingleActivity.finish();
+        }
+
+        // 切换摄像头
+        if (id == R.id.switchCameraImageView) {
+            session.switchCamera();
+        }
+        if (id == R.id.pip_video_view) {
+            boolean isFullScreenRemote = fullscreenRenderer.getChildAt(0) == remoteSurfaceView;
+            fullscreenRenderer.removeAllViews();
+            pipRenderer.removeAllViews();
+            if (isFullScreenRemote) {
+                remoteSurfaceView.setZOrderMediaOverlay(true);
+                pipRenderer.addView(remoteSurfaceView);
+                localSurfaceView.setZOrderMediaOverlay(false);
+                fullscreenRenderer.addView(localSurfaceView);
+            } else {
+                localSurfaceView.setZOrderMediaOverlay(true);
+                pipRenderer.addView(localSurfaceView);
+                remoteSurfaceView.setZOrderMediaOverlay(false);
+                fullscreenRenderer.addView(remoteSurfaceView);
+            }
+        }
+
+        // 切换到语音拨打
+        if (id == R.id.outgoingAudioOnlyImageView || id == R.id.incomingAudioOnlyImageView || id == R.id.connectedAudioOnlyImageView) {
+            if (session != null) {
+                if (callSingleActivity != null) callSingleActivity.isAudioOnly = true;
+                session.switchToAudio();
+            }
+        }
+
+        // 小窗
+        /*if (id == R.id.minimizeImageView) {
+            if (callSingleActivity != null) callSingleActivity.showFloatingView();
+        }*/
+    }
+
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        fullscreenRenderer.removeAllViews();
+        pipRenderer.removeAllViews();
+    }
+}

+ 73 - 0
webrtc/src/main/java/com/wdkl/core/voip/NineGridView.java

@@ -0,0 +1,73 @@
+package com.wdkl.core.voip;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.GridLayout;
+
+import com.wdkl.webrtc.R;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Created by dds on 2020/7/5.
+ * ddssingsong@163.com
+ */
+public class NineGridView extends GridLayout {
+    private Map<String, View> map = new ConcurrentHashMap<>();
+
+    public NineGridView(Context context) {
+        this(context, null);
+    }
+
+    public NineGridView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    private void init() {
+        setColumnCount(3);
+        setRowCount(3);
+    }
+
+    public void addView(String userId, View view) {
+        map.put(userId, view);
+        resetView();
+    }
+
+
+    public void removeView(String userId) {
+        map.remove(userId);
+        resetView();
+    }
+
+    private void resetView() {
+        this.removeAllViews();
+        int i = 0;
+        Iterator<Map.Entry<String, View>> inter = map.entrySet().iterator();
+        while (inter.hasNext()) {
+            Map.Entry<String, View> entry = inter.next();
+            String key = entry.getKey();
+            View view = map.get(key);
+            GridLayout.Spec rowSpec = GridLayout.spec(i / 3, 1f);
+            GridLayout.Spec columnSpec = GridLayout.spec(i % 3, 1f);
+            GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(rowSpec, columnSpec);
+            layoutParams.height = 0;
+            layoutParams.width = 0;
+            if (i / 3 == 0)
+                layoutParams.bottomMargin = getResources().getDimensionPixelSize(R.dimen.dp_2);
+            if (i % 3 == 1) {
+                layoutParams.leftMargin = getResources().getDimensionPixelSize(R.dimen.dp_2);
+                layoutParams.rightMargin = getResources().getDimensionPixelSize(R.dimen.dp_2);
+            }
+            this.addView(view, layoutParams);
+            i++;
+        }
+
+
+    }
+
+
+}

+ 153 - 0
webrtc/src/main/java/com/wdkl/core/voip/RomUtil.java

@@ -0,0 +1,153 @@
+package com.wdkl.core.voip;
+
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * Created by dds on 2018/3/20.
+ */
+
+public class RomUtil {
+    private static final String TAG = "RomUtil";
+
+    public static final String ROM_MIUI = "MIUI";
+    public static final String ROM_EMUI = "EMUI";
+    public static final String ROM_FLYME = "FLYME";
+    public static final String ROM_OPPO = "OPPO";
+    public static final String ROM_SMARTISAN = "SMARTISAN";
+
+    public static final String ROM_VIVO = "VIVO";
+    public static final String ROM_QIKU = "QIKU";
+
+    public static final String ROM_LENOVO = "LENOVO";
+    public static final String ROM_SAMSUNG = "SAMSUNG";
+
+    private static final String KEY_VERSION_MIUI = "ro.miui.ui.version.name";
+    private static final String KEY_VERSION_EMUI = "ro.build.version.emui";
+    private static final String KEY_VERSION_OPPO = "ro.build.version.opporom";
+    private static final String KEY_VERSION_SMARTISAN = "ro.smartisan.version";
+    private static final String KEY_VERSION_VIVO = "ro.vivo.os.version";
+    private static final String KEY_VERSION_GIONEE = "ro.gn.sv.version";
+    private static final String KEY_VERSION_LENOVO = "ro.lenovo.lvp.version";
+    private static final String KEY_VERSION_FLYME = "ro.build.display.id";
+
+
+    private static final String KEY_EMUI_VERSION_CODE = "ro.build.hw_emui_api_level";
+
+    private static final String KEY_MIUI_VERSION_CODE = "ro.miui.ui.version.code";
+    private static final String KEY_MIUI_HANDY_MODE_SF = "ro.miui.has_handy_mode_sf";
+    private static final String KEY_MIUI_REAL_BLUR = "ro.miui.has_real_blur";
+
+    private static final String KEY_FLYME_PUBLISHED = "ro.flyme.published";
+    private static final String KEY_FLYME_FLYME = "ro.meizu.setupwizard.flyme";
+
+    private static final String KEY_FLYME_ICON_FALG = "persist.sys.use.flyme.icon";
+    private static final String KEY_FLYME_SETUP_FALG = "ro.meizu.setupwizard.flyme";
+    private static final String KEY_FLYME_PUBLISH_FALG = "ro.flyme.published";
+
+    private static final String KEY_VIVO_OS_NAME = "ro.vivo.os.name";
+    private static final String KEY_VIVO_OS_VERSION = "ro.vivo.os.version";
+    private static final String KEY_VIVO_ROM_VERSION = "ro.vivo.rom.version";
+
+    public static boolean isEmui() {
+        return check(ROM_EMUI);
+    }
+
+    public static boolean isMiui() {
+        return check(ROM_MIUI);
+    }
+
+    public static boolean isVivo() {
+        return check(ROM_VIVO);
+    }
+
+    public static boolean isOppo() {
+        return check(ROM_OPPO);
+    }
+
+    public static boolean isFlyme() {
+        return check(ROM_FLYME);
+    }
+
+    public static boolean isQiku() {
+        return check(ROM_QIKU) || check("360");
+    }
+
+    public static boolean isSmartisan() {
+        return check(ROM_SMARTISAN);
+    }
+
+    private static String sName;
+
+    public static String getName() {
+        if (sName == null) {
+            check("");
+        }
+        return sName;
+    }
+
+    private static String sVersion;
+
+    public static String getVersion() {
+        if (sVersion == null) {
+            check("");
+        }
+        return sVersion;
+    }
+
+    public static boolean check(String rom) {
+        if (sName != null) {
+            return sName.equals(rom);
+        }
+
+        if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_MIUI))) {
+            sName = ROM_MIUI;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_EMUI))) {
+            sName = ROM_EMUI;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_OPPO))) {
+            sName = ROM_OPPO;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_VIVO))) {
+            sName = ROM_VIVO;
+        } else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_SMARTISAN))) {
+            sName = ROM_SMARTISAN;
+        } else {
+            sVersion = Build.DISPLAY;
+            if (sVersion.toUpperCase().contains(ROM_FLYME)) {
+                sName = ROM_FLYME;
+            } else {
+                sVersion = Build.UNKNOWN;
+                sName = Build.MANUFACTURER.toUpperCase();
+            }
+        }
+        return sName.equals(rom);
+    }
+
+    public static String getProp(String name) {
+        String line = null;
+        BufferedReader input = null;
+        try {
+            Process p = Runtime.getRuntime().exec("getprop " + name);
+            input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
+            line = input.readLine();
+            input.close();
+        } catch (IOException ex) {
+            Log.e(TAG, "Unable to read prop " + name, ex);
+            return null;
+        } finally {
+            if (input != null) {
+                try {
+                    input.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return line;
+    }
+
+}

+ 265 - 0
webrtc/src/main/java/com/wdkl/core/voip/SettingsCompat.java

@@ -0,0 +1,265 @@
+package com.wdkl.core.voip;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Build;
+import android.provider.Settings;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+
+/**
+ * Created by dds on 2018/3/20.
+ */
+
+public class SettingsCompat {
+    private static final String TAG = "ezy-settings-compat";
+    public static final int REQUEST_SYSTEM_ALERT_WINDOW = 11001;
+
+    private static final int OP_WRITE_SETTINGS = 23;
+    private static final int OP_SYSTEM_ALERT_WINDOW = 24;
+
+    public static boolean canDrawOverlays(Context context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return Settings.canDrawOverlays(context);
+        } else {
+            return checkOp(context, OP_SYSTEM_ALERT_WINDOW);
+        }
+    }
+
+    public static boolean canWriteSettings(Context context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return Settings.System.canWrite(context);
+        } else {
+            return checkOp(context, OP_WRITE_SETTINGS);
+        }
+    }
+
+    public static boolean setDrawOverlays(Context context, boolean allowed) {
+        return setMode(context, OP_SYSTEM_ALERT_WINDOW, allowed);
+    }
+
+    public static boolean setWriteSettings(Context context, boolean allowed) {
+        return setMode(context, OP_WRITE_SETTINGS, allowed);
+    }
+
+    public static void manageDrawOverlays(Activity context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            try {
+                Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
+                intent.setData(Uri.parse("package:" + context.getPackageName()));
+                context.startActivityForResult(intent, REQUEST_SYSTEM_ALERT_WINDOW);
+                return;
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+        if (manageDrawOverlaysForRom(context)) {
+            Log.d("SettingsCompat", "打开设置页面");
+        }
+
+    }
+
+    public static void manageWriteSettings(Context context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
+            intent.setData(Uri.parse("package:" + context.getPackageName()));
+            context.startActivity(intent);
+        }
+    }
+
+    private static boolean manageDrawOverlaysForRom(Context context) {
+        if (RomUtil.isMiui()) {
+            return manageDrawOverlaysForMiui(context);
+        }
+        if (RomUtil.isEmui()) {
+            return manageDrawOverlaysForEmui(context);
+        }
+        if (RomUtil.isFlyme()) {
+            return manageDrawOverlaysForFlyme(context);
+        }
+        if (RomUtil.isOppo()) {
+            return manageDrawOverlaysForOppo(context);
+        }
+        if (RomUtil.isVivo()) {
+            return manageDrawOverlaysForVivo(context);
+        }
+        if (RomUtil.isQiku()) {
+            return manageDrawOverlaysForQihu(context);
+        }
+        if (RomUtil.isSmartisan()) {
+            return manageDrawOverlaysForSmartisan(context);
+        }
+        return false;
+    }
+
+
+    private static boolean checkOp(Context context, int op) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+            try {
+                Method method = AppOpsManager.class.getDeclaredMethod("checkOp", int.class, int.class, String.class);
+                return AppOpsManager.MODE_ALLOWED == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
+            } catch (Exception e) {
+                Log.e(TAG, Log.getStackTraceString(e));
+            }
+        }
+        return false;
+    }
+
+    // 可设置Android 4.3/4.4的授权状态
+    private static boolean setMode(Context context, int op, boolean allowed) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            return false;
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+            try {
+                @SuppressLint("PrivateApi") Method method = AppOpsManager.class.getDeclaredMethod("setMode", int.class, int.class, String.class, int.class);
+                method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName(), allowed ? AppOpsManager.MODE_ALLOWED : AppOpsManager
+                        .MODE_IGNORED);
+                return true;
+            } catch (Exception e) {
+                Log.e(TAG, Log.getStackTraceString(e));
+
+            }
+        }
+        return false;
+    }
+
+    private static boolean startSafely(Context context, Intent intent) {
+        if (context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0) {
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            context.startActivity(intent);
+            return true;
+        } else {
+            Log.e(TAG, "Intent is not available! " + intent);
+            return false;
+        }
+    }
+
+    // 小米
+    private static boolean manageDrawOverlaysForMiui(Context context) {
+        Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR");
+        intent.putExtra("extra_pkgname", context.getPackageName());
+        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            Intent intent1 = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+            intent1.setData(Uri.fromParts("package", context.getPackageName(), null));
+            return startSafely(context, intent1);
+        }
+        return false;
+    }
+
+    private final static String HUAWEI_PACKAGE = "com.huawei.systemmanager";
+
+    // 华为
+    private static boolean manageDrawOverlaysForEmui(Context context) {
+        Intent intent = new Intent();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            intent.setClassName(HUAWEI_PACKAGE, "com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity");
+            if (startSafely(context, intent)) {
+                return true;
+            }
+        }
+        // Huawei Honor P6|4.4.4|3.0
+        intent.setClassName(HUAWEI_PACKAGE, "com.huawei.notificationmanager.ui.NotificationManagmentActivity");
+        intent.putExtra("showTabsNumber", 1);
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        intent.setClassName(HUAWEI_PACKAGE, "com.huawei.permissionmanager.ui.MainActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        return false;
+    }
+
+    // VIVO
+    private static boolean manageDrawOverlaysForVivo(Context context) {
+        // 不支持直接到达悬浮窗设置页,只能到 i管家 首页
+        Intent intent = new Intent("com.iqoo.secure");
+        intent.setClassName("com.iqoo.secure", "com.iqoo.secure.MainActivity");
+        // com.iqoo.secure.ui.phoneoptimize.SoftwareManagerActivity
+        // com.iqoo.secure.ui.phoneoptimize.FloatWindowManager
+        return startSafely(context, intent);
+    }
+
+    // OPPO
+    private static boolean manageDrawOverlaysForOppo(Context context) {
+        Intent intent = new Intent();
+        intent.putExtra("packageName", context.getPackageName());
+        // OPPO A53|5.1.1|2.1
+        intent.setAction("com.oppo.safe");
+        intent.setClassName("com.oppo.safe", "com.oppo.safe.permission.floatwindow.FloatWindowListActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        // OPPO R7s|4.4.4|2.1
+        intent.setAction("com.color.safecenter");
+        intent.setClassName("com.color.safecenter", "com.color.safecenter.permission.floatwindow.FloatWindowListActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        intent.setAction("com.coloros.safecenter");
+        intent.setClassName("com.coloros.safecenter", "com.coloros.safecenter.sysfloatwindow.FloatWindowListActivity");
+        return startSafely(context, intent);
+    }
+
+    // 魅族
+    private static boolean manageDrawOverlaysForFlyme(Context context) {
+        Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC");
+        intent.setClassName("com.meizu.safe", "com.meizu.safe.security.AppSecActivity");
+        intent.putExtra("packageName", context.getPackageName());
+        return startSafely(context, intent);
+    }
+
+    // 360
+    private static boolean manageDrawOverlaysForQihu(Context context) {
+        Intent intent = new Intent();
+        intent.setClassName("com.android.settings", "com.android.settings.Settings$OverlaySettingsActivity");
+        if (startSafely(context, intent)) {
+            return true;
+        }
+        intent.setClassName("com.qihoo360.mobilesafe", "com.qihoo360.mobilesafe.ui.index.AppEnterActivity");
+        return startSafely(context, intent);
+    }
+
+    // 锤子
+    private static boolean manageDrawOverlaysForSmartisan(Context context) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            return false;
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            // 锤子 坚果|5.1.1|2.5.3
+            Intent intent = new Intent("com.smartisanos.security.action.SWITCHED_PERMISSIONS_NEW");
+            intent.setClassName("com.smartisanos.security", "com.smartisanos.security.SwitchedPermissions");
+            intent.putExtra("index", 17); // 不同版本会不一样
+            return startSafely(context, intent);
+        } else {
+            // 锤子 坚果|4.4.4|2.1.2
+            Intent intent = new Intent("com.smartisanos.security.action.SWITCHED_PERMISSIONS");
+            intent.setClassName("com.smartisanos.security", "com.smartisanos.security.SwitchedPermissions");
+            intent.putExtra("permission", new String[]{Manifest.permission.SYSTEM_ALERT_WINDOW});
+
+            //        Intent intent = new Intent("com.smartisanos.security.action.MAIN");
+            //        intent.setClassName("com.smartisanos.security", "com.smartisanos.security.MainActivity");
+            return startSafely(context, intent);
+        }
+    }
+}

+ 321 - 0
webrtc/src/main/java/com/wdkl/core/voip/SingleCallFragment.java

@@ -0,0 +1,321 @@
+package com.wdkl.core.voip;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Chronometer;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.wdkl.core.ui.event.MsgEvent;
+import com.wdkl.skywebrtc.CallSession;
+import com.wdkl.skywebrtc.EnumType;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.webrtc.R;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+/**
+ * <pre>
+ *     author : Jasper
+ *     e-mail : 229605030@qq.com
+ *     time   : 2021/02/01
+ *     desc   :
+ * </pre>
+ */
+public abstract class SingleCallFragment extends Fragment {
+    private static final String TAG = "SingleCallFragment";
+    ImageView minimizeImageView;
+    ImageView portraitImageView;// 用户头像
+    TextView nameTextView; // 用户昵称
+    TextView descTextView;  // 状态提示用语
+    Chronometer durationTextView; // 通话时长
+
+    ImageView outgoingHangupImageView;
+    ImageView incomingHangupImageView;
+    ImageView acceptImageView;
+    TextView tvStatus;
+    View outgoingActionContainer;
+    View incomingActionContainer;
+    View connectedActionContainer;
+
+    View lytParent;
+
+    boolean isOutgoing = false;
+
+    SkyEngineKit gEngineKit;
+
+
+    CallSingleActivity callSingleActivity;
+
+    CallHandler handler;
+    boolean endWithNoAnswerFlag = false;
+    boolean isConnectionClosed = false;
+
+    public static final int WHAT_DELAY_END_CALL = 0x01;
+
+    public static final int WHAT_NO_NET_WORK_END_CALL = 0x02;
+
+    EnumType.CallState currentState;
+    HeadsetPlugReceiver headsetPlugReceiver;
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setRetainInstance(true);
+        if (!EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().register(this);
+        }
+        handler = new CallHandler();
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        View view = inflater.inflate(getLayout(), container, false);
+        initView(view);
+        init();
+        return view;
+    }
+
+    @Override
+    public void onDestroyView() {
+        if (durationTextView != null)
+            durationTextView.stop();
+        refreshMessage(true);
+        super.onDestroyView();
+    }
+
+    @Override
+    public void onDestroy() {
+        handler.removeCallbacksAndMessages(null);
+        if (EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().unregister(this);
+        }
+        super.onDestroy();
+    }
+
+
+    abstract int getLayout();
+
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    public void onEvent(MsgEvent<Object> messageEvent) {
+        int code = messageEvent.getCode();
+        Log.d(TAG, "onEvent code = $code; endWithNoAnswerFlag = $endWithNoAnswerFlag");
+        if (code == MsgEvent.CODE_ON_CALL_ENDED) {
+            if (endWithNoAnswerFlag) {
+                didCallEndWithReason(EnumType.CallEndReason.Timeout);
+            } else if (isConnectionClosed) {
+                didCallEndWithReason(EnumType.CallEndReason.SignalError);
+            } else {
+                if (callSingleActivity != null) {
+                    callSingleActivity.finish();
+                }
+            }
+        } else if (code == MsgEvent.CODE_ON_REMOTE_RING) {
+            descTextView.setText("对方已响铃");
+        }
+    }
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        callSingleActivity = (CallSingleActivity) getActivity();
+        if (callSingleActivity != null) {
+            isOutgoing = callSingleActivity.isOutgoing();
+            gEngineKit = callSingleActivity.getEngineKit();
+            headsetPlugReceiver = new HeadsetPlugReceiver();
+            IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_HEADSET_PLUG);
+            callSingleActivity.registerReceiver(headsetPlugReceiver, filter);
+        }
+    }
+
+    @Override
+    public void onDetach() {
+        super.onDetach();
+        callSingleActivity.unregisterReceiver(headsetPlugReceiver);  //注销监听
+        callSingleActivity = null;
+    }
+
+
+    public void initView(View view) {
+        lytParent = view.findViewById(R.id.lytParent);
+        minimizeImageView = view.findViewById(R.id.minimizeImageView);
+        portraitImageView = view.findViewById(R.id.portraitImageView);
+        nameTextView = view.findViewById(R.id.nameTextView);
+        descTextView = view.findViewById(R.id.descTextView);
+        durationTextView = view.findViewById(R.id.durationTextView);
+        outgoingHangupImageView = view.findViewById(R.id.outgoingHangupImageView);
+        incomingHangupImageView = view.findViewById(R.id.incomingHangupImageView);
+        acceptImageView = view.findViewById(R.id.acceptImageView);
+        tvStatus = view.findViewById(R.id.tvStatus);
+        outgoingActionContainer = view.findViewById(R.id.outgoingActionContainer);
+        incomingActionContainer = view.findViewById(R.id.incomingActionContainer);
+        connectedActionContainer = view.findViewById(R.id.connectedActionContainer);
+
+        durationTextView.setVisibility(View.GONE);
+//        nameTextView.setText();
+//        portraitImageView.setImageResource(R.mipmap.icon_default_header);
+        if (isOutgoing) {
+            handler.sendEmptyMessageDelayed(WHAT_DELAY_END_CALL, 30 * 1000);//30s之后未接通,则挂断电话
+        }
+    }
+
+    public void init() {
+    }
+
+    // ======================================界面回调================================
+    public void didCallEndWithReason(EnumType.CallEndReason callEndReason) {
+        switch (callEndReason) {
+            case Busy: {
+                tvStatus.setText("对方忙线中");
+                break;
+            }
+            case SignalError: {
+                tvStatus.setText("连接断开");
+                break;
+            }
+            case RemoteSignalError: {
+                tvStatus.setText("对方网络断开");
+                break;
+            }
+            case Hangup: {
+                tvStatus.setText("挂断");
+                break;
+            }
+            case MediaError: {
+                tvStatus.setText("媒体错误");
+                break;
+            }
+            case RemoteHangup: {
+                tvStatus.setText("对方挂断");
+                break;
+            }
+            case OpenCameraFailure: {
+                tvStatus.setText("打开摄像头错误");
+                break;
+            }
+            case Timeout: {
+                tvStatus.setText("对方未接听");
+                break;
+            }
+            case AcceptByOtherClient: {
+                tvStatus.setText("在其它设备接听");
+                break;
+            }
+        }
+        incomingActionContainer.setVisibility(View.GONE);
+        outgoingActionContainer.setVisibility(View.GONE);
+        if (connectedActionContainer != null)
+            connectedActionContainer.setVisibility(View.GONE);
+        refreshMessage(false);
+        new Handler(Looper.getMainLooper()).postDelayed(() -> {
+            if (callSingleActivity != null) {
+                callSingleActivity.finish();
+            }
+
+        }, 1500);
+    }
+
+    public void didChangeState(EnumType.CallState state) {
+
+    }
+
+    public void didChangeMode(Boolean isAudio) {
+    }
+
+    public void didCreateLocalVideoTrack() {
+    }
+
+    public void didReceiveRemoteVideoTrack(String userId) {
+    }
+
+    public void didUserLeave(String userId) {
+    }
+
+    public void didError(String error) {
+    }
+
+    public void didDisconnected(String error) {
+        handler.sendEmptyMessage(WHAT_NO_NET_WORK_END_CALL);
+    }
+
+    private void refreshMessage(Boolean isForCallTime) {
+        if (callSingleActivity == null) {
+            return;
+        }
+        // 刷新消息; demo中没有消息,不用处理这儿快逻辑
+    }
+
+    public void startRefreshTime() {
+        CallSession session = SkyEngineKit.Instance().getCurrentSession();
+        if (session == null) return;
+        if (durationTextView != null) {
+            durationTextView.setVisibility(View.VISIBLE);
+            durationTextView.setBase(SystemClock.elapsedRealtime() - (System.currentTimeMillis() - session.getStartTime()));
+            durationTextView.start();
+        }
+    }
+
+    void runOnUiThread(Runnable runnable) {
+        if (callSingleActivity != null) {
+            callSingleActivity.runOnUiThread(runnable);
+        }
+    }
+
+    class CallHandler extends Handler {
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            if (msg.what == WHAT_DELAY_END_CALL) {
+                if (currentState != EnumType.CallState.Connected) {
+                    endWithNoAnswerFlag = true;
+                    if (callSingleActivity != null) {
+                        SkyEngineKit.Instance().endCall();
+                    }
+                }
+            } else if (msg.what == WHAT_NO_NET_WORK_END_CALL) {
+                isConnectionClosed = true;
+                if (callSingleActivity != null) {
+                    SkyEngineKit.Instance().endCall();
+                }
+            }
+        }
+
+    }
+
+
+    class HeadsetPlugReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.hasExtra("state")) {
+                CallSession session = SkyEngineKit.Instance().getCurrentSession();
+                if (session == null) {
+                    return;
+                }
+                if (intent.getIntExtra("state", 0) == 0) { //拔出耳机
+                    session.toggleHeadset(false);
+                } else if (intent.getIntExtra("state", 0) == 1) { //插入耳机
+                    session.toggleHeadset(true);
+                }
+            }
+        }
+    }
+}

+ 7 - 0
webrtc/src/main/java/com/wdkl/core/voip/Utils.java

@@ -0,0 +1,7 @@
+package com.wdkl.core.voip;
+
+public class Utils {
+
+    public static String ACTION_VOIP_RECEIVER = "com.wdkl.voip.Receiver";
+
+}

+ 111 - 0
webrtc/src/main/java/com/wdkl/core/voip/VoipEvent.java

@@ -0,0 +1,111 @@
+package com.wdkl.core.voip;
+
+import android.media.AudioManager;
+import android.net.Uri;
+import android.util.Log;
+
+import com.wdkl.core.socket.SocketManager;
+import com.wdkl.skywebrtc.inter.ISkyEvent;
+import com.wdkl.webrtc.R;
+
+import java.util.List;
+
+/**
+ * Created by dds on 2019/8/25.
+ * android_shuai@163.com
+ */
+public class VoipEvent implements ISkyEvent {
+    private static final String TAG = "VoipEvent";
+    private AsyncPlayer ringPlayer;
+
+    public VoipEvent() {
+        ringPlayer = new AsyncPlayer(null);
+    }
+
+    @Override
+    public void createRoom(String room, int roomSize) {
+        SocketManager.getInstance().createRoom(room, roomSize);
+    }
+
+    @Override
+    public void sendInvite(String room, List<String> userIds, boolean audioOnly) {
+        SocketManager.getInstance().sendInvite(room, userIds, audioOnly);
+    }
+
+    @Override
+    public void sendRefuse(String room, String inviteId, int refuseType) {
+        SocketManager.getInstance().sendRefuse(room, inviteId, refuseType);
+    }
+
+    @Override
+    public void sendTransAudio(String toId) {
+        SocketManager.getInstance().sendTransAudio(toId);
+    }
+
+    @Override
+    public void sendDisConnect(String room, String toId, boolean isCrashed) {
+        SocketManager.getInstance().sendDisconnect(room, toId);
+    }
+
+    @Override
+    public void sendCancel(String mRoomId, List<String> toIds) {
+        SocketManager.getInstance().sendCancel(mRoomId, toIds);
+    }
+
+
+    @Override
+    public void sendJoin(String room) {
+        SocketManager.getInstance().sendJoin(room);
+    }
+
+    @Override
+    public void sendRingBack(String targetId, String room) {
+        SocketManager.getInstance().sendRingBack(targetId, room);
+    }
+
+    @Override
+    public void sendLeave(String room, String userId) {
+        SocketManager.getInstance().sendLeave(room, userId);
+    }
+
+
+    @Override
+    public void sendOffer(String userId, String sdp) {
+        SocketManager.getInstance().sendOffer(userId, sdp);
+    }
+
+    @Override
+    public void sendAnswer(String userId, String sdp) {
+        SocketManager.getInstance().sendAnswer(userId, sdp);
+
+    }
+
+    @Override
+    public void sendIceCandidate(String userId, String id, int label, String candidate) {
+        SocketManager.getInstance().sendIceCandidate(userId, id, label, candidate);
+    }
+
+    @Override
+    public void onRemoteRing() {
+
+    }
+
+
+    //==============================================================================
+    @Override
+    public void shouldStartRing(boolean isComing) {
+        if (isComing) {
+            //Uri uri = Uri.parse("android.resource://" + SocketManager.getInstance().getContext().getPackageName() + "/" + R.raw.incoming_call_ring);
+            //ringPlayer.play(SocketManager.getInstance().getContext(), uri, true, AudioManager.STREAM_RING);
+        } else {
+            //Uri uri = Uri.parse("android.resource://" + SocketManager.getInstance().getContext().getPackageName() + "/" + R.raw.wr_ringback);
+            ringPlayer.play(SocketManager.getInstance().getContext(), R.raw.wr_ringback, true, AudioManager.STREAM_MUSIC);
+        }
+    }
+
+    @Override
+    public void shouldStopRing() {
+        Log.d(TAG, "shouldStopRing begin");
+        ringPlayer.stop();
+    }
+}

+ 232 - 0
webrtc/src/main/java/com/wdkl/core/voip/VoipReceiver.java

@@ -0,0 +1,232 @@
+package com.wdkl.core.voip;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.blankj.utilcode.util.LogUtils;
+import com.wdkl.core.base.BaseActivity;
+import com.wdkl.core.socket.SocketManager;
+import com.wdkl.core.util.ActivityStackManager;
+import com.wdkl.permission.Permissions;
+import com.wdkl.skywebrtc.SkyEngineKit;
+import com.wdkl.webrtc.R;
+import com.tapadoo.alerter.Alerter;
+
+import java.util.ArrayList;
+
+/**
+ * Created by dds on 2019/8/25.
+ * android_shuai@163.com
+ */
+public class VoipReceiver extends BroadcastReceiver {
+    private static final String TAG = "VoipReceiver";
+    private AsyncPlayer ringPlayer;
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        if (Utils.ACTION_VOIP_RECEIVER.equals(action)) {
+            String room = intent.getStringExtra("room");
+            boolean audioOnly = intent.getBooleanExtra("audioOnly", true);
+            String inviteId = intent.getStringExtra("inviteId");
+            String inviteUserName = intent.getStringExtra("inviteUserName");
+            String userList = intent.getStringExtra("userList");
+            String[] list = userList.split(",");
+            SkyEngineKit.init(new VoipEvent());
+            //todo 处理邀请人名称
+            if (inviteUserName == null) {
+                inviteUserName = "p2pChat";
+            }
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                //if (com.wdkl.core.util.Utils.isAppRunningForeground(context)) {
+                    onForegroundOrBeforeVersionO(context, room, userList, inviteId, audioOnly, inviteUserName, true);
+                //} else {
+                //    onBackgroundAfterVersionO(context, room, userList, inviteId, audioOnly, inviteUserName);
+                //}
+            } else {
+                onForegroundOrBeforeVersionO(context,
+                        room,
+                        userList,
+                        inviteId,
+                        audioOnly,
+                        inviteUserName,
+                        com.wdkl.core.util.Utils.isAppRunningForeground(context)
+                );
+            }
+        }
+    }
+
+    private void onBackgroundAfterVersionO(Context context,
+            String room, String userList,
+            String inviteId, Boolean audioOnly, String inviteUserName
+    ) {
+        String[] strArr = userList.split(",");
+        ArrayList<String> list = new ArrayList<>();
+        for (String str : strArr)
+            list.add(str);
+        SkyEngineKit.init(new VoipEvent());
+        //BaseActivity activity = (BaseActivity) ActivityStackManager.getInstance().getTopActivity();
+        // 权限检测
+        String[] per;
+        if (audioOnly) {
+            per = new String[]{Manifest.permission.RECORD_AUDIO};
+        } else {
+            per = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
+        }
+        boolean hasPermission = true;  //Permissions.has(activity, per);
+        if (hasPermission) {
+            onBackgroundHasPermission(context, room, list, inviteId, audioOnly, inviteUserName);
+        } /*else {
+            CallForegroundNotification notification = new CallForegroundNotification(SocketManager.getInstance().getContext());
+            notification.sendRequestIncomingPermissionsNotification(
+                    activity,
+                    room,
+                    userList,
+                    inviteId,
+                    inviteUserName,
+                    audioOnly
+            );
+        }*/
+    }
+
+    private void onBackgroundHasPermission(
+            Context context, String room, ArrayList<String> list,
+            String inviteId, Boolean audioOnly, String inviteUserName) {
+        boolean b = SkyEngineKit.Instance().startInCall(SocketManager.getInstance().getContext(), room, inviteId, audioOnly);
+        LogUtils.dTag(TAG, "onBackgroundHasPermission b = " + b );
+        if (b) {
+            //App.getInstance().setOtherUserId(inviteId);
+            if (list.size() == 1) {
+                CallForegroundNotification notification = new CallForegroundNotification(SocketManager.getInstance().getContext());
+                notification.sendIncomingCallNotification(
+                        SocketManager.getInstance().getContext(),
+                        inviteId,
+                        false,
+                        inviteUserName,
+                        audioOnly,
+                        true
+                );
+            }
+        }
+    }
+
+    private void onForegroundOrBeforeVersionO(Context context,
+            String room, String userList,
+            String inviteId, Boolean audioOnly, String inviteUserName, Boolean isForeGround
+    ) {
+        String[] strArr = userList.split(",");
+        ArrayList<String> list = new ArrayList<>();
+        for (String str : strArr)
+            list.add(str);
+        SkyEngineKit.init(new VoipEvent());
+        //BaseActivity activity = (BaseActivity) ActivityStackManager.getInstance().getTopActivity();
+        // 权限检测
+        String[] per;
+        if (audioOnly) {
+            per = new String[]{Manifest.permission.RECORD_AUDIO};
+        } else {
+            per = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA};
+        }
+        boolean hasPermission = true;  //Permissions.has(activity, per);
+        LogUtils.dTag(TAG, "onForegroundOrBeforeVersionO hasPermission = " + hasPermission + ", isForeGround = " + isForeGround);
+        if (hasPermission) {
+            onHasPermission(context, room, list, inviteId, audioOnly, inviteUserName);
+        } /*else {
+
+            ringPlayer = new AsyncPlayer(null);
+            shouldStartRing(true); //来电先响铃
+            if (isForeGround) {
+                Alerter.create(activity).setTitle("来电通知")
+                        .setText(
+                                "您收到" + inviteUserName + "的来电邀请,请允许"
+                                        + (audioOnly ? "录音"
+                                        : "录音和相机") + "权限来通话"
+                        )
+                        .enableSwipeToDismiss()
+                        .setBackgroundColorRes(R.color.colorAccent) // or setBackgroundColorInt(Color.CYAN)
+                        .setDuration(60 * 1000)
+                        .addButton("确定", R.style.AlertButtonBgWhite, v -> {
+                            Permissions.request(activity, per, integer -> {
+                                shouldStopRing();
+                                Log.d(TAG, "Permissions.request integer = " + integer);
+                                if (integer == 0) { //权限同意
+                                    onHasPermission(activity, room, list, inviteId, audioOnly, inviteUserName);
+                                } else {
+                                    onPermissionDenied(room, inviteId);
+                                }
+                                Alerter.hide();
+                            });
+                        })
+                        .addButton("取消", R.style.AlertButtonBgWhite, v -> {
+                            shouldStopRing();
+                            onPermissionDenied(room, inviteId);
+                            Alerter.hide();
+                        }).show();
+            } else {
+                CallForegroundNotification notification = new CallForegroundNotification(SocketManager.getInstance().getContext());
+                notification.sendRequestIncomingPermissionsNotification(
+                        activity,
+                        room,
+                        userList,
+                        inviteId,
+                        inviteUserName,
+                        audioOnly
+                );
+            }
+
+        }*/
+    }
+
+    private void onHasPermission(
+            Context context, String room, ArrayList<String> list,
+            String inviteId, Boolean audioOnly, String inviteUserName
+    ) {
+        boolean b = SkyEngineKit.Instance().startInCall(SocketManager.getInstance().getContext(), room, inviteId, audioOnly);
+        LogUtils.dTag(TAG, "onHasPermission b = " + b);
+        if (b) {
+            //App.getInstance().setOtherUserId(inviteId);
+            LogUtils.dTag(TAG, "onHasPermission list.size() = " + list.size());
+            if (list.size() == 1) {
+                //以视频电话拨打,切换到音频或重走这里,结束掉上一个,防止对方挂断后,下边还有一个通话界面
+                if (context instanceof CallSingleActivity) {
+                    ((CallSingleActivity) context).finish();
+                }
+                CallSingleActivity.openActivity(context, inviteId, false, inviteUserName, audioOnly, true);
+            } else {
+                // 群聊
+            }
+        } /*else {
+            Activity activity = ActivityStackManager.getInstance().getTopActivity();
+            activity.finish(); //销毁掉刚才拉起的
+        }*/
+    }
+
+    // 权限拒绝
+    private void onPermissionDenied(String room, String inviteId) {
+        SkyEngineKit.Instance().sendRefuseOnPermissionDenied(room, inviteId);//通知对方结束
+        Toast.makeText(SocketManager.getInstance().getContext(), "权限被拒绝,无法通话", Toast.LENGTH_SHORT).show();
+    }
+
+    private void shouldStartRing(boolean isComing) {
+        if (isComing) {
+            //Uri uri = Uri.parse("android.resource://" + SocketManager.getInstance().getContext().getPackageName() + "/" + R.raw.incoming_call_ring);
+            //ringPlayer.play(SocketManager.getInstance().getContext(), uri, true, AudioManager.STREAM_RING);
+        } else {
+            //Uri uri = Uri.parse("android.resource://" + SocketManager.getInstance().getContext().getPackageName() + "/" + R.raw.wr_ringback);
+            ringPlayer.play(SocketManager.getInstance().getContext(), R.raw.wr_ringback, true, AudioManager.STREAM_MUSIC);
+        }
+    }
+
+    private void shouldStopRing() {
+        Log.d(TAG, "shouldStopRing begin");
+        ringPlayer.stop();
+    }
+}

+ 37 - 0
webrtc/src/main/java/com/wdkl/net/HttpRequest.java

@@ -0,0 +1,37 @@
+package com.wdkl.net;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * Created by dds on 2018/4/23.
+ */
+
+public interface HttpRequest {
+
+    /**
+     * get请求
+     *
+     * @param url      url
+     * @param params   params
+     * @param callback callback
+     */
+    void get(String url, Map<String, Object> params, ICallback callback);
+
+    /**
+     * post请求
+     *
+     * @param url      url
+     * @param params   params
+     * @param callback callback
+     */
+    void post(String url, Map<String, Object> params, ICallback callback);
+
+    /**
+     * 设置双向证书
+     *
+     * @param certificate certificate
+     * @param pwd         pwd
+     */
+    void setCertificate(InputStream certificate, String pwd);
+}

+ 47 - 0
webrtc/src/main/java/com/wdkl/net/HttpRequestPresenter.java

@@ -0,0 +1,47 @@
+package com.wdkl.net;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * Created by dds on 2019/7/3.
+ * android_shuai@163.com
+ */
+public class HttpRequestPresenter implements HttpRequest {
+    protected HttpRequest httpRequest;
+    private static volatile HttpRequestPresenter instance;
+
+    public HttpRequestPresenter(HttpRequest httpRequest) {
+        this.httpRequest = httpRequest;
+    }
+
+    public static void init(HttpRequest httpRequest) {
+        if (null == instance) {
+            synchronized (HttpRequestPresenter.class) {
+                if (null == instance) {
+                    instance = new HttpRequestPresenter(httpRequest);
+                }
+            }
+        }
+    }
+
+    public static HttpRequestPresenter getInstance() {
+        return instance;
+    }
+
+    @Override
+    public void get(String url, Map<String, Object> params, ICallback callback) {
+        httpRequest.get(url, params, callback);
+    }
+
+    @Override
+    public void post(String url, Map<String, Object> params, ICallback callback) {
+        httpRequest.post(url, params, callback);
+    }
+
+    // 设置双向证书
+    @Override
+    public void setCertificate(InputStream certificate, String pwd) {
+        httpRequest.setCertificate(certificate, pwd);
+    }
+}

+ 12 - 0
webrtc/src/main/java/com/wdkl/net/ICallback.java

@@ -0,0 +1,12 @@
+package com.wdkl.net;
+
+/**
+ * Created by dds on 2018/4/23.
+ */
+
+public interface ICallback {
+
+    void onSuccess(String result);
+
+    void onFailure(int code, Throwable t);
+}

+ 54 - 0
webrtc/src/main/java/com/wdkl/net/urlconn/UrlConnRequest.java

@@ -0,0 +1,54 @@
+package com.wdkl.net.urlconn;
+
+
+import com.wdkl.net.HttpRequest;
+import com.wdkl.net.ICallback;
+
+import java.io.InputStream;
+import java.util.Map;
+
+/**
+ * Created by dds on 2019/12/20.
+ */
+public class UrlConnRequest implements HttpRequest {
+
+
+    public UrlConnRequest() {
+    }
+
+    @Override
+    public void get(String url, Map<String, Object> params, ICallback callback) {
+        try {
+            String param = null;
+            if (params != null) {
+                param = UrlConnUtils.builderUrlParams(params);
+
+            }
+            String s = UrlConnUtils.sendGet(url, param);
+            callback.onSuccess(s);
+
+        } catch (Exception e) {
+            callback.onFailure(-1, e);
+        }
+
+    }
+
+    @Override
+    public void post(String url, Map<String, Object> params, ICallback callback) {
+        try {
+            String postStr = null;
+            if (params != null) {
+                postStr = UrlConnUtils.builderUrlParams(params);
+            }
+            String result = UrlConnUtils.sendPost(url, postStr);
+            callback.onSuccess(result);
+        } catch (Exception e) {
+            callback.onFailure(-1, e);
+        }
+    }
+
+    @Override
+    public void setCertificate(InputStream certificate, String pwd) {
+
+    }
+}

+ 263 - 0
webrtc/src/main/java/com/wdkl/net/urlconn/UrlConnUtils.java

@@ -0,0 +1,263 @@
+package com.wdkl.net.urlconn;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * Created by dds on 2019/11/28.
+ * android_shuai@163.com
+ */
+public class UrlConnUtils {
+    private static final String TAG = "dds_UrlConnUtils";
+
+    public static String sendPost(String serverUrl, String formBody) throws Exception {
+        String result;
+        DataOutputStream out;
+        URL url = new URL(serverUrl);
+        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+        if (serverUrl.startsWith("https")) {
+            trustAllHosts(connection);
+            connection.setHostnameVerifier(DO_NOT_VERIFY);
+        }
+        connection.setDoInput(true);
+        connection.setDoOutput(true);
+        connection.setRequestMethod("POST");
+        connection.setUseCaches(false);
+        connection.setInstanceFollowRedirects(true);
+        connection.addRequestProperty("Content-Type", "application/json");
+        connection.connect();
+        out = new DataOutputStream(connection.getOutputStream());
+        if (formBody != null && !"".equals(formBody)) {
+            out.writeBytes(formBody);
+        }
+        out.flush();
+        int responseCode = connection.getResponseCode();
+        if (responseCode >= 200 && responseCode < 300) {
+            InputStream inputStream = connection.getInputStream();
+            result = inputStream2String(inputStream);
+        } else {
+            throw new Exception(String.format("response code:%d, error msg:%s", responseCode, connection.getResponseMessage()));
+        }
+        connection.disconnect();
+        out.close();
+        return result;
+    }
+
+    public static String sendGet(String serverUrl, String param) throws Exception {
+        String result;
+        String reqUrl = serverUrl + (param == null ? "" : ("?" + param));
+        URL url = new URL(reqUrl);
+
+        // ---------------------------------https--------------------------
+//        HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+//        if (serverUrl.startsWith("https")) {
+//            trustAllHosts(connection);
+//            connection.setHostnameVerifier(DO_NOT_VERIFY);
+//        }
+        // http
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        connection.setDoInput(true);
+        connection.setDoOutput(true);
+        connection.setRequestMethod("GET");
+        connection.setUseCaches(false);
+        connection.setInstanceFollowRedirects(true);
+        connection.addRequestProperty("Content-Type", "application/json");
+        connection.connect();
+        int responseCode = connection.getResponseCode();
+        if (responseCode >= 200 && responseCode < 300) {
+            InputStream inputStream = connection.getInputStream();
+            result = inputStream2String(inputStream);
+        } else {
+            throw new Exception(String.format("response code:%d, error msg:%s", responseCode, connection.getResponseMessage()));
+        }
+        connection.disconnect();
+        return result;
+    }
+
+    public static boolean download(String u, String path) {
+        try {
+            URL url = new URL(u);
+            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+            trustAllHosts(connection);
+            connection.setHostnameVerifier(DO_NOT_VERIFY);
+            connection.setDoInput(true);
+            connection.setDoOutput(true);
+            connection.setRequestMethod("GET");
+            connection.setUseCaches(false);
+            connection.setInstanceFollowRedirects(true);
+            //可设置请求头
+            connection.setRequestProperty("Content-Type", "application/octet-stream");
+            connection.setRequestProperty("Connection", "Keep-Alive");// 维持长连接
+            connection.setRequestProperty("Charset", "UTF-8");
+            connection.connect();
+            byte[] file = input2byte(connection.getInputStream());
+            File file1 = writeBytesToFile(file, path);
+            if (file1.exists()) {
+                return true;
+            }
+        } catch (Exception e) {
+            return false;
+        }
+        return false;
+    }
+
+    /**
+     * 构建json参数
+     */
+    public static String builderJsonParams(Map<String, Object> params) {
+        JSONObject jsonObject;
+        try {
+            Set<String> keySet = params.keySet();
+            List<String> keyList = new ArrayList<>(keySet);
+            Collections.sort(keyList);
+            jsonObject = new JSONObject();
+            for (String key : keyList) {
+                Object value = params.get(key);
+                if (value == null || "".equals(value)) {
+                    continue;
+                }
+                jsonObject.put(key, String.valueOf(params.get(key)));
+            }
+        } catch (JSONException e) {
+            return null;
+        }
+        return jsonObject.toString();
+    }
+
+    /**
+     * 构建post参数
+     */
+    public static String builderUrlParams(Map<String, Object> params) {
+        StringBuilder sb = new StringBuilder();
+        Set<String> keySet = params.keySet();
+        List<String> keyList = new ArrayList<>(keySet);
+        Collections.sort(keyList);
+        for (String key : keyList) {
+            Object value = params.get(key);
+            if (value == null || "".equals(value)) {
+                continue;
+            }
+            sb.append(key).append("=").append(params.get(key)).append("&");
+        }
+        if (sb.length() > 0) {
+            return sb.substring(0, sb.length() - 1);
+        }
+        return null;
+    }
+
+    private static byte[] input2byte(InputStream inStream) throws IOException {
+        ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
+        byte[] buff = new byte[100];
+        int rc = 0;
+        while ((rc = inStream.read(buff, 0, 100)) > 0) {
+            swapStream.write(buff, 0, rc);
+        }
+        return swapStream.toByteArray();
+    }
+
+    private static File writeBytesToFile(byte[] b, String outputFile) {
+        File file = null;
+        FileOutputStream os = null;
+        try {
+            file = new File(outputFile);
+            File parentFile = file.getParentFile();
+            if (!parentFile.exists()) {
+                parentFile.mkdirs();
+            }
+            os = new FileOutputStream(file);
+            os.write(b);
+        } catch (Exception var13) {
+            var13.printStackTrace();
+            if (file != null && file.exists()) {
+                file.delete();
+            }
+        } finally {
+            try {
+                if (os != null) {
+                    os.close();
+                }
+            } catch (IOException var12) {
+                var12.printStackTrace();
+            }
+        }
+        return file;
+    }
+
+    private static String inputStream2String(InputStream inputStream) {
+        ByteArrayOutputStream bos = null;
+        byte[] bytes = new byte[1024];
+        int len = 0;
+        try {
+            bos = new ByteArrayOutputStream();
+            while ((len = inputStream.read(bytes)) != -1) {
+                bos.write(bytes, 0, len);
+            }
+            return new String(bos.toByteArray());
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            if (bos != null) {
+                try {
+                    bos.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        return null;
+    }
+
+    private static void trustAllHosts(HttpsURLConnection connection) {
+        try {
+            SSLContext sc = SSLContext.getInstance("TLS");
+            sc.init(null, trustAllCerts, new java.security.SecureRandom());
+            SSLSocketFactory newFactory = sc.getSocketFactory();
+            connection.setSSLSocketFactory(newFactory);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[]{};
+        }
+
+        public void checkClientTrusted(X509Certificate[] chain, String authType) {
+        }
+
+        public void checkServerTrusted(X509Certificate[] chain, String authType) {
+        }
+    }};
+
+    private static final HostnameVerifier DO_NOT_VERIFY = (hostname, session) -> true;
+}

+ 18 - 0
webrtc/src/main/java/com/wdkl/permission/Consumer.java

@@ -0,0 +1,18 @@
+package com.wdkl.permission;
+
+/**
+ * Represents an operation that accepts a single input argument and returns no
+ * result. Unlike most other functional interfaces, {@code Consumer} is expected
+ * to operate via side-effects.
+ *
+ * @param <T> the type of the input to the operation
+ */
+public interface Consumer<T> {
+
+	/**
+	 * Performs this operation on the given argument.
+	 *
+	 * @param t the input argument
+	 */
+	void accept(T t);
+}

+ 144 - 0
webrtc/src/main/java/com/wdkl/permission/Permissions.java

@@ -0,0 +1,144 @@
+package com.wdkl.permission;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.content.ContextCompat;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.os.Build.VERSION_CODES.M;
+
+/**
+ * Permission-related helpers
+ */
+public class Permissions {
+
+
+    /**
+     * @param callback will be called if request is not canceled, with either
+     *                 {@link PackageManager#PERMISSION_GRANTED} or {@link PackageManager#PERMISSION_DENIED}
+     */
+    public static void request(Activity activity, String permission, Consumer<Integer> callback) {
+        if (Build.VERSION.SDK_INT >= M) {
+            request2(activity, permission, callback);
+        } else {
+            if (has(activity, permission)) {
+                callback.accept(0);
+            } else {
+                callback.accept(-1);
+            }
+        }
+
+    }
+
+    /**
+     * @param callback will be called if request is not canceled, with either
+     *                 {@link PackageManager#PERMISSION_GRANTED} or {@link PackageManager#PERMISSION_DENIED}
+     */
+    public static void request(Activity activity, String[] permissions, Consumer<Integer> callback) {
+        if (Build.VERSION.SDK_INT >= M) {
+            request2(activity, permissions, callback);
+        } else {
+            if (has(activity, permissions)) {
+                callback.accept(0);
+            } else {
+                callback.accept(-1);
+            }
+
+        }
+
+    }
+
+    @RequiresApi(M)
+    public static void request2(Activity activity, String permission, Consumer<Integer> callback) {
+        final FragmentManager fm = activity.getFragmentManager();
+        if (!has(activity, permission)) {
+            fm.beginTransaction().add(new PermissionRequestFragment(new String[]{permission}, callback), null).commitAllowingStateLoss();
+        } else {
+            callback.accept(PERMISSION_GRANTED);
+        }
+    }
+
+    @RequiresApi(M)
+    public static void request2(Activity activity, String[] permissions, Consumer<Integer> callback) {
+        final FragmentManager fm = activity.getFragmentManager();
+        if (!has(activity, permissions)) {
+            fm.beginTransaction().add(new PermissionRequestFragment(permissions, callback), null).commitAllowingStateLoss();
+        } else {
+            callback.accept(PERMISSION_GRANTED);
+        }
+    }
+
+    public static boolean has(Context activity, String... permissions) {
+        List<String> mPermissionListDenied = new ArrayList<>();
+        for (String permission : permissions) {
+            int result = checkPermission(activity, permission);
+            if (result != PERMISSION_GRANTED) {
+                mPermissionListDenied.add(permission);
+            }
+        }
+        return mPermissionListDenied.size() == 0;
+    }
+
+    private static boolean has(Context context, String permission) {
+        return context.checkPermission(permission, Process.myPid(), Process.myUid()) == PERMISSION_GRANTED;
+    }
+
+    private static int checkPermission(Context activity, String permission) {
+        return ContextCompat.checkSelfPermission(activity, permission);
+    }
+
+    @RequiresApi(M)
+    public static class PermissionRequestFragment extends Fragment {
+
+        @SuppressLint("ValidFragment")
+        public PermissionRequestFragment(@NonNull final String[] permissions, @NonNull final Consumer<Integer> callback) {
+            mPermissions = permissions;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onCreate(@Nullable final Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (mPermissions != null) requestPermissions(mPermissions, 0);
+        }
+
+        @Override
+        public void onRequestPermissionsResult(final int request, @NonNull final String[] permissions, @NonNull final int[] results) {
+            getFragmentManager().beginTransaction().remove(this).commit();
+            if (mCallback == null || results.length == 0/* canceled */) return;
+            boolean isGrant = true;
+            for (int result : results) {
+                if (result != PackageManager.PERMISSION_GRANTED) {
+                    isGrant = false;
+                    break;
+                }
+            }
+            mCallback.accept(isGrant ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED);
+        }
+
+        public PermissionRequestFragment() {
+            mPermissions = null;
+            mCallback = null;
+        }
+
+        private final @Nullable
+        String[] mPermissions;
+        private final @Nullable
+        Consumer<Integer> mCallback;
+    }
+}

BIN
webrtc/src/main/res/drawable-xhdpi/av_audio_answer.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_audio_answer_hover.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_camera.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_camera_hover.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_default_header.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_float_audio.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_handfree.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_handfree_hover.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_hang_up.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_hang_up_hover.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_minimize.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_mute.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_mute_hover.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_phone.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_trans_audio.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_video_answer.png


BIN
webrtc/src/main/res/drawable-xhdpi/av_video_answer_hover.png


+ 20 - 0
webrtc/src/main/res/drawable-xhdpi/bg_btn_white.xml

@@ -0,0 +1,20 @@
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+    android:insetLeft="4dp"
+    android:insetTop="6dp"
+    android:insetRight="4dp"
+    android:insetBottom="6dp">
+    <shape
+        android:shape="rectangle"
+        android:tint="@color/colorAccent">
+        <stroke
+            android:width="1dp"
+            android:color="#FFFFFF" />
+        <corners android:radius="25dp" />
+        <solid android:color="#FFFFFF" />
+        <padding
+            android:bottom="4dp"
+            android:left="8dp"
+            android:right="8dp"
+            android:top="4dp" />
+    </shape>
+</inset>

+ 10 - 0
webrtc/src/main/res/drawable/av_audio_answer_selector.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:drawable="@drawable/av_audio_answer" android:state_focused="false" android:state_pressed="false" android:state_selected="false" />
+    <item android:drawable="@drawable/av_audio_answer_hover" android:state_pressed="false" android:state_selected="true" />
+    <!-- Focused states -->
+    <item android:drawable="@drawable/av_audio_answer_hover" android:state_focused="true" android:state_pressed="false" android:state_selected="false" />
+    <!-- Pressed -->
+    <item android:drawable="@drawable/av_audio_answer_hover" android:state_pressed="true" />
+</selector>

+ 16 - 0
webrtc/src/main/res/drawable/av_float_bg.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+
+    <solid android:color="#ffffff" />
+
+    <stroke
+        android:width="0.5px"
+        android:color="#ffffff" />
+
+    <corners android:radius="3dp" />
+
+    <size
+        android:width="60dp"
+        android:height="80dp" />
+</shape>

+ 10 - 0
webrtc/src/main/res/drawable/av_hangup_selector.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:drawable="@drawable/av_hang_up" android:state_focused="false" android:state_pressed="false" android:state_selected="false" />
+    <item android:drawable="@drawable/av_hang_up_hover" android:state_pressed="false" android:state_selected="true" />
+    <!-- Focused states -->
+    <item android:drawable="@drawable/av_hang_up_hover" android:state_focused="true" android:state_pressed="false" android:state_selected="false" />
+    <!-- Pressed -->
+    <item android:drawable="@drawable/av_hang_up_hover" android:state_pressed="true" />
+</selector>

+ 5 - 0
webrtc/src/main/res/drawable/av_mute_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@drawable/av_mute_hover" android:state_selected="true" />
+    <item android:drawable="@drawable/av_mute" />
+</selector>

+ 5 - 0
webrtc/src/main/res/drawable/av_speaker_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@drawable/av_handfree_hover" android:state_selected="true" />
+    <item android:drawable="@drawable/av_handfree" />
+</selector>

+ 10 - 0
webrtc/src/main/res/drawable/av_switch_camera_selector.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:drawable="@drawable/av_camera" android:state_focused="false" android:state_pressed="false" android:state_selected="false" />
+    <item android:drawable="@drawable/av_camera_hover" android:state_pressed="false" android:state_selected="true" />
+    <!-- Focused states -->
+    <item android:drawable="@drawable/av_camera_hover" android:state_focused="true" android:state_pressed="false" android:state_selected="false" />
+    <!-- Pressed -->
+    <item android:drawable="@drawable/av_camera_hover" android:state_pressed="true" />
+</selector>

+ 10 - 0
webrtc/src/main/res/drawable/av_video_answer_selector.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:drawable="@drawable/av_video_answer" android:state_focused="false" android:state_pressed="false" android:state_selected="false" />
+    <item android:drawable="@drawable/av_video_answer_hover" android:state_pressed="false" android:state_selected="true" />
+    <!-- Focused states -->
+    <item android:drawable="@drawable/av_video_answer_hover" android:state_focused="true" android:state_pressed="false" android:state_selected="false" />
+    <!-- Pressed -->
+    <item android:drawable="@drawable/av_video_answer_hover" android:state_pressed="true" />
+</selector>

+ 9 - 0
webrtc/src/main/res/drawable/ic_dashboard_black_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M3,13h8L11,3L3,3v10zM3,21h8v-6L3,15v6zM13,21h8L21,11h-8v10zM13,3v6h8L21,3h-8z" />
+</vector>

+ 9 - 0
webrtc/src/main/res/drawable/ic_home_black_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z" />
+</vector>

+ 9 - 0
webrtc/src/main/res/drawable/ic_notifications_black_24dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z" />
+</vector>

+ 52 - 0
webrtc/src/main/res/layout/activity_launcher.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+
+    tools:context="com.wdkl.LauncherActivity">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:theme="@style/AppTheme.AppBarOverlay">
+
+        <androidx.appcompat.widget.Toolbar
+            android:id="@+id/toolbar"
+            android:layout_width="match_parent"
+            android:layout_height="48dp"
+            app:popupTheme="@style/AppTheme.PopupOverlay" />
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:padding="16dp">
+
+        <EditText
+            android:id="@+id/et_user"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:digits="0123456789qwertyuioplkjhgfdsazxcvbnmQWERTYUIOPLKJHGFDSAZXCVBNM"
+            android:hint="input your user name " />
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="The server side is written by Spring boot" />
+
+        <Button
+            android:id="@+id/button8"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:onClick="java"
+            android:text="ENTER"
+            android:textAllCaps="false" />
+    </LinearLayout>
+
+</LinearLayout>

+ 29 - 0
webrtc/src/main/res/layout/activity_main.xml

@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <com.google.android.material.bottomnavigation.BottomNavigationView
+        android:id="@+id/nav_view"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="0dp"
+        android:layout_marginEnd="0dp"
+        android:background="?android:attr/windowBackground"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:menu="@menu/bottom_nav_menu" />
+
+    <fragment
+        android:id="@+id/nav_host_fragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_constraintBottom_toTopOf="@id/nav_view"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 21 - 0
webrtc/src/main/res/layout/activity_multi_call.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    tools:context="com.wdkl.core.voip.CallMultiActivity">
+
+    <FrameLayout
+        android:id="@+id/meeting_container"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="3"
+        android:background="@color/av_bg_call_black" />
+
+    <include
+        layout="@layout/av_p2p_meeting_action"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="2" />
+</LinearLayout>

+ 9 - 0
webrtc/src/main/res/layout/activity_single_call.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="com.wdkl.core.voip.CallSingleActivity">
+
+</LinearLayout>

+ 64 - 0
webrtc/src/main/res/layout/av_p2p_audio_incoming.xml

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_horizontal"
+    android:orientation="vertical"
+    android:paddingLeft="30dp"
+    android:paddingRight="30dp"
+    android:paddingBottom="20dp">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="20dp"
+        android:orientation="horizontal">
+
+        <LinearLayout
+            android:id="@+id/hangupLinearLayout"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentLeft="true"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/incomingHangupImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_hangup_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="挂断"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/acceptLinearLayout"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/acceptImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_audio_answer_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="接听"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+    </RelativeLayout>
+
+</LinearLayout>

+ 92 - 0
webrtc/src/main/res/layout/av_p2p_audio_outgoing.xml

@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:orientation="vertical"
+    android:paddingBottom="30dp">
+
+    <Chronometer
+        android:id="@+id/durationTextView"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textColor="@android:color/white"
+        tools:visibility="visible" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="20dp"
+        android:orientation="horizontal">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:visibility="invisible">
+
+            <ImageView
+                android:id="@+id/muteImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_mute_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="静音"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <ImageView
+                android:id="@+id/outgoingHangupImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_hangup_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="挂断"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:visibility="invisible">
+
+            <ImageView
+                android:id="@+id/speakerImageView"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:src="@drawable/av_speaker_selector" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="10dp"
+                android:text="免提"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+    </LinearLayout>
+
+</LinearLayout>

+ 0 - 0
webrtc/src/main/res/layout/av_p2p_meeting_action.xml


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است