├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_round.png
│ │ │ └── mine_default_avatar.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── switch_camera.png
│ │ │ ├── ic_launcher_round.png
│ │ │ ├── video_chat_answer.png
│ │ │ └── video_chat_hangup.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── xml
│ │ │ └── network_security_config.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── values-night
│ │ │ └── themes.xml
│ │ ├── drawable
│ │ │ ├── video_chat_hangup_selector.xml
│ │ │ ├── video_chat_answer_selector.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ │ ├── item_contact.xml
│ │ │ ├── activity_contacts.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── activity_video_call_send.xml
│ │ │ └── activity_video_call_receive.xml
│ │ └── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ └── com
│ │ │ └── alick
│ │ │ └── learnwebrtc
│ │ │ ├── bean
│ │ │ ├── Contact.kt
│ │ │ ├── SdpBean.kt
│ │ │ ├── RequestMsgBean.kt
│ │ │ ├── IceCandidateBean.kt
│ │ │ ├── response
│ │ │ │ ├── base
│ │ │ │ │ └── BaseResponse.kt
│ │ │ │ ├── PeersResponse.kt
│ │ │ │ ├── AddContactResponse.kt
│ │ │ │ ├── AllContactResponse.kt
│ │ │ │ ├── CreateRoomResponse.kt
│ │ │ │ ├── RemoveContactResponse.kt
│ │ │ │ ├── IceCandidateResponse.kt
│ │ │ │ ├── SdpAnswerResponse.kt
│ │ │ │ └── SdpOfferResponse.kt
│ │ │ ├── RoomBean.kt
│ │ │ └── request
│ │ │ │ ├── base
│ │ │ │ ├── BaseSignalRequest.kt
│ │ │ │ └── BaseRequest.kt
│ │ │ │ ├── GetAllContactRequest.kt
│ │ │ │ ├── JoinRequest.kt
│ │ │ │ ├── LeaveRequest.kt
│ │ │ │ ├── RingRequest.kt
│ │ │ │ ├── CancelRequest.kt
│ │ │ │ ├── RejectRequest.kt
│ │ │ │ ├── InviteRequest.kt
│ │ │ │ ├── CreateRoomRequest.kt
│ │ │ │ ├── SdpOfferRequest.kt
│ │ │ │ ├── SdpAnswerRequest.kt
│ │ │ │ ├── SendIceCandidateRequest.kt
│ │ │ │ └── SendIceCandidateRemovedRequest.kt
│ │ │ ├── http_api
│ │ │ ├── BaseApiResponse.kt
│ │ │ ├── GetAllRoomListResponse.kt
│ │ │ └── ApiUtils.kt
│ │ │ ├── utils
│ │ │ ├── RoomIdUtils.kt
│ │ │ ├── BLog.kt
│ │ │ ├── MMKVUtils.kt
│ │ │ ├── ToastUtils.kt
│ │ │ ├── FilterUtils.java
│ │ │ ├── AppHolder.java
│ │ │ └── JsonUtils.java
│ │ │ ├── core
│ │ │ ├── sdp
│ │ │ │ ├── CreateSdpObserver.kt
│ │ │ │ └── SetSdpObserver.kt
│ │ │ └── WebRTCEngine.kt
│ │ │ ├── manager
│ │ │ ├── AccountManager.kt
│ │ │ ├── SignalManager.kt
│ │ │ └── WebSocketManager.kt
│ │ │ ├── App.kt
│ │ │ ├── constant
│ │ │ ├── Constant.kt
│ │ │ ├── WebRtcConstant.kt
│ │ │ └── RRType.kt
│ │ │ ├── adapter
│ │ │ └── ContactsAdapter.kt
│ │ │ ├── widget
│ │ │ └── ChatTimeTextView.kt
│ │ │ ├── ContactsActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── VideoCallReceiveActivity.kt
│ │ │ ├── VideoCallSendActivity.kt
│ │ │ └── BaseVideoCallActivity.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── .idea
├── .gitignore
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── vcs.xml
├── compiler.xml
├── dictionaries
│ └── Administrator.xml
├── misc.xml
├── gradle.xml
└── jarRepositories.xml
├── key
└── alick.jks
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── README.md
├── gradle.properties
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "LearnWebRtc"
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/key/alick.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/key/alick.jks
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | LearnWebRtc
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/Contact.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean
2 |
3 | data class Contact(val account: String)
4 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/SdpBean.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean
2 |
3 | data class SdpBean(val description:String)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/switch_camera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-xxhdpi/switch_camera.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/video_chat_answer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-xxhdpi/video_chat_answer.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/video_chat_hangup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-xxhdpi/video_chat_hangup.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/mine_default_avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-xhdpi/mine_default_avatar.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wolongalick/LearnWebRtc/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/RequestMsgBean.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean
2 |
3 | data class RequestMsgBean(val msgType: String, val fromAccount: String)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/IceCandidateBean.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean
2 |
3 | open class IceCandidateBean(val label: Int, val id: String, val candidate: String)
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/http_api/BaseApiResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.http_api
2 |
3 | open class BaseApiResponse(var data:Data?=null, val code:Int=0, val msg:String="")
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/dictionaries/Administrator.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | capturer
5 | mmkv
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/response/base/BaseResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.response.base
2 |
3 | open class BaseResponse(
4 | val msgType: String,val roomId:String="", val fromAccount: String = "", val data: Data? = null
5 | )
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 18 13:48:45 CST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/RoomBean.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean
2 |
3 | data class RoomBean(val roomId: String,val creatorAccount:String) {
4 | var roomName: String = ""
5 | var roomSize: Int = 0
6 | val contactList: MutableList = mutableListOf()
7 | }
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/base/BaseSignalRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request.base
2 |
3 | open class BaseSignalRequest(
4 | msgType: String,
5 | val roomId: String,
6 | val toAccount: String,
7 | data: Any? = null
8 | ) : BaseRequest(msgType, data)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/utils/RoomIdUtils.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.utils
2 |
3 | import java.util.*
4 |
5 | class RoomIdUtils {
6 | companion object{
7 | fun createRoomId():String{
8 | return UUID.randomUUID().toString()
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/GetAllContactRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.request.base.BaseRequest
4 | import com.alick.learnwebrtc.constant.RequestType
5 |
6 | class GetAllContactRequest(): BaseRequest(RequestType.All_CONTACT)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/response/PeersResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.response
2 |
3 | import com.alick.learnwebrtc.bean.response.base.BaseResponse
4 | import com.alick.learnwebrtc.constant.ResponseType
5 |
6 | class PeersResponse() : BaseResponse(ResponseType.PEERS) {
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/response/AddContactResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.response
2 |
3 | import com.alick.learnwebrtc.bean.Contact
4 | import com.alick.learnwebrtc.bean.response.base.BaseResponse
5 |
6 |
7 | class AddContactResponse(msgType: String) : BaseResponse(msgType)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/base/BaseRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request.base
2 |
3 | import com.alick.learnwebrtc.manager.AccountManager
4 |
5 | open class BaseRequest(val msgType: String, val data: Any? = null) {
6 | val fromAccount: String = AccountManager.getAccount()
7 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/response/AllContactResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.response
2 |
3 | import com.alick.learnwebrtc.bean.Contact
4 | import com.alick.learnwebrtc.bean.response.base.BaseResponse
5 |
6 | class AllContactResponse(msgType: String) : BaseResponse>(msgType)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/http_api/GetAllRoomListResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.http_api
2 |
3 | import com.alick.learnwebrtc.bean.RoomBean
4 |
5 | /**
6 | * @author 崔兴旺
7 | * @description
8 | * @date 2021/6/27 13:36
9 | */
10 | class GetAllRoomListResponse : BaseApiResponse>()
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/core/sdp/CreateSdpObserver.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.core.sdp
2 |
3 | import org.webrtc.SdpObserver
4 |
5 | abstract class CreateSdpObserver : SdpObserver {
6 |
7 | final override fun onSetSuccess() {
8 |
9 | }
10 |
11 | final override fun onSetFailure(p0: String?) {
12 |
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/JoinRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
4 | import com.alick.learnwebrtc.constant.RequestType
5 |
6 | class JoinRequest(roomId: String, toAccount: String) :
7 | BaseSignalRequest(RequestType.JOIN, roomId, toAccount)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/LeaveRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
4 | import com.alick.learnwebrtc.constant.RequestType
5 |
6 | class LeaveRequest(roomId: String, toAccount: String) :
7 | BaseSignalRequest(RequestType.LEAVE, roomId, toAccount)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/RingRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
4 | import com.alick.learnwebrtc.constant.RequestType
5 |
6 | class RingRequest(roomId: String, toAccount: String) :
7 | BaseSignalRequest(RequestType.RING, roomId, toAccount)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/manager/AccountManager.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.manager
2 |
3 | import com.alick.learnwebrtc.constant.Constant
4 | import com.alick.learnwebrtc.utils.MMKVUtils
5 |
6 | object AccountManager {
7 | fun getAccount():String{
8 | return MMKVUtils.getString(Constant.MMKV_KEY_ACCOUNT)
9 | }
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/CancelRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
4 | import com.alick.learnwebrtc.constant.RequestType
5 |
6 | class CancelRequest(roomId: String, toAccount: String) :
7 | BaseSignalRequest(RequestType.CANCEL, roomId, toAccount)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/RejectRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
4 | import com.alick.learnwebrtc.constant.RequestType
5 |
6 | class RejectRequest(roomId: String, toAccount: String) :
7 | BaseSignalRequest(RequestType.REJECT, roomId, toAccount)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/InviteRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
4 | import com.alick.learnwebrtc.constant.RequestType
5 |
6 | class InviteRequest(roomId: String, toAccount: String) :
7 | BaseSignalRequest(RequestType.INVITE, roomId, toAccount) {
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/response/CreateRoomResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.response
2 |
3 | import com.alick.learnwebrtc.bean.RoomBean
4 | import com.alick.learnwebrtc.bean.response.base.BaseResponse
5 | import com.alick.learnwebrtc.constant.ResponseType
6 |
7 | class CreateRoomResponse : BaseResponse(ResponseType.CREATE_SUCCESS)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/response/RemoveContactResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.response
2 |
3 | import com.alick.learnwebrtc.bean.Contact
4 | import com.alick.learnwebrtc.bean.response.base.BaseResponse
5 | import com.alick.learnwebrtc.constant.ResponseType
6 |
7 |
8 | class RemoveContactResponse: BaseResponse(ResponseType.REMOVE_CONTACT) {
9 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/core/sdp/SetSdpObserver.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.core.sdp
2 |
3 | import org.webrtc.SdpObserver
4 | import org.webrtc.SessionDescription
5 |
6 | abstract class SetSdpObserver : SdpObserver {
7 | final override fun onCreateSuccess(p0: SessionDescription?) {
8 | }
9 |
10 | final override fun onCreateFailure(p0: String?) {
11 | }
12 |
13 |
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/CreateRoomRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
4 | import com.alick.learnwebrtc.constant.RequestType
5 | import com.alick.learnwebrtc.utils.RoomIdUtils
6 |
7 | class CreateRoomRequest : BaseSignalRequest(
8 | RequestType.CREATE,
9 | RoomIdUtils.createRoomId(),
10 | ""
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/App.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc
2 |
3 | import android.app.Application
4 | import com.alick.learnwebrtc.utils.AppHolder
5 | import com.alick.learnwebrtc.utils.ToastUtils
6 | import com.tencent.mmkv.MMKV
7 |
8 | class App : Application() {
9 | override fun onCreate() {
10 | super.onCreate()
11 | AppHolder.init(this);
12 | MMKV.initialize(this)
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/SdpOfferRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.SdpBean
4 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
5 | import com.alick.learnwebrtc.constant.RequestType
6 |
7 | class SdpOfferRequest(roomId: String, toAccount: String, sdpBean: SdpBean) : BaseSignalRequest(
8 | RequestType.SDP_OFFER, roomId,
9 | toAccount,data = sdpBean
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/SdpAnswerRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.SdpBean
4 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
5 | import com.alick.learnwebrtc.constant.RequestType
6 |
7 | class SdpAnswerRequest(roomId: String, toAccount: String, sdpBean: SdpBean) : BaseSignalRequest(
8 | RequestType.SDP_ANSWER, roomId,
9 | toAccount, data = sdpBean
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/response/IceCandidateResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.response
2 |
3 | import com.alick.learnwebrtc.bean.IceCandidateBean
4 | import com.alick.learnwebrtc.bean.response.base.BaseResponse
5 |
6 | class IceCandidateResponse(
7 | msgType: String,
8 | roomId: String ,
9 | fromAccount: String ,
10 | data: IceCandidateBean
11 | ) : BaseResponse(msgType, roomId, fromAccount, data) {
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/SendIceCandidateRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.IceCandidateBean
4 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
5 | import com.alick.learnwebrtc.constant.RequestType
6 |
7 | class SendIceCandidateRequest(roomId: String,toAccount: String, data: IceCandidateBean) :
8 | BaseSignalRequest(RequestType.CANDIDATE,roomId, toAccount, data) {
9 |
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/response/SdpAnswerResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.response
2 |
3 | import com.alick.learnwebrtc.bean.SdpBean
4 | import com.alick.learnwebrtc.bean.response.base.BaseResponse
5 | import com.alick.learnwebrtc.constant.ResponseType
6 |
7 | class SdpAnswerResponse(
8 | roomId: String = "",
9 | fromAccount: String = "",
10 | data: SdpBean? = null
11 | ) : BaseResponse(ResponseType.SDP_ANSWER, roomId, fromAccount, data) {
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/response/SdpOfferResponse.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.response
2 |
3 | import com.alick.learnwebrtc.bean.SdpBean
4 | import com.alick.learnwebrtc.bean.response.base.BaseResponse
5 | import com.alick.learnwebrtc.constant.ResponseType
6 |
7 | class SdpOfferResponse(
8 | roomId: String = "",
9 | fromAccount: String = "",
10 | data: SdpBean? = null
11 | ) : BaseResponse(ResponseType.SDP_OFFER, roomId, fromAccount, data) {
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/utils/BLog.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.utils
2 |
3 | import android.util.Log
4 |
5 | class BLog {
6 | companion object {
7 |
8 | fun i(msg: String, tag: String = "alick") {
9 | Log.i(tag, msg)
10 | }
11 |
12 | fun d(msg: String, tag: String = "alick") {
13 | Log.d(tag, msg)
14 | }
15 |
16 | fun e(msg: String, tag: String = "alick") {
17 | Log.e(tag, msg)
18 | }
19 | }
20 |
21 |
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/bean/request/SendIceCandidateRemovedRequest.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.bean.request
2 |
3 | import com.alick.learnwebrtc.bean.IceCandidateBean
4 | import com.alick.learnwebrtc.bean.request.base.BaseSignalRequest
5 | import com.alick.learnwebrtc.constant.RequestType
6 |
7 | class SendIceCandidateRemovedRequest(roomId: String,toAccount: String, data: Data) :
8 | BaseSignalRequest(RequestType.REMOVE_CANDIDATES,roomId, toAccount, data) {
9 |
10 | data class Data(val type: String, val candidates: MutableList)
11 |
12 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/utils/MMKVUtils.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.utils
2 |
3 | import com.tencent.mmkv.MMKV
4 |
5 |
6 | class MMKVUtils {
7 | companion object {
8 | private var kv: MMKV = MMKV.defaultMMKV()
9 |
10 | fun getString(key: String): String {
11 | return kv.getString(key, "") ?: ""
12 | }
13 |
14 | fun getInt(key: String): Int {
15 | return kv.getInt(key, 0)
16 | }
17 |
18 | fun setString(key: String, value: String) {
19 | kv.putString(key, value)
20 | }
21 |
22 | fun setInt(key: String, value: Int) {
23 | kv.putInt(key, value)
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/constant/Constant.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.constant
2 |
3 | class Constant {
4 | companion object {
5 | const val MMKV_KEY_OUTER_ADDRESS = "outer_address"
6 | const val MMKV_KEY_INNER_ADDRESS = "inner_address"
7 | const val MMKV_KEY_USING_ADDRESS = "using_address"
8 | const val MMKV_KEY_SELECT_ADDRESS_TYPE = "select_address_type"
9 |
10 | const val MMKV_VALUE_SELECT_ADDRESS_TYPE_OUTER="outer"
11 | const val MMKV_VALUE_SELECT_ADDRESS_TYPE_INNER="inner"
12 |
13 |
14 | const val MMKV_KEY_ACCOUNT = "account"
15 |
16 | const val DEFAULT_OUTER_ADDRESS="39.105.182.221:5001"
17 | const val DEFAULT_INNER_ADDRESS="192.168.129.162:5001"
18 | }
19 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LearnWebRtc
2 | 这是一个用于音视频通话的webrtc安卓端项目
3 |
4 | 服务端项目:https://github.com/wolongalick/LearnWebRtcServer
5 |
6 | # 联系人页面
7 | 
8 |
9 | # 呼叫中
10 | 
11 |
12 | # 来电
13 | 
14 |
15 | # 正在通话
16 |
17 | 
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/utils/ToastUtils.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.utils
2 |
3 | import android.os.Handler
4 | import android.os.Looper
5 | import android.widget.Toast
6 |
7 | object ToastUtils {
8 | private val mainHandler by lazy {
9 | Handler(Looper.getMainLooper())
10 | }
11 |
12 | fun show(msg: String, isError: Boolean = true) {
13 | if (Looper.getMainLooper() == Looper.myLooper()) {
14 | Toast.makeText(AppHolder.getApp(), msg, Toast.LENGTH_SHORT).show()
15 | } else {
16 | mainHandler.post {
17 | Toast.makeText(AppHolder.getApp(), msg, Toast.LENGTH_SHORT).show()
18 | }
19 | }
20 |
21 | if (isError) {
22 | BLog.e(msg)
23 | } else {
24 | BLog.i(msg)
25 | }
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/constant/WebRtcConstant.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.constant
2 |
3 | /**
4 | * WebRtc相关常量
5 | */
6 | class WebRtcConstant {
7 | companion object{
8 | const val AUDIO_ECHO_CANCELLATION_CONSTRAINT = "googEchoCancellation" //回音消除
9 | const val AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT = "googAutoGainControl" //自动增益
10 | const val AUDIO_HIGH_PASS_FILTER_CONSTRAINT = "googHighpassFilter" //高通滤波器
11 | const val AUDIO_NOISE_SUPPRESSION_CONSTRAINT = "googNoiseSuppression" //降噪
12 |
13 | const val SDP_OFFER_TO_RECEIVE_AUDIO = "OfferToReceiveAudio" //提供接收音频
14 | const val SDP_OFFER_TO_RECEIVE_VIDEO = "OfferToReceiveVideo" //提供接收视频
15 |
16 | const val VIDEO_TRACK_ID = "ARDAMSv0"
17 | const val AUDIO_TRACK_ID = "ARDAMSa0"
18 | const val VIDEO_AUDIO_TRACK_ID = "ARDAMS"
19 |
20 | const val HD_VIDEO_WIDTH = 1280
21 | const val HD_VIDEO_HEIGHT = 720
22 | const val BPS_IN_KBPS = 1000
23 |
24 | const val VIDEO_TRACK_TYPE = "video"
25 | }
26 | }
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/video_chat_hangup_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
-
6 |
7 |
8 |
9 |
10 |
11 | -
12 |
13 |
14 |
15 |
16 | -
17 |
18 |
-
19 |
20 |
21 |
22 |
23 |
24 | -
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/video_chat_answer_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
-
6 |
7 |
8 |
9 |
10 |
11 | -
12 |
13 |
14 |
15 |
16 | -
17 |
18 |
-
19 |
20 |
21 |
22 |
23 |
24 | -
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | android.injected.testOnly=false
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_contact.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
22 |
23 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/utils/FilterUtils.java:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.utils;
2 |
3 | import java.util.Collection;
4 | import java.util.Iterator;
5 | import java.util.List;
6 |
7 | /**
8 | * 数据工具类
9 | * Created by cxw on 2017/3/1.
10 | */
11 | public class FilterUtils {
12 | public interface FilterCallback {
13 | /**
14 | * 是否需要移除
15 | *
16 | * @param model
17 | * @return
18 | */
19 | boolean isNeedRemove(E model);
20 | }
21 |
22 | /**
23 | * 过滤集合中的数据
24 | *
25 | * @param list 待处理的数据集合
26 | * @param filterCallback
27 | * @param
28 | * @return 已删除的个数
29 | */
30 | public static int filterList(List list, FilterCallback filterCallback) {
31 | if (list == null) {
32 | return 0;
33 | }
34 | int deleteCount = 0;
35 | Iterator iterator = list.iterator();
36 |
37 | E e;
38 | while (iterator.hasNext()) {
39 | e = iterator.next();
40 | if (filterCallback.isNeedRemove(e)) {
41 | iterator.remove();
42 | deleteCount++;
43 | }
44 | }
45 |
46 | return deleteCount;
47 | }
48 |
49 | /**
50 | * 判断集合数据是否为空
51 | *
52 | * @param collection
53 | * @return
54 | */
55 | public static boolean isEmpty(Collection collection) {
56 | return collection == null || collection.isEmpty();
57 | }
58 |
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/adapter/ContactsAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.alick.learnwebrtc.R
8 | import com.alick.learnwebrtc.bean.Contact
9 | import com.alick.learnwebrtc.manager.AccountManager
10 | import kotlinx.android.synthetic.main.item_contact.view.*
11 |
12 | class ContactsAdapter(private val list: MutableList) :
13 | RecyclerView.Adapter() {
14 |
15 | var onClickVideo: ((Contact) -> Unit)? = null
16 |
17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ContactViewHolder {
18 | return ContactViewHolder(
19 | LayoutInflater.from(parent.context).inflate(R.layout.item_contact, parent, false)
20 | )
21 | }
22 |
23 | override fun onBindViewHolder(holder: ContactViewHolder, position: Int) {
24 | val contact = list[position]
25 | holder.itemView.tvAccount.text = contact.account
26 |
27 | if (contact.account == AccountManager.getAccount()) {
28 | holder.itemView.btnVideo.visibility = View.GONE
29 | } else {
30 | holder.itemView.btnVideo.visibility = View.VISIBLE
31 | holder.itemView.btnVideo.setOnClickListener {
32 | onClickVideo?.invoke(contact)
33 | }
34 | }
35 | }
36 |
37 | override fun getItemCount(): Int {
38 | return list.size
39 | }
40 |
41 | }
42 |
43 | class ContactViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/constant/RRType.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.constant
2 |
3 | class RequestType {
4 | companion object {
5 | const val All_CONTACT: String = "All_CONTACT"
6 | const val CREATE: String = "CREATE"
7 | const val INVITE: String = "INVITE"
8 | const val CANCEL: String = "CANCEL"
9 | const val JOIN: String = "JOIN"
10 | const val RING: String = "RING"
11 | const val REJECT: String = "REJECT"
12 | const val SDP_OFFER: String = "SDP_OFFER"
13 | const val SDP_ANSWER: String = "SDP_ANSWER"
14 | const val CANDIDATE: String = "CANDIDATE"
15 | const val REMOVE_CANDIDATES: String = "REMOVE_CANDIDATES"
16 | const val LEAVE: String = "LEAVE"
17 | }
18 | }
19 |
20 | class ResponseType {
21 | companion object {
22 | const val ADD_CONTACT: String = "ADD_CONTACT"
23 | const val ALL_CONTACT: String = "ALL_CONTACT"
24 | const val REMOVE_CONTACT: String = "REMOVE_CONTACT"
25 | const val CREATE_SUCCESS: String = "CREATE_SUCCESS"
26 | const val PEERS: String = "PEERS"
27 | const val INVITE: String = "INVITE"
28 | const val CANCEL: String = "CANCEL"
29 | const val RING: String = "RING"
30 | const val REJECT: String = "REJECT"
31 | const val JOIN_SUCCESS: String = "JOIN_SUCCESS"
32 | const val SDP_OFFER: String = "SDP_OFFER"
33 | const val SDP_ANSWER: String = "SDP_ANSWER"
34 | const val CANDIDATE: String = "CANDIDATE"
35 | const val REMOVE_CANDIDATES: String = "REMOVE_CANDIDATES"
36 | const val LEAVE: String = "LEAVE"
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/manager/SignalManager.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.manager
2 |
3 | import com.alick.learnwebrtc.bean.request.*
4 |
5 | object SignalManager {
6 |
7 | /**
8 | * 主叫方创建房间
9 | */
10 | fun create(createRoomRequest: CreateRoomRequest) {
11 | WebSocketManager.sendMsg(createRoomRequest)
12 | }
13 |
14 | /**
15 | * 主叫方邀请
16 | */
17 | fun invite(inviteRequest: InviteRequest) {
18 | WebSocketManager.sendMsg(inviteRequest)
19 | }
20 |
21 | /**
22 | * 主叫方取消邀请
23 | */
24 | fun cancel(cancelRequest: CancelRequest){
25 | WebSocketManager.sendMsg(cancelRequest)
26 | }
27 |
28 | /**
29 | * 被叫方响铃
30 | */
31 | fun ring(ringRequest: RingRequest){
32 | WebSocketManager.sendMsg(ringRequest)
33 | }
34 |
35 | /**
36 | * 被叫方拒接
37 | */
38 | fun reject(rejectRequest: RejectRequest){
39 | WebSocketManager.sendMsg(rejectRequest)
40 | }
41 |
42 | /**
43 | * 被叫方接听并加入房间
44 | */
45 | fun join(joinRequest: JoinRequest){
46 | WebSocketManager.sendMsg(joinRequest)
47 | }
48 |
49 | fun sdpOffer(sdpOfferRequest: SdpOfferRequest){
50 | WebSocketManager.sendMsg(sdpOfferRequest)
51 | }
52 |
53 | fun sdpAnswer(sdpAnswerRequest: SdpAnswerRequest){
54 | WebSocketManager.sendMsg(sdpAnswerRequest)
55 | }
56 |
57 | fun candidate(iceCandidateRequest: SendIceCandidateRequest){
58 | WebSocketManager.sendMsg(iceCandidateRequest)
59 | }
60 |
61 | fun removeCandidates(sendIceCandidateRemovedRequest: SendIceCandidateRemovedRequest){
62 | WebSocketManager.sendMsg(sendIceCandidateRemovedRequest)
63 | }
64 |
65 | /**
66 | * 主叫或被叫离开房间
67 | */
68 | fun leave(leaveRequest: LeaveRequest){
69 | WebSocketManager.sendMsg(leaveRequest)
70 | }
71 |
72 | }
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-android-extensions'
5 | }
6 |
7 | android {
8 | compileSdkVersion 30
9 | buildToolsVersion "30.0.0"
10 |
11 | defaultConfig {
12 | applicationId "com.example.learnwebrtc"
13 | minSdkVersion 21
14 | targetSdkVersion 30
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | signingConfigs {
22 | config {
23 | keyAlias 'alick'
24 | keyPassword '123123'
25 | storeFile file('../key/alick.jks')
26 | storePassword '123123'
27 | }
28 | }
29 |
30 | buildTypes {
31 | debug {
32 | debuggable true
33 | minifyEnabled false
34 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
35 | signingConfig signingConfigs.config
36 | }
37 | release {
38 | debuggable true
39 | minifyEnabled false
40 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
41 | signingConfig signingConfigs.config
42 | }
43 | }
44 | compileOptions {
45 | sourceCompatibility JavaVersion.VERSION_1_8
46 | targetCompatibility JavaVersion.VERSION_1_8
47 | }
48 | kotlinOptions {
49 | jvmTarget = '1.8'
50 | }
51 | ndkVersion '21.3.6528147'
52 | }
53 |
54 | dependencies {
55 |
56 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
57 | implementation 'androidx.core:core-ktx:1.3.2'
58 | implementation 'androidx.appcompat:appcompat:1.2.0'
59 | implementation 'com.google.android.material:material:1.3.0'
60 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
61 |
62 | implementation 'com.tencent:mmkv-static:1.0.19'
63 | implementation 'org.java-websocket:Java-WebSocket:1.5.1'
64 | implementation 'org.webrtc:google-webrtc:1.0.32006'
65 | implementation 'pub.devrel:easypermissions:3.0.0'
66 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
67 | implementation "com.google.code.gson:gson:2.8.6"
68 |
69 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/widget/ChatTimeTextView.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.widget
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.View
6 | import java.util.*
7 |
8 | /**
9 | * @author 崔兴旺
10 | * @description
11 | * @date 2020/11/21 23:32
12 | */
13 | class ChatTimeTextView(context: Context, attrs: AttributeSet?) : androidx.appcompat.widget.AppCompatTextView(context, attrs) {
14 |
15 |
16 | private var timerTask: TimerTask? = null
17 | private var timer: Timer? = null
18 | private var chatDuration: Long = 0L
19 |
20 | private val interval: Long = 1000L
21 |
22 |
23 | /**
24 | * 开始会话计时
25 | */
26 | fun beginChatTime() {
27 | visibility = View.VISIBLE
28 | if (timer == null) {
29 | timer = Timer()
30 | }
31 | if (timerTask == null) {
32 | timerTask = object : TimerTask() {
33 | override fun run() {
34 | chatDuration += interval
35 |
36 | val durationBySecond: Long = chatDuration / interval
37 |
38 | var str = ""
39 | if (durationBySecond < 10L) {
40 | str = "00:0${durationBySecond}"
41 | } else if (durationBySecond < 60L) {
42 | str = "00:${durationBySecond}"
43 | } else if (durationBySecond == 60L) {
44 | str = "01:00"
45 | } else {
46 | val min = durationBySecond / 60
47 | if (min < 10) {
48 | str = "0${min}:"
49 | } else if (min < 60) {
50 | str = "${min}:"
51 | }
52 | val second = durationBySecond % 60
53 | if (second < 10) {
54 | str += "0${second}"
55 | } else {
56 | str += "$second"
57 | }
58 | }
59 |
60 | post {
61 | text = str
62 | }
63 | }
64 | }
65 | }
66 | timer?.schedule(timerTask, interval, interval)
67 | }
68 |
69 | fun stopChatTime() {
70 | if (timer != null) {
71 | timer?.cancel()
72 | }
73 | }
74 |
75 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
42 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_contacts.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
24 |
25 |
32 |
33 |
45 |
46 |
52 |
53 |
54 |
58 |
59 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/utils/AppHolder.java:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.utils;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.Application;
5 | import android.content.Context;
6 |
7 | import java.lang.reflect.InvocationTargetException;
8 |
9 | /**
10 | * 功能: Application持有者
11 | * 作者: 崔兴旺
12 | * 日期: 2020/3/6 0006
13 | */
14 | public class AppHolder {
15 | private static Application sApplication;
16 |
17 | private AppHolder() {
18 | }
19 |
20 | /**
21 | * 初始化
22 | *
23 | * @param context 任意context Application|Activity|Service|ContextProvider|BroadcastReceiver
24 | * @date 2019-06-20 11:40
25 | * @author wangzhenzhou
26 | */
27 | public static void init(Context context) {
28 | if (null == context) {
29 | init(getApplicationByReflect());
30 | return;
31 | }
32 | sApplication = (Application) context.getApplicationContext();
33 | }
34 |
35 | /**
36 | * 初始化
37 | *
38 | * @param application Application
39 | * @date 2019-06-20 11:40
40 | * @author wangzhenzhou
41 | */
42 | public static void init(final Application application) {
43 | if (sApplication == null) {
44 | if (application == null) {
45 | AppHolder.sApplication = getApplicationByReflect();
46 | } else {
47 | AppHolder.sApplication = application;
48 | }
49 | }
50 | }
51 |
52 | /**
53 | * 获取持有的Application
54 | *
55 | * @return android.app.Application
56 | * @date 2019-06-20 11:43
57 | * @author wangzhenzhou
58 | */
59 | public static Application getApp() {
60 | if (sApplication != null) {
61 | return sApplication;
62 | }
63 | Application app = getApplicationByReflect();
64 | init(app);
65 | return app;
66 | }
67 |
68 | /**
69 | * 反射获取Application实例
70 | *
71 | * @return android.app.Application
72 | * @date 2019-06-20 11:43
73 | * @author wangzhenzhou
74 | */
75 | private static Application getApplicationByReflect() {
76 | try {
77 | @SuppressLint("PrivateApi")
78 | Class> activityThread = Class.forName("android.app.ActivityThread");
79 | Object thread = activityThread.getMethod("currentActivityThread").invoke(null);
80 | Object app = activityThread.getMethod("getApplication").invoke(thread);
81 | if (app == null) {
82 | throw new NullPointerException("u should init first");
83 | }
84 | return (Application) app;
85 | } catch (NoSuchMethodException e) {
86 | e.printStackTrace();
87 | } catch (IllegalAccessException e) {
88 | e.printStackTrace();
89 | } catch (InvocationTargetException e) {
90 | e.printStackTrace();
91 | } catch (ClassNotFoundException e) {
92 | e.printStackTrace();
93 | }
94 | throw new NullPointerException("u should init first");
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/http_api/ApiUtils.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.http_api
2 |
3 | import com.alick.learnwebrtc.constant.Constant
4 | import com.alick.learnwebrtc.utils.BLog
5 | import com.alick.learnwebrtc.utils.JsonUtils
6 | import com.alick.learnwebrtc.utils.MMKVUtils
7 | import java.io.BufferedReader
8 | import java.io.InputStreamReader
9 | import java.net.HttpURLConnection
10 | import java.net.URL
11 | import java.util.concurrent.ExecutorService
12 | import java.util.concurrent.Executors
13 |
14 | /**
15 | * @author 崔兴旺
16 | * @description
17 | * @date 2021/6/27 12:43
18 | */
19 | class ApiUtils {
20 |
21 | companion object {
22 | val executor: ExecutorService = Executors.newSingleThreadExecutor()
23 | const val API_GET_ROOM_LIST = "/api/get-room-list"
24 |
25 |
26 | inline fun requestGet(
27 | url_path: String,
28 | crossinline onSuccess: ((Data?) -> Unit),
29 | crossinline onFail: ((String) -> Unit)
30 | ) {
31 | executor.execute {
32 | var bufferedReader: BufferedReader? = null
33 | var httpURLConnection: HttpURLConnection? = null
34 | try {//使用该地址创建一个 URL 对象
35 | val url =
36 | URL("http://" + MMKVUtils.getString(Constant.MMKV_KEY_USING_ADDRESS) + url_path)
37 | //使用创建的URL对象的openConnection()方法创建一个HttpURLConnection对象
38 | httpURLConnection = url.openConnection() as HttpURLConnection
39 | //设置HttpURLConnection对象的参数
40 | //设置请求方法为 GET 请求
41 | httpURLConnection.requestMethod = "GET"
42 | //使用输入流
43 | httpURLConnection.doInput = true
44 | //GET 方式,不需要使用输出流
45 | httpURLConnection.doOutput = false
46 | //设置超时
47 | httpURLConnection.connectTimeout = 10000
48 | httpURLConnection.readTimeout = 1000
49 | //连接
50 | httpURLConnection.connect()
51 | //还有很多参数设置 请自行查阅
52 | //连接后,创建一个输入流来读取response
53 | bufferedReader =
54 | BufferedReader(
55 | InputStreamReader(
56 | httpURLConnection.getInputStream(),
57 | "utf-8"
58 | )
59 | )
60 | var line: String?
61 | val stringBuilder = StringBuilder()
62 | //每次读取一行,若非空则添加至 stringBuilder
63 | while (bufferedReader.readLine().also { line = it } != null) {
64 | stringBuilder.append(line)
65 | }
66 | //读取所有的数据后,赋值给 response
67 | val responseString: String = stringBuilder.toString().trim { it <= ' ' }
68 | BLog.i("响应json:$responseString")
69 | onSuccess(JsonUtils.parseJson2Bean(responseString, BaseApiResponse().javaClass).data)
70 | //切换到ui线程更新ui
71 | } catch (e: Exception) {
72 | e.printStackTrace()
73 | onFail(e.message ?: "未知错误")
74 | } finally {
75 | bufferedReader?.close()
76 | httpURLConnection?.disconnect()
77 | }
78 | }
79 |
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
20 |
21 |
25 |
26 |
30 |
31 |
37 |
38 |
39 |
40 |
44 |
45 |
49 |
50 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
70 |
71 |
76 |
77 |
78 |
83 |
84 |
85 |
86 |
87 |
91 |
92 |
96 |
97 |
103 |
104 |
105 |
111 |
112 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/core/WebRTCEngine.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.core
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import com.alick.learnwebrtc.utils.BLog
6 | import org.webrtc.*
7 | import org.webrtc.audio.AudioDeviceModule
8 | import org.webrtc.audio.JavaAudioDeviceModule.*
9 |
10 | class WebRTCEngine(private val context: Context) {
11 |
12 | private val hw_codec = true //视频编解码工具,默认开启
13 |
14 | private val eglBase: EglBase by lazy {
15 | EglBase.create()
16 | }
17 |
18 | private val factory: PeerConnectionFactory by lazy {
19 |
20 | val options = PeerConnectionFactory.Options()
21 |
22 |
23 | val adm: AudioDeviceModule = createJavaAudioDevice()
24 |
25 | val encoderFactory: VideoEncoderFactory
26 | val decoderFactory: VideoDecoderFactory
27 |
28 | if (hw_codec) {
29 | encoderFactory = DefaultVideoEncoderFactory(
30 | eglBase.eglBaseContext, true,
31 | false
32 | )
33 | decoderFactory = DefaultVideoDecoderFactory(eglBase.getEglBaseContext())
34 | } else {
35 | encoderFactory = SoftwareVideoEncoderFactory()
36 | decoderFactory = SoftwareVideoDecoderFactory()
37 | }
38 |
39 | val peerConnectionFactory = PeerConnectionFactory.builder()
40 | .setOptions(options)
41 | .setAudioDeviceModule(adm)
42 | .setVideoEncoderFactory(encoderFactory)
43 | .setVideoDecoderFactory(decoderFactory)
44 | .createPeerConnectionFactory()
45 |
46 | BLog.i("peerConnectionFactory创建成功")
47 | adm.release()
48 | peerConnectionFactory
49 | }
50 |
51 |
52 | init {
53 | PeerConnectionFactory.initialize(
54 | PeerConnectionFactory.InitializationOptions.builder(context)
55 | .setEnableInternalTracer(true)
56 | .createInitializationOptions()
57 | )
58 | }
59 |
60 |
61 | private fun createJavaAudioDevice(): AudioDeviceModule {
62 | // Set audio record error callbacks.
63 | val audioRecordErrorCallback: AudioRecordErrorCallback = object : AudioRecordErrorCallback {
64 | override fun onWebRtcAudioRecordInitError(errorMessage: String) {
65 | BLog.e("onWebRtcAudioRecordInitError: $errorMessage")
66 | }
67 |
68 | override fun onWebRtcAudioRecordStartError(
69 | errorCode: AudioRecordStartErrorCode, errorMessage: String
70 | ) {
71 | BLog.e("onWebRtcAudioRecordStartError: $errorCode. $errorMessage")
72 | }
73 |
74 | override fun onWebRtcAudioRecordError(errorMessage: String) {
75 | BLog.e("onWebRtcAudioRecordError: $errorMessage")
76 | }
77 | }
78 | val audioTrackErrorCallback: AudioTrackErrorCallback = object : AudioTrackErrorCallback {
79 | override fun onWebRtcAudioTrackInitError(errorMessage: String) {
80 | BLog.e("onWebRtcAudioTrackInitError: $errorMessage")
81 | }
82 |
83 | override fun onWebRtcAudioTrackStartError(
84 | errorCode: AudioTrackStartErrorCode, errorMessage: String
85 | ) {
86 | BLog.e("onWebRtcAudioTrackStartError: $errorCode. $errorMessage")
87 | }
88 |
89 | override fun onWebRtcAudioTrackError(errorMessage: String) {
90 | BLog.e("onWebRtcAudioTrackError: $errorMessage")
91 | }
92 | }
93 |
94 | // Set audio record state callbacks.
95 | val audioRecordStateCallback: AudioRecordStateCallback = object : AudioRecordStateCallback {
96 | override fun onWebRtcAudioRecordStart() {
97 | BLog.i("Audio recording starts")
98 | }
99 |
100 | override fun onWebRtcAudioRecordStop() {
101 | BLog.i("Audio recording stops")
102 | }
103 | }
104 |
105 | // Set audio track state callbacks.
106 | val audioTrackStateCallback: AudioTrackStateCallback = object : AudioTrackStateCallback {
107 | override fun onWebRtcAudioTrackStart() {
108 | BLog.i("Audio playout starts")
109 | }
110 |
111 | override fun onWebRtcAudioTrackStop() {
112 | BLog.i("Audio playout stops")
113 | }
114 | }
115 | return builder(context)
116 | // .setSamplesReadyCallback(saveRecordedAudioToFile) //音频保存到文件(暂不实现,详见官方demo)
117 | .setUseHardwareAcousticEchoCanceler(true) //声学回声消除器
118 | .setUseHardwareNoiseSuppressor(true) //噪声抑制
119 | .setAudioRecordErrorCallback(audioRecordErrorCallback)
120 | .setAudioTrackErrorCallback(audioTrackErrorCallback)
121 | .setAudioRecordStateCallback(audioRecordStateCallback)
122 | .setAudioTrackStateCallback(audioTrackStateCallback)
123 | .createAudioDeviceModule()
124 | }
125 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/ContactsActivity.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc
2 |
3 | import android.content.DialogInterface
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.view.View
7 | import androidx.appcompat.app.AlertDialog
8 | import androidx.appcompat.app.AppCompatActivity
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import com.alick.learnwebrtc.adapter.ContactsAdapter
11 | import com.alick.learnwebrtc.bean.Contact
12 | import com.alick.learnwebrtc.bean.RoomBean
13 | import com.alick.learnwebrtc.bean.request.GetAllContactRequest
14 | import com.alick.learnwebrtc.manager.WebSocketManager
15 | import com.alick.learnwebrtc.http_api.ApiUtils
16 | import com.alick.learnwebrtc.http_api.GetAllRoomListResponse
17 | import com.alick.learnwebrtc.utils.FilterUtils
18 | import com.alick.learnwebrtc.utils.ToastUtils
19 | import kotlinx.android.synthetic.main.activity_contacts.*
20 | import org.java_websocket.handshake.ServerHandshake
21 |
22 |
23 | class ContactsActivity : AppCompatActivity() {
24 |
25 | private val allContactsList: MutableList by lazy {
26 | mutableListOf()
27 | }
28 |
29 | private val contactsAdapter: ContactsAdapter by lazy {
30 | ContactsAdapter(allContactsList)
31 | }
32 |
33 | private val webSocketListener = object : WebSocketManager.IWebSocketListener {
34 | override fun onOpen(serverHandshake: ServerHandshake) {
35 | }
36 |
37 | override fun onClose(code: Int, reason: String, remote: Boolean) {
38 | finish()
39 | }
40 |
41 | override fun onMessage(message: String) {
42 | }
43 |
44 | override fun onError(exception: Exception) {
45 | swipeRefreshLayout.isRefreshing = false
46 | }
47 | }
48 |
49 | private val iContactsListener by lazy {
50 | object : WebSocketManager.IContactsListener {
51 | override fun onAdd(contact: Contact) {
52 | allContactsList.add(contact)
53 | runOnUiThread {
54 | contactsAdapter.notifyDataSetChanged()
55 | }
56 | }
57 |
58 | override fun onRemove(contact: Contact) {
59 | FilterUtils.filterList(
60 | allContactsList
61 | ) { model -> model.account == contact.account }
62 | runOnUiThread {
63 | contactsAdapter.notifyDataSetChanged()
64 | }
65 | }
66 |
67 | override fun onGetAll(allContactList: MutableList) {
68 | allContactsList.clear()
69 | allContactsList.addAll(allContactList)
70 | runOnUiThread {
71 | contactsAdapter.notifyDataSetChanged()
72 | swipeRefreshLayout.isRefreshing = false
73 | }
74 | }
75 | }
76 | }
77 |
78 | override fun onCreate(savedInstanceState: Bundle?) {
79 | super.onCreate(savedInstanceState)
80 | setContentView(R.layout.activity_contacts)
81 |
82 | rvContacts.layoutManager = LinearLayoutManager(this)
83 | rvContacts.adapter = contactsAdapter
84 |
85 | contactsAdapter.onClickVideo = {
86 | val intent = Intent(this, VideoCallSendActivity::class.java)
87 | intent.putExtra(BaseVideoCallActivity.INTENT_KEY_TO_ACCOUNT, it.account)
88 | startActivity(intent)
89 | }
90 |
91 |
92 | WebSocketManager.addWebSocketListener(webSocketListener)
93 | WebSocketManager.addIContactsListener(iContactsListener)
94 |
95 | swipeRefreshLayout.setOnRefreshListener {
96 | loadContacts()
97 | }
98 |
99 | swipeRefreshLayout.isRefreshing = true
100 | loadContacts()
101 |
102 | }
103 |
104 | private fun loadContacts() {
105 | WebSocketManager.sendMsg(GetAllContactRequest())
106 | }
107 |
108 | fun back(view: View) {
109 | onBack()
110 | }
111 |
112 | override fun onBackPressed() {
113 | onBack()
114 | }
115 |
116 | private fun onBack() {
117 | val alertDialog: AlertDialog = AlertDialog.Builder(this)
118 | .setTitle("提示")
119 | .setMessage("确定要退出吗")
120 | .setNegativeButton("取消", null)
121 | .setPositiveButton("退出", object : DialogInterface.OnClickListener {
122 | override fun onClick(p0: DialogInterface?, p1: Int) {
123 | WebSocketManager.close()
124 | finish()
125 | }
126 | })
127 | .create()
128 | alertDialog.show()
129 | }
130 |
131 | override fun onDestroy() {
132 | super.onDestroy()
133 | WebSocketManager.removeIContactsListener(iContactsListener)
134 | WebSocketManager.removeWebSocketListener(webSocketListener)
135 | }
136 |
137 | fun roomList(view: View) {
138 | ApiUtils.requestGet>(ApiUtils.API_GET_ROOM_LIST,{
139 | ToastUtils.show(it?.toString() ?:"无数据")
140 | },{
141 | ToastUtils.show(it)
142 | })
143 |
144 | }
145 |
146 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_video_call_send.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
14 |
15 |
19 |
20 |
21 |
22 |
30 |
31 |
40 |
41 |
46 |
47 |
52 |
53 |
60 |
61 |
68 |
69 |
70 |
71 |
72 |
73 |
83 |
84 |
88 |
89 |
90 |
91 |
100 |
101 |
110 |
111 |
121 |
122 |
133 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_video_call_receive.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
18 |
19 |
20 |
24 |
25 |
32 |
33 |
42 |
43 |
47 |
48 |
49 |
50 |
58 |
59 |
64 |
65 |
73 |
74 |
81 |
82 |
83 |
84 |
85 |
94 |
95 |
106 |
107 |
112 |
113 |
122 |
123 |
134 |
135 |
136 |
137 |
148 |
149 |
150 |
151 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc
2 |
3 | import android.Manifest
4 | import android.content.Intent
5 | import android.os.Build
6 | import android.os.Bundle
7 | import android.view.View
8 | import androidx.appcompat.app.AppCompatActivity
9 | import com.alick.learnwebrtc.constant.Constant
10 | import com.alick.learnwebrtc.manager.WebSocketManager
11 | import com.alick.learnwebrtc.utils.MMKVUtils
12 | import com.alick.learnwebrtc.utils.ToastUtils
13 | import kotlinx.android.synthetic.main.activity_main.*
14 | import org.java_websocket.handshake.ServerHandshake
15 | import pub.devrel.easypermissions.AfterPermissionGranted
16 | import pub.devrel.easypermissions.EasyPermissions
17 |
18 |
19 | class MainActivity : AppCompatActivity() {
20 | companion object {
21 | private const val REQUEST_CODE: Int = 1000
22 | }
23 |
24 | private val webSocketListener by lazy {
25 | object : WebSocketManager.IWebSocketListener {
26 | override fun onOpen(serverHandshake: ServerHandshake) {
27 | startActivity(Intent(this@MainActivity, ContactsActivity::class.java))
28 | }
29 |
30 | override fun onClose(code: Int, reason: String, remote: Boolean) {
31 |
32 | }
33 |
34 | override fun onMessage(message: String) {
35 |
36 | }
37 |
38 | override fun onError(exception: Exception) {
39 |
40 | }
41 | }
42 | }
43 |
44 | override fun onCreate(savedInstanceState: Bundle?) {
45 | super.onCreate(savedInstanceState)
46 | setContentView(R.layout.activity_main)
47 |
48 | val outerAddress: String = MMKVUtils.getString(Constant.MMKV_KEY_OUTER_ADDRESS).let {
49 | if (it.isNotBlank()) {
50 | it
51 | } else {
52 | Constant.DEFAULT_OUTER_ADDRESS
53 | }
54 | }
55 | val innerAddress = MMKVUtils.getString(Constant.MMKV_KEY_INNER_ADDRESS).let {
56 | if (it.isNotBlank()) {
57 | it
58 | } else {
59 | Constant.DEFAULT_INNER_ADDRESS
60 | }
61 | }
62 |
63 | etOuterAddress.setText(outerAddress)
64 | etInnerAddress.setText(innerAddress)
65 | etAccount.setText(MMKVUtils.getString(Constant.MMKV_KEY_ACCOUNT))
66 |
67 | when (MMKVUtils.getString(Constant.MMKV_KEY_SELECT_ADDRESS_TYPE)) {
68 | Constant.MMKV_VALUE_SELECT_ADDRESS_TYPE_OUTER -> {
69 | rgAddress.check(R.id.rbOuterAddress)
70 | }
71 | Constant.MMKV_VALUE_SELECT_ADDRESS_TYPE_INNER -> {
72 | rgAddress.check(R.id.rbInnerAddress)
73 | }
74 | }
75 |
76 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
77 | requestPermissions(
78 | arrayOf(
79 | Manifest.permission.CAMERA,
80 | Manifest.permission.RECORD_AUDIO,
81 | Manifest.permission.WRITE_EXTERNAL_STORAGE
82 | ), REQUEST_CODE
83 | )
84 | }
85 |
86 | }
87 |
88 | fun login(view: View) {
89 | val outerAddress = etOuterAddress.text.toString().trim()
90 | val innerAddress = etInnerAddress.text.toString().trim()
91 |
92 | MMKVUtils.setString(Constant.MMKV_KEY_OUTER_ADDRESS, outerAddress)
93 | MMKVUtils.setString(Constant.MMKV_KEY_INNER_ADDRESS, innerAddress)
94 |
95 | val usingAddress = when (rgAddress.checkedRadioButtonId) {
96 | R.id.rbOuterAddress -> {
97 | MMKVUtils.setString(
98 | Constant.MMKV_KEY_SELECT_ADDRESS_TYPE,
99 | Constant.MMKV_VALUE_SELECT_ADDRESS_TYPE_OUTER
100 | )
101 | if (outerAddress.isBlank()) {
102 | ToastUtils.show("请填写外网地址")
103 | return
104 | }
105 | outerAddress
106 | }
107 | R.id.rbInnerAddress -> {
108 | MMKVUtils.setString(
109 | Constant.MMKV_KEY_SELECT_ADDRESS_TYPE,
110 | Constant.MMKV_VALUE_SELECT_ADDRESS_TYPE_INNER
111 | )
112 | if (innerAddress.isBlank()) {
113 | ToastUtils.show("请填写内网地址")
114 | return
115 | }
116 | innerAddress
117 | }
118 | else -> {
119 | ToastUtils.show("请先选择外网或内网")
120 | return
121 | }
122 | }
123 |
124 | MMKVUtils.setString(Constant.MMKV_KEY_USING_ADDRESS, usingAddress)
125 |
126 |
127 | val account = etAccount.text.toString().trim()
128 | MMKVUtils.setString(Constant.MMKV_KEY_ACCOUNT, account)
129 |
130 | WebSocketManager.init(usingAddress, account)
131 | WebSocketManager.addWebSocketListener(webSocketListener)
132 | WebSocketManager.connect()
133 | }
134 |
135 |
136 | override fun onRequestPermissionsResult(
137 | requestCode: Int,
138 | permissions: Array,
139 | grantResults: IntArray
140 | ) {
141 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
142 |
143 | // Forward results to EasyPermissions
144 | EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
145 | }
146 |
147 | @AfterPermissionGranted(REQUEST_CODE)
148 | private fun methodRequiresTwoPermission() {
149 | val perms = arrayOf(
150 | Manifest.permission.CAMERA,
151 | Manifest.permission.RECORD_AUDIO,
152 | Manifest.permission.WRITE_EXTERNAL_STORAGE,
153 |
154 | )
155 | if (EasyPermissions.hasPermissions(this, *perms)) {
156 | // Already have permission, do the thing
157 | // ...
158 | } else {
159 | // Do not have permissions, request them now
160 | EasyPermissions.requestPermissions(this, "音视频通话需要获取摄像头和录音权限", REQUEST_CODE, *perms)
161 | }
162 | }
163 |
164 | override fun onDestroy() {
165 | WebSocketManager.clearAllWebSocketListener()
166 | super.onDestroy()
167 | }
168 |
169 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/VideoCallReceiveActivity.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import com.alick.learnwebrtc.bean.Contact
6 | import com.alick.learnwebrtc.bean.SdpBean
7 | import com.alick.learnwebrtc.bean.request.*
8 | import com.alick.learnwebrtc.bean.response.SdpOfferResponse
9 | import com.alick.learnwebrtc.core.sdp.CreateSdpObserver
10 | import com.alick.learnwebrtc.core.sdp.SetSdpObserver
11 | import com.alick.learnwebrtc.manager.AccountManager
12 | import com.alick.learnwebrtc.manager.SignalManager
13 | import com.alick.learnwebrtc.manager.WebSocketManager
14 | import com.alick.learnwebrtc.utils.BLog
15 | import com.alick.learnwebrtc.utils.ToastUtils
16 | import kotlinx.android.synthetic.main.activity_video_call_receive.*
17 | import org.webrtc.IceCandidate
18 | import org.webrtc.SessionDescription
19 | import org.webrtc.SurfaceViewRenderer
20 |
21 | class VideoCallReceiveActivity : BaseVideoCallActivity() {
22 | companion object {
23 | const val TAG = "VideoCallReceiveActivity"
24 | }
25 |
26 | private val iSignalIncomingListener = object : WebSocketManager.ISignalIncomingListener {
27 | override fun onCancel() {
28 | ToastUtils.show("对方已取消")
29 | finish()
30 | }
31 |
32 | override fun onSdpOffer(sdpOfferResponse: SdpOfferResponse) {
33 | setRemoteSdp(SessionDescription(SessionDescription.Type.OFFER,sdpOfferResponse.data?.description))
34 | createAnswer()
35 | }
36 |
37 | override fun onJoinSuccess() {
38 | ToastUtils.show("接听成功")
39 | runOnUiThread {
40 | tvDuration.beginChatTime()
41 | llIncomingInfo.visibility = View.GONE
42 | tvReject.visibility = View.GONE
43 | tvAnswer.visibility = View.GONE
44 | ivCoverAvatar.visibility = View.GONE
45 | rlLocalVideoContainer.visibility = View.VISIBLE
46 | ivSwitchCamera.visibility = View.VISIBLE
47 | tvHangup.visibility = View.VISIBLE
48 | swapRenderer()
49 | }
50 | }
51 |
52 | override fun onIceCandidateFromRemote(candidate: IceCandidate) {
53 | addIceCandidateFromRemote(candidate)
54 | }
55 |
56 | override fun onIceCandidatesRemovedFromRemote(candidates: MutableList) {
57 |
58 | }
59 |
60 | override fun onLeave() {
61 | ToastUtils.show(" 对方已挂断")
62 | finish()
63 | }
64 | }
65 |
66 | private val iContactsListener = object : WebSocketManager.IContactsListener{
67 | override fun onAdd(contact: Contact) {
68 | //不用处理
69 | }
70 |
71 | override fun onRemove(contact: Contact) {
72 | if(contact.account==fromAccount){
73 | ToastUtils.show("对方异常退出")
74 | leave()
75 | }
76 | }
77 |
78 | override fun onGetAll(allContactList: MutableList) {
79 | //不用处理
80 | }
81 |
82 | }
83 |
84 | override val fromAccount: String
85 | get() = intent.getStringExtra(INTENT_KEY_FROM_ACCOUNT) ?: ""
86 |
87 | override val toAccount: String
88 | get() = AccountManager.getAccount()
89 |
90 | override val mPipRenderer: SurfaceViewRenderer
91 | get() = pipRenderer
92 |
93 | override val mFullscreenRenderer: SurfaceViewRenderer
94 | get() = fullscreenRRenderer
95 |
96 | override val isSender: Boolean
97 | get() = false
98 |
99 | override fun onCreate(savedInstanceState: Bundle?) {
100 | super.onCreate(savedInstanceState)
101 | setContentView(R.layout.activity_video_call_receive)
102 |
103 | roomId = intent.getStringExtra(INTENT_KEY_ROOM_ID) ?: ""
104 |
105 | tvIncomingNickname.text = fromAccount
106 |
107 | WebSocketManager.addISignalIncomingListener(iSignalIncomingListener)
108 | WebSocketManager.addIContactsListener(iContactsListener)
109 |
110 | SignalManager.ring(RingRequest(roomId, fromAccount))
111 |
112 | //被叫方拒接
113 | tvReject.setOnClickListener {
114 | SignalManager.reject(RejectRequest(roomId, fromAccount))
115 | finish()
116 | }
117 |
118 | //被叫方接听
119 | tvAnswer.setOnClickListener {
120 | SignalManager.join(JoinRequest(roomId, fromAccount))
121 | }
122 |
123 | //被叫方挂断
124 | tvHangup.setOnClickListener {
125 | leave()
126 | }
127 |
128 | //切换摄像头
129 | ivSwitchCamera.setOnClickListener {
130 | switchCamera()
131 | }
132 |
133 | init()
134 | }
135 |
136 | private fun leave() {
137 | SignalManager.leave(LeaveRequest(roomId, fromAccount))
138 | finish()
139 | }
140 |
141 | override fun onDestroy() {
142 | super.onDestroy()
143 | WebSocketManager.removeISignalIncomingListener(iSignalIncomingListener)
144 | WebSocketManager.removeIContactsListener(iContactsListener)
145 | tvDuration.stopChatTime()
146 | }
147 |
148 | fun createAnswer(){
149 | peerConnection.createAnswer(object : CreateSdpObserver() {
150 | override fun onCreateSuccess(sessionDescription: SessionDescription) {
151 | BLog.i("--->createAnswer()--->onCreateSuccess(),description:${sessionDescription.description},type:${sessionDescription.type}",TAG)
152 | setLocalSdp(sessionDescription)
153 | }
154 |
155 | override fun onCreateFailure(errorMsg: String) {
156 | BLog.e("--->createAnswer()--->onCreateFailure(),errorMsg:${errorMsg}", TAG)
157 | }
158 |
159 | },sdpMediaConstraints)
160 | }
161 |
162 | fun setRemoteSdp(sessionDescription: SessionDescription) {
163 | peerConnection.setRemoteDescription(object : SetSdpObserver() {
164 | override fun onSetSuccess() {
165 | BLog.i("--->setRemoteSdp()--->onSetSuccess()", TAG)
166 | }
167 |
168 | override fun onSetFailure(errorMsg: String) {
169 | BLog.e("--->setRemoteSdp()--->onSetFailure(),${errorMsg}", TAG)
170 | }
171 |
172 | }, sessionDescription)
173 | }
174 |
175 | fun setLocalSdp(sessionDescription: SessionDescription) {
176 | peerConnection.setLocalDescription(object : SetSdpObserver() {
177 | override fun onSetSuccess() {
178 | BLog.i("--->setLocalSdp()--->onSetSuccess()", TAG)
179 | SignalManager.sdpAnswer(SdpAnswerRequest(roomId,fromAccount, SdpBean(peerConnection.localDescription.description)))
180 | drainCandidates()
181 | }
182 |
183 | override fun onSetFailure(errorMsg: String) {
184 | BLog.e("--->setLocalSdp()--->onSetFailure(),${errorMsg}", TAG)
185 | }
186 |
187 | }, sessionDescription)
188 | }
189 |
190 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/VideoCallSendActivity.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import com.alick.learnwebrtc.bean.Contact
6 | import com.alick.learnwebrtc.bean.RoomBean
7 | import com.alick.learnwebrtc.bean.SdpBean
8 | import com.alick.learnwebrtc.bean.request.*
9 | import com.alick.learnwebrtc.bean.response.SdpAnswerResponse
10 | import com.alick.learnwebrtc.core.sdp.CreateSdpObserver
11 | import com.alick.learnwebrtc.core.sdp.SetSdpObserver
12 | import com.alick.learnwebrtc.manager.AccountManager
13 | import com.alick.learnwebrtc.manager.SignalManager
14 | import com.alick.learnwebrtc.manager.WebSocketManager
15 | import com.alick.learnwebrtc.utils.BLog
16 | import com.alick.learnwebrtc.utils.ToastUtils
17 | import kotlinx.android.synthetic.main.activity_video_call_send.*
18 | import org.webrtc.IceCandidate
19 | import org.webrtc.SessionDescription
20 | import org.webrtc.SurfaceViewRenderer
21 |
22 | class VideoCallSendActivity : BaseVideoCallActivity() {
23 | companion object {
24 | const val TAG = "VideoCallSendActivity"
25 | }
26 |
27 | override val fromAccount: String
28 | get() = AccountManager.getAccount()
29 |
30 | override val toAccount: String
31 | get() = intent.getStringExtra(INTENT_KEY_TO_ACCOUNT) ?: ""
32 |
33 | override val mPipRenderer: SurfaceViewRenderer
34 | get() = pipRenderer
35 |
36 | override val mFullscreenRenderer: SurfaceViewRenderer
37 | get() = fullscreenRRenderer
38 |
39 | override val isSender: Boolean
40 | get()= true
41 |
42 | private val iSignalOutgoingListener = object : WebSocketManager.ISignalOutgoingListener {
43 | override fun onCreateSuccess(roomBean: RoomBean) {
44 | roomId = roomBean.roomId
45 | SignalManager.invite(InviteRequest(roomBean.roomId, toAccount))
46 | }
47 |
48 | override fun onRing() {
49 | ToastUtils.show("对方已响铃")
50 | }
51 |
52 | override fun onReject() {
53 | ToastUtils.show("对方拒接")
54 | finish()
55 | }
56 |
57 | override fun onSdpAnswer(sdpAnswerResponse: SdpAnswerResponse) {
58 | setRemoteSdp(
59 | SessionDescription(
60 | SessionDescription.Type.ANSWER,
61 | sdpAnswerResponse.data?.description
62 | )
63 | )
64 | }
65 |
66 | override fun onJoinSuccess() {
67 | ToastUtils.show("对方已接听")
68 | runOnUiThread {
69 | tvCancel.visibility = View.GONE
70 | llPeerInfo.visibility = View.GONE
71 | ivSwitchCamera.visibility = View.VISIBLE
72 | tvHangup.visibility = View.VISIBLE
73 | rlLocalVideoContainer.visibility = View.VISIBLE
74 | isAnswered = true
75 | tvDuration.beginChatTime()
76 | swapRenderer()
77 | createOffer()
78 | }
79 | }
80 |
81 | override fun onIceCandidateFromRemote(candidate: IceCandidate) {
82 | addIceCandidateFromRemote(candidate)
83 | }
84 |
85 | override fun onIceCandidatesRemovedFromRemote(candidates: MutableList) {
86 |
87 | }
88 |
89 | override fun onLeave() {
90 | ToastUtils.show(" 对方已挂断")
91 | finish()
92 | }
93 | }
94 |
95 | private val iContactsListener = object : WebSocketManager.IContactsListener{
96 | override fun onAdd(contact: Contact) {
97 | //不用处理
98 | }
99 |
100 | override fun onRemove(contact: Contact) {
101 | if(contact.account==toAccount){
102 | ToastUtils.show("对方异常退出")
103 | leave()
104 | }
105 | }
106 |
107 | override fun onGetAll(allContactList: MutableList) {
108 | //不用处理
109 | }
110 |
111 | }
112 |
113 |
114 | override fun onCreate(savedInstanceState: Bundle?) {
115 | super.onCreate(savedInstanceState)
116 | setContentView(R.layout.activity_video_call_send)
117 |
118 | tvNickname.text = toAccount
119 |
120 | //主叫方点击取消按钮
121 | tvCancel.setOnClickListener {
122 | SignalManager.cancel(CancelRequest(roomId, toAccount))
123 | finish()
124 | }
125 |
126 | //主叫方挂断
127 | tvHangup.setOnClickListener {
128 | leave()
129 | }
130 |
131 | //切换摄像头
132 | ivSwitchCamera.setOnClickListener {
133 | switchCamera()
134 | }
135 |
136 | init()
137 |
138 | WebSocketManager.addISignalOutgoingListener(iSignalOutgoingListener)
139 | WebSocketManager.addIContactsListener(iContactsListener)
140 | SignalManager.create(CreateRoomRequest())
141 |
142 |
143 | }
144 |
145 | private fun leave() {
146 | SignalManager.leave(LeaveRequest(roomId, toAccount))
147 | finish()
148 | }
149 |
150 | override fun onDestroy() {
151 | super.onDestroy()
152 | WebSocketManager.removeISignalOutgoingListener(iSignalOutgoingListener)
153 | WebSocketManager.removeIContactsListener(iContactsListener)
154 | tvDuration.stopChatTime()
155 | }
156 |
157 | private fun createOffer() {
158 | peerConnection.createOffer(object : CreateSdpObserver() {
159 | override fun onCreateSuccess(sessionDescription: SessionDescription) {
160 | BLog.i(
161 | "--->createOffer()--->onCreateSuccess(),description:${sessionDescription.description},type:${sessionDescription.type}",
162 | TAG
163 | )
164 | setLocalSdp(sessionDescription)
165 | }
166 |
167 | override fun onCreateFailure(errorMsg: String) {
168 | BLog.e("--->createOffer()--->onCreateFailure(),errorMsg:${errorMsg}", TAG)
169 | }
170 |
171 | }, sdpMediaConstraints)
172 | }
173 |
174 | fun setRemoteSdp(sessionDescription: SessionDescription) {
175 | peerConnection.setRemoteDescription(object : SetSdpObserver() {
176 | override fun onSetSuccess() {
177 | BLog.i("--->setRemoteSdp()--->onSetSuccess()", TAG)
178 | drainCandidates()
179 | }
180 |
181 | override fun onSetFailure(errorMsg: String) {
182 | BLog.e("--->setRemoteSdp()--->onSetFailure(),${errorMsg}", TAG)
183 | }
184 |
185 | }, sessionDescription)
186 | }
187 |
188 | fun setLocalSdp(sessionDescription: SessionDescription) {
189 | peerConnection.setLocalDescription(object : SetSdpObserver() {
190 | override fun onSetSuccess() {
191 | BLog.i("--->setLocalSdp()--->onSetSuccess()", TAG)
192 | SignalManager.sdpOffer(
193 | SdpOfferRequest(
194 | roomId,
195 | toAccount,
196 | SdpBean(peerConnection.localDescription.description)
197 | )
198 | )
199 | }
200 |
201 | override fun onSetFailure(errorMsg: String) {
202 | BLog.e("--->setLocalSdp()--->onSetFailure(),${errorMsg}", TAG)
203 | }
204 | }, sessionDescription)
205 | }
206 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/manager/WebSocketManager.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc.manager
2 |
3 | import android.content.Intent
4 | import com.alick.learnwebrtc.BaseVideoCallActivity
5 | import com.alick.learnwebrtc.VideoCallReceiveActivity
6 | import com.alick.learnwebrtc.bean.Contact
7 | import com.alick.learnwebrtc.bean.RoomBean
8 | import com.alick.learnwebrtc.bean.request.base.BaseRequest
9 | import com.alick.learnwebrtc.bean.response.*
10 | import com.alick.learnwebrtc.bean.response.base.BaseResponse
11 | import com.alick.learnwebrtc.constant.ResponseType
12 | import com.alick.learnwebrtc.utils.AppHolder
13 | import com.alick.learnwebrtc.utils.BLog
14 | import com.alick.learnwebrtc.utils.JsonUtils
15 | import com.alick.learnwebrtc.utils.ToastUtils
16 | import org.java_websocket.client.WebSocketClient
17 | import org.java_websocket.handshake.ServerHandshake
18 | import org.webrtc.IceCandidate
19 | import java.net.URI
20 |
21 | object WebSocketManager {
22 | const val TAG = "WebSocketManager"
23 |
24 |
25 | interface IWebSocketListener {
26 | fun onOpen(serverHandshake: ServerHandshake)
27 | fun onClose(code: Int, reason: String, remote: Boolean)
28 | fun onMessage(message: String)
29 | fun onError(exception: Exception)
30 | }
31 |
32 | interface IContactsListener {
33 | fun onAdd(contact: Contact)
34 | fun onRemove(contact: Contact)
35 | fun onGetAll(allContactList: MutableList)
36 | }
37 |
38 | interface IBaseSignalListener {
39 | /**
40 | * 被叫方加入房间成功
41 | */
42 | fun onJoinSuccess()
43 |
44 | fun onIceCandidateFromRemote(candidate: IceCandidate)
45 |
46 | fun onIceCandidatesRemovedFromRemote(candidates: MutableList)
47 |
48 | /**
49 | * 对方离开房间
50 | */
51 | fun onLeave()
52 | }
53 |
54 | /**
55 | * 主叫方的监听
56 | */
57 | interface ISignalOutgoingListener : IBaseSignalListener {
58 | /**
59 | * 创建房间成功
60 | */
61 | fun onCreateSuccess(roomBean: RoomBean)
62 |
63 | /**
64 | * 对方已响铃
65 | */
66 | fun onRing()
67 |
68 | /**
69 | * 对方拒接
70 | */
71 | fun onReject()
72 |
73 | /**
74 | * 收到来自被叫方的sdp
75 | */
76 | fun onSdpAnswer(sdpAnswerResponse: SdpAnswerResponse)
77 |
78 | }
79 |
80 | /**
81 | * 被叫叫方的监听
82 | */
83 | interface ISignalIncomingListener : IBaseSignalListener {
84 | fun onCancel()
85 |
86 | /**
87 | * 收到来自主叫方的sdp
88 | */
89 | fun onSdpOffer(sdpOfferResponse: SdpOfferResponse)
90 | }
91 |
92 | private val iWebSocketListenerList: MutableList = mutableListOf()
93 | private val iContactsListenerList: MutableList = mutableListOf()
94 | private val iSignalOutgoingListenerList: MutableList = mutableListOf()
95 | private val iSignalIncomingListenerList: MutableList = mutableListOf()
96 |
97 | private lateinit var webSocketClient: WebSocketClient
98 |
99 | fun init(url: String, account: String) {
100 | webSocketClient = object : WebSocketClient(URI("ws://${url}/webSocket/${account}")) {
101 | override fun onOpen(handshakedata: ServerHandshake) {
102 | iWebSocketListenerList.forEach {
103 | it.onOpen(handshakedata)
104 | }
105 | }
106 |
107 | override fun onClose(code: Int, reason: String, remote: Boolean) {
108 | BLog.i("--->onClose,code:${code},reason:${reason},remote:${remote}")
109 | iWebSocketListenerList.forEach {
110 | it.onClose(code, reason, remote)
111 | }
112 | }
113 |
114 | override fun onMessage(message: String) {
115 | BLog.i("接收的消息:${message}", TAG)
116 | iWebSocketListenerList.forEach {
117 | it.onMessage(message)
118 | }
119 |
120 | handleMessage(message)
121 | }
122 |
123 | override fun onError(exception: Exception) {
124 | BLog.e("--->onError,exception:${exception.message}", TAG)
125 | iWebSocketListenerList.forEach {
126 | it.onError(exception)
127 | }
128 | }
129 | }
130 | }
131 |
132 | private fun handleMessage(message: String) {
133 | val baseResponse = JsonUtils.parseJson2Bean(message, BaseResponse::class.java)
134 | when (baseResponse.msgType) {
135 | ResponseType.ADD_CONTACT -> {
136 | iContactsListenerList.forEach { listener ->
137 | val contact = JsonUtils.parseJson2Bean(
138 | message,
139 | AddContactResponse::class.java
140 | ).data
141 | contact?.let {
142 | if (it.account != AccountManager.getAccount()) {
143 | listener.onAdd(
144 | it
145 | )
146 | }
147 | }
148 |
149 | }
150 | }
151 | ResponseType.ALL_CONTACT -> {
152 | val allContactList = JsonUtils.parseJson2Bean(
153 | message,
154 | AllContactResponse::class.java
155 | )!!.data!!.toMutableList()
156 | iContactsListenerList.forEach { listener ->
157 | listener.onGetAll(
158 | allContactList
159 | )
160 | }
161 | }
162 | ResponseType.REMOVE_CONTACT -> {
163 | val contact = JsonUtils.parseJson2Bean(
164 | message,
165 | RemoveContactResponse::class.java
166 | ).data!!
167 | iContactsListenerList.forEach {
168 | it.onRemove(
169 | contact
170 | )
171 | }
172 | }
173 | ResponseType.CREATE_SUCCESS -> {
174 | val roomBean = JsonUtils.parseJson2Bean(
175 | message,
176 | CreateRoomResponse::class.java
177 | ).data!!
178 | iSignalOutgoingListenerList.forEach {
179 | it.onCreateSuccess(roomBean)
180 | }
181 | }
182 | ResponseType.INVITE -> {
183 | val intent = Intent(AppHolder.getApp(), VideoCallReceiveActivity::class.java)
184 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
185 | intent.putExtra(BaseVideoCallActivity.INTENT_KEY_ROOM_ID, baseResponse.roomId)
186 | intent.putExtra(
187 | BaseVideoCallActivity.INTENT_KEY_FROM_ACCOUNT,
188 | baseResponse.fromAccount
189 | )
190 | AppHolder.getApp().startActivity(intent)
191 | }
192 | ResponseType.CANCEL -> {
193 | iSignalIncomingListenerList.forEach {
194 | it.onCancel()
195 | }
196 | }
197 | ResponseType.RING -> {
198 | iSignalOutgoingListenerList.forEach {
199 | it.onRing()
200 | }
201 | }
202 | ResponseType.REJECT -> {
203 | iSignalOutgoingListenerList.forEach {
204 | it.onReject()
205 | }
206 | }
207 | ResponseType.JOIN_SUCCESS -> {
208 | iSignalOutgoingListenerList.forEach {
209 | it.onJoinSuccess()
210 | }
211 | iSignalIncomingListenerList.forEach {
212 | it.onJoinSuccess()
213 | }
214 | }
215 | ResponseType.SDP_OFFER -> {
216 | val sdpOfferResponse = JsonUtils.parseJson2Bean(
217 | message,
218 | SdpOfferResponse::class.java
219 | )
220 | iSignalIncomingListenerList.forEach {
221 | it.onSdpOffer(sdpOfferResponse)
222 | }
223 | }
224 | ResponseType.SDP_ANSWER -> {
225 | val sdpAnswerResponse = JsonUtils.parseJson2Bean(
226 | message,
227 | SdpAnswerResponse::class.java
228 | )
229 | iSignalOutgoingListenerList.forEach {
230 | it.onSdpAnswer(sdpAnswerResponse)
231 | }
232 | }
233 | ResponseType.CANDIDATE->{
234 | val iceCandidateResponse = JsonUtils.parseJson2Bean(message,IceCandidateResponse::class.java)
235 | iceCandidateResponse?.data?.let { iceCandidateBean ->
236 | val candidate = IceCandidate(iceCandidateBean.id,iceCandidateBean.label,iceCandidateBean.candidate)
237 | iSignalOutgoingListenerList.forEach {
238 | it.onIceCandidateFromRemote(candidate)
239 | }
240 | iSignalIncomingListenerList.forEach {
241 | it.onIceCandidateFromRemote(candidate)
242 | }
243 | }
244 | }
245 | ResponseType.LEAVE->{
246 | iSignalOutgoingListenerList.forEach {
247 | it.onLeave()
248 | }
249 | iSignalIncomingListenerList.forEach {
250 | it.onLeave()
251 | }
252 | }
253 |
254 |
255 | }
256 | }
257 |
258 | fun addWebSocketListener(webSocketListener: IWebSocketListener) {
259 | if (!iWebSocketListenerList.contains(webSocketListener)) {
260 | iWebSocketListenerList.add(webSocketListener)
261 | }
262 | }
263 |
264 | fun removeWebSocketListener(webSocketListener: IWebSocketListener) {
265 | iWebSocketListenerList.remove(webSocketListener)
266 | }
267 |
268 | fun clearAllWebSocketListener() {
269 | iWebSocketListenerList.clear()
270 | }
271 |
272 | fun addIContactsListener(iContactsListener: IContactsListener) {
273 | if (!iContactsListenerList.contains(iContactsListener)) {
274 | iContactsListenerList.add(iContactsListener)
275 | }
276 | }
277 |
278 | fun removeIContactsListener(iContactsListener: IContactsListener) {
279 | iContactsListenerList.remove(iContactsListener)
280 | }
281 |
282 | fun clearAllIContactsListener() {
283 | iContactsListenerList.clear()
284 | }
285 |
286 | fun addISignalOutgoingListener(iSignalListener: ISignalOutgoingListener) {
287 | iSignalOutgoingListenerList.add(iSignalListener)
288 | }
289 |
290 | fun removeISignalOutgoingListener(iSignalListener: ISignalOutgoingListener) {
291 | iSignalOutgoingListenerList.remove(iSignalListener)
292 | }
293 |
294 | fun clearISignalOutgoingListener() {
295 | iSignalOutgoingListenerList.clear()
296 | }
297 |
298 | fun addISignalIncomingListener(iSignalListener: ISignalIncomingListener) {
299 | iSignalIncomingListenerList.add(iSignalListener)
300 | }
301 |
302 | fun removeISignalIncomingListener(iSignalListener: ISignalIncomingListener) {
303 | iSignalIncomingListenerList.remove(iSignalListener)
304 | }
305 |
306 | fun clearISignalIncomingListener() {
307 | iSignalIncomingListenerList.clear()
308 | }
309 |
310 | fun connect() {
311 | webSocketClient.connect()
312 | }
313 |
314 | fun close() {
315 | webSocketClient.close()
316 | }
317 |
318 | fun sendMsg(baseRequest: BaseRequest) {
319 | if (webSocketClient.isClosed) {
320 | ToastUtils.show("socket已关闭")
321 | return
322 | }
323 | if (webSocketClient.isClosing) {
324 | ToastUtils.show("socket正在关闭")
325 | return
326 | }
327 | try {
328 | val json = JsonUtils.parseBean2json(baseRequest)
329 | BLog.i("发送的消息:${json}", TAG)
330 | webSocketClient.send(json)
331 | } catch (e: Exception) {
332 | e.printStackTrace()
333 | ToastUtils.show("发送失败:${e.message}")
334 | }
335 | }
336 |
337 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/utils/JsonUtils.java:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | */
4 | package com.alick.learnwebrtc.utils;
5 |
6 | import android.text.TextUtils;
7 |
8 | import com.google.gson.Gson;
9 | import com.google.gson.GsonBuilder;
10 | import com.google.gson.JsonArray;
11 | import com.google.gson.JsonDeserializationContext;
12 | import com.google.gson.JsonDeserializer;
13 | import com.google.gson.JsonElement;
14 | import com.google.gson.JsonParseException;
15 | import com.google.gson.JsonParser;
16 | import com.google.gson.JsonPrimitive;
17 | import com.google.gson.JsonSerializationContext;
18 | import com.google.gson.JsonSerializer;
19 | import com.google.gson.JsonSyntaxException;
20 | import com.google.gson.TypeAdapter;
21 | import com.google.gson.stream.JsonReader;
22 | import com.google.gson.stream.JsonToken;
23 | import com.google.gson.stream.JsonWriter;
24 |
25 | import org.json.JSONArray;
26 | import org.json.JSONException;
27 | import org.json.JSONObject;
28 |
29 | import java.io.IOException;
30 | import java.lang.reflect.Type;
31 | import java.util.ArrayList;
32 | import java.util.HashMap;
33 | import java.util.Iterator;
34 | import java.util.List;
35 | import java.util.Map;
36 |
37 | /**
38 | * 功能: json解析类
39 | * 作者: 崔兴旺
40 | * 日期: 2019/4/9
41 | */
42 | public class JsonUtils {
43 | private static Gson gson;
44 |
45 | static class DoubleDefault0Adapter implements JsonSerializer, JsonDeserializer {
46 | @Override
47 | public Double deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)throws JsonParseException {
48 | try{
49 | if(json.getAsString().equals("") || json.getAsString().equals("null")) {//定义为double类型,如果后台返回""或者null,则返回0.00
50 | return 0.00;
51 | }
52 | }catch (Exception ignore) {
53 | }
54 | try{
55 | return json.getAsDouble();
56 | }catch (NumberFormatException e) {
57 | throw new JsonSyntaxException(e);
58 | }
59 | }
60 |
61 | @Override
62 | public JsonElement serialize(Double src, Type typeOfSrc, JsonSerializationContext context) {
63 | return new JsonPrimitive(src);
64 | }
65 | }
66 |
67 | static class IntegerDefault0Adapter implements JsonSerializer, JsonDeserializer {
68 | @Override
69 | public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext
70 | context)
71 | throws JsonParseException {
72 | try {
73 | if (json.getAsString().equals("") || json.getAsString().equals("null")) {//定义为int类型,如果后台返回""或者null,则返回0
74 | return 0;
75 | }
76 | } catch (Exception ignore) {
77 | }
78 | try {
79 | return json.getAsInt();
80 | } catch (NumberFormatException e) {
81 | throw new JsonSyntaxException(e);
82 | }
83 | }
84 |
85 | @Override
86 | public JsonElement serialize(Integer src, Type typeOfSrc, JsonSerializationContext context) {
87 | return new JsonPrimitive(src);
88 | }
89 | }
90 |
91 |
92 | static class LongDefault0Adapter implements JsonSerializer, JsonDeserializer {
93 | @Override
94 | public Long deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
95 | throws JsonParseException {
96 | try {
97 | if (json.getAsString().equals("") || json.getAsString().equals("null")) {//定义为long类型,如果后台返回""或者null,则返回0
98 | return 0L;
99 | }
100 | } catch (Exception ignore) {
101 | }
102 | try {
103 | return json.getAsLong();
104 | } catch (NumberFormatException e) {
105 | throw new JsonSyntaxException(e);
106 | }
107 | }
108 |
109 | @Override
110 | public JsonElement serialize(Long src, Type typeOfSrc, JsonSerializationContext context) {
111 | return new JsonPrimitive(src);
112 | }
113 | }
114 |
115 | public static Gson getGson() {
116 | if (gson == null) {
117 | GsonBuilder gsonBuilder = new GsonBuilder();
118 | gsonBuilder.serializeNulls();
119 | TypeAdapter stringTypeAdapter = new TypeAdapter() {
120 | public String read(JsonReader reader) throws IOException {
121 | if (reader.peek() == JsonToken.NULL) {
122 | reader.nextNull();
123 | return "";
124 | }
125 | return reader.nextString();
126 | }
127 |
128 | public void write(JsonWriter writer, String value) throws IOException {
129 | if (value == null) {
130 | // 在这里处理null改为空字符串
131 | writer.value("");
132 | return;
133 | }
134 | writer.value(value);
135 | }
136 | };
137 | //注册自定义String的适配器
138 | gsonBuilder.registerTypeAdapter(String.class, stringTypeAdapter)
139 | .registerTypeAdapter(Double.class, new DoubleDefault0Adapter())
140 | .registerTypeAdapter(double.class, new DoubleDefault0Adapter())
141 | .registerTypeAdapter(Integer.class, new IntegerDefault0Adapter())
142 | .registerTypeAdapter(int.class, new IntegerDefault0Adapter())
143 | .registerTypeAdapter(Long.class, new LongDefault0Adapter())
144 | .registerTypeAdapter(long.class, new LongDefault0Adapter())
145 | ;
146 | gson = gsonBuilder.create();
147 | }
148 | return gson;
149 | }
150 |
151 | public static Object parseMapIterabletoJSON(Object object) throws JSONException {
152 | if (object instanceof Map) {
153 | JSONObject json = new JSONObject();
154 | Map map = (Map) object;
155 | for (Object key : map.keySet()) {
156 | json.put(key.toString(), parseMapIterabletoJSON(map.get(key)));
157 | }
158 | return json;
159 | } else if (object instanceof Iterable) {
160 | JSONArray json = new JSONArray();
161 | for (Object value : ((Iterable) object)) {
162 | json.put(value);
163 | }
164 | return json;
165 | } else {
166 | return object;
167 | }
168 | }
169 |
170 | public static String parseBean2json(Object obj) {
171 | return getGson().toJson(obj);
172 | }
173 |
174 | public static boolean isEmptyObject(JSONObject object) {
175 | return object.names() == null;
176 | }
177 |
178 | public static Map getMap(JSONObject object, String key) throws
179 | JSONException {
180 | return toMap(object.getJSONObject(key));
181 | }
182 |
183 | public static Map toMap(JSONObject object)
184 | throws JSONException {
185 |
186 | Map map = new HashMap();
187 | Iterator keys = object.keys();
188 | while (keys.hasNext()) {
189 | String key = (String) keys.next();
190 | map.put(key, fromJson(object.get(key)));
191 | }
192 | return map;
193 | }
194 |
195 | public static Map parseMap(JSONObject object) throws JSONException {
196 | Map map = new HashMap();
197 | Iterator keys = object.keys();
198 | while (keys.hasNext()) {
199 | String key = (String) keys.next();
200 | map.put(key, fromJson(object.get(key)).toString());
201 | }
202 | return map;
203 | }
204 |
205 | public static List toList(JSONArray array) throws JSONException {
206 | List list = new ArrayList();
207 | for (int i = 0; i < array.length(); i++) {
208 | list.add(fromJson(array.get(i)));
209 | }
210 | return list;
211 | }
212 |
213 | private static Object fromJson(Object json) throws JSONException {
214 | if (json == JSONObject.NULL) {
215 | return null;
216 | } else if (json instanceof JSONObject) {
217 | return toMap((JSONObject) json);
218 | } else if (json instanceof JSONArray) {
219 | return toList((JSONArray) json);
220 | } else {
221 | return json;
222 | }
223 | }
224 |
225 | public static <
226 | T> List parseRootJson2List(String json, Class clazz, String listKey) throws
227 | JSONException {
228 | return parseJson2List(new JSONObject(json).getJSONObject("data").getJSONArray(listKey).toString(), clazz);
229 | }
230 |
231 | public static List parseRootJson2List(String json, Class clazz) throws
232 | JSONException {
233 | return parseJson2List(new JSONObject(json).getJSONArray("data").toString(), clazz);
234 | }
235 |
236 | public static List parseJson2List(String json, Class clazz) throws
237 | JSONException {
238 | List list = null;
239 | try {
240 | list = new ArrayList<>();
241 | if (TextUtils.isEmpty(json)) {
242 | return list;
243 | }
244 |
245 | JsonArray array = new JsonParser().parse(json).getAsJsonArray();
246 | for (JsonElement element : array) {
247 | list.add(getGson().fromJson(element, clazz));
248 | }
249 | } catch (JsonSyntaxException e) {
250 | throw new JSONException(e.getMessage());
251 | }
252 | return list;
253 | }
254 |
255 | public static List parseJson2List(String json, Type type) throws JSONException {
256 | try {
257 | List list = new ArrayList<>();
258 | if (TextUtils.isEmpty(json)) {
259 | return list;
260 | }
261 |
262 | JsonArray array = new JsonParser().parse(json).getAsJsonArray();
263 | for (JsonElement element : array) {
264 | T t = getGson().fromJson(element, type);
265 | list.add(t);
266 | }
267 | return list;
268 | } catch (JsonSyntaxException e) {
269 | throw new JSONException(e.getMessage());
270 | }
271 | }
272 |
273 |
274 | public static T parseJson2Bean(String json, Class clazz) throws JSONException {
275 | try {
276 | return getGson().fromJson(json, clazz);
277 | } catch (JsonSyntaxException e) {
278 | throw new JSONException(e.getMessage());
279 | }
280 | }
281 |
282 | public static T parseRootJson2Bean(String json, Class clazz) throws JSONException {
283 | try {
284 | return getGson().fromJson(new JSONObject(json).getJSONObject("data").toString(), clazz);
285 | } catch (JsonSyntaxException | JSONException e) {
286 | throw new JSONException(e.getMessage());
287 | }
288 | }
289 |
290 | public static T parseRootJson2Bean(String json, Class clazz, String obj_key) throws
291 | JSONException {
292 | try {
293 | return getGson().fromJson(new JSONObject(json).getJSONObject("data").getJSONObject(obj_key).toString(), clazz);
294 | } catch (JsonSyntaxException | JSONException e) {
295 | throw new JSONException(e.getMessage());
296 | }
297 | }
298 |
299 | public static T parseJson2Bean(String json, Type type) throws JSONException {
300 | try {
301 | return getGson().fromJson(json, type);
302 | } catch (JsonSyntaxException e) {
303 | throw new JSONException(e.getMessage());
304 | }
305 | }
306 |
307 | public static String parseMap2String(Map map) throws JSONException {
308 | try {
309 | return getGson().toJson(map);
310 | } catch (Exception e) {
311 | throw new JSONException(e.getMessage());
312 | }
313 | }
314 |
315 | public static T parseMap2Bean(Map map, Class clazz) throws
316 | JSONException {
317 | try {
318 | return getGson().fromJson(getGson().toJson(map), clazz);
319 | } catch (JsonSyntaxException e) {
320 | throw new JSONException(e.getMessage());
321 | }
322 | }
323 |
324 | /**
325 | * 用来兼容Android4.4以下版本的remove方法
326 | *
327 | * @param jsonArray
328 | * @param index
329 | * @return
330 | */
331 | public static JSONArray removeCompatibilityKITKAT(JSONArray jsonArray, int index) throws
332 | JSONException {
333 | JSONArray mJsonArray = null;
334 | try {
335 | mJsonArray = new JSONArray();
336 | if (index < 0)
337 | return mJsonArray;
338 | if (index > jsonArray.length())
339 | return mJsonArray;
340 | for (int i = 0; i < jsonArray.length(); i++) {
341 | if (i != index) {
342 | mJsonArray.put(jsonArray.getJSONObject(i));
343 | }
344 | }
345 | } catch (JSONException e) {
346 | throw new JSONException(e.getMessage());
347 | }
348 | return mJsonArray;
349 | }
350 | }
351 |
--------------------------------------------------------------------------------
/app/src/main/java/com/alick/learnwebrtc/BaseVideoCallActivity.kt:
--------------------------------------------------------------------------------
1 | package com.alick.learnwebrtc
2 |
3 | import android.os.Bundle
4 | import android.os.Environment
5 | import android.util.Log
6 | import androidx.appcompat.app.AppCompatActivity
7 | import com.alick.learnwebrtc.bean.IceCandidateBean
8 | import com.alick.learnwebrtc.bean.request.SendIceCandidateRemovedRequest
9 | import com.alick.learnwebrtc.bean.request.SendIceCandidateRequest
10 | import com.alick.learnwebrtc.constant.WebRtcConstant
11 | import com.alick.learnwebrtc.manager.SignalManager
12 | import com.alick.learnwebrtc.manager.WebSocketManager
13 | import com.alick.learnwebrtc.utils.BLog
14 | import com.alick.learnwebrtc.utils.ToastUtils
15 | import org.java_websocket.handshake.ServerHandshake
16 | import org.webrtc.*
17 | import org.webrtc.PeerConnection.*
18 | import org.webrtc.RendererCommon.ScalingType
19 | import org.webrtc.audio.AudioDeviceModule
20 | import org.webrtc.audio.JavaAudioDeviceModule.*
21 | import java.io.File
22 | import java.util.*
23 |
24 | abstract class BaseVideoCallActivity : AppCompatActivity() {
25 | companion object {
26 | const val TAG = "BaseVideoCallActivity"
27 | const val INTENT_KEY_TO_ACCOUNT: String = "intent_key_to_account"
28 | const val INTENT_KEY_FROM_ACCOUNT: String = "intent_key_from_account"
29 | const val INTENT_KEY_ROOM_ID: String = "intent_key_room_id"
30 | }
31 |
32 | private val loopback = false
33 | private val tracing = true
34 | private val videoCodecHwAcceleration = true
35 | private val isRecordVideo = false //是否需要录制视频
36 | protected var roomId: String = ""
37 | protected var isAnswered = false //是否已接听
38 | private var isUsingFrontCamera: Boolean = false //是否正在使用前置摄像头
39 | private var isInitialized = false //是否已经初始化过了
40 | private var isRendererSwapped = false //是否交换过本地和远程的渲染器位置
41 |
42 | protected abstract val fromAccount: String
43 | protected abstract val toAccount: String
44 |
45 | protected abstract val mPipRenderer: SurfaceViewRenderer
46 |
47 | protected abstract val mFullscreenRenderer: SurfaceViewRenderer
48 |
49 | protected abstract val isSender: Boolean //是否是主叫方
50 |
51 |
52 | private val surfaceTextureHelper: SurfaceTextureHelper by lazy {
53 | val surfaceTextureHelper =
54 | SurfaceTextureHelper.create("CaptureThread", eglBase.eglBaseContext)
55 |
56 | surfaceTextureHelper
57 | }
58 |
59 | private val videoCapturer: CameraVideoCapturer? by lazy {
60 | val videoCapturer = if (Camera2Enumerator.isSupported(this)) {
61 | createCameraCapture(Camera2Enumerator(this))
62 | } else {
63 | createCameraCapture(Camera1Enumerator(true))
64 | }
65 | videoCapturer
66 | }
67 |
68 | private val remoteVideoSink: VideoSink = VideoSink {
69 | if (isRendererSwapped) {
70 | mPipRenderer.onFrame(it)
71 | } else {
72 | mFullscreenRenderer.onFrame(it)
73 | }
74 | }
75 |
76 | private val localVideoSink: VideoSink = VideoSink {
77 | if (isRendererSwapped) {
78 | mFullscreenRenderer.onFrame(it)
79 | } else {
80 | mPipRenderer.onFrame(it)
81 | }
82 | }
83 |
84 | private val remoteSinks: MutableList by lazy {
85 | val remoteSinks = mutableListOf()
86 | remoteSinks.add(remoteVideoSink)
87 | if (isRecordVideo) {
88 | remoteSinks.add(videoFileRenderer)
89 | }
90 | remoteSinks
91 | }
92 |
93 | private val localVideoTrack: VideoTrack by lazy {
94 | videoCapturer?.let {
95 | it.initialize(surfaceTextureHelper, this, videoSource.capturerObserver)
96 | it.startCapture(
97 | WebRtcConstant.HD_VIDEO_WIDTH,
98 | WebRtcConstant.HD_VIDEO_HEIGHT,
99 | WebRtcConstant.BPS_IN_KBPS
100 | )
101 | }
102 | val localVideoTrack = factory.createVideoTrack(
103 | WebRtcConstant.VIDEO_TRACK_ID,
104 | videoSource
105 | )
106 | localVideoTrack.setEnabled(true)
107 | localVideoTrack.addSink(localVideoSink)
108 | localVideoTrack
109 | }
110 |
111 | private val remoteVideoTrack: VideoTrack? by lazy {
112 | for (transceiver in peerConnection.transceivers) {
113 | val track = transceiver.receiver.track()
114 | if (track is VideoTrack) {
115 | return@lazy track
116 | }
117 | }
118 | null
119 | }
120 |
121 | private val videoSource: VideoSource by lazy {
122 | val videoSource = factory.createVideoSource(videoCapturer?.isScreencast ?: false)
123 | videoSource
124 | }
125 |
126 | private val audioSource: AudioSource by lazy {
127 | val audioSource = factory.createAudioSource(audioConstraints)
128 | audioSource
129 | }
130 |
131 | private val videoFileRenderer: VideoFileRenderer by lazy {
132 | val videoFileRenderer = VideoFileRenderer(
133 | File(
134 | getExternalFilesDir("webRtc"),
135 | "${System.currentTimeMillis()}.mp4"
136 | ).absolutePath,
137 | WebRtcConstant.HD_VIDEO_WIDTH,
138 | WebRtcConstant.HD_VIDEO_HEIGHT,
139 | eglBase.eglBaseContext
140 | )
141 | videoFileRenderer
142 | }
143 |
144 | private val localAudioTrack: AudioTrack by lazy {
145 | val localAudioTrack = factory.createAudioTrack(
146 | WebRtcConstant.AUDIO_TRACK_ID,
147 | audioSource
148 | )
149 | localAudioTrack.setEnabled(true)
150 | localAudioTrack
151 | }
152 |
153 | private val localVideoSender: RtpSender? by lazy {
154 | var localVideoSender: RtpSender? = null
155 | for (sender in peerConnection.senders) {
156 | if (sender.track() != null) {
157 | val trackType = sender.track()!!.kind()
158 | if (trackType == WebRtcConstant.VIDEO_TRACK_TYPE) {
159 | Log.i(TAG, "Found video sender.")
160 | localVideoSender = sender
161 | }
162 | }
163 | }
164 | localVideoSender
165 | }
166 |
167 | private val peerConnectionObserver: PeerConnection.Observer = object :
168 | PeerConnection.Observer {
169 | override fun onIceCandidate(candidate: IceCandidate) {
170 | BLog.d("--->onIceCandidate()")
171 | SignalManager.candidate(
172 | SendIceCandidateRequest(
173 | roomId,
174 | toAccount,
175 | IceCandidateBean(
176 | candidate.sdpMLineIndex,
177 | candidate.sdpMid,
178 | candidate.sdp
179 | )
180 | )
181 | )
182 | }
183 |
184 | override fun onIceCandidatesRemoved(candidates: Array) {
185 | BLog.d("--->onIceCandidatesRemoved()")
186 | SignalManager.removeCandidates(
187 | SendIceCandidateRemovedRequest(
188 | roomId,
189 | toAccount,
190 | SendIceCandidateRemovedRequest.Data("remove-candidates", candidates.map {
191 | IceCandidateBean(it.sdpMLineIndex, it.sdpMid, it.sdp)
192 | }.toMutableList())
193 | )
194 | )
195 | }
196 |
197 | override fun onSignalingChange(newState: SignalingState) {
198 | BLog.d("SignalingState: $newState")
199 | }
200 |
201 | override fun onIceConnectionChange(newState: IceConnectionState) {
202 | BLog.d("IceConnectionState: $newState")
203 | when (newState) {
204 | IceConnectionState.CONNECTED -> {
205 | ToastUtils.show("Ice连接成功")
206 | }
207 | IceConnectionState.DISCONNECTED -> {
208 | ToastUtils.show("Ice断开连接")
209 | }
210 | IceConnectionState.FAILED -> {
211 | BLog.i("ICE connection failed.", TAG)
212 | }
213 | }
214 | }
215 |
216 | override fun onConnectionChange(newState: PeerConnectionState) {
217 | BLog.d("PeerConnectionState: $newState")
218 | when (newState) {
219 | PeerConnectionState.CONNECTED -> {
220 | ToastUtils.show("Peer连接成功")
221 | }
222 | PeerConnectionState.DISCONNECTED -> {
223 | ToastUtils.show("Peer断开连接")
224 | }
225 | PeerConnectionState.FAILED -> {
226 | BLog.i("DTLS connection failed.", TAG)
227 | }
228 | }
229 | }
230 |
231 | override fun onIceGatheringChange(newState: IceGatheringState) {
232 | BLog.d("IceGatheringState: $newState")
233 | }
234 |
235 | override fun onIceConnectionReceivingChange(receiving: Boolean) {
236 | BLog.d("IceConnectionReceiving changed to $receiving")
237 | }
238 |
239 | override fun onSelectedCandidatePairChanged(event: CandidatePairChangeEvent) {
240 | BLog.d("Selected candidate pair changed because: $event")
241 | }
242 |
243 | override fun onAddStream(stream: MediaStream) {
244 | BLog.d("--->onAddStream()")
245 | }
246 |
247 | override fun onRemoveStream(stream: MediaStream) {
248 | BLog.d("--->onAddStream()")
249 | }
250 |
251 | override fun onDataChannel(dc: DataChannel) {
252 | BLog.d("New Data channel " + dc.label())
253 | }
254 |
255 | override fun onRenegotiationNeeded() {
256 | BLog.d("--->onRenegotiationNeeded()")
257 | // No need to do anything; AppRTC follows a pre-agreed-upon
258 | // signaling/negotiation protocol.
259 | }
260 |
261 | override fun onAddTrack(receiver: RtpReceiver, mediaStreams: Array) {
262 | BLog.d("--->onAddStream()")
263 | }
264 | }
265 |
266 | private val factory: PeerConnectionFactory by lazy {
267 | val options = PeerConnectionFactory.Options()
268 | if (loopback) {
269 | options.networkIgnoreMask = 0
270 | }
271 | if (tracing) {
272 | PeerConnectionFactory.startInternalTracingCapture(
273 | Environment.getExternalStorageDirectory().absolutePath + File.separator
274 | + "webrtc-trace.txt"
275 | )
276 | }
277 |
278 | val audioDeviceModule: AudioDeviceModule = createJavaAudioDevice()
279 | val encoderFactory: VideoEncoderFactory
280 | val decoderFactory: VideoDecoderFactory
281 |
282 | if (videoCodecHwAcceleration) {
283 | encoderFactory = DefaultVideoEncoderFactory(eglBase.eglBaseContext, true, false)
284 | decoderFactory = DefaultVideoDecoderFactory(eglBase.eglBaseContext)
285 | } else {
286 | encoderFactory = SoftwareVideoEncoderFactory()
287 | decoderFactory = SoftwareVideoDecoderFactory()
288 | }
289 |
290 | val factory = PeerConnectionFactory.builder()
291 | .setOptions(options)
292 | .setAudioDeviceModule(audioDeviceModule)
293 | .setVideoEncoderFactory(encoderFactory)
294 | .setVideoDecoderFactory(decoderFactory)
295 | .createPeerConnectionFactory()
296 |
297 | audioDeviceModule.release()
298 |
299 | factory
300 | }
301 |
302 |
303 | private val eglBase: EglBase by lazy {
304 | EglBase.create()
305 | }
306 |
307 |
308 | protected val videoSink = VideoSink { }
309 |
310 | /**
311 | * 音频约束
312 | */
313 | protected val audioConstraints: MediaConstraints by lazy {
314 | val audioConstraints = MediaConstraints()
315 | audioConstraints.mandatory.add(
316 | MediaConstraints.KeyValuePair(
317 | WebRtcConstant.AUDIO_ECHO_CANCELLATION_CONSTRAINT,
318 | "true"
319 | )
320 | )
321 | audioConstraints.mandatory.add(
322 | MediaConstraints.KeyValuePair(
323 | WebRtcConstant.AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT,
324 | "true"
325 | )
326 | )
327 | audioConstraints.mandatory.add(
328 | MediaConstraints.KeyValuePair(
329 | WebRtcConstant.AUDIO_HIGH_PASS_FILTER_CONSTRAINT,
330 | "true"
331 | )
332 | )
333 | audioConstraints.mandatory.add(
334 | MediaConstraints.KeyValuePair(
335 | WebRtcConstant.AUDIO_NOISE_SUPPRESSION_CONSTRAINT,
336 | "true"
337 | )
338 | )
339 | audioConstraints
340 | }
341 |
342 | /**
343 | * sdp媒体约束
344 | */
345 | protected val sdpMediaConstraints: MediaConstraints by lazy {
346 | val sdpMediaConstraints = MediaConstraints()
347 | sdpMediaConstraints.mandatory.add(
348 | MediaConstraints.KeyValuePair(
349 | WebRtcConstant.SDP_OFFER_TO_RECEIVE_AUDIO,
350 | "true"
351 | )
352 | )
353 | sdpMediaConstraints.mandatory.add(
354 | MediaConstraints.KeyValuePair(
355 | WebRtcConstant.SDP_OFFER_TO_RECEIVE_VIDEO,
356 | "true"
357 | )
358 | )
359 |
360 | sdpMediaConstraints
361 | }
362 |
363 | private var queuedRemoteCandidates: MutableList? = mutableListOf()
364 |
365 | private val iceServers: MutableList by lazy {
366 | val iceServers = mutableListOf()
367 | val turnServer: IceServer =
368 | IceServer.builder("stun:turn2.l.google.com")//google官方的turn服务器
369 | .setUsername("")//无用户名
370 | .setPassword("")//无密码
371 | .createIceServer()
372 |
373 | iceServers.add(turnServer)
374 | iceServers
375 | }
376 |
377 | private val rtcConfig: RTCConfiguration by lazy {
378 | val rtcConfig = RTCConfiguration(iceServers)
379 | // TCP candidates are only useful when connecting to a server that supports
380 | // ICE-TCP.
381 | rtcConfig.tcpCandidatePolicy = TcpCandidatePolicy.DISABLED
382 | rtcConfig.bundlePolicy = BundlePolicy.MAXBUNDLE
383 | rtcConfig.rtcpMuxPolicy = RtcpMuxPolicy.REQUIRE
384 | rtcConfig.continualGatheringPolicy =
385 | ContinualGatheringPolicy.GATHER_CONTINUALLY
386 | // Use ECDSA encryption.
387 | rtcConfig.keyType = KeyType.ECDSA
388 | // Enable DTLS for normal calls and disable for loopback calls.
389 | rtcConfig.enableDtlsSrtp = true//普通呼叫启用DTLS,环回呼叫禁用DTLS。
390 | rtcConfig.sdpSemantics = SdpSemantics.UNIFIED_PLAN
391 |
392 | rtcConfig
393 | }
394 |
395 |
396 | protected val peerConnection: PeerConnection by lazy {
397 | val peerConnection = factory.createPeerConnection(rtcConfig, peerConnectionObserver)
398 |
399 |
400 |
401 | peerConnection!!
402 | }
403 |
404 | private val webSocketListener = object : WebSocketManager.IWebSocketListener {
405 | override fun onOpen(serverHandshake: ServerHandshake) {
406 | }
407 |
408 | override fun onClose(code: Int, reason: String, remote: Boolean) {
409 | finish()
410 | }
411 |
412 | override fun onMessage(message: String) {
413 | }
414 |
415 | override fun onError(exception: Exception) {
416 | }
417 | }
418 |
419 | override fun onCreate(savedInstanceState: Bundle?) {
420 | super.onCreate(savedInstanceState)
421 | WebSocketManager.addWebSocketListener(webSocketListener)
422 | }
423 |
424 | protected fun switchCamera() {
425 | videoCapturer?.switchCamera(object : CameraVideoCapturer.CameraSwitchHandler {
426 | override fun onCameraSwitchDone(isFrontFacing: Boolean) {
427 | isUsingFrontCamera = isFrontFacing
428 | setMirror()
429 | }
430 |
431 | override fun onCameraSwitchError(error: String?) {
432 | ToastUtils.show("切换摄像头失败:${error}")
433 | }
434 |
435 | })
436 | }
437 |
438 | protected fun init() {
439 | PeerConnectionFactory.initialize(
440 | PeerConnectionFactory.InitializationOptions.builder(applicationContext)
441 | .setFieldTrials("WebRTC-IntelVP8/Enabled/")
442 | .setEnableInternalTracer(true)
443 | .createInitializationOptions()
444 | )
445 | initRenderer()
446 |
447 | val mediaStreamLabels = listOf(WebRtcConstant.VIDEO_AUDIO_TRACK_ID)
448 | peerConnection.addTrack(localVideoTrack, mediaStreamLabels)
449 |
450 | // We can add the renderers right away because we don't need to wait for an
451 | // answer to get the remote track.
452 | remoteVideoTrack?.setEnabled(true)
453 | for (remoteSink in remoteSinks) {
454 | remoteVideoTrack?.addSink(remoteSink)
455 | }
456 | peerConnection.addTrack(localAudioTrack, mediaStreamLabels)
457 | findVideoSender()
458 | isInitialized = true
459 | }
460 |
461 | private fun initRenderer() {
462 | mFullscreenRenderer.init(eglBase.eglBaseContext, null)
463 | mFullscreenRenderer.setScalingType(ScalingType.SCALE_ASPECT_FILL)
464 | mFullscreenRenderer.setEnableHardwareScaler(true)
465 |
466 | mPipRenderer.setOnClickListener {
467 | swapRenderer()
468 | }
469 |
470 | mPipRenderer.init(eglBase.eglBaseContext, null)
471 | mPipRenderer.setScalingType(ScalingType.SCALE_ASPECT_FIT)
472 | mPipRenderer.setZOrderMediaOverlay(true)
473 | mPipRenderer.setEnableHardwareScaler(true)
474 |
475 | previewSelf()
476 | }
477 |
478 | private fun findVideoSender() {
479 | localVideoSender
480 | }
481 |
482 | private fun createJavaAudioDevice(): AudioDeviceModule {
483 | // Set audio record error callbacks.
484 | val audioRecordErrorCallback: AudioRecordErrorCallback = object : AudioRecordErrorCallback {
485 | override fun onWebRtcAudioRecordInitError(errorMessage: String) {
486 | BLog.e("onWebRtcAudioRecordInitError: $errorMessage", TAG)
487 | }
488 |
489 | override fun onWebRtcAudioRecordStartError(
490 | errorCode: AudioRecordStartErrorCode, errorMessage: String
491 | ) {
492 | BLog.e("onWebRtcAudioRecordStartError: $errorCode. $errorMessage", TAG)
493 | }
494 |
495 | override fun onWebRtcAudioRecordError(errorMessage: String) {
496 | BLog.e("onWebRtcAudioRecordError: $errorMessage", TAG)
497 | }
498 | }
499 | val audioTrackErrorCallback: AudioTrackErrorCallback = object : AudioTrackErrorCallback {
500 | override fun onWebRtcAudioTrackInitError(errorMessage: String) {
501 | BLog.e("onWebRtcAudioTrackInitError: $errorMessage", TAG)
502 | }
503 |
504 | override fun onWebRtcAudioTrackStartError(
505 | errorCode: AudioTrackStartErrorCode, errorMessage: String
506 | ) {
507 | BLog.e("onWebRtcAudioTrackStartError: $errorCode. $errorMessage", TAG)
508 | }
509 |
510 | override fun onWebRtcAudioTrackError(errorMessage: String) {
511 | BLog.e("onWebRtcAudioTrackError: $errorMessage", TAG)
512 | }
513 | }
514 |
515 | // Set audio record state callbacks.
516 | val audioRecordStateCallback: AudioRecordStateCallback = object : AudioRecordStateCallback {
517 | override fun onWebRtcAudioRecordStart() {
518 | BLog.i("Audio recording starts", TAG)
519 | }
520 |
521 | override fun onWebRtcAudioRecordStop() {
522 | BLog.i("Audio recording stops", TAG)
523 | }
524 | }
525 |
526 | // Set audio track state callbacks.
527 | val audioTrackStateCallback: AudioTrackStateCallback = object : AudioTrackStateCallback {
528 | override fun onWebRtcAudioTrackStart() {
529 | BLog.i("Audio playout starts", TAG)
530 | }
531 |
532 | override fun onWebRtcAudioTrackStop() {
533 | BLog.i("Audio playout stops", TAG)
534 | }
535 | }
536 | return builder(applicationContext)
537 | .setSamplesReadyCallback(null)
538 | .setUseHardwareAcousticEchoCanceler(true)
539 | .setUseHardwareNoiseSuppressor(true)
540 | .setAudioRecordErrorCallback(audioRecordErrorCallback)
541 | .setAudioTrackErrorCallback(audioTrackErrorCallback)
542 | .setAudioRecordStateCallback(audioRecordStateCallback)
543 | .setAudioTrackStateCallback(audioTrackStateCallback)
544 | .createAudioDeviceModule()
545 | }
546 |
547 | /**
548 | * 创建相机媒体流
549 | */
550 | private fun createCameraCapture(enumerator: CameraEnumerator): CameraVideoCapturer? {
551 | val deviceNames = enumerator.deviceNames
552 |
553 | //1.优先使用前置摄像头(用于视频通话时自拍)
554 | for (deviceName in deviceNames) {
555 | if (enumerator.isFrontFacing(deviceName)) {
556 | val videoCapturer: CameraVideoCapturer? =
557 | enumerator.createCapturer(deviceName, null)
558 | if (videoCapturer != null) {
559 | isUsingFrontCamera = true
560 | return videoCapturer
561 | }
562 | }
563 | }
564 |
565 | //2.否则再使用其他摄像头(例如后置摄像头)
566 | for (deviceName in deviceNames) {
567 | if (!enumerator.isFrontFacing(deviceName)) {
568 | val videoCapturer: CameraVideoCapturer? =
569 | enumerator.createCapturer(deviceName, null)
570 | if (videoCapturer != null) {
571 | isUsingFrontCamera = true
572 | return videoCapturer
573 | }
574 | }
575 | }
576 | return null
577 | }
578 |
579 | protected fun drainCandidates() {
580 | BLog.i("--->drainCandidates()", TAG)
581 | synchronized(BaseVideoCallActivity::class.java) {
582 | if (queuedRemoteCandidates != null) {
583 | queuedRemoteCandidates?.let {
584 | for (candidate in it) {
585 | BLog.i("--->drainCandidates(),candidate:${candidate}", TAG)
586 | peerConnection.addIceCandidate(candidate)
587 | }
588 | }
589 | queuedRemoteCandidates = null
590 | }
591 | }
592 | }
593 |
594 | protected fun addIceCandidateFromRemote(candidate: IceCandidate) {
595 | BLog.i("--->addIceCandidateFromRemote()", TAG)
596 | if (queuedRemoteCandidates != null) {
597 | synchronized(BaseVideoCallActivity::class.java) {
598 | BLog.i(
599 | "--->addIceCandidateFromRemote(),先添加到queuedRemoteCandidates集合中,candidate:${candidate}",
600 | TAG
601 | )
602 | queuedRemoteCandidates?.add(candidate)
603 | }
604 | } else {
605 | BLog.i(
606 | "--->addIceCandidateFromRemote(),直接添加到peerConnection中,candidate:${candidate}",
607 | TAG
608 | )
609 | peerConnection.addIceCandidate(candidate)
610 | }
611 | }
612 |
613 | private fun releaseWebRtc() {
614 | if (!isInitialized) {
615 | return
616 | }
617 | mPipRenderer.release()
618 | mFullscreenRenderer.release()
619 | if (isRecordVideo) {
620 | videoFileRenderer.release()
621 | }
622 |
623 | factory.stopAecDump()
624 | peerConnection.dispose()
625 | audioSource.dispose()
626 | videoCapturer?.let {
627 | it.stopCapture()
628 | it.dispose()
629 | }
630 | videoSource.dispose()
631 | surfaceTextureHelper.dispose()
632 | factory.dispose()
633 | eglBase.release()
634 | PeerConnectionFactory.stopInternalTracingCapture()
635 | PeerConnectionFactory.shutdownInternalTracer()
636 | }
637 |
638 | override fun onBackPressed() {
639 |
640 | }
641 |
642 | override fun onDestroy() {
643 | WebSocketManager.removeWebSocketListener(webSocketListener)
644 | releaseWebRtc()
645 | super.onDestroy()
646 | }
647 |
648 | protected fun swapRenderer() {
649 | isRendererSwapped = !isRendererSwapped
650 | setMirror()
651 | }
652 |
653 | private fun setMirror() {
654 | setPipRendererMirror(mPipRenderer)
655 | setFullscreenRendererMirror(mFullscreenRenderer)
656 | }
657 |
658 | //设置画中画的
659 | private fun setPipRendererMirror(pipRenderer: SurfaceViewRenderer) {
660 | if (isUsingFrontCamera) {
661 | if (isRendererSwapped) {
662 | pipRenderer.setMirror(false)//
663 | } else {
664 | pipRenderer.setMirror(true)//自拍默认,需要设置为镜像
665 | }
666 | } else {
667 | if (isRendererSwapped) {
668 | pipRenderer.setMirror(false)
669 | } else {
670 | pipRenderer.setMirror(false)
671 | }
672 | }
673 | }
674 |
675 | //设置全屏的
676 | private fun setFullscreenRendererMirror(fullscreenRenderer: SurfaceViewRenderer) {
677 | if (isUsingFrontCamera) {
678 | if (isRendererSwapped) {
679 | fullscreenRenderer.setMirror(true)//自拍默认,需要设置为镜像
680 | } else {
681 | fullscreenRenderer.setMirror(false)
682 | }
683 | } else {
684 | if (isRendererSwapped) {
685 | fullscreenRenderer.setMirror(false)
686 | } else {
687 | fullscreenRenderer.setMirror(false)
688 | }
689 | }
690 | }
691 |
692 | private fun previewSelf() {
693 | swapRenderer()
694 | }
695 |
696 | }
--------------------------------------------------------------------------------