@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,373 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: live/audio
|
|
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
|
+
- `DeviceStore.shared.openLocalMicrophone` 已成功回调(麦克风打开后音效才生效)
|
|
18
|
+
- 耳返功能须插入**有线耳机**;蓝牙耳机**不支持**耳返(见注意事项 1)
|
|
19
|
+
|
|
20
|
+
## API 调用
|
|
21
|
+
|
|
22
|
+
```swift
|
|
23
|
+
// ── 采集音量(DeviceStore)──────────────────────────────────────
|
|
24
|
+
// 设置麦克风采集音量;范围 0–100,默认 100(影响观众收到的音量)
|
|
25
|
+
DeviceStore.shared.setCaptureVolume(volume: Int)
|
|
26
|
+
|
|
27
|
+
// ── 耳返(AudioEffectStore)────────────────────────────────────
|
|
28
|
+
// 开启 / 关闭耳返(须插入有线耳机;蓝牙耳机不支持)
|
|
29
|
+
AudioEffectStore.shared.setVoiceEarMonitorEnable(enable: Bool)
|
|
30
|
+
|
|
31
|
+
// 设置耳返音量;范围 0–100,默认 100(仅主播本人通过有线耳机听到)
|
|
32
|
+
AudioEffectStore.shared.setVoiceEarMonitorVolume(volume: Int)
|
|
33
|
+
|
|
34
|
+
// ── 变声(AudioEffectStore)────────────────────────────────────
|
|
35
|
+
// 设置变声类型;传 .none 还原原声
|
|
36
|
+
AudioEffectStore.shared.setAudioChangerType(type: AudioChangerType)
|
|
37
|
+
|
|
38
|
+
// ── 混响(AudioEffectStore)────────────────────────────────────
|
|
39
|
+
// 设置混响类型;传 .none 还原
|
|
40
|
+
AudioEffectStore.shared.setAudioReverbType(type: AudioReverbType)
|
|
41
|
+
|
|
42
|
+
// ── 重置(AudioEffectStore)────────────────────────────────────
|
|
43
|
+
// 离房后效果自动失效;但建议主动调用以保持状态清洁
|
|
44
|
+
AudioEffectStore.shared.reset()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> ⚠️ **耳返音量范围 0–100,不是 0–150**。超出 100 的值行为未定义。
|
|
48
|
+
|
|
49
|
+
| 参数 | 类型 | 说明 |
|
|
50
|
+
|------|------|------|
|
|
51
|
+
| `volume` | `Int` | 音量值,范围 `0–100`,默认 `100` |
|
|
52
|
+
| `enable` | `Bool` | `true` 开启耳返,`false` 关闭 |
|
|
53
|
+
| `type` (AudioChangerType) | `AudioChangerType` | 变声类型,见下方枚举 |
|
|
54
|
+
| `type` (AudioReverbType) | `AudioReverbType` | 混响类型,见下方枚举 |
|
|
55
|
+
|
|
56
|
+
### AudioChangerType 枚举(完整)
|
|
57
|
+
|
|
58
|
+
| 值 | 说明 |
|
|
59
|
+
|----|------|
|
|
60
|
+
| `.none` | 原声(无变声) |
|
|
61
|
+
| `.child` | 儿童音 |
|
|
62
|
+
| `.littleGirl` | 萝莉音 |
|
|
63
|
+
| `.man` | 男声 |
|
|
64
|
+
| `.ethereal` | 空灵 |
|
|
65
|
+
| `.cold` | 冷酷 |
|
|
66
|
+
| `.foreignerr` | 外国腔 |
|
|
67
|
+
| `.heavyMachinery` | 重型机械 |
|
|
68
|
+
| `.heavyMetal` | 重金属 |
|
|
69
|
+
| `.strongCurrent` | 强电流 |
|
|
70
|
+
| `.fatso` | 肥仔 |
|
|
71
|
+
| `.trappedBeast` | 困兽 |
|
|
72
|
+
|
|
73
|
+
### AudioReverbType 枚举(完整)
|
|
74
|
+
|
|
75
|
+
| 值 | 说明 |
|
|
76
|
+
|----|------|
|
|
77
|
+
| `.none` | 无混响 |
|
|
78
|
+
| `.ktv` | KTV |
|
|
79
|
+
| `.smallRoom` | 小房间 |
|
|
80
|
+
| `.auditorium` | 礼堂 |
|
|
81
|
+
| `.loud` | 大型会场 |
|
|
82
|
+
| `.deep` | 深沉 |
|
|
83
|
+
| `.magnetic` | 磁性 |
|
|
84
|
+
| `.metallic` | 金属感 |
|
|
85
|
+
|
|
86
|
+
## 代码示例
|
|
87
|
+
|
|
88
|
+
```swift
|
|
89
|
+
import AtomicXCore
|
|
90
|
+
import AVFoundation
|
|
91
|
+
import Combine
|
|
92
|
+
|
|
93
|
+
// MARK: - 音效面板 ViewModel
|
|
94
|
+
|
|
95
|
+
final class AudioEffectPanelViewModel: ObservableObject {
|
|
96
|
+
|
|
97
|
+
// MARK: Published 状态(与 UI 双向绑定)
|
|
98
|
+
|
|
99
|
+
@Published var captureVolume: Int = 100 // 采集音量 (0–100)
|
|
100
|
+
@Published var earMonitorEnabled: Bool = false // 耳返开关
|
|
101
|
+
@Published var earMonitorVolume: Int = 100 // 耳返音量 (0–100)
|
|
102
|
+
@Published var changerType: AudioChangerType = .none // 变声
|
|
103
|
+
@Published var reverbType: AudioReverbType = .none // 混响
|
|
104
|
+
@Published var isWiredHeadphoneConnected: Bool = false // 有线耳机连接状态
|
|
105
|
+
|
|
106
|
+
private var cancellables = Set<AnyCancellable>()
|
|
107
|
+
|
|
108
|
+
init() {
|
|
109
|
+
syncStateFromStore()
|
|
110
|
+
observeAudioRoute()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// MARK: - 状态同步(从 Store 读取当前值)
|
|
114
|
+
|
|
115
|
+
private func syncStateFromStore() {
|
|
116
|
+
// 从 DeviceStore 同步采集音量
|
|
117
|
+
DeviceStore.shared.state
|
|
118
|
+
.map(\.captureVolume)
|
|
119
|
+
.receive(on: DispatchQueue.main)
|
|
120
|
+
.assign(to: &$captureVolume)
|
|
121
|
+
|
|
122
|
+
// 从 AudioEffectStore 同步音效状态
|
|
123
|
+
AudioEffectStore.shared.state
|
|
124
|
+
.receive(on: DispatchQueue.main)
|
|
125
|
+
.sink { [weak self] state in
|
|
126
|
+
guard let self else { return }
|
|
127
|
+
self.earMonitorEnabled = state.isEarMonitorOpened
|
|
128
|
+
self.earMonitorVolume = state.earMonitorVolume // Int, 0–100
|
|
129
|
+
self.changerType = state.audioChangerType
|
|
130
|
+
self.reverbType = state.audioReverbType
|
|
131
|
+
}
|
|
132
|
+
.store(in: &cancellables)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// MARK: - 耳机路由监听
|
|
136
|
+
// ⚠️ 只检测有线耳机(.headphones),蓝牙耳机不支持耳返
|
|
137
|
+
|
|
138
|
+
private func observeAudioRoute() {
|
|
139
|
+
isWiredHeadphoneConnected = checkWiredHeadphoneConnected()
|
|
140
|
+
|
|
141
|
+
NotificationCenter.default
|
|
142
|
+
.publisher(for: AVAudioSession.routeChangeNotification)
|
|
143
|
+
.receive(on: DispatchQueue.main)
|
|
144
|
+
.sink { [weak self] _ in
|
|
145
|
+
guard let self else { return }
|
|
146
|
+
self.isWiredHeadphoneConnected = self.checkWiredHeadphoneConnected()
|
|
147
|
+
// 有线耳机断开时自动关闭耳返
|
|
148
|
+
if !self.isWiredHeadphoneConnected && self.earMonitorEnabled {
|
|
149
|
+
self.setEarMonitorEnabled(false)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
.store(in: &cancellables)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private func checkWiredHeadphoneConnected() -> Bool {
|
|
156
|
+
let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
|
|
157
|
+
// 仅检测有线耳机(.headphones),蓝牙耳机(.bluetoothA2DP)不支持耳返
|
|
158
|
+
return outputs.contains { $0.portType == .headphones }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// MARK: - 采集音量控制(范围 0–100)
|
|
162
|
+
|
|
163
|
+
func setCaptureVolume(_ volume: Int) {
|
|
164
|
+
let clamped = max(0, min(100, volume))
|
|
165
|
+
DeviceStore.shared.setCaptureVolume(volume: clamped)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// MARK: - 耳返控制
|
|
169
|
+
|
|
170
|
+
func setEarMonitorEnabled(_ enable: Bool) {
|
|
171
|
+
guard !enable || isWiredHeadphoneConnected else {
|
|
172
|
+
// 未接有线耳机时禁止开启,通知 UI 展示提示
|
|
173
|
+
print("[AudioEffect] 耳返需要插入有线耳机,蓝牙耳机不支持")
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
AudioEffectStore.shared.setVoiceEarMonitorEnable(enable: enable)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
func setEarMonitorVolume(_ volume: Int) {
|
|
180
|
+
// ⚠️ 范围 0–100,不是 0–150
|
|
181
|
+
let clamped = max(0, min(100, volume))
|
|
182
|
+
AudioEffectStore.shared.setVoiceEarMonitorVolume(volume: clamped)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// MARK: - 变声控制
|
|
186
|
+
|
|
187
|
+
func setChangerType(_ type: AudioChangerType) {
|
|
188
|
+
AudioEffectStore.shared.setAudioChangerType(type: type)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// MARK: - 混响控制
|
|
192
|
+
|
|
193
|
+
func setReverbType(_ type: AudioReverbType) {
|
|
194
|
+
AudioEffectStore.shared.setAudioReverbType(type: type)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// MARK: - 重置
|
|
198
|
+
// 注意:离房后音效会自动失效,但建议主动调用保持状态干净
|
|
199
|
+
|
|
200
|
+
func resetAll() {
|
|
201
|
+
AudioEffectStore.shared.reset()
|
|
202
|
+
// 采集音量也恢复默认
|
|
203
|
+
DeviceStore.shared.setCaptureVolume(volume: 100)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// MARK: - 音效面板 ViewController
|
|
208
|
+
|
|
209
|
+
final class AudioEffectPanelViewController: UIViewController {
|
|
210
|
+
|
|
211
|
+
private let viewModel = AudioEffectPanelViewModel()
|
|
212
|
+
private var cancellables = Set<AnyCancellable>()
|
|
213
|
+
|
|
214
|
+
// MARK: UI 元素
|
|
215
|
+
|
|
216
|
+
private let captureVolumeSlider = UISlider()
|
|
217
|
+
private let earMonitorSwitch = UISwitch()
|
|
218
|
+
private let earMonitorSlider = UISlider()
|
|
219
|
+
private let changerSegment = UISegmentedControl(items: ["原声", "儿童", "萝莉", "男声"])
|
|
220
|
+
private let reverbSegment = UISegmentedControl(items: ["无", "KTV", "小房间", "金属"])
|
|
221
|
+
private let resetButton = UIButton(type: .system)
|
|
222
|
+
|
|
223
|
+
override func viewDidLoad() {
|
|
224
|
+
super.viewDidLoad()
|
|
225
|
+
setupUI()
|
|
226
|
+
bindViewModel()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// MARK: - UI 绑定
|
|
230
|
+
|
|
231
|
+
private func bindViewModel() {
|
|
232
|
+
// 采集音量(0–100)
|
|
233
|
+
viewModel.$captureVolume
|
|
234
|
+
.map { Float($0) }
|
|
235
|
+
.receive(on: DispatchQueue.main)
|
|
236
|
+
.assign(to: \.value, on: captureVolumeSlider)
|
|
237
|
+
.store(in: &cancellables)
|
|
238
|
+
|
|
239
|
+
// 耳返开关(仅有线耳机连接时可用)
|
|
240
|
+
viewModel.$earMonitorEnabled
|
|
241
|
+
.receive(on: DispatchQueue.main)
|
|
242
|
+
.assign(to: \.isOn, on: earMonitorSwitch)
|
|
243
|
+
.store(in: &cancellables)
|
|
244
|
+
|
|
245
|
+
// 耳返 Switch 启用状态(未连有线耳机时置灰)
|
|
246
|
+
viewModel.$isWiredHeadphoneConnected
|
|
247
|
+
.receive(on: DispatchQueue.main)
|
|
248
|
+
.assign(to: \.isEnabled, on: earMonitorSwitch)
|
|
249
|
+
.store(in: &cancellables)
|
|
250
|
+
|
|
251
|
+
// 耳返音量(0–100)
|
|
252
|
+
viewModel.$earMonitorVolume
|
|
253
|
+
.map { Float($0) }
|
|
254
|
+
.receive(on: DispatchQueue.main)
|
|
255
|
+
.assign(to: \.value, on: earMonitorSlider)
|
|
256
|
+
.store(in: &cancellables)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// MARK: - 控件回调
|
|
260
|
+
|
|
261
|
+
@objc private func captureVolumeChanged(_ slider: UISlider) {
|
|
262
|
+
viewModel.setCaptureVolume(Int(slider.value))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@objc private func earMonitorSwitchChanged(_ sw: UISwitch) {
|
|
266
|
+
viewModel.setEarMonitorEnabled(sw.isOn)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@objc private func earMonitorVolumeChanged(_ slider: UISlider) {
|
|
270
|
+
viewModel.setEarMonitorVolume(Int(slider.value))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@objc private func changerSegmentChanged(_ segment: UISegmentedControl) {
|
|
274
|
+
let types: [AudioChangerType] = [.none, .child, .littleGirl, .man]
|
|
275
|
+
guard segment.selectedSegmentIndex < types.count else { return }
|
|
276
|
+
viewModel.setChangerType(types[segment.selectedSegmentIndex])
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@objc private func reverbSegmentChanged(_ segment: UISegmentedControl) {
|
|
280
|
+
let types: [AudioReverbType] = [.none, .ktv, .smallRoom, .metallic]
|
|
281
|
+
guard segment.selectedSegmentIndex < types.count else { return }
|
|
282
|
+
viewModel.setReverbType(types[segment.selectedSegmentIndex])
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
@objc private func resetTapped() {
|
|
286
|
+
viewModel.resetAll()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// MARK: - 基础 UI 搭建
|
|
290
|
+
|
|
291
|
+
private func setupUI() {
|
|
292
|
+
view.backgroundColor = .systemBackground
|
|
293
|
+
|
|
294
|
+
captureVolumeSlider.minimumValue = 0
|
|
295
|
+
captureVolumeSlider.maximumValue = 100 // ⚠️ 最大值 100,不是 150
|
|
296
|
+
captureVolumeSlider.addTarget(self, action: #selector(captureVolumeChanged), for: .valueChanged)
|
|
297
|
+
|
|
298
|
+
earMonitorSwitch.addTarget(self, action: #selector(earMonitorSwitchChanged), for: .valueChanged)
|
|
299
|
+
|
|
300
|
+
earMonitorSlider.minimumValue = 0
|
|
301
|
+
earMonitorSlider.maximumValue = 100 // ⚠️ 最大值 100,不是 150
|
|
302
|
+
earMonitorSlider.addTarget(self, action: #selector(earMonitorVolumeChanged), for: .valueChanged)
|
|
303
|
+
|
|
304
|
+
changerSegment.selectedSegmentIndex = 0
|
|
305
|
+
changerSegment.addTarget(self, action: #selector(changerSegmentChanged), for: .valueChanged)
|
|
306
|
+
|
|
307
|
+
reverbSegment.selectedSegmentIndex = 0
|
|
308
|
+
reverbSegment.addTarget(self, action: #selector(reverbSegmentChanged), for: .valueChanged)
|
|
309
|
+
|
|
310
|
+
resetButton.setTitle("重置音效", for: .normal)
|
|
311
|
+
resetButton.addTarget(self, action: #selector(resetTapped), for: .touchUpInside)
|
|
312
|
+
|
|
313
|
+
[captureVolumeSlider, earMonitorSwitch, earMonitorSlider,
|
|
314
|
+
changerSegment, reverbSegment, resetButton].forEach { view.addSubview($0) }
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**直播结束时重置(在主播下播回调中调用)**:
|
|
320
|
+
```swift
|
|
321
|
+
func onAnchorStopBroadcast() {
|
|
322
|
+
// 离房后音效自动失效,但主动 reset 可保持本地状态干净
|
|
323
|
+
AudioEffectStore.shared.reset()
|
|
324
|
+
DeviceStore.shared.setCaptureVolume(volume: 100)
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## 调用时序
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
主播开播,麦克风打开成功
|
|
332
|
+
│
|
|
333
|
+
▼
|
|
334
|
+
【可选】预设音效
|
|
335
|
+
├─ DeviceStore.shared.setCaptureVolume(volume: 100) // 采集音量(0–100)
|
|
336
|
+
├─ AudioEffectStore.shared.setAudioChangerType(.ktv) // 变声
|
|
337
|
+
└─ AudioEffectStore.shared.setAudioReverbType(.ktv) // 混响
|
|
338
|
+
│
|
|
339
|
+
▼
|
|
340
|
+
主播开播中:用户通过面板实时调整
|
|
341
|
+
├─ 采集音量:setCaptureVolume(volume:) // 0–100
|
|
342
|
+
├─ 耳返(有线耳机限定):setVoiceEarMonitorEnable / setVoiceEarMonitorVolume(0–100)
|
|
343
|
+
├─ 变声:setAudioChangerType(type:)
|
|
344
|
+
└─ 混响:setAudioReverbType(type:)
|
|
345
|
+
│
|
|
346
|
+
▼
|
|
347
|
+
主播下播 / 退出直播间
|
|
348
|
+
│
|
|
349
|
+
├─ AudioEffectStore.shared.reset() ← 主动重置,保持状态干净
|
|
350
|
+
└─ DeviceStore.shared.setCaptureVolume(volume: 100)
|
|
351
|
+
|
|
352
|
+
有线耳机连接监听(贯穿整个直播生命周期)
|
|
353
|
+
├─ AVAudioSession.routeChangeNotification 触发
|
|
354
|
+
├─ 有线耳机拔出 → 自动关闭耳返,置灰耳返开关
|
|
355
|
+
└─ 有线耳机插入 → 允许用户开启耳返
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## 平台特有注意事项
|
|
359
|
+
|
|
360
|
+
### 1. 蓝牙耳机不支持耳返
|
|
361
|
+
**蓝牙耳机(AirPods 等)不支持耳返功能**。耳返只对有线耳机(`AVAudioSessionPortHeadphones`)有效。检测耳机时只判断 `.headphones` 类型,不要包含 `.bluetoothA2DP`,否则会误判导致用户开启耳返后听不到声音。
|
|
362
|
+
|
|
363
|
+
### 2. 耳返音量范围 0–100
|
|
364
|
+
`setVoiceEarMonitorVolume` 和 `AudioEffectState.earMonitorVolume` 的有效范围是 **0–100**,不是 0–150。UI 滑块的 `maximumValue` 应设为 `100`。
|
|
365
|
+
|
|
366
|
+
### 3. 离房后音效自动失效
|
|
367
|
+
离开直播间后,`AudioEffectStore` 的音效参数自动失效,无需手动 reset 来"清除效果"。但调用 `reset()` 可以将本地 `AudioEffectState` 状态归零,避免下次开播时 UI 显示残留的旧值。
|
|
368
|
+
|
|
369
|
+
### 4. AVAudioSession 配置
|
|
370
|
+
音效功能依赖 SDK 内部的 `AVAudioSession` 配置(通常为 `.playAndRecord`)。若 App 自行修改了 `AVAudioSession.category`,可能导致耳返或混响失效。建议将音频会话管理统一交给 SDK,不要在直播期间手动调用 `AVAudioSession.setCategory`。
|
|
371
|
+
|
|
372
|
+
### 5. 变声与混响可叠加使用
|
|
373
|
+
`setAudioChangerType` 与 `setAudioReverbType` 互不干扰,可同时生效(如"萝莉音 + KTV 混响")。需要单独还原某一项时,传入对应的 `.none` 即可,不影响另一项设置。
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: live/barrage
|
|
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
|
+
- 已成功加入直播间(房间 ID 即 `liveID`)
|
|
18
|
+
|
|
19
|
+
## API 调用
|
|
20
|
+
|
|
21
|
+
```swift
|
|
22
|
+
// 创建弹幕实例(与直播间绑定)
|
|
23
|
+
let barrageStore = BarrageStore.create(liveID: liveID)
|
|
24
|
+
|
|
25
|
+
// 发送文本弹幕
|
|
26
|
+
// ⚠️ extensionInfo 类型为 [String: String]?,不是 [String: Any]?
|
|
27
|
+
barrageStore.sendTextMessage(
|
|
28
|
+
text: String,
|
|
29
|
+
extensionInfo: [String: String]?,
|
|
30
|
+
completion: CompletionClosure?
|
|
31
|
+
)
|
|
32
|
+
// CompletionClosure = (Result<Void, ErrorInfo>) -> Void
|
|
33
|
+
|
|
34
|
+
// 发送自定义消息
|
|
35
|
+
barrageStore.sendCustomMessage(
|
|
36
|
+
businessID: String,
|
|
37
|
+
data: String, // JSON 字符串
|
|
38
|
+
completion: CompletionClosure?
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// 插入本地提示(不广播,仅当前客户端可见)
|
|
42
|
+
barrageStore.appendLocalTip(message: Barrage)
|
|
43
|
+
|
|
44
|
+
// 订阅消息列表状态变化
|
|
45
|
+
// BarrageStore 上没有 eventPublisher,只通过 state 订阅
|
|
46
|
+
barrageStore.state // StatePublisher<BarrageState>
|
|
47
|
+
// BarrageState.messageList: [Barrage]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
> ⚠️ **注意**:`BarrageStore` 上没有独立的 `eventPublisher`,消息通过 `state.messageList` 分发。
|
|
51
|
+
> ⚠️ **注意**:`disableSendMessage` 属于 `LiveAudienceStore`,不属于 `BarrageStore`,见 [live/audience-manage](live/audience-manage.md)。
|
|
52
|
+
|
|
53
|
+
| 参数 | 类型 | 说明 |
|
|
54
|
+
|------|------|------|
|
|
55
|
+
| `liveID` | `String` | 直播间唯一标识,与进房 ID 保持一致 |
|
|
56
|
+
| `text` | `String` | 消息文本内容 |
|
|
57
|
+
| `extensionInfo` | `[String: String]?` | 扩展信息字典(值必须为 String,不支持嵌套) |
|
|
58
|
+
| `businessID` | `String` | 自定义消息类型标识,如 `"gift_notify"` |
|
|
59
|
+
| `data` | `String` | JSON 格式的自定义消息体 |
|
|
60
|
+
|
|
61
|
+
## 代码示例
|
|
62
|
+
|
|
63
|
+
### 完整弹幕集成
|
|
64
|
+
|
|
65
|
+
```swift
|
|
66
|
+
import AtomicXCore
|
|
67
|
+
import Combine
|
|
68
|
+
|
|
69
|
+
final class BarrageManager {
|
|
70
|
+
|
|
71
|
+
// MARK: - 属性
|
|
72
|
+
|
|
73
|
+
private let barrageStore: BarrageStore
|
|
74
|
+
private var cancellables = Set<AnyCancellable>()
|
|
75
|
+
|
|
76
|
+
/// 经节流后供 UI 使用的消息列表(最多 500 条)
|
|
77
|
+
@Published private(set) var displayMessages: [Barrage] = []
|
|
78
|
+
|
|
79
|
+
// 节流定时器
|
|
80
|
+
private var throttleTimer: Timer?
|
|
81
|
+
private let throttleInterval: TimeInterval = 0.3 // 300ms
|
|
82
|
+
|
|
83
|
+
// MARK: - 初始化
|
|
84
|
+
|
|
85
|
+
init(liveID: String) {
|
|
86
|
+
// 步骤1: 创建与直播间绑定的 BarrageStore
|
|
87
|
+
self.barrageStore = BarrageStore.create(liveID: liveID)
|
|
88
|
+
|
|
89
|
+
// 步骤2: 订阅消息列表变化(通过 state,不是 eventPublisher)
|
|
90
|
+
subscribeMessageList()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// MARK: - 订阅
|
|
94
|
+
|
|
95
|
+
private func subscribeMessageList() {
|
|
96
|
+
barrageStore.state
|
|
97
|
+
.map(\.messageList)
|
|
98
|
+
.receive(on: DispatchQueue.main)
|
|
99
|
+
.sink { [weak self] messages in
|
|
100
|
+
// 步骤3: 节流处理,避免高频刷新
|
|
101
|
+
self?.scheduleThrottledUpdate(messages: messages)
|
|
102
|
+
}
|
|
103
|
+
.store(in: &cancellables)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// MARK: - 节流 + 循环缓冲
|
|
107
|
+
|
|
108
|
+
private func scheduleThrottledUpdate(messages: [Barrage]) {
|
|
109
|
+
// 取消上一个待执行的 timer
|
|
110
|
+
throttleTimer?.invalidate()
|
|
111
|
+
throttleTimer = Timer.scheduledTimer(
|
|
112
|
+
withTimeInterval: throttleInterval,
|
|
113
|
+
repeats: false
|
|
114
|
+
) { [weak self] _ in
|
|
115
|
+
guard let self else { return }
|
|
116
|
+
// 循环缓冲:超过 500 条时截取最新 500 条
|
|
117
|
+
let capped = messages.count > 500
|
|
118
|
+
? Array(messages.suffix(500))
|
|
119
|
+
: messages
|
|
120
|
+
self.displayMessages = capped
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// MARK: - 展示层(TableView 配置建议)
|
|
125
|
+
|
|
126
|
+
/// 配置弹幕 Cell 的异步渲染
|
|
127
|
+
func configureBarrageCell(_ cell: UITableViewCell) {
|
|
128
|
+
// 步骤4: 异步渲染降低主线程压力
|
|
129
|
+
cell.layer.drawsAsynchronously = true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// MARK: - 发送文本弹幕
|
|
133
|
+
|
|
134
|
+
func sendText(_ text: String,
|
|
135
|
+
completion: ((Result<Void, ErrorInfo>) -> Void)? = nil) {
|
|
136
|
+
// 步骤5: 发送文本消息
|
|
137
|
+
// extensionInfo 类型为 [String: String]?,值只能是字符串
|
|
138
|
+
barrageStore.sendTextMessage(
|
|
139
|
+
text: text,
|
|
140
|
+
extensionInfo: nil
|
|
141
|
+
) { result in
|
|
142
|
+
switch result {
|
|
143
|
+
case .success:
|
|
144
|
+
// 成功时 messageList 会自动更新,无需手动插入
|
|
145
|
+
completion?(.success(()))
|
|
146
|
+
case .failure(let error):
|
|
147
|
+
print("[Barrage] 发送失败 code=\(error.code) msg=\(error.message)")
|
|
148
|
+
completion?(.failure(error))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// MARK: - 发送自定义弹幕(如礼物通知)
|
|
154
|
+
|
|
155
|
+
func sendGiftNotification(giftName: String, count: Int) {
|
|
156
|
+
// 步骤6: 自定义消息示例(JSON 格式)
|
|
157
|
+
let payload: [String: Any] = [
|
|
158
|
+
"gift_name": giftName,
|
|
159
|
+
"count": count,
|
|
160
|
+
"timestamp": Date().timeIntervalSince1970
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
guard let jsonData = try? JSONSerialization.data(withJSONObject: payload),
|
|
164
|
+
let jsonString = String(data: jsonData, encoding: .utf8) else {
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
barrageStore.sendCustomMessage(
|
|
169
|
+
businessID: "gift_notify", // 接收端通过此 ID 识别消息类型
|
|
170
|
+
data: jsonString
|
|
171
|
+
) { result in
|
|
172
|
+
if case .failure(let error) = result {
|
|
173
|
+
print("[Barrage] 自定义消息发送失败 code=\(error.code) msg=\(error.message)")
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// MARK: - 本地提示(不广播)
|
|
179
|
+
|
|
180
|
+
func appendSystemTip(_ text: String) {
|
|
181
|
+
// 步骤7: 插入仅本端可见的系统提示
|
|
182
|
+
var tip = Barrage()
|
|
183
|
+
tip.textContent = text
|
|
184
|
+
barrageStore.appendLocalTip(message: tip)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### UI 绑定(UIKit)
|
|
190
|
+
|
|
191
|
+
```swift
|
|
192
|
+
// UIKit: 订阅 displayMessages 刷新 TableView
|
|
193
|
+
barrageManager.$displayMessages
|
|
194
|
+
.receive(on: DispatchQueue.main)
|
|
195
|
+
.sink { [weak self] messages in
|
|
196
|
+
self?.tableView.reloadData()
|
|
197
|
+
// 自动滚动到最新消息
|
|
198
|
+
if !messages.isEmpty {
|
|
199
|
+
let lastIndex = IndexPath(row: messages.count - 1, section: 0)
|
|
200
|
+
self?.tableView.scrollToRow(at: lastIndex, at: .bottom, animated: true)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
.store(in: &cancellables)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 自定义消息接收解析
|
|
207
|
+
|
|
208
|
+
```swift
|
|
209
|
+
// 接收端解析自定义弹幕
|
|
210
|
+
func parseCustomBarrage(_ barrage: Barrage) {
|
|
211
|
+
guard barrage.messageType == .custom else { return }
|
|
212
|
+
|
|
213
|
+
switch barrage.businessID {
|
|
214
|
+
case "gift_notify":
|
|
215
|
+
// 解析礼物通知
|
|
216
|
+
if let data = barrage.data?.data(using: .utf8),
|
|
217
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
218
|
+
let giftName = json["gift_name"] as? String,
|
|
219
|
+
let count = json["count"] as? Int {
|
|
220
|
+
showGiftAnimation(giftName: giftName, count: count)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case "like_action":
|
|
224
|
+
// 解析点赞动效
|
|
225
|
+
showLikeAnimation()
|
|
226
|
+
|
|
227
|
+
default:
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## 调用时序
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
进房成功
|
|
237
|
+
│
|
|
238
|
+
▼
|
|
239
|
+
BarrageStore.create(liveID:) // 创建实例
|
|
240
|
+
│
|
|
241
|
+
▼
|
|
242
|
+
订阅 barrageStore.state.messageList // 建立状态监听(无独立 eventPublisher)
|
|
243
|
+
│
|
|
244
|
+
├─ 收到消息更新
|
|
245
|
+
│ │
|
|
246
|
+
│ ▼
|
|
247
|
+
│ 节流处理(300ms Timer)
|
|
248
|
+
│ │
|
|
249
|
+
│ ▼
|
|
250
|
+
│ 循环缓冲截取(max 500)
|
|
251
|
+
│ │
|
|
252
|
+
│ ▼
|
|
253
|
+
│ 主线程刷新 UI
|
|
254
|
+
│
|
|
255
|
+
├─ 用户发送弹幕
|
|
256
|
+
│ │
|
|
257
|
+
│ ▼
|
|
258
|
+
│ sendTextMessage / sendCustomMessage
|
|
259
|
+
│ ├─ .success → messageList 自动更新,无需手动插入
|
|
260
|
+
│ └─ .failure(ErrorInfo) → 展示错误提示
|
|
261
|
+
│ ├─ code -2380 全员禁言
|
|
262
|
+
│ └─ code -2381 已被禁言
|
|
263
|
+
│
|
|
264
|
+
└─ 退出直播间
|
|
265
|
+
│
|
|
266
|
+
▼
|
|
267
|
+
cancellables.removeAll() // 取消所有订阅
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## 平台特有注意事项
|
|
271
|
+
|
|
272
|
+
### 1. Combine 订阅生命周期
|
|
273
|
+
`cancellables` 需与 ViewController / ViewModel 的生命周期保持一致。在 `deinit` 或 `viewDidDisappear` 时调用 `cancellables.removeAll()`,防止在直播间退出后仍收到回调。
|
|
274
|
+
|
|
275
|
+
### 2. extensionInfo 类型为 `[String: String]?`
|
|
276
|
+
`sendTextMessage` 的 `extensionInfo` 参数类型是 `[String: String]?`,不是 `[String: Any]?`。若需要传递复杂结构,请先将值 JSON 序列化为字符串再放入字典。
|
|
277
|
+
|
|
278
|
+
### 3. 键盘遮挡弹幕列表
|
|
279
|
+
iOS 上键盘弹出会遮盖底部弹幕输入框。监听 `UIResponder.keyboardWillShowNotification` 动态调整 `tableView` 的 `contentInset.bottom`,确保最新弹幕可见。
|
|
280
|
+
|
|
281
|
+
### 4. 模拟器限制
|
|
282
|
+
模拟器网络行为与真机存在差异,建议在真机上测试弹幕高并发场景(弹幕风暴)以验证节流效果。
|
|
283
|
+
|
|
284
|
+
### 5. 自定义消息 data 大小限制
|
|
285
|
+
`sendCustomMessage` 的 `data` 字段建议不超过 **4KB**,超出可能导致消息发送失败或被截断。礼物动画资源等大内容应使用 URL 而非内嵌数据。
|