Przeglądaj źródła

集成janus信令,模拟分机呼叫特殊处理,因为转换盒同一时间只能创建一个通话,但是可能会存在多个分机呼叫情况,所以分机呼叫的时候若主机接听了则需要主机来创建通话,分机转换盒这边被动加入通话。这个和其他豪华版分机处理不同

weizhengliang 3 lat temu
rodzic
commit
3dd681df94
45 zmienionych plików z 4093 dodań i 238 usunięć
  1. 0 11
      app/src/main/code/com/wdkl/app/ncs/application/Application.kt
  2. 3 3
      build.gradle
  3. 4 2
      conversion_box/build.gradle
  4. 87 186
      conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/activity/MainActivity.kt
  5. 0 0
      conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/fragment/BaseCallFragment.kt.bak
  6. 266 0
      conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/fragment/CallFragment.kt
  7. 0 0
      conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/fragment/SkyCallFragment.kt.bak
  8. 0 3
      conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/helper/AppUpdateHelper.java
  9. 189 0
      conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/helper/AsyncPlayer.java
  10. 1 2
      conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/helper/CallDialogHelper.java
  11. 0 2
      conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/helper/RingPlayHelper.java
  12. 56 0
      conversion_box/src/main/res/layout/call_fragment_layout.xml
  13. 27 26
      conversion_box/src/main/res/layout/main_activity_layout.xml
  14. 1 0
      janus/.gitignore
  15. 40 0
      janus/build.gradle
  16. 0 0
      janus/consumer-rules.pro
  17. 21 0
      janus/proguard-rules.pro
  18. 6 0
      janus/src/main/AndroidManifest.xml
  19. 26 0
      janus/src/main/java/com/wdkl/ncs/janus/client/CallSessionCallback.java
  20. 878 0
      janus/src/main/java/com/wdkl/ncs/janus/client/JanusClient.java
  21. 33 0
      janus/src/main/java/com/wdkl/ncs/janus/client/JanusMessageType.java
  22. 19 0
      janus/src/main/java/com/wdkl/ncs/janus/client/PluginHandle.java
  23. 47 0
      janus/src/main/java/com/wdkl/ncs/janus/client/Transaction.java
  24. 383 0
      janus/src/main/java/com/wdkl/ncs/janus/client/VideoRoomCallback.java
  25. 110 0
      janus/src/main/java/com/wdkl/ncs/janus/client/WebSocketChannel.java
  26. 35 0
      janus/src/main/java/com/wdkl/ncs/janus/entity/MsgEvent.java
  27. 54 0
      janus/src/main/java/com/wdkl/ncs/janus/entity/Publisher.java
  28. 68 0
      janus/src/main/java/com/wdkl/ncs/janus/entity/Room.java
  29. 24 0
      janus/src/main/java/com/wdkl/ncs/janus/render/ProxyVideoSink.java
  30. 171 0
      janus/src/main/java/com/wdkl/ncs/janus/render/VideoFileRenderer.java
  31. 100 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/AudioFocusManager.java
  32. 189 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/Peer.java
  33. 757 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/WebRTCEngine.java
  34. 72 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/AnswerSdpObserver.java
  35. 9 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreateAnswerCallback.java
  36. 9 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreateOfferCallback.java
  37. 20 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreatePeerConnectionCallback.java
  38. 128 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CustomPCObserver.java
  39. 79 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/OfferSdpObserver.java
  40. 14 0
      janus/src/main/java/com/wdkl/ncs/janus/util/Constant.java
  41. 37 0
      janus/src/main/java/com/wdkl/ncs/janus/util/EnumType.java
  42. 123 0
      janus/src/main/java/com/wdkl/ncs/janus/util/OSUtils.java
  43. 2 0
      middleware/src/main/code/com/wdkl/ncs/android/middleware/common/Constant.java
  44. 2 2
      rtc-chat/src/main/java/com/wdkl/skywebrtc/engine/webrtc/WebRTCEngine.java
  45. 3 1
      settings.gradle

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

@@ -9,11 +9,6 @@ import com.enation.javashop.utils.base.config.BaseConfig
 import com.wdkl.app.ncs.conversion_box.helper.AnrFcExceptionUtil
 import com.wdkl.app.ncs.conversion_box.helper.NetHelper
 //import com.wdkl.app.ncs.conversion_box.helper.XCrashUtils
-import com.wdkl.core.socket.SocketManager
-import com.wdkl.core.voip.VoipEvent
-import com.wdkl.net.HttpRequestPresenter
-import com.wdkl.net.urlconn.UrlConnRequest
-import com.wdkl.skywebrtc.SkyEngineKit
 import serialporttest.utils.SerialPortUtil
 
 /**
@@ -96,12 +91,6 @@ class Application : BaseApplication() {
             SerialPortUtil.getInstance().openSerialPort()
         }
 
-        // 初始化网络请求
-        HttpRequestPresenter.init(UrlConnRequest())
-        // 初始化信令
-        SkyEngineKit.init(VoipEvent())
-        SocketManager.getInstance().init(applicationContext)
-
         //anr catcher
         AnrFcExceptionUtil.getInstance(this).initFCException()
 

+ 3 - 3
build.gradle

@@ -2,7 +2,7 @@ buildscript {
     /**
      * Kotlin统一版本
      */
-    ext.kotlin_version = '1.2.40'
+    ext.kotlin_version = '1.3.21'
 
     /**
      * Aop编制版本
@@ -47,12 +47,12 @@ buildscript {
     /**
      * APP版本码
      */
-    ext.app_version_code = 13
+    ext.app_version_code = 14
 
     /**
      * APP版本号
      */
-    ext.app_version = "1.0.13"
+    ext.app_version = "1.1.1"
 
     /**
      * 项目依赖库

+ 4 - 2
conversion_box/build.gradle

@@ -81,11 +81,13 @@ dependencies {
     //compile project(':bedlib')
 
     //web rtc
-    compile project(':webrtc')
+    //compile project(':webrtc')
     //compile project(':libwebrtc')
-    compile project(':rtc-chat')
+    //compile project(':rtc-chat')
     compile project(':welcome')
 
+    compile project(':janus')
+
     if(componentTag){
         debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
         releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'

+ 87 - 186
conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/activity/MainActivity.kt

@@ -22,10 +22,6 @@ import com.wdkl.app.ncs.conversion_box.fragment.*
 import com.wdkl.app.ncs.conversion_box.helper.*
 import com.wdkl.app.ncs.conversion_box.launch.MainLaunch
 import com.wdkl.app.ncs.conversion_box.settings.SettingConfig
-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.android.lib.base.BaseActivity
 import com.wdkl.ncs.android.lib.base.BaseApplication
 import com.wdkl.ncs.android.lib.utils.*
@@ -49,10 +45,12 @@ import com.wdkl.ncs.android.middleware.tcp.enums.DeviceTypeEnum
 import com.wdkl.ncs.android.middleware.tcp.enums.TcpAction
 import com.wdkl.ncs.android.middleware.tcp.enums.TcpType
 import com.wdkl.ncs.android.middleware.utils.AppUtil
-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.EnumType
 import kotlinx.android.synthetic.main.conversion_box_main_lay.*
 import kotlinx.android.synthetic.main.main_activity_layout.*
 import kotlinx.android.synthetic.main.sky_voice_call_layout.*
@@ -64,6 +62,7 @@ import serialporttest.utils.SerialPort485Util
 import serialporttest.utils.SerialPortUtil
 import java.io.DataOutputStream
 import java.io.IOException
+import java.math.BigInteger
 import java.util.*
 import java.util.UUID
 import kotlin.collections.ArrayList
@@ -71,7 +70,7 @@ import kotlin.collections.ArrayList
 
 @Router(path = "/conversion_box/main")
 class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBinding>(), MainActivityContract.View,
-    SerialPortUtil.ISerialPortBedOnclickEvent, SerialPort485Util.ISerialPortData, IUserState, CallSession.CallSessionCallback {
+    SerialPortUtil.ISerialPortBedOnclickEvent, SerialPort485Util.ISerialPortData {
     private val TAG = "MainActivity"
 
     private lateinit var receiver: TimeReceiver
@@ -93,12 +92,13 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
     private var isMacRegister: Boolean = false
 
     private var showMcuVersion = false
-    private var callEnded: Boolean = false
     private var tcpConnect: Boolean = false
     private var loaded: Boolean = false
 
-    private var gEngineKit: SkyEngineKit? = null
-    private val handler = Handler(Looper.getMainLooper())
+    //通话界面fragment
+    private var callFragment: Fragment? = null
+    private var targetSip = ""
+
     //正在通话分机串口地址
     private var curDeviceUart = ""
     //当前呼叫或通话分机id
@@ -134,9 +134,6 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
 
         EventBus.getDefault().register(this)
 
-        // 添加登录回调
-        SocketManager.getInstance().addUserStateCallback(this)
-
         //串口监听
         setSerialListner()
         SerialPortHelper.sipRegState("2")
@@ -145,7 +142,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
         updateNetState()
 
         //启动主fragment
-        switchFragment(R.id.callingbed2_main_frame, MainFragment(), mainFragment)
+        switchFragment(R.id.conversion_main_frame, MainFragment(), mainFragment)
 
         val buildUrl = UrlManager.build()
         serverIp =  buildUrl.buyer.substringAfterLast("//").substringBefore(":")
@@ -170,25 +167,6 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
         }
     }
 
-    override fun userLogin() {
-        updateStatus(SipStatus.REGISTERCOM)
-        SerialPortHelper.sipRegState("1")
-    }
-
-    override fun userLogout() {
-        updateStatus(SipStatus.REGISTERFAIL)
-        SerialPortHelper.sipRegState("2")
-    }
-
-    override fun userError() {
-        updateStatus(SipStatus.REGISTERFAIL)
-        SerialPortHelper.sipRegState("2")
-        //退出登录等待重连
-        if (SocketManager.getInstance().socketOpen()) {
-            SocketManager.getInstance().unConnect()
-        }
-    }
-
     //开启网络调试
     private fun openNetwrokDebug() {
         val commands = arrayListOf(
@@ -240,7 +218,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
         tv_btn_home.setOnClickListener {
             //首页
             if (!mainFragment.equals(curFragment)) {
-                switchFragment(R.id.callingbed2_main_frame, MainFragment(), mainFragment)
+                switchFragment(R.id.conversion_main_frame, MainFragment(), mainFragment)
             }
         }
 
@@ -384,7 +362,6 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
 
         //设备启用,进入正常模式
         if (Constant.DEVICE_STATUS == 1) {
-            connectSocket()
             tv_device_status.setText("设备状态: 已启用")
             if (!isMacRegister) {
                 isMacRegister = true
@@ -409,29 +386,6 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
         //checkAppVersion()
     }
 
-    private fun connectSocket(){
-        //拿到sip账号之后再注册
-        if (!TextUtils.isEmpty(Constant.SIP_ID)) {
-            // 连接socket登录
-            SocketManager.getInstance().connect(Urls.WS, Constant.SIP_ID, 0)
-            initSkyEngine()
-        }
-    }
-
-    private fun initSkyEngine() {
-        try {
-            SkyEngineKit.init(VoipEvent())
-            gEngineKit = SkyEngineKit.Instance()
-        } catch (e: NotInitializedException) {
-            SkyEngineKit.init(VoipEvent())
-            try {
-                gEngineKit = SkyEngineKit.Instance()
-            } catch (ex: NotInitializedException) {
-                ex.printStackTrace()
-            }
-        }
-    }
-
     private fun initCallTimer() {
         var overTime = SettingConfig.getSipOverTime(activity)
         if (overTime <= 0) {
@@ -561,7 +515,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                             //分机接听
                             RingPlayHelper.stopRingTone()
                             VoiceUtil.acceptAudioCall(curDeviceId, fromId, curInteractionVO!!.id)
-                            checkCallSuccess()
+                            showCallFragment()
                         } else {
                             //分机呼叫
                             if (curInteractionVO == null) {
@@ -576,8 +530,11 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                         //分机挂断
                         if (Constant.CALL_STATE == Constant.CALL_CALLING) {
                             if (addr.equals(curDeviceUart, true)) {
-                                gEngineKit!!.endCall()
+                                /*EventBus.getDefault().post(MessageEvent("handoff", Constant.EVENT_END_CALL))
                                 Constant.CALL_STATE = Constant.CALL_STANDBY
+                                VoiceUtil.handoffAudioCall(curDeviceId, fromId, curInteractionVO!!.id)*/
+
+                                handoffCall()
                             } else {
                                 cancelOutCall(addr, false)
                             }
@@ -660,7 +617,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
     }
 
     //分机接听后检查通话是否正常连接上
-    private fun checkCallSuccess() {
+    /*private fun checkCallSuccess() {
         //3s还未连接上则判定为通话失败
         Handler().postDelayed({
             val session = gEngineKit!!.currentSession
@@ -673,10 +630,10 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                 }
             }
         }, 3000)
-    }
+    }*/
 
     private fun startCall(uart: String) {
-        if (SocketManager.getInstance().socketOpen() && Constant.TCP_CONNECTED) {
+        if (Constant.TCP_CONNECTED) {
             //通过串口地址查找到对应分机设备id
             if (FrameHelper.frameDeviceList.size > 0 ) {
                 for (frameDevice in FrameHelper.frameDeviceList) {
@@ -762,7 +719,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
 
 
     //创建会话
-    private fun startSipCall(targetId: String): Boolean {
+    /*private fun startSipCall(targetId: String): Boolean {
         val room = UUID.randomUUID().toString() + System.currentTimeMillis()
         val b = gEngineKit!!.startOutCall(activity, room, targetId, true)
         if (b) {
@@ -775,10 +732,10 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
             }
         }
         return b
-    }
+    }*/
 
     //语音接通
-    private fun joinAudioCall() {
+    /*private fun joinAudioCall() {
         val session = gEngineKit?.getCurrentSession()
         if (session != null) {
             Log.e("dds", "audio call session state: " + session.state)
@@ -789,7 +746,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                 callEnd()
             }
         }
-    }
+    }*/
 
     //通过床位分机查找所在房间的门灯
     private fun getDoorLightAddr(bedAddr: String) : String? {
@@ -944,7 +901,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
 
     fun handleTcpModel(messageEvent: MessageEvent) {
         when (messageEvent.getType()) {
-            Constant.EVENT_SIP_CALL_STATUS -> {
+            /*Constant.EVENT_SIP_CALL_STATUS -> {
                 //收到sip通话邀请,加入通话
                 Log.e(TAG, "EVENT_SIP_CALL_STATUS: " + messageEvent.message)
                 if (messageEvent.message is String) {
@@ -958,7 +915,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                         joinAudioCall()
                     }, 300)
                 }
-            }
+            }*/
 
             //Sip注册状态
             Constant.EVENT_SIP_REGISTER_STATUS -> {
@@ -993,6 +950,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                             curDeviceUart = interactionVO.toEthMac.toUpperCase(Locale.ROOT)
                             curInteractionVO = interactionVO
                             curDeviceId = interactionVO.toDeviceId
+                            targetSip = interactionVO.fromSipId
 
                             //关闭门灯
                             val doorAddr = getDoorLightAddr(curDeviceUart)
@@ -1005,7 +963,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                             if (Constant.autoAnswer) {
                                 //自动接听
                                 VoiceUtil.acceptAudioCall(tcpModel.toId, fromId, interactionVO?.id)
-                                checkCallSuccess()
+                                showCallFragment()
                             } else {
                                 //响铃并手动接听
                                 SerialPortHelper.callInChannel(curDeviceUart)
@@ -1019,7 +977,10 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                             curDeviceUart = interactionVO.fromEthMac.toUpperCase(Locale.ROOT)
                             curInteractionVO = interactionVO
                             curDeviceId = interactionVO.fromDeviceId
-                            if (!startSipCall(curInteractionVO!!.toSipId)) {
+                            targetSip = interactionVO.toSipId
+                            showCallFragment()
+
+                            /*if (!startSipCall(curInteractionVO!!.toSipId)) {
                                 //通话失败,重置并返回主界面
                                 Constant.CALL_STATE = Constant.CALL_STANDBY
                                 updateCallText("待机中")
@@ -1030,9 +991,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                                     fromId,
                                     curInteractionVO!!.id
                                 )
-                            } else {
-                                checkCallSuccess()
-                            }
+                            }*/
                             //关闭门灯
                             val doorAddr = getDoorLightAddr(curDeviceUart)
                             if (!TextUtils.isEmpty(doorAddr)) {
@@ -1117,6 +1076,7 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                                     }*/
                                 }
                             //}
+                            EventBus.getDefault().post(MessageEvent("endcall", Constant.EVENT_END_CALL))
                         } else if (tcpModel.getAction() == TcpAction.VoiceAction.CANCEL) {
                             //对方呼叫时取消
                             val interactionVO = Gson().fromJson(tcpModel.data.toString(), InteractionVO::class.java)
@@ -1146,10 +1106,13 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                         } else if (tcpModel.action == TcpAction.VoiceAction.RS485ACCEPT) {
                             RingPlayHelper.stopRingTone()
                             VoiceUtil.acceptAudioCall(curDeviceId, fromId, curInteractionVO!!.id)
-                            checkCallSuccess()
+                            showCallFragment()
                         } else if (tcpModel.action == TcpAction.VoiceAction.RS485HANDOFF) {
-                            gEngineKit!!.endCall()
+                            /*EventBus.getDefault().post(MessageEvent("handoff", Constant.EVENT_END_CALL))
                             Constant.CALL_STATE = Constant.CALL_STANDBY
+                            VoiceUtil.handoffAudioCall(curDeviceId, fromId, curInteractionVO!!.id)*/
+
+                            handoffCall()
                         } else if (tcpModel.action == TcpAction.VoiceAction.RS485REJECT) {
                             val ethMac = tcpModel.data.toString()
                             rejectCall()
@@ -1217,114 +1180,66 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
             Constant.EVENT_RESTART_APP -> {
                 AppUpdateHelper.restartApp(activity)
             }
-        }
-    }
-
 
-    /********************************************************
-     ********************* webrtc通话回调 ********************
-     * 注意: 如涉及到UI更新的需要在主线程处理,务必注意
-     *******************************************************/
-    override fun didChangeState(state: EnumType.CallState?) {
-        Log.e("dds", "didChangeState: " + state)
-        handler.post {
-            if (state == EnumType.CallState.Connected) {
-                //更新界面显示
-                callEnded = false
-                Constant.CALL_STATE = Constant.CALL_CALLING
-                updateCallText("通话中")
-                SerialPortHelper.openSoundChannel(curDeviceUart)
-            }
-        }
-    }
+            Constant.EVENT_REMOVE_CALL_FRAGMENT -> {
+                SerialPortHelper.closeSoundChannel(curDeviceUart)
+                Constant.CALL_STATE = Constant.CALL_STANDBY
+                updateCallText("待机中")
 
-    override fun didDisconnected(userId: String?) {
-        handler.post {
-            callEnd()
-        }
-    }
+                //VoiceUtil.handoffAudioCall(curDeviceId, fromId, curInteractionVO!!.id)
 
-    override fun didError(error: String?) {
-        handler.post {
-            callEnd()
+                removeCallFragment()
+            }
         }
     }
 
-    //处理本地视频画面
-    override fun didCreateLocalVideoTrack() {
-        Log.e("dds", "didCreateLocalVideoTrack")
+    fun inCalling() {
+        Constant.CALL_STATE = Constant.CALL_CALLING
+        updateCallText("通话中")
+        SerialPortHelper.openSoundChannel(curDeviceUart)
     }
 
-    //处理远端视频画面
-    override fun didReceiveRemoteVideoTrack(userId: String?) {
-        Log.e("dds", "didReceiveRemoteVideoTrack  userId: " + userId)
+    fun handoffCall() {
+        EventBus.getDefault().post(MessageEvent("handoff", Constant.EVENT_END_CALL))
+        VoiceUtil.handoffAudioCall(curDeviceId, fromId, curInteractionVO!!.id)
+        Constant.CALL_STATE = Constant.CALL_STANDBY
     }
 
-    override fun didCallEndWithReason(callEndReason: EnumType.CallEndReason?) {
-        handler.post {
-            when (callEndReason) {
-                EnumType.CallEndReason.Busy -> {
-                    //showMessage("对方忙线中")
-                }
-                EnumType.CallEndReason.AcceptByOtherClient -> {
-                    //showMessage("通话中")
-                }
-                EnumType.CallEndReason.Hangup -> {
-                    //showMessage("挂断")
-                }
-                EnumType.CallEndReason.MediaError -> {
-                    //showMessage("媒体错误")
-                }
-                EnumType.CallEndReason.OpenCameraFailure -> {
-                    //showMessage("打开摄像头错误")
-                }
-                EnumType.CallEndReason.RemoteHangup -> {
-                    showMessage("对方挂断")
-                }
-                EnumType.CallEndReason.RemoteSignalError -> {
-                    showMessage("对方网络断开")
-                }
-                EnumType.CallEndReason.SignalError -> {
-                    showMessage("连接断开")
-                }
-                EnumType.CallEndReason.Timeout -> {
-                    showMessage("对方未接听")
-                }
-            }
-
-            callEnd()
+    private fun showCallFragment() {
+        //模拟分机呼叫特殊性,所有呼叫都是通过转换盒,但是转换盒只有一台,同一时间只能建立一个呼叫,为保证其他分机也能呼叫(不是通话),
+        //所有模拟分机的通话都由对方来创建room,然后转换盒加入room来通话,所以房间id都是对方的,转换盒是被动加入通话
+        if (callFragment == null) {
+            var fragment = CallFragment()
+            var bundle = Bundle()
+            bundle.putInt("call_state", 1)
+            bundle.putBoolean("audio_only", true)
+            bundle.putString("targetSip", targetSip)
+            fragment.arguments = bundle
+            addCallFragment(fragment)
         }
     }
 
-    override fun didChangeMode(isAudioOnly: Boolean) {
-        handler.post {
-            //
+    private fun addCallFragment(fragment: Fragment) {
+        if (callFragment != null) {
+            supportFragmentManager.beginTransaction()
+                .remove(callFragment)
+                .commit()
+            callFragment = null
         }
-    }
 
-    override fun didUserLeave(userId: String?) {
-        handler.post {
-            callEnd()
-        }
+        callFragment = fragment
+        supportFragmentManager.beginTransaction()
+            .add(R.id.call_frame, fragment)
+            .commit()
     }
 
-    //通话结束
-    private fun callEnd() {
-        if (callEnded) {
-            return
-        }
-        callEnded = true
-
-        Log.e("dds", "call end !!!!!!!!!!!!!!!!!!")
-        VoiceUtil.handoffAudioCall(curDeviceId, fromId, curInteractionVO!!.id)
-
-        if (gEngineKit != null && gEngineKit!!.currentSession != null && gEngineKit!!.currentSession.state != EnumType.CallState.Idle) {
-            gEngineKit!!.endCall()
+    private fun removeCallFragment() {
+        if (callFragment != null) {
+            supportFragmentManager.beginTransaction()
+                .remove(callFragment)
+                .commit()
+            callFragment = null
         }
-
-        SerialPortHelper.closeSoundChannel(curDeviceUart)
-        Constant.CALL_STATE = Constant.CALL_STANDBY
-        updateCallText("待机中")
     }
 
 
@@ -1389,12 +1304,12 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
     private fun updateTcpState() {
         if (Constant.TCP_CONNECTED) {
             view_title_layout_iv_tcp.setImageResource(R.mipmap.ic_tcp_success)
-            if (initialized && Constant.DEVICE_STATUS == 1 && SocketManager.getInstance().userState == 0) {
-                //如果socket断开了则重连
-                connectSocket()
-            }
+            view_title_layout_tv_point.setBackgroundResource(R.color.green)
+            SerialPortHelper.sipRegState("1")
         } else {
             view_title_layout_iv_tcp.setImageResource(R.mipmap.ic_tcp_fail)
+            view_title_layout_tv_point.setBackgroundResource(R.color.red_color)
+            SerialPortHelper.sipRegState("2")
         }
     }
 
@@ -1460,12 +1375,6 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                     updateSettings(false)
                     updateTcpState()
 
-                    if (SocketManager.getInstance().userState == 1) {
-                        view_title_layout_tv_point.setBackgroundResource(R.color.green)
-                    } else {
-                        view_title_layout_tv_point.setBackgroundResource(R.color.red_color)
-                    }
-
                     if (Constant.LATER_RESTART && Constant.CALL_STATE == Constant.CALL_STANDBY) {
                         AppUpdateHelper.restartApp(activity)
                     }
@@ -1476,14 +1385,6 @@ class MainActivity :BaseActivity<MainActivityPresenter, MainActivityLayoutBindin
                 updateNetState()
                 if (!initialized) {
                     presenter.loadTcpServerHost()
-                } else if (NetHelper.getInstance().networkAvailable()) {
-                    /*if (!Constant.TCP_CONNECTED && !TextUtils.isEmpty(Constant.TCP_SERVER_URL)) {
-                        TcpClient.getInstance().doConnect()
-                    }*/
-                    if (Constant.DEVICE_STATUS == 1 && SocketManager.getInstance().userState == 0) {
-                        //如果socket断开了则重连
-                        connectSocket()
-                    }
                 }
             }
         }

conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/fragment/BaseCallFragment.kt → conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/fragment/BaseCallFragment.kt.bak


+ 266 - 0
conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/fragment/CallFragment.kt

@@ -0,0 +1,266 @@
+package com.wdkl.app.ncs.conversion_box.fragment
+
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.support.v4.app.Fragment
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.enation.javashop.utils.base.tool.BaseToolActivity
+import com.wdkl.app.ncs.conversion_box.R
+import com.wdkl.app.ncs.conversion_box.activity.MainActivity
+import com.wdkl.app.ncs.conversion_box.helper.SerialPortHelper
+import com.wdkl.ncs.android.lib.utils.showMessage
+import com.wdkl.ncs.android.middleware.common.Constant
+import com.wdkl.ncs.android.middleware.common.MessageEvent
+import com.wdkl.ncs.android.middleware.tcp.channel.VoiceUtil
+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.EnumType
+import kotlinx.android.synthetic.main.call_fragment_layout.*
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import java.math.BigInteger
+
+class CallFragment: Fragment(), CallSessionCallback {
+
+    private lateinit var baseActivity: BaseToolActivity
+    private val handler = Handler(Looper.getMainLooper())
+
+    private var janusClient: JanusClient? = null
+    private var room: Room?=null
+    private var videoRoomCallback: VideoRoomCallback? = null
+    private var outGoing = false
+    private var targetSip = ""
+
+    //通话状态:0-去电, 1-来电
+    protected var callState : Int = 0
+    protected var onlyAudio: Boolean = true
+
+    private var callEnded: Boolean = false
+    private var callSuccess: Boolean = false
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        retainInstance = true
+
+        callState = arguments.getInt("call_state")
+        onlyAudio = arguments.getBoolean("audio_only")
+        targetSip = arguments.getString("targetSip", "")
+    }
+
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+
+        /**初始化宿主Activity*/
+        baseActivity = activity as BaseToolActivity
+
+        return inflater.inflate(R.layout.call_fragment_layout, null)
+    }
+
+    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        init()
+    }
+
+    private fun init() {
+        //初始化 engine
+        WebRTCEngine.getInstance().init(true, this.context)
+        //初始化 janusClient
+        janusClient = JanusClient(com.wdkl.ncs.janus.util.Constant.JANUS_URL, Constant.SIP_ID.toBigInteger())
+
+        if (callState == 0) {
+            //去电
+            outGoing = true
+            janusClient!!.callState = EnumType.CallState.Outgoing
+            room = Room(Constant.SIP_ID.toBigInteger())
+        } else if (callState == 1) {
+            //来电
+            outGoing = false
+            janusClient!!.callState = EnumType.CallState.Incoming
+            room = Room(targetSip.toBigInteger())
+        }
+
+        videoRoomCallback = VideoRoomCallback(janusClient, room, Constant.SIP_ID.toBigInteger())
+        videoRoomCallback!!.callSessionCallback = this
+        janusClient!!.setJanusCallback(videoRoomCallback)
+
+        //1s后再加入
+        handler.postDelayed({
+            if (janusClient != null && !callEnded) {
+                janusClient!!.connect()
+            }
+        }, 1000)
+
+        //5s之后通话还未成功直接退出
+        handler.postDelayed({
+            if (!callSuccess && !callEnded) {
+                if (activity != null) {
+                    (activity as MainActivity).handoffCall()
+                }
+            }
+        }, 5000)
+    }
+
+    override fun onStart() {
+        EventBus.getDefault().register(this)
+        super.onStart()
+    }
+
+    override fun onStop() {
+        EventBus.getDefault().unregister(this)
+        super.onStop()
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        Constant.CALL_STATE = Constant.CALL_STANDBY
+        handler.removeCallbacksAndMessages(null)
+    }
+
+
+
+    /********************************************************
+     ********************* webrtc通话回调 ********************
+     * 注意: 如涉及到UI更新的需要在主线程处理,务必注意
+     *******************************************************/
+    override fun didChangeState(state: EnumType.CallState?) {
+        Log.e("dds", "didChangeState: " + state)
+        handler.post {
+            if (state == EnumType.CallState.Connected && !callEnded) {
+                //更新界面显示
+                callSuccess = true
+                Constant.CALL_STATE = Constant.CALL_CALLING
+                if (tv_call_text != null) {
+                    tv_call_text.setText("通话中...")
+                }
+                if (activity != null) {
+                    (activity as MainActivity).inCalling()
+                }
+            }
+        }
+    }
+
+    override fun didDisconnected(userId: String?) {
+        handler.post {
+            callEnd()
+        }
+    }
+
+    override fun didError(error: String?) {
+        handler.post {
+            callEnd()
+        }
+    }
+
+    //处理本地视频画面
+    override fun didCreateLocalVideoTrack() {
+        Log.e("dds", "didCreateLocalVideoTrack")
+    }
+
+    //处理远端视频画面
+    override fun didReceiveRemoteVideoTrack(userId: BigInteger?) {
+        Log.e("dds", "didReceiveRemoteVideoTrack  userId: " + userId)
+    }
+
+    override fun didCallEndWithReason(callEndReason: EnumType.CallEndReason?) {
+        handler.post {
+            when (callEndReason) {
+                EnumType.CallEndReason.Busy -> {
+                    //showMessage("对方忙线中")
+                }
+                EnumType.CallEndReason.AcceptByOtherClient -> {
+                    //showMessage("通话中")
+                }
+                EnumType.CallEndReason.Hangup -> {
+                    //showMessage("挂断")
+                }
+                EnumType.CallEndReason.MediaError -> {
+                    //showMessage("媒体错误")
+                }
+                EnumType.CallEndReason.OpenCameraFailure -> {
+                    //showMessage("打开摄像头错误")
+                }
+                EnumType.CallEndReason.RemoteHangup -> {
+                    showMessage("挂断")
+                }
+                EnumType.CallEndReason.RemoteSignalError -> {
+                    showMessage("网络断开")
+                }
+                EnumType.CallEndReason.SignalError -> {
+                    showMessage("连接断开")
+                }
+                EnumType.CallEndReason.Timeout -> {
+                    showMessage("未接听")
+                }
+            }
+
+            callEnd()
+        }
+    }
+
+    override fun didChangeMode(isAudioOnly: Boolean) {
+        handler.post {
+            //
+        }
+    }
+
+    override fun didUserLeave(userId: BigInteger?) {
+        handler.post {
+            callEnd()
+        }
+    }
+
+    override fun didHangUp(handlerId: BigInteger) {
+        Log.e("hangup", "socket hangup")
+        handler.post {
+            callEnd()
+        }
+    }
+
+    //通话结束
+    private fun callEnd() {
+        if (callEnded) {
+            return
+        }
+        callEnded = true
+
+        Log.e("dds", "call end !!!!!!!!!!!!!!!!!!")
+
+        if (janusClient!!.webSocketChannel != null) {
+            janusClient!!.callState = EnumType.CallState.Idle
+            if (outGoing) {
+                janusClient!!.destroyRoom(janusClient!!.currentHandleId, null)
+            } else {
+                janusClient!!.leaveRoom()
+            }
+
+            janusClient!!.disConnect()
+        }
+
+        targetSip = ""
+        Constant.CALL_STATE = Constant.CALL_STANDBY
+
+        backToMain()
+    }
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    fun onMoonEvent(messageEvent: MessageEvent) {
+        when (messageEvent.getType()) {
+            Constant.EVENT_END_CALL -> {
+                callEnd()
+            }
+        }
+    }
+
+    //返回主界面
+    private fun backToMain() {
+        EventBus.getDefault().post(MessageEvent("BackCall", Constant.EVENT_REMOVE_CALL_FRAGMENT))
+    }
+}

conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/fragment/SkyCallFragment.kt → conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/fragment/SkyCallFragment.kt.bak


+ 0 - 3
conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/helper/AppUpdateHelper.java

@@ -13,9 +13,6 @@ import android.os.Environment;
 import android.util.Log;
 
 import com.wdkl.ncs.android.component.welcome.activity.WelcomeActivity;
-import com.wdkl.skywebrtc.CallSession;
-import com.wdkl.skywebrtc.EnumType;
-import com.wdkl.skywebrtc.SkyEngineKit;
 
 import java.io.BufferedReader;
 import java.io.File;

+ 189 - 0
conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/helper/AsyncPlayer.java

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

+ 1 - 2
conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/helper/CallDialogHelper.java

@@ -13,7 +13,6 @@ import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 
 import com.wdkl.app.ncs.conversion_box.R;
-import com.wdkl.core.voip.AsyncPlayer;
 
 public class CallDialogHelper {
 
@@ -41,7 +40,7 @@ public class CallDialogHelper {
             //去电
             outCall.setVisibility(View.VISIBLE);
             inCall.setVisibility(View.GONE);
-            ringPlayer.play(activity, R.raw.wr_ringback, true, AudioManager.STREAM_MUSIC);
+            ringPlayer.play(activity, R.raw.ring_back2, true, AudioManager.STREAM_MUSIC);
         } else {
             //来电
             outCall.setVisibility(View.GONE);

+ 0 - 2
conversion_box/src/main/java/com/wdkl/app/ncs/conversion_box/helper/RingPlayHelper.java

@@ -3,8 +3,6 @@ package com.wdkl.app.ncs.conversion_box.helper;
 import android.content.Context;
 import android.media.AudioManager;
 
-import com.wdkl.core.voip.AsyncPlayer;
-
 public class RingPlayHelper {
 
     private static AsyncPlayer ringPlayer;

+ 56 - 0
conversion_box/src/main/res/layout/call_fragment_layout.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@color/black">
+
+    <TextView
+        android:id="@+id/tv_call_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:gravity="center"
+        android:text="连接中..."
+        android:textColor="#9E9E9F"
+        android:textSize="20sp" />
+
+    <LinearLayout
+        android:id="@+id/call_btn_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_centerHorizontal="true"
+        android:gravity="bottom"
+        android:orientation="horizontal">
+
+        <View
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" />
+
+        <ImageView
+            android:id="@+id/voice_call_ring_hangoff"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            android:src="@drawable/selector_call_hangup" />
+
+        <View
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" />
+
+        <ImageView
+            android:id="@+id/voice_call_ring_pickup_audio"
+            android:layout_width="40dp"
+            android:layout_height="40dp"
+            android:src="@drawable/selector_call_answer" />
+
+        <View
+            android:layout_width="0dp"
+            android:layout_height="1dp"
+            android:layout_weight="1" />
+    </LinearLayout>
+
+</RelativeLayout>
+</layout>

+ 27 - 26
conversion_box/src/main/res/layout/main_activity_layout.xml

@@ -77,21 +77,29 @@
                 android:textSize="24sp"/>
         </RelativeLayout>
 
-        <!--fragment区域-->
+        <!--设备列表fragment区域-->
         <FrameLayout
-            android:id="@+id/callingbed2_main_frame"
+            android:id="@+id/conversion_main_frame"
             android:layout_width="580dp"
-            android:layout_height="match_parent"
+            android:layout_height="400dp"
             android:layout_marginTop="10dp"
             android:layout_marginBottom="10dp"
             android:layout_marginRight="10dp"
-            android:layout_above="@id/ll_bottom"
             android:layout_below="@id/activity_calling_bed_layout_title" />
 
+        <!--通话界面-->
+        <FrameLayout
+            android:id="@+id/call_frame"
+            android:layout_width="580dp"
+            android:layout_height="match_parent"
+            android:layout_marginRight="10dp"
+            android:layout_below="@id/conversion_main_frame"
+            android:layout_above="@id/ll_bottom" />
+
         <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:layout_toRightOf="@id/callingbed2_main_frame"
+            android:layout_toRightOf="@id/conversion_main_frame"
             android:layout_below="@id/activity_calling_bed_layout_title"
             android:layout_above="@id/ll_bottom"
             android:orientation="vertical">
@@ -114,66 +122,59 @@
                 android:id="@+id/tv_device_status"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="10dp"
-                android:textSize="20sp"
+                android:layout_marginTop="2dp"
+                android:textSize="16sp"
                 android:textColor="@color/main_color"
                 android:text="设备状态:" />
             <TextView
                 android:id="@+id/tv_call_state"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="10dp"
-                android:textSize="20sp"
+                android:layout_marginTop="2dp"
+                android:textSize="16sp"
                 android:textColor="@color/main_color"
                 android:text="呼叫状态: 待机中" />
             <TextView
                 android:id="@+id/tv_uart0_info"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="10dp"
-                android:textSize="20sp"
+                android:layout_marginTop="2dp"
+                android:textSize="16sp"
                 android:textColor="@color/main_color"
                 android:text="串口0:" />
             <TextView
                 android:id="@+id/tv_uart_info"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="10dp"
-                android:textSize="20sp"
+                android:layout_marginTop="2dp"
+                android:textSize="16sp"
                 android:textColor="@color/main_color"
                 android:text="串口1:" />
             <TextView
                 android:id="@+id/tv_tcp_info"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="10dp"
-                android:textSize="20sp"
+                android:layout_marginTop="2dp"
+                android:textSize="16sp"
                 android:textColor="@color/main_color"
                 android:text="Received TCP:" />
             <TextView
                 android:id="@+id/tv_send_tcp_info"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="10dp"
-                android:textSize="20sp"
+                android:layout_marginTop="2dp"
+                android:textSize="16sp"
                 android:textColor="@color/main_color"
                 android:text="Send TCP:" />
             <TextView
                 android:id="@+id/tv_serial_device"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="10dp"
-                android:textSize="20sp"
+                android:layout_marginTop="2dp"
+                android:textSize="16sp"
                 android:textColor="@color/main_color"
                 android:text=""/>
         </LinearLayout>
 
-        <!--通话界面-->
-        <FrameLayout
-            android:id="@+id/call_frame"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:visibility="gone"/>
-
     </RelativeLayout>
 </layout>

+ 1 - 0
janus/.gitignore

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

+ 40 - 0
janus/build.gradle

@@ -0,0 +1,40 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion target_sdk_version
+    buildToolsVersion build_tools_version
+
+    defaultConfig {
+        minSdkVersion min_sdk_version
+        targetSdkVersion target_sdk_version
+        versionCode app_version_code
+        versionName app_version
+
+        //testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+        //consumerProguardFiles "consumer-rules.pro"
+
+        compileOptions {
+            sourceCompatibility JavaVersion.VERSION_1_8
+            targetCompatibility JavaVersion.VERSION_1_8
+        }
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+
+        debug {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+
+    compile 'org.webrtc:google-webrtc:1.0.32006'
+    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

+ 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;
+    }
+}

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

@@ -0,0 +1,383 @@
+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));
+                            Log.i(TAG,"获取到远程视频流");
+                            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;
+    }
+}

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

@@ -0,0 +1,757 @@
+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 = 720;
+    private static final int VIDEO_RESOLUTION_HEIGHT = 1280;
+    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) {
+        //Log.d("call", Log.getStackTraceString(new Throwable()));
+        if (audioManager != null) {
+            isSpeakerOn = enable;
+            audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+
+            if (enable) {
+                if (!setBluetoothHeadsetOn()) {   //优先蓝牙
+                    audioManager.setMode(AudioManager.MODE_NORMAL);
+                    audioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+                            audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL),
+                            AudioManager.FX_KEY_CLICK);
+                    audioManager.setSpeakerphoneOn(true);
+                }
+            } else {
+                //5.0以上
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                    //设置mode
+                    audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
+                } else {
+                    //设置mode
+                    audioManager.setMode(AudioManager.MODE_IN_CALL);
+                }
+                //设置音量,解决有些机型切换后没声音或者声音突然变大的问题
+                audioManager.setStreamVolume(
+                        AudioManager.STREAM_VOICE_CALL,
+                        audioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL),
+                        AudioManager.FX_KEY_CLICK
+                );
+                audioManager.setSpeakerphoneOn(false);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 切换耳机
+     * @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 {
+            audioManager.setMode(AudioManager.MODE_NORMAL);
+        }
+    }
+
+    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);
+        }
+    }
+}

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

@@ -0,0 +1,14 @@
+package com.wdkl.ncs.janus.util;
+
+public class Constant {
+    public final static String GATEWAY_URL="8.129.220.143";
+    //public final static String GATEWAY_URL="172.28.100.100";
+    //public final static String GATEWAY_URL="119.23.151.229";
+    public final static String GATEWAY_WS_PORT="8188";
+    public final static String JANUS_URL = "ws://"+Constant.GATEWAY_URL+":" + Constant.GATEWAY_WS_PORT;
+
+    public final static String[] STUN_SERVER = new String[]{"stun:8.129.220.143:3478"};
+    //public final static String[] STUN_SERVER = new String[]{"stun:172.28.100.100:3478"};
+    //public final static String[] STUN_SERVER = new String[]{"stun:119.23.151.229:3478"};
+    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;
+    }
+}

+ 2 - 0
middleware/src/main/code/com/wdkl/ncs/android/middleware/common/Constant.java

@@ -154,4 +154,6 @@ public class Constant {
     public static final int EVENT_SERIAL_EVENT = 0x10;
 
     public static final int EVENT_RESTART_APP = 0x11;
+
+    public static final int EVENT_END_CALL = 0x12;
 }

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

@@ -64,8 +64,8 @@ public class WebRTCEngine implements IEngine, Peer.IPeerEvent {
     private SurfaceViewRenderer localRenderer;
 
     // 服务器实例列表
-    //private String serverIP = "8.129.220.143";
-    private String serverIP = "172.28.100.100";
+    private String serverIP = "8.129.220.143";
+    //private String serverIP = "172.28.100.100";
     //private String serverIP = "172.168.0.254";
     //private String serverIP = "119.23.151.229";
     //private String serverIP = "192.168.8.5";

+ 3 - 1
settings.gradle

@@ -1 +1,3 @@
-include ':app', ':common', ':welcome', ':home', ':resource', ':middleware', ':extra', ':conversion_box', ':traditionlib', 'webrtc', 'rtc-chat', 'libwebrtc'
+include ':app', ':common', ':welcome', ':home', ':resource', ':middleware', ':extra', ':conversion_box', ':traditionlib', ':janus'
+
+//'webrtc', 'rtc-chat', 'libwebrtc'