@tencent-rtc/trtc-agent-skills 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/README.zh.md +173 -0
- package/bin/cli.js +434 -0
- package/knowledge-base/index.yaml +454 -0
- package/knowledge-base/platform-slice-template.md +233 -0
- package/knowledge-base/scenario-spec.md +350 -0
- package/knowledge-base/scenarios/conference/base/general-conference.md +365 -0
- package/knowledge-base/scenarios/conference/base/webinar-conference.md +130 -0
- package/knowledge-base/scenarios/conference/medical/1v1-video-consultation.md +145 -0
- package/knowledge-base/scenarios/conference/medical/medical-multidoctor-consultation.md +113 -0
- package/knowledge-base/scenarios/live/entertainment-live-room.md +118 -0
- package/knowledge-base/slice-spec.md +546 -0
- package/knowledge-base/slices/conference/web/ai-tools.md +225 -0
- package/knowledge-base/slices/conference/web/beauty-effects.md +188 -0
- package/knowledge-base/slices/conference/web/device-control.md +338 -0
- package/knowledge-base/slices/conference/web/login-auth.md +261 -0
- package/knowledge-base/slices/conference/web/network-quality.md +190 -0
- package/knowledge-base/slices/conference/web/official-roomkit-api.md +298 -0
- package/knowledge-base/slices/conference/web/official-roomkit-login-ui.md +246 -0
- package/knowledge-base/slices/conference/web/participant-list.md +238 -0
- package/knowledge-base/slices/conference/web/participant-management.md +718 -0
- package/knowledge-base/slices/conference/web/prejoin-check.md +293 -0
- package/knowledge-base/slices/conference/web/room-call.md +213 -0
- package/knowledge-base/slices/conference/web/room-chat.md +426 -0
- package/knowledge-base/slices/conference/web/room-lifecycle.md +534 -0
- package/knowledge-base/slices/conference/web/room-schedule.md +281 -0
- package/knowledge-base/slices/conference/web/screen-share.md +211 -0
- package/knowledge-base/slices/conference/web/video-layout.md +675 -0
- package/knowledge-base/slices/conference/web/virtual-background.md +197 -0
- package/knowledge-base/slices/conference/web/webinar-interaction.md +206 -0
- package/knowledge-base/slices/live/anchor-lifecycle.md +122 -0
- package/knowledge-base/slices/live/anchor-preview.md +90 -0
- package/knowledge-base/slices/live/anchor-room-config.md +104 -0
- package/knowledge-base/slices/live/audience-list.md +86 -0
- package/knowledge-base/slices/live/audience-manage.md +92 -0
- package/knowledge-base/slices/live/audience-watch.md +85 -0
- package/knowledge-base/slices/live/audio.md +116 -0
- package/knowledge-base/slices/live/barrage.md +88 -0
- package/knowledge-base/slices/live/beauty.md +99 -0
- package/knowledge-base/slices/live/coguest-apply.md +105 -0
- package/knowledge-base/slices/live/device-control.md +91 -0
- package/knowledge-base/slices/live/error-codes.md +167 -0
- package/knowledge-base/slices/live/gift.md +84 -0
- package/knowledge-base/slices/live/ios/.gitkeep +0 -0
- package/knowledge-base/slices/live/ios/anchor-lifecycle.md +313 -0
- package/knowledge-base/slices/live/ios/anchor-preview.md +228 -0
- package/knowledge-base/slices/live/ios/anchor-room-config.md +257 -0
- package/knowledge-base/slices/live/ios/audience-list.md +353 -0
- package/knowledge-base/slices/live/ios/audience-manage.md +381 -0
- package/knowledge-base/slices/live/ios/audience-watch.md +286 -0
- package/knowledge-base/slices/live/ios/audio.md +373 -0
- package/knowledge-base/slices/live/ios/barrage.md +285 -0
- package/knowledge-base/slices/live/ios/beauty.md +323 -0
- package/knowledge-base/slices/live/ios/coguest-apply.md +506 -0
- package/knowledge-base/slices/live/ios/device-control.md +286 -0
- package/knowledge-base/slices/live/ios/error-codes.md +270 -0
- package/knowledge-base/slices/live/ios/gift.md +315 -0
- package/knowledge-base/slices/live/ios/live-list.md +269 -0
- package/knowledge-base/slices/live/ios/login-auth.md +247 -0
- package/knowledge-base/slices/live/live-list.md +82 -0
- package/knowledge-base/slices/live/login-auth.md +78 -0
- package/package.json +34 -0
- package/skills/trtc/SKILL.md +326 -0
- package/skills/trtc/room-builder/SKILL.md +138 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/README.md +108 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/backend-contract.zh-CN.md +162 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/integration.zh-CN.md +154 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/docs/theme.zh-CN.md +78 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/index.html +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/package.json +28 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/postcss.config.js +5 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/App.vue +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/ConsultationManagePanel.vue +838 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LanguageSwitch.vue +102 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/LoadingSpinner.vue +6 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalAlert.vue +34 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalBusinessPanel.vue +148 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalButton.vue +49 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalConfirmDialog.vue +68 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalDataPanel.vue +196 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/MedicalRecordPanel.vue +270 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/components/PrescriptionPanel.vue +363 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/basic-info-config.ts +29 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.d.ts +4 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/lib-generate-test-usersig-es.min.js +2 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/config/runtime-config.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/env.d.ts +32 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationChatPanel.vue +123 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationMembersPanel.vue +230 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationTranscriptionPanel.vue +135 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/ConsultationVideoStage.vue +113 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/InviteDoctorDialog.vue +132 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/components/KickMemberConfirmDialog.vue +50 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/types.ts +77 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationChat.ts +97 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationDevices.ts +48 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationParticipants.ts +121 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/useConsultationPermissions.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/features/consultation/utils.ts +70 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/en-US/index.ts +553 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/index.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/medicalTranslate.ts +85 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/state.ts +49 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/i18n/zh-CN/index.ts +463 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/main.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/appointments.ts +96 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/mock/users.ts +79 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/router/index.ts +63 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/index.ts +25 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/appointmentService.ts +77 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/authService.ts +38 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/launchContext.ts +31 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/integration/userService.ts +35 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/appointmentService.ts +43 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/authService.ts +33 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/mock/userService.ts +43 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/services/adapters/types.ts +135 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/shared/icons.ts +53 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/index.css +106 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/tailwind.css +3 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/styles/theme.css +209 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/auth.ts +50 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/format.ts +24 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/navigation.ts +12 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/utils/session.ts +28 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorConsultationView.vue +777 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/DoctorDashboardView.vue +678 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/LoginView.vue +441 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationFinishedView.vue +185 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientConsultationView.vue +1003 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientSelectDoctorView.vue +317 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/src/views/PatientWaitingView.vue +454 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.json +21 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/tsconfig.node.json +8 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation/vite.config.ts +17 -0
- package/skills/trtc/room-builder/templates/scenarios/medical-consultation//346/216/245/345/205/245/350/257/264/346/230/216.md +6 -0
- package/skills/trtc/room-builder/tools/render_ai_instructions.py +226 -0
- package/skills/trtc-apply/SKILL.md +97 -0
- package/skills/trtc-apply/guardrails/apply_lib/__init__.py +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/__pycache__/rule_parser.cpython-313.pyc +0 -0
- package/skills/trtc-apply/guardrails/apply_lib/rule_parser.py +268 -0
- package/skills/trtc-docs/SKILL.md +207 -0
- package/skills/trtc-onboarding/SKILL.md +839 -0
- package/skills/trtc-onboarding/reference/path-a1-demo.md +103 -0
- package/skills/trtc-onboarding/reference/path-a2-integrate.md +693 -0
- package/skills/trtc-onboarding/reference/path-b-troubleshoot.md +115 -0
- package/skills/trtc-onboarding/reference/path-c-expand.md +43 -0
- package/skills/trtc-onboarding/reference/reporting-protocol.md +174 -0
- package/skills/trtc-onboarding/reference/supported-matrix.md +100 -0
- package/skills/trtc-onboarding/reference/usersig-handling.md +140 -0
- package/skills/trtc-search/SKILL.md +221 -0
- package/skills/trtc-topic/SKILL.md +638 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_read.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/gate_slice_write.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/__pycache__/stop_require_apply_evidence.cpython-313.pyc +0 -0
- package/skills/trtc-topic/guardrails/gate_slice_read.py +133 -0
- package/skills/trtc-topic/guardrails/gate_slice_write.py +169 -0
- package/skills/trtc-topic/guardrails/stop_require_apply_evidence.py +97 -0
- package/skills/trtc-topic/references/execution-units.yaml +58 -0
- package/skills/trtc-topic/runtime/README.md +50 -0
- package/skills/trtc-topic/runtime/RUNTIME.md +128 -0
- package/skills/trtc-topic/runtime/lib/__init__.py +0 -0
- package/skills/trtc-topic/runtime/lib/platforms.py +194 -0
- package/skills/trtc-topic/runtime/package-lock.json +1211 -0
- package/skills/trtc-topic/runtime/package.json +13 -0
- package/skills/trtc-topic/runtime/telemetry-bridge.mjs +339 -0
- package/skills/trtc-topic/runtime/telemetry_collector.py +293 -0
- package/skills/trtc-topic/scripts/STATE-MACHINE-GUIDE.md +186 -0
- package/skills/trtc-topic/scripts/__pycache__/apply.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/apply.py +581 -0
- package/skills/trtc-topic/scripts/finalize_session.py +113 -0
- package/skills/trtc-topic/scripts/init_slice_queue.py +96 -0
- package/skills/trtc-topic/scripts/lib/__pycache__/state_machine.cpython-313.pyc +0 -0
- package/skills/trtc-topic/scripts/lib/state_machine.py +328 -0
- package/skills/trtc-topic/scripts/next_slice.py +137 -0
- package/skills/trtc-topic/tests/README.md +70 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_apply_cli.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_end_to_end.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_finalize_session.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_gates.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_session_resolver.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_state_machine.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_stop_require_apply.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.2.pyc +0 -0
- package/skills/trtc-topic/tests/__pycache__/test_topic_skill_invariants.cpython-313-pytest-9.0.3.pyc +0 -0
- package/skills/trtc-topic/tests/conftest.py +72 -0
- package/skills/trtc-topic/tests/test_apply_cli.py +480 -0
- package/skills/trtc-topic/tests/test_end_to_end.py +305 -0
- package/skills/trtc-topic/tests/test_finalize_session.py +51 -0
- package/skills/trtc-topic/tests/test_gates.py +316 -0
- package/skills/trtc-topic/tests/test_session_resolver.py +260 -0
- package/skills/trtc-topic/tests/test_state_machine.py +414 -0
- package/skills/trtc-topic/tests/test_stop_require_apply.py +99 -0
- package/skills/trtc-topic/tests/test_topic_skill_invariants.py +130 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: live/anchor-room-config
|
|
3
|
+
platform: ios
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 直播间配置 — iOS 实现
|
|
7
|
+
|
|
8
|
+
## 前置条件
|
|
9
|
+
|
|
10
|
+
**依赖安装(Podfile)**
|
|
11
|
+
```ruby
|
|
12
|
+
pod 'AtomicXCore', '~> 4.0'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**前置状态**:
|
|
16
|
+
- `LoginStore.shared` 登录成功(须完成登录)
|
|
17
|
+
- 摄像头/麦克风设备已打开(`DeviceStore.shared.openLocalCamera` 成功)
|
|
18
|
+
|
|
19
|
+
## API 调用(真实签名)
|
|
20
|
+
|
|
21
|
+
```swift
|
|
22
|
+
// 构建直播间配置
|
|
23
|
+
// ⚠️ LiveInfo 必须通过 init(seatTemplate:) 初始化,不是 LiveInfo()
|
|
24
|
+
var liveInfo = LiveInfo(seatTemplate: .videoDynamicGrid9Seats)
|
|
25
|
+
liveInfo.liveID = "your-live-id"
|
|
26
|
+
liveInfo.liveName = "直播间名称"
|
|
27
|
+
liveInfo.coverURL = "https://cdn.example.com/cover.jpg"
|
|
28
|
+
liveInfo.notice = "欢迎来到我的直播间"
|
|
29
|
+
liveInfo.isPublicVisible = true
|
|
30
|
+
|
|
31
|
+
// 创建直播间
|
|
32
|
+
// ⚠️ 第一参数无标签(unnamed),completion 返回 LiveInfo
|
|
33
|
+
LiveListStore.shared.createLive(_ liveInfo: LiveInfo,
|
|
34
|
+
completion: LiveInfoCompletionClosure?)
|
|
35
|
+
// LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void
|
|
36
|
+
|
|
37
|
+
// 更新直播间元数据(仅房主/管理员可调用)
|
|
38
|
+
LiveListStore.shared.updateLiveMetaData(_ metaData: [String: String],
|
|
39
|
+
completion: CompletionClosure?)
|
|
40
|
+
|
|
41
|
+
// 更新直播间基础信息
|
|
42
|
+
LiveListStore.shared.updateLiveInfo(_ liveInfo: LiveInfo,
|
|
43
|
+
modifyFlag: LiveInfo.ModifyFlag,
|
|
44
|
+
completion: CompletionClosure?)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**LiveInfo 初始化与关键字段**
|
|
48
|
+
```swift
|
|
49
|
+
struct LiveInfo {
|
|
50
|
+
// ⚠️ 正确初始化方式:必须传 seatTemplate
|
|
51
|
+
init(seatTemplate: SeatLayoutTemplate)
|
|
52
|
+
|
|
53
|
+
var liveID: String
|
|
54
|
+
var liveName: String
|
|
55
|
+
var coverURL: String
|
|
56
|
+
var backgroundURL: String
|
|
57
|
+
var notice: String
|
|
58
|
+
var seatTemplate: SeatLayoutTemplate // 座位布局模板
|
|
59
|
+
var seatMode: TakeSeatMode // .apply 或 .free
|
|
60
|
+
var isPublicVisible: Bool
|
|
61
|
+
var isGiftEnabled: Bool
|
|
62
|
+
var isMessageDisable: Bool
|
|
63
|
+
var categoryList: [NSNumber]
|
|
64
|
+
var metaData: [String: String]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
enum SeatLayoutTemplate {
|
|
68
|
+
case videoDynamicGrid9Seats // 9格视频动态布局(最常用)
|
|
69
|
+
case videoDynamicFloat7Seats // 7格视频浮动布局
|
|
70
|
+
case videoFixedGrid9Seats
|
|
71
|
+
case videoFixedFloat7Seats
|
|
72
|
+
case videoLandscape4Seats
|
|
73
|
+
case audioSalon(seatCount: Int)
|
|
74
|
+
case karaoke(seatCount: Int)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// createLive 的回调类型
|
|
78
|
+
typealias LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| 参数 | 类型 | 说明 |
|
|
82
|
+
|------|------|------|
|
|
83
|
+
| `liveInfo.seatTemplate` | `SeatLayoutTemplate` | 必须通过 `init(seatTemplate:)` 传入,不可省略 |
|
|
84
|
+
| `liveID` | `String` | 直播间唯一 ID;仅含 ASCII,长度 ≤ 48 字节 |
|
|
85
|
+
| `liveName` | `String` | 显示名称;UTF-8,长度 ≤ 30 字节(约 10 个汉字) |
|
|
86
|
+
| `metaData` | `[String: String]` | 键值对扩展信息;最多 10 key,单值 ≤ 2 KB,总 ≤ 16 KB |
|
|
87
|
+
|
|
88
|
+
## 代码示例
|
|
89
|
+
|
|
90
|
+
```swift
|
|
91
|
+
import AtomicXCore
|
|
92
|
+
|
|
93
|
+
// MARK: - 创建直播间
|
|
94
|
+
|
|
95
|
+
func createLive(liveID: String, liveName: String, coverURL: String) {
|
|
96
|
+
// ✅ 正确:使用 init(seatTemplate:) 初始化
|
|
97
|
+
var liveInfo = LiveInfo(seatTemplate: .videoDynamicGrid9Seats)
|
|
98
|
+
liveInfo.liveID = liveID
|
|
99
|
+
liveInfo.liveName = liveName.isEmpty ? "我的直播间" : liveName
|
|
100
|
+
liveInfo.coverURL = coverURL
|
|
101
|
+
liveInfo.isPublicVisible = true
|
|
102
|
+
|
|
103
|
+
// ⚠️ createLive 第一参数无标签,completion 返回 LiveInfo(不是 Void)
|
|
104
|
+
LiveListStore.shared.createLive(liveInfo) { result in
|
|
105
|
+
switch result {
|
|
106
|
+
case .success(let createdLiveInfo):
|
|
107
|
+
// 回调返回完整 LiveInfo(含服务端写入的字段,如 createTime)
|
|
108
|
+
print("[RoomConfig] 直播间创建成功, liveID: \(createdLiveInfo.liveID)")
|
|
109
|
+
print("[RoomConfig] 创建时间: \(createdLiveInfo.createTime)")
|
|
110
|
+
// 跳转直播中页面
|
|
111
|
+
|
|
112
|
+
case .failure(let errorInfo):
|
|
113
|
+
handleCreateLiveError(errorInfo)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// MARK: - 参数校验
|
|
119
|
+
|
|
120
|
+
func validateLiveName(_ name: String) -> Bool {
|
|
121
|
+
// liveName 限制:UTF-8 字节数 ≤ 30
|
|
122
|
+
let byteCount = name.utf8.count
|
|
123
|
+
guard byteCount <= 30 else {
|
|
124
|
+
print("[RoomConfig] 名称超过 30 字节(约 10 个汉字),当前 \(byteCount) 字节")
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// MARK: - 更新直播间元数据(开播后动态更新)
|
|
131
|
+
|
|
132
|
+
func updateMetaData(_ updates: [String: String]) {
|
|
133
|
+
LiveListStore.shared.updateLiveMetaData(updates) { result in
|
|
134
|
+
switch result {
|
|
135
|
+
case .success:
|
|
136
|
+
print("[RoomConfig] MetaData 更新成功")
|
|
137
|
+
case .failure(let errorInfo):
|
|
138
|
+
if errorInfo.code == -2300 {
|
|
139
|
+
print("[RoomConfig] 权限不足,仅房主/管理员可更新 MetaData")
|
|
140
|
+
} else {
|
|
141
|
+
print("[RoomConfig] MetaData 更新失败, code: \(errorInfo.code)")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// MARK: - MetaData 校验(最多 10 key,单值 ≤ 2KB,总 ≤ 16KB)
|
|
148
|
+
|
|
149
|
+
func buildValidatedMetaData(_ raw: [String: String]) -> [String: String] {
|
|
150
|
+
var validated: [String: String] = [:]
|
|
151
|
+
let maxKeyCount = 10
|
|
152
|
+
let maxValueBytes = 2 * 1024 // 2 KB
|
|
153
|
+
let maxTotalBytes = 16 * 1024 // 16 KB
|
|
154
|
+
var totalBytes = 0
|
|
155
|
+
|
|
156
|
+
for (key, value) in raw.prefix(maxKeyCount) {
|
|
157
|
+
let valueBytes = value.utf8.count
|
|
158
|
+
guard valueBytes <= maxValueBytes else {
|
|
159
|
+
print("[RoomConfig] key '\(key)' 值超过 2KB,已跳过")
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
guard totalBytes + valueBytes <= maxTotalBytes else {
|
|
163
|
+
print("[RoomConfig] MetaData 总大小超过 16KB,已停止添加")
|
|
164
|
+
break
|
|
165
|
+
}
|
|
166
|
+
validated[key] = value
|
|
167
|
+
totalBytes += valueBytes
|
|
168
|
+
}
|
|
169
|
+
return validated
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// MARK: - 错误处理
|
|
173
|
+
|
|
174
|
+
func handleCreateLiveError(_ errorInfo: ErrorInfo) {
|
|
175
|
+
switch errorInfo.code {
|
|
176
|
+
case -2105: print("直播间 ID 格式非法(须为 ASCII,≤ 48 字节)")
|
|
177
|
+
case -2107: print("直播间名称非法(UTF-8,≤ 30 字节)")
|
|
178
|
+
case -2108: print("您已在其他直播间,请先退出后再试")
|
|
179
|
+
default: print("创建失败(code: \(errorInfo.code)): \(errorInfo.message)")
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## 调用时序
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
设备已就绪(openLocalCamera + openLocalMicrophone 成功)
|
|
188
|
+
│
|
|
189
|
+
▼
|
|
190
|
+
构建 LiveInfo
|
|
191
|
+
// ⚠️ 必须:LiveInfo(seatTemplate: .videoDynamicGrid9Seats)
|
|
192
|
+
// 设置 liveID / liveName / coverURL 等字段
|
|
193
|
+
│
|
|
194
|
+
▼
|
|
195
|
+
客户端校验
|
|
196
|
+
├── liveName UTF-8 字节数 ≤ 30?
|
|
197
|
+
├── MetaData 单值 ≤ 2KB?
|
|
198
|
+
└── MetaData 总大小 ≤ 16KB?
|
|
199
|
+
│
|
|
200
|
+
▼
|
|
201
|
+
// ⚠️ 第一参数无标签
|
|
202
|
+
LiveListStore.shared.createLive(liveInfo) { result in ... }
|
|
203
|
+
│
|
|
204
|
+
├─ .failure(errorInfo)
|
|
205
|
+
│ ├─ code -2105 → liveID 格式错误
|
|
206
|
+
│ ├─ code -2107 → liveName 超长/非法
|
|
207
|
+
│ └─ code -2108 → 已在其他房间
|
|
208
|
+
│
|
|
209
|
+
└─ .success(createdLiveInfo) ← 包含服务端写入字段
|
|
210
|
+
│
|
|
211
|
+
▼
|
|
212
|
+
进入直播中状态
|
|
213
|
+
(监听 liveListEventPublisher 生命周期事件)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## 平台特有注意事项
|
|
217
|
+
|
|
218
|
+
### 1. LiveInfo 必须用 `init(seatTemplate:)` 初始化
|
|
219
|
+
`LiveInfo` **没有无参初始化方法**(`LiveInfo()` 不正确)。必须传入 `SeatLayoutTemplate`:
|
|
220
|
+
```swift
|
|
221
|
+
// ✅ 正确
|
|
222
|
+
var liveInfo = LiveInfo(seatTemplate: .videoDynamicGrid9Seats)
|
|
223
|
+
|
|
224
|
+
// ❌ 错误:LiveInfo() 无此初始化方法
|
|
225
|
+
var liveInfo = LiveInfo()
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 2. createLive 第一参数无标签
|
|
229
|
+
`createLive` 签名为 `createLive(_ liveInfo: LiveInfo, completion:)`,第一参数**无标签**:
|
|
230
|
+
```swift
|
|
231
|
+
// ✅ 正确(无标签)
|
|
232
|
+
LiveListStore.shared.createLive(liveInfo) { result in ... }
|
|
233
|
+
|
|
234
|
+
// ❌ 错误(带标签)
|
|
235
|
+
LiveListStore.shared.createLive(liveInfo: liveInfo) { ... }
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 3. createLive completion 返回 LiveInfo,不是 Void
|
|
239
|
+
`LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void`,成功时回调携带服务端确认后的 `LiveInfo`(含 `createTime` 等服务端字段),应使用该对象而非本地构建的对象:
|
|
240
|
+
```swift
|
|
241
|
+
case .success(let createdLiveInfo): // ✅ 使用服务端返回的 LiveInfo
|
|
242
|
+
navigateToLiveRoom(liveInfo: createdLiveInfo)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 4. liveName 字节数计算
|
|
246
|
+
Swift 中汉字使用 UTF-8 编码,每个汉字占 3 字节:
|
|
247
|
+
```swift
|
|
248
|
+
let byteCount = liveName.utf8.count // ✅ 字节数(正确)
|
|
249
|
+
let charCount = liveName.count // ❌ 字符数不等于字节数
|
|
250
|
+
```
|
|
251
|
+
30 字节 ≈ 10 个汉字 = 30 个英文字母。
|
|
252
|
+
|
|
253
|
+
### 5. liveID 生成建议
|
|
254
|
+
建议在服务端生成 liveID 并下发:
|
|
255
|
+
- 仅含字母、数字、下划线、连字符
|
|
256
|
+
- 长度控制在 8~32 字节
|
|
257
|
+
- 示例:`live_10001_1711593600`
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: live/audience-list
|
|
3
|
+
platform: ios
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 观众列表 — iOS 实现
|
|
7
|
+
|
|
8
|
+
## 前置条件
|
|
9
|
+
|
|
10
|
+
**依赖安装(Podfile)**
|
|
11
|
+
```ruby
|
|
12
|
+
pod 'AtomicXCore', '~> 4.0'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**最低系统要求**:iOS 13.0+,Xcode 14.0+
|
|
16
|
+
|
|
17
|
+
**前置登录与进房**:必须在 `LoginStore.shared.login` 成功且 `liveCoreView.joinLive` 成功后才可调用 `fetchAudienceList`。
|
|
18
|
+
|
|
19
|
+
## API 调用
|
|
20
|
+
|
|
21
|
+
```swift
|
|
22
|
+
// 创建观众列表模块(每个直播间独立实例)
|
|
23
|
+
let audienceStore = LiveAudienceStore.create(liveID: String)
|
|
24
|
+
|
|
25
|
+
// 拉取当前观众列表快照
|
|
26
|
+
audienceStore.fetchAudienceList(completion: CompletionClosure?)
|
|
27
|
+
// CompletionClosure = (Result<Void, ErrorInfo>) -> Void
|
|
28
|
+
// ErrorInfo: .code: Int, .message: String
|
|
29
|
+
|
|
30
|
+
// 订阅观众实时事件(Combine)
|
|
31
|
+
audienceStore.liveAudienceEventPublisher // PassthroughSubject<LiveAudienceEvent, Never>
|
|
32
|
+
|
|
33
|
+
// 订阅状态变化(包含 audienceList 和 audienceCount)
|
|
34
|
+
audienceStore.state // StatePublisher<LiveAudienceState>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**LiveUserInfo 字段**
|
|
38
|
+
|
|
39
|
+
| 字段 | 类型 | 说明 |
|
|
40
|
+
|------|------|------|
|
|
41
|
+
| `userID` | `String` | 用户唯一 ID |
|
|
42
|
+
| `userName` | `String` | 显示名称 |
|
|
43
|
+
| `avatarURL` | `String` | 头像 URL |
|
|
44
|
+
|
|
45
|
+
**LiveAudienceState 字段**
|
|
46
|
+
|
|
47
|
+
| 字段 | 类型 | 说明 |
|
|
48
|
+
|------|------|------|
|
|
49
|
+
| `audienceList` | `[LiveUserInfo]` | 当前观众数组快照 |
|
|
50
|
+
| `audienceCount` | `UInt` | 观众总数(近似值,受频控影响) |
|
|
51
|
+
| `messageBannedUserList` | `[LiveUserInfo]` | 已被禁言的用户列表 |
|
|
52
|
+
|
|
53
|
+
**LiveAudienceEvent 枚举**
|
|
54
|
+
|
|
55
|
+
| 事件 | 说明 |
|
|
56
|
+
|------|------|
|
|
57
|
+
| `.onAudienceJoined(audience: LiveUserInfo)` | 有观众进入直播间 |
|
|
58
|
+
| `.onAudienceLeft(audience: LiveUserInfo)` | 有观众离开直播间 |
|
|
59
|
+
| `.onAudienceMessageDisabled(audience: LiveUserInfo, isDisable: Bool)` | 某观众禁言状态变更 |
|
|
60
|
+
| `.onOwnerJoined(owner: LiveUserInfo)` | 房主进入直播间(4.1.0+) |
|
|
61
|
+
| `.onOwnerLeft(owner: LiveUserInfo)` | 房主离开直播间(4.1.0+) |
|
|
62
|
+
| `.onAdminJoined(admin: LiveUserInfo)` | 管理员进入直播间(4.1.0+) |
|
|
63
|
+
| `.onAdminLeft(admin: LiveUserInfo)` | 管理员离开直播间(4.1.0+) |
|
|
64
|
+
|
|
65
|
+
> **MUST**: switch LiveAudienceEvent 时必须添加 `default` 分支,因为枚举可能在后续版本继续扩展。
|
|
66
|
+
|
|
67
|
+
## 代码示例
|
|
68
|
+
|
|
69
|
+
### 1. 创建、拉取列表与订阅事件
|
|
70
|
+
|
|
71
|
+
```swift
|
|
72
|
+
import AtomicXCore
|
|
73
|
+
import Combine
|
|
74
|
+
import UIKit
|
|
75
|
+
|
|
76
|
+
final class AudienceListViewModel {
|
|
77
|
+
|
|
78
|
+
// MARK: - Properties
|
|
79
|
+
|
|
80
|
+
private(set) var audienceList: [LiveUserInfo] = []
|
|
81
|
+
private(set) var audienceCount: UInt = 0
|
|
82
|
+
|
|
83
|
+
private var audienceStore: LiveAudienceStore?
|
|
84
|
+
private var cancellables = Set<AnyCancellable>()
|
|
85
|
+
|
|
86
|
+
// UI 刷新回调(通知 ViewController)
|
|
87
|
+
var onListUpdated: (() -> Void)?
|
|
88
|
+
var onCountUpdated: ((UInt) -> Void)?
|
|
89
|
+
|
|
90
|
+
// MARK: - 初始化(进房成功后调用)
|
|
91
|
+
|
|
92
|
+
func setup(liveID: String) {
|
|
93
|
+
// Step 1: 创建 LiveAudienceStore 实例
|
|
94
|
+
audienceStore = LiveAudienceStore.create(liveID: liveID)
|
|
95
|
+
|
|
96
|
+
// Step 2: 订阅实时事件
|
|
97
|
+
subscribeEvents()
|
|
98
|
+
|
|
99
|
+
// Step 3: 拉取初始列表快照
|
|
100
|
+
fetchInitialList()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// MARK: - 销毁(退出直播间时调用)
|
|
104
|
+
|
|
105
|
+
func teardown() {
|
|
106
|
+
cancellables.removeAll()
|
|
107
|
+
audienceStore = nil
|
|
108
|
+
audienceList = []
|
|
109
|
+
audienceCount = 0
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// MARK: - 订阅实时进离场事件
|
|
113
|
+
|
|
114
|
+
private func subscribeEvents() {
|
|
115
|
+
audienceStore?.liveAudienceEventPublisher
|
|
116
|
+
.receive(on: DispatchQueue.main)
|
|
117
|
+
.sink { [weak self] event in
|
|
118
|
+
guard let self = self else { return }
|
|
119
|
+
switch event {
|
|
120
|
+
case .onAudienceJoined(let audience):
|
|
121
|
+
self.handleAudienceJoined(audience)
|
|
122
|
+
case .onAudienceLeft(let audience):
|
|
123
|
+
self.handleAudienceLeft(audience)
|
|
124
|
+
case .onAudienceMessageDisabled(let audience, let isDisable):
|
|
125
|
+
// 禁言状态变更,可在此刷新 UI 标记
|
|
126
|
+
print("[AudienceList] 用户 \(audience.userID) 禁言状态变更: \(isDisable)")
|
|
127
|
+
default:
|
|
128
|
+
break // 兼容 4.1.0+ 新增 case(onOwnerJoined/Left, onAdminJoined/Left)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
.store(in: &cancellables)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// MARK: - 拉取初始列表
|
|
135
|
+
|
|
136
|
+
private func fetchInitialList() {
|
|
137
|
+
audienceStore?.fetchAudienceList { [weak self] result in
|
|
138
|
+
DispatchQueue.main.async {
|
|
139
|
+
guard let self = self else { return }
|
|
140
|
+
switch result {
|
|
141
|
+
case .success:
|
|
142
|
+
// 从 state.value 中取最新快照(state 是 StatePublisher,必须通过 .value 访问)
|
|
143
|
+
self.audienceList = self.audienceStore?.state.value.audienceList ?? []
|
|
144
|
+
self.audienceCount = self.audienceStore?.state.value.audienceCount ?? 0
|
|
145
|
+
self.onListUpdated?()
|
|
146
|
+
self.onCountUpdated?(self.audienceCount)
|
|
147
|
+
case .failure(let error):
|
|
148
|
+
print("[AudienceList] fetchAudienceList failed code=\(error.code) msg=\(error.message)")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// MARK: - 事件处理
|
|
155
|
+
|
|
156
|
+
private func handleAudienceJoined(_ audience: LiveUserInfo) {
|
|
157
|
+
// 避免重复插入
|
|
158
|
+
guard !audienceList.contains(where: { $0.userID == audience.userID }) else { return }
|
|
159
|
+
audienceList.insert(audience, at: 0) // 新观众插入列表头部
|
|
160
|
+
audienceCount = audienceStore?.state.value.audienceCount ?? audienceCount + 1
|
|
161
|
+
onListUpdated?()
|
|
162
|
+
onCountUpdated?(audienceCount)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private func handleAudienceLeft(_ audience: LiveUserInfo) {
|
|
166
|
+
audienceList.removeAll { $0.userID == audience.userID }
|
|
167
|
+
audienceCount = audienceStore?.state.value.audienceCount ?? (audienceCount > 0 ? audienceCount - 1 : 0)
|
|
168
|
+
onListUpdated?()
|
|
169
|
+
onCountUpdated?(audienceCount)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 2. 单列 UICollectionView 展示
|
|
175
|
+
|
|
176
|
+
```swift
|
|
177
|
+
import UIKit
|
|
178
|
+
|
|
179
|
+
final class AudienceListViewController: UIViewController {
|
|
180
|
+
|
|
181
|
+
private let viewModel = AudienceListViewModel()
|
|
182
|
+
|
|
183
|
+
// MARK: - UI
|
|
184
|
+
|
|
185
|
+
private lazy var countLabel: UILabel = {
|
|
186
|
+
let label = UILabel()
|
|
187
|
+
label.font = .systemFont(ofSize: 14, weight: .medium)
|
|
188
|
+
label.textColor = .white
|
|
189
|
+
return label
|
|
190
|
+
}()
|
|
191
|
+
|
|
192
|
+
private lazy var collectionView: UICollectionView = {
|
|
193
|
+
let layout = UICollectionViewFlowLayout()
|
|
194
|
+
layout.scrollDirection = .vertical
|
|
195
|
+
layout.itemSize = CGSize(width: UIScreen.main.bounds.width, height: 60)
|
|
196
|
+
layout.minimumLineSpacing = 0
|
|
197
|
+
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
|
198
|
+
cv.register(AudienceCell.self, forCellWithReuseIdentifier: "AudienceCell")
|
|
199
|
+
cv.dataSource = self
|
|
200
|
+
cv.backgroundColor = .clear
|
|
201
|
+
return cv
|
|
202
|
+
}()
|
|
203
|
+
|
|
204
|
+
// MARK: - Setup
|
|
205
|
+
|
|
206
|
+
init(liveID: String) {
|
|
207
|
+
super.init(nibName: nil, bundle: nil)
|
|
208
|
+
viewModel.setup(liveID: liveID)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
required init?(coder: NSCoder) { fatalError() }
|
|
212
|
+
|
|
213
|
+
override func viewDidLoad() {
|
|
214
|
+
super.viewDidLoad()
|
|
215
|
+
setupUI()
|
|
216
|
+
bindViewModel()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
deinit {
|
|
220
|
+
viewModel.teardown()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private func setupUI() {
|
|
224
|
+
view.addSubview(countLabel)
|
|
225
|
+
view.addSubview(collectionView)
|
|
226
|
+
// 布局代码省略
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private func bindViewModel() {
|
|
230
|
+
viewModel.onListUpdated = { [weak self] in
|
|
231
|
+
self?.collectionView.reloadData()
|
|
232
|
+
}
|
|
233
|
+
viewModel.onCountUpdated = { [weak self] count in
|
|
234
|
+
// ⚠️ 人数仅展示,加"约"字说明非精确值
|
|
235
|
+
self?.countLabel.text = "约 \(count) 人在看"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
extension AudienceListViewController: UICollectionViewDataSource {
|
|
241
|
+
func collectionView(_ cv: UICollectionView,
|
|
242
|
+
numberOfItemsInSection section: Int) -> Int {
|
|
243
|
+
viewModel.audienceList.count
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func collectionView(_ cv: UICollectionView,
|
|
247
|
+
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
248
|
+
let cell = cv.dequeueReusableCell(withReuseIdentifier: "AudienceCell",
|
|
249
|
+
for: indexPath) as! AudienceCell
|
|
250
|
+
cell.configure(with: viewModel.audienceList[indexPath.item])
|
|
251
|
+
return cell
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// MARK: - 双列布局(可选)
|
|
256
|
+
|
|
257
|
+
extension AudienceListViewController {
|
|
258
|
+
/// 切换为双列显示(适合大观众列表)
|
|
259
|
+
func switchToDoubleColumnLayout() {
|
|
260
|
+
guard let layout = collectionView.collectionViewLayout
|
|
261
|
+
as? UICollectionViewFlowLayout else { return }
|
|
262
|
+
let padding: CGFloat = 8
|
|
263
|
+
let itemWidth = (UIScreen.main.bounds.width - padding * 3) / 2
|
|
264
|
+
layout.itemSize = CGSize(width: itemWidth, height: 56)
|
|
265
|
+
layout.minimumInteritemSpacing = padding
|
|
266
|
+
layout.minimumLineSpacing = padding
|
|
267
|
+
layout.sectionInset = UIEdgeInsets(top: padding, left: padding,
|
|
268
|
+
bottom: padding, right: padding)
|
|
269
|
+
collectionView.reloadData()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// MARK: - Cell
|
|
274
|
+
|
|
275
|
+
final class AudienceCell: UICollectionViewCell {
|
|
276
|
+
|
|
277
|
+
private let avatarImageView = UIImageView()
|
|
278
|
+
private let nameLabel = UILabel()
|
|
279
|
+
|
|
280
|
+
override init(frame: CGRect) {
|
|
281
|
+
super.init(frame: frame)
|
|
282
|
+
setupUI()
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
required init?(coder: NSCoder) { fatalError() }
|
|
286
|
+
|
|
287
|
+
private func setupUI() {
|
|
288
|
+
avatarImageView.layer.cornerRadius = 20
|
|
289
|
+
avatarImageView.clipsToBounds = true
|
|
290
|
+
contentView.addSubview(avatarImageView)
|
|
291
|
+
contentView.addSubview(nameLabel)
|
|
292
|
+
// 布局代码省略
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
func configure(with user: LiveUserInfo) {
|
|
296
|
+
nameLabel.text = user.userName
|
|
297
|
+
// 加载头像(使用业务图片加载框架)
|
|
298
|
+
if let url = URL(string: user.avatarURL) {
|
|
299
|
+
// e.g. avatarImageView.sd_setImage(with: url)
|
|
300
|
+
_ = url // 占位
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## 调用时序
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
LoginStore.login 成功
|
|
310
|
+
│
|
|
311
|
+
▼
|
|
312
|
+
liveCoreView.joinLive 成功
|
|
313
|
+
│
|
|
314
|
+
▼
|
|
315
|
+
LiveAudienceStore.create(liveID:) ← 每个直播间独立实例
|
|
316
|
+
│
|
|
317
|
+
▼
|
|
318
|
+
audienceStore.liveAudienceEventPublisher 订阅(PassthroughSubject)
|
|
319
|
+
│
|
|
320
|
+
▼
|
|
321
|
+
audienceStore.fetchAudienceList ← 获取初始快照
|
|
322
|
+
│
|
|
323
|
+
├─ .failure(ErrorInfo) → 检查登录态 / 进房状态(error.code / error.message)
|
|
324
|
+
└─ .success → 读取 state.audienceList → reloadData
|
|
325
|
+
│
|
|
326
|
+
▼
|
|
327
|
+
实时事件推送
|
|
328
|
+
├─ .onAudienceJoined(audience:) → 插入列表头 → reloadData
|
|
329
|
+
├─ .onAudienceLeft(audience:) → 移除列表项 → reloadData
|
|
330
|
+
└─ .onAudienceMessageDisabled(audience:isDisable:) → 更新禁言标记
|
|
331
|
+
│
|
|
332
|
+
▼
|
|
333
|
+
退出直播间
|
|
334
|
+
└─ audienceStore = nil(释放订阅与资源)
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## 平台特有注意事项
|
|
338
|
+
|
|
339
|
+
### 1. AnyCancellable 必须存储在实例变量中
|
|
340
|
+
|
|
341
|
+
若将 `liveAudienceEventPublisher.sink` 返回的 `AnyCancellable` 存在局部变量而非 `Set<AnyCancellable>` 实例变量中,订阅会在函数返回后立即释放,导致后续所有事件静默丢弃。务必使用 `.store(in: &cancellables)` 持久化订阅。
|
|
342
|
+
|
|
343
|
+
### 2. 主线程刷新 UI
|
|
344
|
+
|
|
345
|
+
`liveAudienceEventPublisher` 的事件可能在后台线程分发。始终使用 `.receive(on: DispatchQueue.main)` 切换到主线程再更新 `audienceList` 和 `collectionView`,否则会触发 `UIKit` 主线程访问警告或崩溃。
|
|
346
|
+
|
|
347
|
+
### 3. audienceCount 类型为 UInt
|
|
348
|
+
|
|
349
|
+
`LiveAudienceState.audienceCount` 是 `UInt` 类型(非 `Int`)。UI 展示时建议显示"约 X 人在看",而非精确数字。由于频控(40条/秒),在万人以上直播间中 `audienceCount` 可能与实际值相差数百,不要用它做业务逻辑判断(如活动奖励门槛)。
|
|
350
|
+
|
|
351
|
+
### 4. 离场延迟:90 秒心跳窗口
|
|
352
|
+
|
|
353
|
+
观众因网络断开(非主动退出)时,系统需等待约 90 秒心跳超时后才会触发 `onAudienceLeft`。UI 上观众头像可能在对方已断网后仍显示数分钟,属正常现象,无需特殊处理。
|