实现秒开秒切
秒开和秒切可以优化直播体验,让观众能够更快速地进入直播间并切换直播间,提升观看的连贯性和流畅度,详见什么是秒开秒切。本文介绍如何通过声网秒开秒切场景化 API(VideoLoader API)集成秒开秒切功能到实时音视频互动中。
示例项目
声网在 GitHub 上提供开源 VideoLoaderAPI
示例项目供你参考。
准备开发环境
前提条件
请确保已准备好符合要求的开发环境,详见准备开发环境。
集成 VideoLoader API
将声网 VideoLoader API 集成到你的项目中。添加 Android/lib_videoloaderapi/src/main/java/io/agora/videoloaderapi 目录下的文件到项目中,具体文件如下:
OnLiveRoomItemTouchEventHandler
文件OnPageScrollEventHandler.kt
文件OnPageScorllEventHandler2.kt
文件OnRoomListScrollEventHandler.kt
文件VideoLoader.kt
文件VideoLoaderImpl.kt
文件
为方便后续代码升级,请不要修改你添加的这些文件的名称和路径。
使用 VideoLoader API 前的 API 调用
本节展示在实现秒开、秒切功能前的准备工作。
1. 初始化 RtcEngine
调用 RTC SDK 中的 create
创建并初始化 RtcEngine
对象。
// 初始化声网 RtcEngine
private val mRtcEngine by lazy {
RtcEngine.create(RtcEngineConfig().apply {
mContext = applicationContext
// 传入你从控制台获取的声网项目的 APP ID
mAppId = BuildConfig.AGORA_APP_ID
mEventHandler = object : IRtcEngineEventHandler() {}
})
}
2. 使用通配 Token
你需要先在服务端生成 Token,再在后续客户端实现步骤中,传入 Token 参数进行鉴权。
为加快用户加入频道的速度,你可以使用通配 Token,详见使用通配 Token 最佳实践。
通配 Token 的使用会带来诸如“炸房”的风险,请结合具体需求决定是否使用通配 Token。
实现秒开
本节介绍如何在直播场景中实现观众观看直播时的丝滑秒开体验。
1. 实现直播间列表 UI 模块
实现一个展示直播间列表的 UI 模块,下面以一个 RecyclerView
举例。
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvRooms"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="7.5dp"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
android:clipToPadding="false"
android:clipChildren="false"
android:paddingBottom="50dp"
android:visibility="visible" />
2. 监听直播间列表的滑动事件
创建一个 OnRoomListScrollEventHandler
对象,并将其作为直播间列表滑动事件的代理注册给直播间列表的 UI。创建 OnRoomListScrollEventHandler
对象时,你需要在构造函数的参数中传入如下:
mRtcEngine
:之前初始化的RtcEngine
对象。localUid
:本地用户的uid
。
OnRoomListScrollEventHandler
对象监听到直播间列表的滑动事件后,会驱动 OnRoomListScrollEventHandler
类内部封装的最佳实践,并对屏幕内出现的直播间进行频道预加载(preloadChannel
)。
// 代码片段来源于 RoomListActivity
class RoomListActivity : AppCompatActivity() {
private val mBinding by lazy { ShowRoomListActivityBinding.inflate(LayoutInflater.from(this)) }
// 创建 OnRoomListScrollEventHandler 对象
private val onRoomListScrollEventHandler: OnRoomListScrollEventHandler = object : OnRoomListScrollEventHandler(mRtcEngine, RtcEngineInstance.localUid()) {}
// 业务服务模块
private val mService by lazy { ShowServiceProtocol.getImplInstance() }
override fun onCreate(savedInstanceState: Bundle?) {
// 将 OnRoomListScrollEventHandler 对象设置给直播间列表,监听滑动事件
mBinding.rvRooms.addOnScrollListener(onRoomListScrollEventHandler as OnRoomListScrollEventHandler)
// 获取房间列表
mService.getRoomList { roomList ->
// 获取房间列表后,将结果传递给 onRoomListScrollEventHandler 对象
onRoomListScrollEventHandler?.updateRoomList(roomList)
}
}
}
3. 监听单个直播间的触碰事件
创建 OnLiveRoomItemTouchEventHandler
对象,将该对象作为单个直播间的触碰事件的代理注册给单个直播间 UI。创建 OnLiveRoomItemTouchEventHandler
对象时,你需要在构造函数的参数中传入如下:
mRtcEngine
:之前初始化的RtcEngine
对象。roomInfo
:VideoLoader.RoomInfo
对象。
OnLiveRoomItemTouchEventHandler
对象监听到单个直播间的触碰事件后,会驱动 OnLiveRoomItemTouchEventHandler
类内部封装的最佳实践,并进入用户点击的直播间,此时你无需在业务层调用 joinChannel
或类似的用于加入频道的方法。
// 代码片段来源于 RoomListActivity
class RoomListActivity : AppCompatActivity() {
private val mBinding by lazy { ShowRoomListActivityBinding.inflate(LayoutInflater.from(this)) }
private lateinit var mRoomAdapter: BindingSingleAdapter<ShowRoomDetailModel, ShowRoomItemBinding>
override fun onCreate(savedInstanceState: Bundle?) {
mRoomAdapter = object : BindingSingleAdapter<ShowRoomDetailModel, ShowRoomItemBinding>() {
override fun onBindViewHolder(
holder: BindingViewHolder<ShowRoomItemBinding>,
position: Int
) {
// 单个直播间的 UI
val roomInfo = getItem(position) ?: return
// 创建 OnLiveRoomItemTouchEventHandler 对象
val onTouchEventHandler = object : OnLiveRoomItemTouchEventHandler(
// 之前初始化的 RtcEngine 对象
mRtcEngine,
// 房间信息
VideoLoader.RoomInfo(
roomInfo.roomId,
arrayListOf(
VideoLoader.AnchorInfo(
roomInfo.roomId,
roomInfo.ownerId.toInt(),
// 上文提到的 Token
// 示例代码中为通配 Token
RtcEngineInstance.generalToken()
)
)
),
RtcEngineInstance.localUid()
) {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
when (event!!.action) {
MotionEvent.ACTION_UP -> {
if (RtcEngineInstance.generalToken() != "") {
super.onTouch(v, event)
// 监听到 ACTION_UP 事件,进入直播间内页面
goLiveDetailActivity(list, position, roomInfo)
}
}
}
return true
}
// 通知你渲染主播画面,详见下文解释
override fun onRequireRenderVideo(info: VideoLoader.AnchorInfo): VideoLoader.VideoCanvasContainer? {
// 渲染主播画面的最佳时机
return ...
}
}
// 将 OnLiveRoomItemTouchEventHandler 对象设置给单个直播间,监听触碰事件
binding.root.setOnTouchListener(onTouchEventHandler)
}
}
mBinding.rvRooms.adapter = mRoomAdapter
}
}
收到 onRequireRenderVideo
事件的时刻是渲染主播画面的最佳时机。因此建议你提前创建好主播画面的容器,在收到 onRequireRenderVideo
事件通知后,将容器返回给 OnLiveRoomItemTouchEventHandler
对象,该对象会自动将主播画面添加并渲染在该容器上。
实现秒切
本节介绍如何在直播场景内实现观众秒速切换直播间观看直播。
1. 实现直播间滑动切换 UI 模块
实现一个直播间列表的滑动切换模块,下面以 ViewPager2
为例。
<androidx.viewpager2.widget.ViewPager2 xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewPager2"
android:orientation="vertical"
android:layout_width="match_parent"
android:overScrollMode="never"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:layout_scrollEffect="none"
android:layout_height="match_parent" />
2. 监听直播间的切换事件
创建一个 OnPageScrollEventHandler
对象,并将其注册为直播间 ViewPager2
的滑动事件代理。创建 OnPageScrollEventHandler
对象时,你需要在构造函数的参数中传入如下:
mRtcEngine
:之前初始化的RtcEngine
对象。localUid
:本地用户的uid
。needPreJoin
:设置是否需要提前加入所处直播间的上下两个直播间。如果设为是(true
),则会带来更好的秒切效果,但也会增加费用。sliceMode
:切换直播间出图的时机。
OnPageScrollEventHandler
对象监听到直播间 ViewPager2
的滑动事件后,会驱动 OnPageScrollEventHandler
类内部封装的最佳实践,并在最佳时机对不同直播间的音视频订阅行为进行切换。OnPageScrollEventHandler
对象会触发对应位置(position
)的直播间的如下事件:
onPageStartLoading
:刚开始加载/显示直播间:onPageLoaded
:直播间加载/显示完毕onPageLeft
:已离开直播间onRequireRenderVideo
:对应直播间主播视图的最佳渲染时机。此时你必须传入对应直播间的主播画面的容器,OnPageScrollEventHandler
对象会将主播画面添加并渲染在该容器上。
// 代码片段来源于 LiveViewPagerActivity
class LiveViewPagerActivity : AppCompatActivity() {
private val mBinding by lazy { LiveViewPagerActivityBinding.inflate(LayoutInflater.from(this)) }
// 使用一个循环数组储存可以切换的直播间列表
private val vpFragments = SparseArray<LiveViewPagerFragment>()
// 创建 OnPageScrollEventHandler 对象
private var onPageScrollEventHandler: OnPageScrollEventHandler? = object : OnPageScrollEventHandler(
// 之前初始化的 RtcEngine 对象
RtcEngineInstance.rtcEngine,
// 本地用户的 uid
RtcEngineInstance.localUid(),
// 设置是否提前加入所处直播间的上下两个直播间
// 如果是(true),则会带来更好的秒切效果,但是会增加费用
needPreJoin,
// 通过 onPageScrollStateChanged 事件,切换直播间出图的时机
sliceMode
) {
override fun onPageScrollStateChanged(state: Int) {
when (state) {
ViewPager2.SCROLL_STATE_SETTLING -> binding.viewPager2.isUserInputEnabled = false
ViewPager2.SCROLL_STATE_IDLE -> binding.viewPager2.isUserInputEnabled = true
}
super.onPageScrollStateChanged(state)
}
override fun onPageStartLoading(position: Int) {
// 通知对应 position 的直播间开始显示
vpFragments[position]?.startLoadPageSafely()
}
override fun onPageLoaded(position: Int) {
// 通知对应 position 的直播间已经显示完毕
vpFragments[position]?.onPageLoaded()
}
override fun onPageLeft(position: Int) {
// 通知对应 position 的直播间已经离开
vpFragments[position]?.stopLoadPage(true)
}
override fun onRequireRenderVideo(position: Int, info: VideoLoader.AnchorInfo): VideoLoader.VideoCanvasContainer? {
// 对应直播间主播画面的最佳渲染时机,通知对应 position 的直播间返回对应主播画面的容器
return vpFragments[position]?.initAnchorVideoView(info)
}
}
// Activity 被创建时执行的操作,详见下文
override fun onCreate(savedInstanceState: Bundle?) {
// 待补充
...
}
}
你需要在 Activity 创建时调用 updateRoomList
将初始的直播间列表信息传给 onPageScrollEventHandler
对象;同时在 FragmentStateAdapter
的 createFragment
事件内调用 onRoomCreated
通知 onPageScrollEventHandler
对应的直播间被创建。因此,在 override fun onCreate(savedInstanceState: Bundle?) {
后补充代码,使其最终如下:
override fun onCreate(savedInstanceState: Bundle?) {
// 初始化的 Activity 时,需要将初始的直播间列表信息传给 onPageScrollEventHandler 对象
onPageScrollEventHandler?.updateRoomList(list)
// 1 代表 ViewPager2 将在内存中至少保留当前页面两侧各一个页面(Fragment)
binding.viewPager2.offscreenPageLimit = 1
// 创建 FragmentStateAdapter,管理代表直播间的页面
val fragmentAdapter = object : FragmentStateAdapter(this) {
// 如果 ViewPager2 是可滑动的,则代表容纳无限多个页面
// 否则,返回 1,只容纳一个页面
override fun getItemCount() = if (mScrollable) Int.MAX_VALUE else 1
// 为每个直播间创建一个 LiveViewPagerFragment
override fun createFragment(position: Int): Fragment {
val roomInfo = if (mScrollable) {
mRoomInfoList[position % mRoomInfoList.size]
} else {
mRoomInfoList[selectedRoomIndex]
}
return LiveViewPagerFragment.newInstance(
roomInfo,
onPageScrollEventHandler as OnPageScrollEventHandler, position
).apply {
// 将创建的 LiveViewPagerFragment 存储在 vpFragments 中
vpFragments.put(position, this)
// 创建直播间内的主播列表
val anchorList = arrayListOf(
VideoLoader.AnchorInfo(
roomInfo.roomId,
roomInfo.ownerId.toInt(),
RtcEngineInstance.generalToken()
)
)
// onRoomCreated 通知 onPageScrollEventHandler 对应的直播间已被创建
onPageScrollEventHandler?.onRoomCreated(
position,
VideoLoader.RoomInfo(roomInfo.roomId, anchorList),
position == binding.viewPager2.currentItem
)
}
}
}
binding.viewPager2.adapter = fragmentAdapter
// 设置用户是否可以手动滑动页面
// 一般的直播间场景中,观众可以手动滑动页面,主播不可手动滑动页面
binding.viewPager2.isUserInputEnabled = mScrollable
if (mScrollable) {
// 如果可以滑动
// 将 OnPageScrollEventHandler 对象设置给 ViewPager2,监听页面改变事件
// 并计算出最新位置
binding.viewPager2.registerOnPageChangeCallback(
onPageScrollEventHandler as OnPageChangeCallback
)
binding.viewPager2.setCurrentItem(
Int.MAX_VALUE / 2 - Int.MAX_VALUE / 2 % mRoomInfoList.size + selectedRoomIndex,
false
)
} else {
// 如果不可滑动
// 将当前位置设为 0
currLoadPosition = 0
}
}
使用 VideoLoader API 后释放资源
使用声网秒开秒切场景化 API(VideoLoader API)后,在离开直播场景时,无需你在业务层主动调用 leaveChannel
,按照如下示例代码释放资源即可:
VideoLoader.getImplInstance(mRtcEngine).cleanCache()
VideoLoader.release()
RtcEngineEx.destroy()