关于android:RTC-脚手架的设计和实现

4次阅读

共计 5908 个字符,预计需要花费 15 分钟才能阅读完成。

图片起源:https://699pic.com/tupian-401…

作者:AirLand

什么是 RTC?

RTC 即 Real-Time Communication 的简称是一种给行业提供高并发、低延时、高清晦涩、安全可靠的全场景、全互动、全实时的音视频服务的终端服务。下面是比拟官网的解释,艰深的来讲就是一种可能实现一对一、多对多音视频通话等泛滥性能的服务。目前提供该项服务的服务商有很多例如:声网、云信、火山引擎、腾讯云等。

背景

目前云音乐旗下 APP 泛滥,其中波及到 RTC 业务的不在少数,例如:常见的音视频连麦、PK、派对房,1v1 聊天等。因为业务线不同,性能不同,开发者也不同,大家各写一套,一直的反复造轮子,因而为了防止反复的开发工作晋升开发效率,须要有一套通用的 RTC 框架。

设计思路

在讲具体的方案设计之前,先讲一下我的设计思路:

  1. 性能内聚 :须要将性能都封装在一个容器里,对外通过接口提供办法调用
  2. 业务隔离 :不同的业务须要有不同的性能容器
  3. 对立调用 :所有性能容器须要有对立的调用入口
  4. 状态保护 :须要对状态进行精准保护
  5. 切换无感 :进行性能容器切换时候,无感知
  6. 外围可控 :对外围链路可监控,故障预警

基于以上 6 点,大抵的架构设计如图所示,这里先不必深究图中的模块示意什么,前面会讲到,这里只是先理解一下大抵的架构:

接下来我就来讲讲具体的实现过程。

方案设计

前言:

RTC 的业务场景尽管很多,但实质上却相差无几,都是用户退出到一个独特的房间,而后在房间内进行实时的音视频通信。具体到理论我的项目中大抵又可分为两种:全场景 RTC 和局部场景 RTC。

  • 全场景 RTC:整个业务都是通过 RTC 技术实现例如:1v1 音视频通话、派对房等。
  • 局部场景 RTC:即整个业务链路中只有一部分应用了 RTC 技术,往往这种业务会波及到引擎的切换。

不论是哪一种场景,承载外围性能的引擎都是必不可少的,因而咱们首先就从引擎开始着手,另外为了不便形容,后续便将引擎对立称作 Player。

1、Player 的封装

在与 RTC 相关联的业务中会波及到不同类型的 Player,例如:主播开播(推流 Player),观众观看直播(拉流 Player)以及 RTC Player 等。它们的性能尽管各不相同,但用法却有相似之处,例如都有启动 start,终止 stop 等。因而咱们能够将不同的 Player 形象出一个独特的接口 IPlayer 相干代码如下:

interface IPlayer<DS : IDataSource, CB : ICallback> {fun start(ds: DS)

    fun stop()

    fun <T : Any> setParam(key: String, value: T?)

    ......
}

其中 IDataSourceICallback 别离是启动 Player 所须要的数据源和回调,前面的文章中也会屡次提到,特地是 IDataSource 它是 Player 启动的源头就好比打电话时的电话号码。

在这里遇到的一个问题点就是因为 Player 内聚了所有的性能除了有一些通用办法外,也有着属于本人特有的办法,例如:静音,音量调节等。这些办法泛滥而且各不相同无奈在 IPlayer 接口中全副列出,即便能全副列出,但随着业务的迭代 Player 中的办法必定会一直变动,不可能每更改一个办法就改一下接口,这显然不合乎程序设计准则。那么如何将不同的办法抽象化,让下层通过调用同一个办法来执行不同的操作呢?这里通过:

  fun <T : Any> setParam(key: String, value: T?)

来实现,其中 key 示意办法的惟一标记,value 示意办法的入参。这样下层只须要通过调用 setParam 传入相应的办法标记和办法入参即可调用到对应的办法了。那么如何做到呢?答案也很简略通过一个中间层建设起一一映射关系。然而 Player 的类型泛滥,要是每写一个 Player 都要写一个映射逻辑就太麻烦了。所以这里通过 APT 编译时注解再联合 [javapoet](https://github.com/square/jav…
) 主动生成这个中间层并给它命名为 xxxPlayerWrapper 其外部生成一个 convert 办法,在这个办法外部实现一一映射逻辑。接下来咱们看看具体实现过程:

  1. 首先定义了两个注解别离作用于具体的 Player 和对应的办法例如:

    @Retention(RetentionPolicy.CLASS)
    @Target({ElementType.TYPE})
    public @interface PlayerClass {
    }
    
    @Retention(RetentionPolicy.CLASS)
    @Target({ElementType.METHOD})
    public @interface PlayerMethod {String name();
    }
    
    @PlayerClass
    open class xxxPlayer : IPlayer<xxxDataSource, xxxCallback>() {@PlayerMethod(name = "key1")
     fun method1(v: String) {.... 具体实现}
    }
  2. 一一映射关系建设:

xxxPlayer 和 xxxPlayerWrapper 之间是一个相互依赖关系,互为彼此的成员变量。当调用 xxxPlayer 的接口办法 setParam(key: String, value: T?) 时,会间接调用到 xxxPlayerWrapper 的 convert 办法,convert 办法会依据 key 来找到其所对应的办法名,最初间接调用到 Player 的具体方法。

因为所有的 Player 都有这个逻辑因而能够将这部分再形象成一个 AbsPlayer:

abstract class AbsPlayer<DS : IDataSource, CB : ICallback>
    : IPlayer<DS, CB>{
    var dataSource: DS? = null
    private val wrapper by lazy {
        val ret = kotlin.runCatching {val clazz = Class.forName(this::class.java.canonicalName + "Wrapper")
            val signature = arrayOf(this::class.java)
            clazz.constructors.find {signature.contentEquals(it.parameterTypes)
            }?.newInstance(this) as? PlayerWrapper
        }
        ret.exceptionOrNull()?.printStackTrace()
        ret.getOrNull()}


    override fun <T : Any> setParam(key: String, value: T?) {wrapper?.convert(key, value)
    }
    //...... 省略其余无关代码
}

最初整个 Player 的类图如下所示:

这里咱们不关注 Player 的性能具体是如何实现的,比方如何推流,如何拉流,如何进行 RTC 等。毕竟每个我的项目底层所用的服务商 sdk 各不相同,技术实现也不同,因而这里咱们只从架构的层面去探讨。

2、Player 的切换

Player 的切换针对的就是局部场景 RTC,这里咱们引入 SwitchablePlayer 的概念专门用于此种场景,而其自身也继承自 AbsPlayer,具备 Player 的所有性能。只不过这些性能是通过装璜者模式由其外部真正的 Player 来实现,同时减少了 Switch 的能力。再讲到 Switch 能力之前先来思考几个问题。

  1. 何时触发 Switch?
  2. 如何进行 Switch?
  3. Switch 的指标对象 Player 从何而来?

第一个问题何时触发 Switch:咱们晓得只有触发 Switch 就意味着须要启动另外的 Player,而启动 Player 又须要下面提到的 IDataSource,因而咱们只须要判断启动 Player 所传入的 IDataSource 类型和以后 Player 的 IDataSource 类型是否雷同,如果不同便可触发。判断的具体逻辑是比照以后 Player 泛型参数的 IDataSource 类型(AbsPlayer<DS : IDataSource, CB : ICallback> 第一个范型参数 )和传入的 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 对应的流程如图所示:

在应用时调用者能够依据本人的业务定义相干 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 流程的封装须要搞清楚两件事件:

  1. RTC 的主体流程是怎么的
  2. 业务调用方须要的是什么,关注的又是什么

因为 RTC 的主体流程和日常打电话类似,所以笔者以此类比,这样大家更容易了解。下图所示即为整个通话过程。

搞清楚整个流程后,接下来就是搞清楚第二件事件,业务调用方须要的是什么,关注的又是什么。联合上图来看关注的大略有三点:

  • 第一就是须要具备拨打和挂断的入口;(Player 的 Start 和 Stop
  • 第二就是要能晓得以后的通话状态比方是否正在连通,是否曾经接通,是否通话完结;(Player 的 状态保护
  • 第三就是一些反馈比方对方未接通,对方不在服务区,手机号是空号等。(Player 的 外围事件回调即之前提到的 ICallback

而至于它是如何连通的,底层做了哪些操作,拨打电话的人对此毫不关心。基于上述咱们的整体功能设计所要关注的点就有了。

1、通过设计一个 manager 来治理 Player 并对外裸露 Start 和 Stop 办法。

2、对 Player 进行状态保护,并让其状态可被下层监听。

3、Player 的一些外围事件回调也可被下层监听。

其中第一点和第三点比较简单,这里就不做过多的赘述。第二点状态保护,笔者应用了 StateMachine 状态机来实现,在不同的状态执行不同的操作,同时每一种状态都对应一个状态码,下层能够通过监听状态码来感知状态变动。

状态码和外围事件的设置这里应用了 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 脚手架产生的背景,并以通俗易懂的形式一步步论述设计过程以及最终实现。在此期间发现问题,解决问题,引出思考。因为受限于篇幅,不能将每一个点都进行详尽的介绍,有趣味的同学如有疑难,能够留言,一起探讨学习。

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0