第一次提交

This commit is contained in:
xuhuixiang
2025-08-08 15:05:12 +08:00
parent 3e7f1d9089
commit ad722304bf
795 changed files with 19580 additions and 9048 deletions

View 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>

View 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发起acceptB收到了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场景下可能会自动接受会导致么有加频道前变成connectedunmute声音无效
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之后马上joindidLeaveChannelWith会在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)
}
}
}

View 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
}

View 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()
}
}
}

View 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)
}
}

View File

@@ -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
)

View File

@@ -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)
}

View File

@@ -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
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}