@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,381 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: live/audience-manage
|
|
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.isLogin == true`
|
|
17
|
+
- 已成功进入直播间
|
|
18
|
+
- 当前用户为房主(执行 `setAdministrator` / `revokeAdministrator`)或管理员(执行 `kickUserOutOfRoom`)
|
|
19
|
+
|
|
20
|
+
## API 调用
|
|
21
|
+
|
|
22
|
+
```swift
|
|
23
|
+
// 创建观众管理模块(通过 LiveAudienceStore 工厂方法)
|
|
24
|
+
let liveAudienceStore = LiveAudienceStore.create(liveID: liveID)
|
|
25
|
+
|
|
26
|
+
// 踢出观众(需房主或管理员权限)
|
|
27
|
+
liveAudienceStore.kickUserOutOfRoom(
|
|
28
|
+
userID: String,
|
|
29
|
+
completion: CompletionClosure?
|
|
30
|
+
)
|
|
31
|
+
// CompletionClosure = (Result<Void, ErrorInfo>) -> Void
|
|
32
|
+
|
|
33
|
+
// 设置管理员(仅房主)
|
|
34
|
+
liveAudienceStore.setAdministrator(
|
|
35
|
+
userID: String,
|
|
36
|
+
completion: CompletionClosure?
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// 撤销管理员(仅房主)
|
|
40
|
+
liveAudienceStore.revokeAdministrator(
|
|
41
|
+
userID: String,
|
|
42
|
+
completion: CompletionClosure?
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// 禁言 / 解除禁言(需房主或管理员权限)
|
|
46
|
+
liveAudienceStore.disableSendMessage(
|
|
47
|
+
userID: String,
|
|
48
|
+
isDisable: Bool,
|
|
49
|
+
completion: CompletionClosure?
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// 订阅观众管理事件
|
|
53
|
+
liveAudienceStore.liveAudienceEventPublisher // PassthroughSubject<LiveAudienceEvent, Never>
|
|
54
|
+
// 包含:
|
|
55
|
+
// .onAudienceMessageDisabled(audience: LiveUserInfo, isDisable: Bool)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
| 参数 | 类型 | 说明 |
|
|
59
|
+
|------|------|------|
|
|
60
|
+
| `userID` | `String` | 目标用户的唯一标识 |
|
|
61
|
+
| `isDisable` | `Bool` | `true` = 禁言,`false` = 解除禁言 |
|
|
62
|
+
| `completion` | `CompletionClosure?` | `(Result<Void, ErrorInfo>) -> Void`,`ErrorInfo` 含 `.code` 和 `.message` |
|
|
63
|
+
|
|
64
|
+
## 代码示例
|
|
65
|
+
|
|
66
|
+
### 完整管理操作集成
|
|
67
|
+
|
|
68
|
+
```swift
|
|
69
|
+
import AtomicXCore
|
|
70
|
+
import Combine
|
|
71
|
+
|
|
72
|
+
final class AudienceManageManager {
|
|
73
|
+
|
|
74
|
+
// MARK: - 属性
|
|
75
|
+
|
|
76
|
+
private let audienceStore: LiveAudienceStore
|
|
77
|
+
private var cancellables = Set<AnyCancellable>()
|
|
78
|
+
|
|
79
|
+
/// 当前用户角色(从 LiveStore 获取)
|
|
80
|
+
private var currentUserRole: UserRole = .audience
|
|
81
|
+
|
|
82
|
+
// MARK: - 初始化
|
|
83
|
+
|
|
84
|
+
init(liveID: String) {
|
|
85
|
+
// 通过工厂方法创建,确保每个直播间独立实例
|
|
86
|
+
self.audienceStore = LiveAudienceStore.create(liveID: liveID)
|
|
87
|
+
subscribeAudienceEvents()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// MARK: - 监听观众管理事件
|
|
91
|
+
|
|
92
|
+
private func subscribeAudienceEvents() {
|
|
93
|
+
audienceStore.liveAudienceEventPublisher
|
|
94
|
+
.receive(on: DispatchQueue.main)
|
|
95
|
+
.sink { [weak self] event in
|
|
96
|
+
guard let self else { return }
|
|
97
|
+
switch event {
|
|
98
|
+
case .onAudienceMessageDisabled(let audience, let isDisable):
|
|
99
|
+
// 某用户禁言状态变更,刷新列表 UI
|
|
100
|
+
print("[Manage] 用户 \(audience.userID) 禁言状态: \(isDisable)")
|
|
101
|
+
case .onAudienceJoined, .onAudienceLeft:
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
.store(in: &cancellables)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// MARK: - 权限校验
|
|
109
|
+
|
|
110
|
+
/// 校验是否有踢人权限
|
|
111
|
+
private func canKick(targetRole: UserRole) -> Bool {
|
|
112
|
+
switch currentUserRole {
|
|
113
|
+
case .owner:
|
|
114
|
+
// 房主可以踢任何人
|
|
115
|
+
return true
|
|
116
|
+
case .admin:
|
|
117
|
+
// 管理员只能踢普通观众,不能踢其他管理员或房主
|
|
118
|
+
return targetRole == .audience
|
|
119
|
+
case .audience:
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// 校验是否有设置/撤销管理员权限
|
|
125
|
+
private func canManageAdmin() -> Bool {
|
|
126
|
+
return currentUserRole == .owner
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// MARK: - 踢出观众
|
|
130
|
+
|
|
131
|
+
func kickUser(_ userID: String,
|
|
132
|
+
targetRole: UserRole,
|
|
133
|
+
completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
|
|
134
|
+
// 步骤1: 调用前校验权限
|
|
135
|
+
guard canKick(targetRole: targetRole) else {
|
|
136
|
+
print("[Manage] 权限不足,无法踢出用户: \(userID)")
|
|
137
|
+
completion?(.failure(ErrorInfo(code: -1, message: "权限不足,无法执行此操作")))
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 步骤2: 踢出用户
|
|
142
|
+
audienceStore.kickUserOutOfRoom(userID: userID) { result in
|
|
143
|
+
DispatchQueue.main.async {
|
|
144
|
+
switch result {
|
|
145
|
+
case .success:
|
|
146
|
+
print("[Manage] 已踢出用户: \(userID)")
|
|
147
|
+
completion?(.success(()))
|
|
148
|
+
case .failure(let error):
|
|
149
|
+
print("[Manage] 踢出失败 code=\(error.code) msg=\(error.message)")
|
|
150
|
+
completion?(.failure(error))
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// MARK: - 管理员设置(仅房主)
|
|
157
|
+
|
|
158
|
+
func setAdministrator(_ userID: String,
|
|
159
|
+
completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
|
|
160
|
+
// 步骤3: 校验房主权限
|
|
161
|
+
guard canManageAdmin() else {
|
|
162
|
+
completion?(.failure(ErrorInfo(code: -1, message: "权限不足,无法执行此操作")))
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
audienceStore.setAdministrator(userID: userID) { result in
|
|
167
|
+
DispatchQueue.main.async {
|
|
168
|
+
switch result {
|
|
169
|
+
case .success:
|
|
170
|
+
print("[Manage] 已设置管理员: \(userID)")
|
|
171
|
+
completion?(.success(()))
|
|
172
|
+
case .failure(let error):
|
|
173
|
+
print("[Manage] 设置管理员失败 code=\(error.code) msg=\(error.message)")
|
|
174
|
+
completion?(.failure(error))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
func revokeAdministrator(_ userID: String,
|
|
181
|
+
completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
|
|
182
|
+
guard canManageAdmin() else {
|
|
183
|
+
completion?(.failure(ErrorInfo(code: -1, message: "权限不足,无法执行此操作")))
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
audienceStore.revokeAdministrator(userID: userID) { result in
|
|
188
|
+
DispatchQueue.main.async {
|
|
189
|
+
switch result {
|
|
190
|
+
case .success:
|
|
191
|
+
print("[Manage] 已撤销管理员: \(userID)")
|
|
192
|
+
completion?(.success(()))
|
|
193
|
+
case .failure(let error):
|
|
194
|
+
print("[Manage] 撤销管理员失败 code=\(error.code) msg=\(error.message)")
|
|
195
|
+
completion?(.failure(error))
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// MARK: - 禁言管理
|
|
202
|
+
|
|
203
|
+
func muteUser(_ userID: String,
|
|
204
|
+
completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
|
|
205
|
+
audienceStore.disableSendMessage(userID: userID, isDisable: true) { result in
|
|
206
|
+
DispatchQueue.main.async {
|
|
207
|
+
switch result {
|
|
208
|
+
case .success:
|
|
209
|
+
print("[Manage] 已禁言用户: \(userID)")
|
|
210
|
+
completion?(.success(()))
|
|
211
|
+
case .failure(let error):
|
|
212
|
+
print("[Manage] 禁言失败 code=\(error.code) msg=\(error.message)")
|
|
213
|
+
completion?(.failure(error))
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func unmuteUser(_ userID: String,
|
|
220
|
+
completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
|
|
221
|
+
audienceStore.disableSendMessage(userID: userID, isDisable: false) { result in
|
|
222
|
+
DispatchQueue.main.async {
|
|
223
|
+
completion?(result)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// MARK: - 错误处理
|
|
229
|
+
|
|
230
|
+
func handleManageError(_ error: ErrorInfo, for operation: String) {
|
|
231
|
+
switch error.code {
|
|
232
|
+
case -2300:
|
|
233
|
+
showAlert(title: "权限不足", message: "该操作仅房主可执行")
|
|
234
|
+
case -2301:
|
|
235
|
+
showAlert(title: "权限不足", message: "该操作需要管理员或房主权限")
|
|
236
|
+
case -2302:
|
|
237
|
+
showToast("该用户已不在直播间")
|
|
238
|
+
default:
|
|
239
|
+
showToast("\(operation)失败:\(error.message)")
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// MARK: - 用户角色
|
|
245
|
+
|
|
246
|
+
enum UserRole {
|
|
247
|
+
case owner // 房主
|
|
248
|
+
case admin // 管理员
|
|
249
|
+
case audience // 普通观众
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 观众列表操作菜单
|
|
254
|
+
|
|
255
|
+
```swift
|
|
256
|
+
// 观众列表 Cell 上的操作菜单(权限判断)
|
|
257
|
+
func showAudienceActionMenu(for audience: LiveUserInfo,
|
|
258
|
+
audienceRole: UserRole,
|
|
259
|
+
currentRole: UserRole) {
|
|
260
|
+
var actions: [UIAlertAction] = []
|
|
261
|
+
|
|
262
|
+
// 仅房主或管理员可踢人(管理员只能踢普通观众)
|
|
263
|
+
let canKick = (currentRole == .owner) ||
|
|
264
|
+
(currentRole == .admin && audienceRole == .audience)
|
|
265
|
+
if canKick {
|
|
266
|
+
actions.append(UIAlertAction(title: "踢出直播间", style: .destructive) { [weak self] _ in
|
|
267
|
+
self?.confirmKick(userID: audience.userID)
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 仅房主可禁言/设置管理员
|
|
272
|
+
if currentRole == .owner {
|
|
273
|
+
actions.append(UIAlertAction(title: "禁言", style: .default) { [weak self] _ in
|
|
274
|
+
self?.manageManager.muteUser(audience.userID) { result in
|
|
275
|
+
if case .failure(let error) = result {
|
|
276
|
+
self?.manageManager.handleManageError(error, for: "禁言")
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
if audienceRole == .audience {
|
|
282
|
+
actions.append(UIAlertAction(title: "设为管理员", style: .default) { [weak self] _ in
|
|
283
|
+
self?.manageManager.setAdministrator(audience.userID) { result in
|
|
284
|
+
if case .failure(let error) = result {
|
|
285
|
+
self?.manageManager.handleManageError(error, for: "设为管理员")
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
} else if audienceRole == .admin {
|
|
290
|
+
actions.append(UIAlertAction(title: "撤销管理员", style: .default) { [weak self] _ in
|
|
291
|
+
self?.manageManager.revokeAdministrator(audience.userID) { result in
|
|
292
|
+
if case .failure(let error) = result {
|
|
293
|
+
self?.manageManager.handleManageError(error, for: "撤销管理员")
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
guard !actions.isEmpty else { return }
|
|
301
|
+
|
|
302
|
+
let sheet = UIAlertController(
|
|
303
|
+
title: audience.userName,
|
|
304
|
+
message: nil,
|
|
305
|
+
preferredStyle: .actionSheet
|
|
306
|
+
)
|
|
307
|
+
actions.forEach { sheet.addAction($0) }
|
|
308
|
+
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
|
|
309
|
+
present(sheet, animated: true)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private func confirmKick(userID: String) {
|
|
313
|
+
let alert = UIAlertController(
|
|
314
|
+
title: "踢出直播间",
|
|
315
|
+
message: "确定要将该用户移出直播间吗?",
|
|
316
|
+
preferredStyle: .alert
|
|
317
|
+
)
|
|
318
|
+
alert.addAction(UIAlertAction(title: "确定", style: .destructive) { [weak self] _ in
|
|
319
|
+
self?.manageManager.kickUser(userID, targetRole: .audience) { result in
|
|
320
|
+
if case .failure(let error) = result {
|
|
321
|
+
self?.manageManager.handleManageError(error, for: "踢出用户")
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
|
326
|
+
present(alert, animated: true)
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## 调用时序
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
进入直播间
|
|
334
|
+
│
|
|
335
|
+
▼
|
|
336
|
+
LiveAudienceStore.create(liveID:)
|
|
337
|
+
│
|
|
338
|
+
▼
|
|
339
|
+
订阅 liveAudienceEventPublisher // 监听禁言状态变更等事件
|
|
340
|
+
│
|
|
341
|
+
├─ 房主操作流程
|
|
342
|
+
│ │
|
|
343
|
+
│ ├─ setAdministrator(userID:)
|
|
344
|
+
│ │ ├─ .success → 更新观众列表角色标记
|
|
345
|
+
│ │ └─ .failure(code: -2300) → 非房主,隐藏入口
|
|
346
|
+
│ │
|
|
347
|
+
│ ├─ revokeAdministrator(userID:)
|
|
348
|
+
│ │ ├─ .success → 更新观众列表角色标记
|
|
349
|
+
│ │ └─ .failure(code: -2300) → 非房主,隐藏入口
|
|
350
|
+
│ │
|
|
351
|
+
│ └─ disableSendMessage(userID:isDisable:)
|
|
352
|
+
│ ├─ .success → onAudienceMessageDisabled 事件推送
|
|
353
|
+
│ └─ .failure(ErrorInfo) → 展示 error.message
|
|
354
|
+
│
|
|
355
|
+
└─ 房主/管理员踢人流程
|
|
356
|
+
│
|
|
357
|
+
├─ 权限校验(canKick)
|
|
358
|
+
├─ 二次确认弹窗
|
|
359
|
+
└─ kickUserOutOfRoom(userID:)
|
|
360
|
+
├─ .success → 刷新观众列表
|
|
361
|
+
├─ .failure(code: -2301) → 权限不足
|
|
362
|
+
└─ .failure(code: -2302) → 用户已离开,刷新列表
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## 平台特有注意事项
|
|
366
|
+
|
|
367
|
+
### 1. ErrorInfo 替代 Error
|
|
368
|
+
所有 completion 使用 `(Result<Void, ErrorInfo>) -> Void`,不是 `Result<Void, Error>`。错误信息通过 `error.code`(Int)和 `error.message`(String)访问,不要调用 `localizedDescription`。
|
|
369
|
+
|
|
370
|
+
### 2. 管理员权限 UI 实时刷新
|
|
371
|
+
当房主撤销某用户的管理员权限时,该用户应立即看到操作按钮的变化(如隐藏踢人按钮)。通过订阅观众列表状态变化来驱动 UI 更新,不要依赖本地缓存的角色信息。
|
|
372
|
+
|
|
373
|
+
### 3. iPad 上的 ActionSheet
|
|
374
|
+
在 iPad 上,`UIAlertController` 的 `.actionSheet` 样式需要设置 `popoverPresentationController` 的 `sourceView` 和 `sourceRect`,否则会崩溃。
|
|
375
|
+
|
|
376
|
+
```swift
|
|
377
|
+
if let popover = sheet.popoverPresentationController {
|
|
378
|
+
popover.sourceView = cell
|
|
379
|
+
popover.sourceRect = cell.bounds
|
|
380
|
+
}
|
|
381
|
+
```
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: live/audience-watch
|
|
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` 成功后才可调用 `joinLive`。
|
|
18
|
+
|
|
19
|
+
**权限说明**:观众使用 `.playView` 仅拉流,**无需**申请摄像头/麦克风权限。若观众发起连麦则需相机与麦克风权限(参见 live/device-control)。
|
|
20
|
+
|
|
21
|
+
## API 调用(真实签名)
|
|
22
|
+
|
|
23
|
+
```swift
|
|
24
|
+
// LiveCoreView 初始化:观众端用 .playView(拉流模式)
|
|
25
|
+
// 参数名是 viewType(不是 liveScene)
|
|
26
|
+
LiveCoreView(viewType: .playView, frame: CGRect = .zero)
|
|
27
|
+
|
|
28
|
+
// 加入直播间并开始拉流
|
|
29
|
+
// ⚠️ completion 返回 LiveInfo(不是 Void)
|
|
30
|
+
LiveListStore.shared.joinLive(liveID: String,
|
|
31
|
+
completion: LiveInfoCompletionClosure?)
|
|
32
|
+
// LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void
|
|
33
|
+
|
|
34
|
+
// 离开直播间并释放媒体资源
|
|
35
|
+
LiveListStore.shared.leaveLive(completion: CompletionClosure?)
|
|
36
|
+
// CompletionClosure = (Result<Void, ErrorInfo>) -> Void
|
|
37
|
+
|
|
38
|
+
// 订阅直播事件(Combine)
|
|
39
|
+
LiveListStore.shared.liveListEventPublisher // PassthroughSubject<LiveListEvent, Never>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**LiveListEvent 完整签名**
|
|
43
|
+
```swift
|
|
44
|
+
enum LiveListEvent {
|
|
45
|
+
// 直播结束(主播主动结束 / 服务端强制终止)
|
|
46
|
+
case onLiveEnded(liveID: String, reason: LiveEndedReason, message: String)
|
|
47
|
+
|
|
48
|
+
// 被踢出直播间(管理员操作)
|
|
49
|
+
case onKickedOutOfLive(liveID: String, reason: LiveKickedOutReason, message: String)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**通用类型**
|
|
54
|
+
```swift
|
|
55
|
+
typealias CompletionClosure = (Result<Void, ErrorInfo>) -> Void
|
|
56
|
+
typealias LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void
|
|
57
|
+
|
|
58
|
+
struct ErrorInfo {
|
|
59
|
+
var code: Int
|
|
60
|
+
var message: String
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 代码示例
|
|
65
|
+
|
|
66
|
+
```swift
|
|
67
|
+
import AtomicXCore
|
|
68
|
+
import Combine
|
|
69
|
+
|
|
70
|
+
var cancellables = Set<AnyCancellable>()
|
|
71
|
+
var isInLive = false
|
|
72
|
+
|
|
73
|
+
// MARK: - 进入直播间(joinLive)
|
|
74
|
+
|
|
75
|
+
func joinLive(liveID: String) {
|
|
76
|
+
// ⚠️ joinLive completion 是 LiveInfoCompletionClosure,成功时返回 LiveInfo
|
|
77
|
+
LiveListStore.shared.joinLive(liveID: liveID) { result in
|
|
78
|
+
switch result {
|
|
79
|
+
case .success(let liveInfo):
|
|
80
|
+
isInLive = true
|
|
81
|
+
print("[AudienceWatch] 进房成功: \(liveID)")
|
|
82
|
+
print("[AudienceWatch] 直播间名称: \(liveInfo.liveName)")
|
|
83
|
+
print("[AudienceWatch] 主播: \(liveInfo.liveOwner.userID)")
|
|
84
|
+
// 进房成功后启用弹幕/礼物等功能
|
|
85
|
+
|
|
86
|
+
case .failure(let errorInfo):
|
|
87
|
+
// errorInfo: ErrorInfo(.code + .message)
|
|
88
|
+
print("[AudienceWatch] 进房失败, code: \(errorInfo.code), msg: \(errorInfo.message)")
|
|
89
|
+
handleJoinError(errorInfo)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// MARK: - 离开直播间(leaveLive)
|
|
95
|
+
|
|
96
|
+
func leaveLive() {
|
|
97
|
+
guard isInLive else { return }
|
|
98
|
+
isInLive = false
|
|
99
|
+
|
|
100
|
+
// ⚠️ leaveLive completion 是 CompletionClosure(Result<Void, ErrorInfo>)
|
|
101
|
+
LiveListStore.shared.leaveLive { result in
|
|
102
|
+
switch result {
|
|
103
|
+
case .success:
|
|
104
|
+
print("[AudienceWatch] 退出直播间成功")
|
|
105
|
+
case .failure(let errorInfo):
|
|
106
|
+
// 即使失败也清理本地状态,避免残留
|
|
107
|
+
print("[AudienceWatch] 退出直播间失败, code: \(errorInfo.code)")
|
|
108
|
+
}
|
|
109
|
+
cleanupSubscriptions()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// MARK: - 订阅直播事件(先订阅再 joinLive,防止事件丢失)
|
|
114
|
+
|
|
115
|
+
func subscribeLiveEvents(currentLiveID: String) {
|
|
116
|
+
LiveListStore.shared.liveListEventPublisher
|
|
117
|
+
.receive(on: DispatchQueue.main)
|
|
118
|
+
.sink { event in
|
|
119
|
+
switch event {
|
|
120
|
+
// ⚠️ onLiveEnded 有三个关联值:liveID, reason, message
|
|
121
|
+
case .onLiveEnded(let liveID, _, let message)
|
|
122
|
+
where liveID == currentLiveID:
|
|
123
|
+
print("[AudienceWatch] 直播已结束: \(message)")
|
|
124
|
+
handleLiveEnded()
|
|
125
|
+
|
|
126
|
+
// ⚠️ onKickedOutOfLive 有三个关联值:liveID, reason, message
|
|
127
|
+
case .onKickedOutOfLive(let liveID, _, let message)
|
|
128
|
+
where liveID == currentLiveID:
|
|
129
|
+
print("[AudienceWatch] 被踢出直播间: \(message)")
|
|
130
|
+
handleKickedOut()
|
|
131
|
+
|
|
132
|
+
default:
|
|
133
|
+
break
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
.store(in: &cancellables)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// MARK: - 事件处理
|
|
140
|
+
|
|
141
|
+
func handleLiveEnded() {
|
|
142
|
+
isInLive = false
|
|
143
|
+
cleanupSubscriptions()
|
|
144
|
+
print("[AudienceWatch] 直播已结束,返回列表")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func handleKickedOut() {
|
|
148
|
+
isInLive = false
|
|
149
|
+
cleanupSubscriptions()
|
|
150
|
+
print("[AudienceWatch] 您已被移出直播间")
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// MARK: - 进房错误处理
|
|
154
|
+
|
|
155
|
+
func handleJoinError(_ errorInfo: ErrorInfo) {
|
|
156
|
+
switch errorInfo.code {
|
|
157
|
+
case -1002: print("请先登录后再进入直播间")
|
|
158
|
+
case -2001: print("直播间不存在或已结束")
|
|
159
|
+
default: print("进房失败(code: \(errorInfo.code)): \(errorInfo.message)")
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// MARK: - 清理订阅
|
|
164
|
+
|
|
165
|
+
func cleanupSubscriptions() {
|
|
166
|
+
cancellables.removeAll()
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**完整进房 + 退房流程**:
|
|
171
|
+
```swift
|
|
172
|
+
// ① 先订阅事件(防止进房前的事件丢失)
|
|
173
|
+
subscribeLiveEvents(currentLiveID: liveID)
|
|
174
|
+
|
|
175
|
+
// ② 进入直播间
|
|
176
|
+
joinLive(liveID: liveID)
|
|
177
|
+
|
|
178
|
+
// ③ 退出时(页面消失、收到 onLiveEnded / onKickedOutOfLive)
|
|
179
|
+
leaveLive()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
**App 生命周期处理**:
|
|
183
|
+
```swift
|
|
184
|
+
import Combine
|
|
185
|
+
|
|
186
|
+
func observeAppLifecycle(liveID: String) {
|
|
187
|
+
// 进入后台:停止拉流,避免后台占用解码资源
|
|
188
|
+
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
|
|
189
|
+
.sink { _ in
|
|
190
|
+
// 若业务要求后台不能播放,调用 leaveLive 并在前台重新 joinLive
|
|
191
|
+
// leaveLive()
|
|
192
|
+
}
|
|
193
|
+
.store(in: &cancellables)
|
|
194
|
+
|
|
195
|
+
// 回到前台:恢复播放
|
|
196
|
+
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
|
|
197
|
+
.sink { _ in
|
|
198
|
+
// 如已调用 leaveLive,需重新 joinLive
|
|
199
|
+
// joinLive(liveID: liveID)
|
|
200
|
+
}
|
|
201
|
+
.store(in: &cancellables)
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## 调用时序
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
LoginStore.login 成功
|
|
209
|
+
│
|
|
210
|
+
▼
|
|
211
|
+
subscribeLiveEvents(currentLiveID:) ← ① 先订阅,防止事件丢失
|
|
212
|
+
│
|
|
213
|
+
▼
|
|
214
|
+
LiveListStore.shared.joinLive(liveID: String, completion: LiveInfoCompletionClosure?)
|
|
215
|
+
│
|
|
216
|
+
├─ .failure(errorInfo)
|
|
217
|
+
│ ├─ code -1002 → 先登录
|
|
218
|
+
│ ├─ code -2001 → 直播已结束 → 返回列表
|
|
219
|
+
│ └─ 其他 → showAlert(code + message)
|
|
220
|
+
│
|
|
221
|
+
└─ .success(liveInfo) ← 携带完整 LiveInfo
|
|
222
|
+
│
|
|
223
|
+
isInLive = true
|
|
224
|
+
启用弹幕/礼物功能
|
|
225
|
+
│
|
|
226
|
+
▼
|
|
227
|
+
┌──────────────────────────────────┐
|
|
228
|
+
│ 直播进行中 │
|
|
229
|
+
│ onLiveEnded(liveID,reason,msg) │
|
|
230
|
+
│ onKickedOutOfLive(liveID,r,msg) │
|
|
231
|
+
└──────────────────────────────────┘
|
|
232
|
+
│
|
|
233
|
+
[用户退出 / 收到事件]
|
|
234
|
+
│
|
|
235
|
+
▼
|
|
236
|
+
LiveListStore.shared.leaveLive(completion: CompletionClosure?)
|
|
237
|
+
│
|
|
238
|
+
├─ .success / .failure → 清理本地状态(无论成功失败都 cleanup)
|
|
239
|
+
│
|
|
240
|
+
▼
|
|
241
|
+
cancellables.removeAll()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## 平台特有注意事项
|
|
245
|
+
|
|
246
|
+
### 1. joinLive completion 返回 LiveInfo,不是 Void
|
|
247
|
+
```swift
|
|
248
|
+
// ✅ 正确
|
|
249
|
+
LiveListStore.shared.joinLive(liveID: liveID) { result in
|
|
250
|
+
if case .success(let liveInfo) = result {
|
|
251
|
+
print(liveInfo.liveName) // 使用服务端返回的 LiveInfo
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ❌ 错误:completion 没有直接的 list/room 参数
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 2. leaveLive completion 是 `Result<Void, ErrorInfo>`
|
|
259
|
+
`leaveLive` 回调是 `CompletionClosure`(`Result<Void, ErrorInfo>`),即使失败也应清理本地状态:
|
|
260
|
+
```swift
|
|
261
|
+
LiveListStore.shared.leaveLive { result in
|
|
262
|
+
// 无论 .success 还是 .failure,都需要清理本地状态
|
|
263
|
+
isInLive = false
|
|
264
|
+
cancellables.removeAll()
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 3. LiveListEvent 关联值有三个字段
|
|
269
|
+
```swift
|
|
270
|
+
// ✅ 正确
|
|
271
|
+
case .onLiveEnded(let liveID, let reason, let message):
|
|
272
|
+
case .onKickedOutOfLive(let liveID, let reason, let message):
|
|
273
|
+
|
|
274
|
+
// ❌ 错误(只有一个关联值)
|
|
275
|
+
case .onLiveEnded(let liveID):
|
|
276
|
+
case .onKickedOutOfLive(let liveID):
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 4. viewDidDisappear vs deinit 中调用 leaveLive
|
|
280
|
+
建议在 `viewDidDisappear` 中调用而非 `deinit`,原因:iOS push/pop 导航栈时,上级页面不会被销毁(`deinit` 不调用),但 `viewDidDisappear` 会触发。如在 `deinit` 中释放,可能导致用户返回列表后资源未释放直到页面从栈中弹出。
|
|
281
|
+
|
|
282
|
+
### 5. 强引用导致 LiveCoreView 无法释放
|
|
283
|
+
若闭包中捕获 `self` 导致循环引用,`leaveLive` 的回调永远不执行。始终使用 `[weak self]` 捕获 ViewController 引用。
|
|
284
|
+
|
|
285
|
+
### 6. 后台播放与 App Store 合规
|
|
286
|
+
若 App 允许后台音频播放,需在 `Info.plist` 的 `UIBackgroundModes` 中声明 `audio`,否则 App 进入后台后音频会被系统静音,且审核可能被拒。若不支持后台播放,进后台时调用 `leaveLive` 是更安全的选择。
|