前言
最近工作上需求完结一个语音通话的 APP,在数据传输层运用网关的信道,通话的树立需求应用层自己完结,于是就有了这篇文章。由于涉及到工作上的一些事务逻辑,所以这儿只会介绍一些完结的思路和接口。废话不多说,直接开端!
协议挑选
了解到 Android 层相关的协议有这些:Voice over Internet Protocol (VoIP)、Session Initiation Protocol (SIP)、Real-time Transport Protocol (RTP)、Web Real-Time Communication (WebRTC)。了解了一圈下来,发现 SIP 协议比其他的愈加简略实用,Android 源码也有完结 SIP 协议的相关逻辑。可是咱们没办法直接运用,由于 Android 源码的 SIP 协议和 Android 底层 Service 进行通讯,而咱们现在是和网关进行通讯。所以接下来咱们自己完结一下。
SIP 协议在 Android 12 及以上版别处于抛弃状况,至于为什么被抛弃,Android 官方并没有清晰阐明。看了这篇 blog ,得到的解释大致是,各 APP 并没有运用到 Android 的 SIP 完结,而是运用自己界说的或许第三方的 SIP 完结,所以就停止了这部分代码的保护。
SIP 协议的简略了解
SIP 是一种应用层协议,用于树立通话双方的语音衔接,而其在传输层能够选用 TCP 或 UDP 。
如下是运用 SIP 协议进行一次简略通讯的流程,F1、F2 等表明的是通讯的过程。愈加详细的解释能够查看这个 链接:
能够看到有如下几种音讯类型:INVITE
,Trying
,Ringing
,OK
,ACK
,BYE
。
这几种音讯类型的作用如下:
-
INVITE
:用于树立会话的恳求。比方,在电话体系中,当一个用户测验拨打另一个用户的电话时,将会发送一个INVITE
音讯。 -
Trying
:预期呼应的音讯,指示用户署理服务器(UAS)已收到恳求并正在处理它,但还没有完结。这不是终究的呼应,仅仅一种暂时呼应,用于表明正在处理恳求。 -
Ringing
:这是另一种预期呼应,表明正在对呼叫的目标端进行正告,例如,使电话振铃或播映呼叫等待音乐。Ringing
音讯的呼应码是 180。 -
OK
:这是一种终究呼应,表明恳求现已被成功处理。关于INVITE
音讯,200OK
表明被呼叫方承受了呼叫;关于BYE
音讯,200OK
表明会话现已被成功完毕。 -
ACK
:这是一个特殊的恳求,用于承认接纳到了INVITE
恳求的终究呼应。比方,在电话体系中,当被呼叫方承受了呼叫并发送了 200OK
音讯后,呼叫方将会发送一个ACK
音讯来承认接纳到了这个呼应。 -
BYE
:这是用于完毕现已树立的会话恳求。在电话体系中,当一方想要挂断电话时,将会发送一个BYE
音讯。
当时事务场景下的 SIP 协议
详细到当时的事务场景,咱们经过网关作为中介来树立衔接,这是和通用的 SIP 协议不太一样的点。简略来说,咱们需求用网关来代替通用 SIP 协议的署理服务器。考虑之后,咱们能够得出如下的 SIP 协议时序图(不考虑异常情况,一次成功的通讯流程):
其实和通用的 SIP 协议还是有很多相似之处的,一次成功的通讯包含以下几个过程(当时场景为设备 A 约请设备 B 进行语音通讯):
- 设备 A 发送多条
INVITE
音讯用于树立会话 - 设备 B 接纳到
INVITE
音讯之后,立即回复RING
音讯,奉告设备 A 音讯已接纳到 - 设备 B 假如挑选接听,则发送
ACCEPT
音讯 - 设备 A 接纳到
ACCEPT
音讯之后立即回复PREPARE_CONFIRM
音讯给设备 B - 双方相互传递
VOICE_DATA
进行语音通讯 - 设备 A 此刻需求挂断电话时,向设备 B 发送
HANGUP_END
音讯
逻辑看起来非常完备,可是还有一些问题咱们需求进行了解和分析:
-
为什么需求有
RING
音讯?为了让设备 A 知道,对方现已开端预备和你打电话了,由于假如不发送
RING
音讯,那么就会发送拒绝接听或许正忙的音讯,撤销衔接的树立。 -
为什么需求有
PREPARE_CONFIRM
音讯?- 保证语音数据传输的时序问题。在有
PREPARE_CONFIRM
音讯时,A 在收到ACCEPT
音讯之后开端发送数据,B 在接纳到PREPARE_CONFIRM
音讯之后开端发送数据。 - 保证两者的状况同步。假如没有
PREPARE_CONFIRM
音讯,B 在点击接听之后开端发送数据,ACCEPT
音讯假如 A 没有收到,B 会一向处于接听中的状况。
- 保证语音数据传输的时序问题。在有
-
为什么
HANGUP_END
音讯不需求应对?这儿假如 A 向 B 发送
HANGUP_END
音讯,B 没有收到。A 会进入已挂断的状况,B 检测音频数据的码率,假如码率为 0 并持续一段时间,则主动挂断。所需能够省去HANGUP_END
音讯的应对。
SIP 协议的完结细节分析
经过如上的介绍,咱们大致能够了解到一些 SIP 协议的基本逻辑,可是假如需求真实完结还有一些细节问题需求处理,比方过错状况要怎么处理等等。要处理相关的细节,咱们得画出接纳方和发送方的状况图。实际上,状况图会比时序图稍微复杂一点。SIP 状况图:
在状况图中,咱们有以下几种元素:开端状况、完毕状况、发送音讯、接纳音讯、中间状况、同步逻辑、用户操作等。这儿由于接纳方和发送方的状况流通不太一样,所以咱们运用两个状况图进行展现。详细的流通逻辑能够结合时序图进行了解,这儿咱们先简略描述一下状况和音讯的类别。
状况的类别,这儿由于完毕的状况过多,咱们独自运用 enum class 表明:
object IDLE // 初始状况
object CALLING // 呼叫中
object RINGING // 响铃中
object PREPARE // 通话预备中
object CHATTING // 通话中
enum class ENDED { // 由于完毕的状况过多,独自运用 enum class 表明
HANGUP, // 手动完毕
OTHER_TERMINATED, // 对方停止对话衔接完毕
SELF_TERMINATED, // 自己停止对话衔接完毕
NETWORK_ERROR, // 网络过错完毕
RINGING_TIMEOUT, // 响铃超时完毕
CALLING_TIMEOUT, // 呼叫超时完毕
PREPARE_TIMEOUT, // 接听预备阶段超时完毕
OTHER_BUSY, // 对方正忙,忙线完毕
SELF_BUSY, // 自己正忙,忙线完毕
DATA_INTERRUPT, // 数据中止完毕
}
音讯的类别:
enum class MessageType(val code: Int) {
INVITE(101), // 约请通话
RING(102), // 约请通话呼叫应对
ACCEPT(200), // 承受通话约请
PREPARE_CONFIRM(201), // 承认承受通话约请
CONTINUE(202), // 持续通话
VOICE_DATA(300), // 传输音频数据
BUSY_END(401), // 用户正忙
TERMINATE_END(402), // 用户停止对话衔接的树立
HANGUP_END(403), // 用户挂断电话
TIMEOUT_END(404), // 超时完毕
DATA_INTERRUPT_END(405) // 数据中止完毕
}
如上就是协议层需求处理的细节,包括:状况的流通,音讯的处理及发送,将状况流通奉告感兴趣的模块等。
SIP 协议的详细完结
总算到了协议的完结环节,经过上小节的细节分析,咱们大致能够得到如下的完结思路:
- 协议模块需求拿到发送音讯的句柄,进行音讯的发送
- 收到相关音讯之后,协议模块运用状况形式进行状况的跳转操作,并经过 eventBus 告诉给关注状况流通的模块
- 若收到音讯后没有状况流通,但需求做相关操作(如发送音讯),或许状况流通由用户触发。此刻,经过调用协议模块的相关函数完结。如
start()
、hangUp()
办法等 - 需求在某个流通节点上进行操作的逻辑,经过阻拦器来完结对应的逻辑(如:接纳到
ACCEPT
音讯时,需求撤销对应的计数器)
第二点和第四点是比较关键的环节,咱们来详细看一下它们的接口界说。
状况流通的完结
这儿运用到了状况形式来完结协议状况的流通,咱们能够简略查看一下 SessionStatus
接口:
sealed interface SessionStatus {
fun transferStatus(messageType: MessageType): SessionStatus
fun canCreateNewReceiverSession(): Boolean
fun canCreateNewCallerSession(): Boolean
fun isActive(): Boolean
}
-
transferStatus(messageType: MessageType)
办法接纳音讯回来下一个的状况,如此来处理状况的流通。 -
canCreateNewReceiverSession()
当时状况(呼叫方/接纳方)是否能够创立新的接纳者会话 -
canCreateNewCallerSession()
当时状况(呼叫方/接纳方)是否能够创立新的发起者会话 -
isActive
是否为活泼的状况
如下是 RINGING
状况的简略完结:
object RINGING: SessionStatus {
override fun transferStatus(messageType: MessageType): SessionStatus {
return when (messageType) {
MessageType.ACCEPT -> CHATTING
MessageType.TIMEOUT_END -> ENDED.RINGING_TIMEOUT
MessageType.TERMINATE_END -> ENDED.TERMINATED
else -> this
}
}
override fun canCreateNewReceiverSession() = false
override fun canCreateNewCallerSession() = true
override fun isActive() = true
}
canCreateNewCallerSession
和 canCreateNewReceiverSession
别离回来 true
和 false
,表明该状况能够拨打电话,可是不能接听电话。其他状况的流通能够参考状况图,逻辑类似,就不过多赘述了。
状况流通的阻拦
在协议的状况流经过程中,咱们还需求在状况流通的途径上履行一些操作。比方:从 RING
状况接纳到 ACCEPT
音讯跳转到 CHATTING
状况之后,咱们需求撤销 RINGING
状况的计时器(能够回顾一下细节分析时的状况图)。
简略来看,这些逻辑咱们能够写在 SessionStatus
的 transferStatus
办法里边,由于这儿处理了接纳音讯,进行状况跳转的逻辑。但这样会违背 SessionStatus
类的单一责任原则,SessionStatus
类应该只用来处理状况跳转。
一个比较合理的解决方案是咱们运用署理形式对协议流通的过程进行阻拦,将协议流经过程中的逻辑处理放在子类中完结。
如下是阻拦器的接口:
interface ISessionInterceptor {
val messageReceived: MessageType // 接纳到的音讯
val transferredState: SessionStatus // 流通到的状况
fun doIntercept(sessionData: SessionData)
}
从 RING
状况接纳到 ACCEPT
音讯跳转到 CHATTING
状况的详细完结:
class AcceptToChattingInterceptor: ISessionInterceptor {
override val messageReceived = MessageType.ACCEPT
override val transferredState = CHATTING
override fun doIntercept(sessionData: SessionData) {
// ... logic code
}
}
完结上是非常简略的,由于咱们有多个阻拦器,便利起见,咱们运用工厂来管理一下:
class SessionInterceptorFactory(
private val sessionData: SessionData
) {
private val interceptors = mutableListOf(
AcceptToChattingInterceptor(),
// ... other interceptor
).associateBy {
Pair(it.messageReceived, it.transferredState)
}
fun doIntercept(messageType: MessageType, sessionStatus: SessionStatus) {
interceptors[Pair(messageType, sessionStatus)]?.doIntercept(sessionData)
}
}
实际运用时,调用 doIntercept()
办法即可,详细运用哪个阻拦器由工厂类去分发。
interceptorFactory.doIntercept(messageType, newStatus)
实在是太高雅了!
多线程下竞争态问题解决
创立 Session
的时候是在非主线程中操作的,需求必定的时间,大约 50ms,没有限定详细的线程。这个时候会有多线程竞态问题需求处理。
问题项:
- 收到
INVITE
音讯,会创立RecieverSession
(接纳方的 Session),此刻假如收到了TERMINATE
音讯,那么协议将不会对此进行处理,导致接纳方不会中止 - 同一时间对多个设备进行拨号,会屡次创立
CallerSession
(拨打方 Session),会导致终究的Session
纷歧定是最后一个拨号的设备
这两者在本质上其实是同一个问题:多线程环境下,代码履行不同步
解决方案:
- 加锁。加锁是一种比较符合直觉的简略完结,但假如代码完结逻辑分布在多个类中,需求在多个类中进行加锁,其实不太便利代码的保护
- 运用
Handler
。将使命post
到Handler
中处理。处理音讯前开启同步屏障,暂停音讯的处理。音讯处理之后,封闭同步屏障,重新开端处理音讯 - 运用
Flow
。收到音讯时emit
到流中,运用collect
对音讯进行堵塞式处理
终究的完结:
终究挑选运用 Flow
来完结,由于在代码书写层面简略易懂,一起也非常便利后期的保护,拓展性也很强。
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val sipMessages = MutableSharedFlow<String?>()
override fun onVoiceDataReceive(dataJson: String?) {
scope.launch {
// produce
sipMessages.emit(dataJson)
}
}
fun registerProcessSipMessages() {
scope.launch {
sipMessages.map {
// do transform
it?.toBean<MessageWrapper>()
}.flowOn(Dispatchers.Default)
// consume
.collect { message ->
// process message
}
}
}
总结
在完结这个协议之前,个人感觉这项使命还是有必定难度的,由于要处理的细节点太多了,状况、音讯、操作都比较多,并且有点杂乱。假如这时候直接写代码的话,会让人感觉一头雾水。想到哪写到哪,写的有问题再回来修修补补,功率会比较低。
最好的姿势是:先把时序图、状况图、类图这些都画出来,把心里幻想的完结和逻辑详细化。写代码之前,先把大致主意和逻辑整理清楚。一边书写,一边发现问题,再一边更新。我想,这样操作完之后,会让复杂代码的书写变得愈加简单~
REFERENCE
RFC 3261: SIP: Session Initiation Protocol
SIP协议入门攻略
github.com/Tinder/Stat…