第一次提交
This commit is contained in:
7
lib_callapi/.gitignore
vendored
Normal file
7
lib_callapi/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/build
|
||||
arm64-v8a/
|
||||
armeabi-v7a/
|
||||
x86/
|
||||
x86_64/
|
||||
agora-rtm-sdk.jar
|
||||
third-party/
|
||||
62
lib_callapi/build.gradle
Normal file
62
lib_callapi/build.gradle
Normal file
@@ -0,0 +1,62 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'maven-publish'
|
||||
apply plugin: 'de.undercouch.download'
|
||||
|
||||
android {
|
||||
namespace 'io.agora.callapi'
|
||||
compileSdk 33
|
||||
|
||||
defaultConfig {
|
||||
minSdk 21
|
||||
targetSdk 33
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug{
|
||||
minifyEnabled false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
}
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
api 'com.google.code.gson:gson:2.9.0'
|
||||
|
||||
// rtc
|
||||
api "io.agora.rtc:full-sdk:4.5.0"
|
||||
// rtm
|
||||
api 'io.agora:agora-rtm-lite:2.2.2'
|
||||
// im
|
||||
api 'io.hyphenate:hyphenate-chat:4.3.0'
|
||||
}
|
||||
0
lib_callapi/consumer-rules.pro
Normal file
0
lib_callapi/consumer-rules.pro
Normal file
21
lib_callapi/proguard-rules.pro
vendored
Normal file
21
lib_callapi/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
4
lib_callapi/src/main/AndroidManifest.xml
Normal file
4
lib_callapi/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-sdk tools:overrideLibrary="io.agora.onetoone" />
|
||||
</manifest>
|
||||
1443
lib_callapi/src/main/java/io/agora/onetoone/CallApiImpl.kt
Normal file
1443
lib_callapi/src/main/java/io/agora/onetoone/CallApiImpl.kt
Normal file
@@ -0,0 +1,1443 @@
|
||||
package io.agora.onetoone
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.TextureView
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import com.google.gson.Gson
|
||||
import io.agora.callapi.BuildConfig
|
||||
import io.agora.onetoone.extension.*
|
||||
import io.agora.onetoone.report.APIReporter
|
||||
import io.agora.onetoone.report.APIType
|
||||
import io.agora.onetoone.report.ApiCostEvent
|
||||
import io.agora.onetoone.signalClient.ISignalClientListener
|
||||
import io.agora.rtc2.*
|
||||
import io.agora.rtc2.video.VideoCanvas
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
|
||||
enum class CallAutoSubscribeType(val value: Int) {
|
||||
None(0),
|
||||
Video(1),
|
||||
AudioVideo(2),
|
||||
}
|
||||
enum class CallAction(val value: Int) {
|
||||
Call(0),
|
||||
CancelCall(1),
|
||||
Accept(2),
|
||||
Reject(3),
|
||||
Hangup(4),
|
||||
AudioCall(10);
|
||||
companion object {
|
||||
fun fromValue(value: Int): CallAction? {
|
||||
return CallAction.values().find { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object CallCustomEvent {
|
||||
const val stateChange = "stateChange"
|
||||
const val eventChange = "eventChange"
|
||||
}
|
||||
|
||||
/*
|
||||
* Timing for callee to join RTC during call
|
||||
* 被叫呼叫中加入RTC的时机
|
||||
*/
|
||||
enum class CalleeJoinRTCTiming(val value: Int) {
|
||||
// Join channel and push video stream when receiving call, higher cost for callee but faster video display
|
||||
// 在收到呼叫时即加入频道并推送视频流,被叫时费用较高但出图更快
|
||||
Calling(0),
|
||||
|
||||
// Join channel and push video stream only after actively accepting call, lower cost for callee but slower video display
|
||||
// 在收到呼叫后,主动发起接受后才加入频道并推送视频流,被叫时费用较低但出图较慢
|
||||
Accepted(1)
|
||||
}
|
||||
|
||||
class CallApiImpl constructor(
|
||||
val context: Context
|
||||
): ICallApi, ISignalClientListener, IRtcEngineEventHandler() {
|
||||
|
||||
companion object {
|
||||
const val kReportCategory = "2.1.2"
|
||||
const val kPublisher = "publisher"
|
||||
// Call timing information that will be output step by step when connected
|
||||
// 呼叫时的耗时信息,会在connected时抛出分步耗时
|
||||
const val kCostTimeMap = "costTimeMap"
|
||||
const val kRemoteUserId = "remoteUserId"
|
||||
const val kFromUserId = "fromUserId"
|
||||
const val kFromRoomId = "fromRoomId"
|
||||
const val kFromUserExtension = "fromUserExtension"
|
||||
|
||||
// ⚠️ Do not modify the following two values, clients may make business decisions based on this rejectReason/call busy (e.g. user busy)
|
||||
// ⚠️不允许修改下列两项值,客户可能会根据该rejectReason/call busy 来做业务判断(例如用户忙)
|
||||
const val kRejectReason = "rejectReason"
|
||||
const val kRejectReasonCallBusy = "The user is currently busy"
|
||||
|
||||
// Whether internally rejected, currently marked as peer call busy when receiving internal rejection
|
||||
// 是否内部拒绝,收到内部拒绝目前标记为对端call busy
|
||||
const val kRejectByInternal = "rejectByInternal"
|
||||
|
||||
// Whether internally cancelled call, currently marked as remote calling timeout when receiving internal call cancellation
|
||||
// 是否内部取消呼叫,收到内部取消呼叫目前标记为对端 remote calling timeout
|
||||
const val kCancelCallByInternal = "cancelCallByInternal"
|
||||
|
||||
const val kHangupReason = "hangupReason"
|
||||
|
||||
// Message ID being sent
|
||||
// 发送的消息id
|
||||
private const val kMessageId = "messageId"
|
||||
}
|
||||
|
||||
private val kCurrentMessageVersion = "1.0"
|
||||
private val kMessageAction = "message_action"
|
||||
private val kMessageVersion = "message_version"
|
||||
private val kMessageTs = "message_timestamp"
|
||||
private val kCallId = "callId"
|
||||
|
||||
private val TAG = "CallApiImpl_LOG"
|
||||
private val delegates = mutableListOf<ICallApiListener>()
|
||||
private val localFrameProxy = CallLocalFirstFrameProxy(this)
|
||||
private var config: CallConfig? = null
|
||||
set(value) {
|
||||
field?.signalClient?.removeListener(this)
|
||||
field = value
|
||||
field?.signalClient?.addListener(this)
|
||||
}
|
||||
private var prepareConfig: PrepareConfig? = null
|
||||
private var connectInfo = CallConnectInfo()
|
||||
private var isChannelJoined = false
|
||||
// Message ID
|
||||
// 消息id
|
||||
private var messageId: Int = 0
|
||||
|
||||
private var tempRemoteCanvasView = TextureView(context)
|
||||
private var tempLocalCanvasView = TextureView(context)
|
||||
// Default timing for joining RTC
|
||||
// 默认的加入rtc时机
|
||||
private var defaultCalleeJoinRTCTiming = CalleeJoinRTCTiming.Calling
|
||||
|
||||
private var reporter: APIReporter? = null
|
||||
|
||||
// Current state
|
||||
// 当前状态
|
||||
private var state: CallStateType = CallStateType.Idle
|
||||
set(value) {
|
||||
val prevState = field
|
||||
field = value
|
||||
if (prevState == value) { return }
|
||||
when(value) {
|
||||
CallStateType.Calling -> {
|
||||
tempRemoteCanvasView.alpha = 0f
|
||||
// If prepareConfig?.callTimeoutSeconds == 0, no timeout will be set internally
|
||||
// 如果prepareConfig?.callTimeoutSeconds == 0,内部不做超时
|
||||
val timeout = prepareConfig?.callTimeoutMillisecond ?: 0L
|
||||
if (timeout <= 0L) {
|
||||
return
|
||||
}
|
||||
// Start timer, if no response after timeout, call no response
|
||||
// 开启定时器,如果超时无响应,调用no response
|
||||
connectInfo.scheduledTimer({
|
||||
_cancelCall(cancelCallByInternal = true) { }
|
||||
updateAndNotifyState(CallStateType.Prepared, CallStateReason.CallingTimeout)
|
||||
notifyEvent(CallEvent.CallingTimeout)
|
||||
}, timeout)
|
||||
}
|
||||
CallStateType.Prepared -> {
|
||||
connectInfo.scheduledTimer(null)
|
||||
if (prevState != CallStateType.Idle) {
|
||||
_prepareForCall(prepareConfig!!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
CallStateType.Connecting -> {
|
||||
reporter?.startDurationEvent(ApiCostEvent.FIRST_FRAME_PERCEIVED)
|
||||
}
|
||||
CallStateType.Connected -> {
|
||||
muteRemoteAudio(false)
|
||||
tempRemoteCanvasView.alpha = 1f
|
||||
connectInfo.scheduledTimer(null)
|
||||
val ext = mapOf<String, Any>(
|
||||
"channelName" to (connectInfo.callingRoomId ?: ""),
|
||||
"userId" to (config?.userId ?: 0)
|
||||
)
|
||||
reporter?.endDurationEvent(ApiCostEvent.FIRST_FRAME_PERCEIVED, ext)
|
||||
reporter?.endDurationEvent(ApiCostEvent.FIRST_FRAME_ACTUAL, ext)
|
||||
}
|
||||
CallStateType.Idle, CallStateType.Failed -> {
|
||||
leaveRTC()
|
||||
connectInfo.clean()
|
||||
isPreparing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
/// RTC connection for join channel ex, used for leaving channel ex and checking if already joined ex channel
|
||||
/// join channel ex的connection,用来leave channel ex和判断是否已经加入ex channel
|
||||
private var rtcConnection: RtcConnection? = null
|
||||
|
||||
// Callback when joining RTC is completed
|
||||
// 加入RTC完成回调
|
||||
private var joinRtcCompletion: ((AGError?) -> Unit)? = null
|
||||
|
||||
// Callback when first frame of video/audio is rendered
|
||||
// 首帧 出图/出声 回调
|
||||
private var firstFrameCompletion: (() -> Unit)? = null
|
||||
|
||||
private var isPreparing = false
|
||||
|
||||
init {
|
||||
callPrint("init-- CallApiImpl")
|
||||
}
|
||||
|
||||
// Get NTP time
|
||||
// 获取ntp时间
|
||||
private fun getTimeInMs(): Long {
|
||||
return System.currentTimeMillis()
|
||||
}
|
||||
|
||||
private fun getCost(ts: Int? = null): Long {
|
||||
val cts = connectInfo.callTs ?: return 0
|
||||
return if (ts != null) {
|
||||
ts - cts
|
||||
} else {
|
||||
getTimeInMs() - cts
|
||||
}
|
||||
}
|
||||
|
||||
private fun messageDic(action: CallAction): Map<String, Any> {
|
||||
val map = mutableMapOf<String, Any>(
|
||||
kMessageAction to action.value,
|
||||
kMessageVersion to kCurrentMessageVersion,
|
||||
kMessageTs to getTimeInMs(),
|
||||
kFromUserId to (config?.userId ?: 0),
|
||||
kCallId to connectInfo.callId
|
||||
)
|
||||
prepareConfig?.userExtension?.let {
|
||||
map[kFromUserExtension] = it
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
private fun callMessageDic(remoteUserId: Int, callType: CallType, fromRoomId: String, callExtension: Map<String, Any>): Map<String, Any> {
|
||||
val message = messageDic(action = if(callType == CallType.Video) CallAction.Call else CallAction.AudioCall).toMutableMap()
|
||||
message[kRemoteUserId] = remoteUserId
|
||||
message[kFromRoomId] = fromRoomId
|
||||
var userExtension = message[kFromUserExtension] as? Map<String, Any> ?: emptyMap()
|
||||
userExtension = userExtension + callExtension
|
||||
message[kFromUserExtension] = userExtension
|
||||
return message
|
||||
}
|
||||
|
||||
private fun cancelCallMessageDic(cancelByInternal: Boolean): Map<String, Any> {
|
||||
val message = messageDic(CallAction.CancelCall).toMutableMap()
|
||||
message[kCancelCallByInternal] = if (cancelByInternal) 1 else 0
|
||||
return message
|
||||
}
|
||||
|
||||
private fun rejectMessageDic(reason: String?, rejectByInternal: Boolean): Map<String, Any> {
|
||||
val message = messageDic(CallAction.Reject).toMutableMap()
|
||||
message[kRejectReason] = reason ?: ""
|
||||
message[kRejectByInternal] = if (rejectByInternal) 1 else 0
|
||||
return message
|
||||
}
|
||||
|
||||
private fun hangupMessageDic(reason: String?): Map<String, Any> {
|
||||
val message = messageDic(CallAction.Hangup).toMutableMap()
|
||||
message[kHangupReason] = reason ?: ""
|
||||
return message
|
||||
}
|
||||
|
||||
private fun getNtpTimeInMs(): Long {
|
||||
val currentNtpTime = config?.rtcEngine?.ntpWallTimeInMs ?: 0L
|
||||
return if (currentNtpTime != 0L) {
|
||||
currentNtpTime
|
||||
} else {
|
||||
Log.e(TAG, "getNtpTimeInMs ntpWallTimeInMs is zero!!!!!!!!!!")
|
||||
System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
|
||||
private fun canJoinRtcOnCalling(eventInfo: Map<String, Any>): Boolean {
|
||||
var emptyCount = 0
|
||||
delegates.forEach {
|
||||
val isEnable: Boolean? = it.canJoinRtcOnCalling(eventInfo)
|
||||
if (isEnable != null) {
|
||||
if (isEnable) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
emptyCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
// If no protocol is implemented, use default value
|
||||
// 如果一个协议都没有实现,使用默认值
|
||||
if (emptyCount == delegates.size) {
|
||||
callPrint("join rtc strategy callback not found, use default")
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun notifyCallConnected() {
|
||||
val config = config ?: return
|
||||
val ntpTime = getNtpTimeInMs()
|
||||
connectInfo.callConnectedTs = ntpTime
|
||||
val callUserId = (if (connectInfo.callingRoomId == prepareConfig?.roomId) config.userId else connectInfo.callingUserId) ?: 0
|
||||
delegates.forEach { listener ->
|
||||
listener.onCallConnected(
|
||||
roomId = connectInfo.callingRoomId ?: "",
|
||||
callUserId = callUserId,
|
||||
currentUserId = config.userId,
|
||||
timestamp = ntpTime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyCallDisconnected(hangupUserId: Int) {
|
||||
val config = config ?: return
|
||||
val ntpTime = getNtpTimeInMs()
|
||||
delegates.forEach { listener ->
|
||||
listener.onCallDisconnected(
|
||||
roomId = connectInfo.callingRoomId ?: "",
|
||||
hangupUserId = hangupUserId,
|
||||
currentUserId = config.userId,
|
||||
timestamp = ntpTime,
|
||||
duration = ntpTime - connectInfo.callConnectedTs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyTokenPrivilegeWillExpire() {
|
||||
delegates.forEach { listener ->
|
||||
listener.tokenPrivilegeWillExpire()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkConnectedSuccess(reason: CallStateReason) {
|
||||
if (rtcConnection == null) {
|
||||
callWarningPrint("checkConnectedSuccess fail, connection not found")
|
||||
return
|
||||
}
|
||||
val firstFrameWaittingDisabled = prepareConfig?.firstFrameWaittingDisabled ?: false
|
||||
callPrint("checkConnectedSuccess: firstFrameWaittingDisabled: ${firstFrameWaittingDisabled}, isRetrieveFirstFrame: ${connectInfo.isRetrieveFirstFrame} state: $state")
|
||||
if (firstFrameWaittingDisabled) {
|
||||
if (state != CallStateType.Connecting) { return }
|
||||
} else {
|
||||
if (!connectInfo.isRetrieveFirstFrame || state != CallStateType.Connecting) {return}
|
||||
}
|
||||
/*
|
||||
* 1. Due to callee joining channel and subscribing/publishing stream early, both sides may receive first video frame before callee accepts (becomes connecting)
|
||||
* 2. In 1v1 matching, both sides receive onCall, when A initiates accept, B receives onAccept+A's first frame, causing B to enter connected state before accepting
|
||||
* Therefore:
|
||||
* Becoming connecting: Need to check both "remote accepted" + "local accepted (or initiated call)"
|
||||
* Becoming connected: Need to check both "connecting state" + "received first frame"
|
||||
*
|
||||
* 1.因为被叫提前加频道并订阅流和推流,导致双端收到视频首帧可能会比被叫点accept(变成connecting)比更早
|
||||
* 2.由于匹配1v1时双端都会收到onCall,此时A发起accept,B收到了onAccept+A首帧,会导致B未接受即进入了connected状态
|
||||
* 因此:
|
||||
* 变成connecting: 需要同时检查是否变成了"远端已接受" + "本地已接受(或已发起呼叫)"
|
||||
* 变成connected: 需要同时检查是否是"connecting状态" + "收到首帧"
|
||||
*/
|
||||
changeToConnectedState(reason)
|
||||
}
|
||||
|
||||
private fun changeToConnectedState(reason: CallStateReason) {
|
||||
val eventInfo = mapOf(
|
||||
kFromRoomId to (connectInfo.callingRoomId ?: ""),
|
||||
kFromRoomId to (connectInfo.callingRoomId ?: ""),
|
||||
kFromUserId to (connectInfo.callingUserId ?: 0),
|
||||
kRemoteUserId to (config?.userId ?: 0),
|
||||
kCostTimeMap to connectInfo.callCostMap
|
||||
)
|
||||
updateAndNotifyState(CallStateType.Connected, reason, eventInfo = eventInfo)
|
||||
// notifyEvent(event: CallReason.RecvRemoteFirstFrame, elapsed: elapsed)
|
||||
}
|
||||
// External state notification
|
||||
// 外部状态通知
|
||||
private fun updateAndNotifyState(state: CallStateType,
|
||||
stateReason: CallStateReason = CallStateReason.None,
|
||||
eventReason: String = "",
|
||||
eventInfo: Map<String, Any> = emptyMap()) {
|
||||
callPrint("call change[${connectInfo.callId}] state: $state, stateReason: '$stateReason', eventReason: $eventReason")
|
||||
|
||||
val oldState = this.state
|
||||
// Check connected/disconnected
|
||||
// 检查连接/断开连接状态
|
||||
if (state == CallStateType.Connected && oldState == CallStateType.Connecting) {
|
||||
notifyCallConnected()
|
||||
} else if (state == CallStateType.Prepared && oldState == CallStateType.Connected) {
|
||||
when (stateReason) {
|
||||
// Normally only .remoteCancel, .remoteHangup will be triggered, others are fallback
|
||||
// 正常只会触发.remoteCancel, .remoteHangup,剩余的做兜底
|
||||
CallStateReason.RemoteCancelled, CallStateReason.RemoteHangup, CallStateReason.RemoteRejected, CallStateReason.RemoteCallBusy -> {
|
||||
notifyCallDisconnected(connectInfo.callingUserId ?: 0)
|
||||
}
|
||||
else -> {
|
||||
// .localHangup or bad case
|
||||
// .localHangup 或 bad case
|
||||
notifyCallDisconnected(config?.userId ?: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val ext = mapOf(
|
||||
"state" to state.value,
|
||||
"stateReason" to stateReason.value,
|
||||
"eventReason" to eventReason,
|
||||
"userId" to (config?.userId ?: 0),
|
||||
"callId" to connectInfo.callId
|
||||
)
|
||||
reportCustomEvent(CallCustomEvent.stateChange, ext)
|
||||
|
||||
this.state = state
|
||||
delegates.forEach {
|
||||
it.onCallStateChanged(state, stateReason, eventReason, eventInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifySendMessageErrorEvent(error: AGError, reason: String?) {
|
||||
notifyErrorEvent(
|
||||
CallErrorEvent.SendMessageFail,
|
||||
errorType = CallErrorCodeType.Message,
|
||||
errorCode = error.code,
|
||||
message = "${reason ?: ""}${error.msg}"
|
||||
)
|
||||
}
|
||||
|
||||
private fun notifyRtcOccurErrorEvent(errorCode: Int, message: String? = null) {
|
||||
notifyErrorEvent(
|
||||
CallErrorEvent.RtcOccurError,
|
||||
errorType = CallErrorCodeType.Rtc,
|
||||
errorCode = errorCode,
|
||||
message = message
|
||||
)
|
||||
}
|
||||
|
||||
private fun notifyErrorEvent(
|
||||
errorEvent: CallErrorEvent,
|
||||
errorType: CallErrorCodeType,
|
||||
errorCode: Int,
|
||||
message: String?) {
|
||||
callPrint("call change[${connectInfo.callId} errorEvent: ${errorEvent.value}, errorType: ${errorType.value}, errorCode: ${errorCode}, message: ${message ?: ""}")
|
||||
delegates.forEach { listener ->
|
||||
listener.onCallError(errorEvent, errorType, errorCode, message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyEvent(event: CallEvent, reasonCode: String? = null, reasonString: String? = null) {
|
||||
callPrint("call change[${connectInfo.callId}] event: ${event.value} reasonCode: '$reasonCode' reasonString: '$reasonString'")
|
||||
config?.let { config ->
|
||||
val ext = mutableMapOf(
|
||||
"event" to event.value,
|
||||
"userId" to config.userId,
|
||||
"state" to state.value,
|
||||
"callId" to connectInfo.callId
|
||||
)
|
||||
reasonCode?.let {
|
||||
ext["reasonCode"] = it
|
||||
}
|
||||
reasonString?.let {
|
||||
ext["reasonString"] = reasonString
|
||||
}
|
||||
reportCustomEvent(CallCustomEvent.eventChange, ext)
|
||||
|
||||
} ?: callWarningPrint("notifyEvent config == null")
|
||||
delegates.forEach { listener ->
|
||||
listener.onCallEventChanged(event, reasonCode)
|
||||
}
|
||||
when (event) {
|
||||
CallEvent.RemoteUserRecvCall -> reportCostEvent(CallConnectCostType.RemoteUserRecvCall)
|
||||
CallEvent.RemoteJoined -> reportCostEvent(CallConnectCostType.RemoteUserJoinChannel)
|
||||
CallEvent.LocalJoined -> reportCostEvent(CallConnectCostType.LocalUserJoinChannel)
|
||||
CallEvent.CaptureFirstLocalVideoFrame -> reportCostEvent(CallConnectCostType.LocalFirstFrameDidCapture)
|
||||
CallEvent.PublishFirstLocalAudioFrame -> reportCostEvent(CallConnectCostType.LocalFirstFrameDidPublish)
|
||||
CallEvent.PublishFirstLocalVideoFrame -> reportCostEvent(CallConnectCostType.LocalFirstFrameDidPublish)
|
||||
CallEvent.RemoteAccepted -> {
|
||||
reportCostEvent(CallConnectCostType.AcceptCall)
|
||||
checkConnectedSuccess(CallStateReason.RemoteAccepted)
|
||||
}
|
||||
CallEvent.LocalAccepted -> {
|
||||
reportCostEvent(CallConnectCostType.AcceptCall)
|
||||
checkConnectedSuccess(CallStateReason.LocalAccepted)
|
||||
}
|
||||
CallEvent.RecvRemoteFirstFrame -> {
|
||||
reportCostEvent(CallConnectCostType.RecvFirstFrame)
|
||||
checkConnectedSuccess(CallStateReason.RecvRemoteFirstFrame)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun _prepareForCall(prepareConfig: PrepareConfig, completion: ((AGError?) -> Unit)?) {
|
||||
val cfg = config
|
||||
if (cfg == null) {
|
||||
val reason = "config is Empty"
|
||||
callWarningPrint(reason)
|
||||
completion?.invoke(AGError(reason, -1))
|
||||
return
|
||||
}
|
||||
if (isPreparing) {
|
||||
val reason = "is already in preparing"
|
||||
callWarningPrint(reason)
|
||||
completion?.invoke(AGError(reason, -1))
|
||||
return
|
||||
}
|
||||
when (state) {
|
||||
CallStateType.Calling, CallStateType.Connecting, CallStateType.Connected -> {
|
||||
val reason = "currently busy"
|
||||
callWarningPrint(reason)
|
||||
completion?.invoke(AGError(reason, -1))
|
||||
return
|
||||
}
|
||||
CallStateType.Prepared -> {
|
||||
}
|
||||
CallStateType.Failed, CallStateType.Idle -> {
|
||||
}
|
||||
}
|
||||
|
||||
val tag = UUID.randomUUID().toString()
|
||||
callPrint("prepareForCall[$tag]")
|
||||
this.prepareConfig = prepareConfig.cloneConfig()
|
||||
|
||||
leaveRTC()
|
||||
connectInfo.clean()
|
||||
|
||||
completion?.invoke(null)
|
||||
|
||||
// Different from iOS, Android adds TextureView rendering view to passed container first
|
||||
// 和iOS不同,Android先将渲染视图TextureView添加进传进来的容器
|
||||
setupTextureView()
|
||||
}
|
||||
private fun setupTextureView() {
|
||||
val prepareConfig = prepareConfig ?: return
|
||||
runOnUiThread {
|
||||
// Add remote rendering view
|
||||
// 添加远端渲染视图
|
||||
prepareConfig.remoteView?.let { remoteView ->
|
||||
(tempRemoteCanvasView.parent as? ViewGroup)?.let { parentView ->
|
||||
if (parentView != remoteView) {
|
||||
parentView.removeView(tempRemoteCanvasView)
|
||||
}
|
||||
}
|
||||
if (remoteView.indexOfChild(tempRemoteCanvasView) == -1) {
|
||||
tempRemoteCanvasView.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
remoteView.addView(tempRemoteCanvasView)
|
||||
} else {
|
||||
callPrint("remote canvas already added")
|
||||
}
|
||||
}
|
||||
// Add local rendering view
|
||||
// 添加本地渲染视图
|
||||
prepareConfig.localView?.let { localView ->
|
||||
(tempLocalCanvasView.parent as? ViewGroup)?.let { parentView ->
|
||||
if (parentView != localView) {
|
||||
parentView.removeView(tempLocalCanvasView)
|
||||
}
|
||||
}
|
||||
if (localView.indexOfChild(tempLocalCanvasView) == -1) {
|
||||
tempLocalCanvasView.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
localView.addView(tempLocalCanvasView)
|
||||
} else {
|
||||
callPrint("local canvas already added")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun deinitialize() {
|
||||
updateAndNotifyState(CallStateType.Idle)
|
||||
notifyEvent(CallEvent.Deinitialize)
|
||||
reporter = null
|
||||
}
|
||||
|
||||
// Set remote view
|
||||
// 设置远端画面
|
||||
private fun setupRemoteVideo(uid: Int) {
|
||||
if (connectInfo.callType == CallType.Audio) return
|
||||
|
||||
val engine = config?.rtcEngine ?: return
|
||||
val connection = rtcConnection ?: run {
|
||||
callWarningPrint("_setupRemoteVideo fail: connection or engine is empty")
|
||||
return
|
||||
}
|
||||
val videoCanvas = VideoCanvas(tempRemoteCanvasView)
|
||||
videoCanvas.uid = uid
|
||||
videoCanvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN
|
||||
videoCanvas.mirrorMode = Constants.VIDEO_MIRROR_MODE_AUTO
|
||||
val ret = engine.setupRemoteVideoEx(videoCanvas, connection)
|
||||
callPrint("_setupRemoteVideo ret: $ret, channelId: ${connection.channelId}, uid: $uid")
|
||||
}
|
||||
|
||||
private fun removeRemoteVideo(uid: Int) {
|
||||
val engine = config?.rtcEngine ?: return
|
||||
val connection = rtcConnection ?: run {
|
||||
callWarningPrint("_setupRemoteVideo fail: connection or engine is empty")
|
||||
return
|
||||
}
|
||||
val videoCanvas = VideoCanvas(null)
|
||||
videoCanvas.uid = uid
|
||||
val ret = engine.setupRemoteVideoEx(videoCanvas, connection)
|
||||
callPrint("_setupRemoteVideo ret: $ret, channelId: ${connection.channelId}, uid: $uid")
|
||||
|
||||
(tempRemoteCanvasView.parent as? ViewGroup)?.removeView(tempRemoteCanvasView)
|
||||
tempRemoteCanvasView = TextureView(context)
|
||||
|
||||
// Add remote rendering view
|
||||
// 添加远端渲染视图
|
||||
prepareConfig?.remoteView?.let { remoteView ->
|
||||
if (remoteView.indexOfChild(tempRemoteCanvasView) == -1) {
|
||||
tempRemoteCanvasView.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
remoteView.addView(tempRemoteCanvasView)
|
||||
} else {
|
||||
callWarningPrint("remote view not found in connected state!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupLocalVideo() {
|
||||
if (connectInfo.callType == CallType.Audio) return
|
||||
val engine = config?.rtcEngine ?: run {
|
||||
callWarningPrint("_setupLocalVideo fail: engine is empty")
|
||||
return
|
||||
}
|
||||
config?.rtcEngine?.addHandler(localFrameProxy)
|
||||
|
||||
val videoCanvas = VideoCanvas(tempLocalCanvasView)
|
||||
videoCanvas.setupMode = VideoCanvas.VIEW_SETUP_MODE_ADD
|
||||
videoCanvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN
|
||||
videoCanvas.mirrorMode = Constants.VIDEO_MIRROR_MODE_AUTO
|
||||
engine.setDefaultAudioRoutetoSpeakerphone(true)
|
||||
engine.setupLocalVideo(videoCanvas)
|
||||
val ret = engine.startPreview()
|
||||
if (ret != 0) {
|
||||
notifyErrorEvent(CallErrorEvent.StartCaptureFail, CallErrorCodeType.Rtc, ret, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeLocalVideo() {
|
||||
if (connectInfo.callType == CallType.Audio) return
|
||||
val engine = config?.rtcEngine ?: run {
|
||||
callWarningPrint("_setupLocalVideo fail: engine is empty")
|
||||
return
|
||||
}
|
||||
val canvas = VideoCanvas(tempLocalCanvasView)
|
||||
canvas.setupMode = VideoCanvas.VIEW_SETUP_MODE_REMOVE
|
||||
engine.setupLocalVideo(canvas)
|
||||
}
|
||||
|
||||
/// 判断当前加入的RTC频道和传入的房间id是否一致
|
||||
/// - Parameter roomId: <#roomId description#>
|
||||
/// - Returns: <#description#>
|
||||
private fun isCurrentRTCChannel(roomId: String): Boolean {
|
||||
return rtcConnection?.channelId == roomId
|
||||
}
|
||||
|
||||
/// 当前RTC频道是否加入成功或者正在加入中
|
||||
/// - Returns: <#description#>
|
||||
private fun isChannelJoinedOrJoining(): Boolean {
|
||||
return rtcConnection != null
|
||||
}
|
||||
|
||||
/// 是否初始化完成
|
||||
/// - Returns: <#description#>
|
||||
private fun isInitialized(): Boolean {
|
||||
return when (state) {
|
||||
CallStateType.Idle, CallStateType.Failed -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCallingUser(message: Map<String, Any>) : Boolean {
|
||||
val fromUserId = message[kFromUserId] as? Int ?: return false
|
||||
if (connectInfo.callingUserId != fromUserId) return false
|
||||
return true
|
||||
}
|
||||
|
||||
private fun joinRTCWithMediaOptions(roomId: String, completion: ((AGError?) -> Unit)) {
|
||||
if (!isCurrentRTCChannel(roomId)) {
|
||||
leaveRTC()
|
||||
}
|
||||
val isChannelJoinedOrJoining = isChannelJoinedOrJoining()
|
||||
if (isChannelJoinedOrJoining) {
|
||||
completion.invoke(null)
|
||||
} else {
|
||||
joinRTC(roomId){ error ->
|
||||
completion.invoke(error)
|
||||
}
|
||||
}
|
||||
val publishVideo = connectInfo.callType != CallType.Audio
|
||||
val subscribeVideo = connectInfo.callType != CallType.Audio
|
||||
|
||||
updatePublishStatus(audioStatus = true, videoStatus = publishVideo)
|
||||
updateSubscribeStatus(audioStatus = true, videoStatus = subscribeVideo)
|
||||
|
||||
// Mute audio after joining channel, unmute after connecting
|
||||
// 加入频道后先静音,等connecting后才解除静音
|
||||
muteRemoteAudio(true)
|
||||
}
|
||||
|
||||
private fun joinRTCAsBroadcaster(roomId: String) {
|
||||
joinRTCWithMediaOptions(roomId) { error ->
|
||||
if (error != null) {
|
||||
notifyRtcOccurErrorEvent(error.code, error.msg)
|
||||
} else {
|
||||
notifyEvent(CallEvent.JoinRTCSuccessed)
|
||||
}
|
||||
}
|
||||
setupCanvas()
|
||||
}
|
||||
|
||||
private fun joinRTC(roomId: String, completion:((AGError?) -> Unit)?) {
|
||||
val config = this.config
|
||||
val rtcToken = prepareConfig?.rtcToken
|
||||
if (config == null || rtcToken == null) {
|
||||
completion?.invoke(AGError("config is empty", -1))
|
||||
return
|
||||
}
|
||||
val connection = RtcConnection(roomId, config.userId)
|
||||
val mediaOptions = ChannelMediaOptions()
|
||||
mediaOptions.publishCameraTrack = false
|
||||
mediaOptions.publishMicrophoneTrack = false
|
||||
mediaOptions.autoSubscribeAudio = false
|
||||
mediaOptions.autoSubscribeVideo = false
|
||||
mediaOptions.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER
|
||||
val ret: Int = config.rtcEngine.joinChannelEx(rtcToken, connection, mediaOptions, this)
|
||||
callPrint("joinRTC channel roomId: $roomId uid: ${config.userId} ret = $ret")
|
||||
rtcConnection = connection
|
||||
joinRtcCompletion = {
|
||||
completion?.invoke(null)
|
||||
}
|
||||
firstFrameCompletion = {
|
||||
connectInfo.isRetrieveFirstFrame = true
|
||||
notifyEvent(CallEvent.RecvRemoteFirstFrame)
|
||||
}
|
||||
if (ret != Constants.ERR_OK) {
|
||||
notifyRtcOccurErrorEvent(ret)
|
||||
}
|
||||
notifyEvent(CallEvent.JoinRTCStart)
|
||||
|
||||
reporter?.startDurationEvent(ApiCostEvent.FIRST_FRAME_ACTUAL)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update audio/video stream publishing status
|
||||
* 更新推送音视频流状态
|
||||
* @param audioStatus Whether to publish audio stream
|
||||
* 是否推送音频流
|
||||
* @param videoStatus Whether to publish video stream
|
||||
* 是否推送视频流
|
||||
*/
|
||||
private fun updatePublishStatus(audioStatus: Boolean, videoStatus: Boolean) {
|
||||
val config = config
|
||||
val connection = rtcConnection
|
||||
if (config == null || connection == null) { return}
|
||||
callPrint("updatePublishStatus, audioStatus$audioStatus videoStatus:$videoStatus")
|
||||
|
||||
config.rtcEngine.enableLocalAudio(audioStatus)
|
||||
config.rtcEngine.enableLocalVideo(videoStatus)
|
||||
|
||||
val mediaOptions = ChannelMediaOptions()
|
||||
mediaOptions.publishCameraTrack = videoStatus
|
||||
mediaOptions.publishMicrophoneTrack = audioStatus
|
||||
config.rtcEngine.updateChannelMediaOptionsEx(mediaOptions, connection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update audio/video stream subscription status
|
||||
* 更新音视频流订阅状态
|
||||
* @param audioStatus Audio stream subscription status
|
||||
* 音频流订阅状态
|
||||
* @param videoStatus Video stream subscription status
|
||||
* 视频流订阅状态
|
||||
*/
|
||||
private fun updateSubscribeStatus(audioStatus: Boolean, videoStatus: Boolean) {
|
||||
val config = config ?: run { return }
|
||||
val connection = rtcConnection ?: run { return }
|
||||
callPrint("updateSubscribeStatus, audioStatus$audioStatus, videoStatus:$videoStatus")
|
||||
val mediaOptions = ChannelMediaOptions()
|
||||
mediaOptions.autoSubscribeAudio = audioStatus
|
||||
mediaOptions.autoSubscribeVideo = videoStatus
|
||||
config.rtcEngine.updateChannelMediaOptionsEx(mediaOptions, connection)
|
||||
}
|
||||
|
||||
private fun muteRemoteAudio(isMute: Boolean) {
|
||||
val rtcEngine = config?.rtcEngine ?: return
|
||||
val connection = rtcConnection ?: return
|
||||
|
||||
val uid = connectInfo.callingUserId
|
||||
uid?.let { it ->
|
||||
callPrint("muteRemoteAudio: $isMute uid: $it channelId: ${connection.channelId}")
|
||||
rtcEngine.adjustUserPlaybackSignalVolumeEx(it, if (isMute) 0 else 100, connection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun leaveRTC() {
|
||||
joinRtcCompletion = null
|
||||
val connection = rtcConnection ?: run {
|
||||
//callWarningPrint("leave RTC channel failed, not joined the channel")
|
||||
return
|
||||
}
|
||||
cleanCanvas()
|
||||
updatePublishStatus(audioStatus = false, videoStatus = false)
|
||||
config?.rtcEngine?.stopPreview()
|
||||
val ret = config?.rtcEngine?.leaveChannelEx(connection)
|
||||
callPrint("leave RTC channel[${ret ?: -1}]")
|
||||
rtcConnection = null
|
||||
}
|
||||
|
||||
private fun setupCanvas() {
|
||||
setupLocalVideo()
|
||||
val callingUserId = connectInfo.callingUserId ?: run {
|
||||
callWarningPrint("setupCanvas fail: callingUserId == null")
|
||||
return
|
||||
}
|
||||
setupRemoteVideo(callingUserId)
|
||||
}
|
||||
|
||||
private fun cleanCanvas() {
|
||||
removeLocalVideo()
|
||||
val callingUserId = connectInfo.callingUserId ?: run {
|
||||
callWarningPrint("cleanCanvas fail: callingUserId == null")
|
||||
return
|
||||
}
|
||||
removeRemoteVideo(callingUserId)
|
||||
}
|
||||
|
||||
private fun reportCostEvent(type: CallConnectCostType) {
|
||||
val cost = getCost()
|
||||
connectInfo.callCostMap[type.value] = cost
|
||||
val ext = mapOf(
|
||||
"channelName" to (connectInfo.callingRoomId ?: ""),
|
||||
"callId" to connectInfo.callId,
|
||||
"userId" to (config?.userId ?: 0)
|
||||
)
|
||||
reporter?.reportCostEvent(type.value, cost.toInt(), ext)
|
||||
}
|
||||
|
||||
private fun reportMethod(event: String, ext: Map<String, Any>? = null) {
|
||||
val value = ext ?: mapOf()
|
||||
callPrint("reportMethod event: $event value: $value")
|
||||
var subEvent = event
|
||||
val range = event.indexOf("(")
|
||||
if (range != -1) {
|
||||
subEvent = event.substring(0, range)
|
||||
}
|
||||
|
||||
val extension = mapOf<String, Any>(
|
||||
"callId" to connectInfo.callId,
|
||||
"userId" to (config?.userId ?: 0)
|
||||
)
|
||||
reporter?.reportFuncEvent(
|
||||
name = subEvent,
|
||||
value = value,
|
||||
ext = extension
|
||||
)
|
||||
}
|
||||
|
||||
private fun reportCustomEvent(event: String, ext: Map<String, Any>) {
|
||||
callPrint("reportMethod event: $event value: $ext")
|
||||
reporter?.reportCustomEvent(
|
||||
name = event,
|
||||
ext = ext
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendMessage(
|
||||
userId: String,
|
||||
message: Map<String, Any>,
|
||||
completion: ((AGError?) -> Unit)?
|
||||
) {
|
||||
messageId += 1
|
||||
messageId %= Int.MAX_VALUE
|
||||
val map = message.toMutableMap()
|
||||
map[kMessageId] = messageId
|
||||
val jsonString = Gson().toJson(map).toString()
|
||||
config?.signalClient?.sendMessage(userId, jsonString, completion)
|
||||
}
|
||||
|
||||
//MARK: on Message
|
||||
fun processRespEvent(reason: CallAction, message: Map<String, Any>) {
|
||||
when (reason) {
|
||||
CallAction.Call -> onCall(message, CallType.Video)
|
||||
CallAction.AudioCall -> onCall(message, CallType.Audio)
|
||||
CallAction.CancelCall -> onCancel(message)
|
||||
CallAction.Reject -> onReject(message)
|
||||
CallAction.Accept -> onAccept(message)
|
||||
CallAction.Hangup -> onHangup(message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun _call(
|
||||
remoteUserId: Int,
|
||||
callType: CallType,
|
||||
callExtension: Map<String, Any>,
|
||||
completion: ((AGError?) -> Unit)?
|
||||
) {
|
||||
val fromRoomId = prepareConfig?.roomId
|
||||
val fromUserId = config?.userId
|
||||
if (fromRoomId == null || fromUserId == null) {
|
||||
val reason = "call fail! config or roomId is empty"
|
||||
completion?.invoke(AGError(reason, -1))
|
||||
callWarningPrint(reason)
|
||||
return
|
||||
}
|
||||
if (state != CallStateType.Prepared) {
|
||||
val reason = "call fail! state busy or not initialized"
|
||||
completion?.invoke(AGError(reason, -1))
|
||||
callWarningPrint(reason)
|
||||
return
|
||||
}
|
||||
//Send call message
|
||||
connectInfo.set(
|
||||
callType = callType,
|
||||
userId = remoteUserId,
|
||||
roomId = fromRoomId,
|
||||
callId = UUID.randomUUID().toString(),
|
||||
isLocalAccepted = true
|
||||
)
|
||||
|
||||
val message = callMessageDic(
|
||||
remoteUserId = remoteUserId,
|
||||
callType = callType,
|
||||
fromRoomId = fromRoomId,
|
||||
callExtension = callExtension
|
||||
)
|
||||
sendMessage(remoteUserId.toString(), message) { err ->
|
||||
completion?.invoke(err)
|
||||
if (err != null) {
|
||||
//updateAndNotifyState(CallStateType.Prepared, CallReason.MessageFailed, err.msg)
|
||||
notifySendMessageErrorEvent(err, "call fail: ")
|
||||
//return@sendMessage
|
||||
} else {
|
||||
notifyEvent(CallEvent.RemoteUserRecvCall)
|
||||
}
|
||||
}
|
||||
|
||||
val reason = if (callType == CallType.Video) CallStateReason.LocalVideoCall else CallStateReason.LocalAudioCall
|
||||
val event = if (callType == CallType.Video) CallEvent.LocalVideoCall else CallEvent.LocalAudioCall
|
||||
updateAndNotifyState(CallStateType.Calling, reason, eventInfo = message)
|
||||
notifyEvent(event)
|
||||
joinRTCAsBroadcaster(fromRoomId)
|
||||
}
|
||||
|
||||
private fun _cancelCall(message: Map<String, Any>? = null, cancelCallByInternal: Boolean = false, completion: ((AGError?) -> Unit)? = null) {
|
||||
val userId = connectInfo.callingUserId
|
||||
if (userId == null) {
|
||||
completion?.invoke(AGError("cancelCall fail! callingRoomId is empty", -1))
|
||||
callWarningPrint("cancelCall fail! callingRoomId is empty")
|
||||
return
|
||||
}
|
||||
val msg = message ?: cancelCallMessageDic(cancelCallByInternal)
|
||||
sendMessage(userId.toString(), msg) { err ->
|
||||
completion?.invoke(err)
|
||||
if (err != null) {
|
||||
notifySendMessageErrorEvent(err, "cancel call fail: ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun _reject(remoteUserId: Int, message: Map<String, Any>, completion: ((AGError?) -> Unit)? = null) {
|
||||
sendMessage(remoteUserId.toString(), message, completion)
|
||||
}
|
||||
|
||||
private fun _hangup(remoteUserId: Int, message: Map<String, Any>? = null, completion: ((AGError?) -> Unit)? = null) {
|
||||
sendMessage(remoteUserId.toString(), message ?: messageDic(CallAction.Hangup), completion)
|
||||
}
|
||||
|
||||
// Received call message
|
||||
// 收到呼叫消息
|
||||
fun onCall(message: Map<String, Any>, callType: CallType) {
|
||||
val fromRoomId = message[kFromRoomId] as String
|
||||
val fromUserId = message[kFromUserId] as Int
|
||||
val callId = message[kCallId] as String
|
||||
|
||||
var enableNotify = true
|
||||
when (state) {
|
||||
CallStateType.Idle, CallStateType.Failed -> {
|
||||
// not reachable
|
||||
// _reject(remoteUserId: fromUserId, reason: kRejectReasonCallBusy, true)
|
||||
return
|
||||
}
|
||||
CallStateType.Calling, CallStateType.Connecting, CallStateType.Connected -> {
|
||||
if ((connectInfo.callingUserId ?: 0) != fromUserId) {
|
||||
val reason = rejectMessageDic(kRejectReasonCallBusy, rejectByInternal = true)
|
||||
_reject(fromUserId, reason)
|
||||
return
|
||||
}
|
||||
if (state == CallStateType.Calling) {
|
||||
enableNotify = false
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
connectInfo.set(callType, fromUserId, fromRoomId, callId)
|
||||
defaultCalleeJoinRTCTiming = if (canJoinRtcOnCalling(eventInfo = message)) CalleeJoinRTCTiming.Calling else CalleeJoinRTCTiming.Accepted
|
||||
if (enableNotify) {
|
||||
val reason = if (callType == CallType.Video) CallStateReason.RemoteVideoCall else CallStateReason.RemoteAudioCall
|
||||
val event = if (callType == CallType.Video) CallEvent.RemoteVideoCall else CallEvent.RemoteAudioCall
|
||||
updateAndNotifyState(CallStateType.Calling, reason, eventInfo = message)
|
||||
notifyEvent(event)
|
||||
}
|
||||
callPrint("[calling]defaultCalleeJoinRTCTiming: ${defaultCalleeJoinRTCTiming.value}")
|
||||
if(defaultCalleeJoinRTCTiming == CalleeJoinRTCTiming.Calling) {
|
||||
joinRTCAsBroadcaster(fromRoomId)
|
||||
}
|
||||
|
||||
if (connectInfo.isLocalAccepted && prepareConfig?.firstFrameWaittingDisabled == true) {
|
||||
// If first frame is not associated, in show-to-1v1 scenario, auto-accept may occur, causing connected state before joining channel and unmute audio becomes invalid
|
||||
// 如果首帧不关联,在秀场转1v1场景下,可能会自动接受,会导致么有加频道前变成connected,unmute声音无效
|
||||
checkConnectedSuccess(CallStateReason.LocalAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCancel(message: Map<String, Any>) {
|
||||
// If the operation is not from the user who is currently calling, ignore it
|
||||
// 如果不是来自的正在呼叫的用户的操作,不处理
|
||||
if (!isCallingUser(message)) return
|
||||
|
||||
var stateReason: CallStateReason = CallStateReason.RemoteCancelled
|
||||
var callEvent: CallEvent = CallEvent.RemoteCancelled
|
||||
val cancelCallByInternal = message[kCancelCallByInternal] as? Int
|
||||
if (cancelCallByInternal == 1) {
|
||||
stateReason = CallStateReason.RemoteCallingTimeout
|
||||
callEvent = CallEvent.RemoteCallingTimeout
|
||||
}
|
||||
updateAndNotifyState(state = CallStateType.Prepared, stateReason = stateReason, eventInfo = message)
|
||||
notifyEvent(event = callEvent)
|
||||
}
|
||||
|
||||
private fun onReject(message: Map<String, Any>) {
|
||||
if (!isCallingUser(message)) return
|
||||
var stateReason: CallStateReason = CallStateReason.RemoteRejected
|
||||
var callEvent: CallEvent = CallEvent.RemoteRejected
|
||||
val rejectByInternal = message[kRejectByInternal]
|
||||
if (rejectByInternal == 1) {
|
||||
stateReason = CallStateReason.RemoteCallBusy
|
||||
callEvent = CallEvent.RemoteCallBusy
|
||||
}
|
||||
|
||||
updateAndNotifyState(CallStateType.Prepared, stateReason, eventInfo = message)
|
||||
notifyEvent(callEvent)
|
||||
}
|
||||
|
||||
private fun onAccept(message: Map<String, Any>) {
|
||||
// Must be in calling state and request must come from the calling user
|
||||
// 需要是calling状态,并且来自呼叫的用户的请求
|
||||
if (!isCallingUser(message) || state != CallStateType.Calling) return
|
||||
// Must be isLocalAccepted (initiated call or already accepted), otherwise considered not locally agreed
|
||||
// 并且是isLocalAccepted(发起呼叫或者已经accept过了),否则认为本地没有同意
|
||||
if (connectInfo.isLocalAccepted) {
|
||||
updateAndNotifyState(CallStateType.Connecting, CallStateReason.RemoteAccepted, eventInfo = message)
|
||||
}
|
||||
notifyEvent(CallEvent.RemoteAccepted)
|
||||
}
|
||||
|
||||
private fun onHangup(message: Map<String, Any>) {
|
||||
if (!isCallingUser(message)) return
|
||||
|
||||
updateAndNotifyState(CallStateType.Prepared, CallStateReason.RemoteHangup, eventInfo = message)
|
||||
notifyEvent(CallEvent.RemoteHangup)
|
||||
}
|
||||
|
||||
//MARK: CallApiProtocol
|
||||
override fun getCallId(): String {
|
||||
reportMethod("getCallId")
|
||||
return connectInfo.callId
|
||||
}
|
||||
|
||||
override fun initialize(config: CallConfig) {
|
||||
if (state != CallStateType.Idle) {
|
||||
callWarningPrint("must invoke 'deinitialize' to clean state")
|
||||
return
|
||||
}
|
||||
|
||||
reporter = APIReporter(APIType.CALL, kReportCategory, config.rtcEngine)
|
||||
reportMethod("initialize", mapOf("appId" to config.appId, "userId" to config.userId))
|
||||
this.config = config.cloneConfig()
|
||||
|
||||
// Video best practices
|
||||
// 视频最佳实践
|
||||
|
||||
// 3. API enable first frame acceleration rendering for audio and video
|
||||
// API 开启音视频首帧加速渲染
|
||||
// config.rtcEngine.enableInstantMediaRendering()
|
||||
|
||||
// 4. Enable first frame FEC through private parameters or configuration delivery
|
||||
// 私有参数或配置下发开启首帧 FEC
|
||||
config.rtcEngine.setParameters("{\"rtc.video.quickIntraHighFec\": true}")
|
||||
|
||||
// 5. Set AUT CC mode through private parameters or configuration delivery
|
||||
// 私有参数或配置下发设置 AUT CC mode
|
||||
config.rtcEngine.setParameters("{\"rtc.network.e2e_cc_mode\": 3}") //(Not needed for version 4.3.0 and later, default value changed to 3)
|
||||
//(4.3.0及以后版本不需要设置此项,默认值已改为3)
|
||||
|
||||
// 6. Set VQC resolution adjustment sensitivity through private parameters or configuration delivery
|
||||
// 私有参数或配置下发设置VQC分辨率调节的灵敏度
|
||||
config.rtcEngine.setParameters("{\"che.video.min_holdtime_auto_resize_zoomin\": 1000}")
|
||||
config.rtcEngine.setParameters("{\"che.video.min_holdtime_auto_resize_zoomout\": 1000}")
|
||||
}
|
||||
|
||||
override fun deinitialize(completion: (() -> Unit)) {
|
||||
reportMethod("deinitialize")
|
||||
when (state) {
|
||||
CallStateType.Calling -> {
|
||||
cancelCall { err ->
|
||||
completion.invoke()
|
||||
}
|
||||
deinitialize()
|
||||
}
|
||||
CallStateType.Connecting, CallStateType.Connected -> {
|
||||
val callingUserId = connectInfo.callingUserId ?: 0
|
||||
_hangup(callingUserId) { err ->
|
||||
completion.invoke()
|
||||
}
|
||||
deinitialize()
|
||||
}
|
||||
else -> {
|
||||
deinitialize()
|
||||
completion.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun renewToken(rtcToken: String) {
|
||||
reportMethod("renewToken")
|
||||
val roomId = prepareConfig?.roomId
|
||||
if (roomId == null) {
|
||||
callWarningPrint("renewToken failed, roomid missmatch")
|
||||
return
|
||||
}
|
||||
prepareConfig?.rtcToken = rtcToken
|
||||
callPrint("renewToken with roomId[$roomId]")
|
||||
val connection = rtcConnection ?: return
|
||||
val options = ChannelMediaOptions()
|
||||
options.token = rtcToken
|
||||
val ret = this.config?.rtcEngine?.updateChannelMediaOptionsEx(options, connection)
|
||||
callPrint("rtc[$roomId] renewToken ret = ${ret ?: -1}")
|
||||
}
|
||||
|
||||
override fun onFirstLocalVideoFramePublished(source: Constants.VideoSourceType?, elapsed: Int) {
|
||||
super.onFirstLocalVideoFramePublished(source, elapsed)
|
||||
notifyEvent(event = CallEvent.PublishFirstLocalVideoFrame, reasonString = "elapsed: ${elapsed}ms")
|
||||
}
|
||||
|
||||
override fun onFirstLocalVideoFrame(
|
||||
source: Constants.VideoSourceType?,
|
||||
width: Int,
|
||||
height: Int,
|
||||
elapsed: Int
|
||||
) {
|
||||
super.onFirstLocalVideoFrame(source, width, height, elapsed)
|
||||
notifyEvent(event = CallEvent.CaptureFirstLocalVideoFrame, reasonString = "elapsed: ${elapsed}ms")
|
||||
config?.rtcEngine?.removeHandler(localFrameProxy)
|
||||
}
|
||||
|
||||
override fun onFirstLocalAudioFramePublished(elapsed: Int) {
|
||||
super.onFirstLocalAudioFramePublished(elapsed)
|
||||
notifyEvent(CallEvent.PublishFirstLocalAudioFrame, reasonString = "elapsed: ${elapsed}ms")
|
||||
}
|
||||
|
||||
override fun onFirstRemoteAudioFrame(uid: Int, elapsed: Int) {
|
||||
super.onFirstRemoteAudioFrame(uid, elapsed)
|
||||
val channelId = prepareConfig?.roomId ?: return
|
||||
if (uid != connectInfo.callingUserId) return
|
||||
if (connectInfo.callType != CallType.Audio) return
|
||||
callPrint("firstRemoteAudioFrameOfUid, channelId: $channelId, uid: $uid")
|
||||
runOnUiThread {
|
||||
firstFrameCompletion?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override fun prepareForCall(prepareConfig: PrepareConfig, completion: ((AGError?) -> Unit)?) {
|
||||
reportMethod("prepareForCall", mapOf("roomId" to prepareConfig.roomId))
|
||||
_prepareForCall(prepareConfig) { err ->
|
||||
if (err != null) {
|
||||
updateAndNotifyState(CallStateType.Failed, CallStateReason.RtmSetupFailed, err.msg)
|
||||
completion?.invoke(err)
|
||||
return@_prepareForCall
|
||||
}
|
||||
updateAndNotifyState(CallStateType.Prepared)
|
||||
completion?.invoke(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addListener(listener: ICallApiListener) {
|
||||
reportMethod("addListener")
|
||||
if (delegates.contains(listener)) { return }
|
||||
delegates.add(listener)
|
||||
}
|
||||
|
||||
override fun removeListener(listener: ICallApiListener) {
|
||||
reportMethod("removeListener")
|
||||
delegates.remove(listener)
|
||||
}
|
||||
|
||||
override fun call(remoteUserId: Int, completion: ((AGError?) -> Unit)?) {
|
||||
_call(
|
||||
remoteUserId = remoteUserId,
|
||||
callType = CallType.Video,
|
||||
callExtension = emptyMap(),
|
||||
completion = completion
|
||||
)
|
||||
reportMethod("call", mapOf("remoteUserId" to remoteUserId))
|
||||
}
|
||||
|
||||
override fun call(
|
||||
remoteUserId: Int,
|
||||
callType: CallType,
|
||||
callExtension: Map<String, Any>,
|
||||
completion: ((AGError?) -> Unit)?
|
||||
) {
|
||||
_call(
|
||||
remoteUserId = remoteUserId,
|
||||
callType = callType,
|
||||
callExtension = callExtension,
|
||||
completion = completion
|
||||
)
|
||||
reportMethod("call", mapOf("remoteUserId" to remoteUserId, "callType" to callType.value, "callExtension" to callExtension))
|
||||
}
|
||||
|
||||
override fun cancelCall(completion: ((AGError?) -> Unit)?) {
|
||||
reportMethod("cancelCall")
|
||||
val message = messageDic(CallAction.CancelCall)
|
||||
_cancelCall(message, false, completion)
|
||||
updateAndNotifyState(CallStateType.Prepared, CallStateReason.LocalCancelled, eventInfo = message)
|
||||
notifyEvent(CallEvent.LocalCancelled)
|
||||
}
|
||||
|
||||
// Accept
|
||||
// 接受
|
||||
override fun accept(remoteUserId: Int, completion: ((AGError?) -> Unit)?) {
|
||||
reportMethod("accept", mapOf("remoteUserId" to remoteUserId))
|
||||
val fromUserId = config?.userId
|
||||
val roomId = connectInfo.callingRoomId
|
||||
if (fromUserId == null || roomId == null) {
|
||||
val errReason = "accept fail! current userId or roomId is empty"
|
||||
completion?.invoke(AGError(errReason, -1))
|
||||
return
|
||||
}
|
||||
// Check if state is calling, if it's prepared, it means the caller may have cancelled
|
||||
// 查询是否是calling状态,如果是prapared,表示可能被主叫取消了
|
||||
if (state != CallStateType.Calling) {
|
||||
val errReason = "accept fail! current state is $state not calling"
|
||||
completion?.invoke(AGError(errReason, -1))
|
||||
notifyEvent(CallEvent.StateMismatch, reasonString = errReason)
|
||||
return
|
||||
}
|
||||
|
||||
// By default, start capture and streaming within accept
|
||||
// accept内默认启动一次采集+推流
|
||||
rtcConnection?.let {
|
||||
if (connectInfo.callType != CallType.Audio) {
|
||||
config?.rtcEngine?.startPreview()
|
||||
}
|
||||
val mediaOptions = ChannelMediaOptions()
|
||||
mediaOptions.clientRoleType = Constants.CLIENT_ROLE_BROADCASTER
|
||||
mediaOptions.publishCameraTrack = true
|
||||
mediaOptions.publishMicrophoneTrack = true
|
||||
config?.rtcEngine?.updateChannelMediaOptionsEx(mediaOptions, it)
|
||||
}
|
||||
|
||||
connectInfo.set(userId = remoteUserId, roomId = roomId, isLocalAccepted = true)
|
||||
|
||||
// First check if the callee in presence is self, if so, don't send message again
|
||||
// 先查询presence里是不是正在呼叫的被叫是自己,如果是则不再发送消息
|
||||
val message = messageDic(CallAction.Accept)
|
||||
sendMessage(remoteUserId.toString(), message) { err ->
|
||||
completion?.invoke(err)
|
||||
if (err != null) {
|
||||
notifySendMessageErrorEvent(err, "accept fail: ")
|
||||
}
|
||||
}
|
||||
|
||||
callPrint("[accepted]defaultCalleeJoinRTCTiming: ${defaultCalleeJoinRTCTiming.value}")
|
||||
if (defaultCalleeJoinRTCTiming == CalleeJoinRTCTiming.Accepted) {
|
||||
joinRTCAsBroadcaster(roomId)
|
||||
}
|
||||
updateAndNotifyState(CallStateType.Connecting, CallStateReason.LocalAccepted, eventInfo = message)
|
||||
notifyEvent(CallEvent.LocalAccepted)
|
||||
}
|
||||
|
||||
// Reject call
|
||||
// 拒绝
|
||||
override fun reject(remoteUserId: Int, reason: String?, completion: ((AGError?) -> Unit)?) {
|
||||
reportMethod("reject", mapOf("remoteUserId" to remoteUserId, "reason" to (reason ?: "")))
|
||||
val message = rejectMessageDic(reason, rejectByInternal = false)
|
||||
_reject(remoteUserId, message) { error ->
|
||||
completion?.invoke(error)
|
||||
if (error != null) {
|
||||
notifySendMessageErrorEvent(error, "reject fail: ")
|
||||
}
|
||||
}
|
||||
updateAndNotifyState(CallStateType.Prepared, CallStateReason.LocalRejected, eventInfo = message)
|
||||
notifyEvent(CallEvent.LocalRejected)
|
||||
}
|
||||
|
||||
// Hang up call
|
||||
// 挂断
|
||||
override fun hangup(remoteUserId: Int, reason: String?, completion: ((AGError?) -> Unit)?) {
|
||||
reportMethod("hangup", mapOf("remoteUserId" to remoteUserId))
|
||||
val message = hangupMessageDic(reason)
|
||||
_hangup(remoteUserId, message = message) { error ->
|
||||
completion?.invoke(error)
|
||||
if (error != null) {
|
||||
notifySendMessageErrorEvent(error, "hangup fail: ")
|
||||
}
|
||||
}
|
||||
updateAndNotifyState(CallStateType.Prepared, CallStateReason.LocalHangup, eventInfo = message)
|
||||
notifyEvent(CallEvent.LocalHangup)
|
||||
}
|
||||
|
||||
//MARK: AgoraRtmClientDelegate
|
||||
override fun onTokenPrivilegeWillExpire(channelName: String?) {
|
||||
notifyTokenPrivilegeWillExpire()
|
||||
}
|
||||
// override fun onConnectionFail() {
|
||||
// updateAndNotifyState(CallStateType.Failed, CallStateReason.RtmLost)
|
||||
// notifyEvent(CallEvent.RtmLost)
|
||||
// }
|
||||
|
||||
override fun onMessageReceive(message: String) {
|
||||
callPrint("on event message: $message")
|
||||
val messageDic = jsonStringToMap(message)
|
||||
val messageAction = messageDic[kMessageAction] as? Int ?: 0
|
||||
val msgTs = messageDic[kMessageTs] as? Long
|
||||
val userId = messageDic[kFromUserId] as? Int
|
||||
val messageVersion = messageDic[kMessageVersion] as? String
|
||||
if (messageVersion == null || msgTs == null || userId == null) {
|
||||
callWarningPrint("fail to parse message: $message")
|
||||
return
|
||||
}
|
||||
//TODO: compatible other message version
|
||||
if (kCurrentMessageVersion != messageVersion) { return }
|
||||
CallAction.fromValue(messageAction)?.let {
|
||||
processRespEvent(it, messageDic)
|
||||
}
|
||||
}
|
||||
|
||||
override fun debugInfo(message: String, logLevel: Int) {
|
||||
callPrint(message)
|
||||
}
|
||||
|
||||
// IRtcEngineEventHandler
|
||||
override fun onConnectionStateChanged(state: Int, reason: Int) {
|
||||
callPrint("connectionChangedTo state: $state reason: $reason")
|
||||
}
|
||||
|
||||
override fun onUserJoined(uid: Int, elapsed: Int) {
|
||||
callPrint("didJoinedOfUid: $uid elapsed: $elapsed")
|
||||
if (connectInfo.callingUserId != uid) return
|
||||
runOnUiThread {
|
||||
notifyEvent(CallEvent.RemoteJoined)
|
||||
}
|
||||
}
|
||||
override fun onUserOffline(uid: Int, reason: Int) {
|
||||
callPrint("didOfflineOfUid: $uid, reason: $reason")
|
||||
if (connectInfo.callingUserId != uid) { return }
|
||||
runOnUiThread {
|
||||
notifyEvent(CallEvent.RemoteLeft, reasonCode = "$reason")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLeaveChannel(stats: RtcStats?) {
|
||||
callPrint("didLeaveChannel: $stats")
|
||||
isChannelJoined = false
|
||||
/*
|
||||
* Since leave RTC to didLeaveChannelWith is asynchronous
|
||||
* Setting rtcConnection = nil here will cause didLeaveChannelWith to incorrectly clear the rtc connection after join if joining immediately after leaving
|
||||
*
|
||||
* 由于leave rtc到didLeaveChannelWith是异步的
|
||||
* 这里rtcConnection = nil会导致leave之后马上join,didLeaveChannelWith会在join之后错误的置空了rtc connection
|
||||
*/
|
||||
//rtcConnection = null
|
||||
runOnUiThread {
|
||||
notifyEvent(CallEvent.LocalLeft)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
|
||||
callPrint("join RTC channel, didJoinChannel: $uid, channel: $channel elapsed: $elapsed")
|
||||
if (uid != config?.userId) { return }
|
||||
isChannelJoined = true
|
||||
runOnUiThread {
|
||||
joinRtcCompletion?.invoke(null)
|
||||
joinRtcCompletion = null
|
||||
notifyEvent(CallEvent.LocalJoined)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(err: Int) {
|
||||
runOnUiThread {
|
||||
notifyRtcOccurErrorEvent(err)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoteVideoStateChanged(uid: Int, state: Int, reason: Int, elapsed: Int) {
|
||||
super.onRemoteVideoStateChanged(uid, state, reason, elapsed)
|
||||
val channelId = prepareConfig?.roomId ?: ""
|
||||
if (uid != connectInfo.callingUserId) return
|
||||
callPrint("didLiveRtcRemoteVideoStateChanged channelId: $channelId/${connectInfo.callingRoomId ?: ""} uid: $uid/${connectInfo.callingUserId ?: 0} state: $state reason: $reason")
|
||||
if ((state == 2) && (reason == 6 || reason == 4 || reason == 3 )) {
|
||||
runOnUiThread {
|
||||
firstFrameCompletion?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun jsonStringToMap(jsonString: String): Map<String, Any> {
|
||||
val json = JSONObject(jsonString)
|
||||
val map = mutableMapOf<String, Any>()
|
||||
val keys = json.keys()
|
||||
while (keys.hasNext()) {
|
||||
val key = keys.next()
|
||||
map[key] = json.get(key)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
private fun callPrint(message: String, logLevel: CallLogLevel = CallLogLevel.Normal) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "[CallApi]$message");
|
||||
} else {
|
||||
delegates.forEach { listener ->
|
||||
listener.callDebugInfo(message, logLevel)
|
||||
}
|
||||
}
|
||||
reporter?.writeLog("[CallApi]$message", Constants.LOG_LEVEL_INFO)
|
||||
}
|
||||
|
||||
private fun callWarningPrint(message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "[CallApi]$message");
|
||||
} else {
|
||||
delegates.forEach { listener ->
|
||||
listener.callDebugInfo(message, CallLogLevel.Warning)
|
||||
}
|
||||
}
|
||||
reporter?.writeLog("[CallApi]$message", Constants.LOG_LEVEL_WARNING)
|
||||
}
|
||||
|
||||
private val mHandler = Handler(Looper.getMainLooper())
|
||||
private fun runOnUiThread(runnable: Runnable) {
|
||||
if (Thread.currentThread() == Looper.getMainLooper().thread) {
|
||||
runnable.run()
|
||||
} else {
|
||||
mHandler.post(runnable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
332
lib_callapi/src/main/java/io/agora/onetoone/CallApiProtocol.kt
Normal file
332
lib_callapi/src/main/java/io/agora/onetoone/CallApiProtocol.kt
Normal file
@@ -0,0 +1,332 @@
|
||||
package io.agora.onetoone
|
||||
|
||||
import android.view.ViewGroup
|
||||
import io.agora.onetoone.signalClient.ISignalClient
|
||||
import io.agora.rtc2.RtcEngineEx
|
||||
|
||||
open class CallConfig(
|
||||
// Agora App Id
|
||||
var appId: String = "",
|
||||
// User ID used for sending signaling messages
|
||||
var userId: Int = 0,
|
||||
// RTC engine instance
|
||||
var rtcEngine: RtcEngineEx,
|
||||
// ISignalClient instance
|
||||
var signalClient: ISignalClient
|
||||
){}
|
||||
open class PrepareConfig(
|
||||
// Own RTC channel name, used when calling remote user to join this RTC channel
|
||||
var roomId: String = "",
|
||||
// RTC token, needs to use universal token, channel name should be empty string when creating token
|
||||
var rtcToken: String = "",
|
||||
// Canvas for displaying local stream
|
||||
var localView: ViewGroup? = null,
|
||||
// Canvas for displaying remote stream
|
||||
var remoteView: ViewGroup? = null,
|
||||
// Call timeout duration in milliseconds, if set to 0 no internal timeout logic will be applied
|
||||
var callTimeoutMillisecond: Long = 15000L,
|
||||
// [Optional] User extension field, can be retrieved through kFromUserExtension field when receiving remote messages that change state (e.g. calling/connecting)
|
||||
var userExtension: Map<String, Any>? = null,
|
||||
// Whether to disable waiting for first frame in connected state, true: yes, caller considers call successful upon receiving accept message, callee considers call successful upon clicking accept.
|
||||
// Note: using this method may result in connection without audio/video permissions and no video display due to poor network, false: no, will wait for audio first frame (audio call) or video first frame (video call)
|
||||
var firstFrameWaittingDisabled: Boolean = false
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Call type
|
||||
*/
|
||||
enum class CallType(val value: Int) {
|
||||
Video(0),
|
||||
Audio(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Call state type
|
||||
*/
|
||||
enum class CallStateType(val value: Int) {
|
||||
Idle(0), // Idle
|
||||
Prepared(1), // 1v1 environment creation completed
|
||||
Calling(2), // In calling state
|
||||
Connecting(3), // In connecting state
|
||||
Connected(4), // In call
|
||||
Failed(10); // Error occurred
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: Int): CallStateType {
|
||||
return values().find { it.value == value } ?: Idle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Call state transition reason
|
||||
*/
|
||||
enum class CallStateReason(val value: Int) {
|
||||
None(0),
|
||||
JoinRTCFailed(1), // Failed to join RTC
|
||||
RtmSetupFailed(2), // Failed to setup RTM
|
||||
RtmSetupSuccessed(3), // Successfully setup RTM
|
||||
MessageFailed(4), // Message sending failed
|
||||
LocalRejected(5), // Local user rejected
|
||||
RemoteRejected(6), // Remote user rejected
|
||||
RemoteAccepted(7), // Remote user accepted
|
||||
LocalAccepted(8), // Local user accepted
|
||||
LocalHangup(9), // Local user hung up
|
||||
RemoteHangup(10), // Remote user hung up
|
||||
LocalCancelled(11), // Local user cancelled call
|
||||
RemoteCancelled(12), // Remote user cancelled call
|
||||
RecvRemoteFirstFrame(13), // Received remote first frame (video frame for video call, audio frame for audio call)
|
||||
CallingTimeout(14), // Call timeout
|
||||
CancelByCallerRecall(15), // Call cancelled due to same caller calling different channel
|
||||
RtmLost(16), // RTM connection timeout
|
||||
RemoteCallBusy(17), // Remote user busy
|
||||
RemoteCallingTimeout(18), // Remote call timeout
|
||||
LocalVideoCall(30), // Local initiated video call
|
||||
LocalAudioCall(31), // Local initiated audio call
|
||||
RemoteVideoCall(32), // Remote initiated video call
|
||||
RemoteAudioCall(33), // Remote initiated audio call
|
||||
}
|
||||
|
||||
/*
|
||||
* Call event
|
||||
*/
|
||||
enum class CallEvent(val value: Int) {
|
||||
None(0),
|
||||
Deinitialize(1), // Called deinitialize
|
||||
//MissingReceipts(2), // No message receipt received [Deprecated]
|
||||
CallingTimeout(3), // Call timeout
|
||||
RemoteCallingTimeout(4), // Remote call timeout
|
||||
JoinRTCSuccessed(5), // RTC joined successfully
|
||||
//RtmSetupFailed(6), // RTM setup failed [Deprecated, please use onCallErrorOccur(state: rtmSetupFail)]
|
||||
RtmSetupSuccessed(7), // RTM setup successfully
|
||||
//MessageFailed(8), // Message sending failed [Deprecated, please use onCallErrorOccur(state: sendMessageFail)]
|
||||
StateMismatch(9), // State transition exception
|
||||
JoinRTCStart(10), // Local user has joined RTC channel but not yet successful (JoinChannelEx called)
|
||||
RemoteUserRecvCall(99), // Caller call successful
|
||||
LocalRejected(100), // Local user rejected
|
||||
RemoteRejected(101), // Remote user rejected
|
||||
OnCalling(102), // Changed to calling state [2.1.0 deprecated, please refer to localVideoCall/localAudioCall/remoteVideoCall/remoteAudioCall]
|
||||
RemoteAccepted(103), // Remote user accepted
|
||||
LocalAccepted(104), // Local user accepted
|
||||
LocalHangup(105), // Local user hung up
|
||||
RemoteHangup(106), // Remote user hung up
|
||||
RemoteJoined(107), // Remote user joined RTC channel
|
||||
RemoteLeft(108), // Remote user left RTC channel, RTC channel (eventReason please refer to AgoraUserOfflineReason)
|
||||
LocalCancelled(109), // Local user cancelled call
|
||||
RemoteCancelled(110), // Remote user cancelled call
|
||||
LocalJoined(111), // Local user joined RTC channel
|
||||
LocalLeft(112), // Local user left RTC channel
|
||||
RecvRemoteFirstFrame(113), // Received remote first frame
|
||||
//CancelByCallerRecall(114), // Call cancelled due to same caller calling different channel [Deprecated]
|
||||
RtmLost(115), // RTM connection timeout
|
||||
//RtcOccurError(116), // RTC error occurred [Deprecated, please use onCallErrorOccur(state: rtcOccurError)]
|
||||
RemoteCallBusy(117), // Remote user busy
|
||||
//StartCaptureFail(118), // Start capture failed [Deprecated, please use onCallErrorOccur(state: startCaptureFail)]
|
||||
CaptureFirstLocalVideoFrame(119), // Captured first video frame
|
||||
PublishFirstLocalVideoFrame(120), // Published first video frame successfully
|
||||
PublishFirstLocalAudioFrame(130), // Published first audio frame successfully [2.1.0 supported]
|
||||
LocalVideoCall(140), // Local initiated video call
|
||||
LocalAudioCall(141), // Local initiated audio call
|
||||
RemoteVideoCall(142), // Remote initiated video call
|
||||
RemoteAudioCall(142), // Remote initiated audio call
|
||||
}
|
||||
|
||||
/*
|
||||
* Call error event
|
||||
*/
|
||||
enum class CallErrorEvent(val value: Int) {
|
||||
NormalError(0), // General error
|
||||
RtcOccurError(100), // RTC error occurred
|
||||
StartCaptureFail(110), // RTC start capture failed
|
||||
// RtmSetupFail(200), // RTM initialization failed [Deprecated, replaced by messageManager manually initializing]
|
||||
SendMessageFail(210) // Message error, if using CallRtmMessageManager, it is AgoraRtmErrorCode, for custom channel, it is the corresponding error code of the channel
|
||||
}
|
||||
|
||||
/*
|
||||
* Call error event error code type
|
||||
*/
|
||||
enum class CallErrorCodeType(val value: Int) {
|
||||
Normal(0), // Business type error, temporarily no
|
||||
Rtc(1), // RTC error, using AgoraErrorCode
|
||||
Message(2) // RTM error, using AgoraRtmErrorCode
|
||||
}
|
||||
|
||||
/*
|
||||
* Log level
|
||||
*/
|
||||
enum class CallLogLevel(val value: Int) {
|
||||
Normal(0),
|
||||
Warning(1),
|
||||
Error(2),
|
||||
}
|
||||
|
||||
interface ICallApiListener {
|
||||
/**
|
||||
* State response callback
|
||||
* @param state State type
|
||||
* @param stateReason State transition reason
|
||||
* @param eventReason Event type description
|
||||
* @param eventInfo Extended information, different parameters for different event types, where key is "publisher" for the state change initiator id, empty means it's your own state change
|
||||
*/
|
||||
fun onCallStateChanged(state: CallStateType,
|
||||
stateReason: CallStateReason,
|
||||
eventReason: String,
|
||||
eventInfo: Map<String, Any>)
|
||||
|
||||
/**
|
||||
* Internal detailed event change callback
|
||||
* @param event Event
|
||||
* @param eventReason Event reason, default null, represents different meanings according to different events
|
||||
*/
|
||||
fun onCallEventChanged(event: CallEvent, eventReason: String?) {}
|
||||
|
||||
/**
|
||||
* Internal detailed event change callback
|
||||
* @param errorEvent Error event
|
||||
* @param errorType Error type
|
||||
* @param errorCode Error code
|
||||
* @param message Error message
|
||||
*/
|
||||
fun onCallError(errorEvent: CallErrorEvent,
|
||||
errorType: CallErrorCodeType,
|
||||
errorCode: Int,
|
||||
message: String?) {}
|
||||
|
||||
/**
|
||||
* Call start callback
|
||||
* @param roomId Call channel id
|
||||
* @param callerUserId Caller user id
|
||||
* @param currentUserId Current user id
|
||||
* @param timestamp Call start time, the difference from January 1, 1970, in ms
|
||||
*/
|
||||
fun onCallConnected(roomId: String,
|
||||
callUserId: Int,
|
||||
currentUserId: Int,
|
||||
timestamp: Long) {}
|
||||
|
||||
/**
|
||||
* Call end callback
|
||||
* @param roomId Call channel id
|
||||
* @param hangupUserId User id hung up
|
||||
* @param currentUserId Current user id
|
||||
* @param timestamp Call start time, the difference from January 1, 1970, in ms
|
||||
* @param duration Call duration, in ms
|
||||
*/
|
||||
fun onCallDisconnected(roomId: String,
|
||||
hangupUserId: Int,
|
||||
currentUserId: Int,
|
||||
timestamp: Long,
|
||||
duration: Long) {}
|
||||
|
||||
/**
|
||||
* When calling, determine whether to join RTC
|
||||
* @param eventInfo Extended information received when calling
|
||||
* @return true: can join, false: cannot join
|
||||
*/
|
||||
fun canJoinRtcOnCalling(eventInfo: Map<String, Any>) : Boolean?
|
||||
|
||||
/**
|
||||
* Token is about to expire (external token needs to be obtained and updated)
|
||||
*/
|
||||
fun tokenPrivilegeWillExpire() {}
|
||||
|
||||
/** Log callback
|
||||
* @param message: Log information
|
||||
* @param logLevel: Log priority: 0: normal log, 1: warning log, 2: error log
|
||||
*/
|
||||
fun callDebugInfo(message: String, logLevel: CallLogLevel) {}
|
||||
}
|
||||
|
||||
data class AGError(
|
||||
val msg: String,
|
||||
val code: Int
|
||||
)
|
||||
|
||||
interface ICallApi {
|
||||
|
||||
/**
|
||||
* Initialize configuration
|
||||
* @param config
|
||||
*/
|
||||
fun initialize(config: CallConfig)
|
||||
|
||||
/**
|
||||
* Release cache
|
||||
*/
|
||||
fun deinitialize(completion: (() -> Unit))
|
||||
|
||||
/**
|
||||
* Update your own RTC/RTM token
|
||||
*/
|
||||
fun renewToken(rtcToken: String)
|
||||
|
||||
/**
|
||||
* Prepare call environment, need to call successfully before making a call. If you need to change the RTC channel number of the call, you can repeat the call to ensure that it is called when the call is not in progress (not calling, connecting, connected)
|
||||
* @param prepareConfig
|
||||
* @param completion
|
||||
*/
|
||||
fun prepareForCall(prepareConfig: PrepareConfig, completion: ((AGError?) -> Unit)?)
|
||||
|
||||
/**
|
||||
* Add callback listener
|
||||
* @param listener
|
||||
*/
|
||||
fun addListener(listener: ICallApiListener)
|
||||
|
||||
/**
|
||||
* Remove callback listener
|
||||
* @param listener
|
||||
*/
|
||||
fun removeListener(listener: ICallApiListener)
|
||||
|
||||
/**
|
||||
* Initiate a call invitation, caller calls, establish RTC call connection with the remote user through the RTC channel number set by prepareForCall, default video call
|
||||
* @param remoteUserId Called user id
|
||||
* @param completion
|
||||
*/
|
||||
fun call(remoteUserId: Int, completion: ((AGError?) -> Unit)?)
|
||||
|
||||
/**
|
||||
* Initiate a call invitation, caller calls, establish RTC call connection with the remote user through the RTC channel number set by prepareForCall, default video call
|
||||
* @param remoteUserId Called user id
|
||||
* @param callType Call type: 0: video call, 1: audio call
|
||||
* @param callExtension Call extension field, can be retrieved through kFromUserExtension field when receiving remote messages that change state (e.g. calling/connecting)
|
||||
* @param completion
|
||||
*/
|
||||
fun call(remoteUserId: Int, callType: CallType, callExtension: Map<String, Any>, completion: ((AGError?) -> Unit)?)
|
||||
|
||||
/**
|
||||
* Cancel the ongoing call, caller calls
|
||||
* @param completion
|
||||
*/
|
||||
fun cancelCall(completion: ((AGError?) -> Unit)?)
|
||||
|
||||
/** Accept call, caller calls, caller will receive onAccept
|
||||
*
|
||||
* @param remoteUserId: Called user id
|
||||
* @param completion: <#completion description#>
|
||||
*/
|
||||
fun accept(remoteUserId: Int, completion: ((AGError?) -> Unit)?)
|
||||
|
||||
/**
|
||||
* Accept call, callee calls
|
||||
* @param remoteUserId Called user id
|
||||
* @param reason Rejection reason
|
||||
* @param completion
|
||||
*/
|
||||
fun reject(remoteUserId: Int, reason: String?, completion: ((AGError?) -> Unit)?)
|
||||
|
||||
/**
|
||||
* End call, both caller and callee can call
|
||||
* @param remoteUserId User id hung up
|
||||
* @param reason Hung up reason
|
||||
* @param completion
|
||||
*/
|
||||
fun hangup(remoteUserId: Int, reason: String?, completion: ((AGError?) -> Unit)?)
|
||||
|
||||
/**
|
||||
* Get the callId of the current call, callId is the unique identifier for the current call process, through which the Agora backend service can query the key node duration and state transition time nodes of the current call
|
||||
* @return callId, empty if it's not a call message
|
||||
*/
|
||||
fun getCallId(): String
|
||||
}
|
||||
136
lib_callapi/src/main/java/io/agora/onetoone/CallConnectInfo.kt
Normal file
136
lib_callapi/src/main/java/io/agora/onetoone/CallConnectInfo.kt
Normal file
@@ -0,0 +1,136 @@
|
||||
package io.agora.onetoone
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
enum class CallConnectCostType(val value: String) {
|
||||
// Caller's call successful, receiving call success indicates delivery to peer (callee)
|
||||
// 主叫呼叫成功,收到呼叫成功表示已经送达对端(被叫)
|
||||
RemoteUserRecvCall("remoteUserRecvCall"),
|
||||
|
||||
// Caller receives callee's call acceptance (onAccept)/callee clicks accept (accept)
|
||||
// 主叫收到被叫接受呼叫(onAccept)/被叫点击接受(accept)
|
||||
AcceptCall("acceptCall"),
|
||||
|
||||
// Local user joins channel
|
||||
// 本地用户加入频道
|
||||
LocalUserJoinChannel("localUserJoinChannel"),
|
||||
|
||||
// Local video first frame captured (video calls only)
|
||||
// 本地视频首帧被采集到(仅限视频呼叫)
|
||||
LocalFirstFrameDidCapture("localFirstFrameDidCapture"),
|
||||
|
||||
// Local user successfully pushes first frame (audio or video)
|
||||
// 本地用户推送首帧(音频或者视频)成功
|
||||
LocalFirstFrameDidPublish("localFirstFrameDidPublish"),
|
||||
|
||||
// Remote user joins channel
|
||||
// 远端用户加入频道
|
||||
RemoteUserJoinChannel("remoteUserJoinChannel"),
|
||||
|
||||
// Received first frame from peer
|
||||
// 收到对端首帧
|
||||
RecvFirstFrame("recvFirstFrame")
|
||||
}
|
||||
|
||||
class CallConnectInfo {
|
||||
// Time when video stream retrieval started
|
||||
// 开始获取视频流的时间
|
||||
var startRetrieveFirstFrame: Long? = null
|
||||
private set
|
||||
|
||||
// Whether remote video first frame has been retrieved
|
||||
// 是否获取到对端视频首帧
|
||||
var isRetrieveFirstFrame: Boolean = false
|
||||
|
||||
// Call type
|
||||
// 呼叫类型
|
||||
var callType: CallType = CallType.Video
|
||||
|
||||
// Call session ID
|
||||
// 呼叫的session id
|
||||
var callId: String = ""
|
||||
|
||||
// Channel name during call
|
||||
// 呼叫中的频道名
|
||||
var callingRoomId: String? = null
|
||||
|
||||
// Remote user during call
|
||||
// 呼叫中的远端用户
|
||||
var callingUserId: Int? = null
|
||||
|
||||
// Call start timestamp
|
||||
// 通话开始的时间
|
||||
var callConnectedTs: Long = 0
|
||||
|
||||
// Whether local user has accepted
|
||||
// 本地是否已经同意
|
||||
var isLocalAccepted: Boolean = false
|
||||
|
||||
// Call initiation time
|
||||
// 呼叫开始的时间
|
||||
private var _callTs: Long? = null
|
||||
var callTs: Long?
|
||||
get() = _callTs
|
||||
set(value) {
|
||||
_callTs = value
|
||||
callCostMap.clear()
|
||||
}
|
||||
|
||||
// Timer for call initiation, used for timeout handling
|
||||
// 发起呼叫的定时器,用来处理超时
|
||||
private val mHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
val callCostMap = mutableMapOf<String, Long>()
|
||||
|
||||
// Timer runnable for call initiation, used for timeout handling
|
||||
// 发起呼叫的定时器Runnable,用来处理超时
|
||||
private var timerRunnable: Runnable? = null
|
||||
set(value) {
|
||||
val oldVlaue = field
|
||||
field = value
|
||||
oldVlaue?.let { mHandler.removeCallbacks(it) }
|
||||
}
|
||||
|
||||
fun scheduledTimer(runnable: Runnable?, time: Long = 0) {
|
||||
val oldRunnable = timerRunnable
|
||||
if (oldRunnable != null) {
|
||||
mHandler.removeCallbacks(oldRunnable)
|
||||
timerRunnable = null
|
||||
}
|
||||
if (runnable != null) {
|
||||
timerRunnable = runnable
|
||||
mHandler.postDelayed(runnable, time)
|
||||
}
|
||||
}
|
||||
|
||||
fun clean() {
|
||||
scheduledTimer(null)
|
||||
callingRoomId = null
|
||||
callingUserId = null
|
||||
callTs = null
|
||||
callId = ""
|
||||
isRetrieveFirstFrame = false
|
||||
startRetrieveFirstFrame = null
|
||||
isLocalAccepted = false
|
||||
callConnectedTs = 0
|
||||
}
|
||||
|
||||
fun set(callType: CallType? = null, userId: Int, roomId: String, callId: String? = null, isLocalAccepted: Boolean = false) {
|
||||
if (callType != null) {
|
||||
this.callType = callType
|
||||
}
|
||||
this.callingUserId = userId
|
||||
this.callingRoomId = roomId
|
||||
this.isLocalAccepted = isLocalAccepted
|
||||
if (callId != null) {
|
||||
this.callId = callId
|
||||
}
|
||||
if (callTs == null) {
|
||||
callTs = System.currentTimeMillis()
|
||||
}
|
||||
if (startRetrieveFirstFrame == null) {
|
||||
startRetrieveFirstFrame = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
19
lib_callapi/src/main/java/io/agora/onetoone/CallProxy.kt
Normal file
19
lib_callapi/src/main/java/io/agora/onetoone/CallProxy.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package io.agora.onetoone
|
||||
|
||||
import io.agora.rtc2.Constants
|
||||
import io.agora.rtc2.IRtcEngineEventHandler
|
||||
|
||||
class CallLocalFirstFrameProxy(
|
||||
private val handler : IRtcEngineEventHandler? = null
|
||||
): IRtcEngineEventHandler() {
|
||||
|
||||
override fun onFirstLocalVideoFrame(
|
||||
source: Constants.VideoSourceType?,
|
||||
width: Int,
|
||||
height: Int,
|
||||
elapsed: Int
|
||||
) {
|
||||
super.onFirstLocalVideoFrame(source, width, height, elapsed)
|
||||
handler?.onFirstLocalVideoFrame(source, width, height, elapsed)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.agora.onetoone
|
||||
|
||||
data class CallReportInfo(
|
||||
var msgId: String,
|
||||
var category: String,
|
||||
var event: String,
|
||||
var label: String,
|
||||
var value: Int
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.agora.onetoone.extension
|
||||
|
||||
import io.agora.onetoone.CallConfig
|
||||
import io.agora.onetoone.PrepareConfig
|
||||
|
||||
|
||||
fun Long.getCostMilliseconds(): Long {
|
||||
return System.currentTimeMillis() - this
|
||||
}
|
||||
|
||||
fun PrepareConfig.cloneConfig(): PrepareConfig {
|
||||
return PrepareConfig(roomId, rtcToken, localView, remoteView, callTimeoutMillisecond, userExtension, firstFrameWaittingDisabled)
|
||||
}
|
||||
|
||||
fun CallConfig.cloneConfig(): CallConfig {
|
||||
return CallConfig(appId, userId, rtcEngine, signalClient)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package io.agora.onetoone.report
|
||||
|
||||
import android.util.Log
|
||||
import io.agora.rtc2.Constants
|
||||
import io.agora.rtc2.RtcEngine
|
||||
import org.json.JSONObject
|
||||
import java.util.HashMap
|
||||
|
||||
enum class APIType(val value: Int) {
|
||||
// Call Connection
|
||||
// 呼叫连麦
|
||||
CALL(2),
|
||||
}
|
||||
|
||||
enum class ApiEventType(val value: Int) {
|
||||
API(0),
|
||||
COST(1),
|
||||
CUSTOM(2)
|
||||
}
|
||||
|
||||
object ApiEventKey {
|
||||
const val TYPE = "type"
|
||||
const val DESC = "desc"
|
||||
const val API_VALUE = "apiValue"
|
||||
const val TIMESTAMP = "ts"
|
||||
const val EXT = "ext"
|
||||
}
|
||||
|
||||
object ApiCostEvent {
|
||||
// Channel Usage Time
|
||||
// 频道使用耗时
|
||||
const val CHANNEL_USAGE = "channelUsage"
|
||||
// Actual First Frame Time
|
||||
// 首帧实际耗时
|
||||
const val FIRST_FRAME_ACTUAL = "firstFrameActual"
|
||||
// Perceived First Frame Time
|
||||
// 首帧感官耗时
|
||||
const val FIRST_FRAME_PERCEIVED = "firstFramePerceived"
|
||||
}
|
||||
|
||||
class APIReporter(
|
||||
private val type: APIType,
|
||||
private val version: String,
|
||||
private val rtcEngine: RtcEngine
|
||||
) {
|
||||
private val tag = "APIReporter"
|
||||
private val messageId = "agora:scenarioAPI"
|
||||
private val durationEventStartMap = HashMap<String, Long>()
|
||||
private val category = "${type.value}_Android_$version"
|
||||
|
||||
init {
|
||||
configParameters()
|
||||
}
|
||||
|
||||
// Report regular scenario API
|
||||
// 上报普通场景化API
|
||||
fun reportFuncEvent(name: String, value: Map<String, Any>, ext: Map<String, Any>) {
|
||||
Log.d(tag, "reportFuncEvent: $name value: $value ext: $ext")
|
||||
val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.API.value, ApiEventKey.DESC to name)
|
||||
val labelMap = mapOf(ApiEventKey.API_VALUE to value, ApiEventKey.TIMESTAMP to getCurrentTs(), ApiEventKey.EXT to ext)
|
||||
val event = convertToJSONString(eventMap) ?: ""
|
||||
val label = convertToJSONString(labelMap) ?: ""
|
||||
rtcEngine.sendCustomReportMessage(messageId, category, event, label, 0)
|
||||
}
|
||||
|
||||
fun startDurationEvent(name: String) {
|
||||
Log.d(tag, "startDurationEvent: $name")
|
||||
durationEventStartMap[name] = getCurrentTs()
|
||||
}
|
||||
|
||||
fun endDurationEvent(name: String, ext: Map<String, Any>) {
|
||||
Log.d(tag, "endDurationEvent: $name")
|
||||
val beginTs = durationEventStartMap[name] ?: return
|
||||
durationEventStartMap.remove(name)
|
||||
val ts = getCurrentTs()
|
||||
val cost = (ts - beginTs).toInt()
|
||||
|
||||
innerReportCostEvent(ts, name, cost, ext)
|
||||
}
|
||||
|
||||
// Report timing information
|
||||
// 上报耗时打点信息
|
||||
fun reportCostEvent(name: String, cost: Int, ext: Map<String, Any>) {
|
||||
durationEventStartMap.remove(name)
|
||||
innerReportCostEvent(
|
||||
ts = getCurrentTs(),
|
||||
name = name,
|
||||
cost = cost,
|
||||
ext = ext
|
||||
)
|
||||
}
|
||||
|
||||
// Report custom information
|
||||
// 上报自定义信息
|
||||
fun reportCustomEvent(name: String, ext: Map<String, Any>) {
|
||||
Log.d(tag, "reportCustomEvent: $name ext: $ext")
|
||||
val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.CUSTOM.value, ApiEventKey.DESC to name)
|
||||
val labelMap = mapOf(ApiEventKey.TIMESTAMP to getCurrentTs(), ApiEventKey.EXT to ext)
|
||||
val event = convertToJSONString(eventMap) ?: ""
|
||||
val label = convertToJSONString(labelMap) ?: ""
|
||||
rtcEngine.sendCustomReportMessage(messageId, category, event, label, 0)
|
||||
}
|
||||
|
||||
fun writeLog(content: String, level: Int) {
|
||||
rtcEngine.writeLog(level, content)
|
||||
}
|
||||
|
||||
fun cleanCache() {
|
||||
durationEventStartMap.clear()
|
||||
}
|
||||
|
||||
// ---------------------- private ----------------------
|
||||
|
||||
private fun configParameters() {
|
||||
// For test environment
|
||||
// 测试环境使用
|
||||
//rtcEngine.setParameters("{\"rtc.qos_for_test_purpose\": true}") //测试环境使用
|
||||
// Data reporting
|
||||
// 数据上报
|
||||
rtcEngine.setParameters("{\"rtc.direct_send_custom_event\": true}")
|
||||
// Log writing
|
||||
// 日志写入
|
||||
rtcEngine.setParameters("{\"rtc.log_external_input\": true}")
|
||||
}
|
||||
|
||||
private fun getCurrentTs(): Long {
|
||||
return System.currentTimeMillis()
|
||||
}
|
||||
|
||||
private fun innerReportCostEvent(ts: Long, name: String, cost: Int, ext: Map<String, Any>) {
|
||||
Log.d(tag, "reportCostEvent: $name cost: $cost ms ext: $ext")
|
||||
writeLog("reportCostEvent: $name cost: $cost ms", Constants.LOG_LEVEL_INFO)
|
||||
val eventMap = mapOf(ApiEventKey.TYPE to ApiEventType.COST.value, ApiEventKey.DESC to name)
|
||||
val labelMap = mapOf(ApiEventKey.TIMESTAMP to ts, ApiEventKey.EXT to ext)
|
||||
val event = convertToJSONString(eventMap) ?: ""
|
||||
val label = convertToJSONString(labelMap) ?: ""
|
||||
rtcEngine.sendCustomReportMessage(messageId, category, event, label, cost)
|
||||
}
|
||||
|
||||
private fun convertToJSONString(dictionary: Map<String, Any>): String? {
|
||||
return try {
|
||||
JSONObject(dictionary).toString()
|
||||
} catch (e: Exception) {
|
||||
writeLog("[$tag]convert to json fail: $e dictionary: $dictionary", Constants.LOG_LEVEL_WARNING)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package io.agora.onetoone.signalClient
|
||||
|
||||
open class CallBaseSignalClient {
|
||||
val listeners = mutableListOf<ISignalClientListener>()
|
||||
|
||||
fun addListener(listener: ISignalClientListener) {
|
||||
if (listeners.contains(listener)) return
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: ISignalClientListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package io.agora.onetoone.signalClient
|
||||
|
||||
import android.util.Log
|
||||
import io.agora.onetoone.AGError
|
||||
import io.agora.rtm.*
|
||||
|
||||
interface ICallRtmManagerListener {
|
||||
/**
|
||||
* RTM connection successful
|
||||
*/
|
||||
fun onConnected()
|
||||
|
||||
/**
|
||||
* RTM connection disconnected
|
||||
*/
|
||||
fun onDisconnected()
|
||||
|
||||
/**
|
||||
* Token is about to expire, need to renew token
|
||||
*/
|
||||
fun onTokenPrivilegeWillExpire(channelName: String)
|
||||
}
|
||||
|
||||
fun createRtmManager(appId: String, userId: Int, client: RtmClient? = null) = CallRtmManager(appId, userId, client)
|
||||
|
||||
class CallRtmManager(
|
||||
private val appId: String = "",
|
||||
private val userId: Int = 0,
|
||||
private val client: RtmClient? = null
|
||||
): RtmEventListener {
|
||||
|
||||
private var rtmClient: RtmClient
|
||||
|
||||
var isConnected: Boolean = false
|
||||
|
||||
// Whether RTM has logged in
|
||||
var isLoginedRtm = false
|
||||
|
||||
// Whether the RTM is externally provided; if so, no need to manually logout
|
||||
private var isExternalRtmClient = false
|
||||
|
||||
private val listeners = mutableListOf<ICallRtmManagerListener>()
|
||||
|
||||
init {
|
||||
val rtm = client
|
||||
if (rtm != null) {
|
||||
// If an external rtmClient is provided, assume login is successful by default
|
||||
isLoginedRtm = true
|
||||
isExternalRtmClient = true
|
||||
rtmClient = rtm
|
||||
isConnected = true
|
||||
} else {
|
||||
rtmClient = createRtmClient()
|
||||
}
|
||||
rtmClient.addEventListener(this)
|
||||
callMessagePrint("init-- CallRtmManager")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the internal RTM instance
|
||||
*/
|
||||
fun getRtmClient() : RtmClient = rtmClient
|
||||
|
||||
/**
|
||||
* Login to RTM
|
||||
*/
|
||||
fun login(rtmToken: String, completion: (AGError?) -> Unit) {
|
||||
callMessagePrint("login")
|
||||
if (rtmToken.isEmpty() && !isExternalRtmClient) {
|
||||
val reason = "RTM Token is Empty"
|
||||
completion(AGError(reason, -1))
|
||||
return
|
||||
}
|
||||
val rtmClient = this.rtmClient
|
||||
if (!isLoginedRtm) {
|
||||
loginRTM(rtmClient, rtmToken) { err ->
|
||||
if (err != null) {
|
||||
val errorCode = RtmConstants.RtmErrorCode.getValue(err.errorCode)
|
||||
completion.invoke(AGError(err.errorReason, errorCode))
|
||||
return@loginRTM
|
||||
}
|
||||
completion.invoke(null)
|
||||
}
|
||||
} else {
|
||||
completion.invoke(null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout of RTM
|
||||
*/
|
||||
fun logout() {
|
||||
if (!isExternalRtmClient) {
|
||||
rtmClient.logout(object : ResultCallback<Void> {
|
||||
override fun onSuccess(responseInfo: Void?) {}
|
||||
override fun onFailure(errorInfo: ErrorInfo?) {}
|
||||
})
|
||||
RtmClient.release()
|
||||
isConnected = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the listener
|
||||
* @param listener ICallRtmManagerListener object
|
||||
*/
|
||||
fun addListener(listener: ICallRtmManagerListener) {
|
||||
if (listeners.contains(listener)) return
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the listener
|
||||
* @param listener ICallRtmManagerListener object
|
||||
*/
|
||||
fun removeListener(listener: ICallRtmManagerListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update RTM token
|
||||
* @param rtmToken New rtmToken
|
||||
*/
|
||||
fun renewToken(rtmToken: String) {
|
||||
if (!isLoginedRtm) {
|
||||
// Not logged in successfully, but automatic login is needed; there may be an initial token issue, reinitialize here
|
||||
callMessagePrint("renewToken need to reinit")
|
||||
rtmClient.logout(object : ResultCallback<Void> {
|
||||
override fun onSuccess(responseInfo: Void?) {}
|
||||
override fun onFailure(errorInfo: ErrorInfo?) {}
|
||||
})
|
||||
login(rtmToken) { }
|
||||
return
|
||||
}
|
||||
rtmClient.renewToken(rtmToken, object : ResultCallback<Void> {
|
||||
override fun onSuccess(responseInfo: Void?) {
|
||||
callMessagePrint("rtm renewToken")
|
||||
}
|
||||
override fun onFailure(errorInfo: ErrorInfo?) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------ RtmEventListener ------------------
|
||||
override fun onConnectionStateChanged(
|
||||
channelName: String?,
|
||||
state: RtmConstants.RtmConnectionState?,
|
||||
reason: RtmConstants.RtmConnectionChangeReason?
|
||||
) {
|
||||
super.onConnectionStateChanged(channelName, state, reason)
|
||||
callMessagePrint("rtm connectionStateChanged, channelName: $channelName, state: $state reason: $reason")
|
||||
channelName ?: return
|
||||
if (reason == RtmConstants.RtmConnectionChangeReason.TOKEN_EXPIRED) {
|
||||
listeners.forEach { it.onTokenPrivilegeWillExpire(channelName) }
|
||||
} else if (reason == RtmConstants.RtmConnectionChangeReason.LOST) {
|
||||
isConnected = false
|
||||
} else if (state == RtmConstants.RtmConnectionState.CONNECTED) {
|
||||
if (isConnected) return
|
||||
isConnected = true
|
||||
listeners.forEach { it.onConnected() }
|
||||
} else {
|
||||
if (!isConnected) return
|
||||
isConnected = false
|
||||
listeners.forEach { it.onDisconnected() }
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------ inner private ------------------
|
||||
|
||||
private fun createRtmClient(): RtmClient {
|
||||
val rtmConfig = RtmConfig.Builder(appId, userId.toString()).build()
|
||||
if (rtmConfig.userId.isEmpty()) {
|
||||
callMessagePrint("userId is empty", 2)
|
||||
}
|
||||
if (rtmConfig.appId.isEmpty()) {
|
||||
callMessagePrint("appId is empty", 2)
|
||||
}
|
||||
var rtmClient: RtmClient? = null
|
||||
try {
|
||||
rtmClient = RtmClient.create(rtmConfig)
|
||||
} catch (e: Exception) {
|
||||
callMessagePrint("create rtm client fail: ${e.message}", 2)
|
||||
}
|
||||
return rtmClient!!
|
||||
}
|
||||
|
||||
private fun loginRTM(rtmClient: RtmClient, token: String, completion: (ErrorInfo?) -> Unit) {
|
||||
if (isLoginedRtm) {
|
||||
completion(null)
|
||||
return
|
||||
}
|
||||
rtmClient.logout(object : ResultCallback<Void?> {
|
||||
override fun onSuccess(responseInfo: Void?) {}
|
||||
override fun onFailure(errorInfo: ErrorInfo?) {}
|
||||
})
|
||||
callMessagePrint("will login")
|
||||
rtmClient.login(token, object : ResultCallback<Void?> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
callMessagePrint("login completion")
|
||||
isLoginedRtm = true
|
||||
completion(null)
|
||||
}
|
||||
|
||||
override fun onFailure(p0: ErrorInfo?) {
|
||||
callMessagePrint("login completion: ${p0?.errorCode}")
|
||||
isLoginedRtm = false
|
||||
completion(p0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun callMessagePrint(message: String, logLevel: Int = 0) {
|
||||
val tag = "[MessageManager]"
|
||||
Log.d(tag, message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package io.agora.onetoone.signalClient
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import io.agora.onetoone.AGError
|
||||
import io.agora.onetoone.extension.getCostMilliseconds
|
||||
import io.agora.rtm.*
|
||||
|
||||
fun createRtmSignalClient(client: RtmClient) = CallRtmSignalClient(client)
|
||||
|
||||
class CallRtmSignalClient(
|
||||
client: RtmClient
|
||||
): ISignalClient, CallBaseSignalClient(), RtmEventListener {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CALL_RTM_MSG_MANAGER"
|
||||
}
|
||||
|
||||
private val mHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private var rtmClient: RtmClient
|
||||
|
||||
init {
|
||||
rtmClient = client
|
||||
rtmClient.addEventListener(this)
|
||||
|
||||
callMessagePrint("init-- CallMessageManager ")
|
||||
}
|
||||
|
||||
override fun sendMessage(
|
||||
userId: String,
|
||||
message: String,
|
||||
completion: ((AGError?) -> Unit)?
|
||||
) {
|
||||
if (userId.isEmpty() || userId == "0") {
|
||||
val errorStr = "sendMessage fail, invalid userId[$userId]"
|
||||
callMessagePrint(errorStr)
|
||||
completion?.invoke(AGError(errorStr, -1))
|
||||
return
|
||||
}
|
||||
innerSendMessage(userId, message, completion)
|
||||
}
|
||||
|
||||
// --------------- MARK: AgoraRtmClientDelegate -------------
|
||||
override fun onMessageEvent(event: MessageEvent?) {
|
||||
runOnUiThread {
|
||||
val message = event?.message?.data as? ByteArray ?: return@runOnUiThread
|
||||
val jsonString = String(message, Charsets.UTF_8)
|
||||
listeners.forEach {
|
||||
it.onMessageReceive(jsonString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------- inner private ---------------
|
||||
private fun innerSendMessage(userId: String, message: String, completion:((AGError?)->Unit)?) {
|
||||
if (userId.isEmpty()) {
|
||||
completion?.invoke(AGError("send message fail! userId is empty", -1))
|
||||
return
|
||||
}
|
||||
val options = PublishOptions()
|
||||
options.setChannelType(RtmConstants.RtmChannelType.USER)
|
||||
val startTime = System.currentTimeMillis()
|
||||
callMessagePrint("_sendMessage to '$userId', message: $message")
|
||||
rtmClient.publish(userId, message.toByteArray(), options, object : ResultCallback<Void> {
|
||||
override fun onSuccess(p0: Void?) {
|
||||
callMessagePrint("_sendMessage publish cost ${startTime.getCostMilliseconds()} ms")
|
||||
runOnUiThread { completion?.invoke(null) }
|
||||
}
|
||||
override fun onFailure(errorInfo: ErrorInfo) {
|
||||
val msg = errorInfo.errorReason
|
||||
val code = RtmConstants.RtmErrorCode.getValue(errorInfo.errorCode)
|
||||
callMessagePrint("_sendMessage fail: $msg cost: ${startTime.getCostMilliseconds()} ms", 1)
|
||||
runOnUiThread { completion?.invoke(AGError(msg, code)) }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun callMessagePrint(message: String, logLevel: Int = 0) {
|
||||
val tag = "[MessageManager]"
|
||||
listeners.forEach {
|
||||
it.debugInfo("$tag$message)", logLevel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runOnUiThread(runnable: Runnable) {
|
||||
if (Thread.currentThread() == Looper.getMainLooper().thread) {
|
||||
runnable.run()
|
||||
} else {
|
||||
mHandler.post(runnable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package io.agora.onetoone.signalClient
|
||||
|
||||
import io.agora.onetoone.AGError
|
||||
|
||||
/*
|
||||
* Signaling callback protocol
|
||||
*/
|
||||
interface ISignalClientListener {
|
||||
/**
|
||||
* Callback for receiving messages
|
||||
* @param message The content of the message
|
||||
*/
|
||||
fun onMessageReceive(message: String)
|
||||
|
||||
/**
|
||||
* Signaling log callback
|
||||
* @param message The content of the log message
|
||||
* @param logLevel The priority of the log
|
||||
*/
|
||||
fun debugInfo(message: String, logLevel: Int)
|
||||
}
|
||||
|
||||
/*
|
||||
* Abstract signaling protocol; can use a custom implementation for the information channel
|
||||
*/
|
||||
interface ISignalClient {
|
||||
/**
|
||||
* Send a message to the signaling system from CallApi
|
||||
* @param userId The target user's ID
|
||||
* @param message The message object
|
||||
* @param completion Completion callback
|
||||
*/
|
||||
fun sendMessage(userId: String, message: String, completion: ((AGError?) -> Unit)?)
|
||||
|
||||
/**
|
||||
* Register a callback for the signaling system
|
||||
* @param listener ISignalClientListener object
|
||||
*/
|
||||
fun addListener(listener: ISignalClientListener)
|
||||
|
||||
/**
|
||||
* Remove a callback for the signaling system
|
||||
* @param listener ISignalClientListener object
|
||||
*/
|
||||
fun removeListener(listener: ISignalClientListener)
|
||||
}
|
||||
Reference in New Issue
Block a user