由于所有的 Player 都有这个逻辑因此可以将这部分再抽象成一个 AbsPlayer:
abstract class AbsPlayerIDataSource, CB : ICallback>
: IPlayer
最后整个 Player 的类图如下所示:
image.png
这里我们不关注 Player 的功能具体是如何实现的,比如如何推流,如何拉流,如何进行 RTC 等。毕竟每个项目底层所用的服务商 sdk 各不相同,技术实现也不同,因此这里我们只从架构的层面去探讨。
2、Player 的切换
Player 的切换针对的就是部分场景 RTC,这里我们引入 SwitchablePlayer 的概念专门用于此种场景,而其本身也继承自 AbsPlayer, 具备 Player 的所有功能。只不过这些功能是通过装饰者模式由其内部真正的 Player 来实现,同时增加了 Switch 的能力。再讲到 Switch 能力之前先来思考几个问题。
- 何时触发 Switch?
- 如何进行 Switch?
- Switch 的目标对象 Player 从何而来?
第一个问题何时触发 Switch :我们知道只要触发 Switch 就意味着需要启动另外的 Player,而启动 Player 又需要上面提到的 IDataSource,因此我们只需要判断启动 Player 所传入的 IDataSource 类型和当前 Player 的 IDataSource 类型是否相同,如果不同便可触发。判断的具体逻辑是对比当前 Player 泛型参数的 IDataSource 类型( AbsPlayer第一个范型参数 )和传入的 IDataSource 类型来实现。
private fun isSourceMatch(
player: AbsPlayer<IDataSource, ICallback>?,
ds: IDataSource
): Boolean {
if (player == null) {
return false
} else {
val clazz = player::class.java
var type = getGenericSuperclass(clazz) ?: return false
while (Types.getRawType(type) != AbsPlayer::class.java) {
type = getGenericSuperclass(type) ?: return false
}
return if (type is ParameterizedType) {
val args = type.actualTypeArguments
if (args.isNullOrEmpty()) {
false
} else {
Types.getRawType(args[0]).isInstance(ds) && isSameSource(player, ds)
}
} else {
false
}
}
}
第二个问题如何进行 Switch :这个就比较简单了只需要停止掉当前的 Player 再启动目标 Player 即可。
第三个问题 Switch 的目标对象 Player 从何而来 :SwitchablePlayer 并不清楚业务需要哪些 Player ,只是对 Player 功能的一层包装以及维护 Switch 功能,因此具体的 Player 创建需要由业务层来实现, SwitchablePlayer 只提供一个获取 Player 的抽象方法例如:
abstract fun getPlayer(ds: IDataSource): AbsPlayer<out IDataSource, out ICallback>?
另外由于进行 Switch 的时候会停止掉当前的 Player,而被停止的 Player 是否能复用,如果能复用则可以将其缓存起来,下次使用优先从缓存中获得。整个SwitchablePlayer对应的流程如图所示:
image.png
在使用时调用者可以根据自己的业务定义相关 Player,例如在直播-> PK 的业务中,涉及到两个 Player 的切换即:LivePlayer 和 PKPlayer
class LivePKSwitchablePlayer : SwitchablePlayer(false) {
override fun getPlayer(ds: IDataSource): AbsPlayer<out IDataSource, out ICallback> {
return when (ds) {
is LiveDataSource -> {
LivePlayer()
}
is PKDataSource -> {
PKPlayer()
}
else -> LivePlayer()
}
}
}
3、流程封装
对于整个 RTC 流程的封装需要搞清楚两件事情:
- RTC 的主体流程是怎样的
- 业务调用方需要的是什么,关注的又是什么
由于 RTC 的主体流程和日常打电话相似,所以笔者以此类比,这样大家更容易理解。下图所示即为整个通话过程。
搞清楚整个流程后,接下来就是搞清楚第二件事情,业务调用方需要的是什么,关注的又是什么。结合上图来看关注的大概有三点:
- 第一就是需要具备拨打和挂断的入口;( Player 的 Start 和 Stop )
- 第二就是要能知道当前的通话状态比如是否正在连通,是否已经接通,是否通话结束;( Player 的 状态维护 )
- 第三就是一些反馈比如对方未接通,对方不在服务区,手机号是空号等。( Player 的 核心事件回调即之前提到的 ICallback )
而至于它是如何连通的,底层做了哪些操作,拨打电话的人对此毫不关心。基于上述我们的整体功能设计所要关注的点就有了。
1、通过设计一个 manager 来管理 Player 并对外暴露 Start 和 Stop 方法。
2、对 Player 进行状态维护,并让其状态可被上层监听。
3、Player 的一些核心事件回调也可被上层监听。
其中第一点和第三点比较简单,这里就不做过多的赘述。第二点状态维护,笔者使用了 StateMachine 状态机来实现,在不同的状态执行不同的操作,同时每一种状态都对应一个状态码,上层可以通过监听状态码来感知状态变化。
image.png
状态码和核心事件的设置这里使用了 LiveData 去处理
class RtcHolder : IRtcHolder {
private val _rtcState = MutableLiveData(RtcStatus.IDLE)
private val _rtcEvent = MutableLiveData(RtcEvent.IDLE)
val rtcState = _rtcState.distinctUntilChanged()
val rtcEvent = _rtcEvent.distinctUntilChanged()
private val callBack = object : IRtcCallBack {
override fun onCurrentStateChange(stateCode: Int) {
_rtcState.value = stateCode
}
override fun onEvent(eventCode: Int) {
_rtcEvent.value = eventCode
}
//......省略其他代码
}
init {
//上层状态监听
rtcState.observeForever {
when (it) {
RtcStatus.CONNECT_END -> {
ToastHelper.showToast("通话结束")
}
}
}
}
//......省略其他代码
}
到这里整个脚手架的方案设计就结束了,其中服务商 SDK 封装部分以及监控部分,笔者准备放到下期再来讲解。
总结
本文介绍了 RTC 脚手架产生的背景,并以通俗易懂的方式一步步阐述设计过程以及最终实现。在此期间发现问题,解决问题,引出思考。由于受限于篇幅,不能将每一个点都进行详尽的介绍,有兴趣的同学如有疑问,可以留言,一起探讨学习。
-
RTC
+关注
关注
2文章
527浏览量
66293 -
腾讯云
+关注
关注
0文章
208浏览量
16760
发布评论请先 登录
相关推荐
评论