直播列表

LiveListStoreAtomicXCore 中负责管理直播房间列表、创建、加入以及维护房间状态的核心模块。通过LiveListStore,可以为您的应用构建完整的直播生命周期管理。


核心功能

直播列表拉取:获取当前所有公开的直播间列表,支持分页加载。
直播生命周期管理:提供从创建、开播、加入、离开到结束直播的全套流程接口。
实时事件监听:监听直播结束、用户被踢出等关键事件。

核心概念

核心概念
类型
核心职责与描述
LiveInfo
data class
代表一个直播间的所有信息模型。包含了直播ID (liveID)、名称 (liveName)、房主信息 (liveOwner) 以及自定义元数据 (metaData) 等。
LiveListState
data class
代表直播列表模块的当前状态。其核心属性 liveList 是一个 StateFlow,存储了拉取到的直播列表;currentLive 则代表用户当前所在的直播间信息。
LiveListListener
abstract class
代表直播房间的全局事件。分为 onLiveEnded (直播结束) 和 onKickedOutOfLive (被踢出直播) 两种,用于处理关键的房间状态变更。
LiveListStore
abstract class
代表直播房间的全局事件。分为 .onLiveEnded (直播结束) 和 .onKickedOutOfLive (被踢出直播) 两种,用于处理关键的房间状态变更。

实现步骤

步骤1:组件集成

视频直播:请参考 快速接入 集成 AtomicXCore,并完成 主播开播观众观看 的功能实现。
语聊房:请参考 快速接入 集成 AtomicXCore,并完成 主播开播观众观看 的功能实现。

步骤2:实现观众从直播列表进入直播间

创建一个展示直播列表的页面,该页面使用 RecyclerView 来布局直播间卡片。当用户点击某个卡片时,获取该直播间的 liveId,并跳转到观众观看页面。
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.trtc.tuikit.atomicxcore.api.CompletionHandler
import io.trtc.tuikit.atomicxcore.api.live.LiveInfo
import io.trtc.tuikit.atomicxcore.api.live.LiveListStore
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

class LiveListActivity : AppCompatActivity() {
private val liveListStore = LiveListStore.shared()
private var liveList: List<LiveInfo> = emptyList()
private val coroutineScope = CoroutineScope(Dispatchers.Main)

private lateinit var recyclerView: RecyclerView
private lateinit var adapter: LiveListAdapter

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_live_list)
setupUI()
bindStore()
fetchLiveList()
}

private fun bindStore() {
// 订阅 state,自动接收列表更新
coroutineScope.launch {
liveListStore.liveState.liveList.collect { fetchedList ->
liveList = fetchedList
adapter.notifyDataSetChanged()
}
}
}

private fun fetchLiveList() {
liveListStore.fetchLiveList(
cursor = "",
count = 20,
completion = object : CompletionHandler {
override fun onSuccess() {
println("直播列表拉取成功")
}

override fun onFailure(code: Int, desc: String) {
println("直播列表拉取失败: $desc")
}
}
)
}

// 当用户点击列表中的某个Item时
private fun onLiveItemClick(liveInfo: LiveInfo) {
// 创建观众观看页面,并将 liveId 传入
val intent = Intent(this, YourAudienceActivity::class.java)
intent.putExtra("liveId", liveInfo.liveID)
startActivity(intent)
}


// --- RecyclerView 相关方法 ---
private fun setupUI() {
recyclerView = findViewById(R.id.recyclerView)
adapter = LiveListAdapter(liveList) { liveInfo ->
onLiveItemClick(liveInfo)
}
recyclerView.layoutManager = GridLayoutManager(this, 2)
recyclerView.adapter = adapter
}

override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
}
}

// RecyclerView Adapter
class LiveListAdapter(
private var liveList: List<LiveInfo>,
private val onItemClick: (LiveInfo) -> Unit
) : RecyclerView.Adapter<LiveListAdapter.LiveViewHolder>() {

class LiveViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
// 定义您的ViewHolder视图组件
// val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)
// val coverImageView: ImageView = itemView.findViewById(R.id.coverImageView)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_live_card, parent, false)
return LiveViewHolder(view)
}

override fun onBindViewHolder(holder: LiveViewHolder, position: Int) {
val liveInfo = liveList[position]
// 设置数据到ViewHolder
// holder.titleTextView.text = liveInfo.liveName
// 加载封面图片等

holder.itemView.setOnClickListener {
onItemClick(liveInfo)
}
}

override fun getItemCount(): Int = liveList.size
}

LiveInfo 参数说明
参数名
类型
描述
liveID
String
直播间的唯一标识符
liveName
String
直播间的标题
coverURL
String
直播间的封面图片地址
liveOwner
房主的个人信息
totalViewerCount
Int
直播间的总观看人数
categoryList
List<Int>
直播间的分类标签列表
notice
String
直播间的公告信息
metaData
Map<String: String>
开发者自定义的元数据,用于实现复杂的业务场景

功能进阶

场景一:实现直播列表的分类展示

在 App 的直播广场页,顶部设有"热门"、"音乐"、"游戏"等分类标签。用户点击不同的标签后,下方的直播列表会动态筛选,只展示对应分类的直播间,从而帮助用户快速发现感兴趣的内容。


实现方式

核心是利用 LiveInfo 模型中的 categoryList 属性。当主播开播设置分类后,fetchLiveList 返回的 LiveInfo 对象中就会包含这些分类信息。您的 App 在获取到完整的直播列表后,只需在客户端根据用户选择的分类,对这个列表进行一次简单的筛选,然后刷新 UI 即可。

代码示例

以下示例展示了如何在 LiveListActivity 中扩展一个 LiveListManager 来处理数据和筛选逻辑。
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import io.trtc.tuikit.atomicxcore.api.live.LiveInfo
import io.trtc.tuikit.atomicxcore.api.live.LiveListStore
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

// 1. 创建一个数据管理器来封装数据获取和筛选逻辑
class LiveListManager {
private val liveListStore = LiveListStore.shared()
private var fullLiveList: List<LiveInfo> = emptyList()

// 对外暴露最终的直播列表
private val _filteredLiveList = MutableStateFlow<List<LiveInfo>>(emptyList())
val filteredLiveList: StateFlow<List<LiveInfo>> = _filteredLiveList

init {
// 监听完整列表变化
CoroutineScope(Dispatchers.Main).launch {
liveListStore.liveState.liveList.collect { fetchedList ->
fullLiveList = fetchedList
// 默认将完整列表发布出去
_filteredLiveList.value = fetchedList
}
}
}

fun fetchFirstPage() {
liveListStore.fetchLiveList(cursor = "", count = 20, completion = null)
}

/// 根据分类筛选直播列表
fun filterLiveList(categoryId: Int?) {
if (categoryId == null) {
// 如果 categoryId 为 null,则显示完整列表
_filteredLiveList.value = fullLiveList
return
}

val filteredList = fullLiveList.filter { liveInfo ->
liveInfo.categoryList.contains(categoryId)
}
_filteredLiveList.value = filteredList
}
}

// 2. 在您的 LiveListActivity 中使用 Manager
class LiveListActivity : AppCompatActivity() {
private val manager = LiveListManager()
private lateinit var recyclerView: RecyclerView

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_live_list)
// setupUI()

// 绑定数据
CoroutineScope(Dispatchers.Main).launch {
manager.filteredLiveList.collect { filteredList ->
// 刷新UI
// adapter.updateList(filteredList)
}
}

// 首次拉取
manager.fetchFirstPage()
}

// 当用户点击顶部分类标签时
fun onCategorySelected(categoryId: Int) {
manager.filterLiveList(categoryId)
}

// ... (RecyclerView 相关代码)
}

场景二:实现直播列表的滑动播放

用户可以通过上下滑动来切换直播间,当一个新的直播间滑动到屏幕中央时,视频会自动开始播放预览;当它滑出屏幕时,视频则会自动停止,以节省带宽和设备性能。
说明:
滑动播放仅视频直播场景支持,语聊房场景正在规划中。

交互流程图



实现方式

LiveCoreView 支持多实例使用,我们为每一个 RecyclerView.ViewHolder 都创建一个独立的 LiveCoreView 实例。通过监听 RecyclerView 的滚动状态,我们可以精确地控制即将出现和已经离开屏幕的 ViewHolder 中的 LiveCoreView 何时开始和停止拉流,从而实现“即滑即播、即走即停”的效果。

代码示例

我们创建一个自定义的 LiveFeedViewHolder,它内部持有一个 LiveCoreView。然后在 Activity 中管理这些 ViewHolder 的播放状态。
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import io.trtc.tuikit.atomicxcore.api.live.LiveInfo
import io.trtc.tuikit.atomicxcore.api.view.CoreViewType
import io.trtc.tuikit.atomicxcore.api.view.LiveCoreView

// 1. 自定义 RecyclerView.ViewHolder,内部包含一个 LiveCoreView
class LiveFeedViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private var liveCoreView: LiveCoreView? = null
fun setLiveInfo(liveInfo: LiveInfo) {
// 为新的直播信息创建一个新的 LiveCoreView
liveCoreView = LiveCoreView(itemView.context, viewType = CoreViewType.PLAY_VIEW)
liveCoreView?.let { view ->
(itemView as ViewGroup).addView(view)
view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}

fun startPlay(roomId: String) {
liveCoreView?.startPreviewLiveStream(roomId, false, callback = null)
}
fun stopPlay(roomId: String) {
liveCoreView?.stopPreviewLiveStream(roomId)
}
}

// 2. 在 Activity 中管理播放逻辑
class LiveFeedActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private var liveList: List<LiveInfo> = emptyList()
private var currentPlayingPosition: Int = -1

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_live_feed)
setupRecyclerView()
}

private fun setupRecyclerView() {
recyclerView = findViewById(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerView.adapter = LiveFeedAdapter(liveList) { position ->
playVideoAtPosition(position)
}

// 监听滚动状态
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstVisiblePosition = layoutManager.findFirstCompletelyVisibleItemPosition()
if (firstVisiblePosition != RecyclerView.NO_POSITION) {
playVideoAtPosition(firstVisiblePosition)
}
}
}
})
}

private fun playVideoAtPosition(position: Int) {
// 只有当居中的位置变化时才切换播放
if (currentPlayingPosition != position) {
// 停止当前播放
if (currentPlayingPosition != -1) {
val currentViewHolder = recyclerView.findViewHolderForAdapterPosition(currentPlayingPosition)
if (currentViewHolder is LiveFeedViewHolder) {
val liveInfo = liveList[currentPlayingPosition]
currentViewHolder.stopPlay(liveInfo.liveID)
}
}
// 开始新的播放
val newViewHolder = recyclerView.findViewHolderForAdapterPosition(position)
if (newViewHolder is LiveFeedViewHolder) {
val liveInfo = liveList[position]
newViewHolder.startPlay(liveInfo.liveID)
currentPlayingPosition = position
}
}
}
// RecyclerView Adapter
inner class LiveFeedAdapter(
private var liveList: List<LiveInfo>,
private val onItemClick: (Int) -> Unit
) : RecyclerView.Adapter<LiveFeedViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LiveFeedViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_live_feed, parent, false)
return LiveFeedViewHolder(view)
}

override fun onBindViewHolder(holder: LiveFeedViewHolder, position: Int) {
val liveInfo = liveList[position]
holder.setLiveInfo(liveInfo)
holder.itemView.setOnClickListener {
onItemClick(position)
}
}

override fun getItemCount(): Int = liveList.size
}
}

API 文档

关于 LiveListStore 及其相关类的所有公开接口、属性和方法的详细信息,请参阅随 AtomicXCore 框架的官方 API 文档。本文档使用到的相关 Store 如下:
Store/Component
功能描述
API 文档
LiveCoreView
直播视频流展示与交互的核心视图组件:负责视频流渲染和视图挂件处理,支持主播直播、观众连麦、主播连线等场景。
LiveListStore
直播间全生命周期管理:创建 / 加入 / 离开 / 销毁房间,查询房间列表,修改直播信息(名称、公告等),监听直播状态(例如被踢出、结束)。

常见问题

语聊房的列表和视频直播的列表数据是同一份吗?

它们的数据是同一份,您不需要分开拉取。LiveListStore 是一个全局单例,它负责统一管理应用中所有“直播”房间的生命周期,无论是视频直播还是语聊房。

直播列表中如何区分语聊房和视频直播

LiveListStore 本身不区分房间的业务类型。您需要在获取列表后,在客户端的应用层(业务逻辑或 UI 层)进行筛选和分类。
我们推荐采用以下两种方式进行区分:
方式一(推荐)使用 seatLayoutTemplateID 区分。这是一个用于定义房间布局的模板 ID,关于目前已支持的模板 ID 和效果,请参阅文档 开始直播 > 运行效果。您要在创建房间时指定 ID,然后在获取列表时根据 ID 范围来识别业务场景。
步骤1创建房间时指定 ID 调用 LiveListStore.shared.createLive 时,您需要根据业务场景为 LiveInfoseatLayoutTemplateID 属性传入指定范围的 ID
语聊房场景:传入 1 - 199 范围内的模板 ID
视频直播场景:传入 200 - 999 范围内的模板 ID
步骤2获取列表时筛选 客户端在 LiveListStore.state.liveList 中收到列表数据后,通过判断这个 ID 值所属的范围,从而在进入直播间的时候区分两种业务场景。
重要提示:
创建房间时,如果传入的 seatLayoutTemplateID 范围与您的业务场景(语聊房、视频直播)不匹配,可能会导致麦位布局出现功能异常。
方式二liveID 添加业务前缀。这是一种可选的、纯粹的应用层约定,用于辅助您在客户端快速筛选。
步骤1:创建房间时添加前缀 在您生成 liveID 并调用 createLive 时,为不同业务类型的 liveID 赋予不同的前缀。例如:视频直播的 ID 以 “Live_” 开头 (例如:Live_12345),语聊房的 ID 以 “voice_” 开头 (例如:voice_67890)。
步骤2:获取列表时检查前缀 客户端在拉取到列表后,通过检查 liveID 字符串的前缀来进行区分。