@tencentcloud/sdk-mcp 1.3.4 → 1.3.5

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 (105) hide show
  1. package/package.json +1 -1
  2. package/resource/doc/chatuikit/android/features/theme.md +363 -0
  3. package/resource/doc/chatuikit/android/workflow.md +0 -0
  4. package/resource/doc/chatuikit/{android/client_api_android.md → faq/android_sdk/client-api.md} +0 -1
  5. package/resource/doc/chatuikit/faq/{native_sdk/faq.md → common/product.md} +8 -0
  6. package/resource/doc/chatuikit/faq/flutter_sdk/faq.md +54 -0
  7. package/resource/doc/chatuikit/faq/web_sdk/faq.md +1 -1
  8. package/resource/doc/chatuikit/flutter/features/theme.md +379 -0
  9. package/resource/doc/chatuikit/flutter/workflow.md +816 -0
  10. package/resource/doc/chatuikit/iOS/features/theme.md +416 -0
  11. package/resource/doc/chatuikit/iOS/workflow.md +0 -0
  12. package/resource/doc/chatuikit/react/components/chat-setting.md +124 -0
  13. package/resource/doc/chatuikit/react/components/contact-list.md +1331 -0
  14. package/resource/doc/chatuikit/react/components/search.md +885 -0
  15. package/resource/doc/chatuikit/vue/components/chat-setting.md +88 -0
  16. package/resource/doc/chatuikit/vue/components/contact-list.md +679 -0
  17. package/resource/doc/chatuikit/vue/components/search.md +1384 -0
  18. package/resource/doc/livekit/live/android/workflow.md +137 -4
  19. package/resource/doc/livekit/live/flutter/live-audience-list.md +337 -0
  20. package/resource/doc/livekit/live/flutter/live-audio-effect.md +739 -0
  21. package/resource/doc/livekit/live/flutter/live-barrage.md +578 -0
  22. package/resource/doc/livekit/live/flutter/live-base-beauty.md +484 -0
  23. package/resource/doc/livekit/live/flutter/live-co-guest-interaction.md +606 -0
  24. package/resource/doc/livekit/live/flutter/live-gift.md +812 -0
  25. package/resource/doc/livekit/live/flutter/live-host-interaction.md +1113 -0
  26. package/resource/doc/livekit/live/flutter/live-integration.md +1029 -0
  27. package/resource/doc/livekit/live/flutter/live-live-list.md +861 -0
  28. package/resource/doc/livekit/live/ios/live-audience-list.md +335 -0
  29. package/resource/doc/livekit/live/ios/live-audio-effect.md +373 -0
  30. package/resource/doc/livekit/live/ios/live-barrage.md +529 -0
  31. package/resource/doc/livekit/live/ios/live-base-beauty.md +237 -0
  32. package/resource/doc/livekit/live/ios/live-co-guest-interaction.md +483 -0
  33. package/resource/doc/livekit/live/ios/live-gift.md +785 -0
  34. package/resource/doc/livekit/live/ios/live-host-interaction.md +743 -0
  35. package/resource/doc/livekit/live/ios/live-integration.md +822 -0
  36. package/resource/doc/livekit/live/ios/live-live-list.md +535 -0
  37. package/resource/doc/livekit/live/ios/workflow.md +352 -0
  38. package/resource/doc/rtcengine/faq/web/web.md +2 -2
  39. package/resource/doc/rtcengine/web/how_to_integrated_web_sdk.md +2 -2
  40. package/resource/doc/rtcengine/web/plugin/20-advanced-customized-capture-rendering.md +84 -0
  41. package/resource/doc/rtcengine/web/plugin/21-advanced-auto-play-policy.md +86 -0
  42. package/resource/doc/rtcengine/web/plugin/22-advanced-audio-mixer.md +220 -0
  43. package/resource/doc/rtcengine/web/plugin/23-advanced-support-detection.md +217 -0
  44. package/resource/doc/rtcengine/web/plugin/24-advanced-network-quality.md +137 -0
  45. package/resource/doc/rtcengine/web/plugin/25-advanced-device-change.md +97 -0
  46. package/resource/doc/rtcengine/web/plugin/26-advanced-publish-cdn-stream.md +453 -0
  47. package/resource/doc/rtcengine/web/plugin/27-advanced-small-stream.md +180 -0
  48. package/resource/doc/rtcengine/web/plugin/28-advanced-beauty.md +349 -0
  49. package/resource/doc/rtcengine/web/plugin/29-advanced-water-mark.md +149 -0
  50. package/resource/doc/rtcengine/web/plugin/30-advanced-cross-room-link.md +96 -0
  51. package/resource/doc/rtcengine/web/plugin/31-advanced-plugins-overview.md +405 -0
  52. package/resource/doc/rtcengine/web/plugin/34-advanced-proxy.md +244 -0
  53. package/resource/doc/rtcengine/web/plugin/35-advanced-ai-denoiser.md +84 -0
  54. package/resource/doc/rtcengine/web/plugin/36-advanced-virtual-background.md +202 -0
  55. package/resource/doc/rtcengine/web/plugin/37-advanced-voice-changer.md +98 -0
  56. package/resource/doc/rtcengine/web/plugin/38-advanced-basic-beauty.md +95 -0
  57. package/resource/doc/rtcengine/web/plugin/39-advanced-video-decoder.md +33 -0
  58. package/resource/doc/rtcengine/web/plugin/40-advanced-video-mixer.md +380 -0
  59. package/resource/doc/rtcengine/web/plugin/41-advanced-small-stream-auto-switcher.md +91 -0
  60. package/resource/doc/rtcengine/web/plugin/42-advanced-realtime-transcriber.md +314 -0
  61. package/resource/doc/rtcengine/web/plugin/43-advanced-face-detection.md +180 -0
  62. package/tools/chat/get-native-chat-feature-detail.js +30 -0
  63. package/tools/chat/get-native-chat-feature-detail.js.map +1 -0
  64. package/tools/chat/get-native-chat-integration.js +30 -12
  65. package/tools/chat/get-native-chat-integration.js.map +1 -1
  66. package/tools/common/get-faq.js +2 -4
  67. package/tools/common/get-faq.js.map +1 -1
  68. package/tools/definitions/get_native_chat_feature_detail_def.js +32 -0
  69. package/tools/definitions/get_native_chat_feature_detail_def.js.map +1 -0
  70. package/tools/definitions/get_native_chat_uikit_integration_def.js +18 -6
  71. package/tools/definitions/get_native_chat_uikit_integration_def.js.map +1 -1
  72. package/tools/definitions/{get_native_live_extra_features_def.js → get_native_live_uikit_extra_features_def.js} +3 -3
  73. package/tools/definitions/get_native_live_uikit_extra_features_def.js.map +1 -0
  74. package/tools/definitions/{get_native_live_interaction_def.js → get_native_live_uikit_interaction_def.js} +3 -3
  75. package/tools/definitions/get_native_live_uikit_interaction_def.js.map +1 -0
  76. package/tools/definitions/get_web_chat_uikit_component_detail_def.js +46 -7
  77. package/tools/definitions/get_web_chat_uikit_component_detail_def.js.map +1 -1
  78. package/tools/index.js +6 -4
  79. package/tools/index.js.map +1 -1
  80. package/tools/live/{get_native_live_extra_features.js → get_native_live_uikit_extra_features.js} +4 -4
  81. package/tools/live/get_native_live_uikit_extra_features.js.map +1 -0
  82. package/tools/live/{get_native_live_interaction.js → get_native_live_uikit_interaction.js} +4 -4
  83. package/tools/live/get_native_live_uikit_interaction.js.map +1 -0
  84. package/tools/rtcengine/get-trtc-web-sdk-faq.js +1 -1
  85. package/tools/rtcengine/get-trtc-web-sdk-plugin.js +45 -0
  86. package/tools/rtcengine/get-trtc-web-sdk-plugin.js.map +1 -0
  87. package/utils/handle-component-name.js +0 -16
  88. package/utils/handle-component-name.js.map +1 -1
  89. package/utils/report-cls-client.js +1 -1
  90. package/utils/report-es-client.js +1 -1
  91. package/resource/doc/chatuikit/android/integrate_tuichat_only_android.md +0 -382
  92. package/resource/doc/chatuikit/android/ui_api_android.md +0 -418
  93. package/resource/doc/chatuikit/iOS/integrate_tuichat_only_iOS.md +0 -993
  94. package/resource/doc/chatuikit/iOS/ui_api_iOS.md +0 -526
  95. package/tools/definitions/get_native_live_extra_features_def.js.map +0 -1
  96. package/tools/definitions/get_native_live_interaction_def.js.map +0 -1
  97. package/tools/live/get_native_live_extra_features.js.map +0 -1
  98. package/tools/live/get_native_live_interaction.js.map +0 -1
  99. /package/resource/doc/chatuikit/android/{integrate_tuikit_android.md → integration.md} +0 -0
  100. /package/resource/doc/chatuikit/{flutter/client_api_Flutter.md → faq/flutter_sdk/client-api.md} +0 -0
  101. /package/resource/doc/chatuikit/{iOS/client_api_iOS.md → faq/ios_sdk/client-api.md} +0 -0
  102. /package/resource/doc/chatuikit/flutter/{integrate_tuikit_Flutter.md → integration.md} +0 -0
  103. /package/resource/doc/chatuikit/iOS/{integrate_tuikit_iOS.md → integration.md} +0 -0
  104. /package/resource/doc/chatuikit/react/components/{chatheader.md → chat-header.md} +0 -0
  105. /package/resource/doc/chatuikit/vue/components/{chatheader.md → chat-header.md} +0 -0
@@ -0,0 +1,1113 @@
1
+ **AtomicXCore** 提供了 [CoHostStore](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_co_host_store/CoHostStore-class.html) 和 [BattleStore](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_battle_store/BattleStore-class.html) 两个核心模块,分别用于处理**跨房连线和 PK 对战**。本文档将指导您如何组合使用这两个工具,来完成直播场景下连线到 PK 的完整流程。
2
+
3
+ ## 核心场景
4
+
5
+ 一次完整的"主播连线 PK"通常包含三个核心阶段,其整体流程如下:
6
+
7
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/0d40bdeadcbe11f08d525254001c06ec.png)
8
+
9
+
10
+ ## 实现步骤
11
+
12
+ ### 步骤1:组件集成
13
+
14
+ 请参考 [开始直播](https://write.woa.com/document/191126413785100288) 集成 **AtomicXCore**,并完成 **LiveCoreWidget** 的接入。
15
+
16
+ ### 步骤2:实现跨房连线
17
+
18
+ 此步骤的目标是让两个主播的画面出现在同一个视图中,我们将使用 [CoHostStore](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_co_host_store/api_live_co_host_store-library.html) 来完成。
19
+
20
+ #### 邀请方(主播 A)实现
21
+ 1. **发起连线邀请**
22
+
23
+
24
+ 当主播A在界面上选择目标主播 B 并发起连线时,调用 `requestHostConnection` 方法。
25
+
26
+ ``` java
27
+ import 'package:atomic_x_core/atomicxcore.dart';
28
+
29
+ // 主播A的页面
30
+ class AnchorAPage extends StatefulWidget {
31
+ final String liveId;
32
+
33
+ const AnchorAPage({Key? key, required this.liveId}) : super(key: key);
34
+
35
+ @override
36
+ State<AnchorAPage> createState() => _AnchorAPageState();
37
+ }
38
+
39
+ class _AnchorAPageState extends State<AnchorAPage> {
40
+ late final CoHostStore _coHostStore;
41
+ late final CoHostListener _coHostListener;
42
+
43
+ @override
44
+ void initState() {
45
+ super.initState();
46
+ _coHostStore = CoHostStore.create(widget.liveId);
47
+ _setupListeners();
48
+ }
49
+
50
+ // 用户点击"连线"按钮,并选择了主播B
51
+ Future<void> inviteHostB(String targetHostLiveId) async {
52
+ final layout = CoHostLayoutTemplate.hostDynamicGrid; // 选择一个布局模板
53
+ const timeout = 30; // 邀请超时时间(秒)
54
+
55
+ final result = await _coHostStore.requestHostConnection(
56
+ targetHostLiveID: targetHostLiveId,
57
+ layoutTemplate: layout,
58
+ timeout: timeout,
59
+ );
60
+
61
+ if (result.isSuccess) {
62
+ print('连线邀请已发送,等待对方处理...');
63
+ } else {
64
+ print('邀请发送失败: ${result.errorMessage}');
65
+ }
66
+ }
67
+
68
+ @override
69
+ void dispose() {
70
+ _coHostStore.removeCoHostListener(_coHostListener);
71
+ super.dispose();
72
+ }
73
+ }
74
+ ```
75
+ 2. **监听邀请结果**
76
+
77
+
78
+ 通过 `CoHostListener`,您可以接收到主播 B 的处理结果。
79
+
80
+ ``` java
81
+ // 在 _AnchorAPageState 初始化时设置监听
82
+ void _setupListeners() {
83
+ _coHostListener = CoHostListener(
84
+ onCoHostRequestAccepted: (invitee) {
85
+ print('主播 ${invitee.userName} 同意了你的连线邀请');
86
+ },
87
+ onCoHostRequestRejected: (invitee) {
88
+ print('主播 ${invitee.userName} 拒绝了你的邀请');
89
+ },
90
+ onCoHostRequestTimeout: (inviter, invitee) {
91
+ print('邀请超时,对方未回应');
92
+ },
93
+ onCoHostUserJoined: (userInfo) {
94
+ print('主播 ${userInfo.userName} 已加入连线');
95
+ },
96
+ onCoHostUserLeft: (userInfo) {
97
+ print('主播 ${userInfo.userName} 已离开连线');
98
+ },
99
+ );
100
+ _coHostStore.addCoHostListener(_coHostListener);
101
+ }
102
+ ```
103
+
104
+ #### 受邀方(主播 B)实现
105
+ 1. **接收连线邀请**
106
+
107
+
108
+ 通过 `CoHostListener`,主播B可以监听到来自主播 A 的邀请。
109
+
110
+ ``` java
111
+ import 'package:atomic_x_core/atomicxcore.dart';
112
+
113
+ // 主播B的页面
114
+ class AnchorBPage extends StatefulWidget {
115
+ final String liveId;
116
+
117
+ const AnchorBPage({Key? key, required this.liveId}) : super(key: key);
118
+
119
+ @override
120
+ State<AnchorBPage> createState() => _AnchorBPageState();
121
+ }
122
+
123
+ class _AnchorBPageState extends State<AnchorBPage> {
124
+ late final CoHostStore _coHostStore;
125
+ late final CoHostListener _coHostListener;
126
+
127
+ @override
128
+ void initState() {
129
+ super.initState();
130
+ _coHostStore = CoHostStore.create(widget.liveId);
131
+ _setupListeners();
132
+ }
133
+
134
+ // 在初始化时设置监听
135
+ void _setupListeners() {
136
+ _coHostListener = CoHostListener(
137
+ onCoHostRequestReceived: (inviter, extensionInfo) {
138
+ print('收到主播 ${inviter.userName} 的连线邀请');
139
+ // _showInvitationDialog(inviter);
140
+ },
141
+ );
142
+ _coHostStore.addCoHostListener(_coHostListener);
143
+ }
144
+
145
+ @override
146
+ void dispose() {
147
+ _coHostStore.removeCoHostListener(_coHostListener);
148
+ super.dispose();
149
+ }
150
+ }
151
+ ```
152
+ 2. **响应连线邀请**
153
+
154
+
155
+ 当主播 B 在弹出的对话框中做出选择后,调用相应的方法。
156
+
157
+ ``` java
158
+ // _AnchorBPageState 的一部分
159
+ Future<void> acceptInvitation(String fromHostLiveId) async {
160
+ final result = await _coHostStore.acceptHostConnection(fromHostLiveId);
161
+ if (result.isSuccess) {
162
+ print('已接受连线邀请');
163
+ } else {
164
+ print('接受连线失败: ${result.errorMessage}');
165
+ }
166
+ }
167
+
168
+ Future<void> rejectInvitation(String fromHostLiveId) async {
169
+ final result = await _coHostStore.rejectHostConnection(fromHostLiveId);
170
+ if (result.isSuccess) {
171
+ print('已拒绝连线邀请');
172
+ } else {
173
+ print('拒绝连线失败: ${result.errorMessage}');
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### 步骤3:实现主播 PK
179
+
180
+ 连线成功后,任意一方都可以发起 PK,此步骤我们将使用 [BattleStore](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_battle_store/BattleStore-class.html) 来实现主播 PK。
181
+
182
+ #### 挑战方(例如主播 A)实现
183
+ 1. **发起 PK 挑战**
184
+
185
+
186
+ 当主播 A 点击"PK"按钮时,调用 `requestBattle` 方法。
187
+
188
+ ``` java
189
+ // _AnchorAPageState 的一部分
190
+ late final BattleStore _battleStore;
191
+ late final BattleListener _battleListener;
192
+
193
+ @override
194
+ void initState() {
195
+ super.initState();
196
+ _coHostStore = CoHostStore.create(widget.liveId);
197
+ _battleStore = BattleStore.create(widget.liveId);
198
+ _setupListeners();
199
+ _setupBattleListeners();
200
+ }
201
+
202
+ Future<void> startPK(String opponentUserId) async {
203
+ final config = BattleConfig(duration: 300); // PK 持续 5 分钟
204
+ final result = await _battleStore.requestBattle(
205
+ config: config,
206
+ userIDList: [opponentUserId],
207
+ timeout: 30,
208
+ );
209
+
210
+ if (result.isSuccess) {
211
+ print('PK 请求已发送,battleID: ${result.battleID}');
212
+ } else {
213
+ print('PK 请求失败: ${result.errorMessage}');
214
+ }
215
+ }
216
+ ```
217
+ 2. **监听 PK 状态**
218
+
219
+
220
+ 通过 `BattleListener` 监听 PK 的开始、结束等关键事件。
221
+
222
+ ``` java
223
+ // 在 _AnchorAPageState 的 _setupBattleListeners 方法中添加
224
+ void _setupBattleListeners() {
225
+ _battleListener = BattleListener(
226
+ onBattleStarted: (battleInfo, inviter, invitees) {
227
+ print('PK 开始');
228
+ },
229
+ onBattleEnded: (battleInfo, reason) {
230
+ print('PK 结束,原因: $reason');
231
+ },
232
+ onUserJoinBattle: (battleID, battleUser) {
233
+ print('用户 ${battleUser.userName} 加入了 PK');
234
+ },
235
+ onUserExitBattle: (battleID, battleUser) {
236
+ print('用户 ${battleUser.userName} 退出了 PK');
237
+ },
238
+ );
239
+ _battleStore.addBattleListener(_battleListener);
240
+ }
241
+
242
+ @override
243
+ void dispose() {
244
+ _coHostStore.removeCoHostListener(_coHostListener);
245
+ _battleStore.removeBattleListener(_battleListener);
246
+ super.dispose();
247
+ }
248
+ ```
249
+
250
+ #### 应战方(主播 B)实现
251
+ 1. **接收 PK 挑战**
252
+
253
+
254
+ 通过 `BattleListener` 监听到 PK 邀请。
255
+
256
+ ``` java
257
+ // 在 _AnchorBPageState 的 _setupBattleListeners 方法中添加
258
+ void _setupBattleListeners() {
259
+ _battleListener = BattleListener(
260
+ onBattleRequestReceived: (battleId, inviter, invitee) {
261
+ print('收到主播 ${inviter.userName} 的PK挑战');
262
+ // 弹出对话框,让主播B选择"接受"或"拒绝"
263
+ // _showPKChallengeDialog(battleId);
264
+ },
265
+ onBattleStarted: (battleInfo, inviter, invitees) {
266
+ print('PK 开始');
267
+ },
268
+ onBattleEnded: (battleInfo, reason) {
269
+ print('PK 结束');
270
+ },
271
+ );
272
+ _battleStore.addBattleListener(_battleListener);
273
+ }
274
+ ```
275
+ 2. **响应 PK 挑战**
276
+
277
+
278
+ 当主播 B 做出选择后,调用相应的方法。
279
+
280
+ ``` java
281
+ // _AnchorBPageState 的一部分
282
+ // 用户点击"接受挑战"
283
+ Future<void> acceptPK(String battleId) async {
284
+ final result = await _battleStore.acceptBattle(battleId);
285
+ if (result.isSuccess) {
286
+ print('已接受 PK 挑战');
287
+ } else {
288
+ print('接受 PK 失败: ${result.errorMessage}');
289
+ }
290
+ }
291
+
292
+ // 用户点击"拒绝挑战"
293
+ Future<void> rejectPK(String battleId) async {
294
+ final result = await _battleStore.rejectBattle(battleId);
295
+ if (result.isSuccess) {
296
+ print('已拒绝 PK 挑战');
297
+ } else {
298
+ print('拒绝 PK 失败: ${result.errorMessage}');
299
+ }
300
+ }
301
+ ```
302
+
303
+ ### 运行效果
304
+
305
+ 当您集成以上功能实现后,请分别使用主播 A 和主播 B 进行对应操作,运行效果如下,您可以参考下一章节 [完善 UI 细节](https://write.woa.com/#.E5.AE.8C.E5.96.84-ui-.E7.BB.86.E8.8A.82) 来定制 UI 逻辑。
306
+
307
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/6f74f761dcbe11f08d525254001c06ec.png)
308
+
309
+
310
+ ## 完善 UI 细节
311
+
312
+ 您可以通过 `LiveCoreWidget` 的 `VideoWidgetBuilder` 参数提供的"插槽"能力,在视频流画面上添加自定义视图,用于显示昵称、头像、PK 进度条等信息,或在他们关闭摄像头时提供占位图,以优化视觉体验。
313
+
314
+ ### 实现视频流画面的昵称显示
315
+
316
+ #### 实现效果
317
+
318
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/79cbcbbedcbe11f08304525400454e06.png)
319
+
320
+
321
+ #### 实现方式
322
+ - **步骤1**:创建前景视图 (**CustomCoHostForegroundView**),该视图用于在视频流上方显示用户信息。
323
+
324
+
325
+ > **提示:**
326
+ >
327
+
328
+ > 您也可以参考 TUILiveKit 开源项目中的 [co_host_foreground_widget.dart](https://github.com/Tencent-RTC/TUIKit_Flutter/blob/main/live/livekit/lib/live_stream/features/decorations/co_host/co_host_foreground_widget.dart) 和 [co_host_background_widget.dart](https://github.com/Tencent-RTC/TUIKit_Flutter/blob/main/live/livekit/lib/live_stream/features/decorations/co_host/co_host_background_widget.dart) 文件来了解完整的实现逻辑。
329
+ >
330
+
331
+ ``` java
332
+ import 'package:flutter/material.dart';
333
+ import 'package:rtc_room_engine/rtc_room_engine.dart';
334
+
335
+ /// 自定义的连线主播信息悬浮视图(前景)
336
+ class CustomCoHostForegroundView extends StatelessWidget {
337
+ final SeatFullInfo seatInfo;
338
+
339
+ const CustomCoHostForegroundView({
340
+ Key? key,
341
+ required this.seatInfo,
342
+ }) : super(key: key);
343
+
344
+ @override
345
+ Widget build(BuildContext context) {
346
+ return Container(
347
+ color: Colors.transparent,
348
+ child: Align(
349
+ alignment: Alignment.bottomLeft,
350
+ child: Container(
351
+ margin: const EdgeInsets.all(5.0),
352
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
353
+ decoration: BoxDecoration(
354
+ color: Colors.black.withOpacity(0.5),
355
+ borderRadius: BorderRadius.circular(12),
356
+ ),
357
+ child: Text(
358
+ seatInfo.userInfo.userName,
359
+ style: const TextStyle(
360
+ color: Colors.white,
361
+ fontSize: 14,
362
+ ),
363
+ ),
364
+ ),
365
+ ),
366
+ );
367
+ }
368
+ }
369
+ ```
370
+ - **步骤2**:创建背景视图 (**CustomCoHostBackgroundView**),该视图用于在用户无视频流时作为占位图显示。
371
+
372
+ ``` java
373
+ import 'package:flutter/material.dart';
374
+ import 'package:rtc_room_engine/rtc_room_engine.dart';
375
+
376
+ /// 自定义的连线主播头像占位视图(背景)
377
+ class CustomCoHostBackgroundView extends StatelessWidget {
378
+ final SeatFullInfo seatInfo;
379
+
380
+ const CustomCoHostBackgroundView({
381
+ Key? key,
382
+ required this.seatInfo,
383
+ }) : super(key: key);
384
+
385
+ @override
386
+ Widget build(BuildContext context) {
387
+ final avatarUrl = seatInfo.userInfo.avatarUrl;
388
+ return Container(
389
+ decoration: BoxDecoration(
390
+ color: Colors.grey[800],
391
+ ),
392
+ child: Center(
393
+ child: Column(
394
+ mainAxisSize: MainAxisSize.min,
395
+ children: [
396
+ ClipOval(
397
+ child: avatarUrl.isNotEmpty
398
+ ? Image.network(
399
+ avatarUrl,
400
+ width: 60,
401
+ height: 60,
402
+ fit: BoxFit.cover,
403
+ errorBuilder: (context, error, stackTrace) {
404
+ return _buildDefaultAvatar();
405
+ },
406
+ )
407
+ : _buildDefaultAvatar(),
408
+ ),
409
+ const SizedBox(height: 8),
410
+ Text(
411
+ seatInfo.userInfo.userName,
412
+ style: const TextStyle(
413
+ color: Colors.white,
414
+ fontSize: 12,
415
+ ),
416
+ ),
417
+ ],
418
+ ),
419
+ ),
420
+ );
421
+ }
422
+
423
+ Widget _buildDefaultAvatar() {
424
+ return Container(
425
+ width: 60,
426
+ height: 60,
427
+ color: Colors.grey,
428
+ child: const Icon(Icons.person, size: 40, color: Colors.white),
429
+ );
430
+ }
431
+ }
432
+ ```
433
+ - **步骤3**:通过 `VideoWidgetBuilder` 的 `coHostWidgetBuilder` 回调构建自定义视图,根据 `viewLayer` 的值返回对应的视图。
434
+
435
+ ``` java
436
+ import 'package:flutter/material.dart';
437
+ import 'package:atomic_x_core/atomicxcore.dart';
438
+ import 'package:rtc_room_engine/rtc_room_engine.dart';
439
+
440
+ /// 带有自定义连线视图的直播页面
441
+ class CustomCoHostLiveWidget extends StatefulWidget {
442
+ final String liveId;
443
+
444
+ const CustomCoHostLiveWidget({
445
+ Key? key,
446
+ required this.liveId,
447
+ }) : super(key: key);
448
+
449
+ @override
450
+ State<CustomCoHostLiveWidget> createState() => _CustomCoHostLiveWidgetState();
451
+ }
452
+
453
+ class _CustomCoHostLiveWidgetState extends State<CustomCoHostLiveWidget> {
454
+ late LiveCoreController _controller;
455
+
456
+ @override
457
+ void initState() {
458
+ super.initState();
459
+ _controller = LiveCoreController.create();
460
+ _controller.setLiveID(widget.liveId);
461
+ }
462
+
463
+ @override
464
+ void dispose() {
465
+ _controller.dispose();
466
+ super.dispose();
467
+ }
468
+
469
+ /// 构建连线主播的自定义视图
470
+ Widget _buildCoHostWidget(
471
+ BuildContext context,
472
+ SeatFullInfo seatFullInfo,
473
+ ViewLayer viewLayer,
474
+ ) {
475
+ if (viewLayer == ViewLayer.foreground) {
476
+ // 前景层:始终显示在视频画面的最上层,用于显示昵称等信息
477
+ return CustomCoHostForegroundView(seatInfo: seatFullInfo);
478
+ } else {
479
+ // 背景层:仅在对应用户没有视频流时显示,用于显示头像占位图
480
+ return CustomCoHostBackgroundView(seatInfo: seatFullInfo);
481
+ }
482
+ }
483
+
484
+ @override
485
+ Widget build(BuildContext context) {
486
+ return Scaffold(
487
+ body: LiveCoreWidget(
488
+ controller: _controller,
489
+ videoWidgetBuilder: VideoWidgetBuilder(
490
+ coHostWidgetBuilder: _buildCoHostWidget,
491
+ ),
492
+ ),
493
+ );
494
+ }
495
+ }
496
+ ```
497
+
498
+ #### **参数说明**:
499
+ <table>
500
+ <tr>
501
+ <td rowspan="1" colSpan="1" >**参数**</td>
502
+
503
+ <td rowspan="1" colSpan="1" >**类型**</td>
504
+
505
+ <td rowspan="1" colSpan="1" >**说明**</td>
506
+ </tr>
507
+
508
+ <tr>
509
+ <td rowspan="1" colSpan="1" >`seatFullInfo`</td>
510
+
511
+ <td rowspan="1" colSpan="1" >`SeatFullInfo`</td>
512
+
513
+ <td rowspan="1" colSpan="1" >麦位信息对象,包含麦上用户的详细信息</td>
514
+ </tr>
515
+
516
+ <tr>
517
+ <td rowspan="1" colSpan="1" >`seatFullInfo.userInfo.userId`</td>
518
+
519
+ <td rowspan="1" colSpan="1" >`String`</td>
520
+
521
+ <td rowspan="1" colSpan="1" >麦上用户的 ID</td>
522
+ </tr>
523
+
524
+ <tr>
525
+ <td rowspan="1" colSpan="1" >`seatFullInfo.userInfo.userName`</td>
526
+
527
+ <td rowspan="1" colSpan="1" >`String`</td>
528
+
529
+ <td rowspan="1" colSpan="1" >麦上用户的昵称</td>
530
+ </tr>
531
+
532
+ <tr>
533
+ <td rowspan="1" colSpan="1" >`seatFullInfo.userInfo.avatarUrl`</td>
534
+
535
+ <td rowspan="1" colSpan="1" >`String`</td>
536
+
537
+ <td rowspan="1" colSpan="1" >麦上用户的头像 URL</td>
538
+ </tr>
539
+
540
+ <tr>
541
+ <td rowspan="1" colSpan="1" >`viewLayer`</td>
542
+
543
+ <td rowspan="1" colSpan="1" >`ViewLayer`</td>
544
+
545
+ <td rowspan="1" colSpan="1" >视图层级枚举<br>`ViewLayer.foreground` 表示前景挂件视图,始终显示在视频画面的最上层<br>`ViewLayer.background` 表示背景挂件视图,位于前景视图下层,仅在对应用户没有视频流(例如未开摄像头)的情况下显示,通常用于展示用户的默认头像或占位图</td>
546
+ </tr>
547
+ </table>
548
+
549
+
550
+ ### 实现 PK 用户视图的分数展示
551
+
552
+ 当主播开始 PK 后,可以在对方主播的视频画面上挂载自定义视图,通常用于展示该主播收到的礼物价值或其它 PK 相关信息。
553
+
554
+ #### **实现效果**
555
+
556
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/8c97a64fe4c911f0bbb1525400454e06.png)
557
+
558
+
559
+ #### 实现方式
560
+ - **步骤1**:创建自定义 PK 用户视图,您可以参考 TUILiveKit 开源项目中的 [battle_member_info_widget.dart](https://github.com/Tencent-RTC/TUIKit_Flutter/blob/main/live/livekit/lib/live_stream/features/decorations/battle/battle_member_info_widget.dart) 文件来了解完整的实现逻辑。
561
+
562
+ ``` java
563
+ import 'package:flutter/material.dart';
564
+ import 'package:atomic_x_core/atomicxcore.dart';
565
+ import 'package:rtc_room_engine/rtc_room_engine.dart';
566
+
567
+ /// 自定义 PK 用户视图
568
+ class CustomBattleUserView extends StatefulWidget {
569
+ final String liveId;
570
+ final TUIBattleUser battleUser;
571
+
572
+ const CustomBattleUserView({
573
+ Key? key,
574
+ required this.liveId,
575
+ required this.battleUser,
576
+ }) : super(key: key);
577
+
578
+ @override
579
+ State<CustomBattleUserView> createState() => _CustomBattleUserViewState();
580
+ }
581
+
582
+ class _CustomBattleUserViewState extends State<CustomBattleUserView> {
583
+ late final BattleStore _battleStore;
584
+ late final VoidCallback _scoreChangedListener = _onScoreChanged;
585
+ int _score = 0;
586
+
587
+ @override
588
+ void initState() {
589
+ super.initState();
590
+ _battleStore = BattleStore.create(widget.liveId);
591
+ _subscribeBattleState();
592
+ }
593
+
594
+ /// 订阅 PK 分数变化
595
+ void _subscribeBattleState() {
596
+ _battleStore.battleState.battleScore.addListener(_scoreChangedListener);
597
+ // 初始化分数
598
+ _updateScore(_battleStore.battleState.battleScore.value);
599
+ }
600
+
601
+ void _onScoreChanged() {
602
+ _updateScore(_battleStore.battleState.battleScore.value);
603
+ }
604
+
605
+ void _updateScore(Map<String, int> battleScore) {
606
+ final score = battleScore[widget.battleUser.userId] ?? 0;
607
+ if (mounted && score != _score) {
608
+ setState(() {
609
+ _score = score;
610
+ });
611
+ }
612
+ }
613
+
614
+ @override
615
+ void dispose() {
616
+ _battleStore.battleState.battleScore.removeListener(_scoreChangedListener);
617
+ super.dispose();
618
+ }
619
+
620
+ @override
621
+ Widget build(BuildContext context) {
622
+ return IgnorePointer(
623
+ child: Align(
624
+ alignment: Alignment.bottomRight,
625
+ child: Container(
626
+ margin: const EdgeInsets.all(5),
627
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
628
+ decoration: BoxDecoration(
629
+ color: Colors.black.withOpacity(0.4),
630
+ borderRadius: BorderRadius.circular(12),
631
+ ),
632
+ child: Text(
633
+ '$_score',
634
+ style: const TextStyle(
635
+ color: Colors.white,
636
+ fontSize: 14,
637
+ fontWeight: FontWeight.bold,
638
+ ),
639
+ ),
640
+ ),
641
+ ),
642
+ );
643
+ }
644
+ }
645
+ ```
646
+ - **步骤2**:通过 `VideoWidgetBuilder` 的 `battleWidgetBuilder` 回调构建自定义 PK 视图。
647
+
648
+ ``` java
649
+ import 'package:flutter/material.dart';
650
+ import 'package:atomic_x_core/atomicxcore.dart';
651
+ import 'package:rtc_room_engine/rtc_room_engine.dart';
652
+
653
+ /// 带有自定义 PK 视图的直播页面
654
+ class CustomBattleLiveWidget extends StatefulWidget {
655
+ final String liveId;
656
+
657
+ const CustomBattleLiveWidget({
658
+ Key? key,
659
+ required this.liveId,
660
+ }) : super(key: key);
661
+
662
+ @override
663
+ State<CustomBattleLiveWidget> createState() => _CustomBattleLiveWidgetState();
664
+ }
665
+
666
+ class _CustomBattleLiveWidgetState extends State<CustomBattleLiveWidget> {
667
+ late LiveCoreController _controller;
668
+
669
+ @override
670
+ void initState() {
671
+ super.initState();
672
+ _controller = LiveCoreController.create();
673
+ _controller.setLiveID(widget.liveId);
674
+ }
675
+
676
+ @override
677
+ void dispose() {
678
+ _controller.dispose();
679
+ super.dispose();
680
+ }
681
+
682
+ /// 构建 PK 用户的自定义视图
683
+ Widget _buildBattleWidget(BuildContext context, TUIBattleUser battleUser) {
684
+ return CustomBattleUserView(
685
+ liveId: widget.liveId,
686
+ battleUser: battleUser,
687
+ );
688
+ }
689
+
690
+ @override
691
+ Widget build(BuildContext context) {
692
+ return Scaffold(
693
+ body: LiveCoreWidget(
694
+ controller: _controller,
695
+ videoWidgetBuilder: VideoWidgetBuilder(
696
+ battleWidgetBuilder: _buildBattleWidget,
697
+ ),
698
+ ),
699
+ );
700
+ }
701
+ }
702
+ ```
703
+
704
+ #### **参数说明**:
705
+ <table>
706
+ <tr>
707
+ <td rowspan="1" colSpan="1" >**参数**</td>
708
+
709
+ <td rowspan="1" colSpan="1" >**类型**</td>
710
+
711
+ <td rowspan="1" colSpan="1" >**说明**</td>
712
+ </tr>
713
+
714
+ <tr>
715
+ <td rowspan="1" colSpan="1" >`battleUser`</td>
716
+
717
+ <td rowspan="1" colSpan="1" >`TUIBattleUser`</td>
718
+
719
+ <td rowspan="1" colSpan="1" >PK 用户信息对象</td>
720
+ </tr>
721
+
722
+ <tr>
723
+ <td rowspan="1" colSpan="1" >`battleUser.roomId`</td>
724
+
725
+ <td rowspan="1" colSpan="1" >`String`</td>
726
+
727
+ <td rowspan="1" colSpan="1" >PK 的房间 ID</td>
728
+ </tr>
729
+
730
+ <tr>
731
+ <td rowspan="1" colSpan="1" >`battleUser.userId`</td>
732
+
733
+ <td rowspan="1" colSpan="1" >`String`</td>
734
+
735
+ <td rowspan="1" colSpan="1" >PK 用户 ID</td>
736
+ </tr>
737
+
738
+ <tr>
739
+ <td rowspan="1" colSpan="1" >`battleUser.userName`</td>
740
+
741
+ <td rowspan="1" colSpan="1" >`String`</td>
742
+
743
+ <td rowspan="1" colSpan="1" >PK 用户昵称</td>
744
+ </tr>
745
+
746
+ <tr>
747
+ <td rowspan="1" colSpan="1" >`battleUser.avatarUrl`</td>
748
+
749
+ <td rowspan="1" colSpan="1" >`String`</td>
750
+
751
+ <td rowspan="1" colSpan="1" >PK 用户头像地址</td>
752
+ </tr>
753
+
754
+ <tr>
755
+ <td rowspan="1" colSpan="1" >`battleUser.score`</td>
756
+
757
+ <td rowspan="1" colSpan="1" >`int`</td>
758
+
759
+ <td rowspan="1" colSpan="1" >PK 分数</td>
760
+ </tr>
761
+ </table>
762
+
763
+
764
+ ### 实现视频流画面上的 PK 状态显示
765
+
766
+ #### **实现效果**
767
+
768
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/4fa8e3bedcbf11f09143525400bf7822.png)
769
+
770
+
771
+ #### **实现方式**
772
+ - **步骤1:**创建自定义 PK 全局视图 **CustomBattleContainerView**,您可以参考 TUILiveKit 开源项目中的 [battle_info_widget.dart](https://github.com/Tencent-RTC/TUIKit_Flutter/blob/main/live/livekit/lib/live_stream/features/decorations/battle/battle_info_widget.dart) 文件来实现,即可实现同样的效果。
773
+
774
+ - **步骤2**:通过 `VideoWidgetBuilder` 的 `battleContainerWidgetBuilder` 回调构建自定义 PK 容器视图。
775
+
776
+ ``` java
777
+ import 'package:flutter/material.dart';
778
+ import 'package:atomic_x_core/atomicxcore.dart';
779
+
780
+ /// 带有自定义 PK 容器视图的直播页面
781
+ class CustomBattleContainerLiveWidget extends StatefulWidget {
782
+ final String liveId;
783
+
784
+ const CustomBattleContainerLiveWidget({
785
+ Key? key,
786
+ required this.liveId,
787
+ }) : super(key: key);
788
+
789
+ @override
790
+ State<CustomBattleContainerLiveWidget> createState() => _CustomBattleContainerLiveWidgetState();
791
+ }
792
+
793
+ class _CustomBattleContainerLiveWidgetState extends State<CustomBattleContainerLiveWidget> {
794
+ late LiveCoreController _controller;
795
+
796
+ @override
797
+ void initState() {
798
+ super.initState();
799
+ _controller = LiveCoreController.create();
800
+ _controller.setLiveID(widget.liveId);
801
+ }
802
+
803
+ @override
804
+ void dispose() {
805
+ _controller.dispose();
806
+ super.dispose();
807
+ }
808
+
809
+ /// 构建 PK 容器视图
810
+ Widget _buildBattleContainerWidget(BuildContext context) {
811
+ // CustomBattleContainerView 是您自定义的 PK 全局视图
812
+ return CustomBattleContainerView(liveId: widget.liveId);
813
+ }
814
+
815
+ @override
816
+ Widget build(BuildContext context) {
817
+ return Scaffold(
818
+ body: LiveCoreWidget(
819
+ controller: _controller,
820
+ videoWidgetBuilder: VideoWidgetBuilder(
821
+ battleContainerWidgetBuilder: _buildBattleContainerWidget,
822
+ ),
823
+ ),
824
+ );
825
+ }
826
+ }
827
+ ```
828
+
829
+ ### 组合使用多个自定义视图
830
+
831
+ 在实际场景中,您可能需要同时自定义连线主播视图、PK 用户视图和 PK 容器视图。以下示例展示了如何组合使用:
832
+ ``` java
833
+ import 'package:flutter/material.dart';
834
+ import 'package:atomic_x_core/atomicxcore.dart';
835
+ import 'package:rtc_room_engine/rtc_room_engine.dart';
836
+
837
+ /// 完整的自定义视图直播页面
838
+ class FullCustomLiveWidget extends StatefulWidget {
839
+ final String liveId;
840
+
841
+ const FullCustomLiveWidget({
842
+ Key? key,
843
+ required this.liveId,
844
+ }) : super(key: key);
845
+
846
+ @override
847
+ State<FullCustomLiveWidget> createState() => _FullCustomLiveWidgetState();
848
+ }
849
+
850
+ class _FullCustomLiveWidgetState extends State<FullCustomLiveWidget> {
851
+ late LiveCoreController _controller;
852
+
853
+ @override
854
+ void initState() {
855
+ super.initState();
856
+ _controller = LiveCoreController.create();
857
+ _controller.setLiveID(widget.liveId);
858
+ }
859
+
860
+ @override
861
+ void dispose() {
862
+ _controller.dispose();
863
+ super.dispose();
864
+ }
865
+
866
+ /// 构建连线主播的自定义视图
867
+ Widget _buildCoHostWidget(
868
+ BuildContext context,
869
+ SeatFullInfo seatFullInfo,
870
+ ViewLayer viewLayer,
871
+ ) {
872
+ if (viewLayer == ViewLayer.foreground) {
873
+ return CustomCoHostForegroundView(seatInfo: seatFullInfo);
874
+ } else {
875
+ return CustomCoHostBackgroundView(seatInfo: seatFullInfo);
876
+ }
877
+ }
878
+
879
+ /// 构建 PK 用户的自定义视图
880
+ Widget _buildBattleWidget(BuildContext context, TUIBattleUser userInfo) {
881
+ return CustomBattleUserView(
882
+ liveId: widget.liveId,
883
+ battleUser: userInfo,
884
+ );
885
+ }
886
+
887
+ /// 构建 PK 容器视图
888
+ Widget _buildBattleContainerWidget(BuildContext context) {
889
+ return CustomBattleContainerView(liveId: widget.liveId);
890
+ }
891
+
892
+ @override
893
+ Widget build(BuildContext context) {
894
+ return Scaffold(
895
+ body: LiveCoreWidget(
896
+ controller: _controller,
897
+ videoWidgetBuilder: VideoWidgetBuilder(
898
+ coHostWidgetBuilder: _buildCoHostWidget,
899
+ battleWidgetBuilder: _buildBattleWidget,
900
+ battleContainerWidgetBuilder: _buildBattleContainerWidget,
901
+ ),
902
+ ),
903
+ );
904
+ }
905
+ }
906
+ ```
907
+
908
+ ## 功能进阶
909
+
910
+ ### 通过 REST API 实现 PK 分数更新
911
+
912
+ 通常在**直播主播 PK 场景**下,会将主播收到的礼物价值与 PK 数值挂钩(例如:观众送 "火箭" 礼物,主播 PK 分数增加 500 分),您可以通过我们的 REST API,轻松实现直播 PK 场景下的分数实时更新。
913
+
914
+ > **重要说明:**
915
+ >
916
+
917
+ > LiveKit 后台的 PK 分数系统采用纯数值计算和累加机制,所以您需要根据自身的运营策略和业务需求,调用更新接口前完成 PK 分数的计算,您可以参考如下的 PK 分数计算示例:
918
+ >
919
+ <table>
920
+ <tr>
921
+ <td rowspan="1" colSpan="1" >** 礼物类型**</td>
922
+
923
+ <td rowspan="1" colSpan="1" >**分数计算规则**</td>
924
+
925
+ <td rowspan="1" colSpan="1" >**示例**</td>
926
+ </tr>
927
+
928
+ <tr>
929
+ <td rowspan="1" colSpan="1" >​基础礼物​</td>
930
+
931
+ <td rowspan="1" colSpan="1" >礼物价值 × 5</td>
932
+
933
+ <td rowspan="1" colSpan="1" >10元礼物 → 50分</td>
934
+ </tr>
935
+
936
+ <tr>
937
+ <td rowspan="1" colSpan="1" >​中级礼物​</td>
938
+
939
+ <td rowspan="1" colSpan="1" >礼物价值 × 8</td>
940
+
941
+ <td rowspan="1" colSpan="1" >50元礼物 → 400分</td>
942
+ </tr>
943
+
944
+ <tr>
945
+ <td rowspan="1" colSpan="1" >​高级礼物​</td>
946
+
947
+ <td rowspan="1" colSpan="1" >礼物价值 × 12</td>
948
+
949
+ <td rowspan="1" colSpan="1" >100元礼物 → 1200分</td>
950
+ </tr>
951
+
952
+ <tr>
953
+ <td rowspan="1" colSpan="1" >​特效礼物​</td>
954
+
955
+ <td rowspan="1" colSpan="1" >固定高分数</td>
956
+
957
+ <td rowspan="1" colSpan="1" >520元礼物 → 1314分</td>
958
+ </tr>
959
+ </table>
960
+
961
+
962
+
963
+ #### REST API 调用流程
964
+
965
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/49793681dcc011f0a6e3525400e889b2.png)
966
+
967
+
968
+ #### 关键流程说明
969
+ 1. ​**获取 PK 状态​**:
970
+
971
+ - 回调配置​:您可以通过配置 [PK 状态回调](https://write.woa.com/document/170122621675196416),由 LiveKit 后台在 PK 开始、结束时,主动通知您的系统 PK 状态。
972
+
973
+ - 主动查询​:您的后台服务可主动调用 [PK 状态查询](https://write.woa.com/document/169572228815638528) 接口,随时查询当前 PK 状态。
974
+
975
+ 2. **PK 分数计算**​:您的后台服务根据业务规则(如上述示例),计算 PK 分数增量。
976
+
977
+ 3. **PK 分数更新​**:您的后台服务调用 [修改 PK 分数](https://write.woa.com/document/169571965752266752) 接口,向 LiveKit 后台更新 PK 分数。
978
+
979
+ 4. **LiveKit 后台同步到客户端**​:LiveKit 后台自动将更新后的 PK 分数同步到所有客户端。
980
+
981
+
982
+ #### 涉及的 REST API 接口
983
+ <table>
984
+ <tr>
985
+ <td rowspan="1" colSpan="1" >**接口**</td>
986
+
987
+ <td rowspan="1" colSpan="1" >**功能描述**</td>
988
+
989
+ <td rowspan="1" colSpan="1" >**请求示例**</td>
990
+ </tr>
991
+
992
+ <tr>
993
+ <td rowspan="1" colSpan="1" >主动接口 - 查询 PK 状态</td>
994
+
995
+ <td rowspan="1" colSpan="1" >可根据此接口查询当前房间是否在 PK</td>
996
+
997
+ <td rowspan="1" colSpan="1" >[请求示例](https://write.woa.com/document/169572228815638528)</td>
998
+ </tr>
999
+
1000
+ <tr>
1001
+ <td rowspan="1" colSpan="1" >主动接口 - 修改 PK 分数</td>
1002
+
1003
+ <td rowspan="1" colSpan="1" >将计算后的 PK 数值通过此接口更新</td>
1004
+
1005
+ <td rowspan="1" colSpan="1" >[请求示例](https://write.woa.com/document/169571965752266752)</td>
1006
+ </tr>
1007
+
1008
+ <tr>
1009
+ <td rowspan="1" colSpan="1" >回调配置 - PK 开始时回调</td>
1010
+
1011
+ <td rowspan="1" colSpan="1" >客户后台可以通过该回调及时知晓 PK 开启</td>
1012
+
1013
+ <td rowspan="1" colSpan="1" >[回调示例](https://write.woa.com/document/170122621675196416)</td>
1014
+ </tr>
1015
+
1016
+ <tr>
1017
+ <td rowspan="1" colSpan="1" >回调配置 - PK 结束时回调</td>
1018
+
1019
+ <td rowspan="1" colSpan="1" >客户后台可以通过该回调及时知晓 PK 结束</td>
1020
+
1021
+ <td rowspan="1" colSpan="1" >[回调示例](https://write.woa.com/document/170123212271988736)</td>
1022
+ </tr>
1023
+ </table>
1024
+
1025
+
1026
+ ## API 文档
1027
+
1028
+ 关于 [CoHostStore](https://pub.dev/documentation/atomic_x_core/latest/api_live_co_host_store/CoHostStore-class.html) 及其相关类的所有公开接口、属性和方法的详细信息,请参阅随 [AtomicXCore](https://tencent-rtc.github.io/TUIKit_Flutter/index.html) 框架的官方 API 文档。本指南使用到的相关 Store 如下:
1029
+ <table>
1030
+ <tr>
1031
+ <td rowspan="1" colSpan="1" >**Store/Component**</td>
1032
+
1033
+ <td rowspan="1" colSpan="1" >**功能描述**</td>
1034
+
1035
+ <td rowspan="1" colSpan="1" >**API 文档**</td>
1036
+ </tr>
1037
+
1038
+ <tr>
1039
+ <td rowspan="1" colSpan="1" >**LiveCoreWidget**</td>
1040
+
1041
+ <td rowspan="1" colSpan="1" >直播视频流展示与交互的核心视图组件:负责视频流渲染和视图挂件处理,支持主播直播、观众连麦、主播连线等场景。</td>
1042
+
1043
+ <td rowspan="1" colSpan="1" >[API 文档](https://tencent-rtc.github.io/TUIKit_Flutter/api_view_live_live_core_widget/LiveCoreWidget-class.html)</td>
1044
+ </tr>
1045
+
1046
+ <tr>
1047
+ <td rowspan="1" colSpan="1" >**LiveCoreController**</td>
1048
+
1049
+ <td rowspan="1" colSpan="1" >LiveCoreWidget 的控制器:用于设置直播 ID、控制预览等操作。</td>
1050
+
1051
+ <td rowspan="1" colSpan="1" >[API 文档](https://tencent-rtc.github.io/TUIKit_Flutter/api_view_live_live_core_widget/LiveCoreController-class.html)</td>
1052
+ </tr>
1053
+
1054
+ <tr>
1055
+ <td rowspan="1" colSpan="1" >**VideoWidgetBuilder**</td>
1056
+
1057
+ <td rowspan="1" colSpan="1" >视频视图适配器:用于自定义连线主播、PK 用户、PK 容器等场景的视频流挂件视图。</td>
1058
+
1059
+ <td rowspan="1" colSpan="1" >[API 文档](https://tencent-rtc.github.io/TUIKit_Flutter/api_view_live_live_core_widget/VideoWidgetBuilder-class.html)</td>
1060
+ </tr>
1061
+
1062
+ <tr>
1063
+ <td rowspan="1" colSpan="1" >**DeviceStore**</td>
1064
+
1065
+ <td rowspan="1" colSpan="1" >音视频设备控制:麦克风(开关 / 音量)、摄像头(开关 / 切换 / 画质)、屏幕共享,设备状态实时监听。</td>
1066
+
1067
+ <td rowspan="1" colSpan="1" >[API 文档](https://tencent-rtc.github.io/TUIKit_Flutter/api_device_device_store/DeviceStore-class.html)</td>
1068
+ </tr>
1069
+
1070
+ <tr>
1071
+ <td rowspan="1" colSpan="1" >**CoHostStore**</td>
1072
+
1073
+ <td rowspan="1" colSpan="1" >主播跨房连线:支持多布局模板(动态网格等),发起 / 接受 / 拒绝连线,连麦主播互动管理。</td>
1074
+
1075
+ <td rowspan="1" colSpan="1" >[API 文档](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_co_host_store/api_live_co_host_store-library.html)</td>
1076
+ </tr>
1077
+
1078
+ <tr>
1079
+ <td rowspan="1" colSpan="1" >**BattleStore**</td>
1080
+
1081
+ <td rowspan="1" colSpan="1" >主播 PK 对战:发起 PK(配置时长 / 对手),管理 PK 状态(开始 / 结束),同步分数,监听对战结果。</td>
1082
+
1083
+ <td rowspan="1" colSpan="1" >[API 文档](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_battle_store/BattleStore-class.html)</td>
1084
+ </tr>
1085
+ </table>
1086
+
1087
+
1088
+ ## 常见问题
1089
+
1090
+ ### **为什么发起了连线邀请,对方却没收到?**
1091
+ - 请检查 `targetHostLiveId` 是否正确,并且对方直播间处于正常开播状态。
1092
+
1093
+ - 检查网络连接是否通畅,邀请信令有30秒的默认超时时间。
1094
+
1095
+
1096
+ ### 连线或 PK 过程中,一方主播网络断开或 App 崩溃了怎么办?
1097
+
1098
+ **CoHostStore** 和 **BattleStore** 内部都有心跳和超时检测机制。如果一方异常退出,另一方会通过 **onCoHostUserLeft** 或 **onUserExitBattle** 等事件收到通知,您可以根据这些事件来处理UI,例如提示"对方已掉线"并结束互动。
1099
+
1100
+ ### 为什么 PK 分数只能通过 REST API 更新?
1101
+
1102
+ 因为 REST API 能同时满足 PK 分数的**安全性、实时性、扩展性**需求:
1103
+ - **防篡改保公平**:需鉴权 + 数据校验,每笔更新可追溯来源(例如礼物行为),杜绝手动改分、刷分,保障竞技公平;
1104
+
1105
+ - **多端实时同步**:用标准化格式(例如 JSON)快速对接礼物、PK、展示系统,确保主播 / 观众 / 后台分数实时一致,无延迟;
1106
+
1107
+ - **灵活适配规则**:后端改配置(例如调整礼物对应分数、加成分数)即可适配业务变化,无需改前端,降低迭代成本。
1108
+
1109
+
1110
+ ### 如何管理通过 `VideoWidgetBuilder` 添加的自定义视图的生命周期和事件?
1111
+
1112
+ **LiveCoreWidget** 会自动管理您通过 `coHostWidgetBuilder`、`battleWidgetBuilder`、`battleContainerWidgetBuilder` 回调返回视图的添加和移除,您无需手动处理。如果需要在自定义视图中处理用户交互(例如点击事件),请在创建视图时为其添加相应的事件即可。
1113
+