AtomicXCore 提供了 CoHostStore 和 BattleStore 两个核心模块,分别用于处理跨房连线和 PK 对战。本文档将指导您如何组合使用这两个工具,来完成直播场景下连线到 PK 的完整流程。
核心场景
一次完整的“主播连线 PK”通常包含三个核心阶段,其整体流程如下:

实现步骤
步骤1:组件集成
步骤2:实现跨房连线
邀请方(主播 A)实现
1. 发起连线邀请
当主播A在界面上选择目标主播B并发起连线时,调用
requestHostConnection 方法。import androidx.appcompat.app.AppCompatActivityimport io.trtc.tuikit.atomicxcore.api.CompletionHandlerimport io.trtc.tuikit.atomicxcore.api.live.CoHostLayoutTemplateimport io.trtc.tuikit.atomicxcore.api.live.CoHostStore// 主播A的Activityclass AnchorAActivity : AppCompatActivity() {private val liveId = "主播A的房间ID"private val coHostStore = CoHostStore.create(liveId)// 用户点击"连线"按钮,并选择了主播Bfun inviteHostB(targetHostLiveId: String) {val layout = CoHostLayoutTemplate.HOST_DYNAMIC_GRID // 选择一个布局模板val timeout = 30 // 邀请超时时间(秒)coHostStore.requestHostConnection(targetHostLiveID = targetHostLiveId,layoutTemplate = layout,timeout = timeout,extraInfo = null,completion = object : CompletionHandler {override fun onSuccess() {println("连线邀请已发送,等待对方处理...")}override fun onFailure(code: Int, desc: String) {println("邀请发送失败: $desc")}})}}
2. 监听邀请结果
通过
CoHostListener 的回调方法,您可以接收到主播 B 的处理结果。import android.os.Bundleimport androidx.appcompat.app.AppCompatActivityimport io.trtc.tuikit.atomicxcore.api.live.CoHostListenerimport io.trtc.tuikit.atomicxcore.api.live.CoHostStoreimport io.trtc.tuikit.atomicxcore.api.live.SeatUserInfoclass AnchorAActivity : AppCompatActivity() {private val liveId = "主播A的房间ID"private val coHostStore = CoHostStore.create(liveId)private val coHostListener = object : CoHostListener() {override fun onCoHostRequestAccepted(invitee: SeatUserInfo) {println("主播 ${invitee.userName} 同意了你的连线邀请")}override fun onCoHostRequestRejected(invitee: SeatUserInfo) {println("主播 ${invitee.userName} 拒绝了你的邀请")}override fun onCoHostRequestTimeout(inviter: SeatUserInfo, invitee: SeatUserInfo) {println("邀请超时,对方未回应")}}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_anchor_a)// 初始化CoHostStorecoHostStore.addCoHostListener(coHostListener)}}
受邀方(主播 B)实现
1. 接收连线邀请
通过
CoHostListener,主播B可以监听到来自主播A的邀请。import androidx.appcompat.app.AppCompatActivityimport io.trtc.tuikit.atomicxcore.api.live.CoHostListenerimport io.trtc.tuikit.atomicxcore.api.live.CoHostStoreimport io.trtc.tuikit.atomicxcore.api.live.SeatUserInfo// 主播B的Activityclass AnchorBActivity : AppCompatActivity() {private val liveId = "主播B的房间ID"private val coHostStore = CoHostStore.create(liveId)private val coHostListener = object : CoHostListener() {override fun onCoHostRequestReceived(inviter: SeatUserInfo, extensionInfo: String) {println("收到主播 ${inviter.userName} 的连线邀请")// 显示邀请对话框,响应联线邀请// showInvitationDialog(inviter)}}}
2. 响应连线邀请
当主播B在弹出的对话框中做出选择后,调用相应的方法。
// AnchorBActivity 的一部分fun acceptInvitation(fromHostLiveId: String) {coHostStore.acceptHostConnection(fromHostLiveID = fromHostLiveId, completion = null)}fun rejectInvitation(fromHostLiveId: String) {coHostStore.rejectHostConnection(fromHostLiveID = fromHostLiveId, completion = null)}
步骤3:实现主播 PK
挑战方(例如主播 A)实现
1. 发起 PK 挑战
当主播 A 点击 PK 按钮时,调用
requestBattle 方法。// AnchorAActivity 的一部分class AnchorAActivity : AppCompatActivity() {private val liveId = "主播A的房间ID"private val battleStore = BattleStore.create(liveId)fun startPK(opponentUserId: String) {val config = BattleConfig(duration = 300) // PK持续5分钟battleStore.requestBattle(config = config,userIDList = listOf(opponentUserId),timeout = 30,completion = null)}}
2. 监听 PK 状态
通过
BattleListener 监听 PK 的开始、结束等关键事件。// AnchorAActivity 的一部分class AnchorAActivity : AppCompatActivity() {private val liveId = "主播A的房间ID"private val battleStore = BattleStore.create(liveId)private val battleListener = object : BattleListener() {override fun onBattleStarted(battleInfo: BattleInfo,inviter: SeatUserInfo,invitees: List<SeatUserInfo>) {println("PK 开始")}override fun onBattleEnded(battleInfo: BattleInfo, reason: BattleEndedReason?) {println("PK 结束")}}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// ... 其他初始化代码 ...battleStore.addBattleListener(battleListener)}}
应战方(主播 B)实现
1. 接收 PK 挑战
通过
BattleListener 监听到 PK 邀请。// 在 AnchorBActivity 中添加class AnchorBActivity : AppCompatActivity() {private val liveId = "主播B的房间ID"private val battleStore = BattleStore.create(liveId)private val battleListener = object : BattleListener() {override fun onBattleRequestReceived(battleID: String, inviter: SeatUserInfo, invitee: SeatUserInfo) {println("收到主播 ${inviter.userName} 的PK挑战")// 弹出对话框,让主播B选择"接受"或"拒绝"// showPKChallengeDialog(battleID, inviter)}}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// ... 其他初始化代码 ...battleStore.addBattleListener(battleListener)}}
2. 响应 PK 挑战
当主播 B 做出选择后,调用相应的方法。
// AnchorBActivity 的一部分// 用户点击"接受挑战"fun acceptPK(battleId: String) {battleStore.acceptBattle(battleID = battleId, completion = null)}// 用户点击"拒绝挑战"fun rejectPK(battleId: String) {battleStore.rejectBattle(battleID = battleId, completion = null)}
运行效果

完善 UI 细节
您可以通过
VideoViewAdapter 接口提供的"插槽"能力,在视频流画面上添加自定义视图,用于显示昵称、头像、PK 进度条等信息,或在他们关闭摄像头时提供占位图,以优化视觉体验。实现视频流画面的昵称显示
实现效果

实现方式
步骤1:创建前景视图 (CustomSeatView),该视图用于在视频流上方显示用户信息。
提示:
import android.content.Contextimport android.graphics.Colorimport android.view.Gravityimport android.widget.LinearLayoutimport android.widget.TextView// 自定义的用户信息悬浮视图(前景)class CustomSeatView(context: Context) : LinearLayout(context) {private val nameLabel: TextViewinit {orientation = VERTICALsetBackgroundColor(Color.parseColor("#80000000")) // 半透明黑色背景nameLabel = TextView(context).apply {setTextColor(Color.WHITE)textSize = 14fgravity = Gravity.CENTER}addView(nameLabel)val layoutParams = nameLabel.layoutParams as LayoutParamslayoutParams.setMargins(5, 0, 5, 5)}fun setUserName(userName: String) {nameLabel.text = userName}}
步骤2:创建背景视图 (CustomAvatarView),该视图用于在用户无视频流时作为占位图显示。
import android.content.Contextimport android.graphics.Colorimport android.view.Gravityimport android.widget.ImageViewimport android.widget.LinearLayout// 自定义的头像占位视图(背景)class CustomAvatarView(context: Context) : LinearLayout(context) {private val avatarImageView: ImageViewinit {orientation = VERTICALgravity = Gravity.CENTERsetBackgroundColor(Color.TRANSPARENT)avatarImageView = ImageView(context).apply {setColorFilter(Color.GRAY)scaleType = ImageView.ScaleType.CENTER_CROP}addView(avatarImageView)val layoutParams = avatarImageView.layoutParams as LayoutParamslayoutParams.width = 120 // 60dplayoutParams.height = 120 // 60dp}}
步骤3:实现
VideoViewAdapter.createCoHostView 协议,根据 viewLayer 的值返回对应的视图。import com.tencent.cloud.tuikit.engine.room.TUIRoomDefineimport android.view.Viewimport androidx.appcompat.app.AppCompatActivityimport io.trtc.tuikit.atomicxcore.api.view.VideoViewAdapterimport io.trtc.tuikit.atomicxcore.api.view.ViewLayer// 1. 在您的Activity中,实现 VideoViewAdapter 接口class YourActivity : AppCompatActivity(), VideoViewAdapter {// ... 其他代码 ...// 2. 完整实现接口方法,处理两种 viewLayeroverride fun createCoHostView(coHostUser: TUIRoomDefine.SeatFullInfo?, viewLayer: ViewLayer?): View? {val seatInfo = coHostUser ?: return nullval userId = seatInfo.userIdif (userId.isNullOrEmpty()) {return null}return when (viewLayer) {ViewLayer.FOREGROUND -> {val seatView = CustomSeatView(this)seatView.setUserName(seatInfo.userName ?: "")seatView}ViewLayer.BACKGROUND -> {val avatarView = CustomAvatarView(this)// 您可以在这里通过 seatInfo.userAvatar 加载用户真实头像avatarView}null -> null}}}
参数说明:
参数 | 类型 | 说明 |
seatInfo | SeatFullInfo? | 麦位信息对象,包含麦上用户的详细信息 |
seatInfo.userId | String | 麦上用户的 ID |
seatInfo.userName | String | 麦上用户的昵称 |
seatInfo.userAvatar | String | 麦上用户的头像 URL |
seatInfo.userMicrophoneStatus | DeviceStatus | 麦上用户的麦克风状态 |
seatInfo.userCameraStatus | DeviceStatus | 麦上用户的摄像头状态 |
viewLayer | ViewLayer | 视图层级枚举 FOREGROUND 表示前景挂件视图,始终显示在视频画面的最上层BACKGROUND 表示背景挂件视图,位于前景视图下层,仅在对应用户没有视频流(例如未开摄像头)的情况下显示,通常用于展示用户的默认头像或占位图 |
实现 PK 用户视图的分数展示
当主播开始 PK 后,可以在对方主播的视频画面上挂载自定义视图,通常用于展示该主播收到的礼物价值或其它 PK 相关信息。
实现效果

实现方式
步骤1:创建自定义 PK 用户视图,您可以参考 TUILiveKit 开源项目中的 BattleMemberInfoView.java 文件来了解完整的实现逻辑。
import android.content.Contextimport android.graphics.Colorimport android.view.Gravityimport android.widget.LinearLayoutimport android.widget.TextViewimport com.tencent.cloud.tuikit.engine.extension.TUILiveBattleManager.BattleUserimport io.trtc.tuikit.atomicxcore.api.live.BattleStoreimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launch// 自定义PK 用户视图class CustomBattleUserView(context: Context,private val liveId: String,private val battleUser: BattleUser) : LinearLayout(context) {private lateinit var scoreView: LinearLayoutprivate lateinit var scoreLabel: TextViewprivate val battleStore: BattleStoreinit {orientation = VERTICALgravity = Gravity.BOTTOM or Gravity.ENDsetBackgroundColor(Color.TRANSPARENT)isClickable = falsebattleStore = BattleStore.create(liveId)// UI 布局setupUI()// 订阅分数变化subscribeBattleState()}private fun setupUI() {scoreView = LinearLayout(context).apply {setBackgroundColor(Color.parseColor("#66000000"))orientation = VERTICALgravity = Gravity.CENTER}scoreLabel = TextView(context).apply {setTextColor(Color.WHITE)textSize = 14fgravity = Gravity.CENTER}scoreView.addView(scoreLabel)addView(scoreView)val layoutParams = scoreView.layoutParams as LayoutParamslayoutParams.width = LayoutParams.WRAP_CONTENTlayoutParams.height = 48 // 24dplayoutParams.setMargins(0, 0, 10, 10)}// 订阅 PK 分数变化private fun subscribeBattleState() {CoroutineScope(Dispatchers.Main).launch {battleStore.battleState.battleScore.collect { battleScore ->val score = battleScore[battleUser.userId] ?: 0// 更新 UIscoreLabel.text = score.toString()}}}}
步骤2:实现
VideoViewAdapter.createBattleView 协议// 1. 让您的Activity实现 VideoViewAdapter 接口class YourActivity : AppCompatActivity(), VideoViewAdapter {override fun createBattleView(battleUser: BattleUser?): View? {battleUser ?: return null// CustomBattleUserView 是您自定义的PK用户信息视图return CustomBattleUserView(this, liveId, battleUser)}}
参数说明:
参数 | 类型 | 说明 |
battleUser | BattleUser? | PK 用户信息对象 |
battleUser.roomId | String | PK 的房间 ID |
battleUser.userId | String | PK 用户 ID |
battleUser.userName | String | PK 用户昵称 |
battleUser.avatarUrl | String | PK 用户头像地址 |
battleUser.score | int | PK 分数 |
实现视频流画面上的 PK 状态显示
实现效果

实现方式
步骤1:创建自定义 PK 全局视图 CustomBattleContainerView,您可以参考 TUILiveKit 开源项目中的 BattleInfoView.java 文件来实现,即可实现同样的效果。
步骤2:实现
VideoViewAdapter.createBattleContainerView 协议。// 让您的Activity实现 VideoViewAdapter 接口并设置适配器class YourActivity : AppCompatActivity(), VideoViewAdapter {override fun createBattleContainerView(): View? {return CustomBattleContainerView(this)}}
功能进阶
通过 REST API 实现 PK 分数更新
通常在直播主播 PK 场景下,会将主播收到的礼物价值与 PK 数值挂钩(例如:观众送 "火箭" 礼物,主播 PK 分数增加 500 分),您可以通过我们的 REST API,轻松实现直播 PK 场景下的分数实时更新。
重要说明:
LiveKit 后台的 PK 分数系统采用纯数值计算和累加机制,所以您需要根据自身的运营策略和业务需求,调用更新接口前完成 PK 分数的计算,您可以参考如下的 PK 分数计算示例:
礼物类型 | 分数计算规则 | 示例 |
基础礼物 | 礼物价值 × 5 | 10元礼物 → 50分 |
中级礼物 | 礼物价值 × 8 | 50元礼物 → 400分 |
高级礼物 | 礼物价值 × 12 | 100元礼物 → 1200分 |
特效礼物 | 固定高分数 | 520元礼物 → 1314分 |
REST API 调用流程

关键流程说明
1. 获取 PK 状态:
回调配置:您可以通过配置 PK 状态回调,由 LiveKit 后台在 PK 开始、结束时,主动通知您的系统 PK 状态。
主动查询:您的后台服务可主动调用 PK 状态查询 接口,随时查询当前 PK 状态。
2. PK分数计算:您的后台服务根据业务规则(如上述示例),计算 PK 分数增量。
3. PK分数更新:您的后台服务调用 修改 PK 分数 接口,向 LiveKit 后台更新 PK 分数。
4. LiveKit后台 同步客户端:LiveKit 后台自动将更新后的 PK 分数同步到所有客户端。
涉及的 REST API 接口
API 文档
Store/Component | 功能描述 | API 文档 |
LiveCoreView | 直播视频流展示与交互的核心视图组件:负责视频流渲染和视图挂件处理,支持主播直播、观众连麦、主播连线等场景。 | |
DeviceStore | 音视频设备控制:麦克风(开关 / 音量)、摄像头(开关 / 切换 / 画质)、屏幕共享,设备状态实时监听。 | |
CoHostStore | 主播跨房连线:支持多布局模板(动态网格等),发起 / 接受 / 拒绝连线,连麦主播互动管理。 | |
BattleStore | 主播 PK 对战:发起 PK(配置时长 / 对手),管理 PK 状态(开始 / 结束),同步分数,监听对战结果。 |
常见问题
为什么发起了连线邀请,对方却没收到?
请检查 targetHostLiveId 是否正确,并且对方直播间处于正常开播状态。
检查网络连接是否通畅,邀请信令有30秒的默认超时时间。
连线或PK过程中,一方主播网络断开或App崩溃了怎么办?
CoHostStore 和 BattleStore 内部都有心跳和超时检测机制。如果一方异常退出,另一方会通过 onCoHostUserLeft 或 onUserExitBattle 等事件收到通知,您可以根据这些事件来处理UI,例如提示"对方已掉线"并结束互动。
为什么 PK 分数只能通过 REST API 更新?
因为 REST API 能同时满足 PK 分数的安全性、实时性、扩展性需求:
防篡改保公平:需鉴权 + 数据校验,每笔更新可追溯来源(例如礼物行为),杜绝手动改分、刷分,保障竞技公平;
多端实时同步:用标准化格式(例如 JSON)快速对接礼物、PK、展示系统,确保主播 / 观众 / 后台分数实时一致,无延迟;
灵活适配规则:后端改配置(例如调整礼物对应分数、加成分数)即可适配业务变化,无需改前端,降低迭代成本。
如何管理通过 VideoViewAdapter 添加的自定义视图的生命周期和事件?
LiveCoreView 会自动管理您通过适配器方法返回视图的添加和移除,您无需手动处理。如果需要在自定义视图中处理用户交互(例如点击事件),请在创建视图时为其添加相应的事件即可。