@cometchat/skills 4.1.0 → 4.2.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/bin/install.js +298 -41
- package/package.json +1 -1
- package/skills/cometchat/SKILL.md +146 -27
- package/skills/cometchat-a11y/SKILL.md +476 -0
- package/skills/cometchat-android-v5/SKILL.md +1 -1
- package/skills/cometchat-android-v5-calls/SKILL.md +413 -0
- package/skills/cometchat-android-v5-calls/references/README.md +53 -0
- package/skills/cometchat-android-v5-calls/references/add-calls-to-existing-chat.md +195 -0
- package/skills/cometchat-android-v5-calls/references/audio-controls.md +91 -0
- package/skills/cometchat-android-v5-calls/references/background-handling.md +168 -0
- package/skills/cometchat-android-v5-calls/references/call-layouts.md +123 -0
- package/skills/cometchat-android-v5-calls/references/call-logs.md +113 -0
- package/skills/cometchat-android-v5-calls/references/call-session.md +274 -0
- package/skills/cometchat-android-v5-calls/references/custom-ui.md +150 -0
- package/skills/cometchat-android-v5-calls/references/event-listeners.md +123 -0
- package/skills/cometchat-android-v5-calls/references/group-calls.md +205 -0
- package/skills/cometchat-android-v5-calls/references/idle-timeout.md +111 -0
- package/skills/cometchat-android-v5-calls/references/in-call-chat.md +134 -0
- package/skills/cometchat-android-v5-calls/references/join-session.md +120 -0
- package/skills/cometchat-android-v5-calls/references/migration-v4-to-v5.md +206 -0
- package/skills/cometchat-android-v5-calls/references/participant-management.md +110 -0
- package/skills/cometchat-android-v5-calls/references/picture-in-picture.md +106 -0
- package/skills/cometchat-android-v5-calls/references/raise-hand.md +242 -0
- package/skills/cometchat-android-v5-calls/references/recording.md +101 -0
- package/skills/cometchat-android-v5-calls/references/ringing-integration.md +152 -0
- package/skills/cometchat-android-v5-calls/references/screen-sharing.md +82 -0
- package/skills/cometchat-android-v5-calls/references/server-fcm-voip.md +215 -0
- package/skills/cometchat-android-v5-calls/references/session-settings.md +121 -0
- package/skills/cometchat-android-v5-calls/references/setup.md +137 -0
- package/skills/cometchat-android-v5-calls/references/share-invite.md +151 -0
- package/skills/cometchat-android-v5-calls/references/video-controls.md +87 -0
- package/skills/cometchat-android-v5-calls/references/voip-calling.md +526 -0
- package/skills/cometchat-android-v5-components/SKILL.md +1 -1
- package/skills/cometchat-android-v5-core/SKILL.md +1 -1
- package/skills/cometchat-android-v5-customization/SKILL.md +1 -1
- package/skills/cometchat-android-v5-extensions/SKILL.md +1 -1
- package/skills/cometchat-android-v5-features/SKILL.md +1 -1
- package/skills/cometchat-android-v5-placement/SKILL.md +1 -1
- package/skills/cometchat-android-v5-production/SKILL.md +1 -1
- package/skills/cometchat-android-v5-push/SKILL.md +1 -1
- package/skills/cometchat-android-v5-testing/SKILL.md +1 -1
- package/skills/cometchat-android-v5-theming/SKILL.md +1 -1
- package/skills/cometchat-android-v5-troubleshooting/SKILL.md +1 -1
- package/skills/cometchat-android-v6/SKILL.md +1 -1
- package/skills/cometchat-android-v6-builder-settings/SKILL.md +1 -1
- package/skills/cometchat-android-v6-calls/SKILL.md +375 -0
- package/skills/cometchat-android-v6-calls/references/add-calls-to-existing-chat.md +97 -0
- package/skills/cometchat-android-v6-calls/references/call-layouts.md +91 -0
- package/skills/cometchat-android-v6-calls/references/call-session.md +163 -0
- package/skills/cometchat-android-v6-calls/references/device-management.md +171 -0
- package/skills/cometchat-android-v6-calls/references/group-calls.md +191 -0
- package/skills/cometchat-android-v6-calls/references/idle-timeout.md +107 -0
- package/skills/cometchat-android-v6-calls/references/in-call-chat.md +136 -0
- package/skills/cometchat-android-v6-calls/references/raise-hand.md +186 -0
- package/skills/cometchat-android-v6-calls/references/ringing-integration.md +176 -0
- package/skills/cometchat-android-v6-calls/references/server-fcm-voip.md +127 -0
- package/skills/cometchat-android-v6-calls/references/share-invite.md +105 -0
- package/skills/cometchat-android-v6-compose-components/SKILL.md +2 -2
- package/skills/cometchat-android-v6-compose-customization/SKILL.md +1 -1
- package/skills/cometchat-android-v6-compose-placement/SKILL.md +1 -1
- package/skills/cometchat-android-v6-compose-theming/SKILL.md +1 -1
- package/skills/cometchat-android-v6-core/SKILL.md +1 -1
- package/skills/cometchat-android-v6-events/SKILL.md +1 -1
- package/skills/cometchat-android-v6-extensions/SKILL.md +1 -1
- package/skills/cometchat-android-v6-features/SKILL.md +1 -1
- package/skills/cometchat-android-v6-kotlin-components/SKILL.md +1 -1
- package/skills/cometchat-android-v6-kotlin-customization/SKILL.md +1 -1
- package/skills/cometchat-android-v6-kotlin-placement/SKILL.md +1 -1
- package/skills/cometchat-android-v6-kotlin-theming/SKILL.md +1 -1
- package/skills/cometchat-android-v6-migration/SKILL.md +398 -0
- package/skills/cometchat-android-v6-production/SKILL.md +1 -1
- package/skills/cometchat-android-v6-push/SKILL.md +1 -1
- package/skills/cometchat-android-v6-testing/SKILL.md +1 -1
- package/skills/cometchat-android-v6-troubleshooting/SKILL.md +1 -1
- package/skills/cometchat-angular-calls/SKILL.md +511 -0
- package/skills/cometchat-angular-calls/references/add-calls-to-existing-chat.md +95 -0
- package/skills/cometchat-angular-calls/references/call-layouts.md +106 -0
- package/skills/cometchat-angular-calls/references/call-session.md +167 -0
- package/skills/cometchat-angular-calls/references/custom-ui.md +231 -0
- package/skills/cometchat-angular-calls/references/device-management.md +135 -0
- package/skills/cometchat-angular-calls/references/group-calls.md +412 -0
- package/skills/cometchat-angular-calls/references/idle-timeout.md +145 -0
- package/skills/cometchat-angular-calls/references/in-call-chat.md +148 -0
- package/skills/cometchat-angular-calls/references/lazy-loading-pitfalls.md +205 -0
- package/skills/cometchat-angular-calls/references/migration-v4-to-v5.md +53 -0
- package/skills/cometchat-angular-calls/references/ngzone-and-async-callbacks.md +183 -0
- package/skills/cometchat-angular-calls/references/picture-in-picture.md +285 -0
- package/skills/cometchat-angular-calls/references/raise-hand.md +167 -0
- package/skills/cometchat-angular-calls/references/recording-screen-share.md +267 -0
- package/skills/cometchat-angular-calls/references/ringing-integration.md +131 -0
- package/skills/cometchat-angular-calls/references/server-web-push-vapid.md +100 -0
- package/skills/cometchat-angular-calls/references/share-invite.md +116 -0
- package/skills/cometchat-angular-components/SKILL.md +1 -1
- package/skills/cometchat-angular-core/SKILL.md +1 -1
- package/skills/cometchat-angular-customization/SKILL.md +1 -1
- package/skills/cometchat-angular-features/SKILL.md +1 -1
- package/skills/cometchat-angular-patterns/SKILL.md +1 -1
- package/skills/cometchat-angular-placement/SKILL.md +1 -1
- package/skills/cometchat-angular-production/SKILL.md +1 -1
- package/skills/cometchat-angular-push/SKILL.md +403 -0
- package/skills/cometchat-angular-testing/SKILL.md +333 -0
- package/skills/cometchat-angular-theming/SKILL.md +1 -1
- package/skills/cometchat-angular-troubleshooting/SKILL.md +1 -1
- package/skills/cometchat-astro-patterns/SKILL.md +1 -1
- package/skills/cometchat-calls/SKILL.md +358 -0
- package/skills/cometchat-calls/references/use-case-broadcast.md +194 -0
- package/skills/cometchat-calls/references/use-case-marketplace.md +158 -0
- package/skills/cometchat-calls/references/use-case-support.md +190 -0
- package/skills/cometchat-calls/references/use-case-team.md +169 -0
- package/skills/cometchat-calls/references/use-case-telehealth.md +154 -0
- package/skills/cometchat-components/SKILL.md +1 -1
- package/skills/cometchat-core/SKILL.md +1 -1
- package/skills/cometchat-customization/SKILL.md +2 -2
- package/skills/cometchat-features/SKILL.md +2 -2
- package/skills/cometchat-flutter-v5/SKILL.md +1 -1
- package/skills/cometchat-flutter-v5-calls/SKILL.md +325 -94
- package/skills/cometchat-flutter-v5-calls/references/add-calls-to-existing-chat.md +169 -0
- package/skills/cometchat-flutter-v5-calls/references/call-layouts.md +98 -0
- package/skills/cometchat-flutter-v5-calls/references/call-session.md +330 -0
- package/skills/cometchat-flutter-v5-calls/references/device-management.md +120 -0
- package/skills/cometchat-flutter-v5-calls/references/group-calls.md +187 -0
- package/skills/cometchat-flutter-v5-calls/references/idle-timeout.md +97 -0
- package/skills/cometchat-flutter-v5-calls/references/in-call-chat.md +132 -0
- package/skills/cometchat-flutter-v5-calls/references/migration-v4-to-v5.md +160 -0
- package/skills/cometchat-flutter-v5-calls/references/raise-hand.md +244 -0
- package/skills/cometchat-flutter-v5-calls/references/ringing-integration.md +193 -0
- package/skills/cometchat-flutter-v5-calls/references/server-push-bridge.md +101 -0
- package/skills/cometchat-flutter-v5-calls/references/share-invite.md +142 -0
- package/skills/cometchat-flutter-v5-conversations/SKILL.md +1 -1
- package/skills/cometchat-flutter-v5-core/SKILL.md +1 -1
- package/skills/cometchat-flutter-v5-customization/SKILL.md +1 -1
- package/skills/cometchat-flutter-v5-events/SKILL.md +1 -1
- package/skills/cometchat-flutter-v5-messages/SKILL.md +1 -1
- package/skills/cometchat-flutter-v5-production/SKILL.md +1 -1
- package/skills/cometchat-flutter-v5-push/SKILL.md +1 -1
- package/skills/cometchat-flutter-v5-testing/SKILL.md +390 -0
- package/skills/cometchat-flutter-v5-theming/SKILL.md +1 -1
- package/skills/cometchat-flutter-v5-troubleshooting/SKILL.md +1 -1
- package/skills/cometchat-flutter-v5-users-groups/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-calls/SKILL.md +308 -243
- package/skills/cometchat-flutter-v6-calls/references/add-calls-to-existing-chat.md +134 -0
- package/skills/cometchat-flutter-v6-calls/references/call-layouts.md +102 -0
- package/skills/cometchat-flutter-v6-calls/references/call-session.md +229 -0
- package/skills/cometchat-flutter-v6-calls/references/device-management.md +99 -0
- package/skills/cometchat-flutter-v6-calls/references/group-calls.md +190 -0
- package/skills/cometchat-flutter-v6-calls/references/idle-timeout.md +80 -0
- package/skills/cometchat-flutter-v6-calls/references/in-call-chat.md +122 -0
- package/skills/cometchat-flutter-v6-calls/references/raise-hand.md +268 -0
- package/skills/cometchat-flutter-v6-calls/references/ringing-integration.md +171 -0
- package/skills/cometchat-flutter-v6-calls/references/server-push-bridge.md +93 -0
- package/skills/cometchat-flutter-v6-calls/references/share-invite.md +102 -0
- package/skills/cometchat-flutter-v6-components/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-conversations/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-core/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-customization/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-events/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-features/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-messages/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-migration/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-placement/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-production/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-push/SKILL.md +452 -0
- package/skills/cometchat-flutter-v6-testing/SKILL.md +319 -0
- package/skills/cometchat-flutter-v6-theming/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-troubleshooting/SKILL.md +1 -1
- package/skills/cometchat-flutter-v6-users-groups/SKILL.md +1 -1
- package/skills/cometchat-i18n/SKILL.md +358 -0
- package/skills/cometchat-ios/SKILL.md +5 -5
- package/skills/cometchat-ios-calls/SKILL.md +409 -0
- package/skills/cometchat-ios-calls/references/add-calls-to-existing-chat.md +172 -0
- package/skills/cometchat-ios-calls/references/avaudiosession-routing.md +189 -0
- package/skills/cometchat-ios-calls/references/call-layouts.md +136 -0
- package/skills/cometchat-ios-calls/references/call-session.md +221 -0
- package/skills/cometchat-ios-calls/references/callkit-and-pushkit.md +353 -0
- package/skills/cometchat-ios-calls/references/custom-ui.md +322 -0
- package/skills/cometchat-ios-calls/references/device-management.md +171 -0
- package/skills/cometchat-ios-calls/references/group-calls.md +396 -0
- package/skills/cometchat-ios-calls/references/idle-timeout.md +185 -0
- package/skills/cometchat-ios-calls/references/in-call-chat.md +183 -0
- package/skills/cometchat-ios-calls/references/migration-v4-to-v5.md +138 -0
- package/skills/cometchat-ios-calls/references/picture-in-picture.md +246 -0
- package/skills/cometchat-ios-calls/references/raise-hand.md +239 -0
- package/skills/cometchat-ios-calls/references/recording-screen-share.md +339 -0
- package/skills/cometchat-ios-calls/references/ringing-integration.md +142 -0
- package/skills/cometchat-ios-calls/references/server-apns-pushkit.md +267 -0
- package/skills/cometchat-ios-calls/references/share-invite.md +152 -0
- package/skills/cometchat-ios-calls/references/swiftui-uikit-hosting.md +246 -0
- package/skills/cometchat-ios-calls/references/voip-cert-setup.md +166 -0
- package/skills/cometchat-ios-components/SKILL.md +1 -1
- package/skills/cometchat-ios-core/SKILL.md +1 -1
- package/skills/cometchat-ios-customization/SKILL.md +1 -1
- package/skills/cometchat-ios-features/SKILL.md +1 -1
- package/skills/cometchat-ios-placement/SKILL.md +1 -1
- package/skills/cometchat-ios-production/SKILL.md +1 -1
- package/skills/cometchat-ios-push/SKILL.md +1 -1
- package/skills/cometchat-ios-testing/SKILL.md +371 -0
- package/skills/cometchat-ios-theming/SKILL.md +1 -1
- package/skills/cometchat-ios-troubleshooting/SKILL.md +1 -1
- package/skills/cometchat-native-bare-patterns/SKILL.md +45 -3
- package/skills/cometchat-native-calls/SKILL.md +599 -0
- package/skills/cometchat-native-calls/references/add-calls-to-existing-chat.md +203 -0
- package/skills/cometchat-native-calls/references/call-layouts.md +124 -0
- package/skills/cometchat-native-calls/references/call-session.md +182 -0
- package/skills/cometchat-native-calls/references/custom-ui.md +250 -0
- package/skills/cometchat-native-calls/references/device-management.md +135 -0
- package/skills/cometchat-native-calls/references/expo-vs-bare.md +179 -0
- package/skills/cometchat-native-calls/references/group-calls.md +291 -0
- package/skills/cometchat-native-calls/references/idle-timeout.md +128 -0
- package/skills/cometchat-native-calls/references/in-call-chat.md +143 -0
- package/skills/cometchat-native-calls/references/migration-v4-to-v5.md +72 -0
- package/skills/cometchat-native-calls/references/picture-in-picture.md +276 -0
- package/skills/cometchat-native-calls/references/raise-hand.md +203 -0
- package/skills/cometchat-native-calls/references/recording-screen-share.md +290 -0
- package/skills/cometchat-native-calls/references/ringing-integration.md +118 -0
- package/skills/cometchat-native-calls/references/server-push-bridge.md +135 -0
- package/skills/cometchat-native-calls/references/server-push-payloads.md +193 -0
- package/skills/cometchat-native-calls/references/share-invite.md +144 -0
- package/skills/cometchat-native-calls/references/voip-push-end-to-end.md +317 -0
- package/skills/cometchat-native-components/SKILL.md +1 -1
- package/skills/cometchat-native-core/SKILL.md +1 -1
- package/skills/cometchat-native-customization/SKILL.md +1 -1
- package/skills/cometchat-native-expo-patterns/SKILL.md +18 -1
- package/skills/cometchat-native-features/SKILL.md +1 -1
- package/skills/cometchat-native-placement/SKILL.md +1 -1
- package/skills/cometchat-native-production/SKILL.md +1 -1
- package/skills/cometchat-native-push/SKILL.md +1 -1
- package/skills/cometchat-native-testing/SKILL.md +1 -1
- package/skills/cometchat-native-theming/SKILL.md +1 -1
- package/skills/cometchat-native-troubleshooting/SKILL.md +1 -1
- package/skills/cometchat-nextjs-patterns/SKILL.md +1 -1
- package/skills/cometchat-placement/SKILL.md +1 -1
- package/skills/cometchat-production/SKILL.md +1 -1
- package/skills/cometchat-react-calls/SKILL.md +539 -0
- package/skills/cometchat-react-calls/references/add-calls-to-existing-chat.md +145 -0
- package/skills/cometchat-react-calls/references/call-layouts.md +161 -0
- package/skills/cometchat-react-calls/references/call-session.md +235 -0
- package/skills/cometchat-react-calls/references/custom-ui.md +201 -0
- package/skills/cometchat-react-calls/references/device-management.md +206 -0
- package/skills/cometchat-react-calls/references/group-calls.md +262 -0
- package/skills/cometchat-react-calls/references/idle-timeout.md +175 -0
- package/skills/cometchat-react-calls/references/in-call-chat.md +212 -0
- package/skills/cometchat-react-calls/references/migration-v4-to-v5.md +172 -0
- package/skills/cometchat-react-calls/references/picture-in-picture.md +245 -0
- package/skills/cometchat-react-calls/references/raise-hand.md +238 -0
- package/skills/cometchat-react-calls/references/recording-screen-share.md +143 -0
- package/skills/cometchat-react-calls/references/ringing-integration.md +255 -0
- package/skills/cometchat-react-calls/references/server-web-push-vapid.md +241 -0
- package/skills/cometchat-react-calls/references/share-invite.md +176 -0
- package/skills/cometchat-react-calls/references/testing-calls-on-web.md +198 -0
- package/skills/cometchat-react-calls/references/voip-and-web-push.md +165 -0
- package/skills/cometchat-react-patterns/SKILL.md +1 -1
- package/skills/cometchat-react-push/SKILL.md +448 -0
- package/skills/cometchat-react-router-patterns/SKILL.md +1 -1
- package/skills/cometchat-react-testing/SKILL.md +404 -0
- package/skills/cometchat-theming/SKILL.md +3 -3
- package/skills/cometchat-troubleshooting/SKILL.md +2 -2
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# CallKit + PushKit on iOS — the canonical implementation
|
|
2
|
+
|
|
3
|
+
iOS is the only platform where the OS owns the incoming-call UI. CallKit + PushKit isn't optional for production — without them, calls don't ring on locked devices and Apple rejects production builds. This reference is the full implementation; the SKILL.md sketches it, here is the code.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why two frameworks
|
|
8
|
+
|
|
9
|
+
| Framework | Role |
|
|
10
|
+
|---|---|
|
|
11
|
+
| **PushKit** | Receives high-priority VoIP push payloads, even when the app is terminated. Separate from APNs. |
|
|
12
|
+
| **CallKit** | Presents the system incoming-call UI (lock-screen, even on locked device), reports call state to the OS, integrates with Phone app's "Recents" list. |
|
|
13
|
+
|
|
14
|
+
Combined: a VoIP push wakes the app via PushKit; the app immediately reports the call to CallKit; CallKit takes over and shows the system UI.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Apple Developer setup (one-time, manual)
|
|
19
|
+
|
|
20
|
+
The skill cannot automate these:
|
|
21
|
+
|
|
22
|
+
1. **Create a VoIP Services certificate** in Apple Developer portal → Certificates → "+" → VoIP Services.
|
|
23
|
+
2. **Pin to the bundle ID** of your app.
|
|
24
|
+
3. **Download the .p12** and convert to .pem if your push server needs it (`openssl pkcs12 -in cert.p12 -out cert.pem -nodes`).
|
|
25
|
+
4. **Upload to your push server.** This is a SEPARATE certificate from your standard APNs cert.
|
|
26
|
+
|
|
27
|
+
The skill prints these steps and waits for the user to confirm before proceeding.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Xcode setup
|
|
32
|
+
|
|
33
|
+
In the target's "Signing & Capabilities":
|
|
34
|
+
|
|
35
|
+
1. Click "+" → "Push Notifications" → adds the entitlement.
|
|
36
|
+
2. Click "+" → "Background Modes" → check:
|
|
37
|
+
- ☑ Audio, AirPlay, and Picture in Picture
|
|
38
|
+
- ☑ Voice over IP
|
|
39
|
+
- ☑ Remote notifications
|
|
40
|
+
|
|
41
|
+
Verify the resulting `Info.plist` has:
|
|
42
|
+
|
|
43
|
+
```xml
|
|
44
|
+
<key>UIBackgroundModes</key>
|
|
45
|
+
<array>
|
|
46
|
+
<string>audio</string>
|
|
47
|
+
<string>voip</string>
|
|
48
|
+
<string>remote-notification</string>
|
|
49
|
+
</array>
|
|
50
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
51
|
+
<string>So you can talk during voice and video calls.</string>
|
|
52
|
+
<key>NSCameraUsageDescription</key>
|
|
53
|
+
<string>So you can be seen during video calls.</string>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## App entry — register PushKit + CallKit
|
|
59
|
+
|
|
60
|
+
### SwiftUI (`@main App`)
|
|
61
|
+
|
|
62
|
+
```swift
|
|
63
|
+
import SwiftUI
|
|
64
|
+
import PushKit
|
|
65
|
+
import CallKit
|
|
66
|
+
import CometChatSDK
|
|
67
|
+
import CometChatCallsSDK
|
|
68
|
+
|
|
69
|
+
@main
|
|
70
|
+
struct YourApp: App {
|
|
71
|
+
@StateObject private var callManager = CallManager.shared
|
|
72
|
+
|
|
73
|
+
var body: some Scene {
|
|
74
|
+
WindowGroup {
|
|
75
|
+
ContentView()
|
|
76
|
+
.onAppear {
|
|
77
|
+
CometChat.init(appId: Secrets.cometchatAppID, appSettings: appSettings) { isInitialized, error in
|
|
78
|
+
guard error == nil else { return }
|
|
79
|
+
CometChatCalls.init(callAppSettings: callAppSettings) { _ in
|
|
80
|
+
callManager.registerForPushKit() // wait for SDK ready
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### UIKit (`AppDelegate`)
|
|
90
|
+
|
|
91
|
+
```swift
|
|
92
|
+
import UIKit
|
|
93
|
+
import PushKit
|
|
94
|
+
import CallKit
|
|
95
|
+
|
|
96
|
+
@main
|
|
97
|
+
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
98
|
+
func application(_ app: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
99
|
+
CometChat.init(appId: Secrets.cometchatAppID, appSettings: appSettings) { _, error in
|
|
100
|
+
guard error == nil else { return }
|
|
101
|
+
CometChatCalls.init(callAppSettings: callAppSettings) { _ in
|
|
102
|
+
CallManager.shared.registerForPushKit()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## CallManager — the shared state holder
|
|
113
|
+
|
|
114
|
+
```swift
|
|
115
|
+
import PushKit
|
|
116
|
+
import CallKit
|
|
117
|
+
import CometChatCallsSDK
|
|
118
|
+
|
|
119
|
+
class CallManager: NSObject, ObservableObject {
|
|
120
|
+
static let shared = CallManager()
|
|
121
|
+
|
|
122
|
+
private let pushRegistry = PKPushRegistry(queue: .main)
|
|
123
|
+
private let provider: CXProvider
|
|
124
|
+
private let callController = CXCallController()
|
|
125
|
+
|
|
126
|
+
private override init() {
|
|
127
|
+
let config = CXProviderConfiguration(localizedName: "YourApp")
|
|
128
|
+
config.supportsVideo = true
|
|
129
|
+
config.maximumCallGroups = 1
|
|
130
|
+
config.maximumCallsPerCallGroup = 1
|
|
131
|
+
config.supportedHandleTypes = [.generic]
|
|
132
|
+
config.iconTemplateImageData = UIImage(named: "CallKitIcon")?.pngData()
|
|
133
|
+
config.ringtoneSound = "ringtone.caf"
|
|
134
|
+
|
|
135
|
+
self.provider = CXProvider(configuration: config)
|
|
136
|
+
super.init()
|
|
137
|
+
provider.setDelegate(self, queue: .main)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
func registerForPushKit() {
|
|
141
|
+
pushRegistry.delegate = self
|
|
142
|
+
pushRegistry.desiredPushTypes = [.voIP]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func reportIncomingCall(uuid: UUID, callerName: String, callerUid: String, hasVideo: Bool) {
|
|
146
|
+
let update = CXCallUpdate()
|
|
147
|
+
update.remoteHandle = CXHandle(type: .generic, value: callerUid)
|
|
148
|
+
update.localizedCallerName = callerName
|
|
149
|
+
update.hasVideo = hasVideo
|
|
150
|
+
|
|
151
|
+
provider.reportNewIncomingCall(with: uuid, update: update) { error in
|
|
152
|
+
if let error = error {
|
|
153
|
+
print("Failed to report incoming call: \(error)")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## PushKit delegate — the 5-second rule
|
|
163
|
+
|
|
164
|
+
```swift
|
|
165
|
+
extension CallManager: PKPushRegistryDelegate {
|
|
166
|
+
func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
|
|
167
|
+
guard type == .voIP else { return }
|
|
168
|
+
let token = credentials.token.map { String(format: "%02x", $0) }.joined()
|
|
169
|
+
// Send this token to your server, keyed by the logged-in CometChat UID
|
|
170
|
+
Task {
|
|
171
|
+
await sendVoipTokenToServer(token: token, uid: CometChat.getLoggedInUser()?.uid ?? "")
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {
|
|
176
|
+
guard type == .voIP else { completion(); return }
|
|
177
|
+
|
|
178
|
+
// CRITICAL: report to CallKit IMMEDIATELY. iOS terminates apps that delay this >~5s.
|
|
179
|
+
let data = payload.dictionaryPayload["data"] as? [String: Any] ?? [:]
|
|
180
|
+
let sessionId = data["sessionId"] as? String ?? UUID().uuidString
|
|
181
|
+
let callerName = data["callerName"] as? String ?? "Unknown"
|
|
182
|
+
let callerUid = data["callerUid"] as? String ?? ""
|
|
183
|
+
let hasVideo = (data["callType"] as? String) == "video"
|
|
184
|
+
|
|
185
|
+
let uuid = UUID(uuidString: sessionId) ?? UUID()
|
|
186
|
+
reportIncomingCall(uuid: uuid, callerName: callerName, callerUid: callerUid, hasVideo: hasVideo)
|
|
187
|
+
|
|
188
|
+
// Store sessionId for later when CXAnswerCallAction fires
|
|
189
|
+
self.pendingSessionIds[uuid] = sessionId
|
|
190
|
+
|
|
191
|
+
completion() // ← tell iOS we're done
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// iOS may invalidate the token (OS upgrade, app reinstall)
|
|
195
|
+
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
|
|
196
|
+
// Token will re-register; wait for didUpdate
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**The 5-second rule:** Apple's `PKPushRegistry` documentation says you must call `reportNewIncomingCall` "promptly." Practical limit: ~5 seconds. If you exceed it, iOS terminates your app process and won't deliver future VoIP pushes for ~24 hours (apologies, no public retry mechanism).
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## CallKit delegate — answer + end actions
|
|
206
|
+
|
|
207
|
+
```swift
|
|
208
|
+
extension CallManager: CXProviderDelegate {
|
|
209
|
+
func providerDidReset(_ provider: CXProvider) {
|
|
210
|
+
// OS reset everything — clean up local state
|
|
211
|
+
pendingSessionIds.removeAll()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
|
215
|
+
// User tapped "Answer" on the lock screen
|
|
216
|
+
guard let sessionId = pendingSessionIds[action.callUUID] else {
|
|
217
|
+
action.fail()
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Task {
|
|
222
|
+
do {
|
|
223
|
+
// Accept via Chat SDK
|
|
224
|
+
try await CometChat.acceptCall(sessionID: sessionId)
|
|
225
|
+
|
|
226
|
+
// Configure AVAudioSession for voice/video call audio
|
|
227
|
+
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat)
|
|
228
|
+
try AVAudioSession.sharedInstance().setActive(true)
|
|
229
|
+
|
|
230
|
+
// Build session settings via SessionSettingsBuilder (matches upstream
|
|
231
|
+
// ringing sample at calls-sdk-ios-5/sample-apps/cometchat-calls-sample-
|
|
232
|
+
// app-ringing-ios/CometChatCallsRinging/CallView.swift).
|
|
233
|
+
let settings = SessionSettingsBuilder()
|
|
234
|
+
.setTitle("CometChat Call")
|
|
235
|
+
.startVideoPaused(false)
|
|
236
|
+
.startAudioMuted(false)
|
|
237
|
+
.build()
|
|
238
|
+
|
|
239
|
+
// Single-call joinSession(sessionID:callSetting:container:) — the SDK
|
|
240
|
+
// generates the token internally. Do NOT call generateToken separately.
|
|
241
|
+
// Container is the UIView the ongoing-call UI will be drawn into.
|
|
242
|
+
CometChatCalls.joinSession(
|
|
243
|
+
sessionID: sessionId,
|
|
244
|
+
callSetting: settings,
|
|
245
|
+
container: callContainerView // a UIView with measurable bounds
|
|
246
|
+
) { success in
|
|
247
|
+
// Register listeners on the resulting CallSession AFTER onSuccess
|
|
248
|
+
let session = CometChatCallsSDK.CallSession.shared
|
|
249
|
+
session.addSessionStatusListener(callListener)
|
|
250
|
+
session.addButtonClickListener(callListener)
|
|
251
|
+
action.fulfill() // tell CallKit we answered successfully
|
|
252
|
+
} onError: { error in
|
|
253
|
+
print("joinSession failed: \(error?.errorDescription ?? "unknown")")
|
|
254
|
+
action.fail()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Navigate the app's UI to the ongoing-call screen
|
|
258
|
+
await MainActor.run {
|
|
259
|
+
// ...your nav logic — push the view that hosts callContainerView...
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
action.fail()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
|
268
|
+
// User tapped "End" or call ended remotely.
|
|
269
|
+
// Use CallSession.shared.leaveSession() — `CometChatCalls.endSession()`
|
|
270
|
+
// doesn't exist on iOS (matches upstream ringing sample).
|
|
271
|
+
CometChatCallsSDK.CallSession.shared.leaveSession()
|
|
272
|
+
|
|
273
|
+
// CRITICAL: deactivate audio session (rule 1.5)
|
|
274
|
+
do {
|
|
275
|
+
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
276
|
+
} catch {
|
|
277
|
+
// log but don't block
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
pendingSessionIds.removeValue(forKey: action.callUUID)
|
|
281
|
+
action.fulfill()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
|
285
|
+
CometChatCalls.muteAudio(action.isMuted)
|
|
286
|
+
action.fulfill()
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Outgoing calls — also through CallKit
|
|
294
|
+
|
|
295
|
+
For caller-initiated calls, report via `CXStartCallAction` so the call appears in the iOS Phone app's Recents:
|
|
296
|
+
|
|
297
|
+
```swift
|
|
298
|
+
extension CallManager {
|
|
299
|
+
func startOutgoingCall(receiverUid: String, callType: CallType) {
|
|
300
|
+
let uuid = UUID()
|
|
301
|
+
let handle = CXHandle(type: .generic, value: receiverUid)
|
|
302
|
+
let action = CXStartCallAction(call: uuid, handle: handle)
|
|
303
|
+
action.isVideo = (callType == .video)
|
|
304
|
+
|
|
305
|
+
let transaction = CXTransaction(action: action)
|
|
306
|
+
callController.request(transaction) { error in
|
|
307
|
+
if let error = error {
|
|
308
|
+
print("Failed to start outgoing call: \(error)")
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
extension CallManager {
|
|
315
|
+
// CXProviderDelegate
|
|
316
|
+
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
|
317
|
+
Task {
|
|
318
|
+
do {
|
|
319
|
+
let outgoing = Call(receiverUid: action.handle.value, receiverType: .user,
|
|
320
|
+
callType: action.isVideo ? .video : .voice)
|
|
321
|
+
let initiated = try await CometChat.initiateCall(call: outgoing)
|
|
322
|
+
pendingSessionIds[action.callUUID] = initiated.sessionID
|
|
323
|
+
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
|
|
324
|
+
action.fulfill()
|
|
325
|
+
} catch {
|
|
326
|
+
action.fail()
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
This gives the user a "Calling..." UI in the iOS system. When the receiver accepts, `provider.reportOutgoingCall(connectedAt:)` switches the UI to "Connected."
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Common failure modes
|
|
338
|
+
|
|
339
|
+
| Symptom | Cause | Fix |
|
|
340
|
+
|---|---|---|
|
|
341
|
+
| App terminates 5s after PushKit delivery | Async work before `reportNewIncomingCall` | Report first, then do anything else |
|
|
342
|
+
| "VoIP services not available" at registry setup | Background Modes capability missing | Add VoIP capability in Xcode and rebuild |
|
|
343
|
+
| Token never registers | App not signed with VoIP-capable provisioning profile | Re-download provisioning profile |
|
|
344
|
+
| Call rings but answering does nothing | `CXAnswerCallAction` delegate doesn't fail/fulfill | Always call `action.fulfill()` or `action.fail()` |
|
|
345
|
+
| Music doesn't resume after hangup | Missing `setActive(false, .notifyOthersOnDeactivation)` | Add to `CXEndCallAction` handler |
|
|
346
|
+
| Lock-screen rings but unlocking shows wrong screen | App didn't navigate to ongoing-call view in `CXAnswerCallAction` | Navigate inside the action handler |
|
|
347
|
+
| Outgoing call never shows "Calling..." UI | Started via `initiateCall` directly without `CXStartCallAction` | Use `CXTransaction` flow |
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Testing without a push server
|
|
352
|
+
|
|
353
|
+
Same workaround as RN — call `CallManager.shared.reportIncomingCall(...)` from a debug button. Bypasses PushKit entirely. Useful for testing the in-app + CallKit-UI flow but doesn't exercise the lock-screen ring (which requires PushKit delivery to an unlocked or terminated app).
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Custom call UI on iOS
|
|
2
|
+
|
|
3
|
+
Same shape as the other families' custom-ui references. iOS-specific because of CallKit interplay (rule 1.7: CallKit owns standalone-mode incoming UI, so custom in-app incoming UI is additive-mode-only) and AVFoundation routing.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Two paths
|
|
8
|
+
|
|
9
|
+
1. **Style the kit's `CometChatOngoingCall`** UIViewController via theming + property overrides. Cheapest. Covers most apps.
|
|
10
|
+
2. **Build your own UIViewController on the SDK** — direct `CometChatCalls.joinSession` with your own UIView containers. Maximum control.
|
|
11
|
+
|
|
12
|
+
This reference covers path 2.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Your UIViewController
|
|
20
|
+
├── containerView for remote participant(s) ← UIView the SDK draws into
|
|
21
|
+
├── localPreviewView ← AVCaptureVideoPreviewLayer or SDK preview
|
|
22
|
+
├── controlPanel (mute / camera / end / switch)
|
|
23
|
+
└── statusLabel (call state, duration)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The Calls SDK draws video into UIViews you provide. You don't render WebRTC tracks yourself (unlike web/RN where you wire `<video>` elements).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Custom OngoingCallViewController
|
|
31
|
+
|
|
32
|
+
```swift
|
|
33
|
+
import UIKit
|
|
34
|
+
import AVFoundation
|
|
35
|
+
import CometChatCallsSDK
|
|
36
|
+
|
|
37
|
+
class CustomOngoingCallViewController: UIViewController {
|
|
38
|
+
private let containerView = UIView()
|
|
39
|
+
private let localPreviewView = UIView()
|
|
40
|
+
private lazy var controlPanel = ControlPanelView()
|
|
41
|
+
private let statusLabel = UILabel()
|
|
42
|
+
|
|
43
|
+
private let sessionID: String
|
|
44
|
+
private let isAudioOnly: Bool
|
|
45
|
+
private var listener: CometChatCallsEventsListener?
|
|
46
|
+
|
|
47
|
+
init(sessionID: String, isAudioOnly: Bool) {
|
|
48
|
+
self.sessionID = sessionID
|
|
49
|
+
self.isAudioOnly = isAudioOnly
|
|
50
|
+
super.init(nibName: nil, bundle: nil)
|
|
51
|
+
modalPresentationStyle = .fullScreen
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
required init?(coder: NSCoder) { fatalError() }
|
|
55
|
+
|
|
56
|
+
override func viewDidLoad() {
|
|
57
|
+
super.viewDidLoad()
|
|
58
|
+
view.backgroundColor = .black
|
|
59
|
+
setupLayout()
|
|
60
|
+
startCall()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override func viewWillDisappear(_ animated: Bool) {
|
|
64
|
+
super.viewWillDisappear(animated)
|
|
65
|
+
cleanup()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private func startCall() {
|
|
69
|
+
listener = self
|
|
70
|
+
// Build SessionSettings — match upstream sample shape.
|
|
71
|
+
// For custom UI, you typically build your own controls and just need the SDK
|
|
72
|
+
// to feed video tiles into your container. enableDefaultLayout is not part
|
|
73
|
+
// of SessionSettings — disable individual default-UI elements via the
|
|
74
|
+
// hide* setters on SessionSettingsBuilder if you want a minimal surface.
|
|
75
|
+
let settings = SessionSettingsBuilder()
|
|
76
|
+
.setTitle(isAudioOnly ? "Voice Call" : "Video Call")
|
|
77
|
+
.startVideoPaused(isAudioOnly)
|
|
78
|
+
.startAudioMuted(false)
|
|
79
|
+
.build()
|
|
80
|
+
|
|
81
|
+
// Configure audio session BEFORE joining
|
|
82
|
+
try? configureAudioSession()
|
|
83
|
+
|
|
84
|
+
// Single-call joinSession(sessionID:callSetting:container:) — the SDK
|
|
85
|
+
// generates the token internally. NO separate generateToken call.
|
|
86
|
+
CometChatCalls.joinSession(
|
|
87
|
+
sessionID: sessionID,
|
|
88
|
+
callSetting: settings,
|
|
89
|
+
container: containerView
|
|
90
|
+
) { [weak self] success in
|
|
91
|
+
guard let self = self else { return }
|
|
92
|
+
// Register listeners on the resulting CallSession AFTER onSuccess.
|
|
93
|
+
let session = CometChatCallsSDK.CallSession.shared
|
|
94
|
+
session.addSessionStatusListener(self.listener)
|
|
95
|
+
session.addButtonClickListener(self.listener)
|
|
96
|
+
} onError: { error in
|
|
97
|
+
print("joinSession failed: \(error?.errorDescription ?? "unknown")")
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private func configureAudioSession() throws {
|
|
102
|
+
let session = AVAudioSession.sharedInstance()
|
|
103
|
+
try session.setCategory(
|
|
104
|
+
.playAndRecord,
|
|
105
|
+
mode: isAudioOnly ? .voiceChat : .videoChat,
|
|
106
|
+
options: [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker]
|
|
107
|
+
)
|
|
108
|
+
try session.setActive(true)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private func cleanup() {
|
|
112
|
+
// CometChatCalls.endSession() does NOT exist on iOS — use CallSession.shared.leaveSession()
|
|
113
|
+
CometChatCallsSDK.CallSession.shared.leaveSession()
|
|
114
|
+
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func setupLayout() {
|
|
118
|
+
[containerView, localPreviewView, controlPanel, statusLabel].forEach {
|
|
119
|
+
$0.translatesAutoresizingMaskIntoConstraints = false
|
|
120
|
+
view.addSubview($0)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
NSLayoutConstraint.activate([
|
|
124
|
+
containerView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
125
|
+
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
126
|
+
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
127
|
+
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
128
|
+
|
|
129
|
+
localPreviewView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
|
|
130
|
+
localPreviewView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
|
|
131
|
+
localPreviewView.widthAnchor.constraint(equalToConstant: 120),
|
|
132
|
+
localPreviewView.heightAnchor.constraint(equalToConstant: 160),
|
|
133
|
+
|
|
134
|
+
controlPanel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16),
|
|
135
|
+
controlPanel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
|
136
|
+
controlPanel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
|
|
137
|
+
controlPanel.heightAnchor.constraint(equalToConstant: 80),
|
|
138
|
+
|
|
139
|
+
statusLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
|
|
140
|
+
statusLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
141
|
+
])
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
extension CustomOngoingCallViewController: CometChatCallsEventsListener {
|
|
146
|
+
func onCallEnded() {
|
|
147
|
+
DispatchQueue.main.async { [weak self] in
|
|
148
|
+
self?.cleanup()
|
|
149
|
+
self?.dismiss(animated: true)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func onUserListUpdated(_ users: [Any]) {
|
|
154
|
+
DispatchQueue.main.async { [weak self] in
|
|
155
|
+
self?.statusLabel.text = "\(users.count) participants"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
func onError(_ error: Error) {
|
|
160
|
+
print("Call error: \(error)")
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The `DispatchQueue.main.async` wrapping is the iOS equivalent of NgZone.run on Angular — SDK callbacks fire on background queues; UI updates must be on main.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Control panel
|
|
170
|
+
|
|
171
|
+
```swift
|
|
172
|
+
final class ControlPanelView: UIView {
|
|
173
|
+
let muteButton = UIButton(type: .system)
|
|
174
|
+
let cameraButton = UIButton(type: .system)
|
|
175
|
+
let switchCameraButton = UIButton(type: .system)
|
|
176
|
+
let endButton = UIButton(type: .system)
|
|
177
|
+
|
|
178
|
+
var onMute: (() -> Void)?
|
|
179
|
+
var onCamera: (() -> Void)?
|
|
180
|
+
var onSwitch: (() -> Void)?
|
|
181
|
+
var onEnd: (() -> Void)?
|
|
182
|
+
|
|
183
|
+
override init(frame: CGRect) {
|
|
184
|
+
super.init(frame: frame)
|
|
185
|
+
backgroundColor = UIColor(white: 0, alpha: 0.4)
|
|
186
|
+
layer.cornerRadius = 40
|
|
187
|
+
|
|
188
|
+
let stack = UIStackView(arrangedSubviews: [muteButton, cameraButton, switchCameraButton, endButton])
|
|
189
|
+
stack.distribution = .equalCentering
|
|
190
|
+
stack.alignment = .center
|
|
191
|
+
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
192
|
+
addSubview(stack)
|
|
193
|
+
|
|
194
|
+
NSLayoutConstraint.activate([
|
|
195
|
+
stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
|
196
|
+
stack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
|
197
|
+
stack.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
198
|
+
])
|
|
199
|
+
|
|
200
|
+
muteButton.setTitle("Mute", for: .normal)
|
|
201
|
+
cameraButton.setTitle("Cam", for: .normal)
|
|
202
|
+
switchCameraButton.setTitle("Flip", for: .normal)
|
|
203
|
+
endButton.setTitle("End", for: .normal)
|
|
204
|
+
endButton.tintColor = .red
|
|
205
|
+
|
|
206
|
+
muteButton.addAction(UIAction { [weak self] _ in self?.onMute?() }, for: .touchUpInside)
|
|
207
|
+
cameraButton.addAction(UIAction { [weak self] _ in self?.onCamera?() }, for: .touchUpInside)
|
|
208
|
+
switchCameraButton.addAction(UIAction { [weak self] _ in self?.onSwitch?() }, for: .touchUpInside)
|
|
209
|
+
endButton.addAction(UIAction { [weak self] _ in self?.onEnd?() }, for: .touchUpInside)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
required init?(coder: NSCoder) { fatalError() }
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Wire actions in the VC:
|
|
217
|
+
|
|
218
|
+
```swift
|
|
219
|
+
override func viewDidLoad() {
|
|
220
|
+
super.viewDidLoad()
|
|
221
|
+
// ...
|
|
222
|
+
controlPanel.onMute = { [weak self] in
|
|
223
|
+
guard let self = self else { return }
|
|
224
|
+
self.muted.toggle()
|
|
225
|
+
CometChatCalls.muteAudio(self.muted)
|
|
226
|
+
}
|
|
227
|
+
controlPanel.onCamera = { [weak self] in
|
|
228
|
+
guard let self = self else { return }
|
|
229
|
+
self.cameraOff.toggle()
|
|
230
|
+
CometChatCalls.pauseVideo(self.cameraOff)
|
|
231
|
+
}
|
|
232
|
+
controlPanel.onSwitch = { CometChatCalls.switchCamera() }
|
|
233
|
+
controlPanel.onEnd = { [weak self] in
|
|
234
|
+
self?.cleanup()
|
|
235
|
+
self?.dismiss(animated: true)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Local preview before connect
|
|
243
|
+
|
|
244
|
+
```swift
|
|
245
|
+
private lazy var captureSession = AVCaptureSession()
|
|
246
|
+
private lazy var previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
|
|
247
|
+
|
|
248
|
+
private func startLocalPreview() {
|
|
249
|
+
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front),
|
|
250
|
+
let input = try? AVCaptureDeviceInput(device: device) else { return }
|
|
251
|
+
|
|
252
|
+
captureSession.addInput(input)
|
|
253
|
+
previewLayer.frame = localPreviewView.bounds
|
|
254
|
+
previewLayer.videoGravity = .resizeAspectFill
|
|
255
|
+
localPreviewView.layer.addSublayer(previewLayer)
|
|
256
|
+
|
|
257
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
258
|
+
self.captureSession.startRunning()
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private func stopLocalPreview() {
|
|
263
|
+
captureSession.stopRunning()
|
|
264
|
+
previewLayer.removeFromSuperlayer()
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Stop the preview BEFORE `joinSession` — the SDK takes over the camera and your `AVCaptureSession` will fight with it.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## SwiftUI hosting
|
|
273
|
+
|
|
274
|
+
Wrap with `UIViewControllerRepresentable` (see `references/swiftui-uikit-hosting.md`):
|
|
275
|
+
|
|
276
|
+
```swift
|
|
277
|
+
struct CustomOngoingCallView: UIViewControllerRepresentable {
|
|
278
|
+
let sessionID: String
|
|
279
|
+
let isAudioOnly: Bool
|
|
280
|
+
|
|
281
|
+
func makeUIViewController(context: Context) -> CustomOngoingCallViewController {
|
|
282
|
+
CustomOngoingCallViewController(sessionID: sessionID, isAudioOnly: isAudioOnly)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
func updateUIViewController(_ vc: CustomOngoingCallViewController, context: Context) {}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Present via `.fullScreenCover` (NOT `.sheet`).
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Picture-in-picture
|
|
294
|
+
|
|
295
|
+
iOS supports PiP for video calls via `AVPictureInPictureController`:
|
|
296
|
+
|
|
297
|
+
```swift
|
|
298
|
+
import AVKit
|
|
299
|
+
|
|
300
|
+
class CustomOngoingCallViewController {
|
|
301
|
+
private var pipController: AVPictureInPictureController?
|
|
302
|
+
|
|
303
|
+
func setupPiP() {
|
|
304
|
+
guard AVPictureInPictureController.isPictureInPictureSupported() else { return }
|
|
305
|
+
let layer = AVSampleBufferDisplayLayer()
|
|
306
|
+
containerView.layer.addSublayer(layer)
|
|
307
|
+
let contentSource = AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: layer, playbackDelegate: self)
|
|
308
|
+
pipController = AVPictureInPictureController(contentSource: contentSource)
|
|
309
|
+
pipController?.canStartPictureInPictureAutomaticallyFromInline = true
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Setting up PiP for WebRTC video is involved — the SDK doesn't expose a `CMSampleBuffer` stream directly. For most apps, `enableDefaultLayout(true)` and let the kit's PiP work; custom UI authors who need PiP can use `CometChatPictureInPicture` (kit class) as a hybrid — kit's PiP, custom rest of UI.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## When NOT to go custom
|
|
319
|
+
|
|
320
|
+
The kit's `CometChatOngoingCall` covers 80% of apps. Custom is for design-system requirements that the kit's theming + property overrides can't reach. Same calculus as web/RN/Angular.
|
|
321
|
+
|
|
322
|
+
The dispatcher asks before scaffolding custom and defaults to "no — use kit."
|