wuyunfeng 3 年之前
父節點
當前提交
789554fb39
共有 43 個文件被更改,包括 3881 次插入142 次删除
  1. 6 2
      app/build.gradle
  2. 230 131
      app/src/main/java/com/wdkl/ncs/entraceguard/activity/MainActivity.kt
  3. 2 5
      app/src/main/java/com/wdkl/ncs/entraceguard/application/Application.java
  4. 1 1
      app/src/main/java/com/wdkl/ncs/entraceguard/tcp/TcpClient.java
  5. 1 0
      app/src/main/java/com/wdkl/ncs/entraceguard/tcp/channel/DeviceChannel.java
  6. 1 1
      build.gradle
  7. 5 0
      framework/build.gradle
  8. 1 1
      framework/src/main/java/com/wdkl/ncs/framework/api/UrlManager.kt
  9. 80 0
      framework/src/main/java/com/wdkl/ncs/framework/beans/ServerInfo.java
  10. 2 0
      framework/src/main/java/com/wdkl/ncs/framework/common/Constants.kt
  11. 45 0
      framework/src/main/java/com/wdkl/ncs/framework/utils/ServerInfoUtil.java
  12. 1 0
      janus/.gitignore
  13. 43 0
      janus/build.gradle
  14. 0 0
      janus/consumer-rules.pro
  15. 21 0
      janus/proguard-rules.pro
  16. 25 0
      janus/src/androidTest/java/com/wdkl/ncs/janus/ExampleInstrumentedTest.java
  17. 6 0
      janus/src/main/AndroidManifest.xml
  18. 26 0
      janus/src/main/java/com/wdkl/ncs/janus/client/CallSessionCallback.java
  19. 878 0
      janus/src/main/java/com/wdkl/ncs/janus/client/JanusClient.java
  20. 33 0
      janus/src/main/java/com/wdkl/ncs/janus/client/JanusMessageType.java
  21. 19 0
      janus/src/main/java/com/wdkl/ncs/janus/client/PluginHandle.java
  22. 47 0
      janus/src/main/java/com/wdkl/ncs/janus/client/Transaction.java
  23. 382 0
      janus/src/main/java/com/wdkl/ncs/janus/client/VideoRoomCallback.java
  24. 110 0
      janus/src/main/java/com/wdkl/ncs/janus/client/WebSocketChannel.java
  25. 35 0
      janus/src/main/java/com/wdkl/ncs/janus/entity/MsgEvent.java
  26. 54 0
      janus/src/main/java/com/wdkl/ncs/janus/entity/Publisher.java
  27. 68 0
      janus/src/main/java/com/wdkl/ncs/janus/entity/Room.java
  28. 24 0
      janus/src/main/java/com/wdkl/ncs/janus/render/ProxyVideoSink.java
  29. 171 0
      janus/src/main/java/com/wdkl/ncs/janus/render/VideoFileRenderer.java
  30. 100 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/AudioFocusManager.java
  31. 189 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/Peer.java
  32. 759 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/WebRTCEngine.java
  33. 72 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/AnswerSdpObserver.java
  34. 9 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreateAnswerCallback.java
  35. 9 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreateOfferCallback.java
  36. 20 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreatePeerConnectionCallback.java
  37. 128 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CustomPCObserver.java
  38. 79 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/OfferSdpObserver.java
  39. 21 0
      janus/src/main/java/com/wdkl/ncs/janus/util/Constant.java
  40. 37 0
      janus/src/main/java/com/wdkl/ncs/janus/util/EnumType.java
  41. 123 0
      janus/src/main/java/com/wdkl/ncs/janus/util/OSUtils.java
  42. 17 0
      janus/src/test/java/com/wdkl/ncs/janus/ExampleUnitTest.java
  43. 1 1
      settings.gradle

+ 6 - 2
app/build.gradle

@@ -69,7 +69,7 @@ android {
 
 dependencies {
 
-    implementation project(path: ':rtc-chat')
+//    implementation project(path: ':rtc-chat')
     compile fileTree(dir: 'libs', include: ['*.aar'])
     androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
         exclude group: 'com.android.support', module: 'support-annotations'
@@ -123,6 +123,8 @@ dependencies {
 
     implementation 'com.squareup.okhttp3:okhttp:3.8.0'
 
+
+    implementation 'org.webrtc:google-webrtc:1.0.32006'
     /**
      * netty
      */
@@ -141,7 +143,9 @@ dependencies {
 
     compile project(':framework')
 
-    compile project(':webrtc')
+    compile project(':janus')
+
+//    compile project(':webrtc')
 
 
 

+ 230 - 131
app/src/main/java/com/wdkl/ncs/entraceguard/activity/MainActivity.kt

@@ -1,5 +1,7 @@
 package com.wdkl.ncs.entraceguard.activity
 
+import android.app.AlarmManager
+import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.media.AudioAttributes
@@ -14,10 +16,6 @@ import android.widget.TextView
 import com.alibaba.fastjson.JSON
 import com.google.common.base.Strings
 import com.szeasco.facesdk.helper.GpioHelper
-import com.wdkl.core.consts.Urls
-import com.wdkl.core.socket.IUserState
-import com.wdkl.core.socket.SocketManager
-import com.wdkl.core.voip.VoipEvent
 import com.wdkl.ncs.entraceguard.contracts.MainActivityContract
 import com.wdkl.ncs.entraceguard.di.DaggerApplicationComponent
 import com.wdkl.ncs.entraceguard.model.dos.DeviceDO
@@ -37,10 +35,13 @@ import com.wdkl.ncs.framework.helper.NetHelper
 import com.wdkl.ncs.framework.helper.RingPlayHelper
 import com.wdkl.ncs.framework.utils.MessageEvent
 import com.wdkl.ncs.framework.utils.Util
-import com.wdkl.skywebrtc.CallSession
-import com.wdkl.skywebrtc.EnumType
-import com.wdkl.skywebrtc.SkyEngineKit
-import com.wdkl.skywebrtc.except.NotInitializedException
+import com.wdkl.ncs.janus.client.CallSessionCallback
+import com.wdkl.ncs.janus.client.JanusClient
+import com.wdkl.ncs.janus.client.VideoRoomCallback
+import com.wdkl.ncs.janus.entity.Room
+import com.wdkl.ncs.janus.rtc.WebRTCEngine
+import com.wdkl.ncs.janus.util.Constant
+import com.wdkl.ncs.janus.util.EnumType
 import iot.facereco.smart.terminal.R
 import iot.facereco.smart.terminal.databinding.ActivityMainBinding
 import kotlinx.android.synthetic.main.activity_main.*
@@ -48,9 +49,10 @@ import org.greenrobot.eventbus.EventBus
 import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode
 import org.webrtc.SurfaceViewRenderer
+import java.math.BigInteger
 import java.util.*
 
-class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(), MainActivityContract.View, IUserState, CallSession.CallSessionCallback {
+class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(), MainActivityContract.View, CallSessionCallback /*IUserState, CallSession.CallSessionCallback*/ {
 
     private val mSoundPool: SoundPool by lazy {
         val audioAttributes = AudioAttributes.Builder()
@@ -61,7 +63,7 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
                 .setAudioAttributes(audioAttributes.build())
                 .build()
     }
-    private var gEngineKit: SkyEngineKit? = null
+//    private var gEngineKit: SkyEngineKit? = null
     private var gpioHelper: GpioHelper? = null
 
     private var interactionVO: InteractionVO? = null
@@ -78,6 +80,11 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
     // 陌生人
     var strangerSoundID: Int? = null
 
+    var janusClient: JanusClient? = null
+
+    var videoRoomCallback: VideoRoomCallback? = null
+
+    var room: Room? = null
 
     override fun getLayId(): Int {
         return R.layout.activity_main
@@ -105,21 +112,23 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
             //获取tcp服务器信息
             presenter.getTcpServerInfo()
 
-            try {
-                SkyEngineKit.init(VoipEvent()) //重新初始化
-                gEngineKit = SkyEngineKit.Instance()
-            } catch (e: NotInitializedException) {
-                SkyEngineKit.init(VoipEvent()) //重新初始化
-                try {
-                    gEngineKit = SkyEngineKit.Instance()
-                } catch (ex: NotInitializedException) {
-//                finish()
-                }
-
-            }
+//            try {
+//                SkyEngineKit.init(VoipEvent()) //重新初始化
+//                gEngineKit = SkyEngineKit.Instance()
+//            } catch (e: NotInitializedException) {
+//                SkyEngineKit.init(VoipEvent()) //重新初始化
+//                try {
+//                    gEngineKit = SkyEngineKit.Instance()
+//                } catch (ex: NotInitializedException) {
+////                finish()
+//                }
+//
+//            }
             initResponseCountDownTimer()
             responseCountDownTimer.start() //开始计时,正常呼叫tcp发出,服务器立即响应成功或失败,如果超过8秒没响应,退出程序
             Log.i("WDKL", "IMEI=" + Util.IMEI)
+
+
         }
     }
 
@@ -144,9 +153,22 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
             Constants.sipId = deviceDO.sipId
             Constants.sipPassword = deviceDO.sipPassword
             Constants.deviceId = deviceDO.id
-            SocketManager.getInstance().addUserStateCallback(this)
+            //初始化 engine
+            WebRTCEngine.getInstance().init(false, this)
+
+
+            //初始化 janusClient
+            janusClient = JanusClient(Constant.JANUS_URL, Constants.sipId!!.toBigInteger())
+            janusClient!!.callState = EnumType.CallState.Outgoing
+            room = Room(Constants.sipId!!.toBigInteger())
+            videoRoomCallback = VideoRoomCallback(janusClient, room,  Constants.sipId!!.toBigInteger())
+            videoRoomCallback!!.callSessionCallback = this
+            janusClient!!.setJanusCallback(videoRoomCallback)
+            initCountDownTimer()
+//            SocketManager.getInstance().addUserStateCallback(this)
             //初始化webrtc
-            SocketManager.getInstance().connect(Urls.WS, Constants.sipId, 0)
+//            SocketManager.getInstance().connect(Urls.WS, Constants.sipId, 0)
+            TcpClient.getInstance().sendMsg(EntraceGuardUtil.startVisit(Constants.deviceId).toJson())
         }else{
             showTipView(R.drawable.unregister,"设备未注册或未启用")
             exitApp(3000)
@@ -183,7 +205,12 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
                             //取消响应计时
                             responseCountDownTimer?.cancel()
                             interactionVO = JSON.parseObject(tcpModel.data.toString(), InteractionVO::class.java)
-                            startCall(interactionVO!!.toSipId, false)
+                            janusClient!!.connect()
+
+                            countDownTimer.start()
+                            RingPlayHelper.playRingTone(this, R.raw.alice, true)
+                            WebRTCEngine.getInstance().switchCamera()
+//                            startCall(interactionVO!!.toSipId, false)
                         }
 
                         TcpAction.EntraceGuardAction.FAILED ->{  //护士主机不在线
@@ -203,8 +230,8 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
 
                             showTipView(R.drawable.allow_in,"欢迎光临,正在为您开门")
                             //结束通话
-                            SkyEngineKit.Instance().endCall()
-//                            SocketManager.getInstance().unConnect()
+                            janusClient!!.destroyRoom(janusClient!!.currentHandleId, null)
+                            janusClient!!.disConnect()
                             //处理界面
 
 
@@ -218,7 +245,9 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
                             cancelCountDownTimer()
                             play(strangerSoundID!!)
                             showTipView(R.drawable.busyline,"对方正忙,请稍候在试")
-                            SkyEngineKit.Instance().endCall()
+
+                            janusClient!!.destroyRoom(janusClient!!.currentHandleId, null)
+                            janusClient!!.disConnect()
                             //3秒后退出程序
                             exitApp(3000)
 
@@ -232,22 +261,27 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
                         TcpAction.EntraceGuardAction.OPENSPEAKER -> { //打开音频
                             //停止等待音乐,停止倒计时
                             cancelCountDownTimer()
-                            var session = gEngineKit?.currentSession
-                            Log.i("session", session?.roomId)
-                            if (session != null) {
-                                session.toggleSpeaker(true)
-                                session.toggleMuteAudio(false)
-                            }
+
+                            WebRTCEngine.getInstance().toggleSpeaker(true)
+                            WebRTCEngine.getInstance().muteAudio(false)
+//                            var session = gEngineKit?.currentSession
+//                            Log.i("session", session?.roomId)
+//                            if (session != null) {
+//                                session.toggleSpeaker(true)
+//                                session.toggleMuteAudio(false)
+//                            }
                         }
 
                         TcpAction.EntraceGuardAction.CLOSESPEAKER -> { //关闭音频
                             //停止等待音乐,停止倒计时
                             cancelCountDownTimer()
-                            var session = gEngineKit?.currentSession
-                            if (session != null) {
-                                session.toggleSpeaker(false)
-                                session.toggleMuteAudio(true)
-                            }
+                            WebRTCEngine.getInstance().toggleSpeaker(false)
+                            WebRTCEngine.getInstance().muteAudio(true)
+//                            var session = gEngineKit?.currentSession
+//                            if (session != null) {
+//                                session.toggleSpeaker(false)
+//                                session.toggleMuteAudio(true)
+//                            }
                         }
 
                         TcpAction.EntraceGuardAction.HANGUP -> { //主机直接挂断,不开门显示
@@ -255,7 +289,9 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
                             cancelCountDownTimer()
                             play(strangerSoundID!!)
                             showTipView(R.drawable.not_allow,"未授权通行")
-                            SkyEngineKit.Instance().endCall()
+                            janusClient!!.destroyRoom(janusClient!!.currentHandleId, null)
+                            janusClient!!.disConnect()
+//                            SkyEngineKit.Instance().endCall()
                             //3秒后退出程序
                             exitApp(3000)
                         }
@@ -272,102 +308,108 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
 
                 }
             }
-        }
-    }
-
-
-    override fun userLogout() {
-        Log.i("webrtc", "用户退出登录")
-    }
-
-    override fun userLogin() {
-        Log.i("webrtc", "用户登录成功")
-        //发送视频请求
-        TcpClient.getInstance().sendMsg(EntraceGuardUtil.startVisit(Constants.deviceId).toJson())
-
-    }
-
-    //创建会话
-    private fun startCall(targetId: String, audioOnly: Boolean): Boolean {
-        val room = UUID.randomUUID().toString() + System.currentTimeMillis()
-        val b = gEngineKit!!.startOutCall(this, room, targetId, audioOnly)
-        Log.i("wdlk", "发送通话邀请")
-        if (b) {
-            val session = gEngineKit?.currentSession
-            if (session == null) {
-                return false
-            } else {
-//                Constants.CALL_STATE = Constants.CALL_CALLING
-//                DeviceChannel.calling = true
-
-                initCountDownTimer()
-                countDownTimer.start()
-                RingPlayHelper.playRingTone(this, R.raw.alice, true)
-                session.setSessionCallback(this)
-                session.switchCamera()
-
-                session.toggleSpeaker(false)
-                session.toggleMuteAudio(true)
-
-//                3s还未连接上则判定为通话失败
-                Handler().postDelayed({
-                    if (session.state == EnumType.CallState.Connected) {
-                        //showCalling(onlyAudio)
-                    } else {
-
-                    }
-                    Log.i("创建通话,session state=", session.state.name)
-                }, 3000)
-            }
-        }
-        return b
-    }
-
-    override fun didChangeState(var1: EnumType.CallState?) {
-
-    }
-
-    override fun didDisconnected(userId: String?) {
-
-    }
-
-    override fun didCreateLocalVideoTrack() {
-        Log.d("wzlll", "didCreateLocalVideoTrack")
-        handler.post {
-            if (localSurfaceView == null) {
-                val surfaceView = gEngineKit!!.currentSession.setupLocalVideo(true)
-                if (surfaceView != null) {
-                    localSurfaceView = surfaceView as SurfaceViewRenderer
-                } else {
-                    EventBus.getDefault().post(MessageEvent("back_to_main", Constants.BACK_TO_MAIN_MSG))
+            Constants.TIME->{
+                var tcpModel = messageEvent.tcpModel as TcpModel
+                if(tcpModel.type.equals(TcpType.TIME)&&tcpModel.action.equals(TcpAction.TimeAction.SYNC)){
+                    (this.getSystemService(Context.ALARM_SERVICE) as AlarmManager).setTime(tcpModel.data.toString().toLong()*1000+200)
                 }
-            } else {
-                localSurfaceView!!.setZOrderMediaOverlay(true)
             }
-            surface_view!!.addView(localSurfaceView)
         }
     }
 
 
-    override fun didError(error: String?) {
-
-    }
-
-    override fun didReceiveRemoteVideoTrack(userId: String?) {
-
-    }
-
-    override fun didCallEndWithReason(var1: EnumType.CallEndReason?) {
-
-    }
-
-    override fun didChangeMode(isAudioOnly: Boolean) {
-
-    }
-
-    override fun didUserLeave(userId: String?) {
-
-    }
+//    override fun userLogout() {
+//        Log.i("webrtc", "用户退出登录")
+//    }
+//
+//    override fun userLogin() {
+//        Log.i("webrtc", "用户登录成功")
+//        //发送视频请求
+//        TcpClient.getInstance().sendMsg(EntraceGuardUtil.startVisit(Constants.deviceId).toJson())
+//
+//    }
+
+//    //创建会话
+//    private fun startCall(targetId: String, audioOnly: Boolean): Boolean {
+//        val room = UUID.randomUUID().toString() + System.currentTimeMillis()
+//        val b = gEngineKit!!.startOutCall(this, room, targetId, audioOnly)
+//        Log.i("wdlk", "发送通话邀请")
+//        if (b) {
+//            val session = gEngineKit?.currentSession
+//            if (session == null) {
+//                return false
+//            } else {
+////                Constants.CALL_STATE = Constants.CALL_CALLING
+////                DeviceChannel.calling = true
+//
+//                initCountDownTimer()
+//                countDownTimer.start()
+//                RingPlayHelper.playRingTone(this, R.raw.alice, true)
+//                session.setSessionCallback(this)
+//                session.switchCamera()
+//
+//                session.toggleSpeaker(false)
+//                session.toggleMuteAudio(true)
+//
+////                3s还未连接上则判定为通话失败
+//                Handler().postDelayed({
+//                    if (session.state == EnumType.CallState.Connected) {
+//                        //showCalling(onlyAudio)
+//                    } else {
+//
+//                    }
+//                    Log.i("创建通话,session state=", session.state.name)
+//                }, 3000)
+//            }
+//        }
+//        return b
+//    }
+
+//    override fun didChangeState(var1: EnumType.CallState?) {
+//
+//    }
+//
+//    override fun didDisconnected(userId: String?) {
+//
+//    }
+
+//    override fun didCreateLocalVideoTrack() {
+//        Log.d("wzlll", "didCreateLocalVideoTrack")
+//        handler.post {
+//            if (localSurfaceView == null) {
+//                val surfaceView = gEngineKit!!.currentSession.setupLocalVideo(true)
+//                if (surfaceView != null) {
+//                    localSurfaceView = surfaceView as SurfaceViewRenderer
+//                } else {
+//                    EventBus.getDefault().post(MessageEvent("back_to_main", Constants.BACK_TO_MAIN_MSG))
+//                }
+//            } else {
+//                localSurfaceView!!.setZOrderMediaOverlay(true)
+//            }
+//            surface_view!!.addView(localSurfaceView)
+//        }
+//    }
+//
+//
+//    override fun didError(error: String?) {
+//
+//    }
+//
+//    override fun didReceiveRemoteVideoTrack(userId: String?) {
+//
+//    }
+//
+//    override fun didCallEndWithReason(var1: EnumType.CallEndReason?) {
+//
+//    }
+//
+//    override fun didChangeMode(isAudioOnly: Boolean) {
+//
+//    }
+//
+//    override fun didUserLeave(userId: String?) {
+//
+//    }
 
 
     private fun play(soundID: Int) {
@@ -391,7 +433,9 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
             }
             override fun onFinish() {
                 //呼叫超时,返回到主界面
-                SkyEngineKit.Instance().endCall()
+//                SkyEngineKit.Instance().endCall()
+                janusClient!!.destroyRoom(janusClient!!.currentHandleId, null)
+                janusClient!!.disConnect()
                 RingPlayHelper.stopRingTone()
                 TcpClient.getInstance().sendMsg(EntraceGuardUtil.timeOut(interactionVO).toJson())
                 showTipView(R.drawable.no_reponse,"无人应答,请稍候再试")
@@ -429,4 +473,59 @@ class MainActivity : BaseActivity<MainActivityPresenter, ActivityMainBinding>(),
         messageView.setText(message)
         surface_view.addView(view)
     }
+
+    override fun didChangeState(var1: EnumType.CallState?) {
+
+        when (var1) {
+            EnumType.CallState.Connected -> { // 订阅对方流媒体成功,开始通话计时
+                WebRTCEngine.getInstance().muteAudio(true)
+            }
+        }
+
+
+    }
+
+    override fun didDisconnected(userId: String?) {
+
+    }
+
+    override fun didCreateLocalVideoTrack() {
+        handler.post {
+            if (localSurfaceView == null) {
+                val surfaceView = WebRTCEngine.getInstance().startPreview(true)
+                if (surfaceView != null) {
+                    localSurfaceView = surfaceView as SurfaceViewRenderer
+                } else {
+                    EventBus.getDefault().post(MessageEvent("back_to_main", Constants.BACK_TO_MAIN_MSG))
+                }
+            } else {
+                localSurfaceView!!.setZOrderMediaOverlay(true)
+            }
+            surface_view!!.addView(localSurfaceView)
+        }
+    }
+
+    override fun didError(error: String?) {
+
+    }
+
+    override fun didHangUp(handlerId: BigInteger?) {
+
+    }
+
+    override fun didReceiveRemoteVideoTrack(userId: BigInteger?) {
+
+    }
+
+    override fun didCallEndWithReason(var1: EnumType.CallEndReason?) {
+
+    }
+
+    override fun didChangeMode(isAudioOnly: Boolean) {
+
+    }
+
+    override fun didUserLeave(userId: BigInteger?) {
+
+    }
 }

+ 2 - 5
app/src/main/java/com/wdkl/ncs/entraceguard/application/Application.java

@@ -2,11 +2,8 @@ package com.wdkl.ncs.entraceguard.application;
 
 import com.szeasco.facesdk.FaceSdkApplication;
 import com.szeasco.facesdk.config.FaceSdkConfig;
-import com.wdkl.core.socket.SocketManager;
-import com.wdkl.core.voip.VoipEvent;
 import com.wdkl.ncs.framework.base.BaseApplication;
 import com.wdkl.ncs.framework.helper.NetHelper;
-import com.wdkl.skywebrtc.SkyEngineKit;
 
 public class Application extends BaseApplication {
     @Override
@@ -29,8 +26,8 @@ public class Application extends BaseApplication {
         NetHelper.getInstance().init();
         //初始化sdk
         // 初始化信令
-        SkyEngineKit.init(new VoipEvent());
-        SocketManager.getInstance().init(appContext);
+//        SkyEngineKit.init(new VoipEvent());
+//        SocketManager.getInstance().init(appContext);
     }
 
 

+ 1 - 1
app/src/main/java/com/wdkl/ncs/entraceguard/tcp/TcpClient.java

@@ -61,7 +61,7 @@ public class TcpClient {
                     protected void initChannel(SocketChannel socketChannel) throws Exception {
                         // 这里将LengthFieldBasedFrameDecoder添加到pipeline的首位,因为其需要对接收到的数据
                         // 进行长度字段解码,这里也会对数据进行粘包和拆包处理
-                        socketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 0, 2, 0, 2));
+                        socketChannel.pipeline().addLast(new LengthFieldBasedFrameDecoder(2048, 0, 2, 0, 2));
                         // LengthFieldPrepender是一个编码器,主要是在响应字节数据前面添加字节长度字段
                         socketChannel.pipeline().addLast(new LengthFieldPrepender(2));
                         //心跳包应当小于服务器间隔

+ 1 - 0
app/src/main/java/com/wdkl/ncs/entraceguard/tcp/channel/DeviceChannel.java

@@ -94,6 +94,7 @@ public class DeviceChannel {
                 }
                 break;
             case TIME:
+                EventBus.getDefault().post(new MessageEvent(tcpModel, Constants.TIME));
                 break;
             case EVENT:
                 if (tcpModel.getAction() == TcpAction.EventAction.KEY_CLICK){

+ 1 - 1
build.gradle

@@ -13,7 +13,7 @@ buildscript {
     /**
      * SDK最小支持版本
      */
-    ext.min_sdk_version = 21
+    ext.min_sdk_version = 22
 
     /**
      * SDK目标支持版本

+ 5 - 0
framework/build.gradle

@@ -74,6 +74,11 @@ dependencies {
     compile 'org.greenrobot:eventbus:3.0.0'
 
     /**
+     * json
+     */
+    compile 'com.alibaba:fastjson:1.2.23'
+
+    /**
      *   突破方法数限制
      */
     compile 'com.android.support:multidex:1.0.2'

+ 1 - 1
framework/src/main/java/com/wdkl/ncs/framework/api/UrlManager.kt

@@ -50,6 +50,6 @@ private class ProUrlManager : UrlManager{
 
 
     override val device_url: String
-        get() = "http://8.129.220.143:8006"
+        get() = "http://172.28.100.100:8006"
 
 }

+ 80 - 0
framework/src/main/java/com/wdkl/ncs/framework/beans/ServerInfo.java

@@ -0,0 +1,80 @@
+package com.wdkl.ncs.framework.beans;
+
+import java.io.Serializable;
+
+
+public class ServerInfo implements Serializable {
+
+    /**
+     * api ip地址
+     */
+    private String apiServer;
+    /**
+     * api 接口地址
+     */
+    private Long apiServerPort;
+    /**
+     * webrtc 服务器Ip
+     */
+    private String webrtcServer;
+    /**
+     * webrtc 服务器端口
+     */
+    private Long webrtcServerPort;
+    /**
+     * webrtc stun 服务器地址
+     */
+    private String webrtcStunServer;
+    /**
+     * webrtc stun 端口
+     */
+    private Long webrtcStunServerPort;
+
+    public String getApiServer() {
+        return apiServer;
+    }
+
+    public void setApiServer(String apiServer) {
+        this.apiServer = apiServer;
+    }
+
+    public Long getApiServerPort() {
+        return apiServerPort;
+    }
+
+    public void setApiServerPort(Long apiServerPort) {
+        this.apiServerPort = apiServerPort;
+    }
+
+    public String getWebrtcServer() {
+        return webrtcServer;
+    }
+
+    public void setWebrtcServer(String webrtcServer) {
+        this.webrtcServer = webrtcServer;
+    }
+
+    public Long getWebrtcServerPort() {
+        return webrtcServerPort;
+    }
+
+    public void setWebrtcServerPort(Long webrtcServerPort) {
+        this.webrtcServerPort = webrtcServerPort;
+    }
+
+    public String getWebrtcStunServer() {
+        return webrtcStunServer;
+    }
+
+    public void setWebrtcStunServer(String webrtcStunServer) {
+        this.webrtcStunServer = webrtcStunServer;
+    }
+
+    public Long getWebrtcStunServerPort() {
+        return webrtcStunServerPort;
+    }
+
+    public void setWebrtcStunServerPort(Long webrtcStunServerPort) {
+        this.webrtcStunServerPort = webrtcStunServerPort;
+    }
+}

+ 2 - 0
framework/src/main/java/com/wdkl/ncs/framework/common/Constants.kt

@@ -37,5 +37,7 @@ class Constants {
         const val VISIT_MSG = 0x09
 
         const val ENTRACEGUARD = 0x18
+
+        const val TIME = 0x19
     }
 }

+ 45 - 0
framework/src/main/java/com/wdkl/ncs/framework/utils/ServerInfoUtil.java

@@ -0,0 +1,45 @@
+package com.wdkl.ncs.framework.utils;
+
+import android.os.Environment;
+import android.util.Log;
+
+
+import com.alibaba.fastjson.JSON;
+import com.wdkl.ncs.framework.beans.ServerInfo;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+public class ServerInfoUtil {
+
+
+    public static ServerInfo getServerInfo(){
+
+        String encoding = "UTF-8";
+        File file = new File(Environment.getExternalStorageDirectory() + "/CallingBed2/server_info.json");
+        Long filelength = file.length();
+        byte[] filecontent = new byte[filelength.intValue()];
+        try {
+            FileInputStream in = new FileInputStream(file);
+            in.read(filecontent);
+            in.close();
+        } catch (FileNotFoundException e) {
+        } catch (IOException e) {
+        }
+        try {
+            String content = new String(filecontent, encoding);
+            Log.i("FileString",content);
+            ServerInfo serverInfo = JSON.parseObject(content, ServerInfo.class);
+            return serverInfo;
+        } catch (UnsupportedEncodingException e) {
+
+            return null;
+        } catch (Exception e){
+            return null;
+        }
+    }
+
+}

+ 1 - 0
janus/.gitignore

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

+ 43 - 0
janus/build.gradle

@@ -0,0 +1,43 @@
+plugins {
+    id 'com.android.library'
+}
+
+android {
+    compileSdkVersion 30
+    buildToolsVersion "30.0.3"
+
+    defaultConfig {
+        minSdkVersion 22
+        targetSdkVersion 30
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+
+//    testImplementation 'junit:junit:4.+'
+//    androidTestImplementation 'com.android.support.test:runner:1.0.2'
+//    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+
+    implementation 'org.webrtc:google-webrtc:1.0.32006'
+    implementation 'com.squareup.okhttp3:okhttp:3.8.0'
+    implementation project(path: ':framework')
+//    implementation 'com.squareup.okhttp3:okhttp:3.8.0'
+    //api 'org.greenrobot:eventbus:3.0.0'
+//    compile project(':common')
+}

+ 0 - 0
janus/consumer-rules.pro


+ 21 - 0
janus/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

+ 25 - 0
janus/src/androidTest/java/com/wdkl/ncs/janus/ExampleInstrumentedTest.java

@@ -0,0 +1,25 @@
+package com.wdkl.ncs.janus;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("com.wdkl.ncs.janus.test", appContext.getPackageName());
+    }
+}

+ 6 - 0
janus/src/main/AndroidManifest.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.wdkl.ncs.janus">
+
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+</manifest>

+ 26 - 0
janus/src/main/java/com/wdkl/ncs/janus/client/CallSessionCallback.java

@@ -0,0 +1,26 @@
+package com.wdkl.ncs.janus.client;
+
+import com.wdkl.ncs.janus.util.EnumType;
+
+import java.math.BigInteger;
+
+public interface CallSessionCallback {
+    void didCallEndWithReason(EnumType.CallEndReason var1);
+
+    void didChangeState(EnumType.CallState var1);
+
+    void didChangeMode(boolean isAudioOnly);
+
+    void didCreateLocalVideoTrack();
+
+    void didReceiveRemoteVideoTrack(BigInteger userId);
+
+    void didUserLeave(BigInteger userId);
+
+    void didError(String error);
+
+    void didDisconnected(String userId);
+
+    void didHangUp(BigInteger handlerId);
+
+}

+ 878 - 0
janus/src/main/java/com/wdkl/ncs/janus/client/JanusClient.java

@@ -0,0 +1,878 @@
+package com.wdkl.ncs.janus.client;
+
+import android.util.Log;
+
+import com.wdkl.ncs.janus.rtc.WebRTCEngine;
+import com.wdkl.ncs.janus.util.EnumType;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.IceCandidate;
+import org.webrtc.SessionDescription;
+
+import java.math.BigInteger;
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 参考1:https://blog.csdn.net/Java_lilin/article/details/104007291
+ * 参考2:https://github.com/benwtrent/janus-gateway-android
+ * 参考3:https://zhuanlan.zhihu.com/p/149324861?utm_source=wechat_session
+ */
+public class JanusClient implements WebSocketChannel.WebSocketCallback {
+    private static final String TAG = "JanusClient";
+    private ConcurrentHashMap<BigInteger, PluginHandle> attachedPlugins = new ConcurrentHashMap<>();
+    private ConcurrentHashMap<String, Transaction> transactions = new ConcurrentHashMap<>();
+    private BigInteger sessionId = null;
+    private BigInteger currentHandleId = null;
+    private JanusCallback janusCallback;
+
+    private volatile boolean isKeepAliveRunning;
+    private Thread keepAliveThread;
+
+    private String janusUrl;
+    private WebSocketChannel webSocketChannel;
+
+    private EnumType.CallState callState;
+    private EnumType.CallEndReason endReason;
+    private EnumType.RefuseType refuseType;
+
+    private BigInteger roomId;
+    private BigInteger userId;
+    public static String remoteUsername = "";
+
+    private TimerTask timerTask;
+    private Timer timer;
+
+    public static final int ERROR_CREATE_ROOM = 0x01;
+    public static final int ERROR_ON_MESSAGE = 0x02;
+
+    public JanusClient(String janusUrl, BigInteger userId) {
+        this.janusUrl = janusUrl;
+        this.userId = userId;
+        webSocketChannel = new WebSocketChannel();
+        webSocketChannel.setWebSocketCallback(this);
+
+    }
+
+    public WebSocketChannel getWebSocketChannel() {
+        return webSocketChannel;
+    }
+
+    public void setWebSocketChannel(WebSocketChannel webSocketChannel) {
+        this.webSocketChannel = webSocketChannel;
+    }
+
+    public void setJanusCallback(JanusCallback janusCallback) {
+        this.janusCallback = janusCallback;
+    }
+
+    public BigInteger getSessionId() {
+        return sessionId;
+    }
+
+    public BigInteger getCurrentHandleId() {
+        return currentHandleId;
+    }
+
+    public EnumType.CallState getCallState() {
+        return callState;
+    }
+
+    public void setCallState(EnumType.CallState callState) {
+        this.callState = callState;
+    }
+
+    public EnumType.CallEndReason getEndReason() {
+        return endReason;
+    }
+
+    public void setEndReason(EnumType.CallEndReason endReason) {
+        this.endReason = endReason;
+    }
+
+    public EnumType.RefuseType getRefuseType() {
+        return refuseType;
+    }
+
+    public void setRefuseType(EnumType.RefuseType refuseType) {
+        this.refuseType = refuseType;
+    }
+
+    public BigInteger getRoomId() {
+        return roomId;
+    }
+
+    public void setRoomId(BigInteger roomId) {
+        this.roomId = roomId;
+    }
+
+    public void connect() {
+        webSocketChannel.connect(janusUrl);
+    }
+
+    public void disConnect() {
+        stopKeepAliveTimer();
+        Log.i(TAG, "close ws");
+
+        if (webSocketChannel != null) {
+            try {
+                Log.i(TAG,"close rtc");
+                webSocketChannel.close();
+                WebRTCEngine.getInstance().release();
+            }catch (Exception e){
+               // e.printStackTrace();
+                Log.e(TAG, e.getMessage());
+            }
+
+            webSocketChannel = null;
+        }
+    }
+
+    /**
+     * 创建session,并得到反馈
+     * 发出
+     * {
+     * "janus" : "create",
+     * "transaction" : "<random alphanumeric string>"
+     * }
+     * 成功后的反馈
+     * {
+     * "janus" : "success",
+     * "transaction" : "<same as the request>",
+     * "data" : {
+     * "id" : <unique integer session ID>
+     * }
+     * }
+     * 错误时的反馈
+     * error: a JSON object containing two fields:
+     * code: a numeric error code, as defined in apierror.h;
+     * reason: a verbose string describing the cause of the failure.
+     * <p>
+     * {
+     * "janus" : "error",
+     * "transaction" : "<same as the request>",
+     * "error" : {
+     * "code" : 458
+     * "reason" : "Could not find session 12345678"
+     * }
+     * }
+     * <p>
+     * Once you've created a session, a new endpoint you can use is created in the server. Specifically, the new endpoint is constructed by concatenating the server root and the session identifier you've been returned (e.g., /janus/12345678).
+     * <p>
+     * This endpoint can be used in two different ways:
+     * <p>
+     * using a parameter-less GET request to the endpoint, you'll issue a long-poll request to be notified about events and incoming messages from this session;
+     * using a POST request to send JSON messages, you'll interact with the session itself.
+     */
+    private void createSession() {
+        //先声明回调
+        String tid = randomString(12);
+        transactions.put(tid, new Transaction(tid) {
+            @Override
+            public void onSuccess(JSONObject msg) throws Exception {
+                JSONObject data = msg.getJSONObject("data");
+                sessionId = new BigInteger(data.getString("id"));
+                startKeepAliveTimer();
+                if (janusCallback != null) {
+                    janusCallback.onCreateSession(sessionId);
+                }
+            }
+        });
+        //再发送命令
+        try {
+            JSONObject obj = new JSONObject();
+            obj.put("janus", "create");
+            obj.put("transaction", tid);
+            sendMessage(obj.toString());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 释放session
+     */
+    public void destroySession() {
+        String tid = randomString(12);
+        transactions.put(tid, new Transaction(tid) {
+            @Override
+            public void onSuccess(JSONObject msg) throws Exception {
+                stopKeepAliveTimer();
+                if (janusCallback != null) {
+                    janusCallback.onDestroySession(sessionId);
+                }
+            }
+        });
+        try {
+            JSONObject obj = new JSONObject();
+            obj.put("janus", "destroy");
+            obj.put("transaction", tid);
+            obj.put("session_id", sessionId);
+            sendMessage(obj.toString());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Interacting with the session。建立session后,与session进行交互,得到handle id
+     *
+     * @param pluginName
+     */
+    public void attachPlugin(String pluginName) {
+        String tid = randomString(12);
+        transactions.put(tid, new Transaction(tid) {
+            @Override
+            public void onSuccess(JSONObject msg) throws Exception {
+                JSONObject data = msg.getJSONObject("data");
+                BigInteger handleId = new BigInteger(data.getString("id"));
+                currentHandleId = handleId;
+                if (janusCallback != null) {
+                    janusCallback.onAttached(handleId);
+                }
+                PluginHandle handle = new PluginHandle(handleId);
+                attachedPlugins.put(handleId, handle);
+            }
+        });
+
+        try {
+            JSONObject obj = new JSONObject();
+            obj.put("janus", "attach");
+            obj.put("transaction", tid);
+            obj.put("plugin", pluginName);
+            obj.put("session_id", sessionId);
+            sendMessage(obj.toString());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * attach 到发布者的 handler 上,准备接收视频流
+     * 每个发布者都要 attach 一遍,然后协商 sdp, SFU
+     *
+     * @param feedId
+     */
+    public void subscribeAttach(BigInteger feedId) {
+        String tid = randomString(12);
+        transactions.put(tid, new Transaction(tid, feedId) {
+            @Override
+            public void onSuccess(JSONObject msg, BigInteger feedId) throws Exception {
+                JSONObject data = msg.getJSONObject("data");
+                //其它发布者的 handleId
+                BigInteger handleId = new BigInteger(data.getString("id"));
+                if (janusCallback != null) {
+                    janusCallback.onSubscribeAttached(handleId, feedId);
+                }
+                PluginHandle handle = new PluginHandle(handleId);
+                attachedPlugins.put(handleId, handle);
+            }
+        });
+
+        try {
+            JSONObject obj = new JSONObject();
+            obj.put("janus", "attach");
+            obj.put("transaction", tid);
+            obj.put("plugin", "janus.plugin.videoroom");
+            obj.put("session_id", sessionId);
+            sendMessage(obj.toString());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 给 plugin 发送消息
+     *
+     * @param handleId
+     * @param sdp      webrtc session description
+     */
+    public void createOffer(BigInteger handleId, SessionDescription sdp, Boolean videoOn) {
+        JSONObject message = new JSONObject();
+        try {
+            JSONObject publish = new JSONObject();
+            publish.putOpt("audio", true);
+            publish.putOpt("video", videoOn);
+
+            JSONObject jsep = new JSONObject();
+            jsep.putOpt("type", sdp.type);
+            jsep.putOpt("sdp", sdp.description);
+
+            message.putOpt("janus", "message");
+            message.putOpt("body", publish);
+            message.putOpt("jsep", jsep);
+            message.putOpt("transaction", randomString(12));
+            message.putOpt("session_id", sessionId);
+            message.putOpt("handle_id", handleId);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        sendMessage(message.toString());
+    }
+
+    /**
+     * 开始订阅
+     *
+     * @param subscriptionHandleId
+     * @param sdp
+     */
+    public void subscriptionStart(BigInteger subscriptionHandleId, SessionDescription sdp) {
+        JSONObject message = new JSONObject();
+        try {
+            JSONObject body = new JSONObject();
+            body.putOpt("request", "start");
+            body.putOpt("room", roomId);
+
+            message.putOpt("janus", "message");
+            message.putOpt("body", body);
+            message.putOpt("transaction", randomString(12));
+            message.putOpt("session_id", sessionId);
+            message.putOpt("handle_id", subscriptionHandleId);
+
+            if (sdp != null) {
+                JSONObject jsep = new JSONObject();
+                jsep.putOpt("type", sdp.type);
+                jsep.putOpt("sdp", sdp.description);
+                message.putOpt("jsep", jsep);
+            }
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        sendMessage(message.toString());
+    }
+
+    public void publish(BigInteger handleId, SessionDescription sdp, Boolean videoOn) {
+        JSONObject message = new JSONObject();
+        try {
+            JSONObject publish = new JSONObject();
+            //你也可以用configure请求代替publish。两者的功能在发布上是等效的,但从语义的角度来看,publish是发布时要发送的正确消息。configure请求也可以用于更新发布者会话的某些属性,在这种情况下,就不能用publish请求了
+            publish.putOpt("request", "publish");
+            publish.putOpt("audio", true);
+            publish.putOpt("video", videoOn);
+
+            JSONObject jsep = new JSONObject();
+            jsep.putOpt("type", sdp.type);
+            jsep.putOpt("sdp", sdp.description);
+
+            message.putOpt("janus", "message");
+            message.putOpt("body", publish);
+            message.putOpt("jsep", jsep);
+            message.putOpt("transaction", randomString(12));
+            message.putOpt("session_id", sessionId);
+            message.putOpt("handle_id", handleId);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        sendMessage(message.toString());
+    }
+
+    /**
+     * 订阅
+     *
+     * @param feedId 要订阅的ID
+     */
+    public void subscribe(BigInteger subscriptionHandleId, BigInteger feedId) {
+        JSONObject message = new JSONObject();
+        JSONObject body = new JSONObject();
+        try {
+            body.putOpt("ptype", "subscriber");
+            body.putOpt("request", "join");
+            body.putOpt("room", roomId);
+            body.putOpt("feed", feedId);
+
+            message.put("body", body);
+            message.putOpt("janus", "message");
+            message.putOpt("transaction", randomString(12));
+            message.putOpt("session_id", sessionId);
+            message.putOpt("handle_id", subscriptionHandleId);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        sendMessage(message.toString());
+    }
+
+    public void trickleCandidate(BigInteger handleId, IceCandidate iceCandidate) {
+        JSONObject candidate = new JSONObject();
+        JSONObject message = new JSONObject();
+        try {
+            candidate.putOpt("candidate", iceCandidate.sdp);
+            candidate.putOpt("sdpMid", iceCandidate.sdpMid);
+            candidate.putOpt("sdpMLineIndex", iceCandidate.sdpMLineIndex);
+
+            message.putOpt("janus", "trickle");
+            message.putOpt("candidate", candidate);
+            message.putOpt("transaction", randomString(12));
+            message.putOpt("session_id", sessionId);
+            message.putOpt("handle_id", handleId);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+        sendMessage(message.toString());
+    }
+
+    public void trickleCandidateComplete(BigInteger handleId) {
+        JSONObject candidate = new JSONObject();
+        JSONObject message = new JSONObject();
+        try {
+            candidate.putOpt("completed", true);
+
+            message.putOpt("janus", "trickle");
+            message.putOpt("candidate", candidate);
+            message.putOpt("transaction", randomString(12));
+            message.putOpt("session_id", sessionId);
+            message.putOpt("handle_id", handleId);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+        sendMessage(message.toString());
+    }
+
+    /**
+     * 创建房间
+     *
+     * @param handleId
+     * @param newRoomId {
+     *                  "request" : "create",
+     *                  "room" : <unique numeric ID, optional, chosen by plugin if missing>,
+     *                  "permanent" : <true|false, whether the room should be saved in the config file, default=false>,
+     *                  "description" : "<pretty name of the room, optional>",
+     *                  "secret" : "<password required to edit/destroy the room, optional>",
+     *                  "pin" : "<password required to join the room, optional>",
+     *                  "is_private" : <true|false, whether the room should appear in a list request>,
+     *                  "allowed" : [ array of string tokens users can use to join this room, optional],
+     *                  ...
+     *                  }
+     *                  <p>
+     *                  反馈
+     *                  <p>
+     *                  {
+     *                  "janus": "success",
+     *                  "session_id": 6421975129252707,
+     *                  "transaction": "m2D5tESZ3fwb",
+     *                  "sender": 537399609431468,
+     *                  "plugindata": {
+     *                  "plugin": "janus.plugin.videoroom",
+     *                  "data": {
+     *                  "videoroom": "created",
+     *                  "room": 12345678,
+     *                  "permanent": false
+     *                  }
+     *                  }
+     *                  }
+     *                  <p>
+     *                  {
+     *                  "janus": "success",
+     *                  "session_id": 1359598870403518,
+     *                  "transaction": "4mC6b30kX0sr",
+     *                  "sender": 2692286927487266,
+     *                  "plugindata": {
+     *                  "plugin": "janus.plugin.videoroom",
+     *                  "data": {
+     *                  "videoroom": "event",
+     *                  "error_code": 427,
+     *                  "error": "Room 12345678 already exists"
+     *                  }
+     *                  }
+     *                  }
+     */
+    public void createRoom(BigInteger handleId, BigInteger newRoomId) {
+        roomId = newRoomId;
+
+        String tid = randomString(12);
+        transactions.put(tid, new Transaction(tid) {
+            @Override
+            public void onSuccess(JSONObject msg) throws Exception {
+                JSONObject data = msg.getJSONObject("plugindata").getJSONObject("data");
+                if (data.getString("videoroom").equals("event")) {
+                    if (data.has("error")) {
+                        if (data.getInt("error_code") == 427) {
+                            //拨打
+                            if (EnumType.CallState.Outgoing == callState) {
+                                destroyRoom(handleId, new DestroyRoomCallback() {
+                                    @Override
+                                    public void onSuccess() {
+                                        createRoom(handleId, newRoomId);
+                                    }
+
+                                    @Override
+                                    public void onFailed() {
+                                        janusCallback.onError(ERROR_CREATE_ROOM, "创建房间失败");
+                                    }
+                                });
+                            }
+                            //接收
+                            else if (EnumType.CallState.Incoming == callState) {
+                                janusCallback.onCreateRoom(handleId);
+                            }
+                        } else {
+                            janusCallback.onError(ERROR_CREATE_ROOM, "创建房间失败");
+                        }
+                    }
+                } else if (data.getString("videoroom").equals("created")) {
+                    janusCallback.onCreateRoom(handleId);
+                }
+            }
+
+            @Override
+            public void onError() {
+                janusCallback.onError(ERROR_CREATE_ROOM, "创建房间失败");
+            }
+        });
+
+        JSONObject message = new JSONObject();
+        JSONObject body = new JSONObject();
+        try {
+            body.putOpt("request", "create");
+            body.putOpt("room", newRoomId);
+            message.put("body", body);
+
+            message.putOpt("janus", "message");
+            message.putOpt("transaction", tid);
+            message.putOpt("session_id", sessionId);
+            message.putOpt("handle_id", handleId);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        sendMessage(message.toString());
+    }
+
+    /**
+     * @param handleId "videoroom" : "destroyed",
+     */
+    public void destroyRoom(BigInteger handleId, DestroyRoomCallback destroyRoomCallback) {
+        String tid = randomString(12);
+        transactions.put(tid, new Transaction(tid) {
+            @Override
+            public void onSuccess(JSONObject msg) throws Exception {
+                JSONObject data = msg.getJSONObject("plugindata").getJSONObject("data");
+                if (destroyRoomCallback != null) {
+                    if (data.getString("videoroom").equals("destroyed")) {
+                        destroyRoomCallback.onSuccess();
+                    } else {
+                        destroyRoomCallback.onFailed();
+                    }
+                }
+            }
+
+            @Override
+            public void onError() {
+                if (destroyRoomCallback != null) {
+                    destroyRoomCallback.onFailed();
+                }
+            }
+        });
+
+        JSONObject message = new JSONObject();
+        JSONObject body = new JSONObject();
+        try {
+            body.putOpt("request", "destroy");
+            body.putOpt("room", roomId);
+            message.put("body", body);
+
+            message.putOpt("janus", "message");
+            message.putOpt("transaction", tid);
+            message.putOpt("session_id", sessionId);
+            message.putOpt("handle_id", handleId);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        sendMessage(message.toString());
+    }
+
+    //给插件,发送消息,得到ack和event两个消息
+    public void joinRoom(BigInteger handleId, String displayName) {
+        JSONObject message = new JSONObject();
+        JSONObject body = new JSONObject();
+        try {
+            body.putOpt("display", displayName);
+            body.putOpt("ptype", "publisher");
+            body.putOpt("request", "join");
+            body.putOpt("id", userId); //发布者id,我司系统 sip_id
+            body.putOpt("room", roomId);
+            message.put("body", body);
+
+            message.putOpt("janus", "message");
+            message.putOpt("transaction", randomString(12));
+            message.putOpt("session_id", sessionId);
+            message.putOpt("handle_id", handleId);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        sendMessage(message.toString());
+    }
+
+    public void leaveRoom() {
+        JSONObject message = new JSONObject();
+        JSONObject body = new JSONObject();
+        try {
+            body.putOpt("request", "leave");
+            message.put("body", body);
+
+            message.putOpt("janus", "message");
+            message.putOpt("transaction", randomString(12));
+            message.putOpt("session_id", sessionId);
+            message.putOpt("handle_id", currentHandleId);
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+
+        sendMessage(message.toString());
+    }
+
+
+    private synchronized void sendMessage(String message){
+        if(webSocketChannel!=null){
+            webSocketChannel.sendMessage(message);
+        }
+    }
+
+
+    @Override
+    public void onOpen() {
+        if (sessionId == null) {
+            createSession();
+        }
+    }
+
+    @Override
+    public void onMessage(String message) {
+        //Log.d(TAG, "收到消息》》》" + message);
+        try {
+            JSONObject obj = new JSONObject(message);
+            JanusMessageType type = JanusMessageType.fromString(obj.getString("janus"));
+            String transaction = null;
+            BigInteger sender = null;
+            if (obj.has("transaction")) {
+                transaction = obj.getString("transaction");
+            }
+            //是插件 handleId
+            if (obj.has("sender")) {
+                sender = new BigInteger(obj.getString("sender"));
+            }
+            PluginHandle handle = null;
+            if (sender != null) {
+                handle = attachedPlugins.get(sender);
+            }
+            switch (type) {
+                case keepalive:
+                    break;
+                case ack:
+                    break;
+                case success:
+                    if (transaction != null) {
+                        Transaction cb = transactions.get(transaction);
+                        if (cb != null) {
+                            try {
+                                if (cb.getFeedId() != null) {
+                                    cb.onSuccess(obj, cb.getFeedId());
+                                } else {
+                                    cb.onSuccess(obj);
+                                }
+                                transactions.remove(transaction);
+                            } catch (Exception e) {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+                    break;
+                case error: {
+                    if (transaction != null) {
+                        Transaction cb = transactions.get(transaction);
+                        if (cb != null) {
+                            cb.onError();
+                            transactions.remove(transaction);
+                        }
+                    }
+                    break;
+                }
+                case hangup: {
+
+                    if (handle != null) {
+
+                        if (janusCallback != null) {
+
+                            janusCallback.onHangup(handle.getHandleId());
+                        }
+                    }
+                    break;
+                }
+                case detached: {
+                    if (handle != null) {
+                        if (janusCallback != null) {
+                            janusCallback.onDetached(handle.getHandleId());
+                        }
+                    }
+                    break;
+                }
+                case event: {   //不进行 transaction 处理
+                    if (handle != null) {
+                        JSONObject plugin_data = null;
+                        if (obj.has("plugindata")) {
+                            plugin_data = obj.getJSONObject("plugindata");
+                        }
+                        if (plugin_data != null) {
+                            JSONObject data = null;
+                            JSONObject jsep = null;
+                            if (plugin_data.has("data")) {
+                                data = plugin_data.getJSONObject("data");
+                            }
+                            if (obj.has("jsep")) {
+                                jsep = obj.getJSONObject("jsep");
+                            }
+                            if (janusCallback != null) {
+                                janusCallback.onMessage(sender, handle.getHandleId(), data, jsep);
+                            }
+                        }
+                    }
+                }
+                case trickle:
+                    if (handle != null) {
+                        if (obj.has("candidate")) {
+                            JSONObject candidate = obj.getJSONObject("candidate");
+                            if (janusCallback != null) {
+                                janusCallback.onIceCandidate(handle.getHandleId(), candidate);
+                            }
+                        }
+                    }
+                    break;
+                case destroy:
+                    if (janusCallback != null) {
+                        janusCallback.onDestroySession(sessionId);
+                    }
+                    break;
+                case slowlink:
+                    break;
+            }
+        } catch (JSONException ex) {
+            if (janusCallback != null) {
+                janusCallback.onError(ERROR_ON_MESSAGE, ex.getMessage());
+            }
+        }
+    }
+
+    /**
+     * The long-poll request has a 30 seconds timeout. If it has no event to report, a simple keep-alive message will be triggered:
+     */
+    private void startKeepAliveTimer() {
+        isKeepAliveRunning = true;
+        if(timer!=null){
+            timer.purge();
+        }
+        if(timerTask!=null){
+            timerTask.cancel();
+        }
+        timer = new Timer();
+
+        timerTask=new TimerTask() {
+            @Override
+            public void run() {
+                if (webSocketChannel != null && webSocketChannel.isConnected()) {
+
+                    JSONObject obj = new JSONObject();
+                    try {
+                        obj.put("janus", "keepalive");
+                        obj.put("session_id", sessionId);
+                        obj.put("transaction", randomString(12));
+                        sendMessage(obj.toString());
+                    } catch (JSONException e) {
+                        e.printStackTrace();
+                    }
+
+                } else {
+                    Log.e(TAG, "keepAlive failed websocket is null or not connected");
+                }
+            }
+        };
+        timer.schedule(timerTask,25000,25000);
+
+//        keepAliveThread = new Thread(new Runnable() {
+//            @Override
+//            public void run() {
+//                while (isKeepAliveRunning && !Thread.interrupted()) {
+//                    synchronized (this) {
+//                        try {
+//                            Thread.sleep(25 * 1000);
+//                        } catch (InterruptedException ex) {
+//                            ex.printStackTrace();
+//                        }
+//
+//                    }
+//                }
+//                Log.d(TAG, "keepAlive thread stopped");
+//            }
+//        }, "KeepAlive");
+//        keepAliveThread.start();
+    }
+
+    private void stopKeepAliveTimer() {
+        isKeepAliveRunning = false;
+//        if (keepAliveThread != null) {
+//            keepAliveThread.interrupt();
+//        }
+        if(timer!=null){
+            timer.purge();
+        }
+        if(timerTask!=null){
+            timerTask.cancel();
+        }
+    }
+
+    @Override
+    public void onClosed() {
+        stopKeepAliveTimer();
+    }
+
+    public String randomString(int length) {
+        String str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+        Random random = new Random();
+        StringBuilder sb = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            sb.append(str.charAt(random.nextInt(str.length())));
+        }
+        return sb.toString();
+    }
+
+    public interface DestroyRoomCallback {
+        void onSuccess();
+
+        void onFailed();
+    }
+
+    public interface JanusCallback {
+        void onCreateSession(BigInteger sessionId);
+
+        void onCreateRoom(BigInteger handleId);
+
+        void onAttached(BigInteger handleId);
+
+        /**
+         * 订阅回调
+         *
+         * @param subscribeHandleId 订阅HandlerId
+         * @param feedId            订阅 feedId
+         */
+        void onSubscribeAttached(BigInteger subscribeHandleId, BigInteger feedId);
+
+        void onDetached(BigInteger handleId);
+
+        void onHangup(BigInteger handleId);
+
+        void onMessage(BigInteger sender, BigInteger handleId, JSONObject msg, JSONObject jsep);
+
+        void onIceCandidate(BigInteger handleId, JSONObject candidate);
+
+        void onDestroySession(BigInteger sessionId);
+
+        void onError(int errorCode, String error);
+    }
+}

+ 33 - 0
janus/src/main/java/com/wdkl/ncs/janus/client/JanusMessageType.java

@@ -0,0 +1,33 @@
+package com.wdkl.ncs.janus.client;
+
+public enum JanusMessageType {
+    message,
+    trickle,
+    detach,
+    destroy,
+    keepalive,
+    create,
+    attach,
+    event,
+    error,
+    ack,
+    success,
+    webrtcup,
+    hangup,
+    detached,
+    media,
+    slowlink;
+
+    @Override
+    public String toString() {
+        return name();
+    }
+
+    public boolean EqualsString(String type) {
+        return this.toString().equals(type);
+    }
+
+    public static JanusMessageType fromString(String string) {
+        return (JanusMessageType) valueOf(JanusMessageType.class, string.toLowerCase());
+    }
+}

+ 19 - 0
janus/src/main/java/com/wdkl/ncs/janus/client/PluginHandle.java

@@ -0,0 +1,19 @@
+package com.wdkl.ncs.janus.client;
+
+import java.math.BigInteger;
+
+public class PluginHandle {
+    private BigInteger handleId;
+
+    public PluginHandle(BigInteger handleId) {
+        this.handleId = handleId;
+    }
+
+    public BigInteger getHandleId() {
+        return handleId;
+    }
+
+    public void setHandleId(BigInteger handleId) {
+        this.handleId = handleId;
+    }
+}

+ 47 - 0
janus/src/main/java/com/wdkl/ncs/janus/client/Transaction.java

@@ -0,0 +1,47 @@
+package com.wdkl.ncs.janus.client;
+
+import org.json.JSONObject;
+
+import java.math.BigInteger;
+
+/**
+ * a random string that the client can use to match incoming messages from the server (since, as explained in the Plugins documentation, all messages are asynchronous).
+ */
+public class Transaction {
+    private String tid;
+
+    /**
+     * 如果有 feed 说明这是一个订阅的 Transaction
+     */
+    private BigInteger feedId;
+
+    public Transaction(String tid) {
+        this.tid = tid;
+    }
+
+    public Transaction(String tid, BigInteger feedId) {
+        this.tid = tid;
+        this.feedId = feedId;
+    }
+
+    public void onError() {
+    }
+
+    public void onSuccess(JSONObject data) throws Exception {
+    }
+
+    public void onSuccess(JSONObject data, BigInteger feed) throws Exception {
+    }
+
+    public String getTid() {
+        return tid;
+    }
+
+    public BigInteger getFeedId() {
+        return feedId;
+    }
+
+    public void setFeedId(BigInteger feedId) {
+        this.feedId = feedId;
+    }
+}

+ 382 - 0
janus/src/main/java/com/wdkl/ncs/janus/client/VideoRoomCallback.java

@@ -0,0 +1,382 @@
+package com.wdkl.ncs.janus.client;
+
+import android.util.Log;
+
+
+import com.wdkl.ncs.janus.entity.MsgEvent;
+import com.wdkl.ncs.janus.entity.Publisher;
+import com.wdkl.ncs.janus.entity.Room;
+import com.wdkl.ncs.janus.rtc.Peer;
+import com.wdkl.ncs.janus.rtc.WebRTCEngine;
+import com.wdkl.ncs.janus.rtc.observer.CreateAnswerCallback;
+import com.wdkl.ncs.janus.rtc.observer.CreateOfferCallback;
+import com.wdkl.ncs.janus.rtc.observer.CreatePeerConnectionCallback;
+import com.wdkl.ncs.janus.util.EnumType;
+
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaStream;
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+
+import java.math.BigInteger;
+
+public class VideoRoomCallback implements JanusClient.JanusCallback {
+    private final static String TAG = VideoRoomCallback.class.getSimpleName();
+
+    private JanusClient janusClient;
+    private Peer peer;
+    private BigInteger videoRoomHandlerId;
+    private Room room;
+    private BigInteger userId;
+
+    public CallSessionCallback getCallSessionCallback() {
+        return callSessionCallback;
+    }
+
+    public void setCallSessionCallback(CallSessionCallback callSessionCallback) {
+        this.callSessionCallback = callSessionCallback;
+    }
+
+    private CallSessionCallback callSessionCallback;
+
+    public VideoRoomCallback(JanusClient janusClient, Room room, BigInteger userId) {
+        this.janusClient = janusClient;
+        this.room = room;
+        this.userId = userId;
+    }
+
+    @Override
+    public void onCreateRoom(BigInteger handleId) {
+        videoRoomHandlerId = handleId;
+        janusClient.joinRoom(handleId, userId.toString());
+    }
+
+    @Override
+    public void onCreateSession(BigInteger sessionId) {
+        janusClient.attachPlugin("janus.plugin.videoroom");
+    }
+
+    @Override
+    public void onAttached(BigInteger handleId) {
+        Log.d(TAG, "onAttached");
+        janusClient.createRoom(handleId, room.getId());
+    }
+
+    @Override
+    public void onSubscribeAttached(BigInteger subscriptionHandleId, BigInteger feedId) {
+        Publisher publisher = room.findPublisherById(feedId);
+        if (publisher != null) {
+            publisher.setHandleId(subscriptionHandleId);
+            // 订阅发布者
+            janusClient.subscribe(subscriptionHandleId, feedId);
+        }
+    }
+
+    @Override
+    public void onDetached(BigInteger handleId) {
+        videoRoomHandlerId = null;
+    }
+
+    @Override
+    public void onHangup(BigInteger handleId) {
+        if (callSessionCallback != null) {
+            callSessionCallback.didHangUp(handleId);
+        }
+        //todo 挂断
+//        EventBus.getDefault().post(new MsgEvent<BigInteger>(3));
+    }
+
+    @Override
+    public void onMessage(BigInteger sender, BigInteger handleId, JSONObject data, JSONObject jsep) {
+        if (!data.has("videoroom")) {
+            return;
+        }
+        try {
+            String type = data.getString("videoroom");
+            if ("joined".equals(type)) {
+
+                //================= WebRTC
+                //创建本地PeerConnection,设置本地媒体流
+                WebRTCEngine.getInstance().createLocalPeer(userId, peerConnectionCallback);
+                this.peer = WebRTCEngine.getInstance().getPeer(userId);
+
+                // 加入房间成功
+                // 发送 offer 和网关建立连接
+                peer.createOffer(new CreateOfferCallback() {
+                    @Override
+                    public void onCreateOfferSuccess(SessionDescription sdp) {
+                        // 发布
+                        janusClient.publish(videoRoomHandlerId, sdp, !WebRTCEngine.getInstance().mIsAudioOnly);
+                    }
+
+                    @Override
+                    public void onCreateFailed(String error) {
+
+                    }
+                });
+
+                //userId = BigInteger.valueOf(data.getInt("id"));  //用户id
+
+                JSONArray publishers = data.getJSONArray("publishers");
+                handleNewPublishers(publishers);
+            } else if ("event".equals(type)) {
+                if (data.has("configured") && data.getString("configured").equals("ok")
+                        && jsep != null) {
+                    // sdp 协商成功,收到网关发来的 sdp answer,开始接收
+                    String sdp = jsep.getString("sdp");
+
+                    peer.getPeerConnection().setRemoteDescription(new SdpObserver() {
+                        @Override
+                        public void onCreateSuccess(SessionDescription sdp) {
+                            Log.d(TAG, "setRemoteDescription onCreateSuccess");
+                        }
+
+                        @Override
+                        public void onSetSuccess() {
+                            Log.d(TAG, "setRemoteDescription onSetSuccess");
+                            //加载本地视频
+//                            EventBus.getDefault().post(new MsgEvent<BigInteger>(1));
+                            if (callSessionCallback != null) {
+                                callSessionCallback.didCreateLocalVideoTrack();
+                            }
+                        }
+
+                        @Override
+                        public void onCreateFailure(String error) {
+                            Log.d(TAG, "setRemoteDescription onCreateFailure " + error);
+                        }
+
+                        @Override
+                        public void onSetFailure(String error) {
+                            Log.d(TAG, "setRemoteDescription onSetFailure " + error);
+                        }
+                    }, new SessionDescription(SessionDescription.Type.ANSWER, sdp));
+                } else if (data.has("unpublished")) {
+                    Long unPublishdUserId = data.getLong("unpublished");
+                } else if (data.has("leaving")) {
+                    // 离开
+                    BigInteger leavingUserId = new BigInteger(data.getString("leaving"));
+                    room.removePublisherById(leavingUserId);
+                    //用户离开房间,此用户的peer应当从 engine 中清掉
+//                    WebRTCEngine.getInstance().leaveRoom(leavingUserId);
+
+                    if (room.getPublishers().size() <= 1) {
+                        janusClient.setCallState(EnumType.CallState.Idle);
+                        // EventBus.getDefault().post(new MsgEvent<BigInteger>(3,leavingUserId));
+                        if (callSessionCallback != null) {
+                            callSessionCallback.didUserLeave(leavingUserId);
+                        }
+                    }
+                } else if (data.has("publishers")) {
+                    // 新用户开始发布
+                    JSONArray publishers = data.getJSONArray("publishers");
+                    handleNewPublishers(publishers);
+                } else if (data.has("started") && data.getString("started").equals("ok")) {
+                    // 订阅 start 成功
+                    Log.d(TAG, "subscription started ok");
+                    if (callSessionCallback != null) {
+                        callSessionCallback.didChangeState(EnumType.CallState.Connected);
+                    }
+                }
+            } else if ("attached".equals(type) && jsep != null) {
+                // attach 到了一个Publisher 上,会收到网关转发来的sdp offer
+                String sdp = jsep.getString("sdp");
+                // plugindata.data.id 其它发布者的 userId
+                BigInteger feedId = new BigInteger(data.getString("id"));
+                String display = data.getString("display");
+                Publisher publisher = room.findPublisherById(feedId);
+
+                //创建新 peer connection
+                CreatePeerConnectionCallback remotePeerConnectionCallback = new CreatePeerConnectionCallback() {
+                    @Override
+                    public void onIceGatheringComplete() {
+                        janusClient.trickleCandidateComplete(sender);
+                    }
+
+                    @Override
+                    public void onIceCandidate(IceCandidate candidate) {
+                        janusClient.trickleCandidate(sender, candidate);
+                    }
+
+                    @Override
+                    public void onIceCandidatesRemoved(IceCandidate[] candidates) {
+
+                    }
+
+                    @Override
+                    public void onAddStream(MediaStream stream) {
+                        if (stream.videoTracks.size() > 0) {
+                            stream.audioTracks.get(0).setEnabled(true);
+                            WebRTCEngine.getInstance().getPeer(feedId)._remoteStream = stream;
+                            //todo:此处需其它回调
+                            janusClient.setCallState(EnumType.CallState.Connected);
+
+                            // todo: 添加用户到界面
+                            //handler.post(()->currentFragment.didReceiveRemoteVideoTrack(feedId));
+//                            EventBus.getDefault().post(new MsgEvent<BigInteger>(2, feedId));
+                            if (callSessionCallback != null) {
+                                callSessionCallback.didReceiveRemoteVideoTrack(feedId);
+                            }
+                        }
+                    }
+
+                    @Override
+                    public void onRemoveStream(MediaStream stream) {
+                        peer._remoteStream = null;
+                        //todo:此处需其它回调
+                    }
+
+                    @Override
+                    public void onIceConnected() {
+
+                    }
+
+                    @Override
+                    public void onIceDisconnected() {
+
+                    }
+
+                    @Override
+                    public void onIceConnectFail() {
+
+                    }
+                };
+                WebRTCEngine.getInstance().createRemotePeer(feedId, remotePeerConnectionCallback);
+
+                WebRTCEngine.getInstance().getPeer(feedId).getPeerConnection().setRemoteDescription(new SdpObserver() {
+                    @Override
+                    public void onCreateSuccess(SessionDescription sdp) {
+                        Log.d(TAG, "setRemoteDescription onCreateSuccess");
+                    }
+
+                    @Override
+                    public void onSetSuccess() {
+                        Log.d(TAG, "setRemoteDescription onSetSuccess");
+                        // 回复网关一个 start ,附带自己的 sdp answer
+                        WebRTCEngine.getInstance().getPeer(feedId).createAnswer(new CreateAnswerCallback() {
+                            @Override
+                            public void onSetAnswerSuccess(SessionDescription sdp) {
+                                janusClient.subscriptionStart(publisher.getHandleId(), sdp);
+                            }
+
+                            @Override
+                            public void onSetAnswerFailed(String error) {
+
+                            }
+                        });
+                    }
+
+                    @Override
+                    public void onCreateFailure(String error) {
+                        Log.d(TAG, "setRemoteDescription onCreateFailure " + error);
+                    }
+
+                    @Override
+                    public void onSetFailure(String error) {
+                        Log.d(TAG, "setRemoteDescription onSetFailure " + error);
+                    }
+                }, new SessionDescription(SessionDescription.Type.OFFER, sdp));
+            } else if ("destroyed".equals(type)) {
+//                EventBus.getDefault().post(new MsgEvent<BigInteger>(3));
+            }
+        } catch (JSONException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void onIceCandidate(BigInteger handleId, JSONObject candidate) {
+//        try {
+//            if (!candidate.has("completed")) {
+//                peer.getPeerConnection().addIceCandidate(new IceCandidate(candidate.getString("sdpMid"),
+//                        candidate.getInt("sdpMLineIndex"), candidate.getString("candidate")));
+//            }
+//        } catch (JSONException e) {
+//            e.printStackTrace();
+//        }
+    }
+
+    @Override
+    public void onDestroySession(BigInteger sessionId) {
+    }
+
+    @Override
+    public void onError(int errorCode, String error) {
+        if (errorCode == JanusClient.ERROR_CREATE_ROOM) {
+            Log.e(TAG, error);
+//            EventBus.getDefault().post(new MsgEvent<BigInteger>(4));
+            if (callSessionCallback != null) {
+                callSessionCallback.didError(error);
+            }
+        }
+    }
+
+    private void handleNewPublishers(JSONArray publishers) {
+        for (int i = 0; i < publishers.length(); i++) {
+            try {
+                JSONObject publishObj = publishers.getJSONObject(i);
+                BigInteger feedId = new BigInteger(publishObj.getString("id"));
+                String display = publishObj.getString("display");
+                // subscribeAttach 到发布者的 handle 上
+                janusClient.subscribeAttach(feedId);
+
+                room.addPublisher(new Publisher(feedId, display));
+            } catch (JSONException e) {
+                e.printStackTrace();
+            }
+        }
+
+        StringBuilder sb = new StringBuilder(512);
+        for (Publisher publisher : room.getPublishers()) {
+            sb.append("用户id: " + publisher.getId() + " " + publisher.getDisplay() + " " + publisher.getHandleId());
+        }
+        Log.d(TAG, "当前房间有 :" + room.getPublishers().size() + " 人," + sb.toString());
+    }
+
+    //收集candidate并发送
+    private CreatePeerConnectionCallback peerConnectionCallback = new CreatePeerConnectionCallback() {
+        @Override
+        public void onIceGatheringComplete() {
+            janusClient.trickleCandidateComplete(videoRoomHandlerId);
+        }
+
+        @Override
+        public void onIceCandidate(IceCandidate candidate) {
+            janusClient.trickleCandidate(videoRoomHandlerId, candidate);
+        }
+
+        @Override
+        public void onIceCandidatesRemoved(IceCandidate[] candidates) {
+            peer.getPeerConnection().removeIceCandidates(candidates);
+        }
+
+        @Override
+        public void onAddStream(MediaStream stream) {
+
+        }
+
+        @Override
+        public void onRemoveStream(MediaStream stream) {
+
+        }
+
+        @Override
+        public void onIceConnected() {
+
+        }
+
+        @Override
+        public void onIceDisconnected() {
+
+        }
+
+        @Override
+        public void onIceConnectFail() {
+
+        }
+    };
+}

+ 110 - 0
janus/src/main/java/com/wdkl/ncs/janus/client/WebSocketChannel.java

@@ -0,0 +1,110 @@
+package com.wdkl.ncs.janus.client;
+
+import android.util.Log;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.WebSocket;
+import okhttp3.WebSocketListener;
+
+/**
+ */
+public class WebSocketChannel {
+    private static final String TAG = WebSocketChannel.class.getSimpleName();
+    private WebSocket webSocket;
+    private boolean connected;
+    private WebSocketCallback webSocketCallback;
+    private String url;
+    private String lastMsg = null;
+
+    public void connect(String url) {
+        Log.d(TAG,"连接 websocket");
+        this.url = url;
+        OkHttpClient client = new OkHttpClient();
+        /* WebSocket 子协议只是添加一个 Sec-WebSocket-Protocol 的 http 请求头,告诉服务器我们要使用 janus-protocol 这种协议来通信了。
+         * Response 中也会返回这个头。
+         * Sec-WebSocket-Protocol=janus-protocol"
+         */
+        Request request = new Request.Builder()
+                .header("Sec-WebSocket-Protocol", "janus-protocol")
+                .url(url)
+                .build();
+        webSocket = client.newWebSocket(request, new WebSocketHandler());
+    }
+
+    public boolean isConnected() {
+        return connected;
+    }
+
+    public void sendMessage(String message) {
+        if (webSocket != null && connected) {
+            Log.d(TAG, "send==>>" + message);
+            webSocket.send(message);
+        } else {
+            //lastMsg = message;
+            Log.e(TAG, "send failed socket not connected");
+            //connect(url);
+        }
+    }
+
+    public void close() {
+        if (webSocket != null) {
+            webSocket.close(1000, "manual close");
+            webSocket = null;
+        }
+    }
+
+    private class WebSocketHandler extends WebSocketListener {
+        @Override
+        public void onOpen(WebSocket webSocket, Response response) {
+            connected = true;
+            Log.d(TAG, "onOpen");
+//            if (lastMsg!=null){
+//                sendMessage(lastMsg);
+//                lastMsg = null;
+//            }
+            if (webSocketCallback != null) {
+                webSocketCallback.onOpen();
+            }
+        }
+
+        @Override
+        public void onMessage(WebSocket webSocket, String text) {
+            Log.d(TAG, "onMessage " + text);
+            if (webSocketCallback != null) {
+                webSocketCallback.onMessage(text);
+            }
+        }
+
+        @Override
+        public void onClosed(WebSocket webSocket, int code, String reason) {
+            Log.d(TAG, "onClosed " + reason);
+            connected = false;
+            if (webSocketCallback != null) {
+                webSocketCallback.onClosed();
+            }
+        }
+
+        @Override
+        public void onFailure(WebSocket webSocket, Throwable t, Response response) {
+            Log.d(TAG, "onFailure " + t.getMessage());
+            connected = false;
+            if (webSocketCallback != null) {
+                webSocketCallback.onClosed();
+            }
+        }
+    }
+
+    public void setWebSocketCallback(WebSocketCallback webSocketCallback) {
+        this.webSocketCallback = webSocketCallback;
+    }
+
+    public interface WebSocketCallback {
+        void onOpen();
+
+        void onMessage(String text);
+
+        void onClosed();
+    }
+}

+ 35 - 0
janus/src/main/java/com/wdkl/ncs/janus/entity/MsgEvent.java

@@ -0,0 +1,35 @@
+package com.wdkl.ncs.janus.entity;
+
+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;
+    }
+}
+

+ 54 - 0
janus/src/main/java/com/wdkl/ncs/janus/entity/Publisher.java

@@ -0,0 +1,54 @@
+package com.wdkl.ncs.janus.entity;
+
+import java.math.BigInteger;
+
+/**
+ */
+public class Publisher {
+    private BigInteger id;
+    private String display;
+
+    private BigInteger handleId;
+
+    public Publisher() {
+
+    }
+
+    public Publisher(BigInteger id, String display) {
+        this.id = id;
+        this.display = display;
+    }
+
+    public BigInteger getId() {
+        return id;
+    }
+
+    public void setId(BigInteger id) {
+        this.id = id;
+    }
+
+    public String getDisplay() {
+        return display;
+    }
+
+    public void setDisplay(String display) {
+        this.display = display;
+    }
+
+    public BigInteger getHandleId() {
+        return handleId;
+    }
+
+    public void setHandleId(BigInteger handleId) {
+        this.handleId = handleId;
+    }
+
+    @Override
+    public String toString() {
+        return "Publisher{" +
+                "id=" + id +
+                ", display='" + display + '\'' +
+                ", handleId=" + handleId +
+                '}';
+    }
+}

+ 68 - 0
janus/src/main/java/com/wdkl/ncs/janus/entity/Room.java

@@ -0,0 +1,68 @@
+package com.wdkl.ncs.janus.entity;
+
+import java.math.BigInteger;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ */
+public class Room {
+    private BigInteger id;
+
+    public Room() {
+
+    }
+
+    public Room(BigInteger id) {
+        this.id = id;
+    }
+
+    private Set<Publisher> publishers = new HashSet<>();
+
+    public BigInteger getId() {
+        return id;
+    }
+
+    public void setId(BigInteger id) {
+        this.id = id;
+    }
+
+    public Set<Publisher> getPublishers() {
+        return publishers;
+    }
+
+    public void addPublisher(Publisher publisher) {
+        Iterator<Publisher> it = publishers.iterator();
+        boolean found = false;
+        while (it.hasNext()) {
+            Publisher next = it.next();
+            if (next.getId().equals(publisher.getId())) {
+                found = true;
+                break;
+            }
+        }
+        if (!found) {
+            publishers.add(publisher);
+        }
+    }
+
+    public Publisher findPublisherById(BigInteger id) {
+        for (Publisher next : publishers) {
+            if (next.getId().equals(id)) {
+                return next;
+            }
+        }
+        return null;
+    }
+
+    public void removePublisherById(BigInteger id) {
+        Iterator<Publisher> it = publishers.iterator();
+        while (it.hasNext()) {
+           Publisher next = it.next();
+            if (next.getId().equals(id)) {
+                it.remove();
+            }
+        }
+    }
+}

+ 24 - 0
janus/src/main/java/com/wdkl/ncs/janus/render/ProxyVideoSink.java

@@ -0,0 +1,24 @@
+package com.wdkl.ncs.janus.render;
+
+import org.webrtc.Logging;
+import org.webrtc.VideoFrame;
+import org.webrtc.VideoSink;
+
+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
janus/src/main/java/com/wdkl/ncs/janus/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.ncs.janus.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);
+    }
+  }
+}

+ 100 - 0
janus/src/main/java/com/wdkl/ncs/janus/rtc/AudioFocusManager.java

@@ -0,0 +1,100 @@
+package com.wdkl.ncs.janus.rtc;
+
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.os.Build;
+import android.util.Log;
+
+public class AudioFocusManager implements AudioManager.OnAudioFocusChangeListener {
+    private static String TAG = AudioFocusManager.class.getCanonicalName();
+
+    private AudioManager mAudioManager;
+    private AudioFocusRequest mFocusRequest;
+    private AudioAttributes mAudioAttributes;
+
+    private onRequestFocusResultListener mOnRequestFocusResultListener;
+    private OnAudioFocusChangeListener mAudioFocusChangeListener;
+
+    public AudioFocusManager(Context context) {
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+    }
+
+    public AudioFocusManager(AudioManager audioManager){
+        mAudioManager = audioManager;
+    }
+
+    /**
+     * Request audio focus.
+     */
+    public void requestFocus() {
+        int result;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            if (mFocusRequest == null) {
+                if (mAudioAttributes == null) {
+                    mAudioAttributes = new AudioAttributes.Builder()
+                            .setUsage(AudioAttributes.USAGE_MEDIA)
+                            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+                            .build();
+                }
+                mFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
+                        .setAudioAttributes(mAudioAttributes)
+                        .setWillPauseWhenDucked(true)
+                        .setOnAudioFocusChangeListener(this)
+                        .build();
+            }
+            result = mAudioManager.requestAudioFocus(mFocusRequest);
+        } else {
+            result = mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
+        }
+        if (mOnRequestFocusResultListener != null) {
+            mOnRequestFocusResultListener.onHandleResult(result);
+        }
+    }
+
+
+    @Override
+    public void onAudioFocusChange(int focusChange) {
+        Log.i(TAG,"onAudioFocusChange focusChange");
+        if (mAudioFocusChangeListener != null) {
+            mAudioFocusChangeListener.onAudioFocusChange(focusChange);
+        }
+    }
+
+    /**
+     * Release audio focus.
+     */
+    public void releaseAudioFocus() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            mAudioManager.abandonAudioFocusRequest(mFocusRequest);
+        } else {
+            mAudioManager.abandonAudioFocus(this);
+        }
+    }
+
+    /**
+     * Handle the result of audio focus.
+     */
+    public interface onRequestFocusResultListener {
+        void onHandleResult(int result);
+    }
+
+
+    public void setOnHandleResultListener(onRequestFocusResultListener listener) {
+        mOnRequestFocusResultListener = listener;
+    }
+
+
+    /**
+     * Same as AudioManager.OnAudioFocusChangeListener.
+     */
+    public interface OnAudioFocusChangeListener {
+        void onAudioFocusChange(int focusChange);
+    }
+
+
+    public void setOnAudioFocusChangeListener(OnAudioFocusChangeListener listener) {
+        mAudioFocusChangeListener = listener;
+    }
+}

+ 189 - 0
janus/src/main/java/com/wdkl/ncs/janus/rtc/Peer.java

@@ -0,0 +1,189 @@
+package com.wdkl.ncs.janus.rtc;
+
+import android.content.Context;
+import android.util.Log;
+
+
+import com.wdkl.ncs.janus.render.ProxyVideoSink;
+import com.wdkl.ncs.janus.rtc.observer.AnswerSdpObserver;
+import com.wdkl.ncs.janus.rtc.observer.CreateAnswerCallback;
+import com.wdkl.ncs.janus.rtc.observer.CreateOfferCallback;
+import com.wdkl.ncs.janus.rtc.observer.CreatePeerConnectionCallback;
+import com.wdkl.ncs.janus.rtc.observer.CustomPCObserver;
+import com.wdkl.ncs.janus.rtc.observer.OfferSdpObserver;
+
+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.SessionDescription;
+import org.webrtc.SurfaceViewRenderer;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+
+public class Peer {
+    private final static String TAG = "Peer";
+
+    private final PeerConnection peerConnection;
+    private final BigInteger mUserId;
+
+    private List<IceCandidate> queuedRemoteCandidates;
+    private SessionDescription localSdp;
+    private final PeerConnectionFactory mFactory;
+    private final List<PeerConnection.IceServer> mIceLis;
+    private ScheduledExecutorService executor;
+
+    public MediaStream _remoteStream;
+    public SurfaceViewRenderer renderer;
+    public ProxyVideoSink sink;
+
+    public Peer(PeerConnectionFactory factory, List<PeerConnection.IceServer> list, CreatePeerConnectionCallback peerConnectionCallback, ScheduledExecutorService executor, BigInteger userId) {
+        this.mFactory = factory;
+        this.mIceLis = list;
+        this.executor = executor;
+        this.mUserId = userId;
+        this.queuedRemoteCandidates = new ArrayList<>();
+        this.peerConnection = createPeerConnection(peerConnectionCallback);
+        Log.d(TAG, "create Peer:" + mUserId);
+    }
+
+    public PeerConnection createPeerConnection(CreatePeerConnectionCallback peerConnectionCallback) {
+        PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(mIceLis);
+        if (mFactory != null) {
+            return mFactory.createPeerConnection(rtcConfig, new CustomPCObserver(executor, peerConnectionCallback));
+        } else {
+            return null;
+        }
+    }
+
+    public PeerConnection getPeerConnection() {
+        return peerConnection;
+    }
+
+    public SessionDescription getLocalSdp() {
+        return localSdp;
+    }
+
+    public void setLocalSdp(SessionDescription localSdp) {
+        this.localSdp = localSdp;
+    }
+
+    // 创建offer
+    public void createOffer(CreateOfferCallback offerCallback){
+        if (peerConnection == null) return;
+        Log.d(TAG, "createOffer");
+        peerConnection.createOffer(new OfferSdpObserver(this, offerCallback), offerOrAnswerConstraint());
+    }
+
+    // 创建answer
+    public void createAnswer(CreateAnswerCallback answerCallback) {
+        if (peerConnection == null) return;
+        Log.d(TAG, "createAnswer");
+        peerConnection.createAnswer(new AnswerSdpObserver(this, answerCallback), offerOrAnswerConstraint());
+    }
+
+    //添加本地流
+    public void addLocalStream(MediaStream stream) {
+        if (peerConnection == null) return;
+        Log.d(TAG, "addLocalStream" + mUserId);
+        peerConnection.addStream(stream);
+    }
+
+    // 添加RemoteIceCandidate
+    public synchronized void addRemoteIceCandidate(final IceCandidate candidate) {
+        Log.d(TAG, "addRemoteIceCandidate");
+        if (peerConnection != null) {
+            if (queuedRemoteCandidates != null) {
+               Log.d(TAG, "addRemoteIceCandidate");
+                synchronized (Peer.class) {
+                    if (queuedRemoteCandidates != null) {
+                        queuedRemoteCandidates.add(candidate);
+                    }
+                }
+            } else {
+               Log.d(TAG, "addRemoteIceCandidate");
+                peerConnection.addIceCandidate(candidate);
+            }
+        }
+    }
+
+    // 移除RemoteIceCandidates
+    public void removeRemoteIceCandidates(final IceCandidate[] candidates) {
+        if (peerConnection == null) {
+            return;
+        }
+        drainCandidates();
+        peerConnection.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 (peerConnection != null) {
+            try {
+                peerConnection.close();
+                peerConnection.dispose();
+            } catch (Exception e) {
+
+            }
+        }
+    }
+
+    public void drainCandidates() {
+        Log.i(TAG, "drainCandidates");
+        synchronized (Peer.class) {
+            if (queuedRemoteCandidates != null) {
+                Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates");
+                for (IceCandidate candidate : queuedRemoteCandidates) {
+                    peerConnection.addIceCandidate(candidate);
+                }
+                queuedRemoteCandidates = null;
+            }
+        }
+    }
+
+    public 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);
+        //mediaConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
+        return mediaConstraints;
+    }
+}

+ 759 - 0
janus/src/main/java/com/wdkl/ncs/janus/rtc/WebRTCEngine.java

@@ -0,0 +1,759 @@
+package com.wdkl.ncs.janus.rtc;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothHeadset;
+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.util.Log;
+import android.view.View;
+
+
+import com.wdkl.ncs.janus.render.ProxyVideoSink;
+import com.wdkl.ncs.janus.rtc.observer.CreatePeerConnectionCallback;
+import com.wdkl.ncs.janus.util.Constant;
+
+import org.webrtc.AudioSource;
+import org.webrtc.AudioTrack;
+import org.webrtc.Camera1Enumerator;
+import org.webrtc.CameraEnumerator;
+import org.webrtc.CameraVideoCapturer;
+import org.webrtc.DefaultVideoDecoderFactory;
+import org.webrtc.DefaultVideoEncoderFactory;
+import org.webrtc.EglBase;
+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.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 org.webrtc.voiceengine.WebRtcAudioManager;
+import org.webrtc.voiceengine.WebRtcAudioUtils;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+public class WebRTCEngine {
+    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";
+    public static final String VIDEO_TRACK_TYPE = "video";
+    private static final String VIDEO_CODEC_VP8 = "VP8";
+    private static final String VIDEO_CODEC_VP9 = "VP9";
+    private static final String DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT = "DtlsSrtpKeyAgreement";
+    private static final int VIDEO_RESOLUTION_WIDTH = 1280;
+    private static final int VIDEO_RESOLUTION_HEIGHT = 800;
+    private static final int FPS = 30;
+    private String preferredVideoCodec;
+    private PeerConnectionParameters peerConnectionParameters;
+    private PeerConnectionFactory.Options options = null;
+
+    // 对话实例列表
+    private ConcurrentHashMap<BigInteger, Peer> peerMap;
+    // 服务器实例列表
+    private List<PeerConnection.IceServer> iceServers = new ArrayList<>();
+
+    public boolean mIsAudioOnly;
+    private Context mContext;
+    private AudioManager audioManager;
+    private boolean isSpeakerOn = true;
+
+    private AudioFocusManager audioFocusManager;
+    // 是否使用录屏
+    private boolean screencaptureEnabled = false;
+    private boolean isSwitch = false; // 是否正在切换摄像头
+
+    private ScheduledExecutorService executor;
+    private static final WebRTCEngine instance = new WebRTCEngine();
+    private WebRTCEngine() {
+        // Executor thread is started once in private ctor and is used for all
+        // peer connection API calls to ensure new peer connection factory is
+        // created on the same thread as previously destroyed factory.
+        executor = Executors.newSingleThreadScheduledExecutor();
+        peerMap = new ConcurrentHashMap<>();
+        // 初始化ice地址
+        initIceServer();
+    }
+    public static WebRTCEngine getInstance() {
+        return instance;
+    }
+
+    private void initIceServer(){
+        PeerConnection.IceServer iceServer = null;
+        for(String stunServer: Constant.STUN_SERVER) {
+            if (stunServer.contains("|"))
+            {
+                String[] stunParams = stunServer.split("|");
+                iceServer = PeerConnection.IceServer
+                        .builder(stunParams[0])
+                        .setUsername(stunParams[1])
+                        .setPassword(stunParams[2])
+                        .setTlsCertPolicy(PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK)
+                        .createIceServer();
+            } else {
+                iceServer = PeerConnection.IceServer
+                        .builder(stunServer)
+                        .setTlsCertPolicy(PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK)
+                        .createIceServer();
+            }
+            iceServers.add(iceServer);
+        }
+        if (Constant.TURN_SERVER != null){
+            for(String turnServer:Constant.TURN_SERVER){
+                if (turnServer.contains("|"))
+                {
+                    String[] turnParams = turnServer.split("|");
+                    iceServer = PeerConnection.IceServer
+                            .builder(turnParams[0])
+                            .setUsername(turnParams[1])
+                            .setPassword(turnParams[2])
+                            .setTlsCertPolicy(PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK)
+                            .createIceServer();
+                } else {
+                    iceServer = PeerConnection.IceServer
+                            .builder(turnServer)
+                            .setTlsCertPolicy(PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK)
+                            .createIceServer();
+                }
+                iceServers.add(iceServer);
+            }
+        }
+    }
+
+    public static class PeerConnectionParameters {
+        public final boolean tracing;
+        public final int videoWidth;
+        public final int videoHeight;
+        public final int videoFps;
+        public final String videoCodec;
+        public final boolean videoCodecHwAcceleration;
+        public final int audioStartBitrate;
+        public final String audioCodec;
+        public final boolean noAudioProcessing;
+        public final boolean useOpenSLES;
+        public final boolean disableBuiltInAEC;
+        public final boolean disableBuiltInAGC;
+        public final boolean disableBuiltInNS;
+
+        public PeerConnectionParameters(boolean tracing,
+                                        int videoWidth, int videoHeight, int videoFps, String videoCodec,
+                                        boolean videoCodecHwAcceleration, int audioStartBitrate, String audioCodec,
+                                        boolean noAudioProcessing, boolean useOpenSLES, boolean disableBuiltInAEC,
+                                        boolean disableBuiltInAGC, boolean disableBuiltInNS) {
+            this.tracing = tracing;
+            this.videoWidth = videoWidth;
+            this.videoHeight = videoHeight;
+            this.videoFps = videoFps;
+            this.videoCodec = videoCodec;
+            this.videoCodecHwAcceleration = videoCodecHwAcceleration;
+            this.audioStartBitrate = audioStartBitrate;
+            this.audioCodec = audioCodec;
+            this.noAudioProcessing = noAudioProcessing;
+            this.useOpenSLES = useOpenSLES;
+            this.disableBuiltInAEC = disableBuiltInAEC;
+            this.disableBuiltInAGC = disableBuiltInAGC;
+            this.disableBuiltInNS = disableBuiltInNS;
+        }
+    }
+
+    public void setOptions(PeerConnectionFactory.Options options) {
+        this.options = options;
+    }
+
+    public PeerConnectionFactory createPeerConnectionFactory() {
+
+        // 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());
+        if (encoderFactory == null || decoderFactory == null){
+
+        }
+
+        // 构造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();
+    }
+
+    private PeerConnectionFactory createPeerConnectionFactory1() {
+
+        final VideoEncoderFactory encoderFactory;
+        final VideoDecoderFactory decoderFactory;
+
+        encoderFactory = new DefaultVideoEncoderFactory(
+                mRootEglBase.getEglBaseContext(), false /* enableIntelVp8Encoder */, true);
+        decoderFactory = new DefaultVideoDecoderFactory(mRootEglBase.getEglBaseContext());
+
+        PeerConnectionFactory.initialize(PeerConnectionFactory.InitializationOptions.builder(mContext)
+                .setEnableInternalTracer(true)
+                .createInitializationOptions());
+
+        PeerConnectionFactory.Builder builder = PeerConnectionFactory.builder()
+                .setVideoEncoderFactory(encoderFactory)
+                .setVideoDecoderFactory(decoderFactory);
+
+        Log.d(TAG,
+                "Create peer connection factory. Use video: true");
+
+        // Initialize field trials.
+        PeerConnectionFactory.initializeFieldTrials("");
+
+        // Check preferred video codec.
+        preferredVideoCodec = VIDEO_CODEC_VP8;
+        if (peerConnectionParameters!=null) {
+            if (peerConnectionParameters.videoCodec != null) {
+                if (peerConnectionParameters.videoCodec.equals(VIDEO_CODEC_VP9)) {
+                    preferredVideoCodec = VIDEO_CODEC_VP9;
+                } else if (peerConnectionParameters.videoCodec.equals(VIDEO_CODEC_H264)) {
+                    preferredVideoCodec = VIDEO_CODEC_H264;
+                }
+            }
+        }
+        Log.d(TAG, "Pereferred video codec: " + preferredVideoCodec);
+
+        // Enable/disable OpenSL ES playback.
+        if (peerConnectionParameters!=null && !peerConnectionParameters.useOpenSLES) {
+            Log.d(TAG, "Disable OpenSL ES audio even if device supports it");
+            WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true /* enable */);
+        } else {
+            Log.d(TAG, "Allow OpenSL ES audio if device supports it");
+            WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(false);
+        }
+
+        if (peerConnectionParameters!=null && peerConnectionParameters.disableBuiltInAEC) {
+            Log.d(TAG, "Disable built-in AEC even if device supports it");
+            WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
+        } else {
+            Log.d(TAG, "Enable built-in AEC if device supports it");
+            WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(false);
+        }
+
+        if (peerConnectionParameters!=null && peerConnectionParameters.disableBuiltInAGC) {
+            Log.d(TAG, "Disable built-in AGC even if device supports it");
+            WebRtcAudioUtils.setWebRtcBasedAutomaticGainControl(true);
+        } else {
+            Log.d(TAG, "Enable built-in AGC if device supports it");
+            WebRtcAudioUtils.setWebRtcBasedAutomaticGainControl(false);
+        }
+
+        if (peerConnectionParameters!=null && peerConnectionParameters.disableBuiltInNS) {
+            Log.d(TAG, "Disable built-in NS even if device supports it");
+            WebRtcAudioUtils.setWebRtcBasedNoiseSuppressor(true);
+        } else {
+            Log.d(TAG, "Enable built-in NS if device supports it");
+            WebRtcAudioUtils.setWebRtcBasedNoiseSuppressor(false);
+        }
+
+        if (options != null) {
+            builder.setOptions(options);
+            Log.d(TAG, "Factory networkIgnoreMask option: " + options.networkIgnoreMask);
+        }
+        factory = builder.createPeerConnectionFactory();
+        Log.d(TAG, "Peer connection factory created.");
+        return factory;
+    }
+
+    /**
+     * 创建媒体方式
+     *
+     * @return VideoCapturer
+     */
+    private VideoCapturer createVideoCapture() {
+        if (screencaptureEnabled) {
+            return createScreenCapturer();
+        }
+
+        return createCameraCapture(new Camera1Enumerator(true));
+    }
+
+    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 VideoCapturer createCameraCapture(CameraEnumerator enumerator) {
+        final String[] deviceNames = enumerator.getDeviceNames();
+        Log.d(TAG, "createCameraCapture devices: " + deviceNames.length);
+
+        // First, try to find front facing camera
+        for (String deviceName : deviceNames) {
+            Log.d(TAG, "createCameraCapture device name: " + deviceName);
+            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 MediaConstraints createAudioConstraints() {
+        MediaConstraints audioConstraints = new MediaConstraints();
+
+        //回声消除
+        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
+        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation2", "true"));
+        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googDAEchoCancellation", "true"));
+        //自动增益
+        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
+        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl2", "true"));
+        //噪音处理
+        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
+        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression2", "true"));
+        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googTypingNoiseDetection", "true"));
+        //高音过滤
+        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAudioMirroring", "false"));
+        audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));
+        return audioConstraints;
+    }
+
+    // -----------------------------------对外方法------------------------------------------
+    public void init(boolean mIsAudioOnly, Context mContext) {
+        this.mIsAudioOnly = mIsAudioOnly;
+        this.mContext = mContext;
+        audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+        if (mRootEglBase == null) {
+            mRootEglBase = EglBase.create();
+        }
+        if (factory == null) {
+            factory = createPeerConnectionFactory();
+        }
+        if (_localStream == null) {
+            createLocalStream();
+        }
+    }
+
+    /**
+     * 创建本地流
+     */
+    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);
+        }
+    }
+
+    /**
+     * 开始本地预览
+     * @param isOverlay
+     * @return
+     */
+    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;
+    }
+
+    /**
+     * 静音
+     * @param enable
+     * @return
+     */
+    public boolean muteAudio(boolean enable) {
+        if (_localAudioTrack != null) {
+            _localAudioTrack.setEnabled(!enable);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 结束本地预览
+     */
+    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();
+        }
+    }
+
+    /**
+     * 切换外放
+     * @param enable
+     * @return
+     */
+    public boolean toggleSpeaker(boolean enable) {
+        if (audioManager != null) {
+            isSpeakerOn = enable;
+            audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+
+            if (enable) {
+                if (!setBluetoothHeadsetOn()) {   //优先蓝牙
+                    audioManager.setStreamVolume(AudioManager.MODE_IN_COMMUNICATION,
+                            audioManager.getStreamMaxVolume(AudioManager.MODE_IN_COMMUNICATION),
+                            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_MUSIC,
+                        audioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
+                        AudioManager.FX_KEY_CLICK
+                );
+                audioManager.setSpeakerphoneOn(false);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 切换耳机
+     * @param isHeadset
+     * @return
+     */
+    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;
+    }
+
+    /**
+     * 蓝牙
+     * @return
+     */
+    public boolean setBluetoothHeadsetOn(){
+        if (!isBluetoothHeadsetConnected()){
+            return false;
+        }
+        if (audioManager != null) {
+            audioManager.setStreamVolume(
+                    AudioManager.STREAM_MUSIC,
+                    audioManager.getStreamVolume(AudioManager.STREAM_MUSIC),
+                    AudioManager.FX_KEY_CLICK
+            );
+            audioManager.setSpeakerphoneOn(false);
+
+            audioManager.startBluetoothSco();
+            audioManager.setBluetoothScoOn(true);
+
+            audioFocusManager = new AudioFocusManager(audioManager);
+            audioFocusManager.requestFocus();
+
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 蓝牙是否连接
+     * @return
+     */
+    public boolean isBluetoothHeadsetConnected(){
+        BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        boolean isBluetoothHeadsetConnected =  (bluetoothAdapter != null && bluetoothAdapter.isEnabled()
+                && bluetoothAdapter.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothHeadset.STATE_CONNECTED);
+
+        return isBluetoothHeadsetConnected;
+    }
+
+    public boolean isHeadphonesPlugged() {
+        if (audioManager == null) {
+            return false;
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            @SuppressLint("WrongConstant") 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();
+        }
+    }
+
+    /**
+     * 切换摄像头
+     */
+    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");
+        }
+    }
+
+    public void release() {
+        if (audioManager != null) {
+            audioManager.setMode(AudioManager.MODE_NORMAL);
+
+            if (isBluetoothHeadsetConnected()) {
+                audioManager.stopBluetoothSco();
+                audioManager.setBluetoothScoOn(false);
+            }
+        }
+        if (audioFocusManager!=null){
+            audioFocusManager.releaseAudioFocus();
+        }
+
+        //释放所有连接
+        if (peerMap != null) {
+            for (Map.Entry<BigInteger, Peer> entry: peerMap.entrySet()) {
+                if (entry.getValue() != null) {
+                    entry.getValue().close();
+                }
+            }
+            peerMap.clear();
+        }
+
+        // 停止预览
+        stopPreview();
+
+//        if (factory != null) {
+//            factory.dispose();
+//            factory = null;
+//        }
+//
+//        if (mRootEglBase != null) {
+//            mRootEglBase.release();
+//            mRootEglBase = null;
+//        }
+
+        PeerConnectionFactory.stopInternalTracingCapture();
+        PeerConnectionFactory.shutdownInternalTracer();
+    }
+
+    // ---------------------------------------webrtc
+    public void createLocalPeer(List<BigInteger> userIds, CreatePeerConnectionCallback peerConnectionCallback){
+        for(BigInteger userId:userIds){
+            Peer peer = new Peer(factory,iceServers,peerConnectionCallback,executor,userId);
+            peer.addLocalStream(_localStream);
+            peerMap.put(userId, peer);
+        }
+        if (isHeadphonesPlugged()) {
+            toggleHeadset(true);
+        } else {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+            } else {
+                audioManager.setMode(AudioManager.MODE_IN_CALL);
+            }
+        }
+    }
+
+    public void createLocalPeer(BigInteger userId, CreatePeerConnectionCallback peerConnectionCallback){
+        List<BigInteger> userIds = new ArrayList<>();
+        userIds.add(userId);
+        createLocalPeer(userIds,peerConnectionCallback);
+    }
+
+    public void createRemotePeer(BigInteger userId, CreatePeerConnectionCallback peerConnectionCallback){
+        Peer peer = new Peer(factory,iceServers,peerConnectionCallback,executor,userId);
+        peerMap.put(userId, peer);
+    }
+
+    public void leaveRoom(BigInteger userId) {
+        Peer peer = peerMap.get(userId);
+        if (peer != null) {
+            peer.close();
+            peerMap.remove(userId);
+        }
+        Log.d(TAG, "leaveRoom peers.size() = " + peerMap.size());
+        if (peerMap.size() <= 1) {
+            if (peerMap.size() == 1) {
+                for (Map.Entry<BigInteger, Peer> set : peerMap.entrySet()) {
+                    set.getValue().close();
+                }
+                peerMap.clear();
+            }
+        }
+    }
+
+    public View setupRemoteVideo(BigInteger userId, boolean isO) {
+        Peer peer = peerMap.get(userId);
+        if (peer == null) return null;
+
+        if (peer.renderer == null) {
+            peer.createRender(mRootEglBase, mContext, isO);
+        }
+
+        return peer.renderer;
+    }
+
+    public Peer getPeer(BigInteger userId) {
+        return peerMap.get(userId);
+    }
+}

+ 72 - 0
janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/AnswerSdpObserver.java

@@ -0,0 +1,72 @@
+package com.wdkl.ncs.janus.rtc.observer;
+
+import android.util.Log;
+
+
+import com.wdkl.ncs.janus.rtc.Peer;
+
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+
+public class AnswerSdpObserver implements SdpObserver {
+    private final static String TAG = AnswerSdpObserver.class.getSimpleName();
+
+    Peer peer;
+    CreateAnswerCallback callback;
+
+    public AnswerSdpObserver(Peer peer, CreateAnswerCallback callback){
+        this.peer = peer;
+        this.callback = callback;
+    }
+
+    @Override
+    public void onCreateSuccess(SessionDescription sdp) {
+        Log.d(TAG, "createOffer onCreateSuccess " + sdp.toString());
+        peer.getPeerConnection().setLocalDescription(new SdpObserver() {
+            @Override
+            public void onCreateSuccess(SessionDescription sdp) {
+                Log.d(TAG, "createAnswer setLocalDescription onCreateSuccess");
+            }
+
+            @Override
+            public void onSetSuccess() {
+                Log.d(TAG, "createAnswer setLocalDescription onSetSuccess");
+                // send answer sdp
+                if (callback != null) {
+                    callback.onSetAnswerSuccess(sdp);
+                }
+            }
+
+            @Override
+            public void onCreateFailure(String error) {
+                Log.d(TAG, "createAnswer setLocalDescription onCreateFailure " + error);
+                if (callback != null) {
+                    callback.onSetAnswerFailed(error);
+                }
+            }
+
+            @Override
+            public void onSetFailure(String error) {
+                Log.d(TAG, "createAnswer setLocalDescription onSetFailure " + error);
+                if (callback != null) {
+                    callback.onSetAnswerFailed(error);
+                }
+            }
+        }, sdp);
+    }
+
+    @Override
+    public void onSetSuccess() {
+        Log.d(TAG, "createAnswer onSetSuccess");
+    }
+
+    @Override
+    public void onCreateFailure(String s) {
+        Log.d(TAG, "createAnswer onCreateFailure " + s);
+    }
+
+    @Override
+    public void onSetFailure(String s) {
+        Log.d(TAG, "createAnswer onSetFailure " + s);
+    }
+}

+ 9 - 0
janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreateAnswerCallback.java

@@ -0,0 +1,9 @@
+package com.wdkl.ncs.janus.rtc.observer;
+
+import org.webrtc.SessionDescription;
+
+public interface CreateAnswerCallback {
+    void onSetAnswerSuccess(SessionDescription sdp);
+
+    void onSetAnswerFailed(String error);
+}

+ 9 - 0
janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreateOfferCallback.java

@@ -0,0 +1,9 @@
+package com.wdkl.ncs.janus.rtc.observer;
+
+import org.webrtc.SessionDescription;
+
+public interface CreateOfferCallback {
+    void onCreateOfferSuccess(SessionDescription sdp);
+
+    void onCreateFailed(String error);
+}

+ 20 - 0
janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreatePeerConnectionCallback.java

@@ -0,0 +1,20 @@
+package com.wdkl.ncs.janus.rtc.observer;
+
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaStream;
+
+public interface CreatePeerConnectionCallback {
+    void onIceGatheringComplete();
+
+    void onIceCandidate(IceCandidate candidate);
+
+    void onIceCandidatesRemoved(IceCandidate[] candidates);
+
+    void onAddStream(MediaStream stream);
+
+    void onRemoveStream(MediaStream stream);
+
+    void onIceConnected();
+    void onIceDisconnected();
+    void onIceConnectFail();
+}

+ 128 - 0
janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CustomPCObserver.java

@@ -0,0 +1,128 @@
+package com.wdkl.ncs.janus.rtc.observer;
+
+import android.util.Log;
+
+import org.webrtc.DataChannel;
+import org.webrtc.IceCandidate;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.RtpReceiver;
+
+import java.util.concurrent.ScheduledExecutorService;
+
+public class CustomPCObserver implements PeerConnection.Observer {
+    private final static String TAG = CustomPCObserver.class.getSimpleName();
+
+    private ScheduledExecutorService executor;
+    private CreatePeerConnectionCallback callback;
+
+    public CustomPCObserver(ScheduledExecutorService executor, CreatePeerConnectionCallback callback) {
+        this.executor = executor;
+        this.callback = callback;
+    }
+    @Override
+    public void onIceCandidate(final IceCandidate candidate) {
+        Log.d(TAG,"onIceCandidate");
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                if (callback != null) {
+                    callback.onIceCandidate(candidate);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onIceCandidatesRemoved(final IceCandidate[] candidates) {
+        Log.d(TAG, "onIceCandidatesRemoved");
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                if (callback != null) {
+                    callback.onIceCandidatesRemoved(candidates);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onSignalingChange(PeerConnection.SignalingState newState) {
+        Log.d(TAG, "SignalingState: " + newState);
+    }
+
+    @Override
+    public void onIceConnectionChange(final PeerConnection.IceConnectionState newState) {
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                Log.d(TAG, "IceConnectionState: " + newState);
+                if (newState == PeerConnection.IceConnectionState.CONNECTED) {
+                    callback.onIceConnected();
+                } else if (newState == PeerConnection.IceConnectionState.DISCONNECTED) {
+                    callback.onIceDisconnected();
+                } else if (newState == PeerConnection.IceConnectionState.FAILED) {
+                    callback.onIceConnectFail();
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onIceGatheringChange(PeerConnection.IceGatheringState newState) {
+        Log.d(TAG, "onIceGatheringChange " + newState);
+        if (newState == PeerConnection.IceGatheringState.COMPLETE) {
+            if (callback != null) {
+                callback.onIceGatheringComplete();
+            }
+        }
+    }
+
+    @Override
+    public void onIceConnectionReceivingChange(boolean receiving) {
+        Log.d(TAG, "onIceConnectionReceivingChange changed to " + receiving);
+    }
+
+    @Override
+    public void onAddStream(final MediaStream stream) {
+        Log.d(TAG, "onAddStream");
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                if (callback != null) {
+                    callback.onAddStream(stream);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onRemoveStream(final MediaStream stream) {
+        Log.d(TAG, "onRemoveStream");
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                if (callback != null) {
+                    callback.onRemoveStream(stream);
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onDataChannel(final DataChannel dc) {
+        Log.d(TAG, "New Data channel " + dc.label());
+
+    }
+
+    @Override
+    public void onRenegotiationNeeded() {
+        // No need to do anything; AppRTC follows a pre-agreed-upon
+        // signaling/negotiation protocol.
+    }
+
+    @Override
+    public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
+
+    }
+}

+ 79 - 0
janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/OfferSdpObserver.java

@@ -0,0 +1,79 @@
+package com.wdkl.ncs.janus.rtc.observer;
+
+import android.util.Log;
+
+
+import com.wdkl.ncs.janus.rtc.Peer;
+
+import org.webrtc.SdpObserver;
+import org.webrtc.SessionDescription;
+
+public class OfferSdpObserver implements SdpObserver {
+    private final static String TAG = OfferSdpObserver.class.getSimpleName();
+
+    Peer peer;
+    CreateOfferCallback callback;
+
+    public OfferSdpObserver(Peer peer, CreateOfferCallback callback){
+        this.peer = peer;
+        this.callback = callback;
+    }
+
+    @Override
+    public void onCreateSuccess(SessionDescription originalSdp) {
+        Log.d(TAG, "createOffer sdp创建成功       " + originalSdp.type);
+        final SessionDescription sdp = new SessionDescription(originalSdp.type, originalSdp.description);
+        peer.setLocalSdp(sdp);
+        peer.getPeerConnection().setLocalDescription(new SdpObserver() {
+            @Override
+            public void onCreateSuccess(SessionDescription sessionDescription) {
+                Log.d(TAG, "setLocalDescription sdp创建成功       " + sessionDescription.type);
+            }
+
+            @Override
+            public void onSetSuccess() {
+                Log.d(TAG, "setLocalDescription onSetSuccess");
+                if (peer.getPeerConnection().getRemoteDescription()==null){
+                    //发出offer
+                    if (callback != null) {
+                        callback.onCreateOfferSuccess(sdp);
+                    }
+                } else {
+                    Log.d(TAG, "Remote SDP set succesfully");
+                    peer.drainCandidates();
+                }
+            }
+
+            @Override
+            public void onCreateFailure(String s) {
+                Log.d(TAG, "setLocalDescription onCreateFailure   " + s);
+            }
+
+            @Override
+            public void onSetFailure(String s) {
+                Log.d(TAG, "setLocalDescription onSetFailure   " + s);
+            }
+        },sdp);
+    }
+
+    @Override
+    public void onSetSuccess() {
+        Log.d(TAG, "createOffer onSetSuccess");
+    }
+
+    @Override
+    public void onCreateFailure(String s) {
+        Log.d(TAG, "createOffer onCreateFailure " + s);
+        if (callback != null) {
+            callback.onCreateFailed(s);
+        }
+    }
+
+    @Override
+    public void onSetFailure(String s) {
+        Log.d(TAG, "createOffer onSetFailure " + s);
+        if (callback != null) {
+            callback.onCreateFailed(s);
+        }
+    }
+}

+ 21 - 0
janus/src/main/java/com/wdkl/ncs/janus/util/Constant.java

@@ -0,0 +1,21 @@
+package com.wdkl.ncs.janus.util;
+
+
+import com.wdkl.ncs.framework.beans.ServerInfo;
+import com.wdkl.ncs.framework.utils.ServerInfoUtil;
+
+public class Constant {
+
+    public final static ServerInfo serverInfo = ServerInfoUtil.getServerInfo();
+
+//    public final static String GATEWAY_URL="8.129.220.143";
+    public final static String GATEWAY_URL=(serverInfo==null?"172.28.100.100":serverInfo.getWebrtcServer());
+    public final static String GATEWAY_WS_PORT=(serverInfo==null?"8188":serverInfo.getWebrtcServerPort().toString());
+//    public final static String GATEWAY_URL="120.76.246.253";
+//    public final static String GATEWAY_WS_PORT="5089";
+    public final static String JANUS_URL = "ws://"+Constant.GATEWAY_URL+":" + Constant.GATEWAY_WS_PORT;
+
+//    public final static String[] STUN_SERVER = new String[]{"stun:120.76.246.253:3478"};
+public final static String[] STUN_SERVER = new String[]{"stun:"+(serverInfo==null?GATEWAY_URL:serverInfo.getWebrtcStunServer())+":"+(serverInfo==null?"3478":serverInfo.getWebrtcStunServerPort().toString())};
+    public final static String[] TURN_SERVER = null; //new String[]{"turn:stun.l.google.com:19302|username|password"};
+}

+ 37 - 0
janus/src/main/java/com/wdkl/ncs/janus/util/EnumType.java

@@ -0,0 +1,37 @@
+package com.wdkl.ncs.janus.util;
+
+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,
+    }
+
+
+}

+ 123 - 0
janus/src/main/java/com/wdkl/ncs/janus/util/OSUtils.java

@@ -0,0 +1,123 @@
+package com.wdkl.ncs.janus.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;
+
+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;
+    }
+}

+ 17 - 0
janus/src/test/java/com/wdkl/ncs/janus/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.wdkl.ncs.janus;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 1 - 1
settings.gradle

@@ -1,2 +1,2 @@
-include ':app',':framework',':webrtc',':rtc-chat',':libwebrtc'
+include ':app',':framework',':janus'//:webrtc',':rtc-chat',':libwebrtc'
 rootProject.name = "entraceguard"