@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,313 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: live/anchor-lifecycle
|
|
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
|
+
- `DeviceStore.shared.openLocalMicrophone` 已成功(麦克风就绪)
|
|
19
|
+
- `LiveInfo` 已配置完毕(通过 `init(seatTemplate:)` 初始化,liveID 必填)
|
|
20
|
+
|
|
21
|
+
## API 调用(真实签名)
|
|
22
|
+
|
|
23
|
+
```swift
|
|
24
|
+
// LiveCoreView 初始化:参数名是 viewType(不是 liveScene)
|
|
25
|
+
// CoreViewType 枚举:.pushView(推流/主播端)、.playView(拉流/观众端)
|
|
26
|
+
LiveCoreView(viewType: CoreViewType, frame: CGRect = .zero)
|
|
27
|
+
|
|
28
|
+
// 开播:第一参数无标签;completion 返回 LiveInfo
|
|
29
|
+
LiveListStore.shared.createLive(_ liveInfo: LiveInfo,
|
|
30
|
+
completion: LiveInfoCompletionClosure?)
|
|
31
|
+
// LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void
|
|
32
|
+
|
|
33
|
+
// 结束直播:无 liveID 参数
|
|
34
|
+
LiveListStore.shared.endLive(completion: StopLiveCompletionClosure?)
|
|
35
|
+
// StopLiveCompletionClosure = ... // TODO: 待验证 — 确认实际签名
|
|
36
|
+
|
|
37
|
+
// 订阅直播列表事件(Combine)
|
|
38
|
+
LiveListStore.shared.liveListEventPublisher // PassthroughSubject<LiveListEvent, Never>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**LiveListEvent 完整签名**
|
|
42
|
+
```swift
|
|
43
|
+
enum LiveListEvent {
|
|
44
|
+
// 直播被动结束(服务端强制终止/超时)
|
|
45
|
+
case onLiveEnded(liveID: String, reason: LiveEndedReason, message: String)
|
|
46
|
+
|
|
47
|
+
// 被踢出直播间
|
|
48
|
+
case onKickedOutOfLive(liveID: String, reason: LiveKickedOutReason, message: String)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**关键类型说明**
|
|
53
|
+
```swift
|
|
54
|
+
// LiveInfo 初始化(必须传 seatTemplate)
|
|
55
|
+
var liveInfo = LiveInfo(seatTemplate: .videoDynamicGrid9Seats)
|
|
56
|
+
liveInfo.liveID = "your-live-id"
|
|
57
|
+
liveInfo.liveName = "直播间名称"
|
|
58
|
+
|
|
59
|
+
// 通用回调
|
|
60
|
+
typealias CompletionClosure = (Result<Void, ErrorInfo>) -> Void
|
|
61
|
+
typealias LiveInfoCompletionClosure = (Result<LiveInfo, ErrorInfo>) -> Void
|
|
62
|
+
|
|
63
|
+
struct ErrorInfo {
|
|
64
|
+
var code: Int
|
|
65
|
+
var message: String
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 代码示例
|
|
70
|
+
|
|
71
|
+
```swift
|
|
72
|
+
import AtomicXCore
|
|
73
|
+
import Combine
|
|
74
|
+
|
|
75
|
+
var cancellables = Set<AnyCancellable>()
|
|
76
|
+
var isLiving = false
|
|
77
|
+
|
|
78
|
+
// MARK: - 开播
|
|
79
|
+
|
|
80
|
+
func startLive(liveID: String, liveName: String) {
|
|
81
|
+
// ⚠️ 必须用 init(seatTemplate:),不是 LiveInfo()
|
|
82
|
+
var liveInfo = LiveInfo(seatTemplate: .videoDynamicGrid9Seats)
|
|
83
|
+
liveInfo.liveID = liveID
|
|
84
|
+
liveInfo.liveName = liveName
|
|
85
|
+
|
|
86
|
+
// ⚠️ 第一参数无标签(unnamed first param)
|
|
87
|
+
LiveListStore.shared.createLive(liveInfo) { result in
|
|
88
|
+
switch result {
|
|
89
|
+
case .success(let createdLiveInfo):
|
|
90
|
+
// 成功时回调携带服务端确认的 LiveInfo(含 createTime 等字段)
|
|
91
|
+
isLiving = true
|
|
92
|
+
print("[Lifecycle] 开播成功, liveID: \(createdLiveInfo.liveID)")
|
|
93
|
+
|
|
94
|
+
case .failure(let errorInfo):
|
|
95
|
+
print("[Lifecycle] 开播失败, code: \(errorInfo.code), msg: \(errorInfo.message)")
|
|
96
|
+
handleCreateError(errorInfo)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// MARK: - 结束直播
|
|
102
|
+
|
|
103
|
+
func stopLive() {
|
|
104
|
+
guard isLiving else { return }
|
|
105
|
+
isLiving = false
|
|
106
|
+
|
|
107
|
+
// Step 1: 关闭设备(先关设备再结束房间)
|
|
108
|
+
DeviceStore.shared.closeLocalCamera()
|
|
109
|
+
DeviceStore.shared.closeLocalMicrophone()
|
|
110
|
+
|
|
111
|
+
// Step 2: 结束直播(无 liveID 参数,endLive 结束当前房间)
|
|
112
|
+
LiveListStore.shared.endLive { result in
|
|
113
|
+
switch result {
|
|
114
|
+
case .success:
|
|
115
|
+
print("[Lifecycle] 直播结束成功")
|
|
116
|
+
cleanupSubscriptions()
|
|
117
|
+
|
|
118
|
+
case .failure(let errorInfo):
|
|
119
|
+
// 即使失败也应清理本地资源
|
|
120
|
+
print("[Lifecycle] endLive 失败, code: \(errorInfo.code)")
|
|
121
|
+
cleanupSubscriptions()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// MARK: - 订阅被动结束事件(先订阅再开播,防止事件丢失)
|
|
127
|
+
|
|
128
|
+
func subscribeLiveEvents(currentLiveID: String) {
|
|
129
|
+
LiveListStore.shared.liveListEventPublisher
|
|
130
|
+
.receive(on: DispatchQueue.main)
|
|
131
|
+
.sink { event in
|
|
132
|
+
switch event {
|
|
133
|
+
// ⚠️ onLiveEnded 有三个关联值:liveID, reason, message
|
|
134
|
+
case .onLiveEnded(let liveID, let reason, let message)
|
|
135
|
+
where liveID == currentLiveID:
|
|
136
|
+
print("[Lifecycle] 直播被动结束, reason: \(reason), msg: \(message)")
|
|
137
|
+
handlePassiveEnd(reason: message)
|
|
138
|
+
|
|
139
|
+
// ⚠️ onKickedOutOfLive 有三个关联值:liveID, reason, message
|
|
140
|
+
case .onKickedOutOfLive(let liveID, let reason, let message)
|
|
141
|
+
where liveID == currentLiveID:
|
|
142
|
+
print("[Lifecycle] 被踢出直播间, reason: \(reason), msg: \(message)")
|
|
143
|
+
handleKickedOut(message: message)
|
|
144
|
+
|
|
145
|
+
default:
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
.store(in: &cancellables)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// MARK: - 被动结束处理
|
|
153
|
+
|
|
154
|
+
func handlePassiveEnd(reason: String) {
|
|
155
|
+
isLiving = false
|
|
156
|
+
DeviceStore.shared.closeLocalCamera()
|
|
157
|
+
DeviceStore.shared.closeLocalMicrophone()
|
|
158
|
+
cleanupSubscriptions()
|
|
159
|
+
print("[Lifecycle] 被动结束处理完成:\(reason)")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func handleKickedOut(message: String) {
|
|
163
|
+
isLiving = false
|
|
164
|
+
DeviceStore.shared.closeLocalCamera()
|
|
165
|
+
DeviceStore.shared.closeLocalMicrophone()
|
|
166
|
+
cleanupSubscriptions()
|
|
167
|
+
print("[Lifecycle] 被踢出处理完成:\(message)")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// MARK: - 错误处理
|
|
171
|
+
|
|
172
|
+
func handleCreateError(_ errorInfo: ErrorInfo) {
|
|
173
|
+
switch errorInfo.code {
|
|
174
|
+
case -2105: print("直播间 ID 格式非法")
|
|
175
|
+
case -2107: print("直播间名称非法(超长或含特殊字符)")
|
|
176
|
+
case -2108: print("您已在其他直播间,请先退出")
|
|
177
|
+
default: print("开播失败(code: \(errorInfo.code)): \(errorInfo.message)")
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// MARK: - 清理
|
|
182
|
+
|
|
183
|
+
func cleanupSubscriptions() {
|
|
184
|
+
cancellables.removeAll()
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## 调用时序
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
设备就绪(openLocalCamera + openLocalMicrophone 成功)
|
|
192
|
+
│
|
|
193
|
+
▼
|
|
194
|
+
subscribeLiveEvents(currentLiveID:) ← ① 先订阅事件,防止 createLive 前丢失
|
|
195
|
+
│
|
|
196
|
+
▼
|
|
197
|
+
构建 LiveInfo(seatTemplate: .videoDynamicGrid9Seats)
|
|
198
|
+
设置 liveID + liveName(+ 其他选填字段)
|
|
199
|
+
│
|
|
200
|
+
▼
|
|
201
|
+
// 第一参数无标签
|
|
202
|
+
LiveListStore.shared.createLive(liveInfo) { result in ... }
|
|
203
|
+
│
|
|
204
|
+
├─ .failure(errorInfo)
|
|
205
|
+
│ ├─ code -2105/-2107/-2108 → 展示错误,退出
|
|
206
|
+
│ └─ 其他 → 展示错误信息
|
|
207
|
+
│
|
|
208
|
+
└─ .success(createdLiveInfo)
|
|
209
|
+
│
|
|
210
|
+
▼
|
|
211
|
+
isLiving = true
|
|
212
|
+
UI 更新为「🔴 直播中」
|
|
213
|
+
│
|
|
214
|
+
┌───────────────────────────┐
|
|
215
|
+
│ 直播进行中 │
|
|
216
|
+
│ onLiveEnded →被动结束 │ liveID + reason + message
|
|
217
|
+
│ onKickedOutOfLive→踢出 │ liveID + reason + message
|
|
218
|
+
└───────────────────────────┘
|
|
219
|
+
│
|
|
220
|
+
[用户主动结束 / 被动结束]
|
|
221
|
+
│
|
|
222
|
+
▼
|
|
223
|
+
步骤1: DeviceStore.closeLocalCamera()
|
|
224
|
+
DeviceStore.closeLocalMicrophone()
|
|
225
|
+
步骤2: LiveListStore.shared.endLive(completion:)
|
|
226
|
+
│
|
|
227
|
+
└─ .success / .failure
|
|
228
|
+
│
|
|
229
|
+
▼
|
|
230
|
+
步骤3: cancellables.removeAll() ← 取消事件订阅
|
|
231
|
+
返回上层页面
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## 平台特有注意事项
|
|
235
|
+
|
|
236
|
+
### 1. createLive 第一参数无标签
|
|
237
|
+
```swift
|
|
238
|
+
// ✅ 正确
|
|
239
|
+
LiveListStore.shared.createLive(liveInfo) { result in ... }
|
|
240
|
+
|
|
241
|
+
// ❌ 错误(带标签)
|
|
242
|
+
LiveListStore.shared.createLive(liveInfo: liveInfo) { ... }
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 2. endLive 无 liveID 参数
|
|
246
|
+
`endLive(completion:)` 结束的是 SDK 当前所在的直播间,不需要传 liveID:
|
|
247
|
+
```swift
|
|
248
|
+
// ✅ 正确
|
|
249
|
+
LiveListStore.shared.endLive { result in ... }
|
|
250
|
+
|
|
251
|
+
// ❌ 错误(没有此参数)
|
|
252
|
+
LiveListStore.shared.endLive(liveID: liveID) { ... }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 3. LiveListEvent 关联值有三个字段
|
|
256
|
+
`onLiveEnded` 和 `onKickedOutOfLive` 的关联值均为 `(liveID: String, reason: …, message: String)`,不是只有 `liveID`:
|
|
257
|
+
```swift
|
|
258
|
+
// ✅ 正确
|
|
259
|
+
case .onLiveEnded(let liveID, let reason, let message):
|
|
260
|
+
// 使用 liveID 过滤当前直播间
|
|
261
|
+
|
|
262
|
+
// ❌ 错误(缺少 reason 和 message)
|
|
263
|
+
case .onLiveEnded(let liveID):
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### 4. Combine 订阅生命周期管理
|
|
267
|
+
`liveListEventPublisher` 的 Combine 订阅存储在 `cancellables` 中。务必在退出直播间时调用 `cancellables.removeAll()`,否则:
|
|
268
|
+
- ViewController 被释放后订阅仍存活(内存泄漏)
|
|
269
|
+
- 后续事件可能触发已释放对象的回调(野指针)
|
|
270
|
+
|
|
271
|
+
### 5. endLive 失败时的兜底处理
|
|
272
|
+
网络异常可能导致 `endLive` 回调超时或失败。即使失败也应清理本地资源(关闭设备、取消订阅),并在下次启动时通过服务端状态检查直播间是否仍存在。
|
|
273
|
+
|
|
274
|
+
### 6. 导航返回的拦截
|
|
275
|
+
通过 `viewWillDisappear` + `isMovingFromParent` 检测用户通过导航返回键意外退出:
|
|
276
|
+
```swift
|
|
277
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
278
|
+
super.viewWillDisappear(animated)
|
|
279
|
+
if isLiving && isMovingFromParent {
|
|
280
|
+
stopLive()
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 7. LiveCoreView 初始化参数名是 `viewType`(不是 `liveScene`)
|
|
286
|
+
`LiveCoreView` 的初始化方法参数名为 `viewType`,类型为 `CoreViewType` 枚举(`.pushView` 推流 / `.playView` 拉流):
|
|
287
|
+
```swift
|
|
288
|
+
// ✅ 正确:参数名是 viewType
|
|
289
|
+
let liveCoreView = LiveCoreView(viewType: .pushView)
|
|
290
|
+
|
|
291
|
+
// ❌ 错误:没有 liveScene 参数
|
|
292
|
+
let liveCoreView = LiveCoreView(liveScene: .pushView)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### 8. StatePublisher 读取属性必须通过 `.value`
|
|
296
|
+
所有 Store 的 `.state` 属性类型是 `StatePublisher<T>`,不是 `T` 本身。读取当前状态值必须通过 `.value`:
|
|
297
|
+
```swift
|
|
298
|
+
// ✅ 正确:通过 .value 访问 state 属性
|
|
299
|
+
let count = audienceStore?.state.value.audienceCount ?? 0
|
|
300
|
+
|
|
301
|
+
// ❌ 错误:StatePublisher 上没有 audienceCount 成员
|
|
302
|
+
let count = audienceStore?.state.audienceCount ?? 0
|
|
303
|
+
```
|
|
304
|
+
同理适用于所有 StatePublisher 包装的状态结构体:`LiveListState`、`DeviceState`、`BarrageState` 等。
|
|
305
|
+
|
|
306
|
+
### 9. MUST: 文件头必须 `import AtomicXCore`
|
|
307
|
+
使用 LiveCoreView、LiveListStore、DeviceStore、LiveAudienceStore 等类型时,必须在文件顶部显式 import:
|
|
308
|
+
```swift
|
|
309
|
+
import UIKit
|
|
310
|
+
import AtomicXCore // ← 必须,否则编译报 "cannot find type 'XXX' in scope"
|
|
311
|
+
import Combine // 如果使用 Publisher/AnyCancellable
|
|
312
|
+
```
|
|
313
|
+
不要将 import 写在注释中,必须是真正的 import 语句。
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: live/anchor-preview
|
|
3
|
+
platform: ios
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 主播预览 — iOS 实现
|
|
7
|
+
|
|
8
|
+
## 前置条件
|
|
9
|
+
|
|
10
|
+
**依赖安装(Podfile)**
|
|
11
|
+
```ruby
|
|
12
|
+
pod 'AtomicXCore', '~> 4.0'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Info.plist 权限声明**
|
|
16
|
+
```xml
|
|
17
|
+
<key>NSCameraUsageDescription</key>
|
|
18
|
+
<string>需要访问摄像头以进行视频直播</string>
|
|
19
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
20
|
+
<string>需要访问麦克风以进行语音直播</string>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**前置状态**:
|
|
24
|
+
- `LoginStore.shared` 登录成功(须完成登录)
|
|
25
|
+
- 摄像头/麦克风系统权限已授予
|
|
26
|
+
|
|
27
|
+
## API 调用(真实签名)
|
|
28
|
+
|
|
29
|
+
```swift
|
|
30
|
+
// LiveCoreView 初始化:主播端用 .pushView(推流模式)
|
|
31
|
+
// ⚠️ 参数名是 viewType(不是 liveScene)
|
|
32
|
+
LiveCoreView(viewType: .pushView, frame: CGRect = .zero)
|
|
33
|
+
|
|
34
|
+
// 绑定直播间 ID(创建后必须立即调用,否则黑屏)
|
|
35
|
+
liveCoreView.setLiveID(_ liveID: String)
|
|
36
|
+
|
|
37
|
+
// 1. 打开前/后置摄像头(含 completion 回调)
|
|
38
|
+
DeviceStore.shared.openLocalCamera(isFront: Bool, completion: CompletionClosure?)
|
|
39
|
+
// CompletionClosure = (Result<Void, ErrorInfo>) -> Void
|
|
40
|
+
|
|
41
|
+
// 2. 打开麦克风
|
|
42
|
+
DeviceStore.shared.openLocalMicrophone(completion: CompletionClosure?)
|
|
43
|
+
|
|
44
|
+
// 3. 不中断采集情况下切换前后摄像头
|
|
45
|
+
DeviceStore.shared.switchCamera(isFront: Bool)
|
|
46
|
+
|
|
47
|
+
// 4. 退出预览时关闭设备
|
|
48
|
+
DeviceStore.shared.closeLocalCamera()
|
|
49
|
+
DeviceStore.shared.closeLocalMicrophone()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| 参数 | 类型 | 说明 |
|
|
53
|
+
|------|------|------|
|
|
54
|
+
| `isFront` | `Bool` | `true` = 前置摄像头(默认),`false` = 后置 |
|
|
55
|
+
| `completion` | `CompletionClosure?` | `(Result<Void, ErrorInfo>) -> Void`;`nil` 表示不关心结果 |
|
|
56
|
+
|
|
57
|
+
## 代码示例
|
|
58
|
+
|
|
59
|
+
```swift
|
|
60
|
+
import UIKit
|
|
61
|
+
import AtomicXCore
|
|
62
|
+
import AVFoundation
|
|
63
|
+
import Combine
|
|
64
|
+
|
|
65
|
+
// MARK: - 主播预览核心逻辑(SDK API 使用为主)
|
|
66
|
+
|
|
67
|
+
// Step 1: 检查摄像头权限,通过后打开设备
|
|
68
|
+
func setupPreview() {
|
|
69
|
+
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
|
70
|
+
case .authorized:
|
|
71
|
+
openCameraAndMic()
|
|
72
|
+
case .notDetermined:
|
|
73
|
+
AVCaptureDevice.requestAccess(for: .video) { granted in
|
|
74
|
+
DispatchQueue.main.async {
|
|
75
|
+
if granted { openCameraAndMic() }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
case .denied, .restricted:
|
|
79
|
+
showPermissionAlert(for: .video)
|
|
80
|
+
@unknown default:
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Step 2: 先打开摄像头,成功后再打开麦克风
|
|
86
|
+
func openCameraAndMic() {
|
|
87
|
+
DeviceStore.shared.openLocalCamera(isFront: true) { result in
|
|
88
|
+
switch result {
|
|
89
|
+
case .success:
|
|
90
|
+
print("[Preview] 摄像头打开成功")
|
|
91
|
+
openMicrophone()
|
|
92
|
+
|
|
93
|
+
case .failure(let errorInfo):
|
|
94
|
+
// errorInfo: ErrorInfo(.code + .message)
|
|
95
|
+
print("[Preview] 摄像头打开失败, code: \(errorInfo.code), msg: \(errorInfo.message)")
|
|
96
|
+
handleDeviceError(errorInfo)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
func openMicrophone() {
|
|
102
|
+
DeviceStore.shared.openLocalMicrophone { result in
|
|
103
|
+
switch result {
|
|
104
|
+
case .success:
|
|
105
|
+
print("[Preview] 麦克风打开成功,预览就绪")
|
|
106
|
+
case .failure(let errorInfo):
|
|
107
|
+
print("[Preview] 麦克风打开失败, code: \(errorInfo.code)")
|
|
108
|
+
handleDeviceError(errorInfo)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Step 3: 退出预览时关闭设备
|
|
114
|
+
func teardownPreview() {
|
|
115
|
+
DeviceStore.shared.closeLocalCamera()
|
|
116
|
+
DeviceStore.shared.closeLocalMicrophone()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Step 4: 不中断预览切换前后置摄像头
|
|
120
|
+
func flipCamera(toFront: Bool) {
|
|
121
|
+
DeviceStore.shared.switchCamera(isFront: toFront)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// MARK: - 错误处理(使用 ErrorInfo 的 .code 字段)
|
|
125
|
+
|
|
126
|
+
func handleDeviceError(_ errorInfo: ErrorInfo) {
|
|
127
|
+
let message: String
|
|
128
|
+
switch errorInfo.code {
|
|
129
|
+
case -1101: message = "摄像头权限被拒,请前往系统设置开启"
|
|
130
|
+
case -1102: message = "摄像头被其他应用占用,请关闭后重试"
|
|
131
|
+
case -1103: message = "当前设备不支持摄像头(请使用真机测试)"
|
|
132
|
+
case -1105: message = "麦克风权限被拒,请前往系统设置开启"
|
|
133
|
+
default: message = "设备打开失败(错误码 \(errorInfo.code)),请重试"
|
|
134
|
+
}
|
|
135
|
+
// 业务自行展示 Alert
|
|
136
|
+
print("[Preview] 设备错误:\(message)")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func showPermissionAlert(for mediaType: AVMediaType) {
|
|
140
|
+
let message = mediaType == .video
|
|
141
|
+
? "请在「设置 > 隐私 > 摄像头」中允许本应用访问摄像头"
|
|
142
|
+
: "请在「设置 > 隐私 > 麦克风」中允许本应用访问麦克风"
|
|
143
|
+
// 业务自行展示 Alert + 跳转设置
|
|
144
|
+
if let url = URL(string: UIApplication.openSettingsURLString) {
|
|
145
|
+
UIApplication.shared.open(url)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**App 生命周期处理(后台暂停/前台恢复)**:
|
|
151
|
+
```swift
|
|
152
|
+
import Combine
|
|
153
|
+
|
|
154
|
+
var cancellables = Set<AnyCancellable>()
|
|
155
|
+
|
|
156
|
+
func observeAppLifecycle() {
|
|
157
|
+
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
|
|
158
|
+
.sink { [weak self] _ in
|
|
159
|
+
// 进后台时释放设备,避免摄像头指示灯常亮
|
|
160
|
+
DeviceStore.shared.closeLocalCamera()
|
|
161
|
+
DeviceStore.shared.closeLocalMicrophone()
|
|
162
|
+
}
|
|
163
|
+
.store(in: &cancellables)
|
|
164
|
+
|
|
165
|
+
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
|
|
166
|
+
.sink { [weak self] _ in
|
|
167
|
+
// 回到前台时重新打开(注意:需要重新检查权限)
|
|
168
|
+
openCameraAndMic()
|
|
169
|
+
}
|
|
170
|
+
.store(in: &cancellables)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## 调用时序
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
进入预览界面
|
|
178
|
+
│
|
|
179
|
+
▼
|
|
180
|
+
checkCameraPermission()
|
|
181
|
+
│
|
|
182
|
+
├─ denied → showPermissionAlert → 用户跳转系统设置
|
|
183
|
+
│
|
|
184
|
+
└─ authorized
|
|
185
|
+
│
|
|
186
|
+
▼
|
|
187
|
+
DeviceStore.shared.openLocalCamera(isFront: true, completion:)
|
|
188
|
+
│
|
|
189
|
+
├─ .failure(errorInfo) → handleDeviceError(显示错误提示)
|
|
190
|
+
│
|
|
191
|
+
└─ .success
|
|
192
|
+
│
|
|
193
|
+
▼
|
|
194
|
+
DeviceStore.shared.openLocalMicrophone(completion:)
|
|
195
|
+
│
|
|
196
|
+
├─ .failure(errorInfo) → handleDeviceError
|
|
197
|
+
│
|
|
198
|
+
└─ .success → 预览画面出现,等待用户点击「开始直播」
|
|
199
|
+
│
|
|
200
|
+
[用户点击开始直播]
|
|
201
|
+
│
|
|
202
|
+
▼
|
|
203
|
+
导航到房间配置页
|
|
204
|
+
由配置页调用 createLive(参见 anchor-room-config)
|
|
205
|
+
│
|
|
206
|
+
退出预览界面
|
|
207
|
+
│
|
|
208
|
+
▼
|
|
209
|
+
DeviceStore.shared.closeLocalCamera()
|
|
210
|
+
DeviceStore.shared.closeLocalMicrophone() ← 退出预览时释放设备
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## 平台特有注意事项
|
|
214
|
+
|
|
215
|
+
### 1. openLocalCamera 有 completion 参数,不可忽略
|
|
216
|
+
`DeviceStore.shared.openLocalCamera(isFront: Bool, completion: CompletionClosure?)` 包含 completion 参数。必须在 `.success` 回调中再打开麦克风,避免设备尚未就绪时即执行后续逻辑。
|
|
217
|
+
|
|
218
|
+
### 2. iOS 模拟器不支持摄像头
|
|
219
|
+
所有摄像头相关功能必须在**真实设备**上测试。模拟器调用 `openLocalCamera` 会返回 code `-1103`。
|
|
220
|
+
|
|
221
|
+
### 3. 进入后台时摄像头自动暂停
|
|
222
|
+
iOS 系统在 App 进入后台时会自动挂起摄像头采集。监听 `UIApplication.didEnterBackgroundNotification` 主动关闭设备,再在 `didBecomeActiveNotification` 时重新打开,避免 `-1102` 被占用错误。
|
|
223
|
+
|
|
224
|
+
### 4. 前后摄像头切换(不中断预览)
|
|
225
|
+
预览阶段可直接调用 `DeviceStore.shared.switchCamera(isFront:)` 切换前后摄像头,无需重新调用 `openLocalCamera`,画面切换无黑屏。
|
|
226
|
+
|
|
227
|
+
### 5. ErrorInfo 不是 Swift Error
|
|
228
|
+
设备回调的 `.failure` 分支中,错误对象是 `ErrorInfo` 结构体(`.code: Int` + `.message: String`),不是 `Swift.Error`,不可调用 `localizedDescription`。
|