@tencentcloud/sdk-mcp 1.3.8 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/resource/doc/chatuikit/android/integration.md +466 -484
  3. package/resource/doc/chatuikit/android/workflow.md +713 -0
  4. package/resource/doc/livekit/live/flutter/live-integration.md +208 -167
  5. package/resource/doc/livekit/voice/android/voice-audience-list.md +335 -0
  6. package/resource/doc/livekit/voice/android/voice-audio-effect.md +384 -0
  7. package/resource/doc/livekit/voice/android/voice-barrage.md +569 -0
  8. package/resource/doc/livekit/voice/android/voice-co-guest-interaction.md +748 -0
  9. package/resource/doc/livekit/voice/android/voice-gift.md +833 -0
  10. package/resource/doc/livekit/voice/android/voice-host-interaction.md +728 -0
  11. package/resource/doc/livekit/voice/android/voice-integration.md +886 -0
  12. package/resource/doc/livekit/voice/android/voice-room-list.md +493 -0
  13. package/resource/doc/livekit/voice/android/workflow.md +333 -0
  14. package/resource/doc/livekit/voice/flutter/voice-audience-list.md +337 -0
  15. package/resource/doc/livekit/voice/flutter/voice-audio-effect.md +739 -0
  16. package/resource/doc/livekit/voice/flutter/voice-barrage.md +578 -0
  17. package/resource/doc/livekit/voice/flutter/voice-co-guest-interaction.md +673 -0
  18. package/resource/doc/livekit/voice/flutter/voice-gift.md +812 -0
  19. package/resource/doc/livekit/voice/flutter/voice-integration.md +925 -0
  20. package/resource/doc/livekit/voice/flutter/voice-room-list.md +842 -0
  21. package/resource/doc/livekit/voice/flutter/workflow.md +359 -0
  22. package/resource/doc/livekit/voice/ios/voice-audience-list.md +335 -0
  23. package/resource/doc/livekit/voice/ios/voice-audio-effect.md +373 -0
  24. package/resource/doc/livekit/voice/ios/voice-barrage.md +529 -0
  25. package/resource/doc/livekit/voice/ios/voice-co-guest-interaction.md +701 -0
  26. package/resource/doc/livekit/voice/ios/voice-gift.md +785 -0
  27. package/resource/doc/livekit/voice/ios/voice-host-interaction.md +820 -0
  28. package/resource/doc/livekit/voice/ios/voice-integration.md +853 -0
  29. package/resource/doc/livekit/voice/ios/voice-room-list.md +467 -0
  30. package/resource/doc/livekit/voice/ios/workflow.md +358 -0
  31. package/resource/doc/livekit/voice/uni-app/voice-audience-list.md +264 -0
  32. package/resource/doc/livekit/voice/uni-app/voice-audio-effect.md +332 -0
  33. package/resource/doc/livekit/voice/uni-app/voice-barrage.md +482 -0
  34. package/resource/doc/livekit/voice/uni-app/voice-co-guest-interaction.md +944 -0
  35. package/resource/doc/livekit/voice/uni-app/voice-gift.md +716 -0
  36. package/resource/doc/livekit/voice/uni-app/voice-integration.md +613 -0
  37. package/resource/doc/livekit/voice/uni-app/voice-room-list.md +562 -0
  38. package/resource/doc/livekit/voice/uni-app/workflow.md +369 -0
  39. package/resource/doc/roomkit/vue/integration.md +119 -224
  40. package/resource/doc/roomkit/vue/workflow.md +472 -226
  41. package/resource/doc/tuicallkit/android/integration.md +675 -90
  42. package/resource/doc/tuicallkit/android/workflow.md +432 -0
  43. package/tools/common/present-scenario-choice.js +16 -0
  44. package/tools/common/present-scenario-choice.js.map +1 -0
  45. package/tools/definitions/get_native_call_uikit_integration_def.js +18 -2
  46. package/tools/definitions/get_native_call_uikit_integration_def.js.map +1 -1
  47. package/tools/definitions/get_native_chat_uikit_integration_def.js +29 -7
  48. package/tools/definitions/get_native_chat_uikit_integration_def.js.map +1 -1
  49. package/tools/definitions/get_native_live_uikit_integration_def.js +65 -40
  50. package/tools/definitions/get_native_live_uikit_integration_def.js.map +1 -1
  51. package/tools/definitions/get_native_live_uikit_interaction_def.js +42 -94
  52. package/tools/definitions/get_native_live_uikit_interaction_def.js.map +1 -1
  53. package/tools/definitions/get_web_room_feature_detail_def.js +29 -32
  54. package/tools/definitions/get_web_room_feature_detail_def.js.map +1 -1
  55. package/tools/definitions/get_web_room_uikit_integration_def.js +25 -9
  56. package/tools/definitions/get_web_room_uikit_integration_def.js.map +1 -1
  57. package/tools/definitions/present_scenario_choice_def.js +53 -0
  58. package/tools/definitions/present_scenario_choice_def.js.map +1 -0
  59. package/tools/definitions/record_prompt_def.js +6 -1
  60. package/tools/definitions/record_prompt_def.js.map +1 -1
  61. package/tools/definitions/record_result_def.js +20 -4
  62. package/tools/definitions/record_result_def.js.map +1 -1
  63. package/tools/index.js +2 -0
  64. package/tools/index.js.map +1 -1
  65. package/tools/live/get-native-live-uikit-integration.js +110 -34
  66. package/tools/live/get-native-live-uikit-integration.js.map +1 -1
  67. package/tools/live/get_native_live_uikit_interaction.js +128 -35
  68. package/tools/live/get_native_live_uikit_interaction.js.map +1 -1
  69. package/utils/report-cls-client.js +1 -1
  70. package/utils/report-es-client.js +1 -1
@@ -0,0 +1,842 @@
1
+ `LiveListStore` 是 `AtomicXCore` 中负责管理直播房间列表、创建、加入以及维护房间状态的核心模块。通过`LiveListStore`,开发者可以为应用构建完整的直播生命周期管理。
2
+
3
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/1486fac1df3611f0a4f35254007c27c5.png)
4
+
5
+
6
+ ## 核心功能
7
+ - **直播列表拉取**:获取当前所有公开的直播间列表,支持分页加载。
8
+
9
+ - **直播生命周期管理**:提供从创建、开播、加入、离开到结束直播的全套流程接口。
10
+
11
+ - **直播信息更新**:主播可以随时更新直播间的公开信息,例如封面、公告等。
12
+
13
+ - **实时事件监听**:监听直播结束、用户被踢出等关键事件。
14
+
15
+ - **自定义业务数据**:强大的自定义元数据(`MetaData`)能力,允许您在房间内存储和同步任何业务相关的信息,例如直播状态、音乐信息、自定义角色等。
16
+
17
+
18
+ ## 核心概念
19
+
20
+ 在开始集成之前,下表介绍了 `LiveListStore` 相关的核心概念:
21
+ <table>
22
+ <tr>
23
+ <td rowspan="1" colSpan="1" >**核心概念**</td>
24
+
25
+ <td rowspan="1" colSpan="1" >**类型**</td>
26
+
27
+ <td rowspan="1" colSpan="1" >**核心职责与描述**</td>
28
+ </tr>
29
+
30
+ <tr>
31
+ <td rowspan="1" colSpan="1" >[LiveInfo](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveInfo-class.html)</td>
32
+
33
+ <td rowspan="1" colSpan="1" >`class`</td>
34
+
35
+ <td rowspan="1" colSpan="1" >代表一个直播间的所有信息模型。包含了直播 ID (`liveID`)、名称 (`liveName`)、房主信息 (`liveOwner`) 以及自定义元数据 (`metaData`) 等。</td>
36
+ </tr>
37
+
38
+ <tr>
39
+ <td rowspan="1" colSpan="1" >[LiveListState](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveListState-class.html)</td>
40
+
41
+ <td rowspan="1" colSpan="1" >`class`</td>
42
+
43
+ <td rowspan="1" colSpan="1" >代表直播列表模块的当前状态。其核心属性 `liveList` 是一个 `ValueListenable<List<LiveInfo>>` 类型,存储了拉取到的直播列表;`currentLive` 则代表用户当前所在的直播间信息。</td>
44
+ </tr>
45
+
46
+ <tr>
47
+ <td rowspan="1" colSpan="1" >[LiveListListener](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveListListener-class.html)</td>
48
+
49
+ <td rowspan="1" colSpan="1" >`class`</td>
50
+
51
+ <td rowspan="1" colSpan="1" >代表直播房间的全局事件监听器。包含 `onLiveEnded` (直播结束) 和 `onKickedOutOfLive` (被踢出直播) 两种回调,用于处理关键的房间状态变更。</td>
52
+ </tr>
53
+
54
+ <tr>
55
+ <td rowspan="1" colSpan="1" >[LiveListStore](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveListStore-class.html)</td>
56
+
57
+ <td rowspan="1" colSpan="1" >`class`</td>
58
+
59
+ <td rowspan="1" colSpan="1" >这是与直播列表和房间生命周期交互的核心管理类。它是一个全局单例 (`shared`),负责所有直播房间的创建、加入、信息更新等操作。</td>
60
+ </tr>
61
+ </table>
62
+
63
+
64
+ ## 实现步骤
65
+
66
+ ### 步骤1:组件集成
67
+
68
+ 参考 [快速接入](https://write.woa.com/document/197478016886185984) 集成 **AtomicXCore**,并完成 [构建基础语聊房应用](https://write.woa.com/document/197478016886185984) 的功能实现。
69
+
70
+ ### 步骤2:实现观众从直播列表进入直播间
71
+
72
+ #### 实现方式
73
+ 1. 创建一个展示直播列表的页面,该页面使用 `GridView` 来布局直播间卡片。
74
+
75
+ 2. 当用户点击某个卡片时,获取该直播间的 `liveID`,并跳转到观众观看页面。
76
+
77
+
78
+ #### 代码示例
79
+ ``` java
80
+ import 'package:flutter/material.dart';
81
+ import 'package:atomic_x_core/atomicxcore.dart';
82
+
83
+ /// 直播列表页面
84
+ class LiveListPage extends StatefulWidget {
85
+ const LiveListPage({super.key});
86
+
87
+ @override
88
+ State<LiveListPage> createState() => _LiveListPageState();
89
+ }
90
+
91
+ class _LiveListPageState extends State<LiveListPage> {
92
+ final _liveListStore = LiveListStore.shared;
93
+ final ScrollController _scrollController = ScrollController();
94
+ bool _isLoading = false;
95
+ late final VoidCallback _liveListListener = _onLiveListChanged;
96
+
97
+ @override
98
+ void initState() {
99
+ super.initState();
100
+ _liveListStore.liveState.liveList.addListener(_liveListListener);
101
+ _scrollController.addListener(_onScroll);
102
+ _fetchLiveList();
103
+ }
104
+
105
+ void _onLiveListChanged() {
106
+ setState(() {});
107
+ }
108
+
109
+ void _onScroll() {
110
+ // 滚动到底部时加载更多
111
+ if (_scrollController.position.pixels >=
112
+ _scrollController.position.maxScrollExtent - 200) {
113
+ _loadMore();
114
+ }
115
+ }
116
+
117
+ Future<void> _fetchLiveList() async {
118
+ if (_isLoading) return;
119
+ setState(() => _isLoading = true);
120
+
121
+ final result = await _liveListStore.fetchLiveList(cursor: '', count: 20);
122
+ if (!result.isSuccess) {
123
+ debugPrint('直播列表拉取失败: ${result.message}');
124
+ }
125
+
126
+ setState(() => _isLoading = false);
127
+ }
128
+
129
+ Future<void> _loadMore() async {
130
+ if (_isLoading) return;
131
+
132
+ final cursor = _liveListStore.liveState.liveListCursor.value;
133
+ if (cursor.isEmpty) return; // 没有更多数据
134
+
135
+ setState(() => _isLoading = true);
136
+ await _liveListStore.fetchLiveList(cursor: cursor, count: 20);
137
+ setState(() => _isLoading = false);
138
+ }
139
+
140
+ Future<void> _refresh() async {
141
+ _liveListStore.reset();
142
+ await _fetchLiveList();
143
+ }
144
+
145
+ // 当用户点击列表中的某个卡片时
146
+ void _onLiveCardTap(LiveInfo liveInfo) {
147
+ // 创建观众观看页面,并将 liveID 传入
148
+ Navigator.push(
149
+ context,
150
+ MaterialPageRoute(
151
+ builder: (context) => YourAudiencePage(liveID: liveInfo.liveID),
152
+ ),
153
+ );
154
+ }
155
+
156
+ @override
157
+ void dispose() {
158
+ _liveListStore.liveState.liveList.removeListener(_liveListListener);
159
+ _scrollController.dispose();
160
+ super.dispose();
161
+ }
162
+
163
+ @override
164
+ Widget build(BuildContext context) {
165
+ final liveList = _liveListStore.liveState.liveList.value;
166
+
167
+ return Scaffold(
168
+ appBar: AppBar(title: const Text('直播列表')),
169
+ body: RefreshIndicator(
170
+ onRefresh: _refresh,
171
+ child: liveList.isEmpty
172
+ ? Center(
173
+ child: _isLoading
174
+ ? const CircularProgressIndicator()
175
+ : const Text('暂无直播'),
176
+ )
177
+ : GridView.builder(
178
+ controller: _scrollController,
179
+ padding: const EdgeInsets.all(8),
180
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
181
+ crossAxisCount: 2,
182
+ childAspectRatio: 0.75,
183
+ crossAxisSpacing: 8,
184
+ mainAxisSpacing: 8,
185
+ ),
186
+ itemCount: liveList.length + (_isLoading ? 1 : 0),
187
+ itemBuilder: (context, index) {
188
+ if (index >= liveList.length) {
189
+ return const Center(child: CircularProgressIndicator());
190
+ }
191
+ return _buildLiveCard(liveList[index]);
192
+ },
193
+ ),
194
+ ),
195
+ );
196
+ }
197
+
198
+ Widget _buildLiveCard(LiveInfo liveInfo) {
199
+ return GestureDetector(
200
+ onTap: () => _onLiveCardTap(liveInfo),
201
+ child: Card(
202
+ clipBehavior: Clip.antiAlias,
203
+ child: Column(
204
+ crossAxisAlignment: CrossAxisAlignment.start,
205
+ children: [
206
+ // 封面图
207
+ Expanded(
208
+ child: Stack(
209
+ fit: StackFit.expand,
210
+ children: [
211
+ Image.network(
212
+ liveInfo.coverURL,
213
+ fit: BoxFit.cover,
214
+ errorBuilder: (context, error, stackTrace) {
215
+ return Container(color: Colors.grey[300]);
216
+ },
217
+ ),
218
+ // 直播中标签
219
+ Positioned(
220
+ top: 8,
221
+ left: 8,
222
+ child: Container(
223
+ padding: const EdgeInsets.symmetric(
224
+ horizontal: 6,
225
+ vertical: 2,
226
+ ),
227
+ decoration: BoxDecoration(
228
+ color: Colors.red,
229
+ borderRadius: BorderRadius.circular(4),
230
+ ),
231
+ child: const Text(
232
+ '直播中',
233
+ style: TextStyle(color: Colors.white, fontSize: 10),
234
+ ),
235
+ ),
236
+ ),
237
+ // 观看人数
238
+ Positioned(
239
+ bottom: 8,
240
+ right: 8,
241
+ child: Container(
242
+ padding: const EdgeInsets.symmetric(
243
+ horizontal: 6,
244
+ vertical: 2,
245
+ ),
246
+ decoration: BoxDecoration(
247
+ color: Colors.black54,
248
+ borderRadius: BorderRadius.circular(4),
249
+ ),
250
+ child: Row(
251
+ mainAxisSize: MainAxisSize.min,
252
+ children: [
253
+ const Icon(
254
+ Icons.visibility,
255
+ color: Colors.white,
256
+ size: 12,
257
+ ),
258
+ const SizedBox(width: 4),
259
+ Text(
260
+ '${liveInfo.totalViewerCount}',
261
+ style: const TextStyle(
262
+ color: Colors.white,
263
+ fontSize: 10,
264
+ ),
265
+ ),
266
+ ],
267
+ ),
268
+ ),
269
+ ),
270
+ ],
271
+ ),
272
+ ),
273
+ // 直播信息
274
+ Padding(
275
+ padding: const EdgeInsets.all(8),
276
+ child: Column(
277
+ crossAxisAlignment: CrossAxisAlignment.start,
278
+ children: [
279
+ Text(
280
+ liveInfo.liveName,
281
+ maxLines: 1,
282
+ overflow: TextOverflow.ellipsis,
283
+ style: const TextStyle(fontWeight: FontWeight.bold),
284
+ ),
285
+ const SizedBox(height: 4),
286
+ Row(
287
+ children: [
288
+ CircleAvatar(
289
+ radius: 10,
290
+ backgroundImage: NetworkImage(
291
+ liveInfo.liveOwner.avatarURL,
292
+ ),
293
+ ),
294
+ const SizedBox(width: 4),
295
+ Expanded(
296
+ child: Text(
297
+ liveInfo.liveOwner.userName,
298
+ maxLines: 1,
299
+ overflow: TextOverflow.ellipsis,
300
+ style: TextStyle(
301
+ fontSize: 12,
302
+ color: Colors.grey[600],
303
+ ),
304
+ ),
305
+ ),
306
+ ],
307
+ ),
308
+ ],
309
+ ),
310
+ ),
311
+ ],
312
+ ),
313
+ ),
314
+ );
315
+ }
316
+ }
317
+
318
+ /// 观众观看页面(占位)
319
+ class YourAudiencePage extends StatelessWidget {
320
+ final String liveID;
321
+
322
+ const YourAudiencePage({super.key, required this.liveID});
323
+
324
+ @override
325
+ Widget build(BuildContext context) {
326
+ return Scaffold(
327
+ appBar: AppBar(title: const Text('直播间')),
328
+ body: Center(child: Text('直播间: $liveID')),
329
+ );
330
+ }
331
+ }
332
+ ```
333
+
334
+ **LiveInfo 参数说明**
335
+ <table>
336
+ <tr>
337
+ <td rowspan="1" colSpan="1" >**参数名**</td>
338
+
339
+ <td rowspan="1" colSpan="1" >**类型**</td>
340
+
341
+ <td rowspan="1" colSpan="1" >**描述**</td>
342
+ </tr>
343
+
344
+ <tr>
345
+ <td rowspan="1" colSpan="1" >`liveID`</td>
346
+
347
+ <td rowspan="1" colSpan="1" >`String`</td>
348
+
349
+ <td rowspan="1" colSpan="1" >直播间的唯一标识符</td>
350
+ </tr>
351
+
352
+ <tr>
353
+ <td rowspan="1" colSpan="1" >`liveName`</td>
354
+
355
+ <td rowspan="1" colSpan="1" >`String`</td>
356
+
357
+ <td rowspan="1" colSpan="1" >直播间的标题</td>
358
+ </tr>
359
+
360
+ <tr>
361
+ <td rowspan="1" colSpan="1" >`coverURL`</td>
362
+
363
+ <td rowspan="1" colSpan="1" >`String`</td>
364
+
365
+ <td rowspan="1" colSpan="1" >直播间的封面图片地址</td>
366
+ </tr>
367
+
368
+ <tr>
369
+ <td rowspan="1" colSpan="1" >`liveOwner`</td>
370
+
371
+ <td rowspan="1" colSpan="1" >`LiveUserInfo`</td>
372
+
373
+ <td rowspan="1" colSpan="1" >房主的个人信息</td>
374
+ </tr>
375
+
376
+ <tr>
377
+ <td rowspan="1" colSpan="1" >`totalViewerCount`</td>
378
+
379
+ <td rowspan="1" colSpan="1" >`int`</td>
380
+
381
+ <td rowspan="1" colSpan="1" >直播间的总观看人数</td>
382
+ </tr>
383
+
384
+ <tr>
385
+ <td rowspan="1" colSpan="1" >`categoryList`</td>
386
+
387
+ <td rowspan="1" colSpan="1" >`List<int>`</td>
388
+
389
+ <td rowspan="1" colSpan="1" >直播间的分类标签列表</td>
390
+ </tr>
391
+
392
+ <tr>
393
+ <td rowspan="1" colSpan="1" >`notice`</td>
394
+
395
+ <td rowspan="1" colSpan="1" >`String`</td>
396
+
397
+ <td rowspan="1" colSpan="1" >直播间的公告信息</td>
398
+ </tr>
399
+
400
+ <tr>
401
+ <td rowspan="1" colSpan="1" >`metaData`</td>
402
+
403
+ <td rowspan="1" colSpan="1" >`Map<String, String>`</td>
404
+
405
+ <td rowspan="1" colSpan="1" >开发者自定义的元数据,用于实现复杂的业务场景</td>
406
+ </tr>
407
+ </table>
408
+
409
+
410
+ ## 功能进阶
411
+
412
+ ### 场景一:分类展示直播列表
413
+
414
+ 在 App 的直播广场页,顶部设有“热门”、“音乐”、“游戏”等分类标签。用户点击不同的标签后,下方的直播列表会动态筛选,只展示对应分类的直播间,从而帮助用户快速发现感兴趣的内容。
415
+
416
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/48a37652df3611f08304525400454e06.png)
417
+
418
+
419
+ #### 实现方式
420
+
421
+ 核心是利用 `LiveInfo` 模型中的 `categoryList` 属性。
422
+ 1. 当主播开播设置分类后,`fetchLiveList` 返回的 `LiveInfo` 对象中就会包含这些分类信息。
423
+
424
+ 2. 您的 App 在获取到完整的直播列表后,只需在客户端根据用户选择的分类,对这个列表进行一次简单的筛选,然后刷新 UI 即可。
425
+
426
+
427
+ #### 代码示例
428
+
429
+ 以下示例展示了如何创建一个 `LiveListManager` 来处理数据和筛选逻辑。
430
+ ``` java
431
+ import 'package:flutter/material.dart';
432
+ import 'package:atomic_x_core/atomicxcore.dart';
433
+
434
+ /// 直播列表数据管理器
435
+ class LiveListManager extends ChangeNotifier {
436
+ final _liveListStore = LiveListStore.shared;
437
+ List<LiveInfo> _fullLiveList = [];
438
+ List<LiveInfo> _filteredLiveList = [];
439
+ int? _selectedCategoryId;
440
+
441
+ List<LiveInfo> get filteredLiveList => _filteredLiveList;
442
+ int? get selectedCategoryId => _selectedCategoryId;
443
+
444
+ late final VoidCallback _liveListListener = _onLiveListChanged;
445
+
446
+ LiveListManager() {
447
+ _liveListStore.liveState.liveList.addListener(_liveListListener);
448
+ }
449
+
450
+ void _onLiveListChanged() {
451
+ _fullLiveList = _liveListStore.liveState.liveList.value;
452
+ _applyFilter();
453
+ }
454
+
455
+ Future<void> fetchFirstPage() async {
456
+ _liveListStore.reset();
457
+ await _liveListStore.fetchLiveList(cursor: '', count: 20);
458
+ }
459
+
460
+ /// 根据分类筛选直播列表
461
+ void filterLiveList({int? categoryId}) {
462
+ _selectedCategoryId = categoryId;
463
+ _applyFilter();
464
+ }
465
+
466
+ void _applyFilter() {
467
+ if (_selectedCategoryId == null) {
468
+ // 如果 categoryId 为 null,则显示完整列表
469
+ _filteredLiveList = List.from(_fullLiveList);
470
+ } else {
471
+ // 筛选包含指定分类的直播间
472
+ _filteredLiveList = _fullLiveList.where((liveInfo) {
473
+ return liveInfo.categoryList.contains(_selectedCategoryId);
474
+ }).toList();
475
+ }
476
+ notifyListeners();
477
+ }
478
+
479
+ @override
480
+ void dispose() {
481
+ _liveListStore.liveState.liveList.removeListener(_liveListListener);
482
+ super.dispose();
483
+ }
484
+ }
485
+ ```
486
+ ``` java
487
+ /// 带分类筛选的直播列表页面
488
+ class CategoryLiveListPage extends StatefulWidget {
489
+ const CategoryLiveListPage({super.key});
490
+
491
+ @override
492
+ State<CategoryLiveListPage> createState() => _CategoryLiveListPageState();
493
+ }
494
+
495
+ class _CategoryLiveListPageState extends State<CategoryLiveListPage> {
496
+ final _manager = LiveListManager();
497
+ late final VoidCallback _managerChangedListener = _onManagerChanged;
498
+
499
+ // 分类列表(示例)
500
+ final List<Map<String, dynamic>> _categories = [
501
+ {'id': null, 'name': '全部'},
502
+ {'id': 1, 'name': '音乐'},
503
+ {'id': 2, 'name': '游戏'},
504
+ {'id': 3, 'name': '聊天'},
505
+ ];
506
+
507
+ @override
508
+ void initState() {
509
+ super.initState();
510
+ _manager.addListener(_managerChangedListener);
511
+ _manager.fetchFirstPage();
512
+ }
513
+
514
+ void _onManagerChanged() {
515
+ setState(() {});
516
+ }
517
+
518
+ // 当用户点击顶部分类标签时
519
+ void _onCategoryTap(int? categoryId) {
520
+ _manager.filterLiveList(categoryId: categoryId);
521
+ }
522
+
523
+ @override
524
+ void dispose() {
525
+ _manager.removeListener(_managerChangedListener);
526
+ _manager.dispose();
527
+ super.dispose();
528
+ }
529
+
530
+ @override
531
+ Widget build(BuildContext context) {
532
+ return Scaffold(
533
+ appBar: AppBar(title: const Text('直播广场')),
534
+ body: Column(
535
+ children: [
536
+ // 分类标签栏
537
+ SizedBox(
538
+ height: 50,
539
+ child: ListView.builder(
540
+ scrollDirection: Axis.horizontal,
541
+ padding: const EdgeInsets.symmetric(horizontal: 8),
542
+ itemCount: _categories.length,
543
+ itemBuilder: (context, index) {
544
+ final category = _categories[index];
545
+ final isSelected = _manager.selectedCategoryId == category['id'];
546
+ return Padding(
547
+ padding: const EdgeInsets.symmetric(horizontal: 4),
548
+ child: ChoiceChip(
549
+ label: Text(category['name']),
550
+ selected: isSelected,
551
+ onSelected: (_) => _onCategoryTap(category['id']),
552
+ ),
553
+ );
554
+ },
555
+ ),
556
+ ),
557
+ // 直播列表
558
+ Expanded(
559
+ child: _manager.filteredLiveList.isEmpty
560
+ ? const Center(child: Text('暂无直播'))
561
+ : GridView.builder(
562
+ padding: const EdgeInsets.all(8),
563
+ gridDelegate:
564
+ const SliverGridDelegateWithFixedCrossAxisCount(
565
+ crossAxisCount: 2,
566
+ childAspectRatio: 0.75,
567
+ crossAxisSpacing: 8,
568
+ mainAxisSpacing: 8,
569
+ ),
570
+ itemCount: _manager.filteredLiveList.length,
571
+ itemBuilder: (context, index) {
572
+ final liveInfo = _manager.filteredLiveList[index];
573
+ return _buildLiveCard(liveInfo);
574
+ },
575
+ ),
576
+ ),
577
+ ],
578
+ ),
579
+ );
580
+ }
581
+
582
+ Widget _buildLiveCard(LiveInfo liveInfo) {
583
+ return Card(
584
+ clipBehavior: Clip.antiAlias,
585
+ child: Column(
586
+ crossAxisAlignment: CrossAxisAlignment.start,
587
+ children: [
588
+ Expanded(
589
+ child: Image.network(
590
+ liveInfo.coverURL,
591
+ fit: BoxFit.cover,
592
+ width: double.infinity,
593
+ errorBuilder: (context, error, stackTrace) {
594
+ return Container(color: Colors.grey[300]);
595
+ },
596
+ ),
597
+ ),
598
+ Padding(
599
+ padding: const EdgeInsets.all(8),
600
+ child: Text(
601
+ liveInfo.liveName,
602
+ maxLines: 1,
603
+ overflow: TextOverflow.ellipsis,
604
+ ),
605
+ ),
606
+ ],
607
+ ),
608
+ );
609
+ }
610
+ }
611
+ ```
612
+
613
+ ### 场景二:同步语聊房内自定义状态
614
+
615
+ 在语聊房中,主播可能需要向所有观众同步一些自定义信息,例如“当前房间话题”、“背景音乐信息”等。`LiveListStore` 的 `MetaData` 功能可以实现这一点。
616
+
617
+ #### 实现方式
618
+ 1. 主播端将自定义信息(建议使用 JSON 格式)通过 `updateLiveMetaData` 接口设置到一个或多个 key 中。`AtomicXCore` 会将这个变更实时同步给所有观众。
619
+
620
+ 2. 观众端只需订阅 `LiveListState.currentLive`,监听 `metaData` 的变化,一旦发现关心的 key 更新,就解析其 value 并刷新业务状态。
621
+
622
+
623
+ #### 代码示例
624
+ ``` java
625
+ import 'dart:convert';
626
+ import 'package:flutter/material.dart';
627
+ import 'package:atomic_x_core/atomicxcore.dart';
628
+
629
+ /// 背景音乐模型
630
+ class MusicModel {
631
+ final String musicId;
632
+ final String musicName;
633
+
634
+ MusicModel({required this.musicId, required this.musicName});
635
+
636
+ factory MusicModel.fromJson(Map<String, dynamic> json) {
637
+ return MusicModel(
638
+ musicId: json['musicId'] ?? '',
639
+ musicName: json['musicName'] ?? '',
640
+ );
641
+ }
642
+
643
+ Map<String, dynamic> toJson() {
644
+ return {
645
+ 'musicId': musicId,
646
+ 'musicName': musicName,
647
+ };
648
+ }
649
+ }
650
+
651
+ /// 主播端:推送背景音乐
652
+ class HostMusicController {
653
+ final _liveListStore = LiveListStore.shared;
654
+
655
+ Future<void> updateBackgroundMusic(MusicModel music) async {
656
+ final jsonString = jsonEncode(music.toJson());
657
+
658
+ // 将要更新的元数据
659
+ final metaData = {'music_info': jsonString};
660
+
661
+ // 使用全局单例的 liveListStore 来更新 metaData
662
+ final result = await _liveListStore.updateLiveMetaData(metaData);
663
+ if (result.isSuccess) {
664
+ debugPrint('背景音乐 ${music.musicName} 推送成功');
665
+ } else {
666
+ debugPrint('背景音乐推送失败: ${result.message}');
667
+ }
668
+ }
669
+ }
670
+
671
+ /// 观众端:订阅并响应背景音乐变化
672
+ class AudienceMusicWidget extends StatefulWidget {
673
+ const AudienceMusicWidget({super.key});
674
+
675
+ @override
676
+ State<AudienceMusicWidget> createState() => _AudienceMusicWidgetState();
677
+ }
678
+
679
+ class _AudienceMusicWidgetState extends State<AudienceMusicWidget> {
680
+ final _liveListStore = LiveListStore.shared;
681
+ MusicModel? _currentMusic;
682
+ String? _lastMusicInfo;
683
+ late final VoidCallback _currentLiveChangedListener = _onCurrentLiveChanged;
684
+
685
+ @override
686
+ void initState() {
687
+ super.initState();
688
+ _liveListStore.liveState.currentLive.addListener(_currentLiveChangedListener);
689
+ }
690
+
691
+ void _onCurrentLiveChanged() {
692
+ final currentLive = _liveListStore.liveState.currentLive.value;
693
+ final musicInfo = currentLive.metaData['music_info'];
694
+
695
+ // 检查是否有变化
696
+ if (musicInfo != _lastMusicInfo) {
697
+ _lastMusicInfo = musicInfo;
698
+ _parseMusicInfo(musicInfo);
699
+ }
700
+ }
701
+
702
+ void _parseMusicInfo(String? jsonString) {
703
+ if (jsonString == null || jsonString.isEmpty) {
704
+ setState(() => _currentMusic = null);
705
+ return;
706
+ }
707
+
708
+ try {
709
+ final json = jsonDecode(jsonString) as Map<String, dynamic>;
710
+ final music = MusicModel.fromJson(json);
711
+ setState(() => _currentMusic = music);
712
+ // 刷新业务状态,播放新的音乐
713
+ _playMusic(music);
714
+ } catch (e) {
715
+ debugPrint('解析音乐信息失败: $e');
716
+ }
717
+ }
718
+
719
+ void _playMusic(MusicModel music) {
720
+ // 实现播放音乐的逻辑
721
+ debugPrint('正在播放: ${music.musicName}');
722
+ }
723
+
724
+ @override
725
+ void dispose() {
726
+ _liveListStore.liveState.currentLive.removeListener(_currentLiveChangedListener);
727
+ super.dispose();
728
+ }
729
+
730
+ @override
731
+ Widget build(BuildContext context) {
732
+ return Container(
733
+ padding: const EdgeInsets.all(8),
734
+ decoration: BoxDecoration(
735
+ color: Colors.black54,
736
+ borderRadius: BorderRadius.circular(8),
737
+ ),
738
+ child: Row(
739
+ mainAxisSize: MainAxisSize.min,
740
+ children: [
741
+ const Icon(Icons.music_note, color: Colors.white, size: 16),
742
+ const SizedBox(width: 4),
743
+ Text(
744
+ _currentMusic?.musicName ?? '暂无音乐',
745
+ style: const TextStyle(color: Colors.white, fontSize: 12),
746
+ ),
747
+ ],
748
+ ),
749
+ );
750
+ }
751
+ }
752
+ ```
753
+
754
+ ## API 文档
755
+
756
+ 关于 [LiveListStore](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveListStore-class.html) 及其相关类的所有公开接口、属性和方法的详细信息,请参考 [AtomicXCore](https://tencent-rtc.github.io/TUIKit_Flutter) 框架的官方 API 文档。本文档使用到的相关 Store 如下:
757
+ <table>
758
+ <tr>
759
+ <td rowspan="1" colSpan="1" >**Store/Component**</td>
760
+
761
+ <td rowspan="1" colSpan="1" >**功能描述**</td>
762
+
763
+ <td rowspan="1" colSpan="1" >**API 文档**</td>
764
+ </tr>
765
+
766
+ <tr>
767
+ <td rowspan="1" colSpan="1" >LiveListStore</td>
768
+
769
+ <td rowspan="1" colSpan="1" >直播间全生命周期管理:创建 / 加入 / 离开 / 销毁房间,查询房间列表,修改直播信息(名称、公告等),监听直播状态(例如被踢出、结束)。</td>
770
+
771
+ <td rowspan="1" colSpan="1" >[API 文档](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveListStore-class.html)</td>
772
+ </tr>
773
+ </table>
774
+
775
+
776
+ ## 常见问题
777
+
778
+ ### 语聊房的列表和视频直播的列表数据是同一份吗?
779
+
780
+ 它们的数据是同一份,您不需要分开拉取。`LiveListStore` 是一个全局单例,它负责统一管理应用中所有“直播”房间的生命周期,无论是视频直播还是语聊房。
781
+
782
+ ### 直播列表中如何区分语聊房和视频直播?
783
+
784
+ `LiveListStore` 本身不区分房间的业务类型。您需要在获取列表后,在客户端的应用层(业务逻辑或 UI 层)进行筛选和分类。
785
+
786
+ 我们推荐采用以下两种方式进行区分:
787
+
788
+ **方式一(推荐):使用 seatLayoutTemplateID 区分**
789
+
790
+ 这是一个用于定义房间布局的模板 ID,关于目前已支持的模板 ID 和效果,请参考文档 [开始直播-运行效果](https://write.woa.com/document/197478016886185984)。您要在创建房间时指定 ID,然后在获取列表时根据 ID 范围来识别业务场景。
791
+ - **步骤1:创建房间时指定 ID**
792
+
793
+
794
+ 调用 `LiveListStore.shared.createLive` 时,您需要根据业务场景为 `LiveInfo` 的 `seatLayoutTemplateID` 属性传入指定范围的 ID:
795
+
796
+ - 语聊房场景:传入 1-199 范围内的模板 ID。
797
+
798
+ - 视频直播场景:传入 200-999 范围内的模板 ID。
799
+
800
+ - **步骤2:获取列表时筛选**
801
+
802
+
803
+ 客户端在 `liveState.liveList` 中收到列表数据后,通过判断这个 ID 值所属的范围来区分,从而在进入直播间的时候区分两种业务场景。
804
+
805
+
806
+ > **重要提示:**
807
+ >
808
+
809
+ > 创建房间时,如果传入的 `seatLayoutTemplateID` 范围与您的业务场景(语聊房、视频直播)不匹配,可能会导致麦位布局出现功能异常。
810
+ >
811
+
812
+
813
+ **方式二:为 **`liveID`** 添加业务前缀**
814
+
815
+
816
+ 这是一种可选的、纯粹的应用层约定,用于辅助您在客户端快速筛选。
817
+
818
+ - **步骤1:创建房间时添加前缀**
819
+
820
+
821
+ 在您生成 `liveID` 并调用 `createLive` 时,为不同业务类型的 `liveID` 赋予不同的前缀。例如:视频直播的 ID 以 “Live_” 开头 (例如:Live_12345),语聊房的 ID 以 “voice_” 开头 (例如:voice_67890)。
822
+
823
+ - **步骤2:获取列表时检查前缀**
824
+
825
+
826
+ 客户端在拉取到列表后,通过检查 `liveID` 字符串的前缀来进行区分。
827
+
828
+
829
+ ### 使用 `updateLiveMetaData` 注意事项
830
+
831
+ 为了保证系统的稳定和高效,`metaData` 的使用遵循以下规则:
832
+ - **权限**:只有房主和管理员可以调用 `updateLiveMetaData`。普通观众没有权限。
833
+
834
+ - **数量与大小限制:**
835
+
836
+ - 单个房间最多支持 **10** 个 key。
837
+
838
+ - 每个 key 的长度不超过 **50 字节**,每个 value 的长度不超过 **2KB**。
839
+
840
+ - 单个房间所有 value 的总大小不超过 **16KB**。
841
+
842
+ - **冲突解决**:`metaData` 的更新机制是“后来者覆盖”。如果多个管理员在短时间内修改同一个 key,最后一次的修改会生效。建议在业务设计上避免多人同时修改同一个关键信息。