فهرست منبع

切换到janus信令

weizhengliang 3 سال پیش
والد
کامیت
7526611ec5
62فایلهای تغییر یافته به همراه4321 افزوده شده و 605 حذف شده
  1. 0 7
      app/src/main/code/com/wdkl/app/ncs/application/Application.kt
  2. 3 3
      build.gradle
  3. 4 2
      home/build.gradle
  4. 12 0
      home/src/main/AndroidManifest.xml
  5. 76 273
      home/src/main/code/com/wdkl/ncs/android/component/home/activity/HomeActivity.kt
  6. 0 0
      home/src/main/code/com/wdkl/ncs/android/component/home/activity/VideoActivity.kt.bak
  7. 55 20
      home/src/main/code/com/wdkl/ncs/android/component/home/fragment/BaseCallFragment.kt
  8. 244 219
      home/src/main/code/com/wdkl/ncs/android/component/home/fragment/SkyCallFragment.kt
  9. 0 0
      home/src/main/code/com/wdkl/ncs/android/component/home/fragment/VisitFragment.kt.bak
  10. 0 2
      home/src/main/code/com/wdkl/ncs/android/component/home/helper/AppUpdateHelper.java
  11. 192 0
      home/src/main/code/com/wdkl/ncs/android/component/home/helper/AsyncPlayer.java
  12. 1 42
      home/src/main/code/com/wdkl/ncs/android/component/home/service/TcpHandleService.kt
  13. 0 8
      home/src/main/code/com/wdkl/ncs/android/component/home/util/AnrFcExceptionUtil.java
  14. 1 1
      home/src/main/code/com/wdkl/ncs/android/component/home/util/CallDialogHelper.java
  15. 1 1
      home/src/main/code/com/wdkl/ncs/android/component/home/util/MediaPlayHelper.java
  16. 33 0
      home/src/main/code/com/wdkl/ncs/android/component/home/util/NetHelper.java
  17. 1 1
      home/src/main/code/com/wdkl/ncs/android/component/home/util/RingPlayHelper.java
  18. BIN
      home/src/main/res/drawable/av_handfree_hover.png
  19. BIN
      home/src/main/res/drawable/ic_answer_normal.png
  20. BIN
      home/src/main/res/drawable/ic_answer_press.png
  21. BIN
      home/src/main/res/drawable/ic_nurse.png
  22. 5 0
      home/src/main/res/drawable/selector_call_answer.xml
  23. 0 1
      home/src/main/res/layout/activity_home.xml
  24. 0 7
      home/src/main/res/layout/activity_web_rtc_voip_audio.xml
  25. 1 1
      home/src/main/res/layout/call_dialog_lay.xml
  26. 3 3
      home/src/main/res/layout/fragment_visiting.xml
  27. 74 12
      home/src/main/res/layout/sky_voice_call_layout.xml
  28. 1 1
      home/src/main/res/layout/video_connected_action.xml
  29. BIN
      home/src/main/res/raw/ring_back2.wav
  30. 96 0
      home/src/main/res/values/colors.xml
  31. 1 0
      janus/.gitignore
  32. 40 0
      janus/build.gradle
  33. 0 0
      janus/consumer-rules.pro
  34. 21 0
      janus/proguard-rules.pro
  35. 6 0
      janus/src/main/AndroidManifest.xml
  36. 26 0
      janus/src/main/java/com/wdkl/ncs/janus/client/CallSessionCallback.java
  37. 886 0
      janus/src/main/java/com/wdkl/ncs/janus/client/JanusClient.java
  38. 33 0
      janus/src/main/java/com/wdkl/ncs/janus/client/JanusMessageType.java
  39. 19 0
      janus/src/main/java/com/wdkl/ncs/janus/client/PluginHandle.java
  40. 47 0
      janus/src/main/java/com/wdkl/ncs/janus/client/Transaction.java
  41. 382 0
      janus/src/main/java/com/wdkl/ncs/janus/client/VideoRoomCallback.java
  42. 112 0
      janus/src/main/java/com/wdkl/ncs/janus/client/WebSocketChannel.java
  43. 35 0
      janus/src/main/java/com/wdkl/ncs/janus/entity/MsgEvent.java
  44. 54 0
      janus/src/main/java/com/wdkl/ncs/janus/entity/Publisher.java
  45. 68 0
      janus/src/main/java/com/wdkl/ncs/janus/entity/Room.java
  46. 24 0
      janus/src/main/java/com/wdkl/ncs/janus/render/ProxyVideoSink.java
  47. 171 0
      janus/src/main/java/com/wdkl/ncs/janus/render/VideoFileRenderer.java
  48. 100 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/AudioFocusManager.java
  49. 193 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/Peer.java
  50. 771 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/WebRTCEngine.java
  51. 72 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/AnswerSdpObserver.java
  52. 9 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreateAnswerCallback.java
  53. 9 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreateOfferCallback.java
  54. 20 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CreatePeerConnectionCallback.java
  55. 128 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/CustomPCObserver.java
  56. 79 0
      janus/src/main/java/com/wdkl/ncs/janus/rtc/observer/OfferSdpObserver.java
  57. 37 0
      janus/src/main/java/com/wdkl/ncs/janus/util/EnumType.java
  58. 17 0
      janus/src/main/java/com/wdkl/ncs/janus/util/JanusConstant.java
  59. 123 0
      janus/src/main/java/com/wdkl/ncs/janus/util/OSUtils.java
  60. 16 0
      middleware/src/main/code/com/wdkl/ncs/android/middleware/common/Constants.kt
  61. 17 0
      middleware/src/main/code/com/wdkl/ncs/android/middleware/tcp/channel/VoiceUtil.java
  62. 2 1
      settings.gradle

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

@@ -4,11 +4,8 @@ import com.enation.javashop.android.jrouter.JRouter
 import com.enation.javashop.net.engine.config.NetEngineConfig
 import com.enation.javashop.net.engine.plugin.exception.RestfulExceptionInterceptor
 import com.enation.javashop.utils.base.config.BaseConfig
-import com.wdkl.core.socket.SocketManager
-import com.wdkl.core.voip.VoipEvent
 import com.wdkl.ncs.android.component.home.util.AnrFcExceptionUtil
 import com.wdkl.ncs.android.lib.base.BaseApplication
-import com.wdkl.skywebrtc.SkyEngineKit
 
 /**
  * @author LDD
@@ -80,10 +77,6 @@ class Application : BaseApplication() {
                 .openLogger()
                 .addNetInterceptor(RestfulExceptionInterceptor())
 
-        // 初始化信令
-        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 = 8
+    ext.app_version_code = 10
 
     /**
      * APP版本号
      */
-    ext.app_version = "1.0.8"
+    ext.app_version = "1.1.0"
 
     /**
      * 项目依赖库

+ 4 - 2
home/build.gradle

@@ -123,10 +123,12 @@ dependencies {
 //    compile project(':AmDemo_R')
 
     //web rtc
-    compile project(':webrtc')
+    //compile project(':webrtc')
     //compile project(':libwebrtc')
-    compile project(':rtc-chat')
+    //compile project(':rtc-chat')
     //compile project(':keepalive')
+
+    compile project(':janus')
 }
 
 /**

+ 12 - 0
home/src/main/AndroidManifest.xml

@@ -10,6 +10,18 @@
     <uses-permission android:name="android.permission.WAKE_LOCK" />
     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
 
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 悬浮窗显示 -->
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
+
     <application
         android:allowBackup="true"
         android:supportsRtl="true">

+ 76 - 273
home/src/main/code/com/wdkl/ncs/android/component/home/activity/HomeActivity.kt

@@ -26,15 +26,11 @@ import com.enation.javashop.net.engine.plugin.permission.RxPermissions
 import com.enation.javashop.utils.base.tool.CommonTool
 import com.enation.javashop.utils.base.widget.LoadingDialog
 import com.google.gson.Gson
-import com.wdkl.core.consts.Urls
-import com.wdkl.core.socket.IUserState
-import com.wdkl.core.socket.SocketManager
 import com.wdkl.ncs.android.component.home.BuildConfig
 import com.wdkl.ncs.android.component.home.R
 import com.wdkl.ncs.android.component.home.broadcast.BatteryBroadcastReceiver
 import com.wdkl.ncs.android.component.home.databinding.ActivityHomeBinding
 import com.wdkl.ncs.android.component.home.fragment.SkyCallFragment
-import com.wdkl.ncs.android.component.home.fragment.VisitFragment
 import com.wdkl.ncs.android.component.home.launch.HomeLaunch
 import com.wdkl.ncs.android.component.home.service.TcpHandleService
 import com.wdkl.ncs.android.component.home.service.TcpHandleService.instance.updateLastTime
@@ -60,8 +56,6 @@ import com.wdkl.ncs.android.middleware.tcp.channel.VoiceUtil
 import com.wdkl.ncs.android.middleware.tcp.dto.TcpModel
 import com.wdkl.ncs.android.middleware.tcp.enums.TcpAction
 import com.wdkl.ncs.android.middleware.utils.MessageEvent
-import com.wdkl.skywebrtc.EnumType
-import com.wdkl.skywebrtc.SkyEngineKit
 import io.reactivex.Observable
 import kotlinx.android.synthetic.main.activity_home.*
 import kotlinx.android.synthetic.main.activity_register.*
@@ -69,7 +63,7 @@ import org.greenrobot.eventbus.EventBus
 import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode
 
-class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(), HomeActivityContract.View, IUserState {
+class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(), HomeActivityContract.View {
 
     var TAG = HomeActivity::class.java.getSimpleName()
 
@@ -81,10 +75,6 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
     private var isRegister = true
     private var inited = false
 
-    private val WRITE_EXTERNAL_STORAGE_REQUEST_CODE = 127//这个值是自定义的一个int值,在申请多个权限时要
-
-    private lateinit var loadingDialog: LoadingDialog
-
     var currentFragment: Fragment? = null
 
     var clickTime: Long = 0
@@ -93,11 +83,6 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
 
     private lateinit var receiver: TimeReceiver
 
-    //呼叫倒计时
-    lateinit var countDownTimer: CountDownTimer
-
-    private var callSuccess: Boolean = false
-
     override fun getLayId(): Int {
         return R.layout.activity_home
     }
@@ -108,14 +93,7 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
 
     //初始化
     override fun init() {
-        val permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE)
-
-        if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
-            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_PHONE_STATE), WRITE_EXTERNAL_STORAGE_REQUEST_CODE)
-        } else {
-        }
-
-        loadingDialog = CommonTool.createLoadingDialog(this, R.layout.custom_loading,R.id.loadding_image)
+        requestPermissions()
 
         //保活守护进程
         //DaemonEnv.init(this)
@@ -150,26 +128,6 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
             return@setOnLongClickListener true
         }
 
-        Constants.imei = Util.getIMEI(this)
-        //Constants.imei = "860475031573358"
-        Log.i(TAG, "IMEI " + Constants.imei)
-        Log.i(TAG, "mac " + Constants.mac)
-        tv_device_imei.text = Constants.imei
-
-        //获取TCP服务器IP和端口
-        Thread(Runnable {
-            while (isRegister) {
-                Log.i(TAG,"获取TCP服务器IP和端口")
-                runOnUiThread(Runnable {
-                    presenter.getTcpServerHost()
-                })
-                try {
-                    Thread.sleep(15000)
-                } catch (e: Exception) {
-                }
-            }
-        }).start()
-
         //网络强度监听
         teleManager = (applicationContext.getSystemService(Context.TELEPHONY_SERVICE)) as TelephonyManager
         netType = NetHelper.getInstance().getNetworkState(applicationContext)
@@ -188,11 +146,30 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
                 }, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
             }
         }
+    }
 
-        //注册广播
-        regReceiver()
+    private fun permissionGranted() {
+        Constants.imei = NetHelper.getInstance().imei
+        //Constants.imei = "860475031573358"
+        Log.i(TAG, "IMEI " + Constants.imei)
+        Log.i(TAG, "mac " + Constants.mac)
+        tv_device_imei.text = Constants.imei
 
-        initCountDownTimer()
+        //获取TCP服务器IP和端口
+        Thread{
+            while (isRegister) {
+                Log.i(TAG,"获取TCP服务器IP和端口")
+                runOnUiThread(Runnable {
+                    presenter.getTcpServerHost()
+                })
+                try {
+                    Thread.sleep(15000)
+                } catch (e: Exception) {
+                }
+            }
+        }.start()
+
+        regReceiver()
     }
 
     private fun regReceiver() {
@@ -211,22 +188,6 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
         unregisterReceiver(receiver)
     }
 
-    fun initCountDownTimer() {
-        countDownTimer = object: CountDownTimer(30000, 1000) {
-            override fun onTick(millisUntilFinished: Long) {
-                //
-            }
-
-            override fun onFinish() {
-                //呼叫超时,返回到主界面
-                showMessage("无人应答...")
-                DeviceChannel.calling = false
-                VideoUtil.cancelVideoCall(Constants.deviceId, Constants.interactionId)
-                CallDialogHelper.dismissCallDialog()
-            }
-        }
-    }
-
     /**
      * 返回的tcp信息
      */
@@ -238,42 +199,37 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
             Constants.heartBeat = tcpSeverDTO.readerIdleTime
             tv_server_ip.text = tcpSeverDTO.publicIp
 
-            Thread(Runnable {
-                run {
-                    TcpClient.getInstance().init(Constants.tcpServer, Constants.tcpPort, Constants.heartBeat)
-                }
-            }).start()
+            Thread {
+                //启动tcp连接
+                TcpClient.getInstance().init(Constants.tcpServer, Constants.tcpPort, Constants.heartBeat)
 
-            AppTool.Time.delay(1500) {
-                requestPermissions()
-            }
+                //获取设备数据
+                while (!inited) {
+                    Log.i(TAG, "获取设备数据")
+                    if (!TextUtils.isEmpty(Constants.imei)) {
+                        presenter.getDeviceVO(Constants.imei)
+                    }
+
+                    try {
+                        Thread.sleep(15000)
+                    } catch (e: Exception) {
+                        //
+                    }
+                }
+            }.start()
         }
     }
 
     private fun requestPermissions() {
         Observable.just("").compose(RxPermissions(this).ensure(Manifest.permission.CAMERA,
-                Manifest.permission.READ_EXTERNAL_STORAGE,
-                Manifest.permission.WRITE_EXTERNAL_STORAGE,
-                Manifest.permission.ACCESS_WIFI_STATE,
-                Manifest.permission.RECORD_AUDIO,
-                Manifest.permission.READ_PHONE_STATE)).subscribe {
+            Manifest.permission.READ_EXTERNAL_STORAGE,
+            Manifest.permission.WRITE_EXTERNAL_STORAGE,
+            Manifest.permission.ACCESS_WIFI_STATE,
+            Manifest.permission.BLUETOOTH,
+            Manifest.permission.RECORD_AUDIO,
+            Manifest.permission.READ_PHONE_STATE)).subscribe {
             if (it) {
-                //presenter.getDeviceVO(Constants.imei)  //传imei
-                Thread {
-                    while (!inited) {
-                        Log.i(TAG, "设备数据")
-                        runOnUiThread {
-                            if (!TextUtils.isEmpty(Constants.imei)) {
-                                presenter.getDeviceVO(Constants.imei)
-                            }
-                        }
-                        try {
-                            Thread.sleep(8000)
-                        } catch (e: Exception) {
-                            //
-                        }
-                    }
-                }.start()
+                permissionGranted()
             } else {
                 showMessage("请重新授权,进入App")
                 requestPermissions()
@@ -292,7 +248,6 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
             Constants.sipId = data.sipId
             Constants.ethIp = data.ethIp
             Constants.sipIp = data.sipIp
-            Constants.sipId = data.sipId
             Constants.sipPassword = data.sipPassword
             Constants.userName = data.memberName
             Constants.userRoleName = data.roleName
@@ -330,34 +285,19 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
     private fun initSDK() {
         val intent = Intent(this, TcpHandleService::class.java)
         startService(intent)
+    }
 
-        //WEBRTC 语音初始化
-        // 添加登录回调
-        SocketManager.getInstance().addUserStateCallback(this)
-        // 连接socket:登录
-        SocketManager.getInstance().connect(Urls.WS, Constants.sipId, 0)
-        Log.i(TAG, "Urls.WS " + Urls.WS + " sip_id " + Constants.sipId)
-
-        //问询事件
-        btn_callout.setOnClickListener {
-            if (System.currentTimeMillis() - clickTime > 3000) {
-                if (TcpClientHandler.getConnected() && SocketManager.getInstance().socketOpen() && netAvailable) {
-                    callSuccess = false
-                    VideoUtil.startVideoOutCall(Constants.deviceId)
-                    AppTool.Time.delay(3000) {
-                        if (!callSuccess) {
-                            //呼叫失败
-                            showMessage("呼叫失败,服务器无响应或网络故障!")
-                        }
-                    }
-                } else {
-                    showMessage("通话服务或网络未连接,请检查网络稍后再试")
-                }
-            } else {
-                showMessage("点击太频繁")
-            }
-            clickTime = System.currentTimeMillis()
-        }
+    //开始呼叫
+    private fun startVideoCall() {
+        currentFragment = SkyCallFragment()
+        var bundle = Bundle()
+        bundle.putInt("call_state", 0)
+        bundle.putBoolean("audio_only", false)
+        currentFragment?.arguments = bundle
+        supportFragmentManager.beginTransaction()
+            .add(R.id.frame_visit, currentFragment)
+            .commit()
+        watch_activity_home_linyout.visibility = View.GONE
     }
 
     /**
@@ -366,36 +306,6 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
     override fun setDeviceSettingData(partSettingDO: PartSettingDO) {
     }
 
-    override fun userLogin() {
-        Log.i(TAG, "wdkl rtc 登录成功")
-        netAvailable = true
-        runOnUiThread(Runnable {
-            tv_rtc_status.setBackgroundColor(Color.GREEN)
-        })
-    }
-
-    override fun userLogout() {
-        Log.w(TAG, "wdkl rtc 用户登出")
-        netAvailable = false
-        runOnUiThread(Runnable {
-            tv_rtc_status.setBackgroundColor(Color.RED)
-        })
-        /*if (!SocketManager.getInstance().connectFlag) {
-            reConnectRTC()
-        }*/
-    }
-
-    fun reConnectRTC(){
-        if (SocketManager.getInstance().userState==0){
-            AppTool.Time.delay(5000){
-                SocketManager.getInstance().reConnect()
-                reConnectRTC()
-            }
-        } else {
-            return
-        }
-    }
-
     override fun onError(message: String, type: Int) {
     }
 
@@ -409,12 +319,7 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
     }
 
     override fun destory() {
-        //SpeechUtil.getInstance().release()
-        //注销webRTC
-        SocketManager.getInstance().unConnect()
-
         releaseReceiver()
-        countDownTimer.cancel()
     }
 
     override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
@@ -427,97 +332,23 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
     }
 
     override fun bindEvent() {
+        btn_callout.setOnClickListener {
+            if (System.currentTimeMillis() - clickTime > 3000) {
+                if (TcpClientHandler.getConnected() && netAvailable) {
+                    startVideoCall()
+                } else {
+                    showMessage("通话服务或网络未连接,请检查网络稍后再试")
+                }
+            } else {
+                showMessage("点击太频繁")
+            }
+            clickTime = System.currentTimeMillis()
+        }
     }
 
     @Subscribe(threadMode = ThreadMode.MAIN)
     fun onMoonEvent(messageEvent: MessageEvent) {
-        if (messageEvent.tag == Constants.VIDEO_MSG){
-            //loadingDialog.dismiss()
-            val tcpModel = messageEvent.getMessage() as TcpModel
-            var interactionVO = Gson().fromJson(tcpModel.data.toString(), InteractionVO::class.java)
-            Log.e(TAG, "收到tcp消息" + tcpModel.getType() + " " + tcpModel.getAction() + ", data: " + tcpModel.data.toString())
-            when (tcpModel.action){
-                TcpAction.VideoAction.CALLING->{
-                    DeviceChannel.calling = false
-                    CallDialogHelper.dismissCallDialog()
-                    countDownTimer.cancel()
-                    showMessage("对方忙线中")
-                }
-
-                TcpAction.VideoAction.FAILED->{
-                    callSuccess = true
-                    DeviceChannel.calling = false
-                    showMessage("呼叫失败,找不到设备或对方不在线!")
-                    countDownTimer.cancel()
-                }
-
-                TcpAction.VideoAction.SUCCESS->{
-                    callSuccess = true
-                    DeviceChannel.calling = true
-                    Constants.interactionId = interactionVO.id
-                    Util.wakeUpAndUnlock(this)
-                    countDownTimer.start()
-
-                    CallDialogHelper.dismissCallDialog()
-                    CallDialogHelper.showCallDialog(this@HomeActivity, 0, "探视请求已发送,请等待...", {
-                        //呼出取消
-                        DeviceChannel.calling = false
-                        VideoUtil.cancelVideoCall(Constants.deviceId, Constants.interactionId)
-                        CallDialogHelper.dismissCallDialog()
-                        countDownTimer.cancel()
-                    }, {
-                        //来电接听
-                    }, {
-                        //来电拒接
-                    })
-                }
-
-                TcpAction.VideoAction.ACCEPT-> {
-                    /*Constants.fromId = tcpModel.fromId
-                    DeviceChannel.calling = true
-                    CallDialogHelper.dismissCallDialog()
-                    Constants.interactionId = interactionVO.id
-                    // 加入房间
-                    val roomId = "visit-room-" + Constants.interactionId
-                    CallMultiActivity.openActivity(activity, roomId, false)*/
-                }
-
-                TcpAction.VideoAction.VIDEO_IN_CALL-> {
-                    Constants.fromId = tcpModel.fromId
-                    Constants.visitHostId = tcpModel.fromId
-                    DeviceChannel.calling = true
-                    Constants.interactionId = interactionVO.id
-                    CallDialogHelper.dismissCallDialog()
-                    countDownTimer.cancel()
-                }
-
-                TcpAction.VideoAction.VIDEO_ON -> {
-                    Constants.fromId = tcpModel.fromId
-                    DeviceChannel.calling = true
-                    Constants.interactionId = interactionVO.id
-                }
-
-                TcpAction.VideoAction.REJECT->{
-                    showMessage("对方拒绝")
-                    DeviceChannel.calling = false
-                    CallDialogHelper.dismissCallDialog()
-                    countDownTimer.cancel()
-                }
-
-                TcpAction.VideoAction.HANDOFF -> {
-                    if (SkyEngineKit.Instance().currentSession != null) {
-                        SkyEngineKit.Instance().endCall()
-                    }
-                    if (currentFragment != null) {
-                        supportFragmentManager.beginTransaction()
-                            .remove(currentFragment)
-                            .commit()
-                        currentFragment = null
-                    }
-                    watch_activity_home_linyout.visibility = View.VISIBLE
-                }
-            }
-        } else if (messageEvent.tag == Constants.BACK_TO_MAIN_MSG) {
+        if (messageEvent.tag == Constants.BACK_TO_MAIN_MSG) {
             if (currentFragment != null) {
                 supportFragmentManager.beginTransaction()
                     .remove(currentFragment)
@@ -525,19 +356,10 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
                 currentFragment = null
             }
             watch_activity_home_linyout.visibility = View.VISIBLE
-        } else if (messageEvent.tag == Constants.VISIT_MSG) {
-            currentFragment = SkyCallFragment()
-            var bundle = Bundle()
-            bundle.putInt("call_state", 1)
-            bundle.putBoolean("audio_only", false)
-            currentFragment?.arguments = bundle
-            supportFragmentManager.beginTransaction()
-                .add(R.id.frame_visit, currentFragment)
-                .commit()
-            watch_activity_home_linyout.visibility = View.GONE
         } else if (messageEvent.tag == Constants.EVENT_TCP_BREAK) {
             if (TcpClientHandler.getConnected()) {
                 netAvailable = true
+                tv_rtc_status.setBackgroundColor(Color.GREEN)
             } else {
                 netAvailable = false
                 tv_rtc_status.setBackgroundColor(Color.RED)
@@ -557,35 +379,16 @@ class HomeActivity : BaseActivity<HomeActivityPresenter, ActivityHomeBinding>(),
         override fun onReceive(context: Context, intent: Intent) {
             if (intent.action == Intent.ACTION_TIME_TICK) {
                 if (inited) {
-                    if (SocketManager.getInstance()
-                            .socketOpen() && SocketManager.getInstance().userState == 1 && TcpClientHandler.getConnected()
-                    ) {
+                    if (TcpClientHandler.getConnected()) {
                         tv_rtc_status.setBackgroundColor(Color.GREEN)
                     } else {
-                        SocketManager.getInstance().connect(Urls.WS, Constants.sipId, 0)
+                        tv_rtc_status.setBackgroundColor(Color.RED)
                     }
                 }
             } else if (intent.action == Constants.HOOK_ON) {
                 Log.e(TAG,"手柄放下 ")
                 Constants.hookOn = true
                 AppUtils.switchAudioMode(activity, true)
-                if (DeviceChannel.calling == true) {
-                    val gEngineKit = SkyEngineKit.Instance()
-                    if (gEngineKit != null && gEngineKit.currentSession != null && gEngineKit.currentSession.state != EnumType.CallState.Idle) {
-                        //通话中挂断
-                        gEngineKit.endCall()
-                        if (Constants.visitHostId != -1) {
-                            VideoUtil.handoffVideoCall(Constants.deviceId, Constants.visitHostId, Constants.interactionId)
-                        }
-                        EventBus.getDefault().post(MessageEvent("back_to_main", Constants.BACK_TO_MAIN_MSG))
-                    } else {
-                        //呼出取消
-                        DeviceChannel.calling = false
-                        VideoUtil.cancelVideoCall(Constants.deviceId, Constants.interactionId)
-                        CallDialogHelper.dismissCallDialog()
-                        countDownTimer.cancel()
-                    }
-                }
             } else if (intent.action == Constants.HOOK_OFF) {
                 Log.e(TAG,"手柄拿起 ")
                 Constants.hookOn = false

home/src/main/code/com/wdkl/ncs/android/component/home/activity/VideoActivity.kt → home/src/main/code/com/wdkl/ncs/android/component/home/activity/VideoActivity.kt.bak


+ 55 - 20
home/src/main/code/com/wdkl/ncs/android/component/home/fragment/BaseCallFragment.kt

@@ -1,19 +1,24 @@
 package com.wdkl.ncs.android.component.home.fragment
 
 import android.os.Bundle
+import android.os.CountDownTimer
 import android.support.v4.app.Fragment
 import android.view.LayoutInflater
+import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
+import android.widget.TextView
 import com.enation.javashop.utils.base.tool.BaseToolActivity
-import com.wdkl.core.voip.VoipEvent
+import com.wdkl.ncs.android.component.home.util.RingPlayHelper
 import com.wdkl.ncs.android.component.nursehome.common.Constants
+import com.wdkl.ncs.android.lib.utils.showMessage
+import com.wdkl.ncs.android.middleware.tcp.channel.DeviceChannel
+import com.wdkl.ncs.android.middleware.tcp.channel.VideoUtil
+import com.wdkl.ncs.android.middleware.tcp.channel.VoiceUtil
 import com.wdkl.ncs.android.middleware.utils.MessageEvent
-import com.wdkl.skywebrtc.SkyEngineKit
-import com.wdkl.skywebrtc.except.NotInitializedException
 import org.greenrobot.eventbus.EventBus
 
-abstract class BaseCallFragment: Fragment() {
+abstract class BaseCallFragment: Fragment(), View.OnTouchListener {
 
     private var layout: View? = null
 
@@ -21,14 +26,14 @@ abstract class BaseCallFragment: Fragment() {
 
     //通话状态:0-去电, 1-来电
     protected var callState : Int = 0
-    protected var onlyAudio: Boolean = true
+    protected var onlyAudio: Boolean = false
     //来电设备id
     protected var fromId: Int = -1
     protected var interactionId: Int? = -1
     protected var targetId: String? = null
 
-    protected var gEngineKit: SkyEngineKit? = null
-
+    //计时器
+    lateinit var countDownTimer: CountDownTimer
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -54,21 +59,10 @@ abstract class BaseCallFragment: Fragment() {
     override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        try {
-            SkyEngineKit.init(VoipEvent())
-            gEngineKit = SkyEngineKit.Instance()
-        } catch (e: NotInitializedException) {
-            SkyEngineKit.init(VoipEvent())
-            try {
-                gEngineKit = SkyEngineKit.Instance()
-            } catch (ex: NotInitializedException) {
-                ex.printStackTrace()
-                baseActivity.finish()
-            }
-        }
-
         init()
         bindEvent()
+
+        view!!.setOnTouchListener(this)
     }
 
     override fun onDestroyView() {
@@ -86,6 +80,10 @@ abstract class BaseCallFragment: Fragment() {
         super.onStop()
     }
 
+    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
+        return true
+    }
+
     protected abstract fun getLayId(): Int
 
     protected abstract fun init()
@@ -94,6 +92,43 @@ abstract class BaseCallFragment: Fragment() {
 
     protected abstract fun destroy()
 
+    protected abstract fun callEnd(handoff: Boolean)
+
+    //初始化计时器
+    protected fun initCountDownTimer(view: TextView) {
+        countDownTimer = object: CountDownTimer(30000, 1000) {
+            override fun onTick(millisUntilFinished: Long) {
+                if (view != null) {
+                    val time = millisUntilFinished/1000
+                    view.setText("倒计时: " + time + "秒")
+                }
+            }
+
+            override fun onFinish() {
+                //呼叫超时,返回到主界面
+                RingPlayHelper.stopRingTone()
+                showMessage("无人应答...")
+                DeviceChannel.calling = false
+                Constants.CALL_STATE = Constants.CALL_STANDBY
+                VideoUtil.cancelVideoCall(Constants.deviceId, Constants.interactionId)
+                callEnd(false)
+            }
+        }
+    }
+
+    //开始计时
+    protected fun startTimer() {
+        if (countDownTimer != null) {
+            countDownTimer.start()
+        }
+    }
+
+    //取消计时器
+    protected fun cancelTimer() {
+        if (countDownTimer != null) {
+            countDownTimer.cancel()
+        }
+    }
 
     //返回主界面
     protected fun backToMain() {

+ 244 - 219
home/src/main/code/com/wdkl/ncs/android/component/home/fragment/SkyCallFragment.kt

@@ -9,26 +9,32 @@ import android.view.ViewGroup
 import com.google.gson.Gson
 import com.wdkl.ncs.android.component.home.R
 import com.wdkl.ncs.android.component.home.util.AppUtils
+import com.wdkl.ncs.android.component.home.util.RingPlayHelper
+import com.wdkl.ncs.android.component.home.util.Util
 import com.wdkl.ncs.android.component.nursehome.common.Constants
+import com.wdkl.ncs.android.lib.utils.AppTool
 import com.wdkl.ncs.android.lib.utils.showMessage
 import com.wdkl.ncs.android.middleware.model.vo.InteractionVO
 import com.wdkl.ncs.android.middleware.tcp.channel.DeviceChannel
 import com.wdkl.ncs.android.middleware.tcp.channel.VideoUtil
-import com.wdkl.ncs.android.middleware.tcp.channel.VoiceUtil
 import com.wdkl.ncs.android.middleware.tcp.dto.TcpModel
 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.MessageEvent
-import com.wdkl.skywebrtc.CallSession
-import com.wdkl.skywebrtc.EnumType
-import com.wdkl.skywebrtc.SkyEngineKit
+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 com.wdkl.ncs.janus.util.JanusConstant
 import kotlinx.android.synthetic.main.sky_voice_call_layout.*
 import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode
 import org.webrtc.SurfaceViewRenderer
-import java.util.*
+import java.math.BigInteger
 
-class SkyCallFragment: BaseCallFragment(), CallSession.CallSessionCallback {
+class SkyCallFragment: BaseCallFragment(), CallSessionCallback {
+    private val TAG = "SkyCallFragment"
 
     private var localSurfaceView: SurfaceViewRenderer? = null
     private var remoteSurfaceView: SurfaceViewRenderer? = null
@@ -39,131 +45,123 @@ class SkyCallFragment: BaseCallFragment(), CallSession.CallSessionCallback {
 
     private var outGoing: Boolean = false
 
+
+    private var janusClient: JanusClient? = null
+    private var room: Room?=null
+    private var videoRoomCallback: VideoRoomCallback? = null
+
+    private var callSuccess: Boolean = false
+
     override fun getLayId(): Int {
         return R.layout.sky_voice_call_layout
     }
 
     override fun init() {
-        acceptCall()
+        //初始化计时器
+        initCountDownTimer(sky_voice_call_timeout)
+
+        //初始化 engine
+        WebRTCEngine.getInstance().init(false, this.context)
+
+        //初始化 janusClient
+        janusClient = JanusClient(JanusConstant.JANUS_URL, Constants.sipId!!.toBigInteger())
 
         when (callState) {
             0 -> {
                 //发起通话
                 outGoing = true
+                startOutgoing()
+                RingPlayHelper.playRingTone(baseActivity, R.raw.ring_back2, true)
+
+                janusClient!!.callState = EnumType.CallState.Outgoing
+                room = Room(Constants.sipId!!.toBigInteger())
             }
 
             1 -> {
                 //接受通话
-                outGoing = false
-                DeviceChannel.calling = true
-                val session = gEngineKit?.getCurrentSession()
-                if (session != null) {
-                    session.setSessionCallback(this)
-                }
-                if (onlyAudio) {
-                    Handler().postDelayed({
-                        joinAudioCall()
-                    }, 500)
-                } else {
-                    Handler().postDelayed({
-                        joinVideoCall()
-                    }, 500)
-                }
             }
         }
+
+
+        videoRoomCallback = VideoRoomCallback(janusClient, room, Constants.sipId!!.toBigInteger())
+        videoRoomCallback!!.callSessionCallback = this
+        janusClient!!.setJanusCallback(videoRoomCallback)
     }
 
     override fun bindEvent() {
         //通话挂断
         sky_voice_call_hangup.setOnClickListener {
-            //结束sip通话
-            gEngineKit?.endCall()
+            RingPlayHelper.stopRingTone()
+            if (Constants.CALL_STATE == Constants.CALL_CALLING) {
+                //结束sip通话
+                Constants.CALL_STATE = Constants.CALL_STANDBY
+                if (sky_voice_call_timer != null) {
+                    sky_voice_call_timer.stop()
+                }
 
-            if (Constants.visitHostId != -1) {
-                VideoUtil.handoffVideoCall(Constants.deviceId, Constants.visitHostId, Constants.interactionId)
+                callEnd(true)
+            } else {
+                Constants.CALL_STATE = Constants.CALL_STANDBY
+                DeviceChannel.calling = false
+                VideoUtil.cancelVideoCall(Constants.deviceId, Constants.interactionId)
+                cancelCall()
             }
-
-            DeviceChannel.calling = false
-            sky_voice_call_timer.stop()
         }
     }
 
     override fun destroy() {
         DeviceChannel.calling = false
+        cancelTimer()
+        Constants.CALL_STATE = Constants.CALL_STANDBY
         if (sky_voice_call_timer != null) {
             sky_voice_call_timer.stop()
         }
+        RingPlayHelper.stopRingTone()
     }
 
-    //开始接听
-    private fun acceptCall() {
-        sky_voice_call_calling_text.text = "连接中..."
-        sky_voice_call_outgoing.visibility = View.VISIBLE
-        sky_voice_call_timer.visibility = View.GONE
-    }
+    private fun startOutgoing() {
+        callSuccess = false
+        sky_voice_call_hangup.isEnabled = false
+        VideoUtil.startVideoOutCall(Constants.deviceId)
 
-    //语音接通
-    private fun joinAudioCall() {
-        val session = gEngineKit?.getCurrentSession()
-        if (session != null) {
-            Log.e("dds", "audio call session state: " + session.state)
-
-            if (session.state == EnumType.CallState.Incoming) {
-                session.joinHome(session.roomId)
-                session.toggleSpeaker(true)
-            } else if (session.state == EnumType.CallState.Idle) {
-                callEnd()
-                return
+        DeviceChannel.calling = true
+        Constants.CALL_STATE = Constants.CALL_OUTGOING
+        sky_voice_call_timeout.visibility = View.VISIBLE
+        sky_voice_call_timer.visibility = View.GONE
+        startTimer()
+
+        AppTool.Time.delay(5000) {
+            Log.d("tcp", "call success: $callSuccess")
+            if (!callSuccess && !callEnded) {
+                //呼叫失败
+                showMessage("呼叫失败,服务器无响应或网络故障!")
+                RingPlayHelper.stopRingTone()
+                cancelCall()
             }
-
-            Handler().postDelayed({
-                if (session.state == EnumType.CallState.Connected){
-                    showCalling(onlyAudio)
-                } else {
-                    gEngineKit?.endCall()
-                    callEnd()
-                }
-            }, 1500)
         }
     }
 
-    //视频接通
-    private fun joinVideoCall() {
-        val session = gEngineKit?.getCurrentSession()
-        if (session != null) {
-            Log.e("dds", "video call session state: " + session.state)
-
-            if (session.state == EnumType.CallState.Incoming) {
-                val surfaceView = gEngineKit!!.currentSession.setupLocalVideo(false)
-                if (surfaceView != null) {
-                    localSurfaceView = surfaceView as SurfaceViewRenderer
-                    localSurfaceView!!.setZOrderMediaOverlay(false)
-                    fullscreen_video_frame.addView(localSurfaceView)
-                }
+    //去电界面
+    private fun showOutgoingCall() {
+        Constants.CALL_STATE = Constants.CALL_OUTGOING
+        sky_voice_call_calling_text.text = "正在呼叫,等待接听..."
+        sky_voice_call_outgoing.visibility = View.VISIBLE
+        sky_voice_call_incoming.visibility = View.GONE
 
-                session.joinHome(session.roomId)
-                session.toggleSpeaker(true)
-            } else if (session.state == EnumType.CallState.Idle) {
-                callEnd()
-                return
-            }
+        fullscreen_video_frame.visibility = View.VISIBLE
+        pip_video_frame.visibility = View.VISIBLE
+    }
 
-            Handler().postDelayed({
-                if (session.state == EnumType.CallState.Connected){
-                    showCalling(onlyAudio)
-                } else {
-                    gEngineKit?.endCall()
-                    callEnd()
-                }
-            }, 2000)
-        }
+    //呼叫取消
+    private fun cancelCall() {
+        cancelTimer()
+        DeviceChannel.calling = false
+        Constants.CALL_STATE = Constants.CALL_STANDBY
+        callEnd(false)
     }
 
     private fun showCalling(audioOnly: Boolean) {
         if (audioOnly) {
-            //移除视频画面
-            fullscreen_video_frame.visibility = View.GONE
-            pip_video_frame.visibility = View.GONE
             ll_voice_call.visibility = View.VISIBLE
         } else {
             //显示视频画面
@@ -172,6 +170,7 @@ class SkyCallFragment: BaseCallFragment(), CallSession.CallSessionCallback {
             ll_voice_call.visibility = View.GONE
         }
 
+        cancelTimer()
         sky_voice_call_calling_text.text = "通话中..."
         sky_voice_call_timer.visibility = View.VISIBLE
         sky_voice_call_timer.base = SystemClock.elapsedRealtime()
@@ -186,50 +185,38 @@ class SkyCallFragment: BaseCallFragment(), CallSession.CallSessionCallback {
         }
     }
 
-    //创建会话
-    private fun startCall(targetId: String, audioOnly: Boolean): Boolean {
-        val room = UUID.randomUUID().toString() + System.currentTimeMillis()
-        val b = gEngineKit!!.startOutCall(baseActivity, room, targetId, audioOnly)
-        if (b) {
-            val session = gEngineKit!!.currentSession
-            if (session == null) {
-                return false
-            } else {
-                DeviceChannel.calling = true
-                session.setSessionCallback(this)
-                session.toggleSpeaker(true)
-
-                //3s还未连接上则判定为通话失败
-                Handler().postDelayed({
-                    if (session.state == EnumType.CallState.Connected){
-                        showCalling(onlyAudio)
-                    } else {
-                        gEngineKit?.endCall()
-                        callEnd()
-                    }
-                }, 3000)
+    //通话结束
+    override fun callEnd(handoff: Boolean) {
+        synchronized(this) {
+            if (callEnded) {
+                return
+            }
+            callEnded = true
+            DeviceChannel.calling = false
+            Constants.CALL_STATE = Constants.CALL_STANDBY
+            if (sky_voice_call_timer != null) {
+                sky_voice_call_timer.stop()
             }
-        }
-        return b
-    }
 
-    //通话结束
-    private fun callEnd() {
-        Log.e("dds", "call end !!!!!!!!!!!!!!!!!!")
-        if (callEnded) {
-            return
-        }
-        callEnded = true
+            Log.e(TAG, "call end !!!!!!!!!!!!!!!!!!")
+            if (handoff) {
+                VideoUtil.handoffVideoCall(Constants.deviceId, Constants.visitHostId, Constants.interactionId)
+            }
 
-        DeviceChannel.calling = false
+            if (janusClient!!.webSocketChannel != null) {
+                janusClient!!.callState = EnumType.CallState.Idle
+                if (outGoing) {
+                    janusClient!!.destroyRoom(janusClient!!.currentHandleId, null)
+                } else {
+                    janusClient!!.leaveRoom()
+                }
 
-        if (sky_voice_call_timer != null) {
-            sky_voice_call_timer.stop()
-        }
-        if (gEngineKit != null && gEngineKit!!.currentSession != null && gEngineKit!!.currentSession.state != EnumType.CallState.Idle) {
-            gEngineKit!!.endCall()
+                janusClient!!.setJanusCallback(null)
+                janusClient!!.disConnect()
+            }
+
+            backToMain()
         }
-        backToMain()
     }
 
 
@@ -237,43 +224,52 @@ class SkyCallFragment: BaseCallFragment(), CallSession.CallSessionCallback {
      ********************* webrtc通话回调 ********************
      * 注意: 如涉及到UI更新的需要在主线程处理,务必注意
      *******************************************************/
-    override fun didChangeState(state: EnumType.CallState?) {
-        Log.e("dds", "didChangeState: " + state)
-        /*handler.post {
-            if (state == EnumType.CallState.Connected) {
+    override fun didChangeState(var1: EnumType.CallState?) {
+        Log.e(TAG, "didChangeState: $var1")
+        if (var1 == EnumType.CallState.Connected) {
+            handler.post {
+                RingPlayHelper.stopRingTone()
                 //更新界面显示
-                showCalling(onlyAudio)
+                showCalling(false)
             }
-        }*/
+        }
     }
 
     override fun didDisconnected(userId: String?) {
+        Log.w(TAG, "didDisconnected: $userId")
         handler.post {
-            showMessage("断开连接")
-            callEnd()
+            showMessage("$userId 失去连接")
+            callEnd(true)
         }
     }
 
     override fun didError(error: String?) {
+        Log.e(TAG, "didError: $error")
         handler.post {
             showMessage("通话错误")
-            callEnd()
+            callEnd(true)
+        }
+    }
+
+    override fun didHangUp(handlerId: BigInteger?) {
+        Log.e("hangup", "socket hangup")
+        handler.post {
+            callEnd(true)
         }
     }
 
     //处理本地视频画面
     override fun didCreateLocalVideoTrack() {
-        Log.e("dds", "didCreateLocalVideoTrack")
+        Log.e(TAG, "didCreateLocalVideoTrack")
         handler.post {
-            val session = gEngineKit!!.currentSession
-            if (session != null && !callEnded) {
+            if (!callEnded) {
                 if (localSurfaceView == null) {
-                    val surfaceView = gEngineKit!!.currentSession.setupLocalVideo(true)
-                    Log.e("dds", "didCreateLocalVideoTrack surfaceView: " + surfaceView)
+                    val surfaceView = WebRTCEngine.getInstance().startPreview(true)
+                    Log.e(TAG, "didCreateLocalVideoTrack surfaceView: $surfaceView")
                     if (surfaceView != null) {
                         localSurfaceView = surfaceView as SurfaceViewRenderer
                     } else {
-                        callEnd()
+                        callEnd(true)
                     }
                 } else {
                     localSurfaceView!!.setZOrderMediaOverlay(true)
@@ -283,48 +279,48 @@ class SkyCallFragment: BaseCallFragment(), CallSession.CallSessionCallback {
                     (localSurfaceView!!.parent as ViewGroup).removeView(localSurfaceView)
                 }
 
-                if (outGoing && remoteSurfaceView == null) {
-                    if (fullscreen_video_frame != null && fullscreen_video_frame.getChildCount() != 0) {
+                if (fullscreen_video_frame != null) {
+                    if (fullscreen_video_frame.getChildCount() != 0) {
                         fullscreen_video_frame.removeAllViews()
                     }
                     fullscreen_video_frame.addView(localSurfaceView)
-                } else {
-                    if (pip_video_frame != null && pip_video_frame.getChildCount() != 0) {
-                        pip_video_frame.removeAllViews()
-                    }
-                    pip_video_frame.addView(localSurfaceView)
                 }
             }
         }
     }
 
-    //处理远端视频画面
-    override fun didReceiveRemoteVideoTrack(userId: String?) {
-        Log.e("dds", "didReceiveRemoteVideoTrack  userId: " + userId)
+    override fun didReceiveRemoteVideoTrack(userId: BigInteger?) {
+        Log.e(TAG, "didReceiveRemoteVideoTrack==  userId: $userId, hostSip: ${Constants.hostSipId}")
         handler.post {
-            val session = gEngineKit!!.currentSession
-            if (session != null && !callEnded) {
-                //本地画面
-                if (localSurfaceView != null) {
-                    localSurfaceView!!.setZOrderMediaOverlay(true)
-                    if (outGoing) {
-                        if (localSurfaceView!!.parent != null) {
-                            (localSurfaceView!!.parent as ViewGroup).removeView(localSurfaceView)
+            if (!callEnded) {
+                if (Constants.hostSipId!!.toBigInteger() != userId) {
+                    //本地画面
+                    Log.e(TAG, "didReceiveRemoteVideoTrack, local surfaceView = $localSurfaceView")
+                    if (localSurfaceView != null) {
+                        localSurfaceView!!.setZOrderMediaOverlay(true)
+                        if (outGoing) {
+                            if (localSurfaceView!!.parent != null) {
+                                (localSurfaceView!!.parent as ViewGroup).removeView(localSurfaceView)
+                            }
+                            if (pip_video_frame != null) {
+                                pip_video_frame.addView(localSurfaceView)
+                            }
                         }
-                        pip_video_frame!!.addView(localSurfaceView)
                     }
-                }
 
-                //远端画面
-                val surfaceView = gEngineKit!!.currentSession.setupRemoteVideo(userId, false)
-                Log.e("dds", "didReceiveRemoteVideoTrack,surfaceView = $surfaceView")
-                if (surfaceView != null) {
-                    remoteSurfaceView = surfaceView as SurfaceViewRenderer
-                    fullscreen_video_frame.removeAllViews()
-                    if (remoteSurfaceView!!.parent != null) {
-                        (remoteSurfaceView!!.parent as ViewGroup).removeView(remoteSurfaceView)
+                    //远端画面
+                    val surfaceView = WebRTCEngine.getInstance().setupRemoteVideo(userId, false)
+                    Log.e(TAG, "didReceiveRemoteVideoTrack, remote surfaceView = $surfaceView")
+                    if (surfaceView != null && fullscreen_video_frame != null) {
+                        remoteSurfaceView = surfaceView as SurfaceViewRenderer
+                        fullscreen_video_frame.removeAllViews()
+                        if (remoteSurfaceView!!.parent != null) {
+                            (remoteSurfaceView!!.parent as ViewGroup).removeView(remoteSurfaceView)
+                        }
+                        fullscreen_video_frame.addView(remoteSurfaceView)
                     }
-                    fullscreen_video_frame.addView(remoteSurfaceView)
+                } else {
+                    showMessage("主机已接听")
                 }
             }
         }
@@ -332,37 +328,7 @@ class SkyCallFragment: BaseCallFragment(), CallSession.CallSessionCallback {
 
     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()
+            callEnd(true)
         }
     }
 
@@ -372,10 +338,10 @@ class SkyCallFragment: BaseCallFragment(), CallSession.CallSessionCallback {
         }
     }
 
-    override fun didUserLeave(userId: String?) {
+    override fun didUserLeave(userId: BigInteger?) {
+        Log.w(TAG, "didUserLeave: $userId")
         handler.post {
-            //showMessage("通话结束")
-            callEnd()
+            callEnd(true)
         }
     }
 
@@ -383,22 +349,81 @@ class SkyCallFragment: BaseCallFragment(), CallSession.CallSessionCallback {
 
     @Subscribe(threadMode = ThreadMode.MAIN)
     fun onMoonEvent(messageEvent: MessageEvent) {
-        when (messageEvent.getType()) {
-            /*Constants.EVENT_TCP_MSG -> {
-                if (messageEvent.getMessage() is TcpModel) {
-                    val curTcpModel = messageEvent.getMessage() as TcpModel
-                    if (curTcpModel.getType() == TcpType.VOICE) {
-                        val curInteractionVO = Gson().fromJson(curTcpModel.data.toString(), InteractionVO::class.java)
-                        if (curTcpModel.getAction() == TcpAction.VoiceAction.HANDOFF) {
-                            //对方挂断,不论我方呼出或呼入
-                            if (Constants.interactionId == curInteractionVO.id) {
-                                gEngineKit?.endCall()
-                                callEnd()
-                            }
-                        }
+        if (messageEvent.tag == Constants.VIDEO_MSG){
+            val tcpModel = messageEvent.getMessage() as TcpModel
+            val interactionVO = Gson().fromJson(tcpModel.data.toString(), InteractionVO::class.java)
+            Log.e(TAG, "收到tcp消息" + tcpModel.getType() + " " + tcpModel.getAction() + ", data: " + tcpModel.data.toString())
+            when (tcpModel.action){
+                TcpAction.VideoAction.CALLING->{
+                    //我方呼出,对方通话中
+                    AppTool.Time.delay(3000) {
+                        RingPlayHelper.stopRingTone()
+                        cancelCall()
+                        showMessage("对方忙线中!")
                     }
                 }
-            }*/
+
+                TcpAction.VideoAction.FAILED->{
+                    //我方呼出,对方不在线,设备离线或其它错误
+                    callSuccess = true
+                    AppTool.Time.delay(3000) {
+                        RingPlayHelper.stopRingTone()
+                        cancelCall()
+                        showMessage("呼叫失败,找不到设备或对方不在线!")
+                    }
+                }
+
+                TcpAction.VideoAction.SUCCESS->{
+                    callSuccess = true
+                    DeviceChannel.calling = true
+                    Constants.interactionId = interactionVO.id
+                    Constants.visitHostId = tcpModel.fromId
+                    Util.wakeUpAndUnlock(activity)
+                    sky_voice_call_hangup.isEnabled = true
+                    showOutgoingCall()
+                    janusClient!!.connect()
+                }
+
+                TcpAction.VideoAction.ACCEPT-> {
+                    /*Constants.fromId = tcpModel.fromId
+                    DeviceChannel.calling = true
+                    CallDialogHelper.dismissCallDialog()
+                    Constants.interactionId = interactionVO.id
+                    // 加入房间
+                    val roomId = "visit-room-" + Constants.interactionId
+                    CallMultiActivity.openActivity(activity, roomId, false)*/
+                }
+
+                //主机接听
+                TcpAction.VideoAction.VIDEO_IN_CALL-> {
+                    Constants.hostSipId = interactionVO.toSipId
+                    /*Constants.fromId = tcpModel.fromId
+                    Constants.visitHostId = tcpModel.fromId
+                    DeviceChannel.calling = true
+                    Constants.interactionId = interactionVO.id*/
+                }
+
+                TcpAction.VideoAction.VIDEO_ON -> {
+                    /*Constants.fromId = tcpModel.fromId
+                    DeviceChannel.calling = true
+                    Constants.interactionId = interactionVO.id*/
+                }
+
+                TcpAction.VideoAction.REJECT->{
+                    //我方呼出,对方拒绝
+                    AppTool.Time.delay(1000) {
+                        RingPlayHelper.stopRingTone()
+                        cancelCall()
+                        showMessage("对方拒绝!")
+                    }
+                }
+
+                TcpAction.VideoAction.HANDOFF -> {
+                    if (Constants.interactionId == interactionVO.id) {
+                        callEnd(false)
+                    }
+                }
+            }
         }
     }
 

home/src/main/code/com/wdkl/ncs/android/component/home/fragment/VisitFragment.kt → home/src/main/code/com/wdkl/ncs/android/component/home/fragment/VisitFragment.kt.bak


+ 0 - 2
home/src/main/code/com/wdkl/ncs/android/component/home/helper/AppUpdateHelper.java

@@ -19,8 +19,6 @@ import java.io.PrintWriter;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 
-import static com.blankj.utilcode.util.ActivityUtils.startActivity;
-
 public class AppUpdateHelper {
     private final static String TAG = "AppUpdate";
 

+ 192 - 0
home/src/main/code/com/wdkl/ncs/android/component/home/helper/AsyncPlayer.java

@@ -0,0 +1,192 @@
+package com.wdkl.ncs.android.component.home.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 PlayThread extends Thread {
+        PlayThread() {
+            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;
+            }
+        }
+    }
+
+    public boolean isPlay() {
+        return mState == PLAY;
+    }
+
+    private void enqueueLocked(Command cmd) {
+        mCmdQueue.add(cmd);
+        if (mThread == null) {
+            acquireWakeLock();
+            mThread = new PlayThread();
+            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 - 42
home/src/main/code/com/wdkl/ncs/android/component/home/service/TcpHandleService.kt

@@ -54,48 +54,6 @@ class TcpHandleService : Service(){
     @Subscribe(threadMode = ThreadMode.MAIN)
     fun onMoonEvent(messageEvent: MessageEvent) {
         when(messageEvent.tag){
-            //拨出成功 || 有来电
-            Constants.AUDIO_MSG->{
-                Util.wakeUpAndUnlock(this)
-
-                var tcpModel = messageEvent.getMessage() as TcpModel
-                if (tcpModel.type == TcpType.VOICE){
-//                var gson = GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create()
-                    var interactionVO = Gson().fromJson(tcpModel.data.toString(), InteractionVO::class.java)
-                    var intent = Intent()
-
-                    if (tcpModel.action == TcpAction.VoiceAction.SUCCESS){  //拨出成功
-                        DeviceChannel.calling = true;
-//                        intent.setClass(this, WebRTCVoipAudioActivity::class.java)
-//                        intent.putExtra("targetId", interactionVO?.toSipId)
-//                        intent.putExtra("TcpModel", tcpModel)
-//                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-//                        intent.putExtra(WebRTCVoipAudioActivity().ACTION, WebRTCVoipAudioActivity().CALL)
-                        startActivity(intent)
-                    } else if (tcpModel.action == TcpAction.VoiceAction.CALL){  //有来电
-                        DeviceChannel.calling = true;
-//                        intent.setClass(this, WebRTCVoipAudioRingingActivity::class.java)
-//                        intent.putExtra("targetId", interactionVO?.fromSipId)
-//                        intent.putExtra("TcpModel", tcpModel)
-//                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-//                        startActivity(intent)
-                    }
-                }
-            }
-            /*Constants.VIDEO_MSG->{
-                Util.wakeUpAndUnlock(this)
-                var tcpModel = messageEvent.getMessage() as TcpModel
-
-                if (tcpModel.action == TcpAction.VideoAction.SUCCESS){  //拨出成功
-                    var interactionVO = Gson().fromJson(tcpModel.data.toString(), InteractionVO::class.java)
-                    var intent = Intent()
-                    DeviceChannel.calling = true;
-                    intent.setClass(this, VideoActivity::class.java)
-                    intent.putExtra("iaId", interactionVO.id)
-                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-                    startActivity(intent)
-                }
-            }*/
             //APP升级
             Constants.EVENT_APP_UPDATE->{
                 while (DeviceChannel.calling) {    //通话中不处理,一直等待至结束
@@ -115,6 +73,7 @@ class TcpHandleService : Service(){
                     }
                 }
             }
+
             //网络断开,1000ms重连
             /*Constants.EVENT_TCP_BREAK->{
                 var wakeLock = Util.wakeUpAndUnlock(this)

+ 0 - 8
home/src/main/code/com/wdkl/ncs/android/component/home/util/AnrFcExceptionUtil.java

@@ -11,12 +11,8 @@ import android.util.Log;
 import com.github.anrwatchdog.ANRError;
 import com.github.anrwatchdog.ANRWatchDog;
 import com.wdkl.ncs.android.component.home.activity.HomeActivity;
-import com.wdkl.ncs.android.component.nursehome.common.Constants;
 import com.wdkl.ncs.android.middleware.api.UrlManager;
 import com.wdkl.ncs.android.middleware.tcp.channel.DeviceChannel;
-import com.wdkl.skywebrtc.CallSession;
-import com.wdkl.skywebrtc.EnumType;
-import com.wdkl.skywebrtc.SkyEngineKit;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -94,10 +90,6 @@ public class AnrFcExceptionUtil implements Thread.UncaughtExceptionHandler {
 
     private void restartApp() {
         DeviceChannel.calling = false;
-        CallSession session= SkyEngineKit.Instance().getCurrentSession();
-        if(session!=null&&session.getState()!= EnumType.CallState.Idle){
-            SkyEngineKit.Instance().endCall();
-        }
 
         //重新启动app
         Intent mStartActivity = new Intent(application.getApplicationContext(), HomeActivity.class);

+ 1 - 1
home/src/main/code/com/wdkl/ncs/android/component/home/util/CallDialogHelper.java

@@ -38,7 +38,7 @@ public class CallDialogHelper {
             outCall.setVisibility(View.VISIBLE);
             inCall.setVisibility(View.GONE);
             outText.setText(callText);
-            RingPlayHelper.playRingTone(activity, R.raw.wr_ringback, true);
+            RingPlayHelper.playRingTone(activity, R.raw.ring_back2, true);
         } else {
             //来电
             outCall.setVisibility(View.GONE);

+ 1 - 1
home/src/main/code/com/wdkl/ncs/android/component/home/util/MediaPlayHelper.java

@@ -14,7 +14,7 @@ import android.view.KeyEvent;
 import com.wdkl.ncs.android.component.nursehome.common.Constants;
 import com.wdkl.ncs.android.lib.base.BaseApplication;
 import com.wdkl.ncs.android.middleware.utils.MessageEvent;
-import com.wdkl.skywebrtc.engine.AudioFocusManager;
+import com.wdkl.ncs.janus.rtc.AudioFocusManager;
 
 import org.greenrobot.eventbus.EventBus;
 

+ 33 - 0
home/src/main/code/com/wdkl/ncs/android/component/home/util/NetHelper.java

@@ -1,12 +1,15 @@
 package com.wdkl.ncs.android.component.home.util;
 
+import android.Manifest;
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 import android.os.Build;
+import android.provider.Settings;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 
@@ -438,6 +441,36 @@ public class NetHelper {
         return null;
     }
 
+    public String getIMEI() {
+        //获取序列号
+        String serial = null;
+        try {
+            serial = android.os.Build.SERIAL;
+            if (serial.equalsIgnoreCase("unknown")||serial.equalsIgnoreCase("0123456789abcdef")) {
+                final TelephonyManager mTelephony = (TelephonyManager) BaseApplication.appContext.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                    if (BaseApplication.appContext.getApplicationContext().checkSelfPermission(Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
+                        serial = null;
+                    }
+                }
+                assert mTelephony != null;
+                if (mTelephony.getDeviceId() != null) {
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                        serial = mTelephony.getImei();
+                    } else {
+                        serial = mTelephony.getDeviceId();
+                    }
+                } else {
+                    serial = Settings.Secure.getString(BaseApplication.appContext.getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
+                }
+
+            }
+        } catch (Exception e) {
+
+        }
+        return serial;
+    }
+
 
     /**
      * 根据IP地址获取MAC地址

+ 1 - 1
home/src/main/code/com/wdkl/ncs/android/component/home/util/RingPlayHelper.java

@@ -3,7 +3,7 @@ package com.wdkl.ncs.android.component.home.util;
 import android.content.Context;
 import android.media.AudioManager;
 
-import com.wdkl.core.voip.AsyncPlayer;
+import com.wdkl.ncs.android.component.home.helper.AsyncPlayer;
 
 public class RingPlayHelper {
 

BIN
home/src/main/res/drawable/av_handfree_hover.png


BIN
home/src/main/res/drawable/ic_answer_normal.png


BIN
home/src/main/res/drawable/ic_answer_press.png


BIN
home/src/main/res/drawable/ic_nurse.png


+ 5 - 0
home/src/main/res/drawable/selector_call_answer.xml

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

+ 0 - 1
home/src/main/res/layout/activity_home.xml

@@ -60,7 +60,6 @@
             android:layout_alignParentRight="true"
             android:layout_alignParentTop="true"
             android:layout_marginRight="5dp"
-            android:timeZone="GMT+8"
             android:format12Hour="HH:mm"
             android:format24Hour="HH:mm"
             android:textColor="#2F9DF1"/>

+ 0 - 7
home/src/main/res/layout/activity_web_rtc_voip_audio.xml

@@ -86,13 +86,6 @@
             android:layout_width="55px"
             android:layout_height="55px"
             android:src="@drawable/av_handfree_hover" />
-<!--        <TextView-->
-<!--            android:layout_width="wrap_content"-->
-<!--            android:layout_height="wrap_content"-->
-<!--            android:layout_centerInParent="true"-->
-<!--            android:textSize="14px"-->
-<!--            android:textColor="#ffffff"-->
-<!--            android:text="免提"/>-->
 
     </RelativeLayout>
 

+ 1 - 1
home/src/main/res/layout/call_dialog_lay.xml

@@ -24,7 +24,7 @@
             android:layout_width="40dp"
             android:layout_height="40dp"
             android:layout_marginTop="20dp"
-            android:src="@drawable/av_hangup_selector" />
+            android:src="@drawable/yu_yin_gua_duan" />
     </LinearLayout>
 
 

+ 3 - 3
home/src/main/res/layout/fragment_visiting.xml

@@ -17,7 +17,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginTop="10dp"
-            android:src="@drawable/av_hangup_selector" />
+            android:src="@drawable/yu_yin_gua_duan" />
 
         <TextView
             android:layout_width="wrap_content"
@@ -28,10 +28,10 @@
 
     </LinearLayout>
 
-    <com.wdkl.core.voip.NineGridView
+    <!--<com.wdkl.core.voip.NineGridView
         android:id="@+id/visit_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:layout_above="@id/ll_visit_end"/>
+        android:layout_above="@id/ll_visit_end"/>-->
 
 </RelativeLayout>

+ 74 - 12
home/src/main/res/layout/sky_voice_call_layout.xml

@@ -3,53 +3,75 @@
     <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:background="@android:color/background_dark">
+        android:background="@color/gray_deep">
         <!--全屏视频画面-->
         <FrameLayout
             android:id="@+id/fullscreen_video_frame"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:layout_gravity="center" />
+            android:layout_gravity="center"
+            android:visibility="gone"/>
 
         <!--小窗视频画面-->
         <FrameLayout
             android:id="@+id/pip_video_frame"
-            android:layout_width="70dp"
+            android:layout_width="80dp"
             android:layout_height="100dp"
             android:layout_gravity="top|end"
             android:layout_marginHorizontal="10dp"
-            android:layout_marginTop="10dp" />
+            android:layout_marginTop="10dp"
+            android:visibility="gone"/>
 
         <RelativeLayout
             android:layout_width="match_parent"
             android:layout_height="match_parent">
 
+            <!--语音呼叫layout-->
             <LinearLayout
                 android:id="@+id/ll_voice_call"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_centerInParent="true"
-                android:gravity="center_horizontal"
-                android:visibility="gone">
+                android:gravity="center"
+                android:orientation="vertical">
+
+                <!--<ImageView
+                    android:id="@+id/sky_voice_call_head_img"
+                    android:layout_width="120dp"
+                    android:layout_height="120dp"
+                    android:layout_marginTop="80dp"
+                    android:scaleType="centerInside"
+                    android:src="@drawable/ic_nurse" />-->
 
                 <TextView
                     android:id="@+id/sky_voice_call_calling_text"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
+                    android:layout_marginTop="24dp"
                     android:gravity="center"
-                    android:text="连接中..."
-                    android:textColor="#9E9E9F"
+                    android:text="正在呼叫..."
+                    android:textColor="@color/white"
                     android:textSize="20sp" />
+
+                <TextView
+                    android:id="@+id/sky_voice_call_timeout"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="24dp"
+                    android:gravity="center"
+                    android:text="倒计时: 00"
+                    android:textColor="@color/white"
+                    android:textSize="16sp" />
             </LinearLayout>
 
             <!--呼出-->
             <LinearLayout
                 android:id="@+id/sky_voice_call_outgoing"
-                android:layout_width="match_parent"
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_alignParentBottom="true"
                 android:layout_centerHorizontal="true"
-                android:layout_marginBottom="40dp"
+                android:layout_marginBottom="80dp"
                 android:gravity="center_horizontal"
                 android:orientation="vertical"
                 android:visibility="visible">
@@ -60,16 +82,56 @@
                     android:layout_height="wrap_content"
                     android:gravity="center"
                     android:text="00:00"
-                    android:textColor="@color/color_white"
+                    android:textColor="@color/white"
                     android:textSize="16sp" />
 
                 <ImageView
                     android:id="@+id/sky_voice_call_hangup"
                     android:layout_width="40dp"
                     android:layout_height="40dp"
-                    android:layout_marginTop="10dp"
+                    android:layout_marginTop="20dp"
                     android:src="@drawable/selector_call_hangup" />
             </LinearLayout>
+
+            <!--来电-->
+            <LinearLayout
+                android:id="@+id/sky_voice_call_incoming"
+                android:layout_width="match_parent"
+                android:layout_height="140dp"
+                android:layout_alignParentBottom="true"
+                android:layout_centerHorizontal="true"
+                android:layout_marginBottom="80dp"
+                android:gravity="bottom"
+                android:orientation="horizontal"
+                android:visibility="gone">
+
+                <View
+                    android:layout_width="0dp"
+                    android:layout_height="1dp"
+                    android:layout_weight="2" />
+
+                <ImageView
+                    android:id="@+id/sky_voice_call_ring_reject"
+                    android:layout_width="100dp"
+                    android:layout_height="100dp"
+                    android:src="@drawable/selector_call_hangup" />
+
+                <View
+                    android:layout_width="0dp"
+                    android:layout_height="1dp"
+                    android:layout_weight="1" />
+
+                <ImageView
+                    android:id="@+id/sky_voice_call_ring_pickup_audio"
+                    android:layout_width="100dp"
+                    android:layout_height="100dp"
+                    android:src="@drawable/selector_call_answer" />
+
+                <View
+                    android:layout_width="0dp"
+                    android:layout_height="1dp"
+                    android:layout_weight="2" />
+            </LinearLayout>
         </RelativeLayout>
     </FrameLayout>
 </layout>

+ 1 - 1
home/src/main/res/layout/video_connected_action.xml

@@ -24,7 +24,7 @@
             android:id="@+id/hangup_aciton"
             android:layout_width="40dp"
             android:layout_height="40dp"
-            android:src="@drawable/av_hangup_selector" />
+            android:src="@drawable/yu_yin_gua_duan" />
 
         <TextView
             android:layout_width="wrap_content"

BIN
home/src/main/res/raw/ring_back2.wav


+ 96 - 0
home/src/main/res/values/colors.xml

@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#008577</color>
+    <color name="colorPrimaryDark">#00574B</color>
+    <color name="colorAccent">#D81B60</color>
+
+    <color name="white">#ffffff</color>
+    <color name="text_black">#808080</color>
+    <color name="title_text">#F78B8F</color>
+    <color name="text_room_color">#020202</color>
+    <color name="text_name_color">#B4B4B4</color>
+
+    <color name="drawer_header_text">#2c2c2c</color>
+    <color name="black">#000000</color>
+    <color name="drawer_item_bg_p">#e8e8e8</color>
+    <color name="drawer_item_bg_n">#fffff9</color>
+    <color name="green">#00FF00</color>
+
+    <color name="main_color">#2F9DF1</color>
+    <color name="main_bg">#EAF2F9</color>
+    <color name="content_color">#F3F3F3</color>
+    <color name="transparent">#00000000</color>
+    <color name="transparent_dialog">#DDFFFFFF</color>
+    <!--title颜色-->
+    <color name="title_text_color">#43413f</color>
+    <!--Dialog线颜色-->
+    <color name="dialog_line_color">#e7e7e7</color>
+    <!--红色按钮颜色-->
+    <color name="button_color">#f57550</color>
+
+    <!-- 黄色颜色 -->
+    <color name="yellow_color">#f8c255</color>
+    <!-- 红色颜色 -->
+    <color name="red_color">#FFFD3B30</color>
+    <!--footbar Text-->
+    <color name="footbar_text_color">#7d7d7d</color>
+
+    <color name="zmcx_orange_color">#f29200</color>
+    <!--版本信息字体颜色-->
+    <color name="versioninfo_text_color">#666666</color>
+    <!--公共按钮-->
+    <color name="button_color_n">#e46546</color>
+    <color name="button_color_p">#f37556</color>
+    <!--吐槽字体颜色-->
+    <color name="tuskkomi_et_textcolor">#707070</color>
+    <!--搜索输入框颜色-->
+    <color name="search_et_bg_color">#d2d2d2</color>
+
+    <color name="black_text_color">#333333</color>
+    <color name="black_wlt_color">#666666</color>
+    <color name="gray_text_color">#999999</color>
+
+    <!--APP基本色调-->
+    <color name="app_base_color">#FFF27556</color>
+    <color name="register_text_color">#969696</color>
+
+    <!--sideLetterbar-->
+    <color name="gray">#8c8c8c</color>
+    <color name="gray_deep">#5c5c5c</color>
+    <color name="alpha_gray">#CC646464</color>
+    <color name="theme_gray">#646464</color>
+
+    <!--食安卫士信息-->
+    <color name="saws_tel_btn_color">#ffac51</color>
+    <color name="saws_message_btn_color">#f57550</color>
+    <color name="province_line_border">#B8B8B8</color>
+
+    <!--专题详情-->
+    <color name="special_bg">#FFF2F2F2</color>
+    <color name="sing_bg_color">#eeeeee</color>
+    <!--确认订单-->
+    <color name="duancode_line_color">#dbdbdb</color>
+
+    <!--关于我们-->
+    <color name="about_text_color">#58504e</color>
+
+    <!--产品详情-->
+    <color name="product_detail_rb_bg_color">#cbcbcb</color>
+    <color name="product_bottom_shopcar_bg_color">#d5d5d5</color>
+
+    <!--社区详情输入框-->
+    <color name="board_et_bg_color">#b9bbbe</color>
+
+    <!--删除color-->
+    <color name="delete_text_color">#0090ff</color>
+    <color name="version_background">#b0000000</color>
+
+
+    <!--cost list 列表中的颜色-->
+    <color name="cost_list_title">#C4C4C4</color>
+    <color name="cost_item">#F8F8F8</color>
+
+    <!--右侧菜单栏颜色-->
+    <color name="right_item_bg">#3CA2E0</color>
+    <color name="right_item_select">#B1DAF3</color>
+</resources>

+ 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(':middleware')
+}

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

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

@@ -0,0 +1,886 @@
+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 static final int ERROR_ON_CONNECT = 0x03;
+
+    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();
+    }
+
+    @Override
+    public void onError(String err) {
+        if (janusCallback != null) {
+            janusCallback.onError(ERROR_ON_CONNECT, err);
+        }
+    }
+
+    public String randomString(int length) {
+        String str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+        Random random = new Random();
+        StringBuilder sb = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            sb.append(str.charAt(random.nextInt(str.length())));
+        }
+        return sb.toString();
+    }
+
+    public interface DestroyRoomCallback {
+        void onSuccess();
+
+        void onFailed();
+    }
+
+    public interface JanusCallback {
+        void onCreateSession(BigInteger sessionId);
+
+        void onCreateRoom(BigInteger handleId);
+
+        void onAttached(BigInteger handleId);
+
+        /**
+         * 订阅回调
+         *
+         * @param subscribeHandleId 订阅HandlerId
+         * @param feedId            订阅 feedId
+         */
+        void onSubscribeAttached(BigInteger subscribeHandleId, BigInteger feedId);
+
+        void onDetached(BigInteger handleId);
+
+        void onHangup(BigInteger handleId);
+
+        void onMessage(BigInteger sender, BigInteger handleId, JSONObject msg, JSONObject jsep);
+
+        void onIceCandidate(BigInteger handleId, JSONObject candidate);
+
+        void onDestroySession(BigInteger sessionId);
+
+        void onError(int errorCode, String error);
+    }
+}

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,112 @@
+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.onError(t.getMessage());
+            }
+        }
+    }
+
+    public void setWebSocketCallback(WebSocketCallback webSocketCallback) {
+        this.webSocketCallback = webSocketCallback;
+    }
+
+    public interface WebSocketCallback {
+        void onOpen();
+
+        void onMessage(String text);
+
+        void onClosed();
+
+        void onError(String err);
+    }
+}

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

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

@@ -0,0 +1,193 @@
+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);
+        rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED;
+        //rtcConfig.networkPreference = PeerConnection.AdapterType.WIFI;
+        //rtcConfig.candidateNetworkPolicy = PeerConnection.CandidateNetworkPolicy.LOW_COST;
+        //rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
+        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;
+    }
+}

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

@@ -0,0 +1,771 @@
+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.JanusConstant;
+
+import org.webrtc.AudioSource;
+import org.webrtc.AudioTrack;
+import org.webrtc.Camera1Enumerator;
+import org.webrtc.Camera2Enumerator;
+import org.webrtc.CameraEnumerator;
+import org.webrtc.CameraVideoCapturer;
+import org.webrtc.DefaultVideoDecoderFactory;
+import org.webrtc.DefaultVideoEncoderFactory;
+import org.webrtc.EglBase;
+import org.webrtc.MediaConstraints;
+import org.webrtc.MediaStream;
+import org.webrtc.PeerConnection;
+import org.webrtc.PeerConnectionFactory;
+import org.webrtc.RendererCommon;
+import org.webrtc.ScreenCapturerAndroid;
+import org.webrtc.SurfaceTextureHelper;
+import org.webrtc.SurfaceViewRenderer;
+import org.webrtc.VideoCapturer;
+import org.webrtc.VideoDecoderFactory;
+import org.webrtc.VideoEncoderFactory;
+import org.webrtc.VideoSource;
+import org.webrtc.VideoTrack;
+import org.webrtc.audio.AudioDeviceModule;
+import org.webrtc.audio.JavaAudioDeviceModule;
+import org.webrtc.voiceengine.WebRtcAudioManager;
+import org.webrtc.voiceengine.WebRtcAudioUtils;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+public class WebRTCEngine {
+    private static final String TAG = "WebRTCEngine";
+    private PeerConnectionFactory factory;
+    private EglBase mRootEglBase;
+    private MediaStream _localStream;
+    private VideoSource videoSource;
+    private AudioSource audioSource;
+    private AudioTrack _localAudioTrack;
+    private VideoCapturer captureAndroid;
+    private SurfaceTextureHelper surfaceTextureHelper;
+
+    private ProxyVideoSink localSink;
+    private SurfaceViewRenderer localRenderer;
+
+    private static final String VIDEO_TRACK_ID = "ARDAMSv0";
+    private static final String AUDIO_TRACK_ID = "ARDAMSa0";
+    public static final String VIDEO_CODEC_H264 = "H264";
+    public static final String VIDEO_TRACK_TYPE = "video";
+    private static final String VIDEO_CODEC_VP8 = "VP8";
+    private static final String VIDEO_CODEC_VP9 = "VP9";
+    private static final String DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT = "DtlsSrtpKeyAgreement";
+    private static final int VIDEO_RESOLUTION_WIDTH = 1280;
+    private static final int VIDEO_RESOLUTION_HEIGHT = 720;
+    private static final int FPS = 20;
+    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;
+        if (JanusConstant.STUN_SERVER != null) {
+            for (String stunServer : JanusConstant.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 (JanusConstant.TURN_SERVER != null){
+            for(String turnServer: JanusConstant.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() {
+        VideoCapturer videoCapturer;
+
+        if (screencaptureEnabled) {
+            return createScreenCapturer();
+        }
+
+        if (Build.MODEL.equals("rk3128")) {
+            return createCameraCapture(new Camera1Enumerator(true));
+        } else {
+            if (Camera2Enumerator.isSupported(mContext)) {
+                videoCapturer = createCameraCapture(new Camera2Enumerator(mContext));
+            } else {
+                videoCapturer = createCameraCapture(new Camera1Enumerator(true));
+            }
+            return videoCapturer;
+        }
+    }
+
+    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);
+        }
+    }
+}

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

+ 17 - 0
janus/src/main/java/com/wdkl/ncs/janus/util/JanusConstant.java

@@ -0,0 +1,17 @@
+package com.wdkl.ncs.janus.util;
+
+public class JanusConstant {
+    //public final static String GATEWAY_URL="8.129.220.143";
+    //public static String GATEWAY_URL="172.28.100.100";
+    //public final static String GATEWAY_URL="119.23.151.229";
+    //public final static String GATEWAY_URL="172.18.0.33";
+    //public static String GATEWAY_WS_PORT="8188";
+
+    //public static String JANUS_URL = "ws://172.28.100.100:8188"; //默认的
+    public static String JANUS_URL = "ws://8.129.220.143:8188"; //默认的
+    //public static String STUN1 = "stun:" +GATEWAY_URL+ ":3478";
+    //public static String[] STUN_SERVER = null; //new String[]{STUN1};
+    public static String[] STUN_SERVER = new String[]{"stun:8.129.220.143:3478"};
+
+    public static String[] TURN_SERVER = null; //new String[]{"turn:stun.l.google.com:19302|username|password"};
+}

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

+ 16 - 0
middleware/src/main/code/com/wdkl/ncs/android/middleware/common/Constants.kt

@@ -24,10 +24,26 @@ class Constants {
         var inviteId: String? = ""
         //探视主机id
         var visitHostId: Int? = -1
+        var hostSipId: String? = ""
 
         var fromId: Int? = -1
         var interactionId: Int? = -1
 
+        //待机
+        const val CALL_STANDBY = 0
+
+        //呼叫中
+        const val CALL_OUTGOING = 1
+
+        //来电中
+        const val CALL_INCOMING = 2
+
+        //通话中
+        const val CALL_CALLING = 3
+
+        //通话状态
+        var CALL_STATE = CALL_STANDBY
+
         //手柄拿起
         val HOOK_OFF = "com.android.PhoneWinowManager.HOOK_OFF"
         //手柄放下

+ 17 - 0
middleware/src/main/code/com/wdkl/ncs/android/middleware/tcp/channel/VoiceUtil.java

@@ -1,6 +1,7 @@
 package com.wdkl.ncs.android.middleware.tcp.channel;
 
 import com.wdkl.ncs.android.middleware.model.vo.InteractionVO;
+import com.wdkl.ncs.android.middleware.tcp.TcpClient;
 import com.wdkl.ncs.android.middleware.tcp.dto.TcpModel;
 import com.wdkl.ncs.android.middleware.tcp.enums.TcpAction;
 import com.wdkl.ncs.android.middleware.tcp.enums.TcpType;
@@ -19,6 +20,14 @@ public class VoiceUtil {
         return tcpModel;
     }
 
+    public static TcpModel voiceCall(Integer fromId){
+        TcpModel tcpModel = new TcpModel();
+        tcpModel.setType(TcpType.VOICE);
+        tcpModel.setAction(TcpAction.VoiceAction.CALL);
+        tcpModel.setFromId(fromId);
+        return tcpModel;
+    }
+
     //营养师 管家呼叫 要toId
 //    public static TcpModel voiceCall(Integer fromId, Integer toId){
 //        TcpModel tcpModel = new TcpModel();
@@ -110,4 +119,12 @@ public class VoiceUtil {
         tcpModel.setData(map);
         return tcpModel;
     }
+
+
+
+    //语音呼叫
+    public static void startAudioCall(Integer fromId) {
+        TcpModel tcpModel = VoiceUtil.voiceCall(fromId);
+        TcpClient.getInstance().sendMsg(tcpModel.toJson());
+    }
 }

+ 2 - 1
settings.gradle

@@ -1 +1,2 @@
-include ':app', ':common', ':home', ':resource', ':middleware', 'webrtc', 'rtc-chat', 'libwebrtc'
+include ':app', ':common', ':home', ':resource', ':middleware', ':janus'
+//'webrtc', 'rtc-chat', 'libwebrtc'